Fix issue where reading mime type wrong in video. More tests

This commit is contained in:
Dustin 2022-01-25 15:01:02 -07:00
parent 7ea2d75fcd
commit f1d007e68c
17 changed files with 364 additions and 110 deletions

View File

@ -1,10 +1,11 @@
package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
public class AudioFormat {
public class AudioFormat implements IStreamFormat {
public static final short WAVE_FORMAT_PCM = 1;
static final short WAVE_FORMAT_AAC = 0xff;
private static final short WAVE_FORMAT_MPEGLAYER3 = 0x55;
@ -60,5 +61,16 @@ public class AudioFormat {
temp.get(data);
return data;
}
@Override
public boolean isAllKeyFrames() {
return true;
}
@Override
public @C.TrackType int getTrackType() {
return C.TRACK_TYPE_AUDIO;
}
//TODO: Deal with WAVEFORMATEXTENSIBLE
}

View File

@ -1,5 +1,6 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput;
@ -37,6 +38,12 @@ public class AvcChunkPeeker extends NalChunkPeeker {
return false;
}
/**
* Greatly simplified way to calculate the picOrder
* Full logic is here
* https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/video/h264_poc.cc
* @param nalTypeOffset
*/
void updatePicCountClock(final int nalTypeOffset) {
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(buffer, nalTypeOffset + 1, buffer.length);
//slide_header()
@ -63,7 +70,8 @@ public class AvcChunkPeeker extends NalChunkPeeker {
picCountClock.setIndex(picCountClock.getIndex());
}
private int readSps(ExtractorInput input, int nalTypeOffset) throws IOException {
@VisibleForTesting
int readSps(ExtractorInput input, int nalTypeOffset) throws IOException {
final int spsStart = nalTypeOffset + 1;
nalTypeOffset = seekNextNal(input, spsStart);
spsData = NalUnitUtil.parseSpsNalUnitPayload(buffer, spsStart, pos);

View File

@ -39,6 +39,21 @@ public class AviExtractor implements Extractor {
return sb.toString();
}
static long alignPosition(long position) {
if ((position & 1) == 1) {
position++;
}
return position;
}
static void alignInput(ExtractorInput input) throws IOException {
// This isn't documented anywhere, but most files are aligned to even bytes
// and can have gaps of zeros
if ((input.getPosition() & 1) == 1) {
input.skipFully(1);
}
}
static final String TAG = "AviExtractor";
@VisibleForTesting
static final int PEEK_BYTES = 28;
@ -81,28 +96,14 @@ public class AviExtractor implements Extractor {
//At the start of the movi tag
private long moviOffset;
private long moviEnd;
private AviSeekMap aviSeekMap;
@VisibleForTesting
AviSeekMap aviSeekMap;
// private long indexOffset; //Usually chunkStart
//If partial read
private transient AviTrack chunkHandler;
static void alignInput(ExtractorInput input) throws IOException {
// This isn't documented anywhere, but most files are aligned to even bytes
// and can have gaps of zeros
if ((input.getPosition() & 1) == 1) {
input.skipFully(1);
}
}
static long alignPosition(long position) {
if ((position & 1) == 1) {
position++;
}
return position;
}
/**
*
* @param input
@ -161,7 +162,20 @@ public class AviExtractor implements Extractor {
return byteBuffer;
}
private void setSeekMap(AviSeekMap aviSeekMap) {
@VisibleForTesting
static int getStreamId(int chunkId) {
final int upperChar = chunkId & 0xff;
if (Character.isDigit(upperChar)) {
final int lowerChar = (chunkId >> 8) & 0xff;
if (Character.isDigit(upperChar)) {
return (lowerChar & 0xf) + ((upperChar & 0xf) * 10);
}
}
return -1;
}
@VisibleForTesting
void setSeekMap(AviSeekMap aviSeekMap) {
this.aviSeekMap = aviSeekMap;
output.seekMap(aviSeekMap);
}
@ -191,16 +205,17 @@ public class AviExtractor implements Extractor {
this.output = output;
}
private void parseStream(final ListBox streamList, int streamId) {
@VisibleForTesting
AviTrack parseStream(final ListBox streamList, int streamId) {
final StreamHeaderBox streamHeader = streamList.getChild(StreamHeaderBox.class);
final StreamFormatBox streamFormat = streamList.getChild(StreamFormatBox.class);
if (streamHeader == null) {
Log.w(TAG, "Missing Stream Header");
return;
return null;
}
if (streamFormat == null) {
Log.w(TAG, "Missing Stream Format");
return;
return null;
}
final Format.Builder builder = new Format.Builder();
builder.setId(streamId);
@ -212,31 +227,33 @@ public class AviExtractor implements Extractor {
if (streamName != null) {
builder.setLabel(streamName.getName());
}
final AviTrack aviTrack;
if (streamHeader.isVideo()) {
final String mimeType = streamHeader.getMimeType();
final VideoFormat videoFormat = streamFormat.getVideoFormat();
final String mimeType = videoFormat.getMimeType();
if (mimeType == null) {
Log.w(TAG, "Unknown FourCC: " + toString(streamHeader.getFourCC()));
return;
return null;
}
final VideoFormat videoFormat = streamFormat.getVideoFormat();
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
builder.setWidth(videoFormat.getWidth());
builder.setHeight(videoFormat.getHeight());
builder.setFrameRate(streamHeader.getFrameRate());
builder.setSampleMimeType(mimeType);
final AviTrack aviTrack = new AviTrack(streamId, streamHeader, trackOutput);
if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
Mp4vChunkPeeker mp4vChunkPeeker = new Mp4vChunkPeeker(builder, trackOutput);
aviTrack.setChunkPeeker(mp4vChunkPeeker);
} else if (MimeTypes.VIDEO_H264.equals(mimeType)) {
if (MimeTypes.VIDEO_H264.equals(mimeType)) {
final AvcChunkPeeker avcChunkPeeker = new AvcChunkPeeker(builder, trackOutput, streamHeader.getUsPerSample());
aviTrack.setClock(avcChunkPeeker.getPicCountClock());
aviTrack = new AviTrack(streamId, videoFormat, avcChunkPeeker.getPicCountClock(), trackOutput);
aviTrack.setChunkPeeker(avcChunkPeeker);
} else {
aviTrack = new AviTrack(streamId, videoFormat,
new LinearClock(streamHeader.getUsPerSample()), trackOutput);
if (MimeTypes.VIDEO_MP4V.equals(mimeType)) {
aviTrack.setChunkPeeker(new Mp4vChunkPeeker(builder, trackOutput));
}
}
trackOutput.format(builder.build());
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
aviTracks[streamId] = aviTrack;
} else if (streamHeader.isAudio()) {
final AudioFormat audioFormat = streamFormat.getAudioFormat();
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_AUDIO);
@ -257,8 +274,12 @@ public class AviExtractor implements Extractor {
builder.setInitializationData(Collections.singletonList(audioFormat.getCodecData()));
}
trackOutput.format(builder.build());
aviTracks[streamId] = new AviTrack(streamId, streamHeader, trackOutput);
aviTrack = new AviTrack(streamId, audioFormat, new LinearClock(streamHeader.getUsPerSample()),
trackOutput);
}else {
aviTrack = null;
}
return aviTrack;
}
private int readTracks(ExtractorInput input) throws IOException {
@ -278,7 +299,7 @@ public class AviExtractor implements Extractor {
for (Box box : headerList.getChildren()) {
if (box instanceof ListBox && ((ListBox) box).getListType() == STRL) {
final ListBox streamList = (ListBox) box;
parseStream(streamList, streamId);
aviTracks[streamId] = parseStream(streamList, streamId);
streamId++;
}
}
@ -343,7 +364,8 @@ public class AviExtractor implements Extractor {
for (int i=0;i<seekOffsets.length;i++) {
seekOffsets[i] = new UnboundedIntArray();
}
final int seekFrameRate = (int)(videoTrack.streamHeaderBox.getFrameRate() * 2);
//TODO: Change this to min frame rate
final int seekFrameRate = (int)(1f/(videoTrack.getClock().usPerChunk / 1_000_000f) * 2);
final UnboundedIntArray keyFrameList = new UnboundedIntArray();
while (remaining > 0) {
@ -406,17 +428,6 @@ public class AviExtractor implements Extractor {
setSeekMap(seekMap);
}
private static int getStreamId(int chunkId) {
final int upperChar = chunkId & 0xff;
if (Character.isDigit(upperChar)) {
final int lowerChar = (chunkId >> 8) & 0xff;
if (Character.isDigit(upperChar)) {
return (lowerChar & 0xf) + ((upperChar & 0xf) * 10);
}
}
return -1;
}
@Nullable
private AviTrack getAviTrack(int chunkId) {
final int streamId = getStreamId(chunkId);

View File

@ -3,7 +3,6 @@ package com.google.android.exoplayer2.extractor.avi;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Log;
public class AviSeekMap implements SeekMap {
final long videoUsPerChunk;
@ -58,7 +57,7 @@ public class AviSeekMap implements SeekMap {
int offset = seekOffsets[videoStreamId][seekFrameIndex];
final long outUs = seekFrameIndex * seekIndexFactor * videoUsPerChunk;
final long position = offset + moviOffset;
Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
//Log.d(AviExtractor.TAG, "SeekPoint: us=" + outUs + " pos=" + position);
return new SeekPoints(new SeekPoint(outUs, position));
}

View File

@ -5,7 +5,6 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.Arrays;
@ -16,23 +15,22 @@ public class AviTrack {
final int id;
@NonNull
final StreamHeaderBox streamHeaderBox;
final LinearClock clock;
@NonNull
LinearClock clock;
@Nullable
ChunkPeeker chunkPeeker;
/**
* True indicates all frames are key frames (e.g. Audio, MJPEG)
*/
boolean allKeyFrames;
final boolean allKeyFrames;
final @C.TrackType int trackType;
@NonNull
final TrackOutput trackOutput;
boolean forceKeyFrame;
@NonNull
TrackOutput trackOutput;
@Nullable
ChunkPeeker chunkPeeker;
/**
* Key is frame number value is offset
@ -43,22 +41,19 @@ public class AviTrack {
transient int chunkSize;
transient int chunkRemaining;
AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) {
AviTrack(int id, @NonNull IStreamFormat streamFormat, @NonNull LinearClock clock,
@NonNull TrackOutput trackOutput) {
this.id = id;
this.clock = clock;
this.allKeyFrames = streamFormat.isAllKeyFrames();
this.trackType = streamFormat.getTrackType();
this.trackOutput = trackOutput;
this.streamHeaderBox = streamHeaderBox;
clock = new LinearClock(streamHeaderBox.getUsPerSample());
this.allKeyFrames = streamHeaderBox.isAudio() || (MimeTypes.VIDEO_MJPEG.equals(streamHeaderBox.getMimeType()));
}
public LinearClock getClock() {
return clock;
}
public void setClock(LinearClock clock) {
this.clock = clock;
}
public void setChunkPeeker(ChunkPeeker chunkPeeker) {
this.chunkPeeker = chunkPeeker;
}
@ -78,8 +73,6 @@ public class AviTrack {
if (keyFrames != null) {
return Arrays.binarySearch(keyFrames, clock.getIndex()) >= 0;
}
//Hack: Exo needs at least one frame before it starts playback
//return clock.getIndex() == 0;
return false;
}
@ -92,11 +85,11 @@ public class AviTrack {
}
public boolean isVideo() {
return streamHeaderBox.isVideo();
return trackType == C.TRACK_TYPE_VIDEO;
}
public boolean isAudio() {
return streamHeaderBox.isAudio();
return trackType == C.TRACK_TYPE_AUDIO;
}
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
@ -114,7 +107,13 @@ public class AviTrack {
}
}
public boolean resume(ExtractorInput input) throws IOException {
/**
* Resume a partial read of a chunk
* @param input
* @return
* @throws IOException
*/
boolean resume(ExtractorInput input) throws IOException {
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
if (chunkRemaining == 0) {
done(chunkSize);
@ -124,6 +123,10 @@ public class AviTrack {
}
}
/**
* Done reading a chunk
* @param size
*/
void done(final int size) {
trackOutput.sampleMetadata(
clock.getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);

View File

@ -0,0 +1,9 @@
package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.C;
public interface IStreamFormat {
String getMimeType();
boolean isAllKeyFrames();
@C.TrackType int getTrackType();
}

View File

@ -4,6 +4,8 @@ package com.google.android.exoplayer2.extractor.avi;
* Properly calculates the frame time for H264 frames using PicCount
*/
public class PicCountClock extends LinearClock {
//I believe this is 2 because there is a bottom pic order and a top pic order
private static final int STEP = 2;
//The frame as a calculated from the picCount
private int picIndex;
private int lastPicCount;
@ -19,7 +21,7 @@ public class PicCountClock extends LinearClock {
public void setMaxPicCount(int maxPicCount) {
this.maxPicCount = maxPicCount;
posHalf = maxPicCount / 2; //Not sure why pics are 2x
posHalf = maxPicCount / STEP;
negHalf = -posHalf;
}
@ -40,7 +42,7 @@ public class PicCountClock extends LinearClock {
} else if (delta > posHalf) {
delta -= maxPicCount;
}
picIndex += delta / 2;
picIndex += delta / STEP;
lastPicCount = picCount;
if (maxPicIndex < picIndex) {
maxPicIndex = picIndex;

View File

@ -1,7 +1,5 @@
package com.google.android.exoplayer2.extractor.avi;
import android.util.SparseArray;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
/**
@ -16,31 +14,6 @@ public class StreamHeaderBox extends ResidentBox {
//Videos Stream
static final int VIDS = 'v' | ('i' << 8) | ('d' << 16) | ('s' << 24);
static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24);
private static final SparseArray<String> STREAM_MAP = new SparseArray<>();
static {
//Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG
final String mimeType = MimeTypes.VIDEO_MP4V;
//final String mimeType = MimeTypes.VIDEO_H263;
//I've never seen an Android devices that actually supports MP42
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42");
//Samsung seems to support the rare MP43.
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43");
STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType);
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType);
STREAM_MAP.put(XVID, mimeType);
STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType);
STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType);
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG);
}
StreamHeaderBox(int type, int size, ByteBuffer byteBuffer) {
super(type, size, byteBuffer);
}
@ -64,11 +37,6 @@ public class StreamHeaderBox extends ResidentBox {
return getScale() * 1_000_000L / getRate();
}
public String getMimeType() {
return STREAM_MAP.get(getFourCC());
}
public int getSteamType() {
return byteBuffer.getInt(0);
}

View File

@ -1,8 +1,38 @@
package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer;
import java.util.HashMap;
public class VideoFormat implements IStreamFormat {
static final int XVID = 'X' | ('V' << 8) | ('I' << 16) | ('D' << 24);
private static final HashMap<Integer, String> STREAM_MAP = new HashMap<>();
static {
//Although other types are technically supported, AVI is almost exclusively MP4V and MJPEG
final String mimeType = MimeTypes.VIDEO_MP4V;
//final String mimeType = MimeTypes.VIDEO_H263;
//I've never seen an Android devices that actually supports MP42
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('2' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp42");
//Samsung seems to support the rare MP43.
STREAM_MAP.put('M' | ('P' << 8) | ('4' << 16) | ('3' << 24), MimeTypes.BASE_TYPE_VIDEO+"/mp43");
STREAM_MAP.put('H' | ('2' << 8) | ('6' << 16) | ('4' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('a' | ('v' << 8) | ('c' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('A' | ('V' << 8) | ('C' << 16) | ('1' << 24), MimeTypes.VIDEO_H264);
STREAM_MAP.put('3' | ('V' << 8) | ('I' << 16) | ('D' << 24), mimeType);
STREAM_MAP.put('x' | ('v' << 8) | ('i' << 16) | ('d' << 24), mimeType);
STREAM_MAP.put(XVID, mimeType);
STREAM_MAP.put('D' | ('X' << 8) | ('5' << 16) | ('0' << 24), mimeType);
STREAM_MAP.put('d' | ('i' << 8) | ('v' << 16) | ('x' << 24), mimeType);
STREAM_MAP.put('M' | ('J' << 8) | ('P' << 16) | ('G' << 24), MimeTypes.VIDEO_MJPEG);
STREAM_MAP.put('m' | ('j' << 8) | ('p' << 16) | ('g' << 24), MimeTypes.VIDEO_MJPEG);
}
public class VideoFormat {
private final ByteBuffer byteBuffer;
public VideoFormat(final ByteBuffer byteBuffer) {
@ -17,5 +47,23 @@ public class VideoFormat {
public int getHeight() {
return byteBuffer.getInt(8);
}
// 12 - biPlanes
// 14 - biBitCount
public int getCompression() {
return byteBuffer.getInt(16);
}
public String getMimeType() {
return STREAM_MAP.get(getCompression());
}
@Override
public boolean isAllKeyFrames() {
return MimeTypes.VIDEO_MJPEG.equals(getMimeType());
}
@Override
public int getTrackType() {
return C.TRACK_TYPE_VIDEO;
}
}

View File

@ -13,7 +13,7 @@ public class AudioFormatTest {
@Test
public void getters_givenAacStreamFormat() throws IOException {
final StreamFormatBox streamFormatBox = DataHelper.getAudioStreamFormat();
final StreamFormatBox streamFormatBox = DataHelper.getAacStreamFormat();
final AudioFormat audioFormat = streamFormatBox.getAudioFormat();
Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType());
Assert.assertEquals(2, audioFormat.getChannels());
@ -21,5 +21,6 @@ public class AudioFormatTest {
Assert.assertEquals(48000, audioFormat.getSamplesPerSecond());
Assert.assertEquals(0, audioFormat.getBitsPerSample()); //Not meaningful for AAC
Assert.assertArrayEquals(CODEC_PRIVATE, audioFormat.getCodecData());
Assert.assertEquals(MimeTypes.AUDIO_AAC, audioFormat.getMimeType());
}
}

View File

@ -0,0 +1,38 @@
package com.google.android.exoplayer2.extractor.avi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class AviExtractorRoboTest {
@Test
public void parseStream_givenH264StreamList() throws IOException {
final AviExtractor aviExtractor = new AviExtractor();
final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
aviExtractor.init(fakeExtractorOutput);
final ListBox streamList = DataHelper.getVideoStreamList();
aviExtractor.parseStream(streamList, 0);
FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO);
Assert.assertEquals(MimeTypes.VIDEO_H264, trackOutput.lastFormat.sampleMimeType);
}
@Test
public void parseStream_givenAacStreamList() throws IOException {
final AviExtractor aviExtractor = new AviExtractor();
final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
aviExtractor.init(fakeExtractorOutput);
final ListBox streamList = DataHelper.getAacStreamList();
aviExtractor.parseStream(streamList, 0);
FakeTrackOutput trackOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_VIDEO);
Assert.assertEquals(MimeTypes.AUDIO_AAC, trackOutput.lastFormat.sampleMimeType);
}
}

View File

@ -98,4 +98,54 @@ public class AviExtractorTest {
final int riff = 'R' | ('I' << 8) | ('F' << 16) | ('F' << 24);
Assert.assertEquals("RIFF", AviExtractor.toString(riff));
}
@Test
public void alignPosition_givenOddPosition() {
Assert.assertEquals(2, AviExtractor.alignPosition(1));
}
@Test
public void alignPosition_givenEvenPosition() {
Assert.assertEquals(2, AviExtractor.alignPosition(2));
}
@Test
public void alignInput_givenOddPosition() throws IOException {
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
setData(new byte[16]).build();
fakeExtractorInput.setPosition(1);
AviExtractor.alignInput(fakeExtractorInput);
Assert.assertEquals(2, fakeExtractorInput.getPosition());
}
@Test
public void alignInput_givenEvenPosition() throws IOException {
final FakeExtractorInput fakeExtractorInput = new FakeExtractorInput.Builder().
setData(new byte[16]).build();
fakeExtractorInput.setPosition(4);
AviExtractor.alignInput(fakeExtractorInput);
Assert.assertEquals(4, fakeExtractorInput.getPosition());
}
@Test
public void setSeekMap_givenStubbedSeekMap() throws IOException {
final AviSeekMap aviSeekMap = DataHelper.getAviSeekMap();
final AviExtractor aviExtractor = new AviExtractor();
final FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
aviExtractor.init(fakeExtractorOutput);
aviExtractor.setSeekMap(aviSeekMap);
Assert.assertEquals(aviSeekMap, fakeExtractorOutput.seekMap);
Assert.assertEquals(aviSeekMap, aviExtractor.aviSeekMap);
}
@Test
public void getStreamId_givenInvalidStreamId() {
Assert.assertEquals(-1, AviExtractor.getStreamId(AviExtractor.JUNK));
}
@Test
public void getStreamId_givenValidStreamId() {
Assert.assertEquals(1, AviExtractor.getStreamId('0' | ('1' << 8) | ('d' << 16) | ('c' << 24)));
}
}

View File

@ -1,11 +1,13 @@
package com.google.android.exoplayer2.extractor.avi;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
public class DataHelper {
@ -31,7 +33,14 @@ public class DataHelper {
return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer);
}
public static StreamFormatBox getAudioStreamFormat() throws IOException {
public static StreamHeaderBox getAudioStreamHeader() throws IOException {
final byte[] buffer = getBytes("auds_stream_header.dump");
final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
return new StreamHeaderBox(StreamHeaderBox.STRH, buffer.length, byteBuffer);
}
public static StreamFormatBox getAacStreamFormat() throws IOException {
final byte[] buffer = getBytes("aac_stream_format.dump");
final ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
@ -45,6 +54,26 @@ public class DataHelper {
return new StreamFormatBox(StreamFormatBox.STRF, buffer.length, byteBuffer);
}
public static ListBox getVideoStreamList() throws IOException {
final StreamHeaderBox streamHeaderBox = getVidsStreamHeader();
final StreamFormatBox streamFormatBox = getVideoStreamFormat();
final ArrayList<Box> list = new ArrayList<>(2);
list.add(streamHeaderBox);
list.add(streamFormatBox);
return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()),
AviExtractor.STRL, list);
}
public static ListBox getAacStreamList() throws IOException {
final StreamHeaderBox streamHeaderBox = getAudioStreamHeader();
final StreamFormatBox streamFormatBox = getAacStreamFormat();
final ArrayList<Box> list = new ArrayList<>(2);
list.add(streamHeaderBox);
list.add(streamFormatBox);
return new ListBox((int)(streamHeaderBox.getSize() + streamFormatBox.getSize()),
AviExtractor.STRL, list);
}
public static StreamNameBox getStreamNameBox(final String name) {
byte[] bytes = name.getBytes();
bytes = Arrays.copyOf(bytes, bytes.length + 1);
@ -58,4 +87,18 @@ public class DataHelper {
byteBuffer.put(nalType);
return byteBuffer;
}
public static AviSeekMap getAviSeekMap() throws IOException {
final FakeTrackOutput output = new FakeTrackOutput(false);
final AviTrack videoTrack = new AviTrack(0,
DataHelper.getVideoStreamFormat().getVideoFormat(), new LinearClock(100), output);
final UnboundedIntArray videoArray = new UnboundedIntArray();
videoArray.add(0);
videoArray.add(1024);
final UnboundedIntArray audioArray = new UnboundedIntArray();
audioArray.add(0);
audioArray.add(128);
return new AviSeekMap(videoTrack,
new UnboundedIntArray[]{videoArray, audioArray}, 24, 0L, 0L);
}
}

View File

@ -0,0 +1,18 @@
package com.google.android.exoplayer2.extractor.avi;
import org.junit.Assert;
import org.junit.Test;
/**
* Most of this is covered by the PicOrderClockTest
*/
public class LinearClockTest {
@Test
public void advance() {
final LinearClock linearClock = new LinearClock(100L);
linearClock.setIndex(2);
Assert.assertEquals(200, linearClock.getUs());
linearClock.advance();
Assert.assertEquals(300, linearClock.getUs());
}
}

View File

@ -0,0 +1,45 @@
package com.google.android.exoplayer2.extractor.avi;
import org.junit.Assert;
import org.junit.Test;
public class PicCountClockTest {
@Test
public void us_givenTwoStepsForward() {
final PicCountClock picCountClock = new PicCountClock(100);
picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(2*2);
Assert.assertEquals(2*100, picCountClock.getUs());
}
@Test
public void us_givenThreeStepsBackwards() {
final PicCountClock picCountClock = new PicCountClock(100);
picCountClock.setMaxPicCount(16*2);
picCountClock.setPicCount(4*2); // 400ms
Assert.assertEquals(400, picCountClock.getUs());
picCountClock.setPicCount(1*2);
Assert.assertEquals(1*100, picCountClock.getUs());
}
@Test
public void setIndex_given3Chunks() {
final PicCountClock picCountClock = new PicCountClock(100);
picCountClock.setIndex(3);
Assert.assertEquals(3*100, picCountClock.getUs());
}
@Test
public void us_giveWrapBackwards() {
final PicCountClock picCountClock = new PicCountClock(100);
picCountClock.setMaxPicCount(16*2);
//Need to walk up no faster than maxPicCount / 2
picCountClock.setPicCount(7*2);
picCountClock.setPicCount(11*2);
picCountClock.setPicCount(15*2);
picCountClock.setPicCount(1*2);
Assert.assertEquals(17*100, picCountClock.getUs());
picCountClock.setPicCount(14*2);
Assert.assertEquals(14*100, picCountClock.getUs());
}
}

View File

@ -19,11 +19,10 @@ public class StreamHeaderBoxTest {
Assert.assertTrue(streamHeaderBox.isVideo());
Assert.assertFalse(streamHeaderBox.isAudio());
Assert.assertEquals(StreamHeaderBox.VIDS, streamHeaderBox.getSteamType());
Assert.assertEquals(StreamHeaderBox.XVID, streamHeaderBox.getFourCC());
Assert.assertEquals(VideoFormat.XVID, streamHeaderBox.getFourCC());
Assert.assertEquals(0, streamHeaderBox.getInitialFrames());
Assert.assertEquals(FPS24, streamHeaderBox.getFrameRate(), 0.1);
Assert.assertEquals(US_SAMPLE24FPS, streamHeaderBox.getUsPerSample());
Assert.assertEquals(MimeTypes.VIDEO_MP4V, streamHeaderBox.getMimeType());
Assert.assertEquals(11805L, streamHeaderBox.getLength());
Assert.assertEquals(0, streamHeaderBox.getSuggestedBufferSize());
}

Binary file not shown.