Feat: When hovering over an event's thumbs, play the event using HLS.#4839
Feat: When hovering over an event's thumbs, play the event using HLS.#4839IgorA100 wants to merge 42 commits into
Conversation
… there is an index.m3u8 file. (events.php)
|
@connortechnology |
There was a problem hiding this comment.
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 routemode=mp4hlsto a dedicatedview_hlsendpoint.
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.
There was a problem hiding this comment.
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.ERRORcallbacks receive(event, data), but this handler only declares a single parameter and logs it; that will log the event name and lose the usefuldatapayload (type/details/fatal), making debugging harder. Update the handler signature to accept both parameters and logdata(or at leastdata.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();
});
…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().
|
Ready for re-checking |
- 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
|
@connortechnology |
|
@connortechnology |
And wait for connectedCallback() to create the internal <video> tag and only then assign the play handler to the <video> tag.
|
@connortechnology |
| 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; |
| 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); |
| let video = null; | ||
| const videoSrc = img.getAttribute('video_src'); | ||
| if (videoSrc && currentView !== 'frames' && img.dataset.videoDurationSecs < 60) { | ||
| const eventStart = img.dataset.eventStart; |
| 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'); |
| 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) { |
video.autoplay = falseinstead oftrue, and then usevideo.play().then(_ => {. This will allow for more correct handling of promises and a repeat playback attempt if muted=false is an issue.