@@ -160,6 +160,7 @@ export default function LiveClassRoom() {
160160 const [ studentCamOn , setStudentCamOn ] = useState ( false ) ;
161161 const [ streamActive , setStreamActive ] = useState ( false ) ;
162162 const [ teacherHasScreen , setTeacherHasScreen ] = useState ( false ) ;
163+ const [ audioBlocked , setAudioBlocked ] = useState ( false ) ;
163164
164165 // ── student tiles (teacher's view of all connected students) ────────────────
165166 // Map<viewerSocketId, { stream: MediaStream|null, userId, userName, camOn: bool }>
@@ -171,11 +172,13 @@ export default function LiveClassRoom() {
171172 const studentMicStreamRef = useRef ( null ) ; // student: mic stream
172173 const studentCamStreamRef = useRef ( null ) ; // student: camera stream
173174 const pendingScreenRef = useRef ( null ) ; // student: pending screen stream before mount
175+ const screenStreamIdRef = useRef ( null ) ; // student: expected screen stream.id from teacher
174176
175- // ── video element refs ──────── ──────────────────────────────────────────────
177+ // ── video / audio element refs ──────────────────────────────────────────────
176178 const localCameraRef = useRef ( null ) ; // teacher: self camera preview
177179 const localScreenRef = useRef ( null ) ; // teacher: self screen preview
178- const remoteCameraRef = useRef ( null ) ; // student: teacher's camera
180+ const remoteCameraRef = useRef ( null ) ; // student: teacher's camera (video only)
181+ const remoteAudioRef = useRef ( null ) ; // student: teacher's audio (dedicated element)
179182 const remoteScreenRef = useRef ( null ) ; // student: teacher's screen
180183 const studentSelfVideoRef = useRef ( null ) ; // student: self camera preview
181184
@@ -355,7 +358,10 @@ export default function LiveClassRoom() {
355358 const sender = pc . addTrack ( st , stream ) ;
356359 screenSendersRef . current . set ( vid , sender ) ;
357360 } ) ;
358- socket . emit ( "screen-share-started" , { liveClassId : id } ) ;
361+ socket . emit ( "screen-share-started" , {
362+ liveClassId : id ,
363+ screenStreamId : stream . id ,
364+ } ) ;
359365 st . onended = stopScreenShare ;
360366 } catch {
361367 /* user cancelled */
@@ -715,26 +721,52 @@ export default function LiveClassRoom() {
715721 const pc = new RTCPeerConnection ( ICE_CONFIG ) ;
716722 peerConnRef . current = pc ;
717723
718- pc . ontrack = ( { track } ) => {
724+ pc . ontrack = ( { track, streams } ) => {
719725 if ( track . kind === "audio" ) {
720- const s = remoteCameraRef . current ?. srcObject ?? new MediaStream ( ) ;
721- s . addTrack ( track ) ;
722- if ( remoteCameraRef . current ) remoteCameraRef . current . srcObject = s ;
726+ // Dedicated audio element — avoids autoplay blocking on the video element
727+ const audioEl = remoteAudioRef . current ;
728+ if ( audioEl ) {
729+ const s =
730+ audioEl . srcObject instanceof MediaStream
731+ ? audioEl . srcObject
732+ : new MediaStream ( ) ;
733+ if ( ! s . getAudioTracks ( ) . find ( ( t ) => t . id === track . id ) ) {
734+ s . addTrack ( track ) ;
735+ audioEl . srcObject = s ;
736+ audioEl . play ( ) . catch ( ( ) => setAudioBlocked ( true ) ) ;
737+ }
738+ }
723739 return ;
724740 }
725- if ( track . contentHint === "detail" ) {
726- const stream = new MediaStream ( [ track ] ) ;
727- pendingScreenRef . current = stream ;
741+ // Video: distinguish screen vs camera by stream ID
742+ // (contentHint is a local hint and is NOT transmitted over WebRTC)
743+ const incomingStreamId = streams [ 0 ] ?. id ;
744+ const isScreenTrack =
745+ incomingStreamId &&
746+ screenStreamIdRef . current &&
747+ incomingStreamId === screenStreamIdRef . current ;
748+
749+ if ( isScreenTrack ) {
750+ const screenStream = streams [ 0 ] ?? new MediaStream ( [ track ] ) ;
751+ pendingScreenRef . current = screenStream ;
728752 setTeacherHasScreen ( true ) ;
729753 setStreamActive ( true ) ;
730754 track . onended = ( ) => {
731755 setTeacherHasScreen ( false ) ;
732756 pendingScreenRef . current = null ;
757+ if ( remoteScreenRef . current )
758+ remoteScreenRef . current . srcObject = null ;
733759 } ;
734760 } else {
735- const s = remoteCameraRef . current ?. srcObject ?? new MediaStream ( ) ;
736- s . addTrack ( track ) ;
737- if ( remoteCameraRef . current ) remoteCameraRef . current . srcObject = s ;
761+ // Camera video track
762+ const s =
763+ remoteCameraRef . current ?. srcObject instanceof MediaStream
764+ ? remoteCameraRef . current . srcObject
765+ : new MediaStream ( ) ;
766+ if ( ! s . getVideoTracks ( ) . find ( ( t ) => t . id === track . id ) ) {
767+ s . addTrack ( track ) ;
768+ if ( remoteCameraRef . current ) remoteCameraRef . current . srcObject = s ;
769+ }
738770 setStreamActive ( true ) ;
739771 }
740772 } ;
@@ -839,13 +871,21 @@ export default function LiveClassRoom() {
839871 const onBroadcasterLeft = ( ) => {
840872 setStreamActive ( false ) ;
841873 setTeacherHasScreen ( false ) ;
874+ screenStreamIdRef . current = null ;
875+ pendingScreenRef . current = null ;
842876 if ( remoteCameraRef . current ) remoteCameraRef . current . srcObject = null ;
877+ if ( remoteAudioRef . current ) remoteAudioRef . current . srcObject = null ;
878+ if ( remoteScreenRef . current ) remoteScreenRef . current . srcObject = null ;
843879 peerConnRef . current ?. close ( ) ;
844880 peerConnRef . current = null ;
845881 } ;
846882
847- const onScreenStarted = ( ) => setTeacherHasScreen ( true ) ;
883+ const onScreenStarted = ( { screenStreamId } ) => {
884+ if ( screenStreamId ) screenStreamIdRef . current = screenStreamId ;
885+ setTeacherHasScreen ( true ) ;
886+ } ;
848887 const onScreenStopped = ( ) => {
888+ screenStreamIdRef . current = null ;
849889 setTeacherHasScreen ( false ) ;
850890 if ( remoteScreenRef . current ) remoteScreenRef . current . srcObject = null ;
851891 pendingScreenRef . current = null ;
@@ -1086,12 +1126,23 @@ export default function LiveClassRoom() {
10861126 />
10871127 ) }
10881128
1129+ { /* Student: dedicated hidden audio element for teacher voice */ }
1130+ { ! isTeacher && (
1131+ < audio
1132+ ref = { remoteAudioRef }
1133+ autoPlay
1134+ playsInline
1135+ className = "hidden"
1136+ />
1137+ ) }
1138+
10891139 { /* Student: teacher's camera */ }
10901140 { ! isTeacher && (
10911141 < video
10921142 ref = { remoteCameraRef }
10931143 autoPlay
10941144 playsInline
1145+ muted
10951146 className = { `object-cover
10961147 ${
10971148 teacherHasScreen
@@ -1101,6 +1152,21 @@ export default function LiveClassRoom() {
11011152 />
11021153 ) }
11031154
1155+ { /* Student: audio blocked prompt */ }
1156+ { ! isTeacher && audioBlocked && (
1157+ < button
1158+ onClick = { ( ) => {
1159+ remoteAudioRef . current
1160+ ?. play ( )
1161+ . then ( ( ) => setAudioBlocked ( false ) )
1162+ . catch ( ( ) => { } ) ;
1163+ } }
1164+ className = "absolute top-4 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-4 py-2.5 rounded-xl bg-amber-500/20 text-amber-300 border border-amber-500/30 text-xs font-bold cursor-pointer hover:bg-amber-500/30 transition-colors animate-pulse"
1165+ >
1166+ 🔇 Click to enable audio
1167+ </ button >
1168+ ) }
1169+
11041170 { /* Student: self-view preview (own camera on) */ }
11051171 { ! isTeacher && studentCamOn && (
11061172 < div className = "absolute bottom-4 left-4 z-20" >
0 commit comments