mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Improved comments, improved naming consistency.
This commit is contained in:
parent
14c842e503
commit
3a9f1f9a34
14 changed files with 147 additions and 124 deletions
|
|
@ -29,7 +29,7 @@ import java.io.IOException;
|
||||||
* Corrects the time and PAR for H264 streams
|
* Corrects the time and PAR for H264 streams
|
||||||
* AVC is very rare in AVI due to the rise of the mp4 container
|
* AVC is very rare in AVI due to the rise of the mp4 container
|
||||||
*/
|
*/
|
||||||
public class AvcChunkHandler extends NalChunkPeeker {
|
public class AvcChunkHandler extends NalChunkHandler {
|
||||||
private static final int NAL_TYPE_MASK = 0x1f;
|
private static final int NAL_TYPE_MASK = 0x1f;
|
||||||
private static final int NAL_TYPE_IDR = 5; //I Frame
|
private static final int NAL_TYPE_IDR = 5; //I Frame
|
||||||
private static final int NAL_TYPE_SEI = 6;
|
private static final int NAL_TYPE_SEI = 6;
|
||||||
|
|
@ -63,7 +63,7 @@ public class AvcChunkHandler extends NalChunkPeeker {
|
||||||
if (clock instanceof PicCountClock) {
|
if (clock instanceof PicCountClock) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
//If the clock is regular clock, skip "normal" frames
|
//If the clock is ChunkClock, skip "normal" frames
|
||||||
return nalType >= 0 && nalType <= NAL_TYPE_IDR;
|
return nalType >= 0 && nalType <= NAL_TYPE_IDR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,10 +107,12 @@ public class AvcChunkHandler extends NalChunkPeeker {
|
||||||
final int spsStart = nalTypeOffset + 1;
|
final int spsStart = nalTypeOffset + 1;
|
||||||
nalTypeOffset = seekNextNal(input, spsStart);
|
nalTypeOffset = seekNextNal(input, spsStart);
|
||||||
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);
|
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);
|
||||||
//If we have B Frames, upgrade to PicCountClock
|
//If we can have B Frames, upgrade to PicCountClock
|
||||||
final PicCountClock picCountClock;
|
final PicCountClock picCountClock;
|
||||||
if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) {
|
if (spsData.maxNumRefFrames > 1 && !(clock instanceof PicCountClock)) {
|
||||||
clock = picCountClock = new PicCountClock(clock.durationUs, clock.chunks);
|
picCountClock = new PicCountClock(clock.durationUs, clock.chunks);
|
||||||
|
picCountClock.setIndex(clock.getIndex());
|
||||||
|
clock = picCountClock;
|
||||||
} else {
|
} else {
|
||||||
picCountClock = getPicCountClock();
|
picCountClock = getPicCountClock();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ public class AviExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
trackOutput.format(builder.build());
|
trackOutput.format(builder.build());
|
||||||
if (MimeTypes.AUDIO_MPEG.equals(mimeType)) {
|
if (MimeTypes.AUDIO_MPEG.equals(mimeType)) {
|
||||||
chunkHandler = new Mp3ChunkHandler(streamId, trackOutput, clock,
|
chunkHandler = new MpegAudioChunkHandler(streamId, trackOutput, clock,
|
||||||
audioFormat.getSamplesPerSecond());
|
audioFormat.getSamplesPerSecond());
|
||||||
} else {
|
} else {
|
||||||
chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO,
|
chunkHandler = new ChunkHandler(streamId, ChunkHandler.TYPE_AUDIO,
|
||||||
|
|
@ -367,7 +367,7 @@ public class AviExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
void fixTimings(final int[] keyFrameCounts, final long videoDuration) {
|
void fixTimings(final int[] keyFrameCounts, final long videoDuration) {
|
||||||
for (final ChunkHandler chunkHandler : chunkHandlers) {
|
for (@Nullable final ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (chunkHandler != null) {
|
if (chunkHandler != null) {
|
||||||
if (chunkHandler.isAudio()) {
|
if (chunkHandler.isAudio()) {
|
||||||
final long durationUs = chunkHandler.getClock().durationUs;
|
final long durationUs = chunkHandler.getClock().durationUs;
|
||||||
|
|
@ -452,7 +452,7 @@ public class AviExtractor implements Extractor {
|
||||||
int indexSize = seekIndexes[videoId].getSize();
|
int indexSize = seekIndexes[videoId].getSize();
|
||||||
if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
|
if (indexSize == 0 || chunkHandler.chunks - seekIndexes[videoId].get(indexSize - 1) >= chunksPerKeyFrame) {
|
||||||
keyFrameOffsetsDiv2.add(offset / 2);
|
keyFrameOffsetsDiv2.add(offset / 2);
|
||||||
for (ChunkHandler seekTrack : chunkHandlers) {
|
for (@Nullable ChunkHandler seekTrack : chunkHandlers) {
|
||||||
if (seekTrack != null) {
|
if (seekTrack != null) {
|
||||||
seekIndexes[seekTrack.getId()].add(seekTrack.chunks);
|
seekIndexes[seekTrack.getId()].add(seekTrack.chunks);
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +487,7 @@ public class AviExtractor implements Extractor {
|
||||||
@Nullable
|
@Nullable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
ChunkHandler getChunkHandler(int chunkId) {
|
ChunkHandler getChunkHandler(int chunkId) {
|
||||||
for (ChunkHandler chunkHandler : chunkHandlers) {
|
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) {
|
if (chunkHandler != null && chunkHandler.handlesChunkId(chunkId)) {
|
||||||
return chunkHandler;
|
return chunkHandler;
|
||||||
}
|
}
|
||||||
|
|
@ -536,7 +536,7 @@ public class AviExtractor implements Extractor {
|
||||||
+ " size=" + size + " moviEnd=" + moviEnd);
|
+ " size=" + size + " moviEnd=" + moviEnd);
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
if (chunkHandler.newChunk(chunkId, size, input)) {
|
if (chunkHandler.newChunk(size, input)) {
|
||||||
alignInput(input);
|
alignInput(input);
|
||||||
} else {
|
} else {
|
||||||
this.chunkHandler = chunkHandler;
|
this.chunkHandler = chunkHandler;
|
||||||
|
|
@ -587,20 +587,20 @@ public class AviExtractor implements Extractor {
|
||||||
chunkHandler = null;
|
chunkHandler = null;
|
||||||
if (position <= 0) {
|
if (position <= 0) {
|
||||||
if (moviOffset != 0) {
|
if (moviOffset != 0) {
|
||||||
resetClocks();
|
setIndexes(new int[chunkHandlers.length]);
|
||||||
state = STATE_SEEK_START;
|
state = STATE_SEEK_START;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (aviSeekMap != null) {
|
if (aviSeekMap != null) {
|
||||||
aviSeekMap.setFrames(position, timeUs, chunkHandlers);
|
setIndexes(aviSeekMap.getIndexes(position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetClocks() {
|
private void setIndexes(@NonNull int[] indexes) {
|
||||||
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
for (@Nullable ChunkHandler chunkHandler : chunkHandlers) {
|
||||||
if (chunkHandler != null) {
|
if (chunkHandler != null) {
|
||||||
chunkHandler.getClock().setIndex(0);
|
chunkHandler.setIndex(indexes[chunkHandler.getId()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,18 +99,23 @@ public class AviSeekMap implements SeekMap {
|
||||||
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFrames(final long position, final long timeUs, final ChunkHandler[] chunkHandlers) {
|
/**
|
||||||
|
* Get the ChunkClock indexes by stream id
|
||||||
|
* @param position seek position in the file
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public int[] getIndexes(final long position) {
|
||||||
final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2));
|
final int index = Arrays.binarySearch(keyFrameOffsetsDiv2, (int)((position - seekOffset) / 2));
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new IllegalArgumentException("Position: " + position);
|
throw new IllegalArgumentException("Position: " + position);
|
||||||
}
|
}
|
||||||
for (int i=0;i<chunkHandlers.length;i++) {
|
final int[] indexes = new int[seekIndexes.length];
|
||||||
final ChunkHandler chunkHandler = chunkHandlers[i];
|
for (int i=0;i<indexes.length;i++) {
|
||||||
if (chunkHandler != null) {
|
if (seekIndexes[i].length > index) {
|
||||||
// Log.d(AviExtractor.TAG, "Frame: " + (chunkHandler.isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " frame=" + clock.getIndex() + " key=" + chunkHandler.isKeyFrame());
|
indexes[i] = seekIndexes[i][index];
|
||||||
chunkHandler.setIndex(seekIndexes[i][index]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return indexes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,16 @@ import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of info about a track.
|
* Handles chunk data from a given stream.
|
||||||
* This acts a bridge between AVI and ExoPlayer structures
|
* This acts a bridge between AVI and ExoPlayer
|
||||||
*/
|
*/
|
||||||
public class ChunkHandler {
|
public class ChunkHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant meaning all frames are considered key frames
|
||||||
|
*/
|
||||||
public static final int[] ALL_KEY_FRAMES = new int[0];
|
public static final int[] ALL_KEY_FRAMES = new int[0];
|
||||||
|
|
||||||
public static int TYPE_VIDEO = ('d' << 16) | ('c' << 24);
|
public static int TYPE_VIDEO = ('d' << 16) | ('c' << 24);
|
||||||
public static int TYPE_AUDIO = ('w' << 16) | ('b' << 24);
|
public static int TYPE_AUDIO = ('w' << 16) | ('b' << 24);
|
||||||
|
|
||||||
|
|
@ -39,10 +44,24 @@ public class ChunkHandler {
|
||||||
@NonNull
|
@NonNull
|
||||||
final TrackOutput trackOutput;
|
final TrackOutput trackOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chunk id as it appears in the index and the movi
|
||||||
|
*/
|
||||||
final int chunkId;
|
final int chunkId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondary chunk id. Bad muxers sometimes use uncompressed for key frames
|
||||||
|
*/
|
||||||
final int chunkIdAlt;
|
final int chunkIdAlt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of chunks as calculated by the index
|
||||||
|
*/
|
||||||
int chunks;
|
int chunks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size total size of the stream in bytes calculated by the index
|
||||||
|
*/
|
||||||
int size;
|
int size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,9 +69,18 @@ public class ChunkHandler {
|
||||||
*/
|
*/
|
||||||
int[] keyFrames = new int[0];
|
int[] keyFrames = new int[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the current chunk in bytes
|
||||||
|
*/
|
||||||
transient int chunkSize;
|
transient int chunkSize;
|
||||||
|
/**
|
||||||
|
* Bytes remaining in the chunk to be processed
|
||||||
|
*/
|
||||||
transient int chunkRemaining;
|
transient int chunkRemaining;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stream id in ASCII
|
||||||
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static int getChunkIdLower(int id) {
|
static int getChunkIdLower(int id) {
|
||||||
int tens = id / 10;
|
int tens = id / 10;
|
||||||
|
|
@ -71,6 +99,10 @@ public class ChunkHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return true if this can handle the chunkId
|
||||||
|
*/
|
||||||
public boolean handlesChunkId(int chunkId) {
|
public boolean handlesChunkId(int chunkId) {
|
||||||
return this.chunkId == chunkId || chunkIdAlt == chunkId;
|
return this.chunkId == chunkId || chunkIdAlt == chunkId;
|
||||||
}
|
}
|
||||||
|
|
@ -80,13 +112,9 @@ public class ChunkHandler {
|
||||||
return clock;
|
return clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClock(@NonNull ChunkClock clock) {
|
|
||||||
this.clock = clock;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Sets the list of key frames
|
||||||
* @param keyFrames null means all key frames
|
* @param keyFrames list of frame indexes or {@link #ALL_KEY_FRAMES}
|
||||||
*/
|
*/
|
||||||
void setKeyFrames(@NonNull final int[] keyFrames) {
|
void setKeyFrames(@NonNull final int[] keyFrames) {
|
||||||
this.keyFrames = keyFrames;
|
this.keyFrames = keyFrames;
|
||||||
|
|
@ -104,23 +132,27 @@ public class ChunkHandler {
|
||||||
return (chunkId & TYPE_AUDIO) == TYPE_AUDIO;
|
return (chunkId & TYPE_AUDIO) == TYPE_AUDIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
/**
|
||||||
final int remaining = size - trackOutput.sampleData(input, size, false);
|
* Process a new chunk
|
||||||
if (remaining == 0) {
|
* @param size total size of the chunk
|
||||||
|
* @return True if the chunk has been completely processed. False implies {@link #resume}
|
||||||
|
* will be called
|
||||||
|
*/
|
||||||
|
public boolean newChunk(int size, @NonNull ExtractorInput input) throws IOException {
|
||||||
|
final int sampled = trackOutput.sampleData(input, size, false);
|
||||||
|
if (sampled == size) {
|
||||||
done(size);
|
done(size);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
chunkSize = size;
|
chunkSize = size;
|
||||||
chunkRemaining = remaining;
|
chunkRemaining = size - sampled;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume a partial read of a chunk
|
* Resume a partial read of a chunk
|
||||||
* @param input
|
* May be called multiple times
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
boolean resume(ExtractorInput input) throws IOException {
|
boolean resume(ExtractorInput input) throws IOException {
|
||||||
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
|
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
|
||||||
|
|
@ -133,25 +165,29 @@ public class ChunkHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Done reading a chunk
|
* Done reading a chunk. Send the timing info and advance the clock
|
||||||
* @param size
|
* @param size the amount of data passed to the trackOutput
|
||||||
*/
|
*/
|
||||||
void done(final int size) {
|
void done(final int size) {
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
trackOutput.sampleMetadata(
|
trackOutput.sampleMetadata(
|
||||||
clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);
|
clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);
|
||||||
}
|
}
|
||||||
final ChunkClock clock = getClock();
|
|
||||||
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame());
|
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + clock.getUs() + " size=" + size + " frame=" + clock.getIndex() + " key=" + isKeyFrame());
|
||||||
clock.advance();
|
clock.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the streamId.
|
||||||
|
* @return The unique stream id for this file
|
||||||
|
*/
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return ((chunkId >> 8) & 0xf) + ( chunkId & 0xf) * 10;
|
return ((chunkId >> 8) & 0xf) + (chunkId & 0xf) * 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A seek occurred
|
* A seek occurred
|
||||||
|
* @param index of the chunk
|
||||||
*/
|
*/
|
||||||
public void setIndex(int index) {
|
public void setIndex(int index) {
|
||||||
getClock().setIndex(index);
|
getClock().setIndex(index);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import java.io.IOException;
|
||||||
/**
|
/**
|
||||||
* Peeks an MP4V stream looking for pixelWidthHeightRatio data
|
* Peeks an MP4V stream looking for pixelWidthHeightRatio data
|
||||||
*/
|
*/
|
||||||
public class Mp4vChunkHandler extends NalChunkPeeker {
|
public class Mp4vChunkHandler extends NalChunkHandler {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|
|
||||||
|
|
@ -16,37 +16,45 @@
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.audio.MpegAudioUtil;
|
import com.google.android.exoplayer2.audio.MpegAudioUtil;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class Mp3ChunkHandler extends ChunkHandler {
|
/**
|
||||||
|
* Resolves several issues with Mpeg Audio
|
||||||
|
* 1. That muxers don't always mux MPEG audio on the frame boundary
|
||||||
|
* 2. That some codecs can't handle multiple or partial frames (Pixels)
|
||||||
|
*/
|
||||||
|
public class MpegAudioChunkHandler extends ChunkHandler {
|
||||||
private final MpegAudioUtil.Header header = new MpegAudioUtil.Header();
|
private final MpegAudioUtil.Header header = new MpegAudioUtil.Header();
|
||||||
private final ParsableByteArray scratch = new ParsableByteArray(0);
|
private final ParsableByteArray scratch = new ParsableByteArray(8);
|
||||||
private final int fps;
|
private final int samplesPerSecond;
|
||||||
|
//Bytes remaining in the Mpeg Audio frame
|
||||||
private int frameRemaining;
|
private int frameRemaining;
|
||||||
private long us = 0L;
|
private long timeUs = 0L;
|
||||||
|
|
||||||
Mp3ChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock, int fps) {
|
MpegAudioChunkHandler(int id, @NonNull TrackOutput trackOutput, @NonNull ChunkClock clock,
|
||||||
|
int samplesPerSecond) {
|
||||||
super(id, TYPE_AUDIO, trackOutput, clock);
|
super(id, TYPE_AUDIO, trackOutput, clock);
|
||||||
this.fps = fps;
|
this.samplesPerSecond = samplesPerSecond;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
public boolean newChunk(int size, @NonNull ExtractorInput input) throws IOException {
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
|
//Empty frame, advance the clock and sync
|
||||||
clock.advance();
|
clock.advance();
|
||||||
syncUs();
|
syncTime();
|
||||||
//Log.d(AviExtractor.TAG, "Blank Frame: us=" + us);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
chunkRemaining = size;
|
chunkRemaining = size;
|
||||||
if (process(input)) {
|
if (process(input)) {
|
||||||
//If we scratch is the entire buffer, we didn't find a MP3 header, so just dump the chunk
|
// Fail Over: If the scratch is the entire chunk, we didn't find a MP3 header.
|
||||||
|
// Dump the chunk as is and hope the decoder can handle it.
|
||||||
if (scratch.limit() == size) {
|
if (scratch.limit() == size) {
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
trackOutput.sampleData(scratch, size);
|
trackOutput.sampleData(scratch, size);
|
||||||
|
|
@ -59,7 +67,7 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean resume(ExtractorInput input) throws IOException {
|
boolean resume(@NonNull ExtractorInput input) throws IOException {
|
||||||
if (process(input)) {
|
if (process(input)) {
|
||||||
clock.advance();
|
clock.advance();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -67,6 +75,11 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from input to scratch
|
||||||
|
* @param bytes to attempt to read
|
||||||
|
* @return {@link C#RESULT_END_OF_INPUT} or number of bytes read.
|
||||||
|
*/
|
||||||
int readScratch(ExtractorInput input, int bytes) throws IOException {
|
int readScratch(ExtractorInput input, int bytes) throws IOException {
|
||||||
final int toRead = Math.min(bytes, chunkRemaining);
|
final int toRead = Math.min(bytes, chunkRemaining);
|
||||||
final int read = input.read(scratch.getData(), scratch.limit(), toRead);
|
final int read = input.read(scratch.getData(), scratch.limit(), toRead);
|
||||||
|
|
@ -78,7 +91,12 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
return read;
|
return read;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean findFrame(ExtractorInput input) throws IOException {
|
/**
|
||||||
|
* Attempt to find a frame header in the input
|
||||||
|
* @return true if a frame header was found
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean findFrame(ExtractorInput input) throws IOException {
|
||||||
scratch.reset(0);
|
scratch.reset(0);
|
||||||
scratch.ensureCapacity(scratch.limit() + chunkRemaining);
|
scratch.ensureCapacity(scratch.limit() + chunkRemaining);
|
||||||
int toRead = 4;
|
int toRead = 4;
|
||||||
|
|
@ -96,8 +114,14 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean process(ExtractorInput input) throws IOException {
|
/**
|
||||||
|
* Process the chunk by breaking it in Mpeg audio frames
|
||||||
|
* @return true if the chunk has been completely processed
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
boolean process(ExtractorInput input) throws IOException {
|
||||||
if (frameRemaining == 0) {
|
if (frameRemaining == 0) {
|
||||||
|
//Find the next frame
|
||||||
if (findFrame(input)) {
|
if (findFrame(input)) {
|
||||||
final int scratchBytes = scratch.bytesLeft();
|
final int scratchBytes = scratch.bytesLeft();
|
||||||
trackOutput.sampleData(scratch, scratchBytes);
|
trackOutput.sampleData(scratch, scratchBytes);
|
||||||
|
|
@ -107,14 +131,11 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false);
|
final int bytes = trackOutput.sampleData(input, Math.min(frameRemaining, chunkRemaining), false);
|
||||||
if (bytes == C.RESULT_END_OF_INPUT) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
frameRemaining -= bytes;
|
frameRemaining -= bytes;
|
||||||
if (frameRemaining == 0) {
|
if (frameRemaining == 0) {
|
||||||
trackOutput.sampleMetadata(us, C.BUFFER_FLAG_KEY_FRAME, header.frameSize, 0, null);
|
trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, header.frameSize, 0, null);
|
||||||
//Log.d(AviExtractor.TAG, "MP3: us=" + us);
|
//Log.d(AviExtractor.TAG, "MP3: us=" + us);
|
||||||
us += header.samplesPerFrame * C.MICROS_PER_SECOND / fps;
|
timeUs += header.samplesPerFrame * C.MICROS_PER_SECOND / samplesPerSecond;
|
||||||
}
|
}
|
||||||
chunkRemaining -= bytes;
|
chunkRemaining -= bytes;
|
||||||
return chunkRemaining == 0;
|
return chunkRemaining == 0;
|
||||||
|
|
@ -123,11 +144,11 @@ public class Mp3ChunkHandler extends ChunkHandler {
|
||||||
@Override
|
@Override
|
||||||
public void setIndex(int index) {
|
public void setIndex(int index) {
|
||||||
super.setIndex(index);
|
super.setIndex(index);
|
||||||
syncUs();
|
syncTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncUs() {
|
private void syncTime() {
|
||||||
us = clock.getUs();
|
timeUs = clock.getUs();
|
||||||
frameRemaining = 0;
|
frameRemaining = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ import java.util.Arrays;
|
||||||
* Generic base class for NAL (0x00 0x00 0x01) chunk headers
|
* Generic base class for NAL (0x00 0x00 0x01) chunk headers
|
||||||
* Theses are used by AVC and MP4V (XVID)
|
* Theses are used by AVC and MP4V (XVID)
|
||||||
*/
|
*/
|
||||||
public abstract class NalChunkPeeker extends ChunkHandler {
|
public abstract class NalChunkHandler extends ChunkHandler {
|
||||||
private static final int SEEK_PEEK_SIZE = 256;
|
private static final int SEEK_PEEK_SIZE = 256;
|
||||||
private final int peekSize;
|
private final int peekSize;
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ public abstract class NalChunkPeeker extends ChunkHandler {
|
||||||
transient byte[] buffer;
|
transient byte[] buffer;
|
||||||
transient int pos;
|
transient int pos;
|
||||||
|
|
||||||
NalChunkPeeker(int id, @NonNull TrackOutput trackOutput,
|
NalChunkHandler(int id, @NonNull TrackOutput trackOutput,
|
||||||
@NonNull ChunkClock clock, int peakSize) {
|
@NonNull ChunkClock clock, int peakSize) {
|
||||||
super(id, TYPE_VIDEO, trackOutput, clock);
|
super(id, TYPE_VIDEO, trackOutput, clock);
|
||||||
if (peakSize < 5) {
|
if (peakSize < 5) {
|
||||||
|
|
@ -113,9 +113,9 @@ public abstract class NalChunkPeeker extends ChunkHandler {
|
||||||
|
|
||||||
abstract boolean skip(byte nalType);
|
abstract boolean skip(byte nalType);
|
||||||
|
|
||||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
public boolean newChunk(int size, ExtractorInput input) throws IOException {
|
||||||
peek(input, size);
|
peek(input, size);
|
||||||
return super.newChunk(tag, size, input);
|
return super.newChunk(size, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void peek(ExtractorInput input, final int size) throws IOException {
|
public void peek(ExtractorInput input, final int size) throws IOException {
|
||||||
|
|
@ -16,13 +16,6 @@
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
|
||||||
import com.google.android.exoplayer2.util.Log;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.nio.BufferOverflowException;
|
|
||||||
import java.nio.BufferUnderflowException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
|
@ -39,7 +32,6 @@ public class ResidentBox extends Box {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns shallow copy of this ByteBuffer with the position at 0
|
* Returns shallow copy of this ByteBuffer with the position at 0
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public ByteBuffer getByteBuffer() {
|
public ByteBuffer getByteBuffer() {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer2.extractor.avi;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Human readable stream name
|
* Box containing a human readable stream name
|
||||||
*/
|
*/
|
||||||
public class StreamNameBox extends ResidentBox {
|
public class StreamNameBox extends ResidentBox {
|
||||||
public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24);
|
public static final int STRN = 's' | ('t' << 8) | ('r' << 16) | ('n' << 24);
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,12 @@ public class AvcChunkPeekerTest {
|
||||||
(byte)0xFE,(byte)0x9E,0x10,0,0};
|
(byte)0xFE,(byte)0x9E,0x10,0,0};
|
||||||
|
|
||||||
private FakeTrackOutput fakeTrackOutput;
|
private FakeTrackOutput fakeTrackOutput;
|
||||||
private AvcChunkHandler avcChunkPeeker;
|
private AvcChunkHandler avcChunkHandler;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
fakeTrackOutput = new FakeTrackOutput(false);
|
fakeTrackOutput = new FakeTrackOutput(false);
|
||||||
avcChunkPeeker = new AvcChunkHandler(0, fakeTrackOutput,
|
avcChunkHandler = new AvcChunkHandler(0, fakeTrackOutput,
|
||||||
new ChunkClock(10_000_000L, 24 * 10), FORMAT_BUILDER_AVC);
|
new ChunkClock(10_000_000L, 24 * 10), FORMAT_BUILDER_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,26 +55,26 @@ public class AvcChunkPeekerTest {
|
||||||
|
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(bytes).build();
|
||||||
|
|
||||||
avcChunkPeeker.peek(input, bytes.length);
|
avcChunkHandler.peek(input, bytes.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void peek_givenStreamHeader() throws IOException {
|
public void peek_givenStreamHeader() throws IOException {
|
||||||
peekStreamHeader();
|
peekStreamHeader();
|
||||||
final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock();
|
final PicCountClock picCountClock = avcChunkHandler.getPicCountClock();
|
||||||
Assert.assertNotNull(picCountClock);
|
Assert.assertNotNull(picCountClock);
|
||||||
Assert.assertEquals(64, picCountClock.getMaxPicCount());
|
Assert.assertEquals(64, picCountClock.getMaxPicCount());
|
||||||
Assert.assertEquals(0, avcChunkPeeker.getSpsData().picOrderCountType);
|
Assert.assertEquals(0, avcChunkHandler.getSpsData().picOrderCountType);
|
||||||
Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f);
|
Assert.assertEquals(1.18f, fakeTrackOutput.lastFormat.pixelWidthHeightRatio, 0.01f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void newChunk_givenStreamHeaderAndPSlice() throws IOException {
|
public void newChunk_givenStreamHeaderAndPSlice() throws IOException {
|
||||||
peekStreamHeader();
|
peekStreamHeader();
|
||||||
final PicCountClock picCountClock = avcChunkPeeker.getPicCountClock();
|
final PicCountClock picCountClock = avcChunkHandler.getPicCountClock();
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(P_SLICE).build();
|
||||||
|
|
||||||
avcChunkPeeker.newChunk(0, P_SLICE.length, input);
|
avcChunkHandler.newChunk(P_SLICE.length, input);
|
||||||
|
|
||||||
Assert.assertEquals(12, picCountClock.getLastPicCount());
|
Assert.assertEquals(12, picCountClock.getLastPicCount());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -23,26 +22,25 @@ import org.junit.Test;
|
||||||
public class AviSeekMapTest {
|
public class AviSeekMapTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setFrames_givenExactSeekPointMatch() {
|
public void getFrames_givenExactSeekPointMatch() {
|
||||||
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
||||||
final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset;
|
final long position = aviSeekMap.keyFrameOffsetsDiv2[1] * 2L + aviSeekMap.seekOffset;
|
||||||
final int secs = 4;
|
final int secs = 4;
|
||||||
final ChunkHandler[] chunkHandlers = new ChunkHandler[]{DataHelper.getVideoChunkHandler(secs),
|
final ChunkHandler[] chunkHandlers = new ChunkHandler[]{DataHelper.getVideoChunkHandler(secs),
|
||||||
DataHelper.getAudioChunkHandler(secs)};
|
DataHelper.getAudioChunkHandler(secs)};
|
||||||
|
|
||||||
aviSeekMap.setFrames(position, C.MICROS_PER_SECOND, chunkHandlers);
|
int[] indexes = aviSeekMap.getIndexes(position);
|
||||||
for (int i=0;i<chunkHandlers.length;i++) {
|
for (int i=0;i<chunkHandlers.length;i++) {
|
||||||
Assert.assertEquals(aviSeekMap.seekIndexes[i][1], chunkHandlers[i].getClock().getIndex());
|
Assert.assertEquals(aviSeekMap.seekIndexes[i][1], indexes[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setFrames_givenBadPosition() {
|
public void setFrames_givenBadPosition() {
|
||||||
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
|
||||||
final ChunkHandler[] chunkHandlers = new ChunkHandler[2];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
aviSeekMap.setFrames(1L, C.MICROS_PER_SECOND, chunkHandlers);
|
aviSeekMap.getIndexes(1L);
|
||||||
Assert.fail();
|
Assert.fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//Intentionally blank
|
//Intentionally blank
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2022 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.exoplayer2.extractor.avi;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
public class ChunkHandlerTest {
|
|
||||||
@Test
|
|
||||||
public void setClock_givenLinearClock() {
|
|
||||||
final ChunkClock linearClock = new ChunkClock(1_000_000L, 30);
|
|
||||||
final ChunkHandler chunkHandler = DataHelper.getVideoChunkHandler(1);
|
|
||||||
chunkHandler.setClock(linearClock);
|
|
||||||
|
|
||||||
Assert.assertSame(linearClock, chunkHandler.getClock());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,9 +19,9 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class MockNalChunkPeeker extends NalChunkPeeker {
|
public class MockNalChunkHandler extends NalChunkHandler {
|
||||||
private boolean skip;
|
private boolean skip;
|
||||||
public MockNalChunkPeeker(int peakSize, boolean skip) {
|
public MockNalChunkHandler(int peakSize, boolean skip) {
|
||||||
super(0, new FakeTrackOutput(false), new ChunkClock(1_000_000L, 24), peakSize);
|
super(0, new FakeTrackOutput(false), new ChunkClock(1_000_000L, 24), peakSize);
|
||||||
this.skip = skip;
|
this.skip = skip;
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import java.io.EOFException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
|
@ -26,7 +25,7 @@ public class NalChunkPeekerTest {
|
||||||
@Test
|
@Test
|
||||||
public void construct_givenTooSmallPeekSize() {
|
public void construct_givenTooSmallPeekSize() {
|
||||||
try {
|
try {
|
||||||
new MockNalChunkPeeker(4, false);
|
new MockNalChunkHandler(4, false);
|
||||||
Assert.fail();
|
Assert.fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
//Intentionally blank
|
//Intentionally blank
|
||||||
|
|
@ -36,7 +35,7 @@ public class NalChunkPeekerTest {
|
||||||
@Test
|
@Test
|
||||||
public void peek_givenNoData() {
|
public void peek_givenNoData() {
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, false);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -46,7 +45,7 @@ public class NalChunkPeekerTest {
|
||||||
@Test
|
@Test
|
||||||
public void peek_givenNoNal() {
|
public void peek_givenNoNal() {
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(new byte[10]).build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, false);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, false);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -59,7 +58,7 @@ public class NalChunkPeekerTest {
|
||||||
DataHelper.appendNal(byteBuffer, (byte)32);
|
DataHelper.appendNal(byteBuffer, (byte)32);
|
||||||
|
|
||||||
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
final FakeExtractorInput input = new FakeExtractorInput.Builder().setData(byteBuffer.array()).build();
|
||||||
final MockNalChunkPeeker peeker = new MockNalChunkPeeker(5, true);
|
final MockNalChunkHandler peeker = new MockNalChunkHandler(5, true);
|
||||||
try {
|
try {
|
||||||
peeker.peek(input, 10);
|
peeker.peek(input, 10);
|
||||||
Assert.assertEquals(0, input.getPeekPosition());
|
Assert.assertEquals(0, input.getPeekPosition());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue