如何通过URLSession持续保存已下载视频片段至文档目录以实现边下边播?
当然可以实现边下边播,核心思路是放弃默认的URLSessionDownloadTask,改用URLSessionDataTask来流式接收数据,实时将已下载的片段追加写入目标文件。URLSessionDownloadTask的设计是将文件临时存储在系统目录,只有下载完成后才会暴露临时路径,无法直接获取中间下载的片段;而DataTask的代理方法可以在每次收到数据块时触发,刚好满足实时写入的需求。
实现步骤
- 提前创建目标文件并初始化文件句柄,用于持续追加写入数据;
- 使用
URLSessionDataTask发起请求,在代理方法中实时写入数据块; - 下载完成或出错时关闭文件句柄,完成后续处理;
- 配合播放器(如
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
相关产品推荐
相关产品推荐

