hls: add hash-keyed path helpers + VideoToQueue type
Foundation for migrating HLS playlist output from basename-keyed
(`$VIDEO_PATH/{basename}.m3u8`) to content-hash-keyed
(`$VIDEO_PATH/{hash[..2]}/{hash}/playlist.m3u8`). The basename layout
collides whenever two source videos share a filename — common with
iPhone-style sequential naming (`IMG_NNNN.MOV`) across libraries — so
the loser's playlist gets overwritten and ffmpeg keeps re-queueing the
file every scan.
This commit adds the path layout and type plumbing without touching the
actor pipeline, watcher, or HTTP handlers yet:
- `src/video/hls_paths.rs`: `playlist_for_hash`, `sentinel_for_hash`,
`segment_template_for_hash` built on top of `content_hash::hls_dir`,
with constants for the filenames inside the hash dir. Unit tests
cover the sharded layout and the playlist/sentinel/segment paths
all landing in the same directory (so HLS relative refs resolve).
- `src/content_hash::hls_dir` un-deaded — was waiting for this branch.
- `VideoToQueue` struct in `actors.rs`: pairs a source path with its
content hash so callers that lack a hash (rows mid-backfill) skip
the video rather than fabricate one.
- `playlist_file_for` / `playlist_unsupported_sentinel` retained as
migration-only helpers — they're only needed by the one-shot startup
pass that retires pre-content-hash output.
Follow-ups (separate commits on this branch): wire `hls_paths` through
the queue handler + `PlaylistGenerator`, update the watcher's
`process_new_files` to build `VideoToQueue`, switch `/video/generate`
and `/video/stream` to resolve path→hash and return stable URLs, add
the legacy-layout migration, rewrite `cleanup_orphaned_playlists` for
the new dir shape, and surface progress via Prometheus + `/hls/stats`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,19 @@ impl Handler<ProcessMessage> for StreamActor {
|
||||
}
|
||||
}
|
||||
|
||||
/// A video paired with its content hash, ready to be queued for HLS
|
||||
/// playlist generation. Hash is required because all output paths are
|
||||
/// keyed on it; callers that lack a hash (rows mid-backfill) must skip
|
||||
/// the video rather than fabricate one.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VideoToQueue {
|
||||
pub video_path: PathBuf,
|
||||
pub content_hash: String,
|
||||
}
|
||||
|
||||
/// Legacy basename-keyed playlist path. Used only by the one-shot startup
|
||||
/// migration that retires pre-content-hash output; once cleared, all new
|
||||
/// playlist writes go through [`hls_paths::playlist_for_hash`].
|
||||
pub fn playlist_file_for(playlist_dir: &str, video_path: &Path) -> PathBuf {
|
||||
let filename = video_path
|
||||
.file_name()
|
||||
@@ -55,10 +68,8 @@ pub fn playlist_file_for(playlist_dir: &str, video_path: &Path) -> PathBuf {
|
||||
PathBuf::from(format!("{}/{}.m3u8", playlist_dir, filename))
|
||||
}
|
||||
|
||||
/// Sentinel path written next to a would-be playlist when ffmpeg cannot
|
||||
/// transcode the source (e.g. truncated mp4 with no moov atom). Its presence
|
||||
/// causes future scans to skip the file instead of re-running ffmpeg every
|
||||
/// pass. Delete the `.unsupported` file to force a retry.
|
||||
/// Legacy basename-keyed sentinel path. Same migration-only contract as
|
||||
/// [`playlist_file_for`].
|
||||
pub fn playlist_unsupported_sentinel(playlist_file: &Path) -> PathBuf {
|
||||
let mut s = playlist_file.as_os_str().to_owned();
|
||||
s.push(".unsupported");
|
||||
|
||||
Reference in New Issue
Block a user