Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -161,10 +161,21 @@ export type CaptureErrorCode =
|
||||
| 'codec-unsupported'
|
||||
| 'no-source-selected'
|
||||
| 'capture-failed'
|
||||
| 'wrong-display-surface' // Plan 01-09 D-15-display-surface
|
||||
| 'unknown';
|
||||
|
||||
export function classifyCaptureError(error: unknown): CaptureErrorCode {
|
||||
if (error instanceof Error) {
|
||||
// Plan 01-09 D-15-display-surface: post-grant validation throws
|
||||
// `Error('wrong-display-surface: got "<surface>"…')` when the
|
||||
// operator overrode the `displaySurface: 'monitor'` HINT (the spec
|
||||
// does not treat it as a hard constraint — see Task 2 block comment
|
||||
// near the getDisplayMedia call site for the constraint-hint-vs-
|
||||
// enforcement rationale). Prefix-matching is safe: we control the
|
||||
// message string entirely; it never wraps a DOMException.
|
||||
if (error.message.startsWith('wrong-display-surface')) {
|
||||
return 'wrong-display-surface';
|
||||
}
|
||||
// Our own assertion is the cleanest signal — it never wraps a
|
||||
// DOMException, so prefix-matching is safe and stable.
|
||||
if (error.message.startsWith('vp9 unsupported')) {
|
||||
@@ -210,11 +221,52 @@ async function startRecording(): Promise<void> {
|
||||
}
|
||||
try {
|
||||
assertCodecSupported(); // throws if vp9 missing — no fallback
|
||||
// Plan 01-09 D-15-display-surface (UAT Test 3 "Share this tab"
|
||||
// footgun retirement, disambiguated from the historical
|
||||
// "D-15: operator switching tabs" decision per B-02 fix):
|
||||
// • `displaySurface: 'monitor'` hints Chrome's picker to default
|
||||
// to entire-screen selection instead of the tab list.
|
||||
// • `cursor: 'always'` opportunistically lifts the Phase 5
|
||||
// cursor-visibility refinement (we want the operator's pointer
|
||||
// captured for debugging context).
|
||||
// Both constraints are HINTS per the W3C Screen Capture spec — the
|
||||
// operator can still override via the picker UI. The post-grant
|
||||
// validation block immediately below is the actual enforcement.
|
||||
// Type note: lib.dom.d.ts MediaTrackConstraints omits `cursor`
|
||||
// (the Screen Capture spec defines it but TypeScript's bundled
|
||||
// types lag — https://developer.mozilla.org/docs/Web/API/MediaTrackConstraints/cursor).
|
||||
// Use a typed widening through `DisplayMediaStreamOptions &
|
||||
// {video: Record<...>}` instead of `as any` to stay explicit
|
||||
// about the precise field we're adding.
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: true,
|
||||
video: { displaySurface: 'monitor', cursor: 'always' },
|
||||
audio: false, // SPEC §9 — Phase 2 / CAP-01 territory
|
||||
} as DisplayMediaStreamOptions & {
|
||||
video: { displaySurface: 'monitor'; cursor: 'always' };
|
||||
});
|
||||
mediaStream = stream;
|
||||
// Post-grant validation — the constraint-hint-vs-enforcement gap.
|
||||
// getDisplayMedia's `displaySurface` is a HINT, not a hard
|
||||
// constraint: the operator may pick a tab/window from the picker
|
||||
// UI regardless of what we requested. We enforce monitor-only
|
||||
// here by inspecting the actual track settings. On mismatch we
|
||||
// tear down the stream and throw a routable error — the catch
|
||||
// block below classifies it via classifyCaptureError, broadcasts
|
||||
// RECORDING_ERROR{code: 'wrong-display-surface'}, and re-throws.
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
if (videoTrack !== undefined) {
|
||||
const observed = videoTrack.getSettings().displaySurface;
|
||||
if (observed !== 'monitor') {
|
||||
stream.getTracks().forEach((t) => {
|
||||
try { t.stop(); } catch (_e) { /* defensive — stop may throw on
|
||||
already-stopped tracks; ignore. */ }
|
||||
});
|
||||
mediaStream = null;
|
||||
throw new Error(
|
||||
`wrong-display-surface: got "${observed}", expected "monitor"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Track end detection — RESEARCH.md Example F. Attach to ALL tracks
|
||||
// (Pitfall 6) так, чтобы edge-case-аудио-трек не оставил нас в
|
||||
// несинхронизованном состоянии. Регистрация — один раз на поток;
|
||||
|
||||
Reference in New Issue
Block a user