Fix audio export hang when a video's audio is shorter than the video
Offline export blocks each streaming source until decoded frames are available. For a video whose audio track ends before the video (or a container like WebM/Opus with no exact stream duration), the tail chunks wait for frames that never arrive — the 10s safety valve fires per chunk, so the export appears to hang indefinitely. Add a `finished` flag to ReadAheadBuffer, set when the disk reader hits EOF (cleared on seek). The export-mode wait now breaks immediately once the source is finished and the requested frames aren't present, rendering silence for the missing tail instead of timing out chunk-by-chunk. Only the export path reads the flag, so playback is unaffected.
This commit is contained in:
parent
5844a0f070
commit
70ac0cde00
|
|
@ -77,6 +77,12 @@ pub struct ReadAheadBuffer {
|
||||||
/// When true, `render_from_file` will block-wait for frames instead of
|
/// When true, `render_from_file` will block-wait for frames instead of
|
||||||
/// returning silence on buffer miss. Used during offline export.
|
/// returning silence on buffer miss. Used during offline export.
|
||||||
export_mode: AtomicBool,
|
export_mode: AtomicBool,
|
||||||
|
/// Set by the disk reader when the source reaches EOF (no more frames will
|
||||||
|
/// ever be decoded past the current valid range). Lets export-mode block-waits
|
||||||
|
/// give up immediately for frames past the end of the audio (e.g. a video whose
|
||||||
|
/// audio track is shorter than the video) instead of timing out per chunk.
|
||||||
|
/// Cleared on `reset` (a seek may decode fresh data again).
|
||||||
|
finished: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: See the doc comment on ReadAheadBuffer for the full safety argument.
|
// SAFETY: See the doc comment on ReadAheadBuffer for the full safety argument.
|
||||||
|
|
@ -112,6 +118,7 @@ impl ReadAheadBuffer {
|
||||||
sample_rate,
|
sample_rate,
|
||||||
target_frame: AtomicU64::new(0),
|
target_frame: AtomicU64::new(0),
|
||||||
export_mode: AtomicBool::new(false),
|
export_mode: AtomicBool::new(false),
|
||||||
|
finished: AtomicBool::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,11 +223,24 @@ impl ReadAheadBuffer {
|
||||||
self.export_mode.load(Ordering::Acquire)
|
self.export_mode.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark that the source has reached EOF (set by the disk reader).
|
||||||
|
pub fn set_finished(&self, finished: bool) {
|
||||||
|
self.finished.store(finished, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True once the source hit EOF: no frames past the current valid range
|
||||||
|
/// will ever be decoded (until a `reset`/seek).
|
||||||
|
pub fn is_finished(&self) -> bool {
|
||||||
|
self.finished.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
|
||||||
/// Reset the buffer to start at `new_start` with zero valid frames.
|
/// Reset the buffer to start at `new_start` with zero valid frames.
|
||||||
/// Called by the **disk reader thread** (producer) after a seek.
|
/// Called by the **disk reader thread** (producer) after a seek.
|
||||||
pub fn reset(&self, new_start: u64) {
|
pub fn reset(&self, new_start: u64) {
|
||||||
self.valid_frames.store(0, Ordering::Release);
|
self.valid_frames.store(0, Ordering::Release);
|
||||||
self.start_frame.store(new_start, Ordering::Release);
|
self.start_frame.store(new_start, Ordering::Release);
|
||||||
|
// A seek may decode fresh data past the previous EOF position.
|
||||||
|
self.finished.store(false, Ordering::Release);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write interleaved samples into the buffer, extending the valid range.
|
/// Write interleaved samples into the buffer, extending the valid range.
|
||||||
|
|
@ -1138,7 +1158,12 @@ impl DiskReader {
|
||||||
|
|
||||||
// Decode more data into the buffer.
|
// Decode more data into the buffer.
|
||||||
match reader.decode_next(&mut decode_buf) {
|
match reader.decode_next(&mut decode_buf) {
|
||||||
Ok(0) => {} // EOF
|
Ok(0) => {
|
||||||
|
// EOF: no more frames will be decoded for this buffer until a
|
||||||
|
// seek. Tell export-mode waiters so they stop blocking on
|
||||||
|
// past-the-end frames (e.g. video longer than its audio).
|
||||||
|
buffer.set_finished(true);
|
||||||
|
}
|
||||||
Ok(frames) => {
|
Ok(frames) => {
|
||||||
let was_empty = buffer.valid_frames_count() == 0;
|
let was_empty = buffer.valid_frames_count() == 0;
|
||||||
buffer.write_samples(&decode_buf, frames);
|
buffer.write_samples(&decode_buf, frames);
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,14 @@ impl AudioClipPool {
|
||||||
// Spin-wait with small sleeps until the disk reader fills the buffer
|
// Spin-wait with small sleeps until the disk reader fills the buffer
|
||||||
let mut wait_iters = 0u64;
|
let mut wait_iters = 0u64;
|
||||||
while !ra.has_range(src_start, frames_needed) {
|
while !ra.has_range(src_start, frames_needed) {
|
||||||
|
// The source reached EOF and these frames are past the end of the
|
||||||
|
// audio (e.g. a video whose audio track is shorter than the video,
|
||||||
|
// or a container without exact stream duration). They will never
|
||||||
|
// arrive — stop waiting and let the render fill silence, instead of
|
||||||
|
// burning the 10s safety valve on every remaining chunk.
|
||||||
|
if ra.is_finished() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_micros(100));
|
std::thread::sleep(std::time::Duration::from_micros(100));
|
||||||
wait_iters += 1;
|
wait_iters += 1;
|
||||||
if wait_iters > 100_000 {
|
if wait_iters > 100_000 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue