| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- /*
- * 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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)}`);
- }
- }
- }
|