iPhone 屏幕镜像:HLS 流媒体可在 Safari 和 VLC 中运行,但无法在 Chromecast 上进行流媒体播放(卡在加载媒体上)

问题描述 投票:0回答:1

我正在开发一个项目,其中使用 AVAssetWriter 从 CMSampleBuffer 生成分段的 MPEG-4 文件并使用 HLS 提供它们。在 Safari 和 VLC 中测试时,HLS 流工作得很好,但是当我尝试将流投射到 Chromecast 时,它会卡在加载媒体上,而不会播放视频。

我正在尝试做的事情:

  1. 我正在使用 ReplayKit 捕获实时 iPhone 屏幕流,并通过 AVAssetWriter 处理视频数据 (CMSampleBuffer)。
  2. 流通过初始化和段文件转换为 HLS 格式。
  3. 使用 Swifter 服务器通过 HTTP 提供视频片段。
  4. 生成的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
    }
}

Chromecast 日志

我尝试过的:

  • 确保分段 URL 是绝对的且可通过 HTTP 访问。
  • 仔细检查 HLS 结构以确保其对于 Chromecast 有效(使用 MP2T 段)。
  • 确保服务器上启用 CORS 标头,以便 Chromecast 可以访问文件。
  • 为视频片段尝试不同的 MIME 类型(视频/mp2t、视频/mp4 等)。

我的期望: 我想知道:

  1. 如果在为 Chromecast 提供 HLS 内容时有任何特定要求或限制(例如,某些分段格式或 MIME 类型)。
  2. 为什么 Chromecast 无法流式传输 HLS 内容,即使它在其他播放器(Safari、VLC)中正常工作。
  3. 我可以对 HLS 设置进行任何解决方案或改进,以确保它与 Chromecast 兼容。
http-live-streaming chromecast broadcasting google-cast-sdk cmsamplebuffer
1个回答
0
投票

Chromecast 可以播放 HLS,最近 5-7 年左右的 Chromecast 几乎可以播放 H.264 和其他一些配置文件。 您遇到的问题可能不是由于媒体兼容性造成的。

关键在于错误日志...

尝试从具有 URL https://portal.kickstartthq.com/ 的框架加载 URL http://192.168.1.109:8080/hls.m3u8 是不安全的。 域、协议和端口必须匹配。

我不完全确定问题出在哪里,但您可能与某处定义的内容安全策略(CSP)发生冲突,无论是在您的代码中还是在 Chromecast 接收器 SDK 中。 首先,尝试配置一个策略,明确允许脚本和媒体元素访问您的 HLS 服务器源。

其他一些想法...

  • 一路上,我看到 Chromium 人员争论要阻止外部主机访问本地网络。 我现在找不到有关此问题的帖子,但这可能是问题的一部分。

  • Google 使用 Shaka Player 作为 Chromecast 接收器,它将通过 MediaSource 扩展来处理 HLS。 您可以在 Shaka Player 测试页面上测试您的流,以排除那里的问题。 如果有的话,如果您可以在另一个页面上重现播放问题,可能会使测试变得更容易。

  • 我认为 Chromecast 兼容性不是问题,但您始终可以尝试投射到默认接收器,看看会发生什么。 如果有效,那么您就知道可以播放了。

© www.soinside.com 2019 - 2024. All rights reserved.