Bläddra i källkod

增加网盘音乐的记忆播放
修复网盘播放偶尔闪退的bug
修复网盘搜索的bug

onecold 5 månader sedan
förälder
incheckning
4aa8141614

+ 5 - 6
entry/src/main/ets/pages/NewIndex.ets

@@ -1507,7 +1507,11 @@ struct NewIndex {
             .fontWeight(480)
 
           Blank()
-
+          Image($r('app.media.arrow_right'))
+            .width(22)
+            .height(22)
+            .margin({ left: 20, right: 0 })
+            .align(Alignment.Center)
         }
         .width('100%')
         .height(55)
@@ -1564,11 +1568,6 @@ struct NewIndex {
 
             Blank()
 
-            Image($r('app.media.arrow_right'))
-              .width(22)
-              .height(22)
-              .margin({ left: 20, right: 0 })
-              .align(Alignment.Center)
           }
           .width('100%')
           .height(60)

+ 246 - 38
entry/src/main/ets/view/LocalMusic.ets

@@ -588,6 +588,20 @@ export struct LocalMusic {
 
   // WebDAV认证信息缓存(作为实例变量,比全局上下文更可靠)
   private currentWebDavAuthInfo: WebDavAuthItem | null = null;
+  // 当前实际播放中的记忆key,避免切歌时 currentSong 已切换导致串写
+  private activePlaybackMemoryKey: string = '';
+  // 网盘流地址记忆恢复token,切歌后用于取消旧的延迟seek任务
+  private remoteRestoreSeekToken: number = 0;
+  // 本次是否需要先seek到记忆点再开始播放
+  private waitInitialSeekBeforeStart: boolean = false;
+  // 本次播放记忆点(毫秒)
+  private pendingMemorySeekPosition: number = 0;
+  // 记忆播放seek兜底,避免一直转圈
+  private initialSeekGuardTimer: number = 0;
+  private readonly initialSeekGuardTimeoutMs: number = 2000;
+  private readonly remoteMemorySeekInitialDelayMs: number = 120;
+  private readonly remoteMemorySeekRetryIntervalMs: number = 80;
+  private readonly remoteMemorySeekMaxRetryCount: number = 6;
 
   //瀑布流的列数横竖屏动态切换
   onIsLandscapeChange() {
@@ -2057,6 +2071,7 @@ export struct LocalMusic {
       clearTimeout(this.mediaLibraryWarmupPrefetchTimer);
       this.mediaLibraryWarmupPrefetchTimer = 0;
     }
+    this.clearInitialSeekGuardTimer();
     this.alphaBetBuildVersion++;
     this.gridWaterLastActiveTs = 0;
     this.coverThumbCache.dispose();
@@ -4228,7 +4243,6 @@ export struct LocalMusic {
     }
     .padding({ top: this.topSafeHeight+10, left: 10, right: 10 })
     .width('100%')
-    .visibility(Visibility.Visible)
     .translate({
       x: 0,
       y: (this.isShowTitleBar || !this.autoHideTitle) ? 0 : -80
@@ -12495,7 +12509,7 @@ export struct LocalMusic {
   MenuImageBuilder(cover: string|undefined) {
     Menu(){
       MenuItem({
-        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.square_and_pencil')),
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.picture_2')),
         content: '设为壁纸'
       })
         .onClick(async() => {
@@ -12512,7 +12526,7 @@ export struct LocalMusic {
         })
 
       MenuItem({
-        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.arrow_up_to_line')),
+        symbolStartIcon: new SymbolGlyphModifier($r('sys.symbol.picture')),
         content: '更换封面'
       })
         .onClick(async() => {
@@ -14893,10 +14907,17 @@ export struct LocalMusic {
     if (this.CONTROL_PlayStatus == PlayStatus.INIT) {
       this.stopProgressTask();
       this.startProgressTask();
+      this.clearInitialSeekGuardTimer();
+      this.activePlaybackMemoryKey = this.getPlaybackMemoryKey();
+      this.remoteRestoreSeekToken++;
+      const shouldMemoryPlay = PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false)
+        || (this.isMemoryLastPlay && this.isFirstStartPlay);
+      this.pendingMemorySeekPosition = shouldMemoryPlay ? PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0) : 0;
+      const isRemoteStreamSong = this.currentSong && isRemoteCloudType(this.currentSong.type) &&
+        this.videoUrl.includes('://');
+      this.waitInitialSeekBeforeStart = !!isRemoteStreamSong && this.pendingMemorySeekPosition > 0;
       // 判断是否是HTTP或在线网盘视频,显示加载进度
-      if (this.currentSong && isRemoteCloudType(this.currentSong.type)) {
-        this.isPlayerLoading = true;
-      }
+      this.isPlayerLoading = this.waitInitialSeekBeforeStart;
       this.play(this.videoUrl.toString(),startOffset);
     }
     if (this.CONTROL_PlayStatus == PlayStatus.PAUSE) {
@@ -15127,6 +15148,57 @@ export struct LocalMusic {
     this.replayVisible = Visibility.Visible;
   }
 
+  private clearInitialSeekGuardTimer(): void {
+    if (this.initialSeekGuardTimer) {
+      clearTimeout(this.initialSeekGuardTimer);
+      this.initialSeekGuardTimer = 0;
+    }
+  }
+
+  private ensurePlayerStartedAfterInitialSeek(reason: string): void {
+    try {
+      if (!this.mIjkMediaPlayer.isPlaying()) {
+        this.mIjkMediaPlayer.start();
+      }
+      this.CONTROL_PlayStatus = PlayStatus.PLAY;
+      this.setIsPlaying(true);
+      this.updateSessionPlayState(true);
+      Logger.info(TAG, `记忆播放恢复开播: ${reason}`);
+    } catch (error) {
+      Logger.error(TAG, `记忆播放恢复开播失败(${reason}): ${(error as Error).message}`);
+    }
+  }
+
+  private finishInitialSeekWait(reason: string, shouldStart: boolean): void {
+    this.clearInitialSeekGuardTimer();
+    const wasWaiting = this.waitInitialSeekBeforeStart;
+    this.waitInitialSeekBeforeStart = false;
+    this.pendingMemorySeekPosition = 0;
+    this.isPlayerLoading = false;
+    this.hideLoadIng();
+    if (wasWaiting) {
+      Logger.info(TAG, `结束记忆seek等待: ${reason}`);
+    }
+    if (shouldStart) {
+      this.ensurePlayerStartedAfterInitialSeek(reason);
+    }
+  }
+
+  private startInitialSeekGuardTimer(scene: string): void {
+    this.clearInitialSeekGuardTimer();
+    if (!this.waitInitialSeekBeforeStart || this.pendingMemorySeekPosition <= 0) {
+      return;
+    }
+    const token = this.remoteRestoreSeekToken;
+    this.initialSeekGuardTimer = setTimeout(() => {
+      if (token !== this.remoteRestoreSeekToken || !this.waitInitialSeekBeforeStart) {
+        return;
+      }
+      Logger.warn(TAG, `记忆seek超时兜底开播, scene=${scene}, pos=${this.pendingMemorySeekPosition}`);
+      this.finishInitialSeekWait('seek-timeout', true);
+    }, this.initialSeekGuardTimeoutMs);
+  }
+
 
   private async play(url: string,startOffset?:number) {
     this.lyricController.setLyric(null)
@@ -15148,7 +15220,12 @@ export struct LocalMusic {
 
 
     let that = this;
-    that.showLoadIng();
+    if (this.waitInitialSeekBeforeStart) {
+      that.showLoadIng();
+    } else {
+      that.hideLoadIng();
+    }
+    const startOnPreparedOption = this.waitInitialSeekBeforeStart ? "0" : "1";
     this.isOpenAB = false
     //设置XComponent回调的context
     if (!!this.mContext) {
@@ -15243,7 +15320,7 @@ export struct LocalMusic {
       // 缓冲和播放优化设置
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "1024000"); // 增大缓冲区
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "50"); // 减少最小帧数
-      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1"); // 预加载启动
+      this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", startOnPreparedOption); // 预加载启动
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0"); // 无缓冲播放
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "30000"); // 最大缓存30秒
       this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1"); // 无限制收流
@@ -15290,7 +15367,7 @@ export struct LocalMusic {
     this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100"); // 设置最小缓冲帧数
     this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-frames", "1000"); // 设置最大缓冲
     //启动预加载
-    this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1");
+    this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", startOnPreparedOption);
     // 设置无缓冲,这是播放器的缓冲区,有数据就播放
     this.mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0");
     //跳帧处理,放CPU处理较慢时,进行跳帧处理,保证播放流程,画面和声音同步
@@ -15337,15 +15414,19 @@ export struct LocalMusic {
         LogUtils.getInstance()
           .LOGI("setOnVideoSizeChangedListener-->go:" + width + "," + height + "," + sar_num + "," + sar_den);
 
-        that.hideLoadIng();
+        if (!that.waitInitialSeekBeforeStart) {
+          that.hideLoadIng();
+        }
       }
     );
     this.mIjkMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
     let mOnPreparedListener: OnPreparedListener = new ImplOnPreparedListener(
       async () => {
         LogUtils.getInstance().LOGI("setOnPreparedListener-->go");
-        // 隐藏加载进度
-        this.isPlayerLoading = false;
+        if (!this.waitInitialSeekBeforeStart) {
+          // 无记忆seek时,prepared后可直接隐藏加载
+          this.isPlayerLoading = false;
+        }
         try {
           //保存碰一碰分享的当前的歌曲
           AppStorage.setOrCreate('currentSong',this.currentSong) ;
@@ -15480,6 +15561,11 @@ export struct LocalMusic {
             (this.videoUrl.toLowerCase().endsWith('.rmvb') || this.videoUrl.toLowerCase().endsWith('.flac'))) {
             timeoutPlay = 400
           }
+          // 网盘流地址在prepare后立刻seek容易触发底层不稳定,统一延后记忆恢复
+          if (this.currentSong && isRemoteCloudType(this.currentSong.type) && this.videoUrl.includes('://')) {
+            timeoutPlay = Math.max(timeoutPlay,
+              this.waitInitialSeekBeforeStart ? this.remoteMemorySeekInitialDelayMs : 700);
+          }
 
           //跳过片头片尾关键代码,以下是跳过片头的关键代码
           if (this.modeType === 0) { //文件夹
@@ -15506,23 +15592,39 @@ export struct LocalMusic {
             }
 
           }
-          this.duration = this.mIjkMediaPlayer.getDuration()
+          const playerDuration = this.mIjkMediaPlayer.getDuration()
+          if (playerDuration > 0) {
+            this.duration = playerDuration
+          }
+          const memoryPosition = this.pendingMemorySeekPosition > 0 ?
+            this.pendingMemorySeekPosition : PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0);
+          if (this.waitInitialSeekBeforeStart && memoryPosition > 0) {
+            let targetSeekPosition = memoryPosition;
+            if (this.isOpenJump) {
+              targetSeekPosition = Math.max(targetSeekPosition, this.jumpTopTime * 1000);
+            }
+            this.pendingMemorySeekPosition = targetSeekPosition;
+            this.startInitialSeekGuardTimer('prepared-memory-seek');
+            Logger.info(TAG, `准备记忆播放seek: ${targetSeekPosition}ms, timeout=${timeoutPlay}ms`);
+            setTimeout(() => {
+              if (!this.waitInitialSeekBeforeStart) {
+                return;
+              }
+              this.restorePlaybackPosition();
+            }, timeoutPlay);
+            this.isFirstStartPlay = false
+            this.saveLastPlayList()
+            return
+          }
+
           if (this.isOpenJump) { //用户是否设置跳过片头片尾
             if (this.mIjkMediaPlayer != null) {
               let jumpTopTimeMs = this.jumpTopTime*1000;
               if (jumpTopTimeMs <= this.duration) {
                 // 调用 seekTo 方法,跳到指定时间
-                if(PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY,true)){
-                  console.log('onecold jumpTopTime = '+jumpTopTimeMs);
-                  const position:number = PreferencesUtil.getNumberSync(this.videoUrl, 0);
-                  console.log('onecold position = '+position);
-                  if (position > jumpTopTimeMs) {
-                    jumpTopTimeMs = position
-                  }
-                }
                 setTimeout(() => { //rmvb和flac格式马上记忆播放会报错,会延迟1s在读取记忆播放。
                   if (StrUtil.isNotEmpty(this.videoUrl) && !this.videoUrl.toLowerCase().endsWith('.ts')) {
-                    this.seekTo(this.jumpTopTime * 1000 + ""); // 跳到开头
+                    this.seekTo(jumpTopTimeMs + ""); // 跳到开头或记忆进度中更靠后的位置
                   }
 
                 }, timeoutPlay)
@@ -15543,7 +15645,8 @@ export struct LocalMusic {
             //记忆播放(每一首都会记忆播放)
             if (StrUtil.isNotEmpty(this.videoUrl) && !this.videoUrl.toLowerCase().endsWith('.ts')) {
               setTimeout(() => { //rmvb和flac格式马上记忆播放会报错,会延迟1s在读取记忆播放。
-                if (PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false)) {
+                if (this.pendingMemorySeekPosition > 0 ||
+                  PreferencesUtil.getBooleanSync(SettingPage.iS_MUSIC_MEMORY_PLAY, false)) {
                   this.restorePlaybackPosition() //读取播放记忆功能
                 } else if (this.isMemoryLastPlay && this.isFirstStartPlay) {
                   console.error('onecold 最后一首:读取播放记忆功能  = ' + this.isFirstStartPlay);
@@ -15565,6 +15668,9 @@ export struct LocalMusic {
         } catch (error) {
           let err: BusinessError = error as BusinessError;
           console.error('onecold failed with err: ' + JSON.stringify(err));
+          if (this.waitInitialSeekBeforeStart) {
+            this.finishInitialSeekWait('prepared-exception', true);
+          }
         }
 
 
@@ -15646,13 +15752,15 @@ export struct LocalMusic {
     let mOnSeekCompleteListener: OnSeekCompleteListener = new ImplOnSeekCompleteListener(
       () => {
         LogUtils.getInstance().LOGI("OnSeekCompleteListener-->go");
-        console.info('onecold startPlayOrResumePlay 10643')
         // seek 完成,延迟恢复播放,确保 ijkplayer 内部状态完全稳定
         setTimeout(() => {
-          // seek 完成,隐藏转圈圈并重置标志
-          that.hideLoadIng();
+          const waitingInitialSeek = that.waitInitialSeekBeforeStart;
           that.isSeekTo = false;
-          that.startPlayOrResumePlay();
+          if (waitingInitialSeek) {
+            that.finishInitialSeekWait('seek-complete', true);
+          } else {
+            that.hideLoadIng();
+          }
         }, 200); // 延迟200ms,确保ijkplayer内部状态稳定
 
       }
@@ -15662,7 +15770,9 @@ export struct LocalMusic {
     let mOnInfoListener: OnInfoListener = new ImplOnInfoListener(
       (what: number, extra: number) => {
         LogUtils.getInstance().LOGI("OnInfoListener-->go:" + what + "===" + extra);
-        that.hideLoadIng();
+        if (!that.waitInitialSeekBeforeStart) {
+          that.hideLoadIng();
+        }
       }
     );
     this.mIjkMediaPlayer.setOnInfoListener(mOnInfoListener);
@@ -15670,8 +15780,7 @@ export struct LocalMusic {
 
     let mOnErrorListener: OnErrorListener = new ImplOnErrorListener(
       (what: number, extra: number) => {
-        // 隐藏加载进度
-        this.isPlayerLoading = false;
+        this.finishInitialSeekWait('player-error', false);
         this.stopProgressTask();
         console.info("heanup OnErrorListener-->go:" + what + "===" + extra + " this.videoUrl=" + this.videoUrl)
         console.info('heanup 播放错误,歌曲详情:' + JSON.stringify(this.currentSong))
@@ -16565,6 +16674,10 @@ export struct LocalMusic {
   }
 
   private stop() {
+    this.clearInitialSeekGuardTimer();
+    this.waitInitialSeekBeforeStart = false;
+    this.pendingMemorySeekPosition = 0;
+    this.isPlayerLoading = false;
     this.savePlaybackPosition();
     this.CONTROL_PlayStatus = PlayStatus.INIT;
     this.mIjkMediaPlayer.stop();
@@ -16586,18 +16699,99 @@ export struct LocalMusic {
 
       // 如果播放位置接近视频末尾,则保存 position 为 0
       const playbackPosition = (duration - position < threshold) ? 0 : position;
-      PreferencesUtil.putSync(this.videoUrl, playbackPosition);
+      const memoryKey = this.activePlaybackMemoryKey || this.getPlaybackMemoryKey();
+      PreferencesUtil.putSync(memoryKey, playbackPosition);
+
+    }
+  }
+
+  private buildPlaybackMemoryKey(song?: VideoItem, playbackUrl?: string): string {
+    if (song && isRemoteCloudType(song.type)) {
+      const accountId = song.webdav_account_id || 'unknown';
+      const stablePath = song.remote_rel_path || song.id || song.baiduFsId || song.filePath || playbackUrl || '';
+      return `memory_play_remote:${song.type}:${accountId}:${stablePath}`;
+    }
+    return playbackUrl || '';
+  }
+
+  private getPlaybackMemoryKey(): string {
+    return this.buildPlaybackMemoryKey(this.currentSong, this.videoUrl);
+  }
 
+  private restoreRemotePlaybackPositionSafely(position: number, token: number, retryCount: number = 0): void {
+    if (token !== this.remoteRestoreSeekToken) {
+      return;
+    }
+    if (!this.mIjkMediaPlayer || position <= 0) {
+      if (this.waitInitialSeekBeforeStart) {
+        this.finishInitialSeekWait('invalid-memory-position', true);
+      }
+      return;
     }
+    const duration = this.getActiveDuration();
+    const currentPos = this.mIjkMediaPlayer.getCurrentPosition();
+    let safePosition = position;
+    if (duration > 0) {
+      safePosition = Math.max(0, Math.min(position, duration - 500));
+    } else {
+      safePosition = Math.max(0, position);
+    }
+    if (safePosition <= 0) {
+      if (this.waitInitialSeekBeforeStart) {
+        this.finishInitialSeekWait('memory-position-too-small', true);
+      }
+      return;
+    }
+    if (retryCount === 0 && duration <= 0 && currentPos <= 0) {
+      Logger.info(TAG, `远程记忆seek提前尝试: pos=${safePosition}, duration=${duration}, current=${currentPos}`);
+    }
+    this.seekTo(safePosition + "");
+
+    if (retryCount >= this.remoteMemorySeekMaxRetryCount) {
+      return;
+    }
+    setTimeout(() => {
+      if (token !== this.remoteRestoreSeekToken || !this.waitInitialSeekBeforeStart || !this.mIjkMediaPlayer) {
+        return;
+      }
+      const latestPos = this.mIjkMediaPlayer.getCurrentPosition();
+      if (Math.abs(latestPos - safePosition) <= 1500) {
+        return;
+      }
+      this.restoreRemotePlaybackPositionSafely(position, token, retryCount + 1);
+    }, this.remoteMemorySeekRetryIntervalMs);
   }
 
   //获取记忆播放功能
   private restorePlaybackPosition() {
-    const position: number = PreferencesUtil.getNumberSync(this.videoUrl, 0);
-    if (this.mIjkMediaPlayer != null && position > 0) {
-      this.seekTo(position + "");
-
+    const position: number = this.pendingMemorySeekPosition > 0 ?
+      this.pendingMemorySeekPosition : PreferencesUtil.getNumberSync(this.getPlaybackMemoryKey(), 0);
+    if (!this.mIjkMediaPlayer || position <= 0) {
+      if (this.waitInitialSeekBeforeStart) {
+        this.finishInitialSeekWait('no-memory-position', true);
+      }
+      return;
+    }
+    const isRemoteSong = this.currentSong ? isRemoteCloudType(this.currentSong.type) : false;
+    if (isRemoteSong && this.videoUrl.includes('://')) {
+      const token = this.remoteRestoreSeekToken;
+      this.restoreRemotePlaybackPositionSafely(position, token);
+      return;
     }
+    const duration = this.getActiveDuration();
+    let safePosition = position;
+    if (duration > 0) {
+      safePosition = Math.max(0, Math.min(position, duration - 500));
+    } else {
+      safePosition = Math.max(0, position);
+    }
+    if (safePosition <= 0) {
+      if (this.waitInitialSeekBeforeStart) {
+        this.finishInitialSeekWait('memory-position-too-small', true);
+      }
+      return;
+    }
+    this.seekTo(safePosition + "");
   }
 
   private async seekTo(value: string) {
@@ -16630,14 +16824,28 @@ export struct LocalMusic {
     } else {
       // 本地播放器模式
       let seekPos = Number.parseInt(value);
+      if (Number.isNaN(seekPos)) {
+        return;
+      }
       const duration = this.getActiveDuration();
+      const isRemoteSong = this.currentSong ? isRemoteCloudType(this.currentSong.type) : false;
       if (duration > 0) {
         seekPos = Math.max(0, Math.min(seekPos, duration - 200));
+      } else if (seekPos < 0) {
+        seekPos = 0;
+      }
+      if (isRemoteSong && duration <= 0) {
+        Logger.info(TAG, `seekTo 远程歌曲时长未知,直接尝试seek: ${seekPos}ms`);
       }
       this.isSeekTo = true
-      this.mIjkMediaPlayer.seekTo(seekPos + "");
-      this.setProgress()
-      this.isSeekTo = false
+      try {
+        this.mIjkMediaPlayer.seekTo(seekPos + "");
+        this.setProgress()
+      } catch (error) {
+        Logger.error(TAG, `本地播放器seek失败: ${error}`);
+      } finally {
+        this.isSeekTo = false
+      }
     }
   }
 

+ 22 - 10
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -456,7 +456,7 @@ export struct RemoteMusicPage {
 
   // 更新所有 LazyForEach 数据源
   private updateAllDataSources(): void {
-    this.songDataSource.pushArrayData(this.allVideos);
+    this.songDataSource.pushArrayData(this.getVisibleSongs());
     this.artistDataSource.pushArrayData(this.artists);
     this.albumDataSource.pushArrayData(this.albums);
     this.playlistDataSource.pushArrayData(this.playlists);
@@ -1429,6 +1429,10 @@ export struct RemoteMusicPage {
     if (this.loading) {
       return;
     }
+    // 搜索结果列表不参与分页,避免滚动时被全量列表覆盖
+    if (this.selectedTab === 0 && this.isSearchMode && this.searchText.length > 0) {
+      return;
+    }
     // 详情视图模式下不支持加载更多(筛选数据是一次性加载的)
     if (this.isDetailView || this.filterType !== NavFilterType.None) {
       return;
@@ -2617,6 +2621,15 @@ export struct RemoteMusicPage {
     }
     .width('100%')
     .padding({ top: this.topSafeHeight + 5 })
+    .translate({
+      x: 0,
+      y: (this.isShowTitleBar || !this.autoHideTitle) ? 0 : -80
+    })
+    .opacity((this.isShowTitleBar || !this.autoHideTitle) ? 1 : 0)
+    .animation({
+      duration: 240,
+      curve: Curve.EaseInOut
+    })
     // .backgroundColor($r('app.color.start_window_background'))
   }
 
@@ -2731,9 +2744,12 @@ export struct RemoteMusicPage {
     const sortTime = Date.now() - startTime;
 
     // 更新显示列表
-    if (this.isSearchMode) {
+    if (this.isSearchMode && this.searchText.length > 0) {
       this.filteredList = [...songs];
+    } else {
+      this.allVideos = [...songs];
     }
+    this.songDataSource.pushArrayData(this.getVisibleSongs());
 
     // 记录排序结果
     void ServerLogUtil.info('NavidromeSort', `排序完成: ${sortTypeName}`);
@@ -2752,6 +2768,7 @@ export struct RemoteMusicPage {
     if (keyword.length === 0) {
       this.filteredList = [];
       this.isSearchLoading = false;
+      this.songDataSource.pushArrayData(this.getVisibleSongs());
       void ServerLogUtil.info('NavidromeSearch', '搜索已清空,显示所有歌曲');
       return;
     }
@@ -2800,6 +2817,7 @@ export struct RemoteMusicPage {
       const searchTime = Date.now() - startTime;
       if (!restSongs || restSongs.length === 0) {
         this.filteredList = [];
+        this.songDataSource.pushArrayData(this.getVisibleSongs());
         void ServerLogUtil.warn('NavidromeSearch', `搜索无结果: "${keyword}"`);
         void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
         return;
@@ -2809,7 +2827,6 @@ export struct RemoteMusicPage {
         return;
       }
       this.filteredList = videoItems;
-      this.songDataSource.pushArrayData(this.filteredList)
       this.doSortType(this.sortType);
       void ServerLogUtil.info('NavidromeSearch', `搜索完成: "${keyword}"`);
       void ServerLogUtil.info('NavidromeSearch', `- 搜索耗时: ${searchTime}ms`);
@@ -2820,6 +2837,7 @@ export struct RemoteMusicPage {
         return;
       }
       this.filteredList = [];
+      this.songDataSource.pushArrayData(this.getVisibleSongs());
       const message = (error as Error).message ?? 'Navidrome 搜索失败';
       ToastUtil.showToast(message);
       void ServerLogUtil.error('NavidromeSearch', `搜索失败: ${message}`);
@@ -4184,12 +4202,6 @@ export struct RemoteMusicPage {
       Column() {
         this.topTitleBar()
       }
-      .visibility(this.isShowTitleBar?Visibility.Visible:
-        this.autoHideTitle?Visibility.None:Visibility.Visible)
-      .animation({
-        duration: 500,
-        curve: Curve.Friction  // 可选动画曲线
-      })
 
     }
     .alignContent(Alignment.Top)
@@ -4209,7 +4221,7 @@ export struct RemoteMusicPage {
   getWaterView(){
     Scroll(this.scroller) {
       Column() {
-        if(!this.isWaterFlowScrolling&&this.isShowTitleBar){
+        if(!this.isWaterFlowScrolling&&(this.isShowTitleBar || !this.autoHideTitle)){
           Blank().height(this.topSafeHeight + 115)
         }
         WaterFlow({

+ 157 - 53
ijkplayer/src/main/cpp/ijkplayer/ff_ffplay.c

@@ -3094,6 +3094,35 @@ static int is_realtime(AVFormatContext *s)
     return 0;
 }
 
+static int is_network_url(const char *url)
+{
+    if (!url || !*url)
+        return 0;
+
+    // ijk 自定义协议包装层(底层通常仍是网络流)
+    if (av_stristart(url, "ijkhttphook:", NULL) ||
+        av_stristart(url, "ijkio:", NULL) ||
+        av_stristart(url, "ijklongurl:", NULL) ||
+        av_stristart(url, "ijksegment:", NULL) ||
+        av_stristart(url, "ijkasync:", NULL)) {
+        return 1;
+    }
+
+    // 只要存在 scheme 且不是本地 file://,默认按网络/远程处理
+    if (strstr(url, "://") && !av_stristart(url, "file://", NULL)) {
+        return 1;
+    }
+
+    return av_stristart(url, "http://", NULL) ||
+           av_stristart(url, "https://", NULL) ||
+           av_stristart(url, "ftp://", NULL) ||
+           av_stristart(url, "rtmp://", NULL) ||
+           av_stristart(url, "rtsp://", NULL) ||
+           av_stristart(url, "mmsh://", NULL) ||
+           av_stristart(url, "mms://", NULL) ||
+           av_stristart(url, "tcp://", NULL);
+}
+
 
 
 /* this thread gets the stream from the disk or the network */
@@ -3426,6 +3455,7 @@ static int read_thread(void *arg)
             int64_t seek_target = is->seek_pos;
             int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
             int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
+            int64_t effective_seek_target = seek_target;
 // FIXME the +-2 is due to rounding being not done in the correct direction in generation
 //      of the seek_pos/seek_rel variables
 
@@ -3433,65 +3463,129 @@ static int read_thread(void *arg)
             ffp_notify_msg3(ffp, FFP_MSG_BUFFERING_UPDATE, 0, 0);
 
             int seek_flags = is->seek_flags;
+            const int network_input = is_network_url(ffp->input_filename);
 
-            const char *format_name = (ic->iformat && ic->iformat->name) ? ic->iformat->name : "unknown";
-            av_log(ffp, AV_LOG_WARNING, "heanup seek: format_name=%s\n", format_name);
-
-            int is_asf_format = (ic->iformat && ic->iformat->name &&
-                                 (strcmp(ic->iformat->name, "asf") == 0 ||
-                                  strcmp(ic->iformat->name, "asf_o") == 0 ||
-                                  strstr(ic->iformat->name, "asf") != NULL));
-            int is_flac_format = (ic->iformat && ic->iformat->name &&
-                                  strstr(ic->iformat->name, "flac") != NULL);
-            int use_safe_byte_seek = is_asf_format;
-
-            if (use_safe_byte_seek) {
-                // Avoid avformat_seek_file for fragile formats on network streams.
-                const char *safe_seek_name = is_flac_format ? "FLAC" : "ASF";
-                if (!ic->pb || !(ic->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
-                    av_log(ffp, AV_LOG_WARNING, "heanup: %s stream is not byte-seekable, reject seek\n", safe_seek_name);
-                    ret = -1;
+            if (network_input && !(seek_flags & AVSEEK_FLAG_BYTE)) {
+                // 网络流优先使用字节seek,规避时间戳seek触发 ff_seek_frame_binary 的脆弱路径
+                int64_t bounded_target = seek_target;
+                if (bounded_target < 0) {
+                    bounded_target = 0;
+                }
+                if (ic->duration > 0 && bounded_target > ic->duration) {
+                    bounded_target = ic->duration;
+                }
+                effective_seek_target = bounded_target;
+
+                int64_t file_size = (ic->pb && (ic->pb->seekable & AVIO_SEEKABLE_NORMAL)) ? avio_size(ic->pb) : -1;
+                int64_t duration = ic->duration;
+                if (file_size > 1 && duration > 0) {
+                    int64_t byte_pos = av_rescale_rnd(bounded_target, file_size - 1, duration, AV_ROUND_DOWN);
+                    if (byte_pos < 0)
+                        byte_pos = 0;
+                    if (byte_pos >= file_size)
+                        byte_pos = file_size - 1;
+
+                    av_log(ffp, AV_LOG_WARNING,
+                           "heanup: network byte seek, target=%lld, duration=%lld, file_size=%lld, byte_pos=%lld\n",
+                           (long long)bounded_target, (long long)duration, (long long)file_size, (long long)byte_pos);
+                    int64_t io_seek_ret = avio_seek(ic->pb, byte_pos, SEEK_SET);
+                    if (io_seek_ret >= 0) {
+                        avformat_flush(is->ic);
+                        ret = 0;
+                        av_log(ffp, AV_LOG_WARNING, "heanup: network byte seek by avio_seek success, io_pos=%lld\n",
+                               (long long)io_seek_ret);
+                    } else {
+                        av_log(ffp, AV_LOG_WARNING, "heanup: avio_seek failed(%lld), fallback av_seek_frame byte\n",
+                               (long long)io_seek_ret);
+                        ret = av_seek_frame(is->ic, -1, byte_pos, AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD);
+                    }
                 } else {
-                    int64_t file_size = avio_size(ic->pb);
-                    int64_t duration = ic->duration;
-                    int64_t bounded_target = seek_target;
-
-                    if (duration > 0) {
-                        if (bounded_target < 0)
-                            bounded_target = 0;
-                        if (bounded_target > duration)
-                            bounded_target = duration;
+                    int64_t window = 3LL * AV_TIME_BASE;
+                    seek_min = bounded_target > window ? bounded_target - window : 0;
+                    seek_max = bounded_target + window;
+                    if (ic->duration > 0 && seek_max > ic->duration)
+                        seek_max = ic->duration;
+                    av_log(ffp, AV_LOG_WARNING,
+                           "heanup: network timestamp seek fallback, target=%lld, seek_min=%lld, seek_max=%lld, file_size=%lld, duration=%lld\n",
+                           (long long)bounded_target, (long long)seek_min, (long long)seek_max,
+                           (long long)file_size, (long long)duration);
+                    ret = avformat_seek_file(is->ic, -1, seek_min, bounded_target, seek_max, seek_flags);
+                }
+                if (ret < 0) {
+                    av_log(ffp, AV_LOG_WARNING, "heanup: network seek failed(%d)\n", ret);
+                }
+            } else {
+                const char *format_name = (ic->iformat && ic->iformat->name) ? ic->iformat->name : "unknown";
+                av_log(ffp, AV_LOG_WARNING, "heanup seek: format_name=%s\n", format_name);
+
+                int is_asf_format = (ic->iformat && ic->iformat->name &&
+                                     (strcmp(ic->iformat->name, "asf") == 0 ||
+                                      strcmp(ic->iformat->name, "asf_o") == 0 ||
+                                      strstr(ic->iformat->name, "asf") != NULL));
+                int use_safe_byte_seek = is_asf_format;
+                int64_t local_seek_target = seek_target;
+                int64_t local_seek_min = seek_min;
+                int64_t local_seek_max = seek_max;
+
+                if (local_seek_target < 0) {
+                    local_seek_target = 0;
+                }
+                if (ic->duration > 0 && local_seek_target > ic->duration) {
+                    local_seek_target = ic->duration;
+                }
+                if (is->seek_rel == 0) {
+                    int64_t window = 3LL * AV_TIME_BASE;
+                    local_seek_min = local_seek_target > window ? local_seek_target - window : 0;
+                    local_seek_max = local_seek_target + window;
+                    if (ic->duration > 0 && local_seek_max > ic->duration) {
+                        local_seek_max = ic->duration;
                     }
+                }
+                effective_seek_target = local_seek_target;
 
-                    if (file_size > 1 && duration > 0 && bounded_target >= 0) {
-                        int64_t byte_pos = av_rescale_rnd(bounded_target, file_size - 1, duration, AV_ROUND_DOWN);
-                        if (byte_pos < 0)
-                            byte_pos = 0;
-                        if (byte_pos >= file_size)
-                            byte_pos = file_size - 1;
-
-                        av_log(ffp, AV_LOG_WARNING,
-                               "heanup: %s safe byte seek: target=%lld, bounded_target=%lld, file_size=%lld, duration=%lld, byte_pos=%lld\n",
-                               safe_seek_name, (long long)seek_target, (long long)bounded_target,
-                               (long long)file_size, (long long)duration, (long long)byte_pos);
-                        ret = av_seek_frame(ic, -1, byte_pos, AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD);
-                        if (ret < 0) {
-                            av_log(ffp, AV_LOG_WARNING, "heanup: %s safe byte seek failed, ret=%d\n", safe_seek_name, ret);
-                        }
-                    } else {
-                        av_log(ffp, AV_LOG_WARNING,
-                               "heanup: %s cannot calculate safe byte position, file_size=%lld, duration=%lld, target=%lld\n",
-                               safe_seek_name, (long long)file_size, (long long)duration, (long long)seek_target);
+                if (use_safe_byte_seek) {
+                    // Avoid avformat_seek_file for fragile local ASF streams.
+                    const char *safe_seek_name = "ASF";
+                    if (!ic->pb || !(ic->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
+                        av_log(ffp, AV_LOG_WARNING, "heanup: %s stream is not byte-seekable, reject seek\n", safe_seek_name);
                         ret = -1;
+                    } else {
+                        int64_t file_size = avio_size(ic->pb);
+                        int64_t duration = ic->duration;
+                        int64_t bounded_target = local_seek_target;
+
+                        if (duration > 0) {
+                            if (bounded_target < 0)
+                                bounded_target = 0;
+                            if (bounded_target > duration)
+                                bounded_target = duration;
+                        }
+
+                        if (file_size > 1 && duration > 0 && bounded_target >= 0) {
+                            int64_t byte_pos = av_rescale_rnd(bounded_target, file_size - 1, duration, AV_ROUND_DOWN);
+                            if (byte_pos < 0)
+                                byte_pos = 0;
+                            if (byte_pos >= file_size)
+                                byte_pos = file_size - 1;
+
+                            av_log(ffp, AV_LOG_WARNING,
+                                   "heanup: %s safe byte seek: target=%lld, bounded_target=%lld, file_size=%lld, duration=%lld, byte_pos=%lld\n",
+                                   safe_seek_name, (long long)seek_target, (long long)bounded_target,
+                                   (long long)file_size, (long long)duration, (long long)byte_pos);
+                            ret = av_seek_frame(ic, -1, byte_pos, AVSEEK_FLAG_BYTE | AVSEEK_FLAG_BACKWARD);
+                            if (ret < 0) {
+                                av_log(ffp, AV_LOG_WARNING, "heanup: %s safe byte seek failed, ret=%d\n", safe_seek_name, ret);
+                            }
+                        } else {
+                            av_log(ffp, AV_LOG_WARNING,
+                                   "heanup: %s cannot calculate safe byte position, file_size=%lld, duration=%lld, target=%lld\n",
+                                   safe_seek_name, (long long)file_size, (long long)duration, (long long)seek_target);
+                            ret = -1;
+                        }
                     }
+                } else {
+                    ret = avformat_seek_file(is->ic, -1, local_seek_min, local_seek_target, local_seek_max, seek_flags);
                 }
-            } else {
-                if (is_flac_format) {
-                    // FLAC is VBR-like for byte-position mapping; forcing byte-seek can land mid-frame.
-                    seek_flags &= ~AVSEEK_FLAG_BYTE;
-                    seek_flags |= AVSEEK_FLAG_ANY;
-                }
-                ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, seek_flags);
             }
 
             if (ret < 0) {
@@ -3518,7 +3612,7 @@ static int read_thread(void *arg)
                 if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                    set_clock(&is->extclk, NAN, 0);
                 } else {
-                   set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
+                   set_clock(&is->extclk, effective_seek_target / (double)AV_TIME_BASE, 0);
                 }
 
                 is->latest_video_seek_load_serial = is->videoq.serial;
@@ -4541,9 +4635,19 @@ int ffp_seek_to_l(FFPlayer *ffp, long msec)
     int64_t start_time = 0;
     int64_t seek_pos = milliseconds_to_fftime(msec);
     int64_t duration = milliseconds_to_fftime(ffp_get_duration_l(ffp));
+    const int network_input = is_network_url(ffp->input_filename);
 
     if (!is)
         return EIJK_NULL_IS_PTR;
+    if (!is->ic || !is->ic->pb)
+        return EIJK_NULL_IS_PTR;
+
+    if (!(is->ic->pb->seekable & AVIO_SEEKABLE_NORMAL)) {
+        av_log(ffp, AV_LOG_WARNING, "ffp_seek_to_l reject: stream not seekable, msec=%ld\n", msec);
+        return EIJK_FAILED;
+    }
+
+    (void)network_input;
 
     if (duration > 0 && seek_pos >= duration && ffp->enable_accurate_seek) {
         toggle_pause(ffp, 1);