Add support for extracting H.265/HEVC data in MP4 files.

This commit is contained in:
Oliver Woodman 2015-05-28 17:06:26 +01:00
parent ac54b4f696
commit 5c9c15ae6e
8 changed files with 103 additions and 74 deletions

View file

@ -39,7 +39,8 @@ import java.util.List;
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1");
public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1");
public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
@ -58,6 +59,8 @@ import java.util.List;
public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC");
public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");

View file

@ -20,8 +20,8 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.util.Ac3Util;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
@ -331,25 +331,22 @@ import java.util.List;
Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = stsd.readInt();
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv) {
parseAvcFromParent(stsd, childStartPosition, childAtomSize, durationUs, holder, i);
|| childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v
|| childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1) {
parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, durationUs, holder, i);
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
|| childAtomType == Atom.TYPE_ac_3) {
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs,
holder, i);
} else if (childAtomType == Atom.TYPE_TTML) {
holder.mediaFormat = MediaFormat.createTtmlFormat();
} else if (childAtomType == Atom.TYPE_mp4v) {
holder.mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize,
durationUs);
}
stsd.setPosition(childStartPosition + childAtomSize);
}
return holder;
}
/** Returns the media format for an avc1 box. */
private static void parseAvcFromParent(ParsableByteArray parent, int position, int size,
private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size,
long durationUs, StsdDataHolder out, int entryIndex) {
parent.setPosition(position + Atom.HEADER_SIZE);
@ -361,6 +358,7 @@ import java.util.List;
List<byte[]> initializationData = null;
int childPosition = parent.getPosition();
String mimeType = null;
while (childPosition - position < size) {
parent.setPosition(childPosition);
int childStartPosition = parent.getPosition();
@ -372,9 +370,22 @@ import java.util.List;
Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_avcC) {
Assertions.checkState(mimeType == null);
mimeType = MimeTypes.VIDEO_H264;
Pair<List<byte[]>, Integer> avcCData = parseAvcCFromParent(parent, childStartPosition);
initializationData = avcCData.first;
out.nalUnitLengthFieldLength = avcCData.second;
} else if (childAtomType == Atom.TYPE_hvcC) {
Assertions.checkState(mimeType == null);
mimeType = MimeTypes.VIDEO_H265;
Pair<List<byte[]>, Integer> hvcCData = parseHvcCFromParent(parent, childStartPosition);
initializationData = hvcCData.first;
out.nalUnitLengthFieldLength = hvcCData.second;
} else if (childAtomType == Atom.TYPE_esds) {
Assertions.checkState(mimeType == null);
mimeType = MimeTypes.VIDEO_MP4V;
initializationData =
Collections.singletonList(parseEsdsFromParent(parent, childStartPosition));
} else if (childAtomType == Atom.TYPE_sinf) {
out.trackEncryptionBoxes[entryIndex] =
parseSinfFromParent(parent, childStartPosition, childAtomSize);
@ -383,9 +394,8 @@ import java.util.List;
}
childPosition += childAtomSize;
}
out.mediaFormat = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
durationUs, width, height, pixelWidthHeightRatio, initializationData);
out.mediaFormat = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, durationUs,
width, height, pixelWidthHeightRatio, initializationData);
}
private static Pair<List<byte[]>, Integer> parseAvcCFromParent(ParsableByteArray parent,
@ -401,15 +411,58 @@ import java.util.List;
// expose the AVC profile and level somewhere useful; Most likely in MediaFormat.
int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F;
for (int j = 0; j < numSequenceParameterSets; j++) {
initializationData.add(H264Util.parseChildNalUnit(parent));
initializationData.add(NalUnitUtil.parseChildNalUnit(parent));
}
int numPictureParameterSets = parent.readUnsignedByte();
for (int j = 0; j < numPictureParameterSets; j++) {
initializationData.add(H264Util.parseChildNalUnit(parent));
initializationData.add(NalUnitUtil.parseChildNalUnit(parent));
}
return Pair.create(initializationData, nalUnitLengthFieldLength);
}
private static Pair<List<byte[]>, Integer> parseHvcCFromParent(ParsableByteArray parent,
int position) {
// Skip to the NAL unit length size field.
parent.setPosition(position + Atom.HEADER_SIZE + 21);
int lengthSizeMinusOne = parent.readUnsignedByte() & 0x03;
// Calculate the combined size of all VPS/SPS/PPS bitstreams.
int numberOfArrays = parent.readUnsignedByte();
int csdLength = 0;
int csdStartPosition = parent.getPosition();
for (int i = 0; i < numberOfArrays; i++) {
parent.skipBytes(1); // completeness (1), nal_unit_type (7)
int numberOfNalUnits = parent.readUnsignedShort();
for (int j = 0; j < numberOfNalUnits; j++) {
int nalUnitLength = parent.readUnsignedShort();
csdLength += 4 + nalUnitLength; // Start code and NAL unit.
parent.skipBytes(nalUnitLength);
}
}
// Concatenate the codec-specific data into a single buffer.
parent.setPosition(csdStartPosition);
byte[] buffer = new byte[csdLength];
int bufferPosition = 0;
for (int i = 0; i < numberOfArrays; i++) {
parent.skipBytes(1); // completeness (1), nal_unit_type (7)
int numberOfNalUnits = parent.readUnsignedShort();
for (int j = 0; j < numberOfNalUnits; j++) {
int nalUnitLength = parent.readUnsignedShort();
System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition,
NalUnitUtil.NAL_START_CODE.length);
bufferPosition += NalUnitUtil.NAL_START_CODE.length;
System.arraycopy(parent.data, parent.getPosition(), buffer, bufferPosition, nalUnitLength);
bufferPosition += nalUnitLength;
parent.skipBytes(nalUnitLength);
}
}
List<byte[]> initializationData = csdLength == 0 ? Collections.<byte[]>emptyList()
: Collections.singletonList(buffer);
return Pair.create(initializationData, lengthSizeMinusOne + 1);
}
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
int size) {
int childPosition = position + Atom.HEADER_SIZE;
@ -462,34 +515,6 @@ import java.util.List;
return null;
}
/** Returns the media format for an mp4v box. */
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
long durationUs) {
parent.setPosition(position + Atom.HEADER_SIZE);
parent.skipBytes(24);
int width = parent.readUnsignedShort();
int height = parent.readUnsignedShort();
parent.skipBytes(50);
List<byte[]> initializationData = new ArrayList<>(1);
int childPosition = parent.getPosition();
while (childPosition - position < size) {
parent.setPosition(childPosition);
int childStartPosition = parent.getPosition();
int childAtomSize = parent.readInt();
Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_esds) {
initializationData.add(parseEsdsFromParent(parent, childStartPosition));
}
childPosition += childAtomSize;
}
return MediaFormat.createVideoFormat(
MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, durationUs, width, height, initializationData);
}
private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
int size, long durationUs, StsdDataHolder out, int entryIndex) {
parent.setPosition(position + Atom.HEADER_SIZE);

View file

@ -26,8 +26,8 @@ import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
@ -106,7 +106,7 @@ public final class FragmentedMp4Extractor implements Extractor {
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16];

View file

@ -23,7 +23,7 @@ import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
@ -72,7 +72,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public Mp4Extractor() {
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
containerAtoms = new Stack<>();
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
parserState = STATE_READING_ATOM_HEADER;
}

View file

@ -19,8 +19,8 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray;
@ -104,7 +104,7 @@ import java.util.List;
@Override
public void seek() {
seiReader.seek();
H264Util.clearPrefixFlags(prefixFlags);
NalUnitUtil.clearPrefixFlags(prefixFlags);
sps.reset();
pps.reset();
sei.reset();
@ -128,7 +128,7 @@ import java.util.List;
// Scan the appended data, processing NAL units as they are encountered
while (offset < limit) {
int nextNalUnitOffset = H264Util.findNalUnit(dataArray, offset, limit, prefixFlags);
int nextNalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nextNalUnitOffset < limit) {
// We've seen the start of a NAL unit.
@ -139,7 +139,7 @@ import java.util.List;
feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
}
int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset);
int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nextNalUnitOffset);
int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
switch (nalUnitType) {
case NAL_UNIT_TYPE_IDR:

View file

@ -33,6 +33,7 @@ public class MimeTypes {
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc";
public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8";
public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es";

View file

@ -18,11 +18,11 @@ package com.google.android.exoplayer.util;
import java.nio.ByteBuffer;
/**
* Utility methods for handling H264 data.
* Utility methods for handling H.264/AVC and H.265/HEVC NAL units.
*/
public final class H264Util {
public final class NalUnitUtil {
/** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */
/** Four initial bytes that must prefix NAL units for decoding. */
public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
/**
@ -173,7 +173,7 @@ public final class H264Util {
return result;
}
private H264Util() {
private NalUnitUtil() {
// Prevent instantiation.
}

View file

@ -20,9 +20,9 @@ import junit.framework.TestCase;
import java.util.Arrays;
/**
* Tests for {@link H264Util}.
* Tests for {@link NalUnitUtil}.
*/
public class H264UtilTest extends TestCase {
public class NalUnitUtilTest extends TestCase {
private static final int TEST_PARTIAL_NAL_POSITION = 4;
private static final int TEST_NAL_POSITION = 10;
@ -31,19 +31,19 @@ public class H264UtilTest extends TestCase {
byte[] data = buildTestData();
// Should find NAL unit.
int result = H264Util.findNalUnit(data, 0, data.length, null);
int result = NalUnitUtil.findNalUnit(data, 0, data.length, null);
assertEquals(TEST_NAL_POSITION, result);
// Should find NAL unit whose prefix ends one byte before the limit.
result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null);
result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null);
assertEquals(TEST_NAL_POSITION, result);
// Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive).
result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null);
result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null);
assertEquals(TEST_NAL_POSITION + 3, result);
// Should find NAL unit whose prefix starts at the offset.
result = H264Util.findNalUnit(data, TEST_NAL_POSITION, data.length, null);
result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null);
assertEquals(TEST_NAL_POSITION, result);
// Shouldn't find NAL unit whose prefix starts one byte past the offset.
result = H264Util.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null);
result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null);
assertEquals(data.length, result);
}
@ -54,9 +54,9 @@ public class H264UtilTest extends TestCase {
boolean[] prefixFlags = new boolean[3];
byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);
byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length);
int result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(-1, result);
assertPrefixFlagsCleared(prefixFlags);
@ -64,9 +64,9 @@ public class H264UtilTest extends TestCase {
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3);
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(-3, result);
assertPrefixFlagsCleared(prefixFlags);
@ -75,11 +75,11 @@ public class H264UtilTest extends TestCase {
data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1);
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);
byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(data2.length, result);
result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags);
assertEquals(-2, result);
assertPrefixFlagsCleared(prefixFlags);
@ -89,13 +89,13 @@ public class H264UtilTest extends TestCase {
data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2);
data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3);
byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(data2.length, result);
result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags);
assertEquals(data3.length, result);
result = H264Util.findNalUnit(data4, 0, data4.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags);
assertEquals(-3, result);
assertPrefixFlagsCleared(prefixFlags);
@ -103,9 +103,9 @@ public class H264UtilTest extends TestCase {
prefixFlags = new boolean[3];
data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2);
data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length);
result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags);
assertEquals(data1.length, result);
result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags);
result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags);
assertEquals(4, result);
assertPrefixFlagsCleared(prefixFlags);
}