Enhancement: Refactor FloatingVideo component to improve JWT handling and ensure playback starts from the beginning of recordings

This commit is contained in:
SergeantPanda 2026-04-26 13:28:13 -05:00
parent 1eda68db7a
commit c83e2be866
2 changed files with 31 additions and 9 deletions

View file

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **Empty show/season folders left behind after deleting a recording**. Deleting a recording removes the MKV (or HLS working directory if still in progress) but previously left the parent show / season directories on disk even when they no longer contained any other recordings. After file cleanup, `RecordingViewSet.destroy` now walks up from the deleted file's parent directory and removes any now-empty directories, stopping at the `/data/recordings` library root so the root itself is never touched.
- **Premature DVR concat on graceful Celery worker shutdown**. A `worker_shutting_down` signal handler in `apps/channels/tasks.py` now sets a module-level `_DVR_SHUTTING_DOWN` flag. The `run_recording` loop checks this flag after FFmpeg exits and, if the recording's `end_time` has not yet passed, skips concatenation and persists `status="interrupted"` with `interrupted_reason="server_shutdown"`. The HLS working directory is preserved across the restart so the recovery path on the next worker startup can resume segment numbering from where it left off rather than truncating the show.
- **Channel start/stop notifications missing name**: the "channel started" toast never showed up and the "channel stopped" toast showed the raw UUID instead of the channel name. `channel_name` is now written into the Redis metadata hash at init time and included in every stats payload pushed over WebSocket; the frontend notification functions read `ch.channel_name` from the stats payload directly, falling back to the store lookup and then a formatted placeholder. Both notifications now display the correct channel name.
- **Channel form reset on group creation**: creating a new channel group from within the channel create/edit form no longer wipes all filled-in form fields. The newly created group is also automatically selected in the channel group field after it is saved. (Fixes #545)

View file

@ -119,7 +119,6 @@ export default function FloatingVideo() {
const contentType = useVideoStore((s) => s.contentType);
const metadata = useVideoStore((s) => s.metadata);
const hideVideo = useVideoStore((s) => s.hideVideo);
const accessToken = useAuthStore((s) => s.accessToken);
const videoRef = useRef(null);
const playerRef = useRef(null);
@ -271,10 +270,16 @@ export default function FloatingVideo() {
};
const handleLoadedData = () => {
setIsLoading(false);
// hls.js applies its `startPosition` after MEDIA_ATTACHED, which can
// run later than `loadedmetadata`. Re-seek here as a safety net so a
// hls.js live playlist doesn't snap to the live edge after our first
// seek attempt happened against an empty seekable range.
seekToStart();
};
const handleCanPlay = () => {
setIsLoading(false);
// Final fallback for the Safari native-HLS path where seekable.start(0)
// is sometimes only valid by the time `canplay` fires.
seekToStart();
// Auto-play for VOD content
video.play().catch((e) => {
@ -329,9 +334,13 @@ export default function FloatingVideo() {
if (isHls && Hls.isSupported()) {
hls = new Hls({
// Start playback at the very beginning of the recording rather than
// the live edge. Without this, an in-progress recording would
// open at "now" and hide all already-recorded content.
// Open at the very beginning of the recording rather than the live
// edge. Without this, an in-progress recording would start at "now"
// and hide everything already recorded. hls.js applies this AFTER
// MEDIA_ATTACHED, so the listener-driven `seekToStart()` above is
// also kept as a safety net for the Safari native-HLS path and for
// edge cases where this initial-position logic loses to the user's
// first interaction.
startPosition: 0,
// Allow seeking back to the start of the recording, regardless of
// current playhead position. Recordings can be hours long and the
@ -347,10 +356,14 @@ export default function FloatingVideo() {
liveMaxLatencyDurationCount: 10,
enableWorker: true,
lowLatencyMode: false,
// Inject the JWT into every playlist + segment XHR.
// Inject the JWT into every playlist + segment XHR. Read the token
// from the auth store at request time rather than capturing the
// closure value at hls.js init, so a refreshed access token mid-
// playback is picked up on the next segment fetch.
xhrSetup: (xhr) => {
if (accessToken) {
xhr.setRequestHeader('Authorization', `Bearer ${accessToken}`);
const token = useAuthStore.getState().accessToken;
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
},
});
@ -435,6 +448,14 @@ export default function FloatingVideo() {
? `${window.location.origin}${streamUrl}`
: streamUrl;
// Read the JWT from the auth store at player-creation time rather than
// relying on the closure-captured `accessToken` value. mpegts.js has
// no per-request setup hook (unlike hls.js's xhrSetup), so this header
// is baked into the IO loader for the life of the player; we just want
// to be sure we use the freshest token available at the moment of
// connection rather than whatever React-render snapshot we closed over.
const liveAccessToken = useAuthStore.getState().accessToken;
const player = mpegts.createPlayer(
{
type: 'mpegts',
@ -451,8 +472,8 @@ export default function FloatingVideo() {
autoCleanupMaxBackwardDuration: 120,
autoCleanupMinBackwardDuration: 60,
reuseRedirectedURL: true,
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
headers: liveAccessToken
? { Authorization: `Bearer ${liveAccessToken}` }
: undefined,
}
);