import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import 'package:new_learn/core/logger/app_talker.dart'; /// ============================================================ /// 【ReciteAudioManager】背诵模块单例音频管理 /// —— 对标 Vue utils/audioManager /// /// 规则(与 Vue 完全一致): /// 1. 全局同时只播放一个音频:新 play 会先 stop 旧的; /// 2. hasActive 为 true 时页面层直接忽略点击; /// 3. 播放结束/失败/取消都清空 playingToken(驱动 4 帧动画停在第 1 帧); /// 4. 离开答题/小结页调 stopAll() 释放(对标 cleanupAllAudio)。 /// ============================================================ class ReciteAudioManager { ReciteAudioManager._() { _playerSub = _player.playerStateStream.listen((state) { if (state.processingState == ProcessingState.completed) { playingToken.value = null; currentUrl = null; } }); } static final ReciteAudioManager instance = ReciteAudioManager._(); final AudioPlayer _player = AudioPlayer(); StreamSubscription? _playerSub; /// 当前播放令牌(自增),null = 无播放;4 帧按钮据此判断是否该动 final ValueNotifier playingToken = ValueNotifier(null); /// 当前播放的音频 URL,null = 无播放 /// —— VoiceAnimButton 用它判断"当前播的是不是我的音频", /// 比 token 归属更可靠(自动播放 / 手动播放 / 跨组件触发都能联动)。 String? currentUrl; int _seq = 0; bool get hasActive => playingToken.value != null; /// 播放网络音频,返回本次播放令牌(失败返回 null) Future play(String url) async { if (url.isEmpty) return null; final token = ++_seq; try { await _player.stop(); currentUrl = url; playingToken.value = token; await _player.setUrl(url); await _player.play(); return token; } catch (e) { talker.error('背诵音频播放失败: $e'); if (playingToken.value == token) { playingToken.value = null; currentUrl = null; } return null; } } /// 停止并清空播放态(页面 dispose 时必须调) Future stopAll() async { await _player.stop(); playingToken.value = null; currentUrl = null; } void dispose() { _playerSub?.cancel(); _player.dispose(); } }