diff --git a/example/lib/screens/quill/my_quill_editor.dart b/example/lib/screens/quill/my_quill_editor.dart index be3c48ef..e3ed9942 100644 --- a/example/lib/screens/quill/my_quill_editor.dart +++ b/example/lib/screens/quill/my_quill_editor.dart @@ -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 vidoes on Desktop is not supported yet + // when using iframe platform view + youtubeVideoSupportMode: isDesktop(supportWeb: false) + ? YoutubeVideoSupportMode.customPlayerWithDownloadUrl + : YoutubeVideoSupportMode.iframeView, + ), )), TimeStampEmbedBuilderWidget(), ], diff --git a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart index ad04fd9d..b201e162 100644 --- a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart +++ b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart @@ -37,6 +37,7 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder { return YoutubeVideoApp( videoUrl: videoUrl, readOnly: readOnly, + youtubeVideoSupportMode: configurations.youtubeVideoSupportMode, ); } final ((elementSize), margin, alignment) = getElementAttributes( diff --git a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart b/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart index 0125a3b7..a6ce72cc 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart +++ b/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart @@ -1,53 +1,65 @@ import 'package:flutter/gestures.dart' show TapGestureRecognizer; import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart'; import 'package:flutter_quill/flutter_quill.dart' show DefaultStyles; 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 { - YoutubePlayerController? _youtubeController; - late String? _videoId; + 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? _loadYoutubeVideoWithVideoPlayer; + Future? _loadYoutubeVideoByDownloadUrlFuture; + + /// Null if the video URL is not a YouTube video + String? get _videoId { + return YoutubePlayer.convertUrlToId(widget.videoUrl); + } @override void initState() { super.initState(); - _videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); final videoId = _videoId; - if (videoId != null) { - _youtubeController = YoutubePlayerController( - initialVideoId: videoId, - flags: const YoutubePlayerFlags( - autoPlay: false, - ), - ); - if (isDesktop(supportWeb: false)) { - _loadYoutubeVideoWithVideoPlayer = + 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; } } @@ -56,11 +68,11 @@ class YoutubeVideoAppState extends State { final manifest = await youtubeExplode.videos.streamsClient.getManifest(_videoId); final streamInfo = manifest.muxed.withHighestBitrate(); - final downloadUrl = streamInfo.url; - return downloadUrl.toString(); + final videoDownloadUri = streamInfo.url; + return videoDownloadUri.toString(); } - Widget _videoLink({required DefaultStyles defaultStyles}) { + Widget _clickableVideoLinkText({required DefaultStyles defaultStyles}) { return RichText( text: TextSpan( text: widget.videoUrl, @@ -74,50 +86,58 @@ class YoutubeVideoAppState extends State { @override Widget build(BuildContext context) { final defaultStyles = DefaultStyles.getInstance(context); - final youtubeController = _youtubeController; - if (youtubeController == null) { - if (widget.readOnly) { - return _videoLink(defaultStyles: defaultStyles); - } - - return RichText( - text: TextSpan(text: widget.videoUrl, style: defaultStyles.link), - ); - } + switch (widget.youtubeVideoSupportMode) { + case YoutubeVideoSupportMode.disabled: + throw UnsupportedError('YouTube video links are not supported'); + case YoutubeVideoSupportMode.iframeView: + final youtubeController = _youtubeIframeController; - // Workaround as YoutubePlayer doesn't support Desktop - if (_loadYoutubeVideoWithVideoPlayer != null) { - return FutureBuilder( - future: _loadYoutubeVideoWithVideoPlayer, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator.adaptive()); + if (youtubeController == null) { + if (widget.readOnly) { + return _clickableVideoLinkText(defaultStyles: defaultStyles); } - if (snapshot.hasError) { - return _videoLink(defaultStyles: defaultStyles); - } - return VideoApp( - videoUrl: snapshot.requireData, - readOnly: widget.readOnly, + + 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 FutureBuilder( + 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(); } } diff --git a/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart b/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart index 53039dee..cecc1c2e 100644 --- a/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart +++ b/flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart @@ -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; } diff --git a/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart b/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart new file mode 100644 index 00000000..35e2713e --- /dev/null +++ b/flutter_quill_extensions/lib/models/config/video/editor/youtube_video_support_mode.dart @@ -0,0 +1,17 @@ +/// Enum representing 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 + 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, +}