feat: support YouTube VTT subtitle selection

This commit is contained in:
2026-10-27 13:05:56 +01:00
parent a2c8cc8e4d
commit 0641859d95
12 changed files with 449 additions and 25 deletions
+16 -2
View File
@@ -68,7 +68,8 @@ struct AutomaticQualityPlaybackChecks {
until(model, "compact mode remains usable across empty playback polls while resolving") {
model.isCompact && !model.compactWindow.isTransitioning
}
require(window.level == .floating && window.frame.width < normalFrame.width,
require(model.compactWindow.presentationWindow!.level == .floating &&
model.compactWindow.presentationWindow!.frame.width < normalFrame.width,
"unresolved video has a usable compact window")
model.handleKey(compactKey)
until(model, "compact shortcut restores while YouTube resolution is still pending") {
@@ -86,7 +87,8 @@ struct AutomaticQualityPlaybackChecks {
require(model.isCompact, "loading completion preserves the requested compact mode")
until(model, "decoded metadata updates compact geometry") {
!model.compactWindow.isTransitioning &&
abs(window.frame.width / window.frame.height - model.state.video_aspect_ratio) < 0.01
abs(model.compactWindow.presentationWindow!.frame.width / model.compactWindow.presentationWindow!.frame.height -
model.state.video_aspect_ratio) < 0.01
}
model.handleKey(compactKey)
until(model, "compact shortcut restores during network buffering") {
@@ -100,6 +102,12 @@ struct AutomaticQualityPlaybackChecks {
polled(model)
require(model.youtubeHeight == 1080 && !model.resolvingStream,
"disabled automatic quality leaves a stalled manual/default stream alone")
let captionCountURL = URL(string: ProcessInfo.processInfo.environment["TEREBI_CAPTION_COUNT"]!)!
require(try! String(contentsOf: captionCountURL, encoding: .utf8) == "0",
"Off never requests captions during extraction or playback")
require(model.selectedSubtitle == nil && model.subtitleTracks.count == 1, "YouTube captions default Off")
model.selectSubtitle(0)
until(model, "selected original-auto VTT loads through native HTTP") { model.subtitleText == "Automatic caption" }
model.setAutomaticYouTubeQuality(true)
polled(model)
let position = model.state.position
@@ -110,11 +118,16 @@ struct AutomaticQualityPlaybackChecks {
}
require(model.state.position >= position,
"automatic quality reopening does not reset acknowledged progress")
until(model, "automatic downshift restores the selected caption") { model.subtitleText == "Automatic caption" }
require(model.selectedSubtitle == 0, "automatic downshift retains caption identity")
polled(model)
clock.now = 2063
until(model, "sustained health automatically restores actual 1080p media") {
model.youtubeHeight == 1080 && model.canTogglePlayback && model.state.status == PLAYER_PLAYING && !model.state.seek_pending
}
until(model, "automatic upshift restores the selected caption") { model.subtitleText == "Automatic caption" }
require(model.selectedSubtitle == 0, "automatic upshift retains caption identity")
model.selectSubtitle(nil)
model.setYouTubeQuality(model.youtubeHeight)
require(!model.automaticYouTubeQuality && !model.isBusy && model.state.status == PLAYER_PLAYING,
"reselecting the current manual resolution disables auto without disrupting playback")
@@ -134,6 +147,7 @@ struct AutomaticQualityPlaybackChecks {
model.youtubeHeight == 720 && model.canChangeYouTubeQuality && model.state.status == PLAYER_PLAYING &&
!model.state.seek_pending && (model.bufferedRange.map { $0.upperBound - model.state.position >= 20 } ?? false)
}
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "manual changes preserve Off after automatic changes")
let selectedTitle = model.fileName
let selectedQuality = model.youtubeQuality
mark("gate-1080")
+34 -3
View File
@@ -7,16 +7,45 @@ from pathlib import Path
import shlex
import subprocess
import tempfile
from urllib.parse import urlsplit
from streaming import StreamingServer
class CaptionStreamingServer(StreamingServer):
def __init__(self, fixtures):
super().__init__(fixtures)
self.caption_requests = 0
def _get(self, request):
path = urlsplit(request.path).path
if path == "/caption-count":
self._reply(request, 200, str(self.caption_requests).encode())
return
if path == "/captions.vtt":
if request.headers.get("X-Subtitle") != "caption-private":
self._reply(request, 403, b"Missing caption credentials")
return
self.caption_requests += 1
self._reply(request, 200, b"WEBVTT\n\n00:00.000 --> 02:00.000\nAutomatic caption\n", "text/vtt")
return
super()._get(request)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("binary", type=Path)
parser.add_argument("fixtures", type=Path)
parser.add_argument("app", type=Path)
parser.add_argument("--cast-session", type=Path)
args = parser.parse_args()
if args.cast_session:
with CaptionStreamingServer(args.fixtures) as server:
result = subprocess.run([str(args.cast_session.resolve()), "--youtube-subtitles",
str(args.binary.resolve()), str(args.fixtures.resolve() / "cache.mp4")],
env=dict(os.environ, TEREBI_CAST_YOUTUBE_SOURCE=server.base_url), timeout=150, check=True)
assert server.caption_requests == 2, "only initial selection and local restoration fetch captions"
return result.returncode
macOS = args.app.resolve() / "Contents/MacOS"
extractor = str(macOS / "yt-dlp")
runtime = macOS / "deno"
@@ -25,7 +54,7 @@ def main():
required = ("quality-1080.mp4", "quality-720.mp4")
if any(not (args.fixtures / name).is_file() for name in required):
parser.error("Missing real quality playback fixtures")
with StreamingServer(args.fixtures) as server, tempfile.TemporaryDirectory(prefix="terebi-quality-check-") as directory:
with CaptionStreamingServer(args.fixtures) as server, tempfile.TemporaryDirectory(prefix="terebi-quality-check-") as directory:
root = Path(directory)
(root / "deno").symlink_to(runtime)
formats = []
@@ -37,7 +66,9 @@ def main():
"http_headers": {"X-Terebi-Fixture": "combined"}})
info = {"id": "aqz-KE-bpKQ", "title": "Automatic quality playback fixture", "duration": 120,
"is_live": False, "webpage_url": "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
"extractor": "youtube", "extractor_key": "Youtube", "formats": formats}
"extractor": "youtube", "extractor_key": "Youtube", "formats": formats,
"automatic_captions": {"en-orig": [{"ext": "vtt", "name": "English (Original)",
"url": server.base_url + "/captions.vtt", "http_headers": {"X-Subtitle": "caption-private"}}]}}
metadata = root / "info.json"
metadata.write_text(json.dumps(info))
wrapper = root / "yt-dlp"
@@ -67,7 +98,7 @@ def main():
+ ' "${args[@]:0:${#args[@]}-2}"\n')
wrapper.chmod(0o755)
environment = dict(os.environ, PATH=str(root) + os.pathsep + os.environ.get("PATH", ""),
TEREBI_QUALITY_CONTROL=str(control))
TEREBI_QUALITY_CONTROL=str(control), TEREBI_CAPTION_COUNT=server.base_url + "/caption-count")
try:
result = subprocess.run([str(args.binary.resolve())], env=environment, timeout=90, check=False)
except subprocess.TimeoutExpired:
+30 -3
View File
@@ -211,7 +211,19 @@ struct CastPlaybackCheck {
let suite = "terebi.cast-playback-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults, skipClock: { 100 })
let youtubeSource = ProcessInfo.processInfo.environment["TEREBI_CAST_YOUTUBE_SOURCE"]
let identity = youtubeSource == nil ? source : URL(string: "https://www.youtube.com/watch?v=AAAAAAAAAAA")!
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, _ in
let base = youtubeSource!
return YouTubeVideo(title: "YouTube Cast captions", video: .init(url: base + "/media/cache.mp4",
headers: "X-Terebi-Fixture: combined\r\n"), audio: nil, duration: 120, isLive: false,
webpageURL: url, height: 1080, availableHeights: [1080], chapters: [],
subtitles: [
.init(url: base + "/captions.vtt", format: "vtt", language: "en", label: "English",
isDefault: false, headers: ["X-Subtitle": "caption-private"], selectionID: "youtube:manual:en"),
.init(url: base + "/unavailable.vtt", format: "vtt", language: "fr", label: "Unavailable",
isDefault: false, headers: [:], selectionID: "youtube:manual:fr")])
}, youtubeTitleResolver: { _ in nil }, skipClock: { 100 })
defer { model.shutdown() }
if CommandLine.arguments.count == 5 {
if CommandLine.arguments[3] == "queue" {
@@ -223,7 +235,7 @@ struct CastPlaybackCheck {
return
}
model.setVolume(0)
model.open(source)
model.open(identity)
try await until(model, "local source plays") { model.state.status == PLAYER_PLAYING && model.canSeek }
requirePlaybackPower(system: true, display: true, "local video prevents system and display sleep")
require(model.setPlayback(playing: false), "local pause accepted")
@@ -232,6 +244,10 @@ struct CastPlaybackCheck {
require(model.seekRelative(9.25 - model.state.position), "local restoration position accepted")
require(!model.canCast, "Cast cannot transfer an unacknowledged local seek")
try await until(model, "local position acknowledged") { !model.state.seek_pending && abs(model.state.position - 9.25) < 0.15 }
if youtubeSource != nil {
model.selectSubtitle(0)
try await until(model, "local YouTube caption selected before handoff") { model.subtitleText == "Automatic caption" }
}
model.cast(to: CastDevice(id: "loopback", name: "Loopback TV", host: "127.0.0.1", port: port))
requirePlaybackPower(system: true, display: false, "pending Cast handoff protects the relay")
try await until(model, "receiver load acknowledges paused handoff") {
@@ -240,6 +256,12 @@ struct CastPlaybackCheck {
requirePlaybackPower(system: false, display: false, "paused Cast permits sleep")
require(abs(model.state.position - 9.25) < 0.15, "Cast starts at acknowledged local position")
require(!model.canStepFrame && model.bufferedRange == nil, "local decoder-only controls are not exposed while casting")
if youtubeSource != nil {
require(!model.canSelectSubtitle && model.selectedSubtitle == 0 && model.subtitleText.isEmpty,
"YouTube Cast disables local captions without losing the local choice")
model.selectSubtitle(1)
require(model.commandError == nil && model.selectedSubtitle == 0, "unsupported Cast captions cannot interrupt playback")
}
require(model.setPlayback(playing: true), "remote play accepted")
try await until(model, "receiver play acknowledged") { model.state.status == PLAYER_PLAYING && model.playbackIntent == nil }
requirePlaybackPower(system: true, display: false, "playing Cast protects the relay but permits display sleep")
@@ -266,7 +288,12 @@ struct CastPlaybackCheck {
abs(model.state.position - 24.25) < 0.15 && !model.isBusy
}
requirePlaybackPower(system: false, display: false, "paused local restoration permits sleep")
require(abs((PlaybackProgress(defaults: defaults).position(for: source) ?? 0) - 24.25) < 0.15,
if youtubeSource != nil {
try await until(model, "return to Mac restores local YouTube caption selection") {
model.canSelectSubtitle && model.selectedSubtitle == 0 && model.subtitleText == "Automatic caption"
}
}
require(abs((PlaybackProgress(defaults: defaults).position(for: identity) ?? 0) - 24.25) < 0.15,
"remote history is not overwritten by the abandoned local position")
require(model.volume == 0, "local volume survives receiver volume changes")
require(model.setPlayback(playing: true), "resume local playback before a failed Cast connection")
+11
View File
@@ -879,6 +879,12 @@ struct CastSessionCheck {
try await until("Disconnect STOP reaches receiver") { peer.commands.contains { $0.0 == "STOP" } }
let load = peer.commands.first { $0.0 == "LOAD" }!.1
require(load["currentTime"] as? Double == 9.25 && load["autoplay"] as? Bool == false, "Acknowledged paused handoff")
if ProcessInfo.processInfo.environment["TEREBI_CAST_YOUTUBE_SOURCE"] != nil {
let media = load["media"] as! [String: Any]
require((media["tracks"] as? [Any] ?? []).isEmpty &&
(load["activeTrackIds"] as? [Any] ?? []).isEmpty,
"YouTube Cast LOAD contains no captions or invalid local track selection")
}
require(peer.relayVerified, "Receiver fetches real relay HLS and media bytes")
}
@@ -1034,6 +1040,11 @@ struct CastSessionCheck {
print("PASS: latest slider intent, stale volume feedback, authoritative acknowledgement, external TV changes, timeout")
return
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--youtube-subtitles" {
try await playback(identity: identity, executable: CommandLine.arguments[2], file: CommandLine.arguments[3])
print("PASS: YouTube Cast omits optional captions and restores the local choice on return")
return
}
if CommandLine.arguments.count == 4, CommandLine.arguments[1] == "--playback" {
try await playback(identity: identity, executable: CommandLine.arguments[2], file: CommandLine.arguments[3])
print("PASS: native TLS receiver fetches relay and acknowledges PlayerModel handoff/control/disconnect")
+6 -1
View File
@@ -70,7 +70,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
captions = path.startswith("/captions/")
if alternate or captions:
path = "/" + path.split("/", 2)[2]
expected = "alternate-private" if alternate else "subtitle-private" if captions or path == "/english.jpeg" else "stream-private"
expected = "alternate-private" if alternate else "subtitle-private" if captions or path in ("/english.jpeg", "/denied.vtt") else "stream-private"
if self.headers.get("X-Resource") != expected:
errors.append("wrong resource headers: " + path)
self.send_error(403)
@@ -90,6 +90,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
elif path == "/english.jpeg":
data = (b'WEBVTT\n\n00:00.000 --> 00:10.000\nAlternate English cue\n' if alternate else
b'WEBVTT\n\n00:00.000 --> 00:02.000\nFirst English cue\n\n00:02.000 --> 00:04.000\nSecond English cue\n')
if parse_qs(urlsplit(self.path).query).get("auto") == ["1"]:
data = data.replace(b"First English cue", b"First automatic cue").replace(b"Second English cue", b"Second automatic cue")
elif path == "/denied.vtt":
self.send_error(403)
return
elif path.startswith("/resolved-vod-") and path.endswith(".ts"):
data = (fixtures / path[1:]).read_bytes()
else:
+101 -1
View File
@@ -124,6 +124,7 @@ struct ResolvedPlaybackCheck {
if !input.hasPrefix("https://") {
try await transportProtocol(source: source)
model.shutdown()
try await youtubeSubtitleLifecycle(source: source)
try await automaticSegmentSkips(file: file)
try await providerLifecycle(source: source)
try await dubOnlySelection(source: source)
@@ -137,6 +138,103 @@ struct ResolvedPlaybackCheck {
}
}
static func youtubeSubtitleLifecycle(source: ResolvedSource) async throws {
let suite = "terebi.youtube-subtitle-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
var resolution = 0
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, height in
resolution += 1
let manual = ResolvedSubtitle(url: source.subtitles[0].url + "?revision=\(resolution)",
format: "vtt", language: "en", label: "English", isDefault: false,
headers: ["X-Resource": "subtitle-private"], selectionID: "youtube:manual:en")
let automatic = ResolvedSubtitle(url: manual.url + "&auto=1", format: "vtt",
language: "en", label: "English (auto-generated)", isDefault: false,
headers: ["X-Resource": "subtitle-private"], selectionID: "youtube:auto:en")
let denied = ResolvedSubtitle(url: source.subtitles[0].url.replacingOccurrences(of: "english.jpeg", with: "denied.vtt"),
format: "vtt", language: "fr", label: "Unavailable", isDefault: false,
headers: ["X-Resource": "subtitle-private"], selectionID: "youtube:manual:fr")
let malformed = ResolvedSubtitle(url: source.url, format: "vtt", language: "de",
label: "Malformed", isDefault: false, headers: ["X-Resource": "stream-private"],
selectionID: "youtube:manual:de")
return YouTubeVideo(title: "YouTube caption fixture", video: .init(url: source.url, headers: source.headers),
audio: nil, duration: 0, isLive: false, webpageURL: url, height: height ?? 1080,
availableHeights: [1080, 720], chapters: [],
subtitles: resolution >= 4 ? [manual, denied, malformed] :
(resolution.isMultiple(of: 2) ? [automatic, manual, denied, malformed] : [manual, automatic, denied, malformed]))
}, youtubeTitleResolver: { _ in nil })
defer { model.shutdown() }
model.setVolume(0)
let url = URL(string: "https://www.youtube.com/watch?v=AAAAAAAAAAA")!
model.openStream(url.absoluteString)
try await until("YouTube playback starts with optional captions Off", model: model) {
model.canChangeYouTubeQuality && model.state.position > 0.2
}
require(model.subtitleTracks.count == 4 && model.selectedSubtitle == nil &&
model.subtitleText.isEmpty && model.subtitleError == nil, "YouTube defaults Off")
require(model.setPlayback(playing: false), "pause caption fixture")
try await until("caption fixture pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
model.selectSubtitle(1)
try await until("native caption HTTP loader renders automatic English", model: model) {
model.subtitleText == "First automatic cue"
}
require(model.seekRelative(2.5 - model.state.position), "forward caption seek accepted")
try await until("forward seek uses acknowledged caption position", model: model) {
!model.state.seek_pending && model.subtitleText == "Second automatic cue"
}
model.setYouTubeQuality(720)
try await until("quality preserves auto identity despite reordered tracks and refreshed URLs", model: model) {
model.canChangeYouTubeQuality && model.youtubeHeight == 720 && model.selectedSubtitle == 0 &&
model.subtitleText == "Second automatic cue" && model.state.status == PLAYER_PAUSED
}
require(model.seekRelative(0.5 - model.state.position), "backward caption seek accepted")
try await until("backward seek restores earlier caption", model: model) {
!model.state.seek_pending && model.subtitleText == "First automatic cue"
}
model.selectSubtitle(nil)
model.setYouTubeQuality(1080)
try await until("manual quality preserves explicit Off", model: model) {
model.canChangeYouTubeQuality && model.youtubeHeight == 1080
}
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "quality must not re-enable captions")
model.selectSubtitle(1)
try await until("automatic track can be selected again", model: model) { model.subtitleText == "First automatic cue" }
model.setYouTubeQuality(720)
try await until("missing automatic track switches Off, not to manual English", model: model) {
model.canChangeYouTubeQuality && model.youtubeHeight == 720
}
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "missing identity has no language fallback")
require(model.setPlayback(playing: true), "resume before optional caption failures")
for index in [1, 2] {
let position = model.state.position
model.selectSubtitle(index)
try await until("inaccessible/malformed captions fail while video keeps advancing", model: model) {
model.subtitleError != nil && model.state.status == PLAYER_PLAYING && model.state.position > position + 0.1
}
require(model.commandError == nil && model.state.has_video && model.state.has_audio &&
model.canSeek, "caption failures retain usable video")
}
require(model.setPlayback(playing: false), "pause after optional caption failures")
try await until("video still pauses normally after caption errors", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(0.5 - model.state.position), "video still seeks normally after caption errors")
try await until("post-error seek acknowledged", model: model) { !model.state.seek_pending && abs(model.state.position - 0.5) < 0.1 }
model.selectSubtitle(0)
try await until("valid captions recover without reopening video", model: model) {
model.subtitleText == "First English cue" && model.subtitleError == nil
}
try await model.addStreamToQueue("https://www.youtube.com/watch?v=BBBBBBBBBBB")
model.selectQueueItem(model.queue.items.last!.id)
require(model.subtitleTracks.isEmpty && model.selectedSubtitle == nil &&
model.subtitleText.isEmpty && model.subtitleError == nil, "queue change immediately clears old captions")
try await until("next queued video starts with fresh tracks Off", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && !model.subtitleTracks.isEmpty
}
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "queue selection never inherits prior captions")
model.closeFile()
require(model.subtitleTracks.isEmpty && model.subtitleError == nil, "close clears caption state")
print("PASS: YouTube native VTT fetch, Off/On, seeks, stable auto/manual quality identity, missing tracks, optional failures and queue reset")
}
static func zokoIntervalLifecycle(source: ResolvedSource) async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("terebi.zoko-interval-check." + UUID().uuidString)
@@ -398,7 +496,9 @@ struct ResolvedPlaybackCheck {
}
require(held.count == 2, "quality replacement coalesces the in-flight current-video lookup")
held[1].resume(returning: fixtureSponsors)
try await until("pending metadata survives quality replacement", model: model) { model.sponsoredRanges == [0.5...1.5] }
try await until("pending metadata survives quality replacement and the next quality choice is available", model: model) {
model.sponsoredRanges == [0.5...1.5] && model.canChangeYouTubeQuality
}
model.setYouTubeQuality(1080)
try await until("completed metadata survives quality replacement", model: model) {
!model.isBusy && model.youtubeHeight == 1080 && model.sponsoredRanges == [0.5...1.5]
+107
View File
@@ -167,6 +167,112 @@ struct YouTubeResolverChecks {
try require(try decode(emptyHeaders).video.headers.isEmpty, "absent headers serialize to an empty block")
}
static func subtitles() throws {
func vtt(_ url: String, name: Any = "English", headers: Any? = nil) -> [String: Any] {
var row: [String: Any] = ["ext": "vtt", "url": url, "name": name]
if let headers { row["http_headers"] = headers }
return row
}
let original = "https://www.youtube.com/api/timedtext?lang=en&kind=asr&fmt=vtt"
var object = try fixture(combinedJSON)
try require(try decode(object).subtitles.isEmpty, "absent captions default to Off with no optional tracks")
for payload in [NSNull(), [], true, 17, "captions"] as [Any] {
object["subtitles"] = payload
object["automatic_captions"] = payload
try require(try decode(object).subtitles.isEmpty,
"malformed optional caption containers never block playback")
}
object["subtitles"] = [
"broken": true,
"en": [NSNull(), 7, ["ext": false], ["ext": "vtt", "url": 9],
["ext": "srt", "url": "https://subs.example/convert-me"],
vtt("file:///unsafe.vtt"),
vtt("https://subs.example/unsafe.vtt", headers: ["X-Unsafe": "bad\nvalue"]),
vtt("https://subs.example/bad-headers.vtt", headers: ["X-Bad": 3]),
vtt("https://subs.example/en.vtt?token=first", headers: ["X-Track": "manual"]),
vtt("https://subs.example/duplicate.vtt")],
"fr": [vtt("https://subs.example/fr.vtt", name: ["malformed"])],
"srt-only": [["ext": "srt", "url": "https://subs.example/only.srt"]]
] as [String: Any]
object["automatic_captions"] = [
"en-orig": [vtt(original, headers: ["X-Track": "auto"])],
"en": [vtt(original)],
"fr": [vtt(original + "&tlang=fr", name: "French")],
"de": [vtt("https://subs.example/de.vtt", name: "German (Original)")],
"bad-orig": [NSNull(), vtt("ftp://subs.example/bad.vtt")]
] as [String: Any]
let tracks = try decode(object).subtitles
try require(tracks.count == 3 && tracks.map(\.language) == ["en", "fr", "en"],
"manual VTT and one original auto survive malformed siblings; translations and aliases do not")
try require(tracks.map(\.selectionID) == ["youtube:manual:en", "youtube:manual:fr", "youtube:auto:en"],
"manual and auto identities depend on language metadata, not signed URLs or labels")
try require(tracks[0].url == "https://subs.example/en.vtt?token=first"
&& tracks[0].label == "English" && tracks[1].label == "fr"
&& tracks[2].label == "English (auto-generated)",
"one valid supplied VTT per language preserves name and query; malformed name falls back to language")
try require(tracks.allSatisfy { !$0.isDefault } && ResolvedSource.validSubtitles(tracks),
"optional captions preserve transport safeguards and never enable themselves")
try require(tracks[0].headers == "X-Track: manual\r\n"
&& tracks[1].headers == "User-Agent: combined-agent\r\n"
&& tracks[2].headers == "X-Track: auto\r\n",
"variant headers override the complete root map; absent variant headers inherit root")
var changed = object
changed["automatic_captions"] = ["en": [vtt(original + "&token=renewed", name: "Renamed")]]
let alias = try decode(changed).subtitles.last!
try require(alias.selectionID == tracks.last?.selectionID && alias.url != tracks.last?.url,
"explicit untranslated ASR alias retains original identity after signed URL and label refresh")
changed["automatic_captions"] = [
"en-orig": [vtt(original)],
"en": [vtt(original + "&token=alias")],
"other-orig": [vtt(original)]
]
try require(try decode(changed).subtitles.filter { $0.selectionID?.hasPrefix("youtube:auto:") == true }.count == 1,
"original auto aliases deduplicate by language and by URL")
changed["automatic_captions"] = ["en-orig": [vtt(original)]]
changed["subtitles"] = ["en": [vtt("https://subs.example/empty.vtt", headers: [:])]]
try require(try decode(changed).subtitles.first?.headers == "",
"an explicitly empty caption header map does not inherit root headers")
for unsafe in ["https://user:pass@subs.example/c.vtt", "https://subs.example/c.vtt#fragment",
"https://subs.example:0/c.vtt", "https://subs.example/c vtt"] {
changed["subtitles"] = ["en": [vtt(unsafe)]]
try require(try decode(changed).subtitles.count == 1,
"unsafe optional subtitle URL is skipped without discarding a good auto sibling")
}
var fragmented = vtt("https://subs.example/chunks")
fragmented["protocol"] = "m3u8_native"
changed["subtitles"] = ["en": [fragmented]]
try require(try decode(changed).subtitles.count == 1,
"caption playlists are not treated as immutable VTT documents")
var many: [String: Any] = [:]
for index in 0..<40 {
let key = String(format: "lang-%02d", index)
many[key] = [vtt("https://subs.example/\(key).vtt", name: key)]
}
changed["subtitles"] = many
let capped = try decode(changed).subtitles
try require(capped.count == 32 && capped.last?.selectionID == "youtube:auto:en"
&& capped.first?.language == "lang-00" && capped[30].language == "lang-30",
"deterministic 32-track cap reserves original auto instead of crowding it out with manual languages")
changed["automatic_captions"] = [:]
try require(try decode(changed).subtitles.count == 32,
"manual-only metadata uses the entire track allowance")
changed["automatic_captions"] = ["en-orig": [vtt(original + "&tlang=fr")]]
let translated = try decode(changed).subtitles
try require(translated.count == 32
&& translated.allSatisfy { $0.selectionID?.hasPrefix("youtube:manual:") == true },
"translation query metadata is excluded even under an original-looking key")
for (key, value) in [("is_live", true), ("live_status", "is_live")] as [(String, Any)] {
var broadcast = object
broadcast[key] = value
try require(try decode(broadcast).subtitles.isEmpty,
"live captions are disabled instead of freezing a partial live transcript")
}
changed["url"] = "file:///unsafe-video"
try reject("optional caption tolerance does not weaken video validation") { _ = try decode(changed) }
}
static func chapters() throws {
var object = try fixture(combinedJSON)
try require(try decode(object).chapters.isEmpty, "absent chapters remain playable")
@@ -683,6 +789,7 @@ struct YouTubeResolverChecks {
try videoURLs()
try metadata()
try chapters()
try subtitles()
try malformedMetadata()
try qualities()
try bundledHelpers(root: root)