web player: seekable transcode playback + custom controls
Transcoded videos stream through a live ffmpeg pipe with no byte ranges, so the browser couldn't seek/rewind them at all. Fix it end to end: - get_transcode accepts ?start=<secs> and passes -ss before -i (fast keyframe seek), rebasing timestamps to 0 from there. - The player gets a custom control bar for transcode sources: play/pause, a scrubber driven by the known duration, current/total time, ±10s, mute + volume, and fullscreen, plus space / arrows / j / l / f keyboard shortcuts. Seeking re-requests the stream at the new offset; effective time is offset + element.currentTime. Every seek path (scrubber, chapters, transcript click, resume, save-position, chapter/transcript highlight) now routes through one offset-aware playerSeek()/effTime(). - Direct /files/ videos keep native <video controls> (already seekable). Verified server-side: start=0 vs start=120 yield different streams. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
033572d3bd
commit
c3ff9121f6
2 changed files with 103 additions and 10 deletions
19
src/web.rs
19
src/web.rs
|
|
@ -1746,9 +1746,18 @@ async fn get_sub_vtt(
|
|||
([(header::CONTENT_TYPE, "text/vtt; charset=utf-8")], vtt).into_response()
|
||||
}
|
||||
|
||||
/// Query for [`get_transcode`]: `start` seconds to begin the stream at, so the
|
||||
/// player can scrub a non-seekable live transcode by re-requesting at an offset.
|
||||
#[derive(Deserialize)]
|
||||
struct TranscodeQuery {
|
||||
#[serde(default)]
|
||||
start: Option<f64>,
|
||||
}
|
||||
|
||||
async fn get_transcode(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Path(id): Path<String>,
|
||||
Query(q): Query<TranscodeQuery>,
|
||||
) -> Response {
|
||||
let path = {
|
||||
let lib = state.library.lock_recover();
|
||||
|
|
@ -1760,8 +1769,14 @@ async fn get_transcode(
|
|||
|
||||
let mut cmd = tokio::process::Command::new("ffmpeg");
|
||||
cmd.arg("-hide_banner")
|
||||
.arg("-loglevel").arg("error")
|
||||
.arg("-i").arg(&path)
|
||||
.arg("-loglevel").arg("error");
|
||||
// Seek to the requested offset before the input (fast keyframe seek). The
|
||||
// live pipe isn't byte-range seekable, so the player re-requests the stream
|
||||
// at a new `start` to scrub; ffmpeg rebases timestamps to 0 from there.
|
||||
if let Some(start) = q.start.filter(|s| *s > 0.0) {
|
||||
cmd.arg("-ss").arg(format!("{start:.3}"));
|
||||
}
|
||||
cmd.arg("-i").arg(&path)
|
||||
.arg("-c:v").arg("libx264")
|
||||
.arg("-preset").arg("veryfast")
|
||||
.arg("-crf").arg("23")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue