diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..2cfe942 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: jonatadashi + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce:** +Steps to reproduce the behavior: + +**Expected behavior:** + +**Flutter Doctor:** + +**Additional context** +Add any other context about the problem here. diff --git a/.github/issuecomplete.yml b/.github/issuecomplete.yml new file mode 100644 index 0000000..7f5bdda --- /dev/null +++ b/.github/issuecomplete.yml @@ -0,0 +1,23 @@ +# Configuration for Issue Check: https://github.com/stevenzeck/issue-check + +# The name of the label to apply when an issue does not have all tasks checked +labelName: "status: needs-additional-info" + +# The color of the label in hex format (without #). Will use the label color configured +# in Issues settings. +labelColor: + +# The text of the comment to add to the issue in addition to the label +commentText: > + Hello! It doesn't seem like we have quite enough information to send this to + a human yet to help out. We would love if you could provide more details about + your issue by following the template without modifying any of the pre-filled + text. + +# Whether or not to ensure all checkboxes are checked +checkCheckboxes: false + +# Keywords to look for in the body of the issue +keywords: + - Bug + - Expected behavior diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..fe562b3 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 7 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +exemptAssignees: true +# Label to use when marking an issue as stale +staleLabel: "status: waiting-for-reply" +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false +# Limit to only `issues` or `pulls` +only: issues diff --git a/CHANGELOG.md b/CHANGELOG.md index eff48e9..d9a9e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ +## 2.0.3 +- Fix `Unhandled Exception: MissingPluginException`. +- Fix [#18](https://github.com/jonatadashi/youtube_plyr_iframe/issues/18). +- Added `hideYoutubeLogo`. +- Added `hideEndScreen`. + +## 2.0.2 +- Added `useHybridComposition`. +- Added documentation on how to load thumbnails. + ## 2.0.1 - Fix `Unhandled Exception: Null check operator used on a null value` -Added showTopMenu as requested in [#12](https://github.com/jonatadashi/youtube_plyr_iframe/issues/12). +- Added showTopMenu as requested in [#12](https://github.com/jonatadashi/youtube_plyr_iframe/issues/12). ## 2.0.0 - 💪 Running with sound null safety 💪 diff --git a/README.md b/README.md index 325ccc2..679258b 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,6 @@ For more details see: [https://pub.dev/packages/youtube_plyr_iframe/install](htt [Click here for WEB DEMO](https://jonatadashi.github.io/Web-Example/) -## Example App -[](https://play.google.com/store/apps/details?id=space.starhelix.lore.christianity)[](https://apps.apple.com/app/id1484833082#?platform=iphone) - ## Salient Features * Inline Playback * Supports captions @@ -54,7 +51,17 @@ Since *flutter_inappwebview* relies on Flutter's mechanism for embedding Android ## Setup ### Web -No Configuration Required. +No Configuration required, if developing without the need to load thumbnails. (Inline thumbnails will still load.) + +To load thumbnails the HTML renderer is needed. + +- Run: `flutter run -d chrome --web-renderer html` +- Build: `flutter build web --web-renderer html --profile` + +Read for more information: +- [Web renderers](https://flutter.dev/docs/development/tools/web-renderers) +- [Displaying images on the web +](https://flutter.dev/docs/development/platform-integration/web-images) ### iOS No Configuration Required. @@ -155,6 +162,8 @@ onExitFullScreen() | Called when player exits f invokeJavascript(function) | Invoke custom javascript function. hideTopMenu() | Hides the title and share icons at the top of player (_May violate YouTube's TOS. Use at your own risk._) hidePauseOverlay() | Hides the related videos overlay while player is paused (_May violate YouTube's TOS. Use at your own risk._) +hideYoutubeLogo() | Hides the Youtube Logo (_May violate YouTube's TOS. Use at your own risk._) +hideEndScreen() | Hides the Endscreen Overlay (_May violate YouTube's TOS. Use at your own risk._) ## Youtube Player Parameters All the available parameters. @@ -183,7 +192,7 @@ desktopMode | The controls will be alike Youtube Desktop's contro ## License ``` -Copyright 2020 Sarbagya Dhaubanjar. All rights reserved. +Copyright 2021 Sarbagya Dhaubanjar & Jona T. Feucht. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -194,9 +203,6 @@ are permitted provided that the following conditions are met: copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 1f595f2..1463d40 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - youtube_player_iframe_example + Youtube Plyr IFrame Demo CFBundlePackageType APPL CFBundleShortVersionString diff --git a/example/lib/main.dart b/example/lib/main.dart index 7567028..314d0f3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:youtube_player_iframe_example/pages/oldDemo.dart'; -import 'package:youtube_player_iframe_example/pages/thumbnailDemo.dart'; import 'package:youtube_plyr_iframe/youtube_plyr_iframe.dart'; +import 'pages/oldDemo.dart'; +import 'pages/thumbnailDemo.dart'; + void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(YoutubeApp()); @@ -72,7 +73,7 @@ class _YoutubeAppDemoState extends State { Row( children: [ ElevatedButton( - child: Text("Old Demo"), + child: Text("Inline Demo"), onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (context) => OldDemo()), @@ -219,57 +220,40 @@ class YoutubeViewer extends StatefulWidget { class _YoutubeViewerState extends State { late YoutubePlayerController _controller; - @override - void dispose() { - _controller.showTopMenu(); - super.dispose(); - } @override void initState() { super.initState(); _controller = YoutubePlayerController( - initialVideoId: widget.videoID!, + initialVideoId: widget.videoID!, // livestream example params: YoutubePlayerParams( + //startAt: Duration(minutes: 1, seconds: 5), showControls: true, showFullscreenButton: true, - desktopMode: false, // false for platform design - autoPlay: false, + desktopMode: false, + autoPlay: true, enableCaption: true, showVideoAnnotations: false, enableJavaScript: true, privacyEnhanced: true, - playsInline: false, // iOS only + useHybridComposition: true, + playsInline: false, ), )..listen((value) { if (value.isReady && !value.hasPlayed) { _controller ..hidePauseOverlay() - - // Uncomment below to stop Autoplay - // ..play() + ..play() ..hideTopMenu(); } + if (value.hasPlayed) { + _controller..hideEndScreen(); + } }); - - // Uncomment below for device orientation - // _controller!.onEnterFullscreen = () { - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.landscapeLeft, - // DeviceOrientation.landscapeRight, - // ]); - // log('Entered Fullscreen'); - // }; - // _controller!.onExitFullscreen = () { - // SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // Future.delayed(const Duration(seconds: 1), () { - // _controller!.play(); - // }); - // Future.delayed(const Duration(seconds: 5), () { - // SystemChrome.setPreferredOrientations(DeviceOrientation.values); - // }); - // log('Exited Fullscreen'); - // }; + _controller.onExitFullscreen = () { + _controller.close(); + Navigator.of(context).pop(); + }; } @override @@ -339,8 +323,7 @@ class _YoutubePlayerState extends State { YoutubePlayerController.getThumbnail( videoId: widget.videoID, // todo: get thumbnail quality from list - quality: ThumbnailQuality.max, - webp: false), + quality: ThumbnailQuality.max), fit: BoxFit.fill, ), ), diff --git a/example/lib/pages/oldDemo.dart b/example/lib/pages/oldDemo.dart index 3716ea0..3c0ec54 100644 --- a/example/lib/pages/oldDemo.dart +++ b/example/lib/pages/oldDemo.dart @@ -1,15 +1,13 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:youtube_player_iframe_example/widgets/meta_data_section.dart'; -import 'package:youtube_player_iframe_example/widgets/play_pause_button_bar.dart'; -import 'package:youtube_player_iframe_example/widgets/player_state_section.dart'; -import 'package:youtube_player_iframe_example/widgets/source_input_section.dart'; -import 'package:youtube_player_iframe_example/widgets/volume_slider.dart'; +import 'package:flutter/services.dart'; import 'package:youtube_plyr_iframe/youtube_plyr_iframe.dart'; +import '../widgets/meta_data_section.dart'; +import '../widgets/play_pause_button_bar.dart'; +import '../widgets/player_state_section.dart'; +import '../widgets/source_input_section.dart'; +import '../widgets/volume_slider.dart'; -/// class OldDemo extends StatefulWidget { @override _YoutubeAppDemoState createState() => _YoutubeAppDemoState(); @@ -22,50 +20,38 @@ class _YoutubeAppDemoState extends State { void initState() { super.initState(); _controller = YoutubePlayerController( - initialVideoId: '5qap5aO4i9A', // livestream example + initialVideoId: 'RCQRCTnDYXo', // livestream example params: YoutubePlayerParams( playlist: [ - 'F1B9Fk_SgI0', + 'RCQRCTnDYXo', "MnrJzXM7a6o", "FTQbiNvZqaY", "iYKXdt0LRs8", ], //startAt: Duration(minutes: 1, seconds: 5), showControls: true, - showFullscreenButton: true, - desktopMode: false, // false for platform design + showFullscreenButton: false, + desktopMode: true, // true for youtube design autoPlay: false, enableCaption: true, showVideoAnnotations: false, enableJavaScript: true, privacyEnhanced: true, + useHybridComposition: true, playsInline: true, // iOS only - Auto fullscreen or not ), )..listen((value) { if (value.isReady && !value.hasPlayed) { _controller ..hidePauseOverlay() + ..hideYoutubeLogo() + //..play() ..hideTopMenu(); } + if (value.hasPlayed) { + _controller..hideEndScreen(); + } }); - //Uncomment below for auto rotation on fullscreen - // _controller.onEnterFullscreen = () { - // SystemChrome.setPreferredOrientations([ - // DeviceOrientation.landscapeLeft, - // DeviceOrientation.landscapeRight, - // ]); - // log('Entered Fullscreen'); - // }; - // _controller.onExitFullscreen = () { - // SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // Future.delayed(const Duration(seconds: 1), () { - // _controller.play(); - // }); - // Future.delayed(const Duration(seconds: 5), () { - // SystemChrome.setPreferredOrientations(DeviceOrientation.values); - // }); - // log('Exited Fullscreen'); - // }; } @override @@ -74,44 +60,38 @@ class _YoutubeAppDemoState extends State { return YoutubePlayerControllerProvider( // Passing controller to widgets below. controller: _controller, - child: YoutubeValueBuilder( - key: UniqueKey(), - builder: (context, value) { - if (value!.isReady && !value.hasPlayed) { - Timer(Duration(seconds: 5), () { - _controller.showTopMenu(); - }); - } - return Scaffold( - appBar: AppBar( - title: const Text('Youtube Plyr Demo'), - ), - body: LayoutBuilder( - builder: (context, constraints) { - if (kIsWeb && constraints.maxWidth > 800) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Expanded(child: player), - const SizedBox( - width: 500, - child: SingleChildScrollView( - child: Controls(), - ), - ), - ], - ); - } - return ListView( - children: [ - player, - const Controls(), - ], - ); - }, - ), - ); - }, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + elevation: 0, + brightness: Brightness.dark, + title: const Text('Inline Demo'), + ), + body: LayoutBuilder( + builder: (context, constraints) { + if (kIsWeb && constraints.maxWidth > 800) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Expanded(child: player), + const SizedBox( + width: 500, + child: SingleChildScrollView( + child: Controls(), + ), + ), + ], + ); + } + return ListView( + children: [ + player, + Container(height: 5, color: Colors.black), + const Controls(), + ], + ); + }, + ), ), ); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 38a8a9b..f2e9359 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: youtube_player_iframe_example description: Demonstrates how to use the youtube_plyr_iframe plugin. -version: 2.0.1 +version: 2.0.3 publish_to: none environment: diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 01b7a87..6924705 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -73,7 +73,7 @@ class YoutubePlayerController extends Stream return _controller.stream.listen( (value) { _value = value; - onData!(value); + onData?.call(value); }, onError: onError, onDone: onDone, @@ -275,8 +275,20 @@ class YoutubePlayerController extends Stream void hideTopMenu() => invokeJavascript('hideTopMenu()'); /// Show top menu + /// + /// Might violates Youtube's TOS. Use at your own risk. void showTopMenu() => invokeJavascript('showTopMenu()'); + /// Hide Youtube Logo + /// + /// Might violates Youtube's TOS. Use at your own risk. + void hideYoutubeLogo() => invokeJavascript('hideYoutubeLogo()'); + + /// Hide EndScreen + /// + /// Might violates Youtube's TOS. Use at your own risk. + void hideEndScreen() => invokeJavascript('hideEndScreen()'); + /// Hides pause overlay i.e. related videos shown when player is paused. /// /// Might violates Youtube's TOS. Use at your own risk. @@ -303,7 +315,6 @@ class YoutubePlayerController extends Stream /// /// If videoId is passed as url then no conversion is done. static String? convertUrlToId(String url, {bool trimWhitespaces = true}) { - assert(url.isNotEmpty, 'Url cannot be empty'); if (!url.contains("http") && (url.length == 11)) return url; if (trimWhitespaces) url = url.trim(); @@ -312,7 +323,7 @@ class YoutubePlayerController extends Stream r'^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$', r'^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$', ]) { - RegExpMatch? match = RegExp(regex).firstMatch(url); + Match? match = RegExp(regex).firstMatch(url); if (match != null && match.groupCount >= 1) return match.group(1); } diff --git a/lib/src/helpers/player_fragments.dart b/lib/src/helpers/player_fragments.dart index ad03eeb..bda6387 100644 --- a/lib/src/helpers/player_fragments.dart +++ b/lib/src/helpers/player_fragments.dart @@ -33,10 +33,9 @@ String youtubeIFrameTag(YoutubePlayerController controller) { 'embed/${controller.initialVideoId}', params, ); - return ''; + return """"""; } /// @@ -128,6 +127,23 @@ function hidePauseOverlay() { try { document.querySelector('#player').contentDocument.querySelector('.ytp-pause-overlay').style.display = 'none'; } catch(e) { } return ''; } +function hideYoutubeOverlay() { + try { document.querySelector('#player').contentDocument.querySelector('.ytp-youtube-button').style.display = 'none'; } catch(e) { } + return ''; +} +function hideYoutubeLogo() { + try { document.querySelector('#player').contentDocument.querySelector('.ytp-youtube-button').style.display = 'none'; } catch(e) { } + return ''; +} +function hideEndScreen() { + try { + document.querySelector('#player').contentDocument.querySelector('.ytp-endscreen-content').style.display = 'none'; + document.querySelector('#player').contentDocument.querySelector('.html5-endscreen').style.display = 'none'; + document.querySelector('#player').contentDocument.querySelector('.ytp-endscreen-previous').style.display = 'none'; + document.querySelector('#player').contentDocument.querySelector('.ytp-endscreen-next').style.display = 'none'; + } catch(e) { } + return ''; +} '''; /// diff --git a/lib/src/meta_data.dart b/lib/src/meta_data.dart index 5ba03fc..7740fa6 100644 --- a/lib/src/meta_data.dart +++ b/lib/src/meta_data.dart @@ -27,7 +27,7 @@ class YoutubeMetaData { /// Creates [YoutubeMetaData] from raw json video data. factory YoutubeMetaData.fromRawData(dynamic rawData) { final data = rawData as Map; - final durationInMs = (((data['duration'] ?? 0).toDouble()) * 1000).floor(); + final durationInMs = ((data['duration'] ?? 0).toDouble() * 1000).floor(); return YoutubeMetaData( videoId: data['videoId'], title: data['title'], diff --git a/lib/src/player_params.dart b/lib/src/player_params.dart index 0f816d4..2b36741 100644 --- a/lib/src/player_params.dart +++ b/lib/src/player_params.dart @@ -141,6 +141,13 @@ class YoutubePlayerParams { /// Default is false. final bool privacyEnhanced; + /// Set to `true` to enable Flutter's new Hybrid Composition. The default value is `true`. + /// Hybrid Composition is supported starting with Flutter v1.20+. + /// + /// **NOTE**: It is recommended to use Hybrid Composition only on Android 10+ for a release app, + /// as it can cause framerate drops on animations in Android 9 and lower (see [Hybrid-Composition#performance](https://github.com/flutter/flutter/wiki/Hybrid-Composition#performance)). + final bool useHybridComposition; + /// Defines player parameters for [YoutubePlayer]. const YoutubePlayerParams({ this.autoPlay = true, @@ -163,5 +170,6 @@ class YoutubePlayerParams { this.startAt = Duration.zero, this.desktopMode = false, this.privacyEnhanced = false, + this.useHybridComposition = true, }); } diff --git a/lib/src/players/youtube_player_mobile.dart b/lib/src/players/youtube_player_mobile.dart index 857ca99..fdc0f94 100644 --- a/lib/src/players/youtube_player_mobile.dart +++ b/lib/src/players/youtube_player_mobile.dart @@ -12,7 +12,6 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:youtube_plyr_iframe/src/enums/youtube_error.dart'; import 'package:youtube_plyr_iframe/src/helpers/player_fragments.dart'; -import 'package:youtube_plyr_iframe/youtube_plyr_iframe.dart'; import '../controller.dart'; import '../enums/player_state.dart'; @@ -23,7 +22,7 @@ import '../meta_data.dart'; /// Use [YoutubePlayerIFrame] instead. class RawYoutubePlayer extends StatefulWidget { /// The [YoutubePlayerController]. - final YoutubePlayerController? controller; + final YoutubePlayerController controller; /// Which gestures should be consumed by the youtube player. /// @@ -39,7 +38,7 @@ class RawYoutubePlayer extends StatefulWidget { /// Creates a [RawYoutubePlayer] widget. const RawYoutubePlayer({ Key? key, - this.controller, + required this.controller, this.gestureRecognizers, }) : super(key: key); @@ -49,8 +48,8 @@ class RawYoutubePlayer extends StatefulWidget { class _MobileYoutubePlayerState extends State with WidgetsBindingObserver { - YoutubePlayerController? controller; - late Completer _webController; + late final YoutubePlayerController controller; + late final Completer _webController; PlayerState? _cachedPlayerState; bool _isPlayerReady = false; bool _onLoadStopCalled = false; @@ -60,12 +59,12 @@ class _MobileYoutubePlayerState extends State super.initState(); _webController = Completer(); controller = widget.controller; - WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance?.addObserver(this); } @override void dispose() { - WidgetsBinding.instance!.removeObserver(this); + WidgetsBinding.instance?.removeObserver(this); super.dispose(); } @@ -75,14 +74,14 @@ class _MobileYoutubePlayerState extends State case AppLifecycleState.resumed: if (_cachedPlayerState != null && _cachedPlayerState == PlayerState.playing) { - controller?.play(); + controller.play(); } break; case AppLifecycleState.inactive: break; case AppLifecycleState.paused: - _cachedPlayerState = controller!.value.playerState; - controller?.pause(); + _cachedPlayerState = controller.value.playerState; + controller.pause(); break; default: } @@ -90,198 +89,202 @@ class _MobileYoutubePlayerState extends State @override Widget build(BuildContext context) { - return InAppWebView( - key: ValueKey(controller.hashCode), - initialData: InAppWebViewInitialData( - data: player, - baseUrl: controller!.params.privacyEnhanced - ? Uri.parse('https://www.youtube-nocookie.com') - : Uri.parse('https://www.youtube.com'), - encoding: 'utf-8', - mimeType: 'text/html', + return Container( + decoration: BoxDecoration( + border: Border.all(width: 0, color: Colors.black), + color: Colors.black, ), - gestureRecognizers: widget.gestureRecognizers ?? - { - Factory( - () => VerticalDragGestureRecognizer(), - ), - Factory( - () => HorizontalDragGestureRecognizer(), - ), - }, - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions( - userAgent: userAgent, - mediaPlaybackRequiresUserGesture: false, - transparentBackground: true, - disableContextMenu: true, - supportZoom: false, - disableHorizontalScroll: false, - disableVerticalScroll: false, - useShouldOverrideUrlLoading: false, + child: InAppWebView( + key: ValueKey(controller.hashCode), + initialData: InAppWebViewInitialData( + data: player, + baseUrl: Uri.parse( + controller.params.privacyEnhanced + ? 'https://www.youtube-nocookie.com' + : 'https://www.youtube.com', + ), + encoding: 'utf-8', + mimeType: 'text/html', ), - ios: IOSInAppWebViewOptions( - allowsInlineMediaPlayback: true, - allowsAirPlayForMediaPlayback: true, - allowsPictureInPictureMediaPlayback: true, + gestureRecognizers: widget.gestureRecognizers ?? + { + Factory( + () => VerticalDragGestureRecognizer(), + ), + Factory( + () => HorizontalDragGestureRecognizer(), + ), + }, + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + userAgent: userAgent, + mediaPlaybackRequiresUserGesture: false, + transparentBackground: true, + disableContextMenu: true, + supportZoom: false, + disableHorizontalScroll: false, + disableVerticalScroll: false, + useShouldOverrideUrlLoading: false, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + allowsAirPlayForMediaPlayback: true, + allowsPictureInPictureMediaPlayback: true, + ), + android: AndroidInAppWebViewOptions( + useWideViewPort: false, + useHybridComposition: controller.params.useHybridComposition, + ), ), - android: AndroidInAppWebViewOptions(useWideViewPort: false), + onWebViewCreated: (webController) { + if (!_webController.isCompleted) { + _webController.complete(webController); + } + controller.invokeJavascript = _callMethod; + _addHandlers(webController); + }, + onLoadStop: (_, __) { + _onLoadStopCalled = true; + if (_isPlayerReady) { + controller.add( + controller.value.copyWith(isReady: true), + ); + } + }, + onConsoleMessage: (_, message) { + log(message.message); + }, + onEnterFullscreen: (_) => controller.onEnterFullscreen?.call(), + onExitFullscreen: (_) => controller.onExitFullscreen?.call(), ), - onWebViewCreated: (webController) { - if (!_webController.isCompleted) { - _webController.complete(webController); - } - controller!.invokeJavascript = _callMethod; + ); + } - webController - ..addJavaScriptHandler( - handlerName: 'Ready', - callback: (_) { - _isPlayerReady = true; - if (_onLoadStopCalled) { - controller!.add( - controller!.value.copyWith(isReady: true), - ); - } - }, - ) - ..addJavaScriptHandler( - handlerName: 'StateChange', - callback: (args) { - switch (args.first as int?) { - case -1: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.unStarted, - isReady: true, - ), - ); - break; - case 0: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.ended, - ), - ); - break; - case 1: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.playing, - hasPlayed: true, - error: YoutubeError.none, - ), - ); - break; - case 2: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.paused, - ), - ); - break; - case 3: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.buffering, - ), - ); - break; - case 5: - controller!.add( - controller!.value.copyWith( - playerState: PlayerState.cued, - ), - ); - break; - default: - throw Exception("Invalid player state obtained."); - } - }, - ) - ..addJavaScriptHandler( - handlerName: 'PlaybackQualityChange', - callback: (args) { - controller!.add( - controller!.value - .copyWith(playbackQuality: args.first as String?), + Future _callMethod(String methodName) async { + final webController = await _webController.future; + webController.evaluateJavascript(source: methodName); + } + + void _addHandlers(InAppWebViewController webController) { + webController + ..addJavaScriptHandler( + handlerName: 'Ready', + callback: (_) { + _isPlayerReady = true; + if (_onLoadStopCalled) { + controller.add( + controller.value.copyWith(isReady: true), + ); + } + }, + ) + ..addJavaScriptHandler( + handlerName: 'StateChange', + callback: (args) { + switch (args.first as int) { + case -1: + controller.add( + controller.value.copyWith( + playerState: PlayerState.unStarted, + isReady: true, + ), ); - }, - ) - ..addJavaScriptHandler( - handlerName: 'PlaybackRateChange', - callback: (args) { - final num rate = args.first; - controller!.add( - controller!.value.copyWith(playbackRate: rate.toDouble()), + break; + case 0: + controller.add( + controller.value.copyWith( + playerState: PlayerState.ended, + ), ); - }, - ) - ..addJavaScriptHandler( - handlerName: 'Errors', - callback: (args) { - controller!.add( - controller!.value - .copyWith(error: errorEnum((args.first as int?)!)), + break; + case 1: + controller.add( + controller.value.copyWith( + playerState: PlayerState.playing, + hasPlayed: true, + error: YoutubeError.none, + ), ); - }, - ) - ..addJavaScriptHandler( - handlerName: 'VideoData', - callback: (args) { - controller!.add( - controller!.value.copyWith( - metaData: YoutubeMetaData.fromRawData(args.first)), + break; + case 2: + controller.add( + controller.value.copyWith( + playerState: PlayerState.paused, + ), ); - }, - ) - ..addJavaScriptHandler( - handlerName: 'VideoTime', - callback: (args) { - final position = args.first * 1000; - final num buffered = args.last; - controller!.add( - controller!.value.copyWith( - position: Duration(milliseconds: position.floor()), - buffered: buffered.toDouble(), + break; + case 3: + controller.add( + controller.value.copyWith( + playerState: PlayerState.buffering, ), ); - }, + break; + case 5: + controller.add( + controller.value.copyWith( + playerState: PlayerState.cued, + ), + ); + break; + default: + throw Exception("Invalid player state obtained."); + } + }, + ) + ..addJavaScriptHandler( + handlerName: 'PlaybackQualityChange', + callback: (args) { + controller.add( + controller.value.copyWith(playbackQuality: args.first as String), ); - }, - onLoadStop: (_, __) { - _onLoadStopCalled = true; - if (_isPlayerReady) { - controller!.add( - controller!.value.copyWith(isReady: true), + }, + ) + ..addJavaScriptHandler( + handlerName: 'PlaybackRateChange', + callback: (args) { + final num rate = args.first; + controller.add( + controller.value.copyWith(playbackRate: rate.toDouble()), ); - } - }, - onConsoleMessage: (_, message) { - log(message.message); - }, - onEnterFullscreen: (_) { - if (controller!.onEnterFullscreen != null) { - controller!.onEnterFullscreen!(); - } - }, - onExitFullscreen: (_) { - if (controller!.onExitFullscreen != null) { - controller!.onExitFullscreen!(); - } - }, - ); - } - - Future _callMethod(String methodName) async { - final webController = await _webController.future; - webController.evaluateJavascript(source: methodName); + }, + ) + ..addJavaScriptHandler( + handlerName: 'Errors', + callback: (args) { + controller.add( + controller.value.copyWith(error: errorEnum(args.first as int)), + ); + }, + ) + ..addJavaScriptHandler( + handlerName: 'VideoData', + callback: (args) { + controller.add( + controller.value + .copyWith(metaData: YoutubeMetaData.fromRawData(args.first)), + ); + }, + ) + ..addJavaScriptHandler( + handlerName: 'VideoTime', + callback: (args) { + final position = args.first * 1000; + final num buffered = args.last; + controller.add( + controller.value.copyWith( + position: Duration(milliseconds: position.floor()), + buffered: buffered.toDouble(), + ), + ); + }, + ); } String get player => ''' - ${youtubeIFrameTag(controller!)} + ${youtubeIFrameTag(controller)} '''; - String get userAgent => controller!.params.desktopMode + String get userAgent => controller.params.desktopMode ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36' : 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'; } diff --git a/lib/src/players/youtube_player_web.dart b/lib/src/players/youtube_player_web.dart index 6407a67..234e490 100644 --- a/lib/src/players/youtube_player_web.dart +++ b/lib/src/players/youtube_player_web.dart @@ -13,8 +13,7 @@ import 'package:flutter/material.dart'; import 'package:youtube_plyr_iframe/src/enums/player_state.dart'; import 'package:youtube_plyr_iframe/src/enums/youtube_error.dart'; import 'package:youtube_plyr_iframe/src/helpers/player_fragments.dart'; - -import '../controller.dart'; +import 'package:youtube_plyr_iframe/src/controller.dart'; import '../meta_data.dart'; import 'platform_view_stub.dart' if (dart.library.html) 'dart:ui' as ui; diff --git a/pubspec.yaml b/pubspec.yaml index a99d9b4..e0e7706 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,19 +1,19 @@ name: youtube_plyr_iframe description: Fork of Youtube Player Iframe. Supports web & mobile platforms. -version: 2.0.1 +version: 2.0.3 repository: https://github.com/jonatadashi/youtube_plyr_iframe homepage: https://github.com/jonatadashi/youtube_plyr_iframe environment: sdk: '>=2.12.0 <3.0.0' - flutter: '>=1.22.0' + flutter: '>=1.22.2' dependencies: flutter: sdk: flutter - flutter_inappwebview: ">=5.0.0 <6.0.0" + flutter_inappwebview: ">=5.3.0 <6.0.0" dev_dependencies: flutter_test: - sdk: flutter \ No newline at end of file + sdk: flutter