diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md new file mode 100644 index 000000000000..1318780830f8 --- /dev/null +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release diff --git a/packages/video_player/video_player_web/LICENSE b/packages/video_player/video_player_web/LICENSE new file mode 100644 index 000000000000..c89293372cf3 --- /dev/null +++ b/packages/video_player/video_player_web/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// 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 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/video_player/video_player_web/README.md b/packages/video_player/video_player_web/README.md new file mode 100644 index 000000000000..b8a441c3357c --- /dev/null +++ b/packages/video_player/video_player_web/README.md @@ -0,0 +1,29 @@ +# video_player_web + +The web implementation of [`video_player`][1]. + +## Usage + +To use this plugin in your Flutter Web app, simply add it as a dependency in +your pubspec using a `git` dependency. This is only temporary: in the future +we hope to make this package an "endorsed" implementation of `video_player`, +so that it is automatically included in your Flutter Web app when you depend +on `package:video_player`. + +```yaml +dependencies: + video_player: ^0.10.4 + video_player_web: + git: + url: git://github.com/flutter/plugins.git + path: packages/video_player/video_player_web +``` + +Once you have the `video_player_web` dependency in your pubspec, you should +be able to use `package:video_player` as normal. + +## Autoplay +Playing videos without prior interaction with the site might be prohibited +by the browser and lead to runtime errors. See also: https://goo.gl/xX8pDD. + +[1]: ../video_player \ No newline at end of file diff --git a/packages/video_player/video_player_web/ios/video_player_web.podspec b/packages/video_player/video_player_web/ios/video_player_web.podspec new file mode 100644 index 000000000000..5129b7c69032 --- /dev/null +++ b/packages/video_player/video_player_web/ios/video_player_web.podspec @@ -0,0 +1,20 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'video_player_web' + s.version = '0.0.1' + s.summary = 'No-op implementation of video_player_web web plugin to avoid build issues on iOS' + s.description = <<-DESC +temp fake video_player_web plugin + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end \ No newline at end of file diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart new file mode 100644 index 000000000000..24f8a91047c2 --- /dev/null +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -0,0 +1,196 @@ +import 'dart:async'; +import 'dart:html'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +/// The web implementation of [VideoPlayerPlatform]. +/// +/// This class implements the `package:video_player` functionality for the web. +class VideoPlayerPlugin extends VideoPlayerPlatform { + /// Registers this class as the default instance of [VideoPlayerPlatform]. + static void registerWith(Registrar registrar) { + VideoPlayerPlatform.instance = VideoPlayerPlugin(); + } + + Map _videoPlayers = {}; + + int _textureCounter = 1; + + @override + Future init() async { + return _disposeAllPlayers(); + } + + @override + Future dispose(int textureId) async { + _videoPlayers[textureId].dispose(); + _videoPlayers.remove(textureId); + return null; + } + + void _disposeAllPlayers() { + _videoPlayers.values + .forEach((_VideoPlayer videoPlayer) => videoPlayer.dispose()); + _videoPlayers.clear(); + } + + @override + Future create(DataSource dataSource) async { + final int textureId = _textureCounter; + _textureCounter++; + + final _VideoPlayer player = _VideoPlayer( + uri: Uri.parse(dataSource.uri), + textureId: textureId, + ); + + player.initialize(); + + _videoPlayers[textureId] = player; + return textureId; + } + + @override + Future setLooping(int textureId, bool looping) async { + return _videoPlayers[textureId].setLooping(looping); + } + + @override + Future play(int textureId) async { + return _videoPlayers[textureId].play(); + } + + @override + Future pause(int textureId) async { + return _videoPlayers[textureId].pause(); + } + + @override + Future setVolume(int textureId, double volume) async { + return _videoPlayers[textureId].setVolume(volume); + } + + @override + Future seekTo(int textureId, Duration position) async { + return _videoPlayers[textureId].seekTo(position); + } + + @override + Future getPosition(int textureId) async { + _videoPlayers[textureId].sendBufferingUpdate(); + return _videoPlayers[textureId].getPosition(); + } + + @override + Stream videoEventsFor(int textureId) { + return _videoPlayers[textureId].eventController.stream; + } + + @override + Widget buildView(int textureId) { + return HtmlElementView(viewType: 'videoPlayer-$textureId'); + } +} + +class _VideoPlayer { + _VideoPlayer({this.uri, this.textureId}); + + final StreamController eventController = + StreamController(); + + final Uri uri; + final int textureId; + VideoElement videoElement; + bool isInitialized = false; + + void initialize() { + videoElement = VideoElement() + ..src = uri.toString() + ..autoplay = false + ..controls = false + ..style.border = 'none'; + + // TODO(hterkelsen): Use initialization parameters once they are available + // ignore: undefined_prefixed_name + ui.platformViewRegistry.registerViewFactory( + 'videoPlayer-$textureId', (int viewId) => videoElement); + + videoElement.onCanPlay.listen((dynamic _) { + if (!isInitialized) { + isInitialized = true; + sendInitialized(); + } + }); + videoElement.onError.listen((dynamic error) { + eventController.addError(error); + }); + videoElement.onEnded.listen((dynamic _) { + eventController.add(VideoEvent(eventType: VideoEventType.completed)); + }); + } + + void sendBufferingUpdate() { + eventController.add(VideoEvent( + buffered: _toDurationRange(videoElement.buffered), + eventType: VideoEventType.bufferingUpdate, + )); + } + + void play() { + videoElement.play(); + } + + void pause() { + videoElement.pause(); + } + + void setLooping(bool value) { + videoElement.loop = value; + } + + void setVolume(double value) { + videoElement.volume = value; + } + + void seekTo(Duration position) { + videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; + } + + Duration getPosition() { + return Duration(milliseconds: (videoElement.currentTime * 1000).round()); + } + + void sendInitialized() { + eventController.add( + VideoEvent( + eventType: VideoEventType.initialized, + duration: Duration( + milliseconds: (videoElement.duration * 1000).round(), + ), + size: Size( + videoElement.videoWidth.toDouble() ?? 0.0, + videoElement.videoHeight.toDouble() ?? 0.0, + ), + ), + ); + } + + void dispose() { + videoElement.removeAttribute('src'); + videoElement.load(); + } + + List _toDurationRange(TimeRanges buffered) { + final List durationRange = []; + for (int i = 0; i < buffered.length; i++) { + durationRange.add(DurationRange( + Duration(milliseconds: (buffered.start(i) * 1000).round()), + Duration(milliseconds: (buffered.end(i) * 1000).round()), + )); + } + return durationRange; + } +} diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml new file mode 100644 index 000000000000..89023d16ec8f --- /dev/null +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -0,0 +1,30 @@ +name: video_player_web +description: Web platform implementation of video_player +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web +version: 0.1.0 + +flutter: + plugin: + platforms: + web: + pluginClass: VideoPlayerPlugin + fileName: video_player_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + video_player_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + video_player: + path: ../video_player + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" \ No newline at end of file diff --git a/packages/video_player/video_player_web/test/video_player_web_test.dart b/packages/video_player/video_player_web/test/video_player_web_test.dart new file mode 100644 index 000000000000..efe115d02e5f --- /dev/null +++ b/packages/video_player/video_player_web/test/video_player_web_test.dart @@ -0,0 +1,91 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +@TestOn('browser') + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:video_player_web/video_player_web.dart'; + +void main() { + group('VideoPlayer for Web', () { + int textureId; + + setUp(() async { + VideoPlayerPlatform.instance = VideoPlayerPlugin(); + textureId = await VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.network, + uri: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), + ); + }); + + test('$VideoPlayerPlugin is the live instance', () { + expect(VideoPlayerPlatform.instance, isA()); + }); + + test('can init', () { + expect(VideoPlayerPlatform.instance.init(), completes); + }); + + test('can create', () { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.network, + uri: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), + ), + completion(isNonZero)); + }); + + test('can dispose', () { + expect(VideoPlayerPlatform.instance.dispose(textureId), completes); + }); + + test('can set looping', () { + expect( + VideoPlayerPlatform.instance.setLooping(textureId, true), completes); + }); + + test('can play', () async { + // Mute video to allow autoplay (See https://goo.gl/xX8pDD) + await VideoPlayerPlatform.instance.setVolume(textureId, 0); + expect(VideoPlayerPlatform.instance.play(textureId), completes); + }); + + test('can pause', () { + expect(VideoPlayerPlatform.instance.pause(textureId), completes); + }); + + test('can set volume', () { + expect(VideoPlayerPlatform.instance.setVolume(textureId, 0.8), completes); + }); + + test('can seek to position', () { + expect( + VideoPlayerPlatform.instance.seekTo(textureId, Duration(seconds: 1)), + completes); + }); + + test('can get position', () { + expect(VideoPlayerPlatform.instance.getPosition(textureId), + completion(isInstanceOf())); + }); + + test('can get video event stream', () { + expect(VideoPlayerPlatform.instance.videoEventsFor(textureId), + isInstanceOf>()); + }); + + test('can build view', () { + expect(VideoPlayerPlatform.instance.buildView(textureId), + isInstanceOf()); + }); + }); +}