diff --git a/CHANGELOG.md b/CHANGELOG.md index db67d10d7a..70e3426d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Do not display a warning message when invoking `meetingSession.audioVideo.setVideoCodecSendPreferences` prior to the start of the session. +- Prevent video processing with filters from being throttled when an attendees meeting tab moves into the background. ## [3.16.0] - 2023-06-26 diff --git a/src/videoframeprocessor/DefaultVideoFrameProcessorPipeline.ts b/src/videoframeprocessor/DefaultVideoFrameProcessorPipeline.ts index 8500e2a727..dcef691dc0 100644 --- a/src/videoframeprocessor/DefaultVideoFrameProcessorPipeline.ts +++ b/src/videoframeprocessor/DefaultVideoFrameProcessorPipeline.ts @@ -45,8 +45,52 @@ export default class DefaultVideoFrameProcessorPipeline implements VideoFramePro private hasStarted: boolean = false; private lastTimeOut: ReturnType | undefined; + // Timer on worker thread to overcome background throttling + private workerTimer: Worker; + private isWorkerTimerSupported: boolean = true; - constructor(private logger: Logger, private stages: VideoFrameProcessor[]) {} + constructor(private logger: Logger, private stages: VideoFrameProcessor[]) { + this.workerTimer = this.createWorkerTimer(); + } + + /** + * Create a timer that exists within a web worker. This timer will be used to + * retrigger the process call whenever time expires. + */ + private createWorkerTimer(): Worker { + try { + // Blob representing a script that will start a timer for the length + // of the message posted to it. After timer expiration, it will post + // a message back to the main thread holding the timerID + const timerBlob = new Blob( + [ + `self.onmessage = async function(e){ + var timerID = null; + const awaitTimeout = delay => new Promise( resolve => { + timerID = setTimeout(resolve, delay); + }) + await awaitTimeout(e.data); + postMessage(timerID); + }`, + ], + { type: 'application/javascript' } + ); + // Create the worker and link our process call to execute on + // every message it posts + const worker = new Worker(window.URL.createObjectURL(timerBlob)); + worker.onmessage = event => { + // Store used timer so we can clear it during device closure + this.lastTimeOut = event.data; + // Reprocess a new frame on every timer completion + this.process(event); + }; + return worker; + // If blob: is not passed as a worker-src to csp, then the + // worker timer will fail... therefore no support + } catch (error) { + this.isWorkerTimerSupported = false; + } + } destroy(): void { this.stop(); @@ -55,6 +99,7 @@ export default class DefaultVideoFrameProcessorPipeline implements VideoFramePro stage.destroy(); } } + this.workerTimer?.terminate(); } get framerate(): number { @@ -286,9 +331,14 @@ export default class DefaultVideoFrameProcessorPipeline implements VideoFramePro }); } - // TODO: use requestAnimationFrame which is more organic and allows browser to conserve resources by its choices. - /* @ts-ignore */ - this.lastTimeOut = setTimeout(this.process, nextFrameDelay); + // Post frame delay as message to worker timer to maintain desired fps + if (this.isWorkerTimerSupported) { + this.workerTimer.postMessage(nextFrameDelay); + // If the worker isn't supported, use the timer on main thread + } else { + /* @ts-ignore */ + this.lastTimeOut = setTimeout(this.process, nextFrameDelay); + } }; private forEachObserver( diff --git a/test/dommock/DOMMockBuilder.ts b/test/dommock/DOMMockBuilder.ts index 0def9770f5..74ad87c150 100644 --- a/test/dommock/DOMMockBuilder.ts +++ b/test/dommock/DOMMockBuilder.ts @@ -1046,6 +1046,7 @@ export default class DOMMockBuilder { } }; + GlobalAny.Blob = DOMBlobMock; GlobalAny.matchMedia = function mockMatchMedia(_query: string): MediaQueryList { return new GlobalAny.MediaQueryList(); }; diff --git a/test/videoframeprocessor/DefaultVideoFrameProcessorPipeline.test.ts b/test/videoframeprocessor/DefaultVideoFrameProcessorPipeline.test.ts index 7913423e88..7a62c2dbbb 100644 --- a/test/videoframeprocessor/DefaultVideoFrameProcessorPipeline.test.ts +++ b/test/videoframeprocessor/DefaultVideoFrameProcessorPipeline.test.ts @@ -70,398 +70,428 @@ describe('DefaultVideoFrameProcessorPipeline', () => { domMockBuilder.cleanup(); }); - describe('construction', () => { - it('can be constructed', () => { - assert.exists(pipe); + describe('using worker thread timer', () => { + beforeEach(() => { + const dummyProcessID = 2; + const dummyAwaitTime = 100; + globalThis.Worker = (class MockWorkerTimer { + constructor(_stringUrl: string) { + this.onmessage = () => {}; + } + // @ts-ignore + onmessage(_msg: { data: number }): void {} + terminate(): void {} + // @ts-ignore + private async postMessage(_msg: { data: number }): void { + await new Promise(resolve => setTimeout(resolve, dummyAwaitTime)); + this.onmessage({ data: dummyProcessID }); + } + } as unknown) as typeof Worker; }); + executeDefaultVideoFrameProcessorPipelineTestSuite(); }); - describe('setInputMediaStream', () => { - it('can set the input', async () => { - await pipe.setInputMediaStream(mockVideoStream); - const outputStream = await pipe.getInputMediaStream(); - expect(outputStream.id).to.equal(mockStreamId); - await pipe.setInputMediaStream(null); - }); - - it('catches the failure if videoInput play() fails due to load() being called before play() is finished', async () => { - domMockBehavior.videoElementShouldFail = true; - await pipe.setInputMediaStream(mockVideoStream); - await pipe.setInputMediaStream(null); - }); - - it('can only set MediaStream with video tracks', async () => { - const emptyStream = new MediaStream(); - await pipe.setInputMediaStream(emptyStream); - const outputStream = await pipe.getInputMediaStream(); - expect(outputStream).to.equal(null); - await pipe.setInputMediaStream(null); - }); - - it('can stop the pipeline multiple times', async () => { - await pipe.setInputMediaStream(null); - const outputStream = await pipe.getInputMediaStream(); - expect(outputStream).to.equal(null); - await pipe.setInputMediaStream(null); - }); - - it('can start the pipeline with valid stream and stop with null', async () => { - await pipe.setInputMediaStream(mockVideoStream); - await pipe.setInputMediaStream(null); - }); - - it('can start the pipeline with valid stream and stop with null', async () => { - const pipeObserver = new MockObserver(); - pipe.addObserver(pipeObserver); - - const startCallback = called(pipeObserver.processingDidStart); - const stopCallback = called(pipeObserver.processingDidStop); - await pipe.setInputMediaStream(mockVideoStream); - await startCallback; - - await pipe.setInputMediaStream(null); - await stopCallback; - }); - - it('can start the pipeline with valid stream and dumb processor and stop with null', async () => { - class DummyProcessor extends NoOpVideoFrameProcessor { - width = 0; - height = 0; - canvas = document.createElement('canvas'); - process(_buffers: VideoFrameBuffer[]): Promise { - this.canvas.width = this.width; - this.canvas.height = this.height; - this.width += 1; - this.height += 1; - return Promise.resolve([new CanvasVideoFrameBuffer(this.canvas)]); - } - } - const pipeObserver = new MockObserver(); - - const startCallback = called(pipeObserver.processingDidStart); - const stopCallback = called(pipeObserver.processingDidStop); - - const procs = [new DummyProcessor()]; - pipe.processors = procs; - pipe.addObserver(pipeObserver); - await pipe.setInputMediaStream(mockVideoStream); - await startCallback; - await pipe.setInputMediaStream(null); - await stopCallback; + describe('using main thread timer', () => { + beforeEach(() => { + globalThis.Worker = null; }); + executeDefaultVideoFrameProcessorPipelineTestSuite(); + }); - it('can fail to start pipeline and fire callback if buffers are destroyed', async () => { - class DummyProcessor implements VideoFrameProcessor { - destroy(): Promise { - return; - } - width = 0; - height = 0; - canvas = document.createElement('canvas'); - process(_buffers: VideoFrameBuffer[]): Promise { - this.canvas.width = this.width; - this.canvas.height = this.height; - this.width += 1; - this.height += 1; - const buffer = new CanvasVideoFrameBuffer(this.canvas); - buffer.destroy(); - return Promise.resolve([buffer]); - } - } - - class EmptyMockObserver implements VideoFrameProcessorPipelineObserver {} - const pipeObserver = new MockObserver(); - const pipeObserver2 = new EmptyMockObserver(); - - const failToStartCallback = called(pipeObserver.processingDidFailToStart); - - const procs = [new DummyProcessor()]; - pipe.processors = procs; - pipe.addObserver(pipeObserver); - pipe.addObserver(pipeObserver2); - await pipe.setInputMediaStream(mockVideoStream); - await failToStartCallback; + function executeDefaultVideoFrameProcessorPipelineTestSuite(): void { + describe('construction', () => { + it('can be constructed', () => { + assert.exists(pipe); + }); }); - it('can fail to start pipeline and fire callback if buffers are destroyed', async () => { - class DummyProcessor implements VideoFrameProcessor { - destroy(): Promise { - return; + describe('setInputMediaStream', () => { + it('can set the input', async () => { + await pipe.setInputMediaStream(mockVideoStream); + const outputStream = await pipe.getInputMediaStream(); + expect(outputStream.id).to.equal(mockStreamId); + await pipe.setInputMediaStream(null); + }); + + it('catches the failure if videoInput play() fails due to load() being called before play() is finished', async () => { + domMockBehavior.videoElementShouldFail = true; + await pipe.setInputMediaStream(mockVideoStream); + await pipe.setInputMediaStream(null); + }); + + it('can only set MediaStream with video tracks', async () => { + const emptyStream = new MediaStream(); + await pipe.setInputMediaStream(emptyStream); + const outputStream = await pipe.getInputMediaStream(); + expect(outputStream).to.equal(null); + await pipe.setInputMediaStream(null); + }); + + it('can stop the pipeline multiple times', async () => { + await pipe.setInputMediaStream(null); + const outputStream = await pipe.getInputMediaStream(); + expect(outputStream).to.equal(null); + await pipe.setInputMediaStream(null); + }); + + it('can start the pipeline with valid stream and stop with null', async () => { + await pipe.setInputMediaStream(mockVideoStream); + await pipe.setInputMediaStream(null); + }); + + it('can start the pipeline with valid stream and stop with null', async () => { + const pipeObserver = new MockObserver(); + pipe.addObserver(pipeObserver); + + const startCallback = called(pipeObserver.processingDidStart); + const stopCallback = called(pipeObserver.processingDidStop); + await pipe.setInputMediaStream(mockVideoStream); + await startCallback; + + await pipe.setInputMediaStream(null); + await stopCallback; + }); + + it('can start the pipeline with valid stream and dumb processor and stop with null', async () => { + class DummyProcessor extends NoOpVideoFrameProcessor { + width = 0; + height = 0; + canvas = document.createElement('canvas'); + process(_buffers: VideoFrameBuffer[]): Promise { + this.canvas.width = this.width; + this.canvas.height = this.height; + this.width += 1; + this.height += 1; + return Promise.resolve([new CanvasVideoFrameBuffer(this.canvas)]); + } } - width = 1280; - height = 720; - count = 0; - canvas = document.createElement('canvas'); - process(_buffers: VideoFrameBuffer[]): Promise { - this.canvas.width = this.width; - this.canvas.height = this.height; - this.count += 1; - const buffer = new CanvasVideoFrameBuffer(this.canvas); - if (this.count === 5) { + const pipeObserver = new MockObserver(); + + const startCallback = called(pipeObserver.processingDidStart); + const stopCallback = called(pipeObserver.processingDidStop); + + const procs = [new DummyProcessor()]; + pipe.processors = procs; + pipe.addObserver(pipeObserver); + await pipe.setInputMediaStream(mockVideoStream); + await startCallback; + await pipe.setInputMediaStream(null); + await stopCallback; + }); + + it('can fail to start pipeline and fire callback if buffers are destroyed', async () => { + class DummyProcessor implements VideoFrameProcessor { + destroy(): Promise { + return; + } + width = 0; + height = 0; + canvas = document.createElement('canvas'); + process(_buffers: VideoFrameBuffer[]): Promise { + this.canvas.width = this.width; + this.canvas.height = this.height; + this.width += 1; + this.height += 1; + const buffer = new CanvasVideoFrameBuffer(this.canvas); buffer.destroy(); + return Promise.resolve([buffer]); } - return Promise.resolve([buffer]); } - } - class EmptyMockObserver implements VideoFrameProcessorPipelineObserver {} - const pipeObserver = new MockObserver(); - const pipeObserver2 = new EmptyMockObserver(); + class EmptyMockObserver implements VideoFrameProcessorPipelineObserver {} + const pipeObserver = new MockObserver(); + const pipeObserver2 = new EmptyMockObserver(); - const failToStartCallback = called(pipeObserver.processingDidFailToStart); + const failToStartCallback = called(pipeObserver.processingDidFailToStart); - const procs = [new DummyProcessor()]; - pipe.processors = procs; - pipe.addObserver(pipeObserver); - pipe.addObserver(pipeObserver2); - await pipe.setInputMediaStream(mockVideoStream); - await failToStartCallback; - }); + const procs = [new DummyProcessor()]; + pipe.processors = procs; + pipe.addObserver(pipeObserver); + pipe.addObserver(pipeObserver2); + await pipe.setInputMediaStream(mockVideoStream); + await failToStartCallback; + }); - it('execute callbacks', async () => { - await pipe.process(null); - }); - }); + it('can fail to start pipeline and fire callback if buffers are destroyed', async () => { + class DummyProcessor implements VideoFrameProcessor { + destroy(): Promise { + return; + } + width = 1280; + height = 720; + count = 0; + canvas = document.createElement('canvas'); + process(_buffers: VideoFrameBuffer[]): Promise { + this.canvas.width = this.width; + this.canvas.height = this.height; + this.count += 1; + const buffer = new CanvasVideoFrameBuffer(this.canvas); + if (this.count === 5) { + buffer.destroy(); + } + return Promise.resolve([buffer]); + } + } - describe('getInputMediaStream', () => { - it('can get the input', async () => { - let inputStream = await pipe.getInputMediaStream(); - expect(inputStream).to.be.null; + class EmptyMockObserver implements VideoFrameProcessorPipelineObserver {} + const pipeObserver = new MockObserver(); + const pipeObserver2 = new EmptyMockObserver(); - await pipe.setInputMediaStream(mockVideoStream); - inputStream = await pipe.getInputMediaStream(); - expect(inputStream.id).to.equal(mockStreamId); - await pipe.setInputMediaStream(null); - }); - }); + const failToStartCallback = called(pipeObserver.processingDidFailToStart); - describe('getActiveOutputMediaStream', () => { - it('can get an active output stream', async () => { - const activeStream = new MediaStream(); - // @ts-ignore - activeStream.active = true; - domMockBehavior.createElementCaptureStream = activeStream; - const outputStream = pipe.getActiveOutputMediaStream(); - expect(outputStream).to.deep.equal(activeStream); - // disable the output stream to trigger a recapture - // @ts-ignore - activeStream.active = false; - const activeStream2 = new MediaStream(); - // @ts-ignore - activeStream2.active = true; - domMockBehavior.createElementCaptureStream = activeStream2; - expect(pipe.getActiveOutputMediaStream()).to.deep.equal(activeStream2); - }); + const procs = [new DummyProcessor()]; + pipe.processors = procs; + pipe.addObserver(pipeObserver); + pipe.addObserver(pipeObserver2); + await pipe.setInputMediaStream(mockVideoStream); + await failToStartCallback; + }); - it('can get the same output stream', async () => { - const activeStream = new MediaStream(); - // @ts-ignore - activeStream.active = true; - domMockBehavior.createElementCaptureStream = activeStream; - const outputStream = pipe.getActiveOutputMediaStream(); - const outputStream2 = pipe.getActiveOutputMediaStream(); - expect(outputStream2).to.deep.equal(outputStream); + it('execute callbacks', async () => { + await pipe.process(null); + }); }); - it('can clone audio tracks', async () => { - const activeStream = new MediaStream(); - // @ts-ignore - activeStream.active = true; - domMockBehavior.createElementCaptureStream = activeStream; - - const inputStream = new MediaStream(); - const videoTrack = new MediaStreamTrack(); - // @ts-ignore - videoTrack.kind = 'video'; - inputStream.addTrack(videoTrack); - const audioTrack = new MediaStreamTrack(); - // @ts-ignore - audioTrack.kind = 'audio'; - inputStream.addTrack(audioTrack); - // @ts-ignore - inputStream.active = true; - await pipe.setInputMediaStream(inputStream); - - const outputStream = pipe.getActiveOutputMediaStream(); - expect(outputStream.getAudioTracks().length).to.equal(2); - }); - }); + describe('getInputMediaStream', () => { + it('can get the input', async () => { + let inputStream = await pipe.getInputMediaStream(); + expect(inputStream).to.be.null; - describe('accessor framerate', () => { - it('getter can return the frame rate', () => { - expect(pipe.framerate).to.equal(15); - pipe.framerate = 30; - expect(pipe.framerate).to.equal(30); + await pipe.setInputMediaStream(mockVideoStream); + inputStream = await pipe.getInputMediaStream(); + expect(inputStream.id).to.equal(mockStreamId); + await pipe.setInputMediaStream(null); + }); }); - it('setter can set the frame rate', () => { - pipe.framerate = 30; - expect(pipe.framerate).to.equal(30); + describe('getActiveOutputMediaStream', () => { + it('can get an active output stream', async () => { + const activeStream = new MediaStream(); + // @ts-ignore + activeStream.active = true; + domMockBehavior.createElementCaptureStream = activeStream; + const outputStream = pipe.getActiveOutputMediaStream(); + expect(outputStream).to.deep.equal(activeStream); + // disable the output stream to trigger a recapture + // @ts-ignore + activeStream.active = false; + const activeStream2 = new MediaStream(); + // @ts-ignore + activeStream2.active = true; + domMockBehavior.createElementCaptureStream = activeStream2; + expect(pipe.getActiveOutputMediaStream()).to.deep.equal(activeStream2); + }); + + it('can get the same output stream', async () => { + const activeStream = new MediaStream(); + // @ts-ignore + activeStream.active = true; + domMockBehavior.createElementCaptureStream = activeStream; + const outputStream = pipe.getActiveOutputMediaStream(); + const outputStream2 = pipe.getActiveOutputMediaStream(); + expect(outputStream2).to.deep.equal(outputStream); + }); + + it('can clone audio tracks', async () => { + const activeStream = new MediaStream(); + // @ts-ignore + activeStream.active = true; + domMockBehavior.createElementCaptureStream = activeStream; + + const inputStream = new MediaStream(); + const videoTrack = new MediaStreamTrack(); + // @ts-ignore + videoTrack.kind = 'video'; + inputStream.addTrack(videoTrack); + const audioTrack = new MediaStreamTrack(); + // @ts-ignore + audioTrack.kind = 'audio'; + inputStream.addTrack(audioTrack); + // @ts-ignore + inputStream.active = true; + await pipe.setInputMediaStream(inputStream); + + const outputStream = pipe.getActiveOutputMediaStream(); + expect(outputStream.getAudioTracks().length).to.equal(2); + }); }); - it('setter ignores frame rate less than 0', () => { - pipe.framerate = -5; - expect(pipe.framerate).to.equal(15); + describe('accessor framerate', () => { + it('getter can return the frame rate', () => { + expect(pipe.framerate).to.equal(15); + pipe.framerate = 30; + expect(pipe.framerate).to.equal(30); + }); + + it('setter can set the frame rate', () => { + pipe.framerate = 30; + expect(pipe.framerate).to.equal(30); + }); + + it('setter ignores frame rate less than 0', () => { + pipe.framerate = -5; + expect(pipe.framerate).to.equal(15); + }); }); - }); - describe('addObserver', () => { - it('can add observer', () => { - const pipeObserver = new MockObserver(); - pipe.addObserver(pipeObserver); + describe('addObserver', () => { + it('can add observer', () => { + const pipeObserver = new MockObserver(); + pipe.addObserver(pipeObserver); + }); }); - }); - describe('removeObserver', () => { - it('can remove observer', () => { - const pipeObserver = new MockObserver(); - pipe.addObserver(pipeObserver); - pipe.removeObserver(pipeObserver); + describe('removeObserver', () => { + it('can remove observer', () => { + const pipeObserver = new MockObserver(); + pipe.addObserver(pipeObserver); + pipe.removeObserver(pipeObserver); + }); }); - }); - describe('setter processors', () => { - it('can set the input processors', async () => { - class NullProcessor implements VideoFrameProcessor { - destroy(): Promise { - return Promise.resolve(); - } - process(_buffers: VideoFrameBuffer[]): Promise { - return Promise.resolve(null); + describe('setter processors', () => { + it('can set the input processors', async () => { + class NullProcessor implements VideoFrameProcessor { + destroy(): Promise { + return Promise.resolve(); + } + process(_buffers: VideoFrameBuffer[]): Promise { + return Promise.resolve(null); + } } - } - const procs = [new NoOpVideoFrameProcessor(), new NullProcessor()]; - pipe.processors = procs; - }); + const procs = [new NoOpVideoFrameProcessor(), new NullProcessor()]; + pipe.processors = procs; + }); - it('can set the processor and fail to start due to errors', async () => { - class PipeObserver implements VideoFrameProcessorPipelineObserver { - processingDidFailToStart = stub(); - } + it('can set the processor and fail to start due to errors', async () => { + class PipeObserver implements VideoFrameProcessorPipelineObserver { + processingDidFailToStart = stub(); + } - class PipeObserver2 implements VideoFrameProcessorPipelineObserver { - processingDidStart = stub(); - } - const pipeObserver = new PipeObserver(); - const pipeObserver2 = new PipeObserver2(); + class PipeObserver2 implements VideoFrameProcessorPipelineObserver { + processingDidStart = stub(); + } + const pipeObserver = new PipeObserver(); + const pipeObserver2 = new PipeObserver2(); - pipe.addObserver(pipeObserver); - pipe.addObserver(pipeObserver2); + pipe.addObserver(pipeObserver); + pipe.addObserver(pipeObserver2); - class WrongProcessor implements VideoFrameProcessor { - destroy(): Promise { - return Promise.resolve(); - } - process(_buffers: VideoFrameBuffer[]): Promise { - throw new Error('Method not implemented.'); + class WrongProcessor implements VideoFrameProcessor { + destroy(): Promise { + return Promise.resolve(); + } + process(_buffers: VideoFrameBuffer[]): Promise { + throw new Error('Method not implemented.'); + } } - } - const failToStartCallback = called(pipeObserver.processingDidFailToStart); + const failToStartCallback = called(pipeObserver.processingDidFailToStart); - const procs = [new WrongProcessor()]; - pipe.processors = procs; - await pipe.setInputMediaStream(mockVideoStream); - await failToStartCallback; - }); + const procs = [new WrongProcessor()]; + pipe.processors = procs; + await pipe.setInputMediaStream(mockVideoStream); + await failToStartCallback; + }); - it('can set slow processor and fires processingLatencyTooHigh', async () => { - class PipeObserver2 implements VideoFrameProcessorPipelineObserver { - processingDidStart = sinon.stub(); - } + it('can set slow processor and fires processingLatencyTooHigh', async () => { + class PipeObserver2 implements VideoFrameProcessorPipelineObserver { + processingDidStart = sinon.stub(); + } - const pipeObserver = new MockObserver(); - const pipeObserver2 = new PipeObserver2(); + const pipeObserver = new MockObserver(); + const pipeObserver2 = new PipeObserver2(); - const latencyCallback = called(pipeObserver.processingLatencyTooHigh); - const startCallback = called(pipeObserver.processingDidStart); + const latencyCallback = called(pipeObserver.processingLatencyTooHigh); + const startCallback = called(pipeObserver.processingDidStart); - pipe.addObserver(pipeObserver); - pipe.addObserver(pipeObserver2); + pipe.addObserver(pipeObserver); + pipe.addObserver(pipeObserver2); - class WrongProcessor implements VideoFrameProcessor { - destroy(): Promise { - return Promise.resolve(); - } - async process(buffers: VideoFrameBuffer[]): Promise { - await new Promise(resolve => setTimeout(resolve, (1000 / 15) * 3)); - return buffers; + class WrongProcessor implements VideoFrameProcessor { + destroy(): Promise { + return Promise.resolve(); + } + async process(buffers: VideoFrameBuffer[]): Promise { + await new Promise(resolve => setTimeout(resolve, (1000 / 15) * 3)); + return buffers; + } } - } - const procs = [new WrongProcessor()]; - pipe.processors = procs; - - await pipe.setInputMediaStream(mockVideoStream); - await Promise.all([startCallback, latencyCallback]); + const procs = [new WrongProcessor()]; + pipe.processors = procs; - await pipe.setInputMediaStream(null); - }); - }); + await pipe.setInputMediaStream(mockVideoStream); + await Promise.all([startCallback, latencyCallback]); - describe('getter processors', () => { - it('can get processors', () => { - const procs = [new NoOpVideoFrameProcessor()]; - pipe.processors = procs; - expect(pipe.processors).to.deep.equal(procs); + await pipe.setInputMediaStream(null); + }); }); - }); - - describe('stop', () => { - it('can stop the processing', async () => { - const obs = new MockObserver(); - const startCallback = called(obs.processingDidStart); - const stopCallback = called(obs.processingDidStop); - const procs = [new NoOpVideoFrameProcessor()]; - pipe.processors = procs; - pipe.addObserver(obs); - await pipe.setInputMediaStream(mockVideoStream); - - await startCallback; - pipe.stop(); - await stopCallback; + describe('getter processors', () => { + it('can get processors', () => { + const procs = [new NoOpVideoFrameProcessor()]; + pipe.processors = procs; + expect(pipe.processors).to.deep.equal(procs); + }); }); - }); - - describe('destroy', () => { - it('can stop the processing', async () => { - const obs = new MockObserver(); - - const startCallback = called(obs.processingDidStart); - const stopCallback = called(obs.processingDidStop); - const procs = [new NoOpVideoFrameProcessor()]; - pipe.processors = procs; - pipe.addObserver(obs); - await pipe.setInputMediaStream(mockVideoStream); - await startCallback; - pipe.destroy(); - await stopCallback; + describe('stop', () => { + it('can stop the processing', async () => { + const obs = new MockObserver(); + const startCallback = called(obs.processingDidStart); + const stopCallback = called(obs.processingDidStop); + + const procs = [new NoOpVideoFrameProcessor()]; + pipe.processors = procs; + pipe.addObserver(obs); + await pipe.setInputMediaStream(mockVideoStream); + + await startCallback; + pipe.stop(); + await stopCallback; + }); }); - it('can destroy processors if they exist', async () => { - // clean up processor as well - pipe.processors = null; - pipe.destroy(); - - class MockProcessor extends NoOpVideoFrameProcessor { - destroy = stub(); - } + describe('destroy', () => { + it('can stop the processing', async () => { + const obs = new MockObserver(); + + const startCallback = called(obs.processingDidStart); + const stopCallback = called(obs.processingDidStop); + + const procs = [new NoOpVideoFrameProcessor()]; + pipe.processors = procs; + pipe.addObserver(obs); + await pipe.setInputMediaStream(mockVideoStream); + await startCallback; + pipe.destroy(); + await stopCallback; + }); + + it('can destroy processors if they exist', async () => { + // clean up processor as well + pipe.processors = null; + pipe.destroy(); + + class MockProcessor extends NoOpVideoFrameProcessor { + destroy = stub(); + } - // setting up - const obs = new MockObserver(); - const startCallback = called(obs.processingDidStart); - const stopCallback = called(obs.processingDidStop); + // setting up + const obs = new MockObserver(); + const startCallback = called(obs.processingDidStart); + const stopCallback = called(obs.processingDidStop); - const procs = [new MockProcessor()]; - pipe.processors = procs; - pipe.addObserver(obs); - await pipe.setInputMediaStream(mockVideoStream); + const procs = [new MockProcessor()]; + pipe.processors = procs; + pipe.addObserver(obs); + await pipe.setInputMediaStream(mockVideoStream); - await startCallback; + await startCallback; - pipe.destroy(); - await stopCallback; - expect(procs[0].destroy.called).to.equal(true); + pipe.destroy(); + await stopCallback; + expect(procs[0].destroy.called).to.equal(true); + }); }); - }); + } });