CastController.ets 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /*
  2. * Copyright (c) 2025 Huawei Device Co., Ltd.
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import { common } from '@kit.AbilityKit';
  16. import { avSession } from '@kit.AVSessionKit';
  17. import { BusinessError } from '@kit.BasicServicesKit';
  18. import { hilog } from '@kit.PerformanceAnalysisKit';
  19. import { fileIo } from '@kit.CoreFileKit';
  20. import { media } from '@kit.MediaKit';
  21. import { fileUri } from '@kit.CoreFileKit';
  22. import { VideoItem } from '../viewmodel/VideoItem';
  23. const TAG = 'heanup CastController';
  24. /**
  25. * 投播控制器回调接口
  26. */
  27. export interface CastControllerCallbacks {
  28. onPlayNext?: () => void;
  29. onPlayPrevious?: () => void;
  30. onPositionChanged?: (position: number) => void;
  31. onDurationChanged?: (duration: number) => void;
  32. }
  33. /**
  34. * 投播控制器 - 参考官方 AudioCast-master 示例实现
  35. * 负责管理音频投播的完整生命周期
  36. */
  37. export class CastController {
  38. // 投播控制器实例
  39. private avCastController: avSession.AVCastController | undefined = undefined;
  40. private context: common.UIAbilityContext | undefined = undefined;
  41. // 歌曲列表和当前索引
  42. private songList: VideoItem[] = [];
  43. private musicIndex: number = 0;
  44. // 播放状态(使用 @Track 让 UI 可观察)
  45. @Track state: avSession.PlaybackState = avSession.PlaybackState.PLAYBACK_STATE_INITIAL;
  46. @Track elapsedTime: number = 0;
  47. @Track duration: number = 0;
  48. @Track volume: number = 0;
  49. @Track isCastPlaying: boolean = false;
  50. // 文件描述符引用,避免被垃圾回收
  51. private castFile: fileIo.File | undefined = undefined;
  52. private isSwitchingTrack: boolean = false;
  53. private hasHandledEnd: boolean = false;
  54. // 回调函数
  55. private onPlayNext: (() => void) | undefined = undefined;
  56. private onPlayPrevious: (() => void) | undefined = undefined;
  57. private onPositionChanged: ((position: number) => void) | undefined = undefined;
  58. private onDurationChanged: ((duration: number) => void) | undefined = undefined;
  59. constructor(avCastController: avSession.AVCastController | undefined) {
  60. this.avCastController = avCastController;
  61. this.context = AppStorage.get('context') as common.UIAbilityContext;
  62. }
  63. /**
  64. * 设置回调函数
  65. */
  66. public setCallbacks(callbacks: CastControllerCallbacks): void {
  67. this.onPlayNext = callbacks.onPlayNext;
  68. this.onPlayPrevious = callbacks.onPlayPrevious;
  69. this.onPositionChanged = callbacks.onPositionChanged;
  70. this.onDurationChanged = callbacks.onDurationChanged;
  71. }
  72. /**
  73. * 初始化投播会话
  74. * @param songList 歌曲列表
  75. * @param musicIndex 当前歌曲索引
  76. * @param startPosition 开始播放位置(毫秒)
  77. * @param videoUrl 当前歌曲的视频URL(用于流媒体判断)
  78. */
  79. public async initAVCast(
  80. songList: VideoItem[],
  81. musicIndex: number,
  82. startPosition: number,
  83. videoUrl: string,
  84. duration?: number
  85. ): Promise<void> {
  86. console.info( TAG, '========== initAVCast 开始 ==========');
  87. console.info( TAG, `initAVCast 参数: songList.length=${songList.length}, musicIndex=${musicIndex}, startPosition=${startPosition}, videoUrl=${videoUrl}`);
  88. this.songList = songList;
  89. this.musicIndex = musicIndex;
  90. console.info( TAG, '✅ 更新内部状态完成');
  91. try {
  92. await this.setCastResource(startPosition, videoUrl, duration);
  93. console.info( TAG, '✅ setCastResource 完成');
  94. } catch (error) {
  95. let errorMsg = error instanceof Error ? error.message : String(error);
  96. console.error( TAG, `❌ setCastResource 失败: ${errorMsg}`);
  97. // 不再 throw,而是记录错误
  98. this.isCastPlaying = false;
  99. }
  100. try {
  101. let currentItem = await this.avCastController?.getCurrentItem();
  102. if (currentItem?.description?.duration) {
  103. this.duration = currentItem.description.duration;
  104. console.info( TAG, `✅ 获取歌曲时长成功: ${this.duration}ms`);
  105. }
  106. } catch (error) {
  107. let errorMsg = error instanceof Error ? error.message : String(error);
  108. console.error( TAG, `⚠️ getCurrentItem失败: ${errorMsg}`);
  109. }
  110. this.setAVCastCallback();
  111. console.info( TAG, '========== initAVCast 完成 ==========');
  112. }
  113. /**
  114. * 设置并准备投播资源
  115. * @param startPosition 开始播放位置(毫秒)
  116. * @param videoUrl 视频URL
  117. */
  118. public async setCastResource(startPosition: number, videoUrl: string, duration?: number): Promise<void> {
  119. console.info( TAG, '========== setCastResource 开始 ==========');
  120. if (!this.avCastController || !this.context) {
  121. console.error( TAG, '❌ avCastController 或 context 未初始化');
  122. return;
  123. }
  124. console.info( TAG, '✅ avCastController 和 context 检查通过');
  125. let songItem: VideoItem = this.songList[this.musicIndex];
  126. console.info( TAG, `准备投播歌曲: ${songItem.name}, filePath: ${songItem.filePath}`);
  127. console.info( TAG, `videoUrl: ${videoUrl}`);
  128. // 关闭之前的文件描述符
  129. if (this.castFile) {
  130. try {
  131. fileIo.closeSync(this.castFile);
  132. console.info( TAG, '✅ 关闭之前的文件描述符');
  133. } catch (error) {
  134. hilog.warn(0x0000, TAG, `⚠️ 关闭文件描述符失败: ${error}`);
  135. }
  136. }
  137. let playItem: avSession.AVQueueItem;
  138. try {
  139. // 判断是否是流媒体
  140. let isStreaming = this.isUrl(videoUrl);
  141. console.info( TAG, `是否流媒体: ${isStreaming}`);
  142. let description: avSession.AVMediaDescription = {
  143. assetId: songItem.filePath,
  144. title: songItem.name,
  145. subtitle: 'audio',
  146. artist: songItem.artist || '',
  147. mediaType: 'AUDIO',
  148. mediaSize: songItem.videoSize || 0,
  149. startPosition: startPosition,
  150. duration: duration || 0,
  151. };
  152. console.info( TAG, '✅ 创建 AVMediaDescription 成功');
  153. if (isStreaming) {
  154. // 流媒体使用 mediaUri
  155. description.mediaUri = videoUrl;
  156. console.info( TAG, `✅ 使用流媒体地址投播: ${videoUrl}`);
  157. } else {
  158. // 本地文件使用 fdSrc
  159. // let uri = fileUri.getUriFromPath(songItem.filePath);
  160. //console.info( TAG, `文件URI: ${uri}`);
  161. this.castFile = fileIo.openSync(songItem.filePath, fileIo.OpenMode.READ_ONLY);
  162. console.info( TAG, `✅ 打开文件成功, fd: ${this.castFile.fd}`);
  163. let fdSrc: media.AVFileDescriptor = { fd: this.castFile.fd };
  164. description.fdSrc = fdSrc;
  165. console.info( TAG, `✅ 使用本地文件投播: ${songItem.filePath}, fd: ${this.castFile.fd}`);
  166. }
  167. playItem = {
  168. itemId: this.musicIndex,
  169. description: description
  170. };
  171. console.info( TAG, '✅ 创建 AVQueueItem 成功');
  172. // 先 prepare 再 start
  173. console.info( TAG, '开始调用 prepare...');
  174. await this.avCastController.prepare(playItem);
  175. console.info(TAG, '✅ 投播 prepare 成功');
  176. console.info( TAG, '开始调用 start...');
  177. await this.avCastController.start(playItem);
  178. console.info( TAG, '✅ 投播 start 成功');
  179. // 设置投播状态为 true
  180. this.isCastPlaying = true;
  181. this.isSwitchingTrack = false;
  182. this.hasHandledEnd = false;
  183. this.elapsedTime = startPosition;
  184. this.onPositionChanged?.(this.elapsedTime);
  185. if (typeof description.duration === 'number' && description.duration > 0) {
  186. this.duration = description.duration;
  187. this.onDurationChanged?.(this.duration);
  188. }
  189. console.info(TAG, '✅✅✅ 投播启动成功,isCastPlaying=true ✅✅✅');
  190. } catch (err) {
  191. let errorMsg = err instanceof Error ? err.message : String(err);
  192. console.error( TAG, `❌❌❌ 投播准备失败: ${errorMsg}`);
  193. this.isCastPlaying = false;
  194. }
  195. console.info(TAG, '========== setCastResource 完成 ==========');
  196. }
  197. /**
  198. * 判断是否是 URL
  199. */
  200. private isUrl(str: string): boolean {
  201. if (!str) {
  202. return false;
  203. }
  204. if (str.startsWith('http://') || str.startsWith('https://') ||
  205. str.startsWith('rtmp://') || str.startsWith('rtsp://') ||
  206. str.startsWith('rtp://') || str.startsWith('mms://')) {
  207. return true;
  208. }
  209. return false;
  210. }
  211. /**
  212. * 设置投播状态变化监听器
  213. */
  214. setAVCastCallback(): void {
  215. console.info( TAG, '开始设置投播监听器');
  216. this.unregisterCastListener();
  217. try {
  218. // 1. 监听播放状态变化
  219. this.avCastController?.on('playbackStateChange', ['state'], async (playbackState: avSession.AVPlaybackState) => {
  220. console.info( TAG, `[state回调] state=${playbackState.state}`);
  221. if (playbackState.state) {
  222. this.state = playbackState.state;
  223. // 更新 isCastPlaying 状态
  224. if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PLAY) {
  225. this.isCastPlaying = true;
  226. console.info( TAG, '[state回调] 设置 isCastPlaying=true');
  227. } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PAUSE) {
  228. console.info( TAG, '[state回调] 暂停状态');
  229. // 暂停时保持 isCastPlaying 不变
  230. } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_STOP) {
  231. console.info( TAG, '[state回调] 停止状态,保持 isCastPlaying=' + this.isCastPlaying);
  232. // STOP 状态不改变 isCastPlaying,确保播放完成时继续播放下一首
  233. if (this.shouldAutoPlayNextOnStop()) {
  234. console.info(TAG, '[state回调] STOP 且接近播放结束,触发下一首');
  235. this.hasHandledEnd = true;
  236. this.onPlayNext?.();
  237. }
  238. }
  239. }
  240. });
  241. // 2. 监听位置变化
  242. this.avCastController?.on('playbackStateChange', ['position'], (playbackState: avSession.AVPlaybackState) => {
  243. if (playbackState.position && playbackState.position.elapsedTime !== undefined) {
  244. this.elapsedTime = playbackState.position.elapsedTime;
  245. // 只在每10秒输出一次日志,避免日志过多
  246. if (this.elapsedTime % 10000 < 1000) {
  247. console.info( TAG, `[position回调] elapsedTime=${this.elapsedTime}ms`);
  248. }
  249. // 触发回调
  250. this.onPositionChanged?.(this.elapsedTime);
  251. }
  252. });
  253. // 3. 监听时长变化
  254. this.avCastController?.on('playbackStateChange', ['extras'], (playbackState: avSession.AVPlaybackState) => {
  255. const duration = playbackState?.extras?.duration;
  256. if (typeof duration === 'number') {
  257. this.duration = duration;
  258. console.info( TAG, `[extras回调] duration=${this.duration}ms`);
  259. // 触发回调
  260. this.onDurationChanged?.(this.duration);
  261. }
  262. });
  263. // 4. 监听进度调节完成事件
  264. this.avCastController?.on('seekDone', (position: number) => {
  265. this.elapsedTime = position;
  266. console.info( TAG, `[seekDone回调] position=${position}ms`);
  267. this.onPositionChanged?.(this.elapsedTime);
  268. });
  269. // 5. 监听所有状态变化(调试用)
  270. this.avCastController?.on('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => {
  271. console.info( TAG, `[all回调] 完整状态: ${JSON.stringify(playbackState)}`);
  272. });
  273. // 6. 监听播放完成事件 ⭐ 关键
  274. this.avCastController?.on('endOfStream', () => {
  275. console.info( TAG, '⭐⭐⭐ [endOfStream回调] 播放完成,触发下一首 ⭐⭐⭐');
  276. console.info( TAG, `[endOfStream回调] 当前 isCastPlaying=${this.isCastPlaying}`);
  277. // 确保 isCastPlaying 在播放完成时保持为 true,以便下一首能继续投播
  278. this.isCastPlaying = true;
  279. console.info( TAG, `[endOfStream回调] 强制设置 isCastPlaying=true`);
  280. this.hasHandledEnd = true;
  281. this.onPlayNext?.();
  282. });
  283. this.avCastController?.on('playNext', () => {
  284. console.info( TAG, '⭐⭐⭐ [playNext回调] 触发下一首 ⭐⭐⭐');
  285. this.onPlayNext?.();
  286. });
  287. this.avCastController?.on('playPrevious', () => {
  288. console.info( TAG, '⭐⭐⭐ [playPrevious回调] 触发上一首 ⭐⭐⭐');
  289. this.onPlayPrevious?.();
  290. });
  291. // 6. 监听错误事件
  292. this.avCastController?.on('error', (error: BusinessError) => {
  293. console.error( TAG, `[error回调] 投播错误: code=${error.code}, message=${error.message}`);
  294. this.isCastPlaying = false;
  295. });
  296. console.info( TAG, '✅ 所有监听器设置完成');
  297. } catch (error) {
  298. console.error( TAG, `❌ 设置监听器失败: ${JSON.stringify(error)}`);
  299. }
  300. }
  301. /**
  302. * 播放
  303. */
  304. public async setPlaying(): Promise<void> {
  305. try {
  306. let avCommand: avSession.AVCastControlCommand = { command: 'play' };
  307. await this.avCastController?.sendControlCommand(avCommand);
  308. console.info( TAG, '发送 play 命令成功');
  309. } catch (error) {
  310. console.error( TAG, `play 命令失败: ${JSON.stringify(error)}`);
  311. }
  312. }
  313. /**
  314. * 暂停
  315. */
  316. public async setPause(): Promise<void> {
  317. try {
  318. let avCommand: avSession.AVCastControlCommand = { command: 'pause' };
  319. await this.avCastController?.sendControlCommand(avCommand);
  320. console.info( TAG, '发送 pause 命令成功');
  321. } catch (error) {
  322. console.error( TAG, `pause 命令失败: ${JSON.stringify(error)}`);
  323. }
  324. }
  325. /**
  326. * 停止
  327. */
  328. public async setStop(): Promise<void> {
  329. try {
  330. let avCommand: avSession.AVCastControlCommand = { command: 'stop' };
  331. await this.avCastController?.sendControlCommand(avCommand);
  332. console.info( TAG, '发送 stop 命令成功');
  333. } catch (error) {
  334. console.error( TAG, `stop 命令失败: ${JSON.stringify(error)}`);
  335. }
  336. }
  337. /**
  338. * 跳转
  339. */
  340. public async seek(timeMS: number): Promise<void> {
  341. try {
  342. let avCommand: avSession.AVCastControlCommand = { command: 'seek', parameter: timeMS };
  343. await this.avCastController?.sendControlCommand(avCommand);
  344. console.info( TAG, `发送 seek 命令成功: ${timeMS}ms`);
  345. } catch (error) {
  346. console.error( TAG, `seek 命令失败: ${JSON.stringify(error)}`);
  347. }
  348. }
  349. /**
  350. * 设置音量
  351. */
  352. public async setVolume(volume: number): Promise<void> {
  353. try {
  354. let avCommand: avSession.AVCastControlCommand = { command: 'setVolume', parameter: volume };
  355. await this.avCastController?.sendControlCommand(avCommand);
  356. console.info( TAG, `发送 setVolume 命令成功: ${volume}`);
  357. } catch (error) {
  358. console.error( TAG, `setVolume 命令失败: ${JSON.stringify(error)}`);
  359. }
  360. }
  361. /**
  362. * 设置循环模式
  363. */
  364. public async setLoopMode(mode: number): Promise<void> {
  365. try {
  366. let avCommand: avSession.AVCastControlCommand = { command: 'setLoopMode', parameter: mode };
  367. await this.avCastController?.sendControlCommand(avCommand);
  368. console.info( TAG, `发送 setLoopMode 命令成功: ${mode}`);
  369. } catch (error) {
  370. console.error( TAG, `setLoopMode 命令失败: ${JSON.stringify(error)}`);
  371. }
  372. }
  373. /**
  374. * 播放下一首
  375. * @param songList 最新的歌曲列表
  376. * @param musicIndex 最新的歌曲索引
  377. * @param videoUrl 最新的视频URL
  378. */
  379. public async playNext(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise<void> {
  380. console.info( TAG, `playNext: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
  381. // 更新内部状态
  382. this.songList = songList;
  383. this.musicIndex = musicIndex;
  384. this.isSwitchingTrack = true;
  385. // 直接切换资源,避免 stop 导致投播会话被终止
  386. try {
  387. await this.setCastResource(0, videoUrl, duration);
  388. console.info( TAG, 'playNext: prepare 和 start 完成');
  389. } finally {
  390. this.isSwitchingTrack = false;
  391. }
  392. }
  393. /**
  394. * 播放上一首
  395. * @param songList 最新的歌曲列表
  396. * @param musicIndex 最新的歌曲索引
  397. * @param videoUrl 最新的视频URL
  398. */
  399. public async playPrevious(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise<void> {
  400. console.info( TAG, `playPrevious: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
  401. // 更新内部状态
  402. this.songList = songList;
  403. this.musicIndex = musicIndex;
  404. this.isSwitchingTrack = true;
  405. // 直接切换资源,避免 stop 导致投播会话被终止
  406. try {
  407. await this.setCastResource(0, videoUrl, duration);
  408. console.info( TAG, 'playPrevious: prepare 和 start 完成');
  409. } finally {
  410. this.isSwitchingTrack = false;
  411. }
  412. }
  413. /**
  414. * 获取当前索引
  415. */
  416. public getCurrentIndex(): number {
  417. return this.musicIndex;
  418. }
  419. /**
  420. * 获取是否正在投播
  421. */
  422. public getIsCastPlaying(): boolean {
  423. return this.isCastPlaying;
  424. }
  425. private shouldAutoPlayNextOnStop(): boolean {
  426. if (this.isSwitchingTrack || this.hasHandledEnd) {
  427. return false;
  428. }
  429. if (this.duration <= 0 || this.elapsedTime <= 0) {
  430. return false;
  431. }
  432. return (this.duration - this.elapsedTime) <= 1500;
  433. }
  434. /**
  435. * 释放投播控制器资源
  436. */
  437. public async releaseAVCast(): Promise<void> {
  438. try {
  439. await this.avCastController?.release();
  440. console.info( TAG, '释放投播控制器成功');
  441. } catch (error) {
  442. console.error( TAG, `释放投播控制器失败: ${JSON.stringify(error)}`);
  443. }
  444. // 关闭文件描述符
  445. if (this.castFile) {
  446. try {
  447. fileIo.closeSync(this.castFile);
  448. this.castFile = undefined;
  449. console.info( TAG, '关闭文件描述符成功');
  450. } catch (error) {
  451. console.error( TAG, `关闭文件描述符失败: ${error}`);
  452. }
  453. }
  454. this.isCastPlaying = false;
  455. this.unregisterCastListener();
  456. }
  457. /**
  458. * 移除所有监听器
  459. */
  460. public unregisterCastListener(): void {
  461. try {
  462. this.avCastController?.off('playbackStateChange');
  463. this.avCastController?.off('seekDone');
  464. this.avCastController?.off('playNext');
  465. this.avCastController?.off('playPrevious');
  466. this.avCastController?.off('endOfStream');
  467. this.avCastController?.off('error');
  468. console.info( TAG, '移除监听器成功');
  469. } catch (error) {
  470. console.error( TAG, `移除监听器失败: ${JSON.stringify(error)}`);
  471. }
  472. }
  473. }