Skip to content

Commit c8a99c9

Browse files
authored
feat(extensions): Youtube Video Player Support Mode (singerdmx#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
1 parent a87b8cb commit c8a99c9

File tree

10 files changed

+172
-89
lines changed

10 files changed

+172
-89
lines changed

example/lib/screens/quill/my_quill_editor.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import 'package:cached_network_image/cached_network_image.dart'
44
show CachedNetworkImageProvider;
55
import 'package:desktop_drop/desktop_drop.dart' show DropTarget;
66
import 'package:flutter/material.dart';
7-
import 'package:flutter_quill/extensions.dart' show isAndroid, isIOS, isWeb;
7+
import 'package:flutter_quill/extensions.dart'
8+
show isAndroid, isDesktop, isIOS, isWeb;
89
import 'package:flutter_quill/flutter_quill.dart';
910
import 'package:flutter_quill_extensions/embeds/widgets/image.dart'
1011
show getImageProviderByImageSource, imageFileExtensions;
1112
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
13+
import 'package:flutter_quill_extensions/models/config/video/editor/youtube_video_support_mode.dart';
1214
import 'package:path/path.dart' as path;
1315

1416
import '../../extensions/scaffold_messenger.dart';
@@ -128,6 +130,13 @@ class MyQuillEditor extends StatelessWidget {
128130
);
129131
},
130132
),
133+
videoEmbedConfigurations: QuillEditorVideoEmbedConfigurations(
134+
// Loading YouTube videos on Desktop is not supported yet
135+
// when using iframe platform view
136+
youtubeVideoSupportMode: isDesktop(supportWeb: false)
137+
? YoutubeVideoSupportMode.customPlayerWithDownloadUrl
138+
: YoutubeVideoSupportMode.iframeView,
139+
),
131140
)),
132141
TimeStampEmbedBuilderWidget(),
133142
],

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies:
1919
flutter_quill_test: ^9.3.4
2020
quill_html_converter: ^9.3.4
2121
quill_pdf_converter: ^9.3.4
22-
# Normal packages
22+
# Dart Packages
2323
path: ^1.8.3
2424
equatable: ^2.0.5
2525
cross_file: ^0.3.4

flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
3737
return YoutubeVideoApp(
3838
videoUrl: videoUrl,
3939
readOnly: readOnly,
40+
youtubeVideoSupportMode: configurations.youtubeVideoSupportMode,
4041
);
4142
}
4243
final ((elementSize), margin, alignment) = getElementAttributes(
@@ -53,7 +54,6 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
5354
alignment: alignment,
5455
child: VideoApp(
5556
videoUrl: videoUrl,
56-
context: context,
5757
readOnly: readOnly,
5858
onVideoInit: configurations.onVideoInit,
5959
),

flutter_quill_extensions/lib/embeds/video/toolbar/video_button.dart

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import '../../others/image_video_utils.dart';
99
import '../video.dart';
1010
import 'select_video_source.dart';
1111

12+
// TODO: Add custom callback to validate the video link input
13+
1214
class QuillToolbarVideoButton extends StatelessWidget {
1315
const QuillToolbarVideoButton({
1416
required this.controller,
@@ -72,11 +74,6 @@ class QuillToolbarVideoButton extends StatelessWidget {
7274
final childBuilder =
7375
options.childBuilder ?? baseButtonExtraOptions(context)?.childBuilder;
7476

75-
// final iconColor =
76-
// iconTheme?.iconUnselectedFillColor ?? theme.iconTheme.color;
77-
// final iconFillColor = iconTheme?.iconUnselectedFillColor ??
78-
// (options.fillColor ?? theme.canvasColor);
79-
8077
if (childBuilder != null) {
8178
return childBuilder(
8279
QuillToolbarVideoButtonOptions(
@@ -150,18 +147,6 @@ class QuillToolbarVideoButton extends StatelessWidget {
150147
.onVideoInsertCallback(videoUrl, controller);
151148
await options.videoConfigurations.onVideoInsertedCallback?.call(videoUrl);
152149
}
153-
154-
// if (options.onVideoPickCallback != null) {
155-
// final selector = options.mediaPickSettingSelector ??
156-
// ImageVideoUtils.selectMediaPickSetting;
157-
// final source = await selector(context);
158-
// if (source != null) {
159-
// if (source == MediaPickSetting.gallery) {
160-
// } else {
161-
// await _typeLink(context);
162-
// }
163-
// }
164-
// } else {}
165150
}
166151

167152
Future<String?> _typeLink(BuildContext context) async {
@@ -176,13 +161,4 @@ class QuillToolbarVideoButton extends StatelessWidget {
176161
);
177162
return value;
178163
}
179-
180-
// void _linkSubmitted(String? value) {
181-
// if (value != null && value.isNotEmpty) {
182-
// final index = controller.selection.baseOffset;
183-
// final length = controller.selection.extentOffset - index;
184-
185-
// controller.replaceText(index, length, BlockEmbed.video(value), null);
186-
// }
187-
// }
188164
}

flutter_quill_extensions/lib/embeds/widgets/video_app.dart

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import '../../flutter_quill_extensions.dart';
1313
class VideoApp extends StatefulWidget {
1414
const VideoApp({
1515
required this.videoUrl,
16-
required this.context,
1716
required this.readOnly,
17+
@Deprecated(
18+
'The context is no longer required and will be removed on future releases',
19+
)
20+
BuildContext? context,
1821
super.key,
1922
this.onVideoInit,
2023
});
2124

2225
final String videoUrl;
23-
final BuildContext context;
2426
final bool readOnly;
2527
final void Function(GlobalKey videoContainerKey)? onVideoInit;
2628

@@ -92,29 +94,33 @@ class VideoAppState extends State<VideoApp> {
9294
: _controller.play();
9395
});
9496
},
95-
child: Stack(alignment: Alignment.center, children: [
96-
Center(
97-
child: AspectRatio(
98-
aspectRatio: _controller.value.aspectRatio,
99-
child: VideoPlayer(_controller),
100-
)),
101-
_controller.value.isPlaying
102-
? const SizedBox.shrink()
103-
: Container(
104-
color: const Color(0xfff5f5f5),
105-
child: const Icon(
106-
Icons.play_arrow,
107-
size: 60,
108-
color: Colors.blueGrey,
109-
))
110-
]),
97+
child: Stack(
98+
alignment: Alignment.center,
99+
children: [
100+
Center(
101+
child: AspectRatio(
102+
aspectRatio: _controller.value.aspectRatio,
103+
child: VideoPlayer(_controller),
104+
)),
105+
_controller.value.isPlaying
106+
? const SizedBox.shrink()
107+
: Container(
108+
color: const Color(0xfff5f5f5),
109+
child: const Icon(
110+
Icons.play_arrow,
111+
size: 60,
112+
color: Colors.blueGrey,
113+
),
114+
)
115+
],
116+
),
111117
),
112118
);
113119
}
114120

115121
@override
116122
void dispose() {
117-
super.dispose();
118123
_controller.dispose();
124+
super.dispose();
119125
}
120126
}
Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,143 @@
11
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
2-
import 'package:flutter/widgets.dart';
2+
import 'package:flutter/material.dart';
33
import 'package:flutter_quill/flutter_quill.dart' show DefaultStyles;
4-
import 'package:url_launcher/url_launcher.dart' show launchUrl;
4+
import 'package:url_launcher/url_launcher_string.dart';
5+
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
56
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
67

8+
import '../../models/config/video/editor/youtube_video_support_mode.dart';
9+
import 'video_app.dart';
10+
711
class YoutubeVideoApp extends StatefulWidget {
812
const YoutubeVideoApp({
913
required this.videoUrl,
1014
required this.readOnly,
15+
required this.youtubeVideoSupportMode,
1116
super.key,
1217
});
1318

1419
final String videoUrl;
1520
final bool readOnly;
21+
final YoutubeVideoSupportMode youtubeVideoSupportMode;
1622

1723
@override
1824
YoutubeVideoAppState createState() => YoutubeVideoAppState();
1925
}
2026

2127
class YoutubeVideoAppState extends State<YoutubeVideoApp> {
22-
YoutubePlayerController? _youtubeController;
28+
YoutubePlayerController? _youtubeIframeController;
29+
30+
/// On some platforms such as desktop, Webview is not supported yet
31+
/// as a result the youtube video player package is not supported too
32+
/// this future will be not null and fetch the video url to load it using
33+
/// [VideoApp]
34+
Future<String>? _loadYoutubeVideoByDownloadUrlFuture;
35+
36+
/// Null if the video URL is not a YouTube video
37+
String? get _videoId {
38+
return YoutubePlayer.convertUrlToId(widget.videoUrl);
39+
}
2340

2441
@override
2542
void initState() {
2643
super.initState();
27-
final videoId = YoutubePlayer.convertUrlToId(widget.videoUrl);
28-
if (videoId != null) {
29-
_youtubeController = YoutubePlayerController(
30-
initialVideoId: videoId,
31-
flags: const YoutubePlayerFlags(
32-
autoPlay: false,
33-
),
34-
);
44+
final videoId = _videoId;
45+
if (videoId == null) {
46+
return;
3547
}
48+
switch (widget.youtubeVideoSupportMode) {
49+
case YoutubeVideoSupportMode.disabled:
50+
break;
51+
case YoutubeVideoSupportMode.iframeView:
52+
_youtubeIframeController = YoutubePlayerController(
53+
initialVideoId: videoId,
54+
flags: const YoutubePlayerFlags(
55+
autoPlay: false,
56+
),
57+
);
58+
break;
59+
case YoutubeVideoSupportMode.customPlayerWithDownloadUrl:
60+
_loadYoutubeVideoByDownloadUrlFuture =
61+
_loadYoutubeVideoWithVideoPlayerByVideoUrl();
62+
break;
63+
}
64+
}
65+
66+
Future<String> _loadYoutubeVideoWithVideoPlayerByVideoUrl() async {
67+
final youtubeExplode = YoutubeExplode();
68+
final manifest =
69+
await youtubeExplode.videos.streamsClient.getManifest(_videoId);
70+
final streamInfo = manifest.muxed.withHighestBitrate();
71+
final videoDownloadUri = streamInfo.url;
72+
return videoDownloadUri.toString();
73+
}
74+
75+
Widget _clickableVideoLinkText({required DefaultStyles defaultStyles}) {
76+
return RichText(
77+
text: TextSpan(
78+
text: widget.videoUrl,
79+
style: defaultStyles.link,
80+
recognizer: TapGestureRecognizer()
81+
..onTap = () => launchUrlString(widget.videoUrl),
82+
),
83+
);
3684
}
3785

3886
@override
3987
Widget build(BuildContext context) {
4088
final defaultStyles = DefaultStyles.getInstance(context);
41-
final youtubeController = _youtubeController;
42-
43-
if (youtubeController == null) {
44-
if (widget.readOnly) {
45-
return RichText(
46-
text: TextSpan(
47-
text: widget.videoUrl,
48-
style: defaultStyles.link,
49-
recognizer: TapGestureRecognizer()
50-
..onTap = () => launchUrl(
51-
Uri.parse(widget.videoUrl),
52-
),
89+
90+
switch (widget.youtubeVideoSupportMode) {
91+
case YoutubeVideoSupportMode.disabled:
92+
throw UnsupportedError('YouTube video links are not supported');
93+
case YoutubeVideoSupportMode.iframeView:
94+
final youtubeController = _youtubeIframeController;
95+
96+
if (youtubeController == null) {
97+
if (widget.readOnly) {
98+
return _clickableVideoLinkText(defaultStyles: defaultStyles);
99+
}
100+
101+
return RichText(
102+
text: TextSpan(text: widget.videoUrl, style: defaultStyles.link),
103+
);
104+
}
105+
return YoutubePlayerBuilder(
106+
player: YoutubePlayer(
107+
controller: youtubeController,
108+
showVideoProgressIndicator: true,
53109
),
110+
builder: (context, player) {
111+
return player;
112+
},
113+
);
114+
case YoutubeVideoSupportMode.customPlayerWithDownloadUrl:
115+
assert(
116+
_loadYoutubeVideoByDownloadUrlFuture != null,
117+
'The load youtube video future should not null for "${widget.youtubeVideoSupportMode}" mode',
54118
);
55-
}
56119

57-
return RichText(
58-
text: TextSpan(text: widget.videoUrl, style: defaultStyles.link),
59-
);
120+
return FutureBuilder<String>(
121+
future: _loadYoutubeVideoByDownloadUrlFuture,
122+
builder: (context, snapshot) {
123+
if (snapshot.connectionState == ConnectionState.waiting) {
124+
return const Center(child: CircularProgressIndicator.adaptive());
125+
}
126+
if (snapshot.hasError) {
127+
return _clickableVideoLinkText(defaultStyles: defaultStyles);
128+
}
129+
return VideoApp(
130+
videoUrl: snapshot.requireData,
131+
readOnly: widget.readOnly,
132+
);
133+
},
134+
);
60135
}
61-
62-
return YoutubePlayerBuilder(
63-
player: YoutubePlayer(
64-
controller: youtubeController,
65-
showVideoProgressIndicator: true,
66-
),
67-
builder: (context, player) {
68-
return player;
69-
},
70-
);
71136
}
72137

73138
@override
74139
void dispose() {
75-
_youtubeController?.dispose();
140+
_youtubeIframeController?.dispose();
76141
super.dispose();
77142
}
78143
}

flutter_quill_extensions/lib/models/config/video/editor/video_configurations.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import 'package:flutter/widgets.dart' show GlobalKey;
22
import 'package:meta/meta.dart' show immutable;
33

4+
import 'youtube_video_support_mode.dart';
5+
46
@immutable
57
class QuillEditorVideoEmbedConfigurations {
68
const QuillEditorVideoEmbedConfigurations({
79
this.onVideoInit,
10+
this.youtubeVideoSupportMode = YoutubeVideoSupportMode.iframeView,
811
});
912

1013
/// [onVideoInit] is a callback function that gets triggered when
@@ -21,4 +24,8 @@ class QuillEditorVideoEmbedConfigurations {
2124
/// // Customize other callback functions as needed
2225
/// ```
2326
final void Function(GlobalKey videoContainerKey)? onVideoInit;
27+
28+
/// Specifies how YouTube videos should be loaded if the video URL
29+
/// is YouTube video.
30+
final YoutubeVideoSupportMode youtubeVideoSupportMode;
2431
}

0 commit comments

Comments
 (0)