UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

971 lines (970 loc) 38.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const flatbuffers = require("flatbuffers"); const mediasoup = require("../"); const enhancedEvents_1 = require("../enhancedEvents"); const errors_1 = require("../errors"); const utils = require("../utils"); const notification_1 = require("../fbs/notification"); const FbsConsumer = require("../fbs/consumer"); const ctx = { mediaCodecs: utils.deepFreeze([ { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2, parameters: { foo: 'bar', }, }, { kind: 'video', mimeType: 'video/VP8', clockRate: 90000, }, { kind: 'video', mimeType: 'video/H264', clockRate: 90000, parameters: { 'level-asymmetry-allowed': 1, 'packetization-mode': 1, 'profile-level-id': '4d0032', foo: 'bar', }, }, ]), audioProducerOptions: utils.deepFreeze({ kind: 'audio', rtpParameters: { mid: 'AUDIO', codecs: [ { mimeType: 'audio/opus', payloadType: 111, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, usedtx: 1, foo: 222.222, bar: '333', }, }, ], headerExtensions: [ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, }, { uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', id: 12, }, ], encodings: [{ ssrc: 11111111 }], rtcp: { cname: 'FOOBAR', }, }, appData: { foo: 1, bar: '2' }, }), videoProducerOptions: utils.deepFreeze({ kind: 'video', rtpParameters: { mid: 'VIDEO', codecs: [ { mimeType: 'video/h264', payloadType: 112, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'nack', parameter: 'pli' }, { type: 'goog-remb', parameter: '' }, ], }, { mimeType: 'video/rtx', payloadType: 113, clockRate: 90000, parameters: { apt: 112 }, }, ], headerExtensions: [ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, }, { uri: 'urn:3gpp:video-orientation', id: 13, }, ], encodings: [ { ssrc: 22222222, scalabilityMode: 'L1T5', rtx: { ssrc: 22222223 } }, { ssrc: 22222224, scalabilityMode: 'L1T5', rtx: { ssrc: 22222225 } }, { ssrc: 22222226, scalabilityMode: 'L1T5', rtx: { ssrc: 22222227 } }, { ssrc: 22222228, scalabilityMode: 'L1T5', rtx: { ssrc: 22222229 } }, ], rtcp: { cname: 'FOOBAR', }, }, appData: { foo: 1, bar: '2' }, }), consumerDeviceCapabilities: utils.deepFreeze({ codecs: [ { mimeType: 'audio/opus', kind: 'audio', preferredPayloadType: 100, clockRate: 48000, channels: 2, rtcpFeedback: [{ type: 'nack', parameter: '' }], }, { mimeType: 'video/H264', kind: 'video', preferredPayloadType: 101, clockRate: 90000, parameters: { 'level-asymmetry-allowed': 1, 'packetization-mode': 1, 'profile-level-id': '4d0032', }, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb', parameter: '' }, ], }, { mimeType: 'video/rtx', kind: 'video', preferredPayloadType: 102, clockRate: 90000, parameters: { apt: 101, }, rtcpFeedback: [], }, ], headerExtensions: [ { kind: 'audio', uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', preferredId: 1, preferredEncrypt: false, }, { kind: 'video', uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', preferredId: 1, preferredEncrypt: false, }, { kind: 'video', uri: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', preferredId: 2, preferredEncrypt: false, }, { kind: 'audio', uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', // eslint-disable-line max-len preferredId: 4, preferredEncrypt: false, }, { kind: 'video', uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', // eslint-disable-line max-len preferredId: 4, preferredEncrypt: false, }, { kind: 'audio', uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', preferredId: 10, preferredEncrypt: false, }, { kind: 'video', uri: 'urn:3gpp:video-orientation', preferredId: 11, preferredEncrypt: false, }, { kind: 'video', uri: 'urn:ietf:params:rtp-hdrext:toffset', preferredId: 12, preferredEncrypt: false, }, ], }), }; beforeEach(async () => { ctx.worker = await mediasoup.createWorker(); ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); ctx.webRtcTransport1 = await ctx.router.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); ctx.webRtcTransport2 = await ctx.router.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); ctx.audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); ctx.videoProducer = await ctx.webRtcTransport1.produce(ctx.videoProducerOptions); }); afterEach(async () => { ctx.worker?.close(); if (ctx.worker?.subprocessClosed === false) { await (0, enhancedEvents_1.enhancedOnce)(ctx.worker, 'subprocessclose'); } }); test('transport.consume() succeeds', async () => { const onObserverNewConsumer1 = jest.fn(); ctx.webRtcTransport2.observer.once('newconsumer', onObserverNewConsumer1); expect(ctx.router.canConsume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, })).toBe(true); const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, appData: { baz: 'LOL' }, }); expect(onObserverNewConsumer1).toHaveBeenCalledTimes(1); expect(onObserverNewConsumer1).toHaveBeenCalledWith(audioConsumer); expect(typeof audioConsumer.id).toBe('string'); expect(audioConsumer.producerId).toBe(ctx.audioProducer.id); expect(audioConsumer.closed).toBe(false); expect(audioConsumer.kind).toBe('audio'); expect(typeof audioConsumer.rtpParameters).toBe('object'); expect(audioConsumer.rtpParameters.mid).toBe('0'); expect(audioConsumer.rtpParameters.codecs.length).toBe(1); expect(audioConsumer.rtpParameters.codecs[0]).toEqual({ mimeType: 'audio/opus', payloadType: 100, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, usedtx: 1, foo: 222.222, bar: '333', }, rtcpFeedback: [], }); expect(audioConsumer.type).toBe('simple'); expect(audioConsumer.paused).toBe(false); expect(audioConsumer.producerPaused).toBe(false); expect(audioConsumer.priority).toBe(1); expect(audioConsumer.score).toEqual({ score: 10, producerScore: 0, producerScores: [0], }); expect(audioConsumer.preferredLayers).toBeUndefined(); expect(audioConsumer.currentLayers).toBeUndefined(); expect(audioConsumer.appData).toEqual({ baz: 'LOL' }); const dump1 = await ctx.router.dump(); expect(dump1.mapProducerIdConsumerIds).toEqual(expect.arrayContaining([ { key: ctx.audioProducer.id, values: [audioConsumer.id] }, ])); expect(dump1.mapConsumerIdProducerId).toEqual(expect.arrayContaining([ { key: audioConsumer.id, value: ctx.audioProducer.id }, ])); await expect(ctx.webRtcTransport2.dump()).resolves.toMatchObject({ id: ctx.webRtcTransport2.id, producerIds: [], consumerIds: [audioConsumer.id], }); const onObserverNewConsumer2 = jest.fn(); ctx.webRtcTransport2.observer.once('newconsumer', onObserverNewConsumer2); expect(ctx.router.canConsume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, })).toBe(true); // Pause videoProducer. await ctx.videoProducer.pause(); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, paused: true, preferredLayers: { spatialLayer: 12, temporalLayer: 0 }, appData: { baz: 'LOL' }, }); expect(onObserverNewConsumer2).toHaveBeenCalledTimes(1); expect(onObserverNewConsumer2).toHaveBeenCalledWith(videoConsumer); expect(typeof videoConsumer.id).toBe('string'); expect(videoConsumer.producerId).toBe(ctx.videoProducer.id); expect(videoConsumer.closed).toBe(false); expect(videoConsumer.kind).toBe('video'); expect(typeof videoConsumer.rtpParameters).toBe('object'); expect(videoConsumer.rtpParameters.mid).toBe('1'); expect(videoConsumer.rtpParameters.codecs.length).toBe(2); expect(videoConsumer.rtpParameters.codecs[0]).toEqual({ mimeType: 'video/H264', payloadType: 103, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb', parameter: '' }, ], }); expect(videoConsumer.rtpParameters.codecs[1]).toEqual({ mimeType: 'video/rtx', payloadType: 104, clockRate: 90000, parameters: { apt: 103 }, rtcpFeedback: [], }); expect(videoConsumer.type).toBe('simulcast'); expect(videoConsumer.paused).toBe(true); expect(videoConsumer.producerPaused).toBe(true); expect(videoConsumer.priority).toBe(1); expect(videoConsumer.score).toEqual({ score: 10, producerScore: 0, producerScores: [0, 0, 0, 0], }); expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 3, temporalLayer: 0, }); expect(videoConsumer.currentLayers).toBeUndefined(); expect(videoConsumer.appData).toEqual({ baz: 'LOL' }); const onObserverNewConsumer3 = jest.fn(); ctx.webRtcTransport2.observer.once('newconsumer', onObserverNewConsumer3); expect(ctx.router.canConsume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, })).toBe(true); const videoPipeConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, pipe: true, }); expect(onObserverNewConsumer3).toHaveBeenCalledTimes(1); expect(onObserverNewConsumer3).toHaveBeenCalledWith(videoPipeConsumer); expect(typeof videoPipeConsumer.id).toBe('string'); expect(videoPipeConsumer.producerId).toBe(ctx.videoProducer.id); expect(videoPipeConsumer.closed).toBe(false); expect(videoPipeConsumer.kind).toBe('video'); expect(typeof videoPipeConsumer.rtpParameters).toBe('object'); expect(videoPipeConsumer.rtpParameters.mid).toBeUndefined(); expect(videoPipeConsumer.rtpParameters.codecs.length).toBe(2); expect(videoPipeConsumer.rtpParameters.codecs[0]).toEqual({ mimeType: 'video/H264', payloadType: 103, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb', parameter: '' }, ], }); expect(videoPipeConsumer.rtpParameters.codecs[1]).toEqual({ mimeType: 'video/rtx', payloadType: 104, clockRate: 90000, parameters: { apt: 103 }, rtcpFeedback: [], }); expect(videoPipeConsumer.type).toBe('pipe'); expect(videoPipeConsumer.paused).toBe(false); expect(videoPipeConsumer.producerPaused).toBe(true); expect(videoPipeConsumer.priority).toBe(1); expect(videoPipeConsumer.score).toEqual({ score: 10, producerScore: 10, producerScores: [0, 0, 0, 0], }); expect(videoPipeConsumer.preferredLayers).toBeUndefined(); expect(videoPipeConsumer.currentLayers).toBeUndefined(); expect(videoPipeConsumer.appData).toBeUndefined; const dump2 = await ctx.router.dump(); expect(Array.isArray(dump2.mapProducerIdConsumerIds)).toBe(true); expect(dump2.mapProducerIdConsumerIds).toEqual(expect.arrayContaining([ { key: ctx.audioProducer.id, values: [audioConsumer.id], }, { key: ctx.videoProducer.id, values: expect.arrayContaining([ videoConsumer.id, videoPipeConsumer.id, ]), }, ])); expect(dump2.mapConsumerIdProducerId).toEqual(expect.arrayContaining([ { key: audioConsumer.id, value: ctx.audioProducer.id }, { key: videoConsumer.id, value: ctx.videoProducer.id }, { key: videoPipeConsumer.id, value: ctx.videoProducer.id }, ])); await expect(ctx.webRtcTransport2.dump()).resolves.toMatchObject({ id: ctx.webRtcTransport2.id, producerIds: [], consumerIds: expect.arrayContaining([ audioConsumer.id, videoConsumer.id, videoPipeConsumer.id, ]), }); }, 2000); test('transport.consume() with enableRtx succeeds', async () => { const audioConsumer2 = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, enableRtx: true, }); expect(audioConsumer2.kind).toBe('audio'); expect(audioConsumer2.rtpParameters.codecs.length).toBe(1); expect(audioConsumer2.rtpParameters.codecs[0]).toEqual({ mimeType: 'audio/opus', payloadType: 100, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, usedtx: 1, foo: 222.222, bar: '333', }, rtcpFeedback: [{ type: 'nack', parameter: '' }], }); }, 2000); test('transport.consume() can be created with user provided mid', async () => { const audioConsumer1 = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); expect(audioConsumer1.rtpParameters.mid).toEqual(expect.stringMatching(/^[0-9]+/)); const audioConsumer2 = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, mid: 'custom-mid', rtpCapabilities: ctx.consumerDeviceCapabilities, }); expect(audioConsumer2.rtpParameters.mid).toBe('custom-mid'); const audioConsumer3 = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); expect(audioConsumer3.rtpParameters.mid).toEqual(expect.stringMatching(/^[0-9]+/)); expect(Number(audioConsumer1.rtpParameters.mid) + 1).toBe(Number(audioConsumer3.rtpParameters.mid)); }, 2000); test('transport.consume() with incompatible rtpCapabilities rejects with UnsupportedError', async () => { let invalidDeviceCapabilities; invalidDeviceCapabilities = { codecs: [ { kind: 'audio', mimeType: 'audio/ISAC', preferredPayloadType: 100, clockRate: 32000, channels: 1, }, ], headerExtensions: [], }; expect(ctx.router.canConsume({ producerId: ctx.audioProducer.id, rtpCapabilities: invalidDeviceCapabilities, })).toBe(false); await expect(ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: invalidDeviceCapabilities, })).rejects.toThrow(errors_1.UnsupportedError); invalidDeviceCapabilities = { codecs: [], headerExtensions: [], }; expect(ctx.router.canConsume({ producerId: ctx.audioProducer.id, rtpCapabilities: invalidDeviceCapabilities, })).toBe(false); await expect(ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: invalidDeviceCapabilities, })).rejects.toThrow(errors_1.UnsupportedError); }, 2000); test('consumer.dump() succeeds', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const dump1 = await audioConsumer.dump(); expect(dump1.id).toBe(audioConsumer.id); expect(dump1.producerId).toBe(audioConsumer.producerId); expect(dump1.kind).toBe(audioConsumer.kind); expect(typeof dump1.rtpParameters).toBe('object'); expect(Array.isArray(dump1.rtpParameters.codecs)).toBe(true); expect(dump1.rtpParameters.codecs.length).toBe(1); expect(dump1.rtpParameters.codecs[0].mimeType).toBe('audio/opus'); expect(dump1.rtpParameters.codecs[0].payloadType).toBe(100); expect(dump1.rtpParameters.codecs[0].clockRate).toBe(48000); expect(dump1.rtpParameters.codecs[0].channels).toBe(2); expect(dump1.rtpParameters.codecs[0].parameters).toEqual({ useinbandfec: 1, usedtx: 1, foo: 222.222, bar: '333', }); expect(dump1.rtpParameters.codecs[0].rtcpFeedback).toEqual([]); expect(Array.isArray(dump1.rtpParameters.headerExtensions)).toBe(true); expect(dump1.rtpParameters.headerExtensions.length).toBe(3); expect(dump1.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 1, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', id: 4, parameters: {}, encrypt: false, }, { uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', id: 10, parameters: {}, encrypt: false, }, ]); expect(Array.isArray(dump1.rtpParameters.encodings)).toBe(true); expect(dump1.rtpParameters.encodings.length).toBe(1); expect(dump1.rtpParameters.encodings).toEqual([ expect.objectContaining({ codecPayloadType: 100, ssrc: audioConsumer.rtpParameters.encodings?.[0].ssrc, }), ]); expect(dump1.type).toBe('simple'); expect(Array.isArray(dump1.consumableRtpEncodings)).toBe(true); expect(dump1.consumableRtpEncodings.length).toBe(1); expect(dump1.consumableRtpEncodings).toEqual([ expect.objectContaining({ ssrc: ctx.audioProducer.consumableRtpParameters.encodings?.[0].ssrc, }), ]); expect(dump1.supportedCodecPayloadTypes).toEqual([100]); expect(dump1.paused).toBe(false); expect(dump1.producerPaused).toBe(false); expect(dump1.priority).toBe(1); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, paused: true, }); const dump2 = await videoConsumer.dump(); expect(dump2.id).toBe(videoConsumer.id); expect(dump2.producerId).toBe(videoConsumer.producerId); expect(dump2.kind).toBe(videoConsumer.kind); expect(typeof dump2.rtpParameters).toBe('object'); expect(Array.isArray(dump2.rtpParameters.codecs)).toBe(true); expect(dump2.rtpParameters.codecs.length).toBe(2); expect(dump2.rtpParameters.codecs[0].mimeType).toBe('video/H264'); expect(dump2.rtpParameters.codecs[0].payloadType).toBe(103); expect(dump2.rtpParameters.codecs[0].clockRate).toBe(90000); expect(dump2.rtpParameters.codecs[0].channels).toBeUndefined(); expect(dump2.rtpParameters.codecs[0].parameters).toEqual({ 'packetization-mode': 1, 'profile-level-id': '4d0032', }); expect(dump2.rtpParameters.codecs[0].rtcpFeedback).toEqual([ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, ]); expect(Array.isArray(dump2.rtpParameters.headerExtensions)).toBe(true); expect(dump2.rtpParameters.headerExtensions.length).toBe(4); expect(dump2.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 1, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', id: 4, parameters: {}, encrypt: false, }, { uri: 'urn:3gpp:video-orientation', id: 11, parameters: {}, encrypt: false, }, { uri: 'urn:ietf:params:rtp-hdrext:toffset', id: 12, parameters: {}, encrypt: false, }, ]); expect(Array.isArray(dump2.rtpParameters.encodings)).toBe(true); expect(dump2.rtpParameters.encodings.length).toBe(1); expect(dump2.rtpParameters.encodings).toMatchObject([ { codecPayloadType: 103, ssrc: videoConsumer.rtpParameters.encodings?.[0].ssrc, rtx: { ssrc: videoConsumer.rtpParameters.encodings?.[0].rtx?.ssrc, }, scalabilityMode: 'L4T5', }, ]); expect(Array.isArray(dump2.consumableRtpEncodings)).toBe(true); expect(dump2.consumableRtpEncodings.length).toBe(4); expect(dump2.consumableRtpEncodings[0]).toEqual(expect.objectContaining({ ssrc: ctx.videoProducer.consumableRtpParameters.encodings?.[0].ssrc, scalabilityMode: 'L1T5', })); expect(dump2.consumableRtpEncodings[1]).toEqual(expect.objectContaining({ ssrc: ctx.videoProducer.consumableRtpParameters.encodings?.[1].ssrc, scalabilityMode: 'L1T5', })); expect(dump2.consumableRtpEncodings[2]).toEqual(expect.objectContaining({ ssrc: ctx.videoProducer.consumableRtpParameters.encodings?.[2].ssrc, scalabilityMode: 'L1T5', })); expect(dump2.consumableRtpEncodings[3]).toEqual(expect.objectContaining({ ssrc: ctx.videoProducer.consumableRtpParameters.encodings?.[3].ssrc, scalabilityMode: 'L1T5', })); expect(dump2.supportedCodecPayloadTypes).toEqual([103]); expect(dump2.paused).toBe(true); expect(dump2.producerPaused).toBe(false); expect(dump2.priority).toBe(1); }, 2000); test('consumer.getStats() succeeds', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await expect(audioConsumer.getStats()).resolves.toEqual([ expect.objectContaining({ type: 'outbound-rtp', kind: 'audio', mimeType: 'audio/opus', ssrc: audioConsumer.rtpParameters.encodings?.[0].ssrc, }), ]); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await expect(videoConsumer.getStats()).resolves.toEqual([ expect.objectContaining({ type: 'outbound-rtp', kind: 'video', mimeType: 'video/H264', ssrc: videoConsumer.rtpParameters.encodings?.[0].ssrc, }), ]); }, 2000); test('consumer.pause() and resume() succeed', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const onObserverPause = jest.fn(); const onObserverResume = jest.fn(); audioConsumer.observer.on('pause', onObserverPause); audioConsumer.observer.on('resume', onObserverResume); await audioConsumer.pause(); expect(audioConsumer.paused).toBe(true); await expect(audioConsumer.dump()).resolves.toMatchObject({ paused: true }); await audioConsumer.resume(); expect(audioConsumer.paused).toBe(false); await expect(audioConsumer.dump()).resolves.toMatchObject({ paused: false }); // Even if we don't await for pause()/resume() completion, the observer must // fire 'pause' and 'resume' events if state was the opposite. audioConsumer.pause(); audioConsumer.resume(); audioConsumer.pause(); audioConsumer.pause(); audioConsumer.pause(); await audioConsumer.resume(); expect(onObserverPause).toHaveBeenCalledTimes(3); expect(onObserverResume).toHaveBeenCalledTimes(3); }, 2000); test('producer.pause() and resume() emit events', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const promises = []; const events = []; audioConsumer.observer.once('resume', () => { events.push('resume'); }); audioConsumer.observer.once('pause', () => { events.push('pause'); }); promises.push(ctx.audioProducer.pause()); promises.push(ctx.audioProducer.resume()); await Promise.all(promises); // Must also wait a bit for the corresponding events in the consumer. await new Promise(resolve => setTimeout(resolve, 100)); expect(events).toEqual(['pause', 'resume']); expect(audioConsumer.paused).toBe(false); }, 2000); test('consumer.setPreferredLayers() succeed', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await audioConsumer.setPreferredLayers({ spatialLayer: 1, temporalLayer: 1 }); expect(audioConsumer.preferredLayers).toBeUndefined(); await videoConsumer.setPreferredLayers({ spatialLayer: 2, temporalLayer: 3 }); expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 2, temporalLayer: 3, }); await videoConsumer.setPreferredLayers({ spatialLayer: 3 }); expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 3, temporalLayer: 4, }); await videoConsumer.setPreferredLayers({ spatialLayer: 3, temporalLayer: 0 }); expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 3, temporalLayer: 0, }); await videoConsumer.setPreferredLayers({ spatialLayer: 66, temporalLayer: 66, }); expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 3, temporalLayer: 4, }); }, 2000); test('consumer.setPreferredLayers() with wrong arguments rejects with TypeError', async () => { const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); // @ts-ignore await expect(videoConsumer.setPreferredLayers({})).rejects.toThrow(TypeError); await expect( // @ts-ignore videoConsumer.setPreferredLayers({ foo: '123' })).rejects.toThrow(TypeError); // @ts-ignore await expect(videoConsumer.setPreferredLayers('foo')).rejects.toThrow(TypeError); // Missing spatialLayer. await expect( // @ts-ignore videoConsumer.setPreferredLayers({ temporalLayer: 2 })).rejects.toThrow(TypeError); }, 2000); test('consumer.setPriority() succeed', async () => { const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await videoConsumer.setPriority(2); expect(videoConsumer.priority).toBe(2); }, 2000); test('consumer.setPriority() with wrong arguments rejects with TypeError', async () => { const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); // @ts-ignore await expect(videoConsumer.setPriority()).rejects.toThrow(TypeError); await expect(videoConsumer.setPriority(0)).rejects.toThrow(TypeError); // @ts-ignore await expect(videoConsumer.setPriority('foo')).rejects.toThrow(TypeError); }, 2000); test('consumer.unsetPriority() succeed', async () => { const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await videoConsumer.unsetPriority(); expect(videoConsumer.priority).toBe(1); }, 2000); test('consumer.enableTraceEvent() succeed', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await audioConsumer.enableTraceEvent(['rtp', 'pli']); const dump1 = await audioConsumer.dump(); expect(dump1.traceEventTypes).toEqual(expect.arrayContaining(['rtp', 'pli'])); await audioConsumer.enableTraceEvent([]); const dump2 = await audioConsumer.dump(); expect(dump2.traceEventTypes).toEqual(expect.arrayContaining([])); // @ts-ignore await audioConsumer.enableTraceEvent(['nack', 'FOO', 'fir']); const dump3 = await audioConsumer.dump(); expect(dump3.traceEventTypes).toEqual(expect.arrayContaining(['nack', 'fir'])); await audioConsumer.enableTraceEvent(); const dump4 = await audioConsumer.dump(); expect(dump4.traceEventTypes).toEqual(expect.arrayContaining([])); }, 2000); test('consumer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); // @ts-ignore await expect(audioConsumer.enableTraceEvent(123)).rejects.toThrow(TypeError); // @ts-ignore await expect(audioConsumer.enableTraceEvent('rtp')).rejects.toThrow(TypeError); await expect( // @ts-ignore audioConsumer.enableTraceEvent(['fir', 123.123])).rejects.toThrow(TypeError); }, 2000); test('Consumer emits "producerpause" and "producerresume"', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); await Promise.all([ (0, enhancedEvents_1.enhancedOnce)(audioConsumer, 'producerpause'), // Let's await for pause() to resolve to avoid aborted channel requests // due to worker closure. ctx.audioProducer.pause(), ]); expect(audioConsumer.paused).toBe(false); expect(audioConsumer.producerPaused).toBe(true); await Promise.all([ (0, enhancedEvents_1.enhancedOnce)(audioConsumer, 'producerresume'), // Let's await for resume() to resolve to avoid aborted channel requests // due to worker closure. ctx.audioProducer.resume(), ]); expect(audioConsumer.paused).toBe(false); expect(audioConsumer.producerPaused).toBe(false); }, 2000); test('Consumer emits "score"', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); // Private API. const channel = audioConsumer.channelForTesting; const onScore = jest.fn(); audioConsumer.on('score', onScore); // Simulate a 'score' notification coming through the channel. const builder = new flatbuffers.Builder(); const consumerScore = new FbsConsumer.ConsumerScoreT(9, 10, [8]); const consumerScoreNotification = new FbsConsumer.ScoreNotificationT(consumerScore); const notificationOffset = notification_1.Notification.createNotification(builder, builder.createString(audioConsumer.id), notification_1.Event.CONSUMER_SCORE, notification_1.Body.Consumer_ScoreNotification, consumerScoreNotification.pack(builder)); builder.finish(notificationOffset); const notification = notification_1.Notification.getRootAsNotification(new flatbuffers.ByteBuffer(builder.asUint8Array())); channel.emit(audioConsumer.id, notification_1.Event.CONSUMER_SCORE, notification); channel.emit(audioConsumer.id, notification_1.Event.CONSUMER_SCORE, notification); channel.emit(audioConsumer.id, notification_1.Event.CONSUMER_SCORE, notification); expect(onScore).toHaveBeenCalledTimes(3); expect(audioConsumer.score).toEqual({ score: 9, producerScore: 10, producerScores: [8], }); }, 2000); test('consumer.close() succeeds', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const onObserverClose = jest.fn(); audioConsumer.observer.once('close', onObserverClose); audioConsumer.close(); expect(onObserverClose).toHaveBeenCalledTimes(1); expect(audioConsumer.closed).toBe(true); const routerDump = await ctx.router.dump(); expect(routerDump.mapProducerIdConsumerIds).toEqual(expect.arrayContaining([ { key: ctx.audioProducer.id, values: [] }, { key: ctx.videoProducer.id, values: [videoConsumer.id] }, ])); expect(routerDump.mapConsumerIdProducerId).toEqual([ { key: videoConsumer.id, value: ctx.videoProducer.id }, ]); const transportDump = await ctx.webRtcTransport2.dump(); expect(transportDump).toMatchObject({ id: ctx.webRtcTransport2.id, producerIds: [], consumerIds: [videoConsumer.id], }); }, 2000); test('Consumer methods reject if closed', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); audioConsumer.close(); await expect(audioConsumer.dump()).rejects.toThrow(Error); await expect(audioConsumer.getStats()).rejects.toThrow(Error); await expect(audioConsumer.pause()).rejects.toThrow(Error); await expect(audioConsumer.resume()).rejects.toThrow(Error); // @ts-ignore await expect(audioConsumer.setPreferredLayers({})).rejects.toThrow(Error); await expect(audioConsumer.setPriority(2)).rejects.toThrow(Error); await expect(audioConsumer.requestKeyFrame()).rejects.toThrow(Error); }, 2000); test('Consumer emits "producerclose" if Producer is closed', async () => { const audioConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.audioProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const onObserverClose = jest.fn(); audioConsumer.observer.once('close', onObserverClose); const promise = (0, enhancedEvents_1.enhancedOnce)(audioConsumer, 'producerclose'); ctx.audioProducer.close(); await promise; expect(onObserverClose).toHaveBeenCalledTimes(1); expect(audioConsumer.closed).toBe(true); }, 2000); test('Consumer emits "transportclose" if Transport is closed', async () => { const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: ctx.videoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); const onObserverClose = jest.fn(); videoConsumer.observer.once('close', onObserverClose); const promise = (0, enhancedEvents_1.enhancedOnce)(videoConsumer, 'transportclose'); ctx.webRtcTransport2.close(); await promise; expect(onObserverClose).toHaveBeenCalledTimes(1); expect(videoConsumer.closed).toBe(true); await expect(ctx.router.dump()).resolves.toMatchObject({ mapProducerIdConsumerIds: expect.arrayContaining([ { key: ctx.audioProducer.id, values: [] }, { key: ctx.videoProducer.id, values: [] }, ]), mapConsumerIdProducerId: [], }); }, 2000);