182 lines
7.4 KiB
Markdown
182 lines
7.4 KiB
Markdown
# Fix 05: cache and prefetch playback sources
|
|
|
|
Priority: P0
|
|
Risk: High
|
|
Primary benefit: Time from watch-page shell to playable media
|
|
|
|
## Issue
|
|
|
|
The first audited episode showed the player shell after 0.58s but did not expose playable metadata until 9.75s. A later episode was faster, which strongly suggests cold provider/CDN work is a major component.
|
|
|
|
`BuildWatchData` resolves a provider source for every watch request. It then starts `warmStreamURL` in a goroutine, but that warm-up races the browser and closes the upstream response without creating a reusable application cache. Manual and autoplay episode transitions can therefore repeat source resolution and manifest work.
|
|
|
|
## Goals
|
|
|
|
- Avoid resolving the same `(anime, episode, mode)` source repeatedly.
|
|
- Collapse concurrent identical resolutions into one provider call.
|
|
- Prepare the likely next episode without blocking current playback.
|
|
- Cache only metadata/manifests at first, not large video bodies.
|
|
- Generate fresh proxy tokens when serving cached source metadata.
|
|
- Respect provider expiry and avoid serving stale signed URLs indefinitely.
|
|
|
|
## Non-goals
|
|
|
|
- Do not build a general video CDN.
|
|
- Do not cache complete episodes in memory or SQLite.
|
|
- Do not expose upstream URLs or referers to the browser.
|
|
- Do not make playback depend exclusively on prefetched data.
|
|
|
|
## Cache the raw provider result, not proxy tokens
|
|
|
|
The cache value should contain the provider result before `buildModeSource` signs it:
|
|
|
|
- Upstream stream URL.
|
|
- Referer.
|
|
- Source type.
|
|
- Subtitle upstream URLs and labels.
|
|
- Resolution timestamp.
|
|
- Optional provider-declared expiry if one can be derived safely.
|
|
|
|
On every response, create fresh proxy tokens from the cached raw value. Caching signed proxy tokens would couple cache lifetime to the in-process token store and make expiry harder to reason about.
|
|
|
|
## Suggested cache shape
|
|
|
|
Key:
|
|
|
|
```text
|
|
anime_id | episode | normalized_mode
|
|
```
|
|
|
|
Value:
|
|
|
|
```text
|
|
provider result | resolved_at | fresh_until | stale_until
|
|
```
|
|
|
|
Initial policy:
|
|
|
|
- Fresh TTL: 5 minutes.
|
|
- Stale-on-provider-error window: an additional 10 minutes for completed anime.
|
|
- Maximum entries: 512.
|
|
- LRU or approximate oldest-entry eviction.
|
|
- No negative caching initially; later add a short 15-30 second negative TTL if repeated misses are observed.
|
|
|
|
Provider URLs may expire sooner than expected. Make TTL configurable and log refresh failures. Never infer a multi-hour TTL unless the provider contract supports it.
|
|
|
|
## Singleflight
|
|
|
|
Wrap provider resolution with a request-collapsing mechanism keyed by the same cache key:
|
|
|
|
1. Check fresh cache.
|
|
2. If missing/stale, join an existing in-flight resolution or become its owner.
|
|
3. Resolve through the provider once.
|
|
4. Store the raw provider result.
|
|
5. Return a cloned value to callers.
|
|
|
|
Use a context policy deliberately. If the owning browser request is cancelled, all joined requests should not necessarily fail. A short service-owned timeout may be more appropriate, but it must still stop promptly during application shutdown.
|
|
|
|
## Client-side episode payload prefetch
|
|
|
|
After the current episode reaches playable metadata:
|
|
|
|
1. Identify the next valid episode.
|
|
2. If `navigator.connection.saveData` is not enabled, fetch its minimal `/api/watch/episode/...` payload at low priority.
|
|
3. Store the parsed payload in a bounded in-memory map in the player.
|
|
4. When the user transitions, consume the prefetched payload if its mode and episode match.
|
|
5. Fall back to a normal fetch when absent or expired.
|
|
|
|
Also prefetch on pointer hover/focus of a specific episode link after a short delay. Cancel hover prefetch when the target changes.
|
|
|
|
Do not prefetch several episodes. One likely next episode plus one explicitly hovered episode is enough.
|
|
|
|
## Manifest caching
|
|
|
|
For HLS sources, cache the raw upstream playlist before rewriting it. Rewrite proxy URLs and issue tokens per response.
|
|
|
|
Suggested initial policy:
|
|
|
|
- Completed/VOD media playlist: 30-60 seconds.
|
|
- Airing/live playlist: either no cache or a very short 2-5 second TTL.
|
|
- Maximum body: retain the existing bounded playlist read limit.
|
|
- Key includes upstream URL and referer identity.
|
|
- Never cache media segment bodies in the first version.
|
|
|
|
This cache belongs near `HandleProxyStream`, not inside the browser. It should preserve upstream status and relevant content headers while removing stale `Content-Length` after rewriting.
|
|
|
|
## Connection warm-up
|
|
|
|
Keep Go HTTP transports long-lived so DNS, TCP, and TLS connections are reused. If `warmStreamURL` remains, make it purposeful:
|
|
|
|
- For a playlist, read the bounded manifest and populate the manifest cache.
|
|
- For direct media, avoid an unbounded GET. If a provider supports ranges, a small range probe may validate availability, but only after measuring that it improves time to metadata.
|
|
|
|
Do not download bytes merely to create the appearance of optimization.
|
|
|
|
## Failure behavior
|
|
|
|
- Fresh cache hit: serve immediately.
|
|
- Stale cache plus successful refresh: replace and serve new value.
|
|
- Stale cache plus provider failure: optionally serve stale for completed media and record the event.
|
|
- No cache plus failure: return the existing error path.
|
|
- Media error after cached source: force one source refresh that bypasses cache, then fail normally.
|
|
|
|
## Security
|
|
|
|
- Never log raw upstream URLs, referers, subtitle URLs, or proxy tokens.
|
|
- Keep cached provider metadata process-local unless encryption and retention are designed explicitly.
|
|
- Preserve `proxytarget.Validate` before signing or proxying every cached URL.
|
|
- Bound memory and reject unexpectedly large manifests.
|
|
|
|
## Affected files
|
|
|
|
- `internal/playback/service.go`
|
|
- `internal/playback/watch_data.go`
|
|
- `internal/playback/handler/proxy_stream.go`
|
|
- `internal/playback/handler/hls_playlist.go`
|
|
- New bounded cache implementation under `internal/playback/`
|
|
- `static/player/episodes/nav.ts`
|
|
- `static/player/main.ts`
|
|
- Playback and proxy tests
|
|
|
|
## Tests
|
|
|
|
- Repeated source lookup hits the provider once within TTL.
|
|
- Concurrent identical lookups hit the provider once.
|
|
- Different episode/mode keys do not collide.
|
|
- Expired entries refresh.
|
|
- Forced refresh bypasses cache after media error.
|
|
- Fresh proxy tokens are generated from cached raw data.
|
|
- Raw HLS playlist is cached; rewritten tokenized output is not shared incorrectly.
|
|
- Cache enforces item and body-size limits.
|
|
- Prefetched episode payload is used once and cannot overwrite a newer transition.
|
|
- Save-Data disables automatic prefetch.
|
|
|
|
## Observability
|
|
|
|
Add counters/histograms for:
|
|
|
|
- Source cache hit, miss, stale-hit, eviction.
|
|
- Singleflight owner versus joined requests.
|
|
- Provider source-resolution duration.
|
|
- Manifest cache hit/miss and upstream duration.
|
|
- Prefetch started, used, expired, cancelled, failed.
|
|
- Time from player initialization to metadata and first frame.
|
|
|
|
## Rollout
|
|
|
|
1. Add measurements without behavior changes.
|
|
2. Add source-result cache and singleflight.
|
|
3. Add next-episode payload prefetch.
|
|
4. Add manifest cache only after source-cache results are understood.
|
|
5. Tune TTLs from observed provider expiry and error data.
|
|
|
|
## Acceptance criteria
|
|
|
|
- Repeated identical source requests do not call the provider within the fresh TTL.
|
|
- Concurrent identical source requests collapse into one provider call.
|
|
- Warm episode transition reaches playable metadata under 1.5 seconds at p75.
|
|
- Cold start shows a meaningful loading state and trends below 4 seconds at p75.
|
|
- No upstream URL/token appears in logs or rendered HTML beyond existing opaque proxy tokens.
|
|
- Memory remains bounded under sustained browsing.
|
|
|