diff --git a/packages/audioplayers/example/integration_test/lib_test.dart b/packages/audioplayers/example/integration_test/lib_test.dart index cd9c2b9e3..d96a16106 100644 --- a/packages/audioplayers/example/integration_test/lib_test.dart +++ b/packages/audioplayers/example/integration_test/lib_test.dart @@ -9,7 +9,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'mock_html.dart' if (dart.library.html) 'dart:html' show DomException; import 'platform_features.dart'; import 'source_test_data.dart'; import 'test_utils.dart'; @@ -19,12 +18,15 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final isAndroid = !kIsWeb && Platform.isAndroid; + final isLinux = !kIsWeb && Platform.isLinux; + + final wavUrl1TestData = LibSourceTestData( + source: UrlSource(wavUrl1), + duration: const Duration(milliseconds: 451), + ); final audioTestDataList = [ - if (features.hasUrlSource) - LibSourceTestData( - source: UrlSource(wavUrl1), - duration: const Duration(milliseconds: 451), - ), + if (features.hasUrlSource) wavUrl1TestData, if (features.hasUrlSource) LibSourceTestData( source: UrlSource(wavUrl2), @@ -73,6 +75,7 @@ void main() { // Start all players simultaneously final iterator = List.generate(audioTestDataList.length, (i) => i); + await tester.pump(); await Future.wait( iterator.map((i) => players[i].play(audioTestDataList[i].source)), ); @@ -94,7 +97,7 @@ void main() { // FIXME: Causes media error on Android (see #1333, #1353) // Unexpected platform error: MediaPlayer error with // what:MEDIA_ERROR_UNKNOWN {what:1} extra:MEDIA_ERROR_SYSTEM - skip: !kIsWeb && Platform.isAndroid, + skip: isAndroid, ); testWidgets('play multiple sources consecutively', @@ -103,6 +106,7 @@ void main() { for (var i = 0; i < audioTestDataList.length; i++) { final td = audioTestDataList[i]; + await tester.pump(); await player.play(td.source); await tester.pumpAndSettle(); // Sources take some time to get initialized @@ -130,7 +134,7 @@ void main() { final player = AudioPlayer(); await player.setReleaseMode(ReleaseMode.stop); - final td = audioTestDataList[0]; + final td = wavUrl1TestData; var audioContext = AudioContextConfig( //ignore: avoid_redundant_argument_values @@ -141,6 +145,7 @@ void main() { await AudioPlayer.global.setAudioContext(audioContext); await player.setAudioContext(audioContext); + await tester.pump(); await player.play(td.source); await tester.pumpAndSettle(); await tester.pump(td.duration + const Duration(seconds: 8)); @@ -173,7 +178,7 @@ void main() { await player.setReleaseMode(ReleaseMode.stop); player.setPlayerMode(PlayerMode.lowLatency); - final td = audioTestDataList[0]; + final td = wavUrl1TestData; var audioContext = AudioContextConfig( //ignore: avoid_redundant_argument_values @@ -184,6 +189,7 @@ void main() { await AudioPlayer.global.setAudioContext(audioContext); await player.setAudioContext(audioContext); + await tester.pump(); await player.setSource(td.source); await player.resume(); await tester.pumpAndSettle(); @@ -214,7 +220,9 @@ void main() { group('Logging', () { testWidgets('Emit platform log', (tester) async { final logCompleter = Completer(); - const playerId = 'somePlayerId'; + + // FIXME: Cannot reuse event channel with same id on Linux + final playerId = isLinux ? 'somePlayerId0' : 'somePlayerId'; final player = AudioPlayer(playerId: playerId); final onLogSub = player.onLog.listen( logCompleter.complete, @@ -249,51 +257,37 @@ void main() { group('Errors', () { testWidgets( - 'Throw PlatformException, when playing invalid file', + 'Throw PlatformException, when loading invalid file', (tester) async { final player = AudioPlayer(); try { // Throws PlatformException via MethodChannel: + await tester.pump(); await player.setSource(AssetSource(invalidAsset)); - await player.resume(); fail('PlatformException not thrown'); // ignore: avoid_catches_without_on_clauses } catch (e) { - if (kIsWeb) { - expect(e, isInstanceOf()); - expect((e as DomException).name, 'NotSupportedError'); - } else { - expect(e, isInstanceOf()); - } + expect(e, isInstanceOf()); } await player.dispose(); }, - // Linux provides errors only asynchronously. - skip: !kIsWeb && Platform.isLinux, ); testWidgets( - 'Throw PlatformException, when playing non existent file', + 'Throw PlatformException, when loading non existent file', (tester) async { final player = AudioPlayer(); try { // Throws PlatformException via MethodChannel: + await tester.pump(); await player.setSource(UrlSource('non_existent.txt')); - await player.resume(); fail('PlatformException not thrown'); // ignore: avoid_catches_without_on_clauses } catch (e) { - if (kIsWeb) { - expect(e, isInstanceOf()); - expect((e as DomException).name, 'NotSupportedError'); - } else { - expect(e, isInstanceOf()); - } + expect(e, isInstanceOf()); } await player.dispose(); }, - // Linux provides errors only asynchronously. - skip: !kIsWeb && Platform.isLinux, ); }); @@ -301,7 +295,8 @@ void main() { testWidgets('#create and #dispose', (tester) async { final platform = AudioplayersPlatformInterface.instance; - const playerId = 'somePlayerId'; + // FIXME: Cannot reuse event channel with same id on Linux + final playerId = isLinux ? 'somePlayerId1' : 'somePlayerId'; await platform.create(playerId); await tester.pumpAndSettle(); await platform.dispose(playerId); @@ -318,14 +313,78 @@ void main() { ); } }); + + testWidgets('#setSource #getPosition and #getDuration', (tester) async { + final platform = AudioplayersPlatformInterface.instance; + + // FIXME: Cannot reuse event channel with same id on Linux + final playerId = isLinux ? 'somePlayerId2' : 'somePlayerId'; + await platform.create(playerId); + + final preparedCompleter = Completer(); + final eventStream = platform.getEventStream(playerId); + final onPreparedSub = eventStream + .where((event) => event.eventType == AudioEventType.prepared) + .map((event) => event.isPrepared!) + .listen( + (isPrepared) { + if (isPrepared) { + preparedCompleter.complete(); + } + }, + onError: preparedCompleter.completeError, + ); + await tester.pump(); + await platform.setSourceUrl( + playerId, + (wavUrl1TestData.source as UrlSource).url, + ); + await preparedCompleter.future.timeout(const Duration(seconds: 30)); + + expect(await platform.getCurrentPosition(playerId), 0); + expect( + await platform.getDuration(playerId), + wavUrl1TestData.duration.inMilliseconds, + ); + + await onPreparedSub.cancel(); + await platform.dispose(playerId); + }); }); group('Platform event channel', () { + // TODO(gustl22): remove once https://github.com/flutter/flutter/issues/126209 is fixed + testWidgets( + 'Reuse same platform event channel id', + (tester) async { + final platform = AudioplayersPlatformInterface.instance; + + const playerId = 'somePlayerId'; + await platform.create(playerId); + + final eventStreamSub = platform.getEventStream(playerId).listen((_) {}); + + await eventStreamSub.cancel(); + await platform.dispose(playerId); + + // Recreate player with same player Id + await platform.create(playerId); + + final eventStreamSub2 = + platform.getEventStream(playerId).listen((_) {}); + + await eventStreamSub2.cancel(); + await platform.dispose(playerId); + }, + skip: isLinux, + ); + testWidgets('Emit platform error', (tester) async { final errorCompleter = Completer(); final platform = AudioplayersPlatformInterface.instance; - const playerId = 'somePlayerId'; + // FIXME: Cannot reuse event channel with same id on Linux + final playerId = isLinux ? 'somePlayerId3' : 'somePlayerId'; await platform.create(playerId); final eventStreamSub = platform @@ -350,6 +409,8 @@ void main() { testWidgets('Emit global platform error', (tester) async { final errorCompleter = Completer(); final global = GlobalAudioplayersPlatformInterface.instance; + + /* final eventStreamSub = */ global .getGlobalEventStream() .listen((_) {}, onError: errorCompleter.complete); @@ -364,7 +425,7 @@ void main() { expect(platformException.code, 'SomeGlobalErrorCode'); expect(platformException.message, 'SomeGlobalErrorMessage'); // FIXME: cancelling the global event stream leads to - // MissingPluginException on Android + // MissingPluginException on Android, if dispose app afterwards // await eventStreamSub.cancel(); }); }); diff --git a/packages/audioplayers/example/integration_test/mock_html.dart b/packages/audioplayers/example/integration_test/mock_html.dart deleted file mode 100644 index 2186b4f79..000000000 --- a/packages/audioplayers/example/integration_test/mock_html.dart +++ /dev/null @@ -1,6 +0,0 @@ -class DomException { - String name; - String? message; - - DomException({required this.name, this.message}); -} diff --git a/packages/audioplayers/lib/src/audioplayer.dart b/packages/audioplayers/lib/src/audioplayer.dart index c74e890ea..b7fd010fa 100644 --- a/packages/audioplayers/lib/src/audioplayer.dart +++ b/packages/audioplayers/lib/src/audioplayer.dart @@ -99,6 +99,10 @@ class AudioPlayer { Stream get onSeekComplete => eventStream .where((event) => event.eventType == AudioEventType.seekComplete); + Stream get _onPrepared => eventStream + .where((event) => event.eventType == AudioEventType.prepared) + .map((event) => event.isPrepared!); + /// Stream of log events. Stream get onLog => eventStream .where((event) => event.eventType == AudioEventType.log) @@ -150,8 +154,8 @@ class AudioPlayer { onError: _eventStreamController.addError, ); creatingCompleter.complete(); - } on Exception catch (e, st) { - creatingCompleter.completeError(e, st); + } on Exception catch (e, stackTrace) { + creatingCompleter.completeError(e, stackTrace); } } @@ -279,8 +283,27 @@ class AudioPlayer { /// This will delegate to one of the specific methods below depending on /// the source type. Future setSource(Source source) async { - await creatingCompleter.future; - return source.setOnPlayer(this); + // Implementations of setOnPlayer also call `creatingCompleter.future` + await source.setOnPlayer(this); + } + + Future _completePrepared(Future Function() fun) async { + final preparedCompleter = Completer(); + final onPreparedSubscription = _onPrepared.listen( + (isPrepared) { + if (isPrepared) { + preparedCompleter.complete(); + } + }, + onError: (Object e, [StackTrace? stackTrace]) { + if (preparedCompleter.isCompleted == false) { + preparedCompleter.completeError(e, stackTrace); + } + }, + ); + await fun(); + await preparedCompleter.future.timeout(const Duration(seconds: 30)); + onPreparedSubscription.cancel(); } /// Sets the URL to a remote link. @@ -290,7 +313,9 @@ class AudioPlayer { Future setSourceUrl(String url) async { _source = UrlSource(url); await creatingCompleter.future; - return _platform.setSourceUrl(playerId, url, isLocal: false); + await _completePrepared( + () => _platform.setSourceUrl(playerId, url, isLocal: false), + ); } /// Sets the URL to a file in the users device. @@ -300,7 +325,9 @@ class AudioPlayer { Future setSourceDeviceFile(String path) async { _source = DeviceFileSource(path); await creatingCompleter.future; - return _platform.setSourceUrl(playerId, path, isLocal: true); + await _completePrepared( + () => _platform.setSourceUrl(playerId, path, isLocal: true), + ); } /// Sets the URL to an asset in your Flutter application. @@ -312,13 +339,17 @@ class AudioPlayer { _source = AssetSource(path); final url = await audioCache.load(path); await creatingCompleter.future; - return _platform.setSourceUrl(playerId, url.path, isLocal: true); + await _completePrepared( + () => _platform.setSourceUrl(playerId, url.path, isLocal: true), + ); } Future setSourceBytes(Uint8List bytes) async { _source = BytesSource(bytes); await creatingCompleter.future; - return _platform.setSourceBytes(playerId, bytes); + await _completePrepared( + () => _platform.setSourceBytes(playerId, bytes), + ); } /// Get audio duration after setting url. diff --git a/packages/audioplayers/test/fake_audioplayers_platform.dart b/packages/audioplayers/test/fake_audioplayers_platform.dart index e445dff7f..3b31e568e 100644 --- a/packages/audioplayers/test/fake_audioplayers_platform.dart +++ b/packages/audioplayers/test/fake_audioplayers_platform.dart @@ -123,6 +123,9 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface { @override Future setSourceBytes(String playerId, Uint8List bytes) async { calls.add(FakeCall(id: playerId, method: 'setSourceBytes', value: bytes)); + eventStreamControllers[playerId]?.add( + const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true), + ); } @override @@ -132,6 +135,9 @@ class FakeAudioplayersPlatform extends AudioplayersPlatformInterface { bool? isLocal, }) async { calls.add(FakeCall(id: playerId, method: 'setSourceUrl', value: url)); + eventStreamControllers[playerId]?.add( + const AudioEvent(eventType: AudioEventType.prepared, isPrepared: true), + ); } @override diff --git a/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/MediaPlayerPlayer.kt b/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/MediaPlayerPlayer.kt index 7c2b0f9b3..a177d8718 100644 --- a/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/MediaPlayerPlayer.kt +++ b/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/MediaPlayerPlayer.kt @@ -86,7 +86,7 @@ class MediaPlayerPlayer( } override fun prepare() { - mediaPlayer.prepare() + mediaPlayer.prepareAsync() } override fun reset() { diff --git a/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/WrappedPlayer.kt b/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/WrappedPlayer.kt index 1bd41e241..8b88faaeb 100644 --- a/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/WrappedPlayer.kt +++ b/packages/audioplayers_android/android/src/main/kotlin/xyz/luan/audioplayers/player/WrappedPlayer.kt @@ -95,7 +95,15 @@ class WrappedPlayer internal constructor( } var released = true - var prepared = false + + var prepared: Boolean = false + set(value) { + if (field != value) { + field = value + ref.handlePrepared(this, value) + } + } + var playing = false var shouldSeekTo = -1 diff --git a/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift b/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift index 01e450952..36764622c 100644 --- a/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift +++ b/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift @@ -186,10 +186,11 @@ public class SwiftAudioplayersDarwinPlugin: NSObject, FlutterPlugin { } player.setSourceUrl(url: url!, isLocal: isLocal, completer: { - result(1) + player.eventHandler.onPrepared(isPrepared: true) }, completerError: { - result(FlutterError(code: "DarwinAudioError", message: "AVPlayerItem.Status.failed on setSourceUrl", details: nil)) + player.eventHandler.onError(code: "DarwinAudioError", message: "AVPlayerItem.Status.failed on setSourceUrl", details: nil) }) + result(1) return } else if method == "setSourceBytes" { result(FlutterError(code: "DarwinAudioError", message: "setSourceBytes is not currently implemented on iOS", details: nil)) @@ -342,6 +343,12 @@ class AudioPlayersStreamHandler: NSObject, FlutterStreamHandler { } } + func onPrepared(isPrepared: Bool) { + if let eventSink = self.sink { + eventSink(["event": "audio.onPrepared", "value": isPrepared]) + } + } + func onLog(message: String) { if let eventSink = self.sink { eventSink(["event": "audio.onLog", "value": message]) diff --git a/packages/audioplayers_linux/linux/audio_player.cc b/packages/audioplayers_linux/linux/audio_player.cc index fa0a30775..4613463e2 100644 --- a/packages/audioplayers_linux/linux/audio_player.cc +++ b/packages/audioplayers_linux/linux/audio_player.cc @@ -153,11 +153,11 @@ void AudioPlayer::OnError(const gchar *code, const gchar *message, void AudioPlayer::OnMediaStateChange(GstObject *src, GstState *old_state, GstState *new_state) { - if(!playbin) { - this->OnError("LinuxAudioError", "Player was already disposed (OnMediaStateChange).", nullptr, nullptr); + if (!playbin) { + this->OnError("LinuxAudioError", "Player was already disposed (OnMediaStateChange).", nullptr, nullptr); return; } - + if (src == GST_OBJECT(playbin)) { if (*new_state == GST_STATE_READY) { if (this->_isInitialized) { @@ -174,6 +174,7 @@ void AudioPlayer::OnMediaStateChange(GstObject *src, GstState *old_state, } else if (*new_state >= GST_STATE_PAUSED) { if (!this->_isInitialized) { this->_isInitialized = true; + this->OnPrepared(true); if (this->_isPlaying) { Resume(); } @@ -184,6 +185,16 @@ void AudioPlayer::OnMediaStateChange(GstObject *src, GstState *old_state, } } +void AudioPlayer::OnPrepared(bool isPrepared) { + if (this->_eventChannel) { + g_autoptr(FlValue) map = fl_value_new_map(); + fl_value_set_string(map, "event", + fl_value_new_string("audio.onPrepared")); + fl_value_set_string(map, "value", fl_value_new_bool(isPrepared)); + fl_event_channel_send(this->_eventChannel, map, nullptr, nullptr); + } +} + void AudioPlayer::OnPositionUpdate() { if (this->_eventChannel) { g_autoptr(FlValue) map = fl_value_new_map(); @@ -280,7 +291,7 @@ void AudioPlayer::SetPlayback(int64_t position, double rate) { if (rate != 0 && _playbackRate != rate) { _playbackRate = rate; } - + if (!_isInitialized) { return; } @@ -400,7 +411,7 @@ void AudioPlayer::Dispose() { if(!playbin) throw "Player was already disposed (Dispose)"; if(_isPlaying) _isPlaying = false; if(_isInitialized) _isInitialized = false; - + g_source_remove(_refreshId); if(bus) { @@ -424,7 +435,7 @@ void AudioPlayer::Dispose() { // audiobin gets unreferenced (2x) via playbin panorama = nullptr; } - + GstState playbinState; gst_element_get_state(playbin, &playbinState, NULL, GST_CLOCK_TIME_NONE); if(playbinState > GST_STATE_NULL) { diff --git a/packages/audioplayers_linux/linux/audio_player.h b/packages/audioplayers_linux/linux/audio_player.h index 646e7b0b0..262a84f9d 100644 --- a/packages/audioplayers_linux/linux/audio_player.h +++ b/packages/audioplayers_linux/linux/audio_player.h @@ -100,4 +100,6 @@ class AudioPlayer { void OnSeekCompleted(); void OnPlaybackEnded(); + + void OnPrepared(bool isPrepared); }; diff --git a/packages/audioplayers_platform_interface/lib/src/api/audio_event.dart b/packages/audioplayers_platform_interface/lib/src/api/audio_event.dart index 57920a1be..5ab3acebe 100644 --- a/packages/audioplayers_platform_interface/lib/src/api/audio_event.dart +++ b/packages/audioplayers_platform_interface/lib/src/api/audio_event.dart @@ -6,6 +6,7 @@ enum AudioEventType { duration, seekComplete, complete, + prepared, } /// Event emitted from the platform implementation. @@ -19,6 +20,7 @@ class AudioEvent { this.duration, this.position, this.logMessage, + this.isPrepared, }); /// The type of the event. @@ -33,6 +35,9 @@ class AudioEvent { /// Log message in the player scope. final String? logMessage; + /// Whether the source is prepared to be played. + final bool? isPrepared; + @override bool operator ==(Object other) { return identical(this, other) || diff --git a/packages/audioplayers_platform_interface/lib/src/audioplayers_platform.dart b/packages/audioplayers_platform_interface/lib/src/audioplayers_platform.dart index 0b45a3695..e56fe2457 100644 --- a/packages/audioplayers_platform_interface/lib/src/audioplayers_platform.dart +++ b/packages/audioplayers_platform_interface/lib/src/audioplayers_platform.dart @@ -243,6 +243,12 @@ mixin EventChannelAudioplayersPlatform return const AudioEvent(eventType: AudioEventType.complete); case 'audio.onSeekComplete': return const AudioEvent(eventType: AudioEventType.seekComplete); + case 'audio.onPrepared': + final isPrepared = map.getBool('value'); + return AudioEvent( + eventType: AudioEventType.prepared, + isPrepared: isPrepared, + ); case 'audio.onLog': final value = map.getString('value'); return AudioEvent( diff --git a/packages/audioplayers_web/lib/wrapped_player.dart b/packages/audioplayers_web/lib/wrapped_player.dart index 365b57c90..7ed642f60 100644 --- a/packages/audioplayers_web/lib/wrapped_player.dart +++ b/packages/audioplayers_web/lib/wrapped_player.dart @@ -4,6 +4,7 @@ import 'dart:html'; import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart'; import 'package:audioplayers_web/num_extension.dart'; import 'package:audioplayers_web/web_audio_js.dart'; +import 'package:flutter/services.dart'; class WrappedPlayer { final String playerId; @@ -23,6 +24,7 @@ class WrappedPlayer { StreamSubscription? _playerLoadedDataSubscription; StreamSubscription? _playerPlaySubscription; StreamSubscription? _playerSeekedSubscription; + StreamSubscription? _playerErrorSubscription; WrappedPlayer(this.playerId); @@ -67,6 +69,8 @@ class WrappedPlayer { p.volume = _currentVolume; p.playbackRate = _currentPlaybackRate; + _setupStreams(p); + // setup stereo panning final audioContext = JsAudioContext(); final source = audioContext.createMediaElementSource(player!); @@ -74,8 +78,19 @@ class WrappedPlayer { source.connect(_stereoPanner!); _stereoPanner?.connect(audioContext.destination); - _playerPlaySubscription = p.onPlay.listen( + // Preload the source + p.load(); + } + + void _setupStreams(AudioElement p) { + _playerLoadedDataSubscription = p.onLoadedData.listen( (_) { + eventStreamController.add( + const AudioEvent( + eventType: AudioEventType.prepared, + isPrepared: true, + ), + ); eventStreamController.add( AudioEvent( eventType: AudioEventType.duration, @@ -85,7 +100,7 @@ class WrappedPlayer { }, onError: eventStreamController.addError, ); - _playerLoadedDataSubscription = p.onLoadedData.listen( + _playerPlaySubscription = p.onPlay.listen( (_) { eventStreamController.add( AudioEvent( @@ -118,13 +133,24 @@ class WrappedPlayer { _playerEndedSubscription = p.onEnded.listen( (_) { _pausedAt = 0; - player?.currentTime = 0; + p.currentTime = 0; eventStreamController.add( const AudioEvent(eventType: AudioEventType.complete), ); }, onError: eventStreamController.addError, ); + _playerErrorSubscription = p.onError.listen( + (_) { + eventStreamController.addError( + PlatformException( + code: p.error?.code.toString() ?? 'WebAudioError', + message: p.error?.message, + ), + ); + }, + onError: eventStreamController.addError, + ); } bool shouldLoop() => _currentReleaseMode == ReleaseMode.loop; @@ -149,6 +175,8 @@ class WrappedPlayer { _playerSeekedSubscription = null; _playerPlaySubscription?.cancel(); _playerPlaySubscription = null; + _playerErrorSubscription?.cancel(); + _playerErrorSubscription = null; } Future start(double position) async { diff --git a/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp b/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp index d0dcc7d58..bd5347ac6 100644 --- a/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp +++ b/packages/audioplayers_windows/windows/MediaEngineWrapper.cpp @@ -68,13 +68,13 @@ namespace switch((MF_MEDIA_ENGINE_EVENT)eventCode) { - case MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA: + case MF_MEDIA_ENGINE_EVENT_LOADEDDATA: m_onLoadedCB(); break; case MF_MEDIA_ENGINE_EVENT_ERROR: m_errorCB((MF_MEDIA_ENGINE_ERR)param1, (HRESULT)param2); break; - case MF_MEDIA_ENGINE_EVENT_PLAYING: + case MF_MEDIA_ENGINE_EVENT_CANPLAY: m_bufferingStateChangeCB(MediaEngineWrapper::BufferingState::HAVE_ENOUGH); break; case MF_MEDIA_ENGINE_EVENT_WAITING: @@ -389,4 +389,4 @@ void MediaEngineWrapper::OnSeekCompleted() } } -} // namespace media \ No newline at end of file +} // namespace media diff --git a/packages/audioplayers_windows/windows/audio_player.cpp b/packages/audioplayers_windows/windows/audio_player.cpp index 650b30f12..96490a604 100644 --- a/packages/audioplayers_windows/windows/audio_player.cpp +++ b/packages/audioplayers_windows/windows/audio_player.cpp @@ -30,37 +30,45 @@ AudioPlayer::AudioPlayer( auto onPlaybackEndedCB = std::bind(&AudioPlayer::OnPlaybackEnded, this); auto onTimeUpdateCB = std::bind(&AudioPlayer::OnTimeUpdate, this); auto onSeekCompletedCB = std::bind(&AudioPlayer::OnSeekCompleted, this); + auto onLoadedCB = std::bind(&AudioPlayer::SendInitialized, this); // Create and initialize the MediaEngineWrapper which manages media playback m_mediaEngineWrapper = winrt::make_self( - nullptr, onError, onBufferingStateChanged, onPlaybackEndedCB, + onLoadedCB, onError, onBufferingStateChanged, onPlaybackEndedCB, onTimeUpdateCB, onSeekCompletedCB); m_mediaEngineWrapper->Initialize(); } +// This method should be called asynchronously, to avoid freezing UI void AudioPlayer::SetSourceUrl(std::string url) { if (_url != url) { _url = url; - // Create a source resolver to create an IMFMediaSource for the content - // URL. This will create an instance of an inbuilt OS media source for - // playback. An application can skip this step and instantiate a custom - // IMFMediaSource implementation instead. - winrt::com_ptr sourceResolver; - THROW_IF_FAILED(MFCreateSourceResolver(sourceResolver.put())); - constexpr uint32_t sourceResolutionFlags = - MF_RESOLUTION_MEDIASOURCE | - MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE | - MF_RESOLUTION_READ; - MF_OBJECT_TYPE objectType = {}; - - winrt::com_ptr mediaSource; - THROW_IF_FAILED(sourceResolver->CreateObjectFromURL( - winrt::to_hstring(url).c_str(), sourceResolutionFlags, nullptr, - &objectType, reinterpret_cast(mediaSource.put_void()))); - _isInitialized = false; - m_mediaEngineWrapper->SetMediaSource(mediaSource.get()); + + try { + // Create a source resolver to create an IMFMediaSource for the content + // URL. This will create an instance of an inbuilt OS media source for + // playback. An application can skip this step and instantiate a custom + // IMFMediaSource implementation instead. + winrt::com_ptr sourceResolver; + THROW_IF_FAILED(MFCreateSourceResolver(sourceResolver.put())); + constexpr uint32_t sourceResolutionFlags = + MF_RESOLUTION_MEDIASOURCE | + MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE | + MF_RESOLUTION_READ; + MF_OBJECT_TYPE objectType = {}; + + winrt::com_ptr mediaSource; + THROW_IF_FAILED(sourceResolver->CreateObjectFromURL( + winrt::to_hstring(url).c_str(), sourceResolutionFlags, nullptr, + &objectType, reinterpret_cast(mediaSource.put_void()))); + + m_mediaEngineWrapper->SetMediaSource(mediaSource.get()); + } catch (...) { + // Forward errors to event stream, as this is called asynchronously + this->OnError("WindowsAudioError", "Error setting url to '" + url + "'.", nullptr); + } } } @@ -97,10 +105,18 @@ void AudioPlayer::OnMediaStateChange( media::MediaEngineWrapper::BufferingState bufferingState) { if (bufferingState != media::MediaEngineWrapper::BufferingState::HAVE_NOTHING) { - if (!this->_isInitialized) { - this->_isInitialized = true; - this->SendInitialized(); - } + // TODO(Gustl22): add buffering state + } +} + +void AudioPlayer::OnPrepared(bool isPrepared) { + if (this->_eventHandler) { + this->_eventHandler->Success( + std::make_unique(flutter::EncodableMap( + {{flutter::EncodableValue("event"), + flutter::EncodableValue("audio.onPrepared")}, + {flutter::EncodableValue("value"), + flutter::EncodableValue(isPrepared)}}))); } } @@ -165,8 +181,12 @@ void AudioPlayer::OnLog(const std::string& message) { } void AudioPlayer::SendInitialized() { - OnDurationUpdate(); - OnTimeUpdate(); + if (!this->_isInitialized) { + this->_isInitialized = true; + OnPrepared(true); + OnDurationUpdate(); + OnTimeUpdate(); + } } void AudioPlayer::Dispose() { diff --git a/packages/audioplayers_windows/windows/audio_player.h b/packages/audioplayers_windows/windows/audio_player.h index f0515141f..1b8ca8f81 100644 --- a/packages/audioplayers_windows/windows/audio_player.h +++ b/packages/audioplayers_windows/windows/audio_player.h @@ -107,6 +107,8 @@ class AudioPlayer { void OnSeekCompleted(); + void OnPrepared(bool isPrepared); + std::string _playerId; flutter::MethodChannel* _methodChannel; diff --git a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp index 6e3bada81..1321915f6 100644 --- a/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp +++ b/packages/audioplayers_windows/windows/audioplayers_windows_plugin.cpp @@ -182,12 +182,8 @@ void AudioplayersWindowsPlugin::HandleMethodCall( return; } - try { - player->SetSourceUrl(url); - result->Success(EncodableValue(1)); - } catch (...) { - result->Error("WindowsAudioError", "Error setting url to '" + url + "'.", nullptr); - } + std::thread(&AudioPlayer::SetSourceUrl, player, url).detach(); + result->Success(EncodableValue(1)); } else if (method_call.method_name().compare("getDuration") == 0) { result->Success(EncodableValue(player->GetDuration() / 10000)); } else if (method_call.method_name().compare("setVolume") == 0) {