From 377c9021840fd3ccf0e12bd29ad4624de944a730 Mon Sep 17 00:00:00 2001 From: Bryce Tham Date: Fri, 31 Jan 2025 11:25:04 -0500 Subject: [PATCH 1/4] feat: add createDisplayMedia API --- src/device/device-management.spec.ts | 158 +++++++++++++++++++++++---- src/device/device-management.ts | 132 ++++++++++++++++------ 2 files changed, 235 insertions(+), 55 deletions(-) diff --git a/src/device/device-management.spec.ts b/src/device/device-management.spec.ts index 3d3d344..d4488a6 100644 --- a/src/device/device-management.spec.ts +++ b/src/device/device-management.spec.ts @@ -1,3 +1,4 @@ +import { WebrtcCoreError, WebrtcCoreErrorType } from '../errors'; import * as media from '../media'; import { LocalCameraStream } from '../media/local-camera-stream'; import { LocalDisplayStream } from '../media/local-display-stream'; @@ -7,14 +8,15 @@ import { createBrowserMock } from '../mocks/create-browser-mock'; import MediaStreamStub from '../mocks/media-stream-stub'; import { createMockedStream, createMockedStreamWithAudio } from '../util/test-utils'; import { + CaptureController, createCameraAndMicrophoneStreams, createCameraStream, + createDisplayMedia, createDisplayStream, createDisplayStreamWithAudio, createMicrophoneStream, getDevices, } from './device-management'; -import { WebrtcCoreError, WebrtcCoreErrorType } from '../errors'; jest.mock('../mocks/media-stream-stub'); @@ -25,9 +27,7 @@ describe('Device Management', () => { const mockStream = createMockedStream(); describe('createMicrophoneStream', () => { - jest - .spyOn(media, 'getUserMedia') - .mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream)); + jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); it('should call getUserMedia', async () => { expect.assertions(1); @@ -76,9 +76,7 @@ describe('Device Management', () => { }); describe('createCameraStream', () => { - jest - .spyOn(media, 'getUserMedia') - .mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream)); + jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); it('should call getUserMedia', async () => { expect.assertions(1); @@ -125,9 +123,7 @@ describe('Device Management', () => { }); describe('createCameraAndMicrophoneStreams', () => { - jest - .spyOn(media, 'getUserMedia') - .mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream)); + jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); it('should call getUserMedia', async () => { expect.assertions(1); @@ -173,24 +169,148 @@ describe('Device Management', () => { }); }); + describe('createDisplayMedia', () => { + jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); + + it('should call getDisplayMedia with video only', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: false }); + }); + + it('should call getDisplayMedia with both video and audio', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream }, + audio: { constructor: LocalSystemAudioStream }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: true }); + }); + + it('should call getDisplayMedia with video and audio constraints', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream, constraints: { frameRate: 5 } }, + audio: { constructor: LocalSystemAudioStream, constraints: { autoGainControl: false } }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: { frameRate: 5 }, + audio: { autoGainControl: false }, + }); + }); + + it('should preserve the content hint', async () => { + expect.assertions(1); + + const [localDisplayStream] = await createDisplayMedia({ + video: { constructor: LocalDisplayStream, videoContentHint: 'motion' }, + }); + expect(localDisplayStream.contentHint).toBe('motion'); + }); + + it('should call getDisplayMedia with the preferCurrentTab option', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream, preferCurrentTab: true }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: false, + preferCurrentTab: true, + }); + }); + + it('should call getDisplayMedia with the selfBrowserSurface option', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream, selfBrowserSurface: 'include' }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: false, + selfBrowserSurface: 'include', + }); + }); + + it('should call getDisplayMedia with the surfaceSwitching option', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream, surfaceSwitching: 'include' }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: false, + surfaceSwitching: 'include', + }); + }); + + it('should call getDisplayMedia with the monitorTypeSurfaces option', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream, monitorTypeSurfaces: 'exclude' }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: false, + monitorTypeSurfaces: 'exclude', + }); + }); + + it('should call getDisplayMedia with the systemAudio option', async () => { + expect.assertions(1); + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream }, + audio: { constructor: LocalSystemAudioStream, systemAudio: 'exclude' }, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: true, + systemAudio: 'exclude', + }); + }); + + it('should call getDisplayMedia with the controller option', async () => { + expect.assertions(1); + + const fakeController: CaptureController = {} as CaptureController; + + await createDisplayMedia({ + video: { constructor: LocalDisplayStream }, + controller: fakeController, + }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ + video: true, + audio: false, + controller: fakeController, + }); + }); + }); + describe('createDisplayStream', () => { - jest - .spyOn(media, 'getDisplayMedia') - .mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream)); + jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); it('should call getDisplayMedia', async () => { expect.assertions(1); await createDisplayStream(LocalDisplayStream); - expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true }); + expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: false }); }); it('should return a LocalDisplayStream instance', async () => { - expect.assertions(2); + expect.assertions(1); const localDisplayStream = await createDisplayStream(LocalDisplayStream); expect(localDisplayStream).toBeInstanceOf(LocalDisplayStream); - expect(localDisplayStream.contentHint).toBeUndefined(); }); it('should preserve the content hint', async () => { @@ -202,9 +322,7 @@ describe('Device Management', () => { }); describe('createDisplayStreamWithAudio', () => { - jest - .spyOn(media, 'getDisplayMedia') - .mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream)); + jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); // This mock implementation is needed because createDisplayStreamWithAudio will create a new // MediaStream from the video track of the mocked stream, so we need to make sure this new @@ -235,7 +353,7 @@ describe('Device Management', () => { const mockStreamWithAudio = createMockedStreamWithAudio(); jest .spyOn(media, 'getDisplayMedia') - .mockReturnValueOnce(Promise.resolve(mockStreamWithAudio as unknown as MediaStream)); + .mockReturnValueOnce(Promise.resolve(mockStreamWithAudio)); const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio( LocalDisplayStream, diff --git a/src/device/device-management.ts b/src/device/device-management.ts index e700310..3d91cf8 100644 --- a/src/device/device-management.ts +++ b/src/device/device-management.ts @@ -25,6 +25,13 @@ export type VideoDeviceConstraints = Pick< 'aspectRatio' | 'deviceId' | 'facingMode' | 'frameRate' | 'height' | 'width' >; +// CaptureController is experimental so TypeScript doesn't have a type for it yet. +// So we define the interface ourselves here. +// See https://developer.mozilla.org/en-US/docs/Web/API/CaptureController. +export interface CaptureController { + setFocusBehavior(behavior: 'auto' | 'always' | 'never'): Promise; +} + /** * Creates a camera stream. Please note that the constraint params in second getUserMedia call would NOT take effect when: * @@ -114,29 +121,102 @@ export async function createCameraAndMicrophoneStreams< } /** - * Creates a LocalDisplayStream with the given parameters. + * Creates a LocalDisplayStream and a LocalSystemAudioStream with the given parameters. * - * @param constructor - Constructor for the local display stream. - * @param videoContentHint - An optional parameter to give a hint for the content of the stream. - * @returns A Promise that resolves to a LocalDisplayStream or an error. + * This is a more advanced version of createDisplayStreamWithAudio that allows the user to specify + * additional display media options and constraints. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#options. + * + * @param options - An object containing the options for creating the display and system audio streams. + * @param options.video - An object containing the video stream options. + * @param options.video.constructor - Constructor for the local display stream. + * @param options.video.constraints - Video device constraints. + * @param options.video.videoContentHint - A hint for the content of the stream. + * @param options.video.preferCurrentTab - Whether to offer the current tab as the most prominent capture source. + * @param options.video.selfBrowserSurface - Whether to allow the user to select the current tab for capture. + * @param options.video.surfaceSwitching - Whether to allow the user to dynamically switch the shared tab during screen-sharing. + * @param options.video.monitorTypeSurfaces - Whether to offer the user the option to choose display surfaces whose type is monitor. + * @param options.audio - An object containing the audio stream options. If present, a system audio stream will be created. + * @param options.audio.constructor - Constructor for the local system audio stream. + * @param options.audio.constraints - Audio device constraints. + * @param options.audio.systemAudio - Whether to include the system audio among the possible audio sources offered to the user. + * @param options.controller - CaptureController to further manipulate the capture session. + * @returns A Promise that resolves to a LocalDisplayStream and a LocalSystemAudioStream or an + * error. If no system audio is available, the LocalSystemAudioStream will be resolved as null + * instead. */ -export async function createDisplayStream( - constructor: Constructor, - videoContentHint?: VideoContentHint -): Promise { +export async function createDisplayMedia< + T extends LocalDisplayStream, + U extends LocalSystemAudioStream +>(options: { + video: { + constructor: Constructor; + constraints?: VideoDeviceConstraints; + videoContentHint?: VideoContentHint; + preferCurrentTab?: boolean; + selfBrowserSurface?: 'include' | 'exclude'; + surfaceSwitching?: 'include' | 'exclude'; + monitorTypeSurfaces?: 'include' | 'exclude'; + }; + audio?: { + constructor: Constructor; + constraints?: AudioDeviceConstraints; + systemAudio?: 'include' | 'exclude'; + }; + controller?: CaptureController; +}): Promise<[T, U | null]> { let stream; + const videoConstraints = options.video.constraints || true; + const audioConstraints = options.audio?.constraints || !!options.audio; try { - stream = await media.getDisplayMedia({ video: true }); + stream = await media.getDisplayMedia({ + video: videoConstraints, + audio: audioConstraints, + controller: options.controller, + preferCurrentTab: options.video.preferCurrentTab, + selfBrowserSurface: options.video.selfBrowserSurface, + surfaceSwitching: options.video.surfaceSwitching, + systemAudio: options.audio?.systemAudio, + monitorTypeSurfaces: options.video.monitorTypeSurfaces, + } as any); // eslint-disable-line @typescript-eslint/no-explicit-any } catch (error) { throw new WebrtcCoreError( WebrtcCoreErrorType.CREATE_STREAM_FAILED, - `Failed to create display stream: ${error}` + `Failed to create display and/or system audio streams: ${error}` ); } - const localDisplayStream = new constructor(stream); - if (videoContentHint) { - localDisplayStream.contentHint = videoContentHint; + // eslint-disable-next-line new-cap + const localDisplayStream = new options.video.constructor( + new MediaStream(stream.getVideoTracks()) + ); + if (options.video.videoContentHint) { + localDisplayStream.contentHint = options.video.videoContentHint; } + let localSystemAudioStream = null; + if (options.audio && stream.getAudioTracks().length > 0) { + // eslint-disable-next-line new-cap + localSystemAudioStream = new options.audio.constructor( + new MediaStream(stream.getAudioTracks()) + ); + } + return [localDisplayStream, localSystemAudioStream]; +} + +/** + * Creates a LocalDisplayStream with the given parameters. + * + * @param constructor - Constructor for the local display stream. + * @param videoContentHint - An optional parameter to give a hint for the content of the stream. + * @returns A Promise that resolves to a LocalDisplayStream or an error. + */ +export async function createDisplayStream( + constructor: Constructor, + videoContentHint?: VideoContentHint +): Promise { + const [localDisplayStream] = await createDisplayMedia({ + video: { constructor, videoContentHint }, + }); return localDisplayStream; } @@ -158,28 +238,10 @@ export async function createDisplayStreamWithAudio< systemAudioStreamConstructor: Constructor, videoContentHint?: VideoContentHint ): Promise<[T, U | null]> { - let stream; - try { - stream = await media.getDisplayMedia({ video: true, audio: true }); - } catch (error) { - throw new WebrtcCoreError( - WebrtcCoreErrorType.CREATE_STREAM_FAILED, - `Failed to create display and system audio streams: ${error}` - ); - } - // eslint-disable-next-line new-cap - const localDisplayStream = new displayStreamConstructor(new MediaStream(stream.getVideoTracks())); - if (videoContentHint) { - localDisplayStream.contentHint = videoContentHint; - } - let localSystemAudioStream = null; - if (stream.getAudioTracks().length > 0) { - // eslint-disable-next-line new-cap - localSystemAudioStream = new systemAudioStreamConstructor( - new MediaStream(stream.getAudioTracks()) - ); - } - return [localDisplayStream, localSystemAudioStream]; + return createDisplayMedia({ + video: { constructor: displayStreamConstructor, videoContentHint }, + audio: { constructor: systemAudioStreamConstructor }, + }); } /** From ee85f18aa15381069df717ea2968f980fe70ee7c Mon Sep 17 00:00:00 2001 From: Bryce Tham Date: Fri, 31 Jan 2025 13:15:09 -0500 Subject: [PATCH 2/4] chore: update constructor param names --- src/device/device-management.spec.ts | 29 ++++++++++++---------- src/device/device-management.ts | 36 +++++++++++++++------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/device/device-management.spec.ts b/src/device/device-management.spec.ts index d4488a6..25a5af2 100644 --- a/src/device/device-management.spec.ts +++ b/src/device/device-management.spec.ts @@ -176,7 +176,7 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream }, + video: { displayStreamConstructor: LocalDisplayStream }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: false }); }); @@ -185,8 +185,8 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream }, - audio: { constructor: LocalSystemAudioStream }, + video: { displayStreamConstructor: LocalDisplayStream }, + audio: { systemAudioStreamConstructor: LocalSystemAudioStream }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: true }); }); @@ -195,8 +195,11 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream, constraints: { frameRate: 5 } }, - audio: { constructor: LocalSystemAudioStream, constraints: { autoGainControl: false } }, + video: { displayStreamConstructor: LocalDisplayStream, constraints: { frameRate: 5 } }, + audio: { + systemAudioStreamConstructor: LocalSystemAudioStream, + constraints: { autoGainControl: false }, + }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: { frameRate: 5 }, @@ -208,7 +211,7 @@ describe('Device Management', () => { expect.assertions(1); const [localDisplayStream] = await createDisplayMedia({ - video: { constructor: LocalDisplayStream, videoContentHint: 'motion' }, + video: { displayStreamConstructor: LocalDisplayStream, videoContentHint: 'motion' }, }); expect(localDisplayStream.contentHint).toBe('motion'); }); @@ -217,7 +220,7 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream, preferCurrentTab: true }, + video: { displayStreamConstructor: LocalDisplayStream, preferCurrentTab: true }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, @@ -230,7 +233,7 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream, selfBrowserSurface: 'include' }, + video: { displayStreamConstructor: LocalDisplayStream, selfBrowserSurface: 'include' }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, @@ -243,7 +246,7 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream, surfaceSwitching: 'include' }, + video: { displayStreamConstructor: LocalDisplayStream, surfaceSwitching: 'include' }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, @@ -256,7 +259,7 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream, monitorTypeSurfaces: 'exclude' }, + video: { displayStreamConstructor: LocalDisplayStream, monitorTypeSurfaces: 'exclude' }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, @@ -269,8 +272,8 @@ describe('Device Management', () => { expect.assertions(1); await createDisplayMedia({ - video: { constructor: LocalDisplayStream }, - audio: { constructor: LocalSystemAudioStream, systemAudio: 'exclude' }, + video: { displayStreamConstructor: LocalDisplayStream }, + audio: { systemAudioStreamConstructor: LocalSystemAudioStream, systemAudio: 'exclude' }, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, @@ -285,7 +288,7 @@ describe('Device Management', () => { const fakeController: CaptureController = {} as CaptureController; await createDisplayMedia({ - video: { constructor: LocalDisplayStream }, + video: { displayStreamConstructor: LocalDisplayStream }, controller: fakeController, }); expect(media.getDisplayMedia).toHaveBeenCalledWith({ diff --git a/src/device/device-management.ts b/src/device/device-management.ts index 3d91cf8..bdaa709 100644 --- a/src/device/device-management.ts +++ b/src/device/device-management.ts @@ -38,12 +38,12 @@ export interface CaptureController { * 1. Previous captured video stream from the same device is not stopped. * 2. Previous createCameraStream() call for the same device is in progress. * - * @param constructor - Constructor for the local camera stream. + * @param cameraStreamConstructor - Constructor for the local camera stream. * @param constraints - Video device constraints. * @returns A LocalCameraStream object or an error. */ export async function createCameraStream( - constructor: Constructor, + cameraStreamConstructor: Constructor, constraints?: VideoDeviceConstraints ): Promise { let stream: MediaStream; @@ -55,18 +55,19 @@ export async function createCameraStream( `Failed to create camera stream: ${error}` ); } - return new constructor(stream); + // eslint-disable-next-line new-cap + return new cameraStreamConstructor(stream); } /** * Creates a LocalMicrophoneStream with the given constraints. * - * @param constructor - Constructor for the local microphone stream. + * @param microphoneStreamConstructor - Constructor for the local microphone stream. * @param constraints - Audio device constraints. * @returns A LocalMicrophoneStream object or an error. */ export async function createMicrophoneStream( - constructor: Constructor, + microphoneStreamConstructor: Constructor, constraints?: AudioDeviceConstraints ): Promise { let stream: MediaStream; @@ -78,7 +79,8 @@ export async function createMicrophoneStream( `Failed to create microphone stream: ${error}` ); } - return new constructor(stream); + // eslint-disable-next-line new-cap + return new microphoneStreamConstructor(stream); } /** @@ -130,7 +132,7 @@ export async function createCameraAndMicrophoneStreams< * * @param options - An object containing the options for creating the display and system audio streams. * @param options.video - An object containing the video stream options. - * @param options.video.constructor - Constructor for the local display stream. + * @param options.video.displayStreamConstructor - Constructor for the local display stream. * @param options.video.constraints - Video device constraints. * @param options.video.videoContentHint - A hint for the content of the stream. * @param options.video.preferCurrentTab - Whether to offer the current tab as the most prominent capture source. @@ -138,7 +140,7 @@ export async function createCameraAndMicrophoneStreams< * @param options.video.surfaceSwitching - Whether to allow the user to dynamically switch the shared tab during screen-sharing. * @param options.video.monitorTypeSurfaces - Whether to offer the user the option to choose display surfaces whose type is monitor. * @param options.audio - An object containing the audio stream options. If present, a system audio stream will be created. - * @param options.audio.constructor - Constructor for the local system audio stream. + * @param options.audio.systemAudioStreamConstructor - Constructor for the local system audio stream. * @param options.audio.constraints - Audio device constraints. * @param options.audio.systemAudio - Whether to include the system audio among the possible audio sources offered to the user. * @param options.controller - CaptureController to further manipulate the capture session. @@ -151,7 +153,7 @@ export async function createDisplayMedia< U extends LocalSystemAudioStream >(options: { video: { - constructor: Constructor; + displayStreamConstructor: Constructor; constraints?: VideoDeviceConstraints; videoContentHint?: VideoContentHint; preferCurrentTab?: boolean; @@ -160,7 +162,7 @@ export async function createDisplayMedia< monitorTypeSurfaces?: 'include' | 'exclude'; }; audio?: { - constructor: Constructor; + systemAudioStreamConstructor: Constructor; constraints?: AudioDeviceConstraints; systemAudio?: 'include' | 'exclude'; }; @@ -187,7 +189,7 @@ export async function createDisplayMedia< ); } // eslint-disable-next-line new-cap - const localDisplayStream = new options.video.constructor( + const localDisplayStream = new options.video.displayStreamConstructor( new MediaStream(stream.getVideoTracks()) ); if (options.video.videoContentHint) { @@ -196,7 +198,7 @@ export async function createDisplayMedia< let localSystemAudioStream = null; if (options.audio && stream.getAudioTracks().length > 0) { // eslint-disable-next-line new-cap - localSystemAudioStream = new options.audio.constructor( + localSystemAudioStream = new options.audio.systemAudioStreamConstructor( new MediaStream(stream.getAudioTracks()) ); } @@ -206,16 +208,16 @@ export async function createDisplayMedia< /** * Creates a LocalDisplayStream with the given parameters. * - * @param constructor - Constructor for the local display stream. + * @param displayStreamConstructor - Constructor for the local display stream. * @param videoContentHint - An optional parameter to give a hint for the content of the stream. * @returns A Promise that resolves to a LocalDisplayStream or an error. */ export async function createDisplayStream( - constructor: Constructor, + displayStreamConstructor: Constructor, videoContentHint?: VideoContentHint ): Promise { const [localDisplayStream] = await createDisplayMedia({ - video: { constructor, videoContentHint }, + video: { displayStreamConstructor, videoContentHint }, }); return localDisplayStream; } @@ -239,8 +241,8 @@ export async function createDisplayStreamWithAudio< videoContentHint?: VideoContentHint ): Promise<[T, U | null]> { return createDisplayMedia({ - video: { constructor: displayStreamConstructor, videoContentHint }, - audio: { constructor: systemAudioStreamConstructor }, + video: { displayStreamConstructor, videoContentHint }, + audio: { systemAudioStreamConstructor }, }); } From de695d1a0462781ef0fc422a83246c2bf4a24713 Mon Sep 17 00:00:00 2001 From: Bryce Tham Date: Mon, 3 Feb 2025 13:12:21 -0500 Subject: [PATCH 3/4] fix: redefine types in media/index.ts --- src/device/device-management.spec.ts | 2 +- src/device/device-management.ts | 10 ++-------- src/media/index.ts | 24 +++++++++++++++++++++--- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/device/device-management.spec.ts b/src/device/device-management.spec.ts index 25a5af2..609c7e5 100644 --- a/src/device/device-management.spec.ts +++ b/src/device/device-management.spec.ts @@ -1,5 +1,6 @@ import { WebrtcCoreError, WebrtcCoreErrorType } from '../errors'; import * as media from '../media'; +import { CaptureController } from '../media'; import { LocalCameraStream } from '../media/local-camera-stream'; import { LocalDisplayStream } from '../media/local-display-stream'; import { LocalMicrophoneStream } from '../media/local-microphone-stream'; @@ -8,7 +9,6 @@ import { createBrowserMock } from '../mocks/create-browser-mock'; import MediaStreamStub from '../mocks/media-stream-stub'; import { createMockedStream, createMockedStreamWithAudio } from '../util/test-utils'; import { - CaptureController, createCameraAndMicrophoneStreams, createCameraStream, createDisplayMedia, diff --git a/src/device/device-management.ts b/src/device/device-management.ts index bdaa709..60af284 100644 --- a/src/device/device-management.ts +++ b/src/device/device-management.ts @@ -1,5 +1,6 @@ import { WebrtcCoreError, WebrtcCoreErrorType } from '../errors'; import * as media from '../media'; +import { CaptureController } from '../media'; import { LocalCameraStream } from '../media/local-camera-stream'; import { LocalDisplayStream } from '../media/local-display-stream'; import { LocalMicrophoneStream } from '../media/local-microphone-stream'; @@ -25,13 +26,6 @@ export type VideoDeviceConstraints = Pick< 'aspectRatio' | 'deviceId' | 'facingMode' | 'frameRate' | 'height' | 'width' >; -// CaptureController is experimental so TypeScript doesn't have a type for it yet. -// So we define the interface ourselves here. -// See https://developer.mozilla.org/en-US/docs/Web/API/CaptureController. -export interface CaptureController { - setFocusBehavior(behavior: 'auto' | 'always' | 'never'): Promise; -} - /** * Creates a camera stream. Please note that the constraint params in second getUserMedia call would NOT take effect when: * @@ -181,7 +175,7 @@ export async function createDisplayMedia< surfaceSwitching: options.video.surfaceSwitching, systemAudio: options.audio?.systemAudio, monitorTypeSurfaces: options.video.monitorTypeSurfaces, - } as any); // eslint-disable-line @typescript-eslint/no-explicit-any + }); } catch (error) { throw new WebrtcCoreError( WebrtcCoreErrorType.CREATE_STREAM_FAILED, diff --git a/src/media/index.ts b/src/media/index.ts index ebec565..c615142 100644 --- a/src/media/index.ts +++ b/src/media/index.ts @@ -6,6 +6,15 @@ export enum DeviceKind { VideoInput = 'videoinput', } +// CaptureController is experimental so TypeScript doesn't have a type for it yet. +// So we define the interface ourselves here. +// See https://developer.mozilla.org/en-US/docs/Web/API/CaptureController. +export interface CaptureController { + setFocusBehavior( + behavior: 'focus-capturing-application' | 'focus-captured-surface' | 'no-focus-change' + ): Promise; +} + /** * Prompts the user for permission to use a media input which produces a MediaStream with tracks * containing the requested types of media. @@ -21,14 +30,23 @@ export async function getUserMedia(constraints: MediaStreamConstraints): Promise /** * Prompts the user for permission to use a user's display media and audio. If a video track is - * absent from the constraints argument, one will still be provided. + * absent from the constraints argument, one will still be provided. Includes experimental options + * found in https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#options. * * @param constraints - A MediaStreamConstraints object specifying the types of media to request, - * along with any requirements for each type. + * along with any requirements for each type, as well as experimental options. * @returns A Promise whose fulfillment handler receives a MediaStream object when the requested * media has successfully been obtained. */ -export function getDisplayMedia(constraints: MediaStreamConstraints): Promise { +export function getDisplayMedia( + constraints: MediaStreamConstraints & { + controller?: CaptureController; + selfBrowserSurface?: 'include' | 'exclude'; + surfaceSwitching?: 'include' | 'exclude'; + systemAudio?: 'include' | 'exclude'; + monitorTypeSurfaces?: 'include' | 'exclude'; + } +): Promise { return navigator.mediaDevices.getDisplayMedia(constraints); } From a59db525fc6c77510bf9230d64c7daef5a43e7fe Mon Sep 17 00:00:00 2001 From: Bryce Tham Date: Tue, 4 Feb 2025 08:32:21 -0500 Subject: [PATCH 4/4] chore: change mockReturnValue to mockResolvedValue --- src/device/device-management.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/device/device-management.spec.ts b/src/device/device-management.spec.ts index 609c7e5..1224073 100644 --- a/src/device/device-management.spec.ts +++ b/src/device/device-management.spec.ts @@ -27,7 +27,7 @@ describe('Device Management', () => { const mockStream = createMockedStream(); describe('createMicrophoneStream', () => { - jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getUserMedia').mockResolvedValue(mockStream); it('should call getUserMedia', async () => { expect.assertions(1); @@ -76,7 +76,7 @@ describe('Device Management', () => { }); describe('createCameraStream', () => { - jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getUserMedia').mockResolvedValue(mockStream); it('should call getUserMedia', async () => { expect.assertions(1); @@ -123,7 +123,7 @@ describe('Device Management', () => { }); describe('createCameraAndMicrophoneStreams', () => { - jest.spyOn(media, 'getUserMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getUserMedia').mockResolvedValue(mockStream); it('should call getUserMedia', async () => { expect.assertions(1); @@ -170,7 +170,7 @@ describe('Device Management', () => { }); describe('createDisplayMedia', () => { - jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getDisplayMedia').mockResolvedValue(mockStream); it('should call getDisplayMedia with video only', async () => { expect.assertions(1); @@ -300,7 +300,7 @@ describe('Device Management', () => { }); describe('createDisplayStream', () => { - jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getDisplayMedia').mockResolvedValue(mockStream); it('should call getDisplayMedia', async () => { expect.assertions(1); @@ -325,7 +325,7 @@ describe('Device Management', () => { }); describe('createDisplayStreamWithAudio', () => { - jest.spyOn(media, 'getDisplayMedia').mockReturnValue(Promise.resolve(mockStream)); + jest.spyOn(media, 'getDisplayMedia').mockResolvedValue(mockStream); // This mock implementation is needed because createDisplayStreamWithAudio will create a new // MediaStream from the video track of the mocked stream, so we need to make sure this new @@ -354,9 +354,7 @@ describe('Device Management', () => { expect.assertions(2); const mockStreamWithAudio = createMockedStreamWithAudio(); - jest - .spyOn(media, 'getDisplayMedia') - .mockReturnValueOnce(Promise.resolve(mockStreamWithAudio)); + jest.spyOn(media, 'getDisplayMedia').mockResolvedValueOnce(mockStreamWithAudio); const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio( LocalDisplayStream,