You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过URLSession持续保存已下载视频片段至文档目录以实现边下边播?

当然可以实现边下边播,核心思路是放弃默认的URLSessionDownloadTask,改用URLSessionDataTask来流式接收数据,实时将已下载的片段追加写入目标文件。URLSessionDownloadTask的设计是将文件临时存储在系统目录,只有下载完成后才会暴露临时路径,无法直接获取中间下载的片段;而DataTask的代理方法可以在每次收到数据块时触发,刚好满足实时写入的需求。

实现步骤

  1. 提前创建目标文件并初始化文件句柄,用于持续追加写入数据;
  2. 使用URLSessionDataTask发起请求,在代理方法中实时写入数据块;
  3. 下载完成或出错时关闭文件句柄,完成后续处理;
  4. 配合播放器(如AVPlayer)监听文件变化,实现边下边播。

修改后的代码示例

首先在你的MediaCacheManager中添加必要的属性:

class MediaCacheManager: NSObject {
    private var fileHandle: FileHandle?
    private var destinationURL: URL?
    private var totalBytesReceived: Int64 = 0
    var progress: Float = 0.0
    // 其他已有属性...
}

然后重写下载方法,改用DataTask并初始化文件:

public func downloadVideo(url: URL) {
    // 准备目标文件路径
    guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
        print("无法获取Application Support目录")
        return
    }
    let videoDir = appSupportDir.appendingPathComponent("telehealthVideo")
    do {
        try FileManager.default.createDirectory(at: videoDir, withIntermediateDirectories: true)
    } catch {
        print("创建视频目录失败:\(error.localizedDescription)")
        return
    }
    destinationURL = videoDir.appendingPathComponent("myVideo.mp4")
    
    // 重置目标文件(如果已存在则删除重建)
    if FileManager.default.fileExists(atPath: destinationURL!.path) {
        try? FileManager.default.removeItem(at: destinationURL!)
    }
    FileManager.default.createFile(atPath: destinationURL!.path, contents: nil)
    
    // 初始化文件句柄
    do {
        fileHandle = try FileHandle(forWritingTo: destinationURL!)
    } catch {
        print("打开文件句柄失败:\(error.localizedDescription)")
        return
    }
    
    // 发起DataTask请求
    let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
    let dataTask = session.dataTask(with: url)
    dataTask.resume()
}

实现URLSessionDataDelegate代理方法,处理实时数据和下载状态:

extension MediaCacheManager: URLSessionDataDelegate {
    // 收到数据块时触发,实时写入文件
    public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        guard let fileHandle = fileHandle else { return }
        
        // 追加写入数据
        fileHandle.seekToEndOfFile()
        fileHandle.write(data)
        
        // 更新下载进度
        totalBytesReceived += Int64(data.count)
        if let totalBytesExpected = dataTask.response?.expectedContentLength, totalBytesExpected > 0 {
            progress = Float(totalBytesReceived) / Float(totalBytesExpected)
            DispatchQueue.main.async {
                debugPrint("下载进度:\(self.progress)")
            }
        }
        
        // 通知播放器有新内容可用(可通过Notification/闭包回调)
        // NotificationCenter.default.post(name: .videoDataUpdated, object: destinationURL)
    }
    
    // 下载完成/出错时关闭文件句柄
    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        fileHandle?.closeFile()
        fileHandle = nil
        
        if let error = error {
            print("下载失败:\(error.localizedDescription)")
        } else {
            print("下载完成")
            // 后续处理(如保存到相册等)
            // saveVideoToAlbum(videoURL: destinationURL!, albumName: "MyAlbum")
        }
    }
}

额外注意事项

  • 播放器兼容性:AVPlayer支持播放未完全下载的本地文件,但要求视频格式是可流式的(比如MP4的moov原子位于文件头部)。如果视频的moov在文件末尾,可能需要下载一部分后才能正常播放,建议后端提供符合流式标准的视频文件。
  • 线程安全:URLSession的代理方法运行在后台队列,操作文件句柄时如果涉及UI更新,要切换到主队列;如果需要多线程操作文件,建议用串行队列包裹文件句柄的读写逻辑。
  • 断点续传:如果需要支持暂停/继续,需保存totalBytesReceived,下次请求时设置HTTP请求头Range: bytes=\(totalBytesReceived)-,从断点处继续下载。

内容的提问来源于stack exchange,提问作者rauldhasmana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 09:08:21