Przeglądaj źródła

feat(cast): 优化远程文件投屏功能支持本地缓存

- 集成 NetworkKit 获取本机真实 IP 地址替换默认 localhost
- 添加 SMB 和 FTP 远程文件本地缓存管理机制
- 实现投屏时优先使用本地缓存文件提升播放稳定性
- 新增本地回环地址检测逻辑优化流媒体判断
- 统一本地文件路径处理逻辑简化投屏流程
chendeben 7 miesięcy temu
rodzic
commit
546ce64aaa

+ 35 - 2
entry/src/main/ets/common/network/SmbStreamingHttpServer.ets

@@ -4,10 +4,11 @@ import { BusinessError } from '@kit.BasicServicesKit';
 import { httpServer, HttpRequest, HttpResponse } from '@webabcd/harmony-httpserver';
 import { readSmbRange, SmbConnectionInfo } from './SmbRangeReader';
 import { readFtpRange, FtpConnectionInfo } from './FtpRangeReader';
+import { connection } from '@kit.NetworkKit';
 
 const TAG = 'SmbStreamingHttpServer';
 const STREAM_ROUTE_PREFIX = '/smb-stream';
-const LOCAL_HOST = '127.0.0.1';
+const DEFAULT_HOST = '127.0.0.1';
 const MAX_CHUNK_SIZE = 512 * 1024; // 512KB per response
 const RANGE_WAIT_TIMEOUT_MS = 60000;
 const RANGE_POLL_INTERVAL_MS = 100;
@@ -70,6 +71,7 @@ export default class SmbStreamingHttpServer {
 
   async getStreamingUrl(options: StreamingSessionOptions): Promise<string> {
     await this.ensureServerStarted();
+    const host = await this.resolveLocalHost();
     const now = Date.now();
     const existing = this.sessions.get(options.sessionKey);
     if (existing) {
@@ -99,7 +101,38 @@ export default class SmbStreamingHttpServer {
       Logger.info(TAG, `注册SMB流会话: ${options.sessionKey}`);
     }
     this.cleanupExpiredSessions();
-    return `http://${LOCAL_HOST}:${this.port}${STREAM_ROUTE_PREFIX}/${encodeURIComponent(options.sessionKey)}`;
+    return `http://${host}:${this.port}${STREAM_ROUTE_PREFIX}/${encodeURIComponent(options.sessionKey)}`;
+  }
+
+  private async resolveLocalHost(): Promise<string> {
+    try {
+      const netHandle = await connection.getDefaultNet();
+      const addressGetter = (connection as unknown as { getAddressesByNetwork?: (handle: unknown) => Promise<unknown[]> })
+        .getAddressesByNetwork;
+      if (!addressGetter) {
+        return DEFAULT_HOST;
+      }
+      const addresses = await addressGetter(netHandle);
+      if (addresses && addresses.length > 0) {
+        for (let i = 0; i < addresses.length; i++) {
+          const raw = addresses[i] as Record<string, unknown>;
+          const address = (raw.address ?? raw.addr ?? raw.ip) as string | undefined;
+          if (!address) {
+            continue;
+          }
+          if (address.startsWith('127.') || address === '0.0.0.0') {
+            continue;
+          }
+          if (address.includes(':')) {
+            continue;
+          }
+          return address;
+        }
+      }
+    } catch (error) {
+      Logger.warn(TAG, `解析本机IP失败: ${(error as Error).message}`);
+    }
+    return DEFAULT_HOST;
   }
 
   private async ensureServerStarted(): Promise<void> {

+ 49 - 6
entry/src/main/ets/controller/CastController.ets

@@ -21,6 +21,10 @@ import { fileIo } from '@kit.CoreFileKit';
 import { media } from '@kit.MediaKit';
 import { fileUri } from '@kit.CoreFileKit';
 import { VideoItem } from '../viewmodel/VideoItem';
+import { RemoteDriveManager } from '../common/util/RemoteDriveManager';
+import { RemoteCacheManager } from '../common/network/RemoteCacheManager';
+import { RemoteCacheType } from '../common/network/RemoteSongCache';
+import { extractFtpRelativePath, extractSmbRelativePath, isFtpType, isSmbType } from '../common/util/RemotePlayerUtil';
 
 const TAG = 'heanup CastController';
 
@@ -159,7 +163,38 @@ export class CastController {
 
     try {
       // 判断是否是流媒体
-      let isStreaming = this.isUrl(videoUrl);
+      let localCachePath: string | undefined = undefined;
+      if (this.isLoopbackUrl(videoUrl) && (isSmbType(songItem.type) || isFtpType(songItem.type))) {
+        try {
+          const manager = RemoteDriveManager.getInstance();
+          const accountId = songItem.webdav_account_id;
+          if (accountId) {
+            const account = await manager.getWebDavAccountById(accountId);
+            if (account) {
+              if (isSmbType(songItem.type)) {
+                const relative = extractSmbRelativePath(songItem, account.smbShare);
+                if (relative) {
+                  localCachePath = await RemoteCacheManager.ensureCached(RemoteCacheType.SMB, {
+                    account,
+                    remotePath: relative
+                  });
+                }
+              } else if (isFtpType(songItem.type)) {
+                const relative = extractFtpRelativePath(songItem);
+                if (relative) {
+                  localCachePath = await RemoteCacheManager.ensureCached(RemoteCacheType.FTP, {
+                    account,
+                    remotePath: relative
+                  });
+                }
+              }
+            }
+          }
+        } catch (error) {
+          console.warn(TAG, `投播缓存准备失败,继续使用流媒体地址: ${(error as Error).message}`);
+        }
+      }
+      let isStreaming = !localCachePath && this.isUrl(videoUrl);
      console.info( TAG, `是否流媒体: ${isStreaming}`);
 
       let description: avSession.AVMediaDescription = {
@@ -180,15 +215,13 @@ export class CastController {
        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);
+        const localPath = localCachePath || videoUrl || songItem.filePath;
+        this.castFile = fileIo.openSync(localPath, 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}`);
+        console.info( TAG, `✅ 使用本地文件投播: ${localPath}, fd: ${this.castFile.fd}`);
       }
 
       playItem = {
@@ -242,6 +275,16 @@ export class CastController {
     return false;
   }
 
+  private isLoopbackUrl(url: string): boolean {
+    if (!url) {
+      return false;
+    }
+    if (url.startsWith('http://127.') || url.startsWith('http://localhost') || url.startsWith('https://localhost')) {
+      return true;
+    }
+    return false;
+  }
+
   /**
    * 设置投播状态变化监听器
    */

+ 3 - 2
entry/src/main/ets/view/LocalMusic.ets

@@ -14598,13 +14598,14 @@ export struct LocalMusic {
       Logger.info('heanup initQueueItem', `使用流媒体地址投播: ${this.videoUrl}`);
     } else {
       // 本地文件使用 fdSrc
-      let uri = fileUri.getUriFromPath(item.filePath);
+      const localPath = this.videoUrl || item.filePath;
+      let uri = fileUri.getUriFromPath(localPath);
       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}`);
+      Logger.info('heanup initQueueItem', `使用本地文件投播: ${localPath}, fd: ${this.castFile.fd}`);
     }
 
     this.castItem = {