From db726c2c61240ab23de52eabe563d80452f4aa26 Mon Sep 17 00:00:00 2001 From: srgooglo Date: Fri, 13 May 2022 15:13:02 +0200 Subject: [PATCH] added `internal-nms` --- packages/streaming-server/src/index.js | 4 +- .../streaming-server/src/internal-nms/ctx.js | 13 + .../src/internal-nms/index.js | 236 +++ .../src/internal-nms/lib/amf_rules.js | 1214 +++++++++++++++ .../src/internal-nms/lib/av.js | 515 +++++++ .../src/internal-nms/lib/bitop.js | 53 + .../src/internal-nms/lib/logger.js | 53 + .../src/internal-nms/lib/utils.js | 106 ++ .../src/internal-nms/rtmp_client.js | 793 ++++++++++ .../src/internal-nms/rtmp_handshake.js | 111 ++ .../internal-nms/servers/fission_server.js | 103 ++ .../src/internal-nms/servers/relay_server.js | 255 ++++ .../src/internal-nms/servers/rtmp_server.js | 87 ++ .../src/internal-nms/servers/trans_server.js | 105 ++ .../sessionsModels/fission_session.js | 51 + .../sessionsModels/flv_session.js | 218 +++ .../sessionsModels/relay_session.js | 60 + .../sessionsModels/rtmp_session.js | 1306 +++++++++++++++++ .../sessionsModels/trans_session.js | 114 ++ packages/streaming-server/src/nms | 1 - 20 files changed, 5395 insertions(+), 3 deletions(-) create mode 100644 packages/streaming-server/src/internal-nms/ctx.js create mode 100644 packages/streaming-server/src/internal-nms/index.js create mode 100644 packages/streaming-server/src/internal-nms/lib/amf_rules.js create mode 100644 packages/streaming-server/src/internal-nms/lib/av.js create mode 100644 packages/streaming-server/src/internal-nms/lib/bitop.js create mode 100644 packages/streaming-server/src/internal-nms/lib/logger.js create mode 100644 packages/streaming-server/src/internal-nms/lib/utils.js create mode 100644 packages/streaming-server/src/internal-nms/rtmp_client.js create mode 100644 packages/streaming-server/src/internal-nms/rtmp_handshake.js create mode 100644 packages/streaming-server/src/internal-nms/servers/fission_server.js create mode 100644 packages/streaming-server/src/internal-nms/servers/relay_server.js create mode 100644 packages/streaming-server/src/internal-nms/servers/rtmp_server.js create mode 100644 packages/streaming-server/src/internal-nms/servers/trans_server.js create mode 100644 packages/streaming-server/src/internal-nms/sessionsModels/fission_session.js create mode 100644 packages/streaming-server/src/internal-nms/sessionsModels/flv_session.js create mode 100644 packages/streaming-server/src/internal-nms/sessionsModels/relay_session.js create mode 100644 packages/streaming-server/src/internal-nms/sessionsModels/rtmp_session.js create mode 100644 packages/streaming-server/src/internal-nms/sessionsModels/trans_session.js delete mode 160000 packages/streaming-server/src/nms diff --git a/packages/streaming-server/src/index.js b/packages/streaming-server/src/index.js index f58a55f0..cc882c06 100644 --- a/packages/streaming-server/src/index.js +++ b/packages/streaming-server/src/index.js @@ -12,8 +12,8 @@ import { Server } from "linebridge/dist/server" import { SessionsManager, DbManager } from "./managers" import { getStreamingKeyFromStreamPath, cpu } from "./lib" -import MediaServer from "./nms" -import FlvSession from "./nms/sessionsModels/flv_session" +import MediaServer from "./internal-nms" +import FlvSession from "./internal-nms/sessionsModels/flv_session" import { StreamingKey } from "./models" diff --git a/packages/streaming-server/src/internal-nms/ctx.js b/packages/streaming-server/src/internal-nms/ctx.js new file mode 100644 index 00000000..99688688 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/ctx.js @@ -0,0 +1,13 @@ +const EventEmitter = require("events") + +let sessions = new Map() +let publishers = new Map() +let idlePlayers = new Set() +let nodeEvent = new EventEmitter() +let stat = { + inbytes: 0, + outbytes: 0, + accepted: 0 +} + +module.exports = { sessions, publishers, idlePlayers, nodeEvent, stat } \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/index.js b/packages/streaming-server/src/internal-nms/index.js new file mode 100644 index 00000000..82d2301c --- /dev/null +++ b/packages/streaming-server/src/internal-nms/index.js @@ -0,0 +1,236 @@ +// +// Created by Mingliang Chen on 17/8/1. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// +const lodash = require("lodash") +const os = require("os") + +const { cpu } = require("../lib") + +const Logger = require("./lib/logger") +const RtmpServer = require("./servers/rtmp_server") +const TransServer = require("./servers/trans_server") +const RelayServer = require("./servers/relay_server") +const FissionServer = require("./servers/fission_server") + +const context = require("./ctx") + +class MediaServer { + constructor(config) { + this.config = config + this.context = context + } + + run() { + Logger.setLogType(this.config.logType) + + if (this.config.rtmp) { + this.nrs = new RtmpServer(this.config) + this.nrs.run() + } + + if (this.config.trans) { + if (this.config.cluster) { + Logger.log("TransServer does not work in cluster mode") + } else { + this.nts = new TransServer(this.config) + this.nts.run() + } + } + + if (this.config.relay) { + if (this.config.cluster) { + Logger.log("RelayServer does not work in cluster mode") + } else { + this.nls = new RelayServer(this.config) + this.nls.run() + } + } + + if (this.config.fission) { + if (this.config.cluster) { + Logger.log("FissionServer does not work in cluster mode") + } else { + this.nfs = new FissionServer(this.config) + this.nfs.run() + } + } + + process.on("uncaughtException", function (err) { + Logger.error("uncaughtException", err) + }) + + process.on("SIGINT", function () { + process.exit() + }) + } + + on = (eventName, listener) => { + context.nodeEvent.on(eventName, listener) + } + + stop = () => { + if (this.nrs) { + this.nrs.stop() + } + if (this.nhs) { + this.nhs.stop() + } + if (this.nls) { + this.nls.stop() + } + if (this.nfs) { + this.nfs.stop() + } + } + + getSession = (id) => { + return context.sessions.get(id) + } + + getSessions = () => { + let stats = {}; + + this.context.sessions.forEach(function (session, id) { + if (session.isStarting) { + let regRes = /\/(.*)\/(.*)/gi.exec(session.publishStreamPath || session.playStreamPath) + + if (regRes === null) { + return + } + + let [app, stream] = lodash.slice(regRes, 1) + + if (!lodash.get(stats, [app, stream])) { + lodash.setWith(stats, [app, stream], { + publisher: null, + subscribers: [] + }, Object) + } + + switch (true) { + case session.isPublishing: { + lodash.setWith(stats, [app, stream, "publisher"], { + app: app, + stream: stream, + clientId: session.id, + connectCreated: session.connectTime, + bytes: session.socket.bytesRead, + ip: session.socket.remoteAddress, + audio: session.audioCodec > 0 ? { + codec: session.audioCodecName, + profile: session.audioProfileName, + samplerate: session.audioSamplerate, + channels: session.audioChannels + } : null, + video: session.videoCodec > 0 ? { + codec: session.videoCodecName, + width: session.videoWidth, + height: session.videoHeight, + profile: session.videoProfileName, + level: session.videoLevel, + fps: session.videoFps + } : null, + }, Object) + break; + } + case !!session.playStreamPath: { + switch (session.constructor.name) { + case "NodeRtmpSession": { + stats[app][stream]["subscribers"].push({ + app: app, + stream: stream, + clientId: session.id, + connectCreated: session.connectTime, + bytes: session.socket.bytesWritten, + ip: session.socket.remoteAddress, + protocol: "rtmp" + }) + + break + } + case "NodeFlvSession": { + stats[app][stream]["subscribers"].push({ + app: app, + stream: stream, + clientId: session.id, + connectCreated: session.connectTime, + bytes: session.req.connection.bytesWritten, + ip: session.req.connection.remoteAddress, + protocol: session.TAG === "websocket-flv" ? "ws" : "http" + }) + + break + } + } + + break + } + } + } + }) + + return stats + } + + getSessionsInfo = () => { + let info = { + inbytes: 0, + outbytes: 0, + rtmp: 0, + http: 0, + ws: 0, + } + + for (let session of this.context.sessions.values()) { + if (session.TAG === "relay") { + continue + } + + let socket = session.TAG === "rtmp" ? session.socket : session.req.socket + + info.inbytes += socket.bytesRead + info.outbytes += socket.bytesWritten + info.rtmp += session.TAG === "rtmp" ? 1 : 0 + info.http += session.TAG === "http-flv" ? 1 : 0 + info.ws += session.TAG === "websocket-flv" ? 1 : 0 + } + + return info + } + + getServerStatus = async () => { + const cpuPercentageUsage = await cpu.percentageUsage() + const sessionsInfo = this.getSessionsInfo() + + return { + os: { + arch: os.arch(), + platform: os.platform(), + release: os.release(), + }, + cpu: { + num: os.cpus().length, + load: cpuPercentageUsage, + model: os.cpus()[0].model, + speed: os.cpus()[0].speed, + }, + net: { + inbytes: this.context.stat.inbytes + sessionsInfo.inbytes, + outbytes: this.context.stat.outbytes + sessionsInfo.outbytes, + }, + mem: { + totle: os.totalmem(), + free: os.freemem() + }, + nodejs: { + uptime: Math.floor(process.uptime()), + version: process.version, + mem: process.memoryUsage() + }, + } + } +} + +module.exports = MediaServer \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/lib/amf_rules.js b/packages/streaming-server/src/internal-nms/lib/amf_rules.js new file mode 100644 index 00000000..631b7eb3 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/lib/amf_rules.js @@ -0,0 +1,1214 @@ +/** + * Created by delian on 3/12/14. + * This module provides encoding and decoding of the AMF0 and AMF3 format + */ +const Logger = require("./logger") + +const amf3dRules = { + 0x00: amf3decUndefined, + 0x01: amf3decNull, + 0x02: amf3decFalse, + 0x03: amf3decTrue, + 0x04: amf3decInteger, + 0x05: amf3decDouble, + 0x06: amf3decString, + 0x07: amf3decXmlDoc, + 0x08: amf3decDate, + 0x09: amf3decArray, + 0x0A: amf3decObject, + 0x0B: amf3decXml, + 0x0C: amf3decByteArray //, + // 0x0D: amf3decVecInt, + // 0x0E: amf3decVecUInt, + // 0x0F: amf3decVecDouble, + // 0x10: amf3decVecObject, + // 0x11: amf3decDict // No dictionary support for the moment! +} + +const amf3eRules = { + "string": amf3encString, + "integer": amf3encInteger, + "double": amf3encDouble, + "xml": amf3encXmlDoc, + "object": amf3encObject, + "array": amf3encArray, + "sarray": amf3encArray, + "binary": amf3encByteArray, + "true": amf3encTrue, + "false": amf3encFalse, + "undefined": amf3encUndefined, + "null": amf3encNull +} + +const amf0dRules = { + 0x00: amf0decNumber, + 0x01: amf0decBool, + 0x02: amf0decString, + 0x03: amf0decObject, + // 0x04: amf0decMovie, // Reserved + 0x05: amf0decNull, + 0x06: amf0decUndefined, + 0x07: amf0decRef, + 0x08: amf0decArray, + // 0x09: amf0decObjEnd, // Should never happen normally + 0x0A: amf0decSArray, + 0x0B: amf0decDate, + 0x0C: amf0decLongString, + // 0x0D: amf0decUnsupported, // Has been never originally implemented by Adobe! + // 0x0E: amf0decRecSet, // Has been never originally implemented by Adobe! + 0x0F: amf0decXmlDoc, + 0x10: amf0decTypedObj, + 0x11: amf0decSwitchAmf3 +} + +const amf0eRules = { + "string": amf0encString, + "integer": amf0encNumber, + "double": amf0encNumber, + "xml": amf0encXmlDoc, + "object": amf0encObject, + "array": amf0encArray, + "sarray": amf0encSArray, + "binary": amf0encString, + "true": amf0encBool, + "false": amf0encBool, + "undefined": amf0encUndefined, + "null": amf0encNull +} + +function amfType(o) { + let jsType = typeof o + + if (o === null) return "null" + if (jsType == "undefined") return "undefined" + if (jsType == "number") { + if (parseInt(o) == o) return "integer" + return "double" + } + if (jsType == "boolean") return o ? "true" : "false" + if (jsType == "string") return "string" + if (jsType == "object") { + if (o instanceof Array) { + if (o.sarray) return "sarray" + return "array" + } + return "object" + } + throw new Error("Unsupported type!") +} + +// AMF3 implementation + +/** + * AMF3 Decode undefined value + * @returns {{len: number, value: undefined}} + */ +function amf3decUndefined() { + return { len: 1, value: undefined } +} + +/** + * AMF3 Encode undefined value + * @returns {Buffer} + */ +function amf3encUndefined() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x00) + return buf +} + +/** + * AMF3 Decode null + * @returns {{len: number, value: null}} + */ +function amf3decNull() { + return { len: 1, value: null } +} + +/** + * AMF3 Encode null + * @returns {Buffer} + */ +function amf3encNull() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x01) + return buf +} + +/** + * AMF3 Decode false + * @returns {{len: number, value: boolean}} + */ +function amf3decFalse() { + return { len: 1, value: false } +} + +/** + * AMF3 Encode false + * @returns {Buffer} + */ +function amf3encFalse() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x02) + return buf +} + +/** + * AMF3 Decode true + * @returns {{len: number, value: boolean}} + */ +function amf3decTrue() { + return { len: 1, value: true } +} + +/** + * AMF3 Encode true + * @returns {Buffer} + */ +function amf3encTrue() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x03) + return buf +} + +/** + * Generic decode of AMF3 UInt29 values + * @param buf + * @returns {{len: number, value: number}} + */ +function amf3decUI29(buf) { + let val = 0 + let len = 1 + let b + + do { + b = buf.readUInt8(len++) + val = (val << 7) + (b & 0x7F) + } while (len < 5 && b > 0x7F) + + if (len == 5) val = val | b // Preserve the major bit of the last byte + + return { len: len, value: val } +} + +/** + * Generic encode of AMF3 UInt29 value + * @param num + * @returns {Buffer} + */ +function amf3encUI29(num) { + let len = 0 + if (num < 0x80) len = 1 + if (num < 0x4000) len = 2 + if (num < 0x200000) len = 3 + if (num >= 0x200000) len = 4 + let buf = Buffer.alloc(len) + switch (len) { + case 1: + buf.writeUInt8(num, 0) + break + case 2: + buf.writeUInt8(num & 0x7F, 0) + buf.writeUInt8((num >> 7) | 0x80, 1) + break + case 3: + buf.writeUInt8(num & 0x7F, 0) + buf.writeUInt8((num >> 7) & 0x7F, 1) + buf.writeUInt8((num >> 14) | 0x80, 2) + break + case 4: + buf.writeUInt8(num & 0xFF, 0) + buf.writeUInt8((num >> 8) & 0x7F, 1) + buf.writeUInt8((num >> 15) | 0x7F, 2) + buf.writeUInt8((num >> 22) | 0x7F, 3) + break + } + return buf +} + +/** + * AMF3 Decode an integer + * @param buf + * @returns {{len: number, value: number}} + */ +function amf3decInteger(buf) { // Invert the integer + let resp = amf3decUI29(buf) + if (resp.value > 0x0FFFFFFF) resp.value = (resp.value & 0x0FFFFFFF) - 0x10000000 + return resp +} + +/** + * AMF3 Encode an integer + * @param num + * @returns {Buffer} + */ +function amf3encInteger(num) { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x4, 0) + return Buffer.concat([buf, amf3encUI29(num & 0x3FFFFFFF)]) // This AND will auto convert the sign bit! +} + +/** + * AMF3 Decode String + * @param buf + * @returns {{len: *, value: (*|String)}} + */ +function amf3decString(buf) { + let sLen = amf3decUI29(buf) + let s = sLen.value & 1 + sLen.value = sLen.value >> 1 // The real length without the lowest bit + if (s) return { len: sLen.value + sLen.len, value: buf.slice(sLen.len, sLen.len + sLen.value).toString("utf8") } + throw new Error("Error, we have a need to decode a String that is a Reference") // TODO: Implement references! +} + +/** + * AMF3 Encode String + * @param str + * @returns {Buffer} + */ +function amf3encString(str) { + let sLen = amf3encUI29(str.length << 1) + let buf = Buffer.alloc(1) + buf.writeUInt8(0x6, 0) + return Buffer.concat([buf, sLen, Buffer.from(str, "utf8")]) +} + +/** + * AMF3 Decode XMLDoc + * @param buf + * @returns {{len: *, value: (*|String)}} + */ +function amf3decXmlDoc(buf) { + let sLen = amf3decUI29(buf) + let s = sLen.value & 1 + sLen.value = sLen.value >> 1 // The real length without the lowest bit + if (s) return { len: sLen.value + sLen.len, value: buf.slice(sLen.len, sLen.len + sLen.value).toString("utf8") } + throw new Error("Error, we have a need to decode a String that is a Reference") // TODO: Implement references! +} + +/** + * AMF3 Encode XMLDoc + * @param str + * @returns {Buffer} + */ +function amf3encXmlDoc(str) { + let sLen = amf3encUI29(str.length << 1) + let buf = Buffer.alloc(1) + buf.writeUInt8(0x7, 0) + return Buffer.concat([buf, sLen, Buffer.from(str, "utf8")]) +} + +/** + * AMF3 Decode Generic XML + * @param buf + * @returns {{len: *, value: (*|String)}} + */ +function amf3decXml(buf) { + let sLen = amf3decUI29(buf) + let s = sLen.value & 1 + sLen.value = sLen.value >> 1 // The real length without the lowest bit + if (s) return { len: sLen.value + sLen.len, value: buf.slice(sLen.len, sLen.len + sLen.value).toString("utf8") } + throw new Error("Error, we have a need to decode a String that is a Reference") // TODO: Implement references! +} + +/** + * AMF3 Encode Generic XML + * @param str + * @returns {Buffer} + */ +function amf3encXml(str) { + let sLen = amf3encUI29(str.length << 1) + let buf = Buffer.alloc(1) + buf.writeUInt8(0x0B, 0) + return Buffer.concat([buf, sLen, Buffer.from(str, "utf8")]) +} + +/** + * AMF3 Decide Byte Array + * @param buf + * @returns {{len: *, value: (Array|string|*|Buffer|Blob)}} + */ +function amf3decByteArray(buf) { + let sLen = amf3decUI29(buf) + let s = sLen.value & 1 + sLen.value = sLen.value >> 1 // The real length without the lowest bit + if (s) return { len: sLen.value + sLen.len, value: buf.slice(sLen.len, sLen.len + sLen.value) } + throw new Error("Error, we have a need to decode a String that is a Reference") // TODO: Implement references! +} + +/** + * AMF3 Encode Byte Array + * @param str + * @returns {Buffer} + */ +function amf3encByteArray(str) { + let sLen = amf3encUI29(str.length << 1) + let buf = Buffer.alloc(1) + buf.writeUInt8(0x0C, 0) + return Buffer.concat([buf, sLen, (typeof str == "string") ? Buffer.from(str, "binary") : str]) +} + +/** + * AMF3 Decode Double + * @param buf + * @returns {{len: number, value: (*|Number)}} + */ +function amf3decDouble(buf) { + return { len: 9, value: buf.readDoubleBE(1) } +} + +/** + * AMF3 Encode Double + * @param num + * @returns {Buffer} + */ +function amf3encDouble(num) { + let buf = Buffer.alloc(9) + buf.writeUInt8(0x05, 0) + buf.writeDoubleBE(num, 1) + return buf +} + +/** + * AMF3 Decode Date + * @param buf + * @returns {{len: *, value: (*|Number)}} + */ +function amf3decDate(buf) { // The UI29 should be 1 + let uTz = amf3decUI29(buf) + let ts = buf.readDoubleBE(uTz.len) + return { len: uTz.len + 8, value: ts } +} + +/** + * AMF3 Encode Date + * @param ts + * @returns {Buffer} + */ +function amf3encDate(ts) { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x8, 0) + let tsBuf = Buffer.alloc(8) + tsBuf.writeDoubleBE(ts, 0) + return Buffer.concat([buf, amf3encUI29(1), tsBuf]) // We always do 1 +} + +/** + * AMF3 Decode Array + * @param buf + * @returns {{len: *, value: *}} + */ +function amf3decArray(buf) { + let count = amf3decUI29(buf) + let obj = amf3decObject(buf.slice(count.len)) + if (count.value & 1) throw new Error("This is a reference to another array, which currently we don\"t support!") + return { len: count.len + obj.len, value: obj.value } +} + +/** + * AMF3 Encode Array + */ +function amf3encArray() { + throw new Error("Encoding arrays is not supported yet!") // TODO: Implement encoding of arrays +} + +/** + * AMF3 Decode Object + * @param buf + */ +function amf3decObject(buf) { + let obj = {} + let pos = 0 + return obj +} + +/** + * AMF3 Encode Object + * @param o + */ +function amf3encObject(o) { + +} + +// AMF0 Implementation + +/** + * AMF0 Decode Number + * @param buf + * @returns {{len: number, value: (*|Number)}} + */ +function amf0decNumber(buf) { + return { len: 9, value: buf.readDoubleBE(1) } +} + +/** + * AMF0 Encode Number + * @param num + * @returns {Buffer} + */ +function amf0encNumber(num) { + let buf = Buffer.alloc(9) + buf.writeUInt8(0x00, 0) + buf.writeDoubleBE(num, 1) + return buf +} + +/** + * AMF0 Decode Boolean + * @param buf + * @returns {{len: number, value: boolean}} + */ +function amf0decBool(buf) { + return { len: 2, value: (buf.readUInt8(1) != 0) } +} + +/** + * AMF0 Encode Boolean + * @param num + * @returns {Buffer} + */ +function amf0encBool(num) { + let buf = Buffer.alloc(2) + buf.writeUInt8(0x01, 0) + buf.writeUInt8((num ? 1 : 0), 1) + return buf +} + +/** + * AMF0 Decode Null + * @returns {{len: number, value: null}} + */ +function amf0decNull() { + return { len: 1, value: null } +} + +/** + * AMF0 Encode Null + * @returns {Buffer} + */ +function amf0encNull() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x05, 0) + return buf +} + +/** + * AMF0 Decode Undefined + * @returns {{len: number, value: undefined}} + */ +function amf0decUndefined() { + return { len: 1, value: undefined } +} + +/** + * AMF0 Encode Undefined + * @returns {Buffer} + */ +function amf0encUndefined() { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x06, 0) + return buf +} + +/** + * AMF0 Decode Date + * @param buf + * @returns {{len: number, value: (*|Number)}} + */ +function amf0decDate(buf) { + // let s16 = buf.readInt16BE(1) + let ts = buf.readDoubleBE(3) + return { len: 11, value: ts } +} + +/** + * AMF0 Encode Date + * @param ts + * @returns {Buffer} + */ +function amf0encDate(ts) { + let buf = Buffer.alloc(11) + buf.writeUInt8(0x0B, 0) + buf.writeInt16BE(0, 1) + buf.writeDoubleBE(ts, 3) + return buf +} + +/** + * AMF0 Decode Object + * @param buf + * @returns {{len: number, value: {}}} + */ +function amf0decObject(buf) { // TODO: Implement references! + let obj = {} + let iBuf = buf.slice(1) + let len = 1 + // Logger.debug("ODec",iBuf.readUInt8(0)) + while (iBuf.readUInt8(0) != 0x09) { + // Logger.debug("Field", iBuf.readUInt8(0), iBuf) + let prop = amf0decUString(iBuf) + // Logger.debug("Got field for property", prop) + len += prop.len + if (iBuf.length < prop.len) { + break + } + if (iBuf.slice(prop.len).readUInt8(0) == 0x09) { + len++ + // Logger.debug("Found the end property") + break + } // END Object as value, we shall leave + if (prop.value == "") break + let val = amf0DecodeOne(iBuf.slice(prop.len)) + // Logger.debug("Got field for value", val) + obj[prop.value] = val.value + len += val.len + iBuf = iBuf.slice(prop.len + val.len) + } + return { len: len, value: obj } +} + +/** + * AMF0 Encode Object + */ +function amf0encObject(o) { + if (typeof o !== "object") return + + let data = Buffer.alloc(1) + data.writeUInt8(0x03, 0) // Type object + let k + for (k in o) { + data = Buffer.concat([data, amf0encUString(k), amf0EncodeOne(o[k])]) + } + let termCode = Buffer.alloc(1) + termCode.writeUInt8(0x09, 0) + return Buffer.concat([data, amf0encUString(""), termCode]) +} + +/** + * AMF0 Decode Reference + * @param buf + * @returns {{len: number, value: string}} + */ +function amf0decRef(buf) { + let index = buf.readUInt16BE(1) + return { len: 3, value: "ref" + index } +} + +/** + * AMF0 Encode Reference + * @param index + * @returns {Buffer} + */ +function amf0encRef(index) { + let buf = Buffer.alloc(3) + buf.writeUInt8(0x07, 0) + buf.writeUInt16BE(index, 1) + return buf +} + +/** + * AMF0 Decode String + * @param buf + * @returns {{len: *, value: (*|string|String)}} + */ +function amf0decString(buf) { + let sLen = buf.readUInt16BE(1) + return { len: 3 + sLen, value: buf.toString("utf8", 3, 3 + sLen) } +} + +/** + * AMF0 Decode Untyped (without the type byte) String + * @param buf + * @returns {{len: *, value: (*|string|String)}} + */ +function amf0decUString(buf) { + let sLen = buf.readUInt16BE(0) + return { len: 2 + sLen, value: buf.toString("utf8", 2, 2 + sLen) } +} + +/** + * Do AMD0 Encode of Untyped String + * @param s + * @returns {Buffer} + */ +function amf0encUString(str) { + let data = Buffer.from(str, "utf8") + let sLen = Buffer.alloc(2) + sLen.writeUInt16BE(data.length, 0) + return Buffer.concat([sLen, data]) +} + +/** + * AMF0 Encode String + * @param str + * @returns {Buffer} + */ +function amf0encString(str) { + let buf = Buffer.alloc(3) + buf.writeUInt8(0x02, 0) + buf.writeUInt16BE(str.length, 1) + return Buffer.concat([buf, Buffer.from(str, "utf8")]) +} + +/** + * AMF0 Decode Long String + * @param buf + * @returns {{len: *, value: (*|string|String)}} + */ +function amf0decLongString(buf) { + let sLen = buf.readUInt32BE(1) + return { len: 5 + sLen, value: buf.toString("utf8", 5, 5 + sLen) } +} + +/** + * AMF0 Encode Long String + * @param str + * @returns {Buffer} + */ +function amf0encLongString(str) { + let buf = Buffer.alloc(5) + buf.writeUInt8(0x0C, 0) + buf.writeUInt32BE(str.length, 1) + return Buffer.concat([buf, Buffer.from(str, "utf8")]) +} + +/** + * AMF0 Decode Array + * @param buf + * @returns {{len: *, value: ({}|*)}} + */ +function amf0decArray(buf) { + // let count = buf.readUInt32BE(1) + let obj = amf0decObject(buf.slice(4)) + return { len: 5 + obj.len, value: obj.value } +} + +/** + * AMF0 Encode Array + */ +function amf0encArray(a) { + let l = 0 + + if (a instanceof Array) { + l = a.length + } else { + l = Object.keys(a).length + } + + Logger.debug("Array encode", l, a) + + let buf = Buffer.alloc(5) + + buf.writeUInt8(8, 0) + buf.writeUInt32BE(l, 1) + + let data = amf0encObject(a) + + return Buffer.concat([buf, data.slice(1)]) +} + +/** + * AMF0 Encode Binary Array into binary Object + * @param aData + * @returns {Buffer} + */ +function amf0cnletray2Object(aData) { + let buf = Buffer.alloc(1) + buf.writeUInt8(0x3, 0) // Object id + return Buffer.concat([buf, aData.slice(5)]) +} + +/** + * AMF0 Encode Binary Object into binary Array + * @param oData + * @returns {Buffer} + */ +function amf0cnvObject2Array(oData) { + let buf = Buffer.alloc(5) + let o = amf0decObject(oData) + let l = Object.keys(o).length + buf.writeUInt32BE(l, 1) + return Buffer.concat([buf, oData.slice(1)]) +} + +/** + * AMF0 Decode XMLDoc + * @param buf + * @returns {{len: *, value: (*|string|String)}} + */ +function amf0decXmlDoc(buf) { + let sLen = buf.readUInt16BE(1) + return { len: 3 + sLen, value: buf.toString("utf8", 3, 3 + sLen) } +} + +/** + * AMF0 Encode XMLDoc + * @param str + * @returns {Buffer} + */ +function amf0encXmlDoc(str) { // Essentially it is the same as string + let buf = Buffer.alloc(3) + buf.writeUInt8(0x0F, 0) + buf.writeUInt16BE(str.length, 1) + return Buffer.concat([buf, Buffer.from(str, "utf8")]) +} + +/** + * AMF0 Decode Strict Array + * @param buf + * @returns {{len: number, value: Array}} + */ +function amf0decSArray(buf) { + let a = [] + let len = 5 + let ret + + for (let count = buf.readUInt32BE(1); count; count--) { + ret = amf0DecodeOne(buf.slice(len)) + a.push(ret.value) + len += ret.len + } + + return { len: len, value: amf0markSArray(a) } +} + +/** + * AMF0 Encode Strict Array + * @param a Array + */ +function amf0encSArray(a) { + Logger.debug("Do strict array!") + + let buf = Buffer.alloc(5) + + buf.writeUInt8(0x0A, 0) + buf.writeUInt32BE(a.length, 1) + + let i + + for (i = 0; i < a.length; i++) { + buf = Buffer.concat([buf, amf0EncodeOne(a[i])]) + } + + return buf +} + +function amf0markSArray(a) { + Object.defineProperty(a, "sarray", { value: true }) + + return a +} + +/** + * AMF0 Decode Typed Object + * @param buf + * @returns {{len: number, value: ({}|*)}} + */ +function amf0decTypedObj(buf) { + let className = amf0decString(buf) + let obj = amf0decObject(buf.slice(className.len - 1)) + + obj.value.__className__ = className.value + + return { len: className.len + obj.len - 1, value: obj.value } +} + +/** + * AMF0 Decode Switch AMF3 Object + * @param buf + * @returns {{len: number, value: ({}|*)}} + */ +function amf0decSwitchAmf3(buf) { + let r = amf3DecodeOne(buf.slice(1)) + + return r +} + +/** + * AMF0 Encode Typed Object + */ +function amf0encTypedObj() { + throw new Error("Error: SArray encoding is not yet implemented!") // TODO: Error +} + +/** + * Decode one value from the Buffer according to the applied rules + * @param rules + * @param buffer + * @returns {*} + */ +function amfXDecodeOne(rules, buffer) { + if (!rules[buffer.readUInt8(0)]) { + Logger.error("Unknown field", buffer.readUInt8(0)) + return null + } + + return rules[buffer.readUInt8(0)](buffer) +} + +/** + * Decode one AMF0 value + * @param buffer + * @returns {*} + */ +function amf0DecodeOne(buffer) { + return amfXDecodeOne(amf0dRules, buffer) +} + +/** + * Decode one AMF3 value + * @param buffer + * @returns {*} + */ +function amf3DecodeOne(buffer) { + return amfXDecodeOne(amf3dRules, buffer) +} + +/** + * Decode a whole buffer of AMF values according to rules and return in array + * @param rules + * @param buffer + * @returns {Array} + */ +function amfXDecode(rules, buffer) { + // We shall receive clean buffer and will respond with an array of values + let resp = [] + let res + + for (let i = 0; i < buffer.length;) { + res = amfXDecodeOne(rules, buffer.slice(i)) + i += res.len + resp.push(res.value) // Add the response + } + + return resp +} + +/** + * Decode a buffer of AMF3 values + * @param buffer + * @returns {Array} + */ +function amf3Decode(buffer) { + return amfXDecode(amf3dRules, buffer) +} + +/** + * Decode a buffer of AMF0 values + * @param buffer + * @returns {Array} + */ +function amf0Decode(buffer) { + return amfXDecode(amf0dRules, buffer) +} + +/** + * Encode one AMF value according to rules + * @param rules + * @param o + * @returns {*} + */ +function amfXEncodeOne(rules, o) { + // Logger.debug("amfXEncodeOne type",o,amfType(o),rules[amfType(o)]) + let f = rules[amfType(o)] + + if (f) { + return f(o) + } + + throw new Error("Unsupported type for encoding!") +} + +/** + * Encode one AMF0 value + * @param o + * @returns {*} + */ +function amf0EncodeOne(o) { + return amfXEncodeOne(amf0eRules, o) +} + +/** + * Encode one AMF3 value + * @param o + * @returns {*} + */ +function amf3EncodeOne(o) { + return amfXEncodeOne(amf3eRules, o) +} + +/** + * Encode an array of values into a buffer + * @param a + * @returns {Buffer} + */ +function amf3Encode(a) { + let buf = Buffer.alloc(0) + a.forEach(function (o) { + buf = Buffer.concat([buf, amf3EncodeOne(o)]) + }) + return buf +} + +/** + * Encode an array of values into a buffer + * @param a + * @returns {Buffer} + */ +function amf0Encode(a) { + let buf = Buffer.alloc(0) + a.forEach(function (o) { + buf = Buffer.concat([buf, amf0EncodeOne(o)]) + }) + return buf +} + + +const rtmpCmdCode = { + "_result": ["transId", "cmdObj", "info"], + "_error": ["transId", "cmdObj", "info", "streamId"], // Info / Streamid are optional + "onStatus": ["transId", "cmdObj", "info"], + "releaseStream": ["transId", "cmdObj", "streamName"], + "getStreamLength": ["transId", "cmdObj", "streamId"], + "getMovLen": ["transId", "cmdObj", "streamId"], + "FCPublish": ["transId", "cmdObj", "streamName"], + "FCUnpublish": ["transId", "cmdObj", "streamName"], + "FCSubscribe": ["transId", "cmdObj", "streamName"], + "onFCPublish": ["transId", "cmdObj", "info"], + "connect": ["transId", "cmdObj", "args"], + "call": ["transId", "cmdObj", "args"], + "createStream": ["transId", "cmdObj"], + "close": ["transId", "cmdObj"], + "play": ["transId", "cmdObj", "streamName", "start", "duration", "reset"], + "play2": ["transId", "cmdObj", "params"], + "deleteStream": ["transId", "cmdObj", "streamId"], + "closeStream": ["transId", "cmdObj"], + "receiveAudio": ["transId", "cmdObj", "bool"], + "receiveVideo": ["transId", "cmdObj", "bool"], + "publish": ["transId", "cmdObj", "streamName", "type"], + "seek": ["transId", "cmdObj", "ms"], + "pause": ["transId", "cmdObj", "pause", "ms"] +} + +const rtmpDataCode = { + "@setDataFrame": ["method", "dataObj"], + "onFI": ["info"], + "onMetaData": ["dataObj"], + "|RtmpSampleAccess": ["bool1", "bool2"], +} + + +/** + * Decode a data! + * @param dbuf + * @returns {{cmd: (*|string|String|*), value: *}} + */ +function decodeAmf0Data(dbuf) { + let buffer = dbuf + let resp = {} + + let cmd = amf0DecodeOne(buffer) + if (cmd) { + resp.cmd = cmd.value + buffer = buffer.slice(cmd.len) + + if (rtmpDataCode[cmd.value]) { + rtmpDataCode[cmd.value].forEach(function (n) { + if (buffer.length > 0) { + let r = amf0DecodeOne(buffer) + if (r) { + buffer = buffer.slice(r.len) + resp[n] = r.value + } + } + }) + } else { + Logger.error("Unknown command", resp) + } + } + + return resp +} + +/** + * Decode a command! + * @param dbuf + * @returns {{cmd: (*|string|String|*), value: *}} + */ +function decodeAMF0Cmd(dbuf) { + let buffer = dbuf + let resp = {} + + let cmd = amf0DecodeOne(buffer) + resp.cmd = cmd.value + buffer = buffer.slice(cmd.len) + + if (rtmpCmdCode[cmd.value]) { + rtmpCmdCode[cmd.value].forEach(function (n) { + if (buffer.length > 0) { + let r = amf0DecodeOne(buffer) + buffer = buffer.slice(r.len) + resp[n] = r.value + } + }) + } else { + Logger.error("Unknown command", resp) + } + return resp +} + +/** + * Encode AMF0 Command + * @param opt + * @returns {*} + */ +function encodeAMF0Cmd(opt) { + let data = amf0EncodeOne(opt.cmd) + + if (rtmpCmdCode[opt.cmd]) { + rtmpCmdCode[opt.cmd].forEach(function (n) { + if (opt.hasOwnProperty(n)) + data = Buffer.concat([data, amf0EncodeOne(opt[n])]) + }) + } else { + Logger.error("Unknown command", opt) + } + // Logger.debug("Encoded as",data.toString("hex")) + return data +} + +function encodeAMF0Data(opt) { + let data = amf0EncodeOne(opt.cmd) + + if (rtmpDataCode[opt.cmd]) { + rtmpDataCode[opt.cmd].forEach(function (n) { + if (opt.hasOwnProperty(n)) + data = Buffer.concat([data, amf0EncodeOne(opt[n])]) + }) + } else { + Logger.error("Unknown data", opt) + } + // Logger.debug("Encoded as",data.toString("hex")) + return data +} + +/** + * + * @param dbuf + * @returns {{}} + */ +function decodeAMF3Cmd(dbuf) { + let buffer = dbuf + let resp = {} + + let cmd = amf3DecodeOne(buffer) + resp.cmd = cmd.value + buffer = buffer.slice(cmd.len) + + if (rtmpCmdCode[cmd.value]) { + rtmpCmdCode[cmd.value].forEach(function (n) { + if (buffer.length > 0) { + let r = amf3DecodeOne(buffer) + buffer = buffer.slice(r.len) + resp[n] = r.value + } + }) + } else { + Logger.error("Unknown command", resp) + } + return resp +} + +/** + * Encode AMF3 Command + * @param opt + * @returns {*} + */ +function encodeAMF3Cmd(opt) { + let data = amf0EncodeOne(opt.cmd) + + if (rtmpCmdCode[opt.cmd]) { + rtmpCmdCode[opt.cmd].forEach(function (n) { + if (opt.hasOwnProperty(n)) + data = Buffer.concat([data, amf3EncodeOne(opt[n])]) + }) + } else { + Logger.error("Unknown command", opt) + } + return data +} + +module.exports = { + decodeAmf3Cmd: decodeAMF3Cmd, + encodeAmf3Cmd: encodeAMF3Cmd, + decodeAmf0Cmd: decodeAMF0Cmd, + encodeAmf0Cmd: encodeAMF0Cmd, + decodeAmf0Data: decodeAmf0Data, + encodeAmf0Data: encodeAMF0Data, + amfType: amfType, + amf0Encode: amf0Encode, + amf0EncodeOne: amf0EncodeOne, + amf0Decode: amf0Decode, + amf0DecodeOne: amf0DecodeOne, + amf3Encode: amf3Encode, + amf3EncodeOne: amf3EncodeOne, + amf3Decode: amf3Decode, + amf3DecodeOne: amf3DecodeOne, + amf0cnvA2O: amf0cnletray2Object, + amf0cnvO2A: amf0cnvObject2Array, + amf0markSArray: amf0markSArray, + amf0decArray: amf0decArray, + amf0decBool: amf0decBool, + amf0decDate: amf0decDate, + amf0decLongString: amf0decLongString, + amf0decNull: amf0decNull, + amf0decNumber: amf0decNumber, + amf0decObject: amf0decObject, + amf0decRef: amf0decRef, + amf0decSArray: amf0decSArray, + amf0decString: amf0decString, + amf0decTypedObj: amf0decTypedObj, + amf0decUndefined: amf0decUndefined, + amf0decXmlDoc: amf0decXmlDoc, + amf0encArray: amf0encArray, + amf0encBool: amf0encBool, + amf0encDate: amf0encDate, + amf0encLongString: amf0encLongString, + amf0encNull: amf0encNull, + amf0encNumber: amf0encNumber, + amf0encObject: amf0encObject, + amf0encRef: amf0encRef, + amf0encSArray: amf0encSArray, + amf0encString: amf0encString, + amf0encTypedObj: amf0encTypedObj, + amf0encUndefined: amf0encUndefined, + amf0encXmlDoc: amf0encXmlDoc, + amf3decArray: amf3decArray, + amf3decByteArray: amf3decByteArray, + amf3decDate: amf3decDate, + amf3decDouble: amf3decDouble, + amf3decFalse: amf3decFalse, + amf3decInteger: amf3decInteger, + amf3decNull: amf3decNull, + amf3decObject: amf3decObject, + amf3decString: amf3decString, + amf3decTrue: amf3decTrue, + amf3decUI29: amf3decUI29, + amf3decUndefined: amf3decUndefined, + amf3decXml: amf3decXml, + amf3decXmlDoc: amf3decXmlDoc, + amf3encArray: amf3encArray, + amf3encByteArray: amf3encByteArray, + amf3encDate: amf3encDate, + amf3encDouble: amf3encDouble, + amf3encFalse: amf3encFalse, + amf3encInteger: amf3encInteger, + amf3encNull: amf3encNull, + amf3encObject: amf3encObject, + amf3encString: amf3encString, + amf3encTrue: amf3encTrue, + amf3encUI29: amf3encUI29, + amf3encUndefined: amf3encUndefined, + amf3encXml: amf3encXml, + amf3encXmlDoc: amf3encXmlDoc +} diff --git a/packages/streaming-server/src/internal-nms/lib/av.js b/packages/streaming-server/src/internal-nms/lib/av.js new file mode 100644 index 00000000..637015fd --- /dev/null +++ b/packages/streaming-server/src/internal-nms/lib/av.js @@ -0,0 +1,515 @@ +// +// Created by Mingliang Chen on 17/12/21. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// + +const Bitop = require("./bitop") + +const AAC_SAMPLE_RATE = [ + 96000, 88200, 64000, 48000, + 44100, 32000, 24000, 22050, + 16000, 12000, 11025, 8000, + 7350, 0, 0, 0 +] + +const AAC_CHANNELS = [ + 0, 1, 2, 3, 4, 5, 6, 8 +] + +const AUDIO_CODEC_NAME = [ + "", + "ADPCM", + "MP3", + "LinearLE", + "Nellymoser16", + "Nellymoser8", + "Nellymoser", + "G711A", + "G711U", + "", + "AAC", + "Speex", + "", + "OPUS", + "MP3-8K", + "DeviceSpecific", + "Uncompressed" +] + +const AUDIO_SOUND_RATE = [ + 5512, 11025, 22050, 44100 +] + +const VIDEO_CODEC_NAME = [ + "", + "Jpeg", + "Sorenson-H263", + "ScreenVideo", + "On2-VP6", + "On2-VP6-Alpha", + "ScreenVideo2", + "H264", + "", + "", + "", + "", + "H265" +] + +function getObjectType(bitop) { + let audioObjectType = bitop.read(5) + if (audioObjectType === 31) { + audioObjectType = bitop.read(6) + 32 + } + return audioObjectType +} + +function getSampleRate(bitop, info) { + info.sampling_index = bitop.read(4) + return info.sampling_index == 0x0f ? bitop.read(24) : AAC_SAMPLE_RATE[info.sampling_index] +} + +function readAACSpecificConfig(aacSequenceHeader) { + let info = {} + let bitop = new Bitop(aacSequenceHeader) + bitop.read(16) + info.object_type = getObjectType(bitop) + info.sample_rate = getSampleRate(bitop, info) + info.chan_config = bitop.read(4) + if (info.chan_config < AAC_CHANNELS.length) { + info.channels = AAC_CHANNELS[info.chan_config] + } + info.sbr = -1 + info.ps = -1 + if (info.object_type == 5 || info.object_type == 29) { + if (info.object_type == 29) { + info.ps = 1 + } + info.ext_object_type = 5 + info.sbr = 1 + info.sample_rate = getSampleRate(bitop, info) + info.object_type = getObjectType(bitop) + } + + return info +} + +function getAACProfileName(info) { + switch (info.object_type) { + case 1: + return "Main" + case 2: + if (info.ps > 0) { + return "HEv2" + } + if (info.sbr > 0) { + return "HE" + } + return "LC" + case 3: + return "SSR" + case 4: + return "LTP" + case 5: + return "SBR" + default: + return "" + } +} + +function readH264SpecificConfig(avcSequenceHeader) { + let info = {} + let profile_idc, width, height, crop_left, crop_right, + crop_top, crop_bottom, frame_mbs_only, n, cf_idc, + num_ref_frames + + let bitop = new Bitop(avcSequenceHeader) + + bitop.read(48) + info.width = 0 + info.height = 0 + + do { + info.profile = bitop.read(8) + info.compat = bitop.read(8) + info.level = bitop.read(8) + info.nalu = (bitop.read(8) & 0x03) + 1 + info.nb_sps = bitop.read(8) & 0x1F + if (info.nb_sps == 0) { + break + } + /* nal size */ + bitop.read(16) + + /* nal type */ + if (bitop.read(8) != 0x67) { + break + } + /* SPS */ + profile_idc = bitop.read(8) + + /* flags */ + bitop.read(8) + + /* level idc */ + bitop.read(8) + + /* SPS id */ + bitop.read_golomb() + + if (profile_idc == 100 || profile_idc == 110 || + profile_idc == 122 || profile_idc == 244 || profile_idc == 44 || + profile_idc == 83 || profile_idc == 86 || profile_idc == 118) { + /* chroma format idc */ + cf_idc = bitop.read_golomb() + + if (cf_idc == 3) { + + /* separate color plane */ + bitop.read(1) + } + + /* bit depth luma - 8 */ + bitop.read_golomb() + + /* bit depth chroma - 8 */ + bitop.read_golomb() + + /* qpprime y zero transform bypass */ + bitop.read(1) + + /* seq scaling matrix present */ + if (bitop.read(1)) { + + for (n = 0; n < (cf_idc != 3 ? 8 : 12); n++) { + + /* seq scaling list present */ + if (bitop.read(1)) { + + /* TODO: scaling_list() + if (n < 6) { + } else { + } + */ + } + } + } + } + + /* log2 max frame num */ + bitop.read_golomb() + + /* pic order cnt type */ + switch (bitop.read_golomb()) { + case 0: + + /* max pic order cnt */ + bitop.read_golomb() + break + + case 1: + + /* delta pic order alwys zero */ + bitop.read(1) + + /* offset for non-ref pic */ + bitop.read_golomb() + + /* offset for top to bottom field */ + bitop.read_golomb() + + /* num ref frames in pic order */ + num_ref_frames = bitop.read_golomb() + + for (n = 0; n < num_ref_frames; n++) { + + /* offset for ref frame */ + bitop.read_golomb() + } + } + + /* num ref frames */ + info.avc_ref_frames = bitop.read_golomb() + + /* gaps in frame num allowed */ + bitop.read(1) + + /* pic width in mbs - 1 */ + width = bitop.read_golomb() + + /* pic height in map units - 1 */ + height = bitop.read_golomb() + + /* frame mbs only flag */ + frame_mbs_only = bitop.read(1) + + if (!frame_mbs_only) { + + /* mbs adaprive frame field */ + bitop.read(1) + } + + /* direct 8x8 inference flag */ + bitop.read(1) + + /* frame cropping */ + if (bitop.read(1)) { + + crop_left = bitop.read_golomb() + crop_right = bitop.read_golomb() + crop_top = bitop.read_golomb() + crop_bottom = bitop.read_golomb() + + } else { + crop_left = 0 + crop_right = 0 + crop_top = 0 + crop_bottom = 0 + } + info.level = info.level / 10.0 + info.width = (width + 1) * 16 - (crop_left + crop_right) * 2 + info.height = (2 - frame_mbs_only) * (height + 1) * 16 - (crop_top + crop_bottom) * 2 + + } while (0) + + return info +} + +function HEVCParsePtl(bitop, hevc, max_sub_layers_minus1) { + let general_ptl = {} + + general_ptl.profile_space = bitop.read(2) + general_ptl.tier_flag = bitop.read(1) + general_ptl.profile_idc = bitop.read(5) + general_ptl.profile_compatibility_flags = bitop.read(32) + general_ptl.general_progressive_source_flag = bitop.read(1) + general_ptl.general_interlaced_source_flag = bitop.read(1) + general_ptl.general_non_packed_constraint_flag = bitop.read(1) + general_ptl.general_frame_only_constraint_flag = bitop.read(1) + bitop.read(32) + bitop.read(12) + general_ptl.level_idc = bitop.read(8) + + general_ptl.sub_layer_profile_present_flag = [] + general_ptl.sub_layer_level_present_flag = [] + + for (let i = 0; i < max_sub_layers_minus1; i++) { + general_ptl.sub_layer_profile_present_flag[i] = bitop.read(1) + general_ptl.sub_layer_level_present_flag[i] = bitop.read(1) + } + + if (max_sub_layers_minus1 > 0) { + for (let i = max_sub_layers_minus1; i < 8; i++) { + bitop.read(2) + } + } + + general_ptl.sub_layer_profile_space = [] + general_ptl.sub_layer_tier_flag = [] + general_ptl.sub_layer_profile_idc = [] + general_ptl.sub_layer_profile_compatibility_flag = [] + general_ptl.sub_layer_progressive_source_flag = [] + general_ptl.sub_layer_interlaced_source_flag = [] + general_ptl.sub_layer_non_packed_constraint_flag = [] + general_ptl.sub_layer_frame_only_constraint_flag = [] + general_ptl.sub_layer_level_idc = [] + + for (let i = 0; i < max_sub_layers_minus1; i++) { + if (general_ptl.sub_layer_profile_present_flag[i]) { + general_ptl.sub_layer_profile_space[i] = bitop.read(2) + general_ptl.sub_layer_tier_flag[i] = bitop.read(1) + general_ptl.sub_layer_profile_idc[i] = bitop.read(5) + general_ptl.sub_layer_profile_compatibility_flag[i] = bitop.read(32) + general_ptl.sub_layer_progressive_source_flag[i] = bitop.read(1) + general_ptl.sub_layer_interlaced_source_flag[i] = bitop.read(1) + general_ptl.sub_layer_non_packed_constraint_flag[i] = bitop.read(1) + general_ptl.sub_layer_frame_only_constraint_flag[i] = bitop.read(1) + bitop.read(32) + bitop.read(12) + } + if (general_ptl.sub_layer_level_present_flag[i]) { + general_ptl.sub_layer_level_idc[i] = bitop.read(8) + } + else { + general_ptl.sub_layer_level_idc[i] = 1 + } + } + return general_ptl +} + +function HEVCParseSPS(SPS, hevc) { + let psps = {} + let NumBytesInNALunit = SPS.length + let NumBytesInRBSP = 0 + let rbsp_array = [] + let bitop = new Bitop(SPS) + + bitop.read(1)//forbidden_zero_bit + bitop.read(6)//nal_unit_type + bitop.read(6)//nuh_reserved_zero_6bits + bitop.read(3)//nuh_temporal_id_plus1 + + for (let i = 2; i < NumBytesInNALunit; i++) { + if (i + 2 < NumBytesInNALunit && bitop.look(24) == 0x000003) { + rbsp_array.push(bitop.read(8)) + rbsp_array.push(bitop.read(8)) + i += 2 + let emulation_prevention_three_byte = bitop.read(8) /* equal to 0x03 */ + } else { + rbsp_array.push(bitop.read(8)) + } + } + + let rbsp = Buffer.from(rbsp_array) + let rbspBitop = new Bitop(rbsp) + + psps.sps_video_parameter_set_id = rbspBitop.read(4) + psps.sps_max_sub_layers_minus1 = rbspBitop.read(3) + psps.sps_temporal_id_nesting_flag = rbspBitop.read(1) + psps.profile_tier_level = HEVCParsePtl(rbspBitop, hevc, psps.sps_max_sub_layers_minus1) + psps.sps_seq_parameter_set_id = rbspBitop.read_golomb() + psps.chroma_format_idc = rbspBitop.read_golomb() + + if (psps.chroma_format_idc == 3) { + psps.separate_colour_plane_flag = rbspBitop.read(1) + } else { + psps.separate_colour_plane_flag = 0 + } + + psps.pic_width_in_luma_samples = rbspBitop.read_golomb() + psps.pic_height_in_luma_samples = rbspBitop.read_golomb() + psps.conformance_window_flag = rbspBitop.read(1) + + if (psps.conformance_window_flag) { + let vert_mult = 1 + (psps.chroma_format_idc < 2) + let horiz_mult = 1 + (psps.chroma_format_idc < 3) + psps.conf_win_left_offset = rbspBitop.read_golomb() * horiz_mult + psps.conf_win_right_offset = rbspBitop.read_golomb() * horiz_mult + psps.conf_win_top_offset = rbspBitop.read_golomb() * vert_mult + psps.conf_win_bottom_offset = rbspBitop.read_golomb() * vert_mult + } + + // Logger.debug(psps) + return psps +} + +function readHEVCSpecificConfig(hevcSequenceHeader) { + let info = {} + info.width = 0 + info.height = 0 + info.profile = 0 + info.level = 0 + // let bitop = new Bitop(hevcSequenceHeader) + // bitop.read(48) + hevcSequenceHeader = hevcSequenceHeader.slice(5) + + do { + let hevc = {} + + if (hevcSequenceHeader.length < 23) { + break + } + + hevc.configurationVersion = hevcSequenceHeader[0] + + if (hevc.configurationVersion != 1) { + break + } + + hevc.general_profile_space = (hevcSequenceHeader[1] >> 6) & 0x03 + hevc.general_tier_flag = (hevcSequenceHeader[1] >> 5) & 0x01 + hevc.general_profile_idc = hevcSequenceHeader[1] & 0x1F + hevc.general_profile_compatibility_flags = (hevcSequenceHeader[2] << 24) | (hevcSequenceHeader[3] << 16) | (hevcSequenceHeader[4] << 8) | hevcSequenceHeader[5] + hevc.general_constraint_indicator_flags = ((hevcSequenceHeader[6] << 24) | (hevcSequenceHeader[7] << 16) | (hevcSequenceHeader[8] << 8) | hevcSequenceHeader[9]) + hevc.general_constraint_indicator_flags = (hevc.general_constraint_indicator_flags << 16) | (hevcSequenceHeader[10] << 8) | hevcSequenceHeader[11] + hevc.general_level_idc = hevcSequenceHeader[12] + hevc.min_spatial_segmentation_idc = ((hevcSequenceHeader[13] & 0x0F) << 8) | hevcSequenceHeader[14] + hevc.parallelismType = hevcSequenceHeader[15] & 0x03 + hevc.chromaFormat = hevcSequenceHeader[16] & 0x03 + hevc.bitDepthLumaMinus8 = hevcSequenceHeader[17] & 0x07 + hevc.bitDepthChromaMinus8 = hevcSequenceHeader[18] & 0x07 + hevc.avgFrameRate = (hevcSequenceHeader[19] << 8) | hevcSequenceHeader[20] + hevc.constantFrameRate = (hevcSequenceHeader[21] >> 6) & 0x03 + hevc.numTemporalLayers = (hevcSequenceHeader[21] >> 3) & 0x07 + hevc.temporalIdNested = (hevcSequenceHeader[21] >> 2) & 0x01 + hevc.lengthSizeMinusOne = hevcSequenceHeader[21] & 0x03 + + let numOfArrays = hevcSequenceHeader[22] + let p = hevcSequenceHeader.slice(23) + + for (let i = 0; i < numOfArrays; i++) { + if (p.length < 3) { + brak + } + let nalutype = p[0] + let n = (p[1]) << 8 | p[2] + // Logger.debug(nalutype, n) + p = p.slice(3) + + for (let j = 0; j < n; j++) { + if (p.length < 2) { + break + } + let k = (p[0] << 8) | p[1] + // Logger.debug("k", k) + if (p.length < 2 + k) { + break + } + p = p.slice(2) + if (nalutype == 33) { + //SPS + let sps = Buffer.alloc(k) + p.copy(sps, 0, 0, k) + // Logger.debug(sps, sps.length) + hevc.psps = HEVCParseSPS(sps, hevc) + info.profile = hevc.general_profile_idc + info.level = hevc.general_level_idc / 30.0 + info.width = hevc.psps.pic_width_in_luma_samples - (hevc.psps.conf_win_left_offset + hevc.psps.conf_win_right_offset) + info.height = hevc.psps.pic_height_in_luma_samples - (hevc.psps.conf_win_top_offset + hevc.psps.conf_win_bottom_offset) + } + p = p.slice(k) + } + } + } while (0) + + return info +} + +function readAVCSpecificConfig(avcSequenceHeader) { + let codec_id = avcSequenceHeader[0] & 0x0f + if (codec_id == 7) { + return readH264SpecificConfig(avcSequenceHeader) + } else if (codec_id == 12) { + return readHEVCSpecificConfig(avcSequenceHeader) + } +} + +function getAVCProfileName(info) { + switch (info.profile) { + case 1: + return "Main" + case 2: + return "Main 10" + case 3: + return "Main Still Picture" + case 66: + return "Baseline" + case 77: + return "Main" + case 100: + return "High" + default: + return "" + } +} + +module.exports = { + AUDIO_SOUND_RATE, + AUDIO_CODEC_NAME, + VIDEO_CODEC_NAME, + readAACSpecificConfig, + getAACProfileName, + readAVCSpecificConfig, + getAVCProfileName, +} diff --git a/packages/streaming-server/src/internal-nms/lib/bitop.js b/packages/streaming-server/src/internal-nms/lib/bitop.js new file mode 100644 index 00000000..047edd2e --- /dev/null +++ b/packages/streaming-server/src/internal-nms/lib/bitop.js @@ -0,0 +1,53 @@ + +class Bitop { + constructor(buffer) { + this.buffer = buffer; + this.buflen = buffer.length; + this.bufpos = 0; + this.bufoff = 0; + this.iserro = false; + } + + read(n) { + let v = 0; + let d = 0; + while (n) { + if (n < 0 || this.bufpos >= this.buflen) { + this.iserro = true; + return 0; + } + + this.iserro = false; + d = this.bufoff + n > 8 ? 8 - this.bufoff : n; + + v <<= d; + v += (this.buffer[this.bufpos] >> (8 - this.bufoff - d)) & (0xff >> (8 - d)); + + this.bufoff += d; + n -= d; + + if (this.bufoff == 8) { + this.bufpos++; + this.bufoff = 0; + } + } + return v; + } + + look(n) { + let p = this.bufpos; + let o = this.bufoff; + let v = this.read(n); + this.bufpos = p; + this.bufoff = o; + return v; + } + + read_golomb() { + let n; + for (n = 0; this.read(1) == 0 && !this.iserro; n++); + return (1 << n) + this.read(n) - 1; + } +} + +module.exports = Bitop; diff --git a/packages/streaming-server/src/internal-nms/lib/logger.js b/packages/streaming-server/src/internal-nms/lib/logger.js new file mode 100644 index 00000000..da087f3a --- /dev/null +++ b/packages/streaming-server/src/internal-nms/lib/logger.js @@ -0,0 +1,53 @@ +const chalk = require("chalk") + +const LOG_TYPES = { + NONE: 0, + ERROR: 1, + NORMAL: 2, + DEBUG: 3, + FFDEBUG: 4 +} + +let logType = LOG_TYPES.NORMAL + +const setLogType = (type) => { + if (typeof type !== "number") return + + logType = type +} + +const logTime = () => { + let nowDate = new Date() + return nowDate.toLocaleDateString() + " " + nowDate.toLocaleTimeString([], { hour12: false }) +} + +const log = (...args) => { + if (logType < LOG_TYPES.NORMAL) return + + console.log(logTime(), process.pid, chalk.bold.green("[INFO]"), ...args) +} + +const error = (...args) => { + if (logType < LOG_TYPES.ERROR) return + + console.log(logTime(), process.pid, chalk.bold.red("[ERROR]"), ...args) +} + +const debug = (...args) => { + if (logType < LOG_TYPES.DEBUG) return + + console.log(logTime(), process.pid, chalk.bold.blue("[DEBUG]"), ...args) +} + +const ffdebug = (...args) => { + if (logType < LOG_TYPES.FFDEBUG) return + + console.log(logTime(), process.pid, chalk.bold.blue("[FFDEBUG]"), ...args) +} + +module.exports = { + LOG_TYPES, + setLogType, + + log, error, debug, ffdebug +} \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/lib/utils.js b/packages/streaming-server/src/internal-nms/lib/utils.js new file mode 100644 index 00000000..e96392ab --- /dev/null +++ b/packages/streaming-server/src/internal-nms/lib/utils.js @@ -0,0 +1,106 @@ +// +// Created by Mingliang Chen on 17/8/23. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// +const Crypto = require("crypto") +const { spawn } = require("child_process") + +const context = require("../ctx") + +function generateNewSessionID() { + let sessionID = "" + const possible = "ABCDEFGHIJKLMNOPQRSTUVWKYZ0123456789" + const numPossible = possible.length + + do { + for (let i = 0; i < 8; i++) { + sessionID += possible.charAt((Math.random() * numPossible) | 0) + } + } while (context.sessions.has(sessionID)) + + return sessionID +} + +function genRandomName() { + let name = "" + const possible = "abcdefghijklmnopqrstuvwxyz0123456789" + const numPossible = possible.length + + for (let i = 0; i < 4; i++) { + name += possible.charAt((Math.random() * numPossible) | 0) + } + + return name +} + +function verifyAuth(signStr, streamId, secretKey) { + if (signStr === undefined) { + return false + } + + let now = Date.now() / 1000 | 0 + let exp = parseInt(signStr.split("-")[0]) + let shv = signStr.split("-")[1] + let str = streamId + "-" + exp + "-" + secretKey + + if (exp < now) { + return false + } + + let md5 = Crypto.createHash("md5") + let ohv = md5.update(str).digest("hex") + + return shv === ohv +} + +function getFFmpegVersion(ffpath) { + return new Promise((resolve, reject) => { + let ffmpeg_exec = spawn(ffpath, ["-version"]) + let version = "" + + ffmpeg_exec.on("error", (e) => { + reject(e) + }) + + ffmpeg_exec.stdout.on("data", (data) => { + try { + version = data.toString().split(/(?:\r\n|\r|\n)/g)[0].split("\ ")[2] + } catch (e) { + } + }) + + ffmpeg_exec.on("close", (code) => { + resolve(version) + }) + }) +} + +function getFFmpegUrl() { + let url = "" + + switch (process.platform) { + case "darwin": + url = "https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-latest-macos64-static.zip" + break + case "win32": + url = "https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-latest-win64-static.zip | https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-latest-win32-static.zip" + break + case "linux": + url = "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-i686-static.tar.xz" + break + default: + url = "http://ffmpeg.org/download.html" + break + } + + return url +} + +module.exports = { + generateNewSessionID, + verifyAuth, + genRandomName, + getFFmpegVersion, + getFFmpegUrl +} diff --git a/packages/streaming-server/src/internal-nms/rtmp_client.js b/packages/streaming-server/src/internal-nms/rtmp_client.js new file mode 100644 index 00000000..f89d63d9 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/rtmp_client.js @@ -0,0 +1,793 @@ +// +// Created by Mingliang Chen on 18/6/21. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// + +const EventEmitter = require("events") +const Crypto = require("crypto") +const Url = require("url") +const Net = require("net") +const AMF = require("./lib/amf_rules") + +const Logger = require("./lib/logger") + +const FLASHVER = "LNX 9,0,124,2" +const RTMP_OUT_CHUNK_SIZE = 60000 +const RTMP_PORT = 1935 + +const RTMP_HANDSHAKE_SIZE = 1536 +const RTMP_HANDSHAKE_UNINIT = 0 +const RTMP_HANDSHAKE_0 = 1 +const RTMP_HANDSHAKE_1 = 2 +const RTMP_HANDSHAKE_2 = 3 + +const RTMP_PARSE_INIT = 0 +const RTMP_PARSE_BASIC_HEADER = 1 +const RTMP_PARSE_MESSAGE_HEADER = 2 +const RTMP_PARSE_EXTENDED_TIMESTAMP = 3 +const RTMP_PARSE_PAYLOAD = 4 + +const RTMP_CHUNK_HEADER_MAX = 18 + +const RTMP_CHUNK_TYPE_0 = 0 // 11-bytes: timestamp(3) + length(3) + stream type(1) + stream id(4) +const RTMP_CHUNK_TYPE_1 = 1 // 7-bytes: delta(3) + length(3) + stream type(1) +const RTMP_CHUNK_TYPE_2 = 2 // 3-bytes: delta(3) +const RTMP_CHUNK_TYPE_3 = 3 // 0-byte + +const RTMP_CHANNEL_PROTOCOL = 2 +const RTMP_CHANNEL_INVOKE = 3 +const RTMP_CHANNEL_AUDIO = 4 +const RTMP_CHANNEL_VIDEO = 5 +const RTMP_CHANNEL_DATA = 6 + +const rtmpHeaderSize = [11, 7, 3, 0] + + +/* Protocol Control Messages */ +const RTMP_TYPE_SET_CHUNK_SIZE = 1 +const RTMP_TYPE_ABORT = 2 +const RTMP_TYPE_ACKNOWLEDGEMENT = 3 // bytes read report +const RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE = 5 // server bandwidth +const RTMP_TYPE_SET_PEER_BANDWIDTH = 6 // client bandwidth + +/* User Control Messages Event (4) */ +const RTMP_TYPE_EVENT = 4 + +const RTMP_TYPE_AUDIO = 8 +const RTMP_TYPE_VIDEO = 9 + +/* Data Message */ +const RTMP_TYPE_FLEX_STREAM = 15 // AMF3 +const RTMP_TYPE_DATA = 18 // AMF0 + +/* Shared Object Message */ +const RTMP_TYPE_FLEX_OBJECT = 16 // AMF3 +const RTMP_TYPE_SHARED_OBJECT = 19 // AMF0 + +/* Command Message */ +const RTMP_TYPE_FLEX_MESSAGE = 17 // AMF3 +const RTMP_TYPE_INVOKE = 20 // AMF0 + +/* Aggregate Message */ +const RTMP_TYPE_METADATA = 22 + +const RTMP_CHUNK_SIZE = 128 +const RTMP_PING_TIME = 60000 +const RTMP_PING_TIMEOUT = 30000 + +const STREAM_BEGIN = 0x00 +const STREAM_EOF = 0x01 +const STREAM_DRY = 0x02 +const STREAM_EMPTY = 0x1f +const STREAM_READY = 0x20 + +const RTMP_TRANSACTION_CONNECT = 1 +const RTMP_TRANSACTION_CREATE_STREAM = 2 +const RTMP_TRANSACTION_GET_STREAM_LENGTH = 3 + +const RtmpPacket = { + create: (fmt = 0, cid = 0) => { + return { + header: { + fmt: fmt, + cid: cid, + timestamp: 0, + length: 0, + type: 0, + stream_id: 0 + }, + clock: 0, + delta: 0, + payload: null, + capacity: 0, + bytes: 0 + } + } +} + +class NodeRtmpClient { + constructor(rtmpUrl) { + this.url = rtmpUrl + this.info = this.rtmpUrlParser(rtmpUrl) + this.isPublish = false + this.launcher = new EventEmitter() + + this.handshakePayload = Buffer.alloc(RTMP_HANDSHAKE_SIZE) + this.handshakeState = RTMP_HANDSHAKE_UNINIT + this.handshakeBytes = 0 + + this.parserBuffer = Buffer.alloc(RTMP_CHUNK_HEADER_MAX) + this.parserState = RTMP_PARSE_INIT + this.parserBytes = 0 + this.parserBasicBytes = 0 + this.parserPacket = null + this.inPackets = new Map() + + this.inChunkSize = RTMP_CHUNK_SIZE + this.outChunkSize = RTMP_CHUNK_SIZE + + this.streamId = 0 + this.isSocketOpen = false + } + + onSocketData(data) { + let bytes = data.length + let p = 0 + let n = 0 + + while (bytes > 0) { + switch (this.handshakeState) { + case RTMP_HANDSHAKE_UNINIT: + // read s0 + // Logger.debug("[rtmp client] read s0") + this.handshakeState = RTMP_HANDSHAKE_0 + this.handshakeBytes = 0 + bytes -= 1 + p += 1 + break + case RTMP_HANDSHAKE_0: + // read s1 + n = RTMP_HANDSHAKE_SIZE - this.handshakeBytes + n = n <= bytes ? n : bytes + data.copy(this.handshakePayload, this.handshakeBytes, p, p + n) + this.handshakeBytes += n + bytes -= n + p += n + if (this.handshakeBytes === RTMP_HANDSHAKE_SIZE) { + // Logger.debug("[rtmp client] read s1") + this.handshakeState = RTMP_HANDSHAKE_1 + this.handshakeBytes = 0 + this.socket.write(this.handshakePayload)// write c2 + // Logger.debug("[rtmp client] write c2") + } + break + case RTMP_HANDSHAKE_1: + //read s2 + n = RTMP_HANDSHAKE_SIZE - this.handshakeBytes + n = n <= bytes ? n : bytes + data.copy(this.handshakePayload, this.handshakeBytes, p, n) + this.handshakeBytes += n + bytes -= n + p += n + if (this.handshakeBytes === RTMP_HANDSHAKE_SIZE) { + // Logger.debug("[rtmp client] read s2") + this.handshakeState = RTMP_HANDSHAKE_2 + this.handshakeBytes = 0 + this.handshakePayload = null + + this.rtmpSendConnect() + } + break + case RTMP_HANDSHAKE_2: + return this.rtmpChunkRead(data, p, bytes) + } + } + } + + onSocketError(e) { + Logger.error("rtmp_client", "onSocketError", e) + this.isSocketOpen = false + this.stop() + } + + onSocketClose() { + // Logger.debug("rtmp_client", "onSocketClose") + this.isSocketOpen = false + this.stop() + } + + onSocketTimeout() { + // Logger.debug("rtmp_client", "onSocketTimeout") + this.isSocketOpen = false + this.stop() + } + + on(event, callback) { + this.launcher.on(event, callback) + } + + startPull() { + this._start() + } + + startPush() { + this.isPublish = true + this._start() + } + + _start() { + this.socket = Net.createConnection(this.info.port, this.info.hostname, () => { + //rtmp handshark c0c1 + let c0c1 = Crypto.randomBytes(1537) + c0c1.writeUInt8(3) + c0c1.writeUInt32BE(Date.now() / 1000, 1) + c0c1.writeUInt32BE(0, 5) + this.socket.write(c0c1) + // Logger.debug("[rtmp client] write c0c1") + }) + this.socket.on("data", this.onSocketData.bind(this)) + this.socket.on("error", this.onSocketError.bind(this)) + this.socket.on("close", this.onSocketClose.bind(this)) + this.socket.on("timeout", this.onSocketTimeout.bind(this)) + this.socket.setTimeout(60000) + } + + stop() { + if (this.streamId > 0) { + if (!this.socket.destroyed) { + if (this.isPublish) { + this.rtmpSendFCUnpublish() + } + this.rtmpSendDeleteStream() + this.socket.destroy() + } + this.streamId = 0 + this.launcher.emit("close") + } + } + + pushAudio(audioData, timestamp) { + if (this.streamId == 0) return + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_AUDIO + packet.header.type = RTMP_TYPE_AUDIO + packet.payload = audioData + packet.header.length = packet.payload.length + packet.header.timestamp = timestamp + let rtmpChunks = this.rtmpChunksCreate(packet) + this.socket.write(rtmpChunks) + } + + pushVideo(videoData, timestamp) { + if (this.streamId == 0) return + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_VIDEO + packet.header.type = RTMP_TYPE_VIDEO + packet.payload = videoData + packet.header.length = packet.payload.length + packet.header.timestamp = timestamp + let rtmpChunks = this.rtmpChunksCreate(packet) + this.socket.write(rtmpChunks) + } + + pushScript(scriptData, timestamp) { + if (this.streamId == 0) return + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_DATA + packet.header.type = RTMP_TYPE_DATA + packet.payload = scriptData + packet.header.length = packet.payload.length + packet.header.timestamp = timestamp + let rtmpChunks = this.rtmpChunksCreate(packet) + this.socket.write(rtmpChunks) + } + + rtmpUrlParser(url) { + let urlInfo = Url.parse(url, true) + urlInfo.app = urlInfo.path.split("/")[1] + urlInfo.port = urlInfo.port ? urlInfo.port : RTMP_PORT + urlInfo.tcurl = urlInfo.href.match(/rtmp:\/\/([^\/]+)\/([^\/]+)/)[0] + urlInfo.stream = urlInfo.path.slice(urlInfo.app.length + 2) + return urlInfo + } + + rtmpChunkBasicHeaderCreate(fmt, cid) { + let out + if (cid >= 64 + 255) { + out = Buffer.alloc(3) + out[0] = (fmt << 6) | 1 + out[1] = (cid - 64) & 0xFF + out[2] = ((cid - 64) >> 8) & 0xFF + } else if (cid >= 64) { + out = Buffer.alloc(2) + out[0] = (fmt << 6) | 0 + out[1] = (cid - 64) & 0xFF + } else { + out = Buffer.alloc(1) + out[0] = (fmt << 6) | cid + } + return out + } + + rtmpChunkMessageHeaderCreate(header) { + let out = Buffer.alloc(rtmpHeaderSize[header.fmt % 4]) + if (header.fmt <= RTMP_CHUNK_TYPE_2) { + out.writeUIntBE(header.timestamp >= 0xffffff ? 0xffffff : header.timestamp, 0, 3) + } + + if (header.fmt <= RTMP_CHUNK_TYPE_1) { + out.writeUIntBE(header.length, 3, 3) + out.writeUInt8(header.type, 6) + } + + if (header.fmt === RTMP_CHUNK_TYPE_0) { + out.writeUInt32LE(header.stream_id, 7) + } + return out + } + + rtmpChunksCreate(packet) { + let header = packet.header + let payload = packet.payload + let payloadSize = header.length + let chunkSize = this.outChunkSize + let chunksOffset = 0 + let payloadOffset = 0 + + let chunkBasicHeader = this.rtmpChunkBasicHeaderCreate(header.fmt, header.cid) + let chunkBasicHeader3 = this.rtmpChunkBasicHeaderCreate(RTMP_CHUNK_TYPE_3, header.cid) + let chunkMessageHeader = this.rtmpChunkMessageHeaderCreate(header) + let useExtendedTimestamp = header.timestamp >= 0xffffff + let headerSize = chunkBasicHeader.length + chunkMessageHeader.length + (useExtendedTimestamp ? 4 : 0) + + let n = headerSize + payloadSize + Math.floor(payloadSize / chunkSize) + if (useExtendedTimestamp) { + n += Math.floor(payloadSize / chunkSize) * 4 + } + if (!(payloadSize % chunkSize)) { + n -= 1 + if (useExtendedTimestamp) { //TODO CHECK + n -= 4 + } + } + + let chunks = Buffer.alloc(n) + chunkBasicHeader.copy(chunks, chunksOffset) + chunksOffset += chunkBasicHeader.length + chunkMessageHeader.copy(chunks, chunksOffset) + chunksOffset += chunkMessageHeader.length + if (useExtendedTimestamp) { + chunks.writeUInt32BE(header.timestamp, chunksOffset) + chunksOffset += 4 + } + while (payloadSize > 0) { + if (payloadSize > chunkSize) { + payload.copy(chunks, chunksOffset, payloadOffset, payloadOffset + chunkSize) + payloadSize -= chunkSize + chunksOffset += chunkSize + payloadOffset += chunkSize + chunkBasicHeader3.copy(chunks, chunksOffset) + chunksOffset += chunkBasicHeader3.length + if (useExtendedTimestamp) { + chunks.writeUInt32BE(header.timestamp, chunksOffset) + chunksOffset += 4 + } + } else { + payload.copy(chunks, chunksOffset, payloadOffset, payloadOffset + payloadSize) + payloadSize -= payloadSize + chunksOffset += payloadSize + payloadOffset += payloadSize + } + } + return chunks + } + + rtmpChunkRead(data, p, bytes) { + let size = 0 + let offset = 0 + let extended_timestamp = 0 + + while (offset < bytes) { + switch (this.parserState) { + case RTMP_PARSE_INIT: + this.parserBytes = 1 + this.parserBuffer[0] = data[p + offset++] + if (0 === (this.parserBuffer[0] & 0x3F)) { + this.parserBasicBytes = 2 + } else if (1 === (this.parserBuffer[0] & 0x3F)) { + this.parserBasicBytes = 3 + } else { + this.parserBasicBytes = 1 + } + this.parserState = RTMP_PARSE_BASIC_HEADER + break + case RTMP_PARSE_BASIC_HEADER: + while (this.parserBytes < this.parserBasicBytes && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= this.parserBasicBytes) { + this.parserState = RTMP_PARSE_MESSAGE_HEADER + } + break + case RTMP_PARSE_MESSAGE_HEADER: + size = rtmpHeaderSize[this.parserBuffer[0] >> 6] + this.parserBasicBytes + while (this.parserBytes < size && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= size) { + this.rtmpPacketParse() + this.parserState = RTMP_PARSE_EXTENDED_TIMESTAMP + } + break + case RTMP_PARSE_EXTENDED_TIMESTAMP: + size = rtmpHeaderSize[this.parserPacket.header.fmt] + this.parserBasicBytes + if (this.parserPacket.header.timestamp === 0xFFFFFF) size += 4 + while (this.parserBytes < size && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= size) { + if (this.parserPacket.header.timestamp === 0xFFFFFF) { + extended_timestamp = this.parserBuffer.readUInt32BE(rtmpHeaderSize[this.parserPacket.header.fmt] + this.parserBasicBytes) + } + + if (0 === this.parserPacket.bytes) { + if (RTMP_CHUNK_TYPE_0 === this.parserPacket.header.fmt) { + this.parserPacket.clock = 0xFFFFFF === this.parserPacket.header.timestamp ? extended_timestamp : this.parserPacket.header.timestamp + this.parserPacket.delta = 0 + } else { + this.parserPacket.delta = 0xFFFFFF === this.parserPacket.header.timestamp ? extended_timestamp : this.parserPacket.header.timestamp + } + this.rtmpPacketAlloc() + } + this.parserState = RTMP_PARSE_PAYLOAD + } + break + case RTMP_PARSE_PAYLOAD: + size = Math.min(this.inChunkSize - (this.parserPacket.bytes % this.inChunkSize), this.parserPacket.header.length - this.parserPacket.bytes) + size = Math.min(size, bytes - offset) + if (size > 0) { + data.copy(this.parserPacket.payload, this.parserPacket.bytes, p + offset, p + offset + size) + } + this.parserPacket.bytes += size + offset += size + + if (this.parserPacket.bytes >= this.parserPacket.header.length) { + this.parserState = RTMP_PARSE_INIT + this.parserPacket.bytes = 0 + this.parserPacket.clock += this.parserPacket.delta + this.rtmpHandler() + } else if (0 === (this.parserPacket.bytes % this.inChunkSize)) { + this.parserState = RTMP_PARSE_INIT + } + break + } + } + } + + rtmpPacketParse() { + let fmt = this.parserBuffer[0] >> 6 + let cid = 0 + if (this.parserBasicBytes === 2) { + cid = 64 + this.parserBuffer[1] + } else if (this.parserBasicBytes === 3) { + cid = 64 + this.parserBuffer[1] + this.parserBuffer[2] << 8 + } else { + cid = this.parserBuffer[0] & 0x3F + } + let hasp = this.inPackets.has(cid) + if (!hasp) { + this.parserPacket = RtmpPacket.create(fmt, cid) + this.inPackets.set(cid, this.parserPacket) + } else { + this.parserPacket = this.inPackets.get(cid) + } + this.parserPacket.header.fmt = fmt + this.parserPacket.header.cid = cid + this.rtmpChunkMessageHeaderRead() + // Logger.log(this.parserPacket) + + } + + rtmpChunkMessageHeaderRead() { + let offset = this.parserBasicBytes + + // timestamp / delta + if (this.parserPacket.header.fmt <= RTMP_CHUNK_TYPE_2) { + this.parserPacket.header.timestamp = this.parserBuffer.readUIntBE(offset, 3) + offset += 3 + } + + // message length + type + if (this.parserPacket.header.fmt <= RTMP_CHUNK_TYPE_1) { + this.parserPacket.header.length = this.parserBuffer.readUIntBE(offset, 3) + this.parserPacket.header.type = this.parserBuffer[offset + 3] + offset += 4 + } + + if (this.parserPacket.header.fmt === RTMP_CHUNK_TYPE_0) { + this.parserPacket.header.stream_id = this.parserBuffer.readUInt32LE(offset) + offset += 4 + } + return offset + } + + rtmpPacketAlloc() { + if (this.parserPacket.capacity < this.parserPacket.header.length) { + this.parserPacket.payload = Buffer.alloc(this.parserPacket.header.length + 1024) + this.parserPacket.capacity = this.parserPacket.header.length + 1024 + } + } + + rtmpHandler() { + switch (this.parserPacket.header.type) { + case RTMP_TYPE_SET_CHUNK_SIZE: + case RTMP_TYPE_ABORT: + case RTMP_TYPE_ACKNOWLEDGEMENT: + case RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: + case RTMP_TYPE_SET_PEER_BANDWIDTH: + return 0 === this.rtmpControlHandler() ? -1 : 0 + case RTMP_TYPE_EVENT: + return 0 === this.rtmpEventHandler() ? -1 : 0 + case RTMP_TYPE_AUDIO: + return this.rtmpAudioHandler() + case RTMP_TYPE_VIDEO: + return this.rtmpVideoHandler() + case RTMP_TYPE_FLEX_MESSAGE: + case RTMP_TYPE_INVOKE: + return this.rtmpInvokeHandler() + case RTMP_TYPE_FLEX_STREAM:// AMF3 + case RTMP_TYPE_DATA: // AMF0 + return this.rtmpDataHandler() + } + } + + rtmpControlHandler() { + let payload = this.parserPacket.payload + switch (this.parserPacket.header.type) { + case RTMP_TYPE_SET_CHUNK_SIZE: + this.inChunkSize = payload.readUInt32BE() + // Logger.debug("set inChunkSize", this.inChunkSize) + break + case RTMP_TYPE_ABORT: + break + case RTMP_TYPE_ACKNOWLEDGEMENT: + break + case RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: + this.ackSize = payload.readUInt32BE() + // Logger.debug("set ack Size", this.ackSize) + break + case RTMP_TYPE_SET_PEER_BANDWIDTH: + break + } + } + + rtmpEventHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + let event = payload.readUInt16BE() + let value = payload.readUInt32BE(2) + // Logger.log("rtmpEventHandler", event, value) + switch (event) { + case 6: + this.rtmpSendPingResponse(value) + break + } + } + + rtmpInvokeHandler() { + let offset = this.parserPacket.header.type === RTMP_TYPE_FLEX_MESSAGE ? 1 : 0 + let payload = this.parserPacket.payload.slice(offset, this.parserPacket.header.length) + let invokeMessage = AMF.decodeAmf0Cmd(payload) + // Logger.log("rtmpInvokeHandler", invokeMessage) + + switch (invokeMessage.cmd) { + case "_result": + this.rtmpCommandOnresult(invokeMessage) + break + case "_error": + this.rtmpCommandOnerror(invokeMessage) + break + case "onStatus": + this.rtmpCommandOnstatus(invokeMessage) + break + } + } + + rtmpCommandOnresult(invokeMessage) { + // Logger.debug(invokeMessage) + switch (invokeMessage.transId) { + case RTMP_TRANSACTION_CONNECT: + this.launcher.emit("status", invokeMessage.info) + this.rtmpOnconnect() + break + case RTMP_TRANSACTION_CREATE_STREAM: + this.rtmpOncreateStream(invokeMessage.info) + break + } + } + + rtmpCommandOnerror(invokeMessage) { + this.launcher.emit("status", invokeMessage.info) + } + + rtmpCommandOnstatus(invokeMessage) { + this.launcher.emit("status", invokeMessage.info) + } + + rtmpOnconnect() { + if (this.isPublish) { + this.rtmpSendReleaseStream() + this.rtmpSendFCPublish() + } + this.rtmpSendCreateStream() + } + + rtmpOncreateStream(sid) { + this.streamId = sid + if (this.isPublish) { + this.rtmpSendPublish() + this.rtmpSendSetChunkSize() + } else { + this.rtmpSendPlay() + this.rtmpSendSetBufferLength(1000) + } + } + + rtmpAudioHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + this.launcher.emit("audio", payload, this.parserPacket.clock) + } + + rtmpVideoHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + this.launcher.emit("video", payload, this.parserPacket.clock) + } + + rtmpDataHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + this.launcher.emit("script", payload, this.parserPacket.clock) + } + + sendInvokeMessage(sid, opt) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_INVOKE + packet.header.type = RTMP_TYPE_INVOKE + packet.header.stream_id = sid + packet.payload = AMF.encodeAmf0Cmd(opt) + packet.header.length = packet.payload.length + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + rtmpSendConnect() { + let opt = { + cmd: "connect", + transId: RTMP_TRANSACTION_CONNECT, + cmdObj: { + app: this.info.app, + flashVer: FLASHVER, + tcUrl: this.info.tcurl, + fpad: 0, + capabilities: 15, + audioCodecs: 3191, + videoCodecs: 252, + videoFunction: 1, + encoding: 0 + } + } + this.sendInvokeMessage(0, opt) + } + + rtmpSendReleaseStream() { + let opt = { + cmd: "releaseStream", + transId: 0, + cmdObj: null, + streamName: this.info.stream, + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendFCPublish() { + let opt = { + cmd: "FCPublish", + transId: 0, + cmdObj: null, + streamName: this.info.stream, + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendCreateStream() { + let opt = { + cmd: "createStream", + transId: RTMP_TRANSACTION_CREATE_STREAM, + cmdObj: null + } + this.sendInvokeMessage(0, opt) + } + + rtmpSendPlay() { + let opt = { + cmd: "play", + transId: 0, + cmdObj: null, + streamName: this.info.stream, + start: -2, + duration: -1, + reset: 1 + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendSetBufferLength(bufferTime) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_PROTOCOL + packet.header.type = RTMP_TYPE_EVENT + packet.payload = Buffer.alloc(10) + packet.header.length = packet.payload.length + packet.payload.writeUInt16BE(0x03) + packet.payload.writeUInt32BE(this.streamId, 2) + packet.payload.writeUInt32BE(bufferTime, 6) + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + rtmpSendPublish() { + let opt = { + cmd: "publish", + transId: 0, + cmdObj: null, + streamName: this.info.stream, + type: "live" + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendSetChunkSize() { + let rtmpBuffer = Buffer.from("02000000000004010000000000000000", "hex") + rtmpBuffer.writeUInt32BE(this.inChunkSize, 12) + this.socket.write(rtmpBuffer) + this.outChunkSize = this.inChunkSize + } + + rtmpSendFCUnpublish() { + let opt = { + cmd: "FCUnpublish", + transId: 0, + cmdObj: null, + streamName: this.info.stream, + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendDeleteStream() { + let opt = { + cmd: "deleteStream", + transId: 0, + cmdObj: null, + streamId: this.streamId + } + this.sendInvokeMessage(this.streamId, opt) + } + + rtmpSendPingResponse(time) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_PROTOCOL + packet.header.type = RTMP_TYPE_EVENT + packet.payload = Buffer.alloc(6) + packet.header.length = packet.payload.length + packet.payload.writeUInt16BE(0x07) + packet.payload.writeUInt32BE(time, 2) + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } +} + +module.exports = NodeRtmpClient \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/rtmp_handshake.js b/packages/streaming-server/src/internal-nms/rtmp_handshake.js new file mode 100644 index 00000000..e466451a --- /dev/null +++ b/packages/streaming-server/src/internal-nms/rtmp_handshake.js @@ -0,0 +1,111 @@ +// +// Created by Mingliang Chen on 17/8/1. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// +const Crypto = require("crypto") + +const MESSAGE_FORMAT_0 = 0 +const MESSAGE_FORMAT_1 = 1 +const MESSAGE_FORMAT_2 = 2 + +const RTMP_SIG_SIZE = 1536 +const SHA256DL = 32 + +const RandomCrud = Buffer.from([ + 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8, + 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57, + 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab, + 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae +]) + +const GenuineFMSConst = "Genuine Adobe Flash Media Server 001" +const GenuineFMSConstCrud = Buffer.concat([Buffer.from(GenuineFMSConst, "utf8"), RandomCrud]) + +const GenuineFPConst = "Genuine Adobe Flash Player 001" +const GenuineFPConstCrud = Buffer.concat([Buffer.from(GenuineFPConst, "utf8"), RandomCrud]) + +function calcHmac(data, key) { + let hmac = Crypto.createHmac("sha256", key) + hmac.update(data) + return hmac.digest() +} + +function GetClientGenuineConstDigestOffset(buf) { + let offset = buf[0] + buf[1] + buf[2] + buf[3] + offset = (offset % 728) + 12 + return offset +} + +function GetServerGenuineConstDigestOffset(buf) { + let offset = buf[0] + buf[1] + buf[2] + buf[3] + offset = (offset % 728) + 776 + return offset +} + +function detectClientMessageFormat(clientsig) { + let computedSignature, msg, providedSignature, sdl + sdl = GetServerGenuineConstDigestOffset(clientsig.slice(772, 776)) + msg = Buffer.concat([clientsig.slice(0, sdl), clientsig.slice(sdl + SHA256DL)], 1504) + computedSignature = calcHmac(msg, GenuineFPConst) + providedSignature = clientsig.slice(sdl, sdl + SHA256DL) + if (computedSignature.equals(providedSignature)) { + return MESSAGE_FORMAT_2 + } + sdl = GetClientGenuineConstDigestOffset(clientsig.slice(8, 12)) + msg = Buffer.concat([clientsig.slice(0, sdl), clientsig.slice(sdl + SHA256DL)], 1504) + computedSignature = calcHmac(msg, GenuineFPConst) + providedSignature = clientsig.slice(sdl, sdl + SHA256DL) + if (computedSignature.equals(providedSignature)) { + return MESSAGE_FORMAT_1 + } + return MESSAGE_FORMAT_0 +} + +function generateS1(messageFormat) { + let randomBytes = Crypto.randomBytes(RTMP_SIG_SIZE - 8) + let handshakeBytes = Buffer.concat([Buffer.from([0, 0, 0, 0, 1, 2, 3, 4]), randomBytes], RTMP_SIG_SIZE) + + let serverDigestOffset + if (messageFormat === 1) { + serverDigestOffset = GetClientGenuineConstDigestOffset(handshakeBytes.slice(8, 12)) + } else { + serverDigestOffset = GetServerGenuineConstDigestOffset(handshakeBytes.slice(772, 776)) + } + + let msg = Buffer.concat([handshakeBytes.slice(0, serverDigestOffset), handshakeBytes.slice(serverDigestOffset + SHA256DL)], RTMP_SIG_SIZE - SHA256DL) + let hash = calcHmac(msg, GenuineFMSConst) + hash.copy(handshakeBytes, serverDigestOffset, 0, 32) + return handshakeBytes +} + +function generateS2(messageFormat, clientsig, callback) { + let randomBytes = Crypto.randomBytes(RTMP_SIG_SIZE - 32) + let challengeKeyOffset + if (messageFormat === 1) { + challengeKeyOffset = GetClientGenuineConstDigestOffset(clientsig.slice(8, 12)) + } else { + challengeKeyOffset = GetServerGenuineConstDigestOffset(clientsig.slice(772, 776)) + } + let challengeKey = clientsig.slice(challengeKeyOffset, challengeKeyOffset + 32) + let hash = calcHmac(challengeKey, GenuineFMSConstCrud) + let signature = calcHmac(randomBytes, hash) + let s2Bytes = Buffer.concat([randomBytes, signature], RTMP_SIG_SIZE) + return s2Bytes +} + +function generateS0S1S2(clientsig) { + let clientType = Buffer.alloc(1, 3) + let messageFormat = detectClientMessageFormat(clientsig) + let allBytes + if (messageFormat === MESSAGE_FORMAT_0) { + // Logger.debug("[rtmp handshake] using simple handshake.") + allBytes = Buffer.concat([clientType, clientsig, clientsig]) + } else { + // Logger.debug("[rtmp handshake] using complex handshake.") + allBytes = Buffer.concat([clientType, generateS1(messageFormat), generateS2(messageFormat, clientsig)]) + } + return allBytes +} + +module.exports = { generateS0S1S2 } diff --git a/packages/streaming-server/src/internal-nms/servers/fission_server.js b/packages/streaming-server/src/internal-nms/servers/fission_server.js new file mode 100644 index 00000000..2383b070 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/servers/fission_server.js @@ -0,0 +1,103 @@ +const fs = require("fs") +const loadash = require("lodash") +const mkdirp = require("mkdirp") + +const Logger = require("../lib/logger") +const FissionSession = require("../sessionsModels/fission_session") +const { getFFmpegVersion, getFFmpegUrl } = require("../lib/utils") + +const context = require("../ctx") + +class NodeFissionServer { + constructor(config) { + this.config = config + this.fissionSessions = new Map() + } + + async run() { + try { + mkdirp.sync(this.config.mediaroot) + fs.accessSync(this.config.mediaroot, fs.constants.W_OK) + } catch (error) { + Logger.error(`Node Media Fission Server startup failed. MediaRoot:${this.config.mediaroot} cannot be written.`) + return + } + + try { + fs.accessSync(this.config.fission.ffmpeg, fs.constants.X_OK) + } catch (error) { + Logger.error(`Node Media Fission Server startup failed. ffmpeg:${this.config.fission.ffmpeg} cannot be executed.`) + return + } + + let version = await getFFmpegVersion(this.config.fission.ffmpeg) + + if (version === "" || parseInt(version.split(".")[0]) < 4) { + Logger.error("Node Media Fission Server startup failed. ffmpeg requires version 4.0.0 above") + Logger.error("Download the latest ffmpeg static program:", getFFmpegUrl()) + return + } + + context.nodeEvent.on("postPublish", this.onPostPublish.bind(this)) + context.nodeEvent.on("donePublish", this.onDonePublish.bind(this)) + + Logger.log(`Node Media Fission Server started, MediaRoot: ${this.config.mediaroot}, ffmpeg version: ${version}`) + } + + async onPostPublish(id, streamPath, args) { + const fixedStreamingKey = streamPath.split("/").pop() + const userspace = await global.resolveUserspaceOfStreamingKey(fixedStreamingKey) + + if (!userspace) { + console.error("No userspace found for streaming key:", fixedStreamingKey) + return false + } + + let regRes = /\/(.*)\/(.*)/gi.exec(streamPath) + let [app, name] = loadash.slice(regRes, 1) + + for (let task of this.config.fission.tasks) { + regRes = /(.*)\/(.*)/gi.exec(task.rule) + let [ruleApp, ruleName] = loadash.slice(regRes, 1) + + if ((app === ruleApp || ruleApp === "*") && (name === ruleName || ruleName === "*")) { + let s = context.sessions.get(id) + + if (s.isLocal && name.split("_")[1]) { + continue + } + + let conf = task + + conf.ffmpeg = this.config.fission.ffmpeg + conf.mediaroot = this.config.mediaroot + conf.rtmpPort = this.config.rtmp.port + conf.streamPath = streamPath + conf.streamApp = app + conf.streamName = name + conf.fixedStreamName = userspace.username + conf.args = args + + let session = new FissionSession(conf) + + this.fissionSessions.set(id, session) + + session.on("end", () => { + this.fissionSessions.delete(id) + }) + + session.run() + } + } + } + + onDonePublish(id, streamPath, args) { + let session = this.fissionSessions.get(id) + + if (session) { + session.end() + } + } +} + +module.exports = NodeFissionServer diff --git a/packages/streaming-server/src/internal-nms/servers/relay_server.js b/packages/streaming-server/src/internal-nms/servers/relay_server.js new file mode 100644 index 00000000..db4ca652 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/servers/relay_server.js @@ -0,0 +1,255 @@ +const fs = require("fs") +const _ = require("lodash") +const querystring = require("querystring") + +const { getFFmpegVersion, getFFmpegUrl } = require("../lib/utils") +const NodeCoreUtils = require("../lib/utils") +const Logger = require("../lib/logger") +const NodeRelaySession = require("../sessionsModels/relay_session") + +const context = require("../ctx") + +class NodeRelayServer { + constructor(config) { + this.config = config + this.staticCycle = null + this.staticSessions = new Map() + this.dynamicSessions = new Map() + } + + async run() { + try { + fs.accessSync(this.config.relay.ffmpeg, fs.constants.X_OK) + } catch (error) { + Logger.error(`Node Media Relay Server startup failed. ffmpeg:${this.config.relay.ffmpeg} cannot be executed.`) + return + } + + let version = await getFFmpegVersion(this.config.relay.ffmpeg) + + if (version === "" || parseInt(version.split(".")[0]) < 4) { + Logger.error("Node Media Relay Server startup failed. ffmpeg requires version 4.0.0 above") + Logger.error("Download the latest ffmpeg static program:", getFFmpegUrl()) + return + } + + context.nodeEvent.on("relayPull", this.onRelayPull.bind(this)) + context.nodeEvent.on("relayPush", this.onRelayPush.bind(this)) + context.nodeEvent.on("prePlay", this.onPrePlay.bind(this)) + context.nodeEvent.on("donePlay", this.onDonePlay.bind(this)) + context.nodeEvent.on("postPublish", this.onPostPublish.bind(this)) + context.nodeEvent.on("donePublish", this.onDonePublish.bind(this)) + + this.staticCycle = setInterval(this.onStatic.bind(this), 1000) + + Logger.log("Node Media Relay Server started") + } + + onStatic() { + if (!this.config.relay.tasks) { + return + } + + let i = this.config.relay.tasks.length + + while (i--) { + if (this.staticSessions.has(i)) { + continue + } + + let conf = this.config.relay.tasks[i] + let isStatic = conf.mode === "static" + + if (isStatic) { + conf.name = conf.name ? conf.name : NodeCoreUtils.genRandomName() + conf.ffmpeg = this.config.relay.ffmpeg + conf.inPath = conf.edge + conf.ouPath = `rtmp://127.0.0.1:${this.config.rtmp.port}/${conf.app}/${conf.name}` + + let session = new NodeRelaySession(conf) + + session.id = i + + session.streamPath = `/${conf.app}/${conf.name}` + + session.on("end", (id) => { + this.staticSessions.delete(id) + }) + + this.staticSessions.set(i, session) + + session.run() + + Logger.log("[relay static pull] start", i, conf.inPath, "to", conf.ouPath) + } + } + } + + //从远端拉推到本地 + onRelayPull(url, app, name) { + let conf = {} + + conf.app = app + conf.name = name + conf.ffmpeg = this.config.relay.ffmpeg + conf.inPath = url + conf.ouPath = `rtmp://127.0.0.1:${this.config.rtmp.port}/${app}/${name}` + + let session = new NodeRelaySession(conf) + + const id = session.id + + context.sessions.set(id, session) + + session.on("end", (id) => { + this.dynamicSessions.delete(id) + }) + + this.dynamicSessions.set(id, session) + + session.run() + + Logger.log("[relay dynamic pull] start id=" + id, conf.inPath, "to", conf.ouPath) + + return id + } + + //从本地拉推到远端 + onRelayPush(url, app, name) { + let conf = {} + + conf.app = app + conf.name = name + conf.ffmpeg = this.config.relay.ffmpeg + conf.inPath = `rtmp://127.0.0.1:${this.config.rtmp.port}/${app}/${name}` + conf.ouPath = url + + let session = new NodeRelaySession(conf) + + const id = session.id + + context.sessions.set(id, session) + + session.on("end", (id) => { + this.dynamicSessions.delete(id) + }) + + this.dynamicSessions.set(id, session) + + session.run() + + Logger.log("[relay dynamic push] start id=" + id, conf.inPath, "to", conf.ouPath) + } + + onPrePlay(id, streamPath, args) { + if (!this.config.relay.tasks) { + return + } + + let regRes = /\/(.*)\/(.*)/gi.exec(streamPath) + let [app, stream] = _.slice(regRes, 1) + let i = this.config.relay.tasks.length + + while (i--) { + let conf = this.config.relay.tasks[i] + let isPull = conf.mode === "pull" + + if (isPull && app === conf.app && !context.publishers.has(streamPath)) { + let hasApp = conf.edge.match(/rtmp:\/\/([^\/]+)\/([^\/]+)/) + + conf.ffmpeg = this.config.relay.ffmpeg + conf.inPath = hasApp ? `${conf.edge}/${stream}` : `${conf.edge}${streamPath}` + conf.ouPath = `rtmp://127.0.0.1:${this.config.rtmp.port}${streamPath}` + + if (Object.keys(args).length > 0) { + conf.inPath += "?" + conf.inPath += querystring.encode(args) + } + + let session = new NodeRelaySession(conf) + + session.id = id + + session.on("end", (id) => { + this.dynamicSessions.delete(id) + }) + + this.dynamicSessions.set(id, session) + + session.run() + + Logger.log("[relay dynamic pull] start id=" + id, conf.inPath, "to", conf.ouPath) + } + } + } + + onDonePlay(id, streamPath, args) { + let session = this.dynamicSessions.get(id) + let publisher = context.sessions.get(context.publishers.get(streamPath)) + if (session && publisher.players.size == 0) { + session.end() + } + } + + onPostPublish(id, streamPath, args) { + if (!this.config.relay.tasks) { + return + } + + let regRes = /\/(.*)\/(.*)/gi.exec(streamPath) + let [app, stream] = _.slice(regRes, 1) + let i = this.config.relay.tasks.length + + while (i--) { + let conf = this.config.relay.tasks[i] + let isPush = conf.mode === "push" + + if (isPush && app === conf.app) { + let hasApp = conf.edge.match(/rtmp:\/\/([^\/]+)\/([^\/]+)/) + + conf.ffmpeg = this.config.relay.ffmpeg + conf.inPath = `rtmp://127.0.0.1:${this.config.rtmp.port}${streamPath}` + conf.ouPath = conf.appendName === false ? conf.edge : (hasApp ? `${conf.edge}/${stream}` : `${conf.edge}${streamPath}`) + + if (Object.keys(args).length > 0) { + conf.ouPath += "?" + conf.ouPath += querystring.encode(args) + } + + let session = new NodeRelaySession(conf) + + session.id = id + + session.on("end", (id) => { + this.dynamicSessions.delete(id) + }) + + this.dynamicSessions.set(id, session) + + session.run() + + Logger.log("[relay dynamic push] start id=" + id, conf.inPath, "to", conf.ouPath) + } + } + } + + onDonePublish(id, streamPath, args) { + let session = this.dynamicSessions.get(id) + + if (session) { + session.end() + } + + for (session of this.staticSessions.values()) { + if (session.streamPath === streamPath) { + session.end() + } + } + } + + stop() { + clearInterval(this.staticCycle) + } +} + +module.exports = NodeRelayServer diff --git a/packages/streaming-server/src/internal-nms/servers/rtmp_server.js b/packages/streaming-server/src/internal-nms/servers/rtmp_server.js new file mode 100644 index 00000000..2793fbf2 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/servers/rtmp_server.js @@ -0,0 +1,87 @@ +// +// Created by Mingliang Chen on 17/8/1. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// + +const Tls = require("tls") +const Fs = require("fs") +const Net = require("net") + +const NodeRtmpSession = require("../sessionsModels/rtmp_session") +const Logger = require("../lib/logger") + +const context = require("../ctx") + +const RTMP_PORT = 1935 +const RTMPS_PORT = 443 + +class NodeRtmpServer { + constructor(config) { + config.rtmp.port = this.port = config.rtmp.port ? config.rtmp.port : RTMP_PORT + + this.tcpServer = Net.createServer((socket) => { + let session = new NodeRtmpSession(config, socket) + session.run() + }) + + if (config.rtmp.ssl){ + config.rtmp.ssl.port = this.sslPort = config.rtmp.ssl.port ? config.rtmp.ssl.port : RTMPS_PORT + try { + const options = { + key: Fs.readFileSync(config.rtmp.ssl.key), + cert: Fs.readFileSync(config.rtmp.ssl.cert) + } + this.tlsServer = Tls.createServer(options, (socket) => { + let session = new NodeRtmpSession(config, socket) + session.run() + }) + } catch (e) { + Logger.error(`Node Media Rtmps Server error while reading ssl certs: <${e}>`) + } + } + } + + run() { + this.tcpServer.listen(this.port, () => { + Logger.log(`Node Media Rtmp Server started on port: ${this.port}`) + }) + + this.tcpServer.on("error", (e) => { + Logger.error(`Node Media Rtmp Server ${e}`) + }) + + this.tcpServer.on("close", () => { + Logger.log("Node Media Rtmp Server Close.") + }) + + if (this.tlsServer) { + this.tlsServer.listen(this.sslPort, () => { + Logger.log(`Node Media Rtmps Server started on port: ${this.sslPort}`) + }) + + this.tlsServer.on("error", (e) => { + Logger.error(`Node Media Rtmps Server ${e}`) + }) + + this.tlsServer.on("close", () => { + Logger.log("Node Media Rtmps Server Close.") + }) + } + } + + stop() { + this.tcpServer.close() + + if (this.tlsServer) { + this.tlsServer.close() + } + + context.sessions.forEach((session, id) => { + if (session instanceof NodeRtmpSession) + session.stop() + }) + } +} + +module.exports = NodeRtmpServer diff --git a/packages/streaming-server/src/internal-nms/servers/trans_server.js b/packages/streaming-server/src/internal-nms/servers/trans_server.js new file mode 100644 index 00000000..54bc0224 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/servers/trans_server.js @@ -0,0 +1,105 @@ +const fs = require("fs") +const lodash = require("lodash") +const mkdirp = require("mkdirp") + +const Logger = require("../lib/logger") +const TransSession = require("../sessionsModels/trans_session") + +const { getFFmpegVersion, getFFmpegUrl } = require("../lib/utils") +const context = require("../ctx") + +class NodeTransServer { + constructor(config) { + this.config = config + this.transSessions = new Map() + } + + async run() { + try { + mkdirp.sync(this.config.mediaroot) + fs.accessSync(this.config.mediaroot, fs.constants.W_OK) + } catch (error) { + Logger.error(`Node Media Trans Server startup failed. MediaRoot:${this.config.mediaroot} cannot be written.`) + return + } + + try { + fs.accessSync(this.config.trans.ffmpeg, fs.constants.X_OK) + } catch (error) { + Logger.error(`Node Media Trans Server startup failed. ffmpeg:${this.config.trans.ffmpeg} cannot be executed.`) + return + } + + let version = await getFFmpegVersion(this.config.trans.ffmpeg) + + if (version === "" || parseInt(version.split(".")[0]) < 4) { + Logger.error("Node Media Trans Server startup failed. ffmpeg requires version 4.0.0 above") + Logger.error("Download the latest ffmpeg static program:", getFFmpegUrl()) + + return + } + + let i = this.config.trans.tasks.length + let apps = "" + + while (i--) { + apps += this.config.trans.tasks[i].app + apps += " " + } + + context.nodeEvent.on("postPublish", this.onPostPublish.bind(this)) + context.nodeEvent.on("donePublish", this.onDonePublish.bind(this)) + + Logger.log(`Node Media Trans Server started for apps: [ ${apps}] , MediaRoot: ${this.config.mediaroot}, ffmpeg version: ${version}`) + } + + async onPostPublish(id, streamPath, args) { + const fixedStreamingKey = streamPath.split("/").pop() + const userspace = await global.resolveUserspaceOfStreamingKey(fixedStreamingKey) + + if (!userspace) { + console.error("No userspace found for streaming key:", fixedStreamingKey) + return false + } + + let regRes = /\/(.*)\/(.*)/gi.exec(streamPath) + let [app, name] = lodash.slice(regRes, 1) + + let i = this.config.trans.tasks.length + + while (i--) { + let conf = { ...this.config.trans.tasks[i] } + + conf.ffmpeg = this.config.trans.ffmpeg + conf.mediaroot = this.config.mediaroot + conf.rtmpPort = this.config.rtmp.port + conf.streamPath = streamPath + conf.streamApp = app + conf.streamName = name + conf.fixedStreamName = userspace.username + conf.args = args + + if (app === conf.app) { + let session = new TransSession(conf) + + this.transSessions.set(id, session) + + session.on("end", () => { + this.transSessions.delete(id) + }) + + session.run() + } + } + } + + onDonePublish(id, streamPath, args) { + let session = this.transSessions.get(id) + + if (session) { + session.end() + } + } +} + +module.exports = NodeTransServer diff --git a/packages/streaming-server/src/internal-nms/sessionsModels/fission_session.js b/packages/streaming-server/src/internal-nms/sessionsModels/fission_session.js new file mode 100644 index 00000000..2e17720c --- /dev/null +++ b/packages/streaming-server/src/internal-nms/sessionsModels/fission_session.js @@ -0,0 +1,51 @@ +const EventEmitter = require("events") +const { spawn } = require("child_process") + +const Logger = require("../lib/logger") + +class NodeFissionSession extends EventEmitter { + constructor(conf) { + super() + this.conf = conf + } + + run() { + let inPath = "rtmp://127.0.0.1:" + this.conf.rtmpPort + this.conf.streamPath + let argv = ["-i", inPath] + + for (let m of this.conf.model) { + let x264 = ["-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency", "-maxrate", m.vb, "-bufsize", m.vb, "-g", parseInt(m.vf) * 2, "-r", m.vf, "-s", m.vs] + let aac = ["-c:a", "aac", "-b:a", m.ab] + let outPath = ["-f", "flv", "rtmp://127.0.0.1:" + this.conf.rtmpPort + "/" + this.conf.streamApp + "/" + this.conf.fixedStreamName + "_" + m.vs.split("x")[1]] + argv.splice(argv.length, 0, ...x264) + argv.splice(argv.length, 0, ...aac) + argv.splice(argv.length, 0, ...outPath) + } + + argv = argv.filter((n) => { return n }) + + this.ffmpeg_exec = spawn(this.conf.ffmpeg, argv) + this.ffmpeg_exec.on("error", (e) => { + Logger.ffdebug(e) + }) + + this.ffmpeg_exec.stdout.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.stderr.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.on("close", (code) => { + Logger.log("[Fission end] " + this.conf.streamPath) + this.emit("end") + }) + } + + end() { + this.ffmpeg_exec.kill() + } +} + +module.exports = NodeFissionSession \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/sessionsModels/flv_session.js b/packages/streaming-server/src/internal-nms/sessionsModels/flv_session.js new file mode 100644 index 00000000..aab86e8a --- /dev/null +++ b/packages/streaming-server/src/internal-nms/sessionsModels/flv_session.js @@ -0,0 +1,218 @@ +const URL = require("url") + +const Logger = require("../lib/logger") +const Utils = require("../lib/utils") +const context = require("../ctx") + +const FlvPacket = { + create: (payload = null, type = 0, time = 0) => { + return { + header: { + length: payload ? payload.length : 0, + timestamp: time, + type: type + }, + payload: payload + } + } +} + +class FlvSession { + constructor(req, res) { + this.req = req + this.res = res + + this.id = Utils.generateNewSessionID() + this.ip = this.req.socket.remoteAddress + + this.playStreamPath = "" + this.playArgs = null + + this.isStarting = false + this.isPlaying = false + this.isIdling = false + + if (this.req.nmsConnectionType === "ws") { + this.res.cork = this.res._socket.cork.bind(this.res._socket) + this.res.uncork = this.res._socket.uncork.bind(this.res._socket) + this.res.on("close", this.onReqClose.bind(this)) + this.res.on("error", this.onReqError.bind(this)) + this.res.write = this.res.send + this.res.end = this.res.close + this.TAG = "websocket-flv" + } else { + this.res.cork = this.res.socket.cork.bind(this.res.socket) + this.res.uncork = this.res.socket.uncork.bind(this.res.socket) + + this.req.socket.on("close", this.onReqClose.bind(this)) + this.req.on("error", this.onReqError.bind(this)) + + this.TAG = "http-flv" + } + + this.numPlayCache = 0 + context.sessions.set(this.id, this) + } + + run() { + let method = this.req.method + + let urlInfo = URL.parse(this.req.url, true) + let streamPath = urlInfo.pathname.split(".")[0] + + this.connectCmdObj = { ip: this.ip, method, streamPath, query: urlInfo.query } + this.connectTime = new Date() + this.isStarting = true + + Logger.log(`[${this.TAG} connect] id=${this.id} ip=${this.ip} args=${JSON.stringify(urlInfo.query)}`) + + context.nodeEvent.emit("preConnect", this.id, this.connectCmdObj) + + if (!this.isStarting) { + this.stop() + return + } + + context.nodeEvent.emit("postConnect", this.id, this.connectCmdObj) + + if (method === "GET") { + this.playStreamPath = streamPath + this.playArgs = urlInfo.query + + this.onPlay() + } else { + this.stop() + } + } + + stop() { + if (this.isStarting) { + this.isStarting = false + + let publisherId = context.publishers.get(this.playStreamPath) + + if (publisherId != null) { + context.sessions.get(publisherId).players.delete(this.id) + context.nodeEvent.emit("donePlay", this.id, this.playStreamPath, this.playArgs) + } + + Logger.log(`[${this.TAG} play] Close stream. id=${this.id} streamPath=${this.playStreamPath}`) + Logger.log(`[${this.TAG} disconnect] id=${this.id}`) + + context.nodeEvent.emit("doneConnect", this.id, this.connectCmdObj) + + this.res.end() + + context.idlePlayers.delete(this.id) + context.sessions.delete(this.id) + } + } + + onReqClose() { + this.stop() + } + + onReqError(e) { + this.stop() + } + + reject() { + Logger.log(`[${this.TAG} reject] id=${this.id}`) + this.stop() + } + + onPlay() { + context.nodeEvent.emit("prePlay", this.id, this.playStreamPath, this.playArgs) + + if (!this.isStarting) { + return + } + + if (!context.publishers.has(this.playStreamPath)) { + Logger.log(`[${this.TAG} play] Stream not found. id=${this.id} streamPath=${this.playStreamPath} `) + + context.idlePlayers.add(this.id) + + this.isIdling = true + + return + } + + this.onStartPlay() + } + + onStartPlay() { + let publisherId = context.publishers.get(this.playStreamPath) + let publisher = context.sessions.get(publisherId) + let players = publisher.players + + players.add(this.id) + + //send FLV header + let FLVHeader = Buffer.from([0x46, 0x4c, 0x56, 0x01, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00]) + + if (publisher.isFirstAudioReceived) { + FLVHeader[4] |= 0b00000100 + } + + if (publisher.isFirstVideoReceived) { + FLVHeader[4] |= 0b00000001 + } + + this.res.write(FLVHeader) + + //send Metadata + if (publisher.metaData != null) { + let packet = FlvPacket.create(publisher.metaData, 18) + let tag = FlvSession.createFlvTag(packet) + this.res.write(tag) + } + + //send aacSequenceHeader + if (publisher.audioCodec == 10) { + let packet = FlvPacket.create(publisher.aacSequenceHeader, 8) + let tag = FlvSession.createFlvTag(packet) + this.res.write(tag) + } + + //send avcSequenceHeader + if (publisher.videoCodec == 7 || publisher.videoCodec == 12) { + let packet = FlvPacket.create(publisher.avcSequenceHeader, 9) + let tag = FlvSession.createFlvTag(packet) + this.res.write(tag) + } + + //send gop cache + if (publisher.flvGopCacheQueue != null) { + for (let tag of publisher.flvGopCacheQueue) { + this.res.write(tag) + } + } + + this.isIdling = false + this.isPlaying = true + + Logger.log(`[${this.TAG} play] Join stream. id=${this.id} streamPath=${this.playStreamPath} `) + context.nodeEvent.emit("postPlay", this.id, this.playStreamPath, this.playArgs) + } + + static createFlvTag(packet) { + let PreviousTagSize = 11 + packet.header.length + let tagBuffer = Buffer.alloc(PreviousTagSize + 4) + + tagBuffer[0] = packet.header.type + tagBuffer.writeUIntBE(packet.header.length, 1, 3) + tagBuffer[4] = (packet.header.timestamp >> 16) & 0xff + tagBuffer[5] = (packet.header.timestamp >> 8) & 0xff + tagBuffer[6] = packet.header.timestamp & 0xff + tagBuffer[7] = (packet.header.timestamp >> 24) & 0xff + + tagBuffer.writeUIntBE(0, 8, 3) + tagBuffer.writeUInt32BE(PreviousTagSize, PreviousTagSize) + packet.payload.copy(tagBuffer, 11, 0, packet.header.length) + + return tagBuffer + } +} + +module.exports = FlvSession \ No newline at end of file diff --git a/packages/streaming-server/src/internal-nms/sessionsModels/relay_session.js b/packages/streaming-server/src/internal-nms/sessionsModels/relay_session.js new file mode 100644 index 00000000..2334cf05 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/sessionsModels/relay_session.js @@ -0,0 +1,60 @@ + +const EventEmitter = require("events") +const { spawn } = require("child_process") + +const Logger = require("../lib/logger") +const NodeCoreUtils = require("../lib/utils") + +const RTSP_TRANSPORT = ["udp", "tcp", "udp_multicast", "http"] + +class NodeRelaySession extends EventEmitter { + constructor(conf) { + super() + this.conf = conf + this.id = NodeCoreUtils.generateNewSessionID() + this.TAG = "relay" + } + + run() { + let format = this.conf.ouPath.startsWith("rtsp://") ? "rtsp" : "flv" + let argv = ["-re", "-i", this.conf.inPath, "-c", "copy", "-f", format, this.conf.ouPath] + + if (this.conf.inPath[0] === "/" || this.conf.inPath[1] === ":") { + argv.unshift("-1") + argv.unshift("-stream_loop") + } + + if (this.conf.inPath.startsWith("rtsp://") && this.conf.rtsp_transport) { + if (RTSP_TRANSPORT.indexOf(this.conf.rtsp_transport) > -1) { + argv.unshift(this.conf.rtsp_transport) + argv.unshift("-rtsp_transport") + } + } + + Logger.log("[relay task] id=" + this.id, "cmd=ffmpeg", argv.join(" ")) + + this.ffmpeg_exec = spawn(this.conf.ffmpeg, argv) + this.ffmpeg_exec.on("error", (e) => { + Logger.ffdebug(e) + }) + + this.ffmpeg_exec.stdout.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.stderr.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.on("close", (code) => { + Logger.log("[relay end] id=" + this.id, "code=" + code) + this.emit("end", this.id) + }) + } + + end() { + this.ffmpeg_exec.kill() + } +} + +module.exports = NodeRelaySession diff --git a/packages/streaming-server/src/internal-nms/sessionsModels/rtmp_session.js b/packages/streaming-server/src/internal-nms/sessionsModels/rtmp_session.js new file mode 100644 index 00000000..8aa244c9 --- /dev/null +++ b/packages/streaming-server/src/internal-nms/sessionsModels/rtmp_session.js @@ -0,0 +1,1306 @@ +// +// Created by Mingliang Chen on 18/4/1. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// + +const QueryString = require("querystring") + +const Logger = require("../lib/logger") +const NodeCoreUtils = require("../lib/utils") +const context = require("../ctx") + +const AV = require("../lib/av") +const { AUDIO_SOUND_RATE, AUDIO_CODEC_NAME, VIDEO_CODEC_NAME } = require("../lib/av") +const AMF = require("../lib/amf_rules") +const Handshake = require("../rtmp_handshake") +const NodeFlvSession = require("./flv_session") + +const N_CHUNK_STREAM = 8 +const RTMP_VERSION = 3 +const RTMP_HANDSHAKE_SIZE = 1536 +const RTMP_HANDSHAKE_UNINIT = 0 +const RTMP_HANDSHAKE_0 = 1 +const RTMP_HANDSHAKE_1 = 2 +const RTMP_HANDSHAKE_2 = 3 + +const RTMP_PARSE_INIT = 0 +const RTMP_PARSE_BASIC_HEADER = 1 +const RTMP_PARSE_MESSAGE_HEADER = 2 +const RTMP_PARSE_EXTENDED_TIMESTAMP = 3 +const RTMP_PARSE_PAYLOAD = 4 + +const MAX_CHUNK_HEADER = 18 + +const RTMP_CHUNK_TYPE_0 = 0 // 11-bytes: timestamp(3) + length(3) + stream type(1) + stream id(4) +const RTMP_CHUNK_TYPE_1 = 1 // 7-bytes: delta(3) + length(3) + stream type(1) +const RTMP_CHUNK_TYPE_2 = 2 // 3-bytes: delta(3) +const RTMP_CHUNK_TYPE_3 = 3 // 0-byte + +const RTMP_CHANNEL_PROTOCOL = 2 +const RTMP_CHANNEL_INVOKE = 3 +const RTMP_CHANNEL_AUDIO = 4 +const RTMP_CHANNEL_VIDEO = 5 +const RTMP_CHANNEL_DATA = 6 + +const rtmpHeaderSize = [11, 7, 3, 0] + +/* Protocol Control Messages */ +const RTMP_TYPE_SET_CHUNK_SIZE = 1 +const RTMP_TYPE_ABORT = 2 +const RTMP_TYPE_ACKNOWLEDGEMENT = 3 // bytes read report +const RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE = 5 // server bandwidth +const RTMP_TYPE_SET_PEER_BANDWIDTH = 6 // client bandwidth + +/* User Control Messages Event (4) */ +const RTMP_TYPE_EVENT = 4 + +const RTMP_TYPE_AUDIO = 8 +const RTMP_TYPE_VIDEO = 9 + +/* Data Message */ +const RTMP_TYPE_FLEX_STREAM = 15 // AMF3 +const RTMP_TYPE_DATA = 18 // AMF0 + +/* Shared Object Message */ +const RTMP_TYPE_FLEX_OBJECT = 16 // AMF3 +const RTMP_TYPE_SHARED_OBJECT = 19 // AMF0 + +/* Command Message */ +const RTMP_TYPE_FLEX_MESSAGE = 17 // AMF3 +const RTMP_TYPE_INVOKE = 20 // AMF0 + +/* Aggregate Message */ +const RTMP_TYPE_METADATA = 22 + +const RTMP_CHUNK_SIZE = 128 +const RTMP_PING_TIME = 60000 +const RTMP_PING_TIMEOUT = 30000 + +const STREAM_BEGIN = 0x00 +const STREAM_EOF = 0x01 +const STREAM_DRY = 0x02 +const STREAM_EMPTY = 0x1f +const STREAM_READY = 0x20 + +const RtmpPacket = { + create: (fmt = 0, cid = 0) => { + return { + header: { + fmt: fmt, + cid: cid, + timestamp: 0, + length: 0, + type: 0, + stream_id: 0 + }, + clock: 0, + payload: null, + capacity: 0, + bytes: 0 + } + } +} + +class NodeRtmpSession { + constructor(config, socket) { + this.config = config + this.socket = socket + this.res = socket + this.id = NodeCoreUtils.generateNewSessionID() + this.ip = socket.remoteAddress + this.TAG = "rtmp" + + this.handshakePayload = Buffer.alloc(RTMP_HANDSHAKE_SIZE) + this.handshakeState = RTMP_HANDSHAKE_UNINIT + this.handshakeBytes = 0 + + this.parserBuffer = Buffer.alloc(MAX_CHUNK_HEADER) + this.parserState = RTMP_PARSE_INIT + this.parserBytes = 0 + this.parserBasicBytes = 0 + this.parserPacket = null + this.inPackets = new Map() + + this.inChunkSize = RTMP_CHUNK_SIZE + this.outChunkSize = config.rtmp.chunk_size ? config.rtmp.chunk_size : RTMP_CHUNK_SIZE + this.pingTime = config.rtmp.ping ? config.rtmp.ping * 1000 : RTMP_PING_TIME + this.pingTimeout = config.rtmp.ping_timeout ? config.rtmp.ping_timeout * 1000 : RTMP_PING_TIMEOUT + this.pingInterval = null + + this.isLocal = this.ip === "127.0.0.1" || this.ip === "::1" || this.ip == "::ffff:127.0.0.1" + this.isStarting = false + this.isPublishing = false + this.isPlaying = false + this.isIdling = false + this.isPause = false + this.isReceiveAudio = true + this.isReceiveVideo = true + this.metaData = null + this.aacSequenceHeader = null + this.avcSequenceHeader = null + this.audioCodec = 0 + this.audioCodecName = "" + this.audioProfileName = "" + this.audioSamplerate = 0 + this.audioChannels = 1 + this.videoCodec = 0 + this.videoCodecName = "" + this.videoProfileName = "" + this.videoWidth = 0 + this.videoHeight = 0 + this.videoFps = 0 + this.videoCount = 0 + this.videoLevel = 0 + this.bitrate = 0 + + this.gopCacheEnable = config.rtmp.gop_cache + this.rtmpGopCacheQueue = null + this.flvGopCacheQueue = null + + this.ackSize = 0 + this.inAckSize = 0 + this.inLastAck = 0 + + this.appname = "" + this.streams = 0 + + this.playStreamId = 0 + this.playStreamPath = "" + this.playArgs = {} + + this.publishStreamId = 0 + this.publishStreamPath = "" + this.publishArgs = {} + + this.players = new Set() + this.numPlayCache = 0 + this.bitrateCache = {} + context.sessions.set(this.id, this) + } + + run() { + this.socket.on("data", this.onSocketData.bind(this)) + this.socket.on("close", this.onSocketClose.bind(this)) + this.socket.on("error", this.onSocketError.bind(this)) + this.socket.on("timeout", this.onSocketTimeout.bind(this)) + this.socket.setTimeout(this.pingTimeout) + this.isStarting = true + } + + stop() { + if (this.isStarting) { + this.isStarting = false + + if (this.playStreamId > 0) { + this.onDeleteStream({ streamId: this.playStreamId }) + } + + if (this.publishStreamId > 0) { + this.onDeleteStream({ streamId: this.publishStreamId }) + } + + if (this.pingInterval != null) { + clearInterval(this.pingInterval) + this.pingInterval = null + } + + Logger.log(`[rtmp disconnect] id=${this.id}`) + context.nodeEvent.emit("doneConnect", this.id, this.connectCmdObj) + + context.sessions.delete(this.id) + this.socket.destroy() + } + } + + reject() { + Logger.log(`[rtmp reject] id=${this.id}`) + this.stop() + } + + flush() { + if (this.numPlayCache > 0) { + this.res.uncork() + } + } + + onSocketClose() { + // Logger.log("onSocketClose") + this.stop() + } + + onSocketError(e) { + // Logger.log("onSocketError", e) + this.stop() + } + + onSocketTimeout() { + // Logger.log("onSocketTimeout") + this.stop() + } + + /** + * onSocketData + * @param {Buffer} data + * @returns + */ + onSocketData(data) { + let bytes = data.length + let p = 0 + let n = 0 + while (bytes > 0) { + switch (this.handshakeState) { + case RTMP_HANDSHAKE_UNINIT: + // Logger.log("RTMP_HANDSHAKE_UNINIT") + this.handshakeState = RTMP_HANDSHAKE_0 + this.handshakeBytes = 0 + bytes -= 1 + p += 1 + break + case RTMP_HANDSHAKE_0: + // Logger.log("RTMP_HANDSHAKE_0") + n = RTMP_HANDSHAKE_SIZE - this.handshakeBytes + n = n <= bytes ? n : bytes + data.copy(this.handshakePayload, this.handshakeBytes, p, p + n) + this.handshakeBytes += n + bytes -= n + p += n + if (this.handshakeBytes === RTMP_HANDSHAKE_SIZE) { + this.handshakeState = RTMP_HANDSHAKE_1 + this.handshakeBytes = 0 + let s0s1s2 = Handshake.generateS0S1S2(this.handshakePayload) + this.socket.write(s0s1s2) + } + break + case RTMP_HANDSHAKE_1: + // Logger.log("RTMP_HANDSHAKE_1") + n = RTMP_HANDSHAKE_SIZE - this.handshakeBytes + n = n <= bytes ? n : bytes + data.copy(this.handshakePayload, this.handshakeBytes, p, n) + this.handshakeBytes += n + bytes -= n + p += n + if (this.handshakeBytes === RTMP_HANDSHAKE_SIZE) { + this.handshakeState = RTMP_HANDSHAKE_2 + this.handshakeBytes = 0 + this.handshakePayload = null + } + break + case RTMP_HANDSHAKE_2: + default: + // Logger.log("RTMP_HANDSHAKE_2") + return this.rtmpChunkRead(data, p, bytes) + } + } + } + + rtmpChunkBasicHeaderCreate(fmt, cid) { + let out + if (cid >= 64 + 255) { + out = Buffer.alloc(3) + out[0] = (fmt << 6) | 1 + out[1] = (cid - 64) & 0xff + out[2] = ((cid - 64) >> 8) & 0xff + } else if (cid >= 64) { + out = Buffer.alloc(2) + out[0] = (fmt << 6) | 0 + out[1] = (cid - 64) & 0xff + } else { + out = Buffer.alloc(1) + out[0] = (fmt << 6) | cid + } + return out + } + + rtmpChunkMessageHeaderCreate(header) { + let out = Buffer.alloc(rtmpHeaderSize[header.fmt % 4]) + if (header.fmt <= RTMP_CHUNK_TYPE_2) { + out.writeUIntBE(header.timestamp >= 0xffffff ? 0xffffff : header.timestamp, 0, 3) + } + + if (header.fmt <= RTMP_CHUNK_TYPE_1) { + out.writeUIntBE(header.length, 3, 3) + out.writeUInt8(header.type, 6) + } + + if (header.fmt === RTMP_CHUNK_TYPE_0) { + out.writeUInt32LE(header.stream_id, 7) + } + return out + } + + /** + * rtmpChunksCreate + * @param {RtmpPacket} packet + * @returns + */ + rtmpChunksCreate(packet) { + let header = packet.header + let payload = packet.payload + let payloadSize = header.length + let chunkSize = this.outChunkSize + let chunksOffset = 0 + let payloadOffset = 0 + let chunkBasicHeader = this.rtmpChunkBasicHeaderCreate(header.fmt, header.cid) + let chunkBasicHeader3 = this.rtmpChunkBasicHeaderCreate(RTMP_CHUNK_TYPE_3, header.cid) + let chunkMessageHeader = this.rtmpChunkMessageHeaderCreate(header) + let useExtendedTimestamp = header.timestamp >= 0xffffff + let headerSize = chunkBasicHeader.length + chunkMessageHeader.length + (useExtendedTimestamp ? 4 : 0) + let n = headerSize + payloadSize + Math.floor(payloadSize / chunkSize) + + if (useExtendedTimestamp) { + n += Math.floor(payloadSize / chunkSize) * 4 + } + if (!(payloadSize % chunkSize)) { + n -= 1 + if (useExtendedTimestamp) { + //TODO CHECK + n -= 4 + } + } + + let chunks = Buffer.alloc(n) + chunkBasicHeader.copy(chunks, chunksOffset) + chunksOffset += chunkBasicHeader.length + chunkMessageHeader.copy(chunks, chunksOffset) + chunksOffset += chunkMessageHeader.length + if (useExtendedTimestamp) { + chunks.writeUInt32BE(header.timestamp, chunksOffset) + chunksOffset += 4 + } + while (payloadSize > 0) { + if (payloadSize > chunkSize) { + payload.copy(chunks, chunksOffset, payloadOffset, payloadOffset + chunkSize) + payloadSize -= chunkSize + chunksOffset += chunkSize + payloadOffset += chunkSize + chunkBasicHeader3.copy(chunks, chunksOffset) + chunksOffset += chunkBasicHeader3.length + if (useExtendedTimestamp) { + chunks.writeUInt32BE(header.timestamp, chunksOffset) + chunksOffset += 4 + } + } else { + payload.copy(chunks, chunksOffset, payloadOffset, payloadOffset + payloadSize) + payloadSize -= payloadSize + chunksOffset += payloadSize + payloadOffset += payloadSize + } + } + return chunks + } + + /** + * rtmpChunkRead + * @param {Buffer} data + * @param {Number} p + * @param {Number} bytes + */ + rtmpChunkRead(data, p, bytes) { + // Logger.log("rtmpChunkRead", p, bytes) + let size = 0 + let offset = 0 + let extended_timestamp = 0 + + while (offset < bytes) { + switch (this.parserState) { + case RTMP_PARSE_INIT: + this.parserBytes = 1 + this.parserBuffer[0] = data[p + offset++] + if (0 === (this.parserBuffer[0] & 0x3f)) { + this.parserBasicBytes = 2 + } else if (1 === (this.parserBuffer[0] & 0x3f)) { + this.parserBasicBytes = 3 + } else { + this.parserBasicBytes = 1 + } + this.parserState = RTMP_PARSE_BASIC_HEADER + break + case RTMP_PARSE_BASIC_HEADER: + while (this.parserBytes < this.parserBasicBytes && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= this.parserBasicBytes) { + this.parserState = RTMP_PARSE_MESSAGE_HEADER + } + break + case RTMP_PARSE_MESSAGE_HEADER: + size = rtmpHeaderSize[this.parserBuffer[0] >> 6] + this.parserBasicBytes + while (this.parserBytes < size && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= size) { + this.rtmpPacketParse() + this.parserState = RTMP_PARSE_EXTENDED_TIMESTAMP + } + break + case RTMP_PARSE_EXTENDED_TIMESTAMP: + size = rtmpHeaderSize[this.parserPacket.header.fmt] + this.parserBasicBytes + if (this.parserPacket.header.timestamp === 0xffffff) size += 4 + while (this.parserBytes < size && offset < bytes) { + this.parserBuffer[this.parserBytes++] = data[p + offset++] + } + if (this.parserBytes >= size) { + if (this.parserPacket.header.timestamp === 0xffffff) { + extended_timestamp = this.parserBuffer.readUInt32BE(rtmpHeaderSize[this.parserPacket.header.fmt] + this.parserBasicBytes) + } else { + extended_timestamp = this.parserPacket.header.timestamp + } + + if (this.parserPacket.bytes === 0) { + if (RTMP_CHUNK_TYPE_0 === this.parserPacket.header.fmt) { + this.parserPacket.clock = extended_timestamp + } else { + this.parserPacket.clock += extended_timestamp + } + this.rtmpPacketAlloc() + } + this.parserState = RTMP_PARSE_PAYLOAD + } + break + case RTMP_PARSE_PAYLOAD: + size = Math.min(this.inChunkSize - (this.parserPacket.bytes % this.inChunkSize), this.parserPacket.header.length - this.parserPacket.bytes) + size = Math.min(size, bytes - offset) + if (size > 0) { + data.copy(this.parserPacket.payload, this.parserPacket.bytes, p + offset, p + offset + size) + } + this.parserPacket.bytes += size + offset += size + + if (this.parserPacket.bytes >= this.parserPacket.header.length) { + this.parserState = RTMP_PARSE_INIT + this.parserPacket.bytes = 0 + if (this.parserPacket.clock > 0xffffffff) { + break + } + this.rtmpHandler() + } else if (0 === this.parserPacket.bytes % this.inChunkSize) { + this.parserState = RTMP_PARSE_INIT + } + break + } + } + + this.inAckSize += data.length + if (this.inAckSize >= 0xf0000000) { + this.inAckSize = 0 + this.inLastAck = 0 + } + if (this.ackSize > 0 && this.inAckSize - this.inLastAck >= this.ackSize) { + this.inLastAck = this.inAckSize + this.sendACK(this.inAckSize) + } + + this.bitrateCache.bytes += bytes + let current_time = Date.now() + let diff = current_time - this.bitrateCache.last_update + if (diff >= this.bitrateCache.intervalMs) { + this.bitrate = Math.round(this.bitrateCache.bytes * 8 / diff) + this.bitrateCache.bytes = 0 + this.bitrateCache.last_update = current_time + } + } + + rtmpPacketParse() { + let fmt = this.parserBuffer[0] >> 6 + let cid = 0 + if (this.parserBasicBytes === 2) { + cid = 64 + this.parserBuffer[1] + } else if (this.parserBasicBytes === 3) { + cid = (64 + this.parserBuffer[1] + this.parserBuffer[2]) << 8 + } else { + cid = this.parserBuffer[0] & 0x3f + } + let hasp = this.inPackets.has(cid) + if (!hasp) { + this.parserPacket = RtmpPacket.create(fmt, cid) + this.inPackets.set(cid, this.parserPacket) + } else { + this.parserPacket = this.inPackets.get(cid) + } + this.parserPacket.header.fmt = fmt + this.parserPacket.header.cid = cid + this.rtmpChunkMessageHeaderRead() + + if (this.parserPacket.header.type > RTMP_TYPE_METADATA) { + Logger.error("rtmp packet parse error.", this.parserPacket) + this.stop() + } + } + + rtmpChunkMessageHeaderRead() { + let offset = this.parserBasicBytes + + // timestamp / delta + if (this.parserPacket.header.fmt <= RTMP_CHUNK_TYPE_2) { + this.parserPacket.header.timestamp = this.parserBuffer.readUIntBE(offset, 3) + offset += 3 + } + + // message length + type + if (this.parserPacket.header.fmt <= RTMP_CHUNK_TYPE_1) { + this.parserPacket.header.length = this.parserBuffer.readUIntBE(offset, 3) + this.parserPacket.header.type = this.parserBuffer[offset + 3] + offset += 4 + } + + if (this.parserPacket.header.fmt === RTMP_CHUNK_TYPE_0) { + this.parserPacket.header.stream_id = this.parserBuffer.readUInt32LE(offset) + offset += 4 + } + return offset + } + + rtmpPacketAlloc() { + if (this.parserPacket.capacity < this.parserPacket.header.length) { + this.parserPacket.payload = Buffer.alloc(this.parserPacket.header.length + 1024) + this.parserPacket.capacity = this.parserPacket.header.length + 1024 + } + } + + rtmpHandler() { + switch (this.parserPacket.header.type) { + case RTMP_TYPE_SET_CHUNK_SIZE: + case RTMP_TYPE_ABORT: + case RTMP_TYPE_ACKNOWLEDGEMENT: + case RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: + case RTMP_TYPE_SET_PEER_BANDWIDTH: + return 0 === this.rtmpControlHandler() ? -1 : 0 + case RTMP_TYPE_EVENT: + return 0 === this.rtmpEventHandler() ? -1 : 0 + case RTMP_TYPE_AUDIO: + return this.rtmpAudioHandler() + case RTMP_TYPE_VIDEO: + return this.rtmpVideoHandler() + case RTMP_TYPE_FLEX_MESSAGE: + case RTMP_TYPE_INVOKE: + return this.rtmpInvokeHandler() + case RTMP_TYPE_FLEX_STREAM: // AMF3 + case RTMP_TYPE_DATA: // AMF0 + return this.rtmpDataHandler() + } + } + + rtmpControlHandler() { + let payload = this.parserPacket.payload + switch (this.parserPacket.header.type) { + case RTMP_TYPE_SET_CHUNK_SIZE: + this.inChunkSize = payload.readUInt32BE() + // Logger.debug("set inChunkSize", this.inChunkSize) + break + case RTMP_TYPE_ABORT: + break + case RTMP_TYPE_ACKNOWLEDGEMENT: + break + case RTMP_TYPE_WINDOW_ACKNOWLEDGEMENT_SIZE: + this.ackSize = payload.readUInt32BE() + // Logger.debug("set ack Size", this.ackSize) + break + case RTMP_TYPE_SET_PEER_BANDWIDTH: + break + } + } + + rtmpEventHandler() { } + + rtmpAudioHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + let sound_format = (payload[0] >> 4) & 0x0f + let sound_type = payload[0] & 0x01 + let sound_size = (payload[0] >> 1) & 0x01 + let sound_rate = (payload[0] >> 2) & 0x03 + + if (this.audioCodec == 0) { + this.audioCodec = sound_format + this.audioCodecName = AUDIO_CODEC_NAME[sound_format] + this.audioSamplerate = AUDIO_SOUND_RATE[sound_rate] + this.audioChannels = ++sound_type + + if (sound_format == 4) { + this.audioSamplerate = 16000 + } else if (sound_format == 5) { + this.audioSamplerate = 8000 + } else if (sound_format == 11) { + this.audioSamplerate = 16000 + } else if (sound_format == 14) { + this.audioSamplerate = 8000 + } + + if (sound_format != 10 && sound_format != 13) { + Logger.log( + `[rtmp publish] Handle audio. id=${this.id} streamPath=${this.publishStreamPath + } sound_format=${sound_format} sound_type=${sound_type} sound_size=${sound_size} sound_rate=${sound_rate} codec_name=${this.audioCodecName} ${this.audioSamplerate} ${this.audioChannels + }ch` + ) + } + } + + if ((sound_format == 10 || sound_format == 13) && payload[1] == 0) { + //cache aac sequence header + this.isFirstAudioReceived = true + this.aacSequenceHeader = Buffer.alloc(payload.length) + payload.copy(this.aacSequenceHeader) + if (sound_format == 10) { + let info = AV.readAACSpecificConfig(this.aacSequenceHeader) + this.audioProfileName = AV.getAACProfileName(info) + this.audioSamplerate = info.sample_rate + this.audioChannels = info.channels + } else { + this.audioSamplerate = 48000 + this.audioChannels = payload[11] + } + + Logger.log( + `[rtmp publish] Handle audio. id=${this.id} streamPath=${this.publishStreamPath + } sound_format=${sound_format} sound_type=${sound_type} sound_size=${sound_size} sound_rate=${sound_rate} codec_name=${this.audioCodecName} ${this.audioSamplerate} ${this.audioChannels + }ch` + ) + } + + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_AUDIO + packet.header.type = RTMP_TYPE_AUDIO + packet.payload = payload + packet.header.length = packet.payload.length + packet.header.timestamp = this.parserPacket.clock + let rtmpChunks = this.rtmpChunksCreate(packet) + let flvTag = NodeFlvSession.createFlvTag(packet) + + //cache gop + if (this.rtmpGopCacheQueue != null) { + if (this.aacSequenceHeader != null && payload[1] === 0) { + //skip aac sequence header + } else { + this.rtmpGopCacheQueue.add(rtmpChunks) + this.flvGopCacheQueue.add(flvTag) + } + } + + for (let playerId of this.players) { + let playerSession = context.sessions.get(playerId) + + if (playerSession.numPlayCache === 0) { + playerSession.res.cork() + } + + if (playerSession instanceof NodeRtmpSession) { + if (playerSession.isStarting && playerSession.isPlaying && !playerSession.isPause && playerSession.isReceiveAudio) { + rtmpChunks.writeUInt32LE(playerSession.playStreamId, 8) + playerSession.res.write(rtmpChunks) + } + } else if (playerSession instanceof NodeFlvSession) { + playerSession.res.write(flvTag, null, e => { + //websocket will throw a error if not set the cb when closed + }) + } + + playerSession.numPlayCache++ + + if (playerSession.numPlayCache === 10) { + process.nextTick(() => playerSession.res.uncork()) + playerSession.numPlayCache = 0 + } + } + } + + rtmpVideoHandler() { + let payload = this.parserPacket.payload.slice(0, this.parserPacket.header.length) + let frame_type = (payload[0] >> 4) & 0x0f + let codec_id = payload[0] & 0x0f + + if (this.videoFps === 0) { + if (this.videoCount++ === 0) { + setTimeout(() => { + this.videoFps = Math.ceil(this.videoCount / 5) + }, 5000) + } + } + + if (codec_id == 7 || codec_id == 12) { + //cache avc sequence header + if (frame_type == 1 && payload[1] == 0) { + this.avcSequenceHeader = Buffer.alloc(payload.length) + payload.copy(this.avcSequenceHeader) + let info = AV.readAVCSpecificConfig(this.avcSequenceHeader) + this.videoWidth = info.width + this.videoHeight = info.height + this.videoProfileName = AV.getAVCProfileName(info) + this.videoLevel = info.level + this.rtmpGopCacheQueue = this.gopCacheEnable ? new Set() : null + this.flvGopCacheQueue = this.gopCacheEnable ? new Set() : null + //Logger.log(`[rtmp publish] avc sequence header`,this.avcSequenceHeader) + } + } + + if (this.videoCodec == 0) { + this.videoCodec = codec_id + this.videoCodecName = VIDEO_CODEC_NAME[codec_id] + Logger.log( + `[rtmp publish] Handle video. id=${this.id} streamPath=${this.publishStreamPath} frame_type=${frame_type} codec_id=${codec_id} codec_name=${this.videoCodecName} ${this.videoWidth + }x${this.videoHeight}` + ) + } + + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_VIDEO + packet.header.type = RTMP_TYPE_VIDEO + packet.payload = payload + packet.header.length = packet.payload.length + packet.header.timestamp = this.parserPacket.clock + let rtmpChunks = this.rtmpChunksCreate(packet) + let flvTag = NodeFlvSession.createFlvTag(packet) + + //cache gop + if ((codec_id == 7 || codec_id == 12) && this.rtmpGopCacheQueue != null) { + if (frame_type == 1 && payload[1] == 1) { + this.rtmpGopCacheQueue.clear() + this.flvGopCacheQueue.clear() + } + if (frame_type == 1 && payload[1] == 0) { + //skip avc sequence header + } else { + this.rtmpGopCacheQueue.add(rtmpChunks) + this.flvGopCacheQueue.add(flvTag) + } + } + + // Logger.log(rtmpChunks) + for (let playerId of this.players) { + let playerSession = context.sessions.get(playerId) + + if (playerSession.numPlayCache === 0) { + playerSession.res.cork() + } + + if (playerSession instanceof NodeRtmpSession) { + if (playerSession.isStarting && playerSession.isPlaying && !playerSession.isPause && playerSession.isReceiveVideo) { + rtmpChunks.writeUInt32LE(playerSession.playStreamId, 8) + playerSession.res.write(rtmpChunks) + } + } else if (playerSession instanceof NodeFlvSession) { + playerSession.res.write(flvTag, null, e => { + //websocket will throw a error if not set the cb when closed + }) + } + + playerSession.numPlayCache++ + + if (playerSession.numPlayCache === 10) { + process.nextTick(() => playerSession.res.uncork()) + playerSession.numPlayCache = 0 + } + } + } + + rtmpDataHandler() { + let offset = this.parserPacket.header.type === RTMP_TYPE_FLEX_STREAM ? 1 : 0 + let payload = this.parserPacket.payload.slice(offset, this.parserPacket.header.length) + let dataMessage = AMF.decodeAmf0Data(payload) + switch (dataMessage.cmd) { + case "@setDataFrame": + if (dataMessage.dataObj) { + this.audioSamplerate = dataMessage.dataObj.audiosamplerate + this.audioChannels = dataMessage.dataObj.stereo ? 2 : 1 + this.videoWidth = dataMessage.dataObj.width + this.videoHeight = dataMessage.dataObj.height + this.videoFps = dataMessage.dataObj.framerate + } + + let opt = { + cmd: "onMetaData", + dataObj: dataMessage.dataObj + } + this.metaData = AMF.encodeAmf0Data(opt) + + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_DATA + packet.header.type = RTMP_TYPE_DATA + packet.payload = this.metaData + packet.header.length = packet.payload.length + let rtmpChunks = this.rtmpChunksCreate(packet) + let flvTag = NodeFlvSession.createFlvTag(packet) + + for (let playerId of this.players) { + let playerSession = context.sessions.get(playerId) + if (playerSession instanceof NodeRtmpSession) { + if (playerSession.isStarting && playerSession.isPlaying && !playerSession.isPause) { + rtmpChunks.writeUInt32LE(playerSession.playStreamId, 8) + playerSession.socket.write(rtmpChunks) + } + } else if (playerSession instanceof NodeFlvSession) { + playerSession.res.write(flvTag, null, e => { + //websocket will throw a error if not set the cb when closed + }) + } + } + break + } + } + + rtmpInvokeHandler() { + let offset = this.parserPacket.header.type === RTMP_TYPE_FLEX_MESSAGE ? 1 : 0 + let payload = this.parserPacket.payload.slice(offset, this.parserPacket.header.length) + let invokeMessage = AMF.decodeAmf0Cmd(payload) + // Logger.log(invokeMessage) + switch (invokeMessage.cmd) { + case "connect": + this.onConnect(invokeMessage) + break + case "releaseStream": + break + case "FCPublish": + break + case "createStream": + this.onCreateStream(invokeMessage) + break + case "publish": + this.onPublish(invokeMessage) + break + case "play": + this.onPlay(invokeMessage) + break + case "pause": + this.onPause(invokeMessage) + break + case "FCUnpublish": + break + case "deleteStream": + this.onDeleteStream(invokeMessage) + break + case "closeStream": + this.onCloseStream() + break + case "receiveAudio": + this.onReceiveAudio(invokeMessage) + break + case "receiveVideo": + this.onReceiveVideo(invokeMessage) + break + } + } + + sendACK(size) { + let rtmpBuffer = Buffer.from("02000000000004030000000000000000", "hex") + rtmpBuffer.writeUInt32BE(size, 12) + this.socket.write(rtmpBuffer) + } + + sendWindowACK(size) { + let rtmpBuffer = Buffer.from("02000000000004050000000000000000", "hex") + rtmpBuffer.writeUInt32BE(size, 12) + this.socket.write(rtmpBuffer) + } + + setPeerBandwidth(size, type) { + let rtmpBuffer = Buffer.from("0200000000000506000000000000000000", "hex") + rtmpBuffer.writeUInt32BE(size, 12) + rtmpBuffer[16] = type + this.socket.write(rtmpBuffer) + } + + setChunkSize(size) { + let rtmpBuffer = Buffer.from("02000000000004010000000000000000", "hex") + rtmpBuffer.writeUInt32BE(size, 12) + this.socket.write(rtmpBuffer) + } + + sendStreamStatus(st, id) { + let rtmpBuffer = Buffer.from("020000000000060400000000000000000000", "hex") + rtmpBuffer.writeUInt16BE(st, 12) + rtmpBuffer.writeUInt32BE(id, 14) + this.socket.write(rtmpBuffer) + } + + sendInvokeMessage(sid, opt) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_INVOKE + packet.header.type = RTMP_TYPE_INVOKE + packet.header.stream_id = sid + packet.payload = AMF.encodeAmf0Cmd(opt) + packet.header.length = packet.payload.length + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + sendDataMessage(opt, sid) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_DATA + packet.header.type = RTMP_TYPE_DATA + packet.payload = AMF.encodeAmf0Data(opt) + packet.header.length = packet.payload.length + packet.header.stream_id = sid + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + sendStatusMessage(sid, level, code, description) { + let opt = { + cmd: "onStatus", + transId: 0, + cmdObj: null, + info: { + level: level, + code: code, + description: description + } + } + this.sendInvokeMessage(sid, opt) + } + + sendRtmpSampleAccess(sid) { + let opt = { + cmd: "|RtmpSampleAccess", + bool1: false, + bool2: false + } + this.sendDataMessage(opt, sid) + } + + sendPingRequest() { + let currentTimestamp = Date.now() - this.startTimestamp + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_PROTOCOL + packet.header.type = RTMP_TYPE_EVENT + packet.header.timestamp = currentTimestamp + packet.payload = Buffer.from([0, 6, (currentTimestamp >> 24) & 0xff, (currentTimestamp >> 16) & 0xff, (currentTimestamp >> 8) & 0xff, currentTimestamp & 0xff]) + packet.header.length = packet.payload.length + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + respondConnect(tid) { + let opt = { + cmd: "_result", + transId: tid, + cmdObj: { + fmsVer: "FMS/3,0,1,123", + capabilities: 31 + }, + info: { + level: "status", + code: "NetConnection.Connect.Success", + description: "Connection succeeded.", + objectEncoding: this.objectEncoding + } + } + this.sendInvokeMessage(0, opt) + } + + respondCreateStream(tid) { + this.streams++ + let opt = { + cmd: "_result", + transId: tid, + cmdObj: null, + info: this.streams + } + this.sendInvokeMessage(0, opt) + } + + respondPlay() { + this.sendStreamStatus(STREAM_BEGIN, this.playStreamId) + this.sendStatusMessage(this.playStreamId, "status", "NetStream.Play.Reset", "Playing and resetting stream.") + this.sendStatusMessage(this.playStreamId, "status", "NetStream.Play.Start", "Started playing stream.") + this.sendRtmpSampleAccess() + } + + onConnect(invokeMessage) { + invokeMessage.cmdObj.app = invokeMessage.cmdObj.app.replace("/", "") //fix jwplayer + context.nodeEvent.emit("preConnect", this.id, invokeMessage.cmdObj) + if (!this.isStarting) { + return + } + this.connectCmdObj = invokeMessage.cmdObj + this.appname = invokeMessage.cmdObj.app + this.objectEncoding = invokeMessage.cmdObj.objectEncoding != null ? invokeMessage.cmdObj.objectEncoding : 0 + this.connectTime = new Date() + this.startTimestamp = Date.now() + this.pingInterval = setInterval(() => { + this.sendPingRequest() + }, this.pingTime) + this.sendWindowACK(5000000) + this.setPeerBandwidth(5000000, 2) + this.setChunkSize(this.outChunkSize) + this.respondConnect(invokeMessage.transId) + this.bitrateCache = { + intervalMs: 1000, + last_update: this.startTimestamp, + bytes: 0, + } + Logger.log(`[rtmp connect] id=${this.id} ip=${this.ip} app=${this.appname} args=${JSON.stringify(invokeMessage.cmdObj)}`) + context.nodeEvent.emit("postConnect", this.id, invokeMessage.cmdObj) + } + + onCreateStream(invokeMessage) { + this.respondCreateStream(invokeMessage.transId) + } + + onPublish(invokeMessage) { + if (typeof invokeMessage.streamName !== "string") { + return + } + this.publishStreamPath = "/" + this.appname + "/" + invokeMessage.streamName.split("?")[0] + this.publishArgs = QueryString.parse(invokeMessage.streamName.split("?")[1]) + this.publishStreamId = this.parserPacket.header.stream_id + context.nodeEvent.emit("prePublish", this.id, this.publishStreamPath, this.publishArgs) + if (!this.isStarting) { + return + } + + if (this.config.auth && this.config.auth.publish && !this.isLocal) { + let results = NodeCoreUtils.verifyAuth(this.publishArgs.sign, this.publishStreamPath, this.config.auth.secret) + if (!results) { + Logger.log(`[rtmp publish] Unauthorized. id=${this.id} streamPath=${this.publishStreamPath} streamId=${this.publishStreamId} sign=${this.publishArgs.sign} `) + this.sendStatusMessage(this.publishStreamId, "error", "NetStream.publish.Unauthorized", "Authorization required.") + return + } + } + + if (context.publishers.has(this.publishStreamPath)) { + this.reject() + Logger.log(`[rtmp publish] Already has a stream. id=${this.id} streamPath=${this.publishStreamPath} streamId=${this.publishStreamId}`) + this.sendStatusMessage(this.publishStreamId, "error", "NetStream.Publish.BadName", "Stream already publishing") + } else if (this.isPublishing) { + Logger.log(`[rtmp publish] NetConnection is publishing. id=${this.id} streamPath=${this.publishStreamPath} streamId=${this.publishStreamId}`) + this.sendStatusMessage(this.publishStreamId, "error", "NetStream.Publish.BadConnection", "Connection already publishing") + } else { + Logger.log(`[rtmp publish] New stream. id=${this.id} streamPath=${this.publishStreamPath} streamId=${this.publishStreamId}`) + context.publishers.set(this.publishStreamPath, this.id) + this.isPublishing = true + + this.sendStatusMessage(this.publishStreamId, "status", "NetStream.Publish.Start", `${this.publishStreamPath} is now published.`) + for (let idlePlayerId of context.idlePlayers) { + let idlePlayer = context.sessions.get(idlePlayerId) + if (idlePlayer && idlePlayer.playStreamPath === this.publishStreamPath) { + idlePlayer.onStartPlay() + context.idlePlayers.delete(idlePlayerId) + } + } + context.nodeEvent.emit("postPublish", this.id, this.publishStreamPath, this.publishArgs) + } + } + + onPlay(invokeMessage) { + if (typeof invokeMessage.streamName !== "string") { + return + } + this.playStreamPath = "/" + this.appname + "/" + invokeMessage.streamName.split("?")[0] + this.playArgs = QueryString.parse(invokeMessage.streamName.split("?")[1]) + this.playStreamId = this.parserPacket.header.stream_id + context.nodeEvent.emit("prePlay", this.id, this.playStreamPath, this.playArgs) + + if (!this.isStarting) { + return + } + + if (this.config.auth && this.config.auth.play && !this.isLocal) { + let results = NodeCoreUtils.verifyAuth(this.playArgs.sign, this.playStreamPath, this.config.auth.secret) + if (!results) { + Logger.log(`[rtmp play] Unauthorized. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId} sign=${this.playArgs.sign}`) + this.sendStatusMessage(this.playStreamId, "error", "NetStream.play.Unauthorized", "Authorization required.") + return + } + } + + if (this.isPlaying) { + Logger.log(`[rtmp play] NetConnection is playing. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId} `) + this.sendStatusMessage(this.playStreamId, "error", "NetStream.Play.BadConnection", "Connection already playing") + } else { + this.respondPlay() + } + + if (context.publishers.has(this.playStreamPath)) { + this.onStartPlay() + } else { + Logger.log(`[rtmp play] Stream not found. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId}`) + this.isIdling = true + context.idlePlayers.add(this.id) + } + } + + onStartPlay() { + let publisherId = context.publishers.get(this.playStreamPath) + let publisher = context.sessions.get(publisherId) + let players = publisher.players + players.add(this.id) + + if (publisher.metaData != null) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_DATA + packet.header.type = RTMP_TYPE_DATA + packet.payload = publisher.metaData + packet.header.length = packet.payload.length + packet.header.stream_id = this.playStreamId + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + if (publisher.audioCodec === 10 || publisher.audioCodec === 13) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_AUDIO + packet.header.type = RTMP_TYPE_AUDIO + packet.payload = publisher.aacSequenceHeader + packet.header.length = packet.payload.length + packet.header.stream_id = this.playStreamId + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + if (publisher.videoCodec === 7 || publisher.videoCodec === 12) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_VIDEO + packet.header.type = RTMP_TYPE_VIDEO + packet.payload = publisher.avcSequenceHeader + packet.header.length = packet.payload.length + packet.header.stream_id = this.playStreamId + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + + if (publisher.rtmpGopCacheQueue != null) { + for (let chunks of publisher.rtmpGopCacheQueue) { + chunks.writeUInt32LE(this.playStreamId, 8) + this.socket.write(chunks) + } + } + + this.isIdling = false + this.isPlaying = true + context.nodeEvent.emit("postPlay", this.id, this.playStreamPath, this.playArgs) + Logger.log(`[rtmp play] Join stream. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId} `) + } + + onPause(invokeMessage) { + this.isPause = invokeMessage.pause + let c = this.isPause ? "NetStream.Pause.Notify" : "NetStream.Unpause.Notify" + let d = this.isPause ? "Paused live" : "Unpaused live" + Logger.log(`[rtmp play] ${d} stream. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId} `) + if (!this.isPause) { + this.sendStreamStatus(STREAM_BEGIN, this.playStreamId) + if (context.publishers.has(this.playStreamPath)) { + //fix ckplayer + let publisherId = context.publishers.get(this.playStreamPath) + let publisher = context.sessions.get(publisherId) + if (publisher.audioCodec === 10 || publisher.audioCodec === 13) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_AUDIO + packet.header.type = RTMP_TYPE_AUDIO + packet.payload = publisher.aacSequenceHeader + packet.header.length = packet.payload.length + packet.header.stream_id = this.playStreamId + packet.header.timestamp = publisher.parserPacket.clock // ?? 0 or clock + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + if (publisher.videoCodec === 7 || publisher.videoCodec === 12) { + let packet = RtmpPacket.create() + packet.header.fmt = RTMP_CHUNK_TYPE_0 + packet.header.cid = RTMP_CHANNEL_VIDEO + packet.header.type = RTMP_TYPE_VIDEO + packet.payload = publisher.avcSequenceHeader + packet.header.length = packet.payload.length + packet.header.stream_id = this.playStreamId + packet.header.timestamp = publisher.parserPacket.clock // ?? 0 or clock + let chunks = this.rtmpChunksCreate(packet) + this.socket.write(chunks) + } + } + } else { + this.sendStreamStatus(STREAM_EOF, this.playStreamId) + } + this.sendStatusMessage(this.playStreamId, c, d) + } + + onReceiveAudio(invokeMessage) { + this.isReceiveAudio = invokeMessage.bool + Logger.log(`[rtmp play] receiveAudio=${this.isReceiveAudio} id=${this.id} `) + } + + onReceiveVideo(invokeMessage) { + this.isReceiveVideo = invokeMessage.bool + Logger.log(`[rtmp play] receiveVideo=${this.isReceiveVideo} id=${this.id} `) + } + + onCloseStream() { + //red5-publisher + let closeStream = { streamId: this.parserPacket.header.stream_id } + this.onDeleteStream(closeStream) + } + + onDeleteStream(invokeMessage) { + if (invokeMessage.streamId == this.playStreamId) { + if (this.isIdling) { + context.idlePlayers.delete(this.id) + this.isIdling = false + } else { + let publisherId = context.publishers.get(this.playStreamPath) + if (publisherId != null) { + context.sessions.get(publisherId).players.delete(this.id) + } + context.nodeEvent.emit("donePlay", this.id, this.playStreamPath, this.playArgs) + this.isPlaying = false + } + Logger.log(`[rtmp play] Close stream. id=${this.id} streamPath=${this.playStreamPath} streamId=${this.playStreamId}`) + if (this.isStarting) { + this.sendStatusMessage(this.playStreamId, "status", "NetStream.Play.Stop", "Stopped playing stream.") + } + this.playStreamId = 0 + this.playStreamPath = "" + } + + if (invokeMessage.streamId == this.publishStreamId) { + if (this.isPublishing) { + Logger.log(`[rtmp publish] Close stream. id=${this.id} streamPath=${this.publishStreamPath} streamId=${this.publishStreamId}`) + context.nodeEvent.emit("donePublish", this.id, this.publishStreamPath, this.publishArgs) + if (this.isStarting) { + this.sendStatusMessage(this.publishStreamId, "status", "NetStream.Unpublish.Success", `${this.publishStreamPath} is now unpublished.`) + } + + for (let playerId of this.players) { + let playerSession = context.sessions.get(playerId) + if (playerSession instanceof NodeRtmpSession) { + playerSession.sendStatusMessage(playerSession.playStreamId, "status", "NetStream.Play.UnpublishNotify", "stream is now unpublished.") + playerSession.flush() + } else { + playerSession.stop() + } + } + + //let the players to idlePlayers + for (let playerId of this.players) { + let playerSession = context.sessions.get(playerId) + context.idlePlayers.add(playerId) + playerSession.isPlaying = false + playerSession.isIdling = true + if (playerSession instanceof NodeRtmpSession) { + playerSession.sendStreamStatus(STREAM_EOF, playerSession.playStreamId) + } + } + + context.publishers.delete(this.publishStreamPath) + if (this.rtmpGopCacheQueue) { + this.rtmpGopCacheQueue.clear() + } + if (this.flvGopCacheQueue) { + this.flvGopCacheQueue.clear() + } + this.players.clear() + this.isPublishing = false + } + this.publishStreamId = 0 + this.publishStreamPath = "" + } + } +} + +module.exports = NodeRtmpSession diff --git a/packages/streaming-server/src/internal-nms/sessionsModels/trans_session.js b/packages/streaming-server/src/internal-nms/sessionsModels/trans_session.js new file mode 100644 index 00000000..d0ecfbfc --- /dev/null +++ b/packages/streaming-server/src/internal-nms/sessionsModels/trans_session.js @@ -0,0 +1,114 @@ +// +// Created by Mingliang Chen on 18/3/9. +// illuspas[a]gmail.com +// Copyright (c) 2018 Nodemedia. All rights reserved. +// + +const fs = require("fs") +const EventEmitter = require("events") +const { spawn } = require("child_process") +const dateFormat = require("dateformat") +const mkdirp = require("mkdirp") + +const Logger = require("../lib/logger") + +class NodeTransSession extends EventEmitter { + constructor(conf) { + super() + this.conf = conf + } + + run() { + let vc = this.conf.vc || "copy" + let ac = this.conf.ac || "copy" + let inPath = "rtmp://127.0.0.1:" + this.conf.rtmpPort + this.conf.streamPath + let ouPath = `${this.conf.mediaroot}/${this.conf.streamApp}/${this.conf.fixedStreamName}` + let mapStr = "" + + if (this.conf.rtmp && this.conf.rtmpApp) { + if (this.conf.rtmpApp === this.conf.streamApp) { + Logger.error("[Transmuxing RTMP] Cannot output to the same app.") + } else { + let rtmpOutput = `rtmp://127.0.0.1:${this.conf.rtmpPort}/${this.conf.rtmpApp}/${this.conf.streamName}` + mapStr += `[f=flv]${rtmpOutput}|` + Logger.log("[Transmuxing RTMP] " + this.conf.streamPath + " to " + rtmpOutput) + } + } + + if (this.conf.mp4) { + this.conf.mp4Flags = this.conf.mp4Flags ? this.conf.mp4Flags : "" + let mp4FileName = dateFormat("yyyy-mm-dd-HH-MM-ss") + ".mp4" + let mapMp4 = `${this.conf.mp4Flags}${ouPath}/${mp4FileName}|` + mapStr += mapMp4 + Logger.log("[Transmuxing MP4] " + this.conf.streamPath + " to " + ouPath + "/" + mp4FileName) + } + if (this.conf.hls) { + this.conf.hlsFlags = this.conf.hlsFlags ? this.conf.hlsFlags : "" + + let hlsFileName = "index.m3u8" + let mapHls = `${this.conf.hlsFlags}${ouPath}/${hlsFileName}|` + + mapStr += mapHls + + Logger.log("[Transmuxing HLS] " + this.conf.streamPath + " to " + ouPath + "/" + hlsFileName) + } + if (this.conf.dash) { + this.conf.dashFlags = this.conf.dashFlags ? this.conf.dashFlags : "" + let dashFileName = "index.mpd" + let mapDash = `${this.conf.dashFlags}${ouPath}/${dashFileName}` + mapStr += mapDash + Logger.log("[Transmuxing DASH] " + this.conf.streamPath + " to " + ouPath + "/" + dashFileName) + } + + mkdirp.sync(ouPath) + + let argv = ["-y", "-i", inPath] + + Array.prototype.push.apply(argv, ["-c:v", vc]) + Array.prototype.push.apply(argv, this.conf.vcParam) + Array.prototype.push.apply(argv, ["-c:a", ac]) + Array.prototype.push.apply(argv, this.conf.acParam) + Array.prototype.push.apply(argv, ["-f", "tee", "-map", "0:a?", "-map", "0:v?", mapStr]) + + argv = argv.filter((n) => { return n }) //去空 + + this.ffmpeg_exec = spawn(this.conf.ffmpeg, argv) + this.ffmpeg_exec.on("error", (e) => { + Logger.ffdebug(e) + }) + + this.ffmpeg_exec.stdout.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.stderr.on("data", (data) => { + Logger.ffdebug(`FF输出:${data}`) + }) + + this.ffmpeg_exec.on("close", (code) => { + + Logger.log("[Transmuxing end] " + this.conf.streamPath) + this.emit("end") + + fs.readdir(ouPath, function (err, files) { + if (!err) { + files.forEach((filename) => { + if (filename.endsWith(".ts") + || filename.endsWith(".m3u8") + || filename.endsWith(".mpd") + || filename.endsWith(".m4s") + || filename.endsWith(".tmp")) { + fs.unlinkSync(ouPath + "/" + filename) + } + }) + } + }) + }) + } + + end() { + this.ffmpeg_exec.kill() + } +} + +module.exports = NodeTransSession \ No newline at end of file diff --git a/packages/streaming-server/src/nms b/packages/streaming-server/src/nms deleted file mode 160000 index 219ca691..00000000 --- a/packages/streaming-server/src/nms +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 219ca691fda874c94712e4ba86a023948940409d