feat: stream YouTube video with coordinated audio and video inputs
Resolve public YouTube URLs through external yt-dlp and play selected streams in the existing native window. Independent bounded network readers keep one authored timeline. Successful seeks clear FFmpeg's sticky aborted-read AVIO error; manifest seekability uses native format flags rather than a top-level I/O handle. Verified real YouTube playback, seeking, frame stepping, compact mode, cancellation, and the complete native regression checks.
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
|
||||
@main
|
||||
struct YouTubeResolverChecks {
|
||||
struct Failure: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
}
|
||||
|
||||
static let page = URL(string: "https://www.youtube.com/watch?v=jNQXAC9IVRw")!
|
||||
static let splitJSON = #"""
|
||||
{
|
||||
"title": "Separate streams", "duration": 19.25, "is_live": false,
|
||||
"http_headers": {"Referer": "https://www.youtube.com/", "User-Agent": "metadata-agent"},
|
||||
"requested_formats": [
|
||||
{"url": "https://media.example/video?token=video", "vcodec": "av01.0.04M.08", "acodec": "none", "dynamic_range": "SDR", "http_headers": {"User-Agent": "video-agent", "X-Input": "video"}},
|
||||
{"url": "https://media.example/audio?token=audio", "vcodec": "none", "acodec": "opus"}
|
||||
]
|
||||
}
|
||||
"""#
|
||||
static let combinedJSON = #"""
|
||||
{"title": "Combined stream", "duration": 12, "is_live": false,
|
||||
"url": "https://media.example/combined", "vcodec": "h264", "acodec": "aac",
|
||||
"http_headers": {"User-Agent": "combined-agent"}}
|
||||
"""#
|
||||
|
||||
static func require(_ condition: @autoclosure () throws -> Bool, _ message: String) throws {
|
||||
if try condition() == false { throw Failure(description: "FAIL: \(message)") }
|
||||
}
|
||||
|
||||
static func reject(_ message: String, _ operation: () throws -> Void) throws {
|
||||
do { try operation() }
|
||||
catch is Failure { throw Failure(description: "FAIL: \(message) assertion failed") }
|
||||
catch { return }
|
||||
throw Failure(description: "FAIL: \(message) was accepted")
|
||||
}
|
||||
|
||||
static func decode(_ json: String) throws -> YouTubeVideo {
|
||||
try YouTubeResolver.decode(Data(json.utf8), webpageURL: page)
|
||||
}
|
||||
|
||||
static func decode(_ object: [String: Any]) throws -> YouTubeVideo {
|
||||
try YouTubeResolver.decode(JSONSerialization.data(withJSONObject: object), webpageURL: page)
|
||||
}
|
||||
|
||||
static func fixture(_ json: String) throws -> [String: Any] {
|
||||
try JSONSerialization.jsonObject(with: Data(json.utf8)) as! [String: Any]
|
||||
}
|
||||
|
||||
static func headerLines(_ text: String) throws -> Set<String> {
|
||||
try require(text.isEmpty || text.hasSuffix("\r\n"), "headers terminate with CRLF")
|
||||
let lines = text.components(separatedBy: "\r\n").filter { !$0.isEmpty }
|
||||
try require(lines.allSatisfy { !$0.contains("\r") && !$0.contains("\n") },
|
||||
"headers contain no bare CR or LF")
|
||||
return Set(lines)
|
||||
}
|
||||
|
||||
static func videoURLs() throws {
|
||||
let valid = [
|
||||
"https://youtube.com/watch?v=jNQXAC9IVRw",
|
||||
"https://www.youtube.com/watch?v=jNQXAC9IVRw&list=PLignored&t=3",
|
||||
"https://m.youtube.com/watch?v=jNQXAC9IVRw",
|
||||
"https://music.youtube.com/watch?v=jNQXAC9IVRw",
|
||||
"https://youtu.be/jNQXAC9IVRw?si=share",
|
||||
"https://www.youtube.com/shorts/jNQXAC9IVRw",
|
||||
"https://www.youtube.com/live/jNQXAC9IVRw",
|
||||
"https://www.youtube.com/embed/jNQXAC9IVRw"
|
||||
]
|
||||
for input in valid {
|
||||
let url = try YouTubeResolver.validateURL(input)
|
||||
try require(url == page, "supported video URLs identify the same video")
|
||||
}
|
||||
let hostile = [
|
||||
"", "not a URL", "javascript:alert(1)", "file:///watch?v=jNQXAC9IVRw",
|
||||
"https://example.com/watch?v=jNQXAC9IVRw",
|
||||
"https://youtube.com.evil.example/watch?v=jNQXAC9IVRw",
|
||||
"https://evil.youtube.com/watch?v=jNQXAC9IVRw",
|
||||
"https://youtube.com@evil.example/watch?v=jNQXAC9IVRw",
|
||||
"https://user:password@www.youtube.com/watch?v=jNQXAC9IVRw",
|
||||
"https://www.youtube.com/playlist?list=PLexample",
|
||||
"https://www.youtube.com/watch?list=PLexample",
|
||||
"https://www.youtube.com/watch?v=short",
|
||||
"https://youtu.be/jNQXAC9IVRwextra", "https://youtu.be/jNQXAC9IVR!",
|
||||
"https://www.youtube.com/shorts/", "https://youtu.be/jNQXAC9IVRw/extra",
|
||||
"https://www.youtube.com/watch?v=jNQXAC9IVR%0A",
|
||||
"https://www.youtube.com/watch?v=jNQXAC9IVR%C3%A9"
|
||||
]
|
||||
for input in hostile {
|
||||
try reject("hostile or malformed video URL") { _ = try YouTubeResolver.validateURL(input) }
|
||||
}
|
||||
let asciiID = try YouTubeResolver.validateURL("https://youtu.be/Abc_123-XYZ")
|
||||
try require(asciiID.absoluteString.hasSuffix("v=Abc_123-XYZ"), "video IDs retain case, underscore and hyphen")
|
||||
}
|
||||
|
||||
static func metadata() throws {
|
||||
let split = try decode(splitJSON)
|
||||
try require(split.title == "Separate streams" && split.duration == 19.25 && !split.isLive,
|
||||
"split streams preserve title, fractional duration and recorded status")
|
||||
try require(split.webpageURL == page, "resolved media retains its webpage URL")
|
||||
try require(split.video.url == "https://media.example/video?token=video"
|
||||
&& split.audio?.url == "https://media.example/audio?token=audio",
|
||||
"video and audio inputs remain distinct and preserve signed query strings")
|
||||
let videoHeaders = try headerLines(split.video.headers)
|
||||
let audioHeaders = try headerLines(split.audio!.headers)
|
||||
try require(videoHeaders == ["User-Agent: video-agent", "X-Input: video"],
|
||||
"selected video headers take precedence as a complete map")
|
||||
try require(audioHeaders == ["Referer: https://www.youtube.com/", "User-Agent: metadata-agent"],
|
||||
"audio without selected headers inherits metadata headers")
|
||||
|
||||
var splitObject = try fixture(splitJSON)
|
||||
var formats = splitObject["requested_formats"] as! [[String: Any]]
|
||||
formats.reverse()
|
||||
formats[0]["http_headers"] = ["X-Input": "audio"]
|
||||
formats[1]["http_headers"] = NSNull()
|
||||
splitObject["requested_formats"] = formats
|
||||
let reversed = try decode(splitObject)
|
||||
let reversedVideoHeaders = try headerLines(reversed.video.headers)
|
||||
let reversedAudioHeaders = try headerLines(reversed.audio!.headers)
|
||||
try require(reversed.video.url == split.video.url && reversed.audio?.url == split.audio?.url,
|
||||
"stream roles depend on codecs rather than array order")
|
||||
try require(reversedVideoHeaders == audioHeaders && reversedAudioHeaders == ["X-Input: audio"],
|
||||
"each input independently selects its own headers or metadata fallback")
|
||||
|
||||
let combined = try decode(combinedJSON)
|
||||
try require(combined.video.url == "https://media.example/combined" && combined.audio == nil,
|
||||
"combined fallback uses one input, not duplicated audio")
|
||||
try require(combined.title == "Combined stream" && combined.duration == 12 && !combined.isLive,
|
||||
"combined metadata is preserved")
|
||||
let combinedHeaders = try headerLines(combined.video.headers)
|
||||
try require(combinedHeaders == ["User-Agent: combined-agent"], "combined input carries headers")
|
||||
|
||||
var selectedCombined = try fixture(combinedJSON)
|
||||
selectedCombined.removeValue(forKey: "url")
|
||||
selectedCombined["requested_formats"] = [
|
||||
["url": "https://media.example/selected-combined", "vcodec": "h264", "acodec": "aac",
|
||||
"http_headers": ["X-Input": "combined"]]
|
||||
]
|
||||
let selected = try decode(selectedCombined)
|
||||
let selectedHeaders = try headerLines(selected.video.headers)
|
||||
try require(selected.audio == nil && selected.video.url == "https://media.example/selected-combined"
|
||||
&& selectedHeaders == ["X-Input: combined"],
|
||||
"a selected combined format is one playable input with its own headers")
|
||||
|
||||
var hls = try fixture(combinedJSON)
|
||||
hls["url"] = "https://media.example/live.m3u8"
|
||||
hls["protocol"] = "m3u8_native"
|
||||
let playlist = try decode(hls)
|
||||
try require(playlist.video.url == "https://media.example/live.m3u8" && playlist.audio == nil,
|
||||
"native HLS remains a usable streaming playlist, not a downloaded file")
|
||||
|
||||
var live = try fixture(combinedJSON)
|
||||
live["is_live"] = true
|
||||
live["duration"] = NSNull()
|
||||
let directLive = try decode(live)
|
||||
try require(directLive.isLive && directLive.duration == 0, "live stream permits unknown duration")
|
||||
live.removeValue(forKey: "is_live")
|
||||
live.removeValue(forKey: "duration")
|
||||
live["live_status"] = "is_live"
|
||||
try require(try decode(live).isLive, "live_status identifies live broadcasts")
|
||||
live["live_status"] = "was_live"
|
||||
try require(!(try decode(live)).isLive, "completed live broadcasts are recorded media")
|
||||
live["live_status"] = "is_upcoming"
|
||||
try reject("upcoming broadcast with no playable live content") { _ = try decode(live) }
|
||||
|
||||
var emptyHeaders = try fixture(combinedJSON)
|
||||
emptyHeaders.removeValue(forKey: "http_headers")
|
||||
try require(try decode(emptyHeaders).video.headers.isEmpty, "absent headers serialize to an empty block")
|
||||
}
|
||||
|
||||
static func malformedMetadata() throws {
|
||||
try reject("non-JSON extractor output") { _ = try decode("not JSON") }
|
||||
for json in ["null", "[]", "{}", #"{"requested_formats":[]}"#] {
|
||||
try reject("missing playable stream metadata") { _ = try decode(json) }
|
||||
}
|
||||
try reject("nonfinite duration") {
|
||||
_ = try decode(#"{"url":"https://media.example/video","vcodec":"h264","acodec":"aac","duration":1e999}"#)
|
||||
}
|
||||
let combined = try fixture(combinedJSON)
|
||||
for key in ["url", "vcodec", "acodec"] {
|
||||
var object = combined
|
||||
object.removeValue(forKey: key)
|
||||
try reject("missing required combined \(key)") { _ = try decode(object) }
|
||||
object[key] = 17
|
||||
try reject("wrong type for combined \(key)") { _ = try decode(object) }
|
||||
}
|
||||
for url in ["", "https://", "file:///tmp/video", "ftp://media.example/video",
|
||||
"data:video/mp4;base64,AA==", "https://media.example/video\r\nX-Injected: yes"] {
|
||||
var object = combined
|
||||
object["url"] = url
|
||||
try reject("invalid or unsafe stream URL") { _ = try decode(object) }
|
||||
}
|
||||
for duration in [-1, "19", true] as [Any] {
|
||||
var object = combined
|
||||
object["duration"] = duration
|
||||
try reject("invalid duration metadata") { _ = try decode(object) }
|
||||
}
|
||||
for key in ["is_live", "title"] {
|
||||
var object = combined
|
||||
object[key] = ["malformed"]
|
||||
try reject("malformed \(key) metadata") { _ = try decode(object) }
|
||||
}
|
||||
var hdr = combined
|
||||
hdr["dynamic_range"] = "HDR10"
|
||||
try reject("HDR input unsupported by the SDR renderer") { _ = try decode(hdr) }
|
||||
for value in ["none", NSNull()] as [Any] {
|
||||
var object = combined
|
||||
object["vcodec"] = value
|
||||
try reject("combined input without video codec") { _ = try decode(object) }
|
||||
}
|
||||
for transport in ["http_dash_segments", "mhtml", "rtmp"] {
|
||||
var object = combined
|
||||
object["protocol"] = transport
|
||||
try reject("fragment assembly or unsupported transport is not a whole playable input") {
|
||||
_ = try decode(object)
|
||||
}
|
||||
}
|
||||
|
||||
let split = try fixture(splitJSON)
|
||||
let formats = split["requested_formats"] as! [[String: Any]]
|
||||
for malformed in ["bad", [formats[0]], [formats[1]], [formats[0], formats[0]],
|
||||
[formats[0], formats[1], formats[1]]] as [Any] {
|
||||
var object = split
|
||||
object["requested_formats"] = malformed
|
||||
try reject("malformed or ambiguous split stream selection") { _ = try decode(object) }
|
||||
}
|
||||
var missingAudio = split
|
||||
var badFormats = formats
|
||||
badFormats[1].removeValue(forKey: "url")
|
||||
missingAudio["requested_formats"] = badFormats
|
||||
try reject("missing separate audio URL is not silently downgraded") { _ = try decode(missingAudio) }
|
||||
|
||||
let badHeaders: [Any] = [
|
||||
"not a header map", ["User-Agent": 5], ["Bad Name": "value"], ["Bad:Name": "value"],
|
||||
["Bad\r\nName": "value"], ["": "value"], ["X-Header": "a\r\nInjected: yes"],
|
||||
["X-Header": "a\nb"], ["X-Header": "a\rb"], ["X-Header": "a\u{0}b"],
|
||||
["X-Header": "a\t b"], ["X-Header": "a\u{7f}b"]
|
||||
]
|
||||
for headers in badHeaders {
|
||||
var object = combined
|
||||
object["http_headers"] = headers
|
||||
try reject("unsafe or malformed metadata headers") { _ = try decode(object) }
|
||||
var perInput = split
|
||||
var selected = formats
|
||||
selected[1]["http_headers"] = headers
|
||||
perInput["requested_formats"] = selected
|
||||
try reject("unsafe or malformed selected audio headers") { _ = try decode(perInput) }
|
||||
}
|
||||
}
|
||||
|
||||
struct Extractor: Sendable {
|
||||
let executable: URL
|
||||
let directory: URL
|
||||
|
||||
init(root: URL, name: String, json: String = YouTubeResolverChecks.splitJSON,
|
||||
body: String = #"/bin/cat "$base/response.json""#) throws {
|
||||
directory = root.appendingPathComponent(name, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false)
|
||||
executable = directory.appendingPathComponent("fake-extractor")
|
||||
let script = "#!/bin/sh\nset -eu\nbase=${0%/*}\n: > \"$base/started\"\n" + body + "\n"
|
||||
try Data(script.utf8).write(to: executable)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: executable.path)
|
||||
try Data(json.utf8).write(to: directory.appendingPathComponent("response.json"))
|
||||
}
|
||||
|
||||
func marker(_ name: String) -> URL { directory.appendingPathComponent(name) }
|
||||
}
|
||||
|
||||
static func awaitMarker(_ extractor: Extractor, _ name: String = "started") async throws {
|
||||
for _ in 0..<400 {
|
||||
if FileManager.default.fileExists(atPath: extractor.marker(name).path) { return }
|
||||
try await Task.sleep(for: .milliseconds(5))
|
||||
}
|
||||
throw Failure(description: "FAIL: extractor did not reach \(name) within the isolated check deadline")
|
||||
}
|
||||
|
||||
static func cancelled(_ task: Task<YouTubeVideo, Error>, _ message: String) async throws {
|
||||
do { _ = try await task.value }
|
||||
catch is CancellationError { return }
|
||||
catch { throw Failure(description: "FAIL: \(message) returned an extraction error instead of cancellation") }
|
||||
throw Failure(description: "FAIL: \(message) returned media instead of cancellation")
|
||||
}
|
||||
|
||||
static func subprocesses(root: URL, runtime: URL) async throws {
|
||||
let successful = try Extractor(root: root, name: "successful split")
|
||||
let result = try await YouTubeResolver.resolve(page, executableURL: successful.executable, runtimeURL: runtime)
|
||||
try require(result.video.url == "https://media.example/video?token=video"
|
||||
&& result.audio?.url == "https://media.example/audio?token=audio"
|
||||
&& result.title == "Separate streams" && result.duration == 19.25,
|
||||
"actual subprocess output becomes usable separate media inputs")
|
||||
|
||||
let denied = try Extractor(root: root, name: "denied", body: #"""
|
||||
/bin/cat "$base/response.json"
|
||||
printf '%s\n' 'ERROR: Sign in to confirm your age: https://media.example/private?secret=not-for-errors' >&2
|
||||
exit 17
|
||||
"""#)
|
||||
do {
|
||||
_ = try await YouTubeResolver.resolve(page, executableURL: denied.executable, runtimeURL: runtime)
|
||||
throw Failure(description: "FAIL: failed extractor returned media despite its exit status")
|
||||
} catch let failure as Failure { throw failure }
|
||||
catch {
|
||||
let text = String(describing: error) + error.localizedDescription
|
||||
try require(text.lowercased().contains("sign-in") || text.lowercased().contains("sign in"),
|
||||
"extractor stderr produces an actionable access error")
|
||||
try require(!text.contains("not-for-errors") && !text.contains("media.example"),
|
||||
"extractor error does not leak signed media URLs or credentials")
|
||||
}
|
||||
|
||||
let noisy = try Extractor(root: root, name: "pipe boundaries", body: #"""
|
||||
/bin/dd if=/dev/zero bs=1024 count=512 2>/dev/null | /usr/bin/tr '\000' x >&2
|
||||
/bin/dd if=/dev/zero bs=1024 count=512 2>/dev/null | /usr/bin/tr '\000' ' '
|
||||
/bin/cat "$base/response.json"
|
||||
"""#)
|
||||
let drained = try await YouTubeResolver.resolve(page, executableURL: noisy.executable, runtimeURL: runtime)
|
||||
try require(drained.audio?.url == result.audio?.url && drained.video.url == result.video.url,
|
||||
"stderr beyond its capture boundary and stdout beyond pipe capacity drain without deadlock")
|
||||
|
||||
let oversized = try Extractor(root: root, name: "oversized stdout", body: #"""
|
||||
/bin/dd if=/dev/zero bs=1024 count=17408 2>/dev/null | /usr/bin/tr '\000' ' '
|
||||
/bin/cat "$base/response.json"
|
||||
"""#)
|
||||
do {
|
||||
_ = try await YouTubeResolver.resolve(page, executableURL: oversized.executable, runtimeURL: runtime)
|
||||
throw Failure(description: "FAIL: oversized extractor output was accepted")
|
||||
} catch let failure as Failure { throw failure }
|
||||
catch {
|
||||
let text = (String(describing: error) + error.localizedDescription).lowercased()
|
||||
try require(text.contains("output") || text.contains("limit") || text.contains("large") || text.contains("metadata"),
|
||||
"bounded stdout rejection explains the output limit")
|
||||
}
|
||||
|
||||
let beforeLaunch = try Extractor(root: root, name: "already cancelled")
|
||||
let preCancelled = Task {
|
||||
withUnsafeCurrentTask { $0?.cancel() }
|
||||
return try await YouTubeResolver.resolve(page, executableURL: beforeLaunch.executable, runtimeURL: runtime)
|
||||
}
|
||||
try await cancelled(preCancelled, "cancellation before launch")
|
||||
try require(!FileManager.default.fileExists(atPath: beforeLaunch.marker("started").path),
|
||||
"a pre-cancelled request never starts the dependency")
|
||||
|
||||
let waitingScript = #"""
|
||||
trap '' TERM INT
|
||||
printf '%s\n' "$$" > "$base/pid"
|
||||
: > "$base/ready"
|
||||
while [ ! -f "$base/release" ]; do /bin/sleep 0.01; done
|
||||
/bin/cat "$base/response.json"
|
||||
"""#
|
||||
let old = try Extractor(root: root, name: "old request", body: waitingScript)
|
||||
let newer = try Extractor(root: root, name: "new request", json: combinedJSON, body: waitingScript)
|
||||
let oldTask = Task { try await YouTubeResolver.resolve(page, executableURL: old.executable, runtimeURL: runtime) }
|
||||
defer { oldTask.cancel() }
|
||||
try await awaitMarker(old, "ready")
|
||||
let newTask = Task { try await YouTubeResolver.resolve(page, executableURL: newer.executable, runtimeURL: runtime) }
|
||||
defer { newTask.cancel() }
|
||||
try await awaitMarker(newer, "ready")
|
||||
oldTask.cancel()
|
||||
try await cancelled(oldTask, "in-flight cancellation")
|
||||
let oldPID = try String(contentsOf: old.marker("pid"), encoding: .utf8)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try require(pid_t(oldPID).map { Darwin.kill($0, 0) == -1 && errno == ESRCH } == true,
|
||||
"cancelled extractor is reaped even when it ignores termination")
|
||||
oldTask.cancel()
|
||||
try Data().write(to: newer.marker("release"))
|
||||
let newResult = try await newTask.value
|
||||
try require(newResult.title == "Combined stream" && newResult.audio == nil
|
||||
&& newResult.video.url == "https://media.example/combined",
|
||||
"cancelling an old request cannot terminate or replace a newer resolution")
|
||||
}
|
||||
|
||||
static func main() async throws {
|
||||
// A broken pipe/cancellation implementation must fail, not leave this check hanging.
|
||||
let watchdog = Task {
|
||||
try await Task.sleep(for: .seconds(30))
|
||||
fatalError("FAIL: resolver checks exceeded their bounded subprocess deadline")
|
||||
}
|
||||
defer { watchdog.cancel() }
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("terebi resolver checks '\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let runtime = root.appendingPathComponent("deno")
|
||||
try FileManager.default.createSymbolicLink(at: runtime, withDestinationURL: URL(fileURLWithPath: "/usr/bin/true"))
|
||||
try videoURLs()
|
||||
try metadata()
|
||||
try malformedMetadata()
|
||||
try await subprocesses(root: root, runtime: runtime)
|
||||
print("YouTube resolver checks passed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user