Skip to content

Commit

Permalink
Fu a defragment - fixes issue #427 (#428)
Browse files Browse the repository at this point in the history
* Add support for FU-A defragmentation as per https://tools.ietf.org/html/rfc3984#section-5.8 (this is needed if your H264 is coming from an RTP stream)

* fix issue with warning due to jar plugin version missing

* Added test for nal unit typ and fix possible OOB error if value supplied is -ve

* use NAL unit

* Fix whitespace issues for PR

* missed a file in whitespace update ...

Co-authored-by: Gareth Floodgate <gareth@floodgate.co.uk>
  • Loading branch information
vidtec and gareth-floodgate committed Sep 15, 2020
1 parent b42e735 commit 6a0588b
Show file tree
Hide file tree
Showing 6 changed files with 516 additions and 2 deletions.
43 changes: 43 additions & 0 deletions pom.xml
Expand Up @@ -122,6 +122,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
Expand All @@ -143,6 +144,25 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<extensions>
<extension>
Expand All @@ -152,6 +172,29 @@
</extension>
</extensions>
</build>

<reporting>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>0.8.5</version>
</plugin>

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<reportSets>
<reportSet>
<reports>
<report>report</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>

<dependencies>
<dependency>
<groupId>junit</groupId>
Expand Down
205 changes: 205 additions & 0 deletions src/main/java/org/jcodec/codecs/h264/io/model/NALReasemble.java
@@ -0,0 +1,205 @@
package org.jcodec.codecs.h264.io.model;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;

public class NALReasemble
{

/** The mask for NAL header type. */
private static final int FU_TYPE_MASK = 0x1F;

/** The mask for FU start & end bits. */
private static final int FU_STARTEND_MASK = 0xC0;

/** The mask for FU start bit. */
private static final int FU_START_MASK = 0x80;

/** The mask for FU end bit. */
private static final int FU_END_MASK = 0x40;

/** The mask for NAL NRI. */
private static final int NRI_MASK = 0x60;

/** The mask for NAL forbidden bit. */
private static final int FORBIDDEN_MASK = 0x80;

// Important Notes for FU-A/B fragments:
//
// - A FU payload MAY have any number of octets and MAY be empty. [RFC3984-p30]
//
// - A fragmented NAL unit MUST NOT be transmitted in one FU; i.e., the
// Start bit and End bit MUST NOT both be set to one in the same FU
// header. [RFC3984-p30]
//
// - If a fragmentation unit is lost, the receiver SHOULD discard all
// following fragmentation units in transmission order corresponding to
// the same fragmented NAL unit. [RFC3984-p31]
//
// - A receiver in an endpoint or in a MANE MAY aggregate the first n-1
// fragments of a NAL unit to an (incomplete) NAL unit, even if fragment
// n of that NAL unit is not received. In this case, the
// forbidden_zero_bit of the NAL unit MUST be set to one to indicate a
// syntax violation. [RFC3984-p31]





/**
* Defragment FU-A NALs into a single NAL.
*
* This method assumes the following are true:
*
* 1. The NALs presented in the list are CONTIGUOUS - i.e.
* any FU packets received have been reordered based on
* RTP sequence number AND any dropped packets been detected
* and the rest of the fragment NALs have NOT been sent on this
* list - i.e. there is no END packet.
* e.g.
* RTP:seq1 RTP:seq3 RTP:seq2 RTP:seq4 RTP:seq5 ---> { [FU-A-S], [FU-A:1], [FU-A:2] }
* [FU-A-S] [FU-A:2] [FU-A:1] X [FU-A-E]
*
* NB: If an issue is detected, the F bit will be set to 1 to indicate
* to the decoder that this NAL is suspicious.
*
* 2. The list of buffers passed into this method are ONLY related to the
* same Fragmentation Unit, i.e. ALL buffers will be used to build
* the resulting NAL.
*
* 3. Each buffer in the list is at position 0, with limit set the the
* length of the data that should be extracted - i.e. payload length.
*
* For FU-A, the following 2 bytes are seen in the payload of each NAL:
*
* | FU indicator | FU header |
* +---------------+---------------+
* |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |F|NRI| TypeX |S|E|R| TypeY |
* +---------------+---------------+
*
* The defragmented NAL takes it's NAL header data from the first FU-A payload
* as follows:
*
* | NAL header |
* +---------------+
* |0|1|2|3|4|5|6|7|
* +-+-+-+-+-+-+-+-+
* |F|NRI| TypeY | NB: F bit is SET if no end marker OR
* +---------------+ start and end marker seen in a packet.
*
* NB: If NO start element is seen, this method will return byte[0]
* THE RTP interface layer should have stripped missing data as per note 1
* already. In this case, the data would never be sent to this method, as
* a missing FU-A-START would mean dropping all fragments, same as passing
* empty list.
*
* @param nals The list of ByteBuffer-based NALs that meet the requirements above.
* @return The (optional) byte[] represented the defragmented NAL - NB: data will be empty
* if the defragmentation failed due to bad data, BUT will be present if some data
* could be decoded but was not 'perfect' - in this instance the forbidden bit is set.
*/
public static byte[] defragmentFUANals(final List<ByteBuffer> nals)
{ // JAVA 8 - Optional<byte[]>

// Require a valid list of NALs to work on.
if (nals == null || nals.isEmpty())
{
return new byte[0];
}

// Peek at the first two bytes of the first NAL.
final ByteBuffer headNAL = nals.get(0);
final NALUnit nalUnit = NALUnit.read(headNAL);
headNAL.rewind();

final byte headFuIndicator = headNAL.get();
final byte headFuHeader = headNAL.get();
headNAL.rewind();

// Ensure that the first NAL is an FU-A-START ONLY (i.e. not STARt, or START & END are not allowed)
if (NALUnitType.FU_A != nalUnit.type ||
(headFuHeader & FU_START_MASK) == 0 || (headFuHeader & FU_STARTEND_MASK) == FU_STARTEND_MASK)
{
return new byte[0];
}

// The required data byte[] is the sum of (1 + (each buffer remaining - 2))
// i.e. add 1 byte for NAL header, then add each FU length - FU Indicator and FU Header.

// JAVA 8 -> byte[] data = new byte[1 + nals.stream().flatMapToInt(n -> IntStream.of(n.remaining() - 2)).sum()];
int datasize = 1;
for (int i = 0; i < nals.size(); i++)
{
datasize += nals.get(i).remaining() - 2;
}

byte[] data = new byte[datasize];
final ByteBuffer out = ByteBuffer.wrap(data);

// Set the NAL header based on parts from the first FU indicator and header.
out.put((byte) ((headFuIndicator & NRI_MASK) | (headFuHeader & FU_TYPE_MASK)));

int validUntilPosition = -1;
boolean shouldSetFbit = false;
for (int i = 0; i < nals.size(); i++)
{
final ByteBuffer nal = nals.get(i);
nal.get(); // Skip first byte.
final byte fuHeader = nal.get();

// Detect any errors in the ordering.
if ((i > 0 && ((fuHeader & FU_START_MASK) == FU_START_MASK)) ||
(i == nals.size() - 1 && ((fuHeader & FU_END_MASK) == 0)))
{
// Found a start indicator AFTER first entry OR
// DID NOT find end indicator at last entry.
// NOT IDEAL, but carry on with defragment.
shouldSetFbit = true;
}

if (nal.hasRemaining())
{
final byte[] buffer = new byte[nal.remaining()];
nal.get(buffer);
out.put(buffer);
}

if ((i != nals.size() - 1 && ((fuHeader & FU_END_MASK) == FU_END_MASK)))
{
// Found end indicator NOT at last entry.
// Need to STOP defragmenting.
shouldSetFbit = true;
validUntilPosition = out.position();
break;
}
}

// If there was an issue with the data, set the F bit - this packet is probably bad.
// i.e. no FU-END, or FU-END seen but more packets in the list.
if (shouldSetFbit)
{
if (validUntilPosition > -1)
{
// If this is a special case for truncating the data, do that first.
data = Arrays.copyOf(data, data.length - (data.length - validUntilPosition));
}

data[0] = (byte) (FORBIDDEN_MASK | data[0]);
}

return data;
}


/**
* Private constructor to prevent general instance creation.
*/
private NALReasemble()
{
// Do nothing.
}

}
Expand Up @@ -24,13 +24,14 @@ public final class NALUnitType {
public final static NALUnitType FILLER_DATA = new NALUnitType(12, "FILLER_DATA", "filler data");
public final static NALUnitType SEQ_PAR_SET_EXT = new NALUnitType(13, "SEQ_PAR_SET_EXT", "sequence parameter set extension");
public final static NALUnitType AUX_SLICE = new NALUnitType(19, "AUX_SLICE", "auxilary slice");
public final static NALUnitType FU_A = new NALUnitType(28, "FU_A", "fragmented unit a");

private final static NALUnitType[] lut;
private final static NALUnitType[] _values;

static {
_values = new NALUnitType[] { NON_IDR_SLICE, SLICE_PART_A, SLICE_PART_B, SLICE_PART_C, IDR_SLICE, SEI, SPS, PPS,
ACC_UNIT_DELIM, END_OF_SEQ, END_OF_STREAM, FILLER_DATA, SEQ_PAR_SET_EXT, AUX_SLICE };
ACC_UNIT_DELIM, END_OF_SEQ, END_OF_STREAM, FILLER_DATA, SEQ_PAR_SET_EXT, AUX_SLICE, FU_A };
lut = new NALUnitType[256];
for (int i = 0; i < _values.length; i++) {
NALUnitType nalUnitType = _values[i];
Expand All @@ -57,7 +58,7 @@ public int getValue() {
}

public static NALUnitType fromValue(int value) {
return value < lut.length ? lut[value] : null;
return value > 0 && value < lut.length ? lut[value] : null;
}

@Override
Expand Down

0 comments on commit 6a0588b

Please sign in to comment.