import Foundation import AVFoundation // MARK: - SMB 流式加载器 /// 将 smb:// URL 包装为 miniplayer-smb:// 自定义协议, /// AVPlayer 请求数据时逐块从 SMB 读取,无需复制整个文件。 final class SMBStreamingLoader: NSObject, AVAssetResourceLoaderDelegate { private var smbURL: URL private var fileHandle: FileHandle? private var contentLength: Int64 = 0 private var contentType: String = "video/mp4" private let readChunkSize = 256 * 1024 // 256KB chunks init(smbURL: URL) { self.smbURL = smbURL super.init() // 根据扩展名推断 content type let ext = smbURL.pathExtension.lowercased() switch ext { case "mp4", "m4v": contentType = "video/mp4" case "mov": contentType = "video/quicktime" case "mkv": contentType = "video/x-matroska" case "avi": contentType = "video/x-msvideo" case "webm": contentType = "video/webm" case "ts": contentType = "video/mp2t" case "m3u8": contentType = "application/vnd.apple.mpegurl" case "mp3", "mpeg": contentType = "audio/mpeg" case "wav": contentType = "audio/wav" case "m4a": contentType = "audio/mp4" case "aac": contentType = "audio/aac" default: contentType = "video/mp4" } } deinit { try? fileHandle?.close() } // 创建包装后的本地 URL func wrappedURL() -> URL { // 用自定义 scheme,path 部分编码原始 smb URL let encoded = smbURL.absoluteString.data(using: .utf8)!.base64EncodedString() .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") return URL(string: "miniplayer-smb://stream/\(encoded)")! } // 从 wrapped URL 解码原始 smb URL static func decodeSMBURL(from wrapped: URL) -> URL? { guard wrapped.scheme == "miniplayer-smb" else { return nil } var encoded = wrapped.path.replacingOccurrences(of: "/stream/", with: "") // 还原 base64 填充 encoded = encoded .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") let padding = (4 - encoded.count % 4) % 4 encoded += String(repeating: "=", count: padding) guard let data = Data(base64Encoded: encoded), let str = String(data: data, encoding: .utf8), let url = URL(string: str) else { return nil } return url } // MARK: - AVAssetResourceLoaderDelegate func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { // 延迟打开 FileHandle,避免阻塞 if fileHandle == nil { do { fileHandle = try FileHandle(forReadingFrom: smbURL) contentLength = Int64(try fileHandle!.seekToEnd()) fileHandle!.seek(toFileOffset: 0) } catch { loadingRequest.finishLoading(with: error) return true } } // 处理 content information request if let infoRequest = loadingRequest.contentInformationRequest { infoRequest.contentType = contentType infoRequest.contentLength = contentLength infoRequest.isByteRangeAccessSupported = true } // 处理 data request if let dataRequest = loadingRequest.dataRequest { let offset = dataRequest.currentOffset > 0 ? dataRequest.currentOffset : (dataRequest.requestedOffset > 0 ? dataRequest.requestedOffset : 0) let length = dataRequest.requestedLength > 0 ? dataRequest.requestedLength : readChunkSize do { try fileHandle?.seek(toOffset: UInt64(offset)) let data = fileHandle?.readData(ofLength: length) ?? Data() if !data.isEmpty { dataRequest.respond(with: data) loadingRequest.finishLoading() } else { // 读到末尾 loadingRequest.finishLoading() } } catch { loadingRequest.finishLoading(with: error) } } else { // 没有 data request 只有 info request(预检) loadingRequest.finishLoading() } return true } func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { // 取消请求,无需额外处理 } }