import Foundation import Network // MARK: - 本地 HTTP 流服务器 /// 将 SMB 文件通过本地 HTTP 端口流式传输给 AVPlayer /// 每播放一个文件创建一个临时服务器,播放结束后销毁 final class LocalStreamServer { private var listener: NWListener? private let smbURL: URL private var fileHandle: FileHandle? private var fileSize: Int64 = 0 private var contentType: String = "video/mp4" private(set) var port: UInt16 = 0 var localURL: URL? { port > 0 ? URL(string: "http://127.0.0.1:\(port)/stream") : nil } init(smbURL: URL) { self.smbURL = smbURL 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 "webm": contentType = "video/webm" case "ts": contentType = "video/mp2t" case "mp3": contentType = "audio/mpeg" case "wav": contentType = "audio/wav" case "m4a": contentType = "audio/mp4" default: contentType = "video/mp4" } } func start() throws { // 打开 SMB 文件获取大小 fileHandle = try FileHandle(forReadingFrom: smbURL) fileSize = Int64(try fileHandle!.seekToEnd()) fileHandle!.seek(toFileOffset: 0) // 随机端口 let params = NWParameters.tcp params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: 0) listener = try NWListener(using: params) listener?.newConnectionHandler = { [weak self] conn in self?.handleConnection(conn) } listener?.start(queue: .global(qos: .userInitiated)) // 获取实际分配的端口 if let endpoint = listener?.port { port = endpoint.rawValue } } func stop() { listener?.cancel() listener = nil try? fileHandle?.close() fileHandle = nil } private func handleConnection(_ conn: NWConnection) { conn.stateUpdateHandler = { state in if case .failed = state { conn.cancel() } } conn.start(queue: .global(qos: .userInitiated)) receiveRequest(conn, buffer: Data()) } private func receiveRequest(_ conn: NWConnection, buffer: Data) { conn.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, error in guard let self else { conn.cancel(); return } var buf = buffer if let data { buf.append(data) } // 检查是否收到完整的 HTTP 请求头(以 \r\n\r\n 结束) if buf.range(of: Data("\r\n\r\n".utf8)) != nil { self.handleHTTPRequest(conn, raw: buf) } else if isComplete || error != nil { conn.cancel() } else { self.receiveRequest(conn, buffer: buf) } } } private func handleHTTPRequest(_ conn: NWConnection, raw: Data) { guard let request = String(data: raw, encoding: .utf8) else { send404(conn); return } let lines = request.components(separatedBy: "\r\n") guard let first = lines.first, first.hasPrefix("GET") else { send404(conn); return } // 解析 Range 头 var rangeStart: Int64 = 0 var rangeEnd: Int64 = fileSize - 1 var isPartial = false for line in lines { if line.lowercased().hasPrefix("range: bytes=") { let val = line.replacingOccurrences(of: "Range: ", with: "", options: .caseInsensitive) .replacingOccurrences(of: "range: ", with: "", options: .caseInsensitive) let parts = val.dropFirst(6).split(separator: "-") if let start = Int64(parts[0]) { rangeStart = start isPartial = true } if parts.count > 1, let end = Int64(parts[1]) { rangeEnd = min(end, fileSize - 1) } break } } let chunkSize = rangeEnd - rangeStart + 1 // 构建响应头 var headers = "" if isPartial { headers += "HTTP/1.1 206 Partial Content\r\n" headers += "Content-Range: bytes \(rangeStart)-\(rangeEnd)/\(fileSize)\r\n" } else { headers += "HTTP/1.1 200 OK\r\n" } headers += "Content-Type: \(contentType)\r\n" headers += "Content-Length: \(chunkSize)\r\n" headers += "Accept-Ranges: bytes\r\n" headers += "Connection: close\r\n" headers += "Access-Control-Allow-Origin: *\r\n" headers += "\r\n" var responseData = Data(headers.utf8) // 读取文件数据 do { try fileHandle?.seek(toOffset: UInt64(rangeStart)) let fileData = fileHandle?.readData(ofLength: Int(chunkSize)) ?? Data() responseData.append(fileData) } catch { send404(conn); return } conn.send(content: responseData, completion: .contentProcessed { _ in conn.cancel() }) } private func send404(_ conn: NWConnection) { let body = "Not Found" let resp = "HTTP/1.1 404 Not Found\r\nContent-Length: \(body.utf8.count)\r\nConnection: close\r\n\r\n\(body)" conn.send(content: Data(resp.utf8), completion: .contentProcessed { _ in conn.cancel() }) } }