mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
648 lines (647 loc) • 25.5 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 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);