/* * Copyright (c) 2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { common } from '@kit.AbilityKit'; import { avSession } from '@kit.AVSessionKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { fileIo } from '@kit.CoreFileKit'; import { media } from '@kit.MediaKit'; import { fileUri } from '@kit.CoreFileKit'; import { VideoItem } from '../viewmodel/VideoItem'; const TAG = 'heanup CastController'; /** * 投播控制器回调接口 */ export interface CastControllerCallbacks { onPlayNext?: () => void; onPlayPrevious?: () => void; onPositionChanged?: (position: number) => void; onDurationChanged?: (duration: number) => void; } /** * 投播控制器 - 参考官方 AudioCast-master 示例实现 * 负责管理音频投播的完整生命周期 */ export class CastController { // 投播控制器实例 private avCastController: avSession.AVCastController | undefined = undefined; private context: common.UIAbilityContext | undefined = undefined; // 歌曲列表和当前索引 private songList: VideoItem[] = []; private musicIndex: number = 0; // 播放状态(使用 @Track 让 UI 可观察) @Track state: avSession.PlaybackState = avSession.PlaybackState.PLAYBACK_STATE_INITIAL; @Track elapsedTime: number = 0; @Track duration: number = 0; @Track volume: number = 0; @Track isCastPlaying: boolean = false; // 文件描述符引用,避免被垃圾回收 private castFile: fileIo.File | undefined = undefined; private isSwitchingTrack: boolean = false; private hasHandledEnd: boolean = false; // 回调函数 private onPlayNext: (() => void) | undefined = undefined; private onPlayPrevious: (() => void) | undefined = undefined; private onPositionChanged: ((position: number) => void) | undefined = undefined; private onDurationChanged: ((duration: number) => void) | undefined = undefined; constructor(avCastController: avSession.AVCastController | undefined) { this.avCastController = avCastController; this.context = AppStorage.get('context') as common.UIAbilityContext; } /** * 设置回调函数 */ public setCallbacks(callbacks: CastControllerCallbacks): void { this.onPlayNext = callbacks.onPlayNext; this.onPlayPrevious = callbacks.onPlayPrevious; this.onPositionChanged = callbacks.onPositionChanged; this.onDurationChanged = callbacks.onDurationChanged; } /** * 初始化投播会话 * @param songList 歌曲列表 * @param musicIndex 当前歌曲索引 * @param startPosition 开始播放位置(毫秒) * @param videoUrl 当前歌曲的视频URL(用于流媒体判断) */ public async initAVCast( songList: VideoItem[], musicIndex: number, startPosition: number, videoUrl: string, duration?: number ): Promise { console.info( TAG, '========== initAVCast 开始 =========='); console.info( TAG, `initAVCast 参数: songList.length=${songList.length}, musicIndex=${musicIndex}, startPosition=${startPosition}, videoUrl=${videoUrl}`); this.songList = songList; this.musicIndex = musicIndex; console.info( TAG, '✅ 更新内部状态完成'); try { await this.setCastResource(startPosition, videoUrl, duration); console.info( TAG, '✅ setCastResource 完成'); } catch (error) { let errorMsg = error instanceof Error ? error.message : String(error); console.error( TAG, `❌ setCastResource 失败: ${errorMsg}`); // 不再 throw,而是记录错误 this.isCastPlaying = false; } try { let currentItem = await this.avCastController?.getCurrentItem(); if (currentItem?.description?.duration) { this.duration = currentItem.description.duration; console.info( TAG, `✅ 获取歌曲时长成功: ${this.duration}ms`); } } catch (error) { let errorMsg = error instanceof Error ? error.message : String(error); console.error( TAG, `⚠️ getCurrentItem失败: ${errorMsg}`); } this.setAVCastCallback(); console.info( TAG, '========== initAVCast 完成 =========='); } /** * 设置并准备投播资源 * @param startPosition 开始播放位置(毫秒) * @param videoUrl 视频URL */ public async setCastResource(startPosition: number, videoUrl: string, duration?: number): Promise { console.info( TAG, '========== setCastResource 开始 =========='); if (!this.avCastController || !this.context) { console.error( TAG, '❌ avCastController 或 context 未初始化'); return; } console.info( TAG, '✅ avCastController 和 context 检查通过'); let songItem: VideoItem = this.songList[this.musicIndex]; console.info( TAG, `准备投播歌曲: ${songItem.name}, filePath: ${songItem.filePath}`); console.info( TAG, `videoUrl: ${videoUrl}`); // 关闭之前的文件描述符 if (this.castFile) { try { fileIo.closeSync(this.castFile); console.info( TAG, '✅ 关闭之前的文件描述符'); } catch (error) { hilog.warn(0x0000, TAG, `⚠️ 关闭文件描述符失败: ${error}`); } } let playItem: avSession.AVQueueItem; try { // 判断是否是流媒体 let isStreaming = this.isUrl(videoUrl); console.info( TAG, `是否流媒体: ${isStreaming}`); let description: avSession.AVMediaDescription = { assetId: songItem.filePath, title: songItem.name, subtitle: 'audio', artist: songItem.artist || '', mediaType: 'AUDIO', mediaSize: songItem.videoSize || 0, startPosition: startPosition, duration: duration || 0, }; console.info( TAG, '✅ 创建 AVMediaDescription 成功'); if (isStreaming) { // 流媒体使用 mediaUri description.mediaUri = videoUrl; console.info( TAG, `✅ 使用流媒体地址投播: ${videoUrl}`); } else { // 本地文件使用 fdSrc // let uri = fileUri.getUriFromPath(songItem.filePath); //console.info( TAG, `文件URI: ${uri}`); this.castFile = fileIo.openSync(songItem.filePath, fileIo.OpenMode.READ_ONLY); console.info( TAG, `✅ 打开文件成功, fd: ${this.castFile.fd}`); let fdSrc: media.AVFileDescriptor = { fd: this.castFile.fd }; description.fdSrc = fdSrc; console.info( TAG, `✅ 使用本地文件投播: ${songItem.filePath}, fd: ${this.castFile.fd}`); } playItem = { itemId: this.musicIndex, description: description }; console.info( TAG, '✅ 创建 AVQueueItem 成功'); // 先 prepare 再 start console.info( TAG, '开始调用 prepare...'); await this.avCastController.prepare(playItem); console.info(TAG, '✅ 投播 prepare 成功'); console.info( TAG, '开始调用 start...'); await this.avCastController.start(playItem); console.info( TAG, '✅ 投播 start 成功'); // 设置投播状态为 true this.isCastPlaying = true; this.isSwitchingTrack = false; this.hasHandledEnd = false; this.elapsedTime = startPosition; this.onPositionChanged?.(this.elapsedTime); if (typeof description.duration === 'number' && description.duration > 0) { this.duration = description.duration; this.onDurationChanged?.(this.duration); } console.info(TAG, '✅✅✅ 投播启动成功,isCastPlaying=true ✅✅✅'); } catch (err) { let errorMsg = err instanceof Error ? err.message : String(err); console.error( TAG, `❌❌❌ 投播准备失败: ${errorMsg}`); this.isCastPlaying = false; } console.info(TAG, '========== setCastResource 完成 =========='); } /** * 判断是否是 URL */ private isUrl(str: string): boolean { if (!str) { return false; } if (str.startsWith('http://') || str.startsWith('https://') || str.startsWith('rtmp://') || str.startsWith('rtsp://') || str.startsWith('rtp://') || str.startsWith('mms://')) { return true; } return false; } /** * 设置投播状态变化监听器 */ setAVCastCallback(): void { console.info( TAG, '开始设置投播监听器'); this.unregisterCastListener(); try { // 1. 监听播放状态变化 this.avCastController?.on('playbackStateChange', ['state'], async (playbackState: avSession.AVPlaybackState) => { console.info( TAG, `[state回调] state=${playbackState.state}`); if (playbackState.state) { this.state = playbackState.state; // 更新 isCastPlaying 状态 if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PLAY) { this.isCastPlaying = true; console.info( TAG, '[state回调] 设置 isCastPlaying=true'); } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PAUSE) { console.info( TAG, '[state回调] 暂停状态'); // 暂停时保持 isCastPlaying 不变 } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_STOP) { console.info( TAG, '[state回调] 停止状态,保持 isCastPlaying=' + this.isCastPlaying); // STOP 状态不改变 isCastPlaying,确保播放完成时继续播放下一首 if (this.shouldAutoPlayNextOnStop()) { console.info(TAG, '[state回调] STOP 且接近播放结束,触发下一首'); this.hasHandledEnd = true; this.onPlayNext?.(); } } } }); // 2. 监听位置变化 this.avCastController?.on('playbackStateChange', ['position'], (playbackState: avSession.AVPlaybackState) => { if (playbackState.position && playbackState.position.elapsedTime !== undefined) { this.elapsedTime = playbackState.position.elapsedTime; // 只在每10秒输出一次日志,避免日志过多 if (this.elapsedTime % 10000 < 1000) { console.info( TAG, `[position回调] elapsedTime=${this.elapsedTime}ms`); } // 触发回调 this.onPositionChanged?.(this.elapsedTime); } }); // 3. 监听时长变化 this.avCastController?.on('playbackStateChange', ['extras'], (playbackState: avSession.AVPlaybackState) => { const duration = playbackState?.extras?.duration; if (typeof duration === 'number') { this.duration = duration; console.info( TAG, `[extras回调] duration=${this.duration}ms`); // 触发回调 this.onDurationChanged?.(this.duration); } }); // 4. 监听进度调节完成事件 this.avCastController?.on('seekDone', (position: number) => { this.elapsedTime = position; console.info( TAG, `[seekDone回调] position=${position}ms`); this.onPositionChanged?.(this.elapsedTime); }); // 5. 监听所有状态变化(调试用) this.avCastController?.on('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => { console.info( TAG, `[all回调] 完整状态: ${JSON.stringify(playbackState)}`); }); // 6. 监听播放完成事件 ⭐ 关键 this.avCastController?.on('endOfStream', () => { console.info( TAG, '⭐⭐⭐ [endOfStream回调] 播放完成,触发下一首 ⭐⭐⭐'); console.info( TAG, `[endOfStream回调] 当前 isCastPlaying=${this.isCastPlaying}`); // 确保 isCastPlaying 在播放完成时保持为 true,以便下一首能继续投播 this.isCastPlaying = true; console.info( TAG, `[endOfStream回调] 强制设置 isCastPlaying=true`); this.hasHandledEnd = true; this.onPlayNext?.(); }); this.avCastController?.on('playNext', () => { console.info( TAG, '⭐⭐⭐ [playNext回调] 触发下一首 ⭐⭐⭐'); this.onPlayNext?.(); }); this.avCastController?.on('playPrevious', () => { console.info( TAG, '⭐⭐⭐ [playPrevious回调] 触发上一首 ⭐⭐⭐'); this.onPlayPrevious?.(); }); // 6. 监听错误事件 this.avCastController?.on('error', (error: BusinessError) => { console.error( TAG, `[error回调] 投播错误: code=${error.code}, message=${error.message}`); this.isCastPlaying = false; }); console.info( TAG, '✅ 所有监听器设置完成'); } catch (error) { console.error( TAG, `❌ 设置监听器失败: ${JSON.stringify(error)}`); } } /** * 播放 */ public async setPlaying(): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'play' }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, '发送 play 命令成功'); } catch (error) { console.error( TAG, `play 命令失败: ${JSON.stringify(error)}`); } } /** * 暂停 */ public async setPause(): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'pause' }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, '发送 pause 命令成功'); } catch (error) { console.error( TAG, `pause 命令失败: ${JSON.stringify(error)}`); } } /** * 停止 */ public async setStop(): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'stop' }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, '发送 stop 命令成功'); } catch (error) { console.error( TAG, `stop 命令失败: ${JSON.stringify(error)}`); } } /** * 跳转 */ public async seek(timeMS: number): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'seek', parameter: timeMS }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, `发送 seek 命令成功: ${timeMS}ms`); } catch (error) { console.error( TAG, `seek 命令失败: ${JSON.stringify(error)}`); } } /** * 设置音量 */ public async setVolume(volume: number): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'setVolume', parameter: volume }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, `发送 setVolume 命令成功: ${volume}`); } catch (error) { console.error( TAG, `setVolume 命令失败: ${JSON.stringify(error)}`); } } /** * 设置循环模式 */ public async setLoopMode(mode: number): Promise { try { let avCommand: avSession.AVCastControlCommand = { command: 'setLoopMode', parameter: mode }; await this.avCastController?.sendControlCommand(avCommand); console.info( TAG, `发送 setLoopMode 命令成功: ${mode}`); } catch (error) { console.error( TAG, `setLoopMode 命令失败: ${JSON.stringify(error)}`); } } /** * 播放下一首 * @param songList 最新的歌曲列表 * @param musicIndex 最新的歌曲索引 * @param videoUrl 最新的视频URL */ public async playNext(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise { console.info( TAG, `playNext: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`); // 更新内部状态 this.songList = songList; this.musicIndex = musicIndex; this.isSwitchingTrack = true; // 直接切换资源,避免 stop 导致投播会话被终止 try { await this.setCastResource(0, videoUrl, duration); console.info( TAG, 'playNext: prepare 和 start 完成'); } finally { this.isSwitchingTrack = false; } } /** * 播放上一首 * @param songList 最新的歌曲列表 * @param musicIndex 最新的歌曲索引 * @param videoUrl 最新的视频URL */ public async playPrevious(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise { console.info( TAG, `playPrevious: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`); // 更新内部状态 this.songList = songList; this.musicIndex = musicIndex; this.isSwitchingTrack = true; // 直接切换资源,避免 stop 导致投播会话被终止 try { await this.setCastResource(0, videoUrl, duration); console.info( TAG, 'playPrevious: prepare 和 start 完成'); } finally { this.isSwitchingTrack = false; } } /** * 获取当前索引 */ public getCurrentIndex(): number { return this.musicIndex; } /** * 获取是否正在投播 */ public getIsCastPlaying(): boolean { return this.isCastPlaying; } private shouldAutoPlayNextOnStop(): boolean { if (this.isSwitchingTrack || this.hasHandledEnd) { return false; } if (this.duration <= 0 || this.elapsedTime <= 0) { return false; } return (this.duration - this.elapsedTime) <= 1500; } /** * 释放投播控制器资源 */ public async releaseAVCast(): Promise { try { await this.avCastController?.release(); console.info( TAG, '释放投播控制器成功'); } catch (error) { console.error( TAG, `释放投播控制器失败: ${JSON.stringify(error)}`); } // 关闭文件描述符 if (this.castFile) { try { fileIo.closeSync(this.castFile); this.castFile = undefined; console.info( TAG, '关闭文件描述符成功'); } catch (error) { console.error( TAG, `关闭文件描述符失败: ${error}`); } } this.isCastPlaying = false; this.unregisterCastListener(); } /** * 移除所有监听器 */ public unregisterCastListener(): void { try { this.avCastController?.off('playbackStateChange'); this.avCastController?.off('seekDone'); this.avCastController?.off('playNext'); this.avCastController?.off('playPrevious'); this.avCastController?.off('endOfStream'); this.avCastController?.off('error'); console.info( TAG, '移除监听器成功'); } catch (error) { console.error( TAG, `移除监听器失败: ${JSON.stringify(error)}`); } } }