mediasoup
Version:
Cutting Edge WebRTC Video Conferencing
697 lines (546 loc) • 16.2 kB
JavaScript
import * as process from 'node:process';
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import fetch from 'node-fetch';
import * as tar from 'tar';
import * as ini from 'ini';
const PKG = JSON.parse(
fs.readFileSync('./package.json', { encoding: 'utf-8' })
);
const IS_WINDOWS = os.platform() === 'win32';
const MAYOR_VERSION = PKG.version.split('.')[0];
const PYTHON = getPython();
const PIP_INVOKE_DIR = path.resolve('worker/pip_invoke');
const WORKER_RELEASE_DIR = 'worker/out/Release';
const WORKER_RELEASE_BIN = IS_WINDOWS
? 'mediasoup-worker.exe'
: 'mediasoup-worker';
const WORKER_RELEASE_BIN_PATH = `${WORKER_RELEASE_DIR}/${WORKER_RELEASE_BIN}`;
const WORKER_PREBUILD_DIR = 'worker/prebuild';
const WORKER_PREBUILD_TAR = getWorkerPrebuildTarName();
const WORKER_PREBUILD_TAR_PATH = `${WORKER_PREBUILD_DIR}/${WORKER_PREBUILD_TAR}`;
const GH_OWNER = 'versatica';
const GH_REPO = 'mediasoup';
// Paths for ESLint to check. Converted to string for convenience.
const ESLINT_PATHS = ['node/src', 'npm-scripts.mjs', 'worker/scripts'].join(
' '
);
// Paths for ESLint to ignore. Converted to string argument for convenience.
const ESLINT_IGNORE_PATTERN_ARGS = ['node/src/fbs']
.map(entry => `--ignore-pattern ${entry}`)
.join(' ');
// Paths for Prettier to check/write. Converted to string for convenience.
// NOTE: Prettier ignores paths in .gitignore so we don't need to care about
// node/src/fbs.
const PRETTIER_PATHS = [
'CHANGELOG.md',
'CONTRIBUTING.md',
'README.md',
'doc',
'node/src',
'node/tsconfig.json',
'npm-scripts.mjs',
'package.json',
'worker/scripts',
].join(' ');
const task = process.argv[2];
const args = process.argv.slice(3).join(' ');
// PYTHONPATH env must be updated now so all invoke calls below will find the
// pip invoke module.
if (process.env.PYTHONPATH) {
if (IS_WINDOWS) {
process.env.PYTHONPATH = `${PIP_INVOKE_DIR};${process.env.PYTHONPATH}`;
} else {
process.env.PYTHONPATH = `${PIP_INVOKE_DIR}:${process.env.PYTHONPATH}`;
}
} else {
process.env.PYTHONPATH = PIP_INVOKE_DIR;
}
run();
async function run() {
logInfo(args ? `[args:"${args}"]` : '');
switch (task) {
// As per NPM documentation (https://docs.npmjs.com/cli/v9/using-npm/scripts)
// `prepare` script:
//
// - Runs BEFORE the package is packed, i.e. during `npm publish` and `npm pack`.
// - Runs on local `npm install` without any arguments.
// - NOTE: If a package being installed through git contains a `prepare` script,
// its dependencies and devDependencies will be installed, and the `prepare`
// script will be run, before the package is packaged and installed.
//
// So here we generate flatbuffers definitions for TypeScript and compile
// TypeScript to JavaScript.
case 'prepare': {
flatcNode();
buildTypescript({ force: false });
break;
}
case 'postinstall': {
// If the user/app provides us with a custom mediasoup-worker binary then
// don't do anything.
if (process.env.MEDIASOUP_WORKER_BIN) {
logInfo('MEDIASOUP_WORKER_BIN environment variable given, skipping');
break;
}
// If MEDIASOUP_LOCAL_DEV is given, or if MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD
// env is given, or if mediasoup package is being installed via git+ssh
// (instead of via npm), and if MEDIASOUP_FORCE_PREBUILT_WORKER_DOWNLOAD env is
// not set, then skip mediasoup-worker prebuilt download.
else if (
(process.env.MEDIASOUP_LOCAL_DEV ||
process.env.MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD ||
process.env.npm_package_resolved?.startsWith('git+ssh://')) &&
!process.env.MEDIASOUP_FORCE_WORKER_PREBUILT_DOWNLOAD
) {
logInfo(
'skipping mediasoup-worker prebuilt download, building it locally'
);
buildWorker();
if (!process.env.MEDIASOUP_LOCAL_DEV) {
cleanWorkerArtifacts();
}
}
// Attempt to download a prebuilt binary. Fallback to building locally.
else if (!(await downloadPrebuiltWorker())) {
logInfo(
`couldn't fetch any mediasoup-worker prebuilt binary, building it locally`
);
buildWorker();
if (!process.env.MEDIASOUP_LOCAL_DEV) {
cleanWorkerArtifacts();
}
}
break;
}
case 'typescript:build': {
installNodeDeps();
buildTypescript({ force: true });
break;
}
case 'typescript:watch': {
deleteNodeLib();
executeCmd(`tsc --project node --watch ${args}`);
break;
}
case 'worker:build': {
buildWorker();
break;
}
case 'worker:prebuild': {
await prebuildWorker();
break;
}
case 'lint:node': {
lintNode();
break;
}
case 'lint:worker': {
lintWorker();
break;
}
case 'format:node': {
formatNode();
break;
}
case 'format:worker': {
installInvoke();
executeCmd(`"${PYTHON}" -m invoke -r worker format`);
break;
}
case 'flatc:node': {
flatcNode();
break;
}
case 'flatc:worker': {
flatcWorker();
break;
}
case 'test:node': {
buildTypescript({ force: false });
testNode();
break;
}
case 'test:worker': {
testWorker();
break;
}
case 'coverage:node': {
buildTypescript({ force: false });
executeCmd(`jest --coverage ${args}`);
executeCmd('open-cli coverage/lcov-report/index.html');
break;
}
case 'release:check': {
checkRelease();
break;
}
case 'release': {
let octokit;
let versionChanges;
try {
octokit = await getOctokit();
versionChanges = await getVersionChanges();
} catch (error) {
logError(error.message);
exitWithError();
}
checkRelease();
executeCmd(`git commit -am '${PKG.version}'`);
executeCmd(`git tag -a ${PKG.version} -m '${PKG.version}'`);
executeCmd(`git push origin v${MAYOR_VERSION}`);
executeCmd(`git push origin '${PKG.version}'`);
logInfo('creating release in GitHub');
await octokit.repos.createRelease({
owner: GH_OWNER,
repo: GH_REPO,
name: PKG.version,
body: versionChanges,
tag_name: PKG.version,
draft: false,
});
executeCmd('npm publish');
break;
}
default: {
logError('unknown task');
exitWithError();
}
}
}
function getPython() {
let python = process.env.PYTHON;
if (!python) {
try {
execSync('python3 --version', { stdio: ['ignore', 'ignore', 'ignore'] });
python = 'python3';
} catch (error) {
python = 'python';
}
}
return python;
}
function getWorkerPrebuildTarName() {
let name = `mediasoup-worker-${PKG.version}-${os.platform()}-${os.arch()}`;
// In Linux we want to know about kernel version since kernel >= 6 supports
// io-uring.
if (os.platform() === 'linux') {
const kernelMajorVersion = Number(os.release().split('.')[0]);
name += `-kernel${kernelMajorVersion}`;
}
return `${name}.tgz`;
}
function installInvoke() {
if (fs.existsSync(PIP_INVOKE_DIR)) {
return;
}
logInfo('installInvoke()');
// Install pip invoke into custom location, so we don't depend on system-wide
// installation.
executeCmd(
`"${PYTHON}" -m pip install --upgrade --no-user --target "${PIP_INVOKE_DIR}" invoke`,
/* exitOnError */ true
);
}
function deleteNodeLib() {
if (!fs.existsSync('node/lib')) {
return;
}
logInfo('deleteNodeLib()');
fs.rmSync('node/lib', { recursive: true, force: true });
}
function buildTypescript({ force = false } = { force: false }) {
if (!force && fs.existsSync('node/lib')) {
return;
}
logInfo('buildTypescript()');
deleteNodeLib();
executeCmd('tsc --project node');
}
function buildWorker() {
logInfo('buildWorker()');
installInvoke();
executeCmd(`"${PYTHON}" -m invoke -r worker mediasoup-worker`);
}
function cleanWorkerArtifacts() {
logInfo('cleanWorkerArtifacts()');
installInvoke();
// Clean build artifacts except `mediasoup-worker`.
executeCmd(`"${PYTHON}" -m invoke -r worker clean-build`);
// Clean downloaded dependencies.
executeCmd(`"${PYTHON}" -m invoke -r worker clean-subprojects`);
// Clean PIP/Meson/Ninja.
executeCmd(`"${PYTHON}" -m invoke -r worker clean-pip`);
}
function lintNode() {
logInfo('lintNode()');
// Ensure there are no rules that are unnecessary or conflict with Prettier
// rules.
executeCmd('eslint-config-prettier .eslintrc.js');
executeCmd(
`eslint -c .eslintrc.js --ext=ts,js,mjs --max-warnings 0 ${ESLINT_IGNORE_PATTERN_ARGS} ${ESLINT_PATHS}`
);
executeCmd(`prettier --check ${PRETTIER_PATHS}`);
}
function lintWorker() {
logInfo('lintWorker()');
installInvoke();
executeCmd(`"${PYTHON}" -m invoke -r worker lint`);
}
function formatNode() {
logInfo('formatNode()');
executeCmd(`prettier --write ${PRETTIER_PATHS}`);
}
function flatcNode() {
logInfo('flatcNode()');
installInvoke();
// Build flatc if needed.
executeCmd(`"${PYTHON}" -m invoke -r worker flatc`);
const buildType = process.env.MEDIASOUP_BUILDTYPE || 'Release';
const extension = IS_WINDOWS ? '.exe' : '';
const flatbuffersWrapFilePath = path.join(
'worker',
'subprojects',
'flatbuffers.wrap'
);
const flatbuffersWrap = ini.parse(
fs.readFileSync(flatbuffersWrapFilePath, {
encoding: 'utf-8',
})
);
const flatbuffersDir = flatbuffersWrap['wrap-file']['directory'];
const flatc = path.resolve(
path.join(
'worker',
'out',
buildType,
'build',
'subprojects',
flatbuffersDir,
`flatc${extension}`
)
);
const out = path.resolve(path.join('node', 'src'));
for (const dirent of fs.readdirSync(path.join('worker', 'fbs'), {
withFileTypes: true,
})) {
if (!dirent.isFile() || path.parse(dirent.name).ext !== '.fbs') {
continue;
}
const filePath = path.resolve(path.join('worker', 'fbs', dirent.name));
executeCmd(
`"${flatc}" --ts --ts-no-import-ext --gen-object-api -o "${out}" "${filePath}"`
);
}
}
function flatcWorker() {
logInfo('flatcWorker()');
installInvoke();
executeCmd(`"${PYTHON}" -m invoke -r worker flatc`);
}
function testNode() {
logInfo('testNode()');
executeCmd(`jest --silent false --detectOpenHandles ${args}`);
}
function testWorker() {
logInfo('testWorker()');
installInvoke();
executeCmd(`"${PYTHON}" -m invoke -r worker test`);
}
function installNodeDeps() {
logInfo('installNodeDeps()');
// Install/update Node deps.
executeCmd('npm ci --ignore-scripts');
// Update package-lock.json.
executeCmd('npm install --package-lock-only --ignore-scripts');
}
function checkRelease() {
logInfo('checkRelease()');
installNodeDeps();
flatcNode();
buildTypescript({ force: true });
buildWorker();
lintNode();
lintWorker();
testNode();
testWorker();
}
function ensureDir(dir) {
logInfo(`ensureDir() [dir:${dir}]`);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
async function prebuildWorker() {
logInfo('prebuildWorker()');
ensureDir(WORKER_PREBUILD_DIR);
return new Promise((resolve, reject) => {
// Generate a gzip file which just contains mediasoup-worker binary without
// any folder.
tar
.create(
{
cwd: WORKER_RELEASE_DIR,
gzip: true,
},
[WORKER_RELEASE_BIN]
)
.pipe(fs.createWriteStream(WORKER_PREBUILD_TAR_PATH))
.on('finish', resolve)
.on('error', reject);
});
}
// Returns a Promise resolving to true if a mediasoup-worker prebuilt binary
// was downloaded and uncompressed, false otherwise.
async function downloadPrebuiltWorker() {
const releaseBase =
process.env.MEDIASOUP_WORKER_PREBUILT_DOWNLOAD_BASE_URL ||
`${PKG.repository.url
.replace(/^git\+/, '')
.replace(/\.git$/, '')}/releases/download`;
const tarUrl = `${releaseBase}/${PKG.version}/${WORKER_PREBUILD_TAR}`;
logInfo(`downloadPrebuiltWorker() [tarUrl:${tarUrl}]`);
ensureDir(WORKER_PREBUILD_DIR);
let res;
try {
res = await fetch(tarUrl);
if (res.status === 404) {
logInfo(
'downloadPrebuiltWorker() | no available mediasoup-worker prebuilt binary for current architecture'
);
return false;
} else if (!res.ok) {
logError(
`downloadPrebuiltWorker() | failed to download mediasoup-worker prebuilt binary: ${res.status} ${res.statusText}`
);
return false;
}
} catch (error) {
logError(
`downloadPrebuiltWorker() | failed to download mediasoup-worker prebuilt binary: ${error}`
);
return false;
}
ensureDir(WORKER_RELEASE_DIR);
return new Promise(resolve => {
// Extract mediasoup-worker in the official mediasoup-worker path.
res.body
.pipe(
tar.extract({
newer: false,
cwd: WORKER_RELEASE_DIR,
})
)
.on('finish', () => {
logInfo(
'downloadPrebuiltWorker() | got mediasoup-worker prebuilt binary'
);
try {
// Give execution permission to the binary.
fs.chmodSync(WORKER_RELEASE_BIN_PATH, 0o775);
} catch (error) {
logWarn(
`downloadPrebuiltWorker() | failed to give execution permissions to the mediasoup-worker prebuilt binary: ${error}`
);
}
// Let's confirm that the fetched mediasoup-worker prebuit binary does
// run in current host. This is to prevent weird issues related to
// different versions of libc in the system and so on.
// So run mediasoup-worker without the required MEDIASOUP_VERSION env and
// expect exit code 41 (see main.cpp).
logInfo(
'downloadPrebuiltWorker() | checking fetched mediasoup-worker prebuilt binary in current host'
);
try {
const resolvedBinPath = path.resolve(WORKER_RELEASE_BIN_PATH);
// This will always fail on purpose, but if status code is 41 then
// it's good.
execSync(`"${resolvedBinPath}"`, {
stdio: ['ignore', 'ignore', 'ignore'],
// Ensure no env is passed to avoid accidents.
env: {},
});
} catch (error) {
if (error.status === 41) {
logInfo(
'downloadPrebuiltWorker() | fetched mediasoup-worker prebuilt binary is valid for current host'
);
resolve(true);
} else {
logError(
`downloadPrebuiltWorker() | fetched mediasoup-worker prebuilt binary fails to run in this host [status:${error.status}]`
);
try {
fs.unlinkSync(WORKER_RELEASE_BIN_PATH);
} catch (error2) {}
resolve(false);
}
}
})
.on('error', error => {
logError(
`downloadPrebuiltWorker() | failed to uncompress downloaded mediasoup-worker prebuilt binary: ${error}`
);
resolve(false);
});
});
}
async function getOctokit() {
if (!process.env.GITHUB_TOKEN) {
throw new Error('missing GITHUB_TOKEN environment variable');
}
// NOTE: Load dep on demand since it's a devDependency.
const { Octokit } = await import('@octokit/rest');
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
return octokit;
}
async function getVersionChanges() {
logInfo('getVersionChanges()');
// NOTE: Load dep on demand since it's a devDependency.
const marked = await import('marked');
const changelog = fs.readFileSync('./CHANGELOG.md', { encoding: 'utf-8' });
const entries = marked.lexer(changelog);
for (let idx = 0; idx < entries.length; ++idx) {
const entry = entries[idx];
if (entry.type === 'heading' && entry.text === PKG.version) {
const changes = entries[idx + 1].raw;
return changes;
}
}
// This should not happen (unless author forgot to update CHANGELOG).
throw new Error(
`no entry found in CHANGELOG.md for version '${PKG.version}'`
);
}
function executeCmd(command, exitOnError = true) {
logInfo(`executeCmd(): ${command}`);
try {
execSync(command, { stdio: ['ignore', process.stdout, process.stderr] });
} catch (error) {
if (exitOnError) {
logError(`executeCmd() failed, exiting: ${error}`);
exitWithError();
} else {
logInfo(`executeCmd() failed, ignoring: ${error}`);
}
}
}
function logInfo(message) {
// eslint-disable-next-line no-console
console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, message);
}
function logWarn(message) {
// eslint-disable-next-line no-console
console.warn(`npm-scripts.mjs \x1b[33m[WARN] [${task}]\x1b\0m`, message);
}
function logError(message) {
// eslint-disable-next-line no-console
console.error(`npm-scripts.mjs \x1b[31m[ERROR] [${task}]\x1b[0m`, message);
}
function exitWithError() {
process.exit(1);
}