mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge c5a72cd44c
into b9c6c98e38
This commit is contained in:
commit
71bcc2228e
@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -7,9 +6,7 @@ import 'package:spotify/spotify.dart' hide Playlist;
|
|||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/services/audio_player/custom_player.dart';
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' as mk;
|
import 'package:media_kit/media_kit.dart' as mk;
|
||||||
|
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -17,40 +14,40 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
part 'audio_players_streams_mixin.dart';
|
part 'audio_players_streams_mixin.dart';
|
||||||
part 'audio_player_impl.dart';
|
part 'audio_player_impl.dart';
|
||||||
|
|
||||||
|
// Constants class for shared constants like port and addresses
|
||||||
|
class Constants {
|
||||||
|
static const defaultServerPort = 8080;
|
||||||
|
static const defaultLocalHost = "localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get network address based on the platform
|
||||||
|
String getNetworkAddress() {
|
||||||
|
return kIsWindows ? Constants.defaultLocalHost : InternetAddress.anyIPv4.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get URI for a given track
|
||||||
|
String getUriForTrack(Track track, int serverPort) {
|
||||||
|
return track is LocalTrack
|
||||||
|
? track.path
|
||||||
|
: "http://${getNetworkAddress()}:$serverPort/stream/${track.id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpotubeMedia class handling media creation logic
|
||||||
class SpotubeMedia extends mk.Media {
|
class SpotubeMedia extends mk.Media {
|
||||||
final Track track;
|
final Track track;
|
||||||
|
static int serverPort = Constants.defaultServerPort;
|
||||||
|
|
||||||
static int serverPort = 0;
|
SpotubeMedia(this.track, {Map<String, dynamic>? extras, super.httpHeaders})
|
||||||
|
: super(
|
||||||
SpotubeMedia(
|
getUriForTrack(track, serverPort),
|
||||||
this.track, {
|
|
||||||
Map<String, dynamic>? extras,
|
|
||||||
super.httpHeaders,
|
|
||||||
}) : super(
|
|
||||||
track is LocalTrack
|
|
||||||
? track.path
|
|
||||||
: "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
|
|
||||||
extras: {
|
extras: {
|
||||||
...?extras,
|
...?extras,
|
||||||
"track": switch (track) {
|
"track": track.toJson(),
|
||||||
LocalTrack() => track.toJson(),
|
|
||||||
SourcedTrack() => track.toJson(),
|
|
||||||
_ => track.toJson(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get uri {
|
String get uri => getUriForTrack(track, serverPort);
|
||||||
return switch (track) {
|
|
||||||
/// [super.uri] must be used instead of [track.path] to prevent wrong
|
|
||||||
/// path format exceptions in Windows causing [extras] to be null
|
|
||||||
LocalTrack() => super.uri,
|
|
||||||
_ =>
|
|
||||||
"http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:"
|
|
||||||
"$serverPort/stream/${track.id}",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SpotubeMedia.fromMedia(mk.Media media) {
|
factory SpotubeMedia.fromMedia(mk.Media media) {
|
||||||
final track = media.uri.startsWith("http")
|
final track = media.uri.startsWith("http")
|
||||||
@ -62,102 +59,100 @@ class SpotubeMedia extends mk.Media {
|
|||||||
httpHeaders: media.httpHeaders,
|
httpHeaders: media.httpHeaders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @override
|
|
||||||
// operator ==(Object other) {
|
|
||||||
// if (other is! SpotubeMedia) return false;
|
|
||||||
|
|
||||||
// final isLocal = track is LocalTrack && other.track is LocalTrack;
|
|
||||||
// return isLocal
|
|
||||||
// ? (other.track as LocalTrack).path == (track as LocalTrack).path
|
|
||||||
// : other.track.id == track.id;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// int get hashCode => track is LocalTrack
|
|
||||||
// ? (track as LocalTrack).path.hashCode
|
|
||||||
// : track.id.hashCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AudioPlayerInterface {
|
// Factory class to create SpotubeMedia instances
|
||||||
final CustomPlayer _mkPlayer;
|
class SpotubeMediaFactory {
|
||||||
|
static SpotubeMedia create(Track track, {Map<String, dynamic>? extras, Map<String, String>? headers}) {
|
||||||
|
return SpotubeMedia(track, extras: extras, httpHeaders: headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AudioPlayerInterface()
|
// Playback state management class
|
||||||
: _mkPlayer = CustomPlayer(
|
class PlaybackStateManager {
|
||||||
configuration: const mk.PlayerConfiguration(
|
final CustomPlayer player;
|
||||||
title: "Spotube",
|
|
||||||
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
|
PlaybackStateManager(this.player);
|
||||||
),
|
|
||||||
) {
|
bool get isPlaying => player.state.playing;
|
||||||
_mkPlayer.stream.error.listen((event) {
|
bool get isPaused => !player.state.playing;
|
||||||
|
bool get isStopped => player.state.playlist.medias.isEmpty;
|
||||||
|
|
||||||
|
Duration get duration => player.state.duration;
|
||||||
|
Duration get position => player.state.position;
|
||||||
|
Duration get bufferedPosition => player.state.buffer;
|
||||||
|
bool get isShuffled => player.shuffled;
|
||||||
|
double get volume => player.state.volume / 100;
|
||||||
|
|
||||||
|
Future<List<mk.AudioDevice>> get devices async => player.state.audioDevices;
|
||||||
|
Future<mk.AudioDevice> get selectedDevice async => player.state.audioDevice;
|
||||||
|
|
||||||
|
PlaylistMode get loopMode => player.state.playlistMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main AudioPlayerInterface class with DI and error handling
|
||||||
|
abstract class AudioPlayerInterface {
|
||||||
|
final CustomPlayer player;
|
||||||
|
final PlaybackStateManager stateManager;
|
||||||
|
|
||||||
|
AudioPlayerInterface(this.player)
|
||||||
|
: stateManager = PlaybackStateManager(player) {
|
||||||
|
player.stream.error.listen((event) {
|
||||||
AppLogger.reportError(event, StackTrace.current);
|
AppLogger.reportError(event, StackTrace.current);
|
||||||
|
// Retry or fallback mechanism can be added here
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the current platform supports the audioplayers plugin
|
// High-level control methods for playback
|
||||||
static const bool _mkSupportedPlatform = true;
|
Future<void> play() async {
|
||||||
|
try {
|
||||||
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
await player.play();
|
||||||
|
} catch (e) {
|
||||||
Duration get duration {
|
AppLogger.reportError(e, StackTrace.current);
|
||||||
return _mkPlayer.state.duration;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Playlist get playlist {
|
Future<void> pause() async {
|
||||||
return _mkPlayer.state.playlist;
|
try {
|
||||||
|
await player.pause();
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.reportError(e, StackTrace.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration get position {
|
Future<void> stop() async {
|
||||||
return _mkPlayer.state.position;
|
try {
|
||||||
|
await player.stop();
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.reportError(e, StackTrace.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration get bufferedPosition {
|
Future<void> seek(Duration position) async {
|
||||||
return _mkPlayer.state.buffer;
|
try {
|
||||||
|
await player.seek(position);
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.reportError(e, StackTrace.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<mk.AudioDevice> get selectedDevice async {
|
// Access state information through the state manager
|
||||||
return _mkPlayer.state.audioDevice;
|
bool get isPlaying => stateManager.isPlaying;
|
||||||
}
|
bool get isPaused => stateManager.isPaused;
|
||||||
|
bool get isStopped => stateManager.isStopped;
|
||||||
Future<List<mk.AudioDevice>> get devices async {
|
Duration get duration => stateManager.duration;
|
||||||
return _mkPlayer.state.audioDevices;
|
Duration get position => stateManager.position;
|
||||||
}
|
Duration get bufferedPosition => stateManager.bufferedPosition;
|
||||||
|
bool get isShuffled => stateManager.isShuffled;
|
||||||
bool get hasSource {
|
double get volume => stateManager.volume;
|
||||||
return _mkPlayer.state.playlist.medias.isNotEmpty;
|
Future<List<mk.AudioDevice>> get devices => stateManager.devices;
|
||||||
}
|
Future<mk.AudioDevice> get selectedDevice => stateManager.selectedDevice;
|
||||||
|
PlaylistMode get loopMode => stateManager.loopMode;
|
||||||
// states
|
}
|
||||||
bool get isPlaying {
|
|
||||||
return _mkPlayer.state.playing;
|
// Example implementation for a specific platform/player
|
||||||
}
|
class MyAudioPlayer extends AudioPlayerInterface {
|
||||||
|
MyAudioPlayer(CustomPlayer player) : super(player);
|
||||||
bool get isPaused {
|
|
||||||
return !_mkPlayer.state.playing;
|
// Additional functionality can be added here if necessary
|
||||||
}
|
|
||||||
|
|
||||||
bool get isStopped {
|
|
||||||
return !hasSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> get isCompleted async {
|
|
||||||
return _mkPlayer.state.completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isShuffled {
|
|
||||||
return _mkPlayer.shuffled;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistMode get loopMode {
|
|
||||||
return _mkPlayer.state.playlistMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current volume of the player, between 0 and 1
|
|
||||||
double get volume {
|
|
||||||
return _mkPlayer.state.volume / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isBuffering {
|
|
||||||
return _mkPlayer.state.buffering;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,28 @@
|
|||||||
part of 'audio_player.dart';
|
|
||||||
|
|
||||||
final audioPlayer = SpotubeAudioPlayer();
|
final audioPlayer = SpotubeAudioPlayer();
|
||||||
|
|
||||||
class SpotubeAudioPlayer extends AudioPlayerInterface
|
class SpotubeAudioPlayer extends AudioPlayerInterface with SpotubeAudioPlayersStreams {
|
||||||
with SpotubeAudioPlayersStreams {
|
// Playback control methods
|
||||||
Future<void> pause() async {
|
Future<void> pause() async => await player.pause();
|
||||||
await _mkPlayer.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resume() async {
|
Future<void> resume() async => await player.play();
|
||||||
await _mkPlayer.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async => await player.stop();
|
||||||
await _mkPlayer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async => await player.seek(position);
|
||||||
await _mkPlayer.seek(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Volume is between 0 and 1
|
/// Set volume between 0 and 1
|
||||||
Future<void> setVolume(double volume) async {
|
Future<void> setVolume(double volume) async {
|
||||||
assert(volume >= 0 && volume <= 1);
|
assert(volume >= 0 && volume <= 1);
|
||||||
await _mkPlayer.setVolume(volume * 100);
|
await player.setVolume(volume * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setSpeed(double speed) async {
|
Future<void> setSpeed(double speed) async => await player.setRate(speed);
|
||||||
await _mkPlayer.setRate(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setAudioDevice(mk.AudioDevice device) async {
|
Future<void> setAudioDevice(mk.AudioDevice device) async => await player.setAudioDevice(device);
|
||||||
await _mkPlayer.setAudioDevice(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async => await player.dispose();
|
||||||
await _mkPlayer.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playlist related
|
|
||||||
|
|
||||||
|
// Playlist control methods
|
||||||
Future<void> openPlaylist(
|
Future<void> openPlaylist(
|
||||||
List<mk.Media> tracks, {
|
List<mk.Media> tracks, {
|
||||||
bool autoPlay = true,
|
bool autoPlay = true,
|
||||||
@ -47,88 +30,59 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
}) async {
|
}) async {
|
||||||
assert(tracks.isNotEmpty);
|
assert(tracks.isNotEmpty);
|
||||||
assert(initialIndex <= tracks.length - 1);
|
assert(initialIndex <= tracks.length - 1);
|
||||||
await _mkPlayer.open(
|
|
||||||
|
await player.open(
|
||||||
mk.Playlist(tracks, index: initialIndex),
|
mk.Playlist(tracks, index: initialIndex),
|
||||||
play: autoPlay,
|
play: autoPlay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get sources {
|
// Helper methods for playlist sources
|
||||||
return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList();
|
List<String> get sources => player.state.playlist.medias.map((e) => e.uri).toList();
|
||||||
}
|
|
||||||
|
|
||||||
String? get currentSource {
|
String? get currentSource {
|
||||||
if (_mkPlayer.state.playlist.index == -1) return null;
|
final index = player.state.playlist.index;
|
||||||
return _mkPlayer.state.playlist.medias
|
if (index == -1) return null;
|
||||||
.elementAtOrNull(_mkPlayer.state.playlist.index)
|
return player.state.playlist.medias.elementAtOrNull(index)?.uri;
|
||||||
?.uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get nextSource {
|
String? get nextSource {
|
||||||
if (loopMode == PlaylistMode.loop &&
|
final isLastTrack = player.state.playlist.index == player.state.playlist.medias.length - 1;
|
||||||
_mkPlayer.state.playlist.index ==
|
if (loopMode == PlaylistMode.loop && isLastTrack) return sources.first;
|
||||||
_mkPlayer.state.playlist.medias.length - 1) {
|
|
||||||
return sources.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mkPlayer.state.playlist.medias
|
return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index + 1)?.uri;
|
||||||
.elementAtOrNull(_mkPlayer.state.playlist.index + 1)
|
|
||||||
?.uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get previousSource {
|
String? get previousSource {
|
||||||
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) {
|
if (loopMode == PlaylistMode.loop && player.state.playlist.index == 0) return sources.last;
|
||||||
return sources.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mkPlayer.state.playlist.medias
|
return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index - 1)?.uri;
|
||||||
.elementAtOrNull(_mkPlayer.state.playlist.index - 1)
|
|
||||||
?.uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int get currentIndex => _mkPlayer.state.playlist.index;
|
int get currentIndex => player.state.playlist.index;
|
||||||
|
|
||||||
Future<void> skipToNext() async {
|
// Playlist navigation methods
|
||||||
await _mkPlayer.next();
|
Future<void> skipToNext() async => await player.next();
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> skipToPrevious() async {
|
Future<void> skipToPrevious() async => await player.previous();
|
||||||
await _mkPlayer.previous();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> jumpTo(int index) async {
|
Future<void> jumpTo(int index) async => await player.jump(index);
|
||||||
await _mkPlayer.jump(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addTrack(mk.Media media) async {
|
// Playlist management methods
|
||||||
await _mkPlayer.add(media);
|
Future<void> addTrack(mk.Media media) async => await player.add(media);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addTrackAt(mk.Media media, int index) async {
|
Future<void> addTrackAt(mk.Media media, int index) async => await player.insert(index, media);
|
||||||
await _mkPlayer.insert(index, media);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeTrack(int index) async {
|
Future<void> removeTrack(int index) async => await player.remove(index);
|
||||||
await _mkPlayer.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> moveTrack(int from, int to) async {
|
Future<void> moveTrack(int from, int to) async => await player.move(from, to);
|
||||||
await _mkPlayer.move(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearPlaylist() async {
|
Future<void> clearPlaylist() async => await player.stop();
|
||||||
_mkPlayer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setShuffle(bool shuffle) async {
|
// Shuffle and loop mode control
|
||||||
await _mkPlayer.setShuffle(shuffle);
|
Future<void> setShuffle(bool shuffle) async => await player.setShuffle(shuffle);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setLoopMode(PlaylistMode loop) async {
|
Future<void> setLoopMode(PlaylistMode loop) async => await player.setPlaylistMode(loop);
|
||||||
await _mkPlayer.setPlaylistMode(loop);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setAudioNormalization(bool normalize) async {
|
Future<void> setAudioNormalization(bool normalize) async => await player.setAudioNormalization(normalize);
|
||||||
await _mkPlayer.setAudioNormalization(normalize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,43 +4,52 @@ import 'package:media_kit/media_kit.dart';
|
|||||||
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
import 'package:flutter_broadcasts/flutter_broadcasts.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
// ignore: implementation_imports
|
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
/// MediaKit [Player] by default doesn't have a state stream.
|
|
||||||
/// This class adds a state stream to the [Player] class.
|
|
||||||
class CustomPlayer extends Player {
|
class CustomPlayer extends Player {
|
||||||
final StreamController<AudioPlaybackState> _playerStateStream;
|
final StreamController<AudioPlaybackState> _playerStateStream = StreamController.broadcast();
|
||||||
final StreamController<bool> _shuffleStream;
|
final StreamController<bool> _shuffleStream = StreamController.broadcast();
|
||||||
|
|
||||||
late final List<StreamSubscription> _subscriptions;
|
late final List<StreamSubscription> _subscriptions;
|
||||||
|
bool _shuffled = false;
|
||||||
bool _shuffled;
|
|
||||||
int _androidAudioSessionId = 0;
|
int _androidAudioSessionId = 0;
|
||||||
String _packageName = "";
|
String _packageName = "";
|
||||||
AndroidAudioManager? _androidAudioManager;
|
AndroidAudioManager? _androidAudioManager;
|
||||||
|
|
||||||
CustomPlayer({super.configuration})
|
CustomPlayer({super.configuration}) {
|
||||||
: _playerStateStream = StreamController.broadcast(),
|
|
||||||
_shuffleStream = StreamController.broadcast(),
|
|
||||||
_shuffled = false {
|
|
||||||
nativePlayer.setProperty("network-timeout", "120");
|
nativePlayer.setProperty("network-timeout", "120");
|
||||||
|
_initPlatformSpecificSetup();
|
||||||
|
_listenToPlayerEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initPlatformSpecificSetup() async {
|
||||||
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
_packageName = packageInfo.packageName;
|
||||||
|
|
||||||
|
if (kIsAndroid) {
|
||||||
|
_androidAudioManager = AndroidAudioManager();
|
||||||
|
_androidAudioSessionId = await _androidAudioManager!.generateAudioSessionId();
|
||||||
|
notifyAudioSessionUpdate(true);
|
||||||
|
await _setAndroidAudioSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setAndroidAudioSession() async {
|
||||||
|
await nativePlayer.setProperty("audiotrack-session-id", _androidAudioSessionId.toString());
|
||||||
|
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToPlayerEvents() {
|
||||||
_subscriptions = [
|
_subscriptions = [
|
||||||
stream.buffering.listen((event) {
|
stream.buffering.listen((_) => _playerStateStream.add(AudioPlaybackState.buffering)),
|
||||||
_playerStateStream.add(AudioPlaybackState.buffering);
|
|
||||||
}),
|
|
||||||
stream.playing.listen((playing) {
|
stream.playing.listen((playing) {
|
||||||
if (playing) {
|
_playerStateStream.add(playing ? AudioPlaybackState.playing : AudioPlaybackState.paused);
|
||||||
_playerStateStream.add(AudioPlaybackState.playing);
|
|
||||||
} else {
|
|
||||||
_playerStateStream.add(AudioPlaybackState.paused);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
stream.completed.listen((isCompleted) async {
|
stream.completed.listen((isCompleted) {
|
||||||
if (!isCompleted) return;
|
if (isCompleted) {
|
||||||
_playerStateStream.add(AudioPlaybackState.completed);
|
_playerStateStream.add(AudioPlaybackState.completed);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
stream.playlist.listen((event) {
|
stream.playlist.listen((event) {
|
||||||
if (event.medias.isEmpty) {
|
if (event.medias.isEmpty) {
|
||||||
@ -51,23 +60,6 @@ class CustomPlayer extends Player {
|
|||||||
AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current);
|
AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current);
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
PackageInfo.fromPlatform().then((packageInfo) {
|
|
||||||
_packageName = packageInfo.packageName;
|
|
||||||
});
|
|
||||||
if (kIsAndroid) {
|
|
||||||
_androidAudioManager = AndroidAudioManager();
|
|
||||||
AudioSession.instance.then((s) async {
|
|
||||||
_androidAudioSessionId =
|
|
||||||
await _androidAudioManager!.generateAudioSessionId();
|
|
||||||
notifyAudioSessionUpdate(true);
|
|
||||||
|
|
||||||
await nativePlayer.setProperty(
|
|
||||||
"audiotrack-session-id",
|
|
||||||
_androidAudioSessionId.toString(),
|
|
||||||
);
|
|
||||||
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> notifyAudioSessionUpdate(bool active) async {
|
Future<void> notifyAudioSessionUpdate(bool active) async {
|
||||||
@ -79,7 +71,7 @@ class CustomPlayer extends Player {
|
|||||||
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
|
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
|
||||||
data: {
|
data: {
|
||||||
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
|
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
|
||||||
"android.media.extra.PACKAGE_NAME": _packageName
|
"android.media.extra.PACKAGE_NAME": _packageName,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -90,6 +82,7 @@ class CustomPlayer extends Player {
|
|||||||
|
|
||||||
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
||||||
Stream<bool> get shuffleStream => _shuffleStream.stream;
|
Stream<bool> get shuffleStream => _shuffleStream.stream;
|
||||||
|
|
||||||
Stream<int> get indexChangeStream {
|
Stream<int> get indexChangeStream {
|
||||||
int oldIndex = state.playlist.index;
|
int oldIndex = state.playlist.index;
|
||||||
return stream.playlist.map((event) => event.index).where((newIndex) {
|
return stream.playlist.map((event) => event.index).where((newIndex) {
|
||||||
@ -106,6 +99,8 @@ class CustomPlayer extends Player {
|
|||||||
_shuffled = shuffle;
|
_shuffled = shuffle;
|
||||||
await super.setShuffle(shuffle);
|
await super.setShuffle(shuffle);
|
||||||
_shuffleStream.add(shuffle);
|
_shuffleStream.add(shuffle);
|
||||||
|
|
||||||
|
// Ensure delay before rearranging playlist
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (shuffle) {
|
if (shuffle) {
|
||||||
await move(state.playlist.index, 0);
|
await move(state.playlist.index, 0);
|
||||||
@ -115,7 +110,6 @@ class CustomPlayer extends Player {
|
|||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
await super.stop();
|
await super.stop();
|
||||||
|
|
||||||
_shuffled = false;
|
_shuffled = false;
|
||||||
_playerStateStream.add(AudioPlaybackState.stopped);
|
_playerStateStream.add(AudioPlaybackState.stopped);
|
||||||
_shuffleStream.add(false);
|
_shuffleStream.add(false);
|
||||||
@ -123,10 +117,10 @@ class CustomPlayer extends Player {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
for (var element in _subscriptions) {
|
await Future.wait(_subscriptions.map((sub) => sub.cancel()));
|
||||||
element.cancel();
|
|
||||||
}
|
|
||||||
await notifyAudioSessionUpdate(false);
|
await notifyAudioSessionUpdate(false);
|
||||||
|
await _playerStateStream.close();
|
||||||
|
await _shuffleStream.close();
|
||||||
return super.dispose();
|
return super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,10 +132,9 @@ class CustomPlayer extends Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setAudioNormalization(bool normalize) async {
|
Future<void> setAudioNormalization(bool normalize) async {
|
||||||
if (normalize) {
|
await nativePlayer.setProperty(
|
||||||
await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5');
|
'af',
|
||||||
} else {
|
normalize ? 'dynaudnorm=g=5:f=250:r=0.9:p=0.5' : ''
|
||||||
await nativePlayer.setProperty('af', '');
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user