perf(android,ios): warm what an episode opens with, and the next one near the end

The bridge's open_stream started no fetch until mpv asked, so every
open was the head and then the tail, one after the other. It now warms
both edges as the desktop does, and the resume block too when the
episode resumes.

warm_episode fetches another episode's opening blocks into the cache
without playing it. Both phones call it for the next episode in the
last two minutes or once the credits start, as the desktop already
did, so autoplay starts from the cache.
This commit is contained in:
nvrl committed 2026-10-13 16:40:18 +02:00
1 parent 8d795c29e6
commit 2546bab0a6
3 files changed
+108 -2

No files matched your search

+20
View File
@@ -13,6 +13,9 @@ private let watchedFraction = 0.9
/// How long a stall lasts before the player offers to open the episode again.
private let stallLimit: TimeInterval = 20
/// How close to the end the next episode's opening blocks are fetched.
private let warmNextSeconds = 120.0
/// How an episode is named in watch state and player readings.
func mediaKey(_ episode: EpisodeRecord) -> String { "\(episode.shareId):\(episode.linkId)" }
@@ -115,6 +118,8 @@ final class PlayerHost {
/// Chapters already auto-skipped in this episode: seeking back into an
/// opening on purpose must not be undone by the setting that skipped it.
@ObservationIgnored private var skipped = Set<Int64>()
/// The episode whose next one has been warmed; see `tick`.
@ObservationIgnored private var warmedAfter: String?
@ObservationIgnored private var pausedByInterruption = false
@ObservationIgnored private var artworkFor: String?
@@ -390,6 +395,21 @@ final class PlayerHost {
skipped.insert(chapter.index).inserted {
player.seek(to: offer.target)
}
// Near the end, fetch the next episode's opening blocks, so autoplay
// or "Play now" starts from the cache. The last two minutes or the
// credits, whichever comes first: late enough that stopping after one
// episode has not cost a download, early enough to matter.
let nearEnd = player.duration > 0 && (
player.duration - player.position < warmNextSeconds
|| chapters.creditsStart.map { player.position >= $0 } == true
)
if nearEnd, let key, warmedAfter != key, let next {
warmedAfter = key
let (shareId, volumeId, linkId) = (next.shareId, next.volumeId, next.linkId)
Task.detached {
try? await NativeRuntime.engine().warmEpisode(shareId: shareId, volumeId: volumeId, linkId: linkId)
}
}
}
/// Re-read the chapters when there is a reason to: a different file, or a