snapshot of https://github.com/jitsi/jitsi-videobridge.git @ fcbf741c646bc237d2ed85a1e86910532532b831 (2026-09-15)
This commit is contained in:
21
rtp/README.md
Normal file
21
rtp/README.md
Normal 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
313
rtp/pom.xml
Normal 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
49
rtp/spotbugs-exclude.xml
Normal 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>
|
||||
171
rtp/src/main/kotlin/org/jitsi/rtp/ByteArrayBufferImpl.java
Normal file
171
rtp/src/main/kotlin/org/jitsi/rtp/ByteArrayBufferImpl.java
Normal 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;
|
||||
}
|
||||
}
|
||||
69
rtp/src/main/kotlin/org/jitsi/rtp/Packet.kt
Normal file
69
rtp/src/main/kotlin/org/jitsi/rtp/Packet.kt
Normal 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
|
||||
}
|
||||
}
|
||||
38
rtp/src/main/kotlin/org/jitsi/rtp/UnparsedPacket.kt
Normal file
38
rtp/src/main/kotlin/org/jitsi/rtp/UnparsedPacket.kt
Normal 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
|
||||
)
|
||||
}
|
||||
101
rtp/src/main/kotlin/org/jitsi/rtp/extensions/Byte.kt
Normal file
101
rtp/src/main/kotlin/org/jitsi/rtp/extensions/Byte.kt
Normal 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()
|
||||
}
|
||||
@@ -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))
|
||||
212
rtp/src/main/kotlin/org/jitsi/rtp/extensions/ByteBuffer.kt
Normal file
212
rtp/src/main/kotlin/org/jitsi/rtp/extensions/ByteBuffer.kt
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
114
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/CompoundRtcpPacket.kt
Normal file
114
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/CompoundRtcpPacket.kt
Normal 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'"
|
||||
)
|
||||
74
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpByePacket.kt
Normal file
74
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpByePacket.kt
Normal 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
|
||||
}
|
||||
}
|
||||
115
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpHeader.kt
Normal file
115
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpHeader.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
108
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpPacket.kt
Normal file
108
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpPacket.kt
Normal 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)}")
|
||||
150
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpReportBlock.kt
Normal file
150
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpReportBlock.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
100
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpRrPacket.kt
Normal file
100
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpRrPacket.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
213
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpSdesPacket.kt
Normal file
213
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpSdesPacket.kt
Normal 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"
|
||||
}
|
||||
233
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpSrPacket.kt
Normal file
233
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpSrPacket.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpXrPacket.kt
Normal file
44
rtp/src/main/kotlin/org/jitsi/rtp/rtcp/RtcpXrPacket.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
289
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RedPacketParser.kt
Normal file
289
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RedPacketParser.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
164
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpHeader.kt
Normal file
164
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpHeader.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
622
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpPacket.kt
Normal file
622
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpPacket.kt
Normal 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
|
||||
}
|
||||
}
|
||||
114
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpSequenceNumber.kt
Normal file
114
rtp/src/main/kotlin/org/jitsi/rtp/rtp/RtpSequenceNumber.kt
Normal 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)
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
135
rtp/src/main/kotlin/org/jitsi/rtp/util/BitReader.kt
Normal file
135
rtp/src/main/kotlin/org/jitsi/rtp/util/BitReader.kt
Normal 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
|
||||
}
|
||||
}
|
||||
73
rtp/src/main/kotlin/org/jitsi/rtp/util/BitWriter.kt
Normal file
73
rtp/src/main/kotlin/org/jitsi/rtp/util/BitWriter.kt
Normal 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
|
||||
}
|
||||
32
rtp/src/main/kotlin/org/jitsi/rtp/util/BufferPool.kt
Normal file
32
rtp/src/main/kotlin/org/jitsi/rtp/util/BufferPool.kt
Normal 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 = { }
|
||||
}
|
||||
}
|
||||
26
rtp/src/main/kotlin/org/jitsi/rtp/util/ByteBufferUtils.kt
Normal file
26
rtp/src/main/kotlin/org/jitsi/rtp/util/ByteBufferUtils.kt
Normal 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)
|
||||
}
|
||||
42
rtp/src/main/kotlin/org/jitsi/rtp/util/FieldParsers.kt
Normal file
42
rtp/src/main/kotlin/org/jitsi/rtp/util/FieldParsers.kt
Normal 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()
|
||||
182
rtp/src/main/kotlin/org/jitsi/rtp/util/RtpUtils.kt
Normal file
182
rtp/src/main/kotlin/org/jitsi/rtp/util/RtpUtils.kt
Normal 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
|
||||
27
rtp/src/main/kotlin/org/jitsi/rtp/util/SrtpUtils.kt
Normal file
27
rtp/src/main/kotlin/org/jitsi/rtp/util/SrtpUtils.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
372
rtp/src/test/kotlin/org/jitsi/rtp/extensions/VlaExtensionTest.kt
Normal file
372
rtp/src/test/kotlin/org/jitsi/rtp/extensions/VlaExtensionTest.kt
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
98
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpByePacketTest.kt
Normal file
98
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpByePacketTest.kt
Normal 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())
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpPacketTest.kt
Normal file
68
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpPacketTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpRrPacketTest.kt
Normal file
113
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpRrPacketTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpSrPacketTest.kt
Normal file
228
rtp/src/test/kotlin/org/jitsi/rtp/rtcp/RtcpSrPacketTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) } })
|
||||
@@ -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>()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
511
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RedRtpPacketTest.kt
Normal file
511
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RedRtpPacketTest.kt
Normal 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
|
||||
)
|
||||
203
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RtpHeaderTest.kt
Normal file
203
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RtpHeaderTest.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
982
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RtpPacketTest.kt
Normal file
982
rtp/src/test/kotlin/org/jitsi/rtp/rtp/RtpPacketTest.kt
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
rtp/src/test/kotlin/org/jitsi/rtp/util/BitReaderTest.kt
Normal file
132
rtp/src/test/kotlin/org/jitsi/rtp/util/BitReaderTest.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
rtp/src/test/kotlin/org/jitsi/rtp/util/RtpUtilsTest.kt
Normal file
120
rtp/src/test/kotlin/org/jitsi/rtp/util/RtpUtilsTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()}" }
|
||||
)
|
||||
}
|
||||
@@ -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()}" }
|
||||
)
|
||||
}
|
||||
@@ -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()}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user