/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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 com.google.android.exoplayer.extractor.ts;
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.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.CodecSpecificDataUtil.SpsData;
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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Parses a continuous H264 byte stream and extracts individual frames.
*/
/* package */ final class H264Reader extends ElementaryStreamReader {
private static final int FRAME_TYPE_I = 2;
private static final int FRAME_TYPE_ALL_I = 7;
private static final int NAL_UNIT_TYPE_IFR = 1; // Coded slice of a non-IDR picture
private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture
private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter
// State that should not be reset on seek.
private boolean hasOutputFormat;
// State that should be reset on seek.
private final SeiReader seiReader;
private final boolean[] prefixFlags;
private final IfrParserBuffer ifrParserBuffer;
private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei;
private boolean foundFirstSample;
private long totalBytesWritten;
// Per sample state that gets reset at the start of each sample.
private boolean isKeyframe;
private long samplePosition;
private long sampleTimeUs;
// Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper;
public H264Reader(TrackOutput output, SeiReader seiReader, boolean idrKeyframesOnly) {
super(output);
this.seiReader = seiReader;
prefixFlags = new boolean[3];
ifrParserBuffer = (idrKeyframesOnly) ? null : new IfrParserBuffer();
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
seiWrapper = new ParsableByteArray();
}
@Override
public void seek() {
seiReader.seek();
NalUnitUtil.clearPrefixFlags(prefixFlags);
sps.reset();
pps.reset();
sei.reset();
if (ifrParserBuffer != null) {
ifrParserBuffer.reset();
}
foundFirstSample = false;
totalBytesWritten = 0;
}
@Override
public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
while (data.bytesLeft() > 0) {
int offset = data.getPosition();
int limit = data.limit();
byte[] dataArray = data.data;
// Append the data to the buffer.
totalBytesWritten += data.bytesLeft();
output.sampleData(data, data.bytesLeft());
// Scan the appended data, processing NAL units as they are encountered
while (offset < limit) {
int nextNalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags);
if (nextNalUnitOffset < limit) {
// We've seen the start of a NAL unit.
// This is the length to the start of the unit. It may be negative if the NAL unit
// actually started in previously consumed data.
int lengthToNalUnit = nextNalUnitOffset - offset;
if (lengthToNalUnit > 0) {
feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
}
int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nextNalUnitOffset);
int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
switch (nalUnitType) {
case NAL_UNIT_TYPE_IDR:
isKeyframe = true;
break;
case NAL_UNIT_TYPE_AUD:
if (foundFirstSample) {
if (ifrParserBuffer != null && ifrParserBuffer.isCompleted()) {
int sliceType = ifrParserBuffer.getSliceType();
isKeyframe |= (sliceType == FRAME_TYPE_I || sliceType == FRAME_TYPE_ALL_I);
ifrParserBuffer.reset();
}
if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps);
}
int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0;
int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit;
output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null);
}
foundFirstSample = true;
samplePosition = totalBytesWritten - bytesWrittenPastNalUnit;
sampleTimeUs = pesTimeUs;
isKeyframe = false;
break;
}
// If the length to the start of the unit is negative then we wrote too many bytes to the
// NAL buffers. Discard the excess bytes when notifying that the unit has ended.
feedNalUnitTargetEnd(pesTimeUs, lengthToNalUnit < 0 ? -lengthToNalUnit : 0);
// Notify the start of the next NAL unit.
feedNalUnitTargetBuffersStart(nalUnitType);
// Continue scanning the data.
offset = nextNalUnitOffset + 3;
} else {
feedNalUnitTargetBuffersData(dataArray, offset, limit);
offset = limit;
}
}
}
}
@Override
public void packetFinished() {
// Do nothing.
}
private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (ifrParserBuffer != null) {
ifrParserBuffer.startNalUnit(nalUnitType);
}
if (!hasOutputFormat) {
sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType);
}
sei.startNalUnit(nalUnitType);
}
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (ifrParserBuffer != null) {
ifrParserBuffer.appendToNalUnit(dataArray, offset, limit);
}
if (!hasOutputFormat) {
sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit);
}
sei.appendToNalUnit(dataArray, offset, limit);
}
private void feedNalUnitTargetEnd(long pesTimeUs, int discardPadding) {
sps.endNalUnit(discardPadding);
pps.endNalUnit(discardPadding);
if (sei.endNalUnit(discardPadding)) {
int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength);
seiWrapper.reset(sei.nalData, unescapedLength);
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(seiWrapper, pesTimeUs, true);
}
}
private void parseMediaFormat(NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
byte[] spsData = new byte[sps.nalLength];
byte[] ppsData = new byte[pps.nalLength];
System.arraycopy(sps.nalData, 0, spsData, 0, sps.nalLength);
System.arraycopy(pps.nalData, 0, ppsData, 0, pps.nalLength);
List<byte[]> initializationData = new ArrayList<>();
initializationData.add(spsData);
initializationData.add(ppsData);
// Unescape and parse the SPS unit.
NalUnitUtil.unescapeStream(sps.nalData, sps.nalLength);
ParsableBitArray bitArray = new ParsableBitArray(sps.nalData);
bitArray.skipBits(32); // NAL header
SpsData parsedSpsData = CodecSpecificDataUtil.parseSpsNalUnit(bitArray);
// Construct and output the format.
output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, parsedSpsData.width, parsedSpsData.height, 0,
parsedSpsData.pixelWidthAspectRatio, initializationData));
hasOutputFormat = true;
}
/**
* A buffer specifically for IFR units that can be used to parse the IFR's slice type.
*/
private static final class IfrParserBuffer {
private static final int DEFAULT_BUFFER_SIZE = 128;
private static final int NOT_SET = -1;
private final ParsableBitArray scratchSliceType;
private byte[] ifrData;
private int ifrLength;
private boolean isFilling;
private int sliceType;
public IfrParserBuffer() {
ifrData = new byte[DEFAULT_BUFFER_SIZE];
scratchSliceType = new ParsableBitArray(ifrData);
reset();
}
/**
* Resets the buffer, clearing any data that it holds.
*/
public void reset() {
isFilling = false;
ifrLength = 0;
sliceType = NOT_SET;
}
/**
* True if enough data was added to the buffer that the slice type was determined.
*/
public boolean isCompleted() {
return sliceType != NOT_SET;
}
/**
* Invoked to indicate that a NAL unit has started, and if it is an IFR then the buffer will
* start.
*/
public void startNalUnit(int nalUnitType) {
if (nalUnitType == NAL_UNIT_TYPE_IFR) {
reset();
isFilling = true;
}
}
/**
* Invoked to pass stream data. The data passed should not include the 3 byte start code.
*
* @param data Holds the data being passed.
* @param offset The offset of the data in {@code data}.
* @param limit The limit (exclusive) of the data in {@code data}.
*/
public void appendToNalUnit(byte[] data, int offset, int limit) {
if (!isFilling) {
return;
}
int readLength = limit - offset;
if (ifrData.length < ifrLength + readLength) {
ifrData = Arrays.copyOf(ifrData, (ifrLength + readLength) * 2);
}
System.arraycopy(data, offset, ifrData, ifrLength, readLength);
ifrLength += readLength;
scratchSliceType.reset(ifrData, ifrLength);
scratchSliceType.skipBits(8);
// first_mb_in_slice
int len = scratchSliceType.peekExpGolombCodedNumLength();
if ((len == -1) || (len > scratchSliceType.bitsLeft())) {
// Not enough yet
return;
}
scratchSliceType.skipBits(len);
// slice_type
len = scratchSliceType.peekExpGolombCodedNumLength();
if ((len == -1) || (len > scratchSliceType.bitsLeft())) {
// Not enough yet
return;
}
sliceType = scratchSliceType.readUnsignedExpGolombCodedInt();
isFilling = false;
}
/**
* @return the slice type of the IFR.
*/
public int getSliceType() {
return sliceType;
}
}
}