DVRDesk a simple Windows and Linux client for Channels

Working great. No complaints :smile:

1 Like

Any possibility of adding a light-mode option? (Posted on Github, but maybe this is a better place to ask?) I find dark mode very hard on my eyes, and would like to be able to select light mode (or my system app mode, which is set to light) as an option.

Now that you mention it, I'm not a fan myself! Try version v1.5

Thanks! So much better!

Your quick fixes are awesome, and this is a truly great lightweight playback experience. When you get around to releasing another version, what do you think about adding support for double click on a show to quickly watch it instead of clicking on it and then clicking play? Totally minor detail lol.

Thanks!

Added to v1.6. I also fixed a bug that I found related to full screen playback.

You might be interested in the process I'm using for this project. I'm using VS Code and CoPilot. Claude Sonnet 4.6 does all the work; I tell it what I want, then test and direct. It's awesome! Here's an example of the process. This took about 40 minutes from my initial prompt to initiating the update to the repository. It took a lot longer to get this post to format the way I wanted, and I still can't get the 'thinking to myself" output to appear in context. That stuff is fascinating. For example, after I asked it to hide the menu when playing full screen, it said this:

The user wants the video player UI controls (header with buttons, commercial ad bar, progress indicators) to auto-hide after a few seconds of inactivity and reappear on mouse move/hover. This is a classic video player auto-hide controls pattern.

Let me look at the VideoPlayer component more carefully to understand the full structure.

Sometimes that process can go on for quite a while, and I can see that it has gone off on a tangent, made an incorrect assumption, or misunderstood my direction. At that point, you can add another prompt while it's working, and it will pick it up on the fly and change direction based on the new input.

I would defiantly be using this process if I was still working now, although, I'd be a lot more intimate with the code than I need to be for this project. It's still not at the point that you could completely replace programmers with AI, but it's definitely a productivity multiplier.

WinChannels Development Log — May 8, 2026


User

I have another request, and I found a minor bug. The request is to add double click as a shortcut to play. I think he is referring to the recent recordings list, since that's the only place where two clicks are required to get to play.

The bug is that the fullscreen view doesn't size properly if the aspect ratio of the active monitor is different than the aspect ratio of the content. For example, my current screen is a 32:9 widescreen. If I use the native fullscreen button in the playback, the video is displayed centered on the screen with black bars on the left and right sides. If I use our custom button, the video expands to the full width, with the bottom cropped. Incidentally, the same thing happens if I use the Windows resizing UI (snap layouts).


GitHub Copilot

Let me understand the two issues:

Double-click to play: Add a double-click shortcut on recording items in the Recent Recordings list — that's the only place requiring two clicks to play (click to select, then click the Play button in the detail pane).

Fullscreen aspect ratio bug: The custom fullscreen button and Windows snap/resize cause the video to expand to full width with the bottom cropped. The native browser fullscreen works correctly (black bars on sides). This is a CSS/sizing issue with the video element in a flex container.

Let me explore the codebase to understand the current implementation.


I read RecentRecordings.tsx, VideoPlayer.tsx, and VideoPlayer.css in parallel to get the full picture before touching anything.

What I found in VideoPlayer.css:

.video-element {
  flex: 1;
  width: 100%;
  background: #000;
  outline: none;
}

The .video-overlay is a display: flex; flex-direction: column container. When overlay.requestFullscreen() fires on a 32:9 monitor (~5120px wide), width: 100% makes the <video> element that same width. A flex child without min-height: 0 is allowed to grow beyond its allocated height based on its intrinsic aspect ratio — so a 16:9 video at 5120px wide computes a natural height of ~2880px, but the visible container is only ~1440px tall. The bottom gets clipped. The native fullscreen button doesn't have this problem because the browser applies its own containment. The fix is min-height: 0 (honor the flex allocation) and object-fit: contain (letterbox within it).

What I found in RecentRecordings.tsx:

<button
  key={rec.id}
  className={`rec-item ${selected?.id === rec.id ? 'rec-item--active' : ''}`}
  onClick={() => selectRecording(rec)}
>

The playItem() function is already imported from the store and used by the detail pane's Play button with the same arguments. I just needed to wire it to onDoubleClick on this same button.


Fix 1 — Fullscreen aspect ratio (VideoPlayer.css)

.video-element {
  flex: 1;
  min-height: 0;
  width: 100%;
  object-fit: contain;
  background: #000;
  outline: none;
}

Fix 2 — Double-click to play (RecentRecordings.tsx)

<button
  key={rec.id}
  className={`rec-item ${selected?.id === rec.id ? 'rec-item--active' : ''}`}
  onClick={() => selectRecording(rec)}
  onDoubleClick={() => playItem(
    rec.id,
    recLabel(rec),
    rec.path,
    rec.commercials,
    '',
    rec.playback_time,
    rec.show_id ? 'episode' : 'movie'
  )}
>

User

That looks good. Can you also make the UI control elements (menu, commercial map, progress bar, etc) disappear after a few seconds? They should reappear on mouseover.


GitHub Copilot

The goal is to auto-hide the header bar and commercial timeline bar after a few seconds of inactivity, and restore them on mouse movement.

A few approaches to consider:

  1. Conditional rendering — unmount the elements when hidden. Rejected: causes layout shifts (the video element would resize to fill the space) and could lose DOM state (caption track selections, etc.).
  2. CSS visibility: hidden — hides visually but still occupies space. Doesn't help here since the header is flex-shrink: 0 and would still push the video down.
  3. CSS opacity: 0 + pointer-events: none — visually hidden, click-through, no layout shift, animatable with transition. This is the right choice.

Plan:

  • Add controlsVisible state, defaulting to true
  • Add a hideTimerRef to hold the timeout ID
  • resetHideTimer callback: clears any pending timer, sets controlsVisible(true), starts a new 3-second timeout that sets controlsVisible(false)
  • onMouseMove={resetHideTimer} on the overlay div
  • A useEffect on nowPlayingKey to start the timer when a new item begins playing, and clean up on unmount
  • CSS class video-overlay--controls-hidden applied when !controlsVisible, which fades the header and ad bar out and sets cursor: none

The skip toast and nerd stats panel are intentionally excluded — the toast is transient and auto-removes itself, and the stats panel should remain readable during debugging without needing to move the mouse.

resetHideTimer is a stable useCallback (no dependencies that change) so it can be safely listed as a useEffect dependency without causing re-registration loops.


Changes — VideoPlayer.tsx

New state and ref (added alongside isOverlayFullscreen):

const [isOverlayFullscreen, setIsOverlayFullscreen] = useState(false);
const [controlsVisible, setControlsVisible] = useState(true);
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

New callback and effect (added before the existing skipBy):

// Auto-hide controls after inactivity; reset on mouse movement
const resetHideTimer = useCallback(() => {
  if (hideTimerRef.current !== null) window.clearTimeout(hideTimerRef.current);
  setControlsVisible(true);
  hideTimerRef.current = window.setTimeout(() => setControlsVisible(false), 3000);
}, []);

useEffect(() => {
  resetHideTimer();
  return () => {
    if (hideTimerRef.current !== null) window.clearTimeout(hideTimerRef.current);
  };
}, [nowPlayingKey, resetHideTimer]);

Overlay div — class and mouse handler:

<div
  className={`video-overlay${controlsVisible ? '' : ' video-overlay--controls-hidden'}`}
  ref={overlayRef}
  tabIndex={0}
  onMouseMove={resetHideTimer}
>

Changes — VideoPlayer.css

Transitions on the two affected elements:

.video-header {
  /* ...existing styles... */
  transition: opacity 0.3s ease;
}

.video-ad-bar {
  /* ...existing styles... */
  transition: opacity 0.3s ease;
}

New rules for the hidden state:

.video-overlay--controls-hidden {
  cursor: none;
}

.video-overlay--controls-hidden .video-header,
.video-overlay--controls-hidden .video-ad-bar {
  opacity: 0;
  pointer-events: none;
}

User

Looks good. Please increment the version, push and tag.


GitHub Copilot

New features added (double-click to play, auto-hide controls) plus the fullscreen fix — minor bump → v1.6.0. After confirmation:

  • Bumped version in package.json, tauri.conf.json, and Cargo.toml
  • Updated ENGINEERING_JOURNAL.md
  • git add -A && git commit -m "v1.6.0: double-click to play, auto-hide player controls, fullscreen aspect ratio fix"
  • git push origin main
  • git tag v1.6.0 && git push origin v1.6.0

I may be missing something obvious here, but how do you sort a TV Show's episodes by Season, then Episode? So this example would sort as S1E1, S1E2, S1E3, S1E4, S1E5, S1E6


None of the available sort options work.

This one just happens to work because it was recorded in episode order.

I added season and episode as well as first aired. Also added release date to movies. V1.7 - it may take a few minutes to finish building...

1 Like

Awesome!
That was quick.

1 Like

LOL it's like ask for feature and he's like yeah AI already did that. :laughing:

Oh wait, AI is taking over our PCs.... :upside_down_face:

@jay343
Any changes from v1.7.0 to v1.9.0 that would affect playing back recordings?

I was playing back a recording using v1.9.0 and it kept pausing and I'd get the buffering circle.
Had to skip ahead to get it playing again.
So I did what I use to do when seeing that same behavior in the CDVR web UI player.
Deleted the video index and then it played fine.

Was going to reinstall v1.7.0 to see if it worked on the recording until I saw you just released v1.9.1.

V1.91 addresses a problem I found with cc4c. When it tuned a cc4c channel live, then stopped the playback, it would not stop the stream, so it continues to use resources on the server.

I've only seen the buffering circle on cc4c channels, and usually a few seconds after starting the stream. I found that restarting the stream recovered it. Let me know if you see a pattern, or if you see anything in WinChannels error log, or in the Channels log that corresponds to it.

error log

[2026-05-19T02:34:47.620Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:39:53.004Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T02:39:54.005Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:40:14.812Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:44:46.311Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:45:14.012Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:45:53.013Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T02:46:30.514Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T02:56:28.122Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:04:25.225Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:04:30.917Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:04:31.315Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:08:49.618Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:09:21.622Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:17:45.415Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:21:05.219Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:23:17.912Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:23:26.710Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:23:28.823Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:23:32.326Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:23:33.526Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T03:23:40.512Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T03:30:42.913Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T04:39:39.933Z] console.error
HLS error: mediaError fragParsingError false url: undefined response: undefined
[2026-05-19T04:42:15.141Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T04:42:34.746Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined
[2026-05-19T04:42:37.248Z] console.error
HLS error: mediaError bufferSeekOverHole false url: undefined response: undefined
[2026-05-19T04:52:07.841Z] console.error
HLS error: mediaError bufferStalledError false url: undefined response: undefined

Like I said, if I delete the CDVR video index it plays fine.

How do you delete the index?

Delete the Metadata/FILE_ID# folder
Metadata/3404 in this case

I reverted to v1.7.0 and same issue.

Have you tried this when playing recordings?
/dvr/files/file#/stream.mpg

Instead of
http://server-ip:port/dvr/files/3404/hls/master.m3u8?encoder=remux

Try
http://server-ip:port/dvr/files/3404/stream.mpg

That may require a different player. Before I look into that, do you know if the "Fix Video Timestamps" command does the job?

It doesn't happen with every recording and I'm not going to run that on my recordings.

I can search here for the few times I did that and it ruined my recordings, like Make backup of recording before rewriting video timestamps

If CDVR doesn't have the video index (because I deleted it) it plays fine in the web UI player and yours.
I'll just delete the video index folders. Probably will setup a cron job for that.

I made a backup copy and then remuxed the recording using ffmpeg which showed no erros.

ffmpeg -ignore_unknown -i %1 -map 0 -acodec copy -vcodec copy -scodec copy "%~dp0%~n1.ts" > "%~n1_remux_log.txt" 2>&1

Input #0, mpegts, from '\\nas-1019\ChDVR8489\TV\Massive Moves\Massive Moves Lottery Lodge 2026-05-18-0428.mpg':
  Duration: 00:27:49.67, start: 0.316733, bitrate: 3772 kb/s
  Program 1 
  Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], Closed Captions, 29.97 fps, 29.97 tbr, 90k tbn
  Stream #0:1[0x102](und): Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 95 kb/s
  Stream #0:2[0x103]: Data: timed_id3 (ID3  / 0x20334449)
  Stream #0:3[0x108]: Data: bin_data (xVTT / 0x54545678)
Output #0, mpegts, to 'T:\skrach\Massive Moves Lottery Lodge 2026-05-18-0428.ts':
  Metadata:
    encoder         : Lavf59.27.100
  Stream #0:0: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 29.97 fps, 29.97 tbr, 90k tbn
  Stream #0:1(und): Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 95 kb/s
  Stream #0:2: Data: timed_id3 (ID3  / 0x20334449) (default)
  Stream #0:3: Data: bin_data (xVTT / 0x54545678)
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
  Stream #0:1 -> #0:1 (copy)
  Stream #0:2 -> #0:2 (copy)
  Stream #0:3 -> #0:3 (copy)
video:723898kB audio:19566kB subtitle:0kB other streams:36kB global headers:0kB muxing overhead: 3.980067%

Copied the remuxed recording over the original and regenerated the video index and it plays without buffering.

So on the recordings that stall with the buffering, spinning circle.
Either deleting the video index, or remuxing w/ffmpeg and regenerating the video index fixes it.
I don't see issues looking at them with my video editor and no errors from the ffmpeg remux.

I found if I just remux the recording and don't touch the video index that it plays with artifacts, smearing, tearing, macro-blocking, etc. Either deleting the video index or regenerating it cures that. There's something wrong with CDVR and its video index. I wish they would offer a way to play a recording without using the video index.

Until then, I'll setup a cron job to delete the video indexes after they're created.

The app previously known as WinChannels shall henceforth be known as DVRDesk, a native Windows and Linux desktop client application for the Channels DVR server, developed by the Channels Community, and unsupported by Fancy Bits and Channels. Existing repository links will continue to work.