Skip to content

Instantly share code, notes, and snippets.

@iamchiwon
Last active October 30, 2023 06:09
Show Gist options
  • Save iamchiwon/2fe35313df71f0abcd89112c0fa0b217 to your computer and use it in GitHub Desktop.
Save iamchiwon/2fe35313df71f0abcd89112c0fa0b217 to your computer and use it in GitHub Desktop.
Youtube Shorts Player in Flutter
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,
)),
),
);
}
}
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