import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/foundation.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? _audioHandlerInitFuture; static Future initializeBackgroundAudio() async { if (_audioHandler != null) { return; } _audioHandlerInitFuture ??= AudioService.init( builder: () => KryzAudioHandler(), config: const AudioServiceConfig( androidNotificationChannelId: 'com.kryzgoflutter.audio.channel', androidNotificationChannelName: 'KRYZ 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? _playbackStateSubscription; StreamSubscription? _mediaItemSubscription; StreamingPlaybackStatus _playbackStatus = StreamingPlaybackStatus.stopped; bool _isDisposed = false; bool _isAttached = false; String _currentTitle = 'KRYZ Live Stream'; String _currentSubtitle = 'KRYZ Radio'; 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 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 play() async { _setPlaybackStatus(StreamingPlaybackStatus.buffering); final handler = await _requireHandler(); await handler.play(); } Future stop() async { final handler = await _requireHandler(); await handler.stop(); _setPlaybackStatus(StreamingPlaybackStatus.stopped); } Future updateMetadataFromLiveInfo(LiveInfoSnapshot? snapshot) async { final handler = await _requireHandler(); await handler.updateMetadataFromLiveInfo(snapshot); } Future _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 ? 'KRYZ Live Stream' : item.title; final subtitle = (item.artist ?? '').trim().isEmpty ? 'KRYZ Radio' : 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(); } }