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

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等)。

诉求

希望了解:

  1. 为Chromecast分发HLS内容有哪些特定要求或限制(如分段格式、MIME类型)。
  2. 为何HLS流在Safari、VLC正常,却无法在Chromecast播放。
  3. 可对HLS配置进行哪些修改以适配Chromecast。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 09:12:02