feat: play resolved Miruro HLS with external VTT and stable episode progress
This commit is contained in:
@@ -119,7 +119,7 @@ Omitted settings/actions retain their defaults. A shortcut string or string arra
|
||||
|
||||
Bindings name explicit actions, not toggles. The same chord may be shared only by inverse pairs shown above: play/pause, mute/unmute, enter/exit fullscreen or compact, enable/disable always-on-top, show/hide buffer info, and enable/disable snapping or automatic quality. A press performs **one currently applicable action**, including pending playback/window intent; already-satisfied actions do nothing. Give each side different chords for dedicated enable/disable keys. Sharing a chord between unrelated actions, duplicate normalized chords, unknown actions, and malformed shortcuts are errors.
|
||||
|
||||
For YouTube, bind `quality_<height>p` (for example, `quality_2160p = "ctrl+4"`) to select an available resolution, or `quality_default` to restore the capped default. Resolution actions are unavailable for local media and absent resolutions; selecting manual/default quality disables automatic quality. Captions are not implemented, so caption and `toggle_*` action names are rejected.
|
||||
For YouTube, bind `quality_<height>p` (for example, `quality_2160p = "ctrl+4"`) to select an available resolution, or `quality_default` to restore the capped default. Resolution actions are unavailable for local media and absent resolutions; selecting manual/default quality disables automatic quality. YouTube captions and caption key actions are not implemented; resolved-source VTT selection uses the captions menu. Caption and `toggle_*` action names are rejected.
|
||||
|
||||
Behavior/UI switches must be TOML booleans; `[ui].compact_title_progress` is a string: `"time"` (default), `"percentage"`, or `"none"`. Unknown keys, unsupported values, malformed TOML, wrong types, unreadable files, and a directory in place of the file are errors, not silent defaults. Top-level `automatic_quality` is no longer accepted: move it under `[playback]`. The old flat `show_buffer_info` and `snap_to_edges` settings are also rejected: move them to `[ui]` as `buffer_info` and `compact_snap_to_edges`. GUI startup errors show a native alert. Settings are read once at startup; control/menu changes are **session-only** and never write the file. Edit it yourself and start a fresh player to apply changes.
|
||||
|
||||
@@ -147,9 +147,65 @@ While playing YouTube, choose **gear → Quality** in either normal or compact m
|
||||
|
||||
The app ignores yt-dlp configuration and plugins, does not read browser cookies, and does not update or fetch extractor components automatically. Private, age-restricted, region-blocked, or sign-in-required videos can fail. If YouTube extraction changes, update the pinned helper versions/checksums and rebuild/reinstall terebi; upgrading an external Homebrew yt-dlp does not change the bundled extractor.
|
||||
|
||||
## Resolved HLS sources (Miruro / Tubular)
|
||||
|
||||
Tubular owns discovery, episode/provider selection and fresh source resolution. Terebi accepts a resolved source document; it does not scrape Miruro, embed `strm.cx`, refresh expired sources or bypass protection.
|
||||
|
||||
```sh
|
||||
terebi --new-instance --source-file '/private/session/source.json' \
|
||||
--progress-file '/private/session/progress.jsonl'
|
||||
```
|
||||
|
||||
`--source-file` requires `--new-instance` and accepts a filesystem path (relative paths resolve against the shell's directory). The CLI validates the document, then forwards its absolute path to the GUI; only paths, not stream URLs or header values, enter launch arguments. Create the session directory with mode `0700` and the source file with mode `0600`. Keep the file unchanged and present for the entire player session; launcher success does not acknowledge that the GUI has read it. The caller owns cleanup.
|
||||
|
||||
Source document syntax (replace example URLs with freshly resolved resources):
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"url": "https://cdn.example/master.m3u8?token=fresh",
|
||||
"headers": {"Origin": "https://ultracloud.cc", "Referer": "https://ultracloud.cc/"},
|
||||
"identity": {"provider": "miruro", "media_id": "OERe_a0YS4h-DlsBDOeXwX7HOO6316tw", "episode": 1},
|
||||
"title": "The Apothecary Diaries — episode 1",
|
||||
"subtitles": [{
|
||||
"url": "https://cdn.example/english.jpeg?token=fresh",
|
||||
"format": "vtt",
|
||||
"language": "en",
|
||||
"label": "English",
|
||||
"default": true,
|
||||
"headers": {"Origin": "https://ultracloud.cc", "Referer": "https://ultracloud.cc/"}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
All fields except `title` are required; `subtitles: []` and `headers: {}` are valid. Subtitle headers never inherit stream headers: Tubular must explicitly supply the appropriate map for each resource. Values are preserved without trimming; invalid HTTP names, duplicate case-insensitive names and control characters are rejected. Documents are limited to 1 MiB, 32 subtitle tracks and 64 headers per resource. Miruro IDs are nonempty ASCII letters/digits/`-`/`_` (up to 512 bytes); episode numbers are integers from 1 through 2147483647.
|
||||
|
||||
Native FFmpeg plays the master and nested playlists, media segments, AES-128 keys and fMP4 init resources with the stream's headers. Relative URLs use native HLS resolution; supplied URLs and query tokens are retained, not synthesized onto unrelated references. Resolved media resources must remain on the initial stream's origin (scheme, host and effective port). All media redirects, including same-origin redirects, are rejected before a second request; this deliberately prevents FFmpeg from forwarding private headers to an unrelated destination. YouTube keeps its existing transport policy.
|
||||
|
||||
Sun can serve MPEG-TS segments as `.jpg`/`image/jpeg`. Resolved HLS uses content probing instead of FFmpeg's filename-extension gate; URLs are not rewritten. The HTTP(S)-only, same-origin and no-redirect restrictions still apply.
|
||||
|
||||
External subtitles use explicit `format: "vtt"` plus WebVTT content, never the filename extension or MIME type. The captions menu shows labels/languages, selects the declared default and permits Off/another track. Plain-text cues follow acknowledged playback position, including backward/forward seeks; WebVTT positioning, CSS and typographic styling are not applied. Subtitle loading is bounded to 4 MiB with 15-second request/20-second resource timeouts. Same-origin subtitle redirects retain that resource's headers; cross-origin redirects are rejected. Load errors appear beside the captions menu, independently of media errors.
|
||||
|
||||
FFmpeg diagnostics omit private detail strings; playback failures still expose the operation and native error. Source/subtitle validation and transport errors do not interpolate supplied URLs or header values.
|
||||
|
||||
Miruro's progress identity is independent of its source address. YouTube records stay version 1; Miruro records use version 2:
|
||||
|
||||
```json
|
||||
{"version":2,"event":"progress","media_identity":{"provider":"miruro","media_id":"OERe_a0YS4h-DlsBDOeXwX7HOO6316tw","episode":1},"position_seconds":123.5,"duration_seconds":1500.0,"completion_eligible":true}
|
||||
```
|
||||
|
||||
Events, acknowledged-position reporting, nullable duration and completion eligibility retain the semantics below. Miruro bookmarks use the same stable episode identity. **Tubular coordination required:** its current `src/video_state.rs` accepts only canonical YouTube/version-1 records; it must adopt this version-2 identity before Miruro watched tracking works. This is the implemented Terebi contract, not a claim that Tubular has already adopted it.
|
||||
|
||||
`make check-subtitles`, `make check-resolved` and `make check-network` use deterministic local VTT/HLS fixtures; `make check-startup` exercises the actual CLI-to-GUI handoff. Live playback is never part of the default suite. With a private, freshly resolved source document, explicitly run `.build/resolved-playback-check /absolute/source.json` for native video/audio and subtitle/seek smoke. Visual rendering and perceived synchronization still require a visible-app check.
|
||||
|
||||
Verification recorded for the direct `ssub`/`sun` source: a freshly resolved native stream decoded video/audio and advanced, selected English VTT, and matched the model's subtitle text to the expected cues after forward/backward acknowledged seeks. Local encrypted MPEG-TS and fMP4 fixtures also verified disguised image suffixes, headers on master/variant/segment/key/init requests, query preservation, foreign-origin/redirect isolation and redacted errors. `TEREBI_CHECK_RESOLVED_ONLY=1 make check-startup` passed actual CLI launch into two independent GUI episodes, resource-specific subtitle headers and version-2 reports. CLI, subtitle, progress, reporting, YouTube resolver and system-playback checks passed.
|
||||
|
||||
The full startup suite is **not green**: runs failed its existing held-Space assertion (expected paused, observed playing) and, in another run, its YouTube end-transition deadline. Their causes are not established; the targeted Miruro launch check does not replace them. Visual subtitle appearance and perceived A/V synchronization remain human verification, and Tubular's version-2 consumer change remains external coordination.
|
||||
|
||||
|
||||
## Resume playback
|
||||
|
||||
Reopening a local video or YouTube video automatically resumes its saved position, including after quitting and relaunching the app. Local files use their resolved file paths; YouTube uses the canonical video webpage URL, so short links and newly extracted media URLs share progress. The app does not automatically reopen the last video on launch.
|
||||
Reopening a local video, YouTube video or resolved Miruro episode automatically resumes its saved position, including after quitting and relaunching the app. Local files use their resolved file paths; YouTube uses the canonical video webpage URL; Miruro uses its opaque media ID and episode number. Newly resolved addresses therefore share progress without saving CDN tokens as identity. The app does not automatically reopen the last video on launch.
|
||||
|
||||
Progress is stored locally in UserDefaults, checkpointed every five seconds of playback progress, and saved on pause, close, replacement, and normal quit. Only authoritative, acknowledged positions are saved—not an unfinished seek or slider drag. Finished videos clear their bookmark and reopen from the beginning; a bookmark beyond the current video's duration also restarts at the beginning. Failed opens preserve existing bookmarks. Live/non-seekable inputs and audio-only media do not record progress.
|
||||
|
||||
@@ -169,7 +225,7 @@ terebi --new-instance --url 'https://www.youtube.com/watch?v=jNQXAC9IVRw' \
|
||||
|
||||
The shared C++ backend owns reporting inside the GUI process, independent of the launcher and UserDefaults. It exclusively creates a regular file with owner-only read/write permissions (`0600`), rejecting existing destinations and symlinks. Parent directories must already exist; the caller owns directory/file cleanup. Concurrent players cannot claim the same destination.
|
||||
|
||||
Each complete line is a version-1 JSON record:
|
||||
Each complete YouTube line is a version-1 JSON record (resolved Miruro sources use the version-2 identity documented above):
|
||||
|
||||
```json
|
||||
{"version":1,"event":"progress","media_url":"https://www.youtube.com/watch?v=jNQXAC9IVRw","position_seconds":123.5,"duration_seconds":200.0,"completion_eligible":true}
|
||||
@@ -301,7 +357,7 @@ Visual correctness, perceived A/V synchronization, and control usability still n
|
||||
|
||||
## Current limits
|
||||
|
||||
Local regular files and YouTube video URLs, selectable YouTube resolutions, automatically selected audio/video tracks within the chosen quality, software decoding, and SDR output. PQ/HLG HDR is rejected rather than rendered incorrectly. No subtitles, explicit codec/audio-track selection, VideoToolbox decoding, or rotation/deinterlacing. Mid-track audio format changes report an error. The URL UI accepts YouTube videos rather than arbitrary web pages or streaming services.
|
||||
Local regular files, YouTube video URLs and resolved Miruro HTTP(S) sources, selectable YouTube resolutions, automatically selected audio/video tracks within the chosen quality, software decoding, and SDR output. PQ/HLG HDR is rejected rather than rendered incorrectly. External resolved-source WebVTT is supported; embedded/YouTube captions, explicit codec/audio-track selection, VideoToolbox decoding and rotation/deinterlacing are not. Mid-track audio format changes report an error. The URL UI accepts YouTube videos; other resolved media uses `--source-file`.
|
||||
|
||||
Forward frame stepping retains the live video decoder cursor instead of seeking and re-decoding the same keyframe group for each repeat. Backward stepping still scans from an earlier keyframe; long-GOP or unindexed files can therefore step backward slowly, without blanking the image or allocating a command per key repeat. Audio is skipped while stepping and rebuilt once on resume from the verified preroll anchor; resuming after a long forward run can also require a longer preroll. Intermediate frames needed by inter-frame dependencies are decoded and discarded; frames are not cached across the entire file. Every accepted step counts. Stepping requires usable decoded presentation timestamps and reports an error rather than inventing frame times when they are absent.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user