mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
Fix VLC resume audio sync
This commit is contained in:
parent
ed7a242a47
commit
e7ddd6d755
6 changed files with 331 additions and 118 deletions
|
|
@ -44,3 +44,4 @@
|
||||||
{"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."}}
|
{"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."}}
|
||||||
|
{"id":"int-b32c40f0","kind":"field_change","created_at":"2026-05-27T06:14:56.320425Z","actor":"dirtydishes","issue_id":"dreamio-0pi","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented MobileVLCKit resume sync changes: removed repeated resume seek-holding, simplified streaming cache options, retained DEBUG resume observation, updated tests, and documented the turn. Real-device validation is tracked in dreamio-yny."}}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
{"_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-yny","title":"Validate VLC resume audio sync on device","description":"Run manual real-device validation for MobileVLCKit direct-stream pause and resume after removing repeated seek-holding. Capture DEBUG resume-observation logs if audio still lags video.","acceptance_criteria":"Problematic direct-file streams are tested on a real iPhone or iPad; resume audio/video timing is recorded; DEBUG resume-observation logs are attached or summarized if lag persists.","status":"open","priority":1,"issue_type":"task","owner":"dishes@dpdrm.com","created_at":"2026-05-27T06:14:11Z","created_by":"dirtydishes","updated_at":"2026-05-27T06:14:11Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"dreamio-0pi","title":"Fix MobileVLCKit streaming resume audio sync","description":"Research MobileVLCKit streaming pause/resume behavior and adjust native VLC playback so audio does not lag behind video on every resume.","acceptance_criteria":"MobileVLCKit streaming resume behavior is researched; repeated resume seek-holding is removed or replaced; relevant tests pass; changes are documented.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-27T05:47:32Z","created_by":"dirtydishes","updated_at":"2026-05-27T06:14:56Z","started_at":"2026-05-27T05:47:35Z","closed_at":"2026-05-27T06:14:56Z","close_reason":"Implemented MobileVLCKit resume sync changes: removed repeated resume seek-holding, simplified streaming cache options, retained DEBUG resume observation, updated tests, and documented the turn. Real-device validation is tracked in dreamio-yny.","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-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}
|
||||||
|
|
|
||||||
|
|
@ -75,23 +75,11 @@ enum NativePlaybackAudioSessionPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NativePlaybackResumePolicy {
|
enum NativePlaybackStreamingOptionsPolicy {
|
||||||
static let freezeInterval: TimeInterval = 0.08
|
static let networkCachingMilliseconds = 1000
|
||||||
static let maximumFreezeDuration: TimeInterval = 1.2
|
|
||||||
static let maximumAllowedSilentAdvance: Int32 = 120
|
|
||||||
|
|
||||||
static func shouldHoldVideoAtPausedTime(
|
static func mediaOptions() -> [String] {
|
||||||
elapsedSinceResume: TimeInterval,
|
[":network-caching=\(networkCachingMilliseconds)"]
|
||||||
hasObservedAudioOutput: Bool,
|
|
||||||
mediaAdvanceMilliseconds: Int32
|
|
||||||
) -> Bool {
|
|
||||||
guard !hasObservedAudioOutput else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard elapsedSinceResume < maximumFreezeDuration else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mediaAdvanceMilliseconds > maximumAllowedSilentAdvance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
|
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
|
||||||
private var didReportReadyForCurrentMedia = false
|
private var didReportReadyForCurrentMedia = false
|
||||||
private var pausedTimeMilliseconds: Int32?
|
private var pausedTimeMilliseconds: Int32?
|
||||||
private var resumeCorrectionGeneration = 0
|
private var resumeObservationGeneration = 0
|
||||||
private var resumeCorrectionStartDate: Date?
|
private var resumeObservationStartDate: Date?
|
||||||
private var hasObservedResumeAudioOutput = false
|
private var hasObservedResumeAudioOutput = false
|
||||||
private var lastToggleDate = Date.distantPast
|
private var lastToggleDate = Date.distantPast
|
||||||
private let minimumToggleInterval: TimeInterval = 0.35
|
private let minimumToggleInterval: TimeInterval = 0.35
|
||||||
|
|
@ -65,7 +65,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
pendingExternalSubtitleDisplayNames.removeAll()
|
pendingExternalSubtitleDisplayNames.removeAll()
|
||||||
externalSubtitleDisplayNamesByTrackID.removeAll()
|
externalSubtitleDisplayNamesByTrackID.removeAll()
|
||||||
didReportReadyForCurrentMedia = false
|
didReportReadyForCurrentMedia = false
|
||||||
resetResumeCorrectionState()
|
resetResumeObservationState()
|
||||||
lastToggleDate = .distantPast
|
lastToggleDate = .distantPast
|
||||||
let media = VLCMedia(url: request.playbackURL)
|
let media = VLCMedia(url: request.playbackURL)
|
||||||
let headerValue = request.headers
|
let headerValue = request.headers
|
||||||
|
|
@ -108,9 +108,9 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
#endif
|
#endif
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
if isResumingFromPause {
|
if isResumingFromPause {
|
||||||
beginResumeCorrection()
|
beginResumeObservation()
|
||||||
} else {
|
} else {
|
||||||
resetResumeCorrectionState()
|
resetResumeObservationState()
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
logPlaybackSnapshot(reason: "after-play-command")
|
logPlaybackSnapshot(reason: "after-play-command")
|
||||||
|
|
@ -131,9 +131,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pausedTimeMilliseconds = mediaPlayer.time.intValue
|
pausedTimeMilliseconds = mediaPlayer.time.intValue
|
||||||
resetResumeCorrectionRuntimeState()
|
resetResumeObservationRuntimeState()
|
||||||
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
|
||||||
|
|
@ -249,7 +248,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
logPlaybackSnapshot(reason: "before-stop")
|
logPlaybackSnapshot(reason: "before-stop")
|
||||||
#endif
|
#endif
|
||||||
didReportReadyForCurrentMedia = false
|
didReportReadyForCurrentMedia = false
|
||||||
resetResumeCorrectionState()
|
resetResumeObservationState()
|
||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
mediaPlayer.drawable = nil
|
mediaPlayer.drawable = nil
|
||||||
mediaPlayer.media = nil
|
mediaPlayer.media = nil
|
||||||
|
|
@ -355,11 +354,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
|
|
||||||
#if canImport(MobileVLCKit)
|
#if canImport(MobileVLCKit)
|
||||||
private func addConservativePlaybackOptions(to media: VLCMedia) {
|
private func addConservativePlaybackOptions(to media: VLCMedia) {
|
||||||
[
|
NativePlaybackStreamingOptionsPolicy.mediaOptions().forEach { media.addOption($0) }
|
||||||
":network-caching=1000",
|
|
||||||
":file-caching=1000",
|
|
||||||
":live-caching=1000"
|
|
||||||
].forEach { media.addOption($0) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func prepareAudioSessionForPlayback(reason: String) {
|
private func prepareAudioSessionForPlayback(reason: String) {
|
||||||
|
|
@ -377,83 +372,57 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func keepAudioSessionWarmAfterPause() {
|
private func beginResumeObservation() {
|
||||||
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 beginResumeCorrection() {
|
|
||||||
guard let pausedTimeMilliseconds else {
|
guard let pausedTimeMilliseconds else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetResumeCorrectionRuntimeState()
|
resetResumeObservationRuntimeState()
|
||||||
resumeCorrectionGeneration += 1
|
resumeObservationGeneration += 1
|
||||||
let generation = resumeCorrectionGeneration
|
let generation = resumeObservationGeneration
|
||||||
resumeCorrectionStartDate = Date()
|
resumeObservationStartDate = Date()
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
print("[DreamioVLC] resume-correction begin pausedTimeMS=\(pausedTimeMilliseconds)")
|
print("[DreamioVLC] resume-observation begin pausedTimeMS=\(pausedTimeMilliseconds)")
|
||||||
|
[0.25, 0.75, 1.5].forEach { delay in
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
|
self?.logResumeObservation(generation: generation, delay: delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
scheduleResumeCorrectionTick(generation: generation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scheduleResumeCorrectionTick(generation: Int) {
|
#if DEBUG
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + NativePlaybackResumePolicy.freezeInterval) { [weak self] in
|
private func logResumeObservation(generation: Int, delay: TimeInterval) {
|
||||||
self?.performResumeCorrectionTick(generation: generation)
|
guard generation == resumeObservationGeneration,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performResumeCorrectionTick(generation: Int) {
|
|
||||||
guard generation == resumeCorrectionGeneration,
|
|
||||||
let pausedTimeMilliseconds,
|
let pausedTimeMilliseconds,
|
||||||
let resumeCorrectionStartDate else {
|
let resumeObservationStartDate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let elapsed = Date().timeIntervalSince(resumeObservationStartDate)
|
||||||
let elapsed = Date().timeIntervalSince(resumeCorrectionStartDate)
|
let advance = max(0, mediaPlayer.time.intValue - pausedTimeMilliseconds)
|
||||||
let currentTimeMilliseconds = mediaPlayer.time.intValue
|
print("[DreamioVLC] resume-observation tick delay=\(String(format: "%.2f", delay)) elapsed=\(String(format: "%.3f", elapsed)) audioObserved=\(hasObservedResumeAudioOutput) advanceMS=\(advance)")
|
||||||
let advance = max(0, currentTimeMilliseconds - pausedTimeMilliseconds)
|
logPlaybackSnapshot(reason: "resume-observation-\(String(format: "%.2f", delay))")
|
||||||
let shouldHold = NativePlaybackResumePolicy.shouldHoldVideoAtPausedTime(
|
|
||||||
elapsedSinceResume: elapsed,
|
|
||||||
hasObservedAudioOutput: hasObservedResumeAudioOutput,
|
|
||||||
mediaAdvanceMilliseconds: advance
|
|
||||||
)
|
|
||||||
|
|
||||||
guard shouldHold else {
|
|
||||||
#if DEBUG
|
|
||||||
print("[DreamioVLC] resume-correction release elapsed=\(String(format: "%.3f", elapsed)) audioObserved=\(hasObservedResumeAudioOutput) advanceMS=\(advance)")
|
|
||||||
#endif
|
|
||||||
resetResumeCorrectionRuntimeState()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaPlayer.time = VLCTime(int: pausedTimeMilliseconds)
|
|
||||||
#if DEBUG
|
|
||||||
print("[DreamioVLC] resume-correction hold elapsed=\(String(format: "%.3f", elapsed)) advanceMS=\(advance) resetToMS=\(pausedTimeMilliseconds)")
|
|
||||||
#endif
|
#endif
|
||||||
scheduleResumeCorrectionTick(generation: generation)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func noteResumeAudioOutputIfNeeded(reason: String) {
|
private func noteResumeAudioOutputIfNeeded(reason: String) {
|
||||||
guard resumeCorrectionStartDate != nil else {
|
guard resumeObservationStartDate != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasObservedResumeAudioOutput = true
|
hasObservedResumeAudioOutput = true
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let loudness = mediaPlayer.momentaryLoudness
|
let loudness = mediaPlayer.momentaryLoudness
|
||||||
print("[DreamioVLC] resume-correction audio-observed reason=\(reason) loudness=\(loudness?.loudnessValue ?? 0) date=\(loudness?.date ?? 0)")
|
print("[DreamioVLC] resume-observation audio-observed reason=\(reason) loudness=\(loudness?.loudnessValue ?? 0) date=\(loudness?.date ?? 0)")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetResumeCorrectionState() {
|
private func resetResumeObservationState() {
|
||||||
pausedTimeMilliseconds = nil
|
pausedTimeMilliseconds = nil
|
||||||
resetResumeCorrectionRuntimeState()
|
resetResumeObservationRuntimeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resetResumeCorrectionRuntimeState() {
|
private func resetResumeObservationRuntimeState() {
|
||||||
resumeCorrectionGeneration += 1
|
resumeObservationGeneration += 1
|
||||||
resumeCorrectionStartDate = nil
|
resumeObservationStartDate = nil
|
||||||
hasObservedResumeAudioOutput = false
|
hasObservedResumeAudioOutput = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,7 +459,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private func logPlaybackSnapshot(reason: String) {
|
private func logPlaybackSnapshot(reason: String) {
|
||||||
let mediaLength = mediaPlayer.media?.length.intValue ?? 0
|
let mediaLength = mediaPlayer.media?.length.intValue ?? 0
|
||||||
print("[DreamioVLC] snapshot reason=\(reason) state=\(stateName(mediaPlayer.state)) isPlaying=\(mediaPlayer.isPlaying) canPause=\(mediaPlayer.canPause) seekable=\(mediaPlayer.isSeekable) currentTime=\(String(format: "%.3f", currentTime)) duration=\(String(format: "%.3f", TimeInterval(max(0, mediaLength)) / 1000)) position=\(String(format: "%.4f", mediaPlayer.position)) audioDelay=\(mediaPlayer.currentAudioPlaybackDelay) pausedTimeMS=\(pausedTimeMilliseconds.map(String.init) ?? "nil") resumeActive=\(resumeCorrectionStartDate != nil) audioObserved=\(hasObservedResumeAudioOutput) readyReported=\(didReportReadyForCurrentMedia)")
|
print("[DreamioVLC] snapshot reason=\(reason) state=\(stateName(mediaPlayer.state)) isPlaying=\(mediaPlayer.isPlaying) canPause=\(mediaPlayer.canPause) seekable=\(mediaPlayer.isSeekable) currentTime=\(String(format: "%.3f", currentTime)) duration=\(String(format: "%.3f", TimeInterval(max(0, mediaLength)) / 1000)) position=\(String(format: "%.4f", mediaPlayer.position)) audioDelay=\(mediaPlayer.currentAudioPlaybackDelay) pausedTimeMS=\(pausedTimeMilliseconds.map(String.init) ?? "nil") resumeActive=\(resumeObservationStartDate != nil) audioObserved=\(hasObservedResumeAudioOutput) readyReported=\(didReportReadyForCurrentMedia)")
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -655,7 +624,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
extension VLCNativePlaybackBackend: VLCMediaPlayerDelegate {
|
extension VLCNativePlaybackBackend: VLCMediaPlayerDelegate {
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if resumeCorrectionStartDate != nil {
|
if resumeObservationStartDate != nil {
|
||||||
logPlaybackSnapshot(reason: "time-change-during-resume")
|
logPlaybackSnapshot(reason: "time-change-during-resume")
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ struct StreamResolverTests {
|
||||||
testSubtitleOptionMappingIncludesNone()
|
testSubtitleOptionMappingIncludesNone()
|
||||||
testNativePlaybackTogglePolicy()
|
testNativePlaybackTogglePolicy()
|
||||||
testNativePlaybackAudioSessionPolicy()
|
testNativePlaybackAudioSessionPolicy()
|
||||||
testNativePlaybackResumePolicy()
|
testNativePlaybackStreamingOptionsPolicy()
|
||||||
print("StreamResolverTests passed")
|
print("StreamResolverTests passed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,39 +529,11 @@ struct StreamResolverTests {
|
||||||
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .unknown), false)
|
assertEqual(NativePlaybackAudioSessionPolicy.shouldPrepareBeforePlayback(from: .unknown), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func testNativePlaybackResumePolicy() {
|
private static func testNativePlaybackStreamingOptionsPolicy() {
|
||||||
assertEqual(
|
assertEqual(NativePlaybackStreamingOptionsPolicy.networkCachingMilliseconds, 1000)
|
||||||
NativePlaybackResumePolicy.shouldHoldVideoAtPausedTime(
|
assertEqual(NativePlaybackStreamingOptionsPolicy.mediaOptions(), [":network-caching=1000"])
|
||||||
elapsedSinceResume: 0.4,
|
assertEqual(NativePlaybackStreamingOptionsPolicy.mediaOptions().contains(":file-caching=1000"), false)
|
||||||
hasObservedAudioOutput: false,
|
assertEqual(NativePlaybackStreamingOptionsPolicy.mediaOptions().contains(":live-caching=1000"), false)
|
||||||
mediaAdvanceMilliseconds: 500
|
|
||||||
),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
assertEqual(
|
|
||||||
NativePlaybackResumePolicy.shouldHoldVideoAtPausedTime(
|
|
||||||
elapsedSinceResume: 0.4,
|
|
||||||
hasObservedAudioOutput: true,
|
|
||||||
mediaAdvanceMilliseconds: 500
|
|
||||||
),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
assertEqual(
|
|
||||||
NativePlaybackResumePolicy.shouldHoldVideoAtPausedTime(
|
|
||||||
elapsedSinceResume: 1.3,
|
|
||||||
hasObservedAudioOutput: false,
|
|
||||||
mediaAdvanceMilliseconds: 500
|
|
||||||
),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
assertEqual(
|
|
||||||
NativePlaybackResumePolicy.shouldHoldVideoAtPausedTime(
|
|
||||||
elapsedSinceResume: 0.4,
|
|
||||||
hasObservedAudioOutput: false,
|
|
||||||
mediaAdvanceMilliseconds: 80
|
|
||||||
),
|
|
||||||
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) {
|
||||||
|
|
|
||||||
281
docs/turns/2026-05-27-fix-vlc-resume-audio-sync.html
Normal file
281
docs/turns/2026-05-27-fix-vlc-resume-audio-sync.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue