UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

648 lines (647 loc) 25.5 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 FbsProducer = require("../fbs/producer"); const ctx = { mediaCodecs: utils.deepFreeze([ { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2, parameters: { foo: '111', }, }, { 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', }, rtcpFeedback: [], // Will be ignored. }, ]), audioProducerOptions: utils.deepFreeze({ kind: 'audio', rtpParameters: { mid: 'AUDIO', codecs: [ { mimeType: 'audio/opus', payloadType: 0, 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, }, ], // Missing encodings on purpose. rtcp: { cname: 'audio-1', }, }, 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' }, { type: 'nack', parameter: 'pli' }, { type: 'goog-remb' }, ], }, { 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, rtx: { ssrc: 22222223 }, scalabilityMode: 'L1T3' }, { ssrc: 22222224, rtx: { ssrc: 22222225 } }, { ssrc: 22222226, rtx: { ssrc: 22222227 } }, { ssrc: 22222228, rtx: { ssrc: 22222229 } }, ], rtcp: { cname: 'video-1', }, }, appData: { foo: 1, bar: '2' }, }), }; 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'], }); }); afterEach(async () => { ctx.worker?.close(); if (ctx.worker?.subprocessClosed === false) { await (0, enhancedEvents_1.enhancedOnce)(ctx.worker, 'subprocessclose'); } }); test('webRtcTransport1.produce() succeeds', async () => { const onObserverNewProducer = jest.fn(); ctx.webRtcTransport1.observer.once('newproducer', onObserverNewProducer); const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); expect(onObserverNewProducer).toHaveBeenCalledTimes(1); expect(onObserverNewProducer).toHaveBeenCalledWith(audioProducer); expect(typeof audioProducer.id).toBe('string'); expect(audioProducer.closed).toBe(false); expect(audioProducer.kind).toBe('audio'); expect(typeof audioProducer.rtpParameters).toBe('object'); expect(audioProducer.type).toBe('simple'); // Private API. expect(typeof audioProducer.consumableRtpParameters).toBe('object'); expect(audioProducer.paused).toBe(false); expect(audioProducer.score).toEqual([]); expect(audioProducer.appData).toEqual({ foo: 1, bar: '2' }); await expect(ctx.router.dump()).resolves.toMatchObject({ mapProducerIdConsumerIds: [{ key: audioProducer.id, values: [] }], mapConsumerIdProducerId: [], }); await expect(ctx.webRtcTransport1.dump()).resolves.toMatchObject({ id: ctx.webRtcTransport1.id, producerIds: [audioProducer.id], consumerIds: [], }); }, 2000); test('webRtcTransport2.produce() succeeds', async () => { const onObserverNewProducer = jest.fn(); ctx.webRtcTransport2.observer.once('newproducer', onObserverNewProducer); const videoProducer = await ctx.webRtcTransport2.produce(ctx.videoProducerOptions); expect(onObserverNewProducer).toHaveBeenCalledTimes(1); expect(onObserverNewProducer).toHaveBeenCalledWith(videoProducer); expect(typeof videoProducer.id).toBe('string'); expect(videoProducer.closed).toBe(false); expect(videoProducer.kind).toBe('video'); expect(typeof videoProducer.rtpParameters).toBe('object'); expect(videoProducer.type).toBe('simulcast'); // Private API. expect(typeof videoProducer.consumableRtpParameters).toBe('object'); expect(videoProducer.paused).toBe(false); expect(videoProducer.score).toEqual([]); expect(videoProducer.appData).toEqual({ foo: 1, bar: '2' }); const dump = await ctx.router.dump(); expect(dump.mapProducerIdConsumerIds).toEqual(expect.arrayContaining([{ key: videoProducer.id, values: [] }])); expect(dump.mapConsumerIdProducerId.length).toBe(0); await expect(ctx.webRtcTransport2.dump()).resolves.toMatchObject({ id: ctx.webRtcTransport2.id, producerIds: [videoProducer.id], consumerIds: [], }); }, 2000); test('webRtcTransport1.produce() without header extensions and rtcp succeeds', async () => { const onObserverNewProducer = jest.fn(); ctx.webRtcTransport1.observer.once('newproducer', onObserverNewProducer); const audioProducer = await ctx.webRtcTransport1.produce({ kind: 'audio', rtpParameters: { mid: 'AUDIO2', codecs: [ { mimeType: 'audio/opus', payloadType: 0, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, usedtx: 1, foo: 222.222, bar: '333', }, }, ], }, appData: { foo: 1, bar: '2' }, }); expect(onObserverNewProducer).toHaveBeenCalledTimes(1); expect(onObserverNewProducer).toHaveBeenCalledWith(audioProducer); expect(typeof audioProducer.id).toBe('string'); expect(audioProducer.closed).toBe(false); expect(audioProducer.kind).toBe('audio'); expect(typeof audioProducer.rtpParameters).toBe('object'); expect(audioProducer.type).toBe('simple'); // Private API. expect(typeof audioProducer.consumableRtpParameters).toBe('object'); expect(audioProducer.paused).toBe(false); expect(audioProducer.score).toEqual([]); expect(audioProducer.appData).toEqual({ foo: 1, bar: '2' }); audioProducer.close(); }, 2000); test('webRtcTransport1.produce() with wrong arguments rejects with TypeError', async () => { await expect(ctx.webRtcTransport1.produce({ // @ts-ignore kind: 'chicken', // @ts-ignore rtpParameters: {}, })).rejects.toThrow(TypeError); await expect(ctx.webRtcTransport1.produce({ kind: 'audio', // @ts-ignore rtpParameters: {}, })).rejects.toThrow(TypeError); // Invalid ssrc. await expect(ctx.webRtcTransport1.produce({ kind: 'audio', rtpParameters: { codecs: [], headerExtensions: [], // @ts-ignore encodings: [{ ssrc: '1111' }], rtcp: { cname: 'qwerty' }, }, })).rejects.toThrow(TypeError); // Missing or empty rtpParameters.encodings. await expect(ctx.webRtcTransport1.produce({ kind: 'video', rtpParameters: { codecs: [ { mimeType: 'video/h264', payloadType: 112, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, }, { mimeType: 'video/rtx', payloadType: 113, clockRate: 90000, parameters: { apt: 112 }, }, ], headerExtensions: [], encodings: [], rtcp: { cname: 'qwerty' }, }, })).rejects.toThrow(TypeError); // Wrong apt in RTX codec. await expect(ctx.webRtcTransport1.produce({ kind: 'audio', rtpParameters: { codecs: [ { mimeType: 'video/h264', payloadType: 112, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, }, { mimeType: 'video/rtx', payloadType: 113, clockRate: 90000, parameters: { apt: 111 }, }, ], headerExtensions: [], encodings: [{ ssrc: 6666, rtx: { ssrc: 6667 } }], rtcp: { cname: 'video-1', }, }, })).rejects.toThrow(TypeError); }, 2000); test('webRtcTransport1.produce() with unsupported codecs rejects with UnsupportedError', async () => { await expect(ctx.webRtcTransport1.produce({ kind: 'audio', rtpParameters: { codecs: [ { mimeType: 'audio/ISAC', payloadType: 108, clockRate: 32000, }, ], headerExtensions: [], encodings: [{ ssrc: 1111 }], rtcp: { cname: 'audio' }, }, })).rejects.toThrow(errors_1.UnsupportedError); // Invalid H264 profile-level-id. await expect(ctx.webRtcTransport1.produce({ kind: 'video', rtpParameters: { codecs: [ { mimeType: 'video/h264', payloadType: 112, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': 'CHICKEN', }, }, { mimeType: 'video/rtx', payloadType: 113, clockRate: 90000, parameters: { apt: 112 }, }, ], headerExtensions: [], encodings: [{ ssrc: 6666, rtx: { ssrc: 6667 } }], }, })).rejects.toThrow(errors_1.UnsupportedError); }, 2000); test('transport.produce() with already used MID or SSRC rejects with Error', async () => { const audioProducerOptions = { kind: 'audio', rtpParameters: { mid: 'AUDIO', codecs: [ { mimeType: 'audio/opus', payloadType: 0, clockRate: 48000, channels: 2, }, ], encodings: [{ ssrc: 33333333 }], }, }; const videoProducerOptions = { kind: 'video', rtpParameters: { mid: 'VIDEO2', codecs: [ { mimeType: 'video/h264', payloadType: 112, clockRate: 90000, parameters: { 'packetization-mode': 1, 'profile-level-id': '4d0032', }, }, ], headerExtensions: [ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, }, ], encodings: [{ ssrc: 22222222 }], rtcp: { cname: 'video-1', }, }, }; await ctx.webRtcTransport1.produce(audioProducerOptions); await expect(ctx.webRtcTransport1.produce(audioProducerOptions)).rejects.toThrow(Error); await ctx.webRtcTransport2.produce(videoProducerOptions); await expect(ctx.webRtcTransport2.produce(videoProducerOptions)).rejects.toThrow(Error); }, 2000); test('transport.produce() with no MID and with single encoding without RID or SSRC rejects with Error', async () => { await expect(ctx.webRtcTransport1.produce({ kind: 'audio', rtpParameters: { codecs: [ { mimeType: 'audio/opus', payloadType: 111, clockRate: 48000, channels: 2, }, ], encodings: [{}], }, })).rejects.toThrow(Error); }, 2000); test('producer.dump() succeeds', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); const dump1 = await audioProducer.dump(); expect(dump1.id).toBe(audioProducer.id); expect(dump1.kind).toBe(audioProducer.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(0); 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(2); expect(dump1.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, parameters: {}, encrypt: false, }, { uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', id: 12, parameters: {}, encrypt: false, }, ]); expect(Array.isArray(dump1.rtpParameters.encodings)).toBe(true); expect(dump1.rtpParameters.encodings.length).toBe(1); expect(dump1.rtpParameters.encodings[0]).toEqual(expect.objectContaining({ codecPayloadType: 0, })); expect(dump1.type).toBe('simple'); const videoProducer = await ctx.webRtcTransport2.produce(ctx.videoProducerOptions); const dump2 = await videoProducer.dump(); expect(dump2.id).toBe(videoProducer.id); expect(dump2.kind).toBe(videoProducer.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(112); 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: 'goog-remb' }, ]); expect(dump2.rtpParameters.codecs[1].mimeType).toBe('video/rtx'); expect(dump2.rtpParameters.codecs[1].payloadType).toBe(113); expect(dump2.rtpParameters.codecs[1].clockRate).toBe(90000); expect(dump2.rtpParameters.codecs[1].channels).toBeUndefined(); expect(dump2.rtpParameters.codecs[1].parameters).toEqual({ apt: 112 }); expect(dump2.rtpParameters.codecs[1].rtcpFeedback).toEqual([]); expect(Array.isArray(dump2.rtpParameters.headerExtensions)).toBe(true); expect(dump2.rtpParameters.headerExtensions.length).toBe(2); expect(dump2.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, parameters: {}, encrypt: false, }, { uri: 'urn:3gpp:video-orientation', id: 13, parameters: {}, encrypt: false, }, ]); expect(Array.isArray(dump2.rtpParameters.encodings)).toBe(true); expect(dump2.rtpParameters.encodings.length).toBe(4); expect(dump2.rtpParameters.encodings).toMatchObject([ { codecPayloadType: 112, ssrc: 22222222, rtx: { ssrc: 22222223 }, scalabilityMode: 'L1T3', }, { codecPayloadType: 112, ssrc: 22222224, rtx: { ssrc: 22222225 } }, { codecPayloadType: 112, ssrc: 22222226, rtx: { ssrc: 22222227 } }, { codecPayloadType: 112, ssrc: 22222228, rtx: { ssrc: 22222229 } }, ]); expect(dump2.type).toBe('simulcast'); }, 2000); test('producer.getStats() succeeds', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); const videoProducer = await ctx.webRtcTransport2.produce(ctx.videoProducerOptions); await expect(audioProducer.getStats()).resolves.toEqual([]); await expect(videoProducer.getStats()).resolves.toEqual([]); }, 2000); test('producer.pause() and resume() succeed', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); const onObserverPause = jest.fn(); const onObserverResume = jest.fn(); audioProducer.observer.on('pause', onObserverPause); audioProducer.observer.on('resume', onObserverResume); await audioProducer.pause(); expect(audioProducer.paused).toBe(true); await expect(audioProducer.dump()).resolves.toMatchObject({ paused: true }); await audioProducer.resume(); expect(audioProducer.paused).toBe(false); await expect(audioProducer.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. audioProducer.pause(); audioProducer.resume(); audioProducer.pause(); audioProducer.pause(); audioProducer.pause(); await audioProducer.resume(); expect(onObserverPause).toHaveBeenCalledTimes(3); expect(onObserverResume).toHaveBeenCalledTimes(3); }, 2000); test('producer.pause() and resume() emit events', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); const promises = []; const events = []; audioProducer.observer.once('resume', () => { events.push('resume'); }); audioProducer.observer.once('pause', () => { events.push('pause'); }); promises.push(audioProducer.pause()); promises.push(audioProducer.resume()); await Promise.all(promises); expect(events).toEqual(['pause', 'resume']); expect(audioProducer.paused).toBe(false); }, 2000); test('producer.enableTraceEvent() succeed', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); await audioProducer.enableTraceEvent(['rtp', 'pli']); const dump1 = await audioProducer.dump(); expect(dump1.traceEventTypes).toEqual(expect.arrayContaining(['rtp', 'pli'])); await audioProducer.enableTraceEvent([]); const dump2 = await audioProducer.dump(); expect(dump2.traceEventTypes).toEqual(expect.arrayContaining([])); // @ts-ignore await audioProducer.enableTraceEvent(['nack', 'FOO', 'fir']); const dump3 = await audioProducer.dump(); expect(dump3.traceEventTypes).toEqual(expect.arrayContaining(['nack', 'fir'])); await audioProducer.enableTraceEvent(); const dump4 = await audioProducer.dump(); expect(dump4.traceEventTypes).toEqual(expect.arrayContaining([])); }, 2000); test('producer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); // @ts-ignore await expect(audioProducer.enableTraceEvent(123)).rejects.toThrow(TypeError); // @ts-ignore await expect(audioProducer.enableTraceEvent('rtp')).rejects.toThrow(TypeError); await expect( // @ts-ignore audioProducer.enableTraceEvent(['fir', 123.123])).rejects.toThrow(TypeError); }, 2000); test('Producer emits "score"', async () => { const videoProducer = await ctx.webRtcTransport2.produce(ctx.videoProducerOptions); // Private API. const channel = videoProducer.channelForTesting; const onScore = jest.fn(); videoProducer.on('score', onScore); // Simulate a 'score' notification coming through the channel. const builder = new flatbuffers.Builder(); const producerScoreNotification = new FbsProducer.ScoreNotificationT([ new FbsProducer.ScoreT( /* encodingIdx */ 0, /* ssrc */ 11, /* rid */ undefined, /* score */ 10), new FbsProducer.ScoreT( /* encodingIdx */ 1, /* ssrc */ 22, /* rid */ undefined, /* score */ 9), ]); const notificationOffset = notification_1.Notification.createNotification(builder, builder.createString(videoProducer.id), notification_1.Event.PRODUCER_SCORE, notification_1.Body.Producer_ScoreNotification, producerScoreNotification.pack(builder)); builder.finish(notificationOffset); const notification = notification_1.Notification.getRootAsNotification(new flatbuffers.ByteBuffer(builder.asUint8Array())); channel.emit(videoProducer.id, notification_1.Event.PRODUCER_SCORE, notification); channel.emit(videoProducer.id, notification_1.Event.PRODUCER_SCORE, notification); channel.emit(videoProducer.id, notification_1.Event.PRODUCER_SCORE, notification); expect(onScore).toHaveBeenCalledTimes(3); expect(videoProducer.score).toEqual([ { ssrc: 11, rid: undefined, score: 10, encodingIdx: 0 }, { ssrc: 22, rid: undefined, score: 9, encodingIdx: 1 }, ]); }, 2000); test('producer.close() succeeds', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); const onObserverClose = jest.fn(); audioProducer.observer.once('close', onObserverClose); audioProducer.close(); expect(onObserverClose).toHaveBeenCalledTimes(1); expect(audioProducer.closed).toBe(true); await expect(ctx.router.dump()).resolves.toMatchObject({ mapProducerIdConsumerIds: [], mapConsumerIdProducerId: [], }); await expect(ctx.webRtcTransport1.dump()).resolves.toMatchObject({ id: ctx.webRtcTransport1.id, producerIds: [], consumerIds: [], }); }, 2000); test('Producer methods reject if closed', async () => { const audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); audioProducer.close(); await expect(audioProducer.dump()).rejects.toThrow(Error); await expect(audioProducer.getStats()).rejects.toThrow(Error); await expect(audioProducer.pause()).rejects.toThrow(Error); await expect(audioProducer.resume()).rejects.toThrow(Error); }, 2000); test('Producer emits "transportclose" if Transport is closed', async () => { const videoProducer = await ctx.webRtcTransport2.produce(ctx.videoProducerOptions); const onObserverClose = jest.fn(); videoProducer.observer.once('close', onObserverClose); const promise = (0, enhancedEvents_1.enhancedOnce)(videoProducer, 'transportclose'); ctx.webRtcTransport2.close(); await promise; expect(onObserverClose).toHaveBeenCalledTimes(1); expect(videoProducer.closed).toBe(true); }, 2000);