300300 fill : rgba (255 , 255 , 255 , 0.9 );
301301 }
302302
303+ .loading-spinner {
304+ width : 50px ;
305+ height : 50px ;
306+ border : 3px solid rgba (255 , 255 , 255 , 0.3 );
307+ border-top : 3px solid # fff ;
308+ border-radius : 50% ;
309+ animation : spin 1s linear infinite;
310+ display : none;
311+ position : absolute;
312+ top : 50% ;
313+ left : 50% ;
314+ margin : -25px 0 0 -25px ; /* Half of width/height for centering */
315+ z-index : 10 ;
316+ pointer-events : none;
317+ }
318+
319+ .loading-spinner .show {
320+ display : block;
321+ }
322+
323+ @keyframes spin {
324+ 0% { transform : rotate (0deg ); }
325+ 100% { transform : rotate (360deg ); }
326+ }
327+
303328 .video-controls {
304329 position : absolute;
305330 bottom : 0 ;
452477 box-shadow : 0 2px 4px rgba (0 , 0 , 0 , 0.5 );
453478 }
454479
480+ .browser-fullscreen-btn {
481+ background : none;
482+ border : none;
483+ color : # fff ;
484+ cursor : pointer;
485+ font-size : 17px ;
486+ padding : 0 !important ;
487+ margin : 0 ;
488+ width : 32px ;
489+ height : 32px ;
490+ display : flex;
491+ align-items : center;
492+ justify-content : center;
493+ box-shadow : none;
494+ outline : none;
495+ }
496+
497+ .browser-fullscreen-btn : hover ,
498+ .browser-fullscreen-btn : focus {
499+ background : none;
500+ border : none;
501+ color : # fff ;
502+ box-shadow : none;
503+ outline : none;
504+ transform : none;
505+ }
506+
507+ /* Web page fullscreen styles */
508+ body .webpage-fullscreen {
509+ padding : 0 ;
510+ overflow : hidden;
511+ }
512+
513+ body .webpage-fullscreen h1 ,
514+ body .webpage-fullscreen .controls {
515+ display : none;
516+ }
517+
518+ body .webpage-fullscreen # video-container {
519+ position : fixed;
520+ top : 0 ;
521+ left : 0 ;
522+ width : 100vw ;
523+ height : 100vh ;
524+ max-width : none;
525+ border-radius : 0 ;
526+ box-shadow : none;
527+ z-index : 1000 ;
528+ }
529+
455530 .fullscreen-btn {
456531 background : none;
457532 border : none;
@@ -545,6 +620,7 @@ <h1>Wayshot 屏幕共享</h1>
545620 < path d ="M8 5v14l11-7z " />
546621 </ svg >
547622 </ div >
623+ < div class ="loading-spinner " id ="loading-spinner "> </ div >
548624 < div class ="video-controls ">
549625 < button id ="play-btn "> ▶</ button >
550626 < div class ="time-display " id ="time-display "> 00:00</ div >
@@ -565,6 +641,7 @@ <h1>Wayshot 屏幕共享</h1>
565641 value ="100 "
566642 />
567643 </ div >
644+ < button class ="browser-fullscreen-btn " id ="browser-fullscreen-btn "> ⤢</ button >
568645 < button class ="fullscreen-btn " id ="fullscreen-btn "> ⛶</ button >
569646 </ div >
570647 </ div >
@@ -585,13 +662,63 @@ <h1>Wayshot 屏幕共享</h1>
585662 const volumeBtn = document . getElementById ( "volume-btn" ) ;
586663 const volumeIcon = document . getElementById ( "volume-icon" ) ;
587664 const volumeSlider = document . getElementById ( "volume-slider" ) ;
665+ const browserFullscreenBtn = document . getElementById ( "browser-fullscreen-btn" ) ;
588666 const fullscreenBtn = document . getElementById ( "fullscreen-btn" ) ;
589667 const playbackStateIndicator = document . getElementById (
590668 "playback-state-indicator" ,
591669 ) ;
670+ const loadingSpinner = document . getElementById ( "loading-spinner" ) ;
592671
593672 let authEnabled = false ;
594673 let hideControlsTimeout ;
674+ let currentPeerConnection = null ;
675+ let currentWhepClient = null ;
676+ let isConnecting = false ;
677+
678+ // Function to close existing connection
679+ async function closeExistingConnection ( ) {
680+ try {
681+ // Close WHEP client if exists (this will send HTTP DELETE)
682+ if ( currentWhepClient ) {
683+ await currentWhepClient . stop ( ) ;
684+ currentWhepClient = null ;
685+ }
686+
687+ // Close peer connection if exists (fallback cleanup)
688+ if ( currentPeerConnection ) {
689+ // Close all transceivers
690+ currentPeerConnection . getTransceivers ( ) . forEach ( transceiver => {
691+ if ( transceiver . stop ) {
692+ transceiver . stop ( ) ;
693+ }
694+ } ) ;
695+
696+ // Close peer connection
697+ currentPeerConnection . close ( ) ;
698+ currentPeerConnection = null ;
699+ }
700+
701+ // Clear video source and reset to black
702+ localVideo . srcObject = null ;
703+ localVideo . load ( ) ; // Reset video element to black
704+
705+ // Reset UI state
706+ playBtn . textContent = "▶" ;
707+ playbackStateIndicator . classList . remove ( "show" ) ;
708+ // Don't hide loading spinner here - let the caller handle it
709+ // loadingSpinner.classList.remove("show");
710+
711+ isConnecting = false ;
712+ } catch ( error ) {
713+ console . warn ( "Error closing connection:" , error ) ;
714+ // Ensure state is reset even if close fails
715+ currentWhepClient = null ;
716+ currentPeerConnection = null ;
717+ localVideo . srcObject = null ;
718+ localVideo . load ( ) ; // Reset video element to black
719+ isConnecting = false ;
720+ }
721+ }
595722
596723 // Switch toggle functionality
597724 authSwitch . addEventListener ( "click" , ( ) => {
@@ -693,6 +820,28 @@ <h1>Wayshot 屏幕共享</h1>
693820 }
694821 }
695822
823+ // Web page fullscreen functionality
824+ let isWebpageFullscreen = false ;
825+
826+ function toggleWebpageFullscreen ( ) {
827+ isWebpageFullscreen = ! isWebpageFullscreen ;
828+
829+ if ( isWebpageFullscreen ) {
830+ // Enter webpage fullscreen
831+ document . body . classList . add ( "webpage-fullscreen" ) ;
832+ browserFullscreenBtn . textContent = "⤢" ;
833+ } else {
834+ // Exit webpage fullscreen
835+ document . body . classList . remove ( "webpage-fullscreen" ) ;
836+ browserFullscreenBtn . textContent = "⤢" ;
837+ }
838+ }
839+
840+ browserFullscreenBtn . addEventListener ( "click" , ( e ) => {
841+ e . stopPropagation ( ) ; // Prevent event from bubbling up to video container
842+ toggleWebpageFullscreen ( ) ;
843+ } ) ;
844+
696845 fullscreenBtn . addEventListener ( "click" , ( e ) => {
697846 e . stopPropagation ( ) ; // Prevent event from bubbling up to video container
698847 toggleVideoFullscreen ( ) ;
@@ -747,36 +896,10 @@ <h1>Wayshot 屏幕共享</h1>
747896 }
748897 }
749898
750- // Listen for fullscreen changes
751- document . addEventListener ( "fullscreenchange" , ( ) => {
752- if ( document . fullscreenElement ) {
753- // Entering fullscreen
754- videoContainer . classList . add ( "fullscreen" ) ;
755- fullscreenBtn . textContent = "⛶" ;
756- isFullscreen = true ;
757- isMouseOverControls = false ;
758- // Initially hide controls in fullscreen
759- videoControls . classList . remove ( "show" ) ;
760- controlBarVisible = false ;
761- // Add mousemove listener for fullscreen controls
762- videoContainer . addEventListener (
763- "mousemove" ,
764- handleVideoContainerMouseMove ,
765- ) ;
766- } else {
767- // Exiting fullscreen
768- videoContainer . classList . remove ( "fullscreen" ) ;
769- fullscreenBtn . textContent = "⛶" ;
770- isFullscreen = false ;
771- isMouseOverControls = false ;
772- // Remove mousemove listener
773- videoContainer . removeEventListener (
774- "mousemove" ,
775- handleVideoContainerMouseMove ,
776- ) ;
777- // Ensure controls are visible in non-fullscreen mode
778- videoControls . classList . add ( "show" ) ;
779- controlBarVisible = true ;
899+ // Keyboard support for webpage fullscreen (Escape key)
900+ document . addEventListener ( "keydown" , ( e ) => {
901+ if ( e . key === "Escape" && isWebpageFullscreen ) {
902+ toggleWebpageFullscreen ( ) ;
780903 }
781904 } ) ;
782905
@@ -906,6 +1029,12 @@ <h1>Wayshot 屏幕共享</h1>
9061029 } ) ;
9071030
9081031 function updatePlaybackStateIndicator ( ) {
1032+ // Don't show playback indicator when loading spinner is showing
1033+ if ( loadingSpinner . classList . contains ( "show" ) ) {
1034+ playbackStateIndicator . classList . remove ( "show" ) ;
1035+ return ;
1036+ }
1037+
9091038 if ( localVideo . paused || localVideo . ended ) {
9101039 playbackStateIndicator . classList . add ( "show" ) ;
9111040 } else {
@@ -956,12 +1085,34 @@ <h1>Wayshot 屏幕共享</h1>
9561085 updatePlaybackStateIndicator ( ) ;
9571086 } ) ;
9581087
959- startWhepBtn . addEventListener ( "click" , ( ) => {
1088+ startWhepBtn . addEventListener ( "click" , async ( ) => {
9601089 const token = tokenInput . value ;
9611090 const useAuth = authEnabled ;
9621091
1092+ // Show loading spinner and clear video immediately when starting connection process
1093+ loadingSpinner . classList . add ( "show" ) ;
1094+ playbackStateIndicator . classList . remove ( "show" ) ;
1095+
1096+ // Clear video immediately to show black background
1097+ localVideo . srcObject = null ;
1098+ localVideo . load ( ) ;
1099+
1100+ // If already connecting or has existing connection, close it first
1101+ if ( isConnecting || currentPeerConnection || currentWhepClient ) {
1102+ await closeExistingConnection ( ) ;
1103+
1104+ // If we're in the middle of connecting, stop here but keep spinner showing
1105+ if ( isConnecting ) {
1106+ return ;
1107+ }
1108+ }
1109+
1110+ // Set connecting state
1111+ isConnecting = true ;
1112+
9631113 //Create peerconnection
964- const pc = ( window . pc = new RTCPeerConnection ( ) ) ;
1114+ const pc = new RTCPeerConnection ( ) ;
1115+ currentPeerConnection = pc ;
9651116
9661117 //Add recv only transceivers
9671118 pc . addTransceiver ( "audio" ) ;
@@ -981,23 +1132,50 @@ <h1>Wayshot 屏幕共享</h1>
9811132 player . muted = false ; // Enable audio
9821133 playBtn . textContent = "❚❚" ;
9831134
1135+ // Hide loading spinner when video starts
1136+ loadingSpinner . classList . remove ( "show" ) ;
1137+
1138+ isConnecting = false ;
1139+
9841140 // Auto-play when video stream starts
9851141 player . play ( ) . catch ( ( err ) => {
9861142 console . log ( "Auto-play failed:" , err ) ;
9871143 playBtn . textContent = "▶" ;
1144+ // Hide loading spinner even if auto-play fails
1145+ loadingSpinner . classList . remove ( "show" ) ;
9881146 } ) ;
9891147 }
9901148 if ( event . track . kind == "audio" ) {
9911149 console . log ( "Audio track received" ) ;
9921150 }
9931151 } ;
1152+
1153+ // Handle connection failure
1154+ pc . onconnectionstatechange = ( ) => {
1155+ console . log ( "Connection state:" , pc . connectionState ) ;
1156+ if ( pc . connectionState === "failed" || pc . connectionState === "disconnected" ) {
1157+ loadingSpinner . classList . remove ( "show" ) ;
1158+ playBtn . textContent = "▶" ;
1159+ isConnecting = false ;
1160+ currentPeerConnection = null ;
1161+ }
1162+ } ;
1163+
9941164 //Create whep client
9951165 const whep = new WHEPClient ( ) ;
1166+ currentWhepClient = whep ;
9961167
9971168 let url = location . origin + "/whep" ;
9981169
9991170 //Start viewing
1000- whep . view ( pc , url , useAuth ? token : null ) ;
1171+ whep . view ( pc , url , useAuth ? token : null ) . catch ( ( err ) => {
1172+ console . error ( "WHEP connection failed:" , err ) ;
1173+ loadingSpinner . classList . remove ( "show" ) ;
1174+ playBtn . textContent = "▶" ;
1175+ isConnecting = false ;
1176+ currentPeerConnection = null ;
1177+ currentWhepClient = null ;
1178+ } ) ;
10011179 } ) ;
10021180
10031181 // Initialize volume
0 commit comments