feat: support Miruro provider tracks, proxies, and native embeds

This commit is contained in:
2026-10-22 19:04:30 +02:00
parent 80d9e705e5
commit 8df500da83
15 changed files with 1201 additions and 203 deletions
+77 -13
View File
@@ -32,12 +32,13 @@ struct ResolvedPlaybackCheck {
NSApp.setActivationPolicy(.prohibited)
let input = CommandLine.arguments[1]
let file = URL(fileURLWithPath: input)
let preference = CommandLine.arguments.first(where: { $0.hasPrefix("--provider=") }).map { String($0.dropFirst(11)) } ?? ""
let source: ResolvedSource
if input.hasPrefix("https://") {
guard case .miruro(let mediaID, let episode) = try StreamURL.parse(input) else {
fatalError("FAIL: URL smoke requires a Miruro episode")
}
source = try await MiruroResolver.resolve(mediaID: mediaID, episode: episode).select().source
source = try await MiruroResolver.resolve(mediaID: mediaID, episode: episode).select(videoProvider: preference).source
} else {
source = try ResolvedSource.load(from: file)
}
@@ -49,7 +50,8 @@ struct ResolvedPlaybackCheck {
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let clock = Clock()
let model = PlayerModel(defaults: defaults, progressFile: CommandLine.arguments.count > 2 ? CommandLine.arguments[2] : nil,
let report = CommandLine.arguments.count > 2 && !CommandLine.arguments[2].hasPrefix("--") ? CommandLine.arguments[2] : nil
let model = PlayerModel(defaults: defaults, configuration: .init(miruroVideoProvider: preference), progressFile: report,
miruroMetadataResolver: { id, episode in
if input.hasPrefix("https://") {
return try await MiruroMetadataResolver.resolve(mediaID: id, episode: episode)
@@ -85,7 +87,8 @@ struct ResolvedPlaybackCheck {
fatalError("FAIL: smoke requires a default subtitle")
}
let track = source.subtitles[index]
let document = try await WebVTTDocument.load(url: track.url, headers: track.headers)
let preparedTrack = try await MiruroTransport.subtitle(track)
let document = try await WebVTTDocument.load(url: preparedTrack.url, headers: preparedTrack.headers)
let playableCues = document.cues.filter { !$0.text.isEmpty && $0.start + 0.1 < model.state.duration }
require(playableCues.count >= 2, "smoke requires two visible subtitle cues")
let target = playableCues[1].start + min(0.1, (playableCues[1].end - playableCues[1].start) / 2)
@@ -105,8 +108,10 @@ struct ResolvedPlaybackCheck {
print("PASS: default VTT selected, forward/backward seek and acknowledged cue timing")
if !input.hasPrefix("https://") { try await segmentSkips(model, clock: clock, file: file) }
if !input.hasPrefix("https://") {
try await transportProtocol(source: source)
model.shutdown()
try await providerLifecycle(source: source)
try await dubOnlySelection(source: source)
try await queueMetadataLifecycle(source: source)
try await streamQueueLifecycle(source: source)
try await enqueueLifecycle(source: source)
@@ -114,7 +119,35 @@ struct ResolvedPlaybackCheck {
}
}
static func providerSources(_ source: ResolvedSource, alternate: Bool = false) throws -> MiruroSources {
static func transportProtocol(source: ResolvedSource) async throws {
let original = URLComponents(string: source.url)!
let base = "\(original.scheme!)://\(original.host!):\(original.port!)"
let deployment = MiruroProxyDeployment(base: URL(string: base.replacingOccurrences(of: "127.0.0.1", with: "localhost") + "/")!, key: [1, 2, 3])
var input = ResolvedSource(url: base + "/transport-master.m3u8?token=private", headers:
"Referer: https://transport.example/\r\nX-Secret: private\r\n",
identity: source.identity, title: source.title, subtitles: [])
input.requirements = .init(proxy: .init(rotate: false, segments: true),
segmentOrigin: base, playlistQuery: ["skip": "fixture"])
let prepared = try await MiruroTransport.prepare(input, deployment: deployment)
let (master, _) = try await URLSession.shared.data(from: URL(string: prepared.url)!)
let nested = String(data: master, encoding: .utf8)!.components(separatedBy: .newlines)
.first { $0.hasPrefix("http://") }!
let (variant, _) = try await URLSession.shared.data(from: URL(string: nested)!)
let lines = String(data: variant, encoding: .utf8)!.components(separatedBy: .newlines)
let segment = lines.first { $0.hasPrefix("http://") }!
let (body, response) = try await URLSession.shared.data(from: URL(string: segment)!)
require((response as? HTTPURLResponse)?.statusCode == 200 && body == Data((0..<256).map(UInt8.init)),
"proxied nested playlists decode once; query and segment-origin rules fetch real resource bytes")
let keyLine = lines.first { $0.hasPrefix("#EXT-X-KEY:") }!
let key = keyLine.components(separatedBy: "\"")[1]
let (_, blocked) = try await URLSession.shared.data(from: URL(string: key)!)
require((blocked as? HTTPURLResponse)?.statusCode == 502,
"manifest key cannot make the relay fetch a foreign loopback destination")
withExtendedLifetime(prepared) {}
print("PASS: proxy codec, already-proxied nested HLS, query/origin rules, credential isolation and private-destination rejection")
}
static func providerSources(_ source: ResolvedSource, alternate: Bool = false, track: String = "ssub") throws -> MiruroSources {
func headers(_ text: String) -> [String: String] {
Dictionary(uniqueKeysWithValues: text.components(separatedBy: "\r\n").filter { !$0.isEmpty }.map {
let parts = $0.split(separator: ":", maxSplits: 1)
@@ -140,11 +173,15 @@ struct ResolvedPlaybackCheck {
header: ["X-Resource": "alternate-private"],
subtitles: [["file": source.subtitles[0].url.replacingOccurrences(of: "/english.jpeg", with: "/alternate/english.jpeg"),
"format": "vtt", "language": "en", "label": "Alternate English", "default": true]]))
config["alternate"] = ["player": "native", "proxy": false]
let origin = URLComponents(string: source.url)!
let segmentOrigin = "\(origin.scheme!)://\(origin.host!):\(origin.port!)"
config["alternate"] = ["player": "native", "proxy": false,
"hls": ["query": ["enabled": true, "params": ["rewrite": "1"]],
"segments": ["enabled": true, "origin": segmentOrigin]]]
}
return try MiruroResolver.decode(JSONSerialization.data(withJSONObject: [
"episodeNumber": 1, "providerOrder": ["sun", "captions", "alternate"], "config": config,
"tracks": [["track": "ssub", "providers": providers]]
"tracks": [["track": track, "providers": providers]]
]), mediaID: "fixture-ID", episode: 1)
}
@@ -171,23 +208,29 @@ struct ResolvedPlaybackCheck {
try await until("configured missing host falls back with independent captions", model: model) {
model.canChangeMiruroProvider && model.state.position > 0.2 && !model.subtitleText.isEmpty
}
require(model.selectedMiruroVideoProvider == "sun" && model.selectedMiruroCaptionProvider == "captions",
require(model.selectedMiruroVideoProvider == "sun:ssub" && model.selectedMiruroCaptionProvider == "captions:ssub",
"startup preferences select independent providers with host fallback")
require(model.setPlayback(playing: false), "pause before provider switches")
try await until("provider pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
let position = model.state.position
let revision = model.state.revision
model.setMiruroCaptionProvider("alternate")
model.setMiruroCaptionProvider("alternate:ssub")
try await until("alternate provider's own VTT is loaded", model: model) { model.subtitleText == "Alternate English cue" }
require(model.state.revision == revision && model.state.position == position && requests == 1,
"caption selection does not reopen video, seek or refetch sources")
model.setMiruroVideoProvider("alternate")
model.setMiruroVideoProvider("alternate:ssub")
try await until("alternate host preserves paused position", model: model) {
model.canChangeMiruroProvider && model.state.status == PLAYER_PAUSED &&
model.state.revision != revision && abs(model.state.position - position) < 0.2
}
require(model.selectedMiruroVideoProvider == "alternate" && model.selectedMiruroCaptionProvider == "alternate",
require(model.selectedMiruroVideoProvider == "alternate:ssub" && model.selectedMiruroCaptionProvider == "alternate:ssub",
"host and captions remain independently selected")
let preparedRevision = model.state.revision
model.setMiruroCaptionProvider("captions:ssub")
try await until("caption change retains prepared video transport", model: model) {
!model.subtitleText.isEmpty && model.selectedMiruroCaptionProvider == "captions:ssub"
}
require(model.state.revision == preparedRevision, "prepared video is not reopened for captions")
model.selectSubtitle(nil)
model.setMiruroCaptionProvider("")
require(model.selectedSubtitle == nil && model.subtitleText.isEmpty, "caption provider respects explicit Off")
@@ -196,15 +239,15 @@ struct ResolvedPlaybackCheck {
model.setMiruroVideoProvider("")
try await until("automatic host returns to Sun and preserves playing intent", model: model) {
model.canChangeMiruroProvider && model.state.status == PLAYER_PLAYING &&
model.selectedMiruroVideoProvider == "sun"
model.selectedMiruroVideoProvider == "sun:ssub"
}
require(model.selectedSubtitle == nil, "host changes preserve explicit subtitle Off")
model.setMiruroVideoProvider("alternate")
model.setMiruroVideoProvider("alternate:ssub")
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
while (model.resolvingStream || model.state.status != PLAYER_PLAYING), ContinuousClock.now < deadline {
try await Task.sleep(for: .milliseconds(10))
}
require(model.commandError != nil && model.selectedMiruroVideoProvider == "sun" &&
require(model.commandError != nil && model.selectedMiruroVideoProvider == "sun:ssub" &&
model.state.status == PLAYER_PLAYING && model.canChangeMiruroProvider,
"failed host fetch retains previous source and resumes playback")
model.setMiruroVideoProvider("")
@@ -219,6 +262,27 @@ struct ResolvedPlaybackCheck {
print("PASS: provider config fallback, independent caption headers, in-place captions, paused/playing host switches and failure retention")
}
static func dubOnlySelection(source: ResolvedSource) async throws {
let providers = try providerSources(source, track: "dub")
let suite = "terebi.dub-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults, miruroResolver: { _, _ in providers },
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
defer { model.shutdown() }
model.openStream("https://barelystarted.miruro.tv/watch/fixture-ID/episode?ep=1")
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
while model.resolvingStream, ContinuousClock.now < deadline { try await Task.sleep(for: .milliseconds(10)) }
require(model.commandError != nil && model.miruroVideoProviders.contains("sun:dub") && model.canChangeMiruroProvider,
"dub-only episode exposes explicit choices without automatic language fallback")
model.setMiruroVideoProvider("sun:dub")
try await until("explicit dub-only choice decodes", model: model) {
model.state.has_video && model.state.position > 0.2 && model.selectedMiruroVideoProvider == "sun:dub"
}
require(model.selectedMiruroCaptionProvider == "captions:dub", "automatic captions follow explicit dub track")
print("PASS: dub-only choices remain accessible and explicit selection plays")
}
static func queueMetadataLifecycle(source: ResolvedSource) async throws {
let suite = "terebi.queue-metadata-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!