UNPKG

rtp.js

Version:

RTP stack for Node.js and browser written in TypeScript

967 lines (966 loc) 36.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RtpPacket = void 0; exports.isRtp = isRtp; const Packet_1 = require("../Packet"); const rtpExtensions_1 = require("./rtpExtensions"); const helpers_1 = require("../../utils/helpers"); const bitOps_1 = require("../../utils/bitOps"); const byteOps_1 = require("../../utils/byteOps"); const FIXED_HEADER_LENGTH = 12; /** * Whether the given buffer view could be a valid RTP packet or not. * * @category RTP */ function isRtp(view) { const firstByte = view.getUint8(0); return (view.byteLength >= FIXED_HEADER_LENGTH && // DOC: https://tools.ietf.org/html/draft-ietf-avtcore-rfc5764-mux-fixes firstByte > 127 && firstByte < 192 && // RTP Version must be 2. firstByte >> 6 === Packet_1.RTP_VERSION); } /** * RTP 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 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P|X| CC |M| PT | sequence number | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | timestamp | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | synchronization source (SSRC) identifier | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | contributing source (CSRC) identifiers | * | .... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | defined by profile | length | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | header extension | * | .... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * ``` * * @category RTP * * @see * - [RFC 3550 section 5.1](https://datatracker.ietf.org/doc/html/rfc3550#section-5.1) * - [RFC 5285 section 4](https://datatracker.ietf.org/doc/html/rfc5285#section-4) */ class RtpPacket extends Packet_1.Packet { // CSRCs. #csrcs = []; // Header extension id. #headerExtensionId; // Buffer view holding the header extension value. Only if One-Byte or Two-Bytes // extensions are used. #headerExtensionView; // One-Byte or Two-Bytes extensions indexed by id. #extensions = new Map(); // Mapping of RTP extension types and their corresponding RTP extension ids. #extensionMapping = {}; // Buffer view holding the entire RTP payload. #payloadView; /** * @param view - If given it will be parsed. Otherwise an empty RTP packet * (with just the minimal fixed header) will be created. * * @throws * - If `view` is given and it does not contain a valid RTP packet. */ constructor(view) { super(view); if (!this.view) { this.view = new DataView(new ArrayBuffer(FIXED_HEADER_LENGTH)); // Set version. this.setVersion(); // Set empty payload. this.#payloadView = new DataView(this.view.buffer, this.view.byteOffset + FIXED_HEADER_LENGTH, 0); return; } if (!isRtp(this.view)) { throw new TypeError('not a RTP packet'); } // Position relative to the DataView byte offset. let pos = 0; // Move to CSRC field. pos += FIXED_HEADER_LENGTH; let csrcCount = this.getCsrcCount(); while (csrcCount-- > 0) { const csrc = this.view.getUint32(pos); this.#csrcs.push(csrc); pos += 4; } // Parse header extension. const hasHeaderExtension = this.hasHeaderExtensionBit(); let headerExtensionView; if (hasHeaderExtension) { // NOTE: This will throw RangeError if there is no space in the buffer. this.#headerExtensionId = this.view.getUint16(pos); pos += 2; const headerExtensionLength = this.view.getUint16(pos) * 4; pos += 2; headerExtensionView = new DataView(this.view.buffer, this.view.byteOffset + pos, headerExtensionLength); pos += headerExtensionLength; } // Parse One-Byte or Two-Bytes extensions. if (headerExtensionView && this.hasOneByteExtensions()) { let extPos = 0; // One-Byte extensions cannot have length 0. while (extPos < headerExtensionView.byteLength) { const extId = (0, bitOps_1.readBitsInDataView)({ view: headerExtensionView, pos: extPos, mask: 0xf0, }); const extLength = (0, bitOps_1.readBitsInDataView)({ view: headerExtensionView, pos: extPos, mask: 0x0f, }) + 1; // id=15 in One-Byte extensions means "stop parsing here". if (extId === 15) { break; } // Valid extension extId. if (extId !== 0) { if (extPos + 1 + extLength > headerExtensionView.byteLength) { throw new RangeError('not enough space for the announced One-Byte extension value'); } // Store the One-Byte extension element in the map. this.#extensions.set(extId, new DataView(headerExtensionView.buffer, headerExtensionView.byteOffset + extPos + 1, extLength)); extPos += 1 + extLength; } // id=0 means alignment. else { ++extPos; } // Counting padding bytes. while (extPos < headerExtensionView.byteLength && headerExtensionView.getUint8(extPos) === 0) { ++extPos; } } } else if (headerExtensionView && this.hasTwoBytesExtensions()) { let extPos = 0; // Two-Byte extensions can have length 0. while (extPos + 1 < headerExtensionView.byteLength) { const extId = headerExtensionView.getUint8(extPos); const extLength = headerExtensionView.getUint8(extPos + 1); // Valid extension id. if (extId !== 0) { if (extPos + 2 + extLength > headerExtensionView.byteLength) { throw new RangeError('not enough space for the announced Two-Bytes extension value'); } // Store the Two-Bytes extension element in the map. this.#extensions.set(extId, new DataView(headerExtensionView.buffer, headerExtensionView.byteOffset + extPos + 2, extLength)); extPos += 2 + extLength; } // id=0 means alignment. else { ++extPos; } // Counting padding bytes. while (extPos < headerExtensionView.byteLength && headerExtensionView.getUint8(extPos) === 0) { ++extPos; } } } // In case there is an extension header which doesn't contain One-Byte or // Two-Bytes extensions, and only in that case, keep the header extension // value in a member. else if (hasHeaderExtension) { this.#headerExtensionView = headerExtensionView; } // Get padding. if (this.hasPaddingBit()) { // NOTE: This will throw RangeError if there is no space in the view. this.padding = this.view.getUint8(this.view.byteLength - 1); } // Get payload. const payloadLength = this.view.byteLength - pos - this.padding; if (payloadLength < 0) { throw new RangeError(`announced padding (${this.padding} bytes) is bigger than available space for payload (${this.view.byteLength - pos} bytes)`); } this.#payloadView = new DataView(this.view.buffer, this.view.byteOffset + pos, payloadLength); pos += payloadLength + 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 RTP packet info. */ dump() { const extensions = Array.from(this.#extensions).map(([extId, extView]) => { return { id: extId, length: extView.byteLength, }; }); return { ...super.dump(), payloadType: this.getPayloadType(), sequenceNumber: this.getSequenceNumber(), timestamp: this.getTimestamp(), ssrc: this.getSsrc(), csrcs: this.getCsrcs(), marker: this.getMarker(), headerExtensionId: this.#headerExtensionId, extensions: extensions, midExt: this.getMidExtension(), ridExt: this.getRidExtension(), repairedRidExt: this.getRepairedRidExtension(), absSendTimeExt: this.getAbsSendTimeExtension(), transportWideSeqNumberExt: this.getTransportWideSeqNumberExtension(), ssrcAudioLevelExt: this.getSsrcAudioLevelExtension(), videoOrientationExt: this.getVideoOrientationExtension(), transmissionOffsetExt: this.getTransmissionOffsetExtension(), payloadLength: this.getPayload().byteLength, }; } /** * @inheritDoc */ getByteLength() { // NOTE: Here, even if serialization is not needed, we cannot bypass full // length computation since the original and unmodified parsed packet // may have useless padding octets in extensions, octets that we ignore // when computing packet length and when serializing the packet. let packetLength = 0; packetLength += FIXED_HEADER_LENGTH; // Add space for CSRC values. packetLength += this.#csrcs.length * 4; // Add space for the header extension. if (this.hasHeaderExtensionBit()) { // Add space for header extension id/length fields. packetLength += 4; // If the packet has One-Byte or Two-Bytes extensions, compute the size of // the extensions map. Otherwise read the header extension length in the // buffer. if (this.#extensions.size > 0 && this.hasOneByteExtensions()) { for (const [extId, extView] of this.#extensions) { if (extId > 0 && extId < 15) { // Add space for extension id/length fields. packetLength += 1 + extView.byteLength; } } // May need to add padding. packetLength = (0, helpers_1.padTo4Bytes)(packetLength); } else if (this.#extensions.size > 0 && this.hasTwoBytesExtensions()) { for (const [extId, extView] of this.#extensions) { if (extId > 0 && extId < 256) { // Add space for extension id/length fields. packetLength += 2 + extView.byteLength; } } // May need to add padding. packetLength = (0, helpers_1.padTo4Bytes)(packetLength); } // Otherwise read the length of the header extension value. else if (this.#headerExtensionView) { packetLength += this.#headerExtensionView.byteLength; } } // Add space for payload. packetLength += this.#payloadView.byteLength; // Add space for padding. packetLength += this.padding; return packetLength; } /** * @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); // Position relative to the DataView byte offset. let pos = 0; // Copy the fixed header into the new buffer. uint8Array.set(new Uint8Array(this.view.buffer, this.view.byteOffset, FIXED_HEADER_LENGTH), pos); // Move to CSRCs. pos += FIXED_HEADER_LENGTH; // NOTE: Before writing the CSRCs we must store the header extension value // (if any) because otherwise we are writing on top of it. if (this.#headerExtensionView) { this.#headerExtensionView = (0, helpers_1.clone)(this.#headerExtensionView); } // Write CSRCs. for (const csrc of this.#csrcs) { view.setUint32(pos, csrc); pos += 4; } // Write header extension. if (this.hasHeaderExtensionBit()) { view.setUint16(pos, this.#headerExtensionId); // Move to the header extension length field. pos += 2; const extLengthPos = pos; // Move to the header extension value. pos += 2; let extLength = 0; if (this.#extensions.size > 0 && this.hasOneByteExtensions()) { for (const [extId, extView] of this.#extensions) { if (extId <= 0 || extId >= 15) { this.#extensions.delete(extId); continue; } if (extView.byteLength === 0) { throw new TypeError('cannot serialize extensions with length 0 in One-Byte mode'); } else if (extView.byteLength > 16) { throw new RangeError('cannot serialize extensions with length > 16 in One-Byte mode'); } (0, bitOps_1.writeBitsInDataView)({ view: view, pos: pos, mask: 0xf0, value: extId, }); (0, bitOps_1.writeBitsInDataView)({ view: view, pos: pos, mask: 0x0f, value: extView.byteLength - 1, }); pos += 1; extLength += 1; uint8Array.set(new Uint8Array(extView.buffer, extView.byteOffset, extView.byteLength), pos); pos += extView.byteLength; extLength += extView.byteLength; } // May need to add padding. pos = (0, helpers_1.padTo4Bytes)(pos); extLength = (0, helpers_1.padTo4Bytes)(extLength); // Write header extension length. view.setUint16(extLengthPos, extLength / 4); } else if (this.#extensions.size > 0 && this.hasTwoBytesExtensions()) { for (const [extId, extView] of this.#extensions) { if (extId <= 0 || extId >= 256) { this.#extensions.delete(extId); continue; } if (extView.byteLength > 255) { throw new RangeError('cannot serialize extensions with length > 255 in Two-Bytes mode'); } view.setUint8(pos, extId); pos += 1; extLength += 1; view.setUint8(pos, extView.byteLength); pos += 1; extLength += 1; uint8Array.set(new Uint8Array(extView.buffer, extView.byteOffset, extView.byteLength), pos); pos += extView.byteLength; extLength += extView.byteLength; } // May need to add padding. pos = (0, helpers_1.padTo4Bytes)(pos); extLength = (0, helpers_1.padTo4Bytes)(extLength); // Write header extension length. view.setUint16(extLengthPos, extLength / 4); } // Extension header doesn't contain One-Byte or Two-Bytes extensions so // honor the original header extension value (if any). else if (this.#headerExtensionView) { extLength = this.#headerExtensionView.byteLength; // Write header extension value. uint8Array.set(new Uint8Array(this.#headerExtensionView.buffer, this.#headerExtensionView.byteOffset, this.#headerExtensionView.byteLength), pos); pos += this.#headerExtensionView.byteLength; // Write header extension length. view.setUint16(extLengthPos, extLength / 4); } } // Write payload. uint8Array.set(new Uint8Array(this.#payloadView.buffer, this.#payloadView.byteOffset, this.#payloadView.byteLength), pos); // Create new payload DataView. const payloadView = new DataView(view.buffer, view.byteOffset + pos, this.#payloadView.byteLength); pos += payloadView.byteLength; // Write padding. if (this.padding > 0) { if (this.padding > 255) { throw new TypeError(`padding (${this.padding} bytes) cannot be higher than 255`); } view.setUint8(pos + this.padding - 1, this.padding); pos += this.padding; } // Assert that current position is equal or less than new buffer length. // NOTE: Don't be strict matching resulting length since we may have // discarded/reduced some padding/alignment octets in the extensions // during the process. if (pos > view.byteLength) { throw new RangeError(`filled length (${pos} bytes) is bigger than the available buffer size (${view.byteLength} bytes)`); } // Update DataView. this.view = view; // Update payload DataView. this.#payloadView = payloadView; this.setSerializationNeeded(false); } /** * @inheritDoc */ clone(buffer, byteOffset, serializationBuffer, serializationByteOffset) { const view = this.cloneInternal(buffer, byteOffset, serializationBuffer, serializationByteOffset); const clonedPacket = new RtpPacket(view); clonedPacket.setExtensionMapping(this.getExtensionMapping()); return clonedPacket; } /** * Get the RTP payload type. */ getPayloadType() { return (0, bitOps_1.readBitsInDataView)({ view: this.view, pos: 1, mask: 0b01111111 }); } /** * Set the RTP payload type. */ setPayloadType(payloadType) { (0, bitOps_1.writeBitsInDataView)({ view: this.view, pos: 1, mask: 0b01111111, value: payloadType, }); } /** * Get the RTP sequence number. */ getSequenceNumber() { return this.view.getUint16(2); } /** * Set the RTP sequence number. */ setSequenceNumber(sequenceNumber) { this.view.setUint16(2, sequenceNumber); } /** * Get the RTP timestamp. */ getTimestamp() { return this.view.getUint32(4); } /** * Set the RTP timestamp. */ setTimestamp(timestamp) { this.view.setUint32(4, timestamp); } /** * Get the RTP SSRC. */ getSsrc() { return this.view.getUint32(8); } /** * Set the RTP SSRC. */ setSsrc(ssrc) { this.view.setUint32(8, ssrc); } /** * Get the RTP CSRC values. */ getCsrcs() { return Array.from(this.#csrcs); } /** * Set the RTP CSRC values. If `csrcs` is not given (or if it's an empty * array) CSRC field will be removed from the RTP packet. * * @remarks * - Serialization is needed after calling this method. */ setCsrcs(csrcs = []) { this.#csrcs = Array.from(csrcs); // Update CSRC count. this.setCsrcCount(this.#csrcs.length); this.setSerializationNeeded(true); } /** * Get the RTP marker flag. */ getMarker() { return (0, bitOps_1.readBitInDataView)({ view: this.view, pos: 1, bit: 7 }); } /** * Set the RTP marker flag. */ setMarker(flag) { (0, bitOps_1.writeBitInDataView)({ view: this.view, pos: 1, bit: 7, flag }); } /** * Whether One-Byte extensions (as per RFC 5285) are enabled. */ hasOneByteExtensions() { return this.#headerExtensionId === 0xbede; } /** * Whether Two-Bytes extensions (as per RFC 5285) are enabled. */ hasTwoBytesExtensions() { return this.#headerExtensionId !== undefined ? (this.#headerExtensionId & 0b1111111111110000) === 0b0001000000000000 : false; } /** * Enable One-Byte extensions (RFC 5285). * * @remarks * - Serialization maybe needed after calling this method. */ enableOneByteExtensions() { if (this.hasOneByteExtensions()) { return; } this.#headerExtensionId = 0xbede; this.#headerExtensionView = undefined; this.setSerializationNeeded(true); } /** * Enable Two-Bytes extensions (RFC 5285). * * @remarks * - Serialization maybe needed after calling this method. */ enableTwoBytesExtensions() { if (this.hasTwoBytesExtensions()) { return; } this.#headerExtensionId = 0b0001000000000000; this.#headerExtensionView = undefined; this.setSerializationNeeded(true); } /** * Get a map with all the extensions indexed by their extension id (RFC 5285). */ getExtensions() { return new Map(this.#extensions); } /** * Get the value of the extension with given id (RFC 5285). */ getExtension(id) { return this.#extensions.get(id); } /** * Set the value of the extension with given id (RFC 5285). * * @remarks * - Serialization is needed after calling this method. */ setExtension(id, value) { // Update header extension bit if needed. if (this.#extensions.size === 0) { this.setHeaderExtensionBit(true); // If neither One-Byte nor Two-Bytes modes are enabled, force One-Byte. if (!this.hasOneByteExtensions() && !this.hasTwoBytesExtensions()) { this.enableOneByteExtensions(); } } this.#extensions.set(id, value); this.setSerializationNeeded(true); } /** * Delete the extension with given id (RFC 5285). * * @remarks * - Serialization maybe needed after calling this method. */ deleteExtension(id) { if (!this.#extensions.delete(id)) { return; } // Update header extension bit if needed. if (this.#extensions.size === 0) { this.setHeaderExtensionBit(false); } this.setSerializationNeeded(true); } /** * Clear all extensions (RFC 5285). * * @remarks * - Serialization maybe needed after calling this method. */ clearExtensions() { if (this.#extensions.size === 0) { return; } this.#extensions.clear(); // Update header extension bit. this.setHeaderExtensionBit(false); this.setSerializationNeeded(true); } /** * Get RTP extension mapping (association of RTP extension types and their * numeric ids in this RTP packet). */ getExtensionMapping() { return (0, helpers_1.clone)(this.#extensionMapping); } /** * Set RTP extension mapping (association of RTP extension types and their * numeric ids in this RTP packet). * * @remarks * - Calling this method is needed before using other methods that read or * write specific RTP extensions. */ setExtensionMapping(extensionMapping) { this.#extensionMapping = (0, helpers_1.clone)(extensionMapping); } /** * Read the value of the {@link RtpExtensionType.MID} RTP extension. */ getMidExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.MID]); if (!view) { return; } return (0, helpers_1.dataViewToString)(view); } /** * Set the value of the {@link RtpExtensionType.MID} RTP extension. */ setMidExtension(mid) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.MID]; if (!extId) { return; } if (mid) { this.setExtension(extId, (0, helpers_1.stringToDataView)(mid)); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.RTP_STREAM_ID} RTP * extension. */ getRidExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.RTP_STREAM_ID]); if (!view) { return; } return (0, helpers_1.dataViewToString)(view); } /** * Set the value of the {@link RtpExtensionType.RTP_STREAM_ID} RTP * extension. */ setRidExtension(rid) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.RTP_STREAM_ID]; if (!extId) { return; } if (rid) { this.setExtension(extId, (0, helpers_1.stringToDataView)(rid)); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.RTP_REPAIRED_STREAM_ID} RTP * extension. */ getRepairedRidExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.RTP_REPAIRED_STREAM_ID]); if (!view) { return; } return (0, helpers_1.dataViewToString)(view); } /** * Set the value of the {@link RtpExtensionType.RTP_REPAIRED_STREAM_ID} RTP * extension. */ setRepairedRidExtension(rrid) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.RTP_REPAIRED_STREAM_ID]; if (!extId) { return; } if (rrid) { this.setExtension(extId, (0, helpers_1.stringToDataView)(rrid)); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.ABS_SEND_TIME} RTP * extension. * * @remarks * - Returned value is "Absolute Send Time" format. See * {@link timeMsToAbsSendTime}. */ getAbsSendTimeExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.ABS_SEND_TIME]); if (!view) { return; } return (0, byteOps_1.read3BytesInDataView)({ view, pos: 0 }); } /** * Set the value of the {@link RtpExtensionType.ABS_SEND_TIME} RTP * extension. * * @remarks * - Given `absSendTime` must be in "Absolute Send Time" format. See * {@link timeMsToAbsSendTime}. */ setAbsSendTimeExtension(absSendTime) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.ABS_SEND_TIME]; if (!extId) { return; } if (absSendTime !== undefined) { const view = new DataView(new ArrayBuffer(3)); (0, byteOps_1.write3BytesInDataView)({ view, pos: 0, value: absSendTime }); this.setExtension(extId, view); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.TRANSPORT_WIDE_SEQ_NUMBER} * RTP extension. */ getTransportWideSeqNumberExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.TRANSPORT_WIDE_SEQ_NUMBER]); if (!view) { return; } return view.getUint16(0); } /** * Set the value of the {@link RtpExtensionType.TRANSPORT_WIDE_SEQ_NUMBER} * RTP extension. */ setTransportWideSeqNumberExtension(sequenceNumber) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.TRANSPORT_WIDE_SEQ_NUMBER]; if (!extId) { return; } if (sequenceNumber !== undefined) { this.setExtension(extId, (0, helpers_1.numberToDataView)(sequenceNumber)); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.SSRC_AUDIO_LEVEL} RTP * extension. */ getSsrcAudioLevelExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.SSRC_AUDIO_LEVEL]); if (!view) { return; } const voice = (0, bitOps_1.readBitInDataView)({ view, pos: 0, bit: 7 }); const volume = (0, bitOps_1.readBitsInDataView)({ view, pos: 0, mask: 0b01111111 }); return { volume, voice }; } /** * Set the value of the {@link RtpExtensionType.SSRC_AUDIO_LEVEL} RTP * extension. */ setSsrcAudioLevelExtension(ssrcAudioLevel) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.SSRC_AUDIO_LEVEL]; if (!extId) { return; } if (ssrcAudioLevel) { const view = new DataView(new ArrayBuffer(1)); (0, bitOps_1.writeBitInDataView)({ view, pos: 0, bit: 7, flag: ssrcAudioLevel.voice }); (0, bitOps_1.writeBitsInDataView)({ view, pos: 0, mask: 0b01111111, value: ssrcAudioLevel.volume, }); this.setExtension(extId, view); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.VIDEO_ORIENTATION} RTP * extension. */ getVideoOrientationExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.VIDEO_ORIENTATION]); if (!view) { return; } const camera = (0, bitOps_1.readBitInDataView)({ view, pos: 0, bit: 3 }); const flip = (0, bitOps_1.readBitInDataView)({ view, pos: 0, bit: 2 }); const rotation = (0, bitOps_1.readBitsInDataView)({ view, pos: 0, mask: 0b00000011 }); return { camera, flip, rotation }; } /** * Set the value of the {@link RtpExtensionType.VIDEO_ORIENTATION} RTP * extension. */ setVideoOrientationExtension(videoOrientation) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.VIDEO_ORIENTATION]; if (!extId) { return; } if (videoOrientation) { const view = new DataView(new ArrayBuffer(1)); (0, bitOps_1.writeBitInDataView)({ view, pos: 0, bit: 3, flag: videoOrientation.camera, }); (0, bitOps_1.writeBitInDataView)({ view, pos: 0, bit: 2, flag: videoOrientation.flip }); (0, bitOps_1.writeBitsInDataView)({ view, pos: 0, mask: 0b00000011, value: videoOrientation.rotation, }); this.setExtension(extId, view); } else { this.deleteExtension(extId); } } /** * Read the value of the {@link RtpExtensionType.TOFFSET} RTP extension. */ getTransmissionOffsetExtension() { const view = this.getExtension(this.#extensionMapping[rtpExtensions_1.RtpExtensionType.TOFFSET]); if (!view) { return; } return (0, byteOps_1.read3BytesInDataView)({ view, pos: 0 }); } /** * Set the value of the {@link RtpExtensionType.TOFFSET} RTP extension. */ setTransmissionOffsetExtension(offset) { const extId = this.#extensionMapping[rtpExtensions_1.RtpExtensionType.TOFFSET]; if (!extId) { return; } if (offset !== undefined) { const view = new DataView(new ArrayBuffer(3)); (0, byteOps_1.write3BytesInDataView)({ view, pos: 0, value: offset }); this.setExtension(extId, view); } else { this.deleteExtension(extId); } } /** * Get the packet payload. */ getPayload() { return this.#payloadView; } /** * Set the packet payload. * * @remarks * - Serialization is needed after calling this method. */ setPayload(view) { this.#payloadView = view; this.setSerializationNeeded(true); } /** * Encode the packet using RTX procedures (as per RFC 4588). * * @param payloadType - The RTX payload type. * @param ssrc - The RTX SSRC. * @param sequenceNumber - The RTX sequence number. * * @remarks * - Serialization is needed after calling this method. */ rtxEncode(payloadType, ssrc, sequenceNumber) { // Rewrite the payload type. this.setPayloadType(payloadType); // Rewrite the SSRC. this.setSsrc(ssrc); const payloadView = new DataView(new ArrayBuffer(2 + this.#payloadView.byteLength)); const payloadUint8Array = new Uint8Array(payloadView.buffer, payloadView.byteOffset, payloadView.byteLength); // Write the original sequence number at the begining of the new payload. payloadView.setUint16(0, this.getSequenceNumber()); // Copy the original payload after the sequence number. payloadUint8Array.set(new Uint8Array(this.#payloadView.buffer, this.#payloadView.byteOffset, this.#payloadView.byteLength), 2); this.#payloadView = payloadView; // Rewrite the sequence number. this.setSequenceNumber(sequenceNumber); // Remove padding. this.setPadding(0); this.setSerializationNeeded(true); } /** * Decode the packet using RTX procedures (as per RFC 4588). * * @param payloadType - The original payload type. * @param ssrc - The original SSRC. * * @remarks * - Serialization is needed after calling this method. * * @throws * - If payload length is less than 2 bytes, so RTX decode is not possible. */ rtxDecode(payloadType, ssrc) { if (this.#payloadView.byteLength < 2) { throw new RangeError('payload length must be greater or equal than 2 bytes'); } // Rewrite the payload type. this.setPayloadType(payloadType); // Rewrite the SSRC. this.setSsrc(ssrc); // Rewrite the sequence number. const sequenceNumber = this.#payloadView.getUint16(0); this.setSequenceNumber(sequenceNumber); // Reduce the payload. this.setPayload(new DataView(this.#payloadView.buffer, this.#payloadView.byteOffset + 2)); // Remove padding. this.setPadding(0); this.setSerializationNeeded(true); } hasHeaderExtensionBit() { return (0, bitOps_1.readBitInDataView)({ view: this.view, pos: 0, bit: 4 }); } setHeaderExtensionBit(flag) { (0, bitOps_1.writeBitInDataView)({ view: this.view, pos: 0, bit: 4, flag }); } getCsrcCount() { return this.view.getUint8(0) & 0x0f; } setCsrcCount(count) { (0, bitOps_1.writeBitsInDataView)({ view: this.view, pos: 0, mask: 0b00001111, value: count, }); } } exports.RtpPacket = RtpPacket;