mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
971 lines (970 loc) • 38.6 kB
JavaScript
"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);