浏览代码

增加cue文件分轨功能

onecold 5 月之前
父节点
当前提交
1ab381b0a8
共有 3 个文件被更改,包括 364 次插入40 次删除
  1. 201 1
      entry/src/main/ets/common/util/CueUtils.ets
  2. 17 13
      entry/src/main/ets/view/CueComptent.ets
  3. 146 26
      entry/src/main/ets/view/LocalMusic.ets

+ 201 - 1
entry/src/main/ets/common/util/CueUtils.ets

@@ -1,9 +1,14 @@
 import { fileIo } from '@kit.CoreFileKit';
 import { util } from '@kit.ArkTS';
 import { UniversalDetector } from '@ohos/juniversalchardet';
+import { FileUtil, StrUtil } from '@pura/harmony-utils';
 import { Utility } from './Utility';
 import { media } from '@kit.MediaKit';
 import { BusinessError } from '@kit.BasicServicesKit';
+import { VideoItem } from '../../viewmodel/VideoItem';
+import { CommonConstants } from '../constants/CommonConstants';
+import MediaTable from './MediaTable';
+import Logger from './Logger';
 
 
 // 类型定义
@@ -23,11 +28,28 @@ export interface CueInfo {
   performer: string;
   genre?: string;
   date?: string;
+  cuePath?: string;
   filePath: string;
   tracks: CueTrack[];
   totalDuration?: number;
 }
 
+export interface CueTrackExtra {
+  marker: string;
+  cuePath: string;
+  sourceFilePath: string;
+  trackNumber: number;
+  title: string;
+  performer: string;
+  startOffset: number;
+  endOffset: number | null;
+  duration: number | null;
+}
+
+const TAG = 'CueUtils';
+const CUE_TRACK_MARKER = 'ttmusic_cue_track';
+const CUE_TRACK_PATH_SUFFIX = '#ttmusic-cue-track=';
+
 // 主解析函数(同步totalDuration参数)
 export async function parseCueFile(cuePath: string): Promise<CueInfo> {
   const file = fileIo.openSync(cuePath,  fileIo.OpenMode.READ_ONLY);
@@ -44,6 +66,7 @@ export async function parseCueFile(cuePath: string): Promise<CueInfo> {
     console.log('onecold  CUE文件内容=', content);
     // 解析基础信息
     const result = parseCueContent(content);
+    result.cuePath = cuePath;
     // 在 parseCueContent 函数末尾,在返回 result 之前添加以下代码
 
     console.log('onecold  cue result =', JSON.stringify(result));
@@ -60,6 +83,183 @@ export async function parseCueFile(cuePath: string): Promise<CueInfo> {
   }
 }
 
+export function parseCueTrackExtra(extraJson?: string): CueTrackExtra | null {
+  if (StrUtil.isEmpty(extraJson)) {
+    return null;
+  }
+  try {
+    const data = JSON.parse(extraJson as string) as Partial<CueTrackExtra>;
+    if (data.marker !== CUE_TRACK_MARKER || StrUtil.isEmpty(data.sourceFilePath)) {
+      return null;
+    }
+    return {
+      marker: CUE_TRACK_MARKER,
+      cuePath: data.cuePath || '',
+      sourceFilePath: data.sourceFilePath || '',
+      trackNumber: Number(data.trackNumber || 0),
+      title: data.title || '',
+      performer: data.performer || '',
+      startOffset: Number(data.startOffset || 0),
+      endOffset: data.endOffset === undefined || data.endOffset === null ? null : Number(data.endOffset),
+      duration: data.duration === undefined || data.duration === null ? null : Number(data.duration)
+    };
+  } catch (error) {
+    Logger.warn(TAG, `parseCueTrackExtra failed: ${(error as Error).message}`);
+    return null;
+  }
+}
+
+export function isCueSplitItem(item?: VideoItem | null): boolean {
+  return parseCueTrackExtra(item?.extra_json) !== null;
+}
+
+export function getCueTrackSourceFilePath(item?: VideoItem | null): string {
+  const extra = parseCueTrackExtra(item?.extra_json);
+  if (extra && StrUtil.isNotEmpty(extra.sourceFilePath)) {
+    return extra.sourceFilePath;
+  }
+  return item?.filePath || '';
+}
+
+export function getCueTrackStartOffset(item?: VideoItem | null): number {
+  return parseCueTrackExtra(item?.extra_json)?.startOffset || 0;
+}
+
+export function getCueTrackEndOffset(item?: VideoItem | null): number {
+  const endOffset = parseCueTrackExtra(item?.extra_json)?.endOffset;
+  return endOffset === null || endOffset === undefined ? 0 : endOffset;
+}
+
+export function getCueTrackDuration(item?: VideoItem | null): number {
+  const extra = parseCueTrackExtra(item?.extra_json);
+  if (!extra) {
+    return 0;
+  }
+  if (extra.duration !== null && extra.duration !== undefined && extra.duration > 0) {
+    return extra.duration;
+  }
+  if (extra.endOffset !== null && extra.endOffset !== undefined && extra.endOffset > extra.startOffset) {
+    return extra.endOffset - extra.startOffset;
+  }
+  return 0;
+}
+
+export function buildCueTrackVirtualFilePath(cuePath: string, trackNumber: number): string {
+  return `${cuePath}${CUE_TRACK_PATH_SUFFIX}${trackNumber}`;
+}
+
+function buildCueTrackVirtualFileName(track: CueTrack): string {
+  const trackNo = track.trackNumber.toString().padStart(2, '0');
+  const title = StrUtil.isNotEmpty(track.title) ? track.title : `Track ${trackNo}`;
+  return `${trackNo}. ${title}`;
+}
+
+function buildCueTrackExtra(cueInfo: CueInfo, track: CueTrack): CueTrackExtra {
+  return {
+    marker: CUE_TRACK_MARKER,
+    cuePath: cueInfo.cuePath || '',
+    sourceFilePath: cueInfo.filePath,
+    trackNumber: track.trackNumber,
+    title: track.title || '',
+    performer: track.performer || '',
+    startOffset: track.startOffset || 0,
+    endOffset: track.endOffset ?? null,
+    duration: track.duration ?? null
+  };
+}
+
+async function resolveCueSourceItem(context: Context, table: MediaTable, cueInfo: CueInfo,
+  sourceItem?: VideoItem): Promise<VideoItem> {
+  if (sourceItem && sourceItem.filePath === cueInfo.filePath && !sourceItem.filePath.toLowerCase().endsWith('.cue')) {
+    return sourceItem;
+  }
+
+  const dbItem = await table.queryVideoByFilePath(cueInfo.filePath);
+  if (dbItem) {
+    return dbItem;
+  }
+
+  return await Utility.uriGetMusicAssetsFromFile(context, cueInfo.filePath, CommonConstants.TYPE_LOCAL, true);
+}
+
+export async function buildCueTrackItems(context: Context, table: MediaTable, cueInfo: CueInfo,
+  sourceItem?: VideoItem): Promise<VideoItem[]> {
+  if (!cueInfo || StrUtil.isEmpty(cueInfo.filePath) || !cueInfo.tracks || cueInfo.tracks.length === 0) {
+    return [];
+  }
+
+  const source = await resolveCueSourceItem(context, table, cueInfo, sourceItem);
+  const albumTitle = cueInfo.title || source.album || FileUtil.getFileName(cueInfo.filePath);
+  const parentPath = source.parentPath || FileUtil.getParentPath(cueInfo.filePath);
+  const items: VideoItem[] = [];
+
+  for (let i = 0; i < cueInfo.tracks.length; i++) {
+    const track = cueInfo.tracks[i];
+    const virtualPath = buildCueTrackVirtualFilePath(cueInfo.cuePath || cueInfo.filePath, track.trackNumber);
+    const displayName = track.title || buildCueTrackVirtualFileName(track);
+    const displayArtist = track.performer || cueInfo.performer || source.artist || '';
+    const displayFileName = buildCueTrackVirtualFileName(track);
+    const item = new VideoItem(
+      displayName,
+      virtualPath,
+      virtualPath,
+      CommonConstants.TYPE_LOCAL,
+      source.videoSize,
+      source.cTime || '',
+      source.size,
+      source.pixelMapPath,
+      displayArtist,
+      albumTitle,
+      displayFileName
+    );
+
+    item.parentPath = parentPath;
+    item.mimeType = source.mimeType;
+    item.sampleRate = source.sampleRate;
+    item.trackCount = source.trackCount;
+    item.bit_rate = source.bit_rate;
+    item.bits_per_raw_sample = source.bits_per_raw_sample;
+    item.channels = source.channels;
+    item.channel_layout = source.channel_layout;
+    item.start_time = source.start_time;
+    item.nb_streams = source.nb_streams;
+    item.nb_programs = source.nb_programs;
+    item.probe_score = source.probe_score;
+    item.md5Str = source.md5Str;
+    item.pixelMapPath = source.pixelMapPath;
+    item.isCustomCover = source.isCustomCover || 0;
+    item.genre = cueInfo.genre || source.genre;
+    item.year = cueInfo.date || source.year;
+    item.disc = source.disc;
+    item.track = `${track.trackNumber}`;
+    item.album = albumTitle;
+    item.artist = displayArtist;
+    item.fileName = displayFileName;
+    item.duration = track.duration && track.duration > 0 ? millisecondsToTime(track.duration) : source.duration;
+    item.extra_json = JSON.stringify(buildCueTrackExtra(cueInfo, track));
+    items.push(item);
+  }
+
+  return items;
+}
+
+export async function importCueTracks(context: Context, table: MediaTable, cueInfo: CueInfo,
+  sourceItem?: VideoItem): Promise<VideoItem[]> {
+  const items = await buildCueTrackItems(context, table, cueInfo, sourceItem);
+  if (items.length === 0) {
+    return [];
+  }
+
+  const savedItems: VideoItem[] = [];
+  for (let i = 0; i < items.length; i++) {
+    const success = await table.saveOrUpdateLocalItem(items[i]);
+    if (success) {
+      savedItems.push(items[i]);
+    }
+  }
+  return savedItems;
+}
+
 // 核心解析逻辑
 function parseCueContent(content: string): CueInfo {
   const result: CueInfo = {
@@ -254,4 +454,4 @@ export async function getHwMediaDuration(filePath: string): Promise<number> {
     console.error(`Duration  extraction failed: ${err.code},  ${err.message}`);
     return 0;
   }
-}
+}

+ 17 - 13
entry/src/main/ets/view/CueComptent.ets

@@ -1,9 +1,4 @@
-import { common } from '@kit.AbilityKit';
 import { CommonConstants } from '../common/constants/CommonConstants';
-import { VideoItem } from '../viewmodel/VideoItem';
-import { ArrayUtil, FileUtil, LogUtil, PreferencesUtil, StrUtil, ToastUtil } from '@pura/harmony-utils';
-import MediaTable from '../common/util/MediaTable';
-import { taskpool } from '@kit.ArkTS';
 import { CueTrack, millisecondsToTime } from '../common/util/CueUtils';
 
 // 批量删除
@@ -14,17 +9,12 @@ export struct CueComptent {
   @State curIndex:number = -1
   onDoItemClick = (item: CueTrack, index: number) => {
   }
+  onSplitTracks = () => {
+  }
   onCancel = () => {
   }
   @Prop cueTracks: CueTrack[]
   @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
-  context = this.getUIContext().getHostContext() as common.UIAbilityContext
-
-
-  async aboutToAppear() {
-
-
-  }
 
   build() {
     Column() {
@@ -57,6 +47,20 @@ export struct CueComptent {
 
       this.getListView()
 
+      Row() {
+        Button('分轨')
+          .width('100%')
+          .height(44)
+          .borderRadius(22)
+          .backgroundColor(this.themeColor)
+          .fontColor(Color.White)
+          .fontSize(15)
+          .onClick(() => {
+            this.onSplitTracks()
+          })
+      }
+      .padding({ left: 24, right: 24, top: 12, bottom: 20 })
+
 
     }
     .backgroundColor($r('app.color.start_window_background'))
@@ -81,7 +85,7 @@ export struct CueComptent {
                   .fontSize(14)
                   .padding({ left: 6 })
                   .fontColor(this.curIndex==index?this.themeColor:$r('app.color.text_color'))
-                  .maxLines(1)
+                  .maxLines(2)
                   .textOverflow({ overflow: this.isLongNameRoLL?TextOverflow.MARQUEE:TextOverflow.Ellipsis })//超长滚动
 
                 Text(item.performer || '')

+ 146 - 26
entry/src/main/ets/view/LocalMusic.ets

@@ -144,7 +144,16 @@ import { KnockController } from '../controller/KnockController';
 import { DeleteComptent } from '../view/DeleteComptent';
 import { ABLoopComptent } from '../view/ABLoopComptent';
 import { FixMessyView } from '../view/FixMessyView';
-import { parseCueFile,CueTrack,CueInfo } from '../common/util/CueUtils';
+import {
+  parseCueFile,
+  CueTrack,
+  CueInfo,
+  getCueTrackDuration,
+  getCueTrackEndOffset,
+  getCueTrackStartOffset,
+  importCueTracks,
+  isCueSplitItem
+} from '../common/util/CueUtils';
 import { CueComptent } from '../view/CueComptent';
 import PlaylistTable from '../common/util/PlaylistTable';
 import { showAddToPlaylistDialog } from '../dialog/AddToPlaylistDialog';
@@ -7433,7 +7442,13 @@ export struct LocalMusic {
         }
 
         AppStorage.setOrCreate('currentSong',this.currentSong) ;
-        this.videoUrl = this.currentSong.filePath
+        try {
+          const localUrl = await this.resolvePlaybackUrlForCurrentSong('doPlay-local');
+          this.videoUrl = localUrl || this.currentSong.filePath
+        } catch (error) {
+          Logger.error(TAG, `本地歌曲构建播放地址失败: ${(error as Error).message}`);
+          this.videoUrl = this.currentSong.filePath
+        }
         this.name = this.currentSong.name
         this.cover = this.currentSong.pixelMapPath
         this.artist = this.currentSong.artist
@@ -7520,6 +7535,28 @@ export struct LocalMusic {
   }
 
   private cueComponentId: number = 0
+  private async handleCueTrackImport(cueinfo: CueInfo): Promise<void> {
+    try {
+      let sourceItem = Utility.getItemByFilePath(this.videoLocalList, cueinfo.filePath);
+      if (!sourceItem) {
+        sourceItem = await this.table.queryVideoByFilePath(cueinfo.filePath) || undefined;
+      }
+
+      const importedItems = await importCueTracks(this.context, this.table, cueinfo, sourceItem);
+      if (ArrayUtil.isEmpty(importedItems)) {
+        ToastUtil.showToast('未导入到任何分轨');
+        return;
+      }
+
+      this.getUIContext().getPromptAction().closeCustomDialog(this.cueComponentId);
+      await this.resetAndLoadFirstPage();
+      ToastUtil.showToast(`分轨完成,已入库 ${importedItems.length} 首`);
+    } catch (error) {
+      Logger.error(TAG, `CUE 分轨入库失败: ${(error as Error).message}`);
+      ToastUtil.showToast('分轨失败');
+    }
+  }
+
   //显示cue分轨列表对话框
   showCueDialog(cueTracks:CueTrack[],cueinfo:CueInfo){
     this.getUIContext().getPromptAction().openCustomDialog({
@@ -7554,6 +7591,9 @@ export struct LocalMusic {
       onCancel:()=>{
         this.getUIContext().getPromptAction().closeCustomDialog(this.cueComponentId)
       },
+      onSplitTracks: async ()=>{
+        await this.handleCueTrackImport(cueinfo)
+      },
       onDoItemClick: async (item: CueTrack, index: number)=>{
         //获取cueinfo对应的filePath
         let globalVideoList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL) as VideoItem[];
@@ -8191,7 +8231,7 @@ export struct LocalMusic {
       return this.favList;
     }
     this.playQueueScope = 'view';
-    return this.videoLocalList.filter((item: VideoItem) => item.type === CommonConstants.TYPE_LOCAL);
+    return Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL);
   }
 
   private async tryLoadNextPageForPlayback(): Promise<void> {
@@ -8205,7 +8245,7 @@ export struct LocalMusic {
       return;
     }
     await this.loadNextPage();
-    const nextList = this.videoLocalList.filter((item: VideoItem) => item.type === CommonConstants.TYPE_LOCAL);
+    const nextList = Utility.getGlobalList(this.videoLocalList, CommonConstants.TYPE_LOCAL);
     if (nextList.length > this.songList.length) {
       this.songList = nextList;
       this.sonDataSource.pushArrayData(this.songList);
@@ -15411,6 +15451,10 @@ export struct LocalMusic {
 
   //startOffset 从cue分轨时间开始播
   private startPlayOrResumePlay(startOffset?:number) {
+    let finalStartOffset = startOffset
+    if ((finalStartOffset === undefined || finalStartOffset === null) && this.currentSong && isCueSplitItem(this.currentSong)) {
+      finalStartOffset = 0
+    }
     //新版本ijkplayer可以直接播放dsf歌,不需要转格式了。所以注释掉代码
     //针对dsf文件高采样率,统一转wav播放
     // const sampleRate = this.currentSong?.sampleRate||0
@@ -15434,7 +15478,7 @@ export struct LocalMusic {
     // }else{
     //   this.startPlay(startOffset)
     // }
-    this.startPlay(startOffset)
+    this.startPlay(finalStartOffset)
     // 仅当前播放的是网盘歌曲时,才提前缓存下一首
     // 排除用户拖动进度条的情况
     if (this.currentSong && isRemoteCloudType(this.currentSong.type) && !this.isSeekTo) {
@@ -15488,8 +15532,9 @@ export struct LocalMusic {
       return;
     }
 
+    const targetFilePath = this.currentSong?.filePath || filePath;
     let lastPlayTime = DateUtil.getTodayStr('yyyy-MM-dd HH:mm:ss')
-    this.table.updateLastPlayedStrByFilePath(filePath, lastPlayTime, (success: boolean, error?: string) => {
+    this.table.updateLastPlayedStrByFilePath(targetFilePath, lastPlayTime, (success: boolean, error?: string) => {
       if (success) {
         this.getHistoryList(false)
         console.log(" onecold 更新最近播放时间成功,数据库已同步");
@@ -15690,8 +15735,27 @@ export struct LocalMusic {
       return;
     }
     const shouldRefreshUi = this.isAppForeground && this.isPageVisible;
-    let position = this.mIjkMediaPlayer.getCurrentPosition();
-    let duration = this.mIjkMediaPlayer.getDuration();
+    const rawPosition = this.mIjkMediaPlayer.getCurrentPosition();
+    const rawDuration = this.mIjkMediaPlayer.getDuration();
+    const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined;
+    const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0;
+    const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0;
+    const cueTrackDuration = cueTrackItem ? getCueTrackDuration(cueTrackItem) : 0;
+    let position = rawPosition;
+    let duration = rawDuration;
+    if (cueTrackItem) {
+      position = Math.max(0, rawPosition - cueTrackStartOffset);
+      if (cueTrackDuration > 0) {
+        duration = cueTrackDuration;
+      } else if (cueTrackEndOffset > cueTrackStartOffset) {
+        duration = cueTrackEndOffset - cueTrackStartOffset;
+      } else if (rawDuration > cueTrackStartOffset) {
+        duration = rawDuration - cueTrackStartOffset;
+      }
+      if (duration > 0 && position > duration) {
+        position = duration;
+      }
+    }
     if (duration <= 0 && this.duration > 0) {
       duration = this.duration;
     }
@@ -15720,7 +15784,7 @@ export struct LocalMusic {
       position = duration;
     }
     if (shouldRefreshUi) {
-      const lyricPosition = position + this.timeOffset * 1000;
+      const lyricPosition = (cueTrackItem ? rawPosition : position) + this.timeOffset * 1000;
       this.isCurrentTime = true;
       this.updateLyricPosition(lyricPosition);
 
@@ -15745,6 +15809,9 @@ export struct LocalMusic {
 
 
     if (this.mIjkMediaPlayer.isPlaying()) {
+      const isCueTrackCompleted = cueTrackItem &&
+        cueTrackEndOffset > cueTrackStartOffset &&
+        rawPosition >= cueTrackEndOffset;
 
       //如果是开启AB循环
       if(this.isOpenAB){
@@ -15755,15 +15822,15 @@ export struct LocalMusic {
       }
 
       // 判断是否播放到结束时间
-      if (this.isOpenJump && duration > this.jumpEndTime * 1000) {
-        if (position >= duration - this.jumpEndTime * 1000) {
+      if ((this.isOpenJump && duration > this.jumpEndTime * 1000 &&
+        position >= duration - this.jumpEndTime * 1000) || isCueTrackCompleted) {
           if(this.playType == 1){//跳过尾部如果是单曲循环就选择单曲循环
             this.CONTROL_PlayStatus = PlayStatus.INIT
             this.startPlayOrResumePlay();
           }else if (this.playType == 2) { //2:正常播放,单片播完
 
             this.showRePlay();
-            this.currentTime = this.stringForTime(this.mIjkMediaPlayer.getDuration());
+            this.currentTime = this.stringForTime(duration);
             this.progressValue = this.PROGRESS_MAX_VALUE;
             this.slideEnable = false;
             this.stop();
@@ -15775,7 +15842,7 @@ export struct LocalMusic {
               Logger.info('heanup setProgress', '连续播放不循环模式:已到达最后一首,停止播放');
               ToastUtil.showToast('已经是最后一首了');
               this.showRePlay();
-              this.currentTime = this.stringForTime(this.mIjkMediaPlayer.getDuration());
+              this.currentTime = this.stringForTime(duration);
               this.progressValue = this.PROGRESS_MAX_VALUE;
               this.slideEnable = false;
               this.stop();
@@ -15791,7 +15858,6 @@ export struct LocalMusic {
             });
           }
 
-        }
       }
 
     }
@@ -16130,17 +16196,21 @@ export struct LocalMusic {
           await this.initLyric(lyricPath);
 
           const applyDuration = (durationMs: number): void => {
-            if (durationMs <= 0) {
+            const cueTrackDuration = this.currentSong && isCueSplitItem(this.currentSong)
+              ? getCueTrackDuration(this.currentSong)
+              : 0;
+            const effectiveDuration = cueTrackDuration > 0 ? cueTrackDuration : durationMs;
+            if (effectiveDuration <= 0) {
               return;
             }
-            this.duration = durationMs;
-            this.durationTime = Math.floor(durationMs / 1000);
+            this.duration = effectiveDuration;
+            this.durationTime = Math.floor(effectiveDuration / 1000);
             this.durationStringTime = secondToTime(this.durationTime);
-            this.totalTime = this.stringForTime(durationMs);
+            this.totalTime = this.stringForTime(effectiveDuration);
             if (this.currentSong) {
               this.currentSong.duration = this.totalTime;
               try {
-                this.avSessionController.setAVMetadataMusic(this.currentSong, durationMs, this.lyricContent);
+                this.avSessionController.setAVMetadataMusic(this.currentSong, effectiveDuration, this.lyricContent);
               } catch (metaError) {
                 Logger.warn(TAG, `heanup 首次播放写入AVSession失败: ${(metaError as Error).message}`);
               }
@@ -16246,8 +16316,12 @@ export struct LocalMusic {
             this.updateSessionPlayState(true)
             this.setCurrentPlayMode()
 
-            if(startOffset&&startOffset>0){//cue分轨播放
-              this.seekTo(startOffset  + "");
+            const cueTrackStartOffset = this.currentSong && isCueSplitItem(this.currentSong)
+              ? getCueTrackStartOffset(this.currentSong)
+              : 0;
+            if ((startOffset && startOffset > 0) || cueTrackStartOffset > 0) {//cue分轨播放
+              const cueSeekValue = cueTrackStartOffset > 0 && (!startOffset || startOffset <= 0) ? 0 : startOffset;
+              this.seekTo((cueSeekValue || 0) + "");
               this.saveLastPlayList()
               return
             }
@@ -16291,7 +16365,7 @@ export struct LocalMusic {
 
           }
           const playerDuration = this.mIjkMediaPlayer.getDuration()
-          if (playerDuration > 0) {
+          if (playerDuration > 0 && (!this.currentSong || !isCueSplitItem(this.currentSong))) {
             this.duration = playerDuration
           }
           const memoryPosition = this.pendingMemorySeekPosition > 0 ?
@@ -16398,7 +16472,7 @@ export struct LocalMusic {
         } else if (this.playType == 2) { //2:正常播放,单曲播完
 
           that.showRePlay();
-          that.currentTime = that.stringForTime(this.mIjkMediaPlayer.getDuration());
+          that.currentTime = that.stringForTime(that.getActiveDuration());
           that.progressValue = this.PROGRESS_MAX_VALUE;
           that.slideEnable = false;
           that.stop();
@@ -17182,6 +17256,9 @@ export struct LocalMusic {
     } else if (this.mIjkMediaPlayer != null) {
       // 本地播放器模式
       curPosition = this.mIjkMediaPlayer.getCurrentPosition();
+      if (this.currentSong && isCueSplitItem(this.currentSong)) {
+        curPosition = Math.max(0, curPosition - getCueTrackStartOffset(this.currentSong));
+      }
     } else {
       return;
     }
@@ -17198,6 +17275,12 @@ export struct LocalMusic {
     await this.seekTo(seeTime + "");
   };
   private getActiveDuration(): number {
+    if (this.currentSong && isCueSplitItem(this.currentSong)) {
+      const cueDuration = getCueTrackDuration(this.currentSong);
+      if (cueDuration > 0) {
+        return cueDuration;
+      }
+    }
     let duration = this.mIjkMediaPlayer.getDuration();
     if (this.castControllerWrapper && this.isCastPlaying) {
       return this.duration > 0 ? this.duration : duration;
@@ -17333,6 +17416,9 @@ export struct LocalMusic {
     } else if (this.mIjkMediaPlayer != null) {
       // 本地播放器模式
       curPosition = this.mIjkMediaPlayer.getCurrentPosition();
+      if (this.currentSong && isCueSplitItem(this.currentSong)) {
+        curPosition = Math.max(0, curPosition - getCueTrackStartOffset(this.currentSong));
+      }
     } else {
       return;
     }
@@ -17413,9 +17499,16 @@ export struct LocalMusic {
   //保持记忆播放功能
   private savePlaybackPosition() {
     if (this.mIjkMediaPlayer != null) {
-      const position = this.mIjkMediaPlayer.getCurrentPosition();
+      let position = this.mIjkMediaPlayer.getCurrentPosition();
       // ToastUtil.showToast('保存记忆播放 = ' + position)
-      const duration = this.mIjkMediaPlayer.getDuration();
+      let duration = this.mIjkMediaPlayer.getDuration();
+      if (this.currentSong && isCueSplitItem(this.currentSong)) {
+        position = Math.max(0, position - getCueTrackStartOffset(this.currentSong));
+        const cueDuration = getCueTrackDuration(this.currentSong);
+        if (cueDuration > 0) {
+          duration = cueDuration;
+        }
+      }
       const threshold = 5000; // 阈值,单位为毫秒(这里设为5秒)
 
       // 如果播放位置接近视频末尾,则保存 position 为 0
@@ -17432,6 +17525,9 @@ export struct LocalMusic {
       const stablePath = song.remote_rel_path || song.id || song.baiduFsId || song.filePath || playbackUrl || '';
       return `memory_play_remote:${song.type}:${accountId}:${stablePath}`;
     }
+    if (song && isCueSplitItem(song)) {
+      return `memory_play_local_cue:${song.filePath}`;
+    }
     return playbackUrl || '';
   }
 
@@ -17525,10 +17621,19 @@ export struct LocalMusic {
     if (this.castControllerWrapper && this.isCastPlaying) {
       try {
         let seekPos = Number.parseInt(value);
+        const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined;
+        const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0;
+        const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0;
         const duration = this.getActiveDuration();
         if (duration > 0) {
           seekPos = Math.max(0, Math.min(seekPos, duration - 200));
         }
+        if (cueTrackItem) {
+          seekPos += cueTrackStartOffset;
+          if (cueTrackEndOffset > cueTrackStartOffset) {
+            seekPos = Math.min(seekPos, cueTrackEndOffset - 200);
+          }
+        }
         this.beginSeekTrace(source, seekPos);
         this.isSeekTo = true;
         Logger.info(
@@ -17553,6 +17658,9 @@ export struct LocalMusic {
       if (Number.isNaN(seekPos)) {
         return;
       }
+      const cueTrackItem = this.currentSong && isCueSplitItem(this.currentSong) ? this.currentSong : undefined;
+      const cueTrackStartOffset = cueTrackItem ? getCueTrackStartOffset(cueTrackItem) : 0;
+      const cueTrackEndOffset = cueTrackItem ? getCueTrackEndOffset(cueTrackItem) : 0;
       const duration = this.getActiveDuration();
       const isRemoteSong = this.currentSong ? isRemoteCloudType(this.currentSong.type) : false;
       if (duration > 0) {
@@ -17560,6 +17668,12 @@ export struct LocalMusic {
       } else if (seekPos < 0) {
         seekPos = 0;
       }
+      if (cueTrackItem) {
+        seekPos += cueTrackStartOffset;
+        if (cueTrackEndOffset > cueTrackStartOffset) {
+          seekPos = Math.min(seekPos, cueTrackEndOffset - 200);
+        }
+      }
       if (isRemoteSong && duration <= 0 && seekPos > 0) {
         Logger.warn(TAG, `seekTo 跳过未知时长远程seek: ${seekPos}ms`);
         return;
@@ -17628,6 +17742,9 @@ export struct LocalMusic {
       Logger.info('heanup backForward', `投播模式当前位置: ${pos}ms`);
     } else if (this.mIjkMediaPlayer) {
       pos = this.mIjkMediaPlayer.getCurrentPosition();
+      if (this.currentSong && isCueSplitItem(this.currentSong)) {
+        pos = Math.max(0, pos - getCueTrackStartOffset(this.currentSong));
+      }
     } else {
       return;
     }
@@ -17648,6 +17765,9 @@ export struct LocalMusic {
       Logger.info('heanup fastForward', `投播模式当前位置: ${pos}ms`);
     } else if (this.mIjkMediaPlayer) {
       pos = this.mIjkMediaPlayer.getCurrentPosition();
+      if (this.currentSong && isCueSplitItem(this.currentSong)) {
+        pos = Math.max(0, pos - getCueTrackStartOffset(this.currentSong));
+      }
     } else {
       return;
     }
@@ -17690,7 +17810,7 @@ export struct LocalMusic {
           Logger.info('heanup playNext', '连续播放不循环模式:已到达最后一首,停止播放');
           ToastUtil.showToast('已经是最后一首了');
           this.showRePlay();
-          this.currentTime = this.stringForTime(this.mIjkMediaPlayer.getDuration());
+          this.currentTime = this.stringForTime(this.getActiveDuration());
           this.progressValue = this.PROGRESS_MAX_VALUE;
           this.slideEnable = false;
           this.stop();