diff --git a/.gitignore b/.gitignore index 4ce4464..373a675 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store .dart_tool/ +pubspec.lock + .packages .pub/ diff --git a/.metadata b/.metadata deleted file mode 100644 index faf287a..0000000 --- a/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b - channel: stable - -project_type: plugin diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index 115d516..0000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,448 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'dart:io'; - -import 'package:video_thumbnail/video_thumbnail.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:path_provider/path_provider.dart'; - -void main() => runApp(MyApp()); - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: DemoHome(), - ); - } -} - -class ThumbnailRequest { - final String video; - final String thumbnailPath; - final ImageFormat imageFormat; - final int maxHeight; - final int maxWidth; - final int timeMs; - final int quality; - - const ThumbnailRequest( - {this.video, - this.thumbnailPath, - this.imageFormat, - this.maxHeight, - this.maxWidth, - this.timeMs, - this.quality}); -} - -class ThumbnailResult { - final Image image; - final int dataSize; - final int height; - final int width; - const ThumbnailResult({this.image, this.dataSize, this.height, this.width}); -} - -Future genThumbnail(ThumbnailRequest r) async { - //WidgetsFlutterBinding.ensureInitialized(); - Uint8List bytes; - final Completer completer = Completer(); - if (r.thumbnailPath != null) { - final thumbnailPath = await VideoThumbnail.thumbnailFile( - video: r.video, - headers: { - "USERHEADER1": "user defined header1", - "USERHEADER2": "user defined header2", - }, - thumbnailPath: r.thumbnailPath, - imageFormat: r.imageFormat, - maxHeight: r.maxHeight, - maxWidth: r.maxWidth, - timeMs: r.timeMs, - quality: r.quality); - - print("thumbnail file is located: $thumbnailPath"); - - final file = File(thumbnailPath); - bytes = file.readAsBytesSync(); - } else { - bytes = await VideoThumbnail.thumbnailData( - video: r.video, - headers: { - "USERHEADER1": "user defined header1", - "USERHEADER2": "user defined header2", - }, - imageFormat: r.imageFormat, - maxHeight: r.maxHeight, - maxWidth: r.maxWidth, - timeMs: r.timeMs, - quality: r.quality); - } - - int _imageDataSize = bytes.length; - print("image size: $_imageDataSize"); - - final _image = Image.memory(bytes); - _image.image - .resolve(ImageConfiguration()) - .addListener(ImageStreamListener((ImageInfo info, bool _) { - completer.complete(ThumbnailResult( - image: _image, - dataSize: _imageDataSize, - height: info.image.height, - width: info.image.width, - )); - })); - return completer.future; -} - -class GenThumbnailImage extends StatefulWidget { - final ThumbnailRequest thumbnailRequest; - - const GenThumbnailImage({Key key, this.thumbnailRequest}) : super(key: key); - - @override - _GenThumbnailImageState createState() => _GenThumbnailImageState(); -} - -class _GenThumbnailImageState extends State { - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: genThumbnail(widget.thumbnailRequest), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final _image = snapshot.data.image; - final _width = snapshot.data.width; - final _height = snapshot.data.height; - final _dataSize = snapshot.data.dataSize; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Text( - "Image ${widget.thumbnailRequest.thumbnailPath == null ? 'data size' : 'file size'}: $_dataSize, width:$_width, height:$_height"), - ), - Container( - color: Colors.grey, - height: 1.0, - ), - _image, - ], - ); - } else if (snapshot.hasError) { - return Container( - padding: EdgeInsets.all(8.0), - color: Colors.red, - child: Text( - "Error:\n${snapshot.error.toString()}", - ), - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Generating the thumbnail for: ${widget.thumbnailRequest.video}..."), - SizedBox( - height: 10.0, - ), - CircularProgressIndicator(), - ]); - } - }, - ); - } -} - -class ImageInFile extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container(); - } -} - -class DemoHome extends StatefulWidget { - @override - _DemoHomeState createState() => _DemoHomeState(); -} - -class _DemoHomeState extends State { - final _editNode = FocusNode(); - final _video = TextEditingController( - text: - "https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4"); - ImageFormat _format = ImageFormat.JPEG; - int _quality = 50; - int _sizeH = 0; - int _sizeW = 0; - int _timeMs = 0; - - GenThumbnailImage _futreImage; - - String _tempDir; - - @override - void initState() { - super.initState(); - getTemporaryDirectory().then((d) => _tempDir = d.path); - } - - @override - Widget build(BuildContext context) { - final _settings = [ - Slider( - value: _sizeH * 1.0, - onChanged: (v) => setState(() { - _editNode.unfocus(); - _sizeH = v.toInt(); - }), - max: 256.0, - divisions: 256, - label: "$_sizeH", - ), - Center( - child: (_sizeH == 0) - ? const Text( - "Original of the video's height or scaled by the source aspect ratio") - : Text("Max height: $_sizeH(px)"), - ), - Slider( - value: _sizeW * 1.0, - onChanged: (v) => setState(() { - _editNode.unfocus(); - _sizeW = v.toInt(); - }), - max: 256.0, - divisions: 256, - label: "$_sizeW", - ), - Center( - child: (_sizeW == 0) - ? const Text( - "Original of the video's width or scaled by source aspect ratio") - : Text("Max width: $_sizeW(px)"), - ), - Slider( - value: _timeMs * 1.0, - onChanged: (v) => setState(() { - _editNode.unfocus(); - _timeMs = v.toInt(); - }), - max: 10.0 * 1000, - divisions: 1000, - label: "$_timeMs", - ), - Center( - child: (_timeMs == 0) - ? const Text("The beginning of the video") - : Text("The closest frame at $_timeMs(ms) of the video"), - ), - Slider( - value: _quality * 1.0, - onChanged: (v) => setState(() { - _editNode.unfocus(); - _quality = v.toInt(); - }), - max: 100.0, - divisions: 100, - label: "$_quality", - ), - Center(child: Text("Quality: $_quality")), - Padding( - padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 8.0), - child: InputDecorator( - decoration: InputDecoration( - border: OutlineInputBorder(), - filled: true, - isDense: true, - labelText: "Thumbnail Format", - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Radio( - groupValue: _format, - value: ImageFormat.JPEG, - onChanged: (v) => setState(() { - _format = v; - _editNode.unfocus(); - }), - ), - const Text("JPEG"), - ]), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Radio( - groupValue: _format, - value: ImageFormat.PNG, - onChanged: (v) => setState(() { - _format = v; - _editNode.unfocus(); - }), - ), - const Text("PNG"), - ]), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Radio( - groupValue: _format, - value: ImageFormat.WEBP, - onChanged: (v) => setState(() { - _format = v; - _editNode.unfocus(); - }), - ), - const Text("WebP"), - ]), - ], - ), - ), - ) - ]; - return Scaffold( - appBar: AppBar( - title: const Text('Thumbnail Plugin example'), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 8.0), - child: TextField( - decoration: InputDecoration( - border: OutlineInputBorder(), - filled: true, - isDense: true, - labelText: "Video URI", - ), - maxLines: null, - controller: _video, - focusNode: _editNode, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.done, - onEditingComplete: () { - _editNode.unfocus(); - }, - ), - ), - for (var i in _settings) i, - Expanded( - child: Container( - color: Colors.grey[300], - child: Scrollbar( - child: ListView( - shrinkWrap: true, - children: [ - (_futreImage != null) ? _futreImage : SizedBox(), - ], - ), - ), - ), - ), - ], - ), - drawer: Drawer( - child: Column( - children: [ - AppBar( - title: const Text("Settings"), - actions: [ - IconButton( - icon: Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ) - ], - ), - for (var i in _settings) i, - ], - ), - ), - floatingActionButton: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - FloatingActionButton( - onPressed: () async { - File video = - await ImagePicker.pickVideo(source: ImageSource.camera); - setState(() { - _video.text = video.path; - }); - }, - child: Icon(Icons.videocam), - tooltip: "Capture a video", - ), - const SizedBox( - width: 5.0, - ), - FloatingActionButton( - onPressed: () async { - File video = - await ImagePicker.pickVideo(source: ImageSource.gallery); - setState(() { - _video.text = video?.path; - }); - }, - child: Icon(Icons.local_movies), - tooltip: "Pick a video", - ), - const SizedBox( - width: 20.0, - ), - FloatingActionButton( - tooltip: "Generate a data of thumbnail", - onPressed: () async { - setState(() { - _futreImage = GenThumbnailImage( - thumbnailRequest: ThumbnailRequest( - video: _video.text, - thumbnailPath: null, - imageFormat: _format, - maxHeight: _sizeH, - maxWidth: _sizeW, - timeMs: _timeMs, - quality: _quality)); - }); - }, - child: const Text("Data"), - ), - const SizedBox( - width: 5.0, - ), - FloatingActionButton( - tooltip: "Generate a file of thumbnail", - onPressed: () async { - setState(() { - _futreImage = GenThumbnailImage( - thumbnailRequest: ThumbnailRequest( - video: _video.text, - thumbnailPath: _tempDir, - imageFormat: _format, - maxHeight: _sizeH, - maxWidth: _sizeW, - timeMs: _timeMs, - quality: _quality)); - }); - }, - child: const Text("File"), - ), - ], - )); - } -} diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index 47d7e94..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,294 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.3" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "5.2.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.11" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.2" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.4" - image_picker: - dependency: "direct dev" - description: - name: image_picker - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.7+15" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - intl: - dependency: transitive - description: - name: intl - url: "https://pub.dartlang.org" - source: hosted - version: "0.16.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.3" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - path_provider: - dependency: "direct dev" - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.24" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.1+2" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.4+6" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.4+3" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.2" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.13" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.8" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - video_thumbnail: - dependency: "direct dev" - description: - path: ".." - relative: true - source: path - version: "0.5.1" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.4" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" -sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 45b93c4..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,147 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" diff --git a/video_thumbnail/.metadata b/video_thumbnail/.metadata new file mode 100644 index 0000000..8093973 --- /dev/null +++ b/video_thumbnail/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + - platform: android + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + - platform: ios + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + - platform: web + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/video_thumbnail/CHANGELOG.md similarity index 88% rename from CHANGELOG.md rename to video_thumbnail/CHANGELOG.md index 34b8d02..15d9f2b 100644 --- a/CHANGELOG.md +++ b/video_thumbnail/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.6.0 +* Thanks for maRci002 + - **Breaking change**: `VideoThumbnail.thumbnailFile` now returns `Future` instead of `Future` + - **Breaking change**: `VideoThumbnail.thumbnailData` now returns `Future` instead of `Future` + - migrating to use platform interface + - add web implementation + ## 0.5.3 * Thanks Ajb Coder for: - Fix: IOException on runtime diff --git a/LICENSE b/video_thumbnail/LICENSE similarity index 100% rename from LICENSE rename to video_thumbnail/LICENSE diff --git a/README.md b/video_thumbnail/README.md similarity index 61% rename from README.md rename to video_thumbnail/README.md index 2dcedb5..f411678 100644 --- a/README.md +++ b/video_thumbnail/README.md @@ -1,9 +1,9 @@ # video_thumbnail -This plugin generates thumbnail from video file or URL. It returns image in memory or writes into a file. It offers rich options to control the image format, resolution and quality. Supports iOS and Android. +This plugin generates thumbnail from video file or URL. It returns image in memory or writes into a file. It offers rich options to control the image format, resolution and quality. Supports iOS / Android / web. - [![pub ver](https://img.shields.io/badge/pub-v0.5.3-blue)](https://pub.dev/packages/video_thumbnail) - [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/justsoft/) +[![pub ver](https://img.shields.io/badge/pub-v0.5.3-blue)](https://pub.dev/packages/video_thumbnail) +[![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/justsoft/) ![video-file](https://github.com/justsoft/video_thumbnail/blob/master/video_file.png?raw=true) ![video-url](https://github.com/justsoft/video_thumbnail/blob/master/video_url.png?raw=true) @@ -11,7 +11,7 @@ This plugin generates thumbnail from video file or URL. It returns image in mem |function|parameter|description|return| |--|--|--|--| |thumbnailData|String `[video]`, optional Map `[headers]`, ImageFormat `[imageFormat]`(JPEG/PNG/WEBP), int `[maxHeight]`(0: for the original resolution of the video, or scaled by the source aspect ratio), [maxWidth]`(0: for the original resolution of the video, or scaled by the source aspect ratio), int `[timeMs]` generates the thumbnail from the frame around the specified millisecond, int `[quality]`(0-100)|generates thumbnail from `[video]`|`[Future]`| -|thumbnailFile|String `[video]`, optional Map `[headers]`, String `[thumbnailPath]`(folder or full path where to store the thumbnail file, null to save to same folder as the video file), ImageFormat `[imageFormat]`(JPEG/PNG/WEBP), int `[maxHeight]`(0: for the original resolution of the video, or scaled by the source aspect ratio), int `[maxWidth]`(0: for the original resolution of the video, or scaled by the source aspect ratio), int `[timeMs]` generates the thumbnail from the frame around the specified millisecond, int `[quality]`(0-100)|creates a file of the thumbnail from the `[video]` |`[Future]`| +|thumbnailFile|String `[video]`, optional Map `[headers]`, String `[thumbnailPath]`(folder or full path where to store the thumbnail file, null to save to same folder as the video file) this ignored on the web, ImageFormat `[imageFormat]`(JPEG/PNG/WEBP), int `[maxHeight]`(0: for the original resolution of the video, or scaled by the source aspect ratio), int `[maxWidth]`(0: for the original resolution of the video, or scaled by the source aspect ratio), int `[timeMs]` generates the thumbnail from the frame around the specified millisecond, int `[quality]`(0-100)|creates a file of the thumbnail from the `[video]` |`[Future]`| Warning: > Giving both the `maxHeight` and `maxWidth` has different result on Android platform, it actually scales the thumbnail to the specified maxHeight and maxWidth. @@ -23,7 +23,7 @@ Warning: add [video_thumbnail](https://pub.dev/packages/video_thumbnail) as a dependency in your pubspec.yaml file. ```yaml dependencies: - video_thumbnail: ^0.5.3 + video_thumbnail: ^0.6.0 ``` **import** ```dart @@ -41,13 +41,15 @@ final uint8list = await VideoThumbnail.thumbnailData( **Generate a thumbnail file from video URL** ```dart -final fileName = await VideoThumbnail.thumbnailFile( +XFile thumbnailFile = await VideoThumbnail.thumbnailFile( video: "https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4", thumbnailPath: (await getTemporaryDirectory()).path, imageFormat: ImageFormat.WEBP, maxHeight: 64, // specify the height of the thumbnail, let the width auto-scaled to keep the source aspect ratio quality: 75, ); + +final image = kIsWeb ? Image.network(thumbnailFile.path) : Image.file(File(thumbnailFile.path)); ``` **Generate a thumbnail file from video Assets declared in pubspec.yaml** @@ -67,5 +69,19 @@ final fileName = await VideoThumbnail.thumbnailFile( ); ``` +## Limitations on the Web platform + +Flutter Thumbnail on the Web platform has some limitations that might surprise developers more familiar with mobile/desktop targets. + +In no particular order: + +### CORS headers +This plugin requires the server hosting the video to include appropriate CORS headers in the response. Specifically, the server must include the `Access-Control-Allow-Origin` and `Access-Control-Allow-Methods` headers in the response to the request. +For more information, please refer to the [Mozilla Developer Network documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). + +### HTTP range headers +This plugin requires the server hosting the video to support HTTP range headers. If the server does not support range requests, the plugin may generate a thumbnail from the first frame of the video instead of the desired frame. +For more information, please refer to the [Mozilla Developer Network documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). + ## Notes Fork or pull requests are always welcome. Currently it seems have a little performance issue while generating WebP thumbnail by using libwebp under iOS. diff --git a/video_thumbnail/analysis_options.yaml b/video_thumbnail/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/video_thumbnail/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/android/.gitignore b/video_thumbnail/android/.gitignore similarity index 100% rename from android/.gitignore rename to video_thumbnail/android/.gitignore diff --git a/android/build.gradle b/video_thumbnail/android/build.gradle similarity index 100% rename from android/build.gradle rename to video_thumbnail/android/build.gradle diff --git a/android/gradle.properties b/video_thumbnail/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to video_thumbnail/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/video_thumbnail/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to video_thumbnail/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/settings.gradle b/video_thumbnail/android/settings.gradle similarity index 100% rename from android/settings.gradle rename to video_thumbnail/android/settings.gradle diff --git a/android/src/main/AndroidManifest.xml b/video_thumbnail/android/src/main/AndroidManifest.xml similarity index 100% rename from android/src/main/AndroidManifest.xml rename to video_thumbnail/android/src/main/AndroidManifest.xml diff --git a/android/src/main/java/xyz/justsoft/video_thumbnail/VideoThumbnailPlugin.java b/video_thumbnail/android/src/main/java/xyz/justsoft/video_thumbnail/VideoThumbnailPlugin.java similarity index 100% rename from android/src/main/java/xyz/justsoft/video_thumbnail/VideoThumbnailPlugin.java rename to video_thumbnail/android/src/main/java/xyz/justsoft/video_thumbnail/VideoThumbnailPlugin.java diff --git a/example/.gitignore b/video_thumbnail/example/.gitignore similarity index 94% rename from example/.gitignore rename to video_thumbnail/example/.gitignore index 9d532b1..7283898 100644 --- a/example/.gitignore +++ b/video_thumbnail/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/example/.metadata b/video_thumbnail/example/.metadata similarity index 100% rename from example/.metadata rename to video_thumbnail/example/.metadata diff --git a/example/README.md b/video_thumbnail/example/README.md similarity index 100% rename from example/README.md rename to video_thumbnail/example/README.md diff --git a/video_thumbnail/example/analysis_options.yaml b/video_thumbnail/example/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/video_thumbnail/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/example/android/.gitignore b/video_thumbnail/example/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to video_thumbnail/example/android/.gitignore diff --git a/example/android/app/build.gradle b/video_thumbnail/example/android/app/build.gradle similarity index 100% rename from example/android/app/build.gradle rename to video_thumbnail/example/android/app/build.gradle diff --git a/example/android/app/src/debug/AndroidManifest.xml b/video_thumbnail/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to video_thumbnail/example/android/app/src/debug/AndroidManifest.xml diff --git a/example/android/app/src/main/AndroidManifest.xml b/video_thumbnail/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to video_thumbnail/example/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/java/xyz/justsoft/video_thumbnail_example/MainActivity.java b/video_thumbnail/example/android/app/src/main/java/xyz/justsoft/video_thumbnail_example/MainActivity.java similarity index 100% rename from example/android/app/src/main/java/xyz/justsoft/video_thumbnail_example/MainActivity.java rename to video_thumbnail/example/android/app/src/main/java/xyz/justsoft/video_thumbnail_example/MainActivity.java diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/video_thumbnail/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to video_thumbnail/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/video_thumbnail/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to video_thumbnail/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/video_thumbnail/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to video_thumbnail/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/video_thumbnail/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to video_thumbnail/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/video_thumbnail/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to video_thumbnail/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/video_thumbnail/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to video_thumbnail/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values/styles.xml b/video_thumbnail/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to video_thumbnail/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/app/src/profile/AndroidManifest.xml b/video_thumbnail/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/android/app/src/profile/AndroidManifest.xml rename to video_thumbnail/example/android/app/src/profile/AndroidManifest.xml diff --git a/example/android/build.gradle b/video_thumbnail/example/android/build.gradle similarity index 100% rename from example/android/build.gradle rename to video_thumbnail/example/android/build.gradle diff --git a/example/android/gradle.properties b/video_thumbnail/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to video_thumbnail/example/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/video_thumbnail/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to video_thumbnail/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/settings.gradle b/video_thumbnail/example/android/settings.gradle similarity index 100% rename from example/android/settings.gradle rename to video_thumbnail/example/android/settings.gradle diff --git a/example/ios/.gitignore b/video_thumbnail/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to video_thumbnail/example/ios/.gitignore diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/video_thumbnail/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to video_thumbnail/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/example/ios/Flutter/Debug.xcconfig b/video_thumbnail/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to video_thumbnail/example/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/video_thumbnail/example/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to video_thumbnail/example/ios/Flutter/Release.xcconfig diff --git a/example/ios/Podfile b/video_thumbnail/example/ios/Podfile similarity index 100% rename from example/ios/Podfile rename to video_thumbnail/example/ios/Podfile diff --git a/example/ios/Podfile.lock b/video_thumbnail/example/ios/Podfile.lock similarity index 100% rename from example/ios/Podfile.lock rename to video_thumbnail/example/ios/Podfile.lock diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/video_thumbnail/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to video_thumbnail/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to video_thumbnail/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/video_thumbnail/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to video_thumbnail/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/video_thumbnail/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to video_thumbnail/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/video_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to video_thumbnail/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.h b/video_thumbnail/example/ios/Runner/AppDelegate.h similarity index 100% rename from example/ios/Runner/AppDelegate.h rename to video_thumbnail/example/ios/Runner/AppDelegate.h diff --git a/example/ios/Runner/AppDelegate.m b/video_thumbnail/example/ios/Runner/AppDelegate.m similarity index 100% rename from example/ios/Runner/AppDelegate.m rename to video_thumbnail/example/ios/Runner/AppDelegate.m diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to video_thumbnail/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/video_thumbnail/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to video_thumbnail/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/video_thumbnail/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to video_thumbnail/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/example/ios/Runner/Info.plist b/video_thumbnail/example/ios/Runner/Info.plist similarity index 97% rename from example/ios/Runner/Info.plist rename to video_thumbnail/example/ios/Runner/Info.plist index 11ff486..c7f845b 100644 --- a/example/ios/Runner/Info.plist +++ b/video_thumbnail/example/ios/Runner/Info.plist @@ -1,60 +1,60 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - video_thumbnail_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - NSCameraUsageDescription - Camera Access Warning - NSMicrophoneUsageDescription - Uses the microphone to record audio. - NSPhotoLibraryAddUsageDescription - Photo Library Access Warning - NSPhotoLibraryUsageDescription - Photo Library Access Warning - - + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + video_thumbnail_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + NSCameraUsageDescription + Camera Access Warning + NSMicrophoneUsageDescription + Uses the microphone to record audio. + NSPhotoLibraryAddUsageDescription + Photo Library Access Warning + NSPhotoLibraryUsageDescription + Photo Library Access Warning + + diff --git a/example/ios/Runner/main.m b/video_thumbnail/example/ios/Runner/main.m similarity index 100% rename from example/ios/Runner/main.m rename to video_thumbnail/example/ios/Runner/main.m diff --git a/video_thumbnail/example/lib/main.dart b/video_thumbnail/example/lib/main.dart new file mode 100644 index 0000000..aadb5cb --- /dev/null +++ b/video_thumbnail/example/lib/main.dart @@ -0,0 +1,456 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: DemoHome(), + ); + } +} + +class ThumbnailRequest { + const ThumbnailRequest({ + required this.video, + required this.thumbnailPath, + required this.imageFormat, + required this.maxHeight, + required this.maxWidth, + required this.timeMs, + required this.quality, + required this.attachHeaders, + }); + + final String video; + final String? thumbnailPath; + final ImageFormat imageFormat; + final int maxHeight; + final int maxWidth; + final int timeMs; + final int quality; + final bool attachHeaders; +} + +class ThumbnailResult { + const ThumbnailResult({ + required this.image, + required this.dataSize, + required this.height, + required this.width, + }); + + final Image image; + final int dataSize; + final int height; + final int width; +} + +Future genThumbnail(ThumbnailRequest r) async { + Uint8List bytes; + final completer = Completer(); + if (r.thumbnailPath != null) { + final thumbnailFile = await VideoThumbnail.thumbnailFile( + video: r.video, + headers: r.attachHeaders + ? const { + 'USERHEADER1': 'user defined header1', + 'USERHEADER2': 'user defined header2', + } + : null, + thumbnailPath: r.thumbnailPath, + imageFormat: r.imageFormat, + maxHeight: r.maxHeight, + maxWidth: r.maxWidth, + timeMs: r.timeMs, + quality: r.quality, + ); + + debugPrint('thumbnail file is located: $thumbnailFile'); + + bytes = await thumbnailFile.readAsBytes(); + } else { + bytes = await VideoThumbnail.thumbnailData( + video: r.video, + headers: r.attachHeaders + ? const { + 'USERHEADER1': 'user defined header1', + 'USERHEADER2': 'user defined header2', + } + : null, + imageFormat: r.imageFormat, + maxHeight: r.maxHeight, + maxWidth: r.maxWidth, + timeMs: r.timeMs, + quality: r.quality, + ); + } + + final imageDataSize = bytes.length; + debugPrint('image size: $imageDataSize'); + + final image = Image.memory(bytes); + image.image.resolve(ImageConfiguration.empty).addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + completer.complete( + ThumbnailResult( + image: image, + dataSize: imageDataSize, + height: info.image.height, + width: info.image.width, + ), + ); + }, + onError: completer.completeError, + ), + ); + return completer.future; +} + +class GenThumbnailImage extends StatefulWidget { + const GenThumbnailImage({ + Key? key, + required this.thumbnailRequest, + }) : super(key: key); + final ThumbnailRequest thumbnailRequest; + + @override + State createState() => _GenThumbnailImageState(); +} + +class _GenThumbnailImageState extends State { + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: genThumbnail(widget.thumbnailRequest), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + final data = snapshot.data!; + final image = data.image; + final width = data.width; + final height = data.height; + final dataSize = data.dataSize; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Image ${widget.thumbnailRequest.thumbnailPath == null ? 'data size' : 'file size'}: $dataSize, width:$width, height:$height", + ), + ), + Container(color: Colors.grey), + image, + ], + ); + } else if (snapshot.hasError) { + return Container( + padding: const EdgeInsets.all(8), + color: Colors.red, + child: Text('Error:\n${snapshot.error}\n\n${snapshot.stackTrace}'), + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Generating the thumbnail for: ${widget.thumbnailRequest.video}...', + ), + const SizedBox(height: 10), + const CircularProgressIndicator(), + ], + ); + } + }, + ); + } +} + +class DemoHome extends StatefulWidget { + const DemoHome({Key? key}) : super(key: key); + + @override + State createState() => _DemoHomeState(); +} + +class _DemoHomeState extends State { + final _editNode = FocusNode(); + final _video = TextEditingController( + text: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4', + ); + ImageFormat _format = ImageFormat.JPEG; + int _quality = 50; + bool _attachHeaders = false; + int _sizeH = 0; + int _sizeW = 0; + int _timeMs = 0; + + GenThumbnailImage? _futureImage; + + String? _tempDir; + + @override + void initState() { + super.initState(); + + if (!kIsWeb) { + getTemporaryDirectory().then((d) => _tempDir = d.path); + } + } + + @override + Widget build(BuildContext context) { + final settings = [ + Slider( + value: _sizeH * 1.0, + onChanged: (v) => setState(() { + _editNode.unfocus(); + _sizeH = v.toInt(); + }), + max: 256, + divisions: 256, + label: '$_sizeH', + ), + Center( + child: (_sizeH == 0) + ? const Text( + "Original of the video's height or scaled by the source aspect ratio", + ) + : Text('Max height: $_sizeH(px)'), + ), + Slider( + value: _sizeW * 1.0, + onChanged: (v) => setState(() { + _editNode.unfocus(); + _sizeW = v.toInt(); + }), + max: 256, + divisions: 256, + label: '$_sizeW', + ), + Center( + child: (_sizeW == 0) + ? const Text( + "Original of the video's width or scaled by source aspect ratio", + ) + : Text('Max width: $_sizeW(px)'), + ), + Slider( + value: _timeMs * 1.0, + onChanged: (v) => setState(() { + _editNode.unfocus(); + _timeMs = v.toInt(); + }), + max: 10.0 * 1000, + divisions: 1000, + label: '$_timeMs', + ), + Center( + child: (_timeMs == 0) + ? const Text('The beginning of the video') + : Text('The closest frame at $_timeMs(ms) of the video'), + ), + Slider( + value: _quality * 1.0, + onChanged: (v) => setState(() { + _editNode.unfocus(); + _quality = v.toInt(); + }), + max: 100, + divisions: 100, + label: '$_quality', + ), + Center(child: Text('Quality: $_quality')), + SwitchListTile( + title: const Text('Attach Headers'), + value: _attachHeaders, + onChanged: (value) => setState(() => _attachHeaders = value), + secondary: const Icon(Icons.http), + ), + Padding( + padding: const EdgeInsets.fromLTRB(2, 10, 2, 8), + child: InputDecorator( + decoration: const InputDecoration( + border: OutlineInputBorder(), + filled: true, + isDense: true, + labelText: 'Thumbnail Format', + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + groupValue: _format, + value: ImageFormat.JPEG, + onChanged: (v) => setState(() { + _format = v!; + _editNode.unfocus(); + }), + ), + const Text('JPEG'), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + groupValue: _format, + value: ImageFormat.PNG, + onChanged: (v) => setState(() { + _format = v!; + _editNode.unfocus(); + }), + ), + const Text('PNG'), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Radio( + groupValue: _format, + value: ImageFormat.WEBP, + onChanged: (v) => setState(() { + _format = v!; + _editNode.unfocus(); + }), + ), + const Text('WebP'), + ], + ), + ], + ), + ), + ) + ]; + return Scaffold( + appBar: AppBar( + title: const Text('Thumbnail Plugin example'), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(2, 10, 2, 8), + child: TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + filled: true, + isDense: true, + labelText: 'Video URI', + ), + maxLines: null, + controller: _video, + focusNode: _editNode, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + onEditingComplete: _editNode.unfocus, + ), + ), + for (var i in settings) i, + Expanded( + child: Container( + color: Colors.grey[300], + child: ListView( + shrinkWrap: true, + children: [ + if (_futureImage != null) _futureImage! else const SizedBox(), + ], + ), + ), + ), + ], + ), + floatingActionButton: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + onPressed: () async { + final video = + await ImagePicker().pickVideo(source: ImageSource.camera); + setState(() { + _video.text = video?.path ?? ''; + }); + }, + tooltip: 'Capture a video', + child: const Icon(Icons.videocam), + ), + const SizedBox( + width: 5, + ), + FloatingActionButton( + onPressed: () async { + final video = + await ImagePicker().pickVideo(source: ImageSource.gallery); + setState(() { + _video.text = video?.path ?? ''; + }); + }, + tooltip: 'Pick a video', + child: const Icon(Icons.local_movies), + ), + const SizedBox( + width: 20, + ), + FloatingActionButton( + tooltip: 'Generate a data of thumbnail', + onPressed: () async { + setState(() { + _futureImage = GenThumbnailImage( + thumbnailRequest: ThumbnailRequest( + video: _video.text, + thumbnailPath: null, + imageFormat: _format, + maxHeight: _sizeH, + maxWidth: _sizeW, + timeMs: _timeMs, + quality: _quality, + attachHeaders: _attachHeaders, + ), + ); + }); + }, + child: const Text('Data'), + ), + const SizedBox( + width: 5, + ), + FloatingActionButton( + tooltip: 'Generate a file of thumbnail', + onPressed: () async { + setState(() { + _futureImage = GenThumbnailImage( + thumbnailRequest: ThumbnailRequest( + video: _video.text, + thumbnailPath: _tempDir, + imageFormat: _format, + maxHeight: _sizeH, + maxWidth: _sizeW, + timeMs: _timeMs, + quality: _quality, + attachHeaders: _attachHeaders, + ), + ); + }); + }, + child: const Text('File'), + ), + ], + ), + ); + } +} diff --git a/example/pubspec.yaml b/video_thumbnail/example/pubspec.yaml similarity index 94% rename from example/pubspec.yaml rename to video_thumbnail/example/pubspec.yaml index 2805862..8a39beb 100644 --- a/example/pubspec.yaml +++ b/video_thumbnail/example/pubspec.yaml @@ -4,20 +4,20 @@ version: 0.1.7 publish_to: 'none' environment: - sdk: ">=2.5.2 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter + image_picker: ^0.8.7+1 + path_provider: ^2.0.14 + video_thumbnail: + path: ../ dev_dependencies: flutter_test: sdk: flutter - - image_picker: '>=0.6.2 <2.0.0' - path_provider: ^1.6.0 - video_thumbnail: - path: ../ + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/example/test/widget_test.dart b/video_thumbnail/example/test/widget_test.dart similarity index 100% rename from example/test/widget_test.dart rename to video_thumbnail/example/test/widget_test.dart diff --git a/video_thumbnail/example/web/favicon.png b/video_thumbnail/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/video_thumbnail/example/web/favicon.png differ diff --git a/video_thumbnail/example/web/icons/Icon-192.png b/video_thumbnail/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/video_thumbnail/example/web/icons/Icon-192.png differ diff --git a/video_thumbnail/example/web/icons/Icon-512.png b/video_thumbnail/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/video_thumbnail/example/web/icons/Icon-512.png differ diff --git a/video_thumbnail/example/web/icons/Icon-maskable-192.png b/video_thumbnail/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/video_thumbnail/example/web/icons/Icon-maskable-192.png differ diff --git a/video_thumbnail/example/web/icons/Icon-maskable-512.png b/video_thumbnail/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/video_thumbnail/example/web/icons/Icon-maskable-512.png differ diff --git a/video_thumbnail/example/web/index.html b/video_thumbnail/example/web/index.html new file mode 100644 index 0000000..d49117c --- /dev/null +++ b/video_thumbnail/example/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + video_thumbnail_example + + + + + + + + + + diff --git a/video_thumbnail/example/web/manifest.json b/video_thumbnail/example/web/manifest.json new file mode 100644 index 0000000..89e5ab0 --- /dev/null +++ b/video_thumbnail/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "video_thumbnail_example", + "short_name": "video_thumbnail_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the video_thumbnail plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/ios/.gitignore b/video_thumbnail/ios/.gitignore similarity index 100% rename from ios/.gitignore rename to video_thumbnail/ios/.gitignore diff --git a/ios/Assets/.gitkeep b/video_thumbnail/ios/Assets/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to video_thumbnail/ios/Assets/.gitkeep diff --git a/ios/Classes/VideoThumbnailPlugin.h b/video_thumbnail/ios/Classes/VideoThumbnailPlugin.h similarity index 100% rename from ios/Classes/VideoThumbnailPlugin.h rename to video_thumbnail/ios/Classes/VideoThumbnailPlugin.h diff --git a/ios/Classes/VideoThumbnailPlugin.m b/video_thumbnail/ios/Classes/VideoThumbnailPlugin.m similarity index 100% rename from ios/Classes/VideoThumbnailPlugin.m rename to video_thumbnail/ios/Classes/VideoThumbnailPlugin.m diff --git a/ios/video_thumbnail.podspec b/video_thumbnail/ios/video_thumbnail.podspec similarity index 100% rename from ios/video_thumbnail.podspec rename to video_thumbnail/ios/video_thumbnail.podspec diff --git a/lib/video_thumbnail.dart b/video_thumbnail/lib/video_thumbnail.dart similarity index 56% rename from lib/video_thumbnail.dart rename to video_thumbnail/lib/video_thumbnail.dart index 78eb749..0553c13 100644 --- a/lib/video_thumbnail.dart +++ b/video_thumbnail/lib/video_thumbnail.dart @@ -7,52 +7,50 @@ /// * [video_thumbnail](https://pub.dev/packages/video_thumbnail) /// import 'dart:async'; -import 'dart:typed_data'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; +import 'package:video_thumbnail_platform_interface/video_thumbnail_platform_interface.dart'; -/// Support most popular image formats. -/// Uses libwebp to encode WebP image on iOS platform. -enum ImageFormat { JPEG, PNG, WEBP } +export 'package:cross_file/cross_file.dart' show XFile; +export 'package:video_thumbnail_platform_interface/video_thumbnail_platform_interface.dart' + show ImageFormat; class VideoThumbnail { - static const MethodChannel _channel = - const MethodChannel('plugins.justsoft.xyz/video_thumbnail'); - /// Generates a thumbnail file under specified thumbnail folder or given full path and name which matches expected ext. /// The video can be a local video file, or an URL repreents iOS or Android native supported video format. /// If the thumbnailPath is ommited or null, a thumbnail image file will be created under the same folder as the video file. /// Specify the maximum height or width for the thumbnail or 0 for same resolution as the original video. /// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format. - static Future thumbnailFile( - {required String video, - Map? headers, - String? thumbnailPath, - ImageFormat imageFormat = ImageFormat.PNG, - int maxHeight = 0, - int maxWidth = 0, - int timeMs = 0, - int quality = 10}) async { + static Future thumbnailFile({ + required String video, + Map? headers, + String? thumbnailPath, + ImageFormat imageFormat = ImageFormat.PNG, + int maxHeight = 0, + int maxWidth = 0, + int timeMs = 0, + int quality = 10, + }) async { assert(video.isNotEmpty); - if (video.isEmpty) return null; - final reqMap = { - 'video': video, - 'headers': headers, - 'path': thumbnailPath, - 'format': imageFormat.index, - 'maxh': maxHeight, - 'maxw': maxWidth, - 'timeMs': timeMs, - 'quality': quality - }; - return await _channel.invokeMethod('file', reqMap); + + return VideoThumbnailPlatform.instance.thumbnailFile( + video: video, + headers: headers, + thumbnailPath: thumbnailPath, + imageFormat: imageFormat, + maxHeight: maxHeight, + maxWidth: maxWidth, + timeMs: timeMs, + quality: quality, + ); } /// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...). /// The video can be a local video file, or an URL repreents iOS or Android native supported video format. /// Specify the maximum height or width for the thumbnail or 0 for same resolution as the original video. /// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format. - static Future thumbnailData({ + static Future thumbnailData({ required String video, Map? headers, ImageFormat imageFormat = ImageFormat.PNG, @@ -62,15 +60,15 @@ class VideoThumbnail { int quality = 10, }) async { assert(video.isNotEmpty); - final reqMap = { - 'video': video, - 'headers': headers, - 'format': imageFormat.index, - 'maxh': maxHeight, - 'maxw': maxWidth, - 'timeMs': timeMs, - 'quality': quality, - }; - return await _channel.invokeMethod('data', reqMap); + + return VideoThumbnailPlatform.instance.thumbnailData( + video: video, + headers: headers, + imageFormat: imageFormat, + maxHeight: maxHeight, + maxWidth: maxWidth, + timeMs: timeMs, + quality: quality, + ); } } diff --git a/pubspec.yaml b/video_thumbnail/pubspec.yaml similarity index 73% rename from pubspec.yaml rename to video_thumbnail/pubspec.yaml index 56b2eaa..b9e5f42 100644 --- a/pubspec.yaml +++ b/video_thumbnail/pubspec.yaml @@ -1,9 +1,9 @@ name: video_thumbnail description: A flutter plugin for creating a thumbnail from a local video file or from a video URL. -version: 0.5.3 -# author: John Zhong , Tairs Rzajevs , Grigori , Hafeez Ahmed , Leynier , Andreas Petrov, julek-kal, Nils Reichardt, Ajb Coder +version: 0.6.0 +# author: John Zhong , Tairs Rzajevs , Grigori , Hafeez Ahmed , Leynier , Andreas Petrov, julek-kal, Nils Reichardt, Ajb Coder, Márton Matuz homepage: https://github.com/justsoft -repository: https://github.com/justsoft/video_thumbnail +repository: https://github.com/justsoft/video_thumbnail/tree/master/video_thumbnail issue_tracker: https://github.com/justsoft/video_thumbnail/issues environment: @@ -13,10 +13,26 @@ environment: dependencies: flutter: sdk: flutter + # TODO(maRci002): remove git path when new package is published + video_thumbnail_platform_interface: + # path: ../video_thumbnail_platform_interface + git: + url: https://github.com/maRci002/video_thumbnail.git + ref: feat-web_implementation + path: video_thumbnail_platform_interface + # TODO(maRci002): remove git path when new package is published + video_thumbnail_web: + # path: ../video_thumbnail_web + git: + url: https://github.com/maRci002/video_thumbnail.git + ref: feat-web_implementation + path: video_thumbnail_web + cross_file: ^0.3.3+4 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec @@ -34,6 +50,8 @@ flutter: pluginClass: VideoThumbnailPlugin ios: pluginClass: VideoThumbnailPlugin + web: + default_package: video_thumbnail_web # To add assets to your plugin package, add an assets section, like this: # assets: diff --git a/test/video_thumbnail_test.dart b/video_thumbnail/test/video_thumbnail_test.dart similarity index 57% rename from test/video_thumbnail_test.dart rename to video_thumbnail/test/video_thumbnail_test.dart index 86dd3b6..b491ffe 100644 --- a/test/video_thumbnail_test.dart +++ b/video_thumbnail/test/video_thumbnail_test.dart @@ -3,12 +3,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; void main() { - const MethodChannel channel = MethodChannel('video_thumbnail'); + const channel = MethodChannel('video_thumbnail'); setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { final m = methodCall.method; - final a = methodCall.arguments; + final a = methodCall.arguments as Map; return '$m=${a["video"]}:${a["path"]}:${a["format"]}:${a["maxhow"]}:${a["quality"]}'; }); @@ -20,13 +20,15 @@ void main() { test('thumbnailData', () async { expect( - await VideoThumbnail.thumbnailFile( - video: 'video', - thumbnailPath: 'path', - imageFormat: ImageFormat.JPEG, - maxWidth: 123, - maxHeight: 123, - quality: 45), - 'file=video:path:0:123:45'); + await VideoThumbnail.thumbnailFile( + video: 'video', + thumbnailPath: 'path', + imageFormat: ImageFormat.JPEG, + maxWidth: 123, + maxHeight: 123, + quality: 45, + ), + 'file=video:path:0:123:45', + ); }); } diff --git a/video_file.png b/video_thumbnail/video_file.png similarity index 100% rename from video_file.png rename to video_thumbnail/video_file.png diff --git a/video_url.png b/video_thumbnail/video_url.png similarity index 100% rename from video_url.png rename to video_thumbnail/video_url.png diff --git a/video_thumbnail_platform_interface/CHANGELOG.md b/video_thumbnail_platform_interface/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/video_thumbnail_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/video_thumbnail_platform_interface/README.md b/video_thumbnail_platform_interface/README.md new file mode 100644 index 0000000..a78e5b9 --- /dev/null +++ b/video_thumbnail_platform_interface/README.md @@ -0,0 +1,12 @@ +# video_thumbnail_platform_interface + +A common platform interface for the [`video_thumbnail`][1] plugin. + +This interface allows platform-specific implementations of the `video_thumbnail` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + +# Usage + +To implement a new platform-specific implementation of `video_thumbnail`, extend [`VideoThumbnailPlatform`][2] with an implementation that performs the platform-specific behavior, and when you register your plugin, set the default `VideoThumbnailPlatform` by calling `VideoThumbnailPlatform.instance = MyPlatformVideoThumbnail()`. + +[1]: ../video_thumbnail +[2]: lib/video_thumbnail_platform_interface.dart \ No newline at end of file diff --git a/video_thumbnail_platform_interface/analysis_options.yaml b/video_thumbnail_platform_interface/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/video_thumbnail_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/video_thumbnail_platform_interface/lib/src/image_format.dart b/video_thumbnail_platform_interface/lib/src/image_format.dart new file mode 100644 index 0000000..dbbfb70 --- /dev/null +++ b/video_thumbnail_platform_interface/lib/src/image_format.dart @@ -0,0 +1,5 @@ +// ignore_for_file: constant_identifier_names + +/// Support most popular image formats. +/// Uses libwebp to encode WebP image on iOS platform. +enum ImageFormat { JPEG, PNG, WEBP } diff --git a/video_thumbnail_platform_interface/lib/src/video_thumbnail_method_channel.dart b/video_thumbnail_platform_interface/lib/src/video_thumbnail_method_channel.dart new file mode 100644 index 0000000..e228c67 --- /dev/null +++ b/video_thumbnail_platform_interface/lib/src/video_thumbnail_method_channel.dart @@ -0,0 +1,60 @@ +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:video_thumbnail_platform_interface/src/image_format.dart'; +import 'package:video_thumbnail_platform_interface/src/video_thumbnail_platform.dart'; + +/// An implementation of [VideoThumbnailPlatform] that uses method channels. +class MethodChannelVideoThumbnail extends VideoThumbnailPlatform { + /// The method channel used to interact with the native platform. + static const methodChannel = + MethodChannel('plugins.justsoft.xyz/video_thumbnail'); + + @override + Future thumbnailFile({ + required String video, + required Map? headers, + required String? thumbnailPath, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) async { + final reqMap = { + 'video': video, + 'headers': headers, + 'path': thumbnailPath, + 'format': imageFormat.index, + 'maxh': maxHeight, + 'maxw': maxWidth, + 'timeMs': timeMs, + 'quality': quality + }; + + final path = await methodChannel.invokeMethod('file', reqMap); + return XFile(path!); + } + + @override + Future thumbnailData({ + required String video, + required Map? headers, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) async { + final reqMap = { + 'video': video, + 'headers': headers, + 'format': imageFormat.index, + 'maxh': maxHeight, + 'maxw': maxWidth, + 'timeMs': timeMs, + 'quality': quality, + }; + final bytes = await methodChannel.invokeMethod('data', reqMap); + return bytes!; + } +} diff --git a/video_thumbnail_platform_interface/lib/src/video_thumbnail_platform.dart b/video_thumbnail_platform_interface/lib/src/video_thumbnail_platform.dart new file mode 100644 index 0000000..84249aa --- /dev/null +++ b/video_thumbnail_platform_interface/lib/src/video_thumbnail_platform.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; + +import 'package:cross_file/cross_file.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:video_thumbnail_platform_interface/src/image_format.dart'; +import 'package:video_thumbnail_platform_interface/src/video_thumbnail_method_channel.dart'; + +abstract class VideoThumbnailPlatform extends PlatformInterface { + /// Constructs a VideoThumbnailPlatform. + VideoThumbnailPlatform() : super(token: _token); + + static final Object _token = Object(); + + static VideoThumbnailPlatform _instance = MethodChannelVideoThumbnail(); + + /// The default instance of [VideoThumbnailPlatform] to use. + /// + /// Defaults to [MethodChannelVideoThumbnail]. + static VideoThumbnailPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [VideoThumbnailPlatform] when + /// they register themselves. + static set instance(VideoThumbnailPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future thumbnailFile({ + required String video, + required Map? headers, + required String? thumbnailPath, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) { + throw UnimplementedError('thumbnailFile() has not been implemented.'); + } + + Future thumbnailData({ + required String video, + required Map? headers, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) { + throw UnimplementedError('thumbnailData() has not been implemented.'); + } +} diff --git a/video_thumbnail_platform_interface/lib/video_thumbnail_platform_interface.dart b/video_thumbnail_platform_interface/lib/video_thumbnail_platform_interface.dart new file mode 100644 index 0000000..35f3a5c --- /dev/null +++ b/video_thumbnail_platform_interface/lib/video_thumbnail_platform_interface.dart @@ -0,0 +1,4 @@ +library video_thumbnail_platform_interface; + +export 'src/image_format.dart'; +export 'src/video_thumbnail_platform.dart'; diff --git a/video_thumbnail_platform_interface/pubspec.yaml b/video_thumbnail_platform_interface/pubspec.yaml new file mode 100644 index 0000000..5c05a05 --- /dev/null +++ b/video_thumbnail_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: video_thumbnail_platform_interface +description: A common platform interface for the video_thumbnail plugin. +version: 1.0.0 +homepage: https://github.com/justsoft +repository: https://github.com/justsoft/video_thumbnail/tree/master/video_thumbnail_platform_interface +issue_tracker: https://github.com/justsoft/video_thumbnail/issues + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.4 + cross_file: ^0.3.3+4 + +dev_dependencies: + lints: ^2.0.0 diff --git a/video_thumbnail_web/CHANGELOG.md b/video_thumbnail_web/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/video_thumbnail_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/video_thumbnail_web/README.md b/video_thumbnail_web/README.md new file mode 100644 index 0000000..f8c22bb --- /dev/null +++ b/video_thumbnail_web/README.md @@ -0,0 +1,12 @@ +# video_thumbnail_web + +The web implementation of [`video_thumbnail`][1]. + +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `video_thumbnail` normally. This package will be automatically included in your app when you do, so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you should add it to your `pubspec.yaml` as usual. + +[1]: ../video_thumbnail \ No newline at end of file diff --git a/video_thumbnail_web/analysis_options.yaml b/video_thumbnail_web/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/video_thumbnail_web/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/video_thumbnail_web/lib/video_thumbnail_web.dart b/video_thumbnail_web/lib/video_thumbnail_web.dart new file mode 100644 index 0000000..e43d116 --- /dev/null +++ b/video_thumbnail_web/lib/video_thumbnail_web.dart @@ -0,0 +1,248 @@ +import 'dart:async'; +import 'dart:html'; +import 'dart:math' as math; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:video_thumbnail_platform_interface/video_thumbnail_platform_interface.dart'; + +// An error code value to error name Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorName = { + 1: 'MEDIA_ERR_ABORTED', + 2: 'MEDIA_ERR_NETWORK', + 3: 'MEDIA_ERR_DECODE', + 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED', +}; + +// An error code value to description Map. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code +const Map _kErrorValueToErrorDescription = { + 1: 'The user canceled the fetching of the video.', + 2: 'A network error occurred while fetching the video, despite having previously been available.', + 3: 'An error occurred while trying to decode the video, despite having previously been determined to be usable.', + 4: 'The video has been found to be unsuitable (missing or in a format not supported by your browser).', +}; + +// The default error message, when the error is an empty string +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message +const String _kDefaultErrorMessage = + 'No further diagnostic information can be determined or provided.'; + +/// A web implementation of the VideoThumbnailPlatform of the VideoThumbnail plugin. +class VideoThumbnailWeb extends VideoThumbnailPlatform { + /// Constructs a VideoThumbnailWeb + VideoThumbnailWeb(); + + static void registerWith(Registrar registrar) { + VideoThumbnailPlatform.instance = VideoThumbnailWeb(); + } + + @override + Future thumbnailFile({ + required String video, + required Map? headers, + required String? thumbnailPath, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) async { + final blob = await _createThumbnail( + videoSrc: video, + headers: headers, + imageFormat: imageFormat, + maxHeight: maxHeight, + maxWidth: maxWidth, + timeMs: timeMs, + quality: quality, + ); + + return XFile(Url.createObjectUrlFromBlob(blob), mimeType: blob.type); + } + + @override + Future thumbnailData({ + required String video, + required Map? headers, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) async { + final blob = await _createThumbnail( + videoSrc: video, + headers: headers, + imageFormat: imageFormat, + maxHeight: maxHeight, + maxWidth: maxWidth, + timeMs: timeMs, + quality: quality, + ); + final path = Url.createObjectUrlFromBlob(blob); + final file = XFile(path, mimeType: blob.type); + final bytes = await file.readAsBytes(); + Url.revokeObjectUrl(path); + + return bytes; + } + + Future _createThumbnail({ + required String videoSrc, + required Map? headers, + required ImageFormat imageFormat, + required int maxHeight, + required int maxWidth, + required int timeMs, + required int quality, + }) async { + final completer = Completer(); + + final video = document.createElement('video') as VideoElement; + final timeSec = math.max(timeMs / 1000, 0); + final fetchVideo = headers != null && headers.isNotEmpty; + + video.onLoadedMetadata.listen((event) { + video.currentTime = timeSec; + + if (fetchVideo) { + Url.revokeObjectUrl(video.src); + } + }); + + video.onSeeked.listen((Event e) async { + if (!completer.isCompleted) { + final canvas = document.createElement('canvas') as CanvasElement; + final ctx = canvas.getContext('2d')! as CanvasRenderingContext2D; + + if (maxWidth == 0 && maxHeight == 0) { + canvas + ..width = video.videoWidth + ..height = video.videoHeight; + ctx.drawImage(video, 0, 0); + } else { + final aspectRatio = video.videoWidth / video.videoHeight; + if (maxWidth == 0) { + maxWidth = (maxHeight * aspectRatio).round(); + } else if (maxHeight == 0) { + maxHeight = (maxWidth / aspectRatio).round(); + } + + final inputAspectRatio = maxWidth / maxHeight; + if (aspectRatio > inputAspectRatio) { + maxHeight = (maxWidth / aspectRatio).round(); + } else { + maxWidth = (maxHeight * aspectRatio).round(); + } + + canvas + ..width = maxWidth + ..height = maxHeight; + ctx.drawImageScaled(video, 0, 0, maxWidth, maxHeight); + } + + try { + final blob = canvas.toBlob( + _imageFormatToCanvasFormat(imageFormat), + quality / 100, + ); + + completer.complete(blob); + } catch (e, s) { + completer.completeError( + PlatformException( + code: 'CANVAS_EXPORT_ERROR', + details: e, + stacktrace: s.toString(), + ), + s, + ); + } + } + }); + + video.onError.listen((Event e) { + // The Event itself (_) doesn't contain info about the actual error. + // We need to look at the HTMLMediaElement.error. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error + if (!completer.isCompleted) { + final error = video.error!; + completer.completeError( + PlatformException( + code: _kErrorValueToErrorName[error.code]!, + message: + error.message != '' ? error.message : _kDefaultErrorMessage, + details: _kErrorValueToErrorDescription[error.code], + ), + ); + } + }); + + if (fetchVideo) { + try { + final blob = await _fetchVideoByHeaders( + videoSrc: videoSrc, + headers: headers!, + ); + + video.src = Url.createObjectUrlFromBlob(blob); + } catch (e, s) { + completer.completeError(e, s); + } + } else { + video + ..crossOrigin = 'Anonymous' + ..src = videoSrc; + } + + return completer.future; + } + + /// Fetching video by [headers]. + /// + /// To avoid reading the video's bytes into memory, set the + /// [HttpRequest.responseType] to 'blob'. This allows the blob to be stored in + /// the browser's disk or memory cache. + Future _fetchVideoByHeaders({ + required String videoSrc, + required Map headers, + }) async { + final completer = Completer(); + + final xhr = HttpRequest() + ..open('GET', videoSrc, async: true) + ..responseType = 'blob'; + headers.forEach(xhr.setRequestHeader); + + xhr.onLoad.first.then((ProgressEvent value) { + completer.complete(xhr.response as Blob); + }); + + xhr.onError.first.then((ProgressEvent value) { + completer.completeError( + PlatformException( + code: 'VIDEO_FETCH_ERROR', + message: 'Status: ${xhr.statusText}', + ), + ); + }); + + xhr.send(); + + return completer.future; + } + + String _imageFormatToCanvasFormat(ImageFormat imageFormat) { + switch (imageFormat) { + case ImageFormat.JPEG: + return 'image/jpeg'; + case ImageFormat.PNG: + return 'image/png'; + case ImageFormat.WEBP: + return 'image/webp'; + } + } +} diff --git a/video_thumbnail_web/pubspec.yaml b/video_thumbnail_web/pubspec.yaml new file mode 100644 index 0000000..a2e7cfa --- /dev/null +++ b/video_thumbnail_web/pubspec.yaml @@ -0,0 +1,35 @@ +name: video_thumbnail_web +description: The web implementation of video_thumbnail. +version: 1.0.0 +homepage: https://github.com/justsoft +repository: https://github.com/justsoft/video_thumbnail/tree/master/video_thumbnail_web +issue_tracker: https://github.com/justsoft/video_thumbnail/issues + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + plugin_platform_interface: ^2.1.4 + # TODO(maRci002): remove git path when new package is published + video_thumbnail_platform_interface: + # path: ../video_thumbnail_platform_interface + git: + url: https://github.com/maRci002/video_thumbnail.git + ref: feat-web_implementation + path: video_thumbnail_platform_interface + cross_file: ^0.3.3+4 + +dev_dependencies: + lints: ^2.0.0 + +flutter: + plugin: + implements: video_thumbnail + platforms: + web: + pluginClass: VideoThumbnailWeb + fileName: video_thumbnail_web.dart