diff --git a/lib/just_audio_media_kit.dart b/lib/just_audio_media_kit.dart index 8466375..ba3b2a3 100644 --- a/lib/just_audio_media_kit.dart +++ b/lib/just_audio_media_kit.dart @@ -1,4 +1,4 @@ -/// package:media_kit bindings for just_audio to support Linux and Windows. +/// `package:media_kit` bindings for `just_audio` to support Linux and Windows. library just_audio_media_kit; import 'dart:collection'; @@ -11,12 +11,12 @@ import 'package:media_kit/media_kit.dart'; import 'package:universal_platform/universal_platform.dart'; class JustAudioMediaKit extends JustAudioPlatform { - JustAudioMediaKit(); + static final _logger = Logger('JustAudioMediaKit'); - /// The internal MPV player's logLevel + /// The internal MPV player's logLevel. static MPVLogLevel mpvLogLevel = MPVLogLevel.error; - /// Sets the demuxer's cache size (in bytes) + /// Sets the demuxer's cache size (in bytes). static int bufferSize = 32 * 1024 * 1024; /// Sets the name of the underlying window & process for native backend. This is visible inside the Windows' volume mixer. @@ -40,15 +40,22 @@ class JustAudioMediaKit extends JustAudioPlatform { /// This uses `scaletempo` under the hood & disables `audio-pitch-correction`. static bool pitch = true; - /// Enables gapless playback via the [`--prefetch-playlist`](https://mpv.io/manual/stable/#options-prefetch-playlist) in libmpv + /// Enables gapless playback via the [`--prefetch-playlist`](https://mpv.io/manual/stable/#options-prefetch-playlist) in libmpv. /// /// This is highly experimental. Use at your own risk. /// /// Check [mpv's docs](https://mpv.io/manual/stable/#options-prefetch-playlist) and - /// [the related issue](https://github.com/Pato05/just_audio_media_kit/issues/11) for more information + /// [the related issue](https://github.com/Pato05/just_audio_media_kit/issues/11) for more information. + /// + /// [prefetchPlaylistSize] can be changed to set the amount of items to prefetch. static bool prefetchPlaylist = false; - static final _logger = Logger('JustAudioMediaKit'); + /// Max amount of items to prefetch in the playlist. + /// + /// Does nothing, if [prefetchPlaylist] is set to false. Default is 2. + /// It is not recommended to change this, because libmpv doesn't prefetch more than 2 items. + static int prefetchPlaylistSize = 2; + final _players = HashMap(); /// Players that are disposing (player id -> future that completes when the player is disposed) @@ -57,7 +64,7 @@ class JustAudioMediaKit extends JustAudioPlatform { /// Initializes the plugin if the platform we're running on is marked /// as true, otherwise it will leave everything unchanged. /// - /// Can also be safely called from Web, even though it'll have no effect + /// Can also be safely called from Web, even though it'll have no effect. static void ensureInitialized({ bool linux = true, bool windows = true, @@ -69,14 +76,14 @@ class JustAudioMediaKit extends JustAudioPlatform { /// The name of the library is generally `libmpv.so` on GNU/Linux and `libmpv-2.dll` on Windows. String? libmpv, }) { - if ((UniversalPlatform.isLinux && linux) || + if (!((UniversalPlatform.isLinux && linux) || (UniversalPlatform.isWindows && windows) || (UniversalPlatform.isAndroid && android) || (UniversalPlatform.isIOS && iOS) || - (UniversalPlatform.isMacOS && macOS)) { - registerWith(); - MediaKit.ensureInitialized(libmpv: libmpv); - } + (UniversalPlatform.isMacOS && macOS))) return; + + registerWith(); + MediaKit.ensureInitialized(libmpv: libmpv); } /// Registers the plugin with [JustAudioPlatform] @@ -88,7 +95,9 @@ class JustAudioMediaKit extends JustAudioPlatform { Future init(InitRequest request) async { if (_players.containsKey(request.id)) { throw PlatformException( - code: 'error', message: 'Player ${request.id} already exists!'); + code: 'error', + message: 'Player ${request.id} already exists!', + ); } _logger.fine('instantiating new player ${request.id}'); @@ -96,24 +105,27 @@ class JustAudioMediaKit extends JustAudioPlatform { _players[request.id] = player; await player.ready(); _logger.fine('player ready! (players: $_players)'); + return player; } @override - Future disposePlayer( - DisposePlayerRequest request) async { + Future disposePlayer(request) async { _logger.fine('disposing player ${request.id}'); // temporary workaround because disposePlayer is called more than once if (_disposingPlayers.containsKey(request.id)) { _logger.fine('disposePlayer() called more than once!'); await _disposingPlayers[request.id]!; + return DisposePlayerResponse(); } if (!_players.containsKey(request.id)) { throw PlatformException( - code: 'error', message: 'Player ${request.id} doesn\'t exist.'); + code: 'error', + message: 'Player ${request.id} doesn\'t exist.', + ); } final future = _players[request.id]!.release(); @@ -123,17 +135,19 @@ class JustAudioMediaKit extends JustAudioPlatform { _disposingPlayers.remove(request.id); _logger.fine('player ${request.id} disposed!'); + return DisposePlayerResponse(); } @override - Future disposeAllPlayers( - DisposeAllPlayersRequest request) async { + Future disposeAllPlayers(request) async { _logger.fine('disposing of all players...'); + if (_players.isNotEmpty) { await Future.wait(_players.values.map((e) => e.release())); _players.clear(); } + return DisposeAllPlayersResponse(); } } diff --git a/lib/mediakit_player.dart b/lib/mediakit_player.dart index 43ee60a..a7a0839 100644 --- a/lib/mediakit_player.dart +++ b/lib/mediakit_player.dart @@ -7,45 +7,81 @@ import 'package:just_audio_platform_interface/just_audio_platform_interface.dart import 'package:logging/logging.dart'; import 'package:media_kit/media_kit.dart'; -/// An [AudioPlayerPlatform] which wraps `package:media_kit`'s [Player] +/// An [AudioPlayerPlatform] which wraps `package:media_kit`'s [Player]. class MediaKitPlayer extends AudioPlayerPlatform { - /// `package:media_kit`'s [Player] + static final _logger = Logger('MediaKitPlayer'); + + /// `package:media_kit`'s [Player]. late final Player _player; - /// The subscriptions that have to be disposed + /// The list of [StreamSubscription]'s that must be disposed on [release] call. late final List _streamSubscriptions; + /// A [Completer] that completes when the player is ready to play. final _readyCompleter = Completer(); - /// Completes when the player is ready + /// Completes when the player is ready to play. Future ready() => _readyCompleter.future; - static final _logger = Logger('MediaKitPlayer'); - + /// The [StreamController] for [PlaybackEventMessage]. final _eventController = StreamController.broadcast(); + + /// The [StreamController] for [PlayerDataMessage]. final _dataController = StreamController.broadcast(); + /// The current processing state of the player. ProcessingStateMessage _processingState = ProcessingStateMessage.idle; + + /// The current buffered position of the player. Duration _bufferedPosition = Duration.zero; + + /// The current position of the player. Duration _position = Duration.zero; - /// The index that's currently playing + /// The current duration of the player. + Duration _duration = Duration.zero; + + /// List of [Player]'s [Media] objects, that are fed one by one to the [_player]. + List? _playlist; + + /// The shuffled order of the [_playlist]. + List _shuffleOrder = []; + + /// Whether the player is currently shuffling. + bool _isShuffling = false; + + /// "Actual" index of the current entry in the [_playlist], which ignores the shuffle order. This value increments by one when [_next] is called. + /// + /// To get the "shuffled" index (e.g., the one that is supposed to be played), use [_shuffledIndex]. + /// To calculate both [_currentIndex] and [_shuffledIndex] at the same time, use [_fixIndecies] method, it is not recommended to set [_currentIndex] directly. int _currentIndex = 0; - /// [LoadRequest.initialPosition] or [seek] request before [Player.play] was called and/or finished loading. + /// Returns the possibly "shuffled" index of the current entry in the [_playlist]. Returns the current index if shuffling is disabled ([_isShuffling]), otherwise returns the shuffled index. + /// + /// To get the "real" index (e.g., the one that ignores the shuffle order), use [_currentIndex]. + /// To calculate both [_currentIndex] and [_shuffledIndex] at the same time, use [_fixIndecies] method, it is not recommended to set [_shuffledIndex] directly. + /// + /// @Zensonaton: Unfortunately, I couldn't find a way to keep the _entryIndex as getter, + /// because in some methods ([seek], [load], etc.), we had to calculate [_currentIndex] from [_shuffledIndex], + /// and this was not possible with a `_entryIndex` getter. + int _shuffledIndex = 0; + + /// Contains the position that the player should [seek] to after the player is ready. Duration? _setPosition; MediaKitPlayer(super.id) { _player = Player( - configuration: PlayerConfiguration( - pitch: JustAudioMediaKit.pitch, - protocolWhitelist: JustAudioMediaKit.protocolWhitelist, - title: JustAudioMediaKit.title, - bufferSize: JustAudioMediaKit.bufferSize, - logLevel: JustAudioMediaKit.mpvLogLevel, - ready: () => _readyCompleter.complete(), - )); - + configuration: PlayerConfiguration( + pitch: JustAudioMediaKit.pitch, + protocolWhitelist: JustAudioMediaKit.protocolWhitelist, + title: JustAudioMediaKit.title, + bufferSize: JustAudioMediaKit.bufferSize, + logLevel: JustAudioMediaKit.mpvLogLevel, + ready: () => _readyCompleter.complete(), + ), + ); + + // Enable prefetching. if (JustAudioMediaKit.prefetchPlaylist && _player.platform is NativePlayer) { (_player.platform as NativePlayer) @@ -53,78 +89,153 @@ class MediaKitPlayer extends AudioPlayerPlatform { } _streamSubscriptions = [ - _player.stream.duration.listen((duration) { - _processingState = ProcessingStateMessage.ready; - if (_setPosition != null && duration.inSeconds > 0) { - unawaited(_player.seek(_setPosition!)); - _setPosition = null; - } - _updatePlaybackEvent(duration: duration); - }), - _player.stream.position.listen((position) { - _position = position; - _updatePlaybackEvent(); - }), - _player.stream.buffering.listen((isBuffering) { - _processingState = isBuffering - ? ProcessingStateMessage.buffering - : ProcessingStateMessage.ready; - _updatePlaybackEvent(); - }), - _player.stream.buffer.listen((buffer) { - _bufferedPosition = buffer; - _updatePlaybackEvent(); - }), - _player.stream.playing.listen((playing) { - _dataController.add(PlayerDataMessage(playing: playing)); - }), - _player.stream.volume.listen((volume) { - _dataController.add(PlayerDataMessage(volume: volume / 100.0)); - }), - _player.stream.completed.listen((completed) { - _bufferedPosition = _position = Duration.zero; - if (completed && - // is at the end of the [Playlist] - _currentIndex == _player.state.playlist.medias.length - 1 && - // is not looping (technically this shouldn't be fired if the player is looping) - _player.state.playlistMode == PlaylistMode.none) { + _player.stream.duration.listen( + (duration) { + _processingState = ProcessingStateMessage.ready; + _duration = duration; + + // If player is ready, and we have a seek request, then seek to that position. + if (_setPosition != null && duration.inSeconds > 0) { + unawaited( + _player.seek(_setPosition!), + ); + + _setPosition = null; + } + + _updatePlaybackEvent(); + }, + ), + _player.stream.position.listen( + (position) { + _position = position; + + _updatePlaybackEvent(); + }, + ), + _player.stream.buffering.listen( + (isBuffering) { + _processingState = isBuffering + ? ProcessingStateMessage.buffering + : ProcessingStateMessage.ready; + + _updatePlaybackEvent(); + }, + ), + _player.stream.buffer.listen( + (buffer) { + _bufferedPosition = buffer; + + _updatePlaybackEvent(); + }, + ), + _player.stream.playing.listen( + (playing) { + _dataController.add( + PlayerDataMessage(playing: playing), + ); + }, + ), + _player.stream.volume.listen( + (volume) { + _dataController.add( + PlayerDataMessage(volume: volume / 100.0), + ); + }, + ), + _player.stream.completed.listen((completed) async { + if (!completed) return; + + final bool isPlaylistEnd = _currentIndex == _playlist!.length - 1; + final bool isLooping = _player.state.playlistMode != PlaylistMode.none; + + if (isPlaylistEnd && !isLooping) { _processingState = ProcessingStateMessage.completed; } else { _processingState = ProcessingStateMessage.ready; } - _updatePlaybackEvent(); - }), - _player.stream.error.listen((error) { - _processingState = ProcessingStateMessage.idle; - _updatePlaybackEvent(); - _logger.severe('ERROR OCCURRED: $error'); - }), - _player.stream.playlist.listen((playlist) { - if (_currentIndex != playlist.index) { - _bufferedPosition = _position = Duration.zero; - _currentIndex = playlist.index; + // Start playing next media after current media got completed. + if (_player.state.playlistMode == PlaylistMode.single) { + await _player.seek(Duration.zero); + } else { + await _next(); } + _updatePlaybackEvent(); }), - _player.stream.playlistMode.listen((playlistMode) { - _dataController.add( - PlayerDataMessage(loopMode: _playlistModeToLoopMode(playlistMode))); - }), - _player.stream.pitch.listen((pitch) { - _dataController.add(PlayerDataMessage(pitch: pitch)); - }), - _player.stream.rate.listen((rate) { - _dataController.add(PlayerDataMessage(speed: rate)); - }), - _player.stream.log.listen((event) { - // ignore: avoid_print - print("MPV: [${event.level}] ${event.prefix}: ${event.text}"); - }), + _player.stream.playlist.listen( + (playlist) { + _updatePlaybackEvent(); + }, + ), + _player.stream.playlistMode.listen( + (playlistMode) { + _dataController.add( + PlayerDataMessage( + loopMode: playlistModeToLoopMode(playlistMode), + ), + ); + }, + ), + _player.stream.pitch.listen( + (pitch) { + _dataController.add( + PlayerDataMessage(pitch: pitch), + ); + }, + ), + _player.stream.rate.listen( + (rate) { + _dataController.add( + PlayerDataMessage(speed: rate), + ); + }, + ), + _player.stream.error.listen( + (error) { + _logger.severe('ERROR OCCURRED: $error'); + + _processingState = ProcessingStateMessage.idle; + // TODO: Pass that error to just_audio. + + _updatePlaybackEvent(); + }, + ), + _player.stream.log.listen( + (event) { + // ignore: avoid_print + print('MPV: [${event.level}] ${event.prefix}: ${event.text}'); + + // TODO: Pass log message to just_audio. + }, + ), ]; } - PlaylistMode _loopModeToPlaylistMode(LoopModeMessage loopMode) { + /// Converts an [AudioSourceMessage] into a [Media] for playback. + static Media audioSourceToMedia(AudioSourceMessage audioSource) { + switch (audioSource) { + case final UriAudioSourceMessage uriSource: + return Media(uriSource.uri, httpHeaders: audioSource.headers); + + case final SilenceAudioSourceMessage silenceSource: + // Source: https://github.com/bleonard252/just_audio_mpv/blob/main/lib/src/mpv_player.dart#L137 + return Media( + 'av://lavfi:anullsrc=d=${silenceSource.duration.inMilliseconds}ms', + ); + } + + // Unknown audio source type. + throw UnsupportedError( + '${audioSource.runtimeType} is currently not supported', + ); + } + + /// Converts [LoopModeMessage] (just_audio) into [PlaylistMode] (media_kit). + /// + /// Opposite of [playlistModeToLoopMode]. + static PlaylistMode loopModeToPlaylistMode(LoopModeMessage loopMode) { return switch (loopMode) { LoopModeMessage.off => PlaylistMode.none, LoopModeMessage.one => PlaylistMode.single, @@ -132,7 +243,10 @@ class MediaKitPlayer extends AudioPlayerPlatform { }; } - LoopModeMessage _playlistModeToLoopMode(PlaylistMode playlistMode) { + /// Converts [PlaylistMode] (media_kit) into [LoopModeMessage] (just_audio). + /// + /// Opposite of [loopModeToPlaylistMode]. + static LoopModeMessage playlistModeToLoopMode(PlaylistMode playlistMode) { return switch (playlistMode) { PlaylistMode.none => LoopModeMessage.off, PlaylistMode.single => LoopModeMessage.one, @@ -140,6 +254,112 @@ class MediaKitPlayer extends AudioPlayerPlatform { }; } + /// Updates the playback event with the current state of the player. + void _updatePlaybackEvent() { + _eventController.add( + PlaybackEventMessage( + processingState: _processingState, + updateTime: DateTime.now(), + updatePosition: _position, + bufferedPosition: _bufferedPosition, + duration: _duration, + icyMetadata: null, + currentIndex: _shuffledIndex, + androidAudioSessionId: null, + ), + ); + } + + /// Sets both [_currentIndex] and [_shuffledIndex] from provided [current] (current index) or [shuffled] (shuffled index). + /// + /// For example, [_next] calls this method with [current] + 1, while methods like [seek] or [load] call this method with [shuffled] index specified. + /// + /// If [current] is specified, then [_shuffledIndex] is calculated from [_currentIndex] and [_shuffleOrder]. + /// If [shuffled] is specified, then [_currentIndex] is calculated from [_shuffledIndex] and [_shuffleOrder]. + void _fixIndecies({ + int? current, + int? shuffled, + }) { + assert( + current != null || shuffled != null, + 'At least one of currentIndex or shuffledIndex must be provided.', + ); + assert( + current == null || shuffled == null, + 'Only one of currentIndex or shuffledIndex must be provided.', + ); + + if (current != null) { + _currentIndex = current; + _shuffledIndex = + _isShuffling ? _shuffleOrder[_currentIndex] : _currentIndex; + } else { + _shuffledIndex = shuffled!; + _currentIndex = + _isShuffling ? _shuffleOrder.indexOf(_shuffledIndex) : _shuffledIndex; + } + } + + /// Plays the next track in the playlist. + /// + /// Don't get confused with [seek], because this method is called only when the current track is completed. + Future _next() async { + if (_playlist == null) return; + + // Seek to the beginning of the current track, if loop mode is set to single. + if (_player.state.playlistMode == PlaylistMode.single) { + await _player.seek(Duration.zero); + + return; + } + + // Check if we have reached the end of the playlist. + if (_currentIndex >= _playlist!.length - 1) { + if (_player.state.playlistMode == PlaylistMode.loop) { + _fixIndecies(current: 0); + + await _sendAudios(); + } else { + await _player.stop(); + } + + return; + } + + // We haven't reached the end of the playlist yet, so play the next track. + _fixIndecies(current: _currentIndex + 1); + + return await _sendAudios(); + } + + /// Sends the next [JustAudioMediaKit.prefetchPlaylistSize] tracks to the player, and plays them. + Future _sendAudios() async { + if (_playlist == null) return; + + // Take [prefetchPlaylistSize] tracks from the playlist and send them to the player. + final int maxSize = + (_currentIndex + JustAudioMediaKit.prefetchPlaylistSize).clamp( + 0, + _playlist!.length, + ); + + await _player.open( + Playlist( + _isShuffling + ? _shuffleOrder + .sublist(_currentIndex, maxSize) + .map( + (index) => _playlist![index], + ) + .toList() + : _playlist!.sublist( + _currentIndex, + maxSize, + ), + ), + ); + } + @override Stream get playbackEventMessageStream => _eventController.stream; @@ -148,134 +368,158 @@ class MediaKitPlayer extends AudioPlayerPlatform { Stream get playerDataMessageStream => _dataController.stream; - /// Updates the playback event - void _updatePlaybackEvent( - {Duration? duration, IcyMetadataMessage? icyMetadata}) { - _eventController.add(PlaybackEventMessage( - processingState: _processingState, - updateTime: DateTime.now(), - updatePosition: _position, - bufferedPosition: _bufferedPosition, - duration: duration, - icyMetadata: icyMetadata, - currentIndex: _currentIndex, - androidAudioSessionId: null, - )); - } - @override Future load(LoadRequest request) async { _logger.finest('load(${request.toMap()})'); - _currentIndex = request.initialIndex ?? 0; - _bufferedPosition = Duration.zero; - _position = Duration.zero; _processingState = ProcessingStateMessage.buffering; + _currentIndex = request.initialIndex ?? 0; + _position = _bufferedPosition = Duration.zero; if (request.audioSourceMessage is ConcatenatingAudioSourceMessage) { - final audioSource = - request.audioSourceMessage as ConcatenatingAudioSourceMessage; - final playable = Playlist( - audioSource.children.map(_convertAudioSourceIntoMediaKit).toList(), - index: _currentIndex); + final src = request.audioSourceMessage as ConcatenatingAudioSourceMessage; + + _shuffleOrder = src.shuffleOrder; + _playlist = src.children.map(audioSourceToMedia).toList(); - await _player.open(playable); + _fixIndecies(shuffled: _currentIndex); } else { - final playable = - _convertAudioSourceIntoMediaKit(request.audioSourceMessage); + final playable = audioSourceToMedia(request.audioSourceMessage); + _logger.finest('playable is ${playable.toString()}'); - await _player.open(playable); + _playlist = [playable]; } + // [_shuffledIndex] contains the index of a track that is supposed to be played. + await _sendAudios(); + if (request.initialPosition != null) { _setPosition = _position = request.initialPosition!; + + // TODO: Fix this seek request here (it doesn't do anything). + await _player.seek(_setPosition!); } _updatePlaybackEvent(); - return LoadResponse(duration: _player.state.duration); + return LoadResponse( + duration: _player.state.duration, + ); } @override - Future play(PlayRequest request) { - return _player.play().then((_) => PlayResponse()); + Future play(PlayRequest request) async { + await _player.play(); + + return PlayResponse(); } @override - Future pause(PauseRequest request) { - return _player.pause().then((_) => PauseResponse()); + Future pause(PauseRequest request) async { + await _player.pause(); + + return PauseResponse(); } @override - Future setVolume(SetVolumeRequest request) { - return _player - .setVolume(request.volume * 100.0) - .then((value) => SetVolumeResponse()); + Future setVolume(SetVolumeRequest request) async { + await _player.setVolume(request.volume * 100.0); + + return SetVolumeResponse(); } @override - Future setSpeed(SetSpeedRequest request) { - return _player.setRate(request.speed).then((_) => SetSpeedResponse()); + Future setSpeed(SetSpeedRequest request) async { + await _player.setRate(request.speed); + + return SetSpeedResponse(); } @override - Future setPitch(SetPitchRequest request) => - _player.setPitch(request.pitch).then((_) => SetPitchResponse()); + Future setPitch(SetPitchRequest request) async { + await _player.setPitch(request.pitch); + + return SetPitchResponse(); + } @override Future setLoopMode(SetLoopModeRequest request) async { - await _player.setPlaylistMode(_loopModeToPlaylistMode(request.loopMode)); + await _player.setPlaylistMode(loopModeToPlaylistMode(request.loopMode)); + return SetLoopModeResponse(); } @override - Future setShuffleMode( - SetShuffleModeRequest request) async { - bool shuffling = request.shuffleMode != ShuffleModeMessage.none; - await _player.setShuffle(shuffling); + Future setShuffleMode(request) async { + _isShuffling = request.shuffleMode != ShuffleModeMessage.none; - _dataController.add(PlayerDataMessage( + _dataController.add( + PlayerDataMessage( shuffleMode: - shuffling ? ShuffleModeMessage.all : ShuffleModeMessage.none)); + _isShuffling ? ShuffleModeMessage.all : ShuffleModeMessage.none, + ), + ); + return SetShuffleModeResponse(); } + @override + Future setShuffleOrder(request) async { + // Not tested. + + if (request.audioSourceMessage is ConcatenatingAudioSourceMessage) { + final src = request.audioSourceMessage as ConcatenatingAudioSourceMessage; + + _shuffleOrder = src.shuffleOrder; + } + + return SetShuffleOrderResponse(); + } + @override Future seek(SeekRequest request) async { _logger.finest('seek(${request.toMap()})'); + if (request.index != null) { - await _player.jump(request.index!); + // If index is the same, then simply seek at the beginning. + if (request.index == _shuffledIndex) { + await _player.seek(Duration.zero); + + return SeekResponse(); + } + + _fixIndecies(shuffled: request.index!); + + await _sendAudios(); } + _position = request.position ?? Duration.zero; if (request.position != null) { - _position = request.position!; - if (_player.state.duration.inSeconds > 0) { await _player.seek(request.position!); } else { _setPosition = request.position!; } - } else { - _position = Duration.zero; } - // reset position on seek + // Reset position after seeking. _updatePlaybackEvent(); + return SeekResponse(); } @override - Future concatenatingInsertAll( - ConcatenatingInsertAllRequest request) async { - // _logger.fine('concatenatingInsertAll(${request.toMap()})'); - for (final source in request.children) { - await _player.add(_convertAudioSourceIntoMediaKit(source)); + Future concatenatingInsertAll(request) async { + _logger.fine('concatenatingInsertAll(${request.toMap()})'); - final length = _player.state.playlist.medias.length; + _shuffleOrder = request.shuffleOrder; - if (length == 0 || length == 1) continue; + for (final source in request.children) { + final mkSource = audioSourceToMedia(source); - if (request.index < (length - 1) && request.index >= 0) { - await _player.move(length, request.index); + if (request.index > _playlist!.length) { + _playlist!.add(mkSource); + } else { + _playlist!.insert(request.index, mkSource); } } @@ -285,52 +529,44 @@ class MediaKitPlayer extends AudioPlayerPlatform { @override Future concatenatingRemoveRange( ConcatenatingRemoveRangeRequest request) async { - for (var i = request.startIndex; i < request.endIndex; i++) { - await _player.remove(request.startIndex); - } + // Not tested. + + _logger.fine('concatenatingRemoveRange(${request.toMap()})'); + + _shuffleOrder = request.shuffleOrder; + + _playlist!.removeRange(request.startIndex, request.endIndex); return ConcatenatingRemoveRangeResponse(); } @override - Future concatenatingMove( - ConcatenatingMoveRequest request) { - return _player - .move( - request.currentIndex, - // not sure why, but apparently there's an underlying difference between just_audio's move implementation - // and media_kit, so let's fix it - request.currentIndex > request.newIndex - ? request.newIndex - : request.newIndex + 1) - .then((_) => ConcatenatingMoveResponse()); + Future concatenatingMove(request) async { + _logger.fine('concatenatingMove(${request.toMap()})'); + + _shuffleOrder = request.shuffleOrder; + + await _player.move( + request.currentIndex, + + // Not sure why, but apparently there's an underlying difference between + // just_audio's move implementation and media_kit, so let's fix it. + request.currentIndex > request.newIndex + ? request.newIndex + : request.newIndex + 1, + ); + + return ConcatenatingMoveResponse(); } /// Release the resources used by this player. Future release() async { - _logger.info('releasing player resources'); + _logger.finest('Releasing player resources'); + await _player.dispose(); - // cancel all stream subscriptions for (final StreamSubscription subscription in _streamSubscriptions) { unawaited(subscription.cancel()); } _streamSubscriptions.clear(); } - - /// Converts an [AudioSourceMessage] into a [Media] for playback - Media _convertAudioSourceIntoMediaKit(AudioSourceMessage audioSource) { - switch (audioSource) { - case final UriAudioSourceMessage uriSource: - return Media(uriSource.uri, httpHeaders: audioSource.headers); - - case final SilenceAudioSourceMessage silenceSource: - // from https://github.com/bleonard252/just_audio_mpv/blob/main/lib/src/mpv_player.dart#L137 - return Media( - 'av://lavfi:anullsrc=d=${silenceSource.duration.inMilliseconds}ms'); - - default: - throw UnsupportedError( - '${audioSource.runtimeType} is currently not supported'); - } - } }