FLV extractor fixes

1. Only output video starting from a keyframe
2. When calculating the timestamp offset to adjust live streams to start
   at t=0, use the timestamp of the first tag from which a sample is actually
   output, rather than just the first audio/video tag. The test streams in
   the referenced GitHub issue start with a video tag whose packet type is
   AVC_PACKET_TYPE_SEQUENCE_HEADER (i.e. does not contain a sample) and whose
   timestamp is set to 0 (i.e. isn't set). The timestamp is set correctly on
   tags that from which a sample is actually output.

Issue: #6111
PiperOrigin-RevId: 256147747
This commit is contained in:
olly 2019-07-02 13:42:05 +01:00 committed by Oliver Woodman
parent 47bc70d480
commit 6febc88dce
6 changed files with 51 additions and 26 deletions

View File

@ -20,6 +20,8 @@
* Wrap decoder exceptions in a new `DecoderException` class and report as * Wrap decoder exceptions in a new `DecoderException` class and report as
renderer error. renderer error.
* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.
* FLV: Fix bug that caused playback of some live streams to not start
([#6111](https://github.com/google/ExoPlayer/issues/6111)).
### 2.10.2 ### ### 2.10.2 ###

View File

@ -86,11 +86,12 @@ import java.util.Collections;
} }
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
if (audioFormat == AUDIO_FORMAT_MP3) { if (audioFormat == AUDIO_FORMAT_MP3) {
int sampleSize = data.bytesLeft(); int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize); output.sampleData(data, sampleSize);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
return true;
} else { } else {
int packetType = data.readUnsignedByte(); int packetType = data.readUnsignedByte();
if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
@ -104,12 +105,15 @@ import java.util.Collections;
Collections.singletonList(audioSpecificConfig), null, 0, null); Collections.singletonList(audioSpecificConfig), null, 0, null);
output.format(format); output.format(format);
hasOutputFormat = true; hasOutputFormat = true;
return false;
} else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {
int sampleSize = data.bytesLeft(); int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize); output.sampleData(data, sampleSize);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
return true;
} else {
return false;
} }
} }
} }
} }

View File

@ -73,6 +73,7 @@ public final class FlvExtractor implements Extractor {
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private @States int state; private @States int state;
private boolean outputFirstSample;
private long mediaTagTimestampOffsetUs; private long mediaTagTimestampOffsetUs;
private int bytesToNextTagHeader; private int bytesToNextTagHeader;
private int tagType; private int tagType;
@ -89,7 +90,6 @@ public final class FlvExtractor implements Extractor {
tagData = new ParsableByteArray(); tagData = new ParsableByteArray();
metadataReader = new ScriptTagPayloadReader(); metadataReader = new ScriptTagPayloadReader();
state = STATE_READING_FLV_HEADER; state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
} }
@Override @Override
@ -131,7 +131,7 @@ public final class FlvExtractor implements Extractor {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
state = STATE_READING_FLV_HEADER; state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET; outputFirstSample = false;
bytesToNextTagHeader = 0; bytesToNextTagHeader = 0;
} }
@ -252,14 +252,16 @@ public final class FlvExtractor implements Extractor {
*/ */
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
boolean wasConsumed = true; boolean wasConsumed = true;
boolean wasSampleOutput = false;
long timestampUs = getCurrentTimestampUs();
if (tagType == TAG_TYPE_AUDIO && audioReader != null) { if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
ensureReadyForMediaOutput(); ensureReadyForMediaOutput();
audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);
} else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
ensureReadyForMediaOutput(); ensureReadyForMediaOutput();
videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs);
} else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
metadataReader.consume(prepareTagData(input), tagTimestampUs); wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
long durationUs = metadataReader.getDurationUs(); long durationUs = metadataReader.getDurationUs();
if (durationUs != C.TIME_UNSET) { if (durationUs != C.TIME_UNSET) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
@ -269,6 +271,11 @@ public final class FlvExtractor implements Extractor {
input.skipFully(tagDataSize); input.skipFully(tagDataSize);
wasConsumed = false; wasConsumed = false;
} }
if (!outputFirstSample && wasSampleOutput) {
outputFirstSample = true;
mediaTagTimestampOffsetUs =
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
}
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
state = STATE_SKIPPING_TO_TAG_HEADER; state = STATE_SKIPPING_TO_TAG_HEADER;
return wasConsumed; return wasConsumed;
@ -291,10 +298,11 @@ public final class FlvExtractor implements Extractor {
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
outputSeekMap = true; outputSeekMap = true;
} }
if (mediaTagTimestampOffsetUs == C.TIME_UNSET) {
mediaTagTimestampOffsetUs =
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
}
} }
private long getCurrentTimestampUs() {
return outputFirstSample
? (mediaTagTimestampOffsetUs + tagTimestampUs)
: (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);
}
} }

View File

@ -63,7 +63,7 @@ import java.util.Map;
} }
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int nameType = readAmfType(data); int nameType = readAmfType(data);
if (nameType != AMF_TYPE_STRING) { if (nameType != AMF_TYPE_STRING) {
// Should never happen. // Should never happen.
@ -72,12 +72,12 @@ import java.util.Map;
String name = readAmfString(data); String name = readAmfString(data);
if (!NAME_METADATA.equals(name)) { if (!NAME_METADATA.equals(name)) {
// We're only interested in metadata. // We're only interested in metadata.
return; return false;
} }
int type = readAmfType(data); int type = readAmfType(data);
if (type != AMF_TYPE_ECMA_ARRAY) { if (type != AMF_TYPE_ECMA_ARRAY) {
// We're not interested in this metadata. // We're not interested in this metadata.
return; return false;
} }
// Set the duration to the value contained in the metadata, if present. // Set the duration to the value contained in the metadata, if present.
Map<String, Object> metadata = readAmfEcmaArray(data); Map<String, Object> metadata = readAmfEcmaArray(data);
@ -87,6 +87,7 @@ import java.util.Map;
durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
} }
} }
return false;
} }
private static int readAmfType(ParsableByteArray data) { private static int readAmfType(ParsableByteArray data) {

View File

@ -58,12 +58,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
* *
* @param data The payload data to consume. * @param data The payload data to consume.
* @param timeUs The timestamp associated with the payload. * @param timeUs The timestamp associated with the payload.
* @return Whether a sample was output.
* @throws ParserException If an error occurs parsing the data. * @throws ParserException If an error occurs parsing the data.
*/ */
public final void consume(ParsableByteArray data, long timeUs) throws ParserException { public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException {
if (parseHeader(data)) { return parseHeader(data) && parsePayload(data, timeUs);
parsePayload(data, timeUs);
}
} }
/** /**
@ -78,10 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
/** /**
* Parses tag payload. * Parses tag payload.
* *
* @param data Buffer where tag payload is stored * @param data Buffer where tag payload is stored.
* @param timeUs Time position of the frame * @param timeUs Time position of the frame.
* @return Whether a sample was output.
* @throws ParserException If an error occurs parsing the payload. * @throws ParserException If an error occurs parsing the payload.
*/ */
protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; protected abstract boolean parsePayload(ParsableByteArray data, long timeUs)
throws ParserException;
} }

View File

@ -47,6 +47,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
// State variables. // State variables.
private boolean hasOutputFormat; private boolean hasOutputFormat;
private boolean hasOutputKeyframe;
private int frameType; private int frameType;
/** /**
@ -60,7 +61,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
@Override @Override
public void seek() { public void seek() {
// Do nothing. hasOutputKeyframe = false;
} }
@Override @Override
@ -77,7 +78,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
} }
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int packetType = data.readUnsignedByte(); int packetType = data.readUnsignedByte();
int compositionTimeMs = data.readInt24(); int compositionTimeMs = data.readInt24();
@ -94,7 +95,12 @@ import com.google.android.exoplayer2.video.AvcConfig;
avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null);
output.format(format); output.format(format);
hasOutputFormat = true; hasOutputFormat = true;
return false;
} else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) {
boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME;
if (!hasOutputKeyframe && !isKeyframe) {
return false;
}
// TODO: Deduplicate with Mp4Extractor. // TODO: Deduplicate with Mp4Extractor.
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
// they're only 1 or 2 bytes long. // they're only 1 or 2 bytes long.
@ -123,8 +129,12 @@ import com.google.android.exoplayer2.video.AvcConfig;
output.sampleData(data, bytesToWrite); output.sampleData(data, bytesToWrite);
bytesWritten += bytesToWrite; bytesWritten += bytesToWrite;
} }
output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0, output.sampleMetadata(
bytesWritten, 0, null); timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null);
hasOutputKeyframe = true;
return true;
} else {
return false;
} }
} }