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 QuillToolbarVideoButtonpull/1962/head v9.5.1
parent
a87b8cbdfa
commit
c8a99c943f
10 changed files with 172 additions and 89 deletions
@ -1,78 +1,143 @@ |
|||||||
import 'package:flutter/gestures.dart' show TapGestureRecognizer; |
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: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 '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 { |
class YoutubeVideoApp extends StatefulWidget { |
||||||
const YoutubeVideoApp({ |
const YoutubeVideoApp({ |
||||||
required this.videoUrl, |
required this.videoUrl, |
||||||
required this.readOnly, |
required this.readOnly, |
||||||
|
required this.youtubeVideoSupportMode, |
||||||
super.key, |
super.key, |
||||||
}); |
}); |
||||||
|
|
||||||
final String videoUrl; |
final String videoUrl; |
||||||
final bool readOnly; |
final bool readOnly; |
||||||
|
final YoutubeVideoSupportMode youtubeVideoSupportMode; |
||||||
|
|
||||||
@override |
@override |
||||||
YoutubeVideoAppState createState() => YoutubeVideoAppState(); |
YoutubeVideoAppState createState() => YoutubeVideoAppState(); |
||||||
} |
} |
||||||
|
|
||||||
class YoutubeVideoAppState extends State<YoutubeVideoApp> { |
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 |
@override |
||||||
void initState() { |
void initState() { |
||||||
super.initState(); |
super.initState(); |
||||||
final videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); |
final videoId = _videoId; |
||||||
if (videoId != null) { |
if (videoId == null) { |
||||||
_youtubeController = YoutubePlayerController( |
return; |
||||||
initialVideoId: videoId, |
|
||||||
flags: const YoutubePlayerFlags( |
|
||||||
autoPlay: false, |
|
||||||
), |
|
||||||
); |
|
||||||
} |
} |
||||||
|
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 |
@override |
||||||
Widget build(BuildContext context) { |
Widget build(BuildContext context) { |
||||||
final defaultStyles = DefaultStyles.getInstance(context); |
final defaultStyles = DefaultStyles.getInstance(context); |
||||||
final youtubeController = _youtubeController; |
|
||||||
|
switch (widget.youtubeVideoSupportMode) { |
||||||
if (youtubeController == null) { |
case YoutubeVideoSupportMode.disabled: |
||||||
if (widget.readOnly) { |
throw UnsupportedError('YouTube video links are not supported'); |
||||||
return RichText( |
case YoutubeVideoSupportMode.iframeView: |
||||||
text: TextSpan( |
final youtubeController = _youtubeIframeController; |
||||||
text: widget.videoUrl, |
|
||||||
style: defaultStyles.link, |
if (youtubeController == null) { |
||||||
recognizer: TapGestureRecognizer() |
if (widget.readOnly) { |
||||||
..onTap = () => launchUrl( |
return _clickableVideoLinkText(defaultStyles: defaultStyles); |
||||||
Uri.parse(widget.videoUrl), |
} |
||||||
), |
|
||||||
|
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( |
return FutureBuilder<String>( |
||||||
text: TextSpan(text: widget.videoUrl, style: defaultStyles.link), |
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 |
@override |
||||||
void dispose() { |
void dispose() { |
||||||
_youtubeController?.dispose(); |
_youtubeIframeController?.dispose(); |
||||||
super.dispose(); |
super.dispose(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -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, |
||||||
|
} |
Loading…
Reference in new issue