UNPKG

mediasoup

Version:

Cutting Edge WebRTC Video Conferencing

991 lines (990 loc) 36.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const pick_port_1 = require("pick-port"); const mediasoup = require("../"); const enhancedEvents_1 = require("../enhancedEvents"); const utils = require("../utils"); const ctx = { mediaCodecs: utils.deepFreeze([ { kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2, }, { kind: 'video', mimeType: 'video/VP8', clockRate: 90000, }, ]), audioProducerOptions: utils.deepFreeze({ kind: 'audio', rtpParameters: { mid: 'AUDIO', codecs: [ { mimeType: 'audio/opus', payloadType: 111, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, foo: 'bar1', }, }, ], headerExtensions: [ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, }, ], encodings: [{ ssrc: 11111111 }], rtcp: { cname: 'FOOBAR', }, }, appData: { foo: 'bar1' }, }), videoProducerOptions: utils.deepFreeze({ kind: 'video', rtpParameters: { mid: 'VIDEO', codecs: [ { mimeType: 'video/VP8', payloadType: 112, clockRate: 90000, rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'goog-remb' }, { type: 'lalala' }, ], }, ], headerExtensions: [ { uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', id: 10, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', id: 11, }, { uri: 'urn:3gpp:video-orientation', id: 13, }, ], encodings: [{ ssrc: 22222222 }, { ssrc: 22222223 }, { ssrc: 22222224 }], rtcp: { cname: 'FOOBAR', }, msid: 'aaaa-bbbb', }, appData: { foo: 'bar2' }, }), dataProducerOptions: utils.deepFreeze({ sctpStreamParameters: { streamId: 666, ordered: false, maxPacketLifeTime: 5000, }, label: 'foo', protocol: 'bar', }), consumerDeviceCapabilities: utils.deepFreeze({ codecs: [ { kind: 'audio', mimeType: 'audio/opus', preferredPayloadType: 100, clockRate: 48000, channels: 2, }, { kind: 'video', mimeType: 'video/VP8', preferredPayloadType: 101, clockRate: 90000, rtcpFeedback: [ { type: 'nack' }, { type: 'ccm', parameter: 'fir' }, { type: 'transport-cc' }, ], }, { kind: 'video', mimeType: 'video/rtx', preferredPayloadType: 102, clockRate: 90000, parameters: { apt: 101, }, rtcpFeedback: [], }, ], headerExtensions: [ { kind: 'video', uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', preferredId: 4, preferredEncrypt: false, direction: 'sendrecv', }, { kind: 'video', uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', preferredId: 5, preferredEncrypt: false, }, { kind: 'audio', uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', preferredId: 6, preferredEncrypt: false, }, ], }), }; beforeEach(async () => { ctx.worker1 = await mediasoup.createWorker(); ctx.worker2 = await mediasoup.createWorker(); ctx.router1 = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); ctx.router2 = await ctx.worker2.createRouter({ mediaCodecs: ctx.mediaCodecs, }); ctx.webRtcTransport1 = await ctx.router1.createWebRtcTransport({ listenInfos: [{ protocol: 'udp', ip: '127.0.0.1' }], enableSctp: true, }); ctx.webRtcTransport2 = await ctx.router2.createWebRtcTransport({ listenIps: ['127.0.0.1'], enableSctp: true, }); ctx.audioProducer = await ctx.webRtcTransport1.produce(ctx.audioProducerOptions); ctx.videoProducer = await ctx.webRtcTransport1.produce(ctx.videoProducerOptions); ctx.dataProducer = await ctx.webRtcTransport1.produceData(ctx.dataProducerOptions); }); afterEach(async () => { ctx.worker1?.close(); ctx.worker2?.close(); if (ctx.worker1?.subprocessClosed === false) { await (0, enhancedEvents_1.enhancedOnce)(ctx.worker1, 'subprocessclose'); } if (ctx.worker2?.subprocessClosed === false) { await (0, enhancedEvents_1.enhancedOnce)(ctx.worker2, 'subprocessclose'); } }); test('router.pipeToRouter() succeeds with audio', async () => { const { pipeConsumer, pipeProducer } = (await ctx.router1.pipeToRouter({ producerId: ctx.audioProducer.id, router: ctx.router2, })); const dump1 = await ctx.router1.dump(); // There should be two Transports in router1: // - WebRtcTransport for audioProducer and videoProducer. // - PipeTransport between router1 and router2. expect(dump1.transportIds.length).toBe(2); const dump2 = await ctx.router2.dump(); // There should be two Transports in router2: // - WebRtcTransport for audioConsumer and videoConsumer. // - PipeTransport between router2 and router1. expect(dump2.transportIds.length).toBe(2); expect(typeof pipeConsumer.id).toBe('string'); expect(pipeConsumer.closed).toBe(false); expect(pipeConsumer.kind).toBe('audio'); expect(typeof pipeConsumer.rtpParameters).toBe('object'); expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); expect(pipeConsumer.rtpParameters.codecs).toEqual([ { mimeType: 'audio/opus', clockRate: 48000, payloadType: 100, channels: 2, parameters: { useinbandfec: 1, foo: 'bar1', }, rtcpFeedback: [], }, ]); expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', id: 6, encrypt: false, parameters: {}, }, { encrypt: false, id: 7, parameters: {}, uri: 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension', }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', id: 10, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', id: 11, encrypt: false, parameters: {}, }, { uri: 'urn:mediasoup:params:rtp-hdrext:packet-id', id: 12, encrypt: false, parameters: {}, }, ]); expect(pipeConsumer.type).toBe('pipe'); expect(pipeConsumer.paused).toBe(false); expect(pipeConsumer.producerPaused).toBe(false); expect(pipeConsumer.score).toEqual({ score: 10, producerScore: 10, producerScores: [], }); expect(pipeConsumer.appData).toEqual({}); expect(pipeProducer.id).toBe(ctx.audioProducer.id); expect(pipeProducer.closed).toBe(false); expect(pipeProducer.kind).toBe('audio'); expect(typeof pipeProducer.rtpParameters).toBe('object'); expect(pipeProducer.rtpParameters.mid).toBeUndefined(); expect(pipeProducer.rtpParameters.codecs).toEqual([ { mimeType: 'audio/opus', payloadType: 100, clockRate: 48000, channels: 2, parameters: { useinbandfec: 1, foo: 'bar1', }, rtcpFeedback: [], }, ]); expect(pipeProducer.rtpParameters.headerExtensions).toEqual([ { uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', id: 6, encrypt: false, parameters: {}, }, { encrypt: false, id: 7, parameters: {}, uri: 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension', }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', id: 10, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', id: 11, encrypt: false, parameters: {}, }, { uri: 'urn:mediasoup:params:rtp-hdrext:packet-id', id: 12, encrypt: false, parameters: {}, }, ]); expect(pipeProducer.paused).toBe(false); }, 2000); test('router.pipeToRouter() succeeds with video', async () => { await ctx.videoProducer.pause(); const { pipeConsumer, pipeProducer } = (await ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: ctx.router2, })); const dump1 = await ctx.router1.dump(); // No new PipeTransport should has been created. The existing one is used. expect(dump1.transportIds.length).toBe(2); const dump2 = await ctx.router2.dump(); // No new PipeTransport should has been created. The existing one is used. expect(dump2.transportIds.length).toBe(2); expect(typeof pipeConsumer.id).toBe('string'); expect(pipeConsumer.closed).toBe(false); expect(pipeConsumer.kind).toBe('video'); expect(typeof pipeConsumer.rtpParameters).toBe('object'); expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); expect(pipeConsumer.rtpParameters.codecs).toEqual([ { mimeType: 'video/VP8', payloadType: 101, clockRate: 90000, parameters: {}, rtcpFeedback: [ { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, ], }, ]); expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ { uri: 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension', id: 7, encrypt: false, parameters: {}, }, { uri: 'urn:3gpp:video-orientation', id: 8, encrypt: false, parameters: {}, }, { uri: 'urn:ietf:params:rtp-hdrext:toffset', id: 9, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', id: 10, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', id: 11, encrypt: false, parameters: {}, }, { uri: 'urn:mediasoup:params:rtp-hdrext:packet-id', id: 12, encrypt: false, parameters: {}, }, ]); expect(pipeConsumer.type).toBe('pipe'); expect(pipeConsumer.paused).toBe(false); expect(pipeConsumer.producerPaused).toBe(true); expect(pipeConsumer.score).toEqual({ score: 10, producerScore: 10, producerScores: [], }); expect(pipeConsumer.appData).toEqual({}); expect(pipeProducer.id).toBe(ctx.videoProducer.id); expect(pipeProducer.closed).toBe(false); expect(pipeProducer.kind).toBe('video'); expect(typeof pipeProducer.rtpParameters).toBe('object'); expect(pipeProducer.rtpParameters.mid).toBeUndefined(); expect(pipeProducer.rtpParameters.codecs).toEqual([ { mimeType: 'video/VP8', payloadType: 101, clockRate: 90000, parameters: {}, rtcpFeedback: [ { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, ], }, ]); expect(pipeProducer.rtpParameters.headerExtensions).toEqual([ { uri: 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension', id: 7, encrypt: false, parameters: {}, }, { uri: 'urn:3gpp:video-orientation', id: 8, encrypt: false, parameters: {}, }, { uri: 'urn:ietf:params:rtp-hdrext:toffset', id: 9, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', id: 10, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', id: 11, encrypt: false, parameters: {}, }, { uri: 'urn:mediasoup:params:rtp-hdrext:packet-id', id: 12, encrypt: false, parameters: {}, }, ]); expect(pipeProducer.paused).toBe(true); }, 2000); test('router.createPipeTransport() with wrong arguments rejects with TypeError', async () => { // @ts-expect-error --- Testing purposes. await expect(ctx.router1.createPipeTransport({})).rejects.toThrow(TypeError); await expect(ctx.router1.createPipeTransport({ listenInfo: { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 4000, max: 3000 }, }, })).rejects.toThrow(TypeError); await expect(ctx.router1.createPipeTransport({ listenIp: '123' })).rejects.toThrow(TypeError); await expect( // @ts-expect-error --- Testing purposes. ctx.router1.createPipeTransport({ listenIp: ['127.0.0.1'] })).rejects.toThrow(TypeError); await expect(ctx.router1.createPipeTransport({ listenInfo: { protocol: 'tcp', ip: '127.0.0.1' }, })).rejects.toThrow(TypeError); await expect(ctx.router1.createPipeTransport({ listenInfo: { protocol: 'udp', ip: '127.0.0.1' }, // @ts-expect-error --- Testing purposes. appData: 'NOT-AN-OBJECT', })).rejects.toThrow(TypeError); }, 2000); test('router.createPipeTransport() with enableRtx succeeds', async () => { const pipeTransport = await ctx.router1.createPipeTransport({ listenInfo: { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 }, }, enableRtx: true, }); expect(pipeTransport.type).toBe('pipe'); const pipeConsumer = await pipeTransport.consume({ producerId: ctx.videoProducer.id, }); expect(typeof pipeConsumer.id).toBe('string'); expect(pipeConsumer.closed).toBe(false); expect(pipeConsumer.kind).toBe('video'); expect(typeof pipeConsumer.rtpParameters).toBe('object'); expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); expect(pipeConsumer.rtpParameters.codecs).toEqual([ { mimeType: 'video/VP8', payloadType: 101, clockRate: 90000, parameters: {}, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, ], }, { mimeType: 'video/rtx', payloadType: 102, clockRate: 90000, parameters: { apt: 101 }, rtcpFeedback: [], }, ]); expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ { uri: 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension', id: 7, encrypt: false, parameters: {}, }, { uri: 'urn:3gpp:video-orientation', id: 8, encrypt: false, parameters: {}, }, { uri: 'urn:ietf:params:rtp-hdrext:toffset', id: 9, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', id: 10, encrypt: false, parameters: {}, }, { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', id: 11, encrypt: false, parameters: {}, }, { uri: 'urn:mediasoup:params:rtp-hdrext:packet-id', id: 12, encrypt: false, parameters: {}, }, ]); expect(pipeConsumer.type).toBe('pipe'); expect(pipeConsumer.paused).toBe(false); expect(pipeConsumer.producerPaused).toBe(false); expect(pipeConsumer.score).toEqual({ score: 10, producerScore: 10, producerScores: [], }); expect(pipeConsumer.appData).toEqual({}); }, 2000); test('pipeTransport.connect() with valid SRTP parameters succeeds', async () => { const pipeTransport = await ctx.router1.createPipeTransport({ listenIp: '127.0.0.1', enableSrtp: true, }); expect(typeof pipeTransport.srtpParameters).toBe('object'); // The master length of AEAD_AES_256_GCM. expect(pipeTransport.srtpParameters?.keyBase64.length).toBe(60); // Valid srtpParameters. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, srtpParameters: { cryptoSuite: 'AEAD_AES_256_GCM', keyBase64: 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', }, })).resolves.toBeUndefined(); }, 2000); test('pipeTransport.connect() with srtpParameters fails if enableSrtp is unset', async () => { const pipeTransport = await ctx.router1.createPipeTransport({ listenInfo: { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 }, }, enableRtx: true, }); expect(pipeTransport.srtpParameters).toBeUndefined(); // No SRTP enabled so passing srtpParameters must fail. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, srtpParameters: { cryptoSuite: 'AEAD_AES_256_GCM', keyBase64: 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', }, })).rejects.toThrow(TypeError); // No SRTP enabled so passing srtpParameters (even if invalid) must fail. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, // @ts-expect-error --- Testing purposes. srtpParameters: 'invalid', })).rejects.toThrow(TypeError); }); test('pipeTransport.connect() with invalid srtpParameters fails', async () => { const pipeTransport = await ctx.router1.createPipeTransport({ listenIp: '127.0.0.1', enableSrtp: true, }); expect(typeof pipeTransport.id).toBe('string'); expect(typeof pipeTransport.srtpParameters).toBe('object'); // The master length of AEAD_AES_256_GCM. expect(pipeTransport.srtpParameters?.keyBase64.length).toBe(60); // Missing srtpParameters. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, })).rejects.toThrow(TypeError); // Invalid srtpParameters. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, // @ts-expect-error --- Testing purposes. srtpParameters: 1, })).rejects.toThrow(TypeError); // Missing srtpParameters.cryptoSuite. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, // @ts-expect-error --- Testing purposes. srtpParameters: { keyBase64: 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', }, })).rejects.toThrow(TypeError); // Missing srtpParameters.keyBase64. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, // @ts-expect-error --- Testing purposes. srtpParameters: { cryptoSuite: 'AEAD_AES_256_GCM', }, })).rejects.toThrow(TypeError); // Invalid srtpParameters.cryptoSuite. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, srtpParameters: { // @ts-expect-error --- Testing purposes. cryptoSuite: 'FOO', keyBase64: 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', }, })).rejects.toThrow(TypeError); // Invalid srtpParameters.cryptoSuite. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, srtpParameters: { // @ts-expect-error --- Testing purposes. cryptoSuite: 123, keyBase64: 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', }, })).rejects.toThrow(TypeError); // Invalid srtpParameters.keyBase64. await expect(pipeTransport.connect({ ip: '127.0.0.2', port: 9999, srtpParameters: { cryptoSuite: 'AEAD_AES_256_GCM', // @ts-expect-error --- Testing purposes. keyBase64: [], }, })).rejects.toThrow(TypeError); }, 2000); test('router.createPipeTransport() with fixed port succeeds', async () => { const port = await (0, pick_port_1.pickPort)({ type: 'udp', ip: '127.0.0.1', reserveTimeout: 0, }); const pipeTransport = await ctx.router1.createPipeTransport({ listenInfo: { protocol: 'udp', ip: '127.0.0.1', port }, }); expect(pipeTransport.tuple.localPort).toEqual(port); }, 2000); test('transport.consume() for a pipe Producer succeeds', async () => { const { pipeProducer } = await ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: ctx.router2, }); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: pipeProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); expect(typeof videoConsumer.id).toBe('string'); expect(videoConsumer.closed).toBe(false); expect(videoConsumer.kind).toBe('video'); expect(typeof videoConsumer.rtpParameters).toBe('object'); expect(videoConsumer.rtpParameters.mid).toBe('0'); expect(videoConsumer.rtpParameters.codecs).toEqual([ { mimeType: 'video/VP8', payloadType: 101, clockRate: 90000, parameters: {}, rtcpFeedback: [ { type: 'nack', parameter: '' }, { type: 'ccm', parameter: 'fir' }, { type: 'transport-cc', parameter: '' }, ], }, { mimeType: 'video/rtx', payloadType: 102, clockRate: 90000, parameters: { apt: 101, }, rtcpFeedback: [], }, ]); expect(videoConsumer.rtpParameters.headerExtensions).toEqual([ { uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', id: 4, encrypt: false, parameters: {}, }, { uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', id: 5, encrypt: false, parameters: {}, }, ]); expect(videoConsumer.rtpParameters.encodings?.length).toBe(1); expect(typeof videoConsumer.rtpParameters.encodings[0].ssrc).toBe('number'); expect(typeof videoConsumer.rtpParameters.encodings[0].rtx).toBe('object'); expect(typeof videoConsumer.rtpParameters.encodings[0].rtx?.ssrc).toBe('number'); expect(videoConsumer.rtpParameters.msid).toBe('aaaa-bbbb'); expect(videoConsumer.type).toBe('simulcast'); expect(videoConsumer.paused).toBe(false); expect(videoConsumer.producerPaused).toBe(false); expect(videoConsumer.score).toEqual({ score: 10, producerScore: 0, producerScores: [0, 0, 0], }); expect(videoConsumer.appData).toEqual({}); }, 2000); test('producer.pause() and producer.resume() are transmitted to pipe Consumer', async () => { await ctx.videoProducer.pause(); // We need to obtain the pipeProducer to await for its 'puase' and 'resume' // events, otherwise we may get errors like this: // InvalidStateError: Channel closed, pending request aborted [method:PRODUCER_PAUSE, id:8] // See related fixed issue: // https://github.com/versatica/mediasoup/issues/1374 const { pipeProducer: pipeVideoProducer } = await ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: ctx.router2, }); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: pipeVideoProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); expect(ctx.videoProducer.paused).toBe(true); expect(videoConsumer.producerPaused).toBe(true); expect(videoConsumer.paused).toBe(false); // NOTE: Let's use a Promise since otherwise there may be race conditions // between events and await lines below. const promise1 = (0, enhancedEvents_1.enhancedOnce)(videoConsumer, 'producerresume'); const promise2 = (0, enhancedEvents_1.enhancedOnce)(pipeVideoProducer.observer, 'resume'); await ctx.videoProducer.resume(); await Promise.all([promise1, promise2]); expect(videoConsumer.producerPaused).toBe(false); expect(videoConsumer.paused).toBe(false); expect(pipeVideoProducer.paused).toBe(false); const promise3 = (0, enhancedEvents_1.enhancedOnce)(videoConsumer, 'producerpause'); const promise4 = (0, enhancedEvents_1.enhancedOnce)(pipeVideoProducer.observer, 'pause'); await ctx.videoProducer.pause(); await Promise.all([promise3, promise4]); expect(videoConsumer.producerPaused).toBe(true); expect(videoConsumer.paused).toBe(false); expect(pipeVideoProducer.paused).toBe(true); }, 2000); test('producer.close() is transmitted to pipe Consumer', async () => { const { pipeProducer } = await ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: ctx.router2, }); const videoConsumer = await ctx.webRtcTransport2.consume({ producerId: pipeProducer.id, rtpCapabilities: ctx.consumerDeviceCapabilities, }); ctx.videoProducer.close(); expect(ctx.videoProducer.closed).toBe(true); if (!videoConsumer.closed) { await (0, enhancedEvents_1.enhancedOnce)(videoConsumer, 'producerclose'); } expect(videoConsumer.closed).toBe(true); }, 2000); test('router.pipeToRouter() with keepId: true fails if both Routers belong to the same Worker', async () => { const router1bis = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); await expect(ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: router1bis, // Default value is true. keepId: true, })).rejects.toThrow(Error); }, 2000); test('router.pipeToRouter() with keepId: false does not fail if both Routers belong to the same Worker', async () => { const router1bis = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); const { pipeProducer } = await ctx.router1.pipeToRouter({ producerId: ctx.videoProducer.id, router: router1bis, keepId: false, }); expect(pipeProducer.id).not.toBe(ctx.videoProducer.id); }, 2000); test('router.pipeToRouter() succeeds with data', async () => { const { pipeDataConsumer, pipeDataProducer } = (await ctx.router1.pipeToRouter({ dataProducerId: ctx.dataProducer.id, router: ctx.router2, })); const dump1 = await ctx.router1.dump(); // There should be two Transports in router1: // - WebRtcTransport for audioProducer, videoProducer and dataProducer. // - PipeTransport between router1 and router2. expect(dump1.transportIds.length).toBe(2); const dump2 = await ctx.router2.dump(); // There should be two Transports in router2: // - WebRtcTransport for audioConsumer, videoConsumer and dataConsumer. // - PipeTransport between router2 and router1. expect(dump2.transportIds.length).toBe(2); expect(typeof pipeDataConsumer.id).toBe('string'); expect(pipeDataConsumer.closed).toBe(false); expect(pipeDataConsumer.type).toBe('sctp'); expect(typeof pipeDataConsumer.sctpStreamParameters).toBe('object'); expect(typeof pipeDataConsumer.sctpStreamParameters?.streamId).toBe('number'); expect(pipeDataConsumer.sctpStreamParameters?.ordered).toBe(false); expect(pipeDataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); expect(pipeDataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); expect(pipeDataConsumer.label).toBe('foo'); expect(pipeDataConsumer.protocol).toBe('bar'); expect(pipeDataProducer.id).toBe(ctx.dataProducer.id); expect(pipeDataProducer.closed).toBe(false); expect(pipeDataProducer.type).toBe('sctp'); expect(typeof pipeDataProducer.sctpStreamParameters).toBe('object'); expect(typeof pipeDataProducer.sctpStreamParameters?.streamId).toBe('number'); expect(pipeDataProducer.sctpStreamParameters?.ordered).toBe(false); expect(pipeDataProducer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); expect(pipeDataProducer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); expect(pipeDataProducer.label).toBe('foo'); expect(pipeDataProducer.protocol).toBe('bar'); }, 2000); test('transport.dataConsume() for a pipe DataProducer succeeds', async () => { const { pipeDataProducer } = await ctx.router1.pipeToRouter({ dataProducerId: ctx.dataProducer.id, router: ctx.router2, }); const dataConsumer = await ctx.webRtcTransport2.consumeData({ dataProducerId: pipeDataProducer.id, }); expect(typeof dataConsumer.id).toBe('string'); expect(dataConsumer.closed).toBe(false); expect(dataConsumer.type).toBe('sctp'); expect(typeof dataConsumer.sctpStreamParameters).toBe('object'); expect(typeof dataConsumer.sctpStreamParameters?.streamId).toBe('number'); expect(dataConsumer.sctpStreamParameters?.ordered).toBe(false); expect(dataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); expect(dataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); expect(dataConsumer.label).toBe('foo'); expect(dataConsumer.protocol).toBe('bar'); }, 2000); test('dataProducer.close() is transmitted to pipe DataConsumer', async () => { const { pipeDataProducer } = await ctx.router1.pipeToRouter({ dataProducerId: ctx.dataProducer.id, router: ctx.router2, }); const dataConsumer = await ctx.webRtcTransport2.consumeData({ dataProducerId: pipeDataProducer.id, }); ctx.dataProducer.close(); expect(ctx.dataProducer.closed).toBe(true); if (!dataConsumer.closed) { await (0, enhancedEvents_1.enhancedOnce)(dataConsumer, 'dataproducerclose'); } expect(dataConsumer.closed).toBe(true); }, 2000); test('router.pipeToRouter() called twice generates a single PipeTransport pair', async () => { const routerA = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); const routerB = await ctx.worker2.createRouter({ mediaCodecs: ctx.mediaCodecs, }); const transportA1 = await routerA.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); const transportA2 = await routerA.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); const audioProducerA1 = await transportA1.produce(ctx.audioProducerOptions); const audioProducerA2 = await transportA2.produce(ctx.audioProducerOptions); await Promise.all([ routerA.pipeToRouter({ producerId: audioProducerA1.id, router: routerB, }), routerA.pipeToRouter({ producerId: audioProducerA2.id, router: routerB, }), ]); const dump1 = await routerA.dump(); // There should be 3 Transports in routerA: // - WebRtcTransport for audioProducerA1 and audioProducerA2. // - PipeTransport between routerA and routerB. expect(dump1.transportIds.length).toBe(3); const dump2 = await routerB.dump(); // There should be 1 Transport in routerB: // - PipeTransport between routerA and routerB. expect(dump2.transportIds.length).toBe(1); }, 2000); test('router.pipeToRouter() called in two Routers passing one to each other as argument generates a single PipeTransport pair', async () => { const routerA = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); const routerB = await ctx.worker2.createRouter({ mediaCodecs: ctx.mediaCodecs, }); const transportA = await routerA.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); const transportB = await routerB.createWebRtcTransport({ listenIps: ['127.0.0.1'], }); const audioProducerA = await transportA.produce(ctx.audioProducerOptions); const audioProducerB = await transportB.produce(ctx.audioProducerOptions); const pipeTransportsA = new Map(); const pipeTransportsB = new Map(); routerA.observer.on('newtransport', transport => { if (transport.constructor.name !== 'PipeTransportImpl') { return; } pipeTransportsA.set(transport.id, transport); transport.observer.on('close', () => pipeTransportsA.delete(transport.id)); }); routerB.observer.on('newtransport', transport => { if (transport.constructor.name !== 'PipeTransportImpl') { return; } pipeTransportsB.set(transport.id, transport); transport.observer.on('close', () => pipeTransportsB.delete(transport.id)); }); await Promise.all([ routerA.pipeToRouter({ producerId: audioProducerA.id, router: routerB, }), routerB.pipeToRouter({ producerId: audioProducerB.id, router: routerA, }), ]); // There should be a single PipeTransport in each Router and they must be // connected. expect(pipeTransportsA.size).toBe(1); expect(pipeTransportsB.size).toBe(1); const pipeTransportA = Array.from(pipeTransportsA.values())[0]; const pipeTransportB = Array.from(pipeTransportsB.values())[0]; expect(pipeTransportA.tuple.localPort).toBe(pipeTransportB.tuple.remotePort); expect(pipeTransportB.tuple.localPort).toBe(pipeTransportA.tuple.remotePort); routerA.close(); expect(pipeTransportsA.size).toBe(0); expect(pipeTransportsB.size).toBe(0); }, 2000); test('router.pipeToRouter() with neither producerId nor dataProducerId fails', async () => { const router1bis = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); await expect(ctx.router1.pipeToRouter({ router: router1bis, })).rejects.toThrow(Error); }, 2000); test('router.pipeToRouter() with both producerId and dataProducerId fails', async () => { const router1bis = await ctx.worker1.createRouter({ mediaCodecs: ctx.mediaCodecs, }); await expect(ctx.router1.pipeToRouter({ producerId: '1234', dataProducerId: '5678', router: router1bis, })).rejects.toThrow(Error); }, 2000);