我正在开发一个项目,其中使用 AVAssetWriter 从 CMSampleBuffer 生成分段的 MPEG-4 文件并使用 HLS 提供它们。在 Safari 和 VLC 中测试时,HLS 流工作得很好,但是当我尝试将流投射到 Chromecast 时,它会卡在加载媒体上,而不会播放视频。
我正在尝试做的事情:
相关代码:
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 可以播放 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 兼容性不是问题,但您始终可以尝试投射到默认接收器,看看会发生什么。 如果有效,那么您就知道可以播放了。