CastController.ets 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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. import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
  24. import { RemoteCacheManager } from '../common/network/RemoteCacheManager';
  25. import { RemoteCacheType, resolveCacheFilePath } from '../common/network/RemoteSongCache';
  26. import { extractFtpRelativePath, extractSmbRelativePath, isFtpType, isSmbType } from '../common/util/RemotePlayerUtil';
  27. import FileManager from '../common/util/FileManager';
  28. const TAG = 'heanup CastController';
  29. /**
  30. * 投播控制器回调接口
  31. */
  32. export interface CastControllerCallbacks {
  33. onPlayNext?: () => void;
  34. onPlayPrevious?: () => void;
  35. onPositionChanged?: (position: number) => void;
  36. onDurationChanged?: (duration: number) => void;
  37. }
  38. /**
  39. * 投播控制器 - 参考官方 AudioCast-master 示例实现
  40. * 负责管理音频投播的完整生命周期
  41. */
  42. export class CastController {
  43. // 投播控制器实例
  44. private avCastController: avSession.AVCastController | undefined = undefined;
  45. private context: common.UIAbilityContext | undefined = undefined;
  46. // 歌曲列表和当前索引
  47. private songList: VideoItem[] = [];
  48. private musicIndex: number = 0;
  49. // 播放状态(使用 @Track 让 UI 可观察)
  50. @Track state: avSession.PlaybackState = avSession.PlaybackState.PLAYBACK_STATE_INITIAL;
  51. @Track elapsedTime: number = 0;
  52. @Track duration: number = 0;
  53. @Track volume: number = 0;
  54. @Track isCastPlaying: boolean = false;
  55. // 文件描述符引用,避免被垃圾回收
  56. private castFile: fileIo.File | undefined = undefined;
  57. private isSwitchingTrack: boolean = false;
  58. private hasHandledEnd: boolean = false;
  59. // 回调函数
  60. private onPlayNext: (() => void) | undefined = undefined;
  61. private onPlayPrevious: (() => void) | undefined = undefined;
  62. private onPositionChanged: ((position: number) => void) | undefined = undefined;
  63. private onDurationChanged: ((duration: number) => void) | undefined = undefined;
  64. constructor(avCastController: avSession.AVCastController | undefined) {
  65. this.avCastController = avCastController;
  66. this.context = AppStorage.get('context') as common.UIAbilityContext;
  67. }
  68. /**
  69. * 设置回调函数
  70. */
  71. public setCallbacks(callbacks: CastControllerCallbacks): void {
  72. this.onPlayNext = callbacks.onPlayNext;
  73. this.onPlayPrevious = callbacks.onPlayPrevious;
  74. this.onPositionChanged = callbacks.onPositionChanged;
  75. this.onDurationChanged = callbacks.onDurationChanged;
  76. }
  77. /**
  78. * 初始化投播会话
  79. * @param songList 歌曲列表
  80. * @param musicIndex 当前歌曲索引
  81. * @param startPosition 开始播放位置(毫秒)
  82. * @param videoUrl 当前歌曲的视频URL(用于流媒体判断)
  83. */
  84. public async initAVCast(
  85. songList: VideoItem[],
  86. musicIndex: number,
  87. startPosition: number,
  88. videoUrl: string,
  89. duration?: number
  90. ): Promise<void> {
  91. console.info( TAG, '========== initAVCast 开始 ==========');
  92. console.info( TAG, `initAVCast 参数: songList.length=${songList.length}, musicIndex=${musicIndex}, startPosition=${startPosition}, videoUrl=${videoUrl}`);
  93. this.songList = songList;
  94. this.musicIndex = musicIndex;
  95. console.info( TAG, '✅ 更新内部状态完成');
  96. try {
  97. await this.setCastResource(startPosition, videoUrl, duration);
  98. console.info( TAG, '✅ setCastResource 完成');
  99. } catch (error) {
  100. let errorMsg = error instanceof Error ? error.message : String(error);
  101. console.error( TAG, `❌ setCastResource 失败: ${errorMsg}`);
  102. // 不再 throw,而是记录错误
  103. this.isCastPlaying = false;
  104. }
  105. try {
  106. let currentItem = await this.avCastController?.getCurrentItem();
  107. if (currentItem?.description?.duration) {
  108. this.duration = currentItem.description.duration;
  109. console.info( TAG, `✅ 获取歌曲时长成功: ${this.duration}ms`);
  110. }
  111. } catch (error) {
  112. let errorMsg = error instanceof Error ? error.message : String(error);
  113. console.error( TAG, `⚠️ getCurrentItem失败: ${errorMsg}`);
  114. }
  115. this.setAVCastCallback();
  116. console.info( TAG, '========== initAVCast 完成 ==========');
  117. }
  118. /**
  119. * 设置并准备投播资源
  120. * @param startPosition 开始播放位置(毫秒)
  121. * @param videoUrl 视频URL
  122. */
  123. public async setCastResource(startPosition: number, videoUrl: string, duration?: number): Promise<void> {
  124. console.info( TAG, '========== setCastResource 开始 ==========');
  125. if (!this.avCastController || !this.context) {
  126. console.error( TAG, '❌ avCastController 或 context 未初始化');
  127. return;
  128. }
  129. console.info( TAG, '✅ avCastController 和 context 检查通过');
  130. let songItem: VideoItem = this.songList[this.musicIndex];
  131. console.info( TAG, `准备投播歌曲: ${songItem.name}, filePath: ${songItem.filePath}`);
  132. console.info( TAG, `videoUrl: ${videoUrl}`);
  133. // 关闭之前的文件描述符
  134. if (this.castFile) {
  135. try {
  136. fileIo.closeSync(this.castFile);
  137. console.info( TAG, '✅ 关闭之前的文件描述符');
  138. } catch (error) {
  139. hilog.warn(0x0000, TAG, `⚠️ 关闭文件描述符失败: ${error}`);
  140. }
  141. }
  142. let playItem: avSession.AVQueueItem;
  143. try {
  144. // 判断是否是流媒体
  145. let localCachePath: string | undefined = undefined;
  146. if (this.isLoopbackUrl(videoUrl) && (isSmbType(songItem.type) || isFtpType(songItem.type))) {
  147. try {
  148. const manager = RemoteDriveManager.getInstance();
  149. const accountId = songItem.webdav_account_id;
  150. if (accountId) {
  151. const account = await manager.getWebDavAccountById(accountId);
  152. if (account) {
  153. if (isSmbType(songItem.type)) {
  154. const relative = extractSmbRelativePath(songItem, account.smbShare);
  155. if (relative) {
  156. const cacheInfo = await resolveCacheFilePath(
  157. RemoteCacheType.SMB,
  158. account.id?.toString(),
  159. relative
  160. );
  161. localCachePath = await this.waitForCachePathReady(cacheInfo.cachePath);
  162. }
  163. } else if (isFtpType(songItem.type)) {
  164. const relative = extractFtpRelativePath(songItem);
  165. if (relative) {
  166. localCachePath = await RemoteCacheManager.ensureCached(RemoteCacheType.FTP, {
  167. account,
  168. remotePath: relative
  169. });
  170. }
  171. }
  172. }
  173. }
  174. } catch (error) {
  175. console.warn(TAG, `投播缓存准备失败,继续使用流媒体地址: ${(error as Error).message}`);
  176. }
  177. }
  178. let isStreaming = !localCachePath && this.isUrl(videoUrl);
  179. console.info( TAG, `是否流媒体: ${isStreaming}`);
  180. let description: avSession.AVMediaDescription = {
  181. assetId: songItem.filePath,
  182. title: songItem.name,
  183. subtitle: 'audio',
  184. artist: songItem.artist || '',
  185. mediaType: 'AUDIO',
  186. mediaSize: songItem.videoSize || 0,
  187. startPosition: startPosition,
  188. duration: duration || 0,
  189. };
  190. console.info( TAG, '✅ 创建 AVMediaDescription 成功');
  191. if (isStreaming) {
  192. // 流媒体使用 mediaUri
  193. description.mediaUri = videoUrl;
  194. console.info( TAG, `✅ 使用流媒体地址投播: ${videoUrl}`);
  195. } else {
  196. // 本地文件使用 fdSrc
  197. const localPath = localCachePath || videoUrl || songItem.filePath;
  198. this.castFile = fileIo.openSync(localPath, fileIo.OpenMode.READ_ONLY);
  199. console.info( TAG, `✅ 打开文件成功, fd: ${this.castFile.fd}`);
  200. let fdSrc: media.AVFileDescriptor = { fd: this.castFile.fd };
  201. description.fdSrc = fdSrc;
  202. console.info( TAG, `✅ 使用本地文件投播: ${localPath}, fd: ${this.castFile.fd}`);
  203. }
  204. playItem = {
  205. itemId: this.musicIndex,
  206. description: description
  207. };
  208. console.info( TAG, '✅ 创建 AVQueueItem 成功');
  209. // 先 prepare 再 start
  210. console.info( TAG, '开始调用 prepare...');
  211. await this.avCastController.prepare(playItem);
  212. console.info(TAG, '✅ 投播 prepare 成功');
  213. console.info( TAG, '开始调用 start...');
  214. await this.avCastController.start(playItem);
  215. console.info( TAG, '✅ 投播 start 成功');
  216. // 设置投播状态为 true
  217. this.isCastPlaying = true;
  218. this.isSwitchingTrack = false;
  219. this.hasHandledEnd = false;
  220. this.elapsedTime = startPosition;
  221. this.onPositionChanged?.(this.elapsedTime);
  222. if (typeof description.duration === 'number' && description.duration > 0) {
  223. this.duration = description.duration;
  224. this.onDurationChanged?.(this.duration);
  225. }
  226. console.info(TAG, '✅✅✅ 投播启动成功,isCastPlaying=true ✅✅✅');
  227. } catch (err) {
  228. let errorMsg = err instanceof Error ? err.message : String(err);
  229. console.error( TAG, `❌❌❌ 投播准备失败: ${errorMsg}`);
  230. this.isCastPlaying = false;
  231. }
  232. console.info(TAG, '========== setCastResource 完成 ==========');
  233. }
  234. /**
  235. * 判断是否是 URL
  236. */
  237. private isUrl(str: string): boolean {
  238. if (!str) {
  239. return false;
  240. }
  241. if (str.startsWith('http://') || str.startsWith('https://') ||
  242. str.startsWith('rtmp://') || str.startsWith('rtsp://') ||
  243. str.startsWith('rtp://') || str.startsWith('mms://')) {
  244. return true;
  245. }
  246. return false;
  247. }
  248. private isLoopbackUrl(url: string): boolean {
  249. if (!url) {
  250. return false;
  251. }
  252. if (url.startsWith('http://127.') || url.startsWith('http://localhost') || url.startsWith('https://localhost')) {
  253. return true;
  254. }
  255. return false;
  256. }
  257. private async waitForCachePathReady(cachePath: string): Promise<string | undefined> {
  258. if (!cachePath) {
  259. return undefined;
  260. }
  261. const exists = await FileManager.isExist(cachePath);
  262. if (exists) {
  263. const size = await FileManager.getFileSize(cachePath);
  264. if (size > 0) {
  265. return cachePath;
  266. }
  267. }
  268. const maxAttempts = 20;
  269. for (let i = 0; i < maxAttempts; i++) {
  270. await new Promise<void>((resolve) => setTimeout(resolve, 100));
  271. if (await FileManager.isExist(cachePath)) {
  272. const size = await FileManager.getFileSize(cachePath);
  273. if (size > 0) {
  274. return cachePath;
  275. }
  276. }
  277. }
  278. return undefined;
  279. }
  280. /**
  281. * 设置投播状态变化监听器
  282. */
  283. setAVCastCallback(): void {
  284. console.info( TAG, '开始设置投播监听器');
  285. this.unregisterCastListener();
  286. try {
  287. // 1. 监听播放状态变化
  288. this.avCastController?.on('playbackStateChange', ['state'], async (playbackState: avSession.AVPlaybackState) => {
  289. console.info( TAG, `[state回调] state=${playbackState.state}`);
  290. if (playbackState.state) {
  291. this.state = playbackState.state;
  292. // 更新 isCastPlaying 状态
  293. if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PLAY) {
  294. this.isCastPlaying = true;
  295. console.info( TAG, '[state回调] 设置 isCastPlaying=true');
  296. } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_PAUSE) {
  297. console.info( TAG, '[state回调] 暂停状态');
  298. // 暂停时保持 isCastPlaying 不变
  299. } else if (playbackState.state === avSession.PlaybackState.PLAYBACK_STATE_STOP) {
  300. console.info( TAG, '[state回调] 停止状态,保持 isCastPlaying=' + this.isCastPlaying);
  301. // STOP 状态不改变 isCastPlaying,确保播放完成时继续播放下一首
  302. if (this.shouldAutoPlayNextOnStop()) {
  303. console.info(TAG, '[state回调] STOP 且接近播放结束,触发下一首');
  304. this.hasHandledEnd = true;
  305. this.onPlayNext?.();
  306. }
  307. }
  308. }
  309. });
  310. // 2. 监听位置变化
  311. this.avCastController?.on('playbackStateChange', ['position'], (playbackState: avSession.AVPlaybackState) => {
  312. if (playbackState.position && playbackState.position.elapsedTime !== undefined) {
  313. this.elapsedTime = playbackState.position.elapsedTime;
  314. // 只在每10秒输出一次日志,避免日志过多
  315. if (this.elapsedTime % 10000 < 1000) {
  316. console.info( TAG, `[position回调] elapsedTime=${this.elapsedTime}ms`);
  317. }
  318. // 触发回调
  319. this.onPositionChanged?.(this.elapsedTime);
  320. }
  321. });
  322. // 3. 监听时长变化
  323. this.avCastController?.on('playbackStateChange', ['extras'], (playbackState: avSession.AVPlaybackState) => {
  324. const duration = playbackState?.extras?.duration;
  325. if (typeof duration === 'number') {
  326. this.duration = duration;
  327. console.info( TAG, `[extras回调] duration=${this.duration}ms`);
  328. // 触发回调
  329. this.onDurationChanged?.(this.duration);
  330. }
  331. });
  332. // 4. 监听进度调节完成事件
  333. this.avCastController?.on('seekDone', (position: number) => {
  334. this.elapsedTime = position;
  335. console.info( TAG, `[seekDone回调] position=${position}ms`);
  336. this.onPositionChanged?.(this.elapsedTime);
  337. });
  338. // 5. 监听所有状态变化(调试用)
  339. this.avCastController?.on('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => {
  340. console.info( TAG, `[all回调] 完整状态: ${JSON.stringify(playbackState)}`);
  341. });
  342. // 6. 监听播放完成事件 ⭐ 关键
  343. this.avCastController?.on('endOfStream', () => {
  344. console.info( TAG, '⭐⭐⭐ [endOfStream回调] 播放完成,触发下一首 ⭐⭐⭐');
  345. console.info( TAG, `[endOfStream回调] 当前 isCastPlaying=${this.isCastPlaying}`);
  346. // 确保 isCastPlaying 在播放完成时保持为 true,以便下一首能继续投播
  347. this.isCastPlaying = true;
  348. console.info( TAG, `[endOfStream回调] 强制设置 isCastPlaying=true`);
  349. this.hasHandledEnd = true;
  350. this.onPlayNext?.();
  351. });
  352. this.avCastController?.on('playNext', () => {
  353. console.info( TAG, '⭐⭐⭐ [playNext回调] 触发下一首 ⭐⭐⭐');
  354. this.onPlayNext?.();
  355. });
  356. this.avCastController?.on('playPrevious', () => {
  357. console.info( TAG, '⭐⭐⭐ [playPrevious回调] 触发上一首 ⭐⭐⭐');
  358. this.onPlayPrevious?.();
  359. });
  360. // 6. 监听错误事件
  361. this.avCastController?.on('error', (error: BusinessError) => {
  362. console.error( TAG, `[error回调] 投播错误: code=${error.code}, message=${error.message}`);
  363. this.isCastPlaying = false;
  364. });
  365. console.info( TAG, '✅ 所有监听器设置完成');
  366. } catch (error) {
  367. console.error( TAG, `❌ 设置监听器失败: ${JSON.stringify(error)}`);
  368. }
  369. }
  370. /**
  371. * 播放
  372. */
  373. public async setPlaying(): Promise<void> {
  374. try {
  375. let avCommand: avSession.AVCastControlCommand = { command: 'play' };
  376. await this.avCastController?.sendControlCommand(avCommand);
  377. console.info( TAG, '发送 play 命令成功');
  378. } catch (error) {
  379. console.error( TAG, `play 命令失败: ${JSON.stringify(error)}`);
  380. }
  381. }
  382. /**
  383. * 暂停
  384. */
  385. public async setPause(): Promise<void> {
  386. try {
  387. let avCommand: avSession.AVCastControlCommand = { command: 'pause' };
  388. await this.avCastController?.sendControlCommand(avCommand);
  389. console.info( TAG, '发送 pause 命令成功');
  390. } catch (error) {
  391. console.error( TAG, `pause 命令失败: ${JSON.stringify(error)}`);
  392. }
  393. }
  394. /**
  395. * 停止
  396. */
  397. public async setStop(): Promise<void> {
  398. try {
  399. let avCommand: avSession.AVCastControlCommand = { command: 'stop' };
  400. await this.avCastController?.sendControlCommand(avCommand);
  401. console.info( TAG, '发送 stop 命令成功');
  402. } catch (error) {
  403. console.error( TAG, `stop 命令失败: ${JSON.stringify(error)}`);
  404. }
  405. }
  406. /**
  407. * 跳转
  408. */
  409. public async seek(timeMS: number): Promise<void> {
  410. try {
  411. let avCommand: avSession.AVCastControlCommand = { command: 'seek', parameter: timeMS };
  412. await this.avCastController?.sendControlCommand(avCommand);
  413. console.info( TAG, `发送 seek 命令成功: ${timeMS}ms`);
  414. } catch (error) {
  415. console.error( TAG, `seek 命令失败: ${JSON.stringify(error)}`);
  416. }
  417. }
  418. /**
  419. * 设置音量
  420. */
  421. public async setVolume(volume: number): Promise<void> {
  422. try {
  423. let avCommand: avSession.AVCastControlCommand = { command: 'setVolume', parameter: volume };
  424. await this.avCastController?.sendControlCommand(avCommand);
  425. console.info( TAG, `发送 setVolume 命令成功: ${volume}`);
  426. } catch (error) {
  427. console.error( TAG, `setVolume 命令失败: ${JSON.stringify(error)}`);
  428. }
  429. }
  430. /**
  431. * 设置循环模式
  432. */
  433. public async setLoopMode(mode: number): Promise<void> {
  434. try {
  435. let avCommand: avSession.AVCastControlCommand = { command: 'setLoopMode', parameter: mode };
  436. await this.avCastController?.sendControlCommand(avCommand);
  437. console.info( TAG, `发送 setLoopMode 命令成功: ${mode}`);
  438. } catch (error) {
  439. console.error( TAG, `setLoopMode 命令失败: ${JSON.stringify(error)}`);
  440. }
  441. }
  442. /**
  443. * 播放下一首
  444. * @param songList 最新的歌曲列表
  445. * @param musicIndex 最新的歌曲索引
  446. * @param videoUrl 最新的视频URL
  447. */
  448. public async playNext(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise<void> {
  449. console.info( TAG, `playNext: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
  450. // 更新内部状态
  451. this.songList = songList;
  452. this.musicIndex = musicIndex;
  453. this.isSwitchingTrack = true;
  454. // 直接切换资源,避免 stop 导致投播会话被终止
  455. try {
  456. await this.setCastResource(0, videoUrl, duration);
  457. console.info( TAG, 'playNext: prepare 和 start 完成');
  458. } finally {
  459. this.isSwitchingTrack = false;
  460. }
  461. }
  462. /**
  463. * 播放上一首
  464. * @param songList 最新的歌曲列表
  465. * @param musicIndex 最新的歌曲索引
  466. * @param videoUrl 最新的视频URL
  467. */
  468. public async playPrevious(songList: VideoItem[], musicIndex: number, videoUrl: string, duration?: number): Promise<void> {
  469. console.info( TAG, `playPrevious: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
  470. // 更新内部状态
  471. this.songList = songList;
  472. this.musicIndex = musicIndex;
  473. this.isSwitchingTrack = true;
  474. // 直接切换资源,避免 stop 导致投播会话被终止
  475. try {
  476. await this.setCastResource(0, videoUrl, duration);
  477. console.info( TAG, 'playPrevious: prepare 和 start 完成');
  478. } finally {
  479. this.isSwitchingTrack = false;
  480. }
  481. }
  482. /**
  483. * 获取当前索引
  484. */
  485. public getCurrentIndex(): number {
  486. return this.musicIndex;
  487. }
  488. /**
  489. * 获取是否正在投播
  490. */
  491. public getIsCastPlaying(): boolean {
  492. return this.isCastPlaying;
  493. }
  494. private shouldAutoPlayNextOnStop(): boolean {
  495. if (this.isSwitchingTrack || this.hasHandledEnd) {
  496. return false;
  497. }
  498. if (this.duration <= 0 || this.elapsedTime <= 0) {
  499. return false;
  500. }
  501. return (this.duration - this.elapsedTime) <= 1500;
  502. }
  503. /**
  504. * 释放投播控制器资源
  505. */
  506. public async releaseAVCast(): Promise<void> {
  507. try {
  508. await this.avCastController?.release();
  509. console.info( TAG, '释放投播控制器成功');
  510. } catch (error) {
  511. console.error( TAG, `释放投播控制器失败: ${JSON.stringify(error)}`);
  512. }
  513. // 关闭文件描述符
  514. if (this.castFile) {
  515. try {
  516. fileIo.closeSync(this.castFile);
  517. this.castFile = undefined;
  518. console.info( TAG, '关闭文件描述符成功');
  519. } catch (error) {
  520. console.error( TAG, `关闭文件描述符失败: ${error}`);
  521. }
  522. }
  523. this.isCastPlaying = false;
  524. this.unregisterCastListener();
  525. }
  526. /**
  527. * 移除所有监听器
  528. */
  529. public unregisterCastListener(): void {
  530. try {
  531. this.avCastController?.off('playbackStateChange');
  532. this.avCastController?.off('seekDone');
  533. this.avCastController?.off('playNext');
  534. this.avCastController?.off('playPrevious');
  535. this.avCastController?.off('endOfStream');
  536. this.avCastController?.off('error');
  537. console.info( TAG, '移除监听器成功');
  538. } catch (error) {
  539. console.error( TAG, `移除监听器失败: ${JSON.stringify(error)}`);
  540. }
  541. }
  542. }