Last active
October 30, 2023 06:09
-
-
Save iamchiwon/2fe35313df71f0abcd89112c0fa0b217 to your computer and use it in GitHub Desktop.
Youtube Shorts Player in Flutter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/cupertino.dart'; | |
import 'package:get/get.dart'; | |
import 'package:webview_player/shorts_player.dart'; | |
void main() { | |
runApp(App()); | |
} | |
class ShortsPlayerController implements IShortsPlayerController { | |
@override | |
RxBool isLoading = true.obs; | |
@override | |
RxString videoId = ''.obs; | |
@override | |
RxString videoTitle = ''.obs; | |
@override | |
RxDouble currentTime = 0.0.obs; | |
@override | |
RxDouble duration = 0.0.obs; | |
int videoIndex = 0; | |
final List<String> videos = [ | |
'C2cfVTGErXk', | |
'bPXcO_1y9lA', | |
'ACLuay9Ro-o', | |
'rffQb5N8c7s' | |
]; | |
ShortsPlayerController() { | |
videoId.value = videos[videoIndex]; | |
} | |
@override | |
void onVideoStarted() { | |
print('Video Started'); | |
} | |
@override | |
onVideoFinished() { | |
print('Video finished'); | |
videoIndex = (videoIndex + 1) % videos.length; | |
videoId.value = videos[videoIndex]; | |
} | |
} | |
class App extends StatelessWidget { | |
IShortsPlayerController controller = ShortsPlayerController(); | |
App({super.key}) { | |
controller.currentTime.listen((currentTime) { | |
print('plyaing ${currentTime}'); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return CupertinoApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Cupertino App', | |
home: Container( | |
color: CupertinoColors.systemBackground, | |
child: Obx(() => ShortsPlayer( | |
controller: controller, | |
)), | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:get/get.dart'; | |
import 'package:webview_flutter/webview_flutter.dart'; | |
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; | |
abstract class IShortsPlayerController { | |
late RxString videoId; | |
late RxBool isLoading; | |
late RxString videoTitle; | |
late RxDouble currentTime; | |
late RxDouble duration; | |
void onVideoStarted(); | |
void onVideoFinished(); | |
} | |
class ShortsPlayer extends StatelessWidget { | |
final String _preScript = """ | |
reportNativeMessage = (message) => { | |
NativeBridge?.postMessage(JSON.stringify(message)); | |
} | |
playVideo = () => { | |
const _shortVideo = document.querySelector(".html5-main-video"); | |
if (_shortVideo) _shortVideo.play(); | |
}; | |
pauseVideo = () => { | |
const _shortVideo = document.querySelector(".html5-main-video"); | |
if (_shortVideo) _shortVideo.pauseVideo(); | |
}; | |
[ | |
".shorts-player-controls-pb", | |
".reel-player-overlay-actions", | |
".reel-player-overlay-metadata", | |
".logo-in-player-endpoint", | |
].forEach( | |
(selector) => (document.querySelector(selector).style.display = "none") | |
); | |
(() => { | |
const _shortVideo = document.querySelector(".html5-main-video"); | |
if (!_shortVideo) { | |
reportNativeMessage({ | |
status: "ERROR", | |
message: "No video found", | |
currentTime: 0, | |
duration: 0, | |
}); | |
return; | |
} | |
_shortVideo.loop = false; | |
_shortVideo.muted = false; | |
reportNativeMessage({ | |
status: "PLAY", | |
title: _shortVideo.title, | |
currentTime: _shortVideo.currentTime, | |
duration: _shortVideo.duration, | |
}); | |
const _reportInterval = setInterval(() => { | |
reportNativeMessage({ | |
status: "PLAYING", | |
currentTime: _shortVideo.currentTime, | |
duration: _shortVideo.duration, | |
}); | |
}, 100); | |
_shortVideo.onpause = () => { | |
if (_reportInterval) clearInterval(_reportInterval); | |
reportNativeMessage({ | |
status: "STOP", | |
currentTime: _shortVideo.currentTime, | |
duration: _shortVideo.duration, | |
}); | |
}; | |
})(); | |
"""; | |
late WebViewController _controller; | |
IShortsPlayerController controller; | |
PlatformWebViewControllerCreationParams _createPlatformParams() { | |
if (WebViewPlatform.instance is WebKitWebViewPlatform) { | |
return WebKitWebViewControllerCreationParams( | |
allowsInlineMediaPlayback: true, | |
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{}, | |
); | |
} else { | |
return const PlatformWebViewControllerCreationParams(); | |
} | |
} | |
ShortsPlayer({required this.controller, super.key}) { | |
final PlatformWebViewControllerCreationParams params = | |
_createPlatformParams(); | |
_controller = WebViewController.fromPlatformCreationParams(params) | |
..setJavaScriptMode(JavaScriptMode.unrestricted) | |
..setBackgroundColor(const Color(0x00000000)) | |
..addJavaScriptChannel("NativeBridge", | |
onMessageReceived: (JavaScriptMessage message) { | |
var data = jsonDecode(message.message); | |
controller.currentTime.value = data['currentTime']; | |
controller.duration.value = data['duration']; | |
if (data['status'] == 'PLAY') { | |
controller.videoTitle.value = data['title']; | |
controller.onVideoStarted(); | |
} else if (data['status'] == 'STOP') { | |
controller.onVideoFinished(); | |
controller.isLoading.value = true; | |
} | |
}) | |
..setNavigationDelegate( | |
NavigationDelegate( | |
onProgress: (int progress) { | |
// Update loading bar. | |
}, | |
onPageStarted: (String url) { | |
controller.isLoading.value = true; | |
}, | |
onPageFinished: (String url) { | |
Future.delayed(const Duration(milliseconds: 300)).then((value) { | |
_controller.runJavaScript(_preScript); | |
controller.isLoading.value = false; | |
}); | |
}, | |
onWebResourceError: (WebResourceError error) {}, | |
onNavigationRequest: (NavigationRequest request) { | |
return NavigationDecision.navigate; | |
}, | |
), | |
); | |
if (controller.videoId.value.isNotEmpty) { | |
_controller.loadRequest(Uri.parse( | |
'https://m.youtube.com/shorts/${controller.videoId.value}')); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Obx(() => Stack( | |
children: [ | |
WebViewWidget(controller: _controller), | |
_Cover( | |
cover: controller.isLoading.value, | |
), | |
], | |
)); | |
} | |
} | |
class _Cover extends StatelessWidget { | |
bool cover; | |
_Cover({this.cover = true}); | |
@override | |
Widget build(BuildContext context) { | |
return cover | |
? Container( | |
color: Colors.black, | |
child: const Center( | |
child: CupertinoActivityIndicator( | |
radius: 16, | |
color: Colors.white, | |
), | |
)) | |
: IgnorePointer( | |
ignoring: false, | |
child: Container( | |
color: Colors.transparent, | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment