Skip to content

Commit 2dc86ca

Browse files
authored
feat: browser compatibility check and warnings implemented
1 parent 89a8210 commit 2dc86ca

File tree

3 files changed

+326
-39
lines changed

3 files changed

+326
-39
lines changed

apps/client/src/contexts/SessionContext.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ type SessionContextType = {
2323
roomState: RoomState | undefined
2424
sessionDurationMinutes: number
2525
rewindFetchGroupSize: number
26+
mediaStreamTrackProcessorMissing: boolean
2627
setSession: (
2728
userId: string,
2829
username: string,
2930
roomState: RoomState,
3031
sessionDurationMinutes: number,
3132
rewindFetchGroupSize: number,
33+
mediaStreamTrackProcessorMissing?: boolean,
3234
) => void
3335
clearSession: () => void
3436
}
@@ -41,19 +43,22 @@ export function SessionProvider({ children }: { children: ReactNode }) {
4143
const [roomState, setRoomState] = useState<RoomState | undefined>(undefined)
4244
const [sessionDurationMinutes, setSessionDurationMinutes] = useState(10) // default fallback
4345
const [rewindFetchGroupSize, setRewindFetchGroupSize] = useState(5) // default fallback
46+
const [mediaStreamTrackProcessorMissing, setMediaStreamTrackProcessorMissing] = useState(false)
4447

4548
function setSession(
4649
userId: string,
4750
username: string,
4851
roomState: RoomState,
4952
sessionDurationMinutes: number,
5053
rewindFetchGroupSize: number,
54+
mediaStreamTrackProcessorMissing: boolean = false,
5155
) {
5256
setUserId(userId)
5357
setUsername(username)
5458
setRoomState(roomState)
5559
setSessionDurationMinutes(sessionDurationMinutes)
5660
setRewindFetchGroupSize(rewindFetchGroupSize)
61+
setMediaStreamTrackProcessorMissing(mediaStreamTrackProcessorMissing)
5762
}
5863

5964
function clearSession() {
@@ -62,11 +67,21 @@ export function SessionProvider({ children }: { children: ReactNode }) {
6267
setRoomState(undefined)
6368
setSessionDurationMinutes(10)
6469
setRewindFetchGroupSize(5)
70+
setMediaStreamTrackProcessorMissing(false)
6571
}
6672

6773
return (
6874
<SessionContext.Provider
69-
value={{ userId, username, roomState, sessionDurationMinutes, rewindFetchGroupSize, setSession, clearSession }}
75+
value={{
76+
userId,
77+
username,
78+
roomState,
79+
sessionDurationMinutes,
80+
rewindFetchGroupSize,
81+
mediaStreamTrackProcessorMissing,
82+
setSession,
83+
clearSession,
84+
}}
7085
>
7186
{children}
7287
</SessionContext.Provider>

apps/client/src/pages/JoinPage.tsx

Lines changed: 254 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,93 @@ import { useSocket } from '@/sockets/SocketContext'
2222
import { SocketClock } from '@/util/socketClock'
2323
import { 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+
25101
export 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

Comments
 (0)