iPhone屏幕镜像HLS流在Safari/VLC正常,Chromecast加载失败
HLS流在Chromecast无法播放的问题排查与适配需求
问题描述
我正在开发一个项目,使用AVAssetWriter将CMSampleBuffer转换为碎片化MPEG-4文件,并通过HLS协议进行分发。该HLS流在Safari和VLC中测试播放完全正常,但尝试投屏到Chromecast时,会卡在媒体加载状态,始终无法播放视频。
实现流程
- 使用ReplayKit捕获iPhone实时屏幕流,通过AVAssetWriter处理视频数据(CMSampleBuffer)。
- 将流转换为包含初始化片段和分段文件的HLS格式。
- 使用Swifter服务器通过HTTP分发视频分段。
- 生成的HLS流包含
init.mp4(初始化片段)和碎片化MPEG-4文件(.m4s视频分段)。
相关代码
class SampleHandler: RPBroadcastSampleHandler { let assetWriter = AVAssetWriter(contentType: .mpeg4Movie) var input: AVAssetWriterInput! var sequence: Int = -1 private let maxSegmentsInMemory = 50 private var sequences: [(sequence: Int, data: Data, duration: Double)] = [] private lazy var server = HttpServer() var m3u8: String { let header = """ #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:\(getTargetDuration()) #EXT-X-MEDIA-SEQUENCE:\(sequences.first?.sequence ?? 0) #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MAP:URI="http://\(self.getWiFiAddress() ?? ""):8080/init.mp4" """ let segmentCount = min(self.maxSegmentsInMemory, sequences.count) let segments = sequences.suffix(segmentCount).map { segment in "#EXTINF:\(segment.duration),\nhttp://\(self.getWiFiAddress() ?? ""):8080/files/sequence\(segment.sequence).m4s" }.joined(separator: "\n") return header + "\n" + segments } func setupAssetWriter() { assetWriter.shouldOptimizeForNetworkUse = true assetWriter.outputFileTypeProfile = .mpeg4AppleHLS assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 5.0, preferredTimescale: 1) assetWriter.delegate = self let settings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: 720, AVVideoHeightKey: 1080, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: 700000, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel ] ] input = AVAssetWriterInput(mediaType: .video, outputSettings: settings) input.expectsMediaDataInRealTime = true assetWriter.add(input) } override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { // Start the HTTP server do { try server.start(8080, forceIPv4: true, priority: .default) setupRoutes() } catch { print("Server failed to start: \(error)") } setupAssetWriter() assetWriter.initialSegmentStartTime = .zero assetWriter.startWriting() assetWriter.startSession(atSourceTime: .zero) } func setupRoutes() { // Serve the HLS playlist server["/hls.m3u8"] = { [weak self] request -> HttpResponse in guard let self = self else { return .notFound } let m3u8Data = self.m3u8.data(using: .utf8)! print("✅ M3U8 => \(self.m3u8) ✅") let body: HttpResponseBody = .data(m3u8Data, contentType: "application/vnd.apple.mpegurl") return .ok(body) } // Serve the initialization segment server["/init.mp4"] = { [weak self] request -> HttpResponse in guard let segmentData = self?.getSegmentData(for: "init.mp4") else { return .notFound // Return 404 if segment data not found } let body: HttpResponseBody = .data(segmentData, contentType: "video/mp4") return .ok(body) } // Serve video segments server["/files/:path"] = { [weak self] request -> HttpResponse in guard let self = self else { return .notFound } // Extract the sequence number from the path let segmentPath = request.path.replacingOccurrences(of: "/files/", with: "") guard let segmentData = self.getSegmentData(for: segmentPath) else { return .notFound // Return 404 if segment data not found } print("✅ SEGMENT PATH => \(segmentPath) \n SEGMENT DATA => \(segmentData) ✅") let body: HttpResponseBody = .data(segmentData, contentType: "video/MP2T") return .ok(body) } } override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { switch sampleBufferType { case .video: if input.isReadyForMoreMediaData { input.append(sampleBuffer) } default: break } } override func broadcastFinished() { assetWriter.finishWriting { print("Finished writing all segments.") } } // Handling segment data func onSegmentData(data: Data, duration: Double) { sequence += 1 // Handle the initial segment if sequence == 0 { saveSegment(data: data, filename: "init.mp4") return } // Save the segment data and duration let segmentFilename = "sequence\(sequence).m4s" saveSegment(data: data, filename: segmentFilename) // Append the new segment to the sequence list with duration sequences.append((sequence: sequence, data: data, duration: duration)) // Remove older segments to keep only the latest in memory if sequences.count > maxSegmentsInMemory { sequences.removeFirst() } } func saveSegment(data: Data, filename: String) { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename) do { try data.write(to: fileURL) } catch { print("Failed to save segment: \(error)") } } func getSegmentData(for filename: String) -> Data? { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename) return try? Data(contentsOf: fileURL) } // Compute the target duration as the max duration of the segments func getTargetDuration() -> Int { return Int((sequences.map { $0.duration }.max() ?? 1.0).rounded(.up)) } } extension SampleHandler: AVAssetWriterDelegate { func assetWriter(_ writer: AVAssetWriter, didOutputSegmentData segmentData: Data, segmentType: AVAssetSegmentType, segmentReport: AVAssetSegmentReport?) { // Retrieve the segment duration from the report let duration = (segmentReport?.trackReports.first?.duration.seconds ?? 1.0).rounded(.up) self.onSegmentData(data: segmentData, duration: duration) } } extension SampleHandler { func getWiFiAddress() -> String? { var address: String? var ifaddr: UnsafeMutablePointer<ifaddrs>? if getifaddrs(&ifaddr) == 0 { var ptr = ifaddr while ptr != nil { guard let interface = ptr?.pointee else { break } let addrFamily = interface.ifa_addr.pointee.sa_family if addrFamily == UInt8(AF_INET) { if let name = String(validatingUTF8: interface.ifa_name), name == "en0" { var addr = interface.ifa_addr.pointee var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) getnameinfo(&addr, socklen_t(interface.ifa_addr.pointee.sa_len), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) address = String(cString: hostname) } } ptr = ptr?.pointee.ifa_next } freeifaddrs(ifaddr) } return address } }
已尝试的排查操作
- 确保分段URL为绝对路径且可通过HTTP访问。
- 检查HLS结构,确保符合Chromecast要求(使用MP2T分段)。
- 服务器启用CORS头,确保Chromecast可访问文件。
- 尝试为视频分段设置不同的MIME类型(video/mp2t、video/mp4等)。
诉求
希望了解:
- 为Chromecast分发HLS内容有哪些特定要求或限制(如分段格式、MIME类型)。
- 为何HLS流在Safari、VLC正常,却无法在Chromecast播放。
- 可对HLS配置进行哪些修改以适配Chromecast。
内容的提问来源于stack exchange,提问作者Ehtisham Rauf
相关产品推荐
相关产品推荐

