99 * @license For private project or commercial purposes contact us at: license.mirotalk@gmail.com or purchase it directly via Code Canyon:
1010 * @license https://codecanyon.net/item/mirotalk-c2c-webrtc-real-time-cam-2-cam-video-conferences-and-screen-sharing/43383005
1111 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com
12- * @version 1.4.71
12+ * @version 1.4.72
1313 */
1414
1515const savedTheme = window . localStorage . getItem ( 'home-theme' ) ;
@@ -215,6 +215,8 @@ let cameraEffects = null;
215215// Devices actually in use, to restore the selects when a swap fails or a fallback device was opened
216216let activeVideoDeviceId = null ;
217217let activeAudioDeviceId = null ;
218+ let cameraAvailable = true ;
219+ let microphoneAvailable = true ;
218220let backgroundImage = null ;
219221let backgroundEffectsBusy = false ;
220222let localMediaStream = null ;
@@ -669,6 +671,8 @@ async function setupLocalMedia(callback, errorBack) {
669671 let stream = await getBestUserMedia ( constraints ) ;
670672 activeVideoDeviceId = getStreamDeviceId ( stream , 'video' ) || null ;
671673 activeAudioDeviceId = getStreamDeviceId ( stream , 'audio' ) || null ;
674+ cameraAvailable = hasVideoTrack ( stream ) ;
675+ microphoneAvailable = hasAudioTrack ( stream ) ;
672676
673677 // Apply noise suppression to the stream if enabled
674678 const originalStream = stream ;
@@ -698,11 +702,12 @@ async function setupLocalMedia(callback, errorBack) {
698702async function enumerateDevices ( ) {
699703 try {
700704 const devices = await navigator . mediaDevices . enumerateDevices ( ) ;
701- const videoDevices = devices . filter ( ( device ) => device . kind === 'videoinput' && device . deviceId !== 'default' ) ;
702- const audioDevices = devices . filter ( ( device ) => device . kind === 'audioinput' && device . deviceId !== 'default' ) ;
703- const speakerDevices = devices . filter (
704- ( device ) => device . kind === 'audiooutput' && device . deviceId !== 'default'
705- ) ;
705+ // Without permission the browser lists devices with an empty id: they cannot be used
706+ const usable = ( kind ) =>
707+ devices . filter ( ( device ) => device . kind === kind && device . deviceId && device . deviceId !== 'default' ) ;
708+ const videoDevices = usable ( 'videoinput' ) ;
709+ const audioDevices = usable ( 'audioinput' ) ;
710+ const speakerDevices = usable ( 'audiooutput' ) ;
706711 removeAllChildren ( audioSource ) ;
707712 removeAllChildren ( videoSource ) ;
708713 removeAllChildren ( audioOutputSource ) ;
@@ -723,7 +728,11 @@ async function enumerateDevices() {
723728 await addChild ( audioOutputSource , device ) ;
724729 LS . DEVICES_COUNT . speaker ++ ;
725730 }
726- elemDisplay ( audioOutputDiv , LS . DEVICES_COUNT . speaker > 0 ) ;
731+ elemDisplay ( audioOutputDiv , true ) ;
732+ if ( LS . DEVICES_COUNT . speaker === 0 ) {
733+ showNoDevice ( audioOutputSource , 'No speaker' ) ;
734+ elemDisable ( testSpeakerBtn , true ) ;
735+ }
727736 } else {
728737 elemDisplay ( audioOutputDiv , false ) ;
729738 }
@@ -990,9 +999,12 @@ function setRemoteMedia(stream, peers, peerId) {
990999 remoteVideoElem = createVideoElement ( peerId + '_remoteVideo' , stream , false ) ;
9911000 remoteVideoElem . style . display = 'block' ;
9921001 remoteVideoAvatarImage . style . display = 'none' ;
993- const remoteVideoSpinner = createVideoSpinner ( ) ;
994- remoteVideoWrap . appendChild ( remoteVideoSpinner ) ;
995- remoteVideoElem . addEventListener ( 'playing' , ( ) => remoteVideoSpinner . remove ( ) , { once : true } ) ;
1002+ // A peer without an active camera sends a disabled placeholder track: nothing to wait for
1003+ if ( peerVideo || peerScreen ) {
1004+ const remoteVideoSpinner = createVideoSpinner ( ) ;
1005+ remoteVideoWrap . appendChild ( remoteVideoSpinner ) ;
1006+ remoteVideoElem . addEventListener ( 'playing' , ( ) => remoteVideoSpinner . remove ( ) , { once : true } ) ;
1007+ }
9961008 }
9971009
9981010 // Audio element (if audio track)
@@ -1434,14 +1446,14 @@ function toggleSettings() {
14341446
14351447function updateBackgroundControls ( ) {
14361448 const imageMode = backgroundEffectSelect . value === 'image' ;
1437- backgroundEffectSelect . disabled = backgroundEffectsBusy || isScreenStreaming ;
1438- backgroundImageBtn . disabled = backgroundEffectsBusy || isScreenStreaming ;
1449+ backgroundEffectSelect . disabled = backgroundEffectsBusy || isScreenStreaming || ! cameraAvailable ;
1450+ backgroundImageBtn . disabled = backgroundEffectsBusy || isScreenStreaming || ! cameraAvailable ;
14391451 backgroundImageBtn . hidden = ! imageMode ;
14401452 backgroundImageName . hidden = ! imageMode || ! backgroundImage ;
14411453 backgroundEffectLoading . hidden = ! backgroundEffectsBusy ;
14421454 backgroundEffectsSection . setAttribute ( 'aria-busy' , String ( backgroundEffectsBusy ) ) ;
1443- videoSource . disabled = backgroundEffectsBusy || isScreenStreaming ;
1444- swapCameraBtn . disabled = backgroundEffectsBusy || isScreenStreaming ;
1455+ videoSource . disabled = backgroundEffectsBusy || isScreenStreaming || ! cameraAvailable ;
1456+ swapCameraBtn . disabled = backgroundEffectsBusy || isScreenStreaming || ! cameraAvailable ;
14451457 screenShareBtn . disabled = backgroundEffectsBusy ;
14461458 initScreenShareBtn . disabled = backgroundEffectsBusy ;
14471459}
@@ -1758,8 +1770,8 @@ async function playTestSound() {
17581770function getVideoConstraints ( deviceId = false ) {
17591771 let videoConstraints = true ;
17601772
1761- elemDisable ( videoQualitySelect , localStorageConfig . video . settings . best_quality ) ;
1762- elemDisable ( videoFpsSelect , localStorageConfig . video . settings . best_quality ) ;
1773+ elemDisable ( videoQualitySelect , localStorageConfig . video . settings . best_quality || ! cameraAvailable ) ;
1774+ elemDisable ( videoFpsSelect , localStorageConfig . video . settings . best_quality || ! cameraAvailable ) ;
17631775
17641776 if ( localStorageConfig . video . settings . best_quality ) {
17651777 resetVideoConstraints ( ) ;
@@ -1872,6 +1884,36 @@ function restoreDeviceSelect(select, deviceId, setLocalStorageConfig) {
18721884function syncDeviceSelects ( ) {
18731885 selectDevice ( videoSource , activeVideoDeviceId ) ;
18741886 selectDevice ( audioSource , activeAudioDeviceId ) ;
1887+ if ( ! cameraAvailable ) disableCameraControls ( ) ;
1888+ if ( ! microphoneAvailable ) disableMicrophoneControls ( ) ;
1889+ }
1890+
1891+ /**
1892+ * Show a disabled placeholder in the select (the saved device preference is left untouched)
1893+ * @param {HTMLSelectElement } select
1894+ * @param {string } label
1895+ */
1896+ function showNoDevice ( select , label ) {
1897+ const placeholder = new Option ( label , '' ) ;
1898+ select . add ( placeholder ) ;
1899+ select . selectedIndex = placeholder . index ;
1900+ select . disabled = true ;
1901+ }
1902+
1903+ // Without a microphone there is nothing to configure
1904+ function disableMicrophoneControls ( ) {
1905+ showNoDevice ( audioSource , 'No microphone' ) ;
1906+ elemDisable ( switchNoiseSuppression , true ) ;
1907+ }
1908+
1909+ // Without a camera there is nothing to configure
1910+ function disableCameraControls ( ) {
1911+ elemDisable ( videoQualitySelect , true ) ;
1912+ elemDisable ( videoFpsSelect , true ) ;
1913+ elemDisable ( switchMaxVideoQuality , true ) ;
1914+ showNoDevice ( videoSource , 'No camera' ) ;
1915+ backgroundEffectSelect . value = 'off' ;
1916+ updateBackgroundControls ( ) ;
18751917}
18761918
18771919function saveLocalStorageConfig ( ) {
@@ -1886,7 +1928,7 @@ function loadLocalStorageConfig() {
18861928 videoQualitySelect . selectedIndex = localStorageConfig . video . settings . quality_index ;
18871929 videoFpsSelect . selectedIndex = localStorageConfig . video . settings . fps_index ;
18881930 audioSource . selectedIndex = localStorageConfig . audio . devices . select . index ;
1889- if ( isAudioOutputSupported ( ) && audioOutputSource . options . length > 0 ) {
1931+ if ( isAudioOutputSupported ( ) && audioOutputSource . options . length > 0 && ! audioOutputSource . disabled ) {
18901932 audioOutputSource . selectedIndex = localStorageConfig ?. speaker ?. devices ?. select ?. index ?? 0 ;
18911933 }
18921934 switchNoiseSuppression . checked = localStorageConfig ?. audio ?. settings ?. noise_suppression ?? false ;
@@ -1965,7 +2007,7 @@ function setLocalVideoStatus(active = true, e = false) {
19652007 emitPeerStatus ( 'video' , active ) ;
19662008 localStorageConfig . video . init . active = active ;
19672009 saveLocalStorageConfig ( ) ;
1968- elemDisable ( videoQualitySelect , ! active || localStorageConfig . video . settings . best_quality ) ;
2010+ elemDisable ( videoQualitySelect , ! active || localStorageConfig . video . settings . best_quality || ! cameraAvailable ) ;
19692011}
19702012
19712013function setLocalScreenStatus ( active = false , e = false ) {
@@ -2581,6 +2623,11 @@ function updatePeerMediaDisplay(peerId, active, type) {
25812623 const peerVideo = document . getElementById ( peerId + '_remoteVideo' ) ;
25822624 const peerVideoAvatarImage = document . getElementById ( peerId + '_remoteVideoAvatar' ) ;
25832625 if ( peerVideoAvatarImage ) elemDisplay ( peerVideoAvatarImage , ! active ) ;
2626+ if ( ! active )
2627+ document
2628+ . getElementById ( peerId + '_remoteVideoWrap' )
2629+ ?. querySelector ( '.video-spinner' )
2630+ ?. remove ( ) ;
25842631 if ( peerVideo ) {
25852632 peerVideo . style . display = active ? 'block' : 'none' ;
25862633 peerVideo . style . objectFit =
0 commit comments