From 78d043b4db9fb505db6d8588ceacf066b912fcae Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sat, 7 Mar 2020 13:50:59 +1100 Subject: [PATCH] Support web, limit position to duration. --- example/pubspec.lock | 16 ++- lib/just_audio.dart | 33 ++++-- lib/just_audio_web.dart | 240 ++++++++++++++++++++++++++++++++++++++++ pubspec.lock | 14 +-- pubspec.yaml | 52 ++------- 5 files changed, 288 insertions(+), 67 deletions(-) create mode 100644 lib/just_audio_web.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index e6b445e..a843d00 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -74,6 +74,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" image: dependency: transitive description: @@ -87,7 +92,7 @@ packages: path: ".." relative: true source: path - version: "0.0.6" + version: "0.1.0" matcher: dependency: transitive description: @@ -116,13 +121,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" petitparser: dependency: transitive description: @@ -197,7 +195,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.14" typed_data: dependency: transitive description: diff --git a/lib/just_audio.dart b/lib/just_audio.dart index 89b7523..652556e 100644 --- a/lib/just_audio.dart +++ b/lib/just_audio.dart @@ -54,6 +54,8 @@ class AudioPlayer { final int _id; + Duration _duration; + Future _durationFuture; final _durationSubject = BehaviorSubject(); @@ -65,6 +67,7 @@ class AudioPlayer { updatePosition: Duration.zero, updateTime: Duration.zero, speed: 1.0, + duration: Duration.zero, ); Stream _eventChannelStream; @@ -100,6 +103,7 @@ class AudioPlayer { updatePosition: Duration(milliseconds: data[2]), updateTime: Duration(milliseconds: data[3]), speed: _speed, + duration: _duration, )); _eventChannelStreamSubscription = _eventChannelStream.listen(_playbackEventSubject.add); @@ -152,7 +156,7 @@ class AudioPlayer { playbackEventStream, // TODO: emit periodically only in playing state. Stream.periodic(period), - (state, _) => state.position); + (state, _) => state.position).distinct(); /// The current volume of the player. double get volume => _volume; @@ -170,9 +174,9 @@ class AudioPlayer { Future setUrl(final String url) async { _durationFuture = _invokeMethod('setUrl', [url]) .then((ms) => ms == null ? null : Duration(milliseconds: ms)); - final duration = await _durationFuture; - _durationSubject.add(duration); - return duration; + _duration = await _durationFuture; + _durationSubject.add(_duration); + return _duration; } /// Loads audio media from a file and completes with the duration of that @@ -197,7 +201,9 @@ class AudioPlayer { /// Get file for caching asset media with proper extension Future _getCacheFile(final String assetPath) async => File(p.join( - (await getTemporaryDirectory()).path, 'just_audio_asset_cache', '$_id${p.extension(assetPath)}')); + (await getTemporaryDirectory()).path, + 'just_audio_asset_cache', + '$_id${p.extension(assetPath)}')); /// Clip the audio to the given [start] and [end] timestamps. This method /// cannot be called from the [AudioPlaybackState.none] state. @@ -322,21 +328,30 @@ class AudioPlaybackEvent { /// The playback speed. final double speed; + /// The media duration. + final Duration duration; + AudioPlaybackEvent({ @required this.state, @required this.buffering, @required this.updateTime, @required this.updatePosition, @required this.speed, + @required this.duration, }); /// The current position of the player. - Duration get position => state == AudioPlaybackState.playing && !buffering - ? updatePosition + + Duration get position { + if (state == AudioPlaybackState.playing && !buffering) { + final result = updatePosition + (Duration(milliseconds: DateTime.now().millisecondsSinceEpoch) - updateTime) * - speed - : updatePosition; + speed; + return result <= duration ? result : duration; + } else { + return updatePosition; + } + } @override String toString() => diff --git a/lib/just_audio_web.dart b/lib/just_audio_web.dart new file mode 100644 index 0000000..b217b18 --- /dev/null +++ b/lib/just_audio_web.dart @@ -0,0 +1,240 @@ +import 'dart:async'; +import 'dart:html'; + +import 'package:async/async.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:just_audio/just_audio.dart'; + +class JustAudioPlugin { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'com.ryanheise.just_audio.methods', + const StandardMethodCodec(), + registrar.messenger); + final JustAudioPlugin instance = JustAudioPlugin(registrar); + channel.setMethodCallHandler(instance.handleMethodCall); + } + + final Registrar registrar; + + JustAudioPlugin(this.registrar); + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'init': + final String id = call.arguments[0]; + new Html5AudioPlayer(id: id, registrar: registrar); + return null; + default: + throw PlatformException(code: 'Unimplemented'); + } + } +} + +abstract class JustAudioPlayer { + final String id; + final Registrar registrar; + final MethodChannel methodChannel; + final PluginEventChannel eventChannel; + final StreamController eventController = StreamController(); + AudioPlaybackState _state = AudioPlaybackState.none; + bool _buffering = false; + + JustAudioPlayer({@required this.id, @required this.registrar}) + : methodChannel = MethodChannel('com.ryanheise.just_audio.methods.$id', + const StandardMethodCodec(), registrar.messenger), + eventChannel = PluginEventChannel('com.ryanheise.just_audio.events.$id', + const StandardMethodCodec(), registrar.messenger) { + methodChannel.setMethodCallHandler(_methodHandler); + eventChannel.controller = eventController; + } + + Future _methodHandler(MethodCall call) async { + final args = call.arguments; + switch (call.method) { + case 'setUrl': + return await setUrl(args[0]); + case 'setClip': + return await setClip(args[0], args[1]); + case 'play': + return await play(); + case 'pause': + return await pause(); + case 'stop': + return await stop(); + case 'setVolume': + return await setVolume(args[0]); + case 'setSpeed': + return await setSpeed(args[0]); + case 'seek': + return await seek(args[0]); + case 'dispose': + return await dispose(); + default: + throw PlatformException(code: 'Unimplemented'); + } + } + + Future setUrl(final String url); + + Future setClip(int start, int end); + + Future play(); + + Future pause(); + + Future stop(); + + Future setVolume(double volume); + + Future setSpeed(double speed); + + Future seek(int position); + + @mustCallSuper + Future dispose() { + eventController.close(); + } + + double getCurrentPosition(); + + broadcastPlaybackEvent() { + var updateTime = DateTime.now().millisecondsSinceEpoch; + eventController.add([ + _state.index, + _buffering, + (getCurrentPosition() * 1000).toInt(), + updateTime, + ]); + } + + transition(AudioPlaybackState state) { + _state = state; + broadcastPlaybackEvent(); + } +} + +class Html5AudioPlayer extends JustAudioPlayer { + AudioElement _audioElement = AudioElement(); + Completer _durationCompleter; + double _volume = 1.0; + double _startPos = 0.0; + double _start = 0.0; + double _end; + CancelableOperation _playOperation; + + Html5AudioPlayer({@required String id, @required Registrar registrar}) + : super(id: id, registrar: registrar) { + _audioElement.addEventListener('durationchange', (event) { + _durationCompleter?.complete(_audioElement.duration); + }); + _audioElement.addEventListener('ended', (event) { + transition(AudioPlaybackState.completed); + }); + _audioElement.addEventListener('seek', (event) { + _buffering = true; + broadcastPlaybackEvent(); + }); + _audioElement.addEventListener('seeked', (event) { + _buffering = false; + broadcastPlaybackEvent(); + }); + } + + @override + Future setUrl(final String url) async { + _interruptPlay(); + transition(AudioPlaybackState.connecting); + _durationCompleter = Completer(); + _audioElement.src = url; + _audioElement.preload = 'auto'; + _audioElement.load(); + final duration = await _durationCompleter.future; + transition(AudioPlaybackState.stopped); + return (duration * 1000).toInt(); + } + + @override + Future setClip(int start, int end) async { + _interruptPlay(); + _start = start / 1000.0; + _end = end / 1000.0; + _startPos = _start; + } + + @override + Future play() async { + _interruptPlay(); + final duration = _end == null ? null : _end - _startPos; + + _audioElement.currentTime = _startPos; + _audioElement.play(); + if (duration != null) { + _playOperation = CancelableOperation.fromFuture(Future.delayed(Duration( + milliseconds: + (duration * 1000 / _audioElement.playbackRate).toInt()))) + .then((_) { + pause(); + _playOperation = null; + }); + } + transition(AudioPlaybackState.playing); + } + + _interruptPlay() { + if (_playOperation != null) { + _playOperation.cancel(); + _playOperation = null; + } + } + + @override + Future pause() async { + _interruptPlay(); + _startPos = _audioElement.currentTime; + _audioElement.pause(); + transition(AudioPlaybackState.paused); + } + + @override + Future stop() async { + _interruptPlay(); + _startPos = _start; + _audioElement.pause(); + _audioElement.currentTime = _start; + transition(AudioPlaybackState.stopped); + } + + @override + Future setVolume(double volume) async { + _volume = volume; + _audioElement.volume = volume; + } + + @override + Future setSpeed(double speed) async { + _audioElement.playbackRate = speed; + } + + @override + Future seek(int position) async { + _interruptPlay(); + _startPos = _start + position / 1000.0; + _audioElement.currentTime = _startPos; + } + + @override + double getCurrentPosition() => _audioElement.currentTime; + + @override + Future dispose() async { + _interruptPlay(); + _audioElement.pause(); + _audioElement.removeAttribute('src'); + _audioElement.load(); + transition(AudioPlaybackState.none); + super.dispose(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4b09777..e516330 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -67,6 +67,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" image: dependency: transitive description: @@ -102,13 +107,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" petitparser: dependency: transitive description: @@ -183,7 +181,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.14" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35cf7b7..067ca69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,51 +12,21 @@ dependencies: path_provider: ^1.5.1 flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter dev_dependencies: flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The androidPackage and pluginClass identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: - androidPackage: com.ryanheise.just_audio - pluginClass: JustAudioPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages + platforms: + android: + package: com.ryanheise.just_audio + pluginClass: JustAudioPlugin + ios: + pluginClass: JustAudioPlugin + web: + pluginClass: JustAudioPlugin + fileName: just_audio_web.dart