From 99960acec3a8cbb71220b856082f87e73a10ece8 Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 14 May 2020 17:14:47 +0800 Subject: [PATCH] Make FLV video seekable by a seekMap. --- .../extractor/flv/FlvExtractor.java | 48 ++++++++++++++++--- .../extractor/flv/ScriptTagPayloadReader.java | 25 ++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 98c5fa73a4..68e93b1f87 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.flv; import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -29,6 +30,7 @@ import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -83,6 +85,7 @@ public final class FlvExtractor implements Extractor { private int tagDataSize; private long tagTimestampUs; private boolean outputSeekMap; + private boolean seekMapIsSeekable; private @MonotonicNonNull AudioTagPayloadReader audioReader; private @MonotonicNonNull VideoTagPayloadReader videoReader; @@ -133,7 +136,12 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - state = STATE_READING_FLV_HEADER; + if (seekMapIsSeekable) { + state = STATE_READING_TAG_HEADER; + } else { + state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; + } outputFirstSample = false; bytesToNextTagHeader = 0; } @@ -263,11 +271,13 @@ public final class FlvExtractor implements Extractor { wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); - long durationUs = metadataReader.getDurationUs(); - if (durationUs != C.TIME_UNSET) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); - outputSeekMap = true; - } + SeekMap seekMap = buildSeekMap(metadataReader.getSeekMapTimes(), + metadataReader.getSeekMapFilePositions(), + metadataReader.getDurationUs(), + input.getLength()); + seekMapIsSeekable = seekMap.isSeekable(); + extractorOutput.seekMap(seekMap); + outputSeekMap = true; } else { input.skipFully(tagDataSize); wasConsumed = false; @@ -301,6 +311,32 @@ public final class FlvExtractor implements Extractor { } } + private SeekMap buildSeekMap(List times, List filePositions, long durationUs, + long flvDataSize) { + if (durationUs == C.TIME_UNSET + || times == null || times.size() == 0 + || filePositions == null || filePositions.size() != times.size()) { + // Key frames information is missing or incomplete. + return new SeekMap.Unseekable(durationUs); + } + int keyFrameSize = times.size(); + int[] sizes = new int[keyFrameSize]; + long[] offsets = new long[keyFrameSize]; + long[] durationsUs = new long[keyFrameSize]; + long[] timesUs = new long[keyFrameSize]; + for (int i = 0; i < keyFrameSize; i++) { + timesUs[i] = (long) (times.get(i) * C.MICROS_PER_SECOND); + offsets[i] = (long) (filePositions.get(i) + 0); + } + for (int i = 0; i < keyFrameSize - 1; i++) { + sizes[i] = (int) (offsets[i + 1] - offsets[i]); + durationsUs[i] = timesUs[i + 1] - timesUs[i]; + } + sizes[keyFrameSize - 1] = (int) (flvDataSize - sizes[keyFrameSize - 2]); + durationsUs[keyFrameSize - 1] = durationUs - timesUs[keyFrameSize - 1]; + return new ChunkIndex(sizes, offsets, durationsUs, timesUs); + } + private long getCurrentTimestampUs() { return outputFirstSample ? (mediaTagTimestampOffsetUs + tagTimestampUs) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 806cc9fad4..1ce75b4c40 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -32,6 +33,9 @@ import java.util.Map; private static final String NAME_METADATA = "onMetaData"; private static final String KEY_DURATION = "duration"; + private static final String KEY_KEY_FRAMES = "keyframes"; + private static final String KEY_FILE_POSITIONS = "filepositions"; + private static final String KEY_TIMES = "times"; // AMF object types private static final int AMF_TYPE_NUMBER = 0; @@ -45,6 +49,9 @@ import java.util.Map; private long durationUs; + private List seekMapFilePositions; + private List seekMapTimes; + public ScriptTagPayloadReader() { super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; @@ -89,6 +96,16 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + if (metadata.containsKey(KEY_KEY_FRAMES)) { + Object frames = metadata.get(KEY_KEY_FRAMES); + if (frames instanceof Map) { + Map framesMap = (Map) metadata.get(KEY_KEY_FRAMES); + if (framesMap.size() > 0) { + seekMapFilePositions = (List) framesMap.get(KEY_FILE_POSITIONS); + seekMapTimes = (List) framesMap.get(KEY_TIMES); + } + } + } return false; } @@ -224,4 +241,12 @@ import java.util.Map; return null; } } + + public List getSeekMapFilePositions() { + return seekMapFilePositions; + } + + public List getSeekMapTimes() { + return seekMapTimes; + } }