Reputation: 1
I'm working on a project where I'm using AVAssetWriter to generate fragmented MPEG-4 files from CMSampleBuffer and serving them using HLS. The HLS stream works perfectly when tested in Safari and VLC, but when I attempt to cast the stream to Chromecast, it gets stuck on loading media without ever playing the video.
What I'm trying to do:
Relevant Code:
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
}
}
What I’ve tried:
My expectations: I would like to know:
Upvotes: 0
Views: 81
Reputation: 163528
Chromecast can play HLS, and recent Chromecasts from the last 5-7 years or so can play pretty much any profile of H.264 and some others. The issue you're having probably isn't due to compatibility of the media.
The key is in that error log...
Unsafe attempt to load URL http://192.168.1.109:8080/hls.m3u8 from frame with URL https://portal.kickstarthq.com/. Domains, protocols and ports must match.
I'm not entirely sure where the problem is, but you're probably running afoul of a Content Security Policy (CSP) defined somewhere, whether in your code or the Chromecast receiver SDK. Firstly, try configuring a policy that explicitly allows script and media element access to your HLS server origin.
Some other thoughts...
Somewhere along the way, I saw the Chromium folks arguing to block access to the local network from external hosts. I can't find the post about this now, but that might be part of the problem.
Google uses Shaka Player for Chromecast receivers, which is going to handle HLS with MediaSource Extensions. You might test your stream on a Shaka Player test page to rule out an issue there. If anything, it might make testing easier if you can reproduce your playback issues on another page.
I don't think Chromecast compatibility is the problem, but you could always try casting to the default receiver and see what happens. If it works, then you know playback is possible.
Upvotes: 0