feat: segment playback slider with resolved YouTube chapters
This commit is contained in:
@@ -113,12 +113,112 @@ struct ResolvedPlaybackCheck {
|
||||
try await providerLifecycle(source: source)
|
||||
try await dubOnlySelection(source: source)
|
||||
try await queueMetadataLifecycle(source: source)
|
||||
try await chapterLifecycle(source: source, file: file)
|
||||
try await streamQueueLifecycle(source: source)
|
||||
try await enqueueLifecycle(source: source)
|
||||
try await sidebarQueueLifecycle(source: source)
|
||||
}
|
||||
}
|
||||
|
||||
static var fixtureChapters: [YouTubeChapter] {
|
||||
[.init(start: 0, end: 1.25, title: "Opening"),
|
||||
.init(start: 1.25, end: 2.5, title: "Ending")]
|
||||
}
|
||||
|
||||
static func requireYouTubeChapters(_ model: PlayerModel, _ expected: [Double], _ context: String) {
|
||||
require(!model.isBusy && model.state.has_video && model.state.has_audio && model.state.duration > 2.5,
|
||||
"\(context): boundaries belong to decoded native media with measured duration")
|
||||
require(model.sliderBoundaries == expected, "\(context): exact fractional chapter boundaries")
|
||||
require(model.visibleIntervals.isEmpty && model.segmentToSkip == nil,
|
||||
"\(context): YouTube chapters never become Miruro bands or skip offers")
|
||||
}
|
||||
|
||||
static func chapterLifecycle(source: ResolvedSource, file: URL) async throws {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("terebi.chapter-check." + UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
let report = directory.appendingPathComponent("single-launch.jsonl")
|
||||
let suite = "terebi.chapter-check." + UUID().uuidString
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let chaptered = URL(string: "https://www.youtube.com/watch?v=AAAAAAAAAAA")!
|
||||
let chapterless = URL(string: "https://www.youtube.com/watch?v=BBBBBBBBBBB")!
|
||||
var requests: [URL] = []
|
||||
// The single-video Tubular launch uses this progress-file initialization plus openStream.
|
||||
let model = PlayerModel(defaults: defaults, progressFile: report.path, youtubeResolver: { url, height in
|
||||
requests.append(url)
|
||||
return YouTubeVideo(title: url == chaptered ? "Chaptered launch" : "Chapterless launch",
|
||||
video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: height ?? 1080, availableHeights: [1080, 720],
|
||||
chapters: url == chaptered ? fixtureChapters + [.init(start: 3, end: 500, title: nil)] : [])
|
||||
}, youtubeTitleResolver: { _ in nil },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
require(model.progressInitializationError == nil, "single Tubular launch initializes progress reporting")
|
||||
for (url, expected) in [(chaptered, [1.25, 2.5]), (chapterless, [])] {
|
||||
model.openStream(url.absoluteString)
|
||||
require(model.sliderBoundaries.isEmpty, "direct open hides prior chapters while resolving")
|
||||
try await until("direct YouTube stream decodes", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2
|
||||
}
|
||||
requireYouTubeChapters(model, expected, "direct openStream")
|
||||
require(model.state.duration < 500, "native duration rejects chapter edges beyond measured media")
|
||||
require(model.setPlayback(playing: false), "pause before saving chapter playback")
|
||||
try await until("chapter playback paused", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
require(model.seekRelative(0.8 - model.state.position), "save chapter playback bookmark")
|
||||
try await until("chapter bookmark acknowledged", model: model) {
|
||||
!model.state.seek_pending && abs(model.state.position - 0.8) < 0.1
|
||||
}
|
||||
model.closeFile()
|
||||
require(model.sliderBoundaries.isEmpty, "close immediately removes chapter boundaries")
|
||||
try await until("chapter playback closed", model: model) { model.state.status == PLAYER_EMPTY }
|
||||
require(model.savedPosition(for: url).map { abs($0 - 0.8) < 0.1 } == true,
|
||||
"acknowledged bookmark persists for canonical YouTube URL")
|
||||
require(model.recentMedia.first?.url == url, "recent entry retains original YouTube URL")
|
||||
model.open(model.recentMedia.first!.url)
|
||||
try await until("recent YouTube reopen restores native bookmark", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position >= 0.7
|
||||
}
|
||||
requireYouTubeChapters(model, expected, "saved/recent open(url)")
|
||||
require(model.setPlayback(playing: false), "pause before chapter quality replacement")
|
||||
try await until("chapter quality pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
let position = model.state.position
|
||||
model.setYouTubeQuality(720)
|
||||
require(model.sliderBoundaries.isEmpty, "quality extraction hides prior chapter boundaries")
|
||||
try await until("chapter quality replacement preserves pause and position", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PAUSED && model.youtubeHeight == 720 &&
|
||||
abs(model.state.position - position) < 0.2
|
||||
}
|
||||
requireYouTubeChapters(model, expected, "chaptered/chapterless quality replacement")
|
||||
}
|
||||
model.open(chaptered)
|
||||
try await until("chaptered media replaces chapterless media", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == "Chaptered launch"
|
||||
}
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "chaptered replacement")
|
||||
model.openResolvedSource(file)
|
||||
require(model.sliderBoundaries.isEmpty, "non-YouTube replacement hides prior chapters while opening")
|
||||
try await until("resolved replacement decodes without YouTube chapters", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.has_video && model.state.has_audio
|
||||
}
|
||||
require(model.sliderBoundaries.isEmpty, "resolved media does not inherit YouTube chapters")
|
||||
model.closeFile()
|
||||
try await until("resolved replacement closes", model: model) { model.state.status == PLAYER_EMPTY }
|
||||
require(model.sliderBoundaries.isEmpty, "empty player has no chapter boundaries")
|
||||
require(requests == [chaptered, chaptered, chaptered, chapterless, chapterless, chapterless, chaptered],
|
||||
"direct, recent, saved and quality opens each resolve fresh chapter metadata")
|
||||
model.shutdown()
|
||||
let rows = try String(contentsOf: report, encoding: .utf8).split(separator: "\n").map {
|
||||
try JSONSerialization.jsonObject(with: Data($0.utf8)) as! [String: Any]
|
||||
}
|
||||
require(rows.contains {
|
||||
$0["version"] as? Int == 1 && $0["media_url"] as? String == chaptered.absoluteString &&
|
||||
$0["event"] as? String == "progress"
|
||||
}, "single Tubular launch reports real playback under its canonical YouTube identity")
|
||||
print("PASS: chaptered/chapterless direct and single Tubular launch, saved/recent reopen, quality, measured duration, replacement and close")
|
||||
}
|
||||
|
||||
static func transportProtocol(source: ResolvedSource) async throws {
|
||||
let original = URLComponents(string: source.url)!
|
||||
let base = "\(original.scheme!)://\(original.host!):\(original.port!)"
|
||||
@@ -298,7 +398,8 @@ struct ResolvedPlaybackCheck {
|
||||
var titleRequests: [URL] = []
|
||||
func video(_ url: URL, title: String, height: Int = 1080) -> YouTubeVideo {
|
||||
YouTubeVideo(title: title, video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: height, availableHeights: [1080, 720])
|
||||
duration: 0, isLive: false, webpageURL: url, height: height, availableHeights: [1080, 720],
|
||||
chapters: height == 720 ? [.init(start: 0.75, end: 1.75, title: nil)] : fixtureChapters)
|
||||
}
|
||||
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, height in
|
||||
if holdFirst || height != nil {
|
||||
@@ -352,6 +453,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("foreground title takes precedence and native playback starts", model: model) {
|
||||
model.state.status == PLAYER_PLAYING && model.queue.currentItem?.displayName == "Playing title"
|
||||
}
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "foreground queued metadata")
|
||||
require(model.setPlayback(playing: false), "pause for metadata preservation")
|
||||
try await until("metadata smoke pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
let position = model.state.position
|
||||
@@ -383,10 +485,14 @@ struct ResolvedPlaybackCheck {
|
||||
for _ in 0..<10 { await Task.yield() }
|
||||
require(model.queue.items[1].displayName == urls[1].absoluteString,
|
||||
"removed/replaced item cannot rename a new row for the same URL")
|
||||
try await until("replacement native media ready for quality change", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == "Playing title"
|
||||
}
|
||||
require(model.setPlayback(playing: false), "pause before quality extraction")
|
||||
try await until("quality smoke pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
model.setYouTubeQuality(720)
|
||||
try await until("quality extraction is held", model: model) { foreground != nil }
|
||||
require(model.sliderBoundaries.isEmpty, "held quality replacement hides previous chapter boundaries")
|
||||
background!.resume(returning: "Replacement title")
|
||||
background = nil
|
||||
try await until("title arrives while quality extraction is held", model: model) {
|
||||
@@ -399,6 +505,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("quality extraction resumes paused native playback", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PAUSED && model.fileName == "Quality title"
|
||||
}
|
||||
requireYouTubeChapters(model, [0.75, 1.75], "quality replacement accepts fresh chapters and preserves pause")
|
||||
print("PASS: early current/queued titles during held playback and quality extraction; serial utility lookup, generic/Miruro titles, reorder, failure and stale replacement/foreground precedence")
|
||||
}
|
||||
|
||||
@@ -424,7 +531,8 @@ struct ResolvedPlaybackCheck {
|
||||
var stale: CheckedContinuation<YouTubeVideo, Never>?
|
||||
let video = YouTubeVideo(title: "Queue YouTube fixture",
|
||||
video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: youtubeURL, height: nil, availableHeights: [])
|
||||
duration: 0, isLive: false, webpageURL: youtubeURL, height: nil, availableHeights: [],
|
||||
chapters: fixtureChapters)
|
||||
let model = PlayerModel(defaults: defaults, streamQueue: queue, youtubeResolver: { _, _ in
|
||||
youtubeRequests += 1
|
||||
if youtubeRequests == 1 {
|
||||
@@ -447,17 +555,20 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("manual stream selection decodes native media", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2
|
||||
}
|
||||
require(model.sliderBoundaries.isEmpty, "selected Miruro media has no stale YouTube chapters")
|
||||
stale!.resume(returning: video)
|
||||
stale = nil
|
||||
// Yield to the canceled resolver; it must not open or redirect its report.
|
||||
for _ in 0..<10 { await Task.yield() }
|
||||
require(model.queue.currentID == secondID && !model.miruroVideoProviders.isEmpty &&
|
||||
!FileManager.default.fileExists(atPath: firstReport.path), "stale extraction cannot replace selected media or report")
|
||||
require(model.sliderBoundaries.isEmpty, "canceled chaptered extraction cannot add boundaries to Miruro media")
|
||||
model.advanceQueue(-1)
|
||||
try await until("previous resolves YouTube afresh", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == video.title
|
||||
}
|
||||
require(youtubeRequests == 2 && model.queue.currentID == firstID, "manual previous preserves queue and resolves current item")
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "batch queue previous")
|
||||
require(model.setPlayback(playing: false), "pause first queued stream")
|
||||
try await until("queue pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
require(model.seekRelative(max(0, model.duration - 0.3) - model.state.position), "seek first stream near natural end")
|
||||
@@ -469,6 +580,7 @@ struct ResolvedPlaybackCheck {
|
||||
model.queue.currentID == secondID && !model.isBusy && model.state.status == PLAYER_PLAYING &&
|
||||
!model.miruroVideoProviders.isEmpty && episodeRequests == 2
|
||||
}
|
||||
require(model.sliderBoundaries.isEmpty, "natural queue advance clears YouTube chapters for Miruro")
|
||||
require(model.setPlayback(playing: false), "pause second stream for restoration")
|
||||
try await until("second paused", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
require(model.seekRelative(0.8 - model.state.position), "save an acknowledged second-stream bookmark")
|
||||
@@ -479,6 +591,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("revisit first report session", model: model) {
|
||||
!model.isBusy && model.fileName == video.title && model.state.status == PLAYER_PLAYING
|
||||
}
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "batch queue revisit")
|
||||
model.advanceQueue(1)
|
||||
try await until("second revisit resumes acknowledged bookmark", model: model) {
|
||||
!model.isBusy && model.queue.currentID == secondID && model.state.status == PLAYER_PLAYING &&
|
||||
@@ -530,7 +643,8 @@ struct ResolvedPlaybackCheck {
|
||||
requests += 1
|
||||
return YouTubeVideo(title: url.absoluteString,
|
||||
video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [],
|
||||
chapters: url == first ? fixtureChapters : [])
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in episodeRequests += 1; return try providerSources(source) },
|
||||
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
|
||||
defer { model.shutdown() }
|
||||
@@ -545,6 +659,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("standalone stream begins before enqueue", model: model) {
|
||||
model.state.status == PLAYER_PLAYING && !model.isBusy && model.state.position > 0.2
|
||||
}
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "single stream before enqueue")
|
||||
require(model.setPlayback(playing: false), "pause original single-video playback")
|
||||
try await until("standalone pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
let selected = model.queue.currentID
|
||||
@@ -556,6 +671,7 @@ struct ResolvedPlaybackCheck {
|
||||
model.state.position == position && model.state.revision == revision &&
|
||||
model.fileName == first.absoluteString && requests == 1 && episodeRequests == 0,
|
||||
"paused enqueue preserves original media, selection and decoder timeline; new items resolve lazily in supplied order")
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "paused enqueue preserves current chapters")
|
||||
require(model.setPlayback(playing: true), "resume original playback")
|
||||
try await until("original playing again", model: model) { model.state.status == PLAYER_PLAYING }
|
||||
let playingRevision = model.state.revision
|
||||
@@ -564,6 +680,7 @@ struct ResolvedPlaybackCheck {
|
||||
model.queue.currentID == selected && model.state.revision == playingRevision &&
|
||||
model.state.status == PLAYER_PLAYING,
|
||||
"repeated native delivery appends while playing without reopening current media")
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "playing enqueue preserves current chapters")
|
||||
try deliver([(third, directory.appendingPathComponent("conflict.jsonl"))])
|
||||
require(model.commandError != nil && model.queue.items.count == 4 && model.queue.currentID == selected,
|
||||
"conflicting identity rejects complete enqueue without changing playback")
|
||||
@@ -588,10 +705,12 @@ struct ResolvedPlaybackCheck {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && !model.miruroVideoProviders.isEmpty &&
|
||||
model.queue.currentURL == second
|
||||
}
|
||||
require(model.sliderBoundaries.isEmpty, "enqueued Miruro advance clears current chapters")
|
||||
model.advanceQueue(1)
|
||||
try await until("enqueued YouTube item decodes", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == third.absoluteString
|
||||
}
|
||||
requireYouTubeChapters(model, [], "chapterless enqueued YouTube advance")
|
||||
model.advanceQueue(-1)
|
||||
try await until("enqueued Miruro report revisits", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.queue.currentURL == second
|
||||
@@ -600,6 +719,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("original single video remains navigable", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == first.absoluteString
|
||||
}
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "enqueued revisit restores chaptered YouTube")
|
||||
var retiredReports: [(URL, Data, URL, URL)] = []
|
||||
for (url, oldReport) in [(first, originalReport), (third, thirdReport)] {
|
||||
let oldID = model.queue.items.first(where: { $0.url == url })!.id
|
||||
@@ -649,7 +769,7 @@ struct ResolvedPlaybackCheck {
|
||||
require(!FileManager.default.fileExists(atPath: fourthReport.path), "unselected enqueued item never owns a writer")
|
||||
let empty = PlayerModel(defaults: defaults, youtubeResolver: { url, _ in
|
||||
YouTubeVideo(title: "Cold enqueue", video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [], chapters: [])
|
||||
}, youtubeTitleResolver: { _ in nil })
|
||||
defer { empty.shutdown() }
|
||||
let coldReport = directory.appendingPathComponent("cold.jsonl")
|
||||
@@ -658,6 +778,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("empty player starts first enqueued item", model: empty) {
|
||||
!empty.isBusy && empty.state.status == PLAYER_PLAYING && empty.queue.currentURL == fourth
|
||||
}
|
||||
requireYouTubeChapters(empty, [], "cold chapterless enqueue")
|
||||
print("PASS: repeated enqueue while paused/playing, standalone navigation, atomic conflicts, independent reports and empty-player start")
|
||||
}
|
||||
|
||||
@@ -677,7 +798,8 @@ struct ResolvedPlaybackCheck {
|
||||
if url == failedURL { throw CocoaError(.fileReadCorruptFile) }
|
||||
let video = YouTubeVideo(title: "Sidebar \(url == first ? "first" : "second")",
|
||||
video: .init(url: source.url, headers: source.headers), audio: nil,
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [])
|
||||
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [],
|
||||
chapters: url == first ? fixtureChapters : [])
|
||||
if url == cancelledURL { return await withCheckedContinuation { pending = $0 } }
|
||||
return video
|
||||
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in try providerSources(source) },
|
||||
@@ -688,6 +810,7 @@ struct ResolvedPlaybackCheck {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2
|
||||
}
|
||||
require(requests == 1 && model.queue.currentURL == first, "empty addition uses fetched source without re-extracting")
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "sidebar first native playback")
|
||||
require(model.setPlayback(playing: false), "pause before sidebar addition")
|
||||
try await until("sidebar pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
|
||||
let selected = model.queue.currentID
|
||||
@@ -722,18 +845,21 @@ struct ResolvedPlaybackCheck {
|
||||
while pending == nil { await Task.yield() }
|
||||
cancelled.cancel()
|
||||
pending!.resume(returning: YouTubeVideo(title: "Cancelled", video: .init(url: source.url, headers: source.headers),
|
||||
audio: nil, duration: 0, isLive: false, webpageURL: cancelledURL, height: nil, availableHeights: []))
|
||||
audio: nil, duration: 0, isLive: false, webpageURL: cancelledURL, height: nil, availableHeights: [],
|
||||
chapters: [.init(start: 0.75, end: 1.75, title: "Canceled metadata")]))
|
||||
do {
|
||||
try await cancelled.value
|
||||
fatalError("FAIL: cancelled sidebar addition accepted")
|
||||
} catch is CancellationError {}
|
||||
require(model.queue.items.count == 4 && model.queue.currentID == selected,
|
||||
"dismissed in-flight addition cannot append a late result")
|
||||
requireYouTubeChapters(model, [1.25, 2.5], "canceled sidebar metadata leaves accepted chapters unchanged")
|
||||
model.selectQueueItem(model.queue.items[1].id)
|
||||
try await until("sidebar-added item plays through native queue selection", model: model) {
|
||||
!model.isBusy && model.state.status == PLAYER_PLAYING && model.queue.currentURL == second &&
|
||||
model.fileName == "Sidebar second"
|
||||
}
|
||||
requireYouTubeChapters(model, [], "sidebar chapterless native selection")
|
||||
print("PASS: sidebar URL fetch/add, empty-player decode, paused/playing preservation, validation, failure, cancellation and selection")
|
||||
}
|
||||
|
||||
@@ -742,12 +868,15 @@ struct ResolvedPlaybackCheck {
|
||||
let ending = MiruroInterval(kind: .outro, start: 2, end: model.duration, episode: 1)
|
||||
let metadata = MiruroMetadata(title: nil, intervals: [opening, ending])
|
||||
require(model.segmentToSkip == nil && !model.skipSegment(), "absent metadata cannot skip")
|
||||
require(model.sliderBoundaries.isEmpty, "absent Miruro metadata leaves slider unsegmented")
|
||||
require(model.seekRelative(0.75 - model.state.position), "enter opening")
|
||||
try await until("opening position acknowledged", model: model) {
|
||||
!model.state.seek_pending && abs(model.state.position - 0.75) < 0.1
|
||||
}
|
||||
model.supplyMetadataForCheck(metadata, episode: 1)
|
||||
require(model.segmentToSkip == opening, "late metadata offers opening at acknowledged paused position")
|
||||
require(model.visibleIntervals == [opening, ending] && model.sliderBoundaries == [0.5, 1.5, 2],
|
||||
"Miruro retains intro/outro bands and interior slider boundaries without the media endpoint")
|
||||
require(model.skipOfferRemainingFraction == 1, "late entry starts with a full countdown background")
|
||||
clock.now = 105
|
||||
require(model.skipOfferRemainingFraction == 0.5, "paused countdown drains by elapsed offer time, not media position")
|
||||
@@ -777,6 +906,7 @@ struct ResolvedPlaybackCheck {
|
||||
require(model.segmentToSkip == nil, "ignored opening stays dismissed when revisited")
|
||||
model.closeFile()
|
||||
require(model.segmentToSkip == nil, "close removes offer")
|
||||
require(model.sliderBoundaries.isEmpty, "closing Miruro media removes slider boundaries")
|
||||
model.openResolvedSource(file)
|
||||
try await until("reopened media playing", model: model) {
|
||||
model.state.status == PLAYER_PLAYING && !model.state.seek_pending && !model.isBusy
|
||||
@@ -787,6 +917,7 @@ struct ResolvedPlaybackCheck {
|
||||
try await until("reopened opening acknowledged", model: model) {
|
||||
!model.state.seek_pending && abs(model.state.position - 0.75) < 0.1
|
||||
}
|
||||
require(model.sliderBoundaries.isEmpty, "reopened Miruro media waits for fresh metadata")
|
||||
model.supplyMetadataForCheck(metadata, episode: 1)
|
||||
try await until("new open resets dismissal after seek settles", model: model) {
|
||||
model.segmentToSkip == opening
|
||||
|
||||
Reference in New Issue
Block a user