Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add generics to device management #61

Merged
merged 1 commit into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this in a later PR, but I think it's kind of weird that we're referencing WCME here, and naming the error class WcmeError, especially since WCME doesn't actually call any of the device management APIs.

*/
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not now but would be nicer to have error codes associated later , so that we know what the cause of failure else anyone using this lib need to figureout what does all getuser media error corresponds to

`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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

): 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