diff --git a/app/src/server.js b/app/src/server.js index e2a26eb2..661e977e 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -38,7 +38,7 @@ dependencies: { * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.0.9 + * @version 1.1.0 * */ diff --git a/package.json b/package.json index d3c939d2..862dcce3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalk", - "version": "1.0.9", + "version": "1.1.0", "description": "A free WebRTC browser-based video call", "main": "server.js", "scripts": { diff --git a/public/js/client.js b/public/js/client.js index eeec14fa..711c103d 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -15,7 +15,7 @@ * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.0.9 + * @version 1.1.0 * */ @@ -1373,14 +1373,14 @@ async function whoAreYou() { videoSelect.selectedIndex = initVideoSelect.selectedIndex; lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value); myVideoChange = true; - await refreshLocalMedia(); await changeInitCamera(initVideoSelect.value); + await handleLocalCameraMirror(); }; initMicrophoneSelect.onchange = async () => { audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex; lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value); myVideoChange = false; - await refreshLocalMedia(); + await changeLocalMicrophone(audioInputSelect.value); }; initSpeakerSelect.onchange = () => { audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex; @@ -1496,8 +1496,8 @@ async function loadLocalStorage() { // Start init cam if (useVideo && initVideoSelect.value) { myVideoChange = true; - await refreshLocalMedia(); await changeInitCamera(initVideoSelect.value); + await handleLocalCameraMirror(); } } @@ -1534,9 +1534,14 @@ async function changeInitCamera(deviceId) { navigator.mediaDevices .getUserMedia({ video: videoConstraints }) .then((camStream) => { + // We going to update init video stream initVideo.srcObject = camStream; initStream = camStream; console.log('Success attached init video stream', initStream.getVideoTracks()[0].getSettings()); + // We going to update also the local video + myVideo.srcObject = camStream; + localVideoMediaStream = camStream; + console.log('Success attached local video stream', localVideoMediaStream.getVideoTracks()[0].getSettings()); checkInitConfig(); }) .catch((err) => { @@ -1545,6 +1550,68 @@ async function changeInitCamera(deviceId) { }); } +/** + * Change local camera by device id + * @param {string} deviceId + */ +async function changeLocalCamera(deviceId) { + if (localVideoMediaStream) { + await stopVideoTracks(localVideoMediaStream); + myVideo.style.display = 'block'; + if (!myVideo.classList.contains('mirror')) { + myVideo.classList.toggle('mirror'); + } + } + + // Get video constraints + let videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default'); + videoConstraints['deviceId'] = { exact: deviceId }; + + navigator.mediaDevices + .getUserMedia({ video: videoConstraints }) + .then((camStream) => { + myVideo.srcObject = camStream; + localVideoMediaStream = camStream; + console.log('Success attached local video stream', localVideoMediaStream.getVideoTracks()[0].getSettings()); + refreshMyStreamToPeers(camStream); + }) + .catch((err) => { + console.error('[Error] changeLocalCamera', err); + userLog('error', 'Error while swapping local camera' + err); + }); +} + +/** + * Change local microphone by device id + * @param {string} deviceId + */ +async function changeLocalMicrophone(deviceId) { + if (localAudioMediaStream) { + await stopAudioTracks(localAudioMediaStream); + } + + // Get audio constraints + let audioConstraints = await getAudioConstraints(); + audioConstraints['deviceId'] = { exact: deviceId }; + + navigator.mediaDevices + .getUserMedia({ audio: audioConstraints }) + .then((micStream) => { + myAudio.srcObject = micStream; + localAudioMediaStream = micStream; + console.log( + 'Success attached local microphone stream', + localAudioMediaStream.getAudioTracks()[0].getSettings(), + ); + getMicrophoneVolumeIndicator(micStream); + refreshMyStreamToPeers(micStream, true); + }) + .catch((err) => { + console.error('[Error] changeLocalMicrophone', err); + userLog('error', 'Error while swapping local microphone' + err); + }); +} + /** * Check peer audio and video &audio=1&video=1 * 1/true = enabled / 0/false = disabled @@ -1573,7 +1640,6 @@ function checkPeerAudioVideo() { * Room and Peer name are ok Join Channel */ async function whoAreYouJoin() { - if (isMobileDevice && myVideoStatus && myAudioStatus) await refreshLocalMedia(); myVideoWrap.style.display = 'inline'; myVideoParagraph.innerText = myPeerName + ' (me)'; setPeerAvatarImgName('myVideoAvatarImage', myPeerName); @@ -2295,7 +2361,7 @@ function enumerateVideoDevices(stream) { * Stop tracks from stream * @param {object} stream */ -function stopTracks(stream) { +async function stopTracks(stream) { stream.getTracks().forEach((track) => { track.stop(); }); @@ -2346,6 +2412,7 @@ async function setupLocalVideoMedia() { try { const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints }); if (stream) { + localVideoMediaStream = stream; await loadLocalMedia(stream, 'video'); console.log('10. Access granted to video device'); } @@ -2373,6 +2440,7 @@ async function setupLocalAudioMedia() { if (stream) { await loadLocalMedia(stream, 'audio'); if (useAudio) { + localAudioMediaStream = stream; await getMicrophoneVolumeIndicator(stream); console.log('10. Access granted to audio device'); } @@ -2403,8 +2471,6 @@ async function loadLocalMedia(stream, kind) { //alert('local video'); console.log('SETUP LOCAL VIDEO STREAM'); - localVideoMediaStream = stream; - // local video elements const myVideoWrap = document.createElement('div'); const myLocalMedia = document.createElement('video'); @@ -2560,8 +2626,8 @@ async function loadLocalMedia(stream, kind) { getId('videoMediaContainer').appendChild(myVideoWrap); myVideoWrap.style.display = 'none'; - logStreamSettingsInfo('localVideoMediaStream', localVideoMediaStream); - attachMediaStream(myLocalMedia, localVideoMediaStream); + logStreamSettingsInfo('localVideoMediaStream', stream); + attachMediaStream(myLocalMedia, stream); adaptAspectRatio(); if (isVideoFullScreenSupported) { @@ -2585,7 +2651,7 @@ async function loadLocalMedia(stream, kind) { handleVideoZoomInOut(myVideoZoomInBtn.id, myVideoZoomOutBtn.id, myLocalMedia.id); - refreshMyVideoStatus(localVideoMediaStream); + refreshMyVideoStatus(stream); if (!useVideo) { const videoBtn = getId('videoBtn'); @@ -2609,7 +2675,6 @@ async function loadLocalMedia(stream, kind) { case 'audio': //alert('local audio'); console.log('SETUP LOCAL AUDIO STREAM'); - localAudioMediaStream = stream; // handle remote audio elements const audioMediaContainer = getId('audioMediaContainer'); const localAudioWrap = document.createElement('div'); @@ -2621,9 +2686,9 @@ async function loadLocalMedia(stream, kind) { localAudioMedia.volume = 0; localAudioWrap.appendChild(localAudioMedia); audioMediaContainer.appendChild(localAudioWrap); - logStreamSettingsInfo('localAudioMediaStream', localAudioMediaStream); - attachMediaStream(localAudioMedia, localAudioMediaStream); - refreshMyAudioStatus(localAudioMediaStream); + logStreamSettingsInfo('localAudioMediaStream', stream); + attachMediaStream(localAudioMedia, stream); + refreshMyAudioStatus(stream); break; default: break; @@ -3715,8 +3780,8 @@ function setAudioBtn() { * Video hide - show button click event */ function setVideoBtn() { - videoBtn.addEventListener('click', (e) => { - handleVideo(e, false); + videoBtn.addEventListener('click', async (e) => { + await handleVideo(e, false); }); } @@ -4384,7 +4449,7 @@ function setupMySettings() { // select audio input audioInputSelect.addEventListener('change', async () => { myVideoChange = false; - await refreshLocalMedia(); + await changeLocalMicrophone(audioInputSelect.value); lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value); }); // select audio output @@ -4395,7 +4460,8 @@ function setupMySettings() { // select video input videoSelect.addEventListener('change', async () => { myVideoChange = true; - await refreshLocalMedia(); + await changeLocalCamera(videoSelect.value); + await handleLocalCameraMirror(); lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value); }); // select video quality @@ -4408,7 +4474,7 @@ function setupMySettings() { } else { // select video fps videoFpsSelect.addEventListener('change', (e) => { - videoMaxFrameRate = parseInt(videoFpsSelect.value); + videoMaxFrameRate = parseInt(videoFpsSelect.value, 10); setLocalMaxFps(videoMaxFrameRate); lsSettings.video_fps = e.currentTarget.selectedIndex; lS.setSettings(lsSettings); @@ -4416,7 +4482,7 @@ function setupMySettings() { } // select screen fps screenFpsSelect.addEventListener('change', (e) => { - screenMaxFrameRate = parseInt(screenFpsSelect.value); + screenMaxFrameRate = parseInt(screenFpsSelect.value, 10); if (isScreenStreaming) setLocalMaxFps(screenMaxFrameRate); lsSettings.screen_fps = e.currentTarget.selectedIndex; lS.setSettings(lsSettings); @@ -4489,8 +4555,8 @@ function loadSettingsFromLocalStorage() { msgerSpeechMsg.checked = speechInMessages; screenFpsSelect.selectedIndex = lsSettings.screen_fps; videoFpsSelect.selectedIndex = lsSettings.video_fps; - screenMaxFrameRate = parseInt(getSelectedIndexValue(screenFpsSelect)); - videoMaxFrameRate = parseInt(getSelectedIndexValue(videoFpsSelect)); + screenMaxFrameRate = parseInt(getSelectedIndexValue(screenFpsSelect), 10); + videoMaxFrameRate = parseInt(getSelectedIndexValue(videoFpsSelect), 10); notifyBySound = lsSettings.sounds; isAudioPitchBar = lsSettings.pitch_bar; switchSounds.checked = notifyBySound; @@ -4538,18 +4604,15 @@ function setupVideoUrlPlayer() { } /** - * Refresh Local media audio video in - out + * Camera mirror */ -async function refreshLocalMedia() { - console.log('Refresh local media', { - audioStatus: myAudioStatus, - videoStatus: myVideoStatus, - }); - // some devices can't swap the video track, if already in execution. - await stopLocalVideoTrack(); - await stopLocalAudioTrack(); - const audioVideoConstraints = await getAudioVideoConstraints(); - navigator.mediaDevices.getUserMedia(audioVideoConstraints).then(gotStream).then(gotDevices).catch(handleError); +async function handleLocalCameraMirror() { + await setMyVideoStatusTrue(); + // This fix IPadPro - Tablet mirror of the back camera + if ((isMobileDevice || isIPadDevice || isTabletDevice) && !isCamMirrored) { + myVideo.classList.toggle('mirror'); + isCamMirrored = true; + } } /** @@ -4582,7 +4645,7 @@ async function getAudioVideoConstraints() { * @returns {object} video constraints */ async function getVideoConstraints(videoQuality) { - const frameRate = { max: videoMaxFrameRate }; + const frameRate = videoMaxFrameRate; switch (videoQuality) { case 'default': @@ -4657,10 +4720,10 @@ async function getAudioConstraints() { * @returns void */ async function refreshConstraints(stream, maxFrameRate) { - if (!useVideo || stream.getVideoTracks().length == 0) return; + if (!useVideo || !hasVideoTrack(stream)) return; stream .getVideoTracks()[0] - .applyConstraints({ frameRate: { max: maxFrameRate } }) + .applyConstraints({ frameRate: maxFrameRate }) .then(() => { logStreamSettingsInfo('refreshConstraints', stream); }) @@ -4675,10 +4738,10 @@ async function refreshConstraints(stream, maxFrameRate) { * @param {string} maxFrameRate desired max frame rate */ function setLocalMaxFps(maxFrameRate) { - if (!useVideo) return; + if (!useVideo || !localVideoMediaStream) return; localVideoMediaStream .getVideoTracks()[0] - .applyConstraints({ frameRate: { max: maxFrameRate } }) + .applyConstraints({ frameRate: maxFrameRate }) .then(() => { logStreamSettingsInfo('setLocalMaxFps', localVideoMediaStream); }) @@ -4692,7 +4755,7 @@ function setLocalMaxFps(maxFrameRate) { * Set local video quality: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/applyConstraints */ async function setLocalVideoQuality() { - if (!useVideo) return; + if (!useVideo || !localVideoMediaStream) return; let videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default'); localVideoMediaStream .getVideoTracks()[0] @@ -4741,95 +4804,6 @@ function attachSinkId(element, sinkId) { } } -/** - * Got Stream and append to local media - * @param {object} stream media stream audio - video - * @returns {object} media Devices Info - */ -async function gotStream(stream) { - const videoFPS = isScreenStreaming ? screenMaxFrameRate : videoMaxFrameRate; - await refreshConstraints(stream, videoFPS); - await refreshMyLocalStream(stream, true); - await refreshMyStreamToPeers(stream, true); - if (myVideoChange) { - setMyVideoStatusTrue(); - // This fix IPadPro - Tablet mirror of the back camera - if ((isMobileDevice || isIPadDevice || isTabletDevice) && !isCamMirrored) { - myVideo.classList.toggle('mirror'); - isCamMirrored = true; - } - } - // Refresh button list in case labels have become available - return navigator.mediaDevices.enumerateDevices(); -} - -/** - * Get audio-video Devices and show it to select box - * https://webrtc.github.io/samples/src/content/devices/input-output/ - * https://github.com/webrtc/samples/tree/gh-pages/src/content/devices/input-output - * @param {object} deviceInfos device infos - */ -function gotDevices(deviceInfos) { - // Handles being called several times to update labels. Preserve values. - const values = selectors.map((select) => select.value); - selectors.forEach((select) => { - while (select.firstChild) { - select.removeChild(select.firstChild); - } - }); - // check devices - for (let i = 0; i !== deviceInfos.length; ++i) { - const deviceInfo = deviceInfos[i]; - // console.log("device-info ------> ", deviceInfo); - const option = document.createElement('option'); - option.value = deviceInfo.deviceId; - - switch (deviceInfo.kind) { - case 'videoinput': - option.text = `📹 ` + deviceInfo.label || `📹 camera ${videoSelect.length + 1}`; - videoSelect.appendChild(option); - break; - - case 'audioinput': - option.text = `🎤 ` + deviceInfo.label || `🎤 microphone ${audioInputSelect.length + 1}`; - audioInputSelect.appendChild(option); - break; - - case 'audiooutput': - option.text = `🔈 ` + deviceInfo.label || `🔈 speaker ${audioOutputSelect.length + 1}`; - audioOutputSelect.appendChild(option); - break; - - default: - console.log('Some other kind of source/device: ', deviceInfo); - } - } // end for devices - - selectors.forEach((select, selectorIndex) => { - if (Array.prototype.slice.call(select.childNodes).some((n) => n.value === values[selectorIndex])) { - select.value = values[selectorIndex]; - } - }); -} - -/** - * Handle getUserMedia error: https://blog.addpipe.com/common-getusermedia-errors/ - * @param {object} err user media error - */ -function handleError(err) { - console.error('navigator.MediaDevices.getUserMedia error: ', err); - switch (err.name) { - case 'OverconstrainedError': - userLog( - 'error', - "GetUserMedia: Your device doesn't support the selected video quality or fps, please select the another one.", - ); - break; - default: - userLog('error', 'GetUserMedia error ' + err); - } -} - /** * AttachMediaStream stream to element * @param {object} element element to attach the stream @@ -5018,6 +4992,8 @@ function handleAudio(e, init, force = null) { const audioStatus = force !== null ? force : !localAudioMediaStream.getAudioTracks()[0].enabled; const audioClassName = audioStatus ? className.audioOn : className.audioOff; + myAudioStatus = audioStatus; + localAudioMediaStream.getAudioTracks()[0].enabled = audioStatus; force != null ? (e.className = audioClassName) : (e.target.className = audioClassName); @@ -5032,23 +5008,35 @@ function handleAudio(e, init, force = null) { lS.setInitConfig(lS.MEDIA_TYPE.audio, audioStatus); } - myAudioStatus = audioStatus; setMyAudioStatus(myAudioStatus); } +/** + * Stop audio track from MediaStream + * @param {MediaStream} stream + */ +async function stopAudioTracks(stream) { + if (!stream) return; + stream.getTracks().forEach((track) => { + if (track.kind === 'audio') track.stop(); + }); +} + /** * Handle Video ON - OFF * @param {object} e event * @param {boolean} init on join room * @param {null|boolean} force video off (default null can be true/false) */ -function handleVideo(e, init, force = null) { +async function handleVideo(e, init, force = null) { if (!useVideo) return; // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getVideoTracks const videoStatus = force !== null ? force : !localVideoMediaStream.getVideoTracks()[0].enabled; const videoClassName = videoStatus ? className.videoOn : className.videoOff; + myVideoStatus = videoStatus; + localVideoMediaStream.getVideoTracks()[0].enabled = videoStatus; force != null ? (e.className = videoClassName) : (e.target.className = videoClassName); @@ -5063,8 +5051,37 @@ function handleVideo(e, init, force = null) { lS.setInitConfig(lS.MEDIA_TYPE.video, videoStatus); } - myVideoStatus = videoStatus; - setMyVideoStatus(myVideoStatus); + if (!videoStatus) { + if (!isScreenStreaming) { + // Stop the video track based on the condition + if (init) { + await stopVideoTracks(initStream); // Stop init video track (camera LED off) + } else { + await stopVideoTracks(localVideoMediaStream); // Stop local video track (camera LED off) + } + } + } else { + if (init) { + // Resume the video track for the init camera (camera LED on) + await changeInitCamera(initVideoSelect.value); + } else if (!isScreenStreaming) { + // Resume the video track for the local camera (camera LED on) + await changeLocalCamera(videoSelect.value); + } + } + + setMyVideoStatus(videoStatus); +} + +/** + * Stop video track from MediaStream + * @param {MediaStream} stream + */ +async function stopVideoTracks(stream) { + if (!stream) return; + stream.getTracks().forEach((track) => { + if (track.kind === 'video') track.stop(); + }); } /** @@ -5132,10 +5149,10 @@ async function stopLocalAudioTrack() { async function toggleScreenSharing(init = false) { try { // Set screen frame rate - screenMaxFrameRate = parseInt(screenFpsSelect.value); + screenMaxFrameRate = parseInt(screenFpsSelect.value, 10); const constraints = { audio: false, - video: { frameRate: { max: screenMaxFrameRate } }, + video: { frameRate: screenMaxFrameRate }, }; // Store webcam video status before screen sharing @@ -5166,10 +5183,11 @@ async function toggleScreenSharing(init = false) { } else { emitPeersAction('screenStop'); adaptAspectRatio(); - await refreshConstraints(screenMediaPromise, videoMaxFrameRate); + //await refreshConstraints(screenMediaPromise, videoMaxFrameRate); } await emitPeerStatus('screen', myScreenStatus); + await stopLocalVideoTrack(); await refreshMyLocalStream(screenMediaPromise); await refreshMyStreamToPeers(screenMediaPromise); @@ -5398,21 +5416,23 @@ async function refreshMyLocalStream(stream, localAudioTrackChange = false) { // enable video if (useVideo || isScreenStreaming) stream.getVideoTracks()[0].enabled = true; - // enable audio - if (localAudioTrackChange && myAudioStatus === false) { + // enable audio if changed and disabled + if (localAudioTrackChange && !myAudioStatus) { audioBtn.className = className.audioOn; setMyAudioStatus(true); myAudioStatus = true; } const tracksToInclude = []; + const videoTrack = hasVideoTrack(stream) ? stream.getVideoTracks()[0] - : localVideoMediaStream && localVideoMediaStream.getVideoTracks()[0]; + : hasVideoTrack(localVideoMediaStream) && localVideoMediaStream.getVideoTracks()[0]; + const audioTrack = hasAudioTrack(stream) && localAudioTrackChange ? stream.getAudioTracks()[0] - : localAudioMediaStream && localAudioMediaStream.getAudioTracks()[0]; + : hasAudioTrack(localAudioMediaStream) && localAudioMediaStream.getAudioTracks()[0]; // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream if (useVideo || isScreenStreaming) { @@ -5440,21 +5460,19 @@ async function refreshMyLocalStream(stream, localAudioTrackChange = false) { } } - // refresh video privacy mode on screen sharing if (isScreenStreaming) { + // refresh video privacy mode on screen sharing isVideoPrivacyActive = false; setVideoPrivacyStatus('myVideo', isVideoPrivacyActive); - } - // adapt video object fit on screen streaming - getId('myVideo').style.objectFit = isScreenStreaming ? 'contain' : 'var(--video-object-fit)'; - - // on toggleScreenSharing video stop from popup bar - if (useVideo || isScreenStreaming) { + // on toggleScreenSharing video stop from popup bar stream.getVideoTracks()[0].onended = () => { toggleScreenSharing(); }; } + + // adapt video object fit on screen streaming + getId('myVideo').style.objectFit = isScreenStreaming ? 'contain' : 'var(--video-object-fit)'; } /** @@ -5612,11 +5630,11 @@ function startMobileRecording(options, audioMixerTracks) { */ function startDesktopRecording(options, audioMixerTracks) { // Get the desired frame rate for screen recording - screenMaxFrameRate = parseInt(screenFpsSelect.value); + screenMaxFrameRate = parseInt(screenFpsSelect.value, 10); // Define constraints for capturing the screen const constraints = { - video: { frameRate: { max: screenMaxFrameRate } }, + video: { frameRate: screenMaxFrameRate }, }; // Request access to screen capture using the specified constraints @@ -6915,6 +6933,7 @@ function setMyHandStatus() { * @param {boolean} status of my audio */ function setMyAudioStatus(status) { + console.log('My audio status', status); const audioClassName = status ? className.audioOn : className.audioOff; audioBtn.className = audioClassName; myAudioStatusIcon.className = audioClassName; @@ -6923,7 +6942,6 @@ function setMyAudioStatus(status) { setTippy(myAudioStatusIcon, status ? 'My audio is on' : 'My audio is off', 'bottom'); setTippy(audioBtn, status ? 'Stop the audio' : 'Start the audio', placement); status ? playSound('on') : playSound('off'); - console.log('My audio status', status); } /** @@ -6935,8 +6953,10 @@ function setMyVideoStatus(status) { // on vdeo OFF display my video avatar name if (myVideoAvatarImage) myVideoAvatarImage.style.display = status ? 'none' : 'block'; if (myVideoStatusIcon) myVideoStatusIcon.className = status ? className.videoOn : className.videoOff; + // send my video status to all peers in the room emitPeerStatus('video', status); + if (!isMobileDevice) { if (myVideoStatusIcon) setTippy(myVideoStatusIcon, status ? 'My video is on' : 'My video is off', 'bottom'); setTippy(videoBtn, status ? 'Stop the video' : 'Start the video', placement);