import Foundation struct CastMediaInput: Sendable { let videoURL: String let videoHeaders: String let audioURL: String? let audioHeaders: String? let local: Bool let restricted: Bool let live: Bool let duration: Double let subtitles: [ResolvedSubtitle] } struct CastMediaDescription: Sendable { let url: URL let contentType: String let duration: Double let live: Bool let tracks: [CastTextTrack] } struct CastTextTrack: Sendable { let id: Int let url: URL let language: String let label: String } private struct CastDeliveryError: LocalizedError { let errorDescription: String? = "Could not deliver cast media. Check the connection and media compatibility. Private details are redacted." } // A start task retains this owner until native probing returns. Cancellation never // frees a handle in use; destruction and worker joins always run off the main actor. private final class CastMediaLifetime: @unchecked Sendable { let handle: OpaquePointer init(input: CastMediaInput) throws { var error = [CChar](repeating: 0, count: 512) let result = input.videoURL.withCString { video in input.videoHeaders.withCString { headers in (input.audioURL ?? "").withCString { audio in (input.audioHeaders ?? "").withCString { audioHeaders in var source = CastMediaSource(video_url: video, video_headers: headers, audio_url: audio, audio_headers: audioHeaders, local: input.local, restricted: input.restricted, live: input.live, duration: input.duration) return cast_media_create(&source, &error) } } } } guard let result else { throw CastDeliveryError() } handle = result } func cancel() { cast_media_cancel(handle) } func start(host: String) throws -> (String, Double) { var url = [CChar](repeating: 0, count: 512) var error = [CChar](repeating: 0, count: 512) var duration = 0.0 guard host.withCString({ cast_media_start(handle, $0, &url, &duration, &error) }) else { throw CastDeliveryError() } return (url.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }, duration) } func subtitle(id: Int, data: Data) throws -> URL { var url = [CChar](repeating: 0, count: 512) let success = data.withUnsafeBytes { cast_media_subtitle(handle, Int32(id), $0.baseAddress, $0.count, &url) } guard success, let result = URL(string: url.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }) else { throw CastDeliveryError() } return result } deinit { // Capture an explicitly Sendable owner instead of crossing an actor with a pointer. let cleanup = CastMediaCleanup(handle: handle) DispatchQueue.global(qos: .utility).async { cast_media_destroy(cleanup.handle) } } } private struct CastMediaCleanup: @unchecked Sendable { let handle: OpaquePointer } @MainActor final class CastMediaServer { private var lifetime: CastMediaLifetime? private var operation: Task? init() {} func start(input: CastMediaInput, receiverHost: String) async throws -> CastMediaDescription { stop() guard input.subtitles.count <= 32 else { throw CastDeliveryError() } let owner = try CastMediaLifetime(input: input) lifetime = owner let task = Task.detached(priority: .userInitiated) { do { let (text, duration) = try owner.start(host: receiverHost) try Task.checkCancellation() guard let url = URL(string: text) else { throw CastDeliveryError() } var tracks: [CastTextTrack] = [] for (index, subtitle) in input.subtitles.enumerated() { try Task.checkCancellation() let prepared = try await MiruroTransport.subtitle(subtitle) let data = try await WebVTTDocument.loadData(url: prepared.url, headers: prepared.headers) guard let text = String(data: data, encoding: .utf8) else { throw CastDeliveryError() } _ = try WebVTTDocument.parse(text) let trackURL = try owner.subtitle(id: index + 1, data: data) tracks.append(CastTextTrack(id: index + 1, url: trackURL, language: subtitle.language, label: subtitle.label)) } try Task.checkCancellation() return CastMediaDescription(url: url, contentType: "application/vnd.apple.mpegurl", duration: duration, live: input.live, tracks: tracks) } catch { owner.cancel() if Task.isCancelled { throw CancellationError() } throw CastDeliveryError() } } operation = task do { let result = try await withTaskCancellationHandler { try await task.value } onCancel: { owner.cancel() task.cancel() } guard lifetime === owner else { throw CancellationError() } operation = nil return result } catch { if lifetime === owner { stop() } if error is CancellationError { throw error } throw CastDeliveryError() } } func stop() { operation?.cancel() operation = nil lifetime?.cancel() lifetime = nil } deinit { operation?.cancel() lifetime?.cancel() } }