All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.udp.RTPPacket.kt Maven / Gradle / Ivy

package dev.kord.voice.udp

import dev.kord.voice.io.ByteArrayView
import dev.kord.voice.io.MutableByteArrayCursor
import dev.kord.voice.io.mutableCursor
import dev.kord.voice.io.view
import io.ktor.utils.io.core.*
import kotlin.experimental.and

internal const val RTP_HEADER_LENGTH = 12

/**
 * Originally from [this GitHub library](https://github.com/vidtec/rtp-packet/blob/0b54fdeab5666089215b0074c64a6735b8937f8d/src/main/java/org/vidtec/rfc3550/rtp/RTPPacket.java).
 *
 * Ported to Kotlin.
 *
 * Contains modifications to support RTP packets from Discord as they do not adhere to the standard RTP spec.
 *
 * Some changes include
 * - Timestamps are stored as UInt (4 bytes) not long.
 * - Sequences are stored as UShort (2 bytes) not long.
 * - Payload Types are stored as UByte (1 byte) not short.
 * - SSRCs are stored as UInt (4 bytes) not long.
 * - And most notably, the extension header is not exactly after the RTP header. Discord instead encrypts it along with the payload...
 */
@OptIn(ExperimentalUnsignedTypes::class)
public data class RTPPacket(
    val paddingBytes: UByte,
    val payloadType: Byte,
    val sequence: UShort,
    val timestamp: UInt,
    val ssrc: UInt,
    val csrcIdentifiers: UIntArray,
    val hasMarker: Boolean,
    val hasExtension: Boolean,
    val payload: ByteArrayView,
) {
    public companion object {
        internal const val VERSION = 2

        public fun fromPacket(packet: ByteReadPacket): RTPPacket? = with(packet) base@{
            if (remaining <= 13) return@base null

            /*
             * first byte | bit table
             * 0 = version
             * 1 = padding bit
             * 2 = extension bit
             * 3-7 = csrc count
             */
            val hasPadding: Boolean
            val hasExtension: Boolean
            val csrcCount: Byte
            with(readByte()) {
                // invalid rtp version
                if ((toInt() and 0xC0) != VERSION shl 6) return@base null

                hasPadding = (toInt() and 0x20) == 0x20
                hasExtension = (toInt() and 0x10) == 0x10
                csrcCount = this and 0x0F
            }

            /*
             * second byte | bit table
             * 0 = marker
             * 1-7 = payload type
             */
            val marker: Boolean
            val payloadType: Byte
            with(readByte()) {
                marker = (this and 0x80.toByte()) == 0x80.toByte()
                payloadType = this and 0x7F
            }

            val sequence = readShort().toUShort()
            val timestamp = readInt().toUInt()
            val ssrc = readInt().toUInt()

            // each csrc takes up 4 bytes, plus more data is required
            if (remaining <= csrcCount * 4 + 1) return@base null
            val csrcIdentifiers = UIntArray(csrcCount.toInt()) { readUInt() }

            val payload = readBytes().view()

            val paddingBytes = if (hasPadding) { payload[payload.viewSize - 1] } else 0

            payload.resize(end = payload.dataStart + payload.viewSize - paddingBytes)

            RTPPacket(
                paddingBytes.toUByte(),
                payloadType,
                sequence,
                timestamp,
                ssrc,
                csrcIdentifiers,
                marker,
                hasExtension,
                payload
            )
        }
    }

    public fun clone(): RTPPacket {
        return RTPPacket(
            paddingBytes,
            payloadType,
            sequence,
            timestamp,
            ssrc,
            csrcIdentifiers.copyOf(),
            hasMarker,
            hasExtension,
            payload.toByteArray().view()
        )
    }

    public fun writeHeader(): ByteArray {
        val buffer = ByteArray(12)
        writeHeader(buffer.mutableCursor())
        return buffer
    }

    public fun writeHeader(buffer: MutableByteArrayCursor): Unit = with(buffer) {
        resize(cursor + RTP_HEADER_LENGTH)

        val hasPadding = if (paddingBytes > 0u) 0x20 else 0x00
        val hasExtension = if (hasExtension) 0x10 else 0x0
        writeByte(((VERSION shl 6) or (hasPadding) or (hasExtension)).toByte())
        writeByte(payloadType)
        writeShort(sequence.toShort())
        writeInt(timestamp.toInt())
        writeInt(ssrc.toInt())
    }

    public fun asByteArray(): ByteArray {
        val buffer = ByteArray(RTP_HEADER_LENGTH + payload.viewSize + paddingBytes.toInt())
        asByteArrayView(buffer.mutableCursor())
        return buffer
    }

    public fun asByteArrayView(buffer: MutableByteArrayCursor): ByteArrayView = with(buffer) {
        resize(cursor + (RTP_HEADER_LENGTH + payload.viewSize + paddingBytes.toInt()))

        val initial = cursor

        // header
        writeHeader(buffer)

        // payload
        writeByteView(payload)

        if (paddingBytes > 0u) {
            writeByteArray(ByteArray(paddingBytes.toInt() - 1) { 0x0 })
            writeByte(paddingBytes.toByte())
        }

        data.view(initial, cursor)!!
    }

    public class Builder(
        public var ssrc: UInt,
        public var timestamp: UInt,
        public var sequence: UShort,
        public var payloadType: Byte,
        public var payload: ByteArray
    ) {
        public var marker: Boolean = false
        public var paddingBytes: UByte = 0u
        public var hasExtension: Boolean = false
        public var csrcIdentifiers: UIntArray = uintArrayOf()

        public fun build(): RTPPacket = RTPPacket(
            paddingBytes,
            payloadType,
            sequence,
            timestamp,
            ssrc,
            csrcIdentifiers,
            marker,
            hasExtension,
            payload.view()
        )
    }
}

public fun RTPPacket(
    ssrc: UInt,
    timestamp: UInt,
    sequence: UShort,
    payloadType: Byte,
    payload: ByteArray,
    builder: RTPPacket.Builder.() -> Unit = { }
): RTPPacket = RTPPacket.Builder(ssrc, timestamp, sequence, payloadType, payload).apply(builder).build()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy