@@ -22,11 +22,93 @@ import { useSocket } from '@/sockets/SocketContext'
2222import { SocketClock } from '@/util/socketClock'
2323import { setClock } from '@/composables/useVideoPipeline'
2424
25+ // Check for required browser APIs
26+ function checkBrowserCompatibility ( ) {
27+ const missingApis : string [ ] = [ ]
28+ const missingDetails : { api : string ; link ?: string ; message ?: string } [ ] = [ ]
29+ const criticalMissing : string [ ] = [ ]
30+ const mediaStreamTrackProcessorMissing = ! ( 'MediaStreamTrackProcessor' in window )
31+
32+ try {
33+ // Check for WebTransport
34+ if ( ! ( 'WebTransport' in window ) ) {
35+ missingApis . push ( 'WebTransport' )
36+ missingDetails . push ( {
37+ api : 'WebTransport' ,
38+ } )
39+ criticalMissing . push ( 'WebTransport' )
40+ }
41+
42+ // Check for WebCodecs
43+ if ( ! ( 'VideoEncoder' in window ) || ! ( 'VideoDecoder' in window ) ) {
44+ missingApis . push ( 'WebCodecs' )
45+ criticalMissing . push ( 'WebCodecs' )
46+ missingDetails . push ( { api : 'WebCodecs' } )
47+ }
48+
49+ // Check for MediaStreamTrackProcessor (non-critical)
50+ if ( mediaStreamTrackProcessorMissing ) {
51+ missingApis . push ( 'MediaStreamTrackProcessor' )
52+ missingDetails . push ( {
53+ api : 'MediaStreamTrackProcessor' ,
54+ link : 'https://caniuse.com/?search=MediaStreamTrackProcessor' ,
55+ message : 'Video publishing will not be available' ,
56+ } )
57+ }
58+
59+ // Check for AudioWorklet - more robust check
60+ try {
61+ if ( ! ( 'AudioContext' in window ) && ! ( 'webkitAudioContext' in window ) ) {
62+ missingApis . push ( 'AudioContext' )
63+ criticalMissing . push ( 'AudioContext' )
64+ missingDetails . push ( { api : 'AudioContext' } )
65+ } else {
66+ const AudioCtx = window . AudioContext || ( window as any ) . webkitAudioContext
67+ if ( ! AudioCtx || ! AudioCtx . prototype || ! ( 'audioWorklet' in AudioCtx . prototype ) ) {
68+ missingApis . push ( 'AudioWorklet' )
69+ criticalMissing . push ( 'AudioWorklet' )
70+ missingDetails . push ( { api : 'AudioWorklet' } )
71+ }
72+ }
73+ } catch ( e ) {
74+ missingApis . push ( 'AudioWorklet' )
75+ criticalMissing . push ( 'AudioWorklet' )
76+ missingDetails . push ( { api : 'AudioWorklet' } )
77+ }
78+
79+ // Check for ReadableStream
80+ if ( ! ( 'ReadableStream' in window ) ) {
81+ missingApis . push ( 'ReadableStream' )
82+ criticalMissing . push ( 'ReadableStream' )
83+ missingDetails . push ( { api : 'ReadableStream' } )
84+ }
85+ } catch ( error ) {
86+ console . error ( 'Error checking browser compatibility:' , error )
87+ // If we can't check, assume incompatible
88+ missingApis . push ( 'Browser compatibility check failed' )
89+ criticalMissing . push ( 'Browser compatibility check failed' )
90+ missingDetails . push ( { api : 'Browser compatibility check failed' } )
91+ }
92+
93+ return {
94+ missingApis,
95+ missingDetails,
96+ criticalMissing,
97+ mediaStreamTrackProcessorMissing,
98+ }
99+ }
100+
25101export default function JoinPage ( ) {
26102 const [ username , setUsername ] = useState ( '' )
27103 const [ roomName , setRoomName ] = useState ( '' )
28104 const [ error , setError ] = useState ( '' )
29105 const [ connecting , setConnecting ] = useState ( false )
106+ const [ compatibilityError , setCompatibilityError ] = useState < string | null > ( null )
107+ const [ compatibilityDetails , setCompatibilityDetails ] = useState < { api : string ; link ?: string ; message ?: string } [ ] > (
108+ [ ] ,
109+ )
110+ const [ hasCriticalMissing , setHasCriticalMissing ] = useState ( false )
111+ const [ hasMediaStreamTrackProcessorMissing , setHasMediaStreamTrackProcessorMissing ] = useState ( false )
30112 const [ roomLimits , setRoomLimits ] = useState ( {
31113 maxRooms : 5 ,
32114 maxUsersPerRoom : 6 ,
@@ -36,10 +118,42 @@ export default function JoinPage() {
36118 const { setSession } = useSession ( )
37119 const { socket : contextSocket , reconnect } = useSocket ( )
38120
121+ // Check browser compatibility on component mount
39122 useEffect ( ( ) => {
40- if ( ! contextSocket || ! contextSocket . connected ) {
41- console . log ( 'WebSocket not connected on page load, reconnecting...' )
42- reconnect ( )
123+ try {
124+ const { missingApis, missingDetails, criticalMissing, mediaStreamTrackProcessorMissing } =
125+ checkBrowserCompatibility ( )
126+
127+ if ( criticalMissing . length > 0 ) {
128+ // Critical APIs missing - block joining and redirect to wiki
129+ setHasCriticalMissing ( true )
130+ setHasMediaStreamTrackProcessorMissing ( mediaStreamTrackProcessorMissing )
131+ setCompatibilityError (
132+ `Your browser is missing critical APIs required for MOQtail: ${ criticalMissing . join ( ', ' ) } .` ,
133+ )
134+ setCompatibilityDetails ( missingDetails )
135+ } else if ( mediaStreamTrackProcessorMissing ) {
136+ // Only MediaStreamTrackProcessor missing - allow joining with warning
137+ setHasCriticalMissing ( false )
138+ setHasMediaStreamTrackProcessorMissing ( true )
139+ setCompatibilityError (
140+ 'MediaStreamTrackProcessor is not available. You can join the session but video publishing will be limited.' ,
141+ )
142+ setCompatibilityDetails ( missingDetails )
143+ } else if ( missingApis . length > 0 ) {
144+ // Other non-critical APIs missing
145+ setHasMediaStreamTrackProcessorMissing ( false )
146+ setCompatibilityError ( `Some APIs are not fully supported: ${ missingApis . join ( ', ' ) } .` )
147+ setCompatibilityDetails ( missingDetails )
148+ } else {
149+ // No missing APIs
150+ setHasMediaStreamTrackProcessorMissing ( false )
151+ }
152+ } catch ( error ) {
153+ console . error ( 'Error during compatibility check:' , error )
154+ setHasCriticalMissing ( true )
155+ setCompatibilityError ( 'Unable to verify browser compatibility. Please use a recent version of Google Chrome.' )
156+ setCompatibilityDetails ( [ ] )
43157 }
44158 } , [ ] )
45159
@@ -77,6 +191,7 @@ export default function JoinPage() {
77191 response . roomState ,
78192 response . sessionDurationMinutes ,
79193 response . rewindFetchGroupSize ,
194+ hasMediaStreamTrackProcessorMissing ,
80195 )
81196 console . log (
82197 'Navigating to /session' ,
@@ -106,6 +221,14 @@ export default function JoinPage() {
106221 e . preventDefault ( )
107222 setError ( '' )
108223
224+ // Check for critical compatibility errors first
225+ if ( hasCriticalMissing ) {
226+ // Redirect to wiki for critical missing APIs
227+ window . open ( 'https://github.com/moqtail/demo-timetravel/wiki' , '_blank' )
228+ setError ( 'Redirecting to setup guide for browser compatibility information.' )
229+ return
230+ }
231+
109232 const trimmedUsername = username . trim ( )
110233 const trimmedRoomName = roomName . trim ( )
111234
@@ -139,6 +262,51 @@ export default function JoinPage() {
139262 < b > MOQtail Demo</ b >
140263 </ h1 >
141264 < h2 > Join a Room</ h2 >
265+ { compatibilityError && (
266+ < div className = "compatibility-error" >
267+ < strong > ⚠️ Browser Compatibility Issue</ strong >
268+ < p > { compatibilityError } </ p >
269+ { compatibilityDetails . length > 0 && (
270+ < div className = "compatibility-details" >
271+ < p >
272+ < strong > Missing APIs:</ strong >
273+ </ p >
274+ < ul >
275+ { compatibilityDetails . map ( ( detail , index ) => (
276+ < li key = { index } >
277+ < strong > { detail . api } </ strong >
278+ { detail . message && < div className = "compatibility-message" > { detail . message } </ div > }
279+ { detail . link && (
280+ < div >
281+ < a
282+ href = { detail . link }
283+ target = "_blank"
284+ rel = "noopener noreferrer"
285+ className = "compatibility-link"
286+ >
287+ → Browser Support Info
288+ </ a >
289+ </ div >
290+ ) }
291+ </ li >
292+ ) ) }
293+ </ ul >
294+ </ div >
295+ ) }
296+ { hasCriticalMissing && (
297+ < div className = "wiki-link-section" >
298+ < a
299+ href = "https://github.com/moqtail/demo-timetravel/wiki"
300+ target = "_blank"
301+ rel = "noopener noreferrer"
302+ className = "wiki-link-button"
303+ >
304+ 📖 View Setup Guide & Browser Requirements
305+ </ a >
306+ </ div >
307+ ) }
308+ </ div >
309+ ) }
142310 < div className = "browser-compatibility" >
143311 Use a recent version of Google Chrome that supports the WebCodecs and WebTransport APIs.
144312 </ div >
@@ -157,7 +325,7 @@ export default function JoinPage() {
157325 role = "textbox"
158326 inputMode = "text"
159327 className = "join-input"
160- disabled = { connecting }
328+ disabled = { connecting || hasCriticalMissing }
161329 maxLength = { 30 }
162330 />
163331 < input
@@ -174,11 +342,11 @@ export default function JoinPage() {
174342 role = "textbox"
175343 inputMode = "text"
176344 className = "join-input"
177- disabled = { connecting }
345+ disabled = { connecting || hasCriticalMissing }
178346 maxLength = { 20 }
179347 />
180- < button className = "join-button" disabled = { connecting } >
181- { connecting ? 'Connecting...' : 'Join' }
348+ < button className = "join-button" disabled = { connecting || hasCriticalMissing } >
349+ { hasCriticalMissing ? 'Browser Not Compatible - Check Wiki' : connecting ? 'Connecting...' : 'Join' }
182350 </ button >
183351 </ form >
184352 < div className = "privacy-notice" >
@@ -293,6 +461,85 @@ export default function JoinPage() {
293461 margin-top: 1rem;
294462 font-weight: 600;
295463 }
464+ .compatibility-error {
465+ background-color: #fdf2f2;
466+ border: 2px solid #f8d7da;
467+ color: #721c24;
468+ padding: 1rem;
469+ border-radius: 6px;
470+ margin-bottom: 1rem;
471+ text-align: left;
472+ }
473+ .compatibility-error strong {
474+ display: block;
475+ margin-bottom: 0.5rem;
476+ font-size: 1rem;
477+ }
478+ .compatibility-error p {
479+ margin: 0;
480+ font-size: 0.9rem;
481+ line-height: 1.4;
482+ }
483+ .compatibility-details {
484+ margin-top: 0.75rem;
485+ }
486+ .compatibility-details p {
487+ margin-bottom: 0.5rem;
488+ font-weight: 600;
489+ }
490+ .compatibility-details ul {
491+ margin: 0;
492+ padding-left: 1.2rem;
493+ list-style-type: disc;
494+ }
495+ .compatibility-details li {
496+ margin-bottom: 0.3rem;
497+ font-size: 0.85rem;
498+ line-height: 1.3;
499+ }
500+ .compatibility-link {
501+ color: #577B9F;
502+ text-decoration: underline;
503+ font-weight: 500;
504+ transition: color 0.2s;
505+ }
506+ .compatibility-link:hover {
507+ color: #D74401;
508+ }
509+ .wiki-link-section {
510+ margin-top: 1rem;
511+ text-align: center;
512+ }
513+ .wiki-link-button {
514+ display: inline-block;
515+ background-color: #577B9F;
516+ color: white;
517+ padding: 0.8rem 1.2rem;
518+ border-radius: 6px;
519+ text-decoration: none;
520+ font-weight: 600;
521+ font-size: 0.9rem;
522+ transition: background-color 0.2s;
523+ }
524+ .wiki-link-button:hover {
525+ background-color: #D74401;
526+ color: white;
527+ text-decoration: none;
528+ }
529+ .join-input:disabled {
530+ background-color: #f5f5f5;
531+ color: #999;
532+ cursor: not-allowed;
533+ opacity: 0.6;
534+ }
535+ .join-button:disabled {
536+ background-color: #bdc3c7;
537+ cursor: not-allowed;
538+ opacity: 0.7;
539+ }
540+ .join-button:disabled:hover {
541+ background-color: #bdc3c7;
542+ }
296543 .privacy-notice {
297544 font-size: 0.75rem;
298545 color: #7f8c8d;
0 commit comments