Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] add recording feature #16

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
8 changes: 7 additions & 1 deletion src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ const ACTIVE_STATES = new Set([
export class SfuClient extends EventTarget {
/** @type {Error[]} */
errors = [];
/**
* @type {{ recording: boolean, webRtc: boolean }}
*/
features = {};
/** @type {SFU_CLIENT_STATE[keyof SFU_CLIENT_STATE]} */
_state = SFU_CLIENT_STATE.DISCONNECTED;
/** @type {Bus | undefined} */
Expand Down Expand Up @@ -418,7 +422,9 @@ export class SfuClient extends EventTarget {
*/
webSocket.addEventListener(
"message",
() => {
(initDataMessage) => {
const { features } = JSON.parse(initDataMessage.data || initDataMessage);
this.features = features;
resolve(new Bus(webSocket));
},
{ once: true }
Expand Down
34 changes: 33 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os from "node:os";

const FALSY_INPUT = new Set(["disable", "false", "none", "no", "0"]);
const FALSY_INPUT = new Set(["disable", "false", "none", "no", "0", "off"]);

// ------------------------------------------------------------
// ------------------ ENV VARIABLES -----------------------
Expand Down Expand Up @@ -167,6 +167,15 @@ export const LOG_TIMESTAMP = !FALSY_INPUT.has(process.env.LOG_TIMESTAMP);
export const LOG_COLOR = process.env.LOG_COLOR
? Boolean(process.env.LOG_COLOR)
: process.stdout.isTTY;
/**
* Whether recording is allowed
* If true, users can request their call to be recorded.
*
* e.g: RECORDING=1 or RECORDING=off
*
* @type {boolean}
*/
export const RECORDING = !FALSY_INPUT.has(process.env.RECORDING);

// ------------------------------------------------------------
// -------------------- SETTINGS --------------------------
Expand Down Expand Up @@ -201,6 +210,24 @@ const baseProducerOptions = {
zeroRtpOnPause: true,
};

export const recording = Object.freeze({
directory: os.tmpdir() + "/recordings",
enabled: RECORDING,
maxDuration: 1000 * 60 * 60, // 1 hour
fileTTL: 1000 * 60 * 60 * 24, // 24 hours
fileType: "mp4",
videoCodec: "libx264",
audioCodec: "aac",
audioLimit: 20,
cameraLimit: 4, // how many camera can be merged into one recording
screenLimit: 1,
});

export const dynamicPorts = Object.freeze({
min: 50000,
max: 59999,
});

export const rtc = Object.freeze({
// https://mediasoup.org/documentation/v3/mediasoup/api/#WorkerSettings
workerSettings: {
Expand Down Expand Up @@ -229,6 +256,11 @@ export const rtc = Object.freeze({
},
],
},
plainTransportOptions: {
listenIp: { ip: "0.0.0.0", announcedIp: PUBLIC_IP },
rtcpMux: true,
comedia: false,
},
// https://mediasoup.org/documentation/v3/mediasoup/api/#WebRtcTransportOptions
rtcTransportOptions: {
maxSctpMessageSize: MAX_BUF_IN,
Expand Down
45 changes: 40 additions & 5 deletions src/models/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as config from "#src/config.js";
import { getAllowedCodecs, Logger } from "#src/utils/utils.js";
import { AuthenticationError, OvercrowdedError } from "#src/utils/errors.js";
import { Session, SESSION_CLOSE_CODE } from "#src/models/session.js";
import { getWorker } from "#src/services/rtc.js";
import { getWorker } from "#src/services/resources.js";
import { Recorder } from "#src/models/recorder.js";

const logger = new Logger("CHANNEL");

Expand All @@ -18,11 +19,27 @@ const mediaCodecs = getAllowedCodecs();
* @property {number} screenCount
*/

/**
* @typedef {Object} Features
* @property {boolean} recording
* @property {boolean} webRtc
*/

/**
* @fires Channel#sessionJoin
* @fires Channel#sessionLeave
* @fires Channel#close
*/

/**
* @typedef {Object} ChannelStats
* @property {string} uuid
* @property {string} remoteAddress
* @property {SessionsStats} sessionsStats
* @property {string} createDate
* @property {boolean} webRtcEnabled
*/

export class Channel extends EventEmitter {
/** @type {Map<string, Channel>} */
static records = new Map();
Expand All @@ -39,6 +56,8 @@ export class Channel extends EventEmitter {
name;
/** @type {WithImplicitCoercion<string>} base 64 buffer key */
key;
/** @type {Recorder | undefined} */
recorder;
/** @type {import("mediasoup").types.Router}*/
router;
/** @type {Map<number, Session>} */
Expand All @@ -56,15 +75,16 @@ export class Channel extends EventEmitter {
* @param {boolean} [options.useWebRtc=true] whether to use WebRTC:
* with webRTC: can stream audio/video
* without webRTC: can only use websocket
* @param {string} [options.uploadRoute] the route to which the recording will be uploaded
*/
static async create(remoteAddress, issuer, { key, useWebRtc = true } = {}) {
static async create(remoteAddress, issuer, { key, useWebRtc = true, uploadRoute } = {}) {
const safeIssuer = `${remoteAddress}::${issuer}`;
const oldChannel = Channel.recordsByIssuer.get(safeIssuer);
if (oldChannel) {
logger.verbose(`reusing channel ${oldChannel.uuid}`);
return oldChannel;
}
const options = { key };
const options = { key, uploadRoute };
if (useWebRtc) {
options.worker = await getWorker();
options.router = await options.worker.createRouter({
Expand Down Expand Up @@ -119,8 +139,9 @@ export class Channel extends EventEmitter {
* @param {string} [options.key]
* @param {import("mediasoup").types.Worker} [options.worker]
* @param {import("mediasoup").types.Router} [options.router]
* @param {string} [options.uploadRoute] the route to which the recording will be uploaded
*/
constructor(remoteAddress, { key, worker, router } = {}) {
constructor(remoteAddress, { key, worker, router, uploadRoute } = {}) {
super();
const now = new Date();
this.createDate = now.toISOString();
Expand All @@ -130,11 +151,24 @@ export class Channel extends EventEmitter {
this.name = `${remoteAddress}*${this.uuid.slice(-5)}`;
this.router = router;
this._worker = worker;
if (config.recording.enabled) {
this.recorder = new Recorder(this, uploadRoute);
}
this._onSessionClose = this._onSessionClose.bind(this);
}

/**
* @returns {Promise<{ uuid: string, remoteAddress: string, sessionsStats: SessionsStats, createDate: string }>}
* @type {Features}
*/
get features() {
return {
recording: Boolean(this.recorder),
webRtc: Boolean(this.router),
};
}

/**
* @returns {Promise<ChannelStats>}
*/
async getStats() {
return {
Expand Down Expand Up @@ -249,6 +283,7 @@ export class Channel extends EventEmitter {
}
clearTimeout(this._closeTimeout);
this.sessions.clear();
this.recorder?.stop();
Channel.records.delete(this.uuid);
/**
* @event Channel#close
Expand Down
Loading