fix-rtmp-server
Version:
Fix Rtmp Server
1,032 lines (931 loc) • 36.3 kB
JavaScript
//
// Created by Mingliang Chen on 17/8/1.
// illuspas[a]gmail.com
// edit : Cengiz AKCAN | 05/03/2018
// edit : adana.web.software@gmail.com
// Copyright (c) 2017 Nodemedia. All rights reserved.
//
const EventEmitter = require('events');
const QueryString = require('querystring');
const AV = require('./node_core_av');
const { AUDIO_SOUND_RATE, AUDIO_CODEC_NAME, VIDEO_CODEC_NAME } = require('./node_core_av');
const AMF = require('./node_core_amf');
const Handshake = require('./node_rtmp_handshake');
const BufferPool = require('./node_core_bufferpool');
const NodeFlvSession = require('./node_flv_session');
const NodeCoreUtils = require('./node_core_utils');
const EXTENDED_TIMESTAMP_TYPE_NOT_USED = 'not-used';
const EXTENDED_TIMESTAMP_TYPE_ABSOLUTE = 'absolute';
const EXTENDED_TIMESTAMP_TYPE_DELTA = 'delta';
const TIMESTAMP_ROUNDOFF = 4294967296;
const STREAM_BEGIN = 0x00;
const STREAM_EOF = 0x01;
const STREAM_DRY = 0x02;
const STREAM_EMPTY = 0x1f;
const STREAM_READY = 0x20;
const RTMP_CHUNK_SIZE = 128;
const RTMP_PING_TIME = 60000;
const RTMP_PING_TIMEOUT = 30000;
class NodeRtmpSession extends EventEmitter {
constructor(config, socket) {
super();
this.TAG = 'rtmp';
this.config = config;
this.bp = new BufferPool(this.handleData());
this.nodeEvent = NodeCoreUtils.nodeEvent;
this.socket = socket;
this.players = null;
this.inChunkSize = RTMP_CHUNK_SIZE;
this.outChunkSize = config.rtmp.chunk_size ? config.rtmp.chunk_size : RTMP_CHUNK_SIZE;
this.previousChunkMessage = {};
this.ping = 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.socket.setTimeout(this.pingTimeout); //Use nodejs network timeout mechanism
this.isStarting = false;
this.isPublishing = false;
this.isPlaying = false;
this.isIdling = false;
this.isFirstAudioReceived = false;
this.isFirstVideoReceived = false;
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.videoLevel = 0;
this.gopCacheEnable = config.rtmp.gop_cache;
this.rtmpGopCacheQueue = null;
this.flvGopCacheQueue = null;
this.ackSize = 0;
this.inLastAck = 0;
this.appname = '';
this.streams = 0;
this.playStreamId = 0;
this.playStreamPath = '';
this.playArgs = '';
this.publishStreamId = 0;
this.publishStreamPath = '';
this.publishArgs = '';
this.on('connect', this.onConnect);
this.on('publish', this.onPublish);
this.on('play', this.onPlay);
this.on('closeStream', this.onCloseStream);
this.on('deleteStream', this.onDeleteStream);
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));
}
run() {
this.isStarting = true;
this.bp.init();
}
stop() {
if (this.isStarting) {
this.isStarting = false;
this.bp.stop();
}
}
reject() {
this.isStarting = false;
}
onSocketData(data) {
this.bp.push(data);
}
onSocketError(e) {
// console.log(`[rtmp socket error] id:${this.id}`,e);
this.stop();
}
onSocketClose() {
// console.log(`[rtmp socket close] id:${this.id}`);
this.stop();
}
onSocketTimeout() {
// console.log(`[rtmp socket timeout] id:${this.id}`);
this.stop();
}
* handleData() {
console.log('[rtmp handshake] start');
if (this.bp.need(1537)) {
if (yield) return;
}
let c0c1 = this.bp.read(1537);
let s0s1s2 = Handshake.generateS0S1S2(c0c1);
this.socket.write(s0s1s2);
if (this.bp.need(1536)) {
if (yield) return;
}
let c2 = this.bp.read(1536);
console.log('[rtmp handshake] done');
console.log('[rtmp message parser] start');
this.bp.readBytes = 0;
while (this.isStarting) {
let message = {};
let chunkMessageHeader = null;
let previousChunk = null;
if (this.bp.need(1)) {
if (yield) break;
}
let chunkBasicHeader = this.bp.read(1);
message.formatType = chunkBasicHeader[0] >> 6;
message.chunkStreamID = chunkBasicHeader[0] & 0x3F;
if (message.chunkStreamID === 0) {
// Chunk basic header 2 64-319
if (this.bp.need(1)) {
if (yield) break;
}
let exCSID = this.bp.read(1);
message.chunkStreamID = exCSID[0] + 64;
} else if (message.chunkStreamID === 1) {
// Chunk basic header 3 64-65599
if (this.bp.need(2)) {
if (yield) break;
}
let exCSID = this.bp.read(2);
message.chunkStreamID = (exCSID[1] << 8) + exCSID[0] + 64;
} else {
// Chunk basic header 1 2-63
}
previousChunk = this.previousChunkMessage[message.chunkStreamID];
if (message.formatType === 0) {
//Type 0 (11 bytes)
if (this.bp.need(11)) {
if (yield) break;
}
chunkMessageHeader = this.bp.read(11);
message.timestamp = chunkMessageHeader.readUIntBE(0, 3);
if (message.timestamp === 0xffffff) {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_ABSOLUTE;
} else {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
}
message.timestampDelta = 0;
message.messageLength = chunkMessageHeader.readUIntBE(3, 3);
message.messageTypeID = chunkMessageHeader[6];
message.messageStreamID = chunkMessageHeader.readUInt32LE(7);
message.receivedLength = 0;
message.chunks = [];
} else if (message.formatType === 1) {
//Type 1 (7 bytes)
if (this.bp.need(7)) {
if (yield) break;
}
chunkMessageHeader = this.bp.read(7);
message.timestampDelta = chunkMessageHeader.readUIntBE(0, 3);
if (message.timestampDelta === 0xffffff) {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_DELTA;
} else {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
}
message.messageLength = chunkMessageHeader.readUIntBE(3, 3);
message.messageTypeID = chunkMessageHeader[6];
if (previousChunk != null) {
message.timestamp = previousChunk.timestamp;
message.messageStreamID = previousChunk.messageStreamID;
message.receivedLength = previousChunk.receivedLength;
message.chunks = previousChunk.chunks;
} else {
console.error(`Chunk reference error for type ${message.formatType}: previous chunk for id ${message.chunkStreamID} is not found`);
break;
}
} else if (message.formatType === 2) {
// Type 2 (3 bytes)
if (this.bp.need(3)) {
if (yield) break;
}
chunkMessageHeader = this.bp.read(3);
message.timestampDelta = chunkMessageHeader.readUIntBE(0, 3);
if (message.timestampDelta === 0xffffff) {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_DELTA;
} else {
message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
}
if (previousChunk != null) {
message.timestamp = previousChunk.timestamp;
message.messageStreamID = previousChunk.messageStreamID;
message.messageLength = previousChunk.messageLength;
message.messageTypeID = previousChunk.messageTypeID;
message.receivedLength = previousChunk.receivedLength;
message.chunks = previousChunk.chunks;
} else {
console.error(`Chunk reference error for type ${message.formatType}: previous chunk for id ${message.chunkStreamID} is not found`);
break;
}
} else if (message.formatType == 3) {
// Type 3 (0 byte)
if (previousChunk != null) {
message.timestamp = previousChunk.timestamp;
message.messageStreamID = previousChunk.messageStreamID;
message.messageLength = previousChunk.messageLength;
message.timestampDelta = previousChunk.timestampDelta;
message.messageTypeID = previousChunk.messageTypeID;
message.receivedLength = previousChunk.receivedLength;
message.chunks = previousChunk.chunks;
} else {
console.error(`Chunk reference error for type ${message.formatType}: previous chunk for id ${message.chunkStreamID} is not found`);
break;
}
} else {
console.error("Unknown format type: " + message.formatType);
break;
}
if (message.extendedTimestampType === EXTENDED_TIMESTAMP_TYPE_ABSOLUTE) {
if (this.bp.need(4)) {
if (yield) break;
}
let extTimestamp = this.bp.read(4);
message.timestamp = extTimestamp.readUInt32BE();
} else if (message.extendedTimestampType === EXTENDED_TIMESTAMP_TYPE_DELTA) {
let extTimestamp = this.bp.read(4);
message.timestampDelta = extTimestamp.readUInt32BE();
}
let chunkBodySize = message.messageLength;
chunkBodySize -= message.receivedLength;
chunkBodySize = Math.min(chunkBodySize, this.inChunkSize);
if (this.bp.need(chunkBodySize)) {
if (yield) break;
}
let chunkBody = this.bp.read(chunkBodySize);
message.receivedLength += chunkBodySize;
message.chunks.push(chunkBody);
if (message.receivedLength == message.messageLength) {
if (message.timestampDelta != null) {
message.timestamp += message.timestampDelta;
if (message.timestamp > TIMESTAMP_ROUNDOFF) {
message.timestamp %= TIMESTAMP_ROUNDOFF;
}
}
let rtmpBody = Buffer.concat(message.chunks);
this.handleRTMPMessage(message, rtmpBody);
message.receivedLength = 0;
message.chunks = [];
rtmpBody = null;
}
this.previousChunkMessage[message.chunkStreamID] = message;
if (this.bp.readBytes >= 0xf0000000) {
this.bp.readBytes = 0;
this.inLastAck = 0;
}
if (this.ackSize > 0 && this.bp.readBytes - this.inLastAck >= this.ackSize) {
this.inLastAck = this.bp.readBytes;
this.sendACK(this.bp.readBytes);
}
}
console.log('[rtmp message parser] done');
this.onCloseStream(this.playStreamId);
this.onCloseStream(this.publishStreamId);
if (this.pingInterval != null) {
clearImmediate(this.pingInterval);
this.pingInterval = null;
}
this.nodeEvent.emit('doneConnect', this.id, this.connectCmdObj);
this.socket.destroy();
this.sessions.delete(this.id);
this.idlePlayers = null;
this.publishers = null;
this.sessions = null;
}
createChunkBasicHeader(fmt, id) {
let out;
if (id >= 64 + 255) {
out = Buffer.alloc(3);
out[0] = (fmt << 6) | 1;
out[1] = (id - 64) & 0xFF;
out[2] = ((id - 64) >> 8) & 0xFF;
} else if (id >= 64) {
out = Buffer.alloc(2);
out[0] = (fmt << 6) | 0;
out[1] = (id - 64) & 0xFF;
} else {
out = Buffer.alloc(1);
out[0] = (fmt << 6) | id;
}
return out;
}
createRtmpMessage(rtmpHeader, rtmpBody) {
let chunkBasicHeader = this.createChunkBasicHeader(0, rtmpHeader.chunkStreamID);
let chunkMessageHeader = Buffer.alloc(11);
let chunkExtendedTimestamp;
let extendedTimestamp = 0;
let useExtendedTimestamp = false
let rtmpBodySize = rtmpBody.length;
let rtmpBodyPos = 0;
let chunkBodys = [];
rtmpHeader.messageLength = rtmpBody.length;
if (rtmpHeader.timestamp >= 0xffffff) {
useExtendedTimestamp = true;
extendedTimestamp = rtmpHeader.timestamp;
chunkExtendedTimestamp = Buffer.alloc(4);
chunkExtendedTimestamp.writeUInt32BE(extendedTimestamp);
}
chunkMessageHeader.writeUIntBE(useExtendedTimestamp ? 0xffffff : rtmpHeader.timestamp, 0, 3);
chunkMessageHeader.writeUIntBE(rtmpHeader.messageLength, 3, 3);
chunkMessageHeader.writeUInt8(rtmpHeader.messageTypeID, 6);
chunkMessageHeader.writeUInt32LE(rtmpHeader.messageStreamID, 7);
chunkBodys.push(chunkBasicHeader);
chunkBodys.push(chunkMessageHeader);
if (useExtendedTimestamp) {
chunkBodys.push(chunkExtendedTimestamp);
}
do {
if (rtmpBodySize > this.outChunkSize) {
chunkBodys.push(rtmpBody.slice(rtmpBodyPos, rtmpBodyPos + this.outChunkSize));
rtmpBodySize -= this.outChunkSize
rtmpBodyPos += this.outChunkSize;
chunkBodys.push(this.createChunkBasicHeader(3, rtmpHeader.chunkStreamID));
if (useExtendedTimestamp) {
chunkBodys.push(chunkExtendedTimestamp);
}
} else {
chunkBodys.push(rtmpBody.slice(rtmpBodyPos, rtmpBodyPos + rtmpBodySize));
rtmpBodySize -= rtmpBodySize;
rtmpBodyPos += rtmpBodySize;
}
} while (rtmpBodySize > 0)
return Buffer.concat(chunkBodys);
}
handleRTMPMessage(rtmpHeader, rtmpBody) {
// console.log(`[rtmp handleRtmpMessage] rtmpHeader.messageTypeID=${rtmpHeader.messageTypeID}`);
switch (rtmpHeader.messageTypeID) {
case 1:
this.inChunkSize = rtmpBody.readUInt32BE();
console.log('[rtmp handleRtmpMessage] Set In chunkSize:' + this.inChunkSize);
break;
case 3:
// console.log('[rtmp handleRtmpMessage] Ack:' + rtmpBody.readUInt32BE());
break;
case 4:
let userControlMessage = {};
userControlMessage.eventType = rtmpBody.readUInt16BE();
userControlMessage.eventData = rtmpBody.slice(2);
this.handleUserControlMessage(userControlMessage);
break;
case 5:
this.ackSize = rtmpBody.readUInt32BE();
// console.log(`[rtmp handleRtmpMessage] WindowAck: ${this.ackSize}`);
break;
case 8:
//Audio Data
this.handleAudioMessage(rtmpHeader, rtmpBody);
break;
case 9:
//Video Data
this.handleVideoMessage(rtmpHeader, rtmpBody);
break;
case 15:
//AMF3 DataMessage
let amf3Data = AMF.decodeAmf0Data(rtmpBody.slice(1));
this.handleAMFDataMessage(rtmpHeader.messageStreamID, amf3Data);
break;
case 17:
//AMF3 CommandMessage
let amf3Cmd = AMF.decodeAmf0Cmd(rtmpBody.slice(1));
this.handleAMFCommandMessage(rtmpHeader.messageStreamID, amf3Cmd);
break;
case 18:
//AMF0 DataMessage
let amf0Data = AMF.decodeAmf0Data(rtmpBody);
this.handleAMFDataMessage(rtmpHeader.messageStreamID, amf0Data);
break;
case 20:
//AMF0 CommandMessage
let amf0Cmd = AMF.decodeAmf0Cmd(rtmpBody);
this.handleAMFCommandMessage(rtmpHeader.messageStreamID, amf0Cmd);
break;
}
}
handleUserControlMessage(userControlMessage) {
switch (userControlMessage.eventType) {
case 3:
let streamID = userControlMessage.eventData.readUInt32BE();
let bufferLength = userControlMessage.eventData.readUInt32BE(4);
console.log(`[rtmp handleUserControlMessage] SetBufferLength: streamID=${streamID} bufferLength=${bufferLength}`);
break;
case 7:
let timestamp = userControlMessage.eventData.readUInt32BE();
// console.log(`[rtmp handleUserControlMessage] PingResponse: timestamp=${timestamp}`);
break;
}
}
handleAMFDataMessage(streamID, dataMessage) {
// console.log('handleAMFDataMessage', dataMessage);
switch (dataMessage.cmd) {
case '@setDataFrame':
if (dataMessage.dataObj != null) {
let opt = {
cmd: 'onMetaData',
cmdObj: dataMessage.dataObj
};
this.metaData = AMF.encodeAmf0Data(opt);
// console.log(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;
}
break;
default:
break;
}
}
handleAMFCommandMessage(streamID, commandMessage) {
// console.log('handleAMFCommandMessage:', commandMessage);
switch (commandMessage.cmd) {
case 'connect':
this.emit('connect', commandMessage.cmdObj);
break;
case 'createStream':
this.respondCreateStream(commandMessage);
break;
case 'FCPublish':
// this.respondFCPublish();
break;
case 'publish':
this.publishStreamPath = '/' + this.appname + '/' + commandMessage.streamName.split('?')[0];
this.publishArgs = QueryString.parse(commandMessage.streamName.split('?')[1]);
this.publishStreamId = streamID;
// console.log('publish streamID=' + streamID);
this.emit('publish');
break;
case 'play':
this.playStreamPath = '/' + this.appname + '/' + commandMessage.streamName.split('?')[0];
this.playArgs = QueryString.parse(commandMessage.streamName.split('?')[1]);
this.playStreamId = streamID;
// console.log('play streamID=' + streamID);
this.emit('play');
break;
case 'closeStream':
this.emit('closeStream', streamID);
break;
case 'deleteStream':
this.emit('deleteStream', streamID);
break;
case 'pause':
// this.pauseOrUnpauseStream();
break;
case 'releaseStream':
// this.respondReleaseStream();
break;
case 'FCUnpublish':
// this.respondFCUnpublish();
break;
default:
console.warn("[rtmp handleCommandMessage] unknown AMF command: " + commandMessage.cmd);
break;
}
}
handleAudioMessage(rtmpHeader, rtmpBody) {
if (!this.isPublishing) {
return;
}
if (!this.isFirstAudioReceived) {
let sound_format = rtmpBody[0];
let sound_type = sound_format & 0x01;
let sound_size = (sound_format >> 1) & 0x01;
let sound_rate = (sound_format >> 2) & 0x03;
sound_format = (sound_format >> 4) & 0x0f;
this.audioCodec = sound_format;
this.audioCodecName = AUDIO_CODEC_NAME[sound_format];
console.log(`[rtmp handleAudioMessage] Parse AudioTagHeader sound_format=${sound_format} sound_type=${sound_type} sound_size=${sound_size} sound_rate=${sound_rate} codec_name=${this.audioCodecName}`);
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) {
//cache aac sequence header
if (rtmpBody[1] == 0) {
this.aacSequenceHeader = Buffer.from(rtmpBody);
this.isFirstAudioReceived = true;
let info = AV.readAACSpecificConfig(this.aacSequenceHeader);
// console.log('[rtmp handleAudioMessage ]',info);
this.audioProfileName = AV.getAACProfileName(info);
this.audioSamplerate = info.sample_rate;
this.audioChannels = info.channels;
}
} else {
this.isFirstAudioReceived = true;
}
}
// console.log('Audio chunkStreamID='+rtmpHeader.chunkStreamID+' '+rtmpHeader.messageStreamID);
// console.log(`Send Audio message timestamp=${rtmpHeader.timestamp} timestampDelta=${rtmpHeader.timestampDelta} bytesRead=${this.socket.bytesRead}`);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
let flvMessage = NodeFlvSession.createFlvMessage(rtmpHeader, rtmpBody);
if (this.rtmpGopCacheQueue != null) {
if (this.aacSequenceHeader != null && rtmpBody[1] == 0) {
//skip aac sequence header
} else {
this.rtmpGopCacheQueue.add(rtmpMessage);
this.flvGopCacheQueue.add(flvMessage);
}
}
for (let playerId of this.players) {
let session = this.sessions.get(playerId);
if (session instanceof NodeRtmpSession) {
rtmpMessage.writeUInt32LE(session.playStreamId, 8);
session.socket.write(rtmpMessage);
} else if (session instanceof NodeFlvSession) {
session.res.write(flvMessage, null, (e) => {
//websocket will throw a error if not set the cb when closed
});
}
}
}
handleVideoMessage(rtmpHeader, rtmpBody) {
if (!this.isPublishing) {
return;
}
let frame_type = rtmpBody[0];
let codec_id = frame_type & 0x0f;
frame_type = (frame_type >> 4) & 0x0f;
if (!this.isFirstVideoReceived) {
this.videoCodec = codec_id;
this.videoCodecName = VIDEO_CODEC_NAME[codec_id];
console.log(`[rtmp handleVideoMessage] Parse VideoTagHeader frame_type=${frame_type} codec_id=${codec_id} codec_name=${this.videoCodecName}`);
if (codec_id == 7 || codec_id == 12) {
//cache avc sequence header
if (frame_type == 1 && rtmpBody[1] == 0) {
this.avcSequenceHeader = Buffer.from(rtmpBody);
this.isFirstVideoReceived = true;
let info = AV.readAVCSpecificConfig(this.avcSequenceHeader);
// console.log('[rtmp handleVideoMessage ]',info);
if (this.videoWidth == 0 || this.videoHeight == 0) {
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;
}
} else {
this.isFirstVideoReceived = true;
}
}
// console.log('Video chunkStreamID='+rtmpHeader.chunkStreamID+' '+rtmpHeader.messageStreamID);
// console.log(`Send Video message timestamp=${rtmpHeader.timestamp} timestampDelta=${rtmpHeader.timestampDelta} `);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
let flvMessage = NodeFlvSession.createFlvMessage(rtmpHeader, rtmpBody);
if ((codec_id == 7 || codec_id == 12) && this.rtmpGopCacheQueue != null) {
if (frame_type == 1 && rtmpBody[1] == 1) {
this.rtmpGopCacheQueue.clear();
this.flvGopCacheQueue.clear();
}
if (frame_type == 1 && rtmpBody[1] == 0) {
//skip avc sequence header
} else {
this.rtmpGopCacheQueue.add(rtmpMessage);
this.flvGopCacheQueue.add(flvMessage);
}
}
for (let playerId of this.players) {
let session = this.sessions.get(playerId);
if (session instanceof NodeRtmpSession) {
rtmpMessage.writeUInt32LE(session.playStreamId, 8);
session.socket.write(rtmpMessage);
} else if (session instanceof NodeFlvSession) {
session.res.write(flvMessage, null, (e) => {
//websocket will throw a error if not set the cb when closed
});
}
}
}
sendACK(size) {
let rtmpBuffer = new Buffer('02000000000004030000000000000000', 'hex');
rtmpBuffer.writeUInt32BE(size, 12);
// //console.log('windowACK: '+rtmpBuffer.hex());
this.socket.write(rtmpBuffer);
}
sendWindowACK(size) {
let rtmpBuffer = new Buffer('02000000000004050000000000000000', 'hex');
rtmpBuffer.writeUInt32BE(size, 12);
// //console.log('windowACK: '+rtmpBuffer.hex());
this.socket.write(rtmpBuffer);
};
setPeerBandwidth(size, type) {
let rtmpBuffer = new Buffer('0200000000000506000000000000000000', 'hex');
rtmpBuffer.writeUInt32BE(size, 12);
rtmpBuffer[16] = type;
// //console.log('setPeerBandwidth: '+rtmpBuffer.hex());
this.socket.write(rtmpBuffer);
};
setChunkSize(size) {
let rtmpBuffer = new Buffer('02000000000004010000000000000000', 'hex');
rtmpBuffer.writeUInt32BE(size, 12);
// //console.log('setChunkSize: '+rtmpBuffer.hex());
this.socket.write(rtmpBuffer);
};
sendStreamStatus(st, id) {
let rtmpBuffer = new Buffer('020000000000060400000000000000000000', 'hex');
rtmpBuffer.writeUInt16BE(st, 12);
rtmpBuffer.writeUInt32BE(id, 14);
this.socket.write(rtmpBuffer);
}
sendRtmpSampleAccess() {
let rtmpHeader = {
chunkStreamID: 5,
timestamp: 0,
messageTypeID: 0x12,
messageStreamID: 1
};
let opt = {
cmd: '|RtmpSampleAccess',
bool1: false,
bool2: false
};
let rtmpBody = AMF.encodeAmf0Data(opt);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
this.socket.write(rtmpMessage);
}
sendStatusMessage(id, level, code, description) {
let rtmpHeader = {
chunkStreamID: 5,
timestamp: 0,
messageTypeID: 0x14,
messageStreamID: id
};
let opt = {
cmd: 'onStatus',
transId: 0,
cmdObj: null,
info: {
level: level,
code: code,
description: description
}
};
let rtmpBody = AMF.encodeAmf0Cmd(opt);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
this.socket.write(rtmpMessage);
}
pingRequest() {
let currentTimestamp = Date.now() - this.startTimestamp;
let rtmpHeader = {
chunkStreamID: 2,
timestamp: currentTimestamp,
messageTypeID: 0x4,
messageStreamID: 0
};
let rtmpBody = new Buffer([0, 6, (currentTimestamp >> 24) & 0xff, (currentTimestamp >> 16) & 0xff, (currentTimestamp >> 8) & 0xff, currentTimestamp & 0xff])
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
this.socket.write(rtmpMessage);
// console.log('pingRequest',rtmpMessage.toString('hex'));
}
respondConnect() {
let rtmpHeader = {
chunkStreamID: 3,
timestamp: 0,
messageTypeID: 0x14,
messageStreamID: 0
};
let opt = {
cmd: '_result',
transId: 1,
cmdObj: {
fmsVer: 'FMS/3,0,1,123',
capabilities: 31
},
info: {
level: 'status',
code: 'NetConnection.Connect.Success',
description: 'Connection succeeded.',
objectEncoding: this.objectEncoding
}
};
let rtmpBody = AMF.encodeAmf0Cmd(opt);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
this.socket.write(rtmpMessage);
}
respondCreateStream(cmd) {
this.streams++;
let rtmpHeader = {
chunkStreamID: 3,
timestamp: 0,
messageTypeID: 0x14,
messageStreamID: 0
};
let opt = {
cmd: "_result",
transId: cmd.transId,
cmdObj: null,
info: this.streams
};
let rtmpBody = AMF.encodeAmf0Cmd(opt);
let rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody);
this.socket.write(rtmpMessage);
}
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(cmdObj) {
cmdObj.app = cmdObj.app.replace('/', '');
this.nodeEvent.emit('preConnect', this.id, cmdObj);
if (!this.isStarting) {
return;
}
this.connectCmdObj = cmdObj;
this.appname = cmdObj.app;
this.objectEncoding = cmdObj.objectEncoding != null ? cmdObj.objectEncoding : 0;
this.sendWindowACK(5000000);
this.setPeerBandwidth(5000000, 2);
this.setChunkSize(this.outChunkSize);
this.respondConnect();
this.startTimestamp = Date.now();
this.connectTime = new Date();
this.pingInterval = setInterval(() => {
this.pingRequest();
}, this.ping);
console.log('[rtmp connect] app: ' + cmdObj.app);
this.nodeEvent.emit('postConnect', this.id, cmdObj);
}
onPublish() {
this.nodeEvent.emit('prePublish', this.id, this.publishStreamPath, this.publishArgs);
if (!this.isStarting) {
return;
}
if (this.config.auth !== undefined && this.config.auth.publish) {
let thisIs = this;
NodeCoreUtils.verifyAuth(thisIs.publishArgs.sign, thisIs.publishStreamPath, thisIs.config.auth.secret).then(function(result){
if (result === "passive") {
console.log(`[rtmp publish] Unauthorized. ID=${thisIs.id} streamPath=${thisIs.publishStreamPath} sign=${thisIs.publishArgs.sign}`);
thisIs.sendStatusMessage(thisIs.publishStreamId, 'error', 'NetStream.publish.Unauthorized', 'Authorization required.');
return;
}
if (thisIs.publishers.has(thisIs.publishStreamPath)) {
console.warn("[rtmp publish] Already has a stream path " + thisIs.publishStreamPath);
thisIs.sendStatusMessage(thisIs.publishStreamId, 'error', 'NetStream.Publish.BadName', 'Stream already publishing');
} else if (thisIs.isPublishing) {
console.warn("[rtmp publish] NetConnection is publishing ");
thisIs.sendStatusMessage(thisIs.publishStreamId, 'error', 'NetStream.Publish.BadConnection', 'Connection already publishing');
} else {
console.log("[rtmp publish] new stream path " + thisIs.publishStreamPath + ' streamId:' + thisIs.publishStreamId);
thisIs.publishers.set(thisIs.publishStreamPath, thisIs.id);
thisIs.isPublishing = true;
thisIs.players = new Set();
thisIs.sendStatusMessage(thisIs.publishStreamId, 'status', 'NetStream.Publish.Start', `${thisIs.publishStreamPath} is now published.`);
for (let idlePlayerId of thisIs.idlePlayers) {
let idlePlayer = thisIs.sessions.get(idlePlayerId);
if (idlePlayer.playStreamPath === thisIs.publishStreamPath) {
idlePlayer.emit('play');
thisIs.idlePlayers.delete(idlePlayerId);
}
}
thisIs.nodeEvent.emit('postPublish', thisIs.id, thisIs.publishStreamPath, thisIs.publishArgs);
}
});
}
}
onPlay() {
this.nodeEvent.emit('prePlay', this.id, this.playStreamPath, this.playArgs);
if (!this.isStarting) {
return;
}
if (this.config.auth !== undefined && this.config.auth.play) {
let thisIs = this;
NodeCoreUtils.verifyAuth(thisIs.playArgs.sign, thisIs.playStreamPath, thisIs.config.auth.secret).then(function(result) {
if (result === "passive") {
console.log(`[rtmp play] Unauthorized. ID=${thisIs.id} streamPath=${thisIs.playStreamPath} sign=${thisIs.playArgs.sign}`);
thisIs.sendStatusMessage(thisIs.playStreamId, 'error', 'NetStream.play.Unauthorized', 'Authorization required.');
return;
}
});
}
/
if (this.isPlaying) {
console.warn("[rtmp play] NetConnection is playing");
this.sendStatusMessage(this.playStreamId, 'error', 'NetStream.Play.BadConnection', 'Connection already playing');
} else if (!this.publishers.has(this.playStreamPath)) {
console.log("[rtmp play] stream not found " + this.playStreamPath + ' streamId:' + this.playStreamId);
this.respondPlay();
// this.sendStreamEmpty();
this.isIdling = true;
this.idlePlayers.add(this.id);
} else {
if (this.isIdling) {
this.sendStatusMessage(this.playStreamId, 'status', 'NetStream.Play.PublishNotify', `${this.publishStreamPath} is now published.`);
} else {
this.respondPlay();
}
let publisherPath = this.publishers.get(this.playStreamPath);
let publisher = this.sessions.get(publisherPath);
let players = publisher.players;
this.isPlaying = true;
//metaData
if (publisher.metaData != null) {
let rtmpHeader = {
chunkStreamID: 5,
timestamp: 0,
messageTypeID: 0x12,
messageStreamID: this.playStreamId
};
let metaDataRtmpMessage = this.createRtmpMessage(rtmpHeader, publisher.metaData);
this.socket.write(metaDataRtmpMessage);
}
//send aacSequenceHeader
if (publisher.audioCodec == 10) {
let rtmpHeader = {
chunkStreamID: 4,
timestamp: 0,
messageTypeID: 0x08,
messageStreamID: this.playStreamId
};
let rtmpMessage = this.createRtmpMessage(rtmpHeader, publisher.aacSequenceHeader);
this.socket.write(rtmpMessage);
}
//send avcSequenceHeader
if (publisher.videoCodec == 7 || publisher.videoCodec == 12) {
let rtmpHeader = {
chunkStreamID: 6,
timestamp: 0,
messageTypeID: 0x09,
messageStreamID: this.playStreamId
};
let rtmpMessage = this.createRtmpMessage(rtmpHeader, publisher.avcSequenceHeader);
this.socket.write(rtmpMessage);
}
//send gop cache
if (publisher.rtmpGopCacheQueue != null) {
for (let rtmpMessage of publisher.rtmpGopCacheQueue) {
rtmpMessage.writeUInt32LE(this.playStreamId, 8);
this.socket.write(rtmpMessage);
}
}
if (this.isIdling) {
this.sendStreamStatus(STREAM_READY, this.playStreamId);
this.isIdling = false;
}
console.log("[rtmp play] join stream " + this.playStreamPath + ' streamId:' + this.playStreamId);
players.add(this.id);
this.nodeEvent.emit('postPlay', this.id, this.playStreamPath, this.playArgs);
}
}
onCloseStream(streamID, del) {
if (this.isIdling && this.playStreamId == streamID) {
this.sendStatusMessage(this.playStreamId, 'status', 'NetStream.Play.Stop', 'Stopped playing stream.');
this.idlePlayers.delete(this.id);
this.isIdling = false;
this.playStreamId = del ? 0 : this.playStreamId;
}
if (this.isPlaying && this.playStreamId == streamID) {
this.sendStatusMessage(this.playStreamId, 'status', 'NetStream.Play.Stop', 'Stopped playing stream.');
let publisherPath = this.publishers.get(this.playStreamPath);
if (publisherPath != null) {
this.sessions.get(publisherPath).players.delete(this.id);
}
this.isPlaying = false;
this.playStreamId = del ? 0 : this.playStreamId;
this.nodeEvent.emit('donePlay', this.id, this.playStreamPath, this.playArgs);
}
if (this.isPublishing && this.publishStreamId == streamID) {
this.sendStatusMessage(this.publishStreamId, 'status', 'NetStream.Unpublish.Success', `${this.publishStreamPath} is now unpublished.`);
for (let playerId of this.players) {
let player = this.sessions.get(playerId);
if (player instanceof NodeRtmpSession) {
player.sendStatusMessage(player.playStreamId, 'status', 'NetStream.Play.UnpublishNotify', 'stream is now unpublished.');
} else {
player.stop();
}
}
//let the players to idlePlayers
for (let playerId of this.players) {
let player = this.sessions.get(playerId);
this.idlePlayers.add(playerId);
player.isPlaying = false;
player.isIdling = true;
if (player instanceof NodeRtmpSession) {
player.sendStreamStatus(STREAM_EOF, player.playStreamId);
}
}
this.players.clear();
this.players = null;
this.publishers.delete(this.publishStreamPath);
this.isPublishing = false;
this.publishStreamId = del ? 0 : this.publishStreamId;
this.nodeEvent.emit('donePublish', this.id, this.publishStreamPath, this.publishArgs);
}
}
onDeleteStream(streamID) {
this.onCloseStream(streamID, true);
}
}
module.exports = NodeRtmpSession