feat(extensions): Youtube Video Player Support Mode (#1916)

* chore: update comment for packages section in pubspec.yaml

* chore: add youtube_explode_dart package

* feat: add YoutubeVideoSupportMode

* docs: add a link to the 'flutter_inappwebview' plugin desktop support issue in YoutubeVideoSupportMode

* chore: add TODO in QuillToolbarVideoButton
pull/1962/head v9.5.1
Ellet 9 months ago committed by GitHub
parent a87b8cbdfa
commit c8a99c943f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      example/lib/screens/quill/my_quill_editor.dart
  2. 2
      example/pubspec.yaml
  3. 2
      flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart
  4. 28
      flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart
  5. 44
      flutter_quill_extensions/lib/embeds/widgets/video_app.dart
  6. 141
      flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart
  7. 7
      flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart
  8. 19
      flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart
  9. 5
      flutter_quill_extensions/pubspec.yaml
  10. 2
      pubspec.yaml

@ -4,11 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart'
show CachedNetworkImageProvider;
import 'package:desktop_drop/desktop_drop.dart' show DropTarget;
import 'package:flutter/material.dart';
import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb;
import 'package:flutter_quill/extensions.dart'
show isAndroid, isDesktop, isIOS, isWeb;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/embeds/widgets/image.dart'
show getImageProviderByImageSource, imageFileExtensions;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:flutter_quill_extensions/models/config/video/editor/youtube_video_support_mode.dart';
import 'package:path/path.dart' as path;
import '../../extensions/scaffold_messenger.dart';
@ -128,6 +130,13 @@ class MyQuillEditor extends StatelessWidget {
);
},
),
videoEmbedConfigurations: QuillEditorVideoEmbedConfigurations(
// Loading YouTube videos on Desktop is not supported yet
// when using iframe platform view
youtubeVideoSupportMode: isDesktop(supportWeb: false)
? YoutubeVideoSupportMode.customPlayerWithDownloadUrl
: YoutubeVideoSupportMode.iframeView,
),
)),
TimeStampEmbedBuilderWidget(),
],

@ -19,7 +19,7 @@ dependencies:
flutter_quill_test: ^9.3.4
quill_html_converter: ^9.3.4
quill_pdf_converter: ^9.3.4
# Normal packages
# Dart Packages
path: ^1.8.3
equatable: ^2.0.5
cross_file: ^0.3.4

@ -37,6 +37,7 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
return YoutubeVideoApp(
videoUrl: videoUrl,
readOnly: readOnly,
youtubeVideoSupportMode: configurations.youtubeVideoSupportMode,
);
}
final ((elementSize), margin, alignment) = getElementAttributes(
@ -53,7 +54,6 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
alignment: alignment,
child: VideoApp(
videoUrl: videoUrl,
context: context,
readOnly: readOnly,
onVideoInit: configurations.onVideoInit,
),

@ -9,6 +9,8 @@ import '../../others/image_video_utils.dart';
import '../video.dart';
import 'select_video_source.dart';
// TODO: Add custom callback to validate the video link input
class QuillToolbarVideoButton extends StatelessWidget {
const QuillToolbarVideoButton({
required this.controller,
@ -72,11 +74,6 @@ class QuillToolbarVideoButton extends StatelessWidget {
final childBuilder =
options.childBuilder ?? baseButtonExtraOptions(context)?.childBuilder;
// final iconColor =
// iconTheme?.iconUnselectedFillColor ?? theme.iconTheme.color;
// final iconFillColor = iconTheme?.iconUnselectedFillColor ??
// (options.fillColor ?? theme.canvasColor);
if (childBuilder != null) {
return childBuilder(
QuillToolbarVideoButtonOptions(
@ -150,18 +147,6 @@ class QuillToolbarVideoButton extends StatelessWidget {
.onVideoInsertCallback(videoUrl, controller);
await options.videoConfigurations.onVideoInsertedCallback?.call(videoUrl);
}
// if (options.onVideoPickCallback != null) {
// final selector = options.mediaPickSettingSelector ??
// ImageVideoUtils.selectMediaPickSetting;
// final source = await selector(context);
// if (source != null) {
// if (source == MediaPickSetting.gallery) {
// } else {
// await _typeLink(context);
// }
// }
// } else {}
}
Future<String?> _typeLink(BuildContext context) async {
@ -176,13 +161,4 @@ class QuillToolbarVideoButton extends StatelessWidget {
);
return value;
}
// void _linkSubmitted(String? value) {
// if (value != null && value.isNotEmpty) {
// final index = controller.selection.baseOffset;
// final length = controller.selection.extentOffset - index;
// controller.replaceText(index, length, BlockEmbed.video(value), null);
// }
// }
}

@ -13,14 +13,16 @@ import '../../flutter_quill_extensions.dart';
class VideoApp extends StatefulWidget {
const VideoApp({
required this.videoUrl,
required this.context,
required this.readOnly,
@Deprecated(
'The context is no longer required and will be removed on future releases',
)
BuildContext? context,
super.key,
this.onVideoInit,
});
final String videoUrl;
final BuildContext context;
final bool readOnly;
final void Function(GlobalKey videoContainerKey)? onVideoInit;
@ -92,29 +94,33 @@ class VideoAppState extends State<VideoApp> {
: _controller.play();
});
},
child: Stack(alignment: Alignment.center, children: [
Center(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)),
_controller.value.isPlaying
? const SizedBox.shrink()
: Container(
color: const Color(0xfff5f5f5),
child: const Icon(
Icons.play_arrow,
size: 60,
color: Colors.blueGrey,
))
]),
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)),
_controller.value.isPlaying
? const SizedBox.shrink()
: Container(
color: const Color(0xfff5f5f5),
child: const Icon(
Icons.play_arrow,
size: 60,
color: Colors.blueGrey,
),
)
],
),
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
super.dispose();
}
}

@ -1,78 +1,143 @@
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart' show DefaultStyles;
import 'package:url_launcher/url_launcher.dart' show launchUrl;
import 'package:url_launcher/url_launcher_string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../../models/config/video/editor/youtube_video_support_mode.dart';
import 'video_app.dart';
class YoutubeVideoApp extends StatefulWidget {
const YoutubeVideoApp({
required this.videoUrl,
required this.readOnly,
required this.youtubeVideoSupportMode,
super.key,
});
final String videoUrl;
final bool readOnly;
final YoutubeVideoSupportMode youtubeVideoSupportMode;
@override
YoutubeVideoAppState createState() => YoutubeVideoAppState();
}
class YoutubeVideoAppState extends State<YoutubeVideoApp> {
YoutubePlayerController? _youtubeController;
YoutubePlayerController? _youtubeIframeController;
/// On some platforms such as desktop, Webview is not supported yet
/// as a result the youtube video player package is not supported too
/// this future will be not null and fetch the video url to load it using
/// [VideoApp]
Future<String>? _loadYoutubeVideoByDownloadUrlFuture;
/// Null if the video URL is not a YouTube video
String? get _videoId {
return YoutubePlayer.convertUrlToId(widget.videoUrl);
}
@override
void initState() {
super.initState();
final videoId = YoutubePlayer.convertUrlToId(widget.videoUrl);
if (videoId != null) {
_youtubeController = YoutubePlayerController(
initialVideoId: videoId,
flags: const YoutubePlayerFlags(
autoPlay: false,
),
);
final videoId = _videoId;
if (videoId == null) {
return;
}
switch (widget.youtubeVideoSupportMode) {
case YoutubeVideoSupportMode.disabled:
break;
case YoutubeVideoSupportMode.iframeView:
_youtubeIframeController = YoutubePlayerController(
initialVideoId: videoId,
flags: const YoutubePlayerFlags(
autoPlay: false,
),
);
break;
case YoutubeVideoSupportMode.customPlayerWithDownloadUrl:
_loadYoutubeVideoByDownloadUrlFuture =
_loadYoutubeVideoWithVideoPlayerByVideoUrl();
break;
}
}
Future<String> _loadYoutubeVideoWithVideoPlayerByVideoUrl() async {
final youtubeExplode = YoutubeExplode();
final manifest =
await youtubeExplode.videos.streamsClient.getManifest(_videoId);
final streamInfo = manifest.muxed.withHighestBitrate();
final videoDownloadUri = streamInfo.url;
return videoDownloadUri.toString();
}
Widget _clickableVideoLinkText({required DefaultStyles defaultStyles}) {
return RichText(
text: TextSpan(
text: widget.videoUrl,
style: defaultStyles.link,
recognizer: TapGestureRecognizer()
..onTap = () => launchUrlString(widget.videoUrl),
),
);
}
@override
Widget build(BuildContext context) {
final defaultStyles = DefaultStyles.getInstance(context);
final youtubeController = _youtubeController;
if (youtubeController == null) {
if (widget.readOnly) {
return RichText(
text: TextSpan(
text: widget.videoUrl,
style: defaultStyles.link,
recognizer: TapGestureRecognizer()
..onTap = () => launchUrl(
Uri.parse(widget.videoUrl),
),
switch (widget.youtubeVideoSupportMode) {
case YoutubeVideoSupportMode.disabled:
throw UnsupportedError('YouTube video links are not supported');
case YoutubeVideoSupportMode.iframeView:
final youtubeController = _youtubeIframeController;
if (youtubeController == null) {
if (widget.readOnly) {
return _clickableVideoLinkText(defaultStyles: defaultStyles);
}
return RichText(
text: TextSpan(text: widget.videoUrl, style: defaultStyles.link),
);
}
return YoutubePlayerBuilder(
player: YoutubePlayer(
controller: youtubeController,
showVideoProgressIndicator: true,
),
builder: (context, player) {
return player;
},
);
case YoutubeVideoSupportMode.customPlayerWithDownloadUrl:
assert(
_loadYoutubeVideoByDownloadUrlFuture != null,
'The load youtube video future should not null for "${widget.youtubeVideoSupportMode}" mode',
);
}
return RichText(
text: TextSpan(text: widget.videoUrl, style: defaultStyles.link),
);
return FutureBuilder<String>(
future: _loadYoutubeVideoByDownloadUrlFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator.adaptive());
}
if (snapshot.hasError) {
return _clickableVideoLinkText(defaultStyles: defaultStyles);
}
return VideoApp(
videoUrl: snapshot.requireData,
readOnly: widget.readOnly,
);
},
);
}
return YoutubePlayerBuilder(
player: YoutubePlayer(
controller: youtubeController,
showVideoProgressIndicator: true,
),
builder: (context, player) {
return player;
},
);
}
@override
void dispose() {
_youtubeController?.dispose();
_youtubeIframeController?.dispose();
super.dispose();
}
}

@ -1,10 +1,13 @@
import 'package:flutter/widgets.dart' show GlobalKey;
import 'package:meta/meta.dart' show immutable;
import 'youtube_video_support_mode.dart';
@immutable
class QuillEditorVideoEmbedConfigurations {
const QuillEditorVideoEmbedConfigurations({
this.onVideoInit,
this.youtubeVideoSupportMode = YoutubeVideoSupportMode.iframeView,
});
/// [onVideoInit] is a callback function that gets triggered when
@ -21,4 +24,8 @@ class QuillEditorVideoEmbedConfigurations {
/// // Customize other callback functions as needed
/// ```
final void Function(GlobalKey videoContainerKey)? onVideoInit;
/// Specifies how YouTube videos should be loaded if the video URL
/// is YouTube video.
final YoutubeVideoSupportMode youtubeVideoSupportMode;
}

@ -0,0 +1,19 @@
/// Enum represents the different modes for handling YouTube video support.
enum YoutubeVideoSupportMode {
/// Disable loading of YouTube videos.
disabled,
/// Load the video using the official YouTube IFrame API.
/// See [YouTube IFrame API](https://developers.google.com/youtube/iframe_api_reference) for more details.
///
/// This will use Platform View on native platforms to use WebView
/// The WebView might not be supported on Desktop and will throw an exception
///
/// See [Flutter InAppWebview Support for Flutter Desktop](https://github.com/pichillilorenzo/flutter_inappwebview/issues/460)
iframeView,
/// Load the video using a custom video player by fetching the YouTube video URL.
/// Note: This might violate YouTube's terms of service.
/// See [YouTube Terms of Service](https://www.youtube.com/static?template=terms) for more details.
customPlayerWithDownloadUrl,
}

@ -28,7 +28,7 @@ dependencies:
flutter:
sdk: flutter
# Normal packages
# Dart Packages
http: ^1.2.0
path: ^1.8.3
meta: ^1.10.0
@ -37,10 +37,11 @@ dependencies:
flutter_quill: ^9.3.12
photo_view: ^0.15.0
youtube_explode_dart: ^2.2.1
# Plugins
video_player: ^2.8.1
youtube_player_flutter: ^9.0.0
youtube_player_flutter: ^9.0.1
url_launcher: ^6.2.1
super_clipboard: ^0.8.15
gal: ^2.3.0

@ -42,7 +42,7 @@ dependencies:
flutter_localizations:
sdk: flutter
# Normal packages
# Dart Packages
intl: ^0.19.0
dart_quill_delta: ^9.3.3
collection: ^1.17.0

Loading…
Cancel
Save