mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
recover stalled native skips
This commit is contained in:
parent
4ca0151f1a
commit
3fde2d7b34
2 changed files with 153 additions and 14 deletions
|
|
@ -6,6 +6,9 @@ import MobileVLCKit
|
|||
|
||||
final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||
private static let seekBufferMilliseconds = 30_000
|
||||
private static let stalledJumpRecoveryDelay: TimeInterval = 3.0
|
||||
private static let stalledJumpProgressTolerance: TimeInterval = 0.75
|
||||
private static let stalledJumpTargetTolerance: TimeInterval = 2.0
|
||||
|
||||
static var isAvailable: Bool {
|
||||
#if canImport(MobileVLCKit)
|
||||
|
|
@ -24,8 +27,11 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
|
||||
#if canImport(MobileVLCKit)
|
||||
private let mediaPlayer = VLCMediaPlayer()
|
||||
private var currentRequest: NativePlaybackRequest?
|
||||
private var recoveryGeneration = 0
|
||||
#endif
|
||||
private var attachedSubtitleURLs = Set<URL>()
|
||||
private var attachedSubtitleCandidates: [SubtitleCandidate] = []
|
||||
private var didAutoSelectSubtitleTrack = false
|
||||
private var didUserSelectSubtitleTrack = false
|
||||
private var autoSelectedSubtitleTrackID: Int32?
|
||||
|
|
@ -50,7 +56,10 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
|
||||
func play(request: NativePlaybackRequest) {
|
||||
#if canImport(MobileVLCKit)
|
||||
currentRequest = request
|
||||
recoveryGeneration += 1
|
||||
attachedSubtitleURLs.removeAll()
|
||||
attachedSubtitleCandidates.removeAll()
|
||||
didAutoSelectSubtitleTrack = false
|
||||
didUserSelectSubtitleTrack = false
|
||||
autoSelectedSubtitleTrackID = nil
|
||||
|
|
@ -58,20 +67,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
hasPendingExternalSubtitleSelection = false
|
||||
pendingExternalSubtitleDisplayNames.removeAll()
|
||||
externalSubtitleDisplayNamesByTrackID.removeAll()
|
||||
let media = VLCMedia(url: request.playbackURL)
|
||||
let headerValue = request.headers
|
||||
.map { "\($0.key): \($0.value)" }
|
||||
.joined(separator: "\r\n")
|
||||
media.addOption(":http-referrer=\(request.referer)")
|
||||
if let userAgent = request.userAgent {
|
||||
media.addOption(":http-user-agent=\(userAgent)")
|
||||
}
|
||||
if !headerValue.isEmpty {
|
||||
media.addOption(":http-header=\(headerValue)")
|
||||
}
|
||||
configureSeekBuffer(for: media)
|
||||
|
||||
mediaPlayer.media = media
|
||||
mediaPlayer.media = configuredMedia(for: request)
|
||||
#if DEBUG
|
||||
print("[DreamioVLC] opening url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString)) seekBufferMilliseconds=\(Self.seekBufferMilliseconds)")
|
||||
#endif
|
||||
|
|
@ -99,6 +95,25 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
|
||||
cachingOptions.forEach { media.addOption($0) }
|
||||
}
|
||||
|
||||
private func configuredMedia(for request: NativePlaybackRequest, startTime: TimeInterval? = nil) -> VLCMedia {
|
||||
let media = VLCMedia(url: request.playbackURL)
|
||||
let headerValue = request.headers
|
||||
.map { "\($0.key): \($0.value)" }
|
||||
.joined(separator: "\r\n")
|
||||
media.addOption(":http-referrer=\(request.referer)")
|
||||
if let userAgent = request.userAgent {
|
||||
media.addOption(":http-user-agent=\(userAgent)")
|
||||
}
|
||||
if !headerValue.isEmpty {
|
||||
media.addOption(":http-header=\(headerValue)")
|
||||
}
|
||||
if let startTime {
|
||||
media.addOption(":start-time=\(Int(startTime.rounded()))")
|
||||
}
|
||||
configureSeekBuffer(for: media)
|
||||
return media
|
||||
}
|
||||
#endif
|
||||
|
||||
func pause() {
|
||||
|
|
@ -138,6 +153,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
}
|
||||
mediaPlayer.position = Float(nextTime / duration)
|
||||
mediaPlayer.play()
|
||||
scheduleStalledJumpRecovery(from: currentTime, targetTime: nextTime)
|
||||
#if DEBUG
|
||||
schedulePostSeekDiagnostics(label: "jump", expectedTime: nextTime)
|
||||
#endif
|
||||
|
|
@ -199,6 +215,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
mediaPlayer.stop()
|
||||
mediaPlayer.drawable = nil
|
||||
mediaPlayer.media = nil
|
||||
currentRequest = nil
|
||||
recoveryGeneration += 1
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -310,6 +328,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
return
|
||||
}
|
||||
attachedSubtitleURLs.insert(candidate.url)
|
||||
attachedSubtitleCandidates.append(candidate)
|
||||
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
|
||||
hasPendingExternalSubtitleSelection = true
|
||||
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
|
||||
|
|
@ -370,6 +389,78 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private func scheduleStalledJumpRecovery(from originalTime: TimeInterval, targetTime: TimeInterval) {
|
||||
recoveryGeneration += 1
|
||||
let generation = recoveryGeneration
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Self.stalledJumpRecoveryDelay) { [weak self] in
|
||||
self?.recoverStalledJumpIfNeeded(
|
||||
generation: generation,
|
||||
originalTime: originalTime,
|
||||
targetTime: targetTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func recoverStalledJumpIfNeeded(
|
||||
generation: Int,
|
||||
originalTime: TimeInterval,
|
||||
targetTime: TimeInterval
|
||||
) {
|
||||
guard generation == recoveryGeneration,
|
||||
let request = currentRequest,
|
||||
mediaPlayer.state == .buffering else {
|
||||
return
|
||||
}
|
||||
|
||||
let hasMadeProgress = abs(currentTime - originalTime) > Self.stalledJumpProgressTolerance
|
||||
let hasReachedTarget = abs(currentTime - targetTime) <= Self.stalledJumpTargetTolerance
|
||||
guard !hasMadeProgress && !hasReachedTarget else {
|
||||
return
|
||||
}
|
||||
|
||||
let subtitleCandidates = attachedSubtitleCandidates
|
||||
let selectedAudioTrackID = mediaPlayer.currentAudioTrackIndex
|
||||
let selectedSubtitleTrackID = mediaPlayer.currentVideoSubTitleIndex
|
||||
let wasUserSubtitleSelection = didUserSelectSubtitleTrack
|
||||
#if DEBUG
|
||||
print("[DreamioVLC] stalled jump recovery target=\(targetTime) original=\(originalTime) current=\(currentTime) position=\(mediaPlayer.position)")
|
||||
#endif
|
||||
recoveryGeneration += 1
|
||||
attachedSubtitleURLs.removeAll()
|
||||
attachedSubtitleCandidates.removeAll()
|
||||
externalSubtitleBaselineTrackIDs.removeAll()
|
||||
hasPendingExternalSubtitleSelection = false
|
||||
pendingExternalSubtitleDisplayNames.removeAll()
|
||||
externalSubtitleDisplayNamesByTrackID.removeAll()
|
||||
didUserSelectSubtitleTrack = wasUserSubtitleSelection
|
||||
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.media = configuredMedia(for: request, startTime: targetTime)
|
||||
mediaPlayer.play()
|
||||
_ = attachSubtitles(subtitleCandidates)
|
||||
scheduleTrackRecovery(audioTrackID: selectedAudioTrackID, subtitleTrackID: selectedSubtitleTrackID)
|
||||
}
|
||||
|
||||
private func scheduleTrackRecovery(audioTrackID: Int32, subtitleTrackID: Int32) {
|
||||
[0.4, 1.2, 2.5].forEach { delay in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if audioTrackID >= 0,
|
||||
self.audioTracks.contains(where: { $0.id == audioTrackID }) {
|
||||
self.mediaPlayer.currentAudioTrackIndex = audioTrackID
|
||||
}
|
||||
if subtitleTrackID >= 0,
|
||||
self.subtitleTracks.contains(where: { $0.id == subtitleTrackID }) {
|
||||
self.mediaPlayer.currentVideoSubTitleIndex = subtitleTrackID
|
||||
}
|
||||
self.onAudioTracksChange?()
|
||||
self.onSubtitleTracksChange?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private func logPlaybackSnapshot(reason: String) {
|
||||
print("[DreamioVLC] snapshot reason=\(reason) state=\(stateName(mediaPlayer.state)) isPlaying=\(mediaPlayer.isPlaying) isSeekable=\(mediaPlayer.isSeekable) time=\(currentTime) duration=\(duration) position=\(mediaPlayer.position) selectedAudio=\(mediaPlayer.currentAudioTrackIndex) selectedSubtitle=\(mediaPlayer.currentVideoSubTitleIndex)")
|
||||
|
|
|
|||
|
|
@ -385,6 +385,54 @@ index c3c2318..0fa779a 100644
|
|||
<h3>Related issues or PRs</h3>
|
||||
<p>Related Beads issue: <code>dreamio-3yb</code>. This is a diagnostic update to the same native seek-buffer investigation. The diff is shown as a plain fallback because <code>@pierre/diffs/ssr</code> is not installed in this workspace.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>New Changes as of 2026-05-25 16:13 EDT</h2>
|
||||
<h3>Summary of changes</h3>
|
||||
<p>Device logs showed VLC accepted the jump request but kept reporting the old timestamp and position while buffering. The backend now detects that stalled jump pattern and performs a native-only recovery by reopening the same media with VLC's <code>:start-time</code> option set to the requested target timestamp.</p>
|
||||
|
||||
<h3>Why this change was made</h3>
|
||||
<p>The seek buffer and alternate seek APIs did not make libVLC apply the seek on this direct stream. Reopening the same media at the target time gives VLC a fresh input pipeline while keeping the behavior inside <code>VLCNativePlaybackBackend</code> and without adding player UI or protocol settings.</p>
|
||||
|
||||
<h3>Code diffs</h3>
|
||||
<pre><code>diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift
|
||||
@@
|
||||
+ private static let stalledJumpRecoveryDelay: TimeInterval = 3.0
|
||||
+ private static let stalledJumpProgressTolerance: TimeInterval = 0.75
|
||||
+ private static let stalledJumpTargetTolerance: TimeInterval = 2.0
|
||||
@@
|
||||
+ private var currentRequest: NativePlaybackRequest?
|
||||
+ private var recoveryGeneration = 0
|
||||
@@
|
||||
+ if let startTime {
|
||||
+ media.addOption(":start-time=\(Int(startTime.rounded()))")
|
||||
+ }
|
||||
@@
|
||||
mediaPlayer.position = Float(nextTime / duration)
|
||||
mediaPlayer.play()
|
||||
+ scheduleStalledJumpRecovery(from: currentTime, targetTime: nextTime)
|
||||
@@
|
||||
+ guard generation == recoveryGeneration,
|
||||
+ let request = currentRequest,
|
||||
+ mediaPlayer.state == .buffering else {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ let hasMadeProgress = abs(currentTime - originalTime) > Self.stalledJumpProgressTolerance
|
||||
+ let hasReachedTarget = abs(currentTime - targetTime) <= Self.stalledJumpTargetTolerance
|
||||
+ guard !hasMadeProgress && !hasReachedTarget else {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ mediaPlayer.stop()
|
||||
+ mediaPlayer.media = configuredMedia(for: request, startTime: targetTime)
|
||||
+ mediaPlayer.play()
|
||||
+ _ = attachSubtitles(subtitleCandidates)
|
||||
+ scheduleTrackRecovery(audioTrackID: selectedAudioTrackID, subtitleTrackID: selectedSubtitleTrackID)</code></pre>
|
||||
|
||||
<h3>Related issues or PRs</h3>
|
||||
<p>Related Beads issue: <code>dreamio-3yb</code>. This remains part of the same native playback seek-buffer branch. The branch now includes a fallback for streams where VLC's in-place seek does not take effect.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue