Skip to content

Commit

Permalink
feat: add generics to device management (#61)
Browse files Browse the repository at this point in the history
Co-authored-by: Bryce Tham <[email protected]>
  • Loading branch information
brycetham and Bryce Tham authored Jul 28, 2023
1 parent 0979047 commit adf048e
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 36 deletions.
111 changes: 99 additions & 12 deletions src/device/device-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createBrowserMock } from '../mocks/create-browser-mock';
import MediaStreamStub from '../mocks/media-stream-stub';
import { createMockedStream, createMockedStreamWithAudio } from '../util/test-utils';
import {
createCameraAndMicrophoneStreams,
createCameraStream,
createDisplayStream,
createDisplayStreamWithAudio,
Expand All @@ -29,18 +30,45 @@ describe('Device Management', () => {
it('should call getUserMedia', async () => {
expect.assertions(1);

await createMicrophoneStream({ deviceId: 'test-device-id' });
await createMicrophoneStream(LocalMicrophoneStream, { deviceId: 'test-device-id' });
expect(media.getUserMedia).toHaveBeenCalledWith({
audio: {
deviceId: 'test-device-id',
},
});
});

it('should call getUserMedia with constraints', async () => {
expect.assertions(1);

await createMicrophoneStream(LocalMicrophoneStream, {
deviceId: 'test-device-id',
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false,
sampleRate: 48000,
sampleSize: 16,
suppressLocalAudioPlayback: false,
});
expect(media.getUserMedia).toHaveBeenCalledWith({
audio: {
deviceId: 'test-device-id',
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false,
sampleRate: 48000,
sampleSize: 16,
suppressLocalAudioPlayback: false,
},
});
});

it('should return a LocalMicrophoneStream instance', async () => {
expect.assertions(1);

const localMicrophoneStream = await createMicrophoneStream({
const localMicrophoneStream = await createMicrophoneStream(LocalMicrophoneStream, {
deviceId: 'test-device-id',
});
expect(localMicrophoneStream).toBeInstanceOf(LocalMicrophoneStream);
Expand All @@ -55,7 +83,7 @@ describe('Device Management', () => {
it('should call getUserMedia', async () => {
expect.assertions(1);

await createCameraStream({ deviceId: 'test-device-id' });
await createCameraStream(LocalCameraStream, { deviceId: 'test-device-id' });
expect(media.getUserMedia).toHaveBeenCalledWith({
video: {
deviceId: 'test-device-id',
Expand All @@ -66,7 +94,7 @@ describe('Device Management', () => {
it('should call getUserMedia with constraints', async () => {
expect.assertions(1);

await createCameraStream({
await createCameraStream(LocalCameraStream, {
deviceId: 'test-device-id',
aspectRatio: 1.777,
width: 1920,
Expand All @@ -89,13 +117,62 @@ describe('Device Management', () => {
it('should return a LocalCameraStream instance', async () => {
expect.assertions(1);

const localCameraStream = await createCameraStream({
const localCameraStream = await createCameraStream(LocalCameraStream, {
deviceId: 'test-device-id',
});
expect(localCameraStream).toBeInstanceOf(LocalCameraStream);
});
});

describe('createCameraAndMicrophoneStreams', () => {
jest
.spyOn(media, 'getUserMedia')
.mockReturnValue(Promise.resolve(mockStream as unknown as MediaStream));

it('should call getUserMedia', async () => {
expect.assertions(1);

await createCameraAndMicrophoneStreams(LocalCameraStream, LocalMicrophoneStream, {
video: { deviceId: 'test-device-id' },
audio: { deviceId: 'test-device-id' },
});
expect(media.getUserMedia).toHaveBeenCalledWith({
video: {
deviceId: 'test-device-id',
},
audio: {
deviceId: 'test-device-id',
},
});
});

it('should return a LocalCameraStream and a LocalMicrophoneStream instance', async () => {
expect.assertions(2);

const [localCameraStream, localMicrophoneStream] = await createCameraAndMicrophoneStreams(
LocalCameraStream,
LocalMicrophoneStream,
{
video: { deviceId: 'test-device-id' },
audio: { deviceId: 'test-device-id' },
}
);
expect(localCameraStream).toBeInstanceOf(LocalCameraStream);
expect(localMicrophoneStream).toBeInstanceOf(LocalMicrophoneStream);
});

it('should return a LocalCameraStream and a LocalMicrophoneStream instance without constraints', async () => {
expect.assertions(2);

const [localCameraStream, localMicrophoneStream] = await createCameraAndMicrophoneStreams(
LocalCameraStream,
LocalMicrophoneStream
);
expect(localCameraStream).toBeInstanceOf(LocalCameraStream);
expect(localMicrophoneStream).toBeInstanceOf(LocalMicrophoneStream);
});
});

describe('createDisplayStream', () => {
jest
.spyOn(media, 'getDisplayMedia')
Expand All @@ -104,22 +181,22 @@ describe('Device Management', () => {
it('should call getDisplayMedia', async () => {
expect.assertions(1);

await createDisplayStream();
await createDisplayStream(LocalDisplayStream);
expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true });
});

it('should return a LocalDisplayStream instance', async () => {
expect.assertions(2);

const localDisplayStream = await createDisplayStream();
const localDisplayStream = await createDisplayStream(LocalDisplayStream);
expect(localDisplayStream).toBeInstanceOf(LocalDisplayStream);
expect(localDisplayStream.contentHint).toBeUndefined();
});

it('should preserve the content hint', async () => {
expect.assertions(1);

const localDisplayStream = await createDisplayStream('motion');
const localDisplayStream = await createDisplayStream(LocalDisplayStream, 'motion');
expect(localDisplayStream.contentHint).toBe('motion');
});
});
Expand All @@ -137,14 +214,17 @@ describe('Device Management', () => {
it('should call getDisplayMedia with audio', async () => {
expect.assertions(1);

await createDisplayStreamWithAudio();
await createDisplayStreamWithAudio(LocalDisplayStream, LocalSystemAudioStream);
expect(media.getDisplayMedia).toHaveBeenCalledWith({ video: true, audio: true });
});

it('should return a LocalDisplayStream instance and null if no audio track exists', async () => {
expect.assertions(2);

const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio();
const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio(
LocalDisplayStream,
LocalSystemAudioStream
);
expect(localDisplayStream).toBeInstanceOf(LocalDisplayStream);
expect(localSystemAudioStream).toBeNull();
});
Expand All @@ -157,15 +237,22 @@ describe('Device Management', () => {
.spyOn(media, 'getDisplayMedia')
.mockReturnValueOnce(Promise.resolve(mockStreamWithAudio as unknown as MediaStream));

const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio();
const [localDisplayStream, localSystemAudioStream] = await createDisplayStreamWithAudio(
LocalDisplayStream,
LocalSystemAudioStream
);
expect(localDisplayStream).toBeInstanceOf(LocalDisplayStream);
expect(localSystemAudioStream).toBeInstanceOf(LocalSystemAudioStream);
});

it('should preserve the content hint', async () => {
expect.assertions(1);

const [localDisplayStream] = await createDisplayStreamWithAudio('motion');
const [localDisplayStream] = await createDisplayStreamWithAudio(
LocalDisplayStream,
LocalSystemAudioStream,
'motion'
);
expect(localDisplayStream.contentHint).toBe('motion');
});
});
Expand Down
123 changes: 99 additions & 24 deletions src/device/device-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { VideoContentHint } from '../media/local-video-stream';

export enum ErrorTypes {
DEVICE_PERMISSION_DENIED = 'DEVICE_PERMISSION_DENIED',
CREATE_CAMERA_STREAM_FAILED = 'CREATE_CAMERA_STREAM_FAILED',
CREATE_MICROPHONE_STREAM_FAILED = 'CREATE_MICROPHONE_STREAM_FAILED',
CREATE_STREAM_FAILED = 'CREATE_CAMERA_STREAM',
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T> = new (...args: any[]) => T;

/**
* Represents a WCME error, which contains error type and error message.
*/
Expand Down Expand Up @@ -54,56 +56,109 @@ export type VideoDeviceConstraints = Pick<
* 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 constraints - Video device constraints.
* @returns A LocalCameraStream object or an error.
*/
export async function createCameraStream(
export async function createCameraStream<T extends LocalCameraStream>(
constructor: Constructor<T>,
constraints?: VideoDeviceConstraints
): Promise<LocalCameraStream> {
): Promise<T> {
let stream: MediaStream;
try {
stream = await media.getUserMedia({ video: { ...constraints } });
} catch (error) {
throw new WcmeError(
ErrorTypes.CREATE_CAMERA_STREAM_FAILED,
`Failed to create camera stream ${error}`
ErrorTypes.CREATE_STREAM_FAILED,
`Failed to create camera stream: ${error}`
);
}
return new LocalCameraStream(stream);
return new constructor(stream);
}

/**
* Creates a LocalMicrophoneStream with the given constraints.
*
* @param constructor - Constructor for the local microphone stream.
* @param constraints - Audio device constraints.
* @returns A LocalMicrophoneStream object or an error.
*/
export async function createMicrophoneStream(
export async function createMicrophoneStream<T extends LocalMicrophoneStream>(
constructor: Constructor<T>,
constraints?: AudioDeviceConstraints
): Promise<LocalMicrophoneStream> {
): Promise<T> {
let stream: MediaStream;
try {
stream = await media.getUserMedia({ audio: { ...constraints } });
} catch (error) {
throw new WcmeError(
ErrorTypes.CREATE_MICROPHONE_STREAM_FAILED,
`Failed to create microphone stream ${error}`
ErrorTypes.CREATE_STREAM_FAILED,
`Failed to create microphone stream: ${error}`
);
}
return new constructor(stream);
}

/**
* Creates a LocalCameraStream and a LocalMicrophoneStream with the given constraints.
*
* @param cameraStreamConstructor - Constructor for the local camera stream.
* @param microphoneStreamConstructor - Constructor for the local microphone stream.
* @param constraints - Object containing video and audio device constraints.
* @param constraints.video - Video device constraints.
* @param constraints.audio - Audio device constraints.
* @returns A Promise that resolves to a LocalCameraStream and a LocalMicrophoneStream or an error.
*/
export async function createCameraAndMicrophoneStreams<
T extends LocalCameraStream,
U extends LocalMicrophoneStream
>(
cameraStreamConstructor: Constructor<T>,
microphoneStreamConstructor: Constructor<U>,
constraints?: { video?: VideoDeviceConstraints; audio?: AudioDeviceConstraints }
): Promise<[T, U]> {
let stream;
try {
stream = await media.getUserMedia({
video: { ...constraints?.video },
audio: { ...constraints?.audio },
});
} catch (error) {
throw new WcmeError(
ErrorTypes.CREATE_STREAM_FAILED,
`Failed to create camera and microphone streams: ${error}`
);
}
return new LocalMicrophoneStream(stream);
// eslint-disable-next-line new-cap
const localCameraStream = new cameraStreamConstructor(new MediaStream(stream.getVideoTracks()));
// eslint-disable-next-line new-cap
const localMicrophoneStream = new microphoneStreamConstructor(
new MediaStream(stream.getAudioTracks())
);
return [localCameraStream, localMicrophoneStream];
}

/**
* 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.
* @returns A Promise that resolves to a LocalDisplayStream or an error.
*/
export async function createDisplayStream(
export async function createDisplayStream<T extends LocalDisplayStream>(
constructor: Constructor<T>,
videoContentHint?: VideoContentHint
): Promise<LocalDisplayStream> {
const stream = await media.getDisplayMedia({ video: true });
const localDisplayStream = new LocalDisplayStream(stream);
): Promise<T> {
let stream;
try {
stream = await media.getDisplayMedia({ video: true });
} catch (error) {
throw new WcmeError(
ErrorTypes.CREATE_STREAM_FAILED,
`Failed to create display stream: ${error}`
);
}
const localDisplayStream = new constructor(stream);
if (videoContentHint) {
localDisplayStream.contentHint = videoContentHint;
}
Expand All @@ -113,21 +168,41 @@ export async function createDisplayStream(
/**
* Creates a LocalDisplayStream and a LocalSystemAudioStream with the given parameters.
*
* @param displayStreamConstructor - Constructor for the local display stream.
* @param systemAudioStreamConstructor - Constructor for the local system audio stream.
* @param videoContentHint - An optional parameter to give a hint for the content of the stream.
* @returns A Promise that resolves to a LocalDisplayStream and a LocalSystemAudioStream. If no system
* audio is available, the LocalSystemAudioStream will be resolved as null instead.
* @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 createDisplayStreamWithAudio(
export async function createDisplayStreamWithAudio<
T extends LocalDisplayStream,
U extends LocalSystemAudioStream
>(
displayStreamConstructor: Constructor<T>,
systemAudioStreamConstructor: Constructor<U>,
videoContentHint?: VideoContentHint
): Promise<[LocalDisplayStream, LocalSystemAudioStream | null]> {
const stream = await media.getDisplayMedia({ video: true, audio: true });
const localDisplayStream = new LocalDisplayStream(new MediaStream(stream.getVideoTracks()));
): Promise<[T, U | null]> {
let stream;
try {
stream = await media.getDisplayMedia({ video: true, audio: true });
} catch (error) {
throw new WcmeError(
ErrorTypes.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) {
localSystemAudioStream = new LocalSystemAudioStream(new MediaStream(stream.getAudioTracks()));
// eslint-disable-next-line new-cap
localSystemAudioStream = new systemAudioStreamConstructor(
new MediaStream(stream.getAudioTracks())
);
}
return [localDisplayStream, localSystemAudioStream];
}
Expand Down

0 comments on commit adf048e

Please sign in to comment.