uyem
Version:
WebRTC client-server SFU application
527 lines • 20 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/******************************************************************************************
* Repository: https://github.com/kolserdav/werift-sfu-react.git
* File name: record.ts
* Author: Sergey Kolmiller
* Email: <uyem.ru@gmail.com>
* License: MIT
* License text: See in LICENSE file
* Copyright: kolserdav, All rights reserved (c)
* Create Date: Wed Aug 24 2022 14:14:09 GMT+0700 (Krasnoyarsk Standard Time)
******************************************************************************************/
const werift = __importStar(require("werift"));
// import FFmpeg from 'fluent-ffmpeg';
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const date_fns_1 = require("date-fns");
const interfaces_1 = require("../types/interfaces");
const db_1 = __importDefault(require("../core/db"));
const lib_1 = require("../utils/lib");
const ffmpeg_1 = __importDefault(require("../utils/ffmpeg"));
class RecordVideo extends db_1.default {
settings;
cloudPath;
videosPath;
dirPath = {};
intervals = {};
mediaRecorders = {};
startTimes = {};
videoSettings = {};
seconds = {};
times = {};
rtc;
ws;
constructor({ settings, rtc, ws, cloudPath, prisma, }) {
super({ prisma });
this.cloudPath = cloudPath;
this.settings = settings;
this.videosPath = (0, lib_1.getVideosDirPath)({ cloudPath });
if (!fs_1.default.existsSync(this.videosPath)) {
fs_1.default.mkdirSync(this.videosPath);
}
this.rtc = rtc;
this.ws = ws;
this.setHandlers();
}
getCurrentTime({ roomId }) {
const d = new Date(this.times[roomId]);
const date = new Date();
const diffs = (0, date_fns_1.differenceInMilliseconds)(date, new Date(date.getFullYear(), date.getMonth(), date.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()));
const _diffs = (diffs / 1000).toFixed(1);
return parseFloat(_diffs);
}
setHandlers() {
this.rtc.onChangeVideoTrack = ({ roomId, target }) => {
if (!this.seconds[roomId]) {
return;
}
const mediaRecorderId = this.getMediaRecorderId(target, this.startTimes[roomId]?.[target] || 0);
const recorder = this.mediaRecorders[roomId][mediaRecorderId];
if (recorder) {
this.stopStreamRecord({
roomId,
userId: target,
pathStr: recorder.path,
time: this.getCurrentTime({ roomId }),
eventName: 'on-change-video-track',
});
}
else {
(0, lib_1.log)('info', 'Recorder is missing', {
roomId,
target,
mediaRecorderId,
recs: this.getMediaRecorderKeys(roomId),
});
}
this.startStreamRecord({
roomId,
userId: target,
time: this.getCurrentTime({ roomId }),
eventName: 'on-change-video-track',
});
};
this.rtc.onChangeMute = this.rtc.onChangeVideoTrack;
this.rtc.onRoomConnect = ({ roomId, userId }) => {
if (!this.seconds[roomId]) {
return;
}
this.startStreamRecord({
roomId,
userId,
time: this.getCurrentTime({ roomId }),
eventName: 'on-room-connect',
});
};
this.rtc.onRoomDisconnect = async ({ roomId, userId, roomUsers }) => {
if (!this.seconds[roomId]) {
return;
}
const mediaRecorderId = this.getMediaRecorderId(userId, this.startTimes[roomId]?.[userId] || 0);
const recorder = this.mediaRecorders[roomId][mediaRecorderId];
if (!recorder) {
return;
}
this.stopStreamRecord({
roomId,
userId,
pathStr: recorder.path,
time: this.getCurrentTime({ roomId }),
eventName: 'on-room-disconnect',
}, async () => {
if (roomUsers.length === 0) {
await this.stopRecord({ id: userId, roomId });
}
});
};
}
getConnId({ roomId, userId }) {
let connId = '';
const peers = Object.keys(this.rtc.streams[roomId]);
peers.forEach((item) => {
const peer = item.split(this.rtc.delimiter);
if (peer[0] === userId.toString() && peer[1] === '0') {
// eslint-disable-next-line prefer-destructuring
connId = peer[2];
}
});
if (connId === '') {
(0, lib_1.log)('error', 'Conn id is missing', {
roomId,
userId,
keys: this.rtc.getKeysStreams(roomId),
});
}
return connId;
}
getVideoSettingsHandler({ id, data: { userId, width, height }, }) {
if (!this.videoSettings[id]) {
this.videoSettings[id] = {};
}
this.videoSettings[id][userId] = { width, height };
}
changeEndTime({ pathStr, time, roomId, }) {
const fileName = pathStr.match(/\/\d+\.?\d*_0_[a-zA-Z0-9_\\-]+.webm/);
const cleanFileName = fileName ? fileName[0].replace(/\//, '').replace(interfaces_1.EXT_WEBM, '') : '';
const fileNames = cleanFileName.split(this.rtc.delimiter);
const newFileName = this.getChunkName({
startTime: fileNames[0],
endTime: time,
userId: fileNames[2],
video: fileNames[3],
audio: fileNames[4],
width: fileNames[5],
height: fileNames[6],
});
setTimeout(() => {
fs_1.default.renameSync(pathStr, path_1.default.resolve(this.dirPath[roomId], newFileName));
}, 3000);
}
getChunkName({ startTime, endTime, userId, video, audio, width, height, }) {
const ul = this.rtc.delimiter;
return `${startTime}${ul}${endTime}${ul}${userId}${ul}${video}${ul}${audio}${ul}${width}${ul}${height}${interfaces_1.EXT_WEBM}`;
}
stopStreamRecord({ roomId, userId, pathStr, time, eventName, }, cb) {
const recorderId = this.getMediaRecorderId(userId, this.startTimes[roomId][userId]);
(0, lib_1.log)('info', 'Stop stream record', { recorderId, roomId, pathStr, time, eventName });
this.mediaRecorders[roomId][recorderId].stop().then(() => {
setTimeout(() => {
this.changeEndTime({
pathStr,
time,
roomId,
});
if (this.mediaRecorders[roomId]?.[recorderId]) {
delete this.mediaRecorders[roomId][recorderId];
if (cb) {
cb();
}
}
}, 1000);
});
}
checkIsMuted({ userId, roomId }) {
const muted = this.rtc.muteds[roomId].find((item) => item === userId);
const adminMuted = this.rtc.adminMuteds[roomId].find((item) => item === userId);
return muted !== undefined || adminMuted !== undefined;
}
checkVideoPlayed({ userId, roomId, }) {
const played = this.rtc.offVideo[roomId].find((item) => item === userId);
return played === undefined;
}
getMediaRecorderId(userId, startTime) {
return `${userId}${this.rtc.delimiter}${startTime}`;
}
getMediaRecorderKeys(roomId) {
return Object.keys(this.mediaRecorders[roomId]) || [];
}
startStreamRecord({ roomId, userId, time, eventName, }) {
const videoPlayed = this.checkVideoPlayed({ roomId, userId });
const audioPlayed = !this.checkIsMuted({ roomId, userId });
if (!videoPlayed && !audioPlayed) {
return;
}
const connId = this.getConnId({ roomId, userId });
const peerId = this.rtc.getPeerId(userId, 0, connId);
const { width, height } = this.videoSettings[roomId][userId];
const _path = path_1.default.resolve(this.dirPath[roomId], `./${this.getChunkName({
startTime: time,
endTime: 0,
userId,
video: videoPlayed ? 1 : 0,
audio: audioPlayed ? 1 : 0,
width,
height,
})}`);
const recorderId = this.getMediaRecorderId(userId, time);
(0, lib_1.log)('info', 'Start stream record', { recorderId, roomId, _path, time, eventName });
this.mediaRecorders[roomId][recorderId] = new werift.MediaRecorder(_path, 2, videoPlayed
? {
width,
height,
tracks: [],
}
: { width: 1, height: 1, tracks: [] });
this.rtc.streams[roomId][peerId].forEach((item) => {
this.mediaRecorders[roomId][recorderId].addTrack(item);
});
if (!this.startTimes[roomId]) {
this.startTimes[roomId] = {};
}
this.startTimes[roomId][userId] = time;
this.mediaRecorders[roomId][recorderId].start();
}
// TODO impl
async recordVideo({ roomId, id }) {
const locale = this.ws.getLocale({ userId: id });
const dir = fs_1.default.readdirSync(this.dirPath[roomId]);
if (!dir.length) {
(0, lib_1.log)('info', 'Stop record without files', { dir, dirPath: this.dirPath[roomId] });
fs_1.default.rmSync(this.dirPath[roomId], { recursive: true, force: true });
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_RECORDING,
id,
connId: '',
data: {
time: this.seconds[roomId],
command: 'stop',
},
},
roomId,
});
delete this.dirPath[roomId];
return;
}
const backgroundImagesPath = (0, lib_1.getBackgroundsDirPath)({ cloudPath: this.cloudPath });
// TODO
const ffmpeg = new ffmpeg_1.default({
dirPath: this.dirPath[roomId],
dir,
roomId: roomId.toString(),
backgroundImagePath: null,
});
const { name, errorCode, time } = await new Promise((resolve) => {
ffmpeg
.createVideo({
loading: (procent) => {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_CREATE_VIDEO,
id,
connId: '',
data: {
procent,
},
},
roomId,
});
},
})
.then((res) => {
resolve(res);
});
});
if (errorCode === 0) {
fs_1.default.rmSync(this.dirPath[roomId], { recursive: true, force: true });
delete this.dirPath[roomId];
await this.videoCreate({
data: {
roomId: roomId.toString(),
name,
time,
},
});
}
else {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'error',
message: locale.serverError,
code: interfaces_1.ErrorCode.serverError,
},
},
roomId,
});
}
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_RECORDING,
id,
connId: '',
data: {
time: this.seconds[roomId],
command: 'stop',
},
},
roomId,
});
}
cleanRoomRecord({ roomId }) {
delete this.seconds[roomId];
delete this.times[roomId];
delete this.startTimes[roomId];
delete this.mediaRecorders[roomId];
}
async stopRecord({ id, roomId }) {
clearInterval(this.intervals[roomId]);
await Promise.all(this.getMediaRecorderKeys(roomId).map((item) => new Promise((resolve) => {
const peer = item.split(this.rtc.delimiter);
const userId = peer[0];
const recorder = this.mediaRecorders[roomId][this.getMediaRecorderId(userId, this.startTimes[roomId][userId])];
if (!recorder) {
(0, lib_1.log)('warn', 'Recorder is missing', { item });
return;
}
this.stopStreamRecord({
roomId,
userId,
pathStr: recorder.path,
time: this.getCurrentTime({ roomId }),
eventName: 'on-stop',
}, () => {
resolve(null);
});
})));
this.cleanRoomRecord({ roomId });
// await this.recordVideo({ roomId, id });
}
async handleVideoRecord(args) {
const { id: roomId, data: { command, userId: id, token }, } = args;
const sendStopRecord = () => {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_RECORDING,
id,
connId: '',
data: {
time: 0,
command: 'stop',
},
},
roomId,
});
};
const locale = this.ws.getLocale({ userId: id });
const { errorCode, unitId } = await this.checkTokenCb({ token });
const isDefault = (0, lib_1.checkDefaultAuth)({ unitId });
if (errorCode !== 0 && !isDefault) {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'warn',
message: locale.forbidden,
code: interfaces_1.ErrorCode.forbidden,
},
},
roomId,
});
sendStopRecord();
return;
}
if (unitId !== id.toString() && !isDefault) {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'warn',
message: locale.notAuthorised,
code: interfaces_1.ErrorCode.notAuthorised,
},
},
roomId,
});
sendStopRecord();
return;
}
const room = await this.roomFindFirst({
where: {
id: roomId.toString(),
},
});
if (room === undefined) {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'error',
message: locale.serverError,
code: interfaces_1.ErrorCode.serverError,
},
},
roomId,
});
sendStopRecord();
return;
}
if (room === null) {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'error',
message: locale.notFound,
code: interfaces_1.ErrorCode.notFound,
},
},
roomId,
});
sendStopRecord();
return;
}
if (unitId !== room.authorId && !isDefault) {
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_ERROR,
id,
connId: '',
data: {
type: 'warn',
message: locale.notAuthorised,
code: interfaces_1.ErrorCode.notAuthorised,
},
},
roomId,
});
sendStopRecord();
return;
}
if (!this.mediaRecorders[roomId]) {
this.mediaRecorders[roomId] = {};
}
switch (command) {
case 'start':
this.seconds[roomId] = 0;
this.times[roomId] = new Date();
this.intervals[roomId] = setInterval(() => {
this.seconds[roomId]++;
this.settings.sendMessage({
msg: {
type: interfaces_1.MessageType.SET_RECORDING,
id,
connId: '',
data: {
time: this.seconds[roomId],
command,
},
},
roomId,
});
}, 1000);
this.dirPath[roomId] = path_1.default.resolve(this.videosPath, `./${roomId}${this.rtc.delimiter}${this.times[roomId].getTime()}`);
fs_1.default.mkdirSync(this.dirPath[roomId]);
this.rtc.getKeysStreams(roomId).forEach((item) => {
const peer = item.split(this.rtc.delimiter);
const userId = peer[0];
this.startStreamRecord({ roomId, userId, time: 0, eventName: 'on-start' });
});
break;
case 'stop':
await this.stopRecord({ id, roomId });
break;
default:
}
}
}
exports.default = RecordVideo;
//# sourceMappingURL=recordVideo.js.map