@mediafish/rtmp-server
Version:
A server that receives an RTMP live stream and populates a readable object stream of the published audio, video, and data messages
219 lines (204 loc) • 6.98 kB
JavaScript
import debug from 'debug';
import {readAllValues} from '@mediafish/amf0';
import {readAudio, readVideo} from '@mediafish/flv';
import {Video, Audio, Data} from './types.js';
import handshakeUtil from './util/handshake.js';
import {readMessage} from './util/chunk.js';
import {processMessage as processProtoCtrl, sendAck} from './util/protocol-control.js';
import {processMessage as processCommand} from './util/command.js';
const print = debug('rtmp-server');
const MIN_MESSAGE_LEN = 5;
export default class ConnectionManager {
constructor(server, socket, maxStreamNum = 1) {
this.server = server;
this.socket = socket;
this.connection = null;
this.streams = new Map();
this.path = '';
this.handshakeState = 'uninitialized';
this.state = 'unconnected';
this.timeout = 15; // in seconds
this.epocTime = Date.now();
this.prevRecievedMsgHeaders = [];
this.maxChunkSize = 4096;
this.windowSize = 5_000_000;
this.lastAckSize = 0;
this.maxStreamNum = maxStreamNum;
let noMoreProcess = false;
socket.on('end', () => {
print('Client disconnected');
this.close();
noMoreProcess = true;
});
socket.setTimeout(this.timeout * 1000);
socket.on('timeout', () => {
print(`Client has been inactive for ${this.timeout} seconds`);
socket.end();
this.close();
noMoreProcess = true;
});
socket.on('data', buff => {
// print(`[ConnectionManager] Enter. buff.length=${buff.length}`);
let offset = 0, err = null;
if (this.handshakeState !== 'handshake-done') {
offset = this.doHandshake(buff, offset);
}
if (this.handshakeState === 'handshake-done') {
while (offset <= buff.length - MIN_MESSAGE_LEN) {
// print(`\tbuff.length=${buff.length}, offset=${offset}`);
[err, offset] = this.processMessage(buff, offset);
if (noMoreProcess) {
break;
}
if (err) {
console.error(err.stack);
break;
}
this.sendAckIfNeeded();
}
}
// print(`[ConnectionManager] Exit. Consumed ${offset} bytes in buff.length=${buff.length}`);
});
}
doHandshake(buff, offset) {
if (this.handshakeState === 'uninitialized') {
let version;
if ((buff.length - offset) < 1) {
return offset;
}
[offset, version] = handshakeUtil.readC0(buff, offset);
if (version < 3) {
return offset;
}
// handshakeUtil.writeS0S1(this.socket, this.epocTime);
this.handshakeState = 'version-sent';
return this.doHandshake(buff, offset);
}
if (this.handshakeState === 'version-sent') {
if ((buff.length - offset) < 1536) {
return offset;
}
let params;
[offset, params] = handshakeUtil.readC1(buff, offset);
// handshakeUtil.writeS2(this.socket, params);
handshakeUtil.writeS0S1S2(this.socket, params);
this.handshakeState = 'ack-sent';
return this.doHandshake(buff, offset);
}
if (this.handshakeState === 'ack-sent') {
if ((buff.length - offset) < 1536) {
return offset;
}
[offset] = handshakeUtil.readC2(buff, offset);
this.handshakeState = 'handshake-done';
print('Handshake done.');
}
return offset;
}
processMessage(buff, offset) {
let chunk, err = null, readable = null;
[offset, chunk] = readMessage(buff, offset, this.prevRecievedMsgHeaders);
if (!chunk) {
return [err, offset];
}
const {messageTypeId, timestamp, messageStreamId} = chunk.messageHeader;
switch (messageTypeId) {
case 1:
case 2:
case 3:
case 5:
case 6:
// Protocol Control Message
print(`Protocol Control Message: messageTypeId=${messageTypeId}`);
err = processProtoCtrl(this, chunk);
break;
case 4:
// User Control Message
print(`User Control Message: messageTypeId=${messageTypeId}`);
break;
case 20:
case 17:
// Command Message
print(`Command Message: messageTypeId=${messageTypeId}`);
err = processCommand(this, chunk);
break;
case 18:
case 15:
// Data Message
print(`Data Message: messageTypeId=${messageTypeId}, messageStreamId=${messageStreamId}`);
readable = this.streams.get(messageStreamId);
if (readable) {
readable.push(new Data(timestamp, readAllValues(chunk.data, 0)));
} else {
print(`Unknown stream ID: ${messageStreamId}`);
}
break;
case 19:
case 16:
// Shared Object Message
print(`Shared Object Message: messageTypeId=${messageTypeId}, messageStreamId=${messageStreamId}`);
break;
case 8:
// Audio Message
print(`Audio Message: messageTypeId=${messageTypeId}, messageStreamId=${messageStreamId}`);
readable = this.streams.get(messageStreamId);
if (readable) {
const audioData = chunk.data;
const [len, aac] = readAudio(audioData, 0, audioData.length);
print(`\tAAC data (len=${len})`);
readable.push(new Audio(timestamp, aac));
} else {
print(`Unknown stream ID: ${messageStreamId}`);
}
break;
case 9:
// Video Message
print(`Video Message: messageTypeId=${messageTypeId}, messageStreamId=${messageStreamId}`);
readable = this.streams.get(messageStreamId);
if (readable) {
const videoData = chunk.data;
const [len, avc] = readVideo(videoData, 0, videoData.length);
print(`\tAVC data (len=${len})`);
readable.push(new Video(timestamp, avc));
} else {
print(`Unknown stream ID: ${messageStreamId}`);
}
break;
case 22:
// Aggregate Message
print(`Aggregate Message: messageTypeId=${messageTypeId}, messageStreamId=${messageStreamId}`);
break;
default:
print(`Unknown message type ID: ${messageTypeId}`);
}
this.prevRecievedMsgHeaders[chunk.basicHeader.chunkStreamId] = chunk.messageHeader;
return [err, offset];
}
get recievedBytes() {
return this.socket.bytesRead;
}
sendAckIfNeeded() {
if (this.lastAckSize === 0 || this.recievedBytes - this.lastAckSize >= this.windowSize) {
sendAck(this.socket, this.recievedBytes);
this.lastAckSize = this.recievedBytes;
}
}
abortMessage(chunkStreamId) {
print(`abortMessage: chunkStreamId=${chunkStreamId}`);
}
close() {
this.server = null;
this.socket.destroy();
this.socket = null;
this.connection = null;
for (const [key, stream] of this.streams) {
stream.push(null);
this.streams.delete(key);
}
this.streams = new Map();
this.path = '';
this.handshakeState = 'uninitialized';
this.state = 'unconnected';
this.prevRecievedMsgHeaders = [];
}
}