Skip to content

Feat: When hovering over an event's thumbs, play the event using HLS.#4839

Open
IgorA100 wants to merge 42 commits into
ZoneMinder:masterfrom
IgorA100:patch-180448
Open

Feat: When hovering over an event's thumbs, play the event using HLS.#4839
IgorA100 wants to merge 42 commits into
ZoneMinder:masterfrom
IgorA100:patch-180448

Conversation

@IgorA100
Copy link
Copy Markdown
Contributor

@IgorA100 IgorA100 commented May 16, 2026

  • If there is an index.m3u8 manifest file for the recorded event, we first attempt playback using HLS.
  • If playback using HLS does not start within 2 seconds, we attempt playback using MP4.
  • If the recorded event is longer than 1 minute and there's a problem with HLS, don't try to play it as MP4, but immediately switch to MJPEG playback.
  • Display the player type during playback (for both Live and recorded events) and the end of playback (for recorded events).
  • Fixed assigning the value of read cookies to stream.muted.
  • If errors occur, output them to the console.
  • Use video.autoplay = false instead of true, and then use video.play().then(_ => {. This will allow for more correct handling of promises and a repeat playback attempt if muted=false is an issue.

@IgorA100 IgorA100 changed the title When hovering over an event's thumbs, play the event using HLS. Feat: When hovering over an event's thumbs, play the event using HLS. May 20, 2026
@IgorA100 IgorA100 marked this pull request as ready for review May 20, 2026 20:34
@IgorA100
Copy link
Copy Markdown
Contributor Author

@connortechnology
Please check.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances thumbnail hover previews for recorded events by attempting HLS playback first (when an index.m3u8 manifest is available), with fallbacks to MP4 and then MJPEG, while also displaying the active player type/status during playback.

Changes:

  • Add HLS-based playback on event thumbnail hover with fallback logic (HLS → MP4 (short events) → MJPEG) and status display updates.
  • Extend event list AJAX responses to include HLS source URLs and event duration metadata.
  • Update Event::getStreamSrc() to route mode=mp4hls to a dedicated view_hls endpoint.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
web/skins/classic/js/skin.js Adds hover playback orchestration for HLS/MP4/MJPEG and injects player-status info into the overlay UI.
web/includes/Event.php Adjusts stream URL generation to support mp4hls via view_hls.
web/ajax/events.php Adds data-video-hls-src and duration attributes for thumbnails, enabling HLS-first hover playback decisions.
Comments suppressed due to low confidence (3)

web/skins/classic/js/skin.js:1506

  • getCookie('zmWatchMuted') is a string, so !!getCookie(...) will incorrectly mute when the cookie is 'false'. This breaks the user setting for RTSP2Web thumbnails; use an explicit string comparison instead of boolean coercion.
    const video = document.createElement('video');
    video.removeAttribute('controls');
    video.style.cssText = 'width: 100%; height: 100%;';
    video.autoplay = true;
    video.muted = !!getCookie('zmWatchMuted');
    video.playsInline = true;

web/skins/classic/js/skin.js:1608

  • The HLS hover player sets video.muted = !!getCookie('zmWatchMuted'), but the cookie value is 'true'/'false' (string). !!'false' is true, so hover playback will always be muted whenever the cookie exists. Use an explicit comparison to 'true' or preserve the prior !== 'false' behavior.
    video.style.cssText = 'width: 100%; height: 100%;';
    video.autoplay = false;
    video.muted = !!getCookie('zmWatchMuted');
    video.playsInline = true;
    container.appendChild(video);

web/skins/classic/js/skin.js:1692

  • The MP4 fallback player uses video.muted = !!getCookie('zmWatchMuted'), which treats the cookie string 'false' as truthy and forces muting. Use explicit string-to-boolean handling for this cookie value.
  const video = document.createElement('video');
  const previewRate = getPreviewRate();
  video.src = src;
  video.autoplay = true;
  video.muted = !!getCookie('zmWatchMuted');
  video.playsInline = true;
  video.playbackRate = previewRate;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread web/skins/classic/js/skin.js Outdated
Comment thread web/skins/classic/js/skin.js Outdated
Comment thread web/skins/classic/js/skin.js
Comment thread web/includes/Event.php Outdated
Comment thread web/ajax/events.php Outdated
@IgorA100 IgorA100 marked this pull request as draft May 21, 2026 07:24
@IgorA100 IgorA100 marked this pull request as ready for review May 21, 2026 10:31
@connortechnology connortechnology requested a review from Copilot May 21, 2026 11:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

web/skins/classic/js/skin.js:1522

  • Hls.Events.ERROR callbacks receive (event, data), but this handler only declares a single parameter and logs it; that will log the event name and lose the useful data payload (type/details/fatal), making debugging harder. Update the handler signature to accept both parameters and log data (or at least data.details/type/fatal).
    if (Hls.isSupported()) {
      const hls = new Hls();
      hls.loadSource(hlsUrl);
      hls.attachMedia(video);
      hls.on(Hls.Events.ERROR, function(e) {
        console.error(e);
        hls.destroy();
        video.remove();
        fallbackToMjpeg();
      });

Comment thread web/skins/classic/js/skin.js
Comment thread web/skins/classic/js/skin.js
@IgorA100 IgorA100 marked this pull request as draft May 21, 2026 15:17
IgorA100 added 5 commits May 21, 2026 18:53
…valid, the return status code is 204 instead of 404. (view_hls.php)

This will avoid errors in the browser console and PHP logs.
….m3u8" manifest file, even if the physical file is missing. (events.php)

This will reduce the load on the file system.
…server response status and load the manifest only if the response status is 200. (skin.js)

This will avoid unnecessary errors.
…atusBar. (skin.js)

Also:
When executing createVideoElement(), autoplay is always false, and after the video is created, we now start playback via thumbnailVideoPlay().
@IgorA100 IgorA100 marked this pull request as ready for review May 21, 2026 16:59
@IgorA100
Copy link
Copy Markdown
Contributor Author

Ready for re-checking

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread web/skins/classic/js/skin.js
Comment thread web/ajax/watch.php Outdated
Comment thread web/skins/classic/js/skin.js
Comment thread web/includes/Event.php Outdated
@IgorA100 IgorA100 marked this pull request as draft May 30, 2026 19:28
IgorA100 and others added 2 commits May 31, 2026 14:44
- When executing Event->Length() , get the duration of the recorded event file if the duration in the database table is 0
- If native HLS playback fails in Safari, we first try playing MP4, and only if MP4 playback fails do we switch to MJPEG playback
@IgorA100
Copy link
Copy Markdown
Contributor Author

@connortechnology
I've made the necessary corrections and comments.
Please recheck.

@IgorA100 IgorA100 marked this pull request as ready for review May 31, 2026 13:28
@connortechnology connortechnology requested a review from Copilot May 31, 2026 16:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread web/skins/classic/js/skin.js
Comment thread web/skins/classic/js/skin.js Outdated
@IgorA100 IgorA100 marked this pull request as draft May 31, 2026 19:04
@IgorA100
Copy link
Copy Markdown
Contributor Author

@connortechnology
Please recheck.

@IgorA100 IgorA100 marked this pull request as ready for review May 31, 2026 20:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread web/skins/classic/js/skin.js
Comment thread web/skins/classic/js/skin.js Outdated
Comment thread web/views/view_hls.php Outdated
Comment thread web/includes/Event.php
@IgorA100 IgorA100 marked this pull request as draft June 4, 2026 07:23
@IgorA100 IgorA100 marked this pull request as ready for review June 4, 2026 11:48
@IgorA100
Copy link
Copy Markdown
Contributor Author

IgorA100 commented Jun 4, 2026

@connortechnology
Please recheck.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Comment on lines 1493 to 1497
const stream = document.createElement('video-stream');
stream.style.cssText = 'width: 100%; height: 100%; display: block;';
stream.background = true;
stream.muted = getCookie('zmWatchMuted') !== 'false';
stream.muted = getCookie('zmWatchMuted') === 'true';
stream.src = url.href;
Comment on lines +1649 to +1656
function checkM3u8File(hlsUrl) {
if (!hlsUrl || currentView === 'frames') return false;

try {
const xhr = new XMLHttpRequest();
xhr.open("GET", hlsUrl, false);
xhr.send();
return (xhr.readyState === 4 && xhr.status === 200);
Comment on lines +1664 to +1667
let video = null;
const videoSrc = img.getAttribute('video_src');
if (videoSrc && currentView !== 'frames' && img.dataset.videoDurationSecs < 60) {
const eventStart = img.dataset.eventStart;
Comment thread web/views/view_hls.php
if ($m3u8_content_check === false) {
header('HTTP/1.1 204 No Content');
header('Cache-Control: no-cache');
ZM\Debug('HLS manifest ' . $m3u8_path .' has not available yet');
Comment thread web/includes/Event.php
Comment on lines 112 to +116
public function Length(){
if(! isset($this->{'Length'})){
//TODO: Do something when no Length found
$duration = 0;
if ( !isset($this->{'Length'}) || (float)$this->{'Length'} <= 0 ) {
$files = glob($this->Path().'{/incomplete.*,/'.$this->{'Id'}.'-video.*}', GLOB_NOSORT | GLOB_BRACE);
if (count($files) > 0) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants