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

org.red5.io.rtp.AV1Packetizer Maven / Gradle / Ivy

There is a newer version: 2.0.15
Show newest version
package org.red5.io.rtp;

import static org.red5.io.obu.OBPConstants.N_MASK;
import static org.red5.io.obu.OBPConstants.OBU_FRAME_TYPE_BITSHIFT;
import static org.red5.io.obu.OBPConstants.OBU_FRAME_TYPE_MASK;
import static org.red5.io.obu.OBPConstants.OBU_FRAME_TYPE_SEQUENCE_HEADER;
import static org.red5.io.obu.OBPConstants.W_BITSHIFT;
import static org.red5.io.obu.OBPConstants.Y_MASK;
import static org.red5.io.obu.OBPConstants.Z_MASK;

import java.util.LinkedList;
import java.util.List;

import org.bouncycastle.util.Arrays;
import org.red5.io.obu.OBUInfo;
import org.red5.io.obu.OBUParser;
import org.red5.io.obu.OBUType;
import org.red5.io.utils.LEB128;
import org.red5.io.utils.LEB128.LEB128Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * AV1 packetizer provides methods to packetize and depacketize AV1 payloads.
 *
 * @see AV1 RTP Specification
 * @see WebRTC AV1 RTP Depacketizer
 *
 * Thanks to the Alliance for Open Media for providing the AV1 RTP specification and Pion for the Go implementation.
 *
 * @author Paul Gregoire
 */
public class AV1Packetizer {

    private static final Logger logger = LoggerFactory.getLogger(AV1Packetizer.class);

    private static final boolean isTrace = logger.isTraceEnabled(), isDebug = logger.isDebugEnabled();

    /*
        AV1 format:

        RTP payload syntax:
             0 1 2 3 4 5 6 7
            +-+-+-+-+-+-+-+-+
            |Z|Y| W |N|-|-|-| (REQUIRED)
            +=+=+=+=+=+=+=+=+ (REPEATED W-1 times, or any times if W = 0)
            |1|             |
            +-+ OBU fragment|
            |1|             | (REQUIRED, leb128 encoded)
            +-+    size     |
            |0|             |
            +-+-+-+-+-+-+-+-+
            |  OBU fragment |
            |     ...       |
            +=+=+=+=+=+=+=+=+
            |     ...       |
            +=+=+=+=+=+=+=+=+ if W > 0, last fragment MUST NOT have size field
            |  OBU fragment |
            |     ...       |
            +=+=+=+=+=+=+=+=+

        OBU syntax:
             0 1 2 3 4 5 6 7
            +-+-+-+-+-+-+-+-+
            |0| type  |X|S|-| (REQUIRED)
            +-+-+-+-+-+-+-+-+
         X: | TID |SID|-|-|-| (OPTIONAL)
            +-+-+-+-+-+-+-+-+
            |1|             |
            +-+ OBU payload |
         S: |1|             | (OPTIONAL, variable length leb128 encoded)
            +-+    size     |
            |0|             |
            +-+-+-+-+-+-+-+-+
            |  OBU payload  |
            |     ...       |
    */

    /*
      Z: MUST be set to 1 if the first OBU element is an OBU fragment that is a continuation of an OBU fragment from
         the previous packet, and MUST be set to 0 otherwise.
      Y: MUST be set to 1 if the last OBU element is an OBU fragment that will continue in the next packet, and MUST be
         set to 0 otherwise.
      N: MUST be set to 1 if the packet is the first packet of a coded video sequence, and MUST be set to 0 otherwise.
    */
    public boolean Z, Y, N;

    /*
      W: Two bit field that describes the number of OBU elements in the packet. This field MUST be set equal to 0 or
          equal to the number of OBU elements contained in the packet. If set to 0, each OBU element MUST be preceded
          by a length field. If not set to 0 (W = 1, 2 or 3) the last OBU element MUST NOT be preceded by length field.
          Instead, the length of the last OBU element contained in the packet can be calculated as follows:
            Length of the last OBU element =
              length of the RTP payload
              length of aggregation header
              length of previous OBU elements including length fields
    */
    public byte W;

    // Collection of OBU Elements; each OBU Element may be a full OBU, or just a fragment of one.
    private List OBUElements;

    // Aggregation item: Z first packet in frame is fragment
    private boolean firstPacketInFrame;

    // Aggregation item: Y last packet in frame is fragment
    private boolean lastPacketInFrame;

    // Aggregation item: N start sequence
    private boolean startSequence;

    private byte[] sequenceHeader;

    /**
     * Depacketizes an AV1 payload; this contains an aggregation header and one or more OBU elements.
     *
     * @param payload AV1 payload
     * @return number of OBU elements
     * @throws Exception on error
     */
    public int depacketize(byte[] payload) throws Exception {
        if (payload == null) {
            throw new Exception("Null packet");
        } else if (payload.length < 2) {
            throw new Exception("Short packet");
        }
        // aggregate header byte is used to indicate if the first and/or last OBU element in the payload is a fragment
        // of an OBU
        firstPacketInFrame = OBUParser.startsWithFragment(payload[0]); // Z first packet in frame is fragment
        lastPacketInFrame = OBUParser.endsWithFragment(payload[0]); // Y last packet in frame is fragment
        startSequence = OBUParser.startsNewCodedVideoSequence(payload[0]); // N
        // obu's in the payload
        int obuCount = OBUParser.obuCount(payload[0]); // W
        if (isDebug) {
            String bits = String.format("%8s", Integer.toBinaryString(0xff & payload[0])).replace(' ', '0');
            logger.debug("Depacketize - first packet in frame: {}, last packet in frame: {}, start sequence: {} count: {} bits: {}", firstPacketInFrame, lastPacketInFrame, startSequence, obuCount, bits);
        }
        if (firstPacketInFrame && startSequence) {
            throw new Exception("Packet cannot be both a fragment and keyframe");
        }
        // ensure storage for the OBU elements
        if (OBUElements == null) {
            OBUElements = new LinkedList<>();
        } else if (startSequence) {
            // if we start a new frame, clear the OBU elements
            OBUElements.clear();
        }
        // example: [ SH MD MD(0,0) FH(0,0) TG(0,0) ] [ MD(0,1) FH(0,1) TG(0,1) ]
        // parse the bodies
        int currentIndex = 1; // skip the aggregation header
        // first obu is index 1
        for (int obuIndex = 1; currentIndex < payload.length; obuIndex++) {
            OBUType obuType = null;
            int obuFragmentLength = (payload.length - currentIndex);
            // W is the obu count expected in the packet
            logger.debug("OBU element #{} of {} array index: {}", obuIndex, obuCount, currentIndex);
            if (obuCount == 0 || obuIndex < obuCount) {
                // read the length of the OBU fragment and if its 127 expect to read 2 bytes
                byte[] fragmentLen = new byte[payload[currentIndex] == 127 ? 2 : 1];
                System.arraycopy(payload, currentIndex, fragmentLen, 0, fragmentLen.length);
                LEB128Result result = LEB128.decode(fragmentLen);
                obuFragmentLength = result.value;
                //logger.trace("Index: {} new index: {}", currentIndex, (currentIndex + result[1]));
                currentIndex += result.bytesRead;
            }
            obuType = OBUType.fromValue((payload[currentIndex] & OBU_FRAME_TYPE_MASK) >>> OBU_FRAME_TYPE_BITSHIFT);
            if (obuType == null && !OBUElements.isEmpty()) {
                byte[] lastFragment = OBUElements.removeLast();
                // read the type for the last fragment
                obuType = OBUType.fromValue((lastFragment[0] & OBU_FRAME_TYPE_MASK) >>> OBU_FRAME_TYPE_BITSHIFT);
                logger.warn("Unknown OBU type, appending data of the last packet fragment: {}", obuType);
                byte[] newLastFragment = new byte[lastFragment.length + obuFragmentLength];
                System.arraycopy(lastFragment, 0, newLastFragment, 0, lastFragment.length);
                System.arraycopy(payload, currentIndex, newLastFragment, lastFragment.length, obuFragmentLength);
                OBUElements.add(newLastFragment);
            } else {
                if (isTrace) {
                    boolean obuExtensionFlag = OBUParser.obuHasExtension(payload[currentIndex]);
                    boolean obuHasSizeField = OBUParser.obuHasSize(payload[currentIndex]);
                    logger.trace("OBU type: {} extension? {} size field? {}, length: {}", obuType, obuExtensionFlag, obuHasSizeField, obuFragmentLength);
                }
                //if (payload.length < (currentIndex + obuElementLength)) {
                //    throw new Exception("Short packet");
                //}
                byte[] obuFragment = new byte[obuFragmentLength];
                System.arraycopy(payload, currentIndex, obuFragment, 0, obuFragmentLength);
                // if we have a sequence header, store it
                if (obuType == OBUType.SEQUENCE_HEADER) {
                    sequenceHeader = Arrays.clone(obuFragment);
                }
                // we don't store temporal delimiter, tile list, or padding
                if (OBUParser.isValidObu(obuType)) {
                    OBUElements.add(obuFragment);
                } else {
                    logger.debug("Skip OBU type for RTP: {}", obuType);
                }
            }
            currentIndex += obuFragmentLength;
        }
        if (isTrace) {
            logger.trace("OBU read completely? {}", (currentIndex == payload.length));
        }
        return OBUElements.size();
    }

    /**
     * Packetizes an AV1 payload.
     *
     * @param payload AV1 payload
     * @param mtu     maximum transmission unit
     * @return list of packets
     */
    public List packetize(byte[] payload, int mtu) {
        List payloads = new LinkedList<>();
        byte frameType = (byte) ((payload[0] & OBU_FRAME_TYPE_MASK) >> OBU_FRAME_TYPE_BITSHIFT);
        if (frameType == OBU_FRAME_TYPE_SEQUENCE_HEADER) {
            sequenceHeader = payload;
        } else {
            int payloadDataIndex = 0;
            int payloadDataRemaining = payload.length;
            byte obuCount = 0;
            // no meta no need to add to metadata size
            int metadataSize = 0;
            if (sequenceHeader != null && sequenceHeader.length > 0) {
                // metadata size is small so 1 byte to hold its leb128 encoded length
                metadataSize += sequenceHeader.length + 1;
                obuCount++;
            }
            do {
                int outOffset = 1; // AV1_PAYLOADER_HEADER_SIZE
                byte[] out = new byte[Math.min(mtu, payloadDataRemaining + metadataSize)];
                out[0] = (byte) (obuCount << W_BITSHIFT);
                if (obuCount == 2) {
                    out[0] ^= N_MASK;
                    out[1] = (byte) LEB128.encode(sequenceHeader.length);
                    System.arraycopy(sequenceHeader, 0, out, 2, sequenceHeader.length);
                    outOffset += sequenceHeader.length + 1; // 1 byte for LEB128
                    sequenceHeader = null;
                }
                int outBufferRemaining = out.length - outOffset;
                System.arraycopy(payload, payloadDataIndex, out, outOffset, outBufferRemaining);
                payloadDataRemaining -= outBufferRemaining;
                payloadDataIndex += outBufferRemaining;
                if (!payloads.isEmpty()) {
                    out[0] ^= Z_MASK;
                }
                if (payloadDataRemaining != 0) {
                    out[0] ^= Y_MASK;
                }
                payloads.add(out);
            } while (payloadDataRemaining > 0);
        }
        return payloads;
    }

    /**
     * Packetizes a list of AV1 OBU, consisting of a sequence header and one or more OBU elements.
     *
     * @param obuElements list of OBUInfo
     * @param mtu         maximum transmission unit
     * @return list of packets
     */
    public List packetize(List obuInfos, int mtu) {
        LinkedList payloads = new LinkedList<>();
        int aggregationHeaderLength = 1;
        int maxFragmentSize = mtu - aggregationHeaderLength - 2;
        for (OBUInfo obuInfo : obuInfos) {
            // obuInfo.data includes the OBU header, but no aggregation header nor size field
            byte frameType = (byte) ((obuInfo.obuType.getValue() & OBU_FRAME_TYPE_MASK) >> OBU_FRAME_TYPE_BITSHIFT);
            byte[] payload = obuInfo.data.array();
            int payloadDataRemaining = payload.length, payloadDataIndex = 0;
            logger.debug("Frame type: {}", frameType);
            // Make sure the fragment/payload size is correct
            if (Math.min(maxFragmentSize, payloadDataRemaining) > 0) {
                while (payloadDataRemaining > 0) {
                    int currentFragmentSize = Math.min(maxFragmentSize, payloadDataRemaining);
                    int leb128Value = LEB128.encode(currentFragmentSize);
                    byte[] out;
                    if (currentFragmentSize >= 127) { // leb takes at least 2 bytes
                        int outLen = aggregationHeaderLength + 2 + currentFragmentSize;
                        out = new byte[outLen];
                        out[1] = (byte) (leb128Value >> 8);
                        out[2] = (byte) leb128Value;
                        System.arraycopy(payload, payloadDataIndex, out, aggregationHeaderLength + 2, currentFragmentSize);
                    } else { // leb expected to be 1 byte
                        out = new byte[aggregationHeaderLength + 1 + currentFragmentSize];
                        out[1] = (byte) leb128Value;
                        System.arraycopy(payload, payloadDataIndex, out, aggregationHeaderLength + 1, currentFragmentSize);
                    }
                    payloads.add(out);
                    payloadDataRemaining -= currentFragmentSize;
                    payloadDataIndex += currentFragmentSize;
                    if (payloads.size() > 1) {
                        out[0] ^= Z_MASK;
                    }
                    if (payloadDataRemaining != 0) {
                        out[0] ^= Y_MASK;
                    }
                }
            }
        }
        return payloads;
    }

    /**
     * Resets the packetizer.
     */
    public void reset() {
        Z = Y = N = false;
        W = 0;
        if (OBUElements != null) {
            OBUElements.clear();
        }
        sequenceHeader = null;
        // reset aggregation items; also done at depacketize
        firstPacketInFrame = lastPacketInFrame = startSequence = false;
    }

    public List getOBUElements() {
        return OBUElements;
    }

    public boolean isFirstPacketInFrame() {
        return firstPacketInFrame;
    }

    public void setFirstPacketInFrame(boolean firstPacketInFrame) {
        this.firstPacketInFrame = firstPacketInFrame;
    }

    public boolean isLastPacketInFrame() {
        return lastPacketInFrame;
    }

    public void setLastPacketInFrame(boolean lastPacketInFrame) {
        this.lastPacketInFrame = lastPacketInFrame;
    }

    public boolean isStartSequence() {
        return startSequence;
    }

    public void setStartSequence(boolean startSequence) {
        this.startSequence = startSequence;
    }

    @Override
    public String toString() {
        return "AV1Packetizer [OBUElements=" + OBUElements + "]";
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy