Fix issue where reading mime type wrong in video. More tests
This commit is contained in:
parent
7ea2d75fcd
commit
f1d007e68c
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
BIN
testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump
vendored
Normal file
BIN
testdata/src/test/assets/extractordumps/avi/auds_stream_header.dump
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user