Files
IKAR/lib/core/media/audio_playback_controller.dart
T

157 lines
5.0 KiB
Dart

import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter/foundation.dart';
import '../calls/active_call.dart';
import 'audio_file_track.dart';
import 'background_audio_handler.dart';
import 'desktop_audio_backend.dart';
// #***! плеер музыки с управлением из шторки
class AudioPlaybackController {
AudioPlaybackController._(this._handler) {
_subscriptions.add(
_handler.playbackState.listen((state) {
playing.value = state.playing;
processingState.value = state.processingState;
bufferedPosition.value = state.bufferedPosition;
}),
);
_subscriptions.add(
_handler.mediaItem.listen((item) {
duration.value = item?.duration ?? Duration.zero;
}),
);
_subscriptions.add(
_handler.positionStream.listen((value) => position.value = value),
);
_subscriptions.add(
_handler.bufferedPositionStream.listen(
(value) => bufferedPosition.value = value,
),
);
_subscriptions.add(
_handler.errors.stream.listen((value) => error.value = value),
);
ActiveCall.instance.current.addListener(_onActiveCallChanged);
}
// #***! синглтон с ленивой инициализацией, audio_service поднимать дорого
static AudioPlaybackController? _instance;
static Future<AudioPlaybackController>? _pending;
static AudioPlaybackController get instance {
final value = _instance;
if (value == null) throw StateError('Audio playback is not initialized');
return value;
}
static bool get isInitialized => _instance != null;
static final ValueNotifier<String?> error = ValueNotifier(null);
static Future<AudioPlaybackController> ensureInitialized(
String notificationChannelName,
) async {
final ready = _instance;
if (ready != null) return ready;
final pending = _pending ??= _create(notificationChannelName);
try {
return await pending;
} catch (_) {
_pending = null;
rethrow;
}
}
static Future<AudioPlaybackController> _create(String channelName) async {
DesktopAudioBackend.ensureRegistered();
final handler = switch (defaultTargetPlatform) {
TargetPlatform.windows ||
TargetPlatform.linux => BackgroundAudioHandler(),
_ => await AudioService.init(
builder: BackgroundAudioHandler.new,
config: AudioServiceConfig(
androidNotificationChannelId: 'qlyra.audio.playback',
androidNotificationChannelName: channelName,
androidNotificationOngoing: false,
androidStopForegroundOnPause: true,
),
),
};
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.music());
return _instance = AudioPlaybackController._(handler);
}
final BackgroundAudioHandler _handler;
final List<StreamSubscription<dynamic>> _subscriptions = [];
bool _pausedByCall = false;
// #***! состояние отдельными notifier, плашки плеера подписаны
final ValueNotifier<bool> playing = ValueNotifier(false);
final ValueNotifier<Duration> position = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> bufferedPosition = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> duration = ValueNotifier(Duration.zero);
final ValueNotifier<AudioProcessingState> processingState = ValueNotifier(
AudioProcessingState.idle,
);
// #***! запуск трека
Future<void> playTrack(AudioFileTrack track) async {
error.value = null;
_pausedByCall = false;
position.value = Duration.zero;
bufferedPosition.value = Duration.zero;
duration.value = Duration.zero;
await _handler.load(track);
await _handler.play();
}
Future<void> toggle() {
_pausedByCall = false;
return playing.value ? _handler.pause() : _handler.play();
}
Future<void> seek(Duration value) => _handler.seek(value);
Future<void> stop() {
_pausedByCall = false;
return _handler.stop();
}
Future<void> dispose() async {
ActiveCall.instance.current.removeListener(_onActiveCallChanged);
for (final subscription in _subscriptions) {
await subscription.cancel();
}
_subscriptions.clear();
await _handler.dispose();
playing.dispose();
position.dispose();
bufferedPosition.dispose();
duration.dispose();
processingState.dispose();
if (identical(_instance, this)) {
_instance = null;
_pending = null;
}
}
// #***! на звонок ставим на паузу потом возвращаем
void _onActiveCallChanged() {
if (ActiveCall.instance.current.value != null) {
if (!playing.value) return;
_pausedByCall = true;
unawaited(_handler.pause());
return;
}
if (!_pausedByCall) return;
_pausedByCall = false;
if (processingState.value == AudioProcessingState.idle) return;
unawaited(_handler.play());
}
}