How to create a video thumbnail in flutter web
Asked Answered
S

1

6

For performance reasons I've been trying to create a thumbnail for a list of videos I upload to the Firebase Firestore.

I thought of different ways to solve this. But I haven't found an actual working solution. The one solution that gave me the most hope was number 5, so stay tuned ;)

  1. There is a number of thumbnail packages, like video_thumbnail but the package doesn't work on web.

  2. I thought of maybe creating a gif from the video and then exporting a single frame but all the packages I found that do this also don't work on web (flutter_video_compress, flutter_ffmpeg

  3. Maybe I could do a 1 second version of the video, so I looked into trimming the video, with he same result. The packages I found don't support web (video_trimmer, video_editor)

  4. I found out there was a screenshot package. So I tried it both with video_player and with HtmlElementView. Both with the same result. Here is the code:

import 'dart:html';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart';
...
Widget build(BuildContext context) {
    if (widget.item.thumbnail != null) {
      return Image.network(widget.item.thumbnail!);
    }

    //This is my video's path
    String path = widget.path;
    VideoElement video;
    ScreenshotController screenshotController = ScreenshotController();

    // ignore:undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(url, (int viewId) {
      // just making sure I don't have some weird issues with crossOrigins
      video = VideoElement()..crossOrigin = 'anonymous';

      // Here I make sure, that the video is ready to play and I seek the first frame
      video.onCanPlay.listen((event) {
        _video.currentTime = 1;
      });

       // Here I make sure, that the video is actually showing a frame
      video.onSeeking.listen((event) async {
        var capture = await screenshotController.capture();
        DaysService.exportThumbnail(capture);
      });

      video.src = path;
      return video;
    });

    return Screenshot(
            controller: screenshotController,
            child: HtmlElementView(
              viewType: path,
           ),
    );
  } 

But I got a big exception that as far as I understand is because I just can't take screenshots on the web? (I guess that's also why none of those thumbnail packages work on the web). Here is the exception stack:

Error: Unexpected null value.
    at Object.throw_ [as throw] (http://localhost:58128/dart_sdk.js:5061:11)
    at Object.nullCheck (http://localhost:58128/dart_sdk.js:5388:30)
    at _engine.PlatformViewLayer.new.preroll (http://localhost:58128/dart_sdk.js:137129:12)
    at _engine.OffsetEngineLayer.new.prerollChildren (http://localhost:58128/dart_sdk.js:136513:15)
    at _engine.OffsetEngineLayer.new.preroll (http://localhost:58128/dart_sdk.js:136788:35)
    at _engine.OffsetEngineLayer.new.prerollChildren (http://localhost:58128/dart_sdk.js:136513:15)
    at _engine.OffsetEngineLayer.new.preroll (http://localhost:58128/dart_sdk.js:136788:35)
    at _engine.TransformEngineLayer.new.prerollChildren (http://localhost:58128/dart_sdk.js:136513:15)
    at _engine.TransformEngineLayer.new.preroll (http://localhost:58128/dart_sdk.js:136788:35)
    at _engine.RootLayer.new.prerollChildren (http://localhost:58128/dart_sdk.js:136513:15)
    at _engine.RootLayer.new.preroll (http://localhost:58128/dart_sdk.js:136508:31)
    at _engine.LayerTree.new.flatten (http://localhost:58128/dart_sdk.js:137408:22)
    at _engine.LayerScene.new.toImage (http://localhost:58128/dart_sdk.js:137170:36)
at layer$.OffsetLayer.new.toImage (http://localhost:58128/packages/flutter/src/rendering/layer.dart.lib.js:1395:30)
    at toImage.next (<anonymous>)
    at runBody (http://localhost:58128/dart_sdk.js:38659:34)
    at Object._async [as async] (http://localhost:58128/dart_sdk.js:38690:7)
at layer$.OffsetLayer.new.toImage (http://localhost:58128/packages/flutter/src/rendering/layer.dart.lib.js:1386:20)
at proxy_box.RenderRepaintBoundary.new.toImage (http://localhost:58128/packages/flutter/src/rendering/proxy_box.dart.lib.js:3158:26)
at screenshot.ScreenshotController.new.<anonymous> (http://localhost:58128/packages/screenshot/screenshot.dart.lib.js:162:39)
    at Generator.next (<anonymous>)
    at runBody (http://localhost:58128/dart_sdk.js:38659:34)
    at Object._async [as async] (http://localhost:58128/dart_sdk.js:38690:7)
at http://localhost:58128/packages/screenshot/screenshot.dart.lib.js:150:68
    at http://localhost:58128/dart_sdk.js:33300:33
    at internalCallback (http://localhost:58128/dart_sdk.js:25436:11)

  1. Finally I found out how html5 and javascript do it (on this site). And gave it a try to transform it into dart code (with the HTML package). Here is my try, I tried to make it as similar as possible:
  @override
  Widget build(BuildContext context) {
    if (widget.item.thumbnail != null) {
      return Image.network(widget.item.thumbnail!);
    }
    //this is my video's path
    String path = widget.path;
    VideoElement video;

    // ignore:undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(path, (int viewId) {
      video = document.createElement('video') as VideoElement;
      // just making sure I don't have some weird issues with crossOrigins
      video.crossOrigin = 'anonymous';

      // Here I make sure, that the video is ready to play and I seek the first frame
      video.onLoadedMetadata.listen((event) {
        video.currentTime = 1;
      });

      // Here I make sure, that the video is actually showing a frame
      video.onSeeking.listen((event) async {
        var canva = document.createElement('canvas') as CanvasElement;
        canva
          ..height = video.videoHeight
          ..width = video.videoWidth;

        canva.context2D..drawImage(video, video.videoWidth, video.videoHeight);
        var data = Uri.parse(canva.toDataUrl()).data;
        DaysService.exportThumbnail(data?.contentAsBytes());
      });
      video.src = path;
      return video;
    });

    //I also still show the video. The endgoal would be to show the Thumbnail tho
    return HtmlElementView(
          viewType: path,
        );
  }

The result of this is a grey picture with the right aspect ratio. I don't know why it's grey, I almost feel like I did something wrong and not that it's not possible because it's web.

If you have any idea how I could maybe fix my solutions or if you have another idea, let me know :)

Samarskite answered 22/11, 2021 at 21:27 Comment(3)
Is there any progress?Unless
I ended up doing it without thumbnail. I think the cleanest option (if nothing changed) would be a function in firebase.Samarskite
I wrote an article on medium to create thumbnail by firebase functions medium.com/@raghavshukla041/…Ambulatory
P
1

You could try ffmpeg_wasm package. It's a JS Interop package of ffmpeg_wasm.

First, you have to initialize and load ffmpeg.

FFmpeg ffmpeg = createFFmpeg(true, "https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg-core.js");

void loadFFmpeg() async {
  await promiseToFuture(ffmpeg.load());
}

You can make sure if ffmpeg is loaded by ffmpeg.isLoaded(); before accessing any functions.

When you pick a file, first you need to write that file to memory.

 ffmpeg.writeFile(
        'writeFile', 'input.mp4', filePickerResult!.files.single.bytes);

Then for this specific use case you can use the following ffmpeg command.

await promiseToFuture(ffmpeg.run7("-i", "input.mp4", '-vf',
          'select=\'eq(n,0)\'', '-vsync', '0', 'frame1.webp'));
var firstFrameData = ffmpeg.readFile('readFile', 'frame1.webp');

The resulting firstFrameData is html blob file.

P.s. I'm the author of that package. I also faced the same problem. For my use case, I also faced a cors error when deploying so I need to download and store that ffmpeg-core and canvaskit js packages in web folder.

Pericles answered 21/4, 2023 at 9:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.