浏览代码

Navidrome 的搜索的歌曲没有歌词问题
重构了下投播的代码,但是还是有问题 没有办法播放下一首

onecold 7 月之前
父节点
当前提交
42fb0c1940

+ 3 - 1
entry/src/main/ets/common/network/NavidromeApi.ets

@@ -103,6 +103,7 @@ interface SubsonicSongEntry {
   coverArt?: string;
   genre?: string;
   created?: string;
+  lyrics?: string;
 }
 
 interface SubsonicArtistBody {
@@ -275,7 +276,8 @@ export class NavidromeApi {
         contentType: entry.contentType,
         coverArt: entry.coverArt,
         genre: entry.genre,
-        created: entry.created
+        created: entry.created,
+        lyrics: entry.lyrics
       });
     }
     void ServerLogUtil.info(TAG, `search2 返回 ${songs.length} 首歌曲`);

+ 163 - 6
entry/src/main/ets/common/network/NavidromeRestApi.ets

@@ -50,7 +50,7 @@ export interface NavidromeRestSong {
   coverArtId?: string;
   coverArtPath?: string;
   embedArtPath?: string;
-  lyrics?: string;
+  lyrics?: string; // JSON格式的歌词,需要通过 convertJsonLyricsToLrc 转换为LRC格式
 }
 
 export interface NavidromeRestArtist {
@@ -231,7 +231,9 @@ export class NavidromeRestApi {
           coverArt: item.coverArt,
           coverArtId: item.coverArtId,
           coverArtPath: item.coverArtPath,
-          embedArtPath: item.embedArtPath
+          embedArtPath: item.embedArtPath,
+          lyrics: item.lyrics,
+
         };
         processed.push(processedItem);
       }
@@ -266,7 +268,8 @@ export class NavidromeRestApi {
       coverArt: song.coverArt,
       coverArtId: song.coverArt,
       coverArtPath: undefined,
-      embedArtPath: undefined
+      embedArtPath: undefined,
+      lyrics: song.lyrics,
     }));
   }
 
@@ -479,12 +482,166 @@ export class NavidromeRestApi {
   }
 
   /**
-   * 获取歌词
+   * 获取歌曲详细信息(用于调试)
+   * 使用 Navidrome REST API: GET /api/song/{id}
+   * @param account Navidrome 账号信息
+   * @param songId 歌曲ID
+   * @returns 歌曲详细信息对象
+   */
+  async getLyricsBySongId(account: WebDavAccount, songId: string): Promise<string | undefined> {
+    try {
+      void ServerLogUtil.info(TAG, `========== 获取歌曲详细信息 ==========`)
+      void ServerLogUtil.info(TAG, `歌曲ID: ${songId}`)
+
+      const path = `/api/song/${songId}`;
+      const song = await this.get<NavidromeRestSong>(account, path);
+
+      if (song) {
+        void ServerLogUtil.info(TAG, `✅ 获取歌曲信息成功`);
+        void ServerLogUtil.info(TAG, `完整JSON:\n${JSON.stringify(song, null, 2)}`);
+
+        // 特别检查歌词字段
+        if (song.lyrics) {
+          void ServerLogUtil.info(TAG, `✅ 歌曲包含歌词字段`);
+          void ServerLogUtil.info(TAG, `原始歌词JSON:\n${song.lyrics}`);
+
+          // 将JSON格式的歌词转换为LRC格式
+          const lrcLyrics = this.convertJsonLyricsToLrc(song.lyrics);
+          void ServerLogUtil.info(TAG, `✅ 转换后的LRC歌词:\n${lrcLyrics}`);
+          return lrcLyrics;
+        } else {
+          void ServerLogUtil.warn(TAG, `⚠️ 歌曲没有歌词字段`);
+        }
+      } else {
+        void ServerLogUtil.error(TAG, `❌ 获取歌曲信息失败,返回为空`);
+      }
+
+      return undefined;
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `❌ 获取歌曲详细信息异常: ${err.message}`);
+      return undefined;
+    }
+  }
+
+  /**
+   * 将Navidrome的JSON格式歌词转换为LRC格式
+   * @param jsonLyrics JSON格式的歌词字符串
+   * @returns LRC格式的歌词字符串
+   */
+  private convertJsonLyricsToLrc(jsonLyrics: string): string {
+    try {
+      void ServerLogUtil.debug(TAG, `开始转换JSON歌词到LRC格式`);
+
+      // 解析JSON,使用明确的类型
+      const jsonData: Array<object> | null = JSON.parse(jsonLyrics) as Array<object> | null;
+
+      // 检查是否是数组格式
+      if (!jsonData || jsonData.length === 0) {
+        void ServerLogUtil.warn(TAG, `歌词不是数组格式或为空,直接返回原文本`);
+        return jsonLyrics;
+      }
+
+      const lrcLines: string[] = [];
+
+      // 遍历所有语言版本
+      for (let i = 0; i < jsonData.length; i++) {
+        const langItem = jsonData[i];
+        if (!langItem) {
+          continue;
+        }
+
+        // 定义歌词行接口
+        interface LyricLine {
+          start: number;
+          value: string;
+        }
+
+        // 定义语言数据接口
+        interface LangData {
+          lang: string;
+          line: LyricLine[];
+        }
+
+        // 使用接口类型进行类型检查
+        if (this.isValidLangData(langItem)) {
+          const langData: LangData = langItem as LangData;
+
+          void ServerLogUtil.debug(TAG, `处理语言: ${langData.lang}, 歌词行数: ${langData.line.length}`);
+
+          // 将每一行转换为LRC格式
+          for (let j = 0; j < langData.line.length; j++) {
+            const lineData = langData.line[j];
+
+            if (lineData && typeof lineData.start === 'number' && typeof lineData.value === 'string') {
+              const start = lineData.start;
+              const value = lineData.value;
+
+              // 转换时间为LRC格式 [mm:ss.ms]
+              const minutes = Math.floor(start / 60000);
+              const seconds = Math.floor((start % 60000) / 1000);
+              const milliseconds = start % 1000;
+
+              const timeTag = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}]`;
+              lrcLines.push(`${timeTag}${value}`);
+            }
+          }
+        }
+      }
+
+      // 按时间排序
+      lrcLines.sort();
+
+      const result = lrcLines.join('\n');
+      void ServerLogUtil.info(TAG, `✅ 成功转换歌词,共 ${lrcLines.length} 行`);
+
+      return result;
+    } catch (error) {
+      const err = error as Error;
+      void ServerLogUtil.error(TAG, `❌ 转换歌词格式失败: ${err.message}`);
+      void ServerLogUtil.debug(TAG, `返回原始歌词文本`);
+      return jsonLyrics;
+    }
+  }
+
+  /**
+   * 检查对象是否是有效的语言数据结构
+   */
+  private isValidLangData(obj: object): boolean {
+    if (!obj || typeof obj !== 'object') {
+      return false;
+    }
+
+    const record = obj as Record<string, object>;
+
+    // 检查是否有 lang 和 line 属性
+    if (!record.lang || !record.line) {
+      return false;
+    }
+
+    // 检查 lang 是否是字符串
+    if (typeof record.lang !== 'string') {
+      return false;
+    }
+
+    // 检查 line 是否是数组
+    if (!Array.isArray(record.line)) {
+      return false;
+    }
+
+    return true;
+  }
+
+
+
+  /**
+   * 获取歌词 (旧方式 - 通过 artist 和 title)
    * 使用 Subsonic API: /rest/getLyrics
    * @param account Navidrome 账号信息
    * @param artist 歌手名(可选)
    * @param title 歌曲名(可选)
-   * @returns 歌词文本,如果获取失败返回空字符串
+   * @returns 歌词文本,如果获取失败返回空字符串返回的歌词没有时间戳,所以废弃使用
+   * @deprecated 建议使用 getLyricsBySongId 代替
    */
   async getLyrics(account: WebDavAccount, artist?: string, title?: string): Promise<string> {
     const httpRequest = http.createHttp();
@@ -495,7 +652,7 @@ export class NavidromeRestApi {
         new QueryParam('p', account.password ?? ''),
         new QueryParam('v', '1.16.1'),
         new QueryParam('c', 'TTMusic'),
-        new QueryParam('f', 'xml')
+        new QueryParam('f', 'json')
       ];
 
       // 添加可选参数

+ 498 - 0
entry/src/main/ets/controller/CastController.ets

@@ -0,0 +1,498 @@
+/*
+ * 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 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
+  ): 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);
+     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): 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',
+        startPosition: startPosition,
+        duration: songItem.videoSize || 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;
+      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,确保播放完成时继续播放下一首
+          }
+        }
+      });
+
+      // 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('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => {
+       console.info( TAG, `[all回调] 完整状态: ${JSON.stringify(playbackState)}`);
+      });
+
+      // 5. 监听播放完成事件 ⭐ 关键
+      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.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): Promise<void> {
+   console.info( TAG, `playNext: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
+
+    // 更新内部状态
+    this.songList = songList;
+    this.musicIndex = musicIndex;
+
+    // 先停止当前播放
+    await this.setStop();
+   console.info( TAG, 'playNext: stop 成功');
+
+    // 准备并播放下一首
+    await this.setCastResource(0, videoUrl);
+   console.info( TAG, 'playNext: prepare 和 start 完成');
+  }
+
+  /**
+   * 播放上一首
+   * @param songList 最新的歌曲列表
+   * @param musicIndex 最新的歌曲索引
+   * @param videoUrl 最新的视频URL
+   */
+  public async playPrevious(songList: VideoItem[], musicIndex: number, videoUrl: string): Promise<void> {
+   console.info( TAG, `playPrevious: 传入索引=${musicIndex}, 当前内部索引=${this.musicIndex}`);
+
+    // 更新内部状态
+    this.songList = songList;
+    this.musicIndex = musicIndex;
+
+    // 先停止当前播放
+    await this.setStop();
+   console.info( TAG, 'playPrevious: stop 成功');
+
+    // 准备并播放上一首
+    await this.setCastResource(0, videoUrl);
+   console.info( TAG, 'playPrevious: prepare 和 start 完成');
+  }
+
+  /**
+   * 获取当前索引
+   */
+  public getCurrentIndex(): number {
+    return this.musicIndex;
+  }
+
+  /**
+   * 获取是否正在投播
+   */
+  public getIsCastPlaying(): boolean {
+    return this.isCastPlaying;
+  }
+
+  /**
+   * 释放投播控制器资源
+   */
+  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('playNext');
+      this.avCastController?.off('playPrevious');
+      this.avCastController?.off('endOfStream');
+      this.avCastController?.off('error');
+     console.info( TAG, '移除监听器成功');
+    } catch (error) {
+      console.error( TAG, `移除监听器失败: ${JSON.stringify(error)}`);
+    }
+  }
+}

+ 479 - 175
entry/src/main/ets/view/LocalMusic.ets

@@ -48,6 +48,7 @@ import { effectKit } from '@kit.ArkGraphics2D';
 import { ColorConversion } from '../common/util/ColorConversion';
 import { Lyric, LyricController, LyricParser, LyricView2 } from '@seagazer/cclyric';
 import { AvSessionController } from '../controller/AvSessionController';
+import { CastController, CastControllerCallbacks } from '../controller/CastController';
 import { repairAudioMetadata, convertDsfToWav, getApiLyric,changeMusicCover} from '../common/util/MusicTagUtils';
 import { extractHwMediaMetadata, FFMpegTags,Utility } from '../common/util/Utility';
 import { TagsContentCover } from '../view/TagsContentCover';
@@ -70,6 +71,7 @@ import { PlayStatus } from '../common/PlayStatus';
 import fs from '@ohos.file.fs';
 import { image } from '@kit.ImageKit';
 import { AVCastPicker, AVCastPickerState, AVCastPickerStyle, avSession } from '@kit.AVSessionKit';
+import { media } from '@kit.MediaKit';
 import { UniversalDetector } from '@ohos/juniversalchardet';
 import { SettingPage } from '../pages/SettingPage';
 import { secondToTime,getTransverterText } from '../common/util/CommUtils';
@@ -868,16 +870,15 @@ export struct LocalMusic {
 
     let event: Callback<InterruptEvent> = (event) => {
       // LogUtils.getInstance().LOGI(`onecold 与其他应用音频冲突 被截断点event: ${JSON.stringify(event)}`);
-      console.info(`onecold 与其他应用音频冲突 被截断点event: ${JSON.stringify(event)}`)
+      console.info(`heanup onecold 与其他应用音频冲突 被截断点event: ${JSON.stringify(event)}`)
       this.savePlaybackPosition()
       if (event.hintType === InterruptHintType.INTERRUPT_HINT_PAUSE) {
         this.pause();
       } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_RESUME) {
-        console.info(`onecold audioInterrupt startPlayOrResumePlay: ${JSON.stringify(event)}`)
-        console.info('onecold startPlayOrResumePlay 514')
+        console.info(`heanup onecold audioInterrupt startPlayOrResumePlay: ${JSON.stringify(event)}`)
         this.startPlayOrResumePlay();
       } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_STOP) {
-        console.info(`onecold 与其他应用音频冲突 被截断点 INTERRUPT_HINT_STOP`)
+        console.info(`heanup onecold 与其他应用音频冲突 被截断点 INTERRUPT_HINT_STOP`)
         if(!this.isSameTimePlay){
           this.stop();
         }
@@ -891,7 +892,7 @@ export struct LocalMusic {
       if (event.reason === DeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) { // 音频设备连接
 
       } else if (event.reason === DeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) { // 音频设备断开连接
-        console.info('onecold 音频设备断开连接, 暂停播放')
+        console.info('heanup onecold 音频设备断开连接, 暂停播放')
         this.pause();
       }
     }
@@ -901,6 +902,7 @@ export struct LocalMusic {
     const eventPause: emitter.InnerEvent = { eventId: EventConstants.EVENT_PLAY_PAUSE }
     // 监听广播事件(hicar断开暂停播放)
     emitter.on(eventPause, (eventData: emitter.EventData) => {
+      console.info('heanup onecold hicar断开暂停播放, 暂停播放')
       this.pause();
 
     });
@@ -914,6 +916,7 @@ export struct LocalMusic {
       if(deviceChanged.type==1&&
         (deviceChanged.deviceDescriptors[0].deviceType== 22||
           deviceChanged.deviceDescriptors[0].deviceType==3)){
+        console.info('heanup onecold 有线耳机断开, 暂停播放')
         this.pause();
       }
     });
@@ -9768,6 +9771,9 @@ export struct LocalMusic {
   eventHub = getContext().eventHub;
   //投播组件
   private avSessionController: AvSessionController = AvSessionController.getInstance(false);
+  // 新的投播控制器(封装了官方示例的逻辑)
+  private castControllerWrapper: CastController | undefined = undefined;
+  // 保留原始的 avCastController 引用(用于兼容旧代码)
   private castController: avSession.AVCastController | undefined = undefined;
   @State isCastPlaying: boolean = false;
   @State currentTime2: number = 0;
@@ -9780,6 +9786,7 @@ export struct LocalMusic {
   // @State isPlaying: boolean = false;
   private castSeek: boolean = false;
   private castItem: avSession.AVQueueItem | undefined = undefined;
+  private castFile: fs.File | undefined = undefined;  // 保持文件描述符引用,避免被垃圾回收
   // @State isBgPlayOpen:boolean = true   //是否启用后台播放
   @State rotateAngle2: number = -9
   @StorageLink('imageColor') imageColor: string = 'rgba(0, 0, 2, 1.00)';
@@ -9884,7 +9891,8 @@ export struct LocalMusic {
     }
 
     // 2. 尝试从服务器获取歌词 (Navidrome/Jellyfin/Emby)
-    if (this.currentSong && (isJellyfinType(this.currentSong.type)|| isEmbyType(this.currentSong.type))) {
+    if (this.currentSong && (isJellyfinType(this.currentSong.type)||isNavidromeType(this.currentSong.type)
+      || isEmbyType(this.currentSong.type))) {
       const manager = RemoteDriveManager.getInstance();
       const getAccount = (accountId: string) => manager.getWebDavAccountById(accountId);
 
@@ -9899,6 +9907,9 @@ export struct LocalMusic {
       };
 
       let serverLyric = '';
+      if (!serverLyric && isNavidromeType(songData.type)) {
+        serverLyric = await lyricService.fetchNavidromeLyric(songData, getAccount,this.currentSong.id);
+      }
       if (!serverLyric && isJellyfinType(songData.type)) {
         serverLyric = await lyricService.fetchJellyfinLyric(songData, getAccount);
       }
@@ -14222,6 +14233,25 @@ export struct LocalMusic {
   private sessionOutputDeviceChange = async (connectState: avSession.ConnectionState,
     device: avSession.OutputDeviceInfo) => {
     let currentDevice: avSession.DeviceInfo = device?.devices?.[0];
+
+    // 将 connectState 转换为可读的字符串
+    let stateName = 'UNKNOWN';
+    switch (connectState) {
+      case avSession.ConnectionState.STATE_CONNECTED:
+        stateName = 'CONNECTED(1)';
+        break;
+      case avSession.ConnectionState.STATE_DISCONNECTED:
+        stateName = 'DISCONNECTED(2)';
+        break;
+      case avSession.ConnectionState.STATE_CONNECTING:
+        stateName = 'CONNECTING(3)';
+        break;
+      default:
+        stateName = `OTHER(${connectState})`;
+    }
+
+    Logger.info('heanup sessionOutputDeviceChange', `设备状态变化: connectState=${stateName}, castCategory=${currentDevice?.castCategory}, isCastPlaying=${this.isCastPlaying}`);
+
     if (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE &&
       connectState === avSession.ConnectionState.STATE_CONNECTED) {
 
@@ -14232,35 +14262,106 @@ export struct LocalMusic {
       (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_LOCAL &&
         connectState === avSession.ConnectionState.STATE_CONNECTED)) {
       if (device) {
+        Logger.info('heanup sessionOutputDeviceChange', '设备断开连接');
+
+        Logger.info('heanup sessionOutputDeviceChange', '用户主动断开连接或已停止播放,调用endCasting');
         this.endCasting();
+
       }
     }
   };
 
   private async startCasting() {
-    this.castController = await this.avSessionController.getAvSession()?.getAVCastController();
-    this.setPlaybackStateChangeListener();
-    this.pause();
-    this.initQueueItem();
-    this.prepare();
-    let refToIsCasting: AbstractProperty<boolean> | undefined = AppStorage.ref('isCasting');
-    refToIsCasting?.set(true);
-    this.castSeek = true;
-    this.isCastPlaying = true;
+    try {
+      Logger.info('heanup startCasting', '========== 开始投播初始化 ==========');
+
+      // 获取原始的 AVCastController
+      let avCastCtrl = await this.avSessionController.getAvSession()?.getAVCastController();
+      if (!avCastCtrl) {
+        Logger.error('heanup startCasting', '获取 AVCastController 失败');
+        return;
+      }
+      this.castController = avCastCtrl;
+      Logger.info('heanup startCasting', '✅ 获取 AVCastController 成功');
+
+      // 创建新的 CastController 封装器
+      this.castControllerWrapper = new CastController(avCastCtrl);
+      Logger.info('heanup startCasting', '✅ 创建 CastController 成功');
+
+      // 设置回调函数
+      let callbacks: CastControllerCallbacks = {
+        onPlayNext: () => {
+          Logger.info('heanup CastController', '收到 playNext 回调');
+          this.playNext();
+        },
+        onPlayPrevious: () => {
+          Logger.info('heanup CastController', '收到 playPrevious 回调');
+          this.playPrevious();
+        },
+        onPositionChanged: (position: number) => {
+          this.positionChange(position);
+        },
+        onDurationChanged: (duration: number) => {
+          this.playDurationChange(duration);
+        }
+      };
+      this.castControllerWrapper.setCallbacks(callbacks);
+      Logger.info('heanup startCasting', '✅ 设置回调成功');
+
+      // 暂停本地播放
+      this.pause();
+      Logger.info('heanup startCasting', '✅ 暂停本地播放成功');
+
+      // 初始化投播会话
+      Logger.info('heanup startCasting', `准备调用 initAVCast: songList.length=${this.songList.length}, curIndex=${this.curIndex}, videoUrl=${this.videoUrl}`);
+
+      await this.castControllerWrapper.initAVCast(
+        this.songList,
+        this.curIndex,
+        this.mIjkMediaPlayer.getCurrentPosition(),
+        this.videoUrl
+      );
+      Logger.info('heanup startCasting', '✅ initAVCast 调用完成');
+
+      let refToIsCasting: AbstractProperty<boolean> | undefined = AppStorage.ref('isCasting');
+      refToIsCasting?.set(true);
+      this.castSeek = true;
+
+      // 从 CastController 获取投播状态
+      this.isCastPlaying = this.castControllerWrapper.getIsCastPlaying();
+      Logger.info('heanup startCasting', `✅ 投播初始化完成, isCastPlaying=${this.isCastPlaying}`);
+
+    } catch (error) {
+      Logger.error('heanup startCasting', `❌ 投播初始化失败: ${error}`);
+    }
   }
 
   async changeCasting() {
+    Logger.info('heanup changeCasting', '切换投播');
     this.castController = await this.avSessionController.getAvSession()?.getAVCastController();
     this.initQueueItem();
-    this.prepare();
+    await this.prepare();
     this.setPlaybackStateChangeListener();
-    this.isCastPlaying = true;
+    // isCastPlaying 会在 playbackStateChangeListener 中根据实际状态更新
     this.castSeek = true;
   }
 
   public endCasting() {
+    Logger.info('heanup endCasting', 'endCasting被调用');
     this.avSessionController.getAvSession()?.stopCasting();
     this.stopCast();
+
+    // 关闭文件描述符
+    if (this.castFile) {
+      try {
+        fs.closeSync(this.castFile);
+        this.castFile = undefined;
+        Logger.info('heanup endCasting', '关闭投播文件描述符');
+      } catch (error) {
+        Logger.warn('heanup endCasting', `关闭文件描述符失败: ${error}`);
+      }
+    }
+
     this.isCastPlaying = false;
     let refToIsCasting: AbstractProperty<boolean> | undefined = AppStorage.ref('isCasting');
     refToIsCasting?.set(false);
@@ -14298,10 +14399,48 @@ export struct LocalMusic {
   }
 
   private setPlaybackStateChangeListener(): void {
-    this.castController?.on('playbackStateChange', 'all', this.playbackStateChangeListener);
-    this.castController?.on('endOfStream', this.reloadCasting);
+    // 注意: HarmonyOS 的投播控制器可能不返回 state 字段
+    // 我们需要通过其他方式来判断播放状态
+
+    // 1. 监听位置变化
+    this.castController?.on('playbackStateChange', ['position'], (playbackState: avSession.AVPlaybackState) => {
+      if (playbackState.position && typeof playbackState.position.elapsedTime !== 'undefined') {
+        Logger.info('heanup playbackStateChange[position]', `elapsedTime=${playbackState.position.elapsedTime}`);
+        this.positionChange(playbackState.position.elapsedTime);
+      }
+    });
+
+    // 2. 监听时长变化
+    this.castController?.on('playbackStateChange', ['extras'], (playbackState: avSession.AVPlaybackState) => {
+      const duration = playbackState?.extras?.duration;
+      if (typeof duration === 'number') {
+        this.playDurationChange(duration);
+      }
+    });
+
+    // 3. 监听所有状态变化(用于调试和状态判断)
+    this.castController?.on('playbackStateChange', 'all', (playbackState: avSession.AVPlaybackState) => {
+      Logger.info('heanup playbackStateChange[all]', `完整状态=${JSON.stringify(playbackState)}`);
+
+      // 检查是否有 state 字段(某些设备可能返回)
+      if (playbackState.state !== undefined) {
+        Logger.info('heanup playbackStateChange', `state字段存在: ${playbackState.state}`);
+        if (playbackState.state === CommonConstants2.PLAYBACK_STATE_PLAY) {
+          this.isCastPlaying = true;
+        }
+      }
+    });
+
+    // 4. 监听播放完成事件
+    this.castController?.on('endOfStream', this.playNextCallback);
     this.castController?.on('playNext', this.playNextCallback);
     this.castController?.on('playPrevious', this.playPreviousCallback);
+
+    // 5. 监听错误事件
+    this.castController?.on('error', (error: BusinessError) => {
+      Logger.error('heanup playbackStateChange', `投播错误: code=${error.code}, message=${error.message}`);
+      this.isCastPlaying = false;
+    });
   }
 
   private updateSessionPlayState(isPlay: boolean): void {
@@ -14331,61 +14470,12 @@ export struct LocalMusic {
     this.durationTime = Math.floor(this.duration / 1000);
     this.durationStringTime = secondToTime((this.durationTime));
   }
-  private playbackStateChangeListener = (playbackState: avSession.AVPlaybackState) => {
-    const duration = playbackState?.extras?.duration;
-    if (typeof duration === 'number') {
-      this.playDurationChange(duration as number);
-    }
-
-    if (typeof playbackState?.position?.elapsedTime !== 'undefined') {
-      this.positionChange(playbackState?.position?.elapsedTime);
-    }
-
-    if (playbackState.state === CommonConstants2.PLAYBACK_STATE_PAUSE ||
-      playbackState.state === CommonConstants2.PLAYBACK_STATE_STOP ||
-      playbackState.state === CommonConstants2.PLAYBACK_STATE_PREPARE ||
-      playbackState.state === CommonConstants2.PLAYBACK_STATE_INITIAL) {
-      this.isCastPlaying = false;
-    }
-
-    if (playbackState.state === CommonConstants2.PLAYBACK_STATE_PLAY) {
-      this.isCastPlaying = true;
-    }
-  };
+  // playbackStateChangeListener 方法已被删除,改用 setPlaybackStateChangeListener 中的内联函数
   private reloadCasting = async () => {
-
-    let item = this.songList[this.curIndex];
-    let uri = fileUri.getUriFromPath(item.filePath);
-    let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
-    // description.startPosition = 0;
-    this.castItem = {
-      itemId: this.curIndex,
-      description: {
-        assetId: item.filePath,
-        title: item.name,
-        artist: '',
-        // 网络资源投播,设置mediaUri; 本地资源投播,将本地文件打开后,相关的文件描述符设置到fdSrc
-        // mediaUri: item.filePath,
-        fdSrc: {
-          fd: file.fd
-        },
-        // 该字段大写,音频'AUDIO',视频'VIDEO'
-        mediaType: 'VIDEO',
-        mediaSize: item.videoSize,
-        //startPosition为投播当前进度,设置该字段可将本机播放进度同步到远端
-        startPosition: this.mIjkMediaPlayer.getCurrentPosition(),
-        // 投播资源播放时长,设置该字段可将本机播放时长同步到远端显示
-        duration: this.duration,
-        // albumCoverUri: 'https://www.example.jpeg',
-        // albumTitle: '《ExampleAlbum》',
-        appName: this.appName,
-        // DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。
-        // drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c',
-      }
-    };
-    this.prepare();
-    this.isCastPlaying = true;
-
+    Logger.info('heanup reloadCasting', '重新加载投播');
+    // 使用 initQueueItem() 方法来准备投播项目,保持逻辑一致
+    this.initQueueItem();
+    await this.prepare();
   };
   public playNextCallback = (): void => {
     this.playNext().catch(() => {
@@ -14399,52 +14489,135 @@ export struct LocalMusic {
   };
 
   async prepare() {
-    await this.castController?.prepare(this.castItem);
-    await this.castController?.start(this.castItem);
+    try {
+      Logger.info('heanup prepare', `开始准备投播: ${this.castItem?.description?.title}`);
+      await this.castController?.prepare(this.castItem);
+      Logger.info('heanup prepare', '投播prepare成功');
+
+      await this.castController?.start(this.castItem);
+      Logger.info('heanup prepare', '投播start成功');
+
+      // 投播启动成功,设置为投播状态
+      this.isCastPlaying = true;
+      Logger.info('heanup prepare', '投播启动成功,设置isCastPlaying=true');
+    } catch (error) {
+      Logger.error('heanup prepare', `投播准备失败: ${error}`);
+      this.isCastPlaying = false;
+    }
   }
 
   public initQueueItem() {
     let item = this.songList[this.curIndex];
 
-    let time = this.currentTime;
-    let uri = fileUri.getUriFromPath(item.filePath);
-    let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
+    // 判断是否是流媒体地址 - 使用 videoUrl 判断而不是 filePath
+    // 因为 filePath 可能是 navidrome:// 等自定义协议,而 videoUrl 才是真实的流地址
+    let isStreaming = this.isUrl(this.videoUrl);
+
+    Logger.info('heanup initQueueItem', `准备投播歌曲: ${item.name}, 是否流媒体: ${isStreaming}, videoUrl: ${this.videoUrl}`);
+
+    // 关闭之前的文件描述符
+    if (this.castFile) {
+      try {
+        fs.closeSync(this.castFile);
+        Logger.info('heanup initQueueItem', '关闭之前的文件描述符');
+      } catch (error) {
+        Logger.warn('heanup initQueueItem', `关闭文件描述符失败: ${error}`);
+      }
+    }
+
+    let description: avSession.AVMediaDescription = {
+      assetId: 'AUDIO-' + item.filePath,
+      title: item.name,
+      artist: item.artist || '',
+      // 该字段大写,音频'AUDIO',视频'VIDEO'
+      mediaType: 'AUDIO',
+      mediaSize: item.videoSize,
+      //startPosition为投播当前进度,设置该字段可将本机播放进度同步到远端
+      startPosition: this.mIjkMediaPlayer.getCurrentPosition(),
+      // 投播资源播放时长,设置该字段可将本机播放时长同步到远端显示
+      duration: this.duration,
+      albumCoverUri: item.pixelMapPath,  // 本地路径电视可能无法访问,暂时注释
+      appName: this.appName,
+      lyricContent: item.lyricContent,
+      // DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。
+      // drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c',
+    };
+    console.log('heanup initQueueItem this.lyricContent', item.lyricContent);
+
+    // 网络资源投播,设置mediaUri; 本地资源投播,将本地文件打开后,相关的文件描述符设置到fdSrc
+    if (isStreaming) {
+      // 流媒体地址使用 mediaUri
+      description.mediaUri = this.videoUrl;
+      Logger.info('heanup initQueueItem', `使用流媒体地址投播: ${this.videoUrl}`);
+    } else {
+      // 本地文件使用 fdSrc
+      let uri = fileUri.getUriFromPath(item.filePath);
+      this.castFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
+      let fdSrc: media.AVFileDescriptor = {
+        fd: this.castFile.fd
+      };
+      description.fdSrc = fdSrc;
+      Logger.info('heanup initQueueItem', `使用本地文件投播: ${item.filePath}, fd: ${this.castFile.fd}`);
+    }
 
     this.castItem = {
       itemId: this.curIndex,
-      description: {
-        assetId: item.filePath,
-        title: item.name,
-        artist: '',
-        // 网络资源投播,设置mediaUri; 本地资源投播,将本地文件打开后,相关的文件描述符设置到fdSrc
-        fdSrc: {
-          fd: file.fd
-        },
-        // 该字段大写,音频'AUDIO',视频'VIDEO'
-        mediaType: 'AUDIO',
-        mediaSize: item.videoSize,
-        //startPosition为投播当前进度,设置该字段可将本机播放进度同步到远端
-        startPosition: this.mIjkMediaPlayer.getCurrentPosition(),
-        // 投播资源播放时长,设置该字段可将本机播放时长同步到远端显示
-        duration: this.duration,
-        // albumCoverUri: 'https://www.example.jpeg',
-        // albumTitle: '《ExampleAlbum》',
-        appName: this.appName,
-        // DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。
-        // drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c',
-      }
+      description: description
     };
+
+    Logger.info('heanup initQueueItem', `投播项目创建完成: ${JSON.stringify({
+      title: description.title,
+      mediaType: description.mediaType,
+      hasMediaUri: !!description.mediaUri,
+      hasFdSrc: !!description.fdSrc,
+      duration: description.duration
+    })}`);
+  }
+
+  // 判断是否是 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;
+    }
+
+    // 扩展的URL正则表达式,支持多种协议
+    const urlPattern = new RegExp(
+      '^((https?|ftp|rtmp|rtsp|mms|ws|rtp|wss):\\/\\/)?' + // 支持多种协议
+        '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // 域名
+        '((\\d{1,3}\\.){3}\\d{1,3})|' + // 或IPv4地址
+        '\\[([a-f\\d]{1,4}:){7}[a-f\\d]{1,4}\\]|' + // 或IPv6地址
+        'localhost)' + // 或localhost
+        '(\\:\\d+)?' + // 端口号(可选)
+        '(\\/[-a-z\\d%_.~+]*)*' + // 路径
+        '(\\?[;&a-z\\d%_.~+=-]*)?' + // 查询字符串(可选)
+        '(\\#[-a-z\\d_]*)?$', // 片段标识符(可选)
+      'i' // 不区分大小写
+    );
+
+    return urlPattern.test(str);
   }
 
   async stopCast() {
     try {
-      this.castController?.off('playbackStateChange');
-      this.castController?.off('endOfStream');
-      this.castController?.off('playNext');
-      this.castController?.off('playPrevious');
-      this.castController?.off('error');
+      // 使用 CastController 释放资源
+      if (this.castControllerWrapper) {
+        await this.castControllerWrapper.releaseAVCast();
+        Logger.info('heanup stopCast', 'CastController 释放投播控制器成功');
+      }
+
+      // 清空引用
+      this.castControllerWrapper = undefined;
+      this.castController = undefined;
+      this.castItem = undefined;
+      this.castFile = undefined;
     } catch (error) {
-      hilog.info(0x0000, TAG, `Failed to setAVCastController. code: ${error.code}, message: ${error.message}`);
+      Logger.error('heanup stopCast', `停止投播失败: ${error}`);
     }
   }
 
@@ -14455,20 +14628,29 @@ export struct LocalMusic {
     this.pause()
   };
 
-  private playOrPause() {
+  private async playOrPause() {
     if (!this.debounce()) {
       return;
     }
 
     if (this.CONTROL_PlayStatus === PlayStatus.PLAY) {
-      this.pause();
-
-
+      await this.pause();
     } else {
-      console.info('onecold startPlayOrResumePlay 11064')
-      this.startPlayOrResumePlay();
-      this.playChange()
-
+      // 如果是投播模式,使用 CastController 的 play 方法
+      if (this.castControllerWrapper && this.isCastPlaying) {
+        try {
+          Logger.info('heanup playOrPause', '投播模式播放');
+          await this.castControllerWrapper.setPlaying();
+          Logger.info('heanup playOrPause', '投播播放成功');
+          this.playChange();
+        } catch (error) {
+          Logger.error('heanup playOrPause', `投播播放失败: ${error}`);
+        }
+      } else {
+        console.info('onecold startPlayOrResumePlay 11064')
+        this.startPlayOrResumePlay();
+        this.playChange()
+      }
     }
   }
 
@@ -14485,25 +14667,32 @@ export struct LocalMusic {
   private sessionStopCallback = (): void => {
     this.stop()
   };
-  private sessionFastForwardCallback = (time?: number) => {
+  private sessionFastForwardCallback = async (time?: number) => {
     if (!time) {
       return;
     }
-    if (this.mIjkMediaPlayer != null) {
-      const curPosition = this.mIjkMediaPlayer.getCurrentPosition()
-      let seeTime = curPosition + time * 1000;
-      // 限制 seekValue 在合法范围内
-      if (seeTime < 0) {
-        seeTime = 0;
-      } else if (seeTime > this.duration) {
-        seeTime = this.duration;
-      }
-      Logger.info('onecold seeTime= ' + seeTime)
-      this.setSeekToActionProgress(seeTime)
-      this.seekTo(seeTime + "")
-    }
 
+    let curPosition: number;
+    if (this.castController && this.isCastPlaying) {
+      // 投播模式,使用UI显示的时间
+      curPosition = this.currentTime2 * 1000;
+    } else if (this.mIjkMediaPlayer != null) {
+      // 本地播放器模式
+      curPosition = this.mIjkMediaPlayer.getCurrentPosition();
+    } else {
+      return;
+    }
 
+    let seeTime = curPosition + time * 1000;
+    // 限制 seekValue 在合法范围内
+    if (seeTime < 0) {
+      seeTime = 0;
+    } else if (seeTime > this.duration) {
+      seeTime = this.duration;
+    }
+    Logger.info('heanup sessionFastForwardCallback', `快进: ${time}秒, 目标位置: ${seeTime}ms`);
+    this.setSeekToActionProgress(seeTime);
+    await this.seekTo(seeTime + "");
   };
   /**
    * Gesture method onActionUpdate.
@@ -14511,8 +14700,8 @@ export struct LocalMusic {
    * @param event Gesture event.手势拖动设置快进和后退
    */
   private setSeekToActionProgress(position: number) {
-    // let position = this.mIjkMediaPlayer.getCurrentPosition();
-    let duration = this.mIjkMediaPlayer.getDuration();
+    // 投播模式使用this.duration,本地模式使用播放器获取
+    let duration = this.duration > 0 ? this.duration : this.mIjkMediaPlayer.getDuration();
     let pos = 0;
     if (duration > 0) {
       this.slideEnable = true;
@@ -14534,29 +14723,38 @@ export struct LocalMusic {
     this.currentTime = this.stringForTime(position);
     this.isCurrentTime = false
   }
-  private sessionRewindCallback = (time?: number) => {
+  private sessionRewindCallback = async (time?: number) => {
     if (!time) {
       return;
     }
-    if (this.mIjkMediaPlayer != null) {
-      const curPosition = this.mIjkMediaPlayer.getCurrentPosition()
-      let seeTime = curPosition - time * 1000;
-      // 限制 seekValue 在合法范围内
-      if (seeTime < 0) {
-        seeTime = 0;
-      } else if (seeTime > this.duration) {
-        seeTime = this.duration;
-      }
-      Logger.info('onecold seeTime= ' + seeTime)
-      this.setSeekToActionProgress(seeTime)
-      this.seekTo(seeTime + "")
+
+    let curPosition: number;
+    if (this.castController && this.isCastPlaying) {
+      // 投播模式,使用UI显示的时间
+      curPosition = this.currentTime2 * 1000;
+    } else if (this.mIjkMediaPlayer != null) {
+      // 本地播放器模式
+      curPosition = this.mIjkMediaPlayer.getCurrentPosition();
+    } else {
+      return;
     }
-  };
-  private sessionSeekCallback = (seekTime: number) => {
-    if (this.mIjkMediaPlayer != null) {
-      const curPosition = this.mIjkMediaPlayer.getCurrentPosition()
-      this.seekTo(seekTime + "")
+
+    let seeTime = curPosition - time * 1000;
+    // 限制 seekValue 在合法范围内
+    if (seeTime < 0) {
+      seeTime = 0;
+    } else if (seeTime > this.duration) {
+      seeTime = this.duration;
     }
+    Logger.info('heanup sessionRewindCallback', `快退: ${time}秒, 目标位置: ${seeTime}ms`);
+    this.setSeekToActionProgress(seeTime);
+    await this.seekTo(seeTime + "");
+  };
+  private sessionSeekCallback = async (seekTime: number) => {
+    Logger.info('heanup sessionSeekCallback', `收到seek请求: ${seekTime}ms, 投播模式: ${this.isCastPlaying}`);
+
+    // 投播模式直接调用seekTo,它会自动处理投播控制器的seek命令
+    await this.seekTo(seekTime + "");
   };
 
   private async getFileSize(filePath: string): Promise<number> {
@@ -14569,8 +14767,18 @@ export struct LocalMusic {
     }
   }
 
-  private pause() {
-    if (this.mIjkMediaPlayer.isPlaying()) {
+  private async pause() {
+    // 如果是投播模式,使用 CastController 的 pause 方法
+    if (this.castControllerWrapper && this.isCastPlaying) {
+      try {
+        Logger.info('heanup pause', '投播模式暂停');
+        await this.castControllerWrapper.setPause();
+        Logger.info('heanup pause', '投播暂停成功');
+      } catch (error) {
+        Logger.error('heanup pause', `投播暂停失败: ${error}`);
+      }
+    } else if (this.mIjkMediaPlayer.isPlaying()) {
+      // 本地播放器模式
       this.savePlaybackPosition();
       this.mIjkMediaPlayer.pause();
       this.setProgress();
@@ -14621,15 +14829,35 @@ export struct LocalMusic {
     }
   }
 
-  private  seekTo(value: string) {
+  private async seekTo(value: string) {
     if (StrUtil.isNotEmpty(this.videoUrl) && this.videoUrl.toLowerCase().endsWith('.wma')) {
       ToastUtil.showToast('wma格式不支持拖动快进。')
       return
     }
-    this.isSeekTo = true
-    this.mIjkMediaPlayer.seekTo(value);
-    this.setProgress()
-    this.isSeekTo = false
+
+    // 如果是投播模式,使用 CastController 的 seek 方法
+    if (this.castControllerWrapper && this.isCastPlaying) {
+      try {
+        let seekPos = Number.parseInt(value);
+        Logger.info('heanup seekTo', `投播模式seek到: ${seekPos}ms, 当前时长: ${this.duration}ms`);
+
+        // 检查是否接近末尾
+        if (this.duration > 0 && seekPos >= this.duration - 500) {
+          Logger.warn('heanup seekTo', `快进位置接近末尾(${seekPos}ms >= ${this.duration}ms),可能触发播放完成`);
+        }
+
+        await this.castControllerWrapper.seek(seekPos);
+        Logger.info('heanup seekTo', `投播seek命令发送成功,位置: ${seekPos}ms`);
+      } catch (error) {
+        Logger.error('heanup seekTo', `投播seek失败: ${error}`);
+      }
+    } else {
+      // 本地播放器模式
+      this.isSeekTo = true
+      this.mIjkMediaPlayer.seekTo(value);
+      this.setProgress()
+      this.isSeekTo = false
+    }
   }
 
   // 添加到下一首播放
@@ -14660,34 +14888,64 @@ export struct LocalMusic {
   }
 
   //后退10s,可再设置设置默认几秒
-  backForward() {
-    if(this.mIjkMediaPlayer){
-      let pos = this.mIjkMediaPlayer.getCurrentPosition();
-      let value = pos - Number(this.fastForwardSeconds) * 1000;
-      this.seekTo(value+"");
+  async backForward() {
+    // 如果是投播模式,从currentTime2获取当前位置
+    let pos: number;
+    if (this.castController && this.isCastPlaying) {
+      pos = this.currentTime2 * 1000; // 使用秒转毫秒
+      Logger.info('heanup backForward', `投播模式当前位置: ${pos}ms`);
+    } else if (this.mIjkMediaPlayer) {
+      pos = this.mIjkMediaPlayer.getCurrentPosition();
+    } else {
+      return;
     }
 
+    let value = pos - Number(this.fastForwardSeconds) * 1000;
+    if (value < 0) {
+      value = 0;
+    }
+    await this.seekTo(value + "");
   }
 
   //快进10s,可再设置设置默认几秒
-  fastForward() {
-    if(this.mIjkMediaPlayer){
-      let pos = this.mIjkMediaPlayer.getCurrentPosition();
-      let value = pos + Number(this.fastForwardSeconds) * 1000;
-      this.seekTo(value+"");
+  async fastForward() {
+    // 如果是投播模式,从currentTime2获取当前位置
+    let pos: number;
+    if (this.castController && this.isCastPlaying) {
+      pos = this.currentTime2 * 1000; // 使用秒转毫秒
+      Logger.info('heanup fastForward', `投播模式当前位置: ${pos}ms`);
+    } else if (this.mIjkMediaPlayer) {
+      pos = this.mIjkMediaPlayer.getCurrentPosition();
+    } else {
+      return;
+    }
+
+    let value = pos + Number(this.fastForwardSeconds) * 1000;
+    // 限制不超过总时长
+    if (this.duration > 0 && value > this.duration) {
+      value = this.duration;
     }
+    await this.seekTo(value + "");
   }
 
   //下一个
   private async playNext() {
+    Logger.info('heanup playNext', '========== playNext 开始 ==========');
+    Logger.info('heanup playNext', `isCastPlaying=${this.isCastPlaying}, castControllerWrapper存在=${!!this.castControllerWrapper}`);
+
     if (!this.debounce()) {
+      Logger.warn('heanup playNext', 'debounce() 返回 false,退出');
       return;
     }
 
+    Logger.info('heanup playNext', `playType=${this.playType}`);
+
     if (this.playType == 3) { //3:随机播放
+      Logger.info('heanup playNext', '随机播放模式');
       await this.randomPlay()
       return;
     }
+
     if (ArrayUtil.isNotEmpty(this.songList)) {
       if (this.curIndex >= this.songList.length - 1) {
         await this.tryLoadNextPageForPlayback();
@@ -14697,13 +14955,10 @@ export struct LocalMusic {
       } else {
         this.curIndex++;
       }
-      this.CONTROL_PlayStatus = PlayStatus.INIT;
-      this.stop();
 
       // 直接使用歌曲列表中的歌曲信息,songList中的数据已经是完整的
       this.currentSong = this.songList[this.curIndex];
-      Logger.info('heanup playNext', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
-
+      console.info('heanup playNext', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
       // 设置videoUrl,WebDAV歌曲会通过webdav_account_id构建完整URL
       this.videoUrl = await setVideoUrlForSong(this.currentSong, {
         context: this.context,
@@ -14713,10 +14968,37 @@ export struct LocalMusic {
       this.artist = this.songList[this.curIndex].artist
       this.name = this.songList[this.curIndex].name
       //this.cover = this.songList[this.curIndex].pixelMapPath
-      this.changeImageAnimation()
+      console.info('heanup playNext 1 ', `this.isCastPlaying: ${ this.isCastPlaying}`)
+      console.info('heanup playNext 2 ', `this.castControllerWrapper: ${this.castControllerWrapper}`)
 
-    }
+      // 从 CastController 获取最新的投播状态
+      if (this.castControllerWrapper) {
+        const controllerIsPlaying = this.castControllerWrapper.getIsCastPlaying();
+        console.info('heanup playNext', `从 CastController 获取的 isCastPlaying=${controllerIsPlaying}`);
+        this.isCastPlaying = controllerIsPlaying;
+      }
 
+      // 如果是投播模式,使用 CastController 播放下一首
+      if (this.castControllerWrapper && this.isCastPlaying) {
+        console.info('heanup playNext', '使用 CastController 播放下一首');
+        try {
+          // 传入最新的歌曲列表、索引和 videoUrl
+          await this.castControllerWrapper.playNext(this.songList, this.curIndex, this.videoUrl);
+          this.isCastPlaying = this.castControllerWrapper.getIsCastPlaying();
+          console.info('heanup playNext', 'CastController 播放下一首成功');
+          return;
+        } catch (error) {
+          console.error('heanup playNext', `CastController 播放下一首失败: ${error}`);
+          // 如果投播失败,回退到本地播放
+          this.isCastPlaying = false;
+        }
+      }
+
+      // 本地播放器模式,需要先stop
+      this.CONTROL_PlayStatus = PlayStatus.INIT;
+      this.stop();
+      this.changeImageAnimation()
+    }
   }
 
   changeImageAnimation() {
@@ -14932,8 +15214,6 @@ export struct LocalMusic {
     }
     if (ArrayUtil.isNotEmpty(this.songList)) {
 
-      this.CONTROL_PlayStatus = PlayStatus.INIT;
-      this.stop();
       this.curIndex = index;
       this.currentSong = this.songList[index];
 
@@ -14945,6 +15225,19 @@ export struct LocalMusic {
 
       this.name = this.currentSong.name
       this.artist = this.currentSong.artist
+
+      // 如果是投播模式,需要为投播设备准备当前歌曲
+      if (this.castController && this.isCastPlaying) {
+        Logger.info('heanup playIndex', '投播模式播放指定歌曲,不调用本地stop()');
+        this.initQueueItem();
+        await this.prepare();
+        this.cover = this.currentSong.pixelMapPath;
+        return;
+      }
+
+      // 本地播放器模式,需要先stop
+      this.CONTROL_PlayStatus = PlayStatus.INIT;
+      this.stop();
       this.changeImageAnimation()
     }
 
@@ -14967,9 +15260,6 @@ export struct LocalMusic {
       this.curIndex--;
     }
 
-    this.CONTROL_PlayStatus = PlayStatus.INIT;
-    this.stop();
-
     // 直接使用歌曲列表中的歌曲信息,songList中的数据已经是完整的
     this.currentSong = this.songList[this.curIndex];
     Logger.info('heanup playPrevious', `直接使用歌曲列表中的信息: ${this.currentSong.name}, type: ${this.currentSong.type}`);
@@ -14981,6 +15271,20 @@ export struct LocalMusic {
     });
     this.name = this.songList[this.curIndex].name
     this.artist = this.songList[this.curIndex].artist
+
+    // 如果是投播模式,需要为投播设备准备上一首
+    if (this.castController && this.isCastPlaying) {
+      Logger.info('heanup playPrevious', '投播模式播放上一首,不调用本地stop()');
+      this.initQueueItem();
+      await this.prepare();
+      // 投播模式下不需要调用本地的 startPlayOrResumePlay
+      this.cover = this.songList[this.curIndex].pixelMapPath;
+      return;
+    }
+
+    // 本地播放器模式,需要先stop
+    this.CONTROL_PlayStatus = PlayStatus.INIT;
+    this.stop();
     this.changeImageAnimation()
   }
 

+ 2 - 1
entry/src/main/ets/view/NavidromePage.ets

@@ -1101,7 +1101,8 @@ export struct NavidromePage {
       year: song.year,
       contentType: song.contentType,
       coverArt: song.coverArt,
-      coverArtId: song.coverArt
+      coverArtId: song.coverArt,
+      lyrics: song.lyrics,
     }));
   }
 

+ 2 - 2
lib/src/main/ets/view/LyricView2.ets

@@ -695,11 +695,11 @@ export struct LyricView2 {
 
     private onPositionChanged(mediaPosition: number) {
         if (this.isLyricEmpty) {
-            printW('The lyric data is empty!')
+            // printW('The lyric data is empty!')
             return
         }
         if (this.listAdapter.isEmpty()) {
-            printW('The lyric lines is empty!')
+            // printW('The lyric lines is empty!')
             return
         }
         let index = this.getIndex(mediaPosition)