Keep audio session warm on native resume

This commit is contained in:
dirtydishes 2026-05-27 00:30:26 -04:00
parent ae5fb256ae
commit f0226c651d
6 changed files with 602 additions and 0 deletions

View file

@ -43,3 +43,4 @@
{"id":"int-8c109835","kind":"field_change","created_at":"2026-05-27T03:47:00.090296Z","actor":"dirtydishes","issue_id":"dreamio-e3u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented VLC playback state hardening, instrumentation, ready-once reporting, refresh throttling, tests, and turn documentation."}} {"id":"int-8c109835","kind":"field_change","created_at":"2026-05-27T03:47:00.090296Z","actor":"dirtydishes","issue_id":"dreamio-e3u","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented VLC playback state hardening, instrumentation, ready-once reporting, refresh throttling, tests, and turn documentation."}}
{"id":"int-3533f9f7","kind":"field_change","created_at":"2026-05-27T04:09:20.72451Z","actor":"dirtydishes","issue_id":"dreamio-ccn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented playback-first native startup and background parallel subtitle resolution."}} {"id":"int-3533f9f7","kind":"field_change","created_at":"2026-05-27T04:09:20.72451Z","actor":"dirtydishes","issue_id":"dreamio-ccn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented playback-first native startup and background parallel subtitle resolution."}}
{"id":"int-c55d4a7b","kind":"field_change","created_at":"2026-05-27T04:20:08.84278Z","actor":"dirtydishes","issue_id":"dreamio-e2q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by gating native startup loading until VLC readiness and startup subtitle candidate processing both complete."}} {"id":"int-c55d4a7b","kind":"field_change","created_at":"2026-05-27T04:20:08.84278Z","actor":"dirtydishes","issue_id":"dreamio-e2q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by gating native startup loading until VLC readiness and startup subtitle candidate processing both complete."}}
{"id":"int-bfb2d962","kind":"field_change","created_at":"2026-05-27T04:30:15.810274Z","actor":"dirtydishes","issue_id":"dreamio-69r","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented AVAudioSession warm-up around native VLC pause/resume and validated simulator build."}}

View file

@ -2,6 +2,7 @@
{"_type":"issue","id":"dreamio-btc","title":"Bound VLC range cache probe startup latency","description":"After enabling MKV range cache probing, some Torrentio/Real-Debrid MKV streams log cache-probe but never reach opening mode before the native-player startup timeout. Add a bounded probe/local-cache startup path that falls back to direct playback when the range probe is slow or inconclusive.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:14:02Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:16:53Z","started_at":"2026-05-26T12:14:11Z","closed_at":"2026-05-26T12:16:53Z","close_reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-btc","title":"Bound VLC range cache probe startup latency","description":"After enabling MKV range cache probing, some Torrentio/Real-Debrid MKV streams log cache-probe but never reach opening mode before the native-player startup timeout. Add a bounded probe/local-cache startup path that falls back to direct playback when the range probe is slow or inconclusive.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:14:02Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:16:53Z","started_at":"2026-05-26T12:14:11Z","closed_at":"2026-05-26T12:16:53Z","close_reason":"Added a short timeout to range-cache probe requests so slow MKV HEAD/range probes fall back to direct VLC startup instead of tripping the native-player startup timeout.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-mun","title":"fix vlc cache loopback port startup","description":"Device logs showed local-cache playback opening http://127.0.0.1:0, because the NWListener ephemeral port was read before the listener reached ready. Wait for the real assigned port before returning the local cache URL.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T22:32:41Z","created_by":"dirtydishes","updated_at":"2026-05-25T22:33:15Z","started_at":"2026-05-25T22:33:14Z","closed_at":"2026-05-25T22:33:15Z","close_reason":"Wait for NWListener ready state before returning the local cache URL; verified tests and simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-mun","title":"fix vlc cache loopback port startup","description":"Device logs showed local-cache playback opening http://127.0.0.1:0, because the NWListener ephemeral port was read before the listener reached ready. Wait for the real assigned port before returning the local cache URL.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T22:32:41Z","created_by":"dirtydishes","updated_at":"2026-05-25T22:33:15Z","started_at":"2026-05-25T22:33:14Z","closed_at":"2026-05-25T22:33:15Z","close_reason":"Wait for NWListener ready state before returning the local cache URL; verified tests and simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-69r","title":"Fix audio lag after native video resume","description":"Audio takes a moment to resume after pausing and playing native video; previous attempts did not resolve the lag.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T04:26:52Z","created_by":"dirtydishes","updated_at":"2026-05-27T04:30:16Z","started_at":"2026-05-27T04:26:56Z","closed_at":"2026-05-27T04:30:16Z","close_reason":"Implemented AVAudioSession warm-up around native VLC pause/resume and validated simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-e2q","title":"Gate native player readiness on startup subtitle loading","description":"The native VLC player reports ready as soon as VLC enters buffering, which hides the loading overlay before startup subtitle candidates finish resolving and attaching. Keep startup loading active until the initial subtitle batch has completed or no candidates are queued.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T04:16:35Z","created_by":"dirtydishes","updated_at":"2026-05-27T04:20:09Z","started_at":"2026-05-27T04:16:37Z","closed_at":"2026-05-27T04:20:09Z","close_reason":"Fixed by gating native startup loading until VLC readiness and startup subtitle candidate processing both complete.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-e2q","title":"Gate native player readiness on startup subtitle loading","description":"The native VLC player reports ready as soon as VLC enters buffering, which hides the loading overlay before startup subtitle candidates finish resolving and attaching. Keep startup loading active until the initial subtitle batch has completed or no candidates are queued.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T04:16:35Z","created_by":"dirtydishes","updated_at":"2026-05-27T04:20:09Z","started_at":"2026-05-27T04:16:37Z","closed_at":"2026-05-27T04:20:09Z","close_reason":"Fixed by gating native startup loading until VLC readiness and startup subtitle candidate processing both complete.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-5cz","title":"Make VLC range cache non-blocking at startup","description":"Native playback startup currently bypasses Dreamio's local range cache after cache probing caused VLC startup timeouts. Reintroduce cache startup only when preparation is fast and safe, otherwise fall back to direct playback immediately, with focused tests and clear logs.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T00:36:56Z","created_by":"dirtydishes","updated_at":"2026-05-27T00:43:03Z","started_at":"2026-05-27T00:37:03Z","closed_at":"2026-05-27T00:43:03Z","close_reason":"Implemented bounded non-blocking range-cache startup for VLC, with direct fallback on timeout, skipped probes, or local server failures; added focused startup policy tests and updated the turn document.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-5cz","title":"Make VLC range cache non-blocking at startup","description":"Native playback startup currently bypasses Dreamio's local range cache after cache probing caused VLC startup timeouts. Reintroduce cache startup only when preparation is fast and safe, otherwise fall back to direct playback immediately, with focused tests and clear logs.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T00:36:56Z","created_by":"dirtydishes","updated_at":"2026-05-27T00:43:03Z","started_at":"2026-05-27T00:37:03Z","closed_at":"2026-05-27T00:43:03Z","close_reason":"Implemented bounded non-blocking range-cache startup for VLC, with direct fallback on timeout, skipped probes, or local server failures; added focused startup policy tests and updated the turn document.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-3sw","title":"Fix VLC range cache fallback for tail-index MKV streams","description":"Video range caching currently refuses streams classified as tail-index containers, causing VLC playback to use direct mode and lose seek prefetch behavior. Investigate the probe logic and enable safe local range caching for these streams without breaking playback startup.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:05:20Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:10:16Z","started_at":"2026-05-26T12:05:38Z","closed_at":"2026-05-26T12:10:16Z","close_reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-3sw","title":"Fix VLC range cache fallback for tail-index MKV streams","description":"Video range caching currently refuses streams classified as tail-index containers, causing VLC playback to use direct mode and lose seek prefetch behavior. Investigate the probe logic and enable safe local range caching for these streams without breaking playback startup.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-26T12:05:20Z","created_by":"dirtydishes","updated_at":"2026-05-26T12:10:16Z","started_at":"2026-05-26T12:05:38Z","closed_at":"2026-05-26T12:10:16Z","close_reason":"Removed the Matroska/WebM extension-level range-cache bypass and added a regression test proving MKV URLs use the cache when the origin advertises byte-range support.","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -64,6 +64,17 @@ enum NativePlaybackTogglePolicy {
} }
} }
enum NativePlaybackAudioSessionPolicy {
static func shouldPrepareBeforePlayback(from state: NativePlaybackToggleState) -> Bool {
switch state {
case .paused, .stopped, .ended, .error:
return true
case .opening, .buffering, .playing, .unknown:
return false
}
}
}
enum NativePlaybackError: LocalizedError { enum NativePlaybackError: LocalizedError {
case backendUnavailable case backendUnavailable
case startupTimedOut case startupTimedOut

View file

@ -1,3 +1,4 @@
import AVFoundation
import UIKit import UIKit
#if canImport(MobileVLCKit) #if canImport(MobileVLCKit)
@ -73,6 +74,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
media.addOption(":http-header=\(headerValue)") media.addOption(":http-header=\(headerValue)")
} }
addConservativePlaybackOptions(to: media) addConservativePlaybackOptions(to: media)
prepareAudioSessionForPlayback(reason: "initial-play")
mediaPlayer.currentAudioPlaybackDelay = 0 mediaPlayer.currentAudioPlaybackDelay = 0
mediaPlayer.media = media mediaPlayer.media = media
@ -91,6 +93,10 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
func play() { func play() {
#if canImport(MobileVLCKit) #if canImport(MobileVLCKit)
let toggleState = playbackToggleState(for: mediaPlayer.state)
if NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: toggleState) {
prepareAudioSessionForPlayback(reason: "resume-from-\(toggleState)")
}
#if DEBUG #if DEBUG
logPlaybackSnapshot(reason: "before-play") logPlaybackSnapshot(reason: "before-play")
#endif #endif
@ -114,6 +120,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
return return
} }
mediaPlayer.pause() mediaPlayer.pause()
keepAudioSessionWarmAfterPause()
#if DEBUG #if DEBUG
logPlaybackSnapshot(reason: "after-pause-command") logPlaybackSnapshot(reason: "after-pause-command")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
@ -342,6 +349,28 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
].forEach { media.addOption($0) } ].forEach { media.addOption($0) }
} }
private func prepareAudioSessionForPlayback(reason: String) {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .moviePlayback, options: [])
try session.setActive(true)
#if DEBUG
print("[DreamioVLC] audio-session prepared reason=\(reason) category=\(session.category.rawValue) mode=\(session.mode.rawValue)")
#endif
} catch {
#if DEBUG
print("[DreamioVLC] audio-session prepare failed reason=\(reason) error=\(error.localizedDescription)")
#endif
}
}
private func keepAudioSessionWarmAfterPause() {
prepareAudioSessionForPlayback(reason: "pause-keep-warm")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in
self?.prepareAudioSessionForPlayback(reason: "pause-keep-warm-follow-up")
}
}
private func isPauseableState(_ state: VLCMediaPlayerState) -> Bool { private func isPauseableState(_ state: VLCMediaPlayerState) -> Bool {
switch state { switch state {
case .playing, .buffering: case .playing, .buffering:

View file

@ -25,6 +25,7 @@ struct StreamResolverTests {
testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks()
testSubtitleOptionMappingIncludesNone() testSubtitleOptionMappingIncludesNone()
testNativePlaybackTogglePolicy() testNativePlaybackTogglePolicy()
testNativePlaybackAudioSessionPolicy()
print("StreamResolverTests passed") print("StreamResolverTests passed")
} }
@ -516,6 +517,17 @@ struct StreamResolverTests {
assertEqual(NativePlaybackTogglePolicy.action(for: .unknown), .waitForTransition) assertEqual(NativePlaybackTogglePolicy.action(for: .unknown), .waitForTransition)
} }
private static func testNativePlaybackAudioSessionPolicy() {
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .paused), true)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .stopped), true)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .ended), true)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .error), true)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .playing), false)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .buffering), false)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .opening), false)
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .unknown), false)
}
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) { private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line) assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
} }

File diff suppressed because one or more lines are too long