snapshot of https://github.com/jitsi/jitsi-videobridge.git @ fcbf741c646bc237d2ed85a1e86910532532b831 (2026-09-15)
Some checks failed
Java CI with Maven / Java 17 (push) Failing after 7m2s
Java CI with Maven / Java 21 (push) Failing after 5m0s

This commit is contained in:
upstream-mirror
2026-09-15 16:12:38 +05:00
commit e1257b7a05
647 changed files with 110946 additions and 0 deletions

21
rtp/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Jitsi RTP
Jitsi RTP contains classes for parsing and creating RTP and RTCP packets.
# Code style
We use [ktlint](https://pinterest.github.io/ktlint/) for linting and autoformatting. It runs as part of
`mvn verify` (the build fails on style violations), and the version used is pinned by `ktlint.version` in the
root `pom.xml`, so no separate installation is needed.
To run only the check, or to autoformat, run these in this module's directory:
```
mvn exec:exec@ktlint-check
mvn exec:exec@ktlint-format
```
If you also want the `ktlint` command-line tool (for editor integration or a git hook), install a version matching
`ktlint.version`, e.g. on macOS with Homebrew:
```
brew install ktlint
```
It can then be run directly with `ktlint` (or `ktlint -F` to autoformat), install a pre-commit hook with
`ktlint installGitPreCommitHook`, and configure IntelliJ IDEA with `ktlint applyToIDEAProject`.

313
rtp/pom.xml Normal file
View File

@@ -0,0 +1,313 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jitsi</groupId>
<artifactId>jvb-parent</artifactId>
<version>2.3-SNAPSHOT</version>
</parent>
<groupId>org.jitsi</groupId>
<artifactId>rtp</artifactId>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jitsi</groupId>
<artifactId>jitsi-utils</artifactId>
<version>${jitsi.utils.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.spotbugs/spotbugs-annotations -->
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>${spotbugs.version}</version>
</dependency>
<!--Testing dependencies-->
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-runner-junit5-jvm</artifactId>
<version>${kotest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-assertions-core-jvm</artifactId>
<version>${kotest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.pkts</groupId>
<artifactId>pkts-core</artifactId>
<version>3.0.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals> <goal>compile</goal> </goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<goals> <goal>test-compile</goal> </goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>17</jvmTarget>
</configuration>
</plugin>
<plugin>
<!-- To try and get both java and kotlin sources in the sources jar -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.basedir}/src/main/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven-source-plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
<executions>
<!-- Replacing default-compile as it is treated specially by maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals> <goal>compile</goal> </goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals> <goal>testCompile</goal> </goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<kotest.framework.classpath.scanning.autoscan.disable>true</kotest.framework.classpath.scanning.autoscan.disable>
</systemPropertyVariables>
</configuration>
</plugin>
<!-- ktlint: check runs in the verify phase; auto-format with "mvn exec:exec@ktlint-format" -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<dependencies>
<dependency>
<groupId>com.pinterest.ktlint</groupId>
<artifactId>ktlint-cli</artifactId>
<version>${ktlint.version}</version>
<classifier>all</classifier>
</dependency>
</dependencies>
<executions>
<execution>
<id>ktlint-check</id>
<phase>verify</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>java</executable>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>-classpath</argument>
<classpath>
<dependency>com.pinterest.ktlint:ktlint-cli</dependency>
</classpath>
<argument>com.pinterest.ktlint.Main</argument>
<argument>--relative</argument>
<argument>src/main/kotlin/**/*.kt</argument>
<argument>src/test/kotlin/**/*.kt</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>ktlint-format</id>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>java</executable>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<argument>-classpath</argument>
<classpath>
<dependency>com.pinterest.ktlint:ktlint-cli</dependency>
</classpath>
<argument>com.pinterest.ktlint.Main</argument>
<argument>--format</argument>
<argument>--relative</argument>
<argument>src/main/kotlin/**/*.kt</argument>
<argument>src/test/kotlin/**/*.kt</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs-maven-plugin.version}</version>
<configuration>
<excludeFilterFile>${project.basedir}/spotbugs-exclude.xml</excludeFilterFile>
<includeTests>true</includeTests>
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals><goal>check</goal></goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs</artifactId>
<version>${spotbugs.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitsi-maven-repository-releases</id>
<layout>default</layout>
<name>Jitsi Maven Repository (Releases)</name>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<url>https://github.com/jitsi/jitsi-maven-repository/raw/master/releases/</url>
</repository>
<repository>
<id>jitsi-maven-repository-snapshots</id>
<layout>default</layout>
<name>Jitsi Maven Repository (Snapshots)</name>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<url>https://github.com/jitsi/jitsi-maven-repository/raw/master/snapshots/</url>
</repository>
</repositories>
<profiles>
<profile>
<id>coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- attached to Maven test phase -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

49
rtp/spotbugs-exclude.xml Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<Match>
<Or>
<!-- Our Packet hierarchy intentionally uses clone() without calling super.clone(). -->
<Bug pattern="CN_IDIOM_NO_SUPER_CALL"/>
<!-- This seems to get triggered by certain kotlin constructs though they look safe -->
<Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/>
<!-- Serialization -->
<Bug pattern="SE_BAD_FIELD"/>
<!-- Serialization -->
<Bug pattern="SE_BAD_FIELD_STORE"/>
<!-- False positives with lazy vals -->
<Bug pattern="NP_NULL_ON_SOME_PATH"/>
<Bug pattern="BC_IMPOSSIBLE_INSTANCEOF"/>
<!-- False positives with kotlin companions -->
<Bug pattern="NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
<!-- Reference exposure, our implementation deliberately uses this. -->
<!-- False positives with Kotlin smart casts to non-null -->
<Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
<Bug pattern="EI_EXPOSE_REP"/>
<Bug pattern="EI_EXPOSE_REP2"/>
<!-- We throw from constructors on illegal arguments, i.e. programming errors. -->
<Bug pattern="CT_CONSTRUCTOR_THROW"/>
<!-- False positives for an unknown reason -->
<Bug pattern="MS_EXPOSE_REP"/>
</Or>
</Match>
<Match>
<Or>
<!-- Tests have a valid reason to ignore return values (expecting exceptions, etc). -->
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
<!-- False positives with kotlin 1.5 -->
<Bug pattern="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE"/>
<!-- Tests are often testing whether a value is not null, and should throw NPE if not -->
<Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
</Or>
<Or>
<Source name="~.*\.*Test.kt" />
<Source name="~.*\.*Test.java" />
</Or>
</Match>
</FindBugsFilter>

View File

@@ -0,0 +1,171 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.jitsi.rtp.util.*;
import org.jitsi.utils.*;
//TODO documentation
@SuppressFBWarnings({"PA_PUBLIC_PRIMITIVE_ATTRIBUTE"})
public abstract class ByteArrayBufferImpl
implements ByteArrayBuffer
{
public byte[] buffer;
public int offset;
public int length;
public ByteArrayBufferImpl(byte[] buffer, int offset, int length)
{
this.buffer = buffer;
this.offset = offset;
this.length = length;
}
public ByteArrayBufferImpl()
{
this.buffer = new byte[0];
this.offset = 0;
this.length = 0;
}
public byte[] getBuffer()
{
return buffer;
}
public int getOffset()
{
return offset;
}
public void setOffset(int offset)
{
//TODO check bounds?
this.offset = offset;
}
public int getLength()
{
return length;
}
public void setLength(int length)
{
//TODO check bounds?
this.length = length;
}
public void setOffsetLength(int offset, int length)
{
//TODO check bounds?
this.offset = offset;
this.length = length;
}
public void readRegionToBuff(int off, int len, byte[] outBuff)
{
int startOffset = this.offset + off;
if (off < 0 || len <= 0 || startOffset + len > this.buffer.length)
return;
if (outBuff.length < len)
return;
System.arraycopy(this.buffer, startOffset, outBuff, 0, len);
}
/**
* Append a byte array to the end of the packet. This may change the data
* buffer of this packet.
*
* @param data byte array to append
* @param len the number of bytes to append
*/
public void append(byte[] data, int len) {
if (data == null || len == 0) {
return;
}
// Ensure the internal buffer is long enough to accommodate data. (The
// method grow will re-allocate the internal buffer if it's too short.)
grow(len);
// Append data.
System.arraycopy(data, 0, buffer, length + offset, len);
length += len;
}
/**
* Grows the internal buffer of this {@code ByteArrayBufferImpl}.
*
* This will change the data buffer of this packet but not the length of the
* valid data. Use this to grow the internal buffer to avoid buffer
* re-allocations when appending data.
*
* @param howMuch the number of bytes by which this {@code NewRawPacket} is to
* grow
*/
public void grow(int howMuch) {
if (howMuch < 0)
throw new IllegalArgumentException("howMuch: " + howMuch);
int newLength = length + howMuch;
if (newLength > buffer.length - offset) {
byte[] newBuffer = BufferPool.Companion.getGetArray().invoke(newLength);
System.arraycopy(buffer, offset, newBuffer, 0, length);
offset = 0;
byte[] oldBuffer = buffer;
setBuffer(newBuffer);
BufferPool.Companion.getReturnArray().invoke(oldBuffer);
}
}
/**
* Shrink the buffer of this packet by specified length
*
* @param len length to shrink
*/
public void shrink(int len)
{
if (len <= 0)
return;
this.length -= len;
if (this.length < 0)
this.length = 0;
}
/**
* @param buffer the buffer to set
*/
public void setBuffer(byte[] buffer)
{
this.buffer = buffer;
}
@Override
public boolean isInvalid()
{
return false;
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp
import org.jitsi.rtp.extensions.bytearray.hashCodeOfSegment
import org.jitsi.rtp.util.BufferPool
abstract class Packet(
buffer: ByteArray,
offset: Int,
length: Int
) : ByteArrayBufferImpl(buffer, offset, length), Cloneable {
inline fun <OtherType : Packet> toOtherType(otherTypeCreator: (ByteArray, Int, Int) -> OtherType): OtherType =
otherTypeCreator(buffer, offset, length)
public abstract override fun clone(): Packet
/**
* A string used to verify the payload of this packet. The same payload must always produce the same verification
* string. This is similar to a hashcode of the payload, but in order to provide more debugging information it also
* includes the length of the payload (and is thus a string).
*
* Different subclasses of [Packet] will have different notions of "payload", and they need to
*/
open val payloadVerification: String
get() = "len=$length hashCode=${buffer.hashCodeOfSegment(offset, offset + length)}"
/**
* Creates a clone of this [Packet]'s buffer. Allocates a new buffer which is large enough to contain the data of
* the packet plus additional [BYTES_TO_LEAVE_AT_END_OF_PACKET] bytes at the end and optionally
* [bytesToLeaveAtStart] bytes at the start.
*
* @param bytesToLeaveAtStart How many bytes to leave at the start of the buffer. This is effectively the offset of
* the new packet.
*/
protected fun cloneBuffer(bytesToLeaveAtStart: Int): ByteArray =
BufferPool.getArray(bytesToLeaveAtStart + length + BYTES_TO_LEAVE_AT_END_OF_PACKET).apply {
System.arraycopy(
buffer,
offset,
this,
bytesToLeaveAtStart,
length
)
}
companion object {
/**
* How much space to leave in the end of a new packet. Having space in the buffer after the payload allows us to
* implement adding the SRT(C)P auth tag efficiently (without having to allocate a new buffer).
*/
const val BYTES_TO_LEAVE_AT_END_OF_PACKET = 20
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp
import org.jitsi.rtp.rtp.RtpPacket
class UnparsedPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : Packet(buffer, offset, length) {
constructor(buffer: ByteArray) : this(buffer, 0, buffer.size)
/**
* Note that we leave the same space at the start as for RTP packets, because an [UnparsedPacket]'s buffer
* might be used directly to create an [RtpPacket].
*/
override fun clone(): UnparsedPacket = UnparsedPacket(
cloneBuffer(RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET),
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET,
length
)
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
/**
* Return the value of the bit at position [bitPos], where
* 0 represents the left-most bit and 7 represents the right-most
* bit of the current byte
*/
fun Byte.getBit(bitPos: Int): Int {
val mask = 0b1 shl (7 - bitPos)
return (this.toInt() and mask) ushr (7 - bitPos)
}
/**
* Return a byte setting or unsetting the bit at [bitPos] of a byte
* according to [isSet]
* Unfortunately we can't do this as an in-place modifier because
* we can't modify the Byte via 'this' in an extension function.
*/
fun Byte.putBit(bitPos: Int, isSet: Boolean): Byte = if (isSet) {
(this.toInt() or (0b10000000 ushr bitPos)).toByte()
} else {
(this.toInt() and (0b10000000 ushr bitPos).inv()).toByte()
}
/**
* Return a byte setting or unsetting the bit or bits specified by [mask]
* of a byte according to [isSet]
* Unfortunately we can't do this as an in-place modifier because
* we can't modify the Byte via 'this' in an extension function.
*/
fun Byte.putBitWithMask(mask: Byte, isSet: Boolean): Byte = if (isSet) {
(this.toInt() or mask.toInt()).toByte()
} else {
(this.toInt() and mask.toInt().inv()).toByte()
}
/**
* Return a byte with the right-most [numBits] bits from [src] put into [dest]
* starting at [bitStartPos]
* [bitStartPos] is a 0 based index into the byte [dest], where the MSB
* is position 0 and the LSB is position 7.
* Given the values:
* bitStartPos = 4
* numBits = 3
* src = 0b101
* dest = 0b00000000
*
* The returned Byte will be:
* 0b00001010
*/
fun Byte.putBits(bitStartPos: Int, numBits: Int, src: Byte): Byte {
// Start the position in src to the first bit we'll assign.
val valueBitPosition = 7 - numBits + 1
var result = this
for (i in 0 until numBits) {
val isSet = src.getBitAsBool(valueBitPosition + i)
result = result.putBit(bitStartPos + i, isSet)
}
return result
}
/**
* Get the bit at [bitPos] as a [Boolean]. Will return [true]
* if the bit is set (1) and [false] if it is unset (0)
*/
fun Byte.getBitAsBool(bitPos: Int): Boolean = getBit(bitPos) == 1
/**
* Get [numBits] bits starting at [bitStartPos], shift them
* all the way to the right and return the result as a [Byte]
* [numBits] must be <= 8
*/
fun Byte.getBits(bitStartPos: Int, numBits: Int): Byte {
// Subtract 1 since the bit at 'bitStartPos' will be included, e.g.:
// A call getBits(4, 2) will read bits 4 and 5
val bitEndPos = bitStartPos + numBits - 1
var result = 0
// shiftOffset represents how far, from the right-most
// bit position, the bit at bitStartPos should be
// at in the result
val shiftOffset = (bitEndPos - bitStartPos)
(bitStartPos..bitEndPos).forEachIndexed { index, value ->
result = result or (getBit(value) shl (shiftOffset - index))
}
return result.toByte()
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright @ 2019 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import org.jitsi.rtp.extensions.bytearray.toHex
import org.jitsi.utils.ByteArrayBuffer
@JvmOverloads
fun ByteArrayBuffer.toHex(maxBytes: Int = Int.MAX_VALUE) = buffer.toHex(offset, maxBytes.coerceAtMost(length))

View File

@@ -0,0 +1,212 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import org.jitsi.rtp.util.BufferPool
import java.nio.ByteBuffer
/**
* Return a (deep) copy of this ByteBuffer.
* The position and mark from the original will NOT
* be carried over (no mark will be set on the copy
* and its position will be 0). The mark of the original
* will not be touched; the position of the original will
* end up what it was before this call was made, BUT, it's value
* will be modified during [clone].
*/
fun ByteBuffer.clone(): ByteBuffer {
val startPosition = this.position()
val clone = ByteBuffer.wrap(BufferPool.getArray(this.capacity()))
this.rewind()
clone.put(this)
this.position(startPosition)
clone.flip()
// TODO(brian): handle if this was a readonly buffer
return clone
}
/**
* Move this [ByteBuffer]'s position back one Byte
*/
fun ByteBuffer.rewindOneByte() {
this.position(this.position() - 1)
}
/**
* Put the right-most 3 bytes of [value]
* into the buffer
*/
fun ByteBuffer.put3Bytes(value: Int) {
this.put(((value and 0x00FF0000) ushr 16).toByte())
this.put(((value and 0x0000FF00) ushr 8).toByte())
this.put((value and 0x000000FF).toByte())
}
fun ByteBuffer.put3Bytes(index: Int, value: Int) {
this.put(index, ((value and 0x00FF0000) ushr 16).toByte())
this.put(index + 1, ((value and 0x0000FF00) ushr 8).toByte())
this.put(index + 2, (value and 0x000000FF).toByte())
}
/**
* Reads the next 3 bytes into the right-most
* 3 bytes of an Int
*/
fun ByteBuffer.get3Bytes(): Int {
val byte1 = (get().toInt() and 0xFF) shl 16
val byte2 = (get().toInt() and 0xFF) shl 8
val byte3 = get().toInt() and 0xFF
return byte1 or byte2 or byte3
}
fun ByteBuffer.get3Bytes(index: Int): Int {
val byte1 = (get(index).toInt() and 0xFF) shl 16
val byte2 = (get(index + 1).toInt() and 0xFF) shl 8
val byte3 = get(index + 2).toInt() and 0xFF
return byte1 or byte2 or byte3
}
/**
* Put the right-most [numBits] bits from [src] into the byte at [byteIndex]
* starting at position [destBitPos]. [destBitPos] is a 0-based index of the
* bit in the byte at [byteIndex], where 0 is the MSB and 7 is the LSB.
*/
fun ByteBuffer.putBits(byteIndex: Int, destBitPos: Int, src: Byte, numBits: Int) {
var byte = get(byteIndex)
byte = byte.putBits(destBitPos, numBits, src)
put(byteIndex, byte)
}
fun ByteBuffer.putBitAsBoolean(byteIndex: Int, destBitPos: Int, isSet: Boolean) {
var byte = get(byteIndex)
byte = byte.putBit(destBitPos, isSet)
put(byteIndex, byte)
}
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
/**
* Print the entire contents of the [ByteBuffer] as hex
* digits
* TODO: should the spacing here (every 4 chunks) be right-justified?
* (meaning, if you have a buffer with a size that isn't a multiple
* of 8, the right-most chunks should be represented as a chunk
* of 8, rather than the left-most. for example:
* data: 01000A00 64006400 7B000000 00
* should probably be printed as:
* data: 01 000A0064 0064007B 00000000
*/
fun ByteBuffer.toHex(): String {
val result = StringBuffer()
val prevPosition = position()
for (i in 0 until limit()) {
val octet = get(i).toInt()
val firstIndex = (octet and 0xF0).ushr(4)
val secondIndex = octet and 0x0F
result.append(HEX_CHARS[firstIndex])
result.append(HEX_CHARS[secondIndex])
if ((i + 1) % 16 == 0) {
result.append("\n")
} else if ((i + 1) % 4 == 0) {
result.append(" ")
}
}
position(prevPosition)
return result.toString()
}
/**
* Returns a newly constructed [ByteBuffer] whose position 0 will
* start at [startPosition] in the current buffer and whose limit
* and capacity will be [size]
*/
fun ByteBuffer.subBuffer(startPosition: Int, size: Int): ByteBuffer {
if (startPosition + size > limit()) {
throw Exception(
"SubBuffer goes beyond the buffer's limit " +
"(limit ${limit()}, requested end of buffer ${startPosition + size})"
)
}
return (duplicate().position(startPosition).limit(startPosition + size) as ByteBuffer).slice()
}
/**
* Returns a newly constructed [ByteBuffer] whose position 0 will
* start at [startPosition] in the current buffer and whose limit
* and capacity will be the amount of bytes between [startPosition] and
* the current buffer's [limit()]
*/
fun ByteBuffer.subBuffer(startPosition: Int): ByteBuffer = subBuffer(startPosition, limit() - startPosition)
/**
* Put [buf] into this buffer starting at [index]
*/
fun ByteBuffer.put(index: Int, buf: ByteBuffer): ByteBuffer {
val currentPosition = position()
position(index)
put(buf)
position(currentPosition)
return this
}
/**
* Shifts the data from [startPos] to [endPos] [numBytes] to the right.
* Note that this method may increase the given buffer's limit, up to
* its capacity.
*
* Note that [startPos] and [endPos] are zero-based.
*/
fun ByteBuffer.shiftDataRight(startPos: Int, endPos: Int, numBytes: Int) {
if (endPos + numBytes >= limit()) {
if (capacity() > endPos + numBytes) {
limit(endPos + numBytes + 1)
}
}
for (index in endPos downTo startPos) {
put(index + numBytes, get(index))
}
}
fun ByteBuffer.shiftDataLeft(startPos: Int, endPos: Int, numBytes: Int) {
for (index in startPos..endPos) {
put(index - numBytes, get(index))
}
}
/**
* Compare the contents of two ByteBuffers, each starting from their position 0
*/
fun ByteBuffer.compareToFromBeginning(other: ByteBuffer): Int {
val thisRewound = this.duplicate().rewind() as ByteBuffer
val otherRewound = other.duplicate().rewind() as ByteBuffer
return thisRewound.compareTo(otherRewound)
}
/**
* Return a new ByteBuffer that includes the contents of this one
* plus [other]
*/
operator fun ByteBuffer.plus(other: ByteBuffer): ByteBuffer {
val newBuf = ByteBuffer.wrap(BufferPool.getArray(limit() + other.limit()))
newBuf.rewind()
newBuf.put(duplicate().rewind() as ByteBuffer)
newBuf.put(other.duplicate().rewind() as ByteBuffer)
newBuf.flip()
return newBuf
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright @ 2019 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import org.jitsi.rtp.Packet
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.rtcp.RtcpHeader
import org.jitsi.rtp.rtp.RtpHeader
/**
* "The process for demultiplexing a packet is as follows. The receiver
* looks at the first byte of the packet."
*
* +----------------+
* | [0..3] -+--> forward to STUN
* | |
* | [16..19] -+--> forward to ZRTP
* | |
* | [20..63] -+--> forward to DTLS
* | |
* | [64..79] -+--> forward to TURN Channel
* | |
* | [128..191] -+--> forward to RTP/RTCP
* +----------------+
*
* See [https://tools.ietf.org/html/rfc7983#section-7]
*
*
* RTP/RTCP are further demultiplexed based on the packet type (second byte)
*
* Note: Kotlin's ranges are implemented efficiently.
*/
private val DTLS_RANGE = 20..63
private val RTP_RTCP_RANGE = 128..191
val RTCP_PACKET_TYPE_RANGE = 192..223
fun Packet.looksLikeRtp(): Boolean {
if (length < RtpHeader.FIXED_HEADER_SIZE_BYTES) {
return false
}
val b0 = buffer[offset].toPositiveInt()
val b1 = buffer[offset + 1].toPositiveInt()
return b0 in RTP_RTCP_RANGE && b1 !in RTCP_PACKET_TYPE_RANGE
}
fun Packet.looksLikeRtcp(): Boolean {
if (length < RtcpHeader.SIZE_BYTES) {
return false
}
val b0 = buffer[offset].toPositiveInt()
val b1 = buffer[offset + 1].toPositiveInt()
return b0 in RTP_RTCP_RANGE && b1 in RTCP_PACKET_TYPE_RANGE
}
fun Packet.looksLikeDtls(): Boolean {
if (length < 1) {
return false
}
return buffer[offset].toPositiveInt() in DTLS_RANGE
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions.bytearray
import org.jitsi.rtp.extensions.getBitAsBool
import org.jitsi.rtp.extensions.putBit
import org.jitsi.rtp.extensions.putBits
import org.jitsi.rtp.util.BufferPool
import org.jitsi.utils.ByteArrayUtils.readInt
import org.jitsi.utils.ByteArrayUtils.readShort
import org.jitsi.utils.ByteArrayUtils.readUint24
import org.jitsi.utils.ByteArrayUtils.writeInt
import org.jitsi.utils.ByteArrayUtils.writeShort
import org.jitsi.utils.ByteArrayUtils.writeUint24
import java.lang.Math.abs
/**
* Put the right-most [numBits] bits from [src] into the byte at [byteIndex]
* starting at position [destBitPos]. [destBitPos] is a 0-based index of the
* bit in the byte at [byteIndex], where 0 is the MSB and 7 is the LSB.
*/
fun ByteArray.putBits(byteIndex: Int, destBitPos: Int, src: Byte, numBits: Int) {
var byte = get(byteIndex)
byte = byte.putBits(destBitPos, numBits, src)
set(byteIndex, byte)
}
fun ByteArray.getBitAsBool(byteOffset: Int, bitOffset: Int): Boolean = get(byteOffset).getBitAsBool(bitOffset)
fun ByteArray.putBitAsBoolean(byteIndex: Int, destBitPos: Int, isSet: Boolean) {
var byte = get(byteIndex)
byte = byte.putBit(destBitPos, isSet)
set(byteIndex, byte)
}
fun ByteArray.getShort(byteIndex: Int): Short = readShort(this, byteIndex)
fun ByteArray.putShort(byteIndex: Int, value: Short) = writeShort(this, byteIndex, value)
fun ByteArray.get3Bytes(byteIndex: Int): Int = readUint24(this, byteIndex)
fun ByteArray.put3Bytes(byteIndex: Int, value: Int) = writeUint24(this, byteIndex, value)
fun ByteArray.getInt(byteIndex: Int): Int = readInt(this, byteIndex)
fun ByteArray.putInt(byteIndex: Int, value: Int) = writeInt(this, byteIndex, value)
fun byteArrayOf(vararg elements: Number): ByteArray = elements.map { it.toByte() }.toByteArray()
/**
* Shifts the data from [startPos] to [endPos] [numBytes] to the right.
* Note that this method may increase the given buffer's limit, up to
* its capacity.
*
* Note that [startPos] and [endPos] are zero-based and numBytes
* must be positive!
*/
fun ByteArray.shiftDataRight(startPos: Int, endPos: Int, numBytes: Int) {
if (numBytes < 0) {
throw Exception("")
}
for (index in endPos downTo startPos) {
set(index + numBytes, get(index))
}
}
fun ByteArray.shiftDataLeft(startPos: Int, endPos: Int, numBytes: Int) {
for (index in startPos..endPos) {
set(index - numBytes, get(index))
}
}
/**
* Shifts the data from [startPos] to [endPos] by [delta] bytes.
* if [delta] is negative, the data will be shifted to the left,
* if [delta] is positive, the data will be shifted to the right
*/
fun ByteArray.shiftData(startPos: Int, endPos: Int, delta: Int) {
when {
delta < 0 -> shiftDataLeft(startPos, endPos, abs(delta))
delta > 0 -> shiftDataRight(startPos, endPos, delta)
}
}
fun ByteArray.cloneFromPool(): ByteArray {
val clone = BufferPool.getArray(size)
System.arraycopy(this, 0, clone, 0, size)
return clone
}
operator fun ByteArray.plus(other: ByteArray): ByteArray {
val newArray = BufferPool.getArray(size + other.size)
System.arraycopy(this, 0, newArray, 0, size)
System.arraycopy(other, 0, newArray, size, other.size)
return newArray
}
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
/**
* Print the contents of the [ByteArray] as hex digits.
*/
fun ByteArray.toHex(
/** Offset to start at. */
offset: Int = 0,
/** Maximum number of elements to print. */
length: Int = (size - offset)
): String {
val result = StringBuffer()
for (i in offset until (offset + length).coerceAtMost(size)) {
val position = i - offset
if (position != 0) {
if (position % 16 == 0) {
result.append("\n")
} else if (position % 4 == 0) {
result.append(" ")
}
}
val byte: Int = this[i].toInt()
val firstIndex = (byte and 0xF0) shr 4
val secondIndex = byte and 0x0F
result.append(HEX_CHARS[firstIndex])
result.append(HEX_CHARS[secondIndex])
}
return result.toString()
}
/**
* Returns the hash code of the segment of this [ByteArray] starting at 'start' and ending in 'end' (exclusive).
*/
fun ByteArray.hashCodeOfSegment(start: Int, end: Int): Int {
var result = 1
for (i in start.coerceIn(0, size) until end.coerceIn(0, size)) {
result = 31 * result + this[i]
}
return result
}
class ByteArrayUtils {
companion object {
val emptyByteArray = BufferPool.getArray(0)
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions.unsigned
import kotlin.experimental.and
/**
* Many times fields in packets are defined as unsigned. Since
* we don't have unsigned types (and attempts at using Kotlin's
* unsigned types have not proven to be very usable), we store
* those types as larger than they actually are ([Byte] and
* [Short] held as [Int] and [Int] held as [Long]). These
* helpers are to do the conversion correctly such that
* we get a properly 'unsigned' version of the parsed field.
* No helper is needed for the reverse operation (the use
* of toInt, toShort and toByte is sufficient).
*/
fun Byte.toPositiveInt(): Int = toInt() and 0xFF
fun Short.toPositiveInt(): Int = toInt() and 0xFFFF
fun Int.toPositiveLong(): Long = toLong() and 0xFFFFFFFF
// TODO: i think these should be able to make the above functions obsolete
fun Number.toPositiveShort(): Short = when (this) {
is Byte -> this.toShort() and 0xFF
else -> this.toShort()
}
fun Number.toPositiveInt(): Int = when (this) {
is Byte -> this.toInt() and 0xFF
is Short -> this.toInt() and 0xFFFF
else -> this.toInt()
}
fun Number.toPositiveLong(): Long = when (this) {
is Byte -> this.toLong() and 0xFF
is Short -> this.toLong() and 0xFFFF
is Int -> this.toLong() and 0xFFFFFFFF
else -> this.toLong()
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.toHex
import org.jitsi.rtp.util.BufferPool
class CompoundRtcpPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
val packets: List<RtcpPacket> by lazy { parse(buffer, offset, length) }
companion object {
fun parse(buffer: ByteArray, offset: Int, length: Int): List<RtcpPacket> {
var bytesRemaining = length
var currOffset = offset
val rtcpPackets = mutableListOf<RtcpPacket>()
while (bytesRemaining >= RtcpHeader.SIZE_BYTES) {
val rtcpPacket = try {
RtcpPacket.parse(buffer, currOffset, bytesRemaining)
} catch (e: InvalidRtcpException) {
throw CompoundRtcpContainedInvalidDataException(buffer, offset, length, currOffset, e.reason)
}
rtcpPackets.add(rtcpPacket)
currOffset += rtcpPacket.length
bytesRemaining -= rtcpPacket.length
}
return rtcpPackets
}
operator fun invoke(packets: List<RtcpPacket>): CompoundRtcpPacket {
val totalLength = packets.map { it.length }.sum()
val buf = BufferPool.getArray(totalLength + BYTES_TO_LEAVE_AT_END_OF_PACKET)
var off = 0
packets.forEach {
System.arraycopy(it.buffer, it.offset, buf, off, it.length)
off += it.length
}
return CompoundRtcpPacket(buf, 0, totalLength)
}
/**
* Create one or more compound RTCP packets from [packets], with each compound packet being
* no more than [mtu] bytes in size (unless an individual packet is bigger than that, in which
* case it will be in a compound packet on its own).
*/
fun createWithMtu(packets: List<RtcpPacket>, mtu: Int = 1500): List<CompoundRtcpPacket> =
packets.chunkMaxSize(mtu) { it.length }.map { CompoundRtcpPacket(it) }
}
override fun clone(): RtcpPacket = CompoundRtcpPacket(cloneBuffer(0), 0, length)
}
/**
* Return a List of Lists, where sub-list is made up of an ordered list
* of values pulled from [this], such that the total size of each sub-list
* is not more than maxSize. (Sizes are evaluated by [evaluate]. If an
* individual element's size is more than [maxSize] it will be returned
* in a sub-list on its own.)
*/
private fun <T : Any> List<T>.chunkMaxSize(maxSize: Int, evaluate: (T) -> Int): List<List<T>> {
val chunks = mutableListOf<List<T>>()
if (this.isEmpty()) {
return chunks
}
var currentChunk = mutableListOf(first())
chunks.add(currentChunk)
var chunkSize = evaluate(currentChunk.first())
// Ignore the first value which we already put in the current chunk
drop(1).forEach {
val size = evaluate(it)
if (chunkSize + size > maxSize) {
currentChunk = mutableListOf(it)
chunks.add(currentChunk)
chunkSize = size
} else {
currentChunk.add(it)
chunkSize += size
}
}
return chunks
}
class CompoundRtcpContainedInvalidDataException(
compoundRtcpBuf: ByteArray,
compoundRtcpOffset: Int,
compoundRtcpLength: Int,
invalidDataOffset: Int,
invalidDataReason: String
) : Exception(
"Compound RTCP contained invalid data. Compound RTCP packet data is: " +
"${compoundRtcpBuf.toHex(compoundRtcpOffset, compoundRtcpLength)} Invalid data " +
"started at offset ${invalidDataOffset - compoundRtcpOffset} and failed due to '$invalidDataReason'"
)

View File

@@ -0,0 +1,74 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.getInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
/**
* An [RtcpByePacket] cannot be changed once it has been created, so we
* parse the fields once (lazily) and store them.
* TODO: technically it COULD be changed, since anyone can change the buffer.
* can we enforce immutability?
*
* https://tools.ietf.org/html/rfc3550#section-6.6
*
* 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| SC | PT=BYE=203 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC/CSRC |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : ... :
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* (opt) | length | reason for leaving ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class RtcpByePacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
val ssrcs: List<Long> by lazy {
val ssrcStartOffset = offset + 4
(0 until reportCount)
.map { buffer.getInt(ssrcStartOffset + it * 4) }
.map(Int::toPositiveLong)
.toList()
}
val reason: String? by lazy {
val headerAndSsrcsLengthBytes = RtcpHeader.SIZE_BYTES + (reportCount - 1) * 4
val hasReason = headerAndSsrcsLengthBytes < length
if (hasReason) {
val reasonLengthOffset = offset + headerAndSsrcsLengthBytes
val reasonLength = buffer[reasonLengthOffset].toInt()
val reasonStr = String(buffer, reasonLengthOffset + 1, reasonLength)
reasonStr
} else {
null
}
}
override fun clone(): RtcpByePacket = RtcpByePacket(cloneBuffer(0), 0, length)
companion object {
const val PT: Int = 203
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.getBitAsBool
import org.jitsi.rtp.extensions.bytearray.putBitAsBoolean
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.util.getBitsAsInt
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getIntAsLong
import org.jitsi.rtp.util.getShortAsInt
import org.jitsi.rtp.util.putNumberAsBits
/**
* Models the RTCP header as defined in https://tools.ietf.org/html/rfc3550#section-6.1
* 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| RC | PT=SR=200 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of sender |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*
* length: 16 bits
* The length of this RTCP packet in 32-bit words minus one,
* including the header and any padding. (The offset of one makes
* zero a valid length and avoids a possible infinite loop in
* scanning a compound RTCP packet, while counting 32-bit words
* avoids a validity check for a multiple of 4.)
*
* @author Brian Baldino
*
* TODO: Use the same utility function in RtpHeader and RtcpHeader
*/
class RtcpHeader {
companion object {
const val SIZE_BYTES = 8
const val VERSION_OFFSET = 0
const val PADDING_OFFSET = 0
const val REPORT_COUNT_OFFSET = 0
const val PACKET_TYPE_OFFSET = 1
const val LENGTH_OFFSET = 2
const val SENDER_SSRC_OFFSET = 4
fun getVersion(buf: ByteArray, headerStartOffset: Int): Int =
buf.getBitsAsInt(headerStartOffset + VERSION_OFFSET, 0, 2)
fun setVersion(buf: ByteArray, headerStartOffset: Int, version: Int) =
buf.putNumberAsBits(headerStartOffset + VERSION_OFFSET, 0, 2, version)
fun hasPadding(buf: ByteArray, headerStartOffset: Int): Boolean =
buf.getBitAsBool(headerStartOffset + PADDING_OFFSET, 2)
fun setPadding(buf: ByteArray, headerStartOffset: Int, hasPadding: Boolean) =
buf.putBitAsBoolean(headerStartOffset + PADDING_OFFSET, 2, hasPadding)
fun getReportCount(buf: ByteArray, headerStartOffset: Int): Int =
buf.getBitsAsInt(headerStartOffset + REPORT_COUNT_OFFSET, 3, 5)
fun setReportCount(buf: ByteArray, headerStartOffset: Int, reportCount: Int) =
buf.putNumberAsBits(headerStartOffset + REPORT_COUNT_OFFSET, 3, 5, reportCount)
fun getPacketType(buf: ByteArray, headerStartOffset: Int): Int =
buf.getByteAsInt(headerStartOffset + PACKET_TYPE_OFFSET)
fun setPacketType(buf: ByteArray, headerStartOffset: Int, packetType: Int) =
buf.set(headerStartOffset + PACKET_TYPE_OFFSET, packetType.toByte())
// TODO document (bytes or words?)
fun getLength(buf: ByteArray, headerStartOffset: Int): Int =
buf.getShortAsInt(headerStartOffset + LENGTH_OFFSET)
fun setLength(buf: ByteArray, headerStartOffset: Int, length: Int) =
buf.putShort(headerStartOffset + LENGTH_OFFSET, length.toShort())
fun getSenderSsrc(buf: ByteArray, headerStartOffset: Int): Long =
buf.getIntAsLong(headerStartOffset + SENDER_SSRC_OFFSET)
fun setSenderSsrc(buf: ByteArray, headerStartOffset: Int, senderSsrc: Long) =
buf.putInt(headerStartOffset + SENDER_SSRC_OFFSET, senderSsrc.toInt())
}
}
data class RtcpHeaderBuilder(
var version: Int = 2,
var hasPadding: Boolean = false,
var reportCount: Int = -1,
var packetType: Int = -1,
var length: Int = -1,
var senderSsrc: Long = 0
) {
fun build(): ByteArray {
val buf = ByteArray(RtcpHeader.SIZE_BYTES)
writeTo(buf, 0)
return buf
}
fun writeTo(buf: ByteArray, offset: Int) {
RtcpHeader.setVersion(buf, offset, version)
RtcpHeader.setPadding(buf, offset, hasPadding)
RtcpHeader.setReportCount(buf, offset, reportCount)
RtcpHeader.setPacketType(buf, offset, packetType)
RtcpHeader.setLength(buf, offset, length)
RtcpHeader.setSenderSsrc(buf, offset, senderSsrc)
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.Packet
import org.jitsi.rtp.extensions.RTCP_PACKET_TYPE_RANGE
import org.jitsi.rtp.extensions.bytearray.toHex
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
/**
* Models the RTCP header as defined in https://tools.ietf.org/html/rfc3550#section-6.1
* 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| RC | PT=SR=200 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of sender |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*
* length: 16 bits
* The length of this RTCP packet in 32-bit words minus one,
* including the header and any padding. (The offset of one makes
* zero a valid length and avoids a possible infinite loop in
* scanning a compound RTCP packet, while counting 32-bit words
* avoids a validity check for a multiple of 4.)
*/
abstract class RtcpPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : Packet(buffer, offset, length) {
var version: Int
get() = RtcpHeader.getVersion(buffer, offset)
set(value) = RtcpHeader.setVersion(buffer, offset, value)
val hasPadding: Boolean
get() = RtcpHeader.hasPadding(buffer, offset)
val reportCount: Int
get() = RtcpHeader.getReportCount(buffer, offset)
val packetType: Int
get() = RtcpHeader.getPacketType(buffer, offset)
val lengthField: Int
get() = RtcpHeader.getLength(buffer, offset)
var senderSsrc: Long
get() = RtcpHeader.getSenderSsrc(buffer, offset)
set(value) = RtcpHeader.setSenderSsrc(buffer, offset, value)
val packetLength: Int = (lengthField + 1) * 4
/**
* Effectively disable the payload verification for RTCP packets, since in practice we change them very often
*/
override val payloadVerification = "rtcp"
abstract override fun clone(): RtcpPacket
companion object {
fun parse(buf: ByteArray, offset: Int, bytesRemaining: Int): RtcpPacket {
val packetType = RtcpHeader.getPacketType(buf, offset)
val packetLengthBytes = (RtcpHeader.getLength(buf, offset) + 1) * 4
if (packetLengthBytes > bytesRemaining) {
throw InvalidRtcpException(buf, offset, "length $packetLengthBytes > available $bytesRemaining")
}
return when (packetType) {
RtcpByePacket.PT -> RtcpByePacket(buf, offset, packetLengthBytes)
RtcpRrPacket.PT -> RtcpRrPacket(buf, offset, packetLengthBytes)
RtcpSrPacket.PT -> RtcpSrPacket(buf, offset, packetLengthBytes)
RtcpSdesPacket.PT -> RtcpSdesPacket(buf, offset, packetLengthBytes)
in RtcpFbPacket.PACKET_TYPES -> RtcpFbPacket.parse(buf, offset, packetLengthBytes)
RtcpXrPacket.PT -> RtcpXrPacket(buf, offset, packetLengthBytes)
else -> {
return when (packetType) {
in RTCP_PACKET_TYPE_RANGE -> UnsupportedRtcpPacket(buf, offset, packetLengthBytes)
else -> throw InvalidRtcpException(buf, offset, "type $packetType")
}
}
}
}
}
}
class InvalidRtcpException(buf: ByteArray, offset: Int, val reason: String) :
Exception("Invalid RTCP packet: $reason: ${buf.toHex(offset, RtcpHeader.SIZE_BYTES)}")

View File

@@ -0,0 +1,150 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.put3Bytes
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.rtp.util.get3BytesAsInt
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getIntAsLong
/**
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | SSRC_1 (SSRC of first source) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | fraction lost | cumulative number of packets lost |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | extended highest sequence number received |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | interarrival jitter |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | last SR (LSR) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | delay since last SR (DLSR) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class RtcpReportBlock(
val ssrc: Long,
val fractionLost: Int,
val cumulativePacketsLost: Int,
val extendedHighestSeqNum: Long,
val interarrivalJitter: Long,
val lastSrTimestamp: Long,
val delaySinceLastSr: Long
) {
constructor(
ssrc: Long,
fractionLost: Int,
cumulativePacketsLost: Int,
seqNumCycles: Int,
seqNum: Int,
interarrivalJitter: Long,
lastSrTimestamp: Long,
delaySinceLastSr: Long
) : this(
ssrc,
fractionLost,
cumulativePacketsLost,
((seqNumCycles shl 16) + seqNum.toShort()).toPositiveLong(),
interarrivalJitter,
lastSrTimestamp,
delaySinceLastSr
)
val seqNumCycles: Int by lazy {
(extendedHighestSeqNum ushr 16).toPositiveInt()
}
val seqNum: Int by lazy {
extendedHighestSeqNum.toShort().toPositiveInt()
}
fun writeTo(buf: ByteArray, offset: Int) {
setSsrc(buf, offset, ssrc)
setFractionLost(buf, offset, fractionLost)
setCumulativePacketsLost(buf, offset, cumulativePacketsLost)
setExtendedHighestSeqNum(buf, offset, extendedHighestSeqNum)
setInterarrivalJitter(buf, offset, interarrivalJitter)
setLastSrTimestamp(buf, offset, lastSrTimestamp)
setDelaySinceLastSr(buf, offset, delaySinceLastSr)
}
companion object {
const val SIZE_BYTES = 24
// Offsets relative to the start of an RTCP Report Block
const val SSRC_OFFSET = 0
const val FRACTION_LOST_OFFSET = 4
const val CUMULATIVE_PACKETS_LOST_OFFSET = 5
const val EXTENDED_HIGHEST_SEQ_NUM_OFFSET = 8
const val INTERARRIVAL_JITTER_OFFSET = 12
const val LAST_SR_TIMESTAMP_OFFSET = 16
const val DELAY_SINCE_LAST_SR_OFFSET = 20
fun fromBuffer(buffer: ByteArray, offset: Int): RtcpReportBlock {
val ssrc = getSsrc(buffer, offset)
val fractionLost = getFractionLost(buffer, offset)
val cumulativePacketsLost = getCumulativePacketsLost(buffer, offset)
val extendedHighestSeqNum = getExtendedHighestSeqNum(buffer, offset)
val interarrivalJitter = getInterarrivalJitter(buffer, offset)
val lastSrTimestamp = getLastSrTimestamp(buffer, offset)
val delaySinceLastSr = getDelaySinceLastSr(buffer, offset)
return RtcpReportBlock(
ssrc,
fractionLost,
cumulativePacketsLost,
extendedHighestSeqNum,
interarrivalJitter,
lastSrTimestamp,
delaySinceLastSr
)
}
fun getSsrc(buffer: ByteArray, offset: Int): Long = buffer.getIntAsLong(offset + SSRC_OFFSET)
fun setSsrc(buf: ByteArray, baseOffset: Int, value: Long) = buf.putInt(baseOffset + SSRC_OFFSET, value.toInt())
fun getFractionLost(buffer: ByteArray, offset: Int): Int = buffer.getByteAsInt(offset + FRACTION_LOST_OFFSET)
fun setFractionLost(buf: ByteArray, baseOffset: Int, value: Int) =
buf.set(baseOffset + FRACTION_LOST_OFFSET, value.toByte())
fun getCumulativePacketsLost(buffer: ByteArray, offset: Int): Int =
buffer.get3BytesAsInt(offset + CUMULATIVE_PACKETS_LOST_OFFSET)
fun setCumulativePacketsLost(buffer: ByteArray, offset: Int, value: Int) =
buffer.put3Bytes(offset + CUMULATIVE_PACKETS_LOST_OFFSET, value)
fun getExtendedHighestSeqNum(buffer: ByteArray, offset: Int): Long =
buffer.getIntAsLong(offset + EXTENDED_HIGHEST_SEQ_NUM_OFFSET)
fun setExtendedHighestSeqNum(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + EXTENDED_HIGHEST_SEQ_NUM_OFFSET, value.toInt())
fun getInterarrivalJitter(buffer: ByteArray, offset: Int): Long =
buffer.getIntAsLong(offset + INTERARRIVAL_JITTER_OFFSET)
fun setInterarrivalJitter(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + INTERARRIVAL_JITTER_OFFSET, value.toInt())
fun getLastSrTimestamp(buffer: ByteArray, offset: Int): Long =
buffer.getIntAsLong(offset + LAST_SR_TIMESTAMP_OFFSET)
fun setLastSrTimestamp(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + LAST_SR_TIMESTAMP_OFFSET, value.toInt())
fun getDelaySinceLastSr(buffer: ByteArray, offset: Int): Long =
buffer.getIntAsLong(offset + DELAY_SINCE_LAST_SR_OFFSET)
fun setDelaySinceLastSr(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + DELAY_SINCE_LAST_SR_OFFSET, value.toInt())
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
/**
* https://tools.ietf.org/html/rfc3550#section-6.4.2
*
* 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| RC | PT=RR=201 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* report | SSRC_1 (SSRC of first source) |
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* 1 | fraction lost | cumulative number of packets lost |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | extended highest sequence number received |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | interarrival jitter |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | last SR (LSR) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | delay since last SR (DLSR) |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* report | SSRC_2 (SSRC of second source) |
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* 2 : ... :
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | profile-specific extensions |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class RtcpRrPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
val reportBlocks: List<RtcpReportBlock> by lazy {
(0 until reportCount).map {
RtcpReportBlock.fromBuffer(buffer, offset + REPORT_BLOCKS_OFFSET + it * RtcpReportBlock.SIZE_BYTES)
}.toList()
}
override fun clone(): RtcpRrPacket = RtcpRrPacket(cloneBuffer(0), 0, length)
companion object {
const val PT = 201
const val REPORT_BLOCKS_OFFSET = 8
}
}
data class RtcpRrPacketBuilder(
var rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
val reportBlocks: List<RtcpReportBlock> = listOf()
) {
init {
require(reportBlocks.size <= 31) { "Too many report blocks ${reportBlocks.size}: RR can contain at most 31" }
}
private fun getLengthValue(): Int = RtpUtils.calculateRtcpLengthFieldValue(sizeBytes)
private val sizeBytes: Int
get() = RtcpHeader.SIZE_BYTES + reportBlocks.size * RtcpReportBlock.SIZE_BYTES
fun build(): RtcpRrPacket {
val buf = BufferPool.getArray(sizeBytes)
writeTo(buf, 0)
return RtcpRrPacket(buf, 0, sizeBytes)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = RtcpRrPacket.PT
reportCount = reportBlocks.size
length = getLengthValue()
}.writeTo(buf, offset)
reportBlocks.forEachIndexed { index, reportBlock ->
reportBlock.writeTo(buf, offset + RtcpRrPacket.REPORT_BLOCKS_OFFSET + index * RtcpReportBlock.SIZE_BYTES)
}
}
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.ByteArrayUtils
import org.jitsi.rtp.extensions.bytearray.toHex
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getIntAsLong
import java.nio.charset.StandardCharsets
/**
* https://tools.ietf.org/html/rfc3550#section-6.5
* 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 |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*
*
*/
class RtcpSdesPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
val sdesChunks: List<SdesChunk> by lazy {
getSdesChunks(buffer, offset, length)
}
override fun clone(): RtcpPacket = RtcpSdesPacket(cloneBuffer(0), 0, length)
companion object {
const val PT = 202
const val CHUNKS_OFFSET = 4
fun getSdesChunks(buf: ByteArray, baseOffset: Int, length: Int): List<SdesChunk> {
var currOffset = baseOffset + CHUNKS_OFFSET
val sdesChunks = mutableListOf<SdesChunk>()
while (currOffset < length) {
val sdesChunk = SdesChunk(buf, currOffset)
sdesChunks.add(sdesChunk)
currOffset += sdesChunk.sizeBytes
}
return sdesChunks
}
}
}
// Offset should point to the start of this sdes chunk
class SdesChunk(
buffer: ByteArray,
offset: Int
) {
val sizeBytes: Int by lazy {
val dataSize = 4 + sdesItems.map(SdesItem::sizeBytes).sum()
dataSize + RtpUtils.getNumPaddingBytes(dataSize)
}
val ssrc: Long by lazy { getSsrc(buffer, offset) }
val sdesItems: List<SdesItem> by lazy {
getSdesItems(buffer, offset)
}
companion object {
const val SSRC_OFFSET = 0
const val SDES_ITEMS_OFFSET = 4
fun getSsrc(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + SSRC_OFFSET)
fun getSdesItems(buf: ByteArray, baseOffset: Int): List<SdesItem> {
var currOffset = baseOffset + SDES_ITEMS_OFFSET
val sdesItems = mutableListOf<SdesItem>()
while (true) {
val currItem = SdesItem.parse(buf, currOffset)
if (currItem == EmptySdesItem) {
break
}
sdesItems.add(currItem)
currOffset += currItem.sizeBytes
}
return sdesItems
}
}
}
enum class SdesItemType(val value: Int) {
EMPTY(0),
UNKNOWN(-1),
CNAME(1);
companion object {
private val map = SdesItemType.values().associateBy(SdesItemType::value)
fun fromInt(type: Int): SdesItemType = map.getOrDefault(type, UNKNOWN)
}
}
/**
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Type | length | data ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* length = the length of the data field
*/
abstract class SdesItem(
val type: SdesItemType
) {
abstract val sizeBytes: Int
companion object {
const val SDES_ITEM_HEADER_SIZE = 2
const val TYPE_OFFSET = 0
const val LENGTH_OFFSET = 1
const val DATA_OFFSET = 2
fun getType(buf: ByteArray, baseOffset: Int): Int = buf.getByteAsInt(baseOffset + TYPE_OFFSET)
fun getLength(buf: ByteArray, baseOffset: Int): Int = buf.getByteAsInt(baseOffset + LENGTH_OFFSET)
fun copyData(buf: ByteArray, baseOffset: Int, dataLength: Int): ByteArray {
if (dataLength <= 0) {
return ByteArrayUtils.emptyByteArray
}
val copy = BufferPool.getArray(dataLength)
System.arraycopy(buf, baseOffset + DATA_OFFSET, copy, 0, dataLength)
return copy
}
/**
* [buf]'s current position should be at the beginning of
* the SDES item
*/
fun parse(buf: ByteArray, offset: Int): SdesItem {
val typeValue = getType(buf, offset)
val type = SdesItemType.fromInt(typeValue)
return when (type) {
SdesItemType.EMPTY -> EmptySdesItem
else -> {
val length = getLength(buf, offset)
return when (type) {
SdesItemType.CNAME -> CnameSdesItem(buf, offset, length)
else -> UnknownSdesItem(typeValue, buf, offset, length)
}
}
}
}
}
}
class UnknownSdesItem(
private val sdesTypeValue: Int,
buf: ByteArray,
offset: Int,
length: Int
) : SdesItem(SdesItemType.UNKNOWN) {
private val dataField = copyData(buf, offset, length)
override val sizeBytes: Int = SDES_ITEM_HEADER_SIZE + dataField.size
override fun toString(): String = "Unknown SDES type($sdesTypeValue) data = ${dataField.toHex()}"
}
object EmptySdesItem : SdesItem(SdesItemType.EMPTY) {
override val sizeBytes: Int = 1
}
/**
* https://tools.ietf.org/html/rfc3550#section-6.5.1
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | CNAME=1 | length | user and domain name ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class CnameSdesItem(buf: ByteArray, offset: Int, length: Int) : SdesItem(SdesItemType.CNAME) {
private val dataField: ByteArray = copyData(buf, offset, length)
override val sizeBytes: Int = SDES_ITEM_HEADER_SIZE + dataField.size
val cname: String by lazy {
String(dataField, StandardCharsets.US_ASCII)
}
override fun toString(): String = "CNAME: $cname"
}

View File

@@ -0,0 +1,233 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.getIntAsLong
/**
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* sender | NTP timestamp, most significant word |
* info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | NTP timestamp, least significant word |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | RTP timestamp |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | sender's packet count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | sender's octet count |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*
* RTCP SenderInfo block
*/
class SenderInfoParser {
companion object {
const val SIZE_BYTES = 20
const val NTP_TS_MSW_OFFSET = 0
const val NTP_TS_LSW_OFFSET = 4
const val RTP_TS_OFFSET = 8
const val SENDERS_PACKET_COUNT_OFFSET = 12
const val SENDERS_OCTET_COUNT_OFFSET = 16
fun getNtpTimestampMsw(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + NTP_TS_MSW_OFFSET)
fun setNtpTimestampMsw(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + NTP_TS_MSW_OFFSET, value.toInt())
fun getNtpTimestampLsw(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + NTP_TS_LSW_OFFSET)
fun setNtpTimestampLsw(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + NTP_TS_LSW_OFFSET, value.toInt())
fun getRtpTimestamp(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + RTP_TS_OFFSET)
fun setRtpTimestamp(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + RTP_TS_OFFSET, value.toInt())
fun getSendersPacketCount(buf: ByteArray, baseOffset: Int): Long =
buf.getIntAsLong(baseOffset + SENDERS_PACKET_COUNT_OFFSET)
fun setSendersPacketCount(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + SENDERS_PACKET_COUNT_OFFSET, value.toInt())
fun getSendersOctetCount(buf: ByteArray, baseOffset: Int): Long =
buf.getIntAsLong(baseOffset + SENDERS_OCTET_COUNT_OFFSET)
fun setSendersOctetCount(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + SENDERS_OCTET_COUNT_OFFSET, value.toInt())
}
}
data class SenderInfoBuilder(
var ntpTimestampMsw: Long = -1,
var ntpTimestampLsw: Long = -1,
var rtpTimestamp: Long = -1,
var sendersPacketCount: Long = -1,
var sendersOctetCount: Long = -1
) {
fun setNtpFromJavaTime(javaTime: Long) {
val wallSecs = javaTime / 1000
val wallMs = javaTime % 1000
ntpTimestampMsw = wallSecs + JAVA_TO_NTP_EPOCH_OFFSET_SECS
ntpTimestampLsw = wallMs * (1L shl 32) / 1000
}
fun writeTo(buf: ByteArray, offset: Int) {
SenderInfoParser.setNtpTimestampMsw(buf, offset, ntpTimestampMsw)
SenderInfoParser.setNtpTimestampLsw(buf, offset, ntpTimestampLsw)
SenderInfoParser.setRtpTimestamp(buf, offset, rtpTimestamp)
SenderInfoParser.setSendersPacketCount(buf, offset, sendersPacketCount)
SenderInfoParser.setSendersOctetCount(buf, offset, sendersOctetCount)
}
companion object {
const val JAVA_TO_NTP_EPOCH_OFFSET_SECS = 2208988800L
}
}
/**
* 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| RC | PT=SR=200 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of sender |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* sender | NTP timestamp, most significant word |
* info +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | NTP timestamp, least significant word |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | RTP timestamp |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | sender's packet count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | sender's octet count |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* report | SSRC_1 (SSRC of first source) |
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* 1 | fraction lost | cumulative number of packets lost |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | extended highest sequence number received |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | interarrival jitter |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | last SR (LSR) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | delay since last SR (DLSR) |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* report | SSRC_2 (SSRC of second source) |
* block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* 2 : ... :
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | profile-specific extensions |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* https://tools.ietf.org/html/rfc3550#section-6.4.1
*/
class RtcpSrPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
val senderInfo: SenderInfo by lazy { SenderInfo() }
val reportBlocks: List<RtcpReportBlock> by lazy {
(0 until reportCount).map {
RtcpReportBlock.fromBuffer(buffer, offset + REPORT_BLOCKS_OFFSET + it * RtcpReportBlock.SIZE_BYTES)
}.toList()
}
override fun clone(): RtcpSrPacket = RtcpSrPacket(cloneBuffer(0), 0, length)
fun cloneWithoutReportBlocks(): RtcpSrPacket = RtcpSrPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
reportCount = 0,
senderSsrc = this.senderSsrc
),
senderInfo = SenderInfoBuilder(
ntpTimestampMsw = this.senderInfo.ntpTimestampMsw,
ntpTimestampLsw = this.senderInfo.ntpTimestampLsw,
rtpTimestamp = this.senderInfo.rtpTimestamp,
sendersPacketCount = this.senderInfo.sendersPacketCount,
sendersOctetCount = this.senderInfo.sendersOctetCount
)
).build()
// SenderInfo is defined differently so that we can scope these variables under a the 'senderInfo'
// member here. Is there a better way?
inner class SenderInfo {
val ntpTimestampMsw: Long = SenderInfoParser.getNtpTimestampMsw(buffer, offset + SENDER_INFO_OFFSET)
val ntpTimestampLsw: Long = SenderInfoParser.getNtpTimestampLsw(buffer, offset + SENDER_INFO_OFFSET)
var rtpTimestamp: Long
get() = SenderInfoParser.getRtpTimestamp(buffer, offset + SENDER_INFO_OFFSET)
set(value) = SenderInfoParser.setRtpTimestamp(buffer, offset + SENDER_INFO_OFFSET, value)
var sendersPacketCount: Long
get() = SenderInfoParser.getSendersPacketCount(buffer, offset + SENDER_INFO_OFFSET)
set(value) = SenderInfoParser.setSendersPacketCount(buffer, offset + SENDER_INFO_OFFSET, value)
var sendersOctetCount: Long
get() = SenderInfoParser.getSendersOctetCount(buffer, offset + SENDER_INFO_OFFSET)
set(value) = SenderInfoParser.setSendersOctetCount(buffer, offset + SENDER_INFO_OFFSET, value)
/**
* https://tools.ietf.org/html/rfc3550#section-4
* In some fields where a more compact representation is
* appropriate, only the middle 32 bits are used; that is, the low 16
* bits of the integer part and the high 16 bits of the fractional part.
* The high 16 bits of the integer part must be determined
* independently.
*/
val compactedNtpTimestamp: Long by lazy {
(((ntpTimestampMsw and 0xFFFF) shl 16) or (ntpTimestampLsw ushr 16)).toPositiveLong()
}
}
companion object {
const val PT = 200
const val SENDER_INFO_OFFSET = RtcpHeader.SIZE_BYTES
const val REPORT_BLOCKS_OFFSET = SENDER_INFO_OFFSET + SenderInfoParser.SIZE_BYTES
}
}
data class RtcpSrPacketBuilder(
var rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var senderInfo: SenderInfoBuilder = SenderInfoBuilder(),
val reportBlocks: List<RtcpReportBlock> = listOf()
) {
init {
require(reportBlocks.size <= 31) { "Too many report blocks ${reportBlocks.size}: SR can contain at most 31" }
}
private val sizeBytes: Int
get() = RtcpHeader.SIZE_BYTES + SenderInfoParser.SIZE_BYTES + reportBlocks.size * RtcpReportBlock.SIZE_BYTES
fun build(): RtcpSrPacket {
val buf = BufferPool.getArray(sizeBytes)
writeTo(buf, 0)
return RtcpSrPacket(buf, 0, sizeBytes)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = RtcpSrPacket.PT
reportCount = reportBlocks.size
length = RtpUtils.calculateRtcpLengthFieldValue(sizeBytes)
}.writeTo(buf, offset)
senderInfo.writeTo(buf, offset + RtcpSrPacket.SENDER_INFO_OFFSET)
reportBlocks.forEachIndexed { index, reportBlock ->
reportBlock.writeTo(buf, offset + RtcpSrPacket.REPORT_BLOCKS_OFFSET + index * RtcpReportBlock.SIZE_BYTES)
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
/**
* TODO: This is a stub implementation, so we can parse and ignore these packets.
*
* https://tools.ietf.org/html/rfc3611#section-2
*
* 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|reserved | PT=XR=207 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : report blocks :
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class RtcpXrPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
override fun clone(): RtcpXrPacket = RtcpXrPacket(cloneBuffer(0), 0, length)
companion object {
const val PT: Int = 207
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
/**
* Represents an RTCP packet whose packet types falls in the valid range
* (according to the spec), but isn't currently implemented/supported.
*/
class UnsupportedRtcpPacket(
buf: ByteArray,
offset: Int,
packetLengthBytes: Int
) : RtcpPacket(buf, offset, packetLengthBytes) {
override fun clone(): UnsupportedRtcpPacket = UnsupportedRtcpPacket(cloneBuffer(0), 0, length)
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb
import org.jitsi.rtp.extensions.bytearray.getInt
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.rtp.rtcp.RtcpHeader
import org.jitsi.rtp.rtcp.RtcpPacket
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.PayloadSpecificRtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbFirPacket
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbPliPacket
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.RtcpFbNackPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.TransportLayerRtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.ccfb.RtcpFbCcfbPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket
/**
* https://tools.ietf.org/html/rfc4585#section-6.1
* 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| FMT | PT | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : Feedback Control Information (FCI) :
* : :
*
* Note that an RTCP FB packet re-interprets the standard report count
* (RC) field of the RTCP header as a FMT field
*/
abstract class RtcpFbPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpPacket(buffer, offset, length) {
var mediaSourceSsrc: Long
get() = getMediaSourceSsrc(buffer, offset)
set(value) = setMediaSourceSsrc(buffer, offset, value)
companion object {
val PACKET_TYPES = listOf(TransportLayerRtcpFbPacket.PT, PayloadSpecificRtcpFbPacket.PT)
const val MEDIA_SOURCE_SSRC_OFFSET = RtcpHeader.SIZE_BYTES
const val HEADER_SIZE = RtcpHeader.SIZE_BYTES + 4
const val FCI_OFFSET = HEADER_SIZE
fun getFmt(buf: ByteArray, baseOffset: Int): Int = RtcpHeader.getReportCount(buf, baseOffset)
fun getMediaSourceSsrc(buf: ByteArray, baseOffset: Int): Long =
buf.getInt(baseOffset + MEDIA_SOURCE_SSRC_OFFSET).toPositiveLong()
fun setMediaSourceSsrc(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + MEDIA_SOURCE_SSRC_OFFSET, value.toInt())
fun parse(buf: ByteArray, offset: Int, length: Int): RtcpFbPacket {
val packetType = RtcpHeader.getPacketType(buf, offset)
val fmt = getFmt(buf, offset)
return when (packetType) {
TransportLayerRtcpFbPacket.PT -> {
when (fmt) {
RtcpFbNackPacket.FMT -> RtcpFbNackPacket(buf, offset, length)
RtcpFbTccPacket.FMT -> RtcpFbTccPacket(buf, offset, length)
RtcpFbCcfbPacket.FMT -> RtcpFbCcfbPacket(buf, offset, length)
else -> UnsupportedRtcpFbPacket(buf, offset, length)
}
}
PayloadSpecificRtcpFbPacket.PT -> {
when (fmt) {
RtcpFbFirPacket.FMT -> RtcpFbFirPacket(buf, offset, length)
RtcpFbPliPacket.FMT -> RtcpFbPliPacket(buf, offset, length)
RtcpFbRembPacket.FMT -> RtcpFbRembPacket(buf, offset, length)
else -> UnsupportedRtcpFbPacket(buf, offset, length)
}
}
else -> throw Exception("Unrecognized RTCPFB payload type: ${packetType.toString(16)}")
}
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb
/**
* Represents an RTCP feedback packet whose packet types falls in the valid range
* (according to the spec), but isn't currently implemented/supported.
*/
class UnsupportedRtcpFbPacket(
buf: ByteArray,
offset: Int,
packetLengthBytes: Int
) : RtcpFbPacket(buf, offset, packetLengthBytes) {
override fun clone(): UnsupportedRtcpFbPacket = UnsupportedRtcpFbPacket(cloneBuffer(0), 0, length)
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
abstract class PayloadSpecificRtcpFbPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpFbPacket(buffer, offset, length) {
companion object {
const val PT = 206
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb
import org.jitsi.rtp.extensions.bytearray.getInt
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
/**
*
* https://tools.ietf.org/html/rfc4585#section-6.1
* 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| FMT | PT | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Seq nr. | Reserved |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | .... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* https://tools.ietf.org/html/rfc5104#section-4.3.1.1
*
* The SSRC field in the FIR FCI block is used to set the media sender
* SSRC, the media source SSRC field in the RTCPFB header is unused for FIR packets.
* Within the common packet header for feedback messages (as defined in
* section 6.1 of [RFC4585]), the "SSRC of packet sender" field
* indicates the source of the request, and the "SSRC of media source"
* is not used and SHALL be set to 0. The SSRCs of the media senders to
* which the FIR command applies are in the corresponding FCI entries.
* A FIR message MAY contain requests to multiple media senders, using
* one FCI entry per target media sender.
*/
// TODO: support multiple FIR blocks
class RtcpFbFirPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : PayloadSpecificRtcpFbPacket(buffer, offset, length) {
override fun clone(): RtcpFbFirPacket = RtcpFbFirPacket(cloneBuffer(0), 0, length)
var mediaSenderSsrc: Long
get() = getMediaSenderSsrc(buffer, offset)
set(value) = setMediaSenderSsrc(buffer, offset, value)
var seqNum: Int
get() = getSeqNum(buffer, offset)
set(value) = setSeqNum(buffer, offset, value)
companion object {
const val FMT = 4
// TODO: support multiple FCI?
const val SIZE_BYTES = HEADER_SIZE + 8
const val MEDIA_SENDER_SSRC_OFFSET = FCI_OFFSET
const val SEQ_NUM_OFFSET = MEDIA_SENDER_SSRC_OFFSET + 4
fun getMediaSenderSsrc(buf: ByteArray, baseOffset: Int): Long =
buf.getInt(baseOffset + MEDIA_SENDER_SSRC_OFFSET).toPositiveLong()
fun setMediaSenderSsrc(buf: ByteArray, baseOffset: Int, value: Long) =
buf.putInt(baseOffset + MEDIA_SENDER_SSRC_OFFSET, value.toInt())
fun getSeqNum(buf: ByteArray, baseOffset: Int): Int = buf.get(baseOffset + SEQ_NUM_OFFSET).toPositiveInt()
fun setSeqNum(buf: ByteArray, baseOffset: Int, value: Int) =
buf.set(baseOffset + SEQ_NUM_OFFSET, value.toByte())
}
}
// TODO: we need an RtcpFbHeader so that we can fill out the mediaSourceSsrc for FB packets
class RtcpFbFirPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var mediaSenderSsrc: Long = -1,
var firCommandSeqNum: Int = -1
) {
fun build(): RtcpFbFirPacket {
val buf = BufferPool.getArray(RtcpFbFirPacket.SIZE_BYTES)
writeTo(buf, 0)
return RtcpFbFirPacket(buf, 0, RtcpFbFirPacket.SIZE_BYTES)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = PayloadSpecificRtcpFbPacket.PT
reportCount = RtcpFbFirPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(RtcpFbFirPacket.SIZE_BYTES)
}
rtcpHeader.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, 0) // SHALL be 0
RtcpFbFirPacket.setMediaSenderSsrc(buf, offset, mediaSenderSsrc)
RtcpFbFirPacket.setSeqNum(buf, offset, firCommandSeqNum)
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
/**
* https://tools.ietf.org/html/rfc4585#section-6.3.1
*
* 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| FMT | PT | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* PLI does not require parameters. Therefore, the length field MUST be
* 2, and there MUST NOT be any Feedback Control Information.
*/
class RtcpFbPliPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : PayloadSpecificRtcpFbPacket(buffer, offset, length) {
override fun clone(): RtcpFbPliPacket = RtcpFbPliPacket(cloneBuffer(0), 0, length)
companion object {
const val FMT = 1
const val SIZE_BYTES = HEADER_SIZE
}
}
class RtcpFbPliPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var mediaSourceSsrc: Long = -1
) {
fun build(): RtcpFbPliPacket {
val buf = BufferPool.getArray(RtcpFbPliPacket.SIZE_BYTES)
writeTo(buf, 0)
return RtcpFbPliPacket(buf, 0, RtcpFbPliPacket.SIZE_BYTES)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = PayloadSpecificRtcpFbPacket.PT
reportCount = RtcpFbPliPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(RtcpFbPliPacket.SIZE_BYTES)
}
rtcpHeader.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, mediaSourceSsrc)
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright @ 2019 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacket.Companion.BR_LEN
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacket.Companion.NUM_SSRC_LEN
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacket.Companion.REMB_LEN
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacket.Companion.getExpAndMantissa
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.getBitsAsInt
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getIntAsLong
import org.jitsi.rtp.util.getShortAsInt
/**
* https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
*
* 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| FMT=15 | PT=206 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Unique identifier 'R' 'E' 'M' 'B' |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Num SSRC | BR Exp | BR Mantissa |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC feedback |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ... |
*
* @author George Politis
* @author Boris Grozev
*/
class RtcpFbRembPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : PayloadSpecificRtcpFbPacket(buffer, offset, length) {
val bitrate: Long = getBitrate(buffer, offset)
val numSsrc: Int = getNumSsrc(buffer, offset)
/**
* one or more SSRC entries which this feedback message applies to.
*/
val ssrcs: List<Long> by lazy {
(0 until numSsrc).map {
getSsrc(buffer, offset, it)
}.toList()
}
override fun clone(): RtcpFbRembPacket = RtcpFbRembPacket(cloneBuffer(0), 0, length)
companion object {
const val FMT = 15
const val REMB_OFF = FCI_OFFSET
const val REMB_LEN = 4
const val NUM_SSRC_OFF = REMB_OFF + REMB_LEN
const val NUM_SSRC_LEN = 1
const val BR_OFF = NUM_SSRC_OFF + NUM_SSRC_LEN
const val BR_LEN = 3
const val SSRCS_OFF = BR_OFF + BR_LEN
fun getBrExp(buf: ByteArray, baseOffset: Int): Int = buf.getBitsAsInt(baseOffset + BR_OFF, 0, 6)
fun getBrMantissa(buf: ByteArray, baseOffset: Int): Int =
(buf.getBitsAsInt(baseOffset + BR_OFF, 6, 2) shl 16) + buf.getShortAsInt(baseOffset + BR_OFF + 1)
fun getBitrate(buf: ByteArray, baseOffset: Int): Long {
val mantissa = getBrMantissa(buf, baseOffset)
val exp = getBrExp(buf, baseOffset)
val brBps = mantissa.toLong() shl exp
if ((brBps shr exp).toInt() != mantissa) {
// This block catches a Java long overflow (i.e. the bitrate larger than Long.MAX_VALUE). Although this
// could potentially indicate a malformed remb or a bug in our code, we chose to interpret as the remote
// party trying to signal unbounded bandwidth.
return Long.MAX_VALUE
}
return brBps
}
fun getNumSsrc(buf: ByteArray, baseOffset: Int): Int = buf.getByteAsInt(baseOffset + NUM_SSRC_OFF)
fun getSsrc(buf: ByteArray, baseOffset: Int, ssrcIndex: Int) =
buf.getIntAsLong(baseOffset + SSRCS_OFF + ssrcIndex * 4)
fun getExpAndMantissa(brBps: Long): Pair<Int, Int> {
// 6 bit Exp
// 18 bit mantissa
var exp = 0
var mantissa = if (brBps > 0) brBps else 0
// 0x3ffff (262143) is the max value that can be put into an 18-bit unsigned integer
while (mantissa > 0x3ffff) {
mantissa = mantissa shr 1
++exp
}
return Pair(exp, mantissa.toInt())
}
fun setRemb(buf: ByteArray, baseOffset: Int) {
buf[baseOffset + REMB_OFF + 0] = 'R'.code.toByte()
buf[baseOffset + REMB_OFF + 1] = 'E'.code.toByte()
buf[baseOffset + REMB_OFF + 2] = 'M'.code.toByte()
buf[baseOffset + REMB_OFF + 3] = 'B'.code.toByte()
}
fun setNumSsrc(buf: ByteArray, off: Int, value: Int) {
buf[off + NUM_SSRC_OFF] = value.toByte()
}
fun setBrExp(buf: ByteArray, baseOffset: Int, value: Int) {
buf[baseOffset + BR_OFF] =
((buf[baseOffset + BR_OFF].toInt() and 0x03) or (value and 0x3f shl 2)).toByte()
}
fun setBrMantissa(buf: ByteArray, baseOffset: Int, value: Int) {
buf[baseOffset + BR_OFF] =
((buf[baseOffset + BR_OFF].toInt() and 0xfc) or ((value shr 16) and 0x03)).toByte()
buf[baseOffset + BR_OFF + 1] = ((value shr 8) and 0xff).toByte()
buf[baseOffset + BR_OFF + 2] = (value and 0xff).toByte()
}
fun setSsrcs(buf: ByteArray, baseOffset: Int, ssrcs: List<Long>) {
var ssrcsOff = baseOffset + SSRCS_OFF
ssrcs.forEach {
buf.putInt(ssrcsOff, it.toInt())
ssrcsOff += 4
}
}
}
}
class RtcpFbRembPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
val ssrcs: List<Long> = emptyList(),
val brBps: Long
) {
private val sizeBytes: Int = RtcpFbPacket.HEADER_SIZE +
REMB_LEN + NUM_SSRC_LEN + BR_LEN + ssrcs.size * 4
fun build(): RtcpFbRembPacket {
val buf = BufferPool.getArray(sizeBytes)
writeTo(buf, 0)
return RtcpFbRembPacket(buf, 0, sizeBytes)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = PayloadSpecificRtcpFbPacket.PT
reportCount = RtcpFbRembPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(sizeBytes)
}
rtcpHeader.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, 0)
RtcpFbRembPacket.setRemb(buf, offset)
RtcpFbRembPacket.setNumSsrc(buf, offset, ssrcs.size)
val expAndMantissa = getExpAndMantissa(brBps)
RtcpFbRembPacket.setBrExp(buf, offset, expAndMantissa.first)
RtcpFbRembPacket.setBrMantissa(buf, offset, expAndMantissa.second)
RtcpFbRembPacket.setSsrcs(buf, offset, ssrcs)
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb
import org.jitsi.rtp.extensions.bytearray.getShort
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import java.util.SortedSet
/**
* https://tools.ietf.org/html/rfc4585#section-6.2.1
*
* 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| FMT | PT | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | PID | BLP |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | (optional) PID | BLP |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
*/
class RtcpFbNackPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : TransportLayerRtcpFbPacket(buffer, offset, length) {
private val numNackBlocks: Int =
(packetLength - HEADER_SIZE) / NackBlock.SIZE_BYTES
val missingSeqNums: SortedSet<Int> by lazy {
(0 until numNackBlocks)
.map {
NackBlock.getMissingSeqNums(buffer, offset + NACK_BLOCK_OFFSET + it * NackBlock.SIZE_BYTES)
}
.flatten()
.toSortedSet()
}
override fun clone(): RtcpFbNackPacket = RtcpFbNackPacket(cloneBuffer(0), 0, length)
companion object {
const val FMT = 1
const val NACK_BLOCK_OFFSET = HEADER_SIZE
}
}
class RtcpFbNackPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var mediaSourceSsrc: Long = -1,
val missingSeqNums: SortedSet<Int> = sortedSetOf()
) {
private val nackBlocks: List<NackBlock> =
missingSeqNums.toList().chunkMaxDifference(16)
.map(List<Int>::toSortedSet)
.map(::NackBlock)
.toList()
private val sizeBytes: Int =
RtcpFbPacket.HEADER_SIZE + nackBlocks.size * NackBlock.SIZE_BYTES
fun build(): RtcpFbNackPacket {
val buf = BufferPool.getArray(sizeBytes)
writeTo(buf, 0)
return RtcpFbNackPacket(buf, 0, sizeBytes)
}
fun writeTo(buf: ByteArray, offset: Int) {
rtcpHeader.apply {
packetType = TransportLayerRtcpFbPacket.PT
reportCount = RtcpFbNackPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(sizeBytes)
}
rtcpHeader.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, mediaSourceSsrc)
nackBlocks.forEachIndexed { index, nackBlock ->
nackBlock.writeTo(buf, offset + RtcpFbNackPacket.NACK_BLOCK_OFFSET + index * NackBlock.SIZE_BYTES)
}
}
}
/**
* Return a List of Lists, where sub-list is made up of an ordered list
* of values pulled from [this], such that the difference between the
* first element and the last element is not more than [maxDifference]
*/
private fun List<Int>.chunkMaxDifference(maxDifference: Int): List<List<Int>> {
val chunks = mutableListOf<List<Int>>()
if (this.isEmpty()) {
return chunks
}
var currentChunk = mutableListOf(first())
chunks.add(currentChunk)
// Ignore the first value which we already put in the current chunk
drop(1).forEach {
if (it - currentChunk.first() > maxDifference) {
currentChunk = mutableListOf(it)
chunks.add(currentChunk)
} else {
currentChunk.add(it)
}
}
return chunks
}
/**
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | PID | BLP |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
private class NackBlock(
val missingSeqNums: SortedSet<Int> = sortedSetOf()
) {
fun writeTo(buf: ByteArray, offset: Int) {
putMissingSeqNums(buf, offset, missingSeqNums)
}
companion object {
const val SIZE_BYTES = 4
fun getMissingSeqNums(buf: ByteArray, offset: Int): List<Int> {
val packetId = buf.getShort(offset + 0).toPositiveInt()
val blp = buf.getShort(offset + 2).toPositiveInt()
val missingSeqNums = mutableListOf(packetId)
for (shiftAmount in 0..15) {
if (((blp ushr shiftAmount) and 0x1) == 1) {
missingSeqNums.add((packetId + shiftAmount + 1) and 0xffff)
}
}
return missingSeqNums
}
/**
* [missingSeqNums].last() - [missingSeqNums].first() MUST be <= 16
* [offset] should point to the start of where this NackBlock will go
*/
fun putMissingSeqNums(buf: ByteArray, offset: Int, missingSeqNums: SortedSet<Int>) {
val packetId = missingSeqNums.first()
buf.putShort(offset, packetId.toShort())
var blpField = 0
for (bitPos in 16 downTo 1) {
if ((bitPos + packetId) in missingSeqNums) {
blpField = blpField or 1
}
// Don't shift the last time
if (bitPos != 1) {
blpField = blpField shl 1
}
}
buf.putShort(offset + 2, blpField.toShort())
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
abstract class TransportLayerRtcpFbPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : RtcpFbPacket(buffer, offset, length) {
companion object {
const val PT = 205
}
}

View File

@@ -0,0 +1,323 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:enum-entry-name-case")
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.ccfb
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.TransportLayerRtcpFbPacket
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.getIntAsLong
import org.jitsi.rtp.util.getShortAsInt
import org.jitsi.utils.MAX_DURATION
import org.jitsi.utils.MIN_DURATION
import org.jitsi.utils.div
import org.jitsi.utils.secs
import org.jitsi.utils.toDouble
import java.time.Duration
import kotlin.math.min
/**
* This class is a port of CongestionControlFeedback in
* congestion_control_feedback.h/congestion_control_feedback.cc in Chrome
* https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/rtp_rtcp/source/rtcp_packet/congestion_control_feedback.h;rcl=eda3abcd0f44ed2e4c36586d35bdc06885abd25c
*
* Because of this, it explicitly does NOT try to conform
* to Kotlin style or idioms, instead striving to match the
* Chrome code as closely as possible in an effort to make
* future updates easier.
*/
enum class EcnMarking(val bits: Byte) {
kNotEct(0), // Not ECN-Capable Transport
kEct1(1), // ECN-Capable Transport
kEct0(2), // Not used by L4s (or webrtc.)
kCe(3); // Congestion experienced
companion object {
fun fromBits(bits: Byte): EcnMarking {
entries.forEach {
if (it.bits == bits) {
return it
}
}
throw IllegalArgumentException("Invalid ECNMarking value: $bits")
}
}
}
sealed class PacketInfo(
val ssrc: Long = 0,
val sequenceNumber: Int = 0
) {
override fun toString(): String = "ssrc=$ssrc, sequenceNumber=$sequenceNumber"
}
class UnreceivedPacketInfo(ssrc: Long = 0, sequenceNumber: Int = 0) : PacketInfo(ssrc, sequenceNumber) {
override fun toString(): String = "Unreceived: " + super.toString()
}
class ReceivedPacketInfo(
ssrc: Long = 0,
sequenceNumber: Int = 0,
val arrivalTimeOffset: Duration = MIN_DURATION,
val ecn: EcnMarking = EcnMarking.kNotEct
) : PacketInfo(ssrc, sequenceNumber) {
override fun toString(): String =
"Received: " + super.toString() + ", arrivalTimeOffset=$arrivalTimeOffset, ecn=$ecn"
}
private const val kHeaderPerMediaSsrcLength = 8
private const val kTimestampLength = 4
class RtcpFbCcfbPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var mediaSourceSsrc: Long = -1,
// `Packets` MUST be sorted in sequence_number order per SSRC. There MUST not
// be missing sequence numbers between `Packets`. `Packets` MUST not include
// duplicate sequence numbers.
val packets: List<PacketInfo> = listOf(),
val reportTimestampCompactNtp: Long
) {
private val groupedPackets = packets.groupBy { it.ssrc }
private fun blockLength(): Int {
var totalSize = RtcpFbPacket.HEADER_SIZE + kTimestampLength
groupedPackets.forEach { (ssrc, packets) ->
val blockSize = packets.size * 2
totalSize += kHeaderPerMediaSsrcLength + blockSize + RtpUtils.getNumPaddingBytes(blockSize)
}
return totalSize
}
fun writeTo(buf: ByteArray, offset: Int) {
val blockLength = blockLength()
assert(blockLength % 4 == 0) // Will never need padding
val packetSize = blockLength
require(buf.size - offset >= packetSize) {
"Buffer of size ${buf.size} with offset $offset too small for CCFB packet of size $packetSize"
}
rtcpHeader.apply {
packetType = TransportLayerRtcpFbPacket.PT
reportCount = RtcpFbCcfbPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(blockLength)
}.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, mediaSourceSsrc)
var position = offset + RtcpFbPacket.HEADER_SIZE
groupedPackets.forEach { (ssrc, packets) ->
check(packets.size <= 16384) {
"Unexpected number of reports: ${packets.size}"
}
buf.putInt(position, ssrc.toInt())
position += 4
val beginSeq = packets.first().sequenceNumber
val numReports = packets.size
buf.putShort(position, beginSeq.toShort())
position += 2
buf.putShort(position, numReports.toShort())
position += 2
packets.forEachIndexed { i, packet ->
val expectedSeq = RtpUtils.applySequenceNumberDelta(beginSeq, i)
check(packet.sequenceNumber == expectedSeq) {
"Sequence number for report $i of $ssrc is wrong " +
"(expected $expectedSeq == $beginSeq + $i, got ${packet.sequenceNumber})"
}
val packetInfo = if (packet is ReceivedPacketInfo) {
0x8000 or to2BitEcn(packet.ecn) or to13BitAto(packet.arrivalTimeOffset)
} else {
0
}
buf.putShort(position, packetInfo.toShort())
position += 2
}
repeat(RtpUtils.getNumPaddingBytes(2 * numReports)) {
buf[position++] = 0x00
}
}
buf.putInt(position, reportTimestampCompactNtp.toInt())
position += 4
assert(position - offset == packetSize)
}
fun build(): RtcpFbCcfbPacket {
val blockLength = blockLength()
val packetSize = blockLength + RtpUtils.getNumPaddingBytes(blockLength)
val buf = BufferPool.getArray(packetSize)
writeTo(buf, 0)
return RtcpFbCcfbPacket(buf, 0, packetSize)
}
}
// Arrival time offset (ATO, 13 bits):
// The arrival time of the RTP packet at the receiver, as an offset before the
// time represented by the Report Timestamp (RTS) field of this RTCP congestion
// control feedback report. The ATO field is in units of 1/1024 seconds (this
// unit is chosen to give exact offsets from the RTS field) so, for example, an
// ATO value of 512 indicates that the corresponding RTP packet arrived exactly
// half a second before the time instant represented by the RTS field. If the
// measured value is greater than 8189/1024 seconds (the value that would be
// coded as 0x1FFD), the value 0x1FFE MUST be reported to indicate an over-range
// measurement. If the measurement is unavailable or if the arrival time of the
// RTP packet is after the time represented by the RTS field, then an ATO value
// of 0x1FFF MUST be reported for the packet.
private fun to13BitAto(arrivalTimeOffset: Duration): Int {
if (arrivalTimeOffset.isNegative) {
return 0x1FFF
}
return min((1024 * arrivalTimeOffset.toDouble()).toInt(), 0x1FFE)
}
private fun atoToTimeDelta(receiveInfo: Int): Duration {
val ato = receiveInfo and 0x1FFF
if (ato == 0x1FFE) {
return MAX_DURATION
}
if (ato == 0x1FFF) {
return MIN_DURATION
}
return ato.secs / 1024
}
private fun toEcnMarking(receiveInfo: Int): EcnMarking = EcnMarking.fromBits(((receiveInfo shr 13) and 0b11).toByte())
private fun to2BitEcn(ecnMarking: EcnMarking): Int = ecnMarking.bits.toInt() shl 13
/*
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P| FMT=11 | PT = 205 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of RTCP packet sender |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of 1st RTP Stream |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| begin_seq | num_reports |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|ECN| Arrival time offset | ... .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
. .
. .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC of nth RTP Stream |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| begin_seq | num_reports |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|ECN| Arrival time offset | ... |
. .
. .
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Report Timestamp (32 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class RtcpFbCcfbPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : TransportLayerRtcpFbPacket(buffer, offset, length) {
/**
* Because much of time this packet is one that we built (not one
* that came in from the network) we don't care about re-parsing all
* of these fields. To avoid doing this work, we put them in this
* data class and make its initialization lazy: they'll only be parsed
* if we access them (which we do for packets that are received from
* the network but not for ones we send out).
*/
private data class CcfbData(
val packets: List<PacketInfo>,
val reportTimestampCompactNtp: Long
)
private val data: CcfbData by lazy(LazyThreadSafetyMode.NONE) {
val packets = mutableListOf<PacketInfo>()
require(packetLength - FCI_OFFSET >= kTimestampLength) {
"CCFB packet is too small for timestamp"
}
val reportTimestampCompactNtp = buffer.getIntAsLong(packetLength - kTimestampLength)
var position = offset + FCI_OFFSET
val end = offset + packetLength - kTimestampLength
while (position + kHeaderPerMediaSsrcLength < end) {
val ssrc = buffer.getIntAsLong(position)
position += 4
val baseSeqno = buffer.getShortAsInt(position)
position += 2
val numReports = buffer.getShortAsInt(position)
position += 2
require(position + 2 * numReports <= end) {
"Reports would extend past end of CCFB packet"
}
for (i in 0 until numReports) {
val packetInfo = buffer.getShortAsInt(position)
position += 2
val seqNo = RtpUtils.applySequenceNumberDelta(baseSeqno, i)
val received = (packetInfo and 0x8000) != 0
packets.add(
if (received) {
ReceivedPacketInfo(
ssrc = ssrc,
sequenceNumber = seqNo,
arrivalTimeOffset = atoToTimeDelta(packetInfo),
ecn = toEcnMarking(packetInfo)
)
} else {
UnreceivedPacketInfo(
ssrc = ssrc,
sequenceNumber = seqNo
)
}
)
}
if ((numReports % 2) != 0) {
// 2 bytes padding
position += 2
}
}
require(position == end) {
"CCFB packet size was incorrect"
}
CcfbData(packets, reportTimestampCompactNtp)
}
val packets: List<PacketInfo>
get() = data.packets
val reportTimestampCompactNtp: Long
get() = data.reportTimestampCompactNtp
override fun clone(): RtcpFbCcfbPacket = RtcpFbCcfbPacket(cloneBuffer(0), 0, length)
companion object {
const val FMT = 11
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc
import kotlin.math.min
// NOTE(brian): We have to store Chunk as an Int to avoid sign issues
typealias Chunk = Int
/**
* This class is a port of TransportFeedback::LastChunk in
* transport_feedback.h/transport_feedback.cc in Chrome
* https://cs.chromium.org/chromium/src/third_party/webrtc/modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h?l=95&rcl=20393ee9b7ba622f254908646a9c31bf87349fc7
*
* Because of this, it explicitly does NOT try to conform
* to Kotlin style or idioms, instead striving to match the
* Chrome code as closely as possible in an effort to make
* future updates easier.
*/
class LastChunk {
fun Empty(): Boolean = size_ == 0
fun Clear() {
size_ = 0
all_same_ = true
has_large_delta_ = false
}
// Return if delta sizes still can be encoded into single chunk with added
// |delta_size|.
fun CanAdd(deltaSize: DeltaSize): Boolean {
if (size_ < kMaxTwoBitCapacity) {
return true
}
if (size_ < kMaxOneBitCapacity && !has_large_delta_ && deltaSize != kLarge) {
return true
}
if (size_ < kMaxRunLengthCapacity && all_same_ && delta_sizes_[0] == deltaSize) {
return true
}
return false
}
// Add |delta_size|, assumes |CanAdd(delta_size)|,
fun Add(deltaSize: DeltaSize) {
if (size_ < kMaxVectorCapacity) {
delta_sizes_[size_] = deltaSize
}
size_++
all_same_ = all_same_ && deltaSize == delta_sizes_[0]
has_large_delta_ = has_large_delta_ || deltaSize == kLarge
}
// Encode chunk as large as possible removing encoded delta sizes.
// Assume CanAdd() == false for some valid delta_size.
fun Emit(): Chunk {
if (all_same_) {
val chunk = EncodeRunLength()
Clear()
return chunk
}
if (size_ == kMaxOneBitCapacity) {
val chunk = EncodeOneBit()
Clear()
return chunk
}
val chunk = EncodeTwoBit(kMaxTwoBitCapacity)
// Remove |kMaxTwoBitCapacity| encoded delta sizes:
// Shift remaining delta sizes and recalculate all_same_ && has_large_delta_.
size_ -= kMaxTwoBitCapacity
all_same_ = true
has_large_delta_ = false
for (i in 0 until size_) {
val deltaSize = delta_sizes_[kMaxTwoBitCapacity + i]
delta_sizes_[i] = deltaSize
all_same_ = all_same_ && deltaSize == delta_sizes_[0]
has_large_delta_ = has_large_delta_ || deltaSize == kLarge
}
return chunk
}
// // Encode all stored delta_sizes into single chunk, pad with 0s if needed.
fun EncodeLast(): Chunk = when {
all_same_ -> EncodeRunLength()
size_ <= kMaxTwoBitCapacity -> EncodeTwoBit(size_)
else -> EncodeOneBit()
}
// Decode up to |max_size| delta sizes from |chunk|.
fun Decode(chunk: Chunk, max_size: Int) {
if ((chunk and 0x8000) == 0) {
DecodeRunLength(chunk, max_size)
} else if ((chunk and 0x4000) == 0) {
DecodeOneBit(chunk, max_size)
} else {
DecodeTwoBit(chunk, max_size)
}
}
// Appends content of the Lastchunk to |deltas|.
fun AppendTo(deltas: MutableList<DeltaSize>) {
if (all_same_) {
repeat(size_) {
deltas.add(delta_sizes_[0])
}
} else {
for (i in 0 until size_) {
deltas.add(delta_sizes_[i])
}
}
}
// private:
/**
*
* Run Length Status Vector Chunk
*
* 0 1
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |T| S | Run Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* T = 0
* S = symbol
* Run Length = Unsigned integer denoting the run length of the symbol
*/
private fun EncodeRunLength(): Chunk = ((delta_sizes_[0] shl 13) or size_)
private fun DecodeRunLength(chunk: Chunk, max_count: Int) {
size_ = min(chunk and 0x1fff, max_count)
val delta_size = (chunk ushr 13) and 0x03
has_large_delta_ = delta_size >= kLarge
all_same_ = true
// To make it consistent with Add function, populate delta_sizes beyound 1st.
for (i in 0 until min(size_, kMaxVectorCapacity)) {
delta_sizes_[i] = delta_size
}
}
/**
* One Bit Status Vector Chunk
*
* 0 1
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |T|S| symbol list |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* T = 1
* S = 0
* Symbol list = 14 entries where 0 = not received, 1 = received 1-byte delta.
*/
private fun EncodeOneBit(): Chunk {
var chunk = 0x8000
for (i in 0 until size_) {
chunk = (chunk or (delta_sizes_[i] shl (kMaxOneBitCapacity - 1 - i)))
}
return chunk
}
private fun DecodeOneBit(chunk: Chunk, max_size: Int) {
size_ = min(kMaxOneBitCapacity, max_size)
has_large_delta_ = false
all_same_ = false
for (i in 0 until size_) {
delta_sizes_[i] = (chunk ushr (kMaxOneBitCapacity - 1 - i)) and 0x01
}
}
/**
* Two Bit Status Vector Chunk
*
* 0 1
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |T|S| symbol list |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* T = 1
* S = 1
* symbol list = 7 entries of two bits each.
*/
private fun EncodeTwoBit(size: Int): Chunk {
var chunk = 0xC000
for (i in 0 until size) {
chunk = (chunk or (delta_sizes_[i] shl (2 * (kMaxTwoBitCapacity - 1 - i))))
}
return chunk
}
private fun DecodeTwoBit(chunk: Chunk, max_size: Int) {
size_ = min(kMaxTwoBitCapacity, max_size)
has_large_delta_ = true
all_same_ = false
for (i in 0 until size_) {
delta_sizes_[i] = (chunk ushr (2 * (kMaxTwoBitCapacity - 1 - i))) and 0x03
}
}
private var size_: Int = 0
private var all_same_: Boolean = true
private var has_large_delta_: Boolean = false
private val delta_sizes_ = Array<DeltaSize>(kMaxVectorCapacity) { 0 }
companion object {
private const val kMaxRunLengthCapacity = 0x1FFF
private const val kMaxOneBitCapacity = 14
private const val kMaxTwoBitCapacity = 7
private const val kMaxVectorCapacity = kMaxOneBitCapacity
private const val kLarge: DeltaSize = 2
}
}

View File

@@ -0,0 +1,505 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.jitsi.rtp.extensions.bytearray.put3Bytes
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.rtp.extensions.unsigned.toPositiveShort
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.rtcp.rtcpfb.RtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.TransportLayerRtcpFbPacket
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kBaseScaleFactor
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kChunkSizeBytes
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kDeltaScaleFactor
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kMaxReportedPackets
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kMaxSizeBytes
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kTimeWrapPeriod
import org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc.RtcpFbTccPacket.Companion.kTransportFeedbackHeaderSizeBytes
import org.jitsi.rtp.rtp.RtpSequenceNumber
import org.jitsi.rtp.rtp.toRtpSequenceNumber
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.get3BytesAsInt
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getShortAsInt
import org.jitsi.utils.micros
import org.jitsi.utils.roundUpTo
import org.jitsi.utils.times
import org.jitsi.utils.toEpochMicro
import org.jitsi.utils.toMicros
import java.time.Duration
import java.time.Instant
sealed class PacketReport(val seqNum: Int)
class UnreceivedPacketReport(seqNum: Int) : PacketReport(seqNum)
// Size in bytes of a delta time in rtcp packet.
// Valid values are 0 (packet wasn't received), 1 or 2.
typealias DeltaSize = Int
class ReceivedPacketReport(seqNum: Int, val deltaTicks: Short) : PacketReport(seqNum) {
val deltaDuration: Duration
get() = deltaTicks.toInt() * kDeltaScaleFactor
}
/**
* This class is a port of TransportFeedback in
* transport_feedback.h/transport_feedback.cc in Chrome
* https://cs.chromium.org/chromium/src/third_party/webrtc/modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h?l=95&rcl=20393ee9b7ba622f254908646a9c31bf87349fc7
*
* Because of this, it explicitly does NOT try to conform
* to Kotlin style or idioms, instead striving to match the
* Chrome code as closely as possible in an effort to make
* future updates easier.
*/
@SuppressFBWarnings(
value = ["NM_METHOD_NAMING_CONVENTION"],
justification = "This class is a port and use the original names."
)
class RtcpFbTccPacketBuilder(
val rtcpHeader: RtcpHeaderBuilder = RtcpHeaderBuilder(),
var mediaSourceSsrc: Long = -1,
val feedbackPacketSeqNum: Int = -1
) {
var base_seq_no_: RtpSequenceNumber = RtpSequenceNumber.INVALID
private set
// The reference time, in ticks. Chrome passes this into BuildFeedbackPacket, but we don't
// hold the times in the same way, so we'll just assign it the first time we see
// a packet in AddReceivedPacket
private var base_time_ticks_: Long = -1
// The amount of packets_ whose status are represented
var num_seq_no_ = 0
private set
// The current chunk we're 'filling out' as packets
// are received
private var last_chunk_ = LastChunk()
// All but last encoded packet chunks.
private val encoded_chunks_ = mutableListOf<Chunk>()
// The size of the entire packet, in bytes
private var size_bytes_ = kTransportFeedbackHeaderSizeBytes
private var last_timestamp_: Instant = Instant.EPOCH
private val packets_ = mutableListOf<PacketReport>()
fun SetBase(base_sequence: Int, ref_timestamp: Instant) {
base_seq_no_ = base_sequence.toRtpSequenceNumber()
base_time_ticks_ = (ref_timestamp.toEpochMicro() % kTimeWrapPeriod.toMicros()) / kBaseScaleFactor.toMicros()
last_timestamp_ = BaseTime()
}
fun AddReceivedPacket(seqNum: Int, timestampIn: Instant): Boolean {
val sequence_number = seqNum.toRtpSequenceNumber()
var timestamp = timestampIn
if (last_timestamp_ > timestamp) {
timestamp += Duration.between(timestamp, last_timestamp_).roundUpTo(kTimeWrapPeriod)
}
var delta_full = Duration.between(last_timestamp_, timestamp).toMicros() % kTimeWrapPeriod.toMicros()
if (delta_full > kTimeWrapPeriod.toMicros() / 2) {
delta_full -= kTimeWrapPeriod.toMicros()
delta_full -= kDeltaScaleFactor.toMicros() / 2
} else {
delta_full += kDeltaScaleFactor.toMicros() / 2
}
delta_full /= kDeltaScaleFactor.toMicros()
val delta = delta_full.toShort()
// If larger than 16bit signed, we can't represent it - need new fb packet.
if (delta.toLong() != delta_full) {
return false
}
var next_seq_no = base_seq_no_ + num_seq_no_
if (sequence_number != next_seq_no) {
val lastSeqNo = next_seq_no - 1
if (sequence_number <= lastSeqNo) {
return false
}
while (next_seq_no != sequence_number) {
if (!AddDeltaSize(0)) {
return false
}
next_seq_no += 1
}
}
val delta_size = if (delta >= 0 && delta <= 0xff) 1 else 2
if (!AddDeltaSize(delta_size)) {
return false
}
packets_.add(ReceivedPacketReport(sequence_number.value, delta))
last_timestamp_ += delta.toInt() * kDeltaScaleFactor
size_bytes_ += delta_size
return true
}
fun BaseTime(): Instant = Instant.EPOCH + base_time_ticks_ * kBaseScaleFactor
private fun AddDeltaSize(deltaSize: DeltaSize): Boolean {
if (num_seq_no_ == kMaxReportedPackets) {
return false
}
val add_chunk_size = if (last_chunk_.Empty()) kChunkSizeBytes else 0
if (size_bytes_ + deltaSize + add_chunk_size > kMaxSizeBytes) {
return false
}
if (last_chunk_.CanAdd(deltaSize)) {
size_bytes_ += add_chunk_size
last_chunk_.Add(deltaSize)
++num_seq_no_
return true
}
if (size_bytes_ + deltaSize + kChunkSizeBytes > kMaxSizeBytes) {
return false
}
encoded_chunks_.add(last_chunk_.Emit())
size_bytes_ += kChunkSizeBytes
last_chunk_.Add(deltaSize)
++num_seq_no_
return true
}
fun build(): RtcpFbTccPacket {
val packetSize = size_bytes_ + RtpUtils.getNumPaddingBytes(size_bytes_)
val buf = BufferPool.getArray(packetSize)
writeTo(buf, 0)
return RtcpFbTccPacket(buf, 0, packetSize)
}
fun writeTo(buf: ByteArray, offset: Int) {
// NOTE: padding is held 'internally' in the TCC FCI, so we don't set
// the padding bit on the header
val paddingBytes = RtpUtils.getNumPaddingBytes(size_bytes_)
rtcpHeader.apply {
packetType = TransportLayerRtcpFbPacket.PT
reportCount = RtcpFbTccPacket.FMT
length = RtpUtils.calculateRtcpLengthFieldValue(size_bytes_ + paddingBytes)
}.writeTo(buf, offset)
RtcpFbPacket.setMediaSourceSsrc(buf, offset, mediaSourceSsrc)
RtcpFbTccPacket.setBaseSeqNum(buf, offset, base_seq_no_.value)
RtcpFbTccPacket.setPacketStatusCount(buf, offset, num_seq_no_)
RtcpFbTccPacket.setReferenceTimeTicks(buf, offset, base_time_ticks_.toInt())
RtcpFbTccPacket.setFeedbackPacketCount(buf, offset, feedbackPacketSeqNum)
var currOffset = RtcpFbTccPacket.PACKET_CHUNKS_OFFSET
encoded_chunks_.forEach {
buf.putShort(currOffset, it.toShort())
currOffset += kChunkSizeBytes
}
if (!last_chunk_.Empty()) {
val chunk = last_chunk_.EncodeLast()
buf.putShort(currOffset, chunk.toShort())
currOffset += kChunkSizeBytes
}
packets_.forEach {
if (it is ReceivedPacketReport) {
when (it.deltaTicks) {
in 0..0xFF -> buf[currOffset++] = it.deltaTicks.toByte()
else -> {
buf.putShort(currOffset, it.deltaTicks)
currOffset += 2
}
}
}
}
repeat(paddingBytes) {
buf[currOffset++] = 0x00
}
}
fun clear() {
num_seq_no_ = 0
size_bytes_ = kTransportFeedbackHeaderSizeBytes
}
}
/**
* NOTE(brian): This class is a port of the rest of the logic in TransportFeedback
* not covered by RtcpFbTccPacketBuilder. Chrome uses a single class for both
* the 'builder' and the 'parser' but because of the way we define packets
* (inheriting from the buffer type and therefore always requiring a valid buffer),
* we separate builders out into their own class. Because of that, this class
* and RtcpFbTccPacketBuilder have overlap in their members.
*
* https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-3.1
* 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| FMT=15 | PT=205 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of packet sender |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC of media source |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | base sequence number | packet status count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | reference time | fb pkt. count |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | packet chunk | packet chunk |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* . .
* . .
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | packet chunk | recv delta | recv delta |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* . .
* . .
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | recv delta | recv delta | zero padding |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* packet status count: 16 bits The number of packets this feedback
* contains status for, starting with the packet identified
* by the base sequence number.
*
* feedback packet count: 8 bits A counter incremented by one for each
* feedback packet sent. Used to detect feedback packet
* losses.
*/
@SuppressFBWarnings(
value = ["NM_METHOD_NAMING_CONVENTION"],
justification = "This class is a port and use the original names."
)
class RtcpFbTccPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : TransportLayerRtcpFbPacket(buffer, offset, length), Iterable<PacketReport> {
/**
* Because much of time this packet is one that we built (not one
* that came in from the network) we don't care about re-parsing all
* of these fields. To avoid doing this work, we put them in this
* data class and make its initialization lazy: they'll only be parsed
* if we access them (which we do for packets that are received from
* the network but not for ones we send out).
*/
private data class TccMemberData(
val base_seq_no_: Int,
var base_time_ticks_: Long,
val encoded_chunks_: MutableList<Chunk>,
var last_chunk_: LastChunk,
var num_seq_no_: Int,
var last_timestamp_: Instant,
val packets_: MutableList<PacketReport>
)
private val data: TccMemberData by lazy(LazyThreadSafetyMode.NONE) {
val base_seq_no_ = getBaseSeqNum(buffer, offset).toRtpSequenceNumber()
val status_count = getPacketStatusCount(buffer, offset)
val encoded_chunks_ = mutableListOf<Chunk>()
val last_chunk_ = LastChunk()
val num_seq_no_: Int
var last_timestamp_: Instant = Instant.EPOCH
val packets_ = mutableListOf<PacketReport>()
val base_time_ticks_ = getReferenceTimeTicks(buffer, offset)
val delta_sizes = mutableListOf<Int>()
var index = offset + PACKET_CHUNKS_OFFSET
val end_index = offset + length
while (delta_sizes.size < status_count) {
if (index + kChunkSizeBytes > end_index) {
throw Exception("Buffer overflow while parsing packet.")
}
val chunk = buffer.getShortAsInt(index)
index += kChunkSizeBytes
encoded_chunks_.add(chunk)
last_chunk_.Decode(chunk, status_count - delta_sizes.size)
last_chunk_.AppendTo(delta_sizes)
}
// Last chunk is stored in the |last_chunk_|.
encoded_chunks_.dropLast(1)
num_seq_no_ = status_count
var seq_no = base_seq_no_
var recv_delta_size = 0
for (delta_size in delta_sizes) {
recv_delta_size += delta_size
}
// Determine if timestamps, that is, recv_delta are included in the packet.
if (end_index >= index + recv_delta_size) {
for (delta_size in delta_sizes) {
if (index + delta_size > end_index) {
throw Exception("Buffer overflow while parsing packet.")
}
when (delta_size) {
0 -> packets_.add(UnreceivedPacketReport(seq_no.value))
1 -> {
val delta = buffer[index]
packets_.add(ReceivedPacketReport(seq_no.value, delta.toPositiveShort()))
last_timestamp_ += delta.toInt() * kDeltaScaleFactor
index += delta_size
}
2 -> {
val delta = buffer.getShortAsInt(index)
packets_.add(ReceivedPacketReport(seq_no.value, delta.toShort()))
last_timestamp_ += delta * kDeltaScaleFactor
index += delta_size
}
3 -> {
throw Exception("Warning: invalid delta size for seq_no $seq_no")
}
}
seq_no += 1
}
} else {
// The packet does not contain receive deltas
for (delta_size in delta_sizes) {
// Use delta sizes to detect if packet was received.
if (delta_size == 0) {
packets_.add(UnreceivedPacketReport(seq_no.value))
} else {
packets_.add(ReceivedPacketReport(seq_no.value, 0))
}
seq_no += 1
}
}
TccMemberData(
base_seq_no_.value,
base_time_ticks_,
encoded_chunks_,
last_chunk_,
num_seq_no_,
last_timestamp_,
packets_
)
}
// All but last encoded packet chunks.
private val encoded_chunks_: MutableList<Chunk>
get() = data.encoded_chunks_
// The current chunk we're 'filling out' as packets
// are received
private var last_chunk_: LastChunk
get() = data.last_chunk_
set(value) {
data.last_chunk_ = value
}
private val base_seq_no_: Int
get() = data.base_seq_no_
private var num_seq_no_: Int
get() = data.num_seq_no_
set(value) {
data.num_seq_no_ = value
}
private val packets_: MutableList<PacketReport>
get() = data.packets_
private var last_timestamp_: Instant
get() = data.last_timestamp_
set(value) {
data.last_timestamp_ = value
}
// The reference time, in ticks.
private var base_time_ticks_: Long
get() = data.base_time_ticks_
set(value) {
data.base_time_ticks_ = value
}
val feedbackSeqNum: Int = getFeedbackPacketCount(buffer, offset)
fun GetPacketStatusCount(): Int = num_seq_no_
fun BaseTime(): Instant = Instant.EPOCH + base_time_ticks_ * kBaseScaleFactor
fun GetBaseDelta(prev_timestamp: Instant): Duration {
var delta = Duration.between(prev_timestamp, BaseTime())
// Compensate for wrap around
if ((delta - kTimeWrapPeriod).abs() < delta.abs()) {
delta -= kTimeWrapPeriod
} else if ((delta + kTimeWrapPeriod).abs() < delta.abs()) {
delta += kTimeWrapPeriod
}
return delta
}
override fun iterator(): Iterator<PacketReport> = packets_.iterator()
override fun clone(): RtcpFbTccPacket = RtcpFbTccPacket(cloneBuffer(0), 0, length)
companion object {
const val FMT = 15
// Convert to multiples of 0.25ms
val kDeltaScaleFactor = 250.micros
// Maximum number of packets_ (including missing) TransportFeedback can report.
const val kMaxReportedPackets = 0xFFFF
const val kChunkSizeBytes = 2
// Fit TCC packets within an MTU and allow for further encapsulation (and perhaps compound RTCP)
const val kMaxSizeBytes = 1200
// Header size:
// * 4 bytes Common RTCP Packet Header
// * 8 bytes Common Packet Format for RTCP Feedback Messages
// * 8 bytes FeedbackPacket header
const val kTransportFeedbackHeaderSizeBytes = 4 + 8 + 8
// Used to convert from microseconds to multiples of 64ms
val kBaseScaleFactor = kDeltaScaleFactor * (1 shl 8)
// The reference time field is 24 bits and are represented as multiples of 64ms
// When the reference time field would need to wrap around
val kTimeWrapPeriod = (1 shl 24).toLong() * kBaseScaleFactor
const val BASE_SEQ_NUM_OFFSET = RtcpFbPacket.HEADER_SIZE
const val PACKET_STATUS_COUNT_OFFSET = RtcpFbPacket.HEADER_SIZE + 2
const val REFERENCE_TIME_OFFSET = RtcpFbPacket.HEADER_SIZE + 4
const val FB_PACKET_COUNT_OFFSET = RtcpFbPacket.HEADER_SIZE + 7
const val PACKET_CHUNKS_OFFSET = RtcpFbPacket.HEADER_SIZE + 8
// baseOffset in all of these refers to the start of the entire RTCP TCC packet
fun getBaseSeqNum(buf: ByteArray, baseOffset: Int): Int = buf.getShortAsInt(baseOffset + BASE_SEQ_NUM_OFFSET)
fun setBaseSeqNum(buf: ByteArray, baseOffset: Int, value: Int) =
buf.putShort(baseOffset + BASE_SEQ_NUM_OFFSET, value.toShort())
fun getPacketStatusCount(buf: ByteArray, baseOffset: Int): Int =
buf.getShortAsInt(baseOffset + PACKET_STATUS_COUNT_OFFSET)
fun setPacketStatusCount(buf: ByteArray, baseOffset: Int, value: Int) =
buf.putShort(baseOffset + PACKET_STATUS_COUNT_OFFSET, value.toShort())
fun getReferenceTimeTicks(buf: ByteArray, baseOffset: Int): Long =
buf.get3BytesAsInt(baseOffset + REFERENCE_TIME_OFFSET).toPositiveLong()
fun setReferenceTimeTicks(buf: ByteArray, baseOffset: Int, refTimeTicks: Int) =
buf.put3Bytes(baseOffset + REFERENCE_TIME_OFFSET, refTimeTicks)
fun getFeedbackPacketCount(buf: ByteArray, baseOffset: Int): Int =
buf.getByteAsInt(baseOffset + FB_PACKET_COUNT_OFFSET)
fun setFeedbackPacketCount(buf: ByteArray, baseOffset: Int, value: Int) =
buf.set(baseOffset + FB_PACKET_COUNT_OFFSET, value.toByte())
}
}

View File

@@ -0,0 +1,289 @@
package org.jitsi.rtp.rtp
import org.jitsi.rtp.Packet.Companion.BYTES_TO_LEAVE_AT_END_OF_PACKET
import org.jitsi.rtp.extensions.bytearray.getBitAsBool
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.rtp.RtpHeader.Companion.FIXED_HEADER_SIZE_BYTES
import org.jitsi.rtp.rtp.RtpPacket.Companion.BYTES_TO_LEAVE_AT_START_OF_PACKET
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils.Companion.getTimestampDiffAsInt
import org.jitsi.rtp.util.getBitsAsInt
import kotlin.IllegalArgumentException
import kotlin.experimental.and
import kotlin.experimental.or
/**
* Parses RED (RFC2198) packets.
*/
class RedPacketParser<PacketType : RtpPacket>(
/**
* A function to create packets from redundancy blocks (used so we can create a packet with the correct class).
*/
val createPacket: (ByteArray, Int, Int) -> PacketType
) {
/**
* Destructively parses a specific [RtpPacket] as RED. The redundant RED blocks are removed from the payload,
* leaving only the primary block as payload, and the payload type is changed to the payload type of the primary
* block. The transformed packet is no longer a RED packet, so the operation must not be performed again.
*
* If [parseRedundancy] is set, the redundancy blocks are parsed as [PacketType] into newly allocated buffers.
*
* @param rtpPacket the packet to parse
* @param parseRedundancy whether to parse redundancy packets
* @return The list of parsed redundancy packets.
*/
fun decapsulate(rtpPacket: RtpPacket, parseRedundancy: Boolean): List<PacketType> = with(rtpPacket) {
var currentOffset = payloadOffset
val redundancyBlockHeaders = mutableListOf<RedundancyBlockHeader>()
lateinit var primaryBlockHeader: PrimaryBlockHeader
do {
if (currentOffset >= offset + length) {
throw IllegalArgumentException(
"Invalid RED packet: no last header block found within the allowed length."
)
}
val blockHeader = BlockHeader.parse(buffer, currentOffset)
when (blockHeader) {
is PrimaryBlockHeader -> primaryBlockHeader = blockHeader
is RedundancyBlockHeader -> redundancyBlockHeaders.add(blockHeader)
}
currentOffset += blockHeader.headerLength
} while (blockHeader is RedundancyBlockHeader)
val packets = if (parseRedundancy) mutableListOf<PacketType>() else null
for ((index, it) in redundancyBlockHeaders.withIndex()) {
val blockLength = it.length
if (currentOffset + blockLength > offset + length) {
throw IllegalArgumentException("Invalid RED packet: blocks extend past the packet length.")
}
if (parseRedundancy) {
val byteArray = BufferPool.getArray(
BYTES_TO_LEAVE_AT_START_OF_PACKET +
FIXED_HEADER_SIZE_BYTES + blockLength +
BYTES_TO_LEAVE_AT_END_OF_PACKET
)
System.arraycopy(
buffer,
offset,
byteArray,
BYTES_TO_LEAVE_AT_START_OF_PACKET,
FIXED_HEADER_SIZE_BYTES
)
RtpHeader.setCsrcCount(byteArray, BYTES_TO_LEAVE_AT_START_OF_PACKET, 0)
RtpHeader.setHasExtensions(byteArray, BYTES_TO_LEAVE_AT_START_OF_PACKET, false)
System.arraycopy(
buffer,
currentOffset,
byteArray,
BYTES_TO_LEAVE_AT_START_OF_PACKET + FIXED_HEADER_SIZE_BYTES,
blockLength
)
val redundancyPacket =
createPacket(byteArray, BYTES_TO_LEAVE_AT_START_OF_PACKET, FIXED_HEADER_SIZE_BYTES + blockLength)
.apply {
payloadType = it.pt.toInt()
timestamp -= it.timestampOffset
// RFC2198 describes how to reconstruct the timestamp and payload type, but not an RTP
// sequence number. However, we operate on the RTP level and need to reconstruct an RTP
// packet. So we make the assumption that the redundant blocks represent the packets
// directly proceeding the primary.
sequenceNumber =
(sequenceNumber - (redundancyBlockHeaders.size - index)).toRtpSequenceNumber().value
}
packets?.add(redundancyPacket)
}
currentOffset += blockLength
}
// We've now read past the redundancy blocks, and currentOffset points to the payload of the primary block.
// Remove RED "in-place" by shifting the header.
val headerLength = headerLength
val newOffset = currentOffset - headerLength
val newLength = length - currentOffset + offset + headerLength
System.arraycopy(
buffer,
offset,
buffer,
newOffset,
headerLength
)
offset = newOffset
length = newLength
payloadType = primaryBlockHeader.pt.toInt()
return packets ?: emptyList()
}
}
sealed class BlockHeader(
/**
* The block's payload type.
*/
val pt: Byte
) {
abstract val headerLength: Int
abstract fun write(buffer: ByteArray, offset: Int): Int
companion object {
/**
* See RFC2198.
*
* Header blocks before the last (for redundant blocks):
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |1| block PT | timestamp offset | block length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* The last header block (for the primary block):
* 0
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |0| block PT |
* +-+-+-+-+-+-+-+-+
*
*/
fun parse(buffer: ByteArray, offset: Int): BlockHeader {
val follow = buffer.getBitAsBool(offset, 0)
val pt = buffer.getBitsAsInt(offset, 1, 7).toByte()
if (!follow) return PrimaryBlockHeader(pt)
val timestampOffset =
(buffer[offset + 1].toPositiveInt() shl 6) + buffer.getBitsAsInt(offset + 2, 0, 6)
val blockLength =
(buffer.getBitsAsInt(offset + 2, 6, 2) shl 8) + buffer[offset + 3].toPositiveInt()
return RedundancyBlockHeader(pt, timestampOffset, blockLength)
}
}
}
class PrimaryBlockHeader(pt: Byte) : BlockHeader(pt) {
override val headerLength = 1
override fun write(buffer: ByteArray, offset: Int): Int {
buffer[offset] = pt and 0x7f.toByte()
return 1
}
}
class RedundancyBlockHeader(
pt: Byte,
/**
* The (negative) offset of the timestamp relative to the RTP packet/primary block.
*/
val timestampOffset: Int,
/**
* The length of the block's payload in bytes.
*/
val length: Int
) : BlockHeader(pt) {
init {
require(timestampOffset <= MAX_TIMESTAMP_OFFSET) { "Invalid timestampOffset: $timestampOffset" }
}
override val headerLength = 4
override fun write(buffer: ByteArray, offset: Int): Int {
buffer[offset] = pt or 0x80.toByte()
buffer[offset + 1] = (timestampOffset shr 6).toByte()
buffer[offset + 2] = ((timestampOffset and 0x3f) shl 2).toByte() or ((length shr 8).toByte() and 0x03.toByte())
buffer[offset + 3] = (length and 0xff).toByte()
return 4
}
companion object {
/**
* The maximum value of the [timestampOffset] field (a 14-bit uint).
*/
const val MAX_TIMESTAMP_OFFSET = 0x3fff
}
}
internal class RtpRedPacket(buffer: ByteArray, offset: Int, length: Int) : RtpPacket(buffer, offset, length) {
override fun clone(): RtpRedPacket = RtpRedPacket(
cloneBuffer(BYTES_TO_LEAVE_AT_START_OF_PACKET),
BYTES_TO_LEAVE_AT_START_OF_PACKET,
length
).also { postClone(it) }
companion object {
val parser = RedPacketParser { b, o, l -> RtpPacket(b, o, l) }
val builder = RedPacketBuilder { b, o, l -> RtpRedPacket(b, o, l) }
}
fun decapsulate(parseRedundancy: Boolean) = parser.decapsulate(this, parseRedundancy)
}
class RedPacketBuilder<PacketType : RtpPacket>(val createPacket: (ByteArray, Int, Int) -> PacketType) {
/**
* Builds and returns a RED packet that contains [primary] as the primary encodings, and [redundancy] as secondary
* encodings.
* Note that this assumes the primary and secondary packets can be encoded in RED and throws as
* [IllegalArgumentException] otherwise (notably when the timestamp difference between the primary and any of the
* redundancy packets is too large).
*/
fun build(redPayloadType: Int, primary: RtpPacket, redundancy: List<RtpPacket>): PacketType {
val bytesNeeded = primary.length + 1 + redundancy.sumOf { it.payloadLength } + redundancy.size * 4
val buf = BufferPool.getArray(bytesNeeded + BYTES_TO_LEAVE_AT_START_OF_PACKET + BYTES_TO_LEAVE_AT_END_OF_PACKET)
var currentOffset = BYTES_TO_LEAVE_AT_START_OF_PACKET
val primaryHeaderLength = primary.headerLength
System.arraycopy(
primary.buffer,
primary.offset,
buf,
currentOffset,
primaryHeaderLength
)
currentOffset += primaryHeaderLength
var redHeaderOffset = currentOffset
// Skip the RED headers and point to the start of RED payloads
currentOffset += 1 + 4 * redundancy.size
redundancy.forEach {
val payloadLength = it.payloadLength
val header = RedundancyBlockHeader(
it.payloadType.toByte(),
getTimestampDiffAsInt(primary.timestamp, it.timestamp),
payloadLength
)
redHeaderOffset += header.write(buf, redHeaderOffset)
System.arraycopy(
it.buffer,
it.payloadOffset,
buf,
currentOffset,
payloadLength
)
currentOffset += payloadLength
}
val primaryHeader = PrimaryBlockHeader(primary.payloadType.toByte())
redHeaderOffset += primaryHeader.write(buf, redHeaderOffset)
System.arraycopy(
primary.buffer,
primary.payloadOffset,
buf,
currentOffset,
primary.payloadLength
)
return createPacket(buf, BYTES_TO_LEAVE_AT_START_OF_PACKET, bytesNeeded).apply {
payloadType = redPayloadType
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import org.jitsi.rtp.extensions.bytearray.putInt
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.rtp.header_extensions.HeaderExtensionHelpers
import org.jitsi.rtp.util.getIntAsLong
import org.jitsi.rtp.util.getShortAsInt
import kotlin.experimental.and
import kotlin.experimental.or
/**
* [RtpHeader] exists only as a set of helper methods to retrieve and set fields inside of
* a [ByteArray].
*
* It covers the RTP header fields until where the variability starts (the CSRCs list)
*
*
* https://tools.ietf.org/html/rfc3550#section-5.1
* 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 |
* | .... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ...extensions (if present)... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* @author Brian Baldino
*/
class RtpHeader {
companion object {
const val FIXED_HEADER_SIZE_BYTES = 12
const val CSRCS_OFFSET = 12
// The size of the RTP Extension header block
const val EXT_HEADER_SIZE_BYTES = 4
const val VERSION = 2
fun getVersion(buf: ByteArray, baseOffset: Int): Int = (buf[baseOffset].toInt() and 0xC0) ushr 6
fun setVersion(buf: ByteArray, baseOffset: Int, version: Int) {
buf[baseOffset] = ((buf[baseOffset].toInt() and 0xC0.inv()) or ((version shl 6) and 0xC0)).toByte()
}
fun hasPadding(buf: ByteArray, baseOffset: Int): Boolean = (buf[baseOffset].toInt() and 0x20) == 0x20
fun setPadding(buf: ByteArray, baseOffset: Int, hasPadding: Boolean) {
buf[baseOffset] = when (hasPadding) {
true -> (buf[baseOffset].toInt() or 0x20).toByte()
false -> (buf[baseOffset].toInt() and 0x20.inv()).toByte()
}
}
fun hasExtensions(buf: ByteArray, baseOffset: Int): Boolean = (buf[baseOffset].toInt() and 0x10) == 0x10
fun setHasExtensions(buf: ByteArray, baseOffset: Int, hasExtension: Boolean) {
buf[baseOffset] = when (hasExtension) {
true -> (buf[baseOffset].toInt() or 0x10).toByte()
false -> (buf[baseOffset].toInt() and 0x10.inv()).toByte()
}
}
fun getCsrcCount(buf: ByteArray, baseOffset: Int): Int = buf[baseOffset].toInt() and 0x0F
fun setCsrcCount(buf: ByteArray, baseOffset: Int, csrcCount: Int) {
buf[baseOffset] = ((buf[baseOffset].toInt() and 0xF0) or ((csrcCount and 0x0F))).toByte()
}
fun getMarker(buf: ByteArray, baseOffset: Int): Boolean = (buf[baseOffset + 1].toInt() and 0x80) == 0x80
fun setMarker(buf: ByteArray, baseOffset: Int, isSet: Boolean) {
buf[baseOffset + 1] = when (isSet) {
true -> (buf[baseOffset + 1].toInt() or 0x80).toByte()
false -> (buf[baseOffset + 1].toInt() and 0x80.inv()).toByte()
}
}
fun getPayloadType(buf: ByteArray, baseOffset: Int): Int =
(buf[baseOffset + 1].toInt() and 0x7F).toPositiveInt()
fun setPayloadType(buf: ByteArray, baseOffset: Int, payloadType: Int) {
buf[baseOffset + 1] = (buf[baseOffset + 1] and 0x80.toByte()) or (payloadType and 0x7F).toByte()
}
fun getSequenceNumber(buf: ByteArray, baseOffset: Int): Int = buf.getShortAsInt(baseOffset + 2)
fun setSequenceNumber(buf: ByteArray, baseOffset: Int, sequenceNumber: Int) {
buf.putShort(baseOffset + 2, sequenceNumber.toShort())
}
fun getTimestamp(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + 4)
fun setTimestamp(buf: ByteArray, baseOffset: Int, timestamp: Long) {
buf.putInt(baseOffset + 4, timestamp.toInt())
}
fun getSsrc(buf: ByteArray, baseOffset: Int): Long = buf.getIntAsLong(baseOffset + 8)
fun setSsrc(buf: ByteArray, baseOffset: Int, ssrc: Long) {
buf.putInt(baseOffset + 8, ssrc.toInt())
}
fun getCsrcs(buf: ByteArray, baseOffset: Int): List<Long> {
val numCsrcs = getCsrcCount(buf, baseOffset)
return (0 until numCsrcs).map { buf.getIntAsLong(CSRCS_OFFSET + (4 * it)) }.toList()
}
/**
* Assumes there is already proper room for the CSRCS. Also updates
* the CSRC count field.
*/
fun setCsrcs(buf: ByteArray, baseOffset: Int, csrcs: List<Long>) {
csrcs.forEachIndexed { index, csrc ->
buf.putInt(CSRCS_OFFSET + (4 * index), csrc.toInt())
}
setCsrcCount(buf, baseOffset, csrcs.size)
}
private fun getFixedHeaderAndCcLength(buf: ByteArray, baseOffset: Int) =
FIXED_HEADER_SIZE_BYTES + getCsrcCount(buf, baseOffset) * 4
/**
* The length of the entire RTP header, including any extensions, in bytes
*/
fun getTotalLength(buf: ByteArray, baseOffset: Int): Int {
val length = getFixedHeaderAndCcLength(buf, baseOffset)
val extLength = if (hasExtensions(buf, baseOffset)) {
// Length points to where the ext header would start
val extHeaderOffset = length
HeaderExtensionHelpers.getExtensionsTotalLength(buf, baseOffset + extHeaderOffset)
} else {
0
}
return length + extLength
}
/**
* The "defined by profile" header extension field. Only valid if hasExtensions is true, otherwise
* returns an invalid value (-1)
*/
fun getExtensionsProfileType(buf: ByteArray, baseOffset: Int): Int = if (hasExtensions(buf, baseOffset)) {
val extHeaderOffset = getFixedHeaderAndCcLength(buf, baseOffset)
HeaderExtensionHelpers.getExtensionsProfileType(buf, baseOffset + extHeaderOffset)
} else {
-1
}
}
}

View File

@@ -0,0 +1,622 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.jitsi.rtp.Packet
import org.jitsi.rtp.extensions.bytearray.hashCodeOfSegment
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.rtp.header_extensions.HeaderExtensionHelpers
import org.jitsi.rtp.rtp.header_extensions.HeaderExtensionParser
import org.jitsi.rtp.rtp.header_extensions.OneByteHeaderExtensionParser
import org.jitsi.rtp.rtp.header_extensions.TwoByteHeaderExtensionParser
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.RtpUtils
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.isPadding
/**
*
* https://tools.ietf.org/html/rfc3550#section-5.1
* 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 |
* | .... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ...extensions (if present)... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | payload |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
@SuppressFBWarnings(
value = ["EI_EXPOSE_REP2"],
justification = "We intentionally pass a reference to our buffer when using observableWhenChanged."
)
open class RtpPacket(
buffer: ByteArray,
offset: Int,
length: Int
) : Packet(buffer, offset, length) {
constructor(buffer: ByteArray) : this(buffer, 0, buffer.size)
var version: Int
get() = RtpHeader.getVersion(buffer, offset)
set(value) = RtpHeader.setVersion(buffer, offset, value)
var hasPadding: Boolean
get() = RtpHeader.hasPadding(buffer, offset)
set(value) = RtpHeader.setPadding(buffer, offset, value)
private var hasEncodedExtensions: Boolean
get() = RtpHeader.hasExtensions(buffer, offset)
set(value) = RtpHeader.setHasExtensions(buffer, offset, value)
val csrcCount: Int
get() = RtpHeader.getCsrcCount(buffer, offset)
var isMarked: Boolean
get() = RtpHeader.getMarker(buffer, offset)
set(value) = RtpHeader.setMarker(buffer, offset, value)
/* The four values below (payloadType, sequenceNumber, timestamp, and ssrc)
* are very frequently accessed in our pipeline; store their values in
* delegated properties, rather than re-reading them from the buffer every time.
*/
private var _payloadType: Int = RtpHeader.getPayloadType(buffer, offset)
var payloadType: Int
get() = _payloadType
set(newValue) {
if (newValue != _payloadType) {
RtpHeader.setPayloadType(this.buffer, this.offset, newValue)
_payloadType = newValue
}
}
private var _sequenceNumber: Int = RtpHeader.getSequenceNumber(buffer, offset)
var sequenceNumber: Int
get() = _sequenceNumber
set(newValue) {
if (newValue != _sequenceNumber) {
RtpHeader.setSequenceNumber(this.buffer, this.offset, newValue)
_sequenceNumber = newValue
}
}
private var _timestamp: Long = RtpHeader.getTimestamp(buffer, offset)
var timestamp: Long
get() = _timestamp
set(newValue) {
if (newValue != _timestamp) {
RtpHeader.setTimestamp(this.buffer, this.offset, newValue)
_timestamp = newValue
}
}
private var _ssrc: Long = RtpHeader.getSsrc(buffer, offset)
var ssrc: Long
get() = _ssrc
set(newValue) {
if (newValue != _ssrc) {
RtpHeader.setSsrc(this.buffer, this.offset, newValue)
_ssrc = newValue
}
}
val csrcs: List<Long>
get() = RtpHeader.getCsrcs(buffer, offset)
val extensionsProfileType: Int
get() = RtpHeader.getExtensionsProfileType(buffer, offset)
/**
* The length of the entire RTP header, including any extensions, in bytes
*/
var headerLength: Int = RtpHeader.getTotalLength(buffer, offset)
protected set
init {
if (headerLength > length) {
throw IllegalArgumentException("RTP packet header length $headerLength > length $length")
}
}
val payloadLength: Int
get() = length - headerLength
val payloadOffset: Int
get() = offset + headerLength
var paddingSize: Int
get() {
if (!hasPadding) {
return 0
}
// The last octet of the padding contains a count of how many
// padding octets should be ignored, including itself.
// It's an 8-bit unsigned number.
return buffer.getByteAsInt(offset + length - 1)
}
set(value) {
if (value > 0) {
hasPadding = true
buffer[offset + length - 1] = value.toByte()
} else {
hasPadding = false
}
}
/**
* The parser to use for header extensions, depending on the type of header extensions in this packet.
*/
private val headerExtensionParser
get() = HeaderExtensionHelpers.getHeaderExtensionParser(extensionsProfileType)
@field:Suppress("ktlint:standard:backing-property-naming")
private val _encodedHeaderExtensions: EncodedHeaderExtensions = EncodedHeaderExtensions()
private val encodedHeaderExtensions: EncodedHeaderExtensions
get() {
_encodedHeaderExtensions.reset()
return _encodedHeaderExtensions
}
/**
* For [RtpPacket] the payload is everything after the RTP Header.
*/
override val payloadVerification: String
get() = "type=RtpPacket len=$payloadLength " +
"hashCode=${buffer.hashCodeOfSegment(payloadOffset, payloadOffset + payloadLength)}"
private var pendingHeaderExtensions: MutableList<PendingHeaderExtension>? = null
private fun getEncodedHeaderExtension(extensionId: Int): HeaderExtension? {
if (!hasEncodedExtensions) return null
encodedHeaderExtensions.forEach { ext ->
if (ext.id == extensionId) {
return ext
}
}
return null
}
var hasExtensions: Boolean
get() = pendingHeaderExtensions?.isNotEmpty() ?: hasEncodedExtensions
set(value) {
val p = pendingHeaderExtensions
if (p != null) {
if (value && p.isEmpty()) {
throw java.lang.IllegalStateException(
"Cannot set hasExtensions to true with empty pending extensions"
)
}
if (!value) {
p.clear()
}
} else {
hasEncodedExtensions = value
}
}
fun getHeaderExtension(extensionId: Int): HeaderExtension? {
val activeHeaderExtensions = pendingHeaderExtensions?.iterator() ?: encodedHeaderExtensions
activeHeaderExtensions.forEach { ext ->
if (ext.id == extensionId) {
return ext
}
}
return null
}
private fun createPendingHeaderExtensions(removeIf: ((HeaderExtension) -> Boolean)?) {
if (pendingHeaderExtensions != null) {
return
}
check(!hasEncodedExtensions || headerExtensionParser != null) {
"Cannot modify header extensions for header extension type ${Integer.toHexString(extensionsProfileType)}"
}
pendingHeaderExtensions = ArrayList<PendingHeaderExtension>().also { l ->
encodedHeaderExtensions.forEach {
if (removeIf == null || !removeIf(it)) {
l.add(PendingHeaderExtension(it))
}
}
}
}
/**
* Removes the header extension (or all header extensions) with the given ID.
*/
fun removeHeaderExtension(id: Int) {
pendingHeaderExtensions?.removeIf { h -> h.id == id }
?: createPendingHeaderExtensions { h -> h.id == id }
}
/**
* Removes all header extensions except those with ID values in [retain]
*/
fun removeHeaderExtensionsExcept(retain: Set<Int>) {
pendingHeaderExtensions?.removeIf { h -> !retain.contains(h.id) }
?: createPendingHeaderExtensions { h -> !retain.contains(h.id) }
}
/**
* Adds an RTP header extension with ID [id] and data length [extDataLength] to this
* packet. The contents of the extension are not set to anything, and the
* caller of this method is responsible for filling them in via the
* [HeaderExtension] reference returned.
*
* This method MUST NOT be called while iterating over the extensions using
* {@link #getHeaderExtensions()}, or while manipulating the state of this
* {@link NewRawPacket}.
*
*/
fun addHeaderExtension(id: Int, extDataLength: Int): HeaderExtension {
require(id in 1..255) {
"Invalid header extension ID $id"
}
require(extDataLength in 0..255) {
"Invalid header extension length $extDataLength"
}
val newHeader = PendingHeaderExtension(id, extDataLength)
if (pendingHeaderExtensions == null) {
createPendingHeaderExtensions(null)
}
pendingHeaderExtensions!!.add(newHeader)
return newHeader
}
private fun pickParserForEncodedHeaders(
pendingHeaderExtensions: List<PendingHeaderExtension>
): HeaderExtensionParser {
pendingHeaderExtensions.forEach {
if (it.id >= 15 || it.dataLengthBytes == 0 || it.dataLengthBytes > 16) {
return TwoByteHeaderExtensionParser
}
}
return OneByteHeaderExtensionParser
}
fun encodeHeaderExtensions() {
val pendingHeaderExtensions = this.pendingHeaderExtensions ?: return
val newParser = pickParserForEncodedHeaders(pendingHeaderExtensions)
// The byte[] of an RtpPacket has the following structure:
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | A: unused | B: hdr + ext | C: payload | D: unused |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// And the regions have the following sizes:
// A: this.offset
// B: this.getHeaderLength()
// C: this.getPayloadLength()
// D: this.buffer.length - this.length - this.offset
// If the newly-encoded header extensions block results in a header
// longer than the current one,
// we will try to extend the packet so that it uses A and/or D if
// possible, in order to avoid allocating new memory.
// We get this early, before we modify the buffer.
val currHeaderLength = headerLength
val currPayloadLength = payloadLength
val baseHeaderLength = RtpHeader.FIXED_HEADER_SIZE_BYTES + csrcCount * 4
val newExtHeaderLength = if (pendingHeaderExtensions.isEmpty()) {
0
} else {
val rawHeaderLength = RtpHeader.EXT_HEADER_SIZE_BYTES +
pendingHeaderExtensions.sumOf { h -> newParser.extHeaderSizeBytes + h.dataLengthBytes }
rawHeaderLength + RtpUtils.getNumPaddingBytes(rawHeaderLength)
}
val newHeaderLength = baseHeaderLength + newExtHeaderLength
val newPacketLength = newHeaderLength + currPayloadLength
val newPayloadOffset: Int
val newBuffer = if (buffer.size >= (newPacketLength + BYTES_TO_LEAVE_AT_END_OF_PACKET)) {
// We don't need a new buffer
if ((offset + currHeaderLength) >= (newPacketLength - currPayloadLength)) {
// Region A (see above) is enough to accommodate the new
// packet, keep the payload where it is.
newPayloadOffset = payloadOffset
} else {
// We have to use region D, so move the payload all the way to the right
newPayloadOffset = buffer.size - currPayloadLength - BYTES_TO_LEAVE_AT_END_OF_PACKET
System.arraycopy(buffer, payloadOffset, buffer, newPayloadOffset, currPayloadLength)
}
buffer
} else {
// We need a new buffer. We will place the payload almost to the end
// (leaving room for an SRTP tag)
BufferPool.getArray(newPacketLength + BYTES_TO_LEAVE_AT_END_OF_PACKET).apply {
newPayloadOffset = size - currPayloadLength - BYTES_TO_LEAVE_AT_END_OF_PACKET
System.arraycopy(buffer, payloadOffset, this, newPayloadOffset, currPayloadLength)
}
}
val newOffset = newPayloadOffset - newHeaderLength
if (buffer !== newBuffer || offset != newOffset) {
// Copy the base header into place.
System.arraycopy(buffer, offset, newBuffer, newOffset, baseHeaderLength)
}
if (pendingHeaderExtensions.isNotEmpty()) {
var off = newOffset + baseHeaderLength
// Write the header extension
newBuffer.putShort(off, newParser.headerExtensionLabel.toShort())
newBuffer.putShort(off + 2, ((newExtHeaderLength - RtpHeader.EXT_HEADER_SIZE_BYTES) / 4).toShort())
off += 4
// Write pending header extension elements
pendingHeaderExtensions.forEach { h ->
val len = h.writeToBuffer(newBuffer, off, newParser)
off += len
}
// Write padding
while (off < newOffset + newHeaderLength) {
newBuffer[off] = 0
off++
}
}
val oldBuffer = buffer
buffer = newBuffer
// Reference comparison to see if we got a new buffer. If so, return the old one to the pool
if (oldBuffer !== newBuffer) {
BufferPool.returnArray(oldBuffer)
}
offset = newOffset
length = newPacketLength
headerLength = newHeaderLength
// ... and set the extension bit.
hasEncodedExtensions = pendingHeaderExtensions.isNotEmpty()
// Clear pending extensions.
this.pendingHeaderExtensions = null
}
override fun clone(): RtpPacket = RtpPacket(
cloneBuffer(BYTES_TO_LEAVE_AT_START_OF_PACKET),
BYTES_TO_LEAVE_AT_START_OF_PACKET,
length
).also { postClone(it) }
/** Extra operations that need to be done after [clone]. All subclasses overriding [clone]
* must call this method on the newly-created clone. */
protected fun postClone(clone: RtpPacket) {
pendingHeaderExtensions?.let { clone.pendingHeaderExtensions = ArrayList(it) }
}
override fun toString(): String = buildString {
append("${this@RtpPacket::class.java.simpleName}: ")
append("PT=$payloadType")
append(", Ssrc=$ssrc")
append(", SeqNum=$sequenceNumber")
append(", M=$isMarked")
append(", X=$hasEncodedExtensions")
append(", Ts=$timestamp")
}
/**
* Represents an RTP header extension.
*/
interface HeaderExtension {
val buffer: ByteArray
val dataOffset: Int
var id: Int
val dataLengthBytes: Int
val totalLengthBytes: Int
fun clone(): HeaderExtension = StandaloneHeaderExtension(this)
}
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
class StandaloneHeaderExtension(ext: HeaderExtension) : HeaderExtension {
override val buffer: ByteArray = ByteArray(ext.dataLengthBytes).also {
System.arraycopy(ext.buffer, ext.dataOffset, it, 0, ext.dataLengthBytes)
}
override val dataOffset = 0
override var id = ext.id
override val dataLengthBytes: Int
get() = buffer.size
override val totalLengthBytes: Int
get() = buffer.size
}
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
inner class EncodedHeaderExtension : HeaderExtension {
private var currExtOffset: Int = 0
private var currExtLength: Int = 0
override val dataLengthBytes: Int
get() = headerExtensionParser!!.getDataLengthBytes(buffer, currExtOffset)
override val buffer: ByteArray
get() = this@RtpPacket.buffer
override var id: Int
get() {
if (currExtLength <= 0) {
return -1
}
return headerExtensionParser!!.getId(buffer, currExtOffset)
}
set(newId) {
if (currExtLength < headerExtensionParser!!.minimumExtSizeBytes) {
throw IllegalStateException("Can't set ID: Header extension too short")
}
headerExtensionParser!!.writeIdAndLength(newId, dataLengthBytes, buffer, currExtOffset)
}
override val dataOffset: Int
get() = headerExtensionParser!!.extHeaderSizeBytes + currExtOffset
override val totalLengthBytes: Int
get() = headerExtensionParser!!.extHeaderSizeBytes + dataLengthBytes
fun setOffsetLength(nextHeaderExtOffset: Int, nextHeaderExtLength: Int) {
currExtOffset = nextHeaderExtOffset
currExtLength = nextHeaderExtLength
}
}
@SuppressFBWarnings(
value = ["EI_EXPOSE_REP", "CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE"],
justification = "We intentionally expose the internal buffer."
)
inner class PendingHeaderExtension(override var id: Int, override val dataLengthBytes: Int) : HeaderExtension {
override val buffer = ByteArray(dataLengthBytes)
override val dataOffset = 0
override val totalLengthBytes: Int
get() = dataLengthBytes + headerExtensionParser!!.extHeaderSizeBytes
constructor(other: HeaderExtension) : this(other.id, other.dataLengthBytes) {
System.arraycopy(other.buffer, other.dataOffset, buffer, 0, dataLengthBytes)
}
fun writeToBuffer(buffer: ByteArray, offset: Int, parser: HeaderExtensionParser): Int {
parser.writeIdAndLength(id, dataLengthBytes, buffer, offset)
System.arraycopy(
this.buffer,
dataOffset,
buffer,
offset + parser.extHeaderSizeBytes,
dataLengthBytes
)
return parser.extHeaderSizeBytes + dataLengthBytes
}
}
inner class EncodedHeaderExtensions : Iterator<HeaderExtension> {
/**
* The offset of the next extension
*/
private var nextOffset = 0
/**
* The remaining length of the extensions headers.
*/
private var remainingLength = 0
private val currHeaderExtension = EncodedHeaderExtension()
override fun hasNext(): Boolean {
if (headerExtensionParser == null) {
return false
}
// Consume any padding
while (remainingLength > 0 && buffer[nextOffset].isPadding()) {
nextOffset++
remainingLength--
}
if (remainingLength <= 0 || nextOffset < 0) {
return false
}
return getNextExtLength() > 0
}
override fun next(): HeaderExtension {
val nextExtLen = getNextExtLength()
if (nextExtLen <= 0) {
throw Exception("Invalid extension length. Did hasNext() return true?")
}
currHeaderExtension.setOffsetLength(nextOffset, nextExtLen)
nextOffset += nextExtLen
remainingLength -= nextExtLen
return currHeaderExtension
}
/**
* Return the entire length (including the header), in bytes, of the 'next' RTP extension
* according to [nextOffset]. If [remainingLength] is less than the minimum size for
* an extension, or the parsed length is larger than [remainingLength], return -1
*/
private fun getNextExtLength(): Int {
headerExtensionParser?.let { parser ->
if (remainingLength < parser.minimumExtSizeBytes) {
return -1
}
val extLen = parser.getEntireLengthBytes(buffer, nextOffset)
return if (extLen > remainingLength) -1 else extLen
} ?: run { return -1 }
}
/**
* Resets this iterator back to the beginning of the extensions
*/
internal fun reset() {
val extLength =
// This treats unknown header extension types as no extensions, which is what we want.
if (headerExtensionParser != null) {
val extensionBlockLength = HeaderExtensionHelpers.getExtensionsTotalLength(
buffer,
offset + RtpHeader.FIXED_HEADER_SIZE_BYTES + csrcCount * 4
)
extensionBlockLength - HeaderExtensionHelpers.TOP_LEVEL_EXT_HEADER_SIZE_BYTES
} else {
0
}
if (extLength <= 0) {
// No extensions
nextOffset = -1
remainingLength = -1
} else {
nextOffset = offset +
RtpHeader.FIXED_HEADER_SIZE_BYTES +
csrcCount * 4 +
RtpHeader.EXT_HEADER_SIZE_BYTES
remainingLength = extLength
}
}
}
companion object {
/**
* The size of the header for individual extensions. Currently we only
* support 1 byte header extensions
*/
const val HEADER_EXT_HEADER_SIZE = 1
/**
* How much space to leave in the beginning of new RTP packets. Having space in the beginning allows us to
* implement adding RTP header extensions efficiently (by keeping the RTP payload in place and shifting the
* header left).
*/
const val BYTES_TO_LEAVE_AT_START_OF_PACKET = 10
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import org.jitsi.rtp.util.RtpUtils
/**
* An inline class representing an RTP sequence number. The class operates just like
* an Int but takes rollover into account for all operations.
*
* This constructor assumes that the value is already coerced. It MUST NOT be used outside this class, but it can not
* be marked private. Use [Int.toRtpSequenceNumber] to create instances.
*/
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
@JvmInline
value class RtpSequenceNumber internal constructor(val value: Int) : Comparable<RtpSequenceNumber> {
// These are intentionally not implemented, because using them as operators leads to inconsistent results. The
// following code:
// var n1 = RtpSequenceNumber(65535)
// var n2 = RtpSequenceNumber(65535)
// ++n1
// n2++
// System.err.println("n1=$n1, n2=$n2")
//
// Produces this result:
// n1=RtpSequenceNumber(value=65536) n2=RtpSequenceNumber(value=0)
//
// Using "+= 1" yields the expected result. Note that if the same code above is in a "should" block, the result is
// different (as expected). So if/when this is brought back, it should be tested outside a regular "should" block!
//
// operator fun inc(): RtpSequenceNumber = plus(1)
// operator fun dec(): RtpSequenceNumber = minus(1)
operator fun plus(num: Int): RtpSequenceNumber = (value + num).toRtpSequenceNumber()
operator fun plus(seqNum: RtpSequenceNumber): RtpSequenceNumber = (value + seqNum.value).toRtpSequenceNumber()
operator fun minus(num: Int): RtpSequenceNumber = plus(-num)
operator fun minus(seqNum: RtpSequenceNumber): RtpSequenceNumber = plus(-seqNum.value)
override operator fun compareTo(other: RtpSequenceNumber): Int = RtpUtils.getSequenceNumberDelta(value, other.value)
operator fun rangeTo(other: RtpSequenceNumber) = RtpSequenceNumberProgression(this, other)
companion object {
val INVALID = RtpSequenceNumber(-1)
}
}
fun Int.toRtpSequenceNumber() = RtpSequenceNumber(this and 0xffff)
// Copied mostly from IntProgression.
// NOTE(brian): technically this should probably inherit from ClosedRange, but
// the inheritance causes issues with boxing and the inline types. See
// https://youtrack.jetbrains.com/issue/KT-30716 and the bug linked there.
class RtpSequenceNumberProgression(
val start: RtpSequenceNumber,
val endInclusive: RtpSequenceNumber,
val step: Int = 1
) : Iterable<RtpSequenceNumber> { // , ClosedRange<RtpSequenceNumber>
override fun iterator(): Iterator<RtpSequenceNumber> =
RtpSequenceNumberProgressionIterator(start, endInclusive, step)
companion object {
fun fromClosedRange(
rangeStart: RtpSequenceNumber,
rangeEnd: RtpSequenceNumber,
step: Int
): RtpSequenceNumberProgression = RtpSequenceNumberProgression(rangeStart, rangeEnd, step)
}
}
// Copied mostly from IntProgressionIterator
class RtpSequenceNumberProgressionIterator(
first: RtpSequenceNumber,
last: RtpSequenceNumber,
val step: Int
) : Iterator<RtpSequenceNumber> {
private val finalElement = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next = if (hasNext) first else finalElement
override fun hasNext(): Boolean = hasNext
override fun next(): RtpSequenceNumber = nextSeqNum()
fun nextSeqNum(): RtpSequenceNumber {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
} else {
next += step
}
return value
}
}
infix fun RtpSequenceNumber.downTo(to: RtpSequenceNumber): RtpSequenceNumberProgression =
RtpSequenceNumberProgression.fromClosedRange(this, to, -1)

View File

@@ -0,0 +1,71 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import org.jitsi.rtp.extensions.bytearray.put3Bytes
import org.jitsi.rtp.rtp.RtpPacket
import org.jitsi.rtp.util.getBitsAsInt
import org.jitsi.rtp.util.getShortAsInt
import java.time.Instant
/**
* https://webrtc.org/experiments/rtp-hdrext/abs-send-time/
*
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ID | LEN | AbsSendTime value |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class AbsSendTimeHeaderExtension {
companion object {
const val DATA_SIZE_BYTES = 3
/**
* One billion.
*/
private const val B = 1_000_000_000
fun setTime(ext: RtpPacket.HeaderExtension, timestampNanos: Long) =
setTime(ext.buffer, ext.dataOffset, timestampNanos)
private fun setTime(buf: ByteArray, offset: Int, timestampNanos: Long) {
val fraction = ((timestampNanos % B) * (1 shl 18) / B)
val seconds = ((timestampNanos / B) % 64) // 6 bits only
val timestamp = ((seconds shl 18) or fraction) and 0x00FFFFFF
buf.put3Bytes(offset, timestamp.toInt())
}
/**
* Gets the timestamp converted to nanoseconds.
*/
fun getTime(ext: RtpPacket.HeaderExtension): Instant = getTime(ext.buffer, ext.dataOffset)
private fun getTime(buf: ByteArray, dataOffset: Int): Instant {
val seconds = buf.getBitsAsInt(dataOffset, 0, 6)
val fraction = (
buf.getBitsAsInt(dataOffset, 6, 2) +
buf.getShortAsInt(dataOffset + 1)
).toDouble() / 0x03ffff
val instantMillis = Instant.ofEpochSecond(seconds.toLong())
return instantMillis.plusNanos((fraction * B).toLong())
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.rtp.RtpPacket
import kotlin.experimental.and
/**
* https://tools.ietf.org/html/rfc6464#section-3
* TODO: this can be held as either 1 byte or 2 byte. (though webrtc clients appear to all use 1 byte)
*
* 0 1
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ID | len=0 |V| level |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class AudioLevelHeaderExtension {
companion object {
private const val AUDIO_LEVEL_MASK = 0x7F.toByte()
fun getAudioLevel(ext: RtpPacket.HeaderExtension): Int = getAudioLevel(ext.buffer, ext.dataOffset)
private fun getAudioLevel(buf: ByteArray, offset: Int): Int = (buf[offset] and AUDIO_LEVEL_MASK).toPositiveInt()
fun getVad(ext: RtpPacket.HeaderExtension): Boolean = getVad(ext.buffer, ext.dataOffset)
private fun getVad(buf: ByteArray, offset: Int): Boolean = (buf[offset].toInt() and 0x80) != 0
}
}

View File

@@ -0,0 +1,936 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.jitsi.rtp.rtp.RtpPacket
import org.jitsi.rtp.util.BitReader
import org.jitsi.rtp.util.BitWriter
/**
* The subset of the fields of an AV1 Dependency Descriptor that can be parsed statelessly.
*/
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
open class Av1DependencyDescriptorStatelessSubset(
val startOfFrame: Boolean,
val endOfFrame: Boolean,
var frameDependencyTemplateId: Int,
var frameNumber: Int,
val newTemplateDependencyStructure: Av1TemplateDependencyStructure?,
) {
open fun clone(): Av1DependencyDescriptorStatelessSubset = Av1DependencyDescriptorStatelessSubset(
startOfFrame = startOfFrame,
endOfFrame = endOfFrame,
frameDependencyTemplateId = frameDependencyTemplateId,
frameNumber = frameNumber,
newTemplateDependencyStructure = newTemplateDependencyStructure?.clone()
)
}
/**
* The AV1 Dependency Descriptor header extension, as defined in https://aomediacodec.github.io/av1-rtp-spec/#appendix
*/
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
class Av1DependencyDescriptorHeaderExtension(
startOfFrame: Boolean,
endOfFrame: Boolean,
frameDependencyTemplateId: Int,
frameNumber: Int,
newTemplateDependencyStructure: Av1TemplateDependencyStructure?,
var activeDecodeTargetsBitmask: Int?,
val customDtis: List<DTI>?,
val customFdiffs: List<Int>?,
val customChains: List<Int>?,
val structure: Av1TemplateDependencyStructure
) : Av1DependencyDescriptorStatelessSubset(
startOfFrame,
endOfFrame,
frameDependencyTemplateId,
frameNumber,
newTemplateDependencyStructure
) {
val frameInfo: FrameInfo by lazy {
val templateIndex = (frameDependencyTemplateId + 64 - structure.templateIdOffset) % 64
if (templateIndex >= structure.templateCount) {
val maxTemplate = (structure.templateIdOffset + structure.templateCount - 1) % 64
throw Av1DependencyException(
"Invalid template ID $frameDependencyTemplateId. " +
"Should be in range ${structure.templateIdOffset} .. $maxTemplate. " +
"Missed a keyframe?"
)
}
val templateVal = structure.templateInfo[templateIndex]
FrameInfo(
spatialId = templateVal.spatialId,
temporalId = templateVal.temporalId,
dti = customDtis ?: templateVal.dti,
fdiff = customFdiffs ?: templateVal.fdiff,
chains = customChains ?: templateVal.chains
)
}
val encodedLength: Int
get() = (unpaddedLengthBits + 7) / 8
private val unpaddedLengthBits: Int
get() {
var length = 24
if (newTemplateDependencyStructure != null ||
activeDecodeTargetsBitmask != null ||
customDtis != null ||
customFdiffs != null ||
customChains != null
) {
length += 5
}
if (newTemplateDependencyStructure != null) {
length += newTemplateDependencyStructure.unpaddedLengthBits
}
if (activeDecodeTargetsBitmask != null &&
(
newTemplateDependencyStructure == null ||
activeDecodeTargetsBitmask != ((1 shl newTemplateDependencyStructure.decodeTargetCount) - 1)
)
) {
length += structure.decodeTargetCount
}
if (customDtis != null) {
length += 2 * structure.decodeTargetCount
}
if (customFdiffs != null) {
customFdiffs.forEach {
length += 2 + it.bitsForFdiff()
}
length += 2
}
if (customChains != null) {
length += 8 * customChains.size
}
return length
}
override fun clone(): Av1DependencyDescriptorHeaderExtension {
val structureCopy = structure.clone()
val newStructure = if (newTemplateDependencyStructure == null) null else structureCopy
return Av1DependencyDescriptorHeaderExtension(
startOfFrame,
endOfFrame,
frameDependencyTemplateId,
frameNumber,
newStructure,
activeDecodeTargetsBitmask,
// These values are not mutable so it's safe to copy them by reference
customDtis,
customFdiffs,
customChains,
structureCopy
)
}
fun write(ext: RtpPacket.HeaderExtension) = write(ext.buffer, ext.dataOffset, ext.dataLengthBytes)
fun write(buffer: ByteArray, offset: Int, length: Int) {
check(length <= encodedLength) {
"Cannot write AV1 DD to buffer: buffer length $length must be at least $encodedLength"
}
val writer = BitWriter(buffer, offset, length)
writeMandatoryDescriptorFields(writer)
if (newTemplateDependencyStructure != null ||
activeDecodeTargetsBitmask != null ||
customDtis != null ||
customFdiffs != null ||
customChains != null
) {
writeOptionalDescriptorFields(writer)
writePadding(writer)
} else {
check(length == 3) {
"AV1 DD without optional descriptors must be 3 bytes in length"
}
}
}
private fun writeMandatoryDescriptorFields(writer: BitWriter) {
writer.writeBit(startOfFrame)
writer.writeBit(endOfFrame)
writer.writeBits(6, frameDependencyTemplateId)
writer.writeBits(16, frameNumber)
}
private fun writeOptionalDescriptorFields(writer: BitWriter) {
val templateDependencyStructurePresent = newTemplateDependencyStructure != null
val activeDecodeTargetsPresent = activeDecodeTargetsBitmask != null &&
(
newTemplateDependencyStructure == null ||
activeDecodeTargetsBitmask != ((1 shl newTemplateDependencyStructure.decodeTargetCount) - 1)
)
val customDtisFlag = customDtis != null
val customFdiffsFlag = customFdiffs != null
val customChainsFlag = customChains != null
writer.writeBit(templateDependencyStructurePresent)
writer.writeBit(activeDecodeTargetsPresent)
writer.writeBit(customDtisFlag)
writer.writeBit(customFdiffsFlag)
writer.writeBit(customChainsFlag)
if (templateDependencyStructurePresent) {
newTemplateDependencyStructure.write(writer)
}
if (activeDecodeTargetsPresent) {
writeActiveDecodeTargets(writer)
}
if (customDtisFlag) {
writeFrameDtis(writer)
}
if (customFdiffsFlag) {
writeFrameFdiffs(writer)
}
if (customChainsFlag) {
writeFrameChains(writer)
}
}
private fun writeActiveDecodeTargets(writer: BitWriter) {
writer.writeBits(structure.decodeTargetCount, activeDecodeTargetsBitmask!!)
}
private fun writeFrameDtis(writer: BitWriter) {
customDtis!!.forEach { dti ->
writer.writeBits(2, dti.dti)
}
}
private fun writeFrameFdiffs(writer: BitWriter) {
customFdiffs!!.forEach { fdiff ->
val bits = fdiff.bitsForFdiff()
writer.writeBits(2, bits / 4)
writer.writeBits(bits, fdiff - 1)
}
writer.writeBits(2, 0)
}
private fun writeFrameChains(writer: BitWriter) {
customChains!!.forEach { chain ->
writer.writeBits(8, chain)
}
}
private fun writePadding(writer: BitWriter) {
writer.writeBits(writer.remainingBits, 0)
}
override fun toString(): String {
val mapper = ObjectMapper()
return JsonNodeFactory.instance.objectNode().apply {
put("startOfFrame", startOfFrame)
put("endOfFrame", endOfFrame)
put("frameDependencyTemplateId", frameDependencyTemplateId)
put("frameNumber", frameNumber)
newTemplateDependencyStructure?.let {
put("templateStructure", it.toString())
}
customDtis?.let {
set<com.fasterxml.jackson.databind.node.ArrayNode>(
"customDTIs",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ArrayNode>(it)
)
}
customFdiffs?.let {
set<com.fasterxml.jackson.databind.node.ArrayNode>(
"customFdiffs",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ArrayNode>(it)
)
}
customChains?.let {
set<com.fasterxml.jackson.databind.node.ArrayNode>(
"customChains",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ArrayNode>(it)
)
}
activeDecodeTargetsBitmask?.let {
if (newTemplateDependencyStructure == null ||
it != ((1 shl newTemplateDependencyStructure.decodeTargetCount) - 1)
) {
put("activeDecodeTargets", Integer.toBinaryString(it))
}
}
}.toString()
}
}
fun Int.bitsForFdiff() = when {
this <= 0x10 -> 4
this <= 0x100 -> 8
this <= 0x1000 -> 12
else -> throw IllegalArgumentException("Invalid FDiff value $this")
}
/**
* The template information about a stream described by AV1 dependency descriptors. This is carried in the
* first packet of a codec video sequence (i.e. the first packet of a keyframe), and is necessary to interpret
* dependency descriptors carried in subsequent packets of the sequence.
*/
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
class Av1TemplateDependencyStructure(
var templateIdOffset: Int,
val templateInfo: List<FrameInfo>,
val decodeTargetProtectedBy: List<Int>,
val decodeTargetLayers: List<DecodeTargetLayer>,
val maxRenderResolutions: List<Resolution>,
val maxSpatialId: Int,
val maxTemporalId: Int
) {
val templateCount
get() = templateInfo.size
val decodeTargetCount
get() = decodeTargetLayers.size
val chainCount: Int =
templateInfo.first().chains.size
init {
check(templateInfo.all { it.chains.size == chainCount }) {
"Templates have inconsistent chain sizes"
}
check(templateInfo.all { it.temporalId <= maxTemporalId }) {
"Incorrect maxTemporalId"
}
check(maxRenderResolutions.isEmpty() || maxRenderResolutions.size == maxSpatialId + 1) {
"Non-zero number of render resolutions does not match maxSpatialId"
}
check(templateInfo.all { it.spatialId <= maxSpatialId }) {
"Incorrect maxSpatialId"
}
}
val unpaddedLengthBits: Int
get() {
var length = 6 // Template ID offset
length += 5 // DT Count - 1
length += templateCount * 2 // templateLayers
length += templateCount * decodeTargetCount * 2 // TemplateDTIs
templateInfo.forEach {
length += it.fdiffCnt * 5 + 1 // TemplateFDiffs
}
// TemplateChains
length += nsBits(decodeTargetCount + 1, chainCount)
if (chainCount > 0) {
decodeTargetProtectedBy.forEach {
length += nsBits(chainCount, it)
}
length += templateCount * chainCount * 4
}
length += 1 // ResolutionsPresent
length += maxRenderResolutions.size * 32 // RenderResolutions
return length
}
fun clone(): Av1TemplateDependencyStructure = Av1TemplateDependencyStructure(
templateIdOffset,
// These objects are not mutable so it's safe to copy them by reference
templateInfo,
decodeTargetProtectedBy,
decodeTargetLayers,
maxRenderResolutions,
maxSpatialId,
maxTemporalId
)
fun write(writer: BitWriter) {
writer.writeBits(6, templateIdOffset)
writer.writeBits(5, decodeTargetCount - 1)
writeTemplateLayers(writer)
writeTemplateDtis(writer)
writeTemplateFdiffs(writer)
writeTemplateChains(writer)
writeRenderResolutions(writer)
}
private fun writeTemplateLayers(writer: BitWriter) {
check(templateInfo[0].spatialId == 0 && templateInfo[0].temporalId == 0) {
"First template must have spatial and temporal IDs 0/0, but found " +
"${templateInfo[0].spatialId}/${templateInfo[0].temporalId}"
}
for (templateNum in 1 until templateInfo.size) {
val layerIdc = when {
templateInfo[templateNum].spatialId == templateInfo[templateNum - 1].spatialId &&
templateInfo[templateNum].temporalId == templateInfo[templateNum - 1].temporalId ->
0
templateInfo[templateNum].spatialId == templateInfo[templateNum - 1].spatialId &&
templateInfo[templateNum].temporalId == templateInfo[templateNum - 1].temporalId + 1 ->
1
templateInfo[templateNum].spatialId == templateInfo[templateNum - 1].spatialId + 1 &&
templateInfo[templateNum].temporalId == 0 ->
2
else ->
throw IllegalStateException(
"Template $templateNum with spatial and temporal IDs " +
"${templateInfo[templateNum].spatialId}/${templateInfo[templateNum].temporalId} " +
"cannot follow template ${templateNum - 1} with spatial and temporal IDs " +
"${templateInfo[templateNum - 1].spatialId}/${templateInfo[templateNum - 1].temporalId}."
)
}
writer.writeBits(2, layerIdc)
}
writer.writeBits(2, 3)
}
private fun writeTemplateDtis(writer: BitWriter) {
templateInfo.forEach { t ->
t.dti.forEach { dti ->
writer.writeBits(2, dti.dti)
}
}
}
private fun writeTemplateFdiffs(writer: BitWriter) {
templateInfo.forEach { t ->
t.fdiff.forEach { fdiff ->
writer.writeBit(true)
writer.writeBits(4, fdiff - 1)
}
writer.writeBit(false)
}
}
private fun writeTemplateChains(writer: BitWriter) {
writer.writeNs(decodeTargetCount + 1, chainCount)
decodeTargetProtectedBy.forEach {
writer.writeNs(chainCount, it)
}
templateInfo.forEach { t ->
t.chains.forEach { chain ->
writer.writeBits(4, chain)
}
}
}
private fun writeRenderResolutions(writer: BitWriter) {
if (maxRenderResolutions.isEmpty()) {
writer.writeBit(false)
} else {
writer.writeBit(true)
maxRenderResolutions.forEach { r ->
writer.writeBits(16, r.width - 1)
writer.writeBits(16, r.height - 1)
}
}
}
/** Return whether, in this structure, it's possible to switch from DT [fromDt] to DT [toDt]
* without a keyframe.
* Note this makes certain assumptions about the encoding structure.
*/
fun canSwitchWithoutKeyframe(fromDt: Int, toDt: Int): Boolean = templateInfo.any {
it.hasInterPictureDependency() && it.dti.size > fromDt && it.dti.size > toDt &&
it.dti[fromDt] != DTI.NOT_PRESENT && it.dti[toDt] == DTI.SWITCH
}
/** Given that we are sending packets for a given DT, return a decodeTargetBitmask corresponding to all DTs
* contained in that DT.
*/
fun getDtBitmaskForDt(dt: Int): Int {
var mask = (1 shl decodeTargetCount) - 1
templateInfo.forEach { frameInfo ->
frameInfo.dti.forEachIndexed { i, dti ->
if (frameInfo.dti[dt] == DTI.NOT_PRESENT && dti != DTI.NOT_PRESENT) {
mask = mask and (1 shl i).inv()
}
}
}
return mask
}
override fun toString(): String {
val mapper = ObjectMapper()
return JsonNodeFactory.instance.objectNode().apply {
put("templateIdOffset", templateIdOffset)
set<com.fasterxml.jackson.databind.node.ObjectNode>(
"templateInfo",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(
templateInfo.map {
it.toString()
}.toIndexedMap()
)
)
set<com.fasterxml.jackson.databind.node.ObjectNode>(
"decodeTargetProtectedBy",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(
decodeTargetProtectedBy.toIndexedMap()
)
)
set<com.fasterxml.jackson.databind.node.ObjectNode>(
"decodeTargetLayers",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(
decodeTargetLayers.map {
it.toString()
}.toIndexedMap()
)
)
if (maxRenderResolutions.isNotEmpty()) {
set<com.fasterxml.jackson.databind.node.ObjectNode>(
"maxRenderResolutions",
mapper.valueToTree<com.fasterxml.jackson.databind.node.ObjectNode>(
maxRenderResolutions.map {
it.toString()
}.toIndexedMap()
)
)
}
put("maxSpatialId", maxSpatialId)
put("maxTemporalId", maxTemporalId)
}.toString()
}
}
fun nsBits(n: Int, v: Int): Int {
require(n > 0)
if (n == 1) return 0
var w = 0
var x = n
while (x != 0) {
x = x shr 1
w++
}
val m = (1 shl w) - n
if (v < m) return w - 1
return w
}
class Av1DependencyDescriptorReader(
buffer: ByteArray,
offset: Int,
val length: Int,
) {
private var startOfFrame = false
private var endOfFrame = false
private var frameDependencyTemplateId = 0
private var frameNumber = 0
private var customDtis: List<DTI>? = null
private var customFdiffs: List<Int>? = null
private var customChains: List<Int>? = null
private var localTemplateDependencyStructure: Av1TemplateDependencyStructure? = null
private var templateDependencyStructure: Av1TemplateDependencyStructure? = null
private var activeDecodeTargetsBitmask: Int? = null
private val reader = BitReader(buffer, offset, length)
constructor(ext: RtpPacket.HeaderExtension) :
this(ext.buffer, ext.dataOffset, ext.dataLengthBytes)
/** Parse those parts of the dependency descriptor that can be parsed statelessly, i.e. without an external
* template dependency structure. The returned object will not be a complete representation of the
* dependency descriptor, because some fields need the external structure to be parseable.
*/
fun parseStateless(): Av1DependencyDescriptorStatelessSubset {
reset()
readMandatoryDescriptorFields()
if (length > 3) {
val templateDependencyStructurePresent = reader.bitAsBoolean()
/* activeDecodeTargetsPresent, customDtisFlag, customFdiffsFlag, and customChainsFlag;
* none of these fields are parseable statelessly.
*/
reader.skipBits(4)
if (templateDependencyStructurePresent) {
localTemplateDependencyStructure = readTemplateDependencyStructure()
}
}
return Av1DependencyDescriptorStatelessSubset(
startOfFrame,
endOfFrame,
frameDependencyTemplateId,
frameNumber,
localTemplateDependencyStructure,
)
}
/** Parse the dependency descriptor in the context of [dep], the currently-applicable template dependency
* structure.*/
fun parse(dep: Av1TemplateDependencyStructure?): Av1DependencyDescriptorHeaderExtension {
reset()
readMandatoryDescriptorFields()
if (length > 3) {
readExtendedDescriptorFields(dep)
} else {
if (dep == null) {
throw Av1DependencyException("No external dependency structure specified for non-first packet")
}
templateDependencyStructure = dep
}
return Av1DependencyDescriptorHeaderExtension(
startOfFrame,
endOfFrame,
frameDependencyTemplateId,
frameNumber,
localTemplateDependencyStructure,
activeDecodeTargetsBitmask,
customDtis,
customFdiffs,
customChains,
templateDependencyStructure!!
)
}
private fun reset() = reader.reset()
private fun readMandatoryDescriptorFields() {
startOfFrame = reader.bitAsBoolean()
endOfFrame = reader.bitAsBoolean()
frameDependencyTemplateId = reader.bits(6)
frameNumber = reader.bits(16)
}
private fun readExtendedDescriptorFields(dep: Av1TemplateDependencyStructure?) {
val templateDependencyStructurePresent = reader.bitAsBoolean()
val activeDecodeTargetsPresent = reader.bitAsBoolean()
val customDtisFlag = reader.bitAsBoolean()
val customFdiffsFlag = reader.bitAsBoolean()
val customChainsFlag = reader.bitAsBoolean()
if (templateDependencyStructurePresent) {
localTemplateDependencyStructure = readTemplateDependencyStructure()
templateDependencyStructure = localTemplateDependencyStructure
} else {
if (dep == null) {
throw Av1DependencyException("No external dependency structure specified for non-first packet")
}
templateDependencyStructure = dep
}
if (activeDecodeTargetsPresent) {
activeDecodeTargetsBitmask = reader.bits(templateDependencyStructure!!.decodeTargetCount)
} else if (templateDependencyStructurePresent) {
activeDecodeTargetsBitmask = (1 shl templateDependencyStructure!!.decodeTargetCount) - 1
}
if (customDtisFlag) {
customDtis = readFrameDtis()
}
if (customFdiffsFlag) {
customFdiffs = readFrameFdiffs()
}
if (customChainsFlag) {
customChains = readFrameChains()
}
}
/* Data for template dependency structure */
private var templateIdOffset: Int = 0
private val templateInfo = mutableListOf<TemplateFrameInfo>()
private val decodeTargetProtectedBy = mutableListOf<Int>()
private val decodeTargetLayers = mutableListOf<DecodeTargetLayer>()
private val maxRenderResolutions = mutableListOf<Resolution>()
private var dtCnt = 0
private fun resetDependencyStructureInfo() {
/* These fields are assembled incrementally when parsing a dependency structure; reset them
* in case we're running a parser more than once.
*/
templateCnt = 0
templateInfo.clear()
decodeTargetProtectedBy.clear()
decodeTargetLayers.clear()
maxRenderResolutions.clear()
}
private fun readTemplateDependencyStructure(): Av1TemplateDependencyStructure {
resetDependencyStructureInfo()
templateIdOffset = reader.bits(6)
val dtCntMinusOne = reader.bits(5)
dtCnt = dtCntMinusOne + 1
readTemplateLayers()
readTemplateDtis()
readTemplateFdiffs()
readTemplateChains()
readDecodeTargetLayers()
val resolutionsPresent = reader.bitAsBoolean()
if (resolutionsPresent) {
readRenderResolutions()
}
return Av1TemplateDependencyStructure(
templateIdOffset,
templateInfo.toList(),
decodeTargetProtectedBy.toList(),
decodeTargetLayers.toList(),
maxRenderResolutions.toList(),
maxSpatialId,
maxTemporalId
)
}
private var templateCnt = 0
private var maxSpatialId = 0
private var maxTemporalId = 0
@SuppressFBWarnings(
value = ["SF_SWITCH_NO_DEFAULT"],
justification = "Artifact of generated Kotlin code."
)
private fun readTemplateLayers() {
var temporalId = 0
var spatialId = 0
var nextLayerIdc: Int
do {
templateInfo.add(templateCnt, TemplateFrameInfo(spatialId, temporalId))
templateCnt++
nextLayerIdc = reader.bits(2)
if (nextLayerIdc == 1) {
temporalId++
if (maxTemporalId < temporalId) {
maxTemporalId = temporalId
}
} else if (nextLayerIdc == 2) {
temporalId = 0
spatialId++
}
} while (nextLayerIdc != 3)
check(templateInfo.size == templateCnt)
maxSpatialId = spatialId
}
private fun readTemplateDtis() {
for (templateIndex in 0 until templateCnt) {
for (dtIndex in 0 until dtCnt) {
templateInfo[templateIndex].dti.add(dtIndex, DTI.fromInt(reader.bits(2)))
}
}
}
private fun readFrameDtis(): List<DTI> = List(templateDependencyStructure!!.decodeTargetCount) {
DTI.fromInt(reader.bits(2))
}
private fun readTemplateFdiffs() {
for (templateIndex in 0 until templateCnt) {
var fdiffCnt = 0
var fdiffFollowsFlag = reader.bitAsBoolean()
while (fdiffFollowsFlag) {
val fdiffMinusOne = reader.bits(4)
templateInfo[templateIndex].fdiff.add(fdiffCnt, fdiffMinusOne + 1)
fdiffCnt++
fdiffFollowsFlag = reader.bitAsBoolean()
}
check(fdiffCnt == templateInfo[templateIndex].fdiffCnt)
}
}
private fun readFrameFdiffs(): List<Int> = buildList {
var nextFdiffSize = reader.bits(2)
while (nextFdiffSize != 0) {
val fdiffMinus1 = reader.bits(4 * nextFdiffSize)
add(fdiffMinus1 + 1)
nextFdiffSize = reader.bits(2)
}
}
private fun readTemplateChains() {
val chainCount = reader.ns(dtCnt + 1)
if (chainCount != 0) {
for (dtIndex in 0 until dtCnt) {
decodeTargetProtectedBy.add(dtIndex, reader.ns(chainCount))
}
for (templateIndex in 0 until templateCnt) {
for (chainIndex in 0 until chainCount) {
templateInfo[templateIndex].chains.add(chainIndex, reader.bits(4))
}
check(templateInfo[templateIndex].chains.size == chainCount)
}
}
}
private fun readFrameChains(): List<Int> = List(templateDependencyStructure!!.chainCount) {
reader.bits(8)
}
private fun readDecodeTargetLayers() {
for (dtIndex in 0 until dtCnt) {
var spatialId = 0
var temporalId = 0
for (templateIndex in 0 until templateCnt) {
if (templateInfo[templateIndex].dti[dtIndex] != DTI.NOT_PRESENT) {
if (templateInfo[templateIndex].spatialId > spatialId) {
spatialId = templateInfo[templateIndex].spatialId
}
if (templateInfo[templateIndex].temporalId > temporalId) {
temporalId = templateInfo[templateIndex].temporalId
}
}
}
decodeTargetLayers.add(dtIndex, DecodeTargetLayer(spatialId, temporalId))
}
check(decodeTargetLayers.size == dtCnt)
}
private fun readRenderResolutions() {
for (spatialId in 0..maxSpatialId) {
val widthMinus1 = reader.bits(16)
val heightMinus1 = reader.bits(16)
maxRenderResolutions.add(spatialId, Resolution(widthMinus1 + 1, heightMinus1 + 1))
}
}
}
open class FrameInfo(
val spatialId: Int,
val temporalId: Int,
open val dti: List<DTI>,
open val fdiff: List<Int>,
open val chains: List<Int>
) {
val fdiffCnt
get() = fdiff.size
override fun equals(other: Any?): Boolean {
if (other !is FrameInfo) {
return false
}
return other.spatialId == spatialId &&
other.temporalId == temporalId &&
other.dti == dti &&
other.fdiff == fdiff &&
other.chains == chains
}
override fun hashCode(): Int {
var result = spatialId
result = 31 * result + temporalId
result = 31 * result + dti.hashCode()
result = 31 * result + fdiff.hashCode()
result = 31 * result + chains.hashCode()
return result
}
override fun toString(): String =
"spatialId=$spatialId, temporalId=$temporalId, dti=$dti, fdiff=$fdiff, chains=$chains"
fun toJson(): String {
val mapper = ObjectMapper()
return JsonNodeFactory.instance.objectNode().apply {
put("spatialId", spatialId)
put("temporalId", temporalId)
set<com.fasterxml.jackson.databind.node.ObjectNode>("dti", mapper.valueToTree(dti.map { it.name }))
set<com.fasterxml.jackson.databind.node.ArrayNode>("fdiff", mapper.valueToTree(fdiff))
set<com.fasterxml.jackson.databind.node.ArrayNode>("chains", mapper.valueToTree(chains))
}.toString()
}
/** Whether the frame has a dependency on a frame earlier than this "picture", the other frames of this
* temporal moment. If it doesn't, it's probably part of a keyframe, and not part of the regular structure.
* Note this makes assumptions about the scalability structure.
*/
fun hasInterPictureDependency(): Boolean = fdiff.any { it > spatialId }
val dtisPresent: List<Int>
get() = dti.withIndex().filter { (_, dti) -> dti != DTI.NOT_PRESENT }.map { (i, _) -> i }
}
/* The only thing this changes from its parent class is to make the lists mutable, so the parent equals() is fine. */
@SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS")
class TemplateFrameInfo(
spatialId: Int,
temporalId: Int,
override val dti: MutableList<DTI> = mutableListOf(),
override val fdiff: MutableList<Int> = mutableListOf(),
override val chains: MutableList<Int> = mutableListOf()
) : FrameInfo(spatialId, temporalId, dti, fdiff, chains)
class DecodeTargetLayer(
val spatialId: Int,
val temporalId: Int
) {
override fun toString(): String = JsonNodeFactory.instance.objectNode().apply {
put("spatialId", spatialId)
put("temporalId", temporalId)
}.toString()
}
data class Resolution(
val width: Int,
val height: Int
) {
override fun toString(): String = JsonNodeFactory.instance.objectNode().apply {
put("width", width)
put("height", height)
}.toString()
}
/** Decode target indication */
enum class DTI(val dti: Int) {
NOT_PRESENT(0),
DISCARDABLE(1),
SWITCH(2),
REQUIRED(3);
companion object {
private val map = DTI.values().associateBy(DTI::dti)
fun fromInt(type: Int) = map[type] ?: throw java.lang.IllegalArgumentException("Bad DTI $type")
}
fun toShortString(): String = when (this) {
NOT_PRESENT -> "N"
DISCARDABLE -> "D"
SWITCH -> "S"
REQUIRED -> "R"
}
}
fun List<DTI>.toShortString(): String = joinToString(separator = "") { it.toShortString() }
class Av1DependencyException(msg: String) : RuntimeException(msg)
fun <T> List<T>.toIndexedMap(): Map<Int, T> = mapIndexed { i, t -> i to t }.toMap()

View File

@@ -0,0 +1,136 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.util.getShortAsInt
import kotlin.experimental.and
import kotlin.experimental.or
/** An abstract parser for header extensions. */
abstract class HeaderExtensionParser {
abstract val headerExtensionLabel: Int
abstract val extHeaderSizeBytes: Int
abstract val minimumExtSizeBytes: Int
/** Determine whether this header extension parser can parse header extensions with the given
* "defined by profile" field.
*/
abstract fun isMatchingType(profileField: Int): Boolean
abstract fun getId(buf: ByteArray, offset: Int): Int
/**
* Write the given ID and length to [buf] at [offset]
*/
abstract fun writeIdAndLength(id: Int, dataLength: Int, buf: ByteArray, offset: Int)
/**
* Return the entire size, in bytes, of the extension in [buf] whose header
* starts at [offset]
*/
fun getEntireLengthBytes(buf: ByteArray, offset: Int): Int = getDataLengthBytes(buf, offset) + extHeaderSizeBytes
/**
* Return the data size, in bytes, of the extension in [buf] whose header
* starts at [offset].
*/
abstract fun getDataLengthBytes(buf: ByteArray, offset: Int): Int
}
object OneByteHeaderExtensionParser : HeaderExtensionParser() {
override val headerExtensionLabel = 0xBEDE
override val extHeaderSizeBytes = 1
override val minimumExtSizeBytes = 2
override fun isMatchingType(profileField: Int): Boolean = profileField == headerExtensionLabel
override fun getId(buf: ByteArray, offset: Int): Int = (buf[offset].toInt() ushr 4) and 0x0F
override fun writeIdAndLength(id: Int, dataLength: Int, buf: ByteArray, offset: Int) {
require(id in 1..14)
require(dataLength in 1..16)
buf[offset] = ((id and 0x0F) shl 4).toByte() or ((dataLength - 1) and 0x0F).toByte()
}
override fun getDataLengthBytes(buf: ByteArray, offset: Int): Int =
((buf[offset] and 0x0F.toByte())).toPositiveInt() + 1
}
object TwoByteHeaderExtensionParser : HeaderExtensionParser() {
/* We don't support "value 256", in the low four bits of the "defined by profile" field. */
override val headerExtensionLabel = 0x1000
private const val HEADER_EXTENSION_MASK = 0xFFF0
override val extHeaderSizeBytes = 2
override val minimumExtSizeBytes = 2
override fun isMatchingType(profileField: Int): Boolean =
(profileField and HEADER_EXTENSION_MASK) == headerExtensionLabel
override fun getId(buf: ByteArray, offset: Int): Int = buf[offset].toInt()
override fun writeIdAndLength(id: Int, dataLength: Int, buf: ByteArray, offset: Int) {
require(id in 1..255)
require(dataLength in 0..255)
buf[offset] = id.toByte()
buf[offset + 1] = dataLength.toByte()
}
override fun getDataLengthBytes(buf: ByteArray, offset: Int): Int = buf[offset + 1].toPositiveInt()
}
private val headerExtensionParsers = arrayOf(OneByteHeaderExtensionParser, TwoByteHeaderExtensionParser)
class HeaderExtensionHelpers {
companion object {
// The size of the header extension block header
const val TOP_LEVEL_EXT_HEADER_SIZE_BYTES = 4
/**
* Return the "defined by profile" header extension type field, as an integer.
*
* [offset] points to the start of the header extensions block. Should
* only be called if it's been verified that the header held in [buf]
* actually contains extensions
*/
fun getExtensionsProfileType(buf: ByteArray, offset: Int) = buf.getShortAsInt(offset)
/**
* Return the length of the entire header extensions block, including
* the header, in bytes.
*
* [offset] points to the start of the header extensions block. Should
* only be called if it's been verified that the header held in [buf]
* actually contains extensions
*/
fun getExtensionsTotalLength(buf: ByteArray, offset: Int) =
TOP_LEVEL_EXT_HEADER_SIZE_BYTES + buf.getShortAsInt(offset + 2) * 4
/**
* Return a header extension parser for header extensions with the given "defined by profile" field
* value, or null if none match (i.e. either cryptex-encrypted or proprietary header extensions).
* Does the right thing for the invalid value -1 returned by [RtpHeader.getExtensionsProfileType]
* when no extensions are present.
*/
fun getHeaderExtensionParser(profileField: Int): HeaderExtensionParser? =
headerExtensionParsers.firstOrNull { it.isMatchingType(profileField) }
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright @ 2021 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import org.jitsi.rtp.rtp.RtpPacket
import java.nio.charset.StandardCharsets
/**
* https://datatracker.ietf.org/doc/html/rfc7941#section-4.1.1
* Note: this is only the One-Byte Format, because we don't support Two-Byte yet.
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ID | len | SDES item text value ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class SdesHeaderExtension {
companion object {
fun getTextValue(ext: RtpPacket.HeaderExtension): String =
getTextValue(ext.buffer, ext.dataOffset, ext.dataLengthBytes)
fun setTextValue(ext: RtpPacket.HeaderExtension, sdesValue: String) {
assert(ext.dataLengthBytes == sdesValue.length) { "buffer size doesn't match SDES value length" }
setTextValue(ext.buffer, ext.dataOffset, sdesValue)
}
private fun getTextValue(buf: ByteArray, offset: Int, dataLength: Int): String {
/* RFC 7941 says the value in RTP is UTF-8. But we use this for MID and RID values
* which are define for SDP in RFC 5888 and RFC 4566 as ASCII only. Thus we don't
* support UTF-8 to keep things simpler. */
return String(buf, offset, dataLength, StandardCharsets.US_ASCII)
}
private fun setTextValue(buf: ByteArray, offset: Int, sdesValue: String) {
System.arraycopy(
sdesValue.toByteArray(StandardCharsets.US_ASCII),
0,
buf,
offset,
sdesValue.length
)
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.rtp.RtpPacket
import org.jitsi.rtp.util.getShortAsInt
/**
* https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#section-2.2
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | ID | L=1 |transport-wide sequence number | zero padding |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
class TccHeaderExtension {
companion object {
const val DATA_SIZE_BYTES = 2
fun getSequenceNumber(ext: RtpPacket.HeaderExtension): Int = getSequenceNumber(ext.buffer, ext.dataOffset)
fun setSequenceNumber(ext: RtpPacket.HeaderExtension, tccSeqNum: Int) {
setSequenceNumber(ext.buffer, ext.dataOffset, tccSeqNum)
}
private fun getSequenceNumber(buf: ByteArray, offset: Int): Int = buf.getShortAsInt(offset)
private fun setSequenceNumber(buf: ByteArray, offset: Int, seqNum: Int) {
buf.putShort(offset, seqNum.toShort())
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright @ 2024-Present 8x8, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.jitsi.rtp.rtp.RtpPacket
import org.jitsi.rtp.rtp.header_extensions.VlaExtension.Stream
import org.jitsi.rtp.util.BitReader
/**
* A parser for the Video Layers Allocation RTP header extension.
* https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/video-layers-allocation00
*/
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT", justification = "False positive")
class VlaExtension {
companion object {
fun parse(ext: RtpPacket.HeaderExtension): ParsedVla {
val empty = ext.dataLengthBytes == 1 && ext.buffer[ext.dataOffset] == 0.toByte()
if (empty) {
return emptyList()
}
val reader = BitReader(ext.buffer, ext.dataOffset, ext.dataLengthBytes)
reader.skipBits(2) // RID
val ns = reader.bits(2) + 1
val slBm = reader.bits(4)
val slBms = IntArray(4) { i -> if (i < ns) slBm else 0 }
if (slBm == 0) {
slBms[0] = reader.bits(4)
if (ns > 1) {
slBms[1] = reader.bits(4)
if (ns > 2) {
slBms[2] = reader.bits(4)
if (ns > 3) {
slBms[3] = reader.bits(4)
}
}
}
if (ns == 1 || ns == 3) {
reader.skipBits(4)
}
}
val slCount = slBms.sumOf { it.countOneBits() }
val tlCountLenBytes = slCount / 4 + if (slCount % 4 != 0) 1 else 0
val tlCountReader = reader.clone(tlCountLenBytes)
reader.skipBits(tlCountLenBytes * 8)
val streams = ArrayList<Stream>(ns)
(0 until ns).forEach { streamIdx ->
val spatialLayers = ArrayList<SpatialLayer>()
val stream = Stream(streamIdx, spatialLayers)
streams.add(stream)
(0 until 4).forEach { slIdx ->
if ((slBms[streamIdx] and (1 shl slIdx)) != 0) {
val targetBitrates = buildList {
repeat(tlCountReader.bits(2) + 1) {
add(reader.leb128())
}
}
spatialLayers.add(
SpatialLayer(
slIdx,
targetBitrates,
null
)
)
}
}
}
(0 until ns).forEach outer@{ streamIdx ->
(0 until 4).forEach { slIdx ->
if ((slBms[streamIdx] and (1 shl slIdx)) != 0) {
if (reader.remainingBits() < 40) {
return@outer
}
val sl = streams[streamIdx].spatialLayers[slIdx]
sl.res = ResolutionAndFrameRate(
reader.bits(16) + 1,
reader.bits(16) + 1,
reader.bits(8)
)
}
}
}
return streams
}
}
data class ResolutionAndFrameRate(
val width: Int,
val height: Int,
val maxFramerate: Int
)
data class SpatialLayer(
val id: Int,
// The target bitrates for each temporal layer in this spatial layer
val targetBitratesKbps: List<Long>,
var res: ResolutionAndFrameRate?
) {
override fun toString(): String = "SpatialLayer(id=$id, targetBitratesKbps=$targetBitratesKbps, " +
"width=${res?.width}, height=${res?.height}, maxFramerate=${res?.maxFramerate})"
}
data class Stream(
val id: Int,
val spatialLayers: List<SpatialLayer>
)
}
typealias ParsedVla = List<Stream>

View File

@@ -0,0 +1,135 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import kotlin.experimental.and
/**
* Read individual bits, and unaligned sets of bits, from a [ByteArray], with an incrementing offset.
*/
/* TODO: put this in jitsi-utils? */
class BitReader(val buf: ByteArray, private val byteOffset: Int = 0, private val byteLength: Int = buf.size) {
private var offset = byteOffset * 8
private val byteBound = byteOffset + byteLength
init {
check(byteOffset >= 0) { "byteOffset must be >= 0" }
check(byteBound <= buf.size) { "byteOffset + byteLength must be <= buf.size" }
}
/** Clone with the current state (offset) and a new length in bytes. */
fun clone(newByteLength: Int): BitReader {
check(offset % 8 == 0) { "Cannot clone BitReader with unaligned offset" }
check(offset / 8 + newByteLength <= byteBound) {
"newByteLength $newByteLength exceeds buffer length $byteLength after offset $byteOffset"
}
return BitReader(buf, offset / 8, newByteLength)
}
fun remainingBits(): Int = byteBound * 8 - offset
/** Read a single bit from the buffer, as a boolean, incrementing the offset. */
fun bitAsBoolean(): Boolean {
val byteIdx = offset / 8
val bitIdx = offset % 8
check(byteIdx < byteBound) {
"offset $offset ($byteIdx/$bitIdx) invalid in buffer of length $byteLength after offset $byteOffset"
}
val byte = buf[byteIdx]
val mask = (1 shl (7 - bitIdx)).toByte()
offset++
return (byte and mask) != 0.toByte()
}
/** Read a single bit from the buffer, as an integer, incrementing the offset. */
fun bit() = if (bitAsBoolean()) 1 else 0
/** Read [n] bits from the buffer, returning them as an unsigned integer. */
fun bits(n: Int): Int {
require(n < Int.SIZE_BITS)
var ret = 0
/* TODO: optimize this */
repeat(n) {
ret = ret shl 1
ret = ret or bit()
}
return ret
}
/** Read [n] bits from the buffer, returning them as an unsigned long. */
fun bitsLong(n: Int): Long {
require(n < Long.SIZE_BITS)
var ret = 0L
/* TODO: optimize this */
repeat(n) {
ret = ret shl 1
ret = ret or bit().toLong()
}
return ret
}
/** Skip forward [n] bits in the buffer. */
fun skipBits(n: Int) {
offset += n
}
/** Read a non-symmetric unsigned integer with max *value* [n] from the buffer.
* (Note: *not* the number of bits.)
* See https://aomediacodec.github.io/av1-rtp-spec/#a82-syntax
*/
fun ns(n: Int): Int {
var w = 0
var x = n
while (x != 0) {
x = x shr 1
w++
}
val m = (1 shl w) - n
val v = bits(w - 1)
if (v < m) {
return v
}
val extraBit = bit()
return (v shl 1) - m + extraBit
}
/**
* Read a LEB128-encoded unsigned integer.
* https://aomediacodec.github.io/av1-spec/#leb128
*/
fun leb128(): Long {
var value = 0L
(0..8).forEach { i ->
val hasNext = bitAsBoolean()
value = value or (bits(7).toLong() shl (i * 7))
if (!hasNext) return value
}
return value
}
/** Reset the reader to the beginning of the buffer */
fun reset() {
offset = byteOffset * 8
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import java.util.*
import kotlin.experimental.or
/**
* Write individual bits, and unaligned sets of bits, to a [ByteArray], with an incrementing offset.
*/
class BitWriter(val buf: ByteArray, val byteOffset: Int = 0, private val byteLength: Int = buf.size) {
private var offset = byteOffset * 8
private val byteBound = byteOffset + byteLength
init {
Arrays.fill(buf, byteOffset, byteBound, 0)
}
fun writeBit(value: Boolean) {
val byteIdx = offset / 8
val bitIdx = offset % 8
check(byteIdx < byteBound) {
"offset $offset ($byteIdx/$bitIdx) invalid in buffer of length $byteLength after offset $byteOffset"
}
if (value) {
buf[byteIdx] = buf[byteIdx] or (1 shl (7 - bitIdx)).toByte()
}
offset++
}
fun writeBits(bits: Int, value: Int) {
check(value < (1 shl bits)) {
"value $value cannot be represented in $bits bits"
}
repeat(bits) { i ->
writeBit((value and (1 shl (bits - i - 1))) != 0)
}
}
fun writeNs(n: Int, v: Int) {
if (n == 1) return
var w = 0
var x = n
while (x != 0) {
x = x shr 1
w++
}
val m = (1 shl w) - n
if (v < m) {
writeBits(w - 1, v)
} else {
writeBits(w, v + m)
}
}
val remainingBits
get() = byteBound * 8 - offset
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
/**
* All [ByteArray]s that the library needs for packets will be acquired via [getArray].
*
* When we're done with a buffer, we'll pass it to [returnArray]. These methods
* can be overridden so that a user of this library can use a pool for the buffers.
*
* @author Brian Baldino
*/
class BufferPool {
companion object {
var getArray: (Int) -> ByteArray = { size -> ByteArray(size) }
var returnArray: (ByteArray) -> Unit = { }
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import java.nio.ByteBuffer
// TODO: i think these should only be used for tests. Move them?
fun byteBufferOf(vararg elements: Byte): ByteBuffer = ByteBuffer.wrap(byteArrayOf(*elements))
fun byteBufferOf(vararg elements: Number): ByteBuffer {
val bytes = elements.map { it.toByte() }.toByteArray()
return ByteBuffer.wrap(bytes)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import org.jitsi.rtp.extensions.bytearray.get3Bytes
import org.jitsi.rtp.extensions.bytearray.getInt
import org.jitsi.rtp.extensions.bytearray.getShort
import org.jitsi.rtp.extensions.bytearray.putBits
import org.jitsi.rtp.extensions.getBits
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
// TODO: could do some kind of 'Offset' inline class? or 'field' which described the offset
// and size?
fun ByteArray.getBitsAsInt(byteOffset: Int, bitStartPos: Int, numBits: Int): Int =
get(byteOffset).getBits(bitStartPos, numBits).toPositiveInt()
fun ByteArray.putNumberAsBits(byteOffset: Int, bitOffset: Int, numBits: Int, value: Number) {
putBits(byteOffset, bitOffset, value.toByte(), numBits)
}
fun ByteArray.getByteAsInt(offset: Int): Int = get(offset).toPositiveInt()
fun ByteArray.getShortAsInt(offset: Int): Int = getShort(offset).toPositiveInt()
fun ByteArray.get3BytesAsInt(offset: Int): Int = get3Bytes(offset).toPositiveInt()
fun ByteArray.getIntAsLong(offset: Int): Long = getInt(offset).toPositiveLong()

View File

@@ -0,0 +1,182 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import org.jitsi.utils.TimeUtils
import java.time.Duration
import java.time.Instant
class RtpUtils {
companion object {
/**
* [sizeBytes] MUST including padding (i.e. it should be 32-bit word aligned)
*/
fun calculateRtcpLengthFieldValue(sizeBytes: Int): Int {
if (sizeBytes % 4 != 0) {
throw Exception("Invalid RTCP size value")
}
return (sizeBytes / 4) - 1
}
/**
* Get the number of bytes needed to pad [dataSizeBytes] bytes to a 4-byte word boundary.
*/
fun getNumPaddingBytes(dataSizeBytes: Int): Int = when (dataSizeBytes % 4) {
0 -> 0
1 -> 3
2 -> 2
3 -> 1
else -> 0 // The above is exhaustive.
}
/**
* Returns the delta between two RTP sequence numbers, taking into account
* rollover. This will return the 'shortest' delta between the two
* sequence numbers in the form of the number you'd add to b to get a. e.g.:
* getSequenceNumberDelta(1, 10) -> -9 (10 + -9 = 1)
* getSequenceNumberDelta(1, 65530) -> 7 (65530 + 7 = 1)
* @return the delta between two RTP sequence numbers (modulo 2^16).
*/
@JvmStatic
fun getSequenceNumberDelta(a: Int, b: Int): Int = getSequenceNumberDeltaAsShort(a, b).toInt()
/**
* Like [getSequenceNumberDelta], but returning the delta as a [Short].
*
* TODO: since [Short] can fully represent sequence number deltas,
* change this to be the public API?
*/
private inline fun getSequenceNumberDeltaAsShort(a: Int, b: Int): Short =
/* Coercing to short forces diff to the range -0x8000 - 0x7fff,
which is what we want. */
(a - b).toShort()
/**
* Apply a delta to a given sequence number and return the result (taking
* rollover into account)
* @param start the starting sequence number
* @param delta the delta to be applied
* @return the sequence number resulting from doing "start + delta"
*/
@JvmStatic
fun applySequenceNumberDelta(start: Int, delta: Int): Int = (start + delta) and 0xffff
/**
* Apply a delta to a given RTP timestamp and return the result (taking
* rollover into account)
* @param start the starting timestamp
* @param delta the delta to be applied
* @return the timestamp result from doing "start + delta"
*/
@JvmStatic
fun applyTimestampDelta(start: Long, delta: Long): Long = (start + delta) and 0xffff_ffffL
@JvmStatic
fun isNewerSequenceNumberThan(a: Int, b: Int): Boolean = getSequenceNumberDeltaAsShort(a, b) > 0
@JvmStatic
fun isOlderSequenceNumberThan(a: Int, b: Int): Boolean = getSequenceNumberDeltaAsShort(a, b) < 0
@JvmStatic
fun isNewerTimestampThan(a: Long, b: Long): Boolean = getTimestampDiffAsInt(a, b) > 0
@JvmStatic
fun isOlderTimestampThan(a: Long, b: Long): Boolean = getTimestampDiffAsInt(a, b) < 0
/**
* Returns the difference between two RTP timestamps.
* @return the difference between two RTP timestamps.
*/
@JvmStatic
fun getTimestampDiff(a: Long, b: Long): Long = getTimestampDiffAsInt(a, b).toLong()
/**
* Returns the difference between two RTP timestamps as an [Int].
*/
inline fun getTimestampDiffAsInt(a: Long, b: Long): Int =
/* Coercing to int forces diff to the range -0x8000_0000 - 0x7fff_ffff,
which is what we want. */
(a - b).toInt()
/**
* Returns a sequence of Ints from olderSeqNum (exclusive) to newerSeqNum (exclusive),
* taking rollover into account
*/
fun sequenceNumbersBetween(olderSeqNum: Int, newerSeqNum: Int): Sequence<Int> {
var currSeqNum = olderSeqNum
return generateSequence {
currSeqNum = (currSeqNum + 1) % 0x1_0000
if (currSeqNum == newerSeqNum) {
null
} else {
currSeqNum
}
}
}
/**
* Given [timestampMs] (a timestamp in milliseconds), convert it to an NTP timestamp represented
* as a pair of ints: the first one being the most significant word and the second being the least
* significant word.
*/
@JvmStatic
fun millisToNtpTimestamp(timestampMs: Long): Long = TimeUtils.toNtpTime(timestampMs)
@JvmStatic
fun convertRtpTimestampToMs(rtpTimestamp: Int, ticksPerSecond: Int): Long =
((rtpTimestamp / (ticksPerSecond.toDouble())) * 1000).toLong()
@JvmStatic
fun convertRtpTimestampToInstant(rtpTimestamp: Int, ticksPerSecond: Int): Instant =
Instant.EPOCH.plus(Duration.ofSeconds(rtpTimestamp.toLong()).dividedBy(ticksPerSecond.toLong()))
}
}
fun Int.isPadding(): Boolean = this.toByte().isPadding()
fun Byte.isPadding(): Boolean = this == 0x00.toByte()
/**
* Returns true if the RTP sequence number represented by [this] represents a more recent RTP packet than the one
* represented by [otherSeqNum]
*/
infix fun Int.isNewerThan(otherSeqNum: Int): Boolean = RtpUtils.isNewerSequenceNumberThan(this, otherSeqNum)
infix fun Int.isOlderThan(otherSeqNum: Int): Boolean = RtpUtils.isOlderSequenceNumberThan(this, otherSeqNum)
infix fun Long.isNewerTimestampThan(otherTimestamp: Long): Boolean = RtpUtils.isNewerTimestampThan(this, otherTimestamp)
infix fun Long.isOlderTimestampThan(otherTimestamp: Long): Boolean = RtpUtils.isOlderTimestampThan(this, otherTimestamp)
/**
* Returns true if getting to [otherSeqNum] from the current sequence number involves wrapping around
*/
infix fun Int.rolledOverTo(otherSeqNum: Int): Boolean =
// If, according to [isOlderThan], [this] is older than [otherSeqNum] and
// yet [otherSeqNum] is less than [this], then we wrapped around to get from [this] to
// [otherSeqNum]
this isOlderThan otherSeqNum && otherSeqNum < this
/**
* Returns true if [this] is sequentially after [otherSeqNum], according to the rules of RTP sequence
* numbers
*/
infix fun Int.isNextAfter(otherSeqNum: Int): Boolean = RtpUtils.getSequenceNumberDelta(this, otherSeqNum) == 1
/**
* Return the amount of packets between the RTP sequence number represented by [this] and the [otherSeqNum]. NOTE:
* [this] must represent an older RTP sequence number than [otherSeqNum] (TODO: validate/enforce that)
*/
infix fun Int.numPacketsTo(otherSeqNum: Int): Int = -RtpUtils.getSequenceNumberDelta(this, otherSeqNum) - 1

View File

@@ -0,0 +1,27 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import org.jitsi.rtp.extensions.bytearray.getInt
import org.jitsi.utils.ByteArrayBuffer
class SrtpUtils {
companion object {
fun getSrtcpIndex(packet: ByteArrayBuffer, authTagLen: Int): Int =
packet.buffer.getInt(packet.offset + packet.length - (4 + authTagLen))
}
}

View File

@@ -0,0 +1,81 @@
package org.jitsi.rtp.extensions
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.UnparsedPacket
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
class ByteArrayBufferTest : ShouldSpec() {
private val buffer = byteArrayOf(
0x90, 0x6f, 0x16, 0xaf,
0x65, 0xf3, 0xe8, 0xce,
0x48, 0x0f, 0x22, 0x3a,
0xbe, 0xde, 0x00, 0x01,
0x10, 0xff, 0x00, 0x00
)
init {
context("ByteArrayBuffer.toHex") {
should("format a full buffer correctly") {
val packet = UnparsedPacket(buffer, 0, buffer.size)
packet.toHex() shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001\n10FF0000"
}
should("honor maxBytes") {
val packet = UnparsedPacket(buffer, 0, buffer.size)
packet.toHex(12) shouldBe "906F16AF 65F3E8CE 480F223A"
}
should("handle large maxBytes") {
val packet = UnparsedPacket(buffer, 0, buffer.size)
packet.toHex(40) shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001\n10FF0000"
}
should("work correctly for offset packets") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 4)
packet.toHex() shouldBe "65F3E8CE 480F223A BEDE0001 10FF0000"
}
should("honor maxBytes for offset packets") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 4)
packet.toHex(12) shouldBe "65F3E8CE 480F223A BEDE0001"
}
should("handle large maxBytes for offset packets") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 4)
packet.toHex(40) shouldBe "65F3E8CE 480F223A BEDE0001 10FF0000"
}
should("handle maxBytes large enough that maxBytes + offset wraps Int") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 4)
packet.toHex(Int.MAX_VALUE - 2) shouldBe "65F3E8CE 480F223A BEDE0001 10FF0000"
}
should("work correctly for short packets") {
val packet = UnparsedPacket(buffer, 0, buffer.size - 4)
packet.toHex() shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001"
}
should("honor maxBytes for short packets") {
val packet = UnparsedPacket(buffer, 0, buffer.size - 4)
packet.toHex(12) shouldBe "906F16AF 65F3E8CE 480F223A"
}
should("handle large maxBytes for short packets") {
val packet = UnparsedPacket(buffer, 0, buffer.size - 4)
packet.toHex(40) shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001"
}
should("work correctly for short packets with offset") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 8)
packet.toHex() shouldBe "65F3E8CE 480F223A BEDE0001"
}
should("honor maxBytes for short packets with offset") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 8)
packet.toHex(8) shouldBe "65F3E8CE 480F223A"
}
should("handle large maxBytes for short packets with offset") {
val packet = UnparsedPacket(buffer, 4, buffer.size - 8)
packet.toHex(40) shouldBe "65F3E8CE 480F223A BEDE0001"
}
should("handle packets with invalidly large length values") {
val packet = UnparsedPacket(buffer, 0, buffer.size + 40)
packet.toHex() shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001\n10FF0000"
}
should("honor maxBytes for packets with invalidly large length values") {
val packet = UnparsedPacket(buffer, 0, buffer.size + 40)
packet.toHex(40) shouldBe "906F16AF 65F3E8CE 480F223A BEDE0001\n10FF0000"
}
}
}
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.util.byteBufferOf
import org.jitsi.test_helpers.matchers.haveSameContentAs
import java.nio.ByteBuffer
class ByteBufferExtensionsTest : ShouldSpec() {
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
init {
context("ByteBuffer") {
context("put3Bytes") {
val buf = ByteBuffer.allocate(4)
context("for a 3 byte value") {
// 424242 = 0x067932
val num: Int = 424242
buf.put3Bytes(num)
buf.rewind()
should("write the bytes correctly") {
buf.get() shouldBe 0x06.toByte()
buf.get() shouldBe 0x79.toByte()
buf.get() shouldBe 0x32.toByte()
}
}
context("for a 2 byte value") {
// 4242 = 0x1092
val num: Int = 4242
buf.put3Bytes(num)
buf.rewind()
should("write the bytes correctly") {
buf.get() shouldBe 0x00.toByte()
buf.get() shouldBe 0x10.toByte()
buf.get() shouldBe 0x92.toByte()
}
}
context("for a 1 byte value") {
// 42 = 0x2A
val num: Int = 42
buf.put3Bytes(num)
buf.rewind()
should("write the bytes correctly") {
buf.get() shouldBe 0x00.toByte()
buf.get() shouldBe 0x00.toByte()
buf.get() shouldBe 0x2A.toByte()
}
}
}
context("get3Bytes") {
val buf = ByteBuffer.allocate(4)
context("for a 3 byte value") {
val num: Int = 0xb1b1b1
buf.put3Bytes(num)
buf.rewind()
should("read them correctly") {
buf.get3Bytes() shouldBe 0xb1b1b1.toInt()
}
}
context("for a 2 byte value") {
val num: Int = 4242
buf.put3Bytes(num)
buf.rewind()
should("read them correctly") {
buf.get3Bytes() shouldBe 4242.toInt()
}
}
context("for a 1 byte value") {
val num: Int = 42
buf.put3Bytes(num)
buf.rewind()
should("read them correctly") {
buf.get3Bytes() shouldBe 42.toInt()
}
}
}
context("putBits") {
should("write the bits into the buffer correctly") {
val buf = ByteBuffer.allocate(4)
val src: Byte = 0b00001111
buf.putBits(1, 0, src, 4)
buf.get(1) shouldBe 0b11110000.toByte()
// Nothing else should've changed
listOf(0, 2, 3).forEach {
buf.get(it) shouldBe 0b00000000.toByte()
}
}
}
context("subBuffer(startPosition, size)") {
val originalBuf = ByteBuffer.allocate(100)
context("created from a default original buffer") {
val subBuf = originalBuf.subBuffer(10, 30)
should("have a correctly set position, limit and capacity") {
subBuf.position() shouldBe 0
subBuf.limit() shouldBe 30
subBuf.capacity() shouldBe 30
}
should("not affect the original buffer in any way") {
originalBuf.position() shouldBe 0
originalBuf.limit() shouldBe 100
originalBuf.capacity() shouldBe 100
}
should("represent the correct spot in the original buffer") {
repeat(subBuf.limit()) { subBuf.put(it, 0x42) }
(0..9).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
(10..39).forEach { originalBuf.get(it) shouldBe 0x42.toByte() }
(40..99).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
}
}
context("created from a buffer whose current position isn't 0") {
originalBuf.position(10)
val subBuf = originalBuf.subBuffer(5, 10)
should("represent the correct spot in the original buffer") {
repeat(subBuf.limit()) { subBuf.put(it, 0x42) }
(0..4).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
(5..14).forEach { originalBuf.get(it) shouldBe 0x42.toByte() }
(15..99).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
}
}
}
context("subBuffer(startPosition)") {
val originalBuf = ByteBuffer.allocate(100)
context("created from a default original buffer") {
val subBuf = originalBuf.subBuffer(10)
should("have a correctly set position, limit and capacity") {
subBuf.position() shouldBe 0
subBuf.limit() shouldBe 90
subBuf.capacity() shouldBe 90
}
should("not affect the original buffer in any way") {
originalBuf.position() shouldBe 0
originalBuf.limit() shouldBe 100
originalBuf.capacity() shouldBe 100
}
should("represent the correct spot in the original buffer") {
repeat(subBuf.limit()) { subBuf.put(it, 0x42) }
(0..9).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
(10..99).forEach { originalBuf.get(it) shouldBe 0x42.toByte() }
}
}
context("created from a buffer whose current position isn't 0") {
originalBuf.position(10)
val subBuf = originalBuf.subBuffer(5)
should("represent the correct spot in the original buffer") {
repeat(subBuf.limit()) { subBuf.put(it, 0x42) }
(0..4).forEach { originalBuf.get(it) shouldBe 0x00.toByte() }
(5..99).forEach { originalBuf.get(it) shouldBe 0x42.toByte() }
}
}
}
context("shiftDataRight") {
context("moving data") {
val buf = byteBufferOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
buf.shiftDataRight(1, 3, 2)
should("move things correctly") {
buf should haveSameContentAs(byteBufferOf(0x01, 0x02, 0x03, 0x02, 0x03, 0x04))
}
}
context("moving data past the current limit but within the capacity") {
val buf = byteBufferOf(0x01, 0x02, 0x03, 0x04, 0x00, 0x0, 0x00, 0x00)
buf.limit(4)
buf.shiftDataRight(1, 3, 2)
should("move things correctly") {
buf.limit() shouldBe 6
buf should haveSameContentAs(byteBufferOf(0x01, 0x02, 0x03, 0x02, 0x03, 0x04))
}
}
context("moving the data past the capacity") {
val buf = byteBufferOf(0x01, 0x02, 0x03, 0x04, 0x00, 0x0, 0x00, 0x00)
shouldThrow<Exception> {
buf.shiftDataRight(4, 6, 10)
}
}
}
context("shiftDataLeft") {
context("moving data") {
val buf = byteBufferOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06)
buf.shiftDataLeft(2, 4, 2)
should("move things correctly") {
buf should haveSameContentAs(byteBufferOf(0x03, 0x04, 0x05, 0x04, 0x05, 0x06))
}
}
context("moving the data past the start") {
val buf = byteBufferOf(0x01, 0x02, 0x03, 0x04, 0x00, 0x0, 0x00, 0x00)
shouldThrow<Exception> {
buf.shiftDataLeft(1, 2, 2)
}
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright @ 2018 - Present, 8x8 Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
class ByteExtensionsTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
init {
context("Byte.getBit") {
should("Correctly get the bit in each position") {
for (onePosition in 0..7) {
val b: Byte = (0b1 shl (7 - onePosition)).toByte()
for (currPosition in 0..7) {
when (currPosition) {
onePosition -> b.getBit(currPosition) shouldBe 1
else -> b.getBit(currPosition) shouldBe 0
}
}
}
}
}
context("Byte.putBit") {
should("Set bits to true correctly") {
val b: Byte = 0x00
b.putBit(0, true) shouldBe 0b10000000.toByte()
b.putBit(3, true) shouldBe 0b00010000.toByte()
b.putBit(7, true) shouldBe 0b00000001.toByte()
}
should("Set bits to false correctly") {
val b: Byte = 0xFF.toByte()
b.putBit(1, false) shouldBe 0b10111111.toByte()
b.putBit(2, false) shouldBe 0b11011111.toByte()
b.putBit(4, false) shouldBe 0b11110111.toByte()
b.putBit(7, false) shouldBe 0b11111110.toByte()
}
should("Support a mix of sets/unsets") {
var b: Byte = 0x00
(0..7).forEach {
b = b.putBit(it, true)
}
b shouldBe 0b11111111.toByte()
(0..7).forEach {
b = b.putBit(it, false)
}
b shouldBe 0b00000000.toByte()
}
}
context("Byte.putBits") {
should("correctly put bits") {
val dest: Byte = 0b00000000
val src: Byte = 0b00000101
val result = dest.putBits(0, 3, src)
result shouldBe 0b10100000.toByte()
}
should("overwrite existing values correctly") {
val dest: Byte = 0b11111111.toByte()
val src: Byte = 0b00000000
val result = dest.putBits(0, 3, src)
result shouldBe 0b00011111.toByte()
}
should("work correctly with different offsets") {
val dest: Byte = 0b00000000
val src: Byte = 0b11001100.toByte()
val result = dest.putBits(2, 4, src)
result shouldBe 0b00110000.toByte()
}
}
}
}

View File

@@ -0,0 +1,372 @@
/*
* Copyright @ 2024-Present 8x8, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.rtp.RtpPacket.HeaderExtension
import org.jitsi.rtp.rtp.header_extensions.ParsedVla
import org.jitsi.rtp.rtp.header_extensions.VlaExtension
import org.jitsi.rtp.rtp.header_extensions.VlaExtension.ResolutionAndFrameRate
import org.jitsi.rtp.rtp.header_extensions.VlaExtension.SpatialLayer
import org.jitsi.rtp.rtp.header_extensions.VlaExtension.Stream
@Suppress("ktlint:standard:no-multi-spaces")
class VlaExtensionTest : ShouldSpec() {
init {
context("Empty") {
parse(0x00) shouldBe emptyList()
}
context("VP8 single stream (with resolution)") {
parse(
0b0000_0001,
0b1000_0000.toByte(),
0b0101_0000,
0b0111_1000,
0b1100_1000.toByte(),
0b0000_0001,
0b0000_0001,
0b0011_1111,
0b0000_0000,
0b1011_0011.toByte(),
0b0010_0001
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(80, 120, 200),
ResolutionAndFrameRate(320, 180, 33)
)
)
)
)
}
context("VP8 single stream (without resolution)") {
parse(
0b0000_0001,
0b1000_0000.toByte(),
0b0101_0000,
0b0111_1000,
0b1100_1000.toByte(),
0b0000_0001
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(80, 120, 200),
null
)
)
)
)
}
context("VP8 simulcast stream (with resolutions)") {
parse(
0b0010_0001,
0b1010_1000.toByte(),
0b0011_1100,
0b0101_1010,
0b1001_0110.toByte(),
0b0000_0001,
0b1100_1000.toByte(),
0b0000_0001,
0b1010_1100.toByte(),
0b0000_0010,
0b1111_0100.toByte(),
0b0000_0011,
0b1110_1110.toByte(),
0b0000_0011,
0b1110_0110.toByte(),
0b0000_0101,
0b1101_0100.toByte(),
0b0000_1001,
0b0000_0001,
0b0011_1111,
0b0000_0000,
0b1011_0011.toByte(),
0b0001_1111,
0b0000_0010,
0b0111_1111,
0b0000_0001,
0b0110_0111,
0b0001_1111,
0b0000_0100,
0b1111_1111.toByte(),
0b0000_0010,
0b1100_1111.toByte(),
0b0001_1111
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(60, 90, 150),
ResolutionAndFrameRate(320, 180, 31)
)
)
),
Stream(
1,
listOf(
SpatialLayer(
0,
listOf(200, 300, 500),
ResolutionAndFrameRate(640, 360, 31)
)
)
),
Stream(
2,
listOf(
SpatialLayer(
0,
listOf(494, 742, 1236),
ResolutionAndFrameRate(1280, 720, 31)
)
)
)
)
}
context("VP8 simulcast stream (without resolutions)") {
parse(
0b1010_0001.toByte(),
0b1010_1000.toByte(),
0b1001_0101.toByte(),
0b0000_0010,
0b1001_1111.toByte(),
0b0000_0011,
0b1011_0100.toByte(),
0b0000_0101,
0b1000_1101.toByte(),
0b0000_1001,
0b1101_0011.toByte(),
0b0000_1101,
0b1110_0000.toByte(),
0b0001_0110,
0b1101_0000.toByte(),
0b0000_1111,
0b1011_1000.toByte(),
0b0001_0111,
0b1000_1000.toByte(),
0b0010_0111
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(277, 415, 692),
null
)
)
),
Stream(
1,
listOf(
SpatialLayer(
0,
listOf(1165, 1747, 2912),
null
)
)
),
Stream(
2,
listOf(
SpatialLayer(
0,
listOf(2000, 3000, 5000),
null
)
)
)
)
}
context("VP9 SVC (with resolutions)") {
parse(
0b0000_0111,
0b1010_1000.toByte(),
0b0100_1101,
0b0110_0100,
0b1000_1110.toByte(),
0b0000_0001,
0b1101_0111.toByte(),
0b0000_0001,
0b1001_0111.toByte(),
0b0000_0010,
0b1000_1101.toByte(),
0b0000_0011,
0b1101_0110.toByte(),
0b0000_0010,
0b1011_1101.toByte(),
0b0000_0011,
0b1111_1001.toByte(),
0b0000_0100,
0b0000_0001,
0b0011_1111,
0b0000_0000,
0b1011_0011.toByte(),
0b0010_0000,
0b0000_0010,
0b0111_1111,
0b0000_0001,
0b0110_0111,
0b0010_0000,
0b0000_0100,
0b1111_1111.toByte(),
0b0000_0010,
0b1100_1111.toByte(),
0b0010_0000
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(77, 100, 142),
ResolutionAndFrameRate(320, 180, 32)
),
SpatialLayer(
1,
listOf(215, 279, 397),
ResolutionAndFrameRate(640, 360, 32)
),
SpatialLayer(
2,
listOf(342, 445, 633),
ResolutionAndFrameRate(1280, 720, 32)
)
)
)
)
}
context("VP9 SVC (without resolutions or TLs)") {
parse(
0b0000_0111,
0b0000_0000,
0b1001_0110.toByte(),
0b0000_0001,
0b1111_0100.toByte(),
0b0000_0011,
0b1010_1010.toByte(),
0b0000_1011
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(150),
null
),
SpatialLayer(
1,
listOf(500),
null
),
SpatialLayer(
2,
listOf(1450),
null
)
)
)
)
}
context("Invalid VLAs") {
context("Resolution incomplete") {
// Cuts short with not enough bytes to contain the resolution. We succeed with no resolution.
parse(
0b0000_0001,
0b1000_0000.toByte(), // #tls
0b0101_0000, // targetBitrate 1
0b0111_1000, // targetBitrate 2
0b1100_1000.toByte(), // targetBitrate 3
0b0000_0001, // targetBitrate 3
0b0000_0001, // width
0b0011_1111, // width
0b0000_0000, // height
0b1011_0011.toByte(), // height
// 0b0010_0001 // maxFramerate
) shouldBe listOf(
Stream(
0,
listOf(
SpatialLayer(
0,
listOf(80, 120, 200),
null
)
)
)
)
}
context("Invalid leb128") {
// Cuts short in the middle of one of the leb128 encoding of one of the target bitrates.
shouldThrow<IllegalStateException> {
parse(
0b0000_0001,
0b1000_0000.toByte(), // #tls
0b0101_0000, // targetBitrate 1
0b0111_1000, // targetBitrate 2
0b1100_1000.toByte(), // targetBitrate 3
// 0b0000_0001, // targetBitrate 3
)
}
}
context("Missing target bitrates") {
// Does not contain the expected number of target bitrates
shouldThrow<IllegalStateException> {
parse(
0b0000_0001, // 1 spatial layer
0b1000_0000.toByte(), // 3 temporal layers
0b0101_0000, // targetBitrate 1
0b1100_1000.toByte(), // targetBitrate 2
0b0000_0001, // targetBitrate 2
// 0b0111_1000, // targetBitrate 3
)
}
}
context("Missing temporal layer counts") {
// Does not contain the expected temporal layer counts
shouldThrow<IllegalStateException> {
parse(
0b0001_0000, // spatial layer bitmask in the next byte, 2 streams
0b1111_1111.toByte(), // 4 spatial layers for each stream
0b0000_0000.toByte(), // 1 temporal layer for each spatial layer of stream 1, more must follow
)
}
}
}
}
}
private fun parse(vararg bytes: Byte): ParsedVla = VlaExtension.parse(RawHeaderExtension(bytes))
@SuppressFBWarnings("CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE")
class RawHeaderExtension(override val buffer: ByteArray) : HeaderExtension {
override val dataOffset: Int = 0
override var id: Int = 0
override val dataLengthBytes: Int = buffer.size
override val totalLengthBytes: Int = buffer.size
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions.bytearray
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.data.forAll
import io.kotest.data.row
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.jitsi.test_helpers.matchers.haveSameContentAs
class ByteArrayExtensionsKtTest : ShouldSpec() {
init {
context("ByteArray.getShort/putShort") {
should("parse the short correctly") {
forAll(
row(byteArrayOf(0x00, 0x00), 0.toShort()),
row(byteArrayOf(0xFF, 0xFF), 65535.toShort())
) { buf, expectedShort ->
buf.getShort(0) shouldBe expectedShort
val array = ByteArray(2)
array.putShort(0, expectedShort)
array should haveSameContentAs(buf)
}
}
}
context("ByteArray.getInt/putInt") {
should("parse the short correctly") {
forAll(
row(byteArrayOf(0x00, 0x00, 0x00, 0x00), 0.toInt()),
row(byteArrayOf(0xFF, 0xFF, 0xFF, 0xFF), 4294967295.toInt())
) { buf, expectedInt ->
buf.getInt(0) shouldBe expectedInt
val array = ByteArray(4)
array.putInt(0, expectedInt)
array should haveSameContentAs(buf)
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.extensions.unsigned
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
internal class UnsignedKtTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
init {
context("converting a byte to a positive int") {
should("convert correctly") {
val byteSizedInts = listOf(0, 1, 63, 127, 128, 244, 255)
byteSizedInts.forEach { byteSizedInt ->
val byte = byteSizedInt.toByte()
val convertedInt = byte.toPositiveInt()
convertedInt shouldBe byteSizedInt
}
}
}
context("converting a short to a positive int") {
val shortSizedInts = listOf(0, 1, 63, 127, 65530, 65535)
shortSizedInts.forEach { shortSizedInt ->
val short = shortSizedInt.toShort()
val convertedInt = short.toPositiveInt()
convertedInt shouldBe shortSizedInt
}
}
context("converting an int to a positive long") {
val intSizedLongs = listOf(0, 1, 63, 127, 2294967195, 4294967295)
intSizedLongs.forEach { intSizedLong ->
val int = intSizedLong.toInt()
val convertedLong = int.toPositiveLong()
convertedLong shouldBe intSizedLong
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.Packet
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbPliPacketBuilder
import org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb.RtcpFbRembPacketBuilder
internal class CompoundRtcpPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
init {
context("Creating a compound packet from a list of packets") {
val rr = RtcpRrPacketBuilder(RtcpHeaderBuilder(), mutableListOf()).build()
val remb = RtcpFbRembPacketBuilder(RtcpHeaderBuilder(), listOf(123), 12345).build()
val pli = RtcpFbPliPacketBuilder(RtcpHeaderBuilder(), 456).build()
val packets = listOf(rr, remb, pli)
val compoundRtcpPacket = CompoundRtcpPacket(packets)
compoundRtcpPacket.packets.size shouldBe packets.size
compoundRtcpPacket.packets.forEachIndexed { i, packet ->
packet.length shouldBe packets[i].length
packet::class shouldBe packets[i]::class
}
}
context("Parsing a compound RTCP packet which contains invalid data") {
val packet = DummyPacket(rtcpByeNoReason + invalidRtcpData)
shouldThrow<CompoundRtcpContainedInvalidDataException> {
CompoundRtcpPacket.parse(packet.buffer, packet.offset, packet.length)
}
}
context("Creating multiple compound packets by limiting to MTU") {
val rr = RtcpRrPacketBuilder(RtcpHeaderBuilder(), mutableListOf()).build()
val compoundPackets = CompoundRtcpPacket.createWithMtu(generateSequence { rr }.take(200).toList())
// rr without report blocks is 8 bytes, so 200 is 2000 bytes; default MTU value is 1500.
compoundPackets.size shouldBe 2
}
}
private val invalidRtcpData = byteArrayOf(
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
private val rtcpByeNoReason = RtcpHeaderBuilder(
packetType = RtcpByePacket.PT,
reportCount = 1,
senderSsrc = 12345L,
length = 1
).build()
}
private class DummyPacket(
buf: ByteArray
) : Packet(buf, 0, buf.size) {
override fun clone(): Packet = DummyPacket(cloneBuffer(0))
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
import org.jitsi.test_helpers.matchers.haveSameContentAs
import java.nio.charset.StandardCharsets
internal class RtcpByePacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
private val rtcpByeNoReason = RtcpHeaderBuilder(
packetType = RtcpByePacket.PT,
reportCount = 1,
senderSsrc = 12345L,
length = 1
).build()
private val byeReason = "Connection terminated"
private val byeReasonData = byeReason.toByteArray(StandardCharsets.US_ASCII)
private val rtcpByeReasonData = byteArrayOf(
byeReasonData.size.toByte(),
*byeReasonData.toTypedArray()
)
private val reasonSize = 1 + byeReasonData.size
private val padding = byteArrayOf(0x00, 0x00)
private val rtcpByeWithReason = RtcpHeaderBuilder(
packetType = RtcpByePacket.PT,
hasPadding = true,
reportCount = 1,
senderSsrc = 12345L,
length = 1 + ((1 + reasonSize + 3) / 4)
).build() + rtcpByeReasonData + padding
init {
context("Creating an RtcpByePacket") {
context("from a buffer") {
context("of a packet without a reason") {
val packet = RtcpByePacket(rtcpByeNoReason, 0, rtcpByeNoReason.size)
should("parse the values correctly") {
packet.ssrcs shouldHaveSize 1
packet.ssrcs shouldContain 12345L
packet.reason shouldBe null
}
}
context("of a packet with a reason") {
val packet = RtcpByePacket(rtcpByeWithReason, 0, rtcpByeWithReason.size)
should("parse the values correctly") {
packet.ssrcs shouldHaveSize 1
packet.ssrcs shouldContain 12345L
packet.reason shouldBe byeReason
}
context("and then serializing it") {
context("via requesting a new buffer") {
val serializedBuf = packet.getBuffer()
should("write all values correctly") {
serializedBuf should haveSameContentAs(rtcpByeWithReason)
}
}
// context("to an existing buffer") {
// val existingBuf = ByteBuffer.allocate(8 + rtcpByeWithReason.limit())
// existingBuf.position(8)
// packet.serializeTo(existingBuf)
// should("write the data to the proper place") {
// val subBuf = existingBuf.subBuffer(8, rtcpByeWithReason.limit())
// subBuf should haveSameContentAs(rtcpByeWithReason)
// }
// should("leave the buffer's position after the field it just wrote") {
// existingBuf.position() shouldBe (8 + rtcpByeWithReason.limit())
// }
// }
}
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.types.shouldBeInstanceOf
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
class RtcpPacketTest : ShouldSpec() {
init {
context("Parsing") {
context("a valid but unsupported RTCP packet") {
val unsupportedRtcpData = byteArrayOf(
// V=2, PT=195, length = 2
0x80, 0xC3, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
should("return UnsupportedRtcpPacket") {
RtcpPacket.parse(unsupportedRtcpData, 0, unsupportedRtcpData.size)
.shouldBeInstanceOf<UnsupportedRtcpPacket>()
}
}
context("an invalid RTCP packet") {
val invalidRtcpData = byteArrayOf(
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
should("throw an InvalidRtcpException") {
shouldThrow<InvalidRtcpException> {
RtcpPacket.parse(invalidRtcpData, 0, invalidRtcpData.size)
}
}
}
context("a packet with invalid length") {
val invalidRtcpLengthData = byteArrayOf(
// V=2, PT=195, length = 8
0x80, 0xC3, 0x00, 0x08,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
should("throw an InvalidRtcpException") {
shouldThrow<InvalidRtcpException> {
RtcpPacket.parse(invalidRtcpLengthData, 0, invalidRtcpLengthData.size)
}
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.extensions.put3Bytes
import org.jitsi.rtp.extensions.unsigned.toPositiveLong
import org.jitsi.test_helpers.matchers.haveSameContentAs
import java.nio.ByteBuffer
class RtcpReportBlockTest : ShouldSpec() {
private val expectedSsrc: Long = 12345
private val expectedFractionLost: Int = 42
private val expectedCumulativeLost: Int = 4242
private val expectedSeqNumCycles: Int = 1
private val expectedSeqNum: Int = 42
private val expectedHighestSeqNum = ((expectedSeqNumCycles shl 16) + expectedSeqNum.toShort()).toPositiveLong()
private val expectedInterarrivalJitter: Long = 4242
private val expectedLastSrTimestamp: Long = 23456
private val expectedDelaySinceLastSr: Long = 34567
val reportBlockData = with(ByteBuffer.allocate(24)) {
putInt(expectedSsrc.toInt())
put(expectedFractionLost.toByte())
put3Bytes(expectedCumulativeLost)
putShort(expectedSeqNumCycles.toShort())
putShort(expectedSeqNum.toShort())
putInt(expectedInterarrivalJitter.toInt())
putInt(expectedLastSrTimestamp.toInt())
putInt(expectedDelaySinceLastSr.toInt())
this.rewind() as ByteBuffer
}
init {
context("creation") {
context("from a buffer") {
val reportBlock = RtcpReportBlock.fromBuffer(reportBlockData.array(), reportBlockData.arrayOffset())
should("read the values correctly") {
reportBlock.ssrc shouldBe expectedSsrc
reportBlock.fractionLost shouldBe expectedFractionLost
reportBlock.cumulativePacketsLost shouldBe expectedCumulativeLost
reportBlock.seqNumCycles shouldBe expectedSeqNumCycles
reportBlock.seqNum shouldBe expectedSeqNum
reportBlock.interarrivalJitter shouldBe expectedInterarrivalJitter
reportBlock.lastSrTimestamp shouldBe expectedLastSrTimestamp
reportBlock.delaySinceLastSr shouldBe expectedDelaySinceLastSr
}
}
context("from values") {
val reportBlock = RtcpReportBlock(
ssrc = expectedSsrc,
fractionLost = expectedFractionLost,
cumulativePacketsLost = expectedCumulativeLost,
extendedHighestSeqNum = expectedHighestSeqNum,
interarrivalJitter = expectedInterarrivalJitter,
lastSrTimestamp = expectedLastSrTimestamp,
delaySinceLastSr = expectedDelaySinceLastSr
)
should("set the values correctly") {
reportBlock.ssrc shouldBe expectedSsrc
reportBlock.fractionLost shouldBe expectedFractionLost
reportBlock.cumulativePacketsLost shouldBe expectedCumulativeLost
reportBlock.extendedHighestSeqNum shouldBe expectedHighestSeqNum
reportBlock.seqNumCycles shouldBe expectedSeqNumCycles
reportBlock.seqNum shouldBe expectedSeqNum
reportBlock.interarrivalJitter shouldBe expectedInterarrivalJitter
reportBlock.lastSrTimestamp shouldBe expectedLastSrTimestamp
reportBlock.delaySinceLastSr shouldBe expectedDelaySinceLastSr
}
should("write the data correctly") {
val buf = ByteArray(RtcpReportBlock.SIZE_BYTES)
reportBlock.writeTo(buf, 0)
buf should haveSameContentAs(reportBlockData.array())
}
}
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.haveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import java.lang.IllegalArgumentException
import java.nio.ByteBuffer
internal class RtcpRrPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
// Report blocks
val reportBlocks = mutableListOf(
RtcpReportBlock(
12345,
42,
4242,
1,
42,
4242,
23456,
34567
),
RtcpReportBlock(
23456,
43,
4343,
2,
43,
4343,
34567,
45678
)
)
val rrPacket = RtcpRrPacketBuilder(
reportBlocks = reportBlocks
).build()
val rrPacketBuffer = ByteBuffer.wrap(rrPacket.buffer, rrPacket.offset, rrPacket.length)
// TODO: write expected buf (copy from below?) and verify serialization
init {
// Header + 2 ReportBlocks
// val packetBuf = ByteBuffer.allocate(8 + 2 * 24)
// with (BitBuffer(packetBuf)) {
// // Header
// putBits(0x2, 2) // Version
// putBoolean(false) // Padding
// putBits(0x2, 5) // Report count
// packetBuf.put(RtcpRrPacket.PT.toByte()) // Payload type
// packetBuf.putShort(0xFFFF.toShort()) // length
// packetBuf.putInt(0xFFFFFFFF.toInt()) // sender ssrc
//
// packetBuf.putInt(ssrc1.toInt())
// packetBuf.put(fractionLost1.toByte()); packetBuf.put3Bytes(cumulativeLost1)
// packetBuf.putShort(seqNumCycles1.toShort()); packetBuf.putShort(seqNum1.toShort())
// packetBuf.putInt(interarrivalJitter1.toInt())
// packetBuf.putInt(lastSrTimestamp1.toInt())
// packetBuf.putInt(delaySinceLastSr1.toInt())
//
// packetBuf.putInt(ssrc2.toInt())
// packetBuf.put(fractionLost2.toByte()); packetBuf.put3Bytes(cumulativeLost2)
// packetBuf.putShort(seqNumCycles2.toShort()); packetBuf.putShort(seqNum2.toShort())
// packetBuf.putInt(interarrivalJitter2.toInt())
// packetBuf.putInt(lastSrTimestamp2.toInt())
// packetBuf.putInt(delaySinceLastSr2.toInt())
// packetBuf.rewind() as ByteBuffer
// }
context("creation") {
context("from a buffer") {
val rrPacket =
RtcpRrPacket(rrPacketBuffer.array(), rrPacketBuffer.arrayOffset(), rrPacketBuffer.limit())
should("read all values correctly") {
rrPacket.packetType shouldBe RtcpRrPacket.PT
rrPacket.reportBlocks should haveSize(2)
}
}
}
context("building") {
context("with too many report blocks") {
should("throw") {
shouldThrow<IllegalArgumentException> {
RtcpRrPacketBuilder(
reportBlocks = generateSequence { reportBlocks[0] }.take(32).toList()
).build()
}
}
}
}
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.jitsi.rtp.util.byteBufferOf
import java.lang.IllegalArgumentException
import java.nio.ByteBuffer
internal class RtcpSrPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
private val expectedSenderInfo = SenderInfoBuilder(
ntpTimestampMsw = 0x7FFFFFFF,
ntpTimestampLsw = 0xFFFFFFFF,
rtpTimestamp = 0xFFFFFFFF,
sendersPacketCount = 0xFFFFFFFF,
sendersOctetCount = 0xFFFFFFFF
)
private val reportBlock1 = RtcpReportBlock(
ssrc = 12345,
fractionLost = 42,
cumulativePacketsLost = 4242,
seqNumCycles = 1,
seqNum = 42,
interarrivalJitter = 4242,
lastSrTimestamp = 23456,
delaySinceLastSr = 34567
)
private val reportBlock2 = RtcpReportBlock(
ssrc = 23456,
fractionLost = 42,
cumulativePacketsLost = 4242,
seqNumCycles = 1,
seqNum = 42,
interarrivalJitter = 4242,
lastSrTimestamp = 23456,
delaySinceLastSr = 34567
)
private val srPacket = RtcpSrPacketBuilder(
RtcpHeaderBuilder(
senderSsrc = 12345
),
expectedSenderInfo,
mutableListOf(reportBlock1, reportBlock2)
).build()
val srPacketData = byteBufferOf(
// V=2,P=false,RC=0,PT=200,L=6(28 bytes)
0x80, 0xc8, 0x00, 0x06,
// Sender SSRC: 1829790331
0x6d, 0x10, 0x62, 0x7b,
// Timestamp, MSW: 3761595357
0xe0, 0x35, 0x63, 0xdd,
// Timestamp, LSW: 17218523
0x01, 0x06, 0xbb, 0xdb,
// RTP timestamp: 3960153838
0xec, 0x0b, 0x26, 0xee,
// Sender's packet count: 72
0x00, 0x00, 0x00, 0x48,
// Sender's octet count: 76643
0x00, 0x01, 0x2b, 0x63
)
val srPacketBuffer = ByteBuffer.wrap(srPacket.buffer, srPacket.offset, srPacket.length)
init {
context("creation") {
context("from a buffer") {
// val srPacket = RtcpSrPacket(srPacketBuffer.array(), srPacketBuffer.arrayOffset(), srPacketBuffer.limit())
val srPacket = RtcpSrPacket(srPacketData.array(), srPacketData.arrayOffset(), srPacketData.limit())
should("read everything correctly") {
srPacket.version shouldBe 2
srPacket.hasPadding shouldBe false
srPacket.reportCount shouldBe 0
srPacket.reportBlocks shouldHaveSize 0
srPacket.packetType shouldBe 200
srPacket.lengthField shouldBe 6
srPacket.senderSsrc shouldBe 1829790331
srPacket.senderInfo.ntpTimestampMsw shouldBe 3761595357
srPacket.senderInfo.ntpTimestampLsw shouldBe 17218523
srPacket.senderInfo.compactedNtpTimestamp shouldBe 1675428102
srPacket.senderInfo.rtpTimestamp shouldBe 3960153838
srPacket.senderInfo.sendersPacketCount shouldBe 72
srPacket.senderInfo.sendersOctetCount shouldBe 76643
// srPacket.packetType shouldBe RtcpSrPacket.PT
// srPacket.reportCount shouldBe 2
// srPacket.lengthField shouldBe 18
// srPacket.senderInfo.ntpTimestampMsw shouldBe expectedSenderInfo.ntpTimestampMsw
// srPacket.senderInfo.ntpTimestampLsw shouldBe expectedSenderInfo.ntpTimestampLsw
// srPacket.senderInfo.compactedNtpTimestamp shouldBe 0xFFFFFFFF.toPositiveLong()
// srPacket.senderInfo.rtpTimestamp shouldBe expectedSenderInfo.rtpTimestamp
// srPacket.senderInfo.sendersPacketCount shouldBe expectedSenderInfo.sendersPacketCount
// srPacket.senderInfo.sendersOctetCount shouldBe expectedSenderInfo.sendersOctetCount
// srPacket.reportBlocks should haveSize(2)
// TODO: verify report block parse
// srPacket.reportBlocks[0] shouldBe reportBlock1
// srPacket.reportBlocks[1] shouldBe reportBlock2
}
}
context("cloneWithoutReportBlocks") {
context("when there were report blocks") {
val clone = srPacket.cloneWithoutReportBlocks()
should("clone everything correctly") {
clone.version shouldBe srPacket.version
clone.hasPadding shouldBe srPacket.hasPadding
clone.reportCount shouldBe 0
clone.packetType shouldBe srPacket.packetType
clone.length shouldBe RtcpHeader.SIZE_BYTES + SenderInfoParser.SIZE_BYTES
clone.senderSsrc shouldBe srPacket.senderSsrc
clone.buffer shouldNotBe srPacket.buffer
}
}
context("when there were no report blocks") {
val srPacketNoReportBlocks = RtcpSrPacketBuilder(
RtcpHeaderBuilder(
senderSsrc = 12345
),
expectedSenderInfo
).build()
val clone = srPacketNoReportBlocks.cloneWithoutReportBlocks()
should("clone everything correctly") {
clone.version shouldBe srPacket.version
clone.hasPadding shouldBe srPacket.hasPadding
clone.reportCount shouldBe 0
clone.packetType shouldBe srPacket.packetType
clone.length shouldBe RtcpHeader.SIZE_BYTES + SenderInfoParser.SIZE_BYTES
clone.senderSsrc shouldBe srPacket.senderSsrc
clone.buffer shouldNotBe srPacket.buffer
}
}
}
// context("from explicit values") {
// val srPacket = RtcpSrPacket(
// header = expectedHeader,
// senderInfo = expectedSenderInfo,
// reportBlocks = mutableListOf(
// reportBlock1,
// reportBlock2
// )
// )
// should("set all values correctly") {
// srPacket.header shouldBe expectedHeader
// srPacket.senderInfo shouldBe expectedSenderInfo
// srPacket.reportBlocks should containAll(reportBlock1, reportBlock2)
// }
// }
// context("from an incomplete set of values") {
// val srPacket = RtcpSrPacket(
// header = RtcpHeader(reportCount = 2, senderSsrc = 12345),
// senderInfo = expectedSenderInfo,
// reportBlocks = mutableListOf(
// reportBlock1,
// reportBlock2
// )
// )
// val parsedPacket = RtcpSrPacket.fromBuffer(srPacket.getBuffer())
// should("set all values correctly") {
// RtcpHeaderTest.rtcpHeaderEquals(parsedPacket.header, expectedHeader)
// parsedPacket.senderInfo shouldBe expectedSenderInfo
// parsedPacket.reportBlocks should containAll(reportBlock1, reportBlock2)
// }
// }
}
// context("serialization") {
// val expectedBuf = ByteBuffer.allocate(1024)
// expectedBuf.put(expectedHeader.getBuffer())
// expectedBuf.put(expectedSenderInfo.getBuffer())
// expectedBuf.put(reportBlock1.getBuffer())
// expectedBuf.put(reportBlock2.getBuffer())
// expectedBuf.flip()
// val srPacket = RtcpSrPacket.fromBuffer(expectedBuf)
//
// context("via getting its buffer") {
// val actualBuf = srPacket.getBuffer()
// should("write all values correctly") {
// actualBuf should haveSameContentAs(expectedBuf)
// }
// }
// context("to an existing buffer") {
// val existingBuf = ByteBuffer.allocate(1024)
// existingBuf.position(8)
// srPacket.serializeTo(existingBuf)
// should("write the data to the proper place") {
// val subBuf = existingBuf.subBuffer(8, expectedBuf.limit())
// subBuf should haveSameContentAs(expectedBuf)
// }
// should("leave the buffer's position after the field it just wrote") {
// existingBuf.position() shouldBe (8 + expectedBuf.limit())
// }
// }
// }
context("building") {
context("with too many report blocks") {
should("throw") {
shouldThrow<IllegalArgumentException> {
RtcpSrPacketBuilder(
reportBlocks = generateSequence { reportBlock1 }.take(32).toList()
).build()
}
}
}
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.payload_specific_fb
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.util.byteBufferOf
internal class RtcpFbRembPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
val rembPacket1Buf = byteBufferOf(
0x8f, 0xce, 0x00, 0x05,
0xfe, 0x93, 0x23, 0x03,
0x00, 0x00, 0x00, 0x00,
0x52, 0x45, 0x4d, 0x42,
0x01, 0x0b, 0xa4, 0x5a,
0x9e, 0x72, 0x01, 0xc1
)
val rembPacket2Buf = byteBufferOf(
0x8f, 0xce, 0x00, 0x06,
0xfe, 0x93, 0x23, 0x03,
0x00, 0x00, 0x00, 0x00,
0x52, 0x45, 0x4d, 0x42,
0x02, 0x0b, 0xb4, 0x5a,
0x9e, 0x72, 0x01, 0xc1,
0x9e, 0x23, 0x54, 0xa3
)
val rembPacket3Buf = byteBufferOf(
0x8f, 0xce, 0x00, 0x06,
0xfe, 0x93, 0x23, 0x03,
0x00, 0x00, 0x00, 0x00,
0x52, 0x45, 0x4d, 0x42,
0x02, 0xff, 0xff, 0xff,
0x9e, 0x72, 0x01, 0xc1,
0x9e, 0x23, 0x54, 0xa3
)
init {
context("Creating an RtcpFbRembPacket") {
context("from normal values") {
val rembPacket = RtcpFbRembPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 4567L
),
ssrcs = listOf(1234L),
brBps = 1_000_000L
).build()
should("set the values correctly") {
rembPacket.senderSsrc shouldBe 4567L
rembPacket.ssrcs shouldBe listOf(1234L)
rembPacket.bitrate shouldBe 1_000_000L
}
}
context("from values which overflow the field size by a lot") {
val rembPacket = RtcpFbRembPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 4567L
),
ssrcs = listOf(1234L),
brBps = Long.MAX_VALUE
).build()
should("set the values correctly") {
rembPacket.senderSsrc shouldBe 4567L
rembPacket.ssrcs shouldBe listOf(1234L)
// the difference between Long.MAX_VALUE and 9223336852482686976L is expected and stems from the
// fact that we (only) have 18 bits of resolution for the mantissa.
rembPacket.bitrate shouldBe 9223336852482686976L
}
}
context("from values which overflow the field size by a little") {
val rembPacket = RtcpFbRembPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 4567L
),
ssrcs = listOf(1234L),
brBps = 50_000_000L
).build()
should("set the values correctly") {
rembPacket.senderSsrc shouldBe 4567L
rembPacket.ssrcs shouldBe listOf(1234L)
// the difference between 50_000_000L and 49999872L is expected and it stems from the fact that we
// (only) have 18 bits of resolution for the mantissa.
rembPacket.bitrate shouldBe 49999872L
}
}
context("creation") {
context("from a buffer of a simple REMB") {
val rembPacket =
RtcpFbRembPacket(rembPacket1Buf.array(), rembPacket1Buf.arrayOffset(), rembPacket1Buf.limit())
should("read everything correctly") {
rembPacket.senderSsrc shouldBe 0xfe932303
rembPacket.numSsrc shouldBe 1
rembPacket.ssrcs shouldBe listOf(0x9e7201c1)
rembPacket.bitrate shouldBe 954728
}
}
context("from a buffer of a REMB with two ssrcs") {
val rembPacket =
RtcpFbRembPacket(rembPacket2Buf.array(), rembPacket2Buf.arrayOffset(), rembPacket2Buf.limit())
should("read everything correctly") {
rembPacket.senderSsrc shouldBe 0xfe932303
rembPacket.numSsrc shouldBe 2
rembPacket.ssrcs shouldBe listOf(0x9e7201c1, 0x9e2354a3)
rembPacket.bitrate shouldBe 971112
}
}
context("from a buffer of a REMB signaling unbound bandwidth") {
val rembPacket =
RtcpFbRembPacket(rembPacket3Buf.array(), rembPacket3Buf.arrayOffset(), rembPacket3Buf.limit())
should("read everything correctly") {
rembPacket.senderSsrc shouldBe 0xfe932303
rembPacket.numSsrc shouldBe 2
rembPacket.ssrcs shouldBe listOf(0x9e7201c1, 0x9e2354a3)
rembPacket.bitrate shouldBe Long.MAX_VALUE
}
}
}
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.util.byteBufferOf
import org.jitsi.test_helpers.matchers.haveSameContentAs
internal class RtcpFbNackPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
// private val samplePacketId = 0
// private val sampleBlp = GenericNackBlp(listOf(1, 3, 5, 7, 9, 11, 13, 15))
// private val sampleMediaSsrc = 12345L
// private val sampleHeader =
// RtcpHeader(length = 3, packetType = TransportLayerFbPacket.PT,
// reportCount = RtcpFbNackPacket.FMT)
// val sampleRtcpFbNackPacketBuf = ByteBuffer.allocate(16).apply {
// put(sampleHeader.getBuffer())
// putInt(sampleMediaSsrc.toInt())
// putShort(samplePacketId.toShort())
// put(sampleBlp.getBuffer())
// }.flip() as ByteBuffer
val multipleNackBlocksBuf = byteBufferOf(
0x81, 0xCD, 0x00, 0x04,
0xA2, 0x78, 0xEB, 0x18,
0x84, 0x50, 0x49, 0x6C,
// packet id = 44832, other nacked packets = 44838, 44848
0xAF, 0x20, 0x80, 0x20,
// packet id = 44850, other nacked packets = 44854, 44856
0xAF, 0x32, 0x00, 0x28
)
val wrappingNackBlockBuf = byteBufferOf(
0x81, 0xCD, 0x00, 0x03,
0xA2, 0x78, 0xEB, 0x18,
0x84, 0x50, 0x49, 0x6C,
// packet id = 65530, other nacked packets = 65534, 0, 7
0xFF, 0xFA, 0x10, 0x28,
)
init {
context("Creating an RtcpFbNackPacket") {
// context("from a buffer") {
// val nackPacket = RtcpFbNackPacket.
// should("parse the values correctly") {
// nackPacket.mediaSourceSsrc shouldBe sampleMediaSsrc
// nackPacket.missingSeqNums.shouldContainInOrder(1, 3, 5, 7, 9, 11, 13, 15)
// }
// should("leave the buffer's position after the parsed data") {
// sampleRtcpFbNackPacketBuf.position() shouldBe sampleRtcpFbNackPacketBuf.limit()
// }
// }
context("from a buffer with multiple NACK blocks") {
val nackPacket = RtcpFbNackPacket(
multipleNackBlocksBuf.array(),
multipleNackBlocksBuf.arrayOffset(),
multipleNackBlocksBuf.limit()
)
should("parse the values correctly") {
nackPacket.missingSeqNums shouldContainExactly sortedSetOf(44832, 44838, 44848, 44850, 44854, 44856)
}
}
context("from a buffer with a wrapping NACK block") {
val nackPacket = RtcpFbNackPacket(
wrappingNackBlockBuf.array(),
wrappingNackBlockBuf.arrayOffset(),
wrappingNackBlockBuf.limit()
)
should("parse the values correctly") {
nackPacket.missingSeqNums shouldContainExactly sortedSetOf(65530, 65534, 0, 7)
}
}
context("from values") {
val nackPacket = RtcpFbNackPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 4567L
),
mediaSourceSsrc = 12345L,
missingSeqNums = (0..15 step 2).toSortedSet()
).build()
should("set the values correctly") {
nackPacket.senderSsrc shouldBe 4567L
nackPacket.mediaSourceSsrc shouldBe 12345L
nackPacket.missingSeqNums.shouldContainExactly((0..15 step 2).toSortedSet())
}
// context("and then serializing it") {
// context("by requesting a buffer") {
// val buf = nackPacket.getBuffer()
// should("serialize the data correctly") {
// buf should haveSameContentAs(sampleRtcpFbNackPacketBuf)
// }
// }
// context("to an existing buffer") {
// val existingBuf = ByteBuffer.allocate(RtcpFbNackPacket.SIZE_BYTES + 8)
// existingBuf.position(8)
// nackPacket.serializeTo(existingBuf)
// should("write the data to the correct place") {
// existingBuf.subBuffer(8) should haveSameContentAs(sampleRtcpFbNackPacketBuf)
// }
// should("leave the buffer's position after the written data") {
// existingBuf.position() shouldBe existingBuf.limit()
// }
// }
// }
}
context("from values too big to fit into a single NACK block") {
val rtcpFbNackPacket =
RtcpFbNackPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(senderSsrc = 2725833496),
mediaSourceSsrc = 2219854188,
missingSeqNums = sortedSetOf(44832, 44838, 44848, 44850, 44854, 44856)
).build()
context("and then serializing it") {
val buf = rtcpFbNackPacket.getBuffer()
should("serialize into multiple nack blocks") {
buf should haveSameContentAs(multipleNackBlocksBuf.array())
}
}
}
}
}
}

View File

@@ -0,0 +1,170 @@
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.ccfb
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.collections.shouldMatchEach
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.utils.abs
import org.jitsi.utils.div
import org.jitsi.utils.isFinite
import org.jitsi.utils.isInfinite
import org.jitsi.utils.ms
import org.jitsi.utils.secs
class RtcpFbCcfbPacketTest : ShouldSpec() {
init {
context("An empty CCFB") {
val builder = RtcpFbCcfbPacketBuilder(RtcpHeaderBuilder(), 0xdeadbeef, listOf(), 100_000)
val packet = builder.build()
packet.packetLength shouldBe
4 + // RTCP header
4 + // common FB header
4 + // sender ssrc
4 // timestamp
packet.mediaSourceSsrc shouldBe 0xdeadbeef
packet.reportTimestampCompactNtp shouldBe 100_000
packet.packets shouldBe listOf()
}
context("A CCFB with two ssrcs, one packet each") {
val packets = listOf<PacketInfo>(
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 1),
ReceivedPacketInfo(ssrc = 2, sequenceNumber = 1)
)
val builder = RtcpFbCcfbPacketBuilder(RtcpHeaderBuilder(), 1, packets, 1)
val packet = builder.build()
packet.packetLength shouldBe
4 + // RTCP header
4 + // common FB header
4 + // sender ssrc
2 * (
8 + // per ssrc header
1 * 2 + // packet info
2 // padding
) +
4 // timestamp
packet.mediaSourceSsrc shouldBe 1
packet.reportTimestampCompactNtp shouldBe 1
packet.packets.shouldMatch(packets)
}
context("A CCFB with two ssrcs, two packets each") {
val packets = listOf<PacketInfo>(
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 1),
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 2),
ReceivedPacketInfo(ssrc = 2, sequenceNumber = 1),
ReceivedPacketInfo(ssrc = 2, sequenceNumber = 2)
)
val builder = RtcpFbCcfbPacketBuilder(RtcpHeaderBuilder(), 1, packets, 1)
val packet = builder.build()
packet.packetLength shouldBe
4 + // RTCP header
4 + // common FB header
4 + // sender ssrc
2 * (
8 + // per ssrc header
2 * 2 // packet info
) +
4 // timestamp
packet.mediaSourceSsrc shouldBe 1
packet.reportTimestampCompactNtp shouldBe 1
packet.packets.shouldMatch(packets)
}
context("A CCFB with missing packets") {
val packets = listOf<PacketInfo>(
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 1, arrivalTimeOffset = 1.ms),
UnreceivedPacketInfo(ssrc = 1, sequenceNumber = 2),
UnreceivedPacketInfo(ssrc = 1, sequenceNumber = 3),
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 4, arrivalTimeOffset = 2.ms)
)
val builder = RtcpFbCcfbPacketBuilder(RtcpHeaderBuilder(), 1, packets, 1)
val packet = builder.build()
packet.packetLength shouldBe
4 + // RTCP header
4 + // common FB header
4 + // sender ssrc
1 * (
8 + // per ssrc header
4 * 2 // packet info
) +
4 // timestamp
packet.mediaSourceSsrc shouldBe 1
packet.reportTimestampCompactNtp shouldBe 1
packet.packets.shouldMatch(packets)
}
context("A CCFB with ECN marks") {
val packets = listOf<PacketInfo>(
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 1, arrivalTimeOffset = 1.ms, ecn = EcnMarking.kEct1),
UnreceivedPacketInfo(ssrc = 1, sequenceNumber = 2),
UnreceivedPacketInfo(ssrc = 1, sequenceNumber = 3),
ReceivedPacketInfo(ssrc = 1, sequenceNumber = 4, arrivalTimeOffset = 2.ms, ecn = EcnMarking.kCe)
)
val builder = RtcpFbCcfbPacketBuilder(RtcpHeaderBuilder(), 1, packets, 1)
val packet = builder.build()
packet.packetLength shouldBe
4 + // RTCP header
4 + // common FB header
4 + // sender ssrc
1 * (
8 + // per ssrc header
4 * 2 // packet info
) +
4 // timestamp
packet.mediaSourceSsrc shouldBe 1
packet.reportTimestampCompactNtp shouldBe 1
packet.packets.shouldMatch(packets)
}
}
}
class PacketInfoMatcher(private val expected: PacketInfo) : Matcher<PacketInfo> {
override fun test(value: PacketInfo): MatcherResult = when (expected) {
is ReceivedPacketInfo -> if (value !is ReceivedPacketInfo) {
MatcherResult(
false,
{ "Value should be $expected but was $value" },
{ "Value should not be $expected" }
)
} else {
// PacketInfo is equal after serializing-deserializing if members are equal
// except for arrival time offset that may differ because of conversion back and
// forth to CompactNtp.
val arrivalTimeOffsetEqual =
(
value.arrivalTimeOffset.isInfinite() && expected.arrivalTimeOffset.isInfinite() &&
value == expected
) ||
(
value.arrivalTimeOffset.isFinite() && expected.arrivalTimeOffset.isFinite() &&
abs(value.arrivalTimeOffset - expected.arrivalTimeOffset) < 1.secs / 1024
)
MatcherResult(
value.ssrc == expected.ssrc &&
value.sequenceNumber == expected.sequenceNumber &&
arrivalTimeOffsetEqual &&
value.ecn == expected.ecn,
{ "Value should be $expected but was $value" },
{ "Value should not be $expected" }
)
}
is UnreceivedPacketInfo -> MatcherResult(
value is UnreceivedPacketInfo &&
value.ssrc == expected.ssrc &&
value.sequenceNumber == expected.sequenceNumber,
{ "Value should be $expected but was $value" },
{ "Value should not be $expected" }
)
}
}
fun match(expected: PacketInfo) = PacketInfoMatcher(expected)
fun List<PacketInfo>.shouldMatch(expected: List<PacketInfo>) =
this.shouldMatchEach(expected.map { { p -> match(it).test(p) } })

View File

@@ -0,0 +1,211 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtcp.rtcpfb.transport_layer_fb.tcc
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.maps.shouldContainKey
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.beInstanceOf
import org.jitsi.rtp.rtcp.RtcpHeaderBuilder
import org.jitsi.rtp.util.byteBufferOf
import org.jitsi.utils.instantOfEpochMicro
import org.jitsi.utils.micros
import java.time.Duration
class RtcpFbTccPacketTest : ShouldSpec() {
fun Int.toTicks(): Short = (this * 4).toShort()
private val tccRleData = byteBufferOf(
// V=2,P=false,FMT=15,PT=205,L=7(32 bytes)
0x8f, 0xcd, 0x00, 0x07,
// Sender SSRC = 839852602
0x32, 0x0f, 0x22, 0x3a,
// Media source SSRC = 2397376430
0x8e, 0xe5, 0x0f, 0xae,
// Base seq num = 0xfffa, packet status count = 9
0xff, 0xfa, 0x00, 0x09,
// Reference Time: 1683633 = 107752512ms, feedback packet count = 87
0x19, 0xb0, 0xb1, 0x57,
// Chunks
// RLE, small delta, length = 9
0x20, 0x09,
// Deltas (9), one byte each
0xd8, 0x00,
0x18, 0x14, 0x18, 0x14,
0x18, 0x14, 0x18,
// Recv delta padding
0x00
)
/**
* These correspond to the Deltas section above.
*/
val expectedTccRlePacketInfo = mapOf<Int, Short>(
0xfffa to 0xd8,
0xfffb to 0x00,
0xfffc to 0x18,
0xfffd to 0x14,
0xfffe to 0x18,
0xffff to 0x14,
0x0000 to 0x18,
0x0001 to 0x14,
0x0002 to 0x18
)
// This also has a negative delta
private val tccMixedChunkTypeData = byteBufferOf(
// V=2,P=false,FMT=15,PT=205,L=9(40 bytes)
0x8f, 0xcd, 0x00, 0x09,
// Sender SSRC = 839852602
0x32, 0x0f, 0x22, 0x3a,
// Media source SSRC = 2397376430
0x8e, 0xe5, 0x0f, 0xae,
// Base seq num = 5376, packet status count = 12
0x15, 0x00, 0x00, 0x0c,
// Reference Time: 1684065 = 107780160ms, feedback packet count = 88
0x19, 0xb2, 0x61, 0x58,
// Chunks
// RLE: small delta, length = 9
0x20, 0x09,
// SV, 2 bit symbols: LD, SD, SD
0xe5, 0x00,
// Deltas (12)
// 2, 0, 0, 0
0x08, 0x00, 0x00, 0x00,
// 22, 1, 0, 0
0x58, 0x04, 0x00, 0x00,
// 8, -1, 1
0x20, 0xff, 0xfc, 0x04,
// 0
0x00,
// Recv delta padding
0x00, 0x00, 0x00
)
val expectedTccMixedChunkTypePacketInfo = mapOf<Int, Short>(
5376 to 2.toTicks(),
5377 to 0.toTicks(),
5378 to 0.toTicks(),
5379 to 0.toTicks(),
5380 to 22.toTicks(),
5381 to 1.toTicks(),
5382 to 0.toTicks(),
5383 to 0.toTicks(),
5384 to 8.toTicks(),
5385 to (-1).toTicks(),
5386 to 1.toTicks(),
5387 to 0.toTicks()
)
private val tccSvChunkData = byteBufferOf(
// V=2,P=false,FMT=15,PT=205,length=5(24 bytes)
0x8f, 0xcd, 0x00, 0x05,
// Sender SSRC: 839852602
0x32, 0x0f, 0x22, 0x3a,
// Media source SSRC: 2397376430
0x8e, 0xe5, 0x0f, 0xae,
// Base seq num = 6227, packet status count = 2
0x18, 0x53, 0x00, 0x02,
// Reference Time: 1684126 (107784064ms), feedback packet count = 162
0x19, 0xb2, 0x9e, 0xa2,
// Chunks
// SV chunk, 2 bit symbols: NR, SD
0xc4, 0x00,
// Deltas (1)
// 00
0x00,
// Recv delta padding
0x00
)
val expectedTccSvChunkPacketInfo = mapOf<Int, Long>(
6227 to -1,
6228 to 107784064 + 27
)
init {
context("Parsing an RtcpFbTccPacket") {
context("with RLE") {
val rtcpFbTccPacket = RtcpFbTccPacket(tccRleData.array(), tccRleData.arrayOffset(), tccRleData.limit())
should("parse the values correctly") {
rtcpFbTccPacket.forEach {
it should beInstanceOf<ReceivedPacketReport>()
it as ReceivedPacketReport
expectedTccRlePacketInfo shouldContainKey it.seqNum
withClue("seqNum ${it.seqNum} deltaTicks") {
it.deltaTicks shouldBe expectedTccRlePacketInfo[it.seqNum]
it.deltaDuration shouldBe Duration.ofNanos(it.deltaTicks * 250 * 1000L)
}
}
}
}
context("with mixed chunk types and a negative delta") {
val rtcpFbTccPacket = RtcpFbTccPacket(
tccMixedChunkTypeData.array(),
tccMixedChunkTypeData.arrayOffset(),
tccMixedChunkTypeData.limit()
)
should("parse the values correctly") {
rtcpFbTccPacket.forEach {
it should beInstanceOf<ReceivedPacketReport>()
it as ReceivedPacketReport
expectedTccMixedChunkTypePacketInfo shouldContainKey it.seqNum
withClue("seqNum ${it.seqNum} deltaTicks") {
it.deltaTicks shouldBe expectedTccMixedChunkTypePacketInfo[it.seqNum]
it.deltaDuration shouldBe Duration.ofNanos(it.deltaTicks * 250 * 1000L)
}
}
}
}
}
context("Creating an RtcpFbTccPacket") {
val rtcpFbTccPacketBuilder = RtcpFbTccPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 839852602
),
mediaSourceSsrc = 2397376430,
feedbackPacketSeqNum = 162
)
rtcpFbTccPacketBuilder.SetBase(6227, instantOfEpochMicro(107784064))
rtcpFbTccPacketBuilder.AddReceivedPacket(6228, instantOfEpochMicro(107784064)) shouldBe true
}
context("Creating and parsing an RtcpFbTccPacket") {
context("with missing packets") {
val kBaseSeqNo = 1000
val kBaseTimestamp = instantOfEpochMicro(10000L)
val rtcpFbTccPacketBuilder = RtcpFbTccPacketBuilder(
rtcpHeader = RtcpHeaderBuilder(
senderSsrc = 839852602
),
mediaSourceSsrc = 2397376430,
feedbackPacketSeqNum = 163
)
rtcpFbTccPacketBuilder.SetBase(kBaseSeqNo, kBaseTimestamp)
rtcpFbTccPacketBuilder.AddReceivedPacket(kBaseSeqNo + 0, kBaseTimestamp)
rtcpFbTccPacketBuilder.AddReceivedPacket(kBaseSeqNo + 3, kBaseTimestamp + 2000.micros)
val coded = rtcpFbTccPacketBuilder.build()
val packet = RtcpFbTccPacket(coded.buffer, coded.offset, coded.length)
val it = packet.iterator()
it.next() should beInstanceOf<ReceivedPacketReport>()
it.next() should beInstanceOf<UnreceivedPacketReport>()
it.next() should beInstanceOf<UnreceivedPacketReport>()
it.next() should beInstanceOf<ReceivedPacketReport>()
}
}
}
}

View File

@@ -0,0 +1,511 @@
package org.jitsi.rtp.rtp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.types.shouldBeTypeOf
import org.jitsi.rtp.Packet
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
import org.jitsi.test_helpers.matchers.haveSameFixedHeader
import org.jitsi.test_helpers.matchers.haveSamePayload
class RedRtpPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
init {
context("Parsing block headers") {
context("Primary") {
val block = BlockHeader.parse(byteArrayOf(0x05), 0)
block.shouldBeTypeOf<PrimaryBlockHeader>()
block.pt shouldBe 5.toByte()
}
context("Redundancy") {
val block = BlockHeader.parse(byteArrayOf(0x85, 0xff, 0xff, 0xff), 0)
block.shouldBeTypeOf<RedundancyBlockHeader>()
block.pt shouldBe 5.toByte()
block.timestampOffset shouldBe 0x3fff
block.length shouldBe 0x3ff
}
}
context("Creating block headers") {
context("Primary") {
val buf = ByteArray(1)
val blockHeader = PrimaryBlockHeader(111)
blockHeader.write(buf, 0)
val parsed = BlockHeader.parse(buf, 0)
parsed.shouldBeTypeOf<PrimaryBlockHeader>()
parsed.pt shouldBe 111.toByte()
}
context("Redundancy") {
val buf = ByteArray(4)
val blockHeader = RedundancyBlockHeader(111, 960, 55)
blockHeader.write(buf, 0)
val parsed = BlockHeader.parse(buf, 0)
parsed.shouldBeTypeOf<RedundancyBlockHeader>()
parsed.pt shouldBe 111.toByte()
parsed.timestampOffset shouldBe 960
parsed.length shouldBe 55
}
context("Redundancy with large timestamp diff") {
shouldThrow<IllegalArgumentException> {
RedundancyBlockHeader(111, RedundancyBlockHeader.MAX_TIMESTAMP_OFFSET + 1, 55)
}
}
}
context("Parsing a RED packet with a single block") {
val packet = RtpRedPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size)
packet.payloadType shouldBe 112
packet.sequenceNumber shouldBe 0x7b2b
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 66
context("Without reading the redundancy blocks") {
packet.decapsulate(parseRedundancy = false)
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 0x7b2b
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 65
}
context("And reading the redundancy blocks") {
val redundantPackets = packet.decapsulate(parseRedundancy = true)
redundantPackets.shouldBeEmpty()
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 0x7b2b
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 65
}
}
context("Parsing a RED packet with multiple blocks") {
val packet = RtpRedPacket(redPacketBytes.clone(), 0, redPacketBytes.size)
packet.payloadType shouldBe 112
packet.sequenceNumber shouldBe 0x7b2b
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 4 + 4 + 1 + 65 + 68 + 62 // block headers + block lengths
context("Without reading the redundancy blocks") {
packet.decapsulate(parseRedundancy = false)
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 0x7b2b
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 62
}
context("And reading the redundancy block") {
// Make sure we test the case when the sequence numbers wrap
packet.sequenceNumber = 1
val redundancyPackets = packet.decapsulate(parseRedundancy = true)
redundancyPackets.size shouldBe 2
redundancyPackets.forEachIndexed { i, p ->
// Offset relative to the sequence number of the primary. The first redundancy packet has
// offset=-2, the second has previous has offset=-1
val offset = i - 2
p.payloadType shouldBe 111
p.sequenceNumber shouldBe ((1 + offset) and 0xffff)
p.timestamp shouldBe (0x44b2836e + offset * 960)
p.ssrc shouldBe packet.ssrc
p.hasExtensions shouldBe false
}
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 1
packet.timestamp shouldBe 0x44b2836e
packet.ssrc shouldBe 0x16493f2d
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 62
}
}
context("Parsing a RED packet with short blocks") {
val buffer = ByteArray(
redPacketBytesShortBlocks.size +
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET +
Packet.BYTES_TO_LEAVE_AT_END_OF_PACKET
)
redPacketBytesShortBlocks.copyInto(buffer, RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET)
val packet = RtpRedPacket(
buffer,
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET,
redPacketBytesShortBlocks.size
)
packet.payloadType shouldBe 112
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 4 + 1 + 3 + 3 // block headers + block lengths
context("Without reading the redundancy blocks") {
packet.decapsulate(parseRedundancy = false)
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 3
}
context("And reading the redundancy block") {
val redundancyPackets = packet.decapsulate(parseRedundancy = true)
redundancyPackets.size shouldBe 1
redundancyPackets[0].payloadType shouldBe 111
redundancyPackets[0].sequenceNumber shouldBe 35005
redundancyPackets[0].timestamp shouldBe 862774129L - 960L
redundancyPackets[0].ssrc shouldBe 624250097L
redundancyPackets[0].hasExtensions shouldBe false
redundancyPackets[0].payloadLength shouldBe 3
packet.payloadType shouldBe 111
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe true
packet.getHeaderExtension(1).shouldHaveId1AndLen1()
packet.payloadLength shouldBe 3
}
}
context("Parsing a RED packet with zero-length blocks") {
val packet = RtpRedPacket(
redPacketBytesZeroLengthBlocks.clone(),
0,
redPacketBytesZeroLengthBlocks.size
)
packet.payloadType shouldBe 112
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe false
packet.payloadLength shouldBe 4 + 1 // block headers
context("Without reading the redundancy blocks") {
packet.decapsulate(parseRedundancy = false)
packet.payloadType shouldBe 0
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe false
packet.payloadLength shouldBe 0
}
context("And reading the redundancy block") {
val redundancyPackets = packet.decapsulate(parseRedundancy = true)
redundancyPackets.size shouldBe 1
redundancyPackets[0].payloadType shouldBe 0
redundancyPackets[0].sequenceNumber shouldBe 35005
redundancyPackets[0].timestamp shouldBe 862774129L
redundancyPackets[0].ssrc shouldBe 624250097L
redundancyPackets[0].hasExtensions shouldBe false
redundancyPackets[0].payloadLength shouldBe 0
packet.payloadType shouldBe 0
packet.sequenceNumber shouldBe 35006
packet.timestamp shouldBe 862774129L
packet.ssrc shouldBe 624250097L
packet.hasExtensions shouldBe false
packet.payloadLength shouldBe 0
}
}
context("Parsing a truncated RED packet") {
context("Without buffer padding") {
val packet = RtpRedPacket(
redPacketBytesZeroLengthBlocks.clone(),
0,
redPacketBytesZeroLengthBlocks.size - 1
)
should("throw IllegalArgumentException") {
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = false)
}
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = true)
}
}
}
context("With buffer padding") {
val buffer = ByteArray(
redPacketBytesZeroLengthBlocks.size - 1 +
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET +
Packet.BYTES_TO_LEAVE_AT_END_OF_PACKET
)
redPacketBytesZeroLengthBlocks.copyInto(
buffer,
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET,
0,
redPacketBytesZeroLengthBlocks.size - 1
)
val packet = RtpRedPacket(
buffer,
RtpPacket.BYTES_TO_LEAVE_AT_START_OF_PACKET,
redPacketBytesZeroLengthBlocks.size - 1
)
should("throw IllegalArgumentException") {
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = false)
}
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = true)
}
}
}
context("With a short buffer") {
val buffer = ByteArray(redPacketBytesZeroLengthBlocks.size - 1)
redPacketBytesZeroLengthBlocks.copyInto(buffer, 0, 0, redPacketBytesZeroLengthBlocks.size - 1)
val packet = RtpRedPacket(buffer, 0, redPacketBytesZeroLengthBlocks.size - 1)
should("throw IllegalArgumentException") {
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = false)
}
shouldThrow<IllegalArgumentException> {
packet.decapsulate(parseRedundancy = true)
}
}
}
}
context("Creating a RED packet without redundancy") {
val rtpPacket = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
}
val redPacket = RtpRedPacket.builder.build(112, rtpPacket, emptyList())
redPacket.length shouldBe rtpPacket.length + 1
redPacket.payloadType shouldBe 112
redPacket.decapsulate(parseRedundancy = false)
val reconstructed = RtpPacket(redPacket.buffer, redPacket.offset, redPacket.length)
reconstructed should haveSameFixedHeader(rtpPacket)
reconstructed should haveSamePayload(rtpPacket)
}
context("Creating a RED packet with redundancy") {
val primary = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
}
val redundancy1 = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
timestamp = primary.timestamp - 1920
sequenceNumber = primary.sequenceNumber - 2
}
val redundancy2 = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
timestamp = primary.timestamp - 960
sequenceNumber = primary.sequenceNumber - 1
}
val redPacket = RtpRedPacket.builder.build(112, primary, listOf(redundancy1, redundancy2))
redPacket.length shouldBe primary.length + 1 + redundancy1.payloadLength + 4 + redundancy2.payloadLength + 4
redPacket.payloadType shouldBe 112
val reconstructedRedundancy = redPacket.decapsulate(parseRedundancy = true)
val reconstructedPrimary = RtpPacket(redPacket.buffer, redPacket.offset, redPacket.length)
reconstructedPrimary should haveSameFixedHeader(primary)
reconstructedPrimary should haveSamePayload(primary)
reconstructedRedundancy shouldHaveSize 2
mapOf(
redundancy1 to reconstructedRedundancy[0],
redundancy2 to reconstructedRedundancy[1]
).forEach { (expected, reconstructed) ->
reconstructed.hasPadding shouldBe expected.hasPadding
reconstructed.hasExtensions shouldBe false
reconstructed.csrcCount shouldBe 0
reconstructed.payloadType shouldBe expected.payloadType
reconstructed.sequenceNumber shouldBe expected.sequenceNumber
reconstructed.ssrc shouldBe expected.ssrc
reconstructed.timestamp shouldBe expected.timestamp
reconstructed should haveSamePayload(expected)
}
}
context("Creating a RED packet with a large timestamp delta") {
val primary = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
}
val redundancy1 = RtpPacket(redPacketBytesSingleBlock.clone(), 0, redPacketBytesSingleBlock.size).apply {
payloadType = 111
timestamp = primary.timestamp - RedundancyBlockHeader.MAX_TIMESTAMP_OFFSET - 1
sequenceNumber = primary.sequenceNumber - 2
}
shouldThrow<IllegalArgumentException> { RtpRedPacket.builder.build(112, primary, listOf(redundancy1)) }
}
}
}
private fun RtpPacket.HeaderExtension?.shouldHaveId1AndLen1() {
this shouldNotBe null
this as RtpPacket.HeaderExtension
id shouldBe 1
dataLengthBytes shouldBe 1
}
private val redPacketBytes = byteArrayOf(
// RTP Header. PT=112, seq=0x7b2b
0x90, 0x70, 0x7b, 0x2b,
// TS
0x44, 0xb2, 0x83, 0x6e,
// SSRC
0x16, 0x49, 0x3f, 0x2d,
// Extension
0xbe, 0xde, 0x00, 0x01,
// ID=1, value=0x3d
0x10, 0x3d, 0x00, 0x00,
// RTP Payload
// RED Header Block 1: follow=true, pt=111, ts_offset=1920, length=65
0xef, 0x1e, 0x00, 0x41,
// RED Header Block 2: follow=true, pt=111, ts_offset=960, length=68
0xef, 0x0f, 0x00, 0x44,
// RED Header Block 3: follow=false, pt=111 (remaining length=62)
0x6f,
// RED Block 1 (length=65):
0x68, 0x2f, 0x3b, 0x25,
0xab, 0x1a, 0x72, 0xfb,
0x75, 0x2d, 0xf0, 0x9b,
0xa2, 0xa3, 0xfc, 0x20,
0x51, 0xf7, 0x9c, 0xe8,
0x75, 0xe9, 0x02, 0xd6,
0xc1, 0xe4, 0xa1, 0x51,
0x02, 0x00, 0x59, 0x55,
0x04, 0x56, 0x5e, 0xed,
0x31, 0x55, 0xb5, 0x04,
0x9d, 0xf6, 0x1c, 0x40,
0x7b, 0xb7, 0x00, 0x0c,
0xd9, 0x7b, 0x5d, 0x13,
0x4c, 0xeb, 0x7d, 0xf1,
0x74, 0xf8, 0xd5, 0xb9,
0x07, 0xda, 0x18, 0x19,
0x92,
// RED Block 2 (length=68):
0x68, 0x2f, 0x3b, 0x08,
0xa5, 0x08, 0xb1, 0x93,
0x5b, 0x65, 0x41, 0x30,
0x55, 0xcd, 0xb7, 0xca,
0xd2, 0x66, 0xc8, 0x53,
0x18, 0x95, 0x9d, 0x49,
0x81, 0x0a, 0xba, 0x67,
0xf2, 0x42, 0xe3, 0xad,
0x26, 0x73, 0x14, 0x52,
0x62, 0x03, 0x3d, 0x1e,
0xdd, 0x58, 0x44, 0x4e,
0xc9, 0x56, 0x2f, 0x77,
0xf9, 0x64, 0xf2, 0x6e,
0x8c, 0x39, 0x35, 0x1a,
0xba, 0x10, 0xe2, 0x85,
0x1f, 0x28, 0x87, 0xb9,
0x02, 0xa4, 0x68, 0x6b,
// RED Block 3 (length is the remainder, 62):
0x68, 0x2f, 0x3b, 0x2f,
0xb9, 0x5f, 0xc4, 0x65,
0x67, 0x7f, 0x5e, 0xa0,
0x46, 0xb0, 0x93, 0xf5,
0xc3, 0x5c, 0x69, 0xd7,
0x1f, 0x75, 0xe9, 0xef,
0xd1, 0x94, 0xdc, 0x47,
0x24, 0x13, 0xf8, 0x6e,
0xda, 0x18, 0xf3, 0x72,
0x5b, 0x73, 0x03, 0x8c,
0xc6, 0x2c, 0x7a, 0xab,
0x38, 0xad, 0x87, 0x6c,
0x9b, 0x08, 0x85, 0x08,
0x8f, 0xa3, 0x72, 0xf1,
0xf4, 0x7d, 0x88, 0xc3,
0x13, 0x84
)
private val redPacketBytesSingleBlock = byteArrayOf(
// RTP Header. PT=112, seq=0x7b2b
0x90, 0x70, 0x7b, 0x2b,
// TS
0x44, 0xb2, 0x83, 0x6e,
// SSRC
0x16, 0x49, 0x3f, 0x2d,
// Extension
0xbe, 0xde, 0x00, 0x01,
// ID=1, value=0x3d
0x10, 0x3d, 0x00, 0x00,
// RTP Payload
// RED Header Block 1: follow=false, pt=111
0x6f,
// RED Block 1 (length=65):
0x68, 0x2f, 0x3b, 0x25,
0xab, 0x1a, 0x72, 0xfb,
0x75, 0x2d, 0xf0, 0x9b,
0xa2, 0xa3, 0xfc, 0x20,
0x51, 0xf7, 0x9c, 0xe8,
0x75, 0xe9, 0x02, 0xd6,
0xc1, 0xe4, 0xa1, 0x51,
0x02, 0x00, 0x59, 0x55,
0x04, 0x56, 0x5e, 0xed,
0x31, 0x55, 0xb5, 0x04,
0x9d, 0xf6, 0x1c, 0x40,
0x7b, 0xb7, 0x00, 0x0c,
0xd9, 0x7b, 0x5d, 0x13,
0x4c, 0xeb, 0x7d, 0xf1,
0x74, 0xf8, 0xd5, 0xb9,
0x07, 0xda, 0x18, 0x19,
0x92
)
private val redPacketBytesShortBlocks = byteArrayOf(
// RTP Header. PT=112, seq=35006
0x90, 0x70, 0x88, 0xBE,
// TS
0x33, 0x6C, 0xE3, 0x71,
// SSRC
0x25, 0x35, 0x4C, 0xF1,
// Extension
0xBE, 0xDE, 0x00, 0x02,
// ID=5, val=0xEA4D; ID=1, val=0xFF
0x51, 0xEA, 0x4D, 0x10, 0xFF, 0x00, 0x00, 0x00,
// RED Header block 1: follow=true, pt=111, ts_offset=960, length=3
0xEF, 0x0F, 0x00, 0x03,
// RED Header block 2: follow=false, pt=111 (remaining length=3)
0x6F,
// Block 1
0xD8, 0xFF, 0xFE,
// Block 2
0xD8, 0xFF, 0xFE
)
private val redPacketBytesZeroLengthBlocks = byteArrayOf(
// RTP Header. PT=112, seq=35006
0x80, 0x70, 0x88, 0xBE,
// TS
0x33, 0x6C, 0xE3, 0x71,
// SSRC
0x25, 0x35, 0x4C, 0xF1,
// RED Header block 1: follow=true, pt=0, ts_offset=0, length=0
0x80, 0x00, 0x00, 0x00,
// RED Header block 2: follow=false, pt=0 (remaining length=0)
0x00
)

View File

@@ -0,0 +1,203 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.shouldBe
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
// TODO: sets shouldn't change any other field
class RtpHeaderTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
// override fun isolationMode(): IsolationMode? = IsolationMode.SingleInstance
private val headerData = byteArrayOf(
// V=2,P=false,X=true,CC=2,M=false,PT=100,SeqNum=16535
0x92, 0x64, 0x40, 0x97,
// Timestamp: 3899068446
0xe8, 0x67, 0x10, 0x1e,
// SSRC: 2828806853
0xa8, 0x9c, 0x2a, 0xc5,
// CSRC 1: 123456
0x00, 0x01, 0xE2, 0x40,
// CSRC 2: 45678
0x00, 0x00, 0xB2, 0x6E,
// 1 extension
0xbe, 0xde, 0x00, 0x01,
0x51, 0x00, 0x02, 0x00
)
init {
context("version") {
context("get") {
should("work correctly") {
RtpHeader.getVersion(headerData, 0) shouldBe 2
}
}
context("set") {
should("work correctly") {
RtpHeader.setVersion(headerData, 0, 3)
RtpHeader.getVersion(headerData, 0) shouldBe 3
RtpHeader.setVersion(headerData, 0, 0)
RtpHeader.getVersion(headerData, 0) shouldBe 0
}
}
}
context("hasPadding") {
context("get") {
should("work correctly") {
RtpHeader.hasPadding(headerData, 0) shouldBe false
}
}
context("set") {
should("work correctly") {
RtpHeader.setPadding(headerData, 0, true)
RtpHeader.hasPadding(headerData, 0) shouldBe true
RtpHeader.setPadding(headerData, 0, false)
RtpHeader.hasPadding(headerData, 0) shouldBe false
}
}
}
context("hasExtensions") {
context("get") {
should("work correctly") {
RtpHeader.hasExtensions(headerData, 0) shouldBe true
}
}
context("set") {
should("work correctly") {
RtpHeader.setHasExtensions(headerData, 0, false)
RtpHeader.hasExtensions(headerData, 0) shouldBe false
RtpHeader.setHasExtensions(headerData, 0, true)
RtpHeader.hasExtensions(headerData, 0) shouldBe true
}
}
}
context("csrcCount") {
context("get") {
should("work correctly") {
RtpHeader.getCsrcCount(headerData, 0) shouldBe 2
}
}
context("set") {
should("work correctly") {
RtpHeader.setCsrcCount(headerData, 0, 0)
RtpHeader.getCsrcCount(headerData, 0) shouldBe 0
}
}
}
context("marker") {
context("get") {
should("work correctly") {
RtpHeader.getMarker(headerData, 0) shouldBe false
}
}
context("set") {
should("work correctly") {
RtpHeader.setMarker(headerData, 0, true)
RtpHeader.getMarker(headerData, 0) shouldBe true
RtpHeader.setMarker(headerData, 0, false)
RtpHeader.getMarker(headerData, 0) shouldBe false
}
should("not clobber payload type") {
val origPt = RtpHeader.getPayloadType(headerData, 0)
RtpHeader.setMarker(headerData, 0, true)
RtpHeader.getMarker(headerData, 0) shouldBe true
RtpHeader.getPayloadType(headerData, 0) shouldBe origPt
RtpHeader.setMarker(headerData, 0, false)
RtpHeader.getMarker(headerData, 0) shouldBe false
RtpHeader.getPayloadType(headerData, 0) shouldBe origPt
}
}
}
context("payloadType") {
context("get") {
should("work correctly") {
RtpHeader.getPayloadType(headerData, 0) shouldBe 100
}
}
context("set") {
should("work correctly") {
RtpHeader.setPayloadType(headerData, 0, 111)
RtpHeader.getPayloadType(headerData, 0) shouldBe 111
RtpHeader.setPayloadType(headerData, 0, 115)
RtpHeader.getPayloadType(headerData, 0) shouldBe 115
}
}
}
context("sequenceNumber") {
context("get") {
should("work correctly") {
RtpHeader.getSequenceNumber(headerData, 0) shouldBe 16535
}
}
context("set") {
should("work correctly") {
RtpHeader.setSequenceNumber(headerData, 0, 4242)
RtpHeader.getSequenceNumber(headerData, 0) shouldBe 4242
}
}
}
context("timestamp") {
context("get") {
should("work correctly") {
RtpHeader.getTimestamp(headerData, 0) shouldBe 3899068446L
}
}
context("set") {
should("work correctly") {
RtpHeader.setTimestamp(headerData, 0, 3899064242)
RtpHeader.getTimestamp(headerData, 0) shouldBe 3899064242
}
}
}
context("ssrc") {
context("get") {
should("work correctly") {
RtpHeader.getSsrc(headerData, 0) shouldBe 2828806853
}
}
context("set") {
should("work correctly") {
RtpHeader.setSsrc(headerData, 0, 424242)
RtpHeader.getSsrc(headerData, 0) shouldBe 424242
}
}
}
context("csrcs") {
context("get") {
should("work correctly") {
RtpHeader.getCsrcs(headerData, 0) shouldContainInOrder (listOf<Long>(123456, 45678))
}
}
context("set") {
should("work correctly") {
RtpHeader.setCsrcs(headerData, 0, listOf<Long>(2468, 1357))
RtpHeader.getCsrcs(headerData, 0) shouldContainInOrder (listOf<Long>(2468, 1357))
}
}
}
context("total length") {
should("be correct") {
RtpHeader.getTotalLength(headerData, 0) shouldBe 28
}
}
}
}

View File

@@ -0,0 +1,982 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.comparables.shouldBeGreaterThanOrEqualTo
import io.kotest.matchers.comparables.shouldBeLessThan
import io.kotest.matchers.comparables.shouldBeLessThanOrEqualTo
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.jitsi.rtp.Packet
import org.jitsi.rtp.UnparsedPacket
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
import org.jitsi.rtp.extensions.bytearray.getShort
import org.jitsi.rtp.extensions.bytearray.putShort
import org.jitsi.rtp.extensions.unsigned.toPositiveInt
import org.jitsi.rtp.util.BufferPool
import org.jitsi.rtp.util.getByteAsInt
import org.jitsi.rtp.util.getIntAsLong
import org.jitsi.test_helpers.matchers.getPayload
import org.jitsi.test_helpers.matchers.haveSameContentAs
import org.jitsi.test_helpers.matchers.haveSameFixedHeader
import org.jitsi.test_helpers.matchers.haveSamePayload
import java.nio.charset.StandardCharsets
import java.util.Collections
class RtpPacketTest : ShouldSpec() {
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
private val rtpHeaderWithXBit = byteArrayOf(
// V=2,P=false,X=true,CC=0,M=false,PT=111,SeqNum=5807
0x90, 0x6f, 0x16, 0xaf,
// Timestamp: 1710483662
0x65, 0xf3, 0xe8, 0xce,
// SSRC: 1208951354
0x48, 0x0f, 0x22, 0x3a
)
private val oneByteHeaderExtensions = byteArrayOf(
// BEDE, length=1
0xbe,
0xde,
0x00,
0x01,
// ExtId=1,Length=0(1 byte),Data=FF,Padding
0x10,
0xff,
0x00,
0x00
)
private val oneByteHeaderExtensionsPaddingBetween = byteArrayOf(
// BEDE, length=2
0xbe, 0xde, 0x00, 0x02,
// ExtId=1,Length=1(2 bytes),Data=FF,Padding
0x11, 0xff, 0xff, 0x00,
// ExtId=2,Length=0(1 byte),Data=FF,Padding
0x20, 0xff, 0x00, 0x00
)
private val twoByteHeaderExtensions = byteArrayOf(
// 1000, length = 12
0x10, 0x00, 0x00, 0x0C,
0x01, 0x00, 0x02, 0x01,
0xff, 0x00, 0x03, 0x04,
0xde, 0xad, 0xbe, 0xef,
0x05, 0x22, 0x73, 0x75,
0x70, 0x65, 0x72, 0x63,
0x61, 0x6c, 0x69, 0x66,
0x72, 0x61, 0x67, 0x69,
0x6c, 0x69, 0x73, 0x74,
0x69, 0x63, 0x65, 0x78,
0x70, 0x69, 0x61, 0x6c,
0x69, 0x64, 0x6f, 0x63,
0x69, 0x6f, 0x75, 0x73
)
private val cryptexHeaderExtensions = byteArrayOf(
0xc0,
0xde,
0x00,
0x01,
0xeb,
0x92,
0x36,
0x52
)
private val rtpHeaderWithNoExtensions = byteArrayOf(
// V=2,P=false,X=false,CC=0,M=false,PT=111,SeqNum=5807
0x80, 0x6f, 0x16, 0xaf,
// Timestamp: 1710483662
0x65, 0xf3, 0xe8, 0xce,
// SSRC: 1208951354
0x48, 0x0f, 0x22, 0x3a
)
private val dummyRtpPayload = byteArrayOf(
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42
)
private val rtpPacketWithExtensions = RtpPacket(rtpHeaderWithXBit + oneByteHeaderExtensions + dummyRtpPayload)
private val rtpPacketNoExtensions = RtpPacket(rtpHeaderWithNoExtensions + dummyRtpPayload)
private val rtpPacketWithExtensionsWithPaddingBetween =
RtpPacket(rtpHeaderWithXBit + oneByteHeaderExtensionsPaddingBetween + dummyRtpPayload)
private val rtpPacketWithTwoByteExtensions =
RtpPacket(rtpHeaderWithXBit + twoByteHeaderExtensions + dummyRtpPayload)
private val rtpPacketWithCryptexExtensions =
RtpPacket(rtpHeaderWithXBit + cryptexHeaderExtensions + dummyRtpPayload)
init {
context("An RTP packet with header extensions") {
val rtpPacket = rtpPacketWithExtensions
should("be parsed correctly") {
rtpPacket.version shouldBe 2
rtpPacket.hasPadding shouldBe false
rtpPacket.hasExtensions shouldBe true
rtpPacket.csrcCount shouldBe 0
rtpPacket.isMarked shouldBe false
rtpPacket.payloadType shouldBe 111
rtpPacket.sequenceNumber shouldBe 5807
rtpPacket.timestamp shouldBe 1710483662L
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
rtpPacket.payloadLength shouldBe dummyRtpPayload.size
rtpPacket.getPayload() should haveSameContentAs(UnparsedPacket(dummyRtpPayload))
rtpPacket.sanityCheck()
}
should("allow changing the ID of a header extension") {
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id = 12
rtpPacket.getHeaderExtension(1) shouldBe null
rtpPacket.getHeaderExtension(12) shouldNotBe null
}
}
context("An RTP packet with header extensions with padding between them") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween
should("be parsed correctly") {
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
val ext1 = rtpPacket.getHeaderExtension(1)
ext1 shouldNotBe null
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 2
ext2.dataLengthBytes shouldBe 1
ext2.buffer.getByteAsInt(ext2.dataOffset) shouldBe 0xFF.toPositiveInt()
}
}
context("An RTP packet with two-byte header extensions") {
val rtpPacket = rtpPacketWithTwoByteExtensions
should("be parsed correctly") {
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext1 = rtpPacket.getHeaderExtension(1)
ext1 shouldNotBe null
ext1 as RtpPacket.HeaderExtension
ext1.dataLengthBytes shouldBe 0
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 2
ext2.dataLengthBytes shouldBe 1
ext2.buffer.getByteAsInt(ext2.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext3 = rtpPacket.getHeaderExtension(3)
ext3 shouldNotBe null
ext3 as RtpPacket.HeaderExtension
ext3.id shouldBe 3
ext3.dataLengthBytes shouldBe 4
ext3.buffer.getIntAsLong(ext3.dataOffset) shouldBe 0xDEADBEEFL
val ext4 = rtpPacket.getHeaderExtension(5)
ext4 shouldNotBe null
ext4 as RtpPacket.HeaderExtension
ext4.id shouldBe 5
ext4.dataLengthBytes shouldBe 34
String(ext4.buffer, ext4.dataOffset, ext4.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"supercalifragilisticexpialidocious"
}
}
context("An RTP packet with cryptex header extensions") {
val rtpPacket = rtpPacketWithCryptexExtensions
should("be parsed correctly") {
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
rtpPacket.extensionsProfileType shouldBe 0xC0DE
rtpPacket.getHeaderExtension(1) shouldBe null
rtpPacket.getHeaderExtension(0xe) shouldBe null
rtpPacket.getHeaderExtension(0xeb) shouldBe null
}
should("not allow header extensions to be modified") {
shouldThrow<IllegalStateException> {
rtpPacket.addHeaderExtension(1, 1)
}
shouldThrow<IllegalStateException> {
rtpPacket.removeHeaderExtension(0xe)
}
}
}
context("Adding a new RTP header extension") {
context("to an RTP packet with existing extensions") {
context("in a buffer that has no more room") {
val rtpPacket = rtpPacketWithExtensions.clone()
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext2 = rtpPacket.getHeaderExtension(3)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 3
ext2.dataLengthBytes shouldBe 2
ext2.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("in a buffer that has room to the right") {
val buf = ByteArray(rtpPacketWithExtensions.length + 100)
System.arraycopy(
rtpPacketWithExtensions.buffer,
0,
buf,
0,
rtpPacketWithExtensions.length
)
val rtpPacket = RtpPacket(buf, 0, rtpPacketWithExtensions.length)
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext2 = rtpPacket.getHeaderExtension(3)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 3
ext2.dataLengthBytes shouldBe 2
ext2.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("in a buffer that has room to the left") {
val spaceOnTheLeft = 8 // this is what we often have with Octo.
val buf = ByteArray(rtpPacketWithExtensions.length + spaceOnTheLeft + 20)
System.arraycopy(
rtpPacketWithExtensions.buffer,
0,
buf,
spaceOnTheLeft,
rtpPacketWithExtensions.length
)
val rtpPacket = RtpPacket(buf, spaceOnTheLeft, rtpPacketWithExtensions.length)
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext2 = rtpPacket.getHeaderExtension(3)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 3
ext2.dataLengthBytes shouldBe 2
ext2.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
}
context("to an RTP packet with no existing extensions") {
context("in a buffer that has no more room") {
val rtpPacket = rtpPacketNoExtensions.clone()
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
// The only difference in the fixed headers is the extension bit.
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(3)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 3
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("in a buffer that has more room to the right") {
val buf = ByteArray(rtpPacketNoExtensions.length + 100)
System.arraycopy(rtpPacketNoExtensions.buffer, 0, buf, 0, rtpPacketNoExtensions.length)
val rtpPacket = RtpPacket(buf, 0, rtpPacketNoExtensions.length)
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
// The only difference in the fixed headers is the extension bit.
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(3)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 3
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("in a buffer that has more room to the left") {
val spaceOnTheLeft = 8 // this is what we often have with Octo.
val buf = ByteArray(rtpPacketNoExtensions.length + spaceOnTheLeft + 20)
System.arraycopy(
rtpPacketNoExtensions.buffer,
0,
buf,
spaceOnTheLeft,
rtpPacketNoExtensions.length
)
val rtpPacket = RtpPacket(buf, spaceOnTheLeft, rtpPacketNoExtensions.length)
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
// The only difference in the fixed headers is the extension bit.
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(3)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 3
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("in tight padding-only packets") {
for (paddingLength in 0..255) {
val length = paddingLength + RtpHeader.FIXED_HEADER_SIZE_BYTES
val rtpPacket = PaddingOnlyPacket.create(length)
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
rtpPacket.sanityCheck()
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(3)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 3
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xDEAD.toShort()
// Payload length includes padding
rtpPacket.payloadLength shouldBe length - RtpHeader.FIXED_HEADER_SIZE_BYTES
rtpPacket.paddingSize shouldBe length - RtpHeader.FIXED_HEADER_SIZE_BYTES
// This will have done a resize, so it should have left space at the end for the SRTP tag.
rtpPacket.offset + rtpPacket.length +
Packet.BYTES_TO_LEAVE_AT_END_OF_PACKET shouldBeLessThanOrEqualTo
rtpPacket.buffer.size
}
}
}
}
context("Adding a long RTP header extension") {
context("to an RTP packet with existing one-byte extensions") {
val rtpPacket = rtpPacketWithExtensions.clone()
val value = "supercalifragilisticexpialidocious".toByteArray(charset = StandardCharsets.US_ASCII)
val newExt = rtpPacket.addHeaderExtension(3, value.size)
System.arraycopy(value, 0, newExt.buffer, newExt.dataOffset, value.size)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 40
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext2 = rtpPacket.getHeaderExtension(3)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 3
ext2.dataLengthBytes shouldBe 34
String(ext2.buffer, ext2.dataOffset, ext2.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"supercalifragilisticexpialidocious"
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("to an RTP packet without extensions") {
val rtpPacket = rtpPacketNoExtensions.clone()
val value = "supercalifragilisticexpialidocious".toByteArray(charset = StandardCharsets.US_ASCII)
val newExt = rtpPacket.addHeaderExtension(3, value.size)
System.arraycopy(value, 0, newExt.buffer, newExt.dataOffset, value.size)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 36
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
val ext2 = rtpPacket.getHeaderExtension(3)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 3
ext2.dataLengthBytes shouldBe 34
String(ext2.buffer, ext2.dataOffset, ext2.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"supercalifragilisticexpialidocious"
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("to an RTP packet with two-byte extensions") {
val rtpPacket = rtpPacketWithTwoByteExtensions.clone()
val value = "Um-dittle-ittl-um-dittle-I".toByteArray(charset = StandardCharsets.US_ASCII)
val newExt = rtpPacket.addHeaderExtension(8, value.size)
System.arraycopy(value, 0, newExt.buffer, newExt.dataOffset, value.size)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 76
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext2 = rtpPacket.getHeaderExtension(5)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 5
ext2.dataLengthBytes shouldBe 34
String(ext2.buffer, ext2.dataOffset, ext2.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"supercalifragilisticexpialidocious"
val ext3 = rtpPacket.getHeaderExtension(8)
ext3 shouldNotBe null
ext3 as RtpPacket.HeaderExtension
ext3.id shouldBe 8
ext3.dataLengthBytes shouldBe 26
String(ext3.buffer, ext3.dataOffset, ext3.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"Um-dittle-ittl-um-dittle-I"
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
}
context("Adding an RTP header extension with a large ID value") {
context("to an RTP packet with existing one-byte extensions") {
val rtpPacket = rtpPacketWithExtensions.clone()
val newExt = rtpPacket.addHeaderExtension(99, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 1
ext.buffer.getByteAsInt(ext.dataOffset) shouldBe 0xFF.toPositiveInt()
val ext2 = rtpPacket.getHeaderExtension(99)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 99
ext2.dataLengthBytes shouldBe 2
ext2.buffer.getShort(ext2.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("to an RTP packet without extensions") {
val rtpPacket = rtpPacketNoExtensions.clone()
val newExt = rtpPacket.addHeaderExtension(99, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
val ext2 = rtpPacket.getHeaderExtension(99)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 99
ext2.dataLengthBytes shouldBe 2
ext2.buffer.getShort(ext2.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
context("to an RTP packet with two-byte extensions") {
val rtpPacket = rtpPacketWithTwoByteExtensions.clone()
val newExt = rtpPacket.addHeaderExtension(99, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 52
rtpPacket.extensionsProfileType shouldBe 0x1000
val ext2 = rtpPacket.getHeaderExtension(5)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.id shouldBe 5
ext2.dataLengthBytes shouldBe 34
String(ext2.buffer, ext2.dataOffset, ext2.dataLengthBytes, StandardCharsets.US_ASCII) shouldBe
"supercalifragilisticexpialidocious"
val ext3 = rtpPacket.getHeaderExtension(99)
ext3 shouldNotBe null
ext3 as RtpPacket.HeaderExtension
ext3.id shouldBe 99
ext3.dataLengthBytes shouldBe 2
ext3.buffer.getShort(ext3.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketWithExtensions)
}
}
}
context("Removing a header extension") {
context("when a header extension remains") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtension(2)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensionsWithPaddingBetween)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xFFFF.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("when no header extension remains") {
val rtpPacket = rtpPacketWithExtensions.clone()
rtpPacket.removeHeaderExtension(1)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketNoExtensions)
rtpPacket.headerLength shouldBe RtpHeader.FIXED_HEADER_SIZE_BYTES
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
rtpPacket.hasExtensions shouldBe false
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("from a two-byte header extension when the header removed is small") {
val rtpPacket = rtpPacketWithTwoByteExtensions.clone()
rtpPacket.removeHeaderExtension(1)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.extensionsProfileType shouldBe 0x1000
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 48
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.dataLengthBytes shouldBe 1
ext2.buffer.getByteAsInt(ext2.dataOffset) shouldBe 0xFF.toPositiveInt()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("from a two-byte header extension when there is a zero-length header") {
val rtpPacket = rtpPacketWithTwoByteExtensions.clone()
rtpPacket.removeHeaderExtension(5)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.extensionsProfileType shouldBe 0x1000
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 12
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.dataLengthBytes shouldBe 0
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.dataLengthBytes shouldBe 1
ext2.buffer.getByteAsInt(ext2.dataOffset) shouldBe 0xFF.toPositiveInt()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("from a two-byte header extension when all remaining headers can fit in a one-byte header") {
val rtpPacket = rtpPacketWithTwoByteExtensions.clone()
rtpPacket.removeHeaderExtension(5)
rtpPacket.removeHeaderExtension(1)
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensions)
rtpPacket.extensionsProfileType shouldBe 0xBEDE
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldNotBe null
ext2 as RtpPacket.HeaderExtension
ext2.dataLengthBytes shouldBe 1
ext2.buffer.getByteAsInt(ext2.dataOffset) shouldBe 0xFF.toPositiveInt()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
}
context("Removing all but a given header extension") {
context("when a header extension remains") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtensionsExcept(Collections.singleton(1))
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensionsWithPaddingBetween)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xFFFF.toShort()
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldBe null
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("when no header extension remains due to no match") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtensionsExcept(Collections.singleton(3))
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketNoExtensions)
rtpPacket.headerLength shouldBe RtpHeader.FIXED_HEADER_SIZE_BYTES
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
rtpPacket.hasExtensions shouldBe false
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("when no header extension remains due to empty exception set") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtensionsExcept(Collections.emptySet())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketNoExtensions)
rtpPacket.headerLength shouldBe RtpHeader.FIXED_HEADER_SIZE_BYTES
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
rtpPacket.hasExtensions shouldBe false
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
}
context("Removing and then adding header extensions") {
context("when an original header extension remains") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtensionsExcept(Collections.singleton(1))
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensionsWithPaddingBetween)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 8
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
ext as RtpPacket.HeaderExtension
ext.id shouldBe 1
ext.dataLengthBytes shouldBe 2
ext.buffer.getShort(ext.dataOffset) shouldBe 0xFFFF.toShort()
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldBe null
val ext3 = rtpPacket.getHeaderExtension(3)
ext3 shouldNotBe null
ext3 as RtpPacket.HeaderExtension
ext3.id shouldBe 3
ext3.dataLengthBytes shouldBe 2
ext3.buffer.getShort(ext3.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
context("when all original header extensions are removed") {
val rtpPacket = rtpPacketWithExtensionsWithPaddingBetween.clone()
rtpPacket.removeHeaderExtensionsExcept(Collections.emptySet())
val newExt = rtpPacket.addHeaderExtension(3, 2)
newExt.buffer.putShort(newExt.dataOffset, 0xDEAD.toShort())
rtpPacket.encodeHeaderExtensions()
should("update the packet correctly") {
rtpPacket.sanityCheck()
rtpPacket should haveSameFixedHeader(rtpPacketWithExtensionsWithPaddingBetween)
rtpPacket.headerLength shouldBe
RtpHeader.FIXED_HEADER_SIZE_BYTES + RtpHeader.EXT_HEADER_SIZE_BYTES + 4
rtpPacket.extensionsProfileType shouldBe 0xBEDE
val ext = rtpPacket.getHeaderExtension(1)
ext shouldBe null
val ext2 = rtpPacket.getHeaderExtension(2)
ext2 shouldBe null
val ext3 = rtpPacket.getHeaderExtension(3)
ext3 shouldNotBe null
ext3 as RtpPacket.HeaderExtension
ext3.id shouldBe 3
ext3.dataLengthBytes shouldBe 2
ext3.buffer.getShort(ext3.dataOffset) shouldBe 0xDEAD.toShort()
rtpPacket should haveSamePayload(rtpPacketNoExtensions)
}
}
}
context("Changing the type of the header extension in the underlying buffer") {
// This is a simplified simulation of what happens when packets are cryptex-encrypted.
val rtpPacket = rtpPacketWithExtensions.clone()
val ext = rtpPacket.getHeaderExtension(1)
ext shouldNotBe null
rtpPacket.buffer.putShort(rtpPacket.offset + RtpHeader.FIXED_HEADER_SIZE_BYTES, 0xC0DE.toShort())
should("cause header extensions to not be readable") {
val ext2 = rtpPacket.getHeaderExtension(1)
ext2 shouldBe null
}
}
}
}
// This imitates PaddingVideoPacket in jmt
class PaddingOnlyPacket private constructor(
buffer: ByteArray,
offset: Int,
length: Int
) : RtpPacket(buffer, offset, length) {
override fun clone(): PaddingOnlyPacket = throw NotImplementedError("clone() not supported for padding packets.")
companion object {
/**
* Creating a PaddingVideoPacket by directly grabbing a buffer in its
* ctor is problematic because we cannot clear the buffer we retrieve
* before calling the parent class' constructor. Because the buffer
* may contain invalid data, any attempts to parse it by parent class(es)
* could fail, so we use a helper here instead
*/
fun create(length: Int): PaddingOnlyPacket {
val buf = BufferPool.getArray(length)
// It's possible we the buffer we pulled from the pool already has
// data in it, and we won't be overwriting it with anything so clear
// out the data
buf.fill(0, 0, length)
return PaddingOnlyPacket(buf, 0, length).apply {
// Recalculate the header length now that we've zero'd everything out
// and set the fields
version = RtpHeader.VERSION
headerLength = RtpHeader.getTotalLength(buffer, offset)
paddingSize = payloadLength
}
}
}
}
fun RtpPacket.sanityCheck() {
offset shouldBeGreaterThanOrEqualTo 0
length shouldBeGreaterThanOrEqualTo 0
offset + length shouldBeLessThanOrEqualTo buffer.size
headerLength shouldBeLessThanOrEqualTo length
headerLength shouldBe RtpHeader.getTotalLength(buffer, offset)
payloadLength shouldBeLessThan length
headerLength + payloadLength shouldBe length
paddingSize shouldBeLessThanOrEqualTo payloadLength
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.sequences.shouldContainInOrder
import io.kotest.matchers.shouldBe
class RtpSequenceNumberTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
init {
context("rollover") {
var num = 65535.toRtpSequenceNumber()
context("via add-and-assign") {
should("work correctly") {
num += 1
num shouldBe 0.toRtpSequenceNumber()
}
}
context("via plus") {
should("work correctly") {
(num + 1) shouldBe 0.toRtpSequenceNumber()
}
}
}
context("reverse rollover") {
var num = 0.toRtpSequenceNumber()
context("via subtract-and-assign") {
should("work correctly") {
num -= 1
num shouldBe 65535.toRtpSequenceNumber()
}
}
context("via minus") {
should("work correctly") {
(num - 1) shouldBe 65535.toRtpSequenceNumber()
}
}
}
context("comparison") {
should("work correctly") {
(1.toRtpSequenceNumber() < 2.toRtpSequenceNumber()) shouldBe true
(1.toRtpSequenceNumber() <= 2.toRtpSequenceNumber()) shouldBe true
(1.toRtpSequenceNumber() <= 1.toRtpSequenceNumber()) shouldBe true
(1.toRtpSequenceNumber() == 1.toRtpSequenceNumber()) shouldBe true
(1.toRtpSequenceNumber() < 0.toRtpSequenceNumber()) shouldBe false
(65534.toRtpSequenceNumber() < 0.toRtpSequenceNumber()) shouldBe true
(32767.toRtpSequenceNumber() < 0.toRtpSequenceNumber()) shouldBe false
(32768.toRtpSequenceNumber() < 0.toRtpSequenceNumber()) shouldBe true
}
}
context("rangeTo") {
should("work correctly") {
(65533.toRtpSequenceNumber()..2.toRtpSequenceNumber()).asSequence().shouldContainInOrder(
65533.toRtpSequenceNumber(),
65534.toRtpSequenceNumber(),
65535.toRtpSequenceNumber(),
0.toRtpSequenceNumber(),
1.toRtpSequenceNumber(),
2.toRtpSequenceNumber()
)
(2.toRtpSequenceNumber() downTo 65533.toRtpSequenceNumber()).asSequence().shouldContainInOrder(
2.toRtpSequenceNumber(),
1.toRtpSequenceNumber(),
0.toRtpSequenceNumber(),
65535.toRtpSequenceNumber(),
65534.toRtpSequenceNumber(),
65533.toRtpSequenceNumber()
)
}
}
}
}

View File

@@ -0,0 +1,605 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import jakarta.xml.bind.DatatypeConverter.parseHexBinary
class Av1DependencyDescriptorHeaderExtensionTest : ShouldSpec() {
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
/* Headers generated by Chrome 110 sending AV1 in its default configuration - L1T1 */
val descL1T1 = parseHexBinary("80000180003a410180ef808680")
val shortDesc = parseHexBinary("400001")
val descL1T3 = parseHexBinary("800001800214eaa860414d141020842701df010d")
/* Header generated by Chrome 112 sending AV1 with L3T3 set. */
val descL3T3 = parseHexBinary(
"d0013481e81485214eafffaaaa863cf0430c10c302afc0aaa0063c00430010c002a000a800060000" +
"40001d954926e082b04a0941b820ac1282503157f974000ca864330e222222eca8655304224230ec" +
"a87753013f00b3027f016704ff02cf"
)
val midDescScalable = parseHexBinary("d10146401c")
val midDescScalable2 = parseHexBinary("c203ce581d30100000")
val longForMid2 = parseHexBinary(
"8003ca80081485214eaaaaa8000600004000100002aa80a8000600004000100002a000a8000600004" +
"00016d549241b5524906d54923157e001974ca864330e222396eca8655304224390eca87753013f00b3027f016704ff02cf"
)
val descS3T3 = parseHexBinary(
"c1000180081485214ea000a8000600004000100002a000a8000600004000100002a000a8000600004" +
"0001d954926caa493655248c55fe5d00032a190cc38e58803b2a1954c10e10843b2a1dd4c01dc010803bc0218077c0434"
)
val descL3T3Key = parseHexBinary(
"8f008581e81485214eaaaaa8000600004000100002aa80a8000600004000100002a000a80006000040" +
"0016d549241b5524906d54923157e001974ca864330e222396eca8655304224390eca87753013f00b3027f016704ff02cf"
)
/* As of Chrome version 111, it doesn't support L3T3_KEY_SHIFT, but it does support L2T2_KEY_SHIFT, so test that. */
val descL2T2KeyShift = parseHexBinary(
"8700ed80e3061eaa82804028280514d14134518010a091889a09409fc059c13fc0b3c0"
)
/* The header Chrome 126 generates for VP8 keyframes when AV1 DD is enabled for VP8. It has no chains. */
val descNoChains = parseHexBinary(
"8000138002044eaaaf2860414d34538a0940413fc0b3c0"
)
init {
context("AV1 Dependency Descriptors") {
context("a descriptor with a single-layer dependency structure") {
val ld1r = Av1DependencyDescriptorReader(descL1T1, 0, descL1T1.size)
val ld1 = ld1r.parse(null)
should("be parsed properly") {
ld1.startOfFrame shouldBe true
ld1.endOfFrame shouldBe false
ld1.frameNumber shouldBe 0x0001
val structure = ld1.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 1
structure.maxTemporalId shouldBe 0
structure.maxSpatialId shouldBe 0
}
should("be parseable statelessly") {
val ld1s = ld1r.parseStateless()
ld1s.startOfFrame shouldBe true
ld1s.endOfFrame shouldBe false
ld1s.frameNumber shouldBe 0x0001
val structure = ld1s.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 1
}
should("calculate correct frame info") {
val ld1i = ld1.frameInfo
ld1i.spatialId shouldBe 0
ld1i.temporalId shouldBe 0
}
should("Calculate its own length properly") {
ld1.encodedLength shouldBe descL1T1.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(ld1.encodedLength)
ld1.write(buf, 0, buf.size)
buf shouldBe descL1T1
}
}
context("a descriptor with a scalable dependency structure") {
val ldsr = Av1DependencyDescriptorReader(descL3T3, 0, descL3T3.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe true
lds.frameNumber shouldBe 0x0134
lds.activeDecodeTargetsBitmask shouldBe 0x1ff
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 9
structure.maxTemporalId shouldBe 2
structure.maxSpatialId shouldBe 2
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
for (fromS in 0..2) {
for (fromT in 0..2) {
val fromDT = 3 * fromS + fromT
for (toS in 0..2) {
for (toT in 0..2) {
val toDT = 3 * toS + toT
/* With this structure you can switch down spatial layers, or to other temporal
* layers within the same spatial layer, without a keyframe; but switching up
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
if (fromS >= toS) {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
} else {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe false
}
}
}
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b000000001
structure.getDtBitmaskForDt(1) shouldBe 0b000000011
structure.getDtBitmaskForDt(2) shouldBe 0b000000111
structure.getDtBitmaskForDt(3) shouldBe 0b000001001
structure.getDtBitmaskForDt(4) shouldBe 0b000011011
structure.getDtBitmaskForDt(5) shouldBe 0b000111111
structure.getDtBitmaskForDt(6) shouldBe 0b001001001
structure.getDtBitmaskForDt(7) shouldBe 0b011011011
structure.getDtBitmaskForDt(8) shouldBe 0b111111111
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descL3T3.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descL3T3
}
}
context("a descriptor with a K-SVC dependency structure") {
val ldsr = Av1DependencyDescriptorReader(descL3T3Key, 0, descL3T3Key.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe false
lds.frameNumber shouldBe 0x0085
lds.activeDecodeTargetsBitmask shouldBe 0x1ff
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 9
structure.maxTemporalId shouldBe 2
structure.maxSpatialId shouldBe 2
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
for (fromS in 0..2) {
for (fromT in 0..2) {
val fromDT = 3 * fromS + fromT
for (toS in 0..2) {
for (toT in 0..2) {
val toDT = 3 * toS + toT
/* With this structure you can switch to other temporal
* layers within the same spatial layer, without a keyframe; but switching
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
if (fromS == toS) {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
} else {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe false
}
}
}
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b000000001
structure.getDtBitmaskForDt(1) shouldBe 0b000000011
structure.getDtBitmaskForDt(2) shouldBe 0b000000111
structure.getDtBitmaskForDt(3) shouldBe 0b000001000
structure.getDtBitmaskForDt(4) shouldBe 0b000011000
structure.getDtBitmaskForDt(5) shouldBe 0b000111000
structure.getDtBitmaskForDt(6) shouldBe 0b001000000
structure.getDtBitmaskForDt(7) shouldBe 0b011000000
structure.getDtBitmaskForDt(8) shouldBe 0b111000000
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descL3T3Key.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descL3T3Key
}
}
context("a descriptor with a K-SVC dependency structure with key shift") {
val ldsr = Av1DependencyDescriptorReader(descL2T2KeyShift, 0, descL2T2KeyShift.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe false
lds.frameNumber shouldBe 0x00ed
lds.activeDecodeTargetsBitmask shouldBe 0x0f
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 4
structure.maxTemporalId shouldBe 1
structure.maxSpatialId shouldBe 1
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
for (fromS in 0..1) {
for (fromT in 0..1) {
val fromDT = 2 * fromS + fromT
for (toS in 0..1) {
for (toT in 0..1) {
val toDT = 2 * toS + toT
/* With this structure you can switch to other temporal
* layers within the same spatial layer, without a keyframe; but switching
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
if (fromS == toS) {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
} else {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe false
}
}
}
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b0001
structure.getDtBitmaskForDt(1) shouldBe 0b0011
structure.getDtBitmaskForDt(2) shouldBe 0b0100
structure.getDtBitmaskForDt(3) shouldBe 0b1100
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descL2T2KeyShift.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descL2T2KeyShift
}
}
context("a descriptor following the dependency structure, specifying decode targets") {
val ldsr = Av1DependencyDescriptorReader(descL3T3, 0, descL3T3.size)
val lds = ldsr.parse(null)
val mdsr = Av1DependencyDescriptorReader(midDescScalable, 0, midDescScalable.size)
val mds = mdsr.parse(lds.newTemplateDependencyStructure)
should("be parsed properly") {
mds.startOfFrame shouldBe true
mds.endOfFrame shouldBe true
mds.frameNumber shouldBe 0x0146
mds.newTemplateDependencyStructure shouldBe null
mds.activeDecodeTargetsBitmask shouldBe 0x7
}
should("calculate correct frame info") {
val mdsi = mds.frameInfo
mdsi.spatialId shouldBe 0
mdsi.temporalId shouldBe 1
}
should("Calculate its own length properly") {
mds.encodedLength shouldBe midDescScalable.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(mds.encodedLength)
mds.write(buf, 0, buf.size)
buf shouldBe midDescScalable
}
}
context("another such descriptor") {
val ldsr = Av1DependencyDescriptorReader(longForMid2, 0, longForMid2.size)
val lds = ldsr.parse(null)
val mdsr = Av1DependencyDescriptorReader(midDescScalable2, 0, midDescScalable2.size)
val mds = mdsr.parse(lds.newTemplateDependencyStructure)
should("be parsed properly") {
mds.startOfFrame shouldBe true
mds.endOfFrame shouldBe true
mds.frameNumber shouldBe 0x03ce
mds.newTemplateDependencyStructure shouldBe null
mds.activeDecodeTargetsBitmask shouldBe 0x7
}
should("calculate correct frame info") {
val mdsi = mds.frameInfo
mdsi.spatialId shouldBe 0
mdsi.temporalId shouldBe 1
}
should("Calculate its own length properly") {
mds.encodedLength shouldBe midDescScalable2.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(mds.encodedLength)
mds.write(buf, 0, buf.size)
buf shouldBe midDescScalable2
}
}
context("a descriptor without a dependency structure") {
val mdsr = Av1DependencyDescriptorReader(midDescScalable, 0, midDescScalable.size)
should("be parseable as the stateless subset") {
val mds = mdsr.parseStateless()
mds.startOfFrame shouldBe true
mds.endOfFrame shouldBe true
mds.frameNumber shouldBe 0x0146
mds.newTemplateDependencyStructure shouldBe null
}
should("fail to parse if the dependency structure is not present") {
shouldThrow<Av1DependencyException> {
mdsr.parse(null)
}
}
}
context("a descriptor without extended fields") {
val ld1r = Av1DependencyDescriptorReader(descL1T1, 0, descL1T1.size)
val ld1 = ld1r.parse(null)
val sd1r = Av1DependencyDescriptorReader(shortDesc, 0, shortDesc.size)
val sd1 = sd1r.parse(ld1.newTemplateDependencyStructure)
should("be parsed properly") {
sd1.startOfFrame shouldBe false
sd1.endOfFrame shouldBe true
sd1.frameNumber shouldBe 0x0001
sd1.newTemplateDependencyStructure shouldBe null
sd1.activeDecodeTargetsBitmask shouldBe null
}
should("calculate correct frame info") {
val sd1i = sd1.frameInfo
sd1i.spatialId shouldBe 0
sd1i.temporalId shouldBe 0
}
should("Calculate its own length properly") {
sd1.encodedLength shouldBe shortDesc.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(sd1.encodedLength)
sd1.write(buf, 0, buf.size)
buf shouldBe shortDesc
}
}
context("A descriptor with a Temporal-only scalability structure ") {
val ldsr = Av1DependencyDescriptorReader(descL1T3, 0, descL1T3.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe false
lds.frameNumber shouldBe 0x0001
lds.activeDecodeTargetsBitmask shouldBe 0x7
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 3
structure.maxTemporalId shouldBe 2
structure.maxSpatialId shouldBe 0
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
val fromS = 0
for (fromT in 0..2) {
val fromDT = 3 * fromS + fromT
val toS = 0
for (toT in 0..2) {
val toDT = 3 * toS + toT
/* With this structure you can switch down spatial layers, or to other temporal
* layers within the same spatial layer, without a keyframe; but switching up
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b001
structure.getDtBitmaskForDt(1) shouldBe 0b011
structure.getDtBitmaskForDt(2) shouldBe 0b111
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descL1T3.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descL1T3
}
}
context("a descriptor with a simulcast dependency structure") {
val ldsr = Av1DependencyDescriptorReader(descS3T3, 0, descS3T3.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe true
lds.frameNumber shouldBe 0x0001
lds.activeDecodeTargetsBitmask shouldBe 0x1ff
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 9
structure.maxTemporalId shouldBe 2
structure.maxSpatialId shouldBe 2
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
for (fromS in 0..2) {
for (fromT in 0..2) {
val fromDT = 3 * fromS + fromT
for (toS in 0..2) {
for (toT in 0..2) {
val toDT = 3 * toS + toT
/* With this structure you can switch to other temporal
* layers within the same spatial layer, without a keyframe; but switching
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
if (fromS == toS) {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
} else {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe false
}
}
}
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b000000001
structure.getDtBitmaskForDt(1) shouldBe 0b000000011
structure.getDtBitmaskForDt(2) shouldBe 0b000000111
structure.getDtBitmaskForDt(3) shouldBe 0b000001000
structure.getDtBitmaskForDt(4) shouldBe 0b000011000
structure.getDtBitmaskForDt(5) shouldBe 0b000111000
structure.getDtBitmaskForDt(6) shouldBe 0b001000000
structure.getDtBitmaskForDt(7) shouldBe 0b011000000
structure.getDtBitmaskForDt(8) shouldBe 0b111000000
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descS3T3.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descS3T3
}
}
context("A descriptor with no chains") {
val ldsr = Av1DependencyDescriptorReader(descNoChains, 0, descNoChains.size)
val lds = ldsr.parse(null)
should("be parsed properly") {
lds.startOfFrame shouldBe true
lds.endOfFrame shouldBe false
lds.frameNumber shouldBe 0x0013
lds.activeDecodeTargetsBitmask shouldBe 0x7
val structure = lds.newTemplateDependencyStructure
structure shouldNotBe null
structure!!.decodeTargetCount shouldBe 3
structure.maxTemporalId shouldBe 2
structure.maxSpatialId shouldBe 0
}
should("calculate correct frame info") {
val ldsi = lds.frameInfo
ldsi.spatialId shouldBe 0
ldsi.temporalId shouldBe 0
}
should("calculate correctly whether layer switching needs keyframes") {
val structure = lds.newTemplateDependencyStructure!!
val fromS = 0
for (fromT in 0..2) {
val fromDT = 3 * fromS + fromT
val toS = 0
for (toT in 0..2) {
val toDT = 3 * toS + toT
/* With this structure you can switch down spatial layers, or to other temporal
* layers within the same spatial layer, without a keyframe; but switching up
* spatial layers needs a keyframe.
*/
withClue("from DT $fromDT to DT $toDT") {
structure.canSwitchWithoutKeyframe(
fromDt = fromDT,
toDt = toDT
) shouldBe true
}
}
}
}
should("calculate DTI bitmasks corresponding to a given DT") {
val structure = lds.newTemplateDependencyStructure!!
structure.getDtBitmaskForDt(0) shouldBe 0b001
structure.getDtBitmaskForDt(1) shouldBe 0b011
structure.getDtBitmaskForDt(2) shouldBe 0b111
}
should("Calculate its own length properly") {
lds.encodedLength shouldBe descNoChains.size
}
should("Be re-encoded to the same bytes") {
val buf = ByteArray(lds.encodedLength)
lds.write(buf, 0, buf.size)
buf shouldBe descNoChains
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import jakarta.xml.bind.DatatypeConverter.parseHexBinary
fun main(args: Array<String>) {
var structure: Av1TemplateDependencyStructure? = null
var line: String?
while (readLine().also { line = it } != null) {
try {
val descBinary = parseHexBinary(line)
val reader = Av1DependencyDescriptorReader(descBinary, 0, descBinary.size)
val desc = reader.parse(structure)
desc.newTemplateDependencyStructure?.let { structure = it }
println(desc.toString())
val frameInfo = desc.frameInfo
println(frameInfo)
} catch (e: Exception) {
println(e.message)
}
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright @ 2021 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.rtp.header_extensions
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.jitsi.rtp.extensions.bytearray.byteArrayOf
import org.jitsi.rtp.rtp.RtpPacket
class SdesHeaderExtensionTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
private val rtpHeaderWithEmptyExtension = byteArrayOf(
// V=2,P=false,X=true,CC=0,M=false,PT=111,SeqNum=5807
0x90, 0x6f, 0x16, 0xaf,
// Timestamp: 1710483662
0x65, 0xf3, 0xe8, 0xce,
// SSRC: 1208951354
0x48, 0x0f, 0x22, 0x3a,
)
private val dummyRtpPayload = byteArrayOf(
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42
)
private val rtpHeaderSimpleSdesExtension = byteArrayOf(
// BEDE, length=1
0xbe,
0xde,
0x00,
0x01,
// ExtId=1,Length=0(1 byte),Data='1',Padding
0x10,
0x31,
0x00,
0x00
)
private val rtpHeaderEmojiSdesExtension = byteArrayOf(
// BEDE, length=2
0xbe, 0xde, 0x00, 0x02,
// ExtId=5,Length=3(4 byte),Data='Poop emoji',Padding
0x53, 0xf0, 0x9f, 0x92,
0xa9, 0x00, 0x00, 0x00
)
private val rtpHeaderMaxSdesExtension = byteArrayOf(
// BEDE, length=5
0xbe, 0xde, 0x00, 0x05,
// ExtId=15,Length=15(16 byte),Data='abcdefghijklmnop',Padding
0xff, 0x61, 0x62, 0x63,
0x64, 0x65, 0x66, 0x67,
0x68, 0x69, 0x6a, 0x6b,
0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x00, 0x00, 0x00
)
private val simpleSdesHeaderExtension = RtpPacket(
rtpHeaderWithEmptyExtension +
rtpHeaderSimpleSdesExtension + dummyRtpPayload
)
private val maxLengthSdesHeaderExtension = RtpPacket(
rtpHeaderWithEmptyExtension +
rtpHeaderMaxSdesExtension + dummyRtpPayload
)
private val emojiSdesHeaderExtension = RtpPacket(
rtpHeaderWithEmptyExtension +
rtpHeaderEmojiSdesExtension + dummyRtpPayload
)
init {
context("RTP with simple SDES extension value 1") {
should("simple SDES should be parsed correctly") {
val rtpPacket = simpleSdesHeaderExtension
rtpPacket shouldNotBe null
val sdesExt = rtpPacket.getHeaderExtension(1)
sdesExt shouldNotBe null
sdesExt as RtpPacket.HeaderExtension
sdesExt.id shouldBe 1
sdesExt.dataLengthBytes shouldBe 1
val payload = SdesHeaderExtension.getTextValue(sdesExt)
payload shouldBe "1"
}
should("allow changing the simple SDES value") {
val rtpPacket = simpleSdesHeaderExtension
rtpPacket shouldNotBe null
val sdesExt = rtpPacket.getHeaderExtension(1)
sdesExt shouldNotBe null
sdesExt as RtpPacket.HeaderExtension
SdesHeaderExtension.setTextValue(sdesExt, "2")
val payload = SdesHeaderExtension.getTextValue(sdesExt)
payload shouldBe "2"
}
}
context("RTP with max len SDES extension value abcdefghijklmop") {
should("max length SDES should be parsed correctly") {
val rtpPacket = maxLengthSdesHeaderExtension
rtpPacket shouldNotBe null
val sdesExt = rtpPacket.getHeaderExtension(15)
sdesExt shouldNotBe null
sdesExt as RtpPacket.HeaderExtension
sdesExt.id shouldBe 15
sdesExt.dataLengthBytes shouldBe 16
val payload = SdesHeaderExtension.getTextValue(sdesExt)
payload shouldBe "abcdefghijklmnop"
}
should("allow changing the max length SDES value") {
val rtpPacket = maxLengthSdesHeaderExtension
rtpPacket shouldNotBe null
val sdesExt = rtpPacket.getHeaderExtension(15)
sdesExt shouldNotBe null
sdesExt as RtpPacket.HeaderExtension
SdesHeaderExtension.setTextValue(sdesExt, "ZYX23acj00yxzABC")
val payload = SdesHeaderExtension.getTextValue(sdesExt)
payload shouldBe "ZYX23acj00yxzABC"
}
}
context("RTP with emjoi SDES extension") {
should("emoji SDES should not be parsed") {
val rtpPacket = emojiSdesHeaderExtension
rtpPacket shouldNotBe null
val sdesExt = rtpPacket.getHeaderExtension(5)
sdesExt shouldNotBe null
sdesExt as RtpPacket.HeaderExtension
sdesExt.id shouldBe 5
sdesExt.dataLengthBytes shouldBe 4
val payload = SdesHeaderExtension.getTextValue(sdesExt)
/* The payload here is UTF-8, but we only parse ASCII.
* Parsing doesn't fail, but the string contains garbage. */
payload shouldNotBe null
}
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright @ 2024 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
class BitReaderTest : ShouldSpec() {
override fun isolationMode(): IsolationMode = IsolationMode.InstancePerLeaf
init {
val bitReader = BitReader(
byteArrayOf(
0b0101_1010.toByte(),
0b0000_1111.toByte(),
0b1010_1010.toByte(),
0b1010_1010.toByte(),
0b0111_1111.toByte()
)
)
val bits = 40
context("Reading single bits as boolean") {
bitReader.bitAsBoolean() shouldBe false
bitReader.bitAsBoolean() shouldBe true
bitReader.bitAsBoolean() shouldBe false
bitReader.bitAsBoolean() shouldBe true
bitReader.bitAsBoolean() shouldBe true
bitReader.remainingBits() shouldBe bits - 5
}
context("Reading single bits as Integer") {
bitReader.bit() shouldBe 0
bitReader.bit() shouldBe 1
bitReader.bit() shouldBe 0
bitReader.bit() shouldBe 1
bitReader.bit() shouldBe 1
bitReader.remainingBits() shouldBe bits - 5
}
context("Reading multiple bits as Integer") {
bitReader.bits(4) shouldBe 0b0101
bitReader.bits(4) shouldBe 0b1010
bitReader.bits(6) shouldBe 0b000011
bitReader.bits(2) shouldBe 0b11
bitReader.remainingBits() shouldBe bits - 16
}
context("Reading multiple bits as Long") {
bitReader.bitsLong(4) shouldBe 0b0101L
bitReader.bitsLong(4) shouldBe 0b1010L
bitReader.bits(6) shouldBe 0b000011L
bitReader.bits(2) shouldBe 0b11L
bitReader.remainingBits() shouldBe bits - 16
}
context("skip bits correctly") {
bitReader.skipBits(4)
bitReader.remainingBits() shouldBe bits - 4
bitReader.bit() shouldBe 1
bitReader.remainingBits() shouldBe bits - 5
}
context("Reading LEB128-encoded unsigned integers") {
bitReader.leb128() shouldBe 0b0101_1010
bitReader.remainingBits() shouldBe bits - 8
bitReader.leb128() shouldBe 0b00001111
bitReader.remainingBits() shouldBe bits - 16
bitReader.leb128() shouldBe 0b111_1111__010_1010__010_1010
bitReader.remainingBits() shouldBe 0
}
context("Cloning") {
bitReader.skipBits(8)
val clone = bitReader.clone(2)
bitReader.remainingBits() shouldBe bits - 8
clone.remainingBits() shouldBe 16
bitReader.bits(8) shouldBe 0b0000_1111
bitReader.remainingBits() shouldBe bits - 16
clone.remainingBits() shouldBe 16
clone.bits(8) shouldBe 0b0000_1111
bitReader.remainingBits() shouldBe bits - 16
clone.remainingBits() shouldBe 8
clone.skipBits(8)
bitReader.remainingBits() shouldBe bits - 16
clone.remainingBits() shouldBe 0
bitReader.bits(8) shouldBe 0b1010_1010
shouldThrow<IllegalStateException> {
clone.bits(8)
}
}
context("Throwing exception when reading past the bounds") {
bitReader.skipBits(bits)
bitReader.remainingBits() shouldBe 0
shouldThrow<IllegalStateException> {
bitReader.bitAsBoolean()
}
shouldThrow<IllegalStateException> {
bitReader.bit()
}
shouldThrow<IllegalStateException> {
bitReader.bits(4)
}
shouldThrow<IllegalStateException> {
bitReader.leb128()
}
shouldThrow<IllegalStateException> {
BitReader(byteArrayOf(0b1111_0000.toByte())).leb128()
}
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
internal class ByteBufferUtilsKtTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf
init {
context("byteBufferOf(Any)") {
should("work correctly with values that can be converted to byte") {
val buf = byteBufferOf(0x00, 0x01, 0xFF, 0xFE)
buf.get(0) shouldBe 0x00.toByte()
buf.get(1) shouldBe 0x01.toByte()
buf.get(2) shouldBe 0xFF.toByte()
buf.get(3) shouldBe 0xFE.toByte()
}
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.rtp.util
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.data.forAll
import io.kotest.data.row
import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.shouldBe
class RtpUtilsTest : ShouldSpec() {
init {
context("getSequenceNumberDelta") {
should("work correctly") {
forAll(
row(1, 10, -9),
row(10, 1, 9),
row(1, 65530, 7),
row(65530, 1, -7),
row(1234, 1234, 0)
) { a, b, expected ->
RtpUtils.getSequenceNumberDelta(a, b) shouldBe expected
}
}
}
context("applySequenceNumberDelta") {
should("work correctly") {
forAll(
row(10, -9, 1),
row(1, 9, 10),
row(65530, 7, 1),
row(1, -7, 65530),
row(1234, 0, 1234)
) { start, delta, expected ->
RtpUtils.applySequenceNumberDelta(start, delta) shouldBe expected
}
}
}
context("isNewerThan") {
should("work correctly") {
forAll(
row(2, 1, true),
row(2, 65530, true),
row(2, 4, false)
) { a, b, expected ->
a isNewerThan b shouldBe expected
}
}
}
context("applyTimestampDelta") {
should("work correctly") {
forAll(
row(10L, -9L, 1L),
row(1L, 9L, 10L),
row(0xffff_fff0L, 0x11L, 1L),
row(1L, -0x11L, 0xffff_fff0L),
row(1234L, 0L, 1234L)
) { start, delta, expected ->
RtpUtils.applyTimestampDelta(start, delta) shouldBe expected
}
}
}
context("isOlderThan") {
should("work correctly") {
forAll(
row(2, 1, false),
row(2, 65530, false),
row(2, 4, true)
) { a, b, expected ->
a isOlderThan b shouldBe expected
}
}
}
context("rolledOverTo") {
should("return true when a rollover has taken place") {
65535 rolledOverTo 1 shouldBe true
65000 rolledOverTo 200 shouldBe true
}
should("return false when a rollover has not occurred") {
0 rolledOverTo 65535 shouldBe false
}
}
context("isNextAfter") {
should("return true for sequential packets") {
2 isNextAfter 1 shouldBe true
0 isNextAfter 65535 shouldBe true
}
should("return false for non-sequential packets") {
2 isNextAfter 0 shouldBe false
1 isNextAfter 2 shouldBe false
1 isNextAfter 65535 shouldBe false
}
}
context("sequenceNumbersBetween") {
should("contain all sequence numbers between 2 values") {
RtpUtils.sequenceNumbersBetween(1, 10).toList() shouldContainInOrder
listOf(2, 3, 4, 5, 6, 7, 8, 9)
}
should("contain all sequence numbers between 2 values, even with rollover") {
RtpUtils.sequenceNumbersBetween(65530, 5).toList() shouldContainInOrder
listOf(65531, 65532, 65533, 65535, 0, 1, 2, 3, 4)
}
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.test_helpers.matchers
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import org.jitsi.rtp.extensions.bytearray.toHex
fun haveSameContentAs(expected: ByteArray) = object : Matcher<ByteArray> {
override fun test(value: ByteArray): MatcherResult {
var matches = true
if (expected.size != value.size) {
matches = false
} else {
for (i in (0 until expected.size)) {
if (value.get(i) != expected.get(i)) {
matches = false
break
}
}
}
return MatcherResult(
matches,
{ "\n${value.toHex()}\nwas supposed to be:\n${expected.toHex()}" },
{ "\n${value.toHex()}\nshould not have equaled \n${expected.toHex()}" }
)
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright @ 2019 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.test_helpers.matchers
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import org.jitsi.rtp.extensions.toHex
import org.jitsi.utils.ByteArrayBuffer
fun ByteArrayBuffer.hasSameContentAs(other: ByteArrayBuffer): Boolean {
if (other.length != length) {
return false
} else {
for (i in (0 until length)) {
if (buffer[offset + i] != other.buffer[other.offset + i]) {
return false
}
}
}
return true
}
fun haveSameContentAs(expected: ByteArrayBuffer) = object : Matcher<ByteArrayBuffer> {
override fun test(value: ByteArrayBuffer): MatcherResult = MatcherResult(
value.hasSameContentAs(expected),
{ "\n${value.toHex()}\nwas supposed to be:\n${expected.toHex()}" },
{ "\n${value.toHex()}\nshould not have equaled \n${expected.toHex()}" }
)
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright @ 2018 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.test_helpers.matchers
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import org.jitsi.rtp.extensions.compareToFromBeginning
import org.jitsi.rtp.extensions.toHex
import java.nio.ByteBuffer
fun haveSameContentAs(expected: ByteBuffer) = object : Matcher<ByteBuffer> {
override fun test(value: ByteBuffer): MatcherResult = MatcherResult(
value.compareToFromBeginning(expected) == 0,
{ "Buffer\n${value.toHex()}\nwas supposed to be:\n${expected.toHex()}" },
{ "Buffer\n${value.toHex()}\nshould not have equaled buffer\n${expected.toHex()}" }
)
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright @ 2019 - present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.test_helpers.matchers
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import org.jitsi.rtp.UnparsedPacket
import org.jitsi.rtp.extensions.toHex
import org.jitsi.rtp.rtp.RtpHeader
import org.jitsi.rtp.rtp.RtpPacket
import org.jitsi.utils.ByteArrayBuffer
fun RtpPacket.getPayload(): ByteArrayBuffer = UnparsedPacket(buffer, payloadOffset, payloadLength)
fun RtpPacket.getFixedHeaderAsBAB(): ByteArrayBuffer = UnparsedPacket(buffer, offset, RtpHeader.FIXED_HEADER_SIZE_BYTES)
fun haveSamePayload(expected: RtpPacket) = object : Matcher<RtpPacket> {
override fun test(value: RtpPacket): MatcherResult {
val valuePayload = value.getPayload()
val expectedPayload = expected.getPayload()
return MatcherResult(
valuePayload.hasSameContentAs(expectedPayload),
{ "\n${valuePayload.toHex()}\nwas supposed to be:\n${expectedPayload.toHex()}" },
{
"\n${valuePayload.toHex()}\nshould not have equaled \n${expectedPayload.toHex()}"
}
)
}
}
fun haveSameFixedHeader(expected: RtpPacket) = object : Matcher<RtpPacket> {
override fun test(value: RtpPacket): MatcherResult {
val valueHeader = value.getFixedHeaderAsBAB()
val expectedHeader = expected.getFixedHeaderAsBAB()
return MatcherResult(
valueHeader.hasSameContentAs(expectedHeader),
{ "\n${valueHeader.toHex()}\nwas supposed to be:\n${expectedHeader.toHex()}" },
{ "\n${valueHeader.toHex()}\nshould not have equaled \n${expectedHeader.toHex()}" }
)
}
}