import 'dart:async'; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:audio_session/audio_session.dart'; import 'package:rhythm_box/platform.dart'; import 'package:rhythm_box/providers/error_notifier.dart'; // ignore: implementation_imports import 'package:rhythm_box/services/audio_player/playback_state.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 { final StreamController _playerStateStream; final StreamController _shuffleStream; late final List _subscriptions; bool _shuffled; int _androidAudioSessionId = 0; String _packageName = ''; AndroidAudioManager? _androidAudioManager; CustomPlayer({super.configuration}) : _playerStateStream = StreamController.broadcast(), _shuffleStream = StreamController.broadcast(), _shuffled = false { nativePlayer.setProperty('network-timeout', '120'); _subscriptions = [ stream.buffering.listen((event) { _playerStateStream.add(AudioPlaybackState.buffering); }), stream.playing.listen((playing) { if (playing) { _playerStateStream.add(AudioPlaybackState.playing); } else { _playerStateStream.add(AudioPlaybackState.paused); } }), stream.completed.listen((isCompleted) async { if (!isCompleted) return; _playerStateStream.add(AudioPlaybackState.completed); }), stream.playlist.listen((event) { if (event.medias.isEmpty) { _playerStateStream.add(AudioPlaybackState.stopped); } }), stream.error.listen((event) { Get.find().logError('[Playback] Error: $event'); }), ]; PackageInfo.fromPlatform().then((packageInfo) { _packageName = packageInfo.packageName; }); if (PlatformInfo.isAndroid) { _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 notifyAudioSessionUpdate(bool active) async { if (PlatformInfo.isAndroid) { sendBroadcast( BroadcastMessage( name: active ? 'android.media.action.OPEN_AUDIO_EFFECT_CONTROL_SESSION' : 'android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION', data: { 'android.media.extra.AUDIO_SESSION': _androidAudioSessionId, 'android.media.extra.PACKAGE_NAME': _packageName }, ), ); } } bool get shuffled => _shuffled; Stream get playerStateStream => _playerStateStream.stream; Stream get shuffleStream => _shuffleStream.stream; Stream get indexChangeStream { int oldIndex = state.playlist.index; return stream.playlist.map((event) => event.index).where((newIndex) { if (newIndex != oldIndex) { oldIndex = newIndex; return true; } return false; }); } @override Future setShuffle(bool shuffle) async { _shuffled = shuffle; await super.setShuffle(shuffle); _shuffleStream.add(shuffle); await Future.delayed(const Duration(milliseconds: 100)); if (shuffle) { await move(state.playlist.index, 0); } } @override Future stop() async { await super.stop(); _shuffled = false; _playerStateStream.add(AudioPlaybackState.stopped); _shuffleStream.add(false); } @override Future dispose() async { for (var element in _subscriptions) { element.cancel(); } await notifyAudioSessionUpdate(false); return super.dispose(); } NativePlayer get nativePlayer => platform as NativePlayer; Future insert(int index, Media media) async { await add(media); await move(state.playlist.medias.length, index); } Future setAudioNormalization(bool normalize) async { if (normalize) { await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5'); } else { await nativePlayer.setProperty('af', ''); } } }