diff --git a/octoprint_multicam/static/css/multicam.css b/octoprint_multicam/static/css/multicam.css index 339d9c7..7033d24 100644 --- a/octoprint_multicam/static/css/multicam.css +++ b/octoprint_multicam/static/css/multicam.css @@ -28,6 +28,18 @@ color: #FFF; } +.webcam_video_container .video-controls { + position: absolute; + bottom: 10px; + right: 8px; + z-index: 10; + opacity: .4 +} + +.webcam_video_container:hover .video-controls { + opacity: 1 +} + .webcam_img_container { outline: 0; background-color: #000 diff --git a/octoprint_multicam/static/js/multicam.js b/octoprint_multicam/static/js/multicam.js index 77086d5..9e0b5f7 100644 --- a/octoprint_multicam/static/js/multicam.js +++ b/octoprint_multicam/static/js/multicam.js @@ -33,7 +33,7 @@ $(function () { webcam_flipV: ko.observable(false), webcamRatioClass: ko.observable('ratio169'), webcamError: ko.observable(false), - webcamMuted: ko.observable(false), + webcamMuted: ko.observable(true), webRTCPeerConnection: ko.observable(null), webcamElementHls: ko.observable(null), webcamElementWebrtc: ko.observable(null) @@ -51,24 +51,26 @@ $(function () { self._getActiveWebcamVideoElement = function () { if (self.WebCamSettings.webcamWebRTCEnabled()) { - return self.WebCamSettings.webcamElementWebrtc; + return self.WebCamSettings.webcamElementWebrtc[0]; } else { - return self.WebCamSettings.webcamElementHls; + return self.WebCamSettings.webcamElementHls[0]; } }; self.launchWebcamPictureInPicture = function () { + console.log("DEBUGG launchWebcamPictureInPicture",self._getActiveWebcamVideoElement()) self._getActiveWebcamVideoElement().requestPictureInPicture(); }; self.launchWebcamFullscreen = function () { + console.log("DEBUGG launchWebcamPictureInPicture",self._getActiveWebcamVideoElement()) self._getActiveWebcamVideoElement().requestFullscreen(); }; self.toggleWebcamMute = function () { self.WebCamSettings.webcamMuted(!self.WebCamSettings.webcamMuted()); - self.WebCamSettings.webcamElementWebrtc.muted = self.WebCamSettings.webcamMuted(); - self.WebCamSettings.webcamElementHls.muted = self.WebCamSettings.webcamMuted(); + self.WebCamSettings.webcamElementWebrtc[0].muted = self.WebCamSettings.webcamMuted(); + self.WebCamSettings.webcamElementHls[0].muted = self.WebCamSettings.webcamMuted(); }; self.onEventSettingsUpdated = function (payload) { @@ -137,6 +139,13 @@ $(function () { self.webcams.forEach((webcam) => { self.unloadWebcam(webcam); }); + + // Unload HLS + if (self.hls != null) { + self.WebCamSettings.webcamElementHls.src = null; + self.hls.destroy(); + self.hls = null; + } } self.loadWebcam = function (webcam) { @@ -182,11 +191,9 @@ $(function () { webcamImage.attr("src", self.WebCamSettings.streamUrlEscaped()) } else if (streamType == "hls") { self._switchToHlsWebcam() - self.WebCamSettings.webcamElementHls.attr("src", self.WebCamSettings.streamUrlEscaped()) self.onWebcamLoadHls(webcam) } else if (isWebRTCAvailable() && streamType == "webrtc") { self._switchToWebRTCWebcam() - self.WebCamSettings.webcamElementWebrtc.attr("src", self.WebCamSettings.streamUrlEscaped()) self.onWebcamLoadRtc(webcam) } else { console.error("Unknown stream type " + streamType) @@ -300,11 +307,15 @@ $(function () { typeof video.canPlayType != undefined && video.canPlayType("application/vnd.apple.mpegurl") == "probably" ) { + console.log("DEBUGG Using native HLS playback") video.src = self.streamUrlEscaped(); } else if (Hls.isSupported()) { + console.log("DEBUGG Using HLS.js playback") self.hls = new Hls(); self.hls.loadSource(self.WebCamSettings.streamUrlEscaped()); self.hls.attachMedia(video); + }else{ + console.error("Error: HLS not supported") } self.WebCamSettings.webcamMjpgEnabled(false); diff --git a/octoprint_multicam/static/js/multicam_settings.js b/octoprint_multicam/static/js/multicam_settings.js index 434b915..82ee613 100644 --- a/octoprint_multicam/static/js/multicam_settings.js +++ b/octoprint_multicam/static/js/multicam_settings.js @@ -18,12 +18,31 @@ $(function () { self.previewWebCamSettings = { streamUrl: ko.observable(undefined), - webcam_rotate90: ko.observable(undefined), - webcam_flipH: ko.observable(undefined), - webcam_flipV: ko.observable(undefined), - webcamRatioClass: ko.observable(undefined), + streamUrlEscaped: ko.pureComputed(function () { + return encodeURI(self.previewWebCamSettings.streamUrl()); + }), webcamLoaded: ko.observable(false), + webcamStreamType: ko.pureComputed(function () { + try { + return self.determineWebcamStreamType(self.previewWebCamSettings.streamUrlEscaped()); + } catch (e) { + console.error(e); + self.previewWebCamSettings.webcamError(true); + return "mjpg"; + } + }), + webcamMjpgEnabled: ko.observable(false), + webcamWebRTCEnabled: ko.observable(false), + webcamHlsEnabled: ko.observable(false), + webcam_rotate90: ko.observable(false), + webcam_flipH: ko.observable(false), + webcam_flipV: ko.observable(false), + webcamRatioClass: ko.observable('ratio169'), webcamError: ko.observable(false), + webcamMuted: ko.observable(true), + webRTCPeerConnection: ko.observable(null), + webcamElementHls: ko.observable(null), + webcamElementWebrtc: ko.observable(null) }; self.reloadChangesMade = ko.observable(false); @@ -55,6 +74,40 @@ $(function () { self.available_ratios = ["16:9", "4:3"]; }; + self.syncWebcamElements = function () { + var webcamElement = $('.multicam_preview_container'); + self.previewWebCamSettings.webcamElementHls = webcamElement.find(".webcam_hls").first(); + self.previewWebCamSettings.webcamElementWebrtc = webcamElement.find(".webcam_webrtc").first(); + }; + + self._getActiveWebcamVideoElement = function () { + if (self.previewWebCamSettings.webcamWebRTCEnabled()) { + return self.previewWebCamSettings.webcamElementWebrtc[0]; + } else { + return self.previewWebCamSettings.webcamElementHls[0]; + } + }; + + self.launchWebcamPictureInPicture = function () { + console.log("DEBUGG launchWebcamPictureInPicture",self._getActiveWebcamVideoElement()) + self._getActiveWebcamVideoElement().requestPictureInPicture(); + }; + + self.launchWebcamFullscreen = function () { + console.log("DEBUGG launchWebcamPictureInPicture",self._getActiveWebcamVideoElement()) + self._getActiveWebcamVideoElement().requestFullscreen(); + }; + + self.toggleWebcamMute = function () { + self.previewWebCamSettings.webcamMuted(!self.previewWebCamSettings.webcamMuted()); + self.previewWebCamSettings.webcamElementWebrtc[0].muted = self.previewWebCamSettings.webcamMuted(); + self.previewWebCamSettings.webcamElementHls[0].muted = self.previewWebCamSettings.webcamMuted(); + }; + + self.onStartup = function () { + self.syncWebcamElements(); + }; + self.onSettingsShown = function () { self.multicam_profiles(self.settings.settings.plugins.multicam.multicam_profiles()); // Force default webcam in settings to avoid confusion @@ -62,10 +115,6 @@ $(function () { self.selectedPreviewProfileIndex(preSelectedProfile); }; - // self.onSettingsBeforeSave = function () { - - // }; - self.onEventSettingsUpdated = function (payload) { //console.log("DEBUGGG onEventSettingsUpdated - Settings", payload); self.multicam_profiles(self.settings.settings.plugins.multicam.multicam_profiles()); @@ -121,9 +170,56 @@ $(function () { self.reloadChangesMade(true); }; + self.unloadWebcam = function () { + //console.log("DEBUGG Unloading webcam",webcam) + var webcamElement = $('.multicam_preview_container'); + var webcamImage = webcamElement.find(".webcam_image") + + //Turn off on handlers during unload + webcamImage.off("load") + webcamImage.off("error") + + //Remove the src of the webcam to unload it from the window + webcamImage.attr("src", "") + + // Unload HLS + if (self.hls != null) { + self.previewWebCamSettings.webcamElementHls.src = null; + self.hls.destroy(); + self.hls = null; + } + }; + + self.onWebcamLoad = function () { + if (self.previewWebCamSettings.webcamLoaded()) return; + self.previewWebCamSettings.webcamError(false) + self.previewWebCamSettings.webcamHlsEnabled(false) + self.previewWebCamSettings.webcamWebRTCEnabled(false) + self.previewWebCamSettings.webcamLoaded(true) + } + + self.onWebcamLoadHls = function () { + if (self.previewWebCamSettings.webcamLoaded()) return; + self.previewWebCamSettings.webcamError(false) + self.previewWebCamSettings.webcamWebRTCEnabled(false) + self.previewWebCamSettings.webcamLoaded(false) + self.previewWebCamSettings.webcamHlsEnabled(true) + } + + self.onWebcamLoadRtc = function () { + if (self.previewWebCamSettings.webcamLoaded()) return; + self.previewWebCamSettings.webcamError(false) + self.previewWebCamSettings.webcamHlsEnabled(false) + self.previewWebCamSettings.webcamLoaded(false) + self.previewWebCamSettings.webcamWebRTCEnabled(true) + } + self.loadWebCamPreviewStream = function () { self.previewWebCamSettings.webcamLoaded(false); self.previewWebCamSettings.webcamError(false); + + self.unloadWebcam(); + let streamUrl = self.previewWebCamSettings.streamUrl(); //console.log("loading from " + streamUrl); // if (snapshotUrl == null || streamUrl == null || snapshotUrl.length == 0 || streamUrl.length == 0) { @@ -131,22 +227,164 @@ $(function () { alert("Camera-Error: Please make sure that stream-url is configured in your camera-settings") return } - // update the new stream-image - $(".webcam_image_preview").on('load', function () { - //console.log("DEBUGGG webcam_image_preview - loaded") - self.previewWebCamSettings.webcamLoaded(true); - self.previewWebCamSettings.webcamError(false); - $("#webcam_image_preview").off('load'); - $("#webcam_image_preview").off('error'); - }); - $(".webcam_image_preview").on('error', function () { - //console.log("DEBUGGG webcam_image_preview - error") + + var streamType = self.previewWebCamSettings.webcamStreamType(); + if (streamType == "mjpg") { + // update the new stream-image + $(".webcam_image_preview").on('load', function () { + self.onWebcamLoad(); + $("#webcam_image_preview").off('load'); + $("#webcam_image_preview").off('error'); + }); + $(".webcam_image_preview").on('error', function () { + self.onWebcamError(); + $("#webcam_image_preview").off('load'); + $("#webcam_image_preview").off('error'); + }); + + self._switchToMjpgWebcam(); + $(".webcam_image_preview").attr("src", self.previewWebCamSettings.streamUrl()); + } else if (streamType == "hls") { + self._switchToHlsWebcam() + self.onWebcamLoadHls() + } else if (isWebRTCAvailable() && streamType == "webrtc") { + self._switchToWebRTCWebcam() + self.onWebcamLoadRtc() + } else { + console.error("Unknown stream type " + streamType) + } + }; + + self._switchToMjpgWebcam = function () { + var webcamElement = $('.multicam_preview_container'); + var webcamImage = webcamElement.find(".webcam_image") + var currentSrc = webcamImage.attr("src"); + + var newSrc = self.previewWebCamSettings.streamUrlEscaped(); + + if (currentSrc != newSrc) { + //if (self.settings.cacheBuster()) { + if (false) { + if (newSrc.lastIndexOf("?") > -1) { + newSrc += "&"; + } else { + newSrc += "?"; + } + newSrc += new Date().getTime(); + } + self.previewWebCamSettings.webcamLoaded(false); - self.previewWebCamSettings.webcamError(true); - $("#webcam_image_preview").off('load'); - $("#webcam_image_preview").off('error'); - }); - $(".webcam_image_preview").attr("src", self.previewWebCamSettings.streamUrl()); + self.previewWebCamSettings.webcamError(false); + webcamImage.attr("src", newSrc); + + self.previewWebCamSettings.webcamHlsEnabled(false); + self.previewWebCamSettings.webcamMjpgEnabled(true); + self.previewWebCamSettings.webcamWebRTCEnabled(false); + } + }; + + self._switchToHlsWebcam = function () { + var video = self.previewWebCamSettings.webcamElementHls[0]; + //video.onresize = self.previewWebCamSettings._updateVideoTagWebcamLayout; + + // Ensure WebRTC is unloaded + if (self.previewWebCamSettings.webRTCPeerConnection != null) { + try { + if (typeof self.previewWebCamSettings.webRTCPeerConnection.close === 'function') { + self.previewWebCamSettings.webRTCPeerConnection.close(); + } + } catch(e) { + console.log("DEBUGG Error closing WebRTC connection", e) + } + self.previewWebCamSettings.webRTCPeerConnection = null; + } + + // Check for native playback options: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType + if ( + video != null && + typeof video.canPlayType != undefined && + video.canPlayType("application/vnd.apple.mpegurl") == "probably" + ) { + console.log("DEBUGG Using native HLS playback") + video.src = self.streamUrlEscaped(); + } else if (Hls.isSupported()) { + console.log("DEBUGG Using HLS.js playback") + self.hls = new Hls(); + self.hls.loadSource(self.previewWebCamSettings.streamUrlEscaped()); + self.hls.attachMedia(video); + }else{ + console.error("Error: HLS not supported") + } + + self.previewWebCamSettings.webcamMjpgEnabled(false); + self.previewWebCamSettings.webcamHlsEnabled(true); + self.previewWebCamSettings.webcamWebRTCEnabled(false); + }; + + self._switchToWebRTCWebcam = function () { + if (!isWebRTCAvailable()) { + return; + } + var video = self.previewWebCamSettings.webcamElementWebrtc[0]; + //video.onresize = self.previewWebCamSettings._updateVideoTagWebcamLayout; + + // Ensure HLS is unloaded + if (self.hls != null) { + self.previewWebCamSettings.webcamElementHls.src = null; + self.hls.destroy(); + self.hls = null; + } + + // Close any existing, disconnected connection + if ( + self.previewWebCamSettings.webRTCPeerConnection != null && + self.previewWebCamSettings.webRTCPeerConnection.connectionState != "connected" + ) { + self.previewWebCamSettings.webRTCPeerConnection.close(); + self.previewWebCamSettings.webRTCPeerConnection = null; + } + + // Open a new connection if necessary + if (self.previewWebCamSettings.webRTCPeerConnection == null) { + self.previewWebCamSettings.webRTCPeerConnection = startWebRTC( + video, + self.previewWebCamSettings.streamUrlEscaped(), + self.settings.streamWebrtcIceServers() + ); + } + + self.previewWebCamSettings.webcamMjpgEnabled(false); + self.previewWebCamSettings.webcamHlsEnabled(false); + self.previewWebCamSettings.webcamWebRTCEnabled(true); + }; + + self.determineWebcamStreamType = function (streamUrl) { + if (!streamUrl) { + throw "Empty streamUrl. Cannot determine stream type."; + } + + var parsed = validateWebcamUrl(streamUrl); + if (!parsed) { + throw "Invalid streamUrl. Cannot determine stream type."; + } + + if (parsed.protocol === "webrtc:" || parsed.protocol === "webrtcs:") { + console.log("DEBUGG Webcam stream type: webrtc") + return "webrtc"; + } + + var lastDotPosition = parsed.pathname.lastIndexOf("."); + if (lastDotPosition !== -1) { + var extension = parsed.pathname.substring(lastDotPosition + 1); + if (extension.toLowerCase() === "m3u8") { + console.log("DEBUGG Webcam stream type: hls") + return "hls"; + } + } + + // By default, 'mjpg' is the stream type. + console.log("DEBUGG Webcam stream type: mjpg") + return "mjpg"; }; self.onAfterBinding = function () { diff --git a/octoprint_multicam/templates/multicam_settings.jinja2 b/octoprint_multicam/templates/multicam_settings.jinja2 index e7849a6..e50fd27 100644 --- a/octoprint_multicam/templates/multicam_settings.jinja2 +++ b/octoprint_multicam/templates/multicam_settings.jinja2 @@ -5,51 +5,74 @@ ClassicWebcam is enabled. Consider disabling it to hide its webcam interface.
{{ _('Webcam stream loading...') }}
{{ _('Webcam stream not loaded') }}
-{{ _('It might not be configured correctly or require authentication. You can change the URL of the stream under "Settings" > "Classic Webcam" > "Stream URL". If you don\'t have a webcam you can also just disable webcam support there.') }}
-{{ _('Currently configured stream URL') }}:
+{{ _('It might not be configured correctly or require authentication. You can + change + the URL of the stream under "Settings" > "Classic Webcam" > "Stream URL". If you + don\'t + have a webcam you can also just disable webcam support there.') }}
+