171 lines
4.8 KiB
Dart
Executable File
171 lines
4.8 KiB
Dart
Executable File
import 'dart:async';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'generated/app_config.dart';
|
|
import 'kryz_audio_handler.dart';
|
|
import 'live_info_business_object.dart';
|
|
|
|
enum StreamingPlaybackStatus { stopped, buffering, playing }
|
|
|
|
class StreamingAudioBusinessObject extends ChangeNotifier {
|
|
StreamingAudioBusinessObject() {
|
|
final initFuture = _audioHandlerInitFuture;
|
|
if (initFuture != null) {
|
|
initFuture.then((handler) {
|
|
if (_isDisposed) {
|
|
return;
|
|
}
|
|
_attachHandler(handler);
|
|
});
|
|
}
|
|
|
|
final handler = _audioHandler;
|
|
if (handler != null) {
|
|
_attachHandler(handler);
|
|
}
|
|
}
|
|
|
|
static AudioHandler? _audioHandler;
|
|
static Future<AudioHandler>? _audioHandlerInitFuture;
|
|
|
|
static Future<void> initializeBackgroundAudio() async {
|
|
if (_audioHandler != null) {
|
|
return;
|
|
}
|
|
|
|
_audioHandlerInitFuture ??= AudioService.init(
|
|
builder: () => KryzAudioHandler(),
|
|
config: const AudioServiceConfig(
|
|
androidNotificationChannelId: 'com.kryzgoflutter.audio.channel',
|
|
androidNotificationChannelName: '${AppConfig.appName} Audio Playback',
|
|
androidNotificationOngoing: true,
|
|
androidStopForegroundOnPause: true,
|
|
),
|
|
).then((handler) {
|
|
_audioHandler = handler;
|
|
return handler;
|
|
}).catchError((error) {
|
|
_audioHandlerInitFuture = null;
|
|
throw error;
|
|
});
|
|
|
|
await _audioHandlerInitFuture;
|
|
}
|
|
|
|
String get streamUrl => KryzAudioHandler.streamUrl;
|
|
|
|
StreamSubscription<PlaybackState>? _playbackStateSubscription;
|
|
StreamSubscription<MediaItem?>? _mediaItemSubscription;
|
|
StreamingPlaybackStatus _playbackStatus = StreamingPlaybackStatus.stopped;
|
|
bool _isDisposed = false;
|
|
bool _isAttached = false;
|
|
String _currentTitle = AppConfig.defaultTitle;
|
|
String _currentSubtitle = AppConfig.defaultArtist;
|
|
|
|
StreamingPlaybackStatus get playbackStatus => _playbackStatus;
|
|
bool get isPlaying => _playbackStatus == StreamingPlaybackStatus.playing;
|
|
bool get isBuffering => _playbackStatus == StreamingPlaybackStatus.buffering;
|
|
bool get isStopped => _playbackStatus == StreamingPlaybackStatus.stopped;
|
|
String get currentTitle => _currentTitle;
|
|
String get currentSubtitle => _currentSubtitle;
|
|
|
|
Future<void> ensureInitialized() async {
|
|
await initializeBackgroundAudio();
|
|
}
|
|
|
|
StreamingPlaybackStatus _mapPlaybackStateToStatus(PlaybackState state) {
|
|
if (state.processingState == AudioProcessingState.completed) {
|
|
return StreamingPlaybackStatus.stopped;
|
|
}
|
|
|
|
if (state.playing) {
|
|
if (state.processingState == AudioProcessingState.ready) {
|
|
return StreamingPlaybackStatus.playing;
|
|
}
|
|
|
|
return StreamingPlaybackStatus.buffering;
|
|
}
|
|
|
|
if (state.processingState == AudioProcessingState.buffering ||
|
|
state.processingState == AudioProcessingState.loading) {
|
|
return StreamingPlaybackStatus.buffering;
|
|
}
|
|
|
|
return StreamingPlaybackStatus.stopped;
|
|
}
|
|
|
|
void _setPlaybackStatus(StreamingPlaybackStatus status) {
|
|
if (_playbackStatus == status) {
|
|
return;
|
|
}
|
|
|
|
_playbackStatus = status;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> play() async {
|
|
_setPlaybackStatus(StreamingPlaybackStatus.buffering);
|
|
final handler = await _requireHandler();
|
|
await handler.play();
|
|
}
|
|
|
|
Future<void> stop() async {
|
|
final handler = await _requireHandler();
|
|
await handler.stop();
|
|
_setPlaybackStatus(StreamingPlaybackStatus.stopped);
|
|
}
|
|
|
|
Future<void> updateMetadataFromLiveInfo(LiveInfoSnapshot? snapshot) async {
|
|
final handler = await _requireHandler();
|
|
await handler.updateMetadataFromLiveInfo(snapshot);
|
|
}
|
|
|
|
Future<KryzAudioHandler> _requireHandler() async {
|
|
await initializeBackgroundAudio();
|
|
final handler = _audioHandler;
|
|
if (handler is! KryzAudioHandler) {
|
|
throw StateError('KryzAudioHandler is not initialized');
|
|
}
|
|
_attachHandler(handler);
|
|
return handler;
|
|
}
|
|
|
|
void _attachHandler(AudioHandler handler) {
|
|
if (_isAttached) {
|
|
return;
|
|
}
|
|
|
|
_isAttached = true;
|
|
_playbackStateSubscription = handler.playbackState.listen((state) {
|
|
_setPlaybackStatus(_mapPlaybackStateToStatus(state));
|
|
});
|
|
_mediaItemSubscription = handler.mediaItem.listen((item) {
|
|
if (item == null) {
|
|
return;
|
|
}
|
|
|
|
final title = item.title.trim().isEmpty ? AppConfig.defaultTitle : item.title;
|
|
final subtitle =
|
|
(item.artist ?? '').trim().isEmpty ? AppConfig.defaultArtist : item.artist!.trim();
|
|
|
|
if (title == _currentTitle && subtitle == _currentSubtitle) {
|
|
return;
|
|
}
|
|
|
|
_currentTitle = title;
|
|
_currentSubtitle = subtitle;
|
|
notifyListeners();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_isDisposed = true;
|
|
_playbackStateSubscription?.cancel();
|
|
_mediaItemSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|