From beccb7d850bbda6d8efdbd720453e099b7660aa0 Mon Sep 17 00:00:00 2001 From: Ben Hagen Date: Wed, 4 Sep 2019 23:07:14 +0200 Subject: [PATCH] [video_player] Add web implementation --- packages/video_player/lib/video_player.dart | 13 +- .../video_player/lib/video_player_plugin.dart | 188 ++++++++++++++++++ packages/video_player/pubspec.yaml | 15 +- 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 packages/video_player/lib/video_player_plugin.dart diff --git a/packages/video_player/lib/video_player.dart b/packages/video_player/lib/video_player.dart index 059799b017b3..05226b383c74 100644 --- a/packages/video_player/lib/video_player.dart +++ b/packages/video_player/lib/video_player.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer') - // This will clear all open videos on the platform when a full restart is - // performed. +// This will clear all open videos on the platform when a full restart is +// performed. ..invokeMethod('init'); class DurationRange { @@ -88,7 +89,9 @@ class VideoPlayerValue { final Size size; bool get initialized => duration != null; + bool get hasError => errorDescription != null; + double get aspectRatio => size != null ? size.width / size.height : 1.0; VideoPlayerValue copyWith({ @@ -498,7 +501,11 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == null ? Container() : Texture(textureId: _textureId); + return _textureId == null + ? Container() + : !kIsWeb + ? Texture(textureId: _textureId) + : HtmlElementView(viewType: _textureId.toString()); } } diff --git a/packages/video_player/lib/video_player_plugin.dart b/packages/video_player/lib/video_player_plugin.dart new file mode 100644 index 000000000000..1d0f6d097377 --- /dev/null +++ b/packages/video_player/lib/video_player_plugin.dart @@ -0,0 +1,188 @@ +import 'dart:async'; +import 'dart:html'; +import 'dart:ui' as ui; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +class VideoPlayer { + VideoPlayer({this.uri, this.eventChannel, this.textureId}); + + final PluginEventChannel> eventChannel; + final StreamController> controller = + StreamController>(); + + final Uri uri; + final int textureId; + VideoElement videoElement; + bool isInitialized = false; + + Map setupVideoPlayer( + PluginEventChannel> eventChannel) { + eventChannel.controller = controller; + videoElement = VideoElement() + ..src = uri.toString() + ..autoplay = false + ..controls = false + ..style.border = 'none'; + + ui.platformViewRegistry.registerViewFactory( + textureId.toString(), (int viewId) => videoElement); + + videoElement.onCanPlay.listen((dynamic _) { + if (!isInitialized) { + isInitialized = true; + sendInitialized(); + } + }); + videoElement.onError.listen((dynamic error) { + controller.addError(error); + }); + videoElement.onEnded.listen((dynamic _) { + final Map event = {"event": "completed"}; + controller.add(event); + }); + + final Map reply = {"textureId": textureId}; + return reply; + } + + void sendBufferingUpdate() { + // TODO: convert TimeRanges + final Map event = { + "event": "bufferingUpdate", + "values": videoElement.buffered, + }; + controller.add(event); + } + + void play() { + videoElement.play(); + } + + void pause() { + videoElement.pause(); + } + + void setLooping(bool value) { + videoElement.loop = value; + } + + void setVolume(double value) { + videoElement.volume = value; + } + + void seekTo(int location) { + videoElement.currentTime = location.toDouble() / 1000; + } + + int getPosition() { + final int position = (videoElement.currentTime * 1000).round(); + return position; + } + + void sendInitialized() { + final Map event = { + 'event': 'initialized', + 'duration': videoElement.duration * 1000, + 'width': videoElement.videoWidth, + 'height': videoElement.videoHeight, + }; + controller.add(event); + } + + void dispose() { + videoElement.removeAttribute('src'); + videoElement.load(); + } +} + +class VideoPlayerPlugin { + VideoPlayerPlugin(this._registrar); + + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel('flutter.io/videoPlayer', + const StandardMethodCodec(), registrar.messenger); + final VideoPlayerPlugin instance = VideoPlayerPlugin(registrar); + channel.setMethodCallHandler(instance.handleMethodCall); + } + + Map videoPlayers = {}; + Registrar _registrar; + + int textureCounter = 1; + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case "init": + // TODO: gracefully handle multiple calls to init + // disposeAllPlayers(); + break; + case "create": + final int textureId = textureCounter; + textureCounter++; + + final PluginEventChannel> eventChannel = + PluginEventChannel>( + 'flutter.io/videoPlayer/videoEvents$textureId', + const StandardMethodCodec(), + _registrar.messenger); + + final VideoPlayer player = VideoPlayer( + uri: Uri.parse(call.arguments['uri']), + eventChannel: eventChannel, + textureId: textureId, + ); + + final Map reply = player.setupVideoPlayer(eventChannel); + + videoPlayers[textureId] = player; + return reply; + + default: + final int textureId = call.arguments["textureId"]; + + final VideoPlayer player = videoPlayers[textureId]; + if (player == null) { + throw Exception( + "No video player associated with texture id $textureId"); + } + + return _onMethodCall(call, textureId, player); + } + } + + void disposeAllPlayers() { + videoPlayers.forEach((_, VideoPlayer videoPlayer) => videoPlayer.dispose()); + videoPlayers.clear(); + } + + dynamic _onMethodCall(MethodCall call, int textureId, VideoPlayer player) { + switch (call.method) { + case "setLooping": + player.setLooping(call.arguments["looping"]); + return null; + case "setVolume": + player.setVolume(call.arguments["volume"]); + return null; + case "play": + player.play(); + return null; + case "pause": + player.pause(); + return null; + case "seekTo": + player.seekTo(call.arguments["location"]); + return null; + case "position": + player.sendBufferingUpdate(); + return player.getPosition(); + case "dispose": + player.dispose(); + videoPlayers.remove(textureId); + return null; + default: + throw UnimplementedError(); + } + } +} diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 96374a133405..ee4560f5d2cb 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -7,14 +7,23 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/video_player flutter: plugin: - androidPackage: io.flutter.plugins.videoplayer - iosPrefix: FLT - pluginClass: VideoPlayerPlugin + platforms: + web: + fileName: video_player_plugin.dart + pluginClass: VideoPlayerPlugin + ios: + classPefix: FLT + pluginClass: VideoPlayerPlugin + android: + package: io.flutter.plugins.videoplayer + pluginClass: VideoPlayerPlugin dependencies: meta: "^1.0.5" flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter dev_dependencies: flutter_test: