UNPKG

node-media-server

Version:

A Node.js implementation of RTMP Server

239 lines (216 loc) 6.33 kB
// @ts-check // // Created by Chen Mingliang on 23/11/30. // illuspas@msn.com // Copyright (c) 2023 NodeMedia. All rights reserved. // const crypto = require("node:crypto"); const Flv = require("../protocol/flv.js"); const Rtmp = require("../protocol/rtmp.js"); const AVPacket = require("../core/avpacket.js"); const BaseSession = require("../session/base_session.js"); const Context = require("../core/context.js"); class BroadcastServer { constructor() { /** @type {BaseSession | null} */ this.publisher = null; /** @type {Map<string, BaseSession>} */ this.subscribers = new Map(); /** @type {Buffer} */ this.flvHeader = Flv.createHeader(true, true); /** @type {Buffer | null} */ this.flvMetaData = null; /** @type {Buffer | null} */ this.flvAudioHeader = null; /** @type {Buffer | null} */ this.flvVideoHeader = null; /** @type {Buffer | null} */ this.rtmpMetaData = null; /** @type {Buffer | null} */ this.rtmpAudioHeader = null; /** @type {Buffer | null} */ this.rtmpVideoHeader = null; /**@type {Set<Buffer> | null} */ this.flvGopCache = null; /**@type {Set<Buffer> | null} */ this.rtmpGopCache = null; } /** * * @param {string} authKey * @param {BaseSession} session * @returns {boolean} */ verifyAuth = (authKey, session) => { if (authKey === "") { return true; } let signStr = session.streamQuery?.sign; if (signStr?.split("-")?.length !== 2) { return false; } let now = Date.now() / 1000 | 0; let exp = parseInt(signStr.split("-")[0]); let shv = signStr.split("-")[1]; let str = session.streamPath + "-" + exp + "-" + authKey; if (exp < now) { return false; } let md5 = crypto.createHash("md5"); let ohv = md5.update(str).digest("hex"); return shv === ohv; }; /** * @param {BaseSession} session * @returns {string | null} */ postPlay = (session) => { if (session.ip !== "") { Context.eventEmitter.emit("prePlay", session); } if (Context.config.auth?.play && session.ip !== "") { if (!this.verifyAuth(Context.config.auth?.secret, session)) { return `play stream ${session.streamPath} authentication verification failed`; } } if (session.ip !== "") { Context.eventEmitter.emit("postPlay", session); } switch (session.protocol) { case "flv": session.sendBuffer(this.flvHeader); if (this.flvMetaData !== null) { session.sendBuffer(this.flvMetaData); } if (this.flvAudioHeader !== null) { session.sendBuffer(this.flvAudioHeader); } if (this.flvVideoHeader !== null) { session.sendBuffer(this.flvVideoHeader); } if (this.flvGopCache !== null) { this.flvGopCache.forEach((v) => { session.sendBuffer(v); }); } break; case "rtmp": if (this.rtmpMetaData != null) { session.sendBuffer(this.rtmpMetaData); } if (this.rtmpAudioHeader != null) { session.sendBuffer(this.rtmpAudioHeader); } if (this.rtmpVideoHeader != null) { session.sendBuffer(this.rtmpVideoHeader); } if (this.rtmpGopCache !== null) { this.rtmpGopCache.forEach((v) => { session.sendBuffer(v); }); } } this.subscribers.set(session.id, session); return null; }; /** * @param {BaseSession} session */ donePlay = (session) => { session.endTime = Date.now(); if (session.ip !== "") { Context.eventEmitter.emit("donePlay", session); } this.subscribers.delete(session.id); }; /** * @param {BaseSession} session * @returns {string | null} */ postPublish = (session) => { Context.eventEmitter.emit("prePublish", session); if (Context.config.auth?.publish) { if (!this.verifyAuth(Context.config.auth?.secret, session)) { return `publish stream ${session.streamPath} authentication verification failed`; } } Context.eventEmitter.emit("postPublish", session); if (this.publisher == null) { this.publisher = session; } else { return `streamPath=${session.streamPath} already has a publisher`; } return null; }; /** * @param {BaseSession} session */ donePublish = (session) => { if (session === this.publisher) { session.endTime = Date.now(); Context.eventEmitter.emit("donePublish", session); this.publisher = null; this.flvMetaData = null; this.flvAudioHeader = null; this.flvVideoHeader = null; this.rtmpMetaData = null; this.rtmpAudioHeader = null; this.rtmpVideoHeader = null; this.flvGopCache?.clear(); this.rtmpGopCache?.clear(); } }; /** * @param {AVPacket} packet */ broadcastMessage = (packet) => { const flvMessage = Flv.createMessage(packet); const rtmpMessage = Rtmp.createMessage(packet); switch (packet.flags) { case 0: this.flvAudioHeader = Buffer.from(flvMessage); this.rtmpAudioHeader = Buffer.from(rtmpMessage); break; case 1: this.flvGopCache?.add(flvMessage); this.rtmpGopCache?.add(rtmpMessage); break; case 2: this.flvVideoHeader = Buffer.from(flvMessage); this.rtmpVideoHeader = Buffer.from(rtmpMessage); break; case 3: this.flvGopCache?.clear(); this.rtmpGopCache?.clear(); this.flvGopCache = new Set(); this.rtmpGopCache = new Set(); this.flvGopCache.add(flvMessage); this.rtmpGopCache.add(rtmpMessage); break; case 4: this.flvGopCache?.add(flvMessage); this.rtmpGopCache?.add(rtmpMessage); break; case 5: this.flvMetaData = Buffer.from(flvMessage); this.rtmpMetaData = Buffer.from(rtmpMessage); break; } if (this.flvGopCache && this.flvGopCache.size > 4096) { this.flvGopCache.clear(); } if (this.rtmpGopCache && this.rtmpGopCache.size > 4096) { this.rtmpGopCache.clear(); } this.subscribers.forEach((v, k) => { switch (v.protocol) { case "flv": v.sendBuffer(flvMessage); break; case "rtmp": v.sendBuffer(rtmpMessage); } }); }; } module.exports = BroadcastServer;