rtp.js
Version:
RTP stack for Node.js and browser written in TypeScript
382 lines (381 loc) • 13.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SdesChunk = exports.SdesPacket = exports.SdesItemType = void 0;
const RtcpPacket_1 = require("./RtcpPacket");
const Serializable_1 = require("../Serializable");
const helpers_1 = require("../../utils/helpers");
// SSRC (4 bytes) + null type (1 byte) + padding (3 bytes).
const SDES_CHUNK_MIN_LENGTH = 8;
/**
* SDES Chunk Item types.
*
* @category RTCP
*/
var SdesItemType;
(function (SdesItemType) {
/**
* Canonical End-Point Identifier SDES Item.
*/
SdesItemType[SdesItemType["CNAME"] = 1] = "CNAME";
/**
* User Name SDES Item.
*/
SdesItemType[SdesItemType["NAME"] = 2] = "NAME";
/**
* Electronic Mail Address SDES Item.
*/
SdesItemType[SdesItemType["EMAIL"] = 3] = "EMAIL";
/**
* Phone Number SDES Item.
*/
SdesItemType[SdesItemType["PHONE"] = 4] = "PHONE";
/**
* Geographic User Location SDES Item.
*/
SdesItemType[SdesItemType["LOC"] = 5] = "LOC";
/**
* Application or Tool Name SDES Item.
*/
SdesItemType[SdesItemType["TOOL"] = 6] = "TOOL";
/**
* Notice/Status SDES Item.
*/
SdesItemType[SdesItemType["NOTE"] = 7] = "NOTE";
/**
* Private Extensions SDES Item.
*/
SdesItemType[SdesItemType["PRIV"] = 8] = "PRIV";
})(SdesItemType || (exports.SdesItemType = SdesItemType = {}));
/**
* RTCP SDES packet.
*
* ```text
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* header |V=2|P| SC | PT=SDES=202 | length |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_1 |
* 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_2 |
* 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* ```
*
* @category RTCP
*
* @see
* - [RFC 3550 section 6.5](https://datatracker.ietf.org/doc/html/rfc3550#section-6.5)
*/
class SdesPacket extends RtcpPacket_1.RtcpPacket {
// SDES Chunks.
#chunks = [];
/**
* @param view - If given it will be parsed. Otherwise an empty RTCP SDES
* packet will be created.
*
* @throws
* - If given `view` does not contain a valid RTCP SDES packet.
*/
constructor(view) {
super(RtcpPacket_1.RtcpPacketType.SDES, view);
if (!this.view) {
this.view = new DataView(new ArrayBuffer(RtcpPacket_1.COMMON_HEADER_LENGTH));
// Write version and packet type.
this.writeCommonHeader();
return;
}
// Position relative to the DataView byte offset.
let pos = 0;
// Move to chunks.
pos += RtcpPacket_1.COMMON_HEADER_LENGTH;
let count = this.getCount();
while (count-- > 0) {
const chunkPos = pos;
let chunkLength = 0;
// First 4 bytes contain the chunk's ssrc.
pos += 4;
chunkLength += 4;
// Read the length of all items in this chunk until we find an item with
// type 0.
while (pos < this.view.byteLength - this.padding) {
const itemType = this.view.getUint8(pos);
++pos;
++chunkLength;
// Item type 0 means padding (to both indicate end of items and also
// beginning of padding).
if (itemType === 0) {
// Read up to 3 more additional null octests.
let additionalNumNullOctets = 0;
while (pos < this.view.byteLength - this.padding &&
additionalNumNullOctets < 3 &&
this.view.getUint8(pos) === 0) {
++pos;
++chunkLength;
++additionalNumNullOctets;
}
break;
}
const itemLength = this.view.getUint8(pos);
++pos;
++chunkLength;
pos += itemLength;
chunkLength += itemLength;
}
const chunkView = new DataView(this.view.buffer, this.view.byteOffset + chunkPos, chunkLength);
const chunk = new SdesChunk(chunkView);
this.#chunks.push(chunk);
}
if (this.#chunks.length !== this.getCount()) {
throw new RangeError(`num of parsed SDES Chunks (${this.#chunks.length}) doesn't match RTCP count field ({${this.getCount()}})`);
}
pos += this.padding;
// Ensure that view length and parsed length match.
if (pos !== this.view.byteLength) {
throw new RangeError(`parsed length (${pos} bytes) does not match view length (${this.view.byteLength} bytes)`);
}
}
/**
* Dump Receiver Report packet info.
*/
dump() {
return {
...super.dump(),
chunks: this.#chunks.map(chunk => chunk.dump()),
};
}
/**
* @inheritDoc
*/
getByteLength() {
if (!this.needsSerialization()) {
return this.view.byteLength;
}
const packetLength = RtcpPacket_1.COMMON_HEADER_LENGTH +
this.#chunks.reduce((sum, chunk) => sum + chunk.getByteLength(), 0) +
this.padding;
return packetLength;
}
/**
* @inheritDoc
*/
needsSerialization() {
return (super.needsSerialization() ||
this.#chunks.some(chunk => chunk.needsSerialization()));
}
/**
* @inheritDoc
*/
serialize(buffer, byteOffset) {
const view = this.serializeBase(buffer, byteOffset);
// Position relative to the DataView byte offset.
let pos = 0;
// Move to chunks.
pos += RtcpPacket_1.COMMON_HEADER_LENGTH;
// Write chunks.
for (const chunk of this.#chunks) {
chunk.serialize(view.buffer, view.byteOffset + pos);
pos += chunk.getByteLength();
}
pos += this.padding;
// Assert that current position is equal than new buffer length.
if (pos !== view.byteLength) {
throw new RangeError(`filled length (${pos} bytes) is different than the available buffer size (${view.byteLength} bytes)`);
}
// Update DataView.
this.view = view;
this.setSerializationNeeded(false);
}
/**
* @inheritDoc
*/
clone(buffer, byteOffset, serializationBuffer, serializationByteOffset) {
const view = this.cloneInternal(buffer, byteOffset, serializationBuffer, serializationByteOffset);
return new SdesPacket(view);
}
/**
* Get SDES Chunks.
*/
getChunks() {
return Array.from(this.#chunks);
}
/**
* Set SDES Chunks.
*
* @remarks
* - Serialization is needed after calling this method.
*/
setChunks(chunks) {
this.#chunks = Array.from(chunks);
// Update RTCP count.
this.setCount(this.#chunks.length);
this.setSerializationNeeded(true);
}
/**
* Add SDES Chunk.
*
* @remarks
* - Serialization is needed after calling this method.
*/
addChunk(chunk) {
this.#chunks.push(chunk);
// Update RTCP count.
this.setCount(this.#chunks.length);
this.setSerializationNeeded(true);
}
}
exports.SdesPacket = SdesPacket;
/**
* SDES Chunk.
*
* @category RTCP
*/
class SdesChunk extends Serializable_1.Serializable {
// SDES Items indexed by type with text as value.
#items = [];
/**
* @param view - If given it will be parsed. Otherwise an empty RTCP SDES
* Chunk will be created.
*/
constructor(view) {
super(view);
if (!this.view) {
this.view = new DataView(new ArrayBuffer(SDES_CHUNK_MIN_LENGTH));
return;
}
if (this.view.byteLength < SDES_CHUNK_MIN_LENGTH) {
throw new TypeError('wrong byte length for a SDES Chunk');
}
else if (this.view.byteLength % 4 !== 0) {
throw new RangeError(`SDES Chunk length must be multiple of 4 bytes but it is ${this.view.byteLength} bytes`);
}
// Position relative to the DataView byte offset.
let pos = 0;
// Move to items.
pos += 4;
while (pos < this.view.byteLength) {
const itemType = this.view.getUint8(pos);
// NOTE: Don't increase pos here since we don't want it increased if 0.
// Item type 0 means padding.
if (itemType === 0) {
break;
}
// So increase it here.
++pos;
const itemLength = this.view.getUint8(pos);
++pos;
const itemView = new DataView(this.view.buffer, this.view.byteOffset + pos, itemLength);
pos += itemLength;
this.#items.push({ type: itemType, text: (0, helpers_1.dataViewToString)(itemView) });
}
// There must be a null octet at the end of the items and up to 3 more null
// octets to pad the chunk to 4 bytes.
const numNullOctets = this.view.byteLength - pos;
if (numNullOctets < 1 || numNullOctets > 4) {
throw new RangeError(`SDES Chunk has wrong number of null octests at the end (${numNullOctets} null octets)`);
}
}
/**
* Dump SDES Chunk info.
*/
dump() {
return {
...super.dump(),
ssrc: this.getSsrc(),
items: this.getItems(),
};
}
/**
* @inheritDoc
*/
getByteLength() {
if (!this.needsSerialization()) {
return this.view.byteLength;
}
// SSRC (4 bytes).
let chunkLength = 4;
chunkLength += this.#items.reduce((sum, { text }) => {
// Item type field + item length field + text length.
return sum + 2 + (0, helpers_1.getStringByteLength)(text);
}, 0);
// The list of items in each chunk MUST be terminated by one or more null
// octets, so add a byte to hold a null byte.
++chunkLength;
// Each chunk must be padded to 4 bytes.
chunkLength = (0, helpers_1.padTo4Bytes)(chunkLength);
return chunkLength;
}
/**
* @inheritDoc
*/
serialize(buffer, byteOffset) {
const bufferData = this.getSerializationBuffer(buffer, byteOffset);
// Create new DataView with new buffer.
const view = new DataView(bufferData.buffer, bufferData.byteOffset, bufferData.byteLength);
const uint8Array = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
let pos = 0;
// Copy the SSRC.
view.setUint32(pos, this.getSsrc());
// Move to items.
pos += 4;
for (const { type, text } of this.#items) {
const itemUint8Array = (0, helpers_1.stringToUint8Array)(text);
view.setUint8(pos, type);
view.setUint8(pos + 1, itemUint8Array.byteLength);
pos += 2;
uint8Array.set(itemUint8Array, pos);
pos += itemUint8Array.byteLength;
}
// NOTE: No need to care about chunk padding since the obtained buffer
// has the proper size (multiple of 4 bytes) and is filled with zeroes.
// Update DataView.
this.view = view;
this.setSerializationNeeded(false);
}
/**
* @inheritDoc
*/
clone(buffer, byteOffset, serializationBuffer, serializationByteOffset) {
const view = this.cloneInternal(buffer, byteOffset, serializationBuffer, serializationByteOffset);
return new SdesChunk(view);
}
/**
* Get SDES Chunk SSRC.
*/
getSsrc() {
return this.view.getUint32(0);
}
/**
* Set SDES Chunk SSRC.
*/
setSsrc(ssrc) {
this.view.setUint32(0, ssrc);
this.setSerializationNeeded(true);
}
/**
* Get SDES Items.
*/
getItems() {
return Array.from(this.#items);
}
/**
* Set SDES Items.
*/
setItems(items) {
this.#items = Array.from(items);
this.setSerializationNeeded(true);
}
/**
* Add SDES Item.
*/
addItem(type, text) {
this.#items.push({ type, text });
this.setSerializationNeeded(true);
}
}
exports.SdesChunk = SdesChunk;