mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +00:00
Add file chooser to UI, Fixed timing issues on H264, Fixed PAR on XVID and H264
This commit is contained in:
parent
d9afe5105b
commit
4c76bf1a9d
7 changed files with 382 additions and 74 deletions
|
|
@ -543,9 +543,8 @@
|
|||
"name": "Misc",
|
||||
"samples": [
|
||||
{
|
||||
"name": "AVI",
|
||||
"uri": "https://drive.google.com/u/0/uc?id=1K6oLKCS56WFbhz33TgilTJBqfMYFTeUd&?export=download",
|
||||
"extension": "avi"
|
||||
"name": "User File",
|
||||
"uri": "content://user"
|
||||
},
|
||||
{
|
||||
"name": "Dizzy (MP4)",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
|||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
|
@ -40,6 +41,8 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
|||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
|
@ -73,6 +76,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
private static final String TAG = "SampleChooserActivity";
|
||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
||||
private static final Uri USER_CONTENT = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority("user").build();
|
||||
|
||||
private String[] uris;
|
||||
private boolean useExtensionRenderers;
|
||||
|
|
@ -80,6 +84,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private ExpandableListView sampleListView;
|
||||
private final ActivityResultLauncher<String[]> openDocumentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.OpenDocument(), uri -> {
|
||||
if (uri != null) {
|
||||
final MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build();
|
||||
startPlayer(Collections.singletonList(mediaItem));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -223,13 +234,25 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||
prefEditor.apply();
|
||||
|
||||
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
||||
final List<MediaItem> mediaItems = playlistHolder.mediaItems;
|
||||
if (!mediaItems.isEmpty()) {
|
||||
final MediaItem mediaItem = mediaItems.get(0);
|
||||
if (mediaItem.localConfiguration != null && USER_CONTENT.equals(mediaItem.localConfiguration.uri)) {
|
||||
openDocumentLauncher.launch(new String[]{"video/*"});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
startPlayer(playlistHolder.mediaItems);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startPlayer(final List<MediaItem> mediaItems) {
|
||||
Intent intent = new Intent(this, PlayerActivity.class);
|
||||
intent.putExtra(
|
||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||
IntentUtil.addToIntent(mediaItems, intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
package com.google.android.exoplayer2.extractor.avi;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AvcAviTrack extends AviTrack{
|
||||
private static final int NAL_TYPE_IRD = 5;
|
||||
private static final int NAL_TYPE_SEI = 6;
|
||||
private static final int NAL_TYPE_SPS = 7;
|
||||
private static final int NAL_MASK = 0x1f;
|
||||
private Format.Builder formatBuilder;
|
||||
private float pixelWidthHeightRatio = 1f;
|
||||
private NalUnitUtil.SpsData spsData;
|
||||
//The frame as a calculated from the picCount
|
||||
private int picFrame;
|
||||
private int lastPicFrame;
|
||||
//Largest picFrame, used when we hit an I frame
|
||||
private int maxPicFrame =-1;
|
||||
private int maxPicCount;
|
||||
private int posHalf;
|
||||
private int negHalf;
|
||||
|
||||
AvcAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput,
|
||||
@NonNull Format.Builder formatBuilder) {
|
||||
super(id, streamHeaderBox, trackOutput);
|
||||
this.formatBuilder = formatBuilder;
|
||||
}
|
||||
|
||||
public void setFormatBuilder(Format.Builder formatBuilder) {
|
||||
this.formatBuilder = formatBuilder;
|
||||
}
|
||||
|
||||
private int seekNal(final ParsableByteArray parsableByteArray) {
|
||||
final byte[] buffer = parsableByteArray.getData();
|
||||
for (int i=parsableByteArray.getPosition();i<buffer.length - 5;i++) {
|
||||
if (buffer[i] == 0 && buffer[i+1] == 0) {
|
||||
if (buffer[i+2] == 1) {
|
||||
parsableByteArray.setPosition(i+3);
|
||||
} else if (buffer[i+2] == 0 && buffer[i+3] == 1) {
|
||||
parsableByteArray.setPosition(i+4);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
return (parsableByteArray.readUnsignedByte() & NAL_MASK);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void processIdr() {
|
||||
lastPicFrame = 0;
|
||||
picFrame = maxPicFrame + 1;
|
||||
}
|
||||
|
||||
private void readSps(int size, ExtractorInput input) throws IOException {
|
||||
final byte[] buffer = new byte[size];
|
||||
input.readFully(buffer, 0, size, false);
|
||||
final ParsableByteArray parsableByteArray = new ParsableByteArray(buffer);
|
||||
int nal;
|
||||
while ((nal = seekNal(parsableByteArray)) >= 0) {
|
||||
if (nal == NAL_TYPE_SPS) {
|
||||
spsData = NalUnitUtil.parseSpsNalUnitPayload(parsableByteArray.getData(), parsableByteArray.getPosition(), parsableByteArray.capacity());
|
||||
maxPicCount = 1 << (spsData.picOrderCntLsbLength);
|
||||
posHalf = maxPicCount / 2; //Not sure why pics are 2x
|
||||
negHalf = -posHalf;
|
||||
//Not sure if this works after the fact
|
||||
if (spsData.pixelWidthHeightRatio != pixelWidthHeightRatio) {
|
||||
formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio);
|
||||
trackOutput.format(formatBuilder.build());
|
||||
}
|
||||
Log.d(AviExtractor.TAG, "SPS Frame: maxPicCount=" + maxPicCount);
|
||||
} else if (nal == NAL_TYPE_IRD) {
|
||||
processIdr();
|
||||
}
|
||||
}
|
||||
parsableByteArray.setPosition(0);
|
||||
trackOutput.sampleData(parsableByteArray, parsableByteArray.capacity());
|
||||
int flags = 0;
|
||||
if (isKeyFrame()) {
|
||||
flags |= C.BUFFER_FLAG_KEY_FRAME;
|
||||
}
|
||||
trackOutput.sampleMetadata(getUs(frame), flags, parsableByteArray.capacity(), 0, null);
|
||||
Log.d(AviExtractor.TAG, "SPS Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame());
|
||||
advance();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getUsFrame() {
|
||||
return picFrame;
|
||||
}
|
||||
|
||||
int getPicOrderCountLsb(byte[] peek) {
|
||||
if (peek[3] != 1) {
|
||||
return 0;
|
||||
}
|
||||
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, 5, peek.length);
|
||||
//slide_header()
|
||||
in.readUnsignedExpGolombCodedInt(); //first_mb_in_slice
|
||||
in.readUnsignedExpGolombCodedInt(); //slice_type
|
||||
in.readUnsignedExpGolombCodedInt(); //pic_parameter_set_id
|
||||
if (spsData.separateColorPlaneFlag) {
|
||||
in.skipBits(2); //colour_plane_id
|
||||
}
|
||||
in.readBits(spsData.frameNumLength); //frame_num
|
||||
if (!spsData.frameMbsOnlyFlag) {
|
||||
boolean field_pic_flag = in.readBit(); // field_pic_flag
|
||||
if (field_pic_flag) {
|
||||
in.readBit(); // bottom_field_flag
|
||||
}
|
||||
}
|
||||
//We skip IDR in the switch
|
||||
if (spsData.picOrderCountType == 0) {
|
||||
int picOrderCountLsb = in.readBits(spsData.picOrderCntLsbLength);
|
||||
Log.d("Test", "FrameNum: " + frame + " cnt=" + picOrderCountLsb);
|
||||
return picOrderCountLsb;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
||||
final int peekSize = Math.min(size, 16);
|
||||
byte[] peek = new byte[peekSize];
|
||||
input.peekFully(peek, 0, peekSize);
|
||||
final int nalType = peek[4] & NAL_MASK;
|
||||
switch (nalType) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4: {
|
||||
final int myPicCount = getPicOrderCountLsb(peek);
|
||||
int delta = myPicCount - lastPicFrame;
|
||||
if (delta < negHalf) {
|
||||
delta += maxPicCount;
|
||||
} else if (delta > posHalf) {
|
||||
delta -= maxPicCount;
|
||||
}
|
||||
picFrame += delta / 2;
|
||||
lastPicFrame = myPicCount;
|
||||
if (maxPicFrame < picFrame) {
|
||||
maxPicFrame = picFrame;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NAL_TYPE_IRD:
|
||||
processIdr();
|
||||
break;
|
||||
case NAL_TYPE_SEI:
|
||||
case NAL_TYPE_SPS:
|
||||
readSps(size, input);
|
||||
return true;
|
||||
}
|
||||
return super.newChunk(tag, size, input);
|
||||
}
|
||||
|
||||
public static String toString(byte[] buffer, int i, final int len) {
|
||||
final StringBuilder sb = new StringBuilder((len - i) * 3);
|
||||
while (i < len) {
|
||||
String hex = Integer.toHexString(buffer[i] & 0xff);
|
||||
if (hex.length() == 1) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(hex);
|
||||
sb.append(' ');
|
||||
i++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -82,9 +82,7 @@ public class AviExtractor implements Extractor {
|
|||
// private long indexOffset; //Usually chunkStart
|
||||
|
||||
//If partial read
|
||||
private transient AviTrack sampleTrack;
|
||||
private transient int sampleRemaining;
|
||||
private transient int sampleSize;
|
||||
private transient AviTrack chunkHandler;
|
||||
|
||||
public AviExtractor() {
|
||||
this(0);
|
||||
|
|
@ -213,14 +211,6 @@ public class AviExtractor implements Extractor {
|
|||
i++;
|
||||
if (streamHeader.isVideo()) {
|
||||
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
||||
final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD);
|
||||
final List<byte[]> codecData;
|
||||
if (codecBox != null) {
|
||||
codecData = Collections.singletonList(codecBox.getData());
|
||||
i++;
|
||||
} else {
|
||||
codecData = null;
|
||||
}
|
||||
final TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
|
||||
final Format.Builder builder = new Format.Builder();
|
||||
builder.setWidth(videoFormat.getWidth());
|
||||
|
|
@ -228,17 +218,30 @@ public class AviExtractor implements Extractor {
|
|||
builder.setFrameRate(streamHeader.getFrameRate());
|
||||
final String mimeType = streamHeader.getMimeType();
|
||||
builder.setSampleMimeType(mimeType);
|
||||
if (MimeTypes.VIDEO_H263.equals(mimeType)) {
|
||||
builder.setSelectionFlags(C.SELECTION_FLAG_FORCED);
|
||||
}
|
||||
//builder.setCodecs(streamHeader.getCodec());
|
||||
if (codecData != null) {
|
||||
builder.setInitializationData(codecData);
|
||||
// final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD);
|
||||
// final List<byte[]> codecData;
|
||||
// if (codecBox != null) {
|
||||
// codecData = Collections.singletonList(codecBox.getData());
|
||||
// i++;
|
||||
// } else {
|
||||
// codecData = null;
|
||||
// }
|
||||
// if (codecData != null) {
|
||||
// builder.setInitializationData(codecData);
|
||||
// }
|
||||
final AviTrack aviTrack;
|
||||
switch (mimeType) {
|
||||
case MimeTypes.VIDEO_MP4V:
|
||||
aviTrack = new Mp4vAviTrack(streamId, streamHeader, trackOutput, builder);
|
||||
break;
|
||||
case MimeTypes.VIDEO_H264:
|
||||
aviTrack = new AvcAviTrack(streamId, streamHeader, trackOutput, builder);
|
||||
break;
|
||||
default:
|
||||
aviTrack = new AviTrack(streamId, streamHeader, trackOutput);
|
||||
}
|
||||
trackOutput.format(builder.build());
|
||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24),
|
||||
new AviTrack(streamId, trackOutput,
|
||||
streamHeader));
|
||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack);
|
||||
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
|
||||
} else if (streamHeader.isAudio()) {
|
||||
final AudioFormat audioFormat = streamFormat.getAudioFormat();
|
||||
|
|
@ -262,7 +265,7 @@ public class AviExtractor implements Extractor {
|
|||
}
|
||||
trackOutput.format(builder.build());
|
||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24),
|
||||
new AviTrack(streamId, trackOutput, streamHeader));
|
||||
new AviTrack(streamId, streamHeader, trackOutput));
|
||||
}
|
||||
}
|
||||
streamId++;
|
||||
|
|
@ -374,20 +377,20 @@ public class AviExtractor implements Extractor {
|
|||
final int[] keyFrames = keyFrameList.array;
|
||||
videoTrack.setKeyFrames(keyFrames);
|
||||
|
||||
//Correct the timings
|
||||
durationUs = videoTrack.usPerSample * videoTrack.frame;
|
||||
|
||||
final SparseArray<int[]> idFrameArray = new SparseArray<>();
|
||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
||||
entry.getValue().pack();
|
||||
idFrameArray.put(entry.getKey(), entry.getValue().array);
|
||||
final AviTrack aviTrack = idTrackMap.get(entry.getKey());
|
||||
//If the index isn't sparse, double check the audio length
|
||||
if (videoTrack.frame == videoTrack.streamHeaderBox.getLength()) {
|
||||
//Sometimes this value is way off
|
||||
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
||||
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
||||
if (deltaPercent >.01) {
|
||||
aviTrack.usPerSample = getDuration()/aviTrack.frame;
|
||||
Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame));
|
||||
}
|
||||
//Sometimes this value is way off
|
||||
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
||||
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
||||
if (deltaPercent >.01) {
|
||||
aviTrack.usPerSample = getDuration()/aviTrack.frame;
|
||||
Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame));
|
||||
}
|
||||
}
|
||||
final AviSeekMap seekMap = new AviSeekMap(videoTrack, seekFrameRate, videoSeekOffset.array,
|
||||
|
|
@ -397,8 +400,10 @@ public class AviExtractor implements Extractor {
|
|||
}
|
||||
|
||||
int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||
if (sampleRemaining != 0) {
|
||||
sampleRemaining -= sampleTrack.trackOutput.sampleData(input, sampleRemaining, false);
|
||||
if (chunkHandler != null) {
|
||||
if (chunkHandler.resume(input)) {
|
||||
chunkHandler = null;
|
||||
}
|
||||
} else {
|
||||
ByteBuffer byteBuffer = allocate(8);
|
||||
final byte[] bytes = byteBuffer.array();
|
||||
|
|
@ -415,33 +420,26 @@ public class AviExtractor implements Extractor {
|
|||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
input.readFully(bytes, 1, 7);
|
||||
int id = byteBuffer.getInt();
|
||||
sampleSize = byteBuffer.getInt();
|
||||
sampleTrack = idTrackMap.get(id);
|
||||
final int id = byteBuffer.getInt();
|
||||
final int size = byteBuffer.getInt();
|
||||
AviTrack sampleTrack = idTrackMap.get(id);
|
||||
if (sampleTrack == null) {
|
||||
if (id == ListBox.LIST) {
|
||||
seekPosition.position = input.getPosition() + 4;
|
||||
} else {
|
||||
seekPosition.position = input.getPosition() + sampleSize;
|
||||
seekPosition.position = input.getPosition() + size;
|
||||
if (id != JUNK) {
|
||||
Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8)
|
||||
+ " size=" + sampleSize + " moviEnd=" + moviEnd);
|
||||
+ " size=" + size + " moviEnd=" + moviEnd);
|
||||
}
|
||||
}
|
||||
return RESULT_SEEK;
|
||||
} else {
|
||||
//sampleOffset = (int)(input.getPosition() - 8 - moviOffset);
|
||||
sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false);
|
||||
//Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo());
|
||||
if (!sampleTrack.newChunk(id, size, input)) {
|
||||
chunkHandler = sampleTrack;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sampleRemaining != 0) {
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
sampleTrack.trackOutput.sampleMetadata(
|
||||
sampleTrack.getUs(), sampleTrack.isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0 , sampleSize, 0, null);
|
||||
//Log.d(TAG, "Frame: " + (sampleTrack.isVideo()? 'V' : 'A') + " us=" + sampleTrack.getUs() + " size=" + sampleSize);
|
||||
sampleTrack.advance();
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ package com.google.android.exoplayer2.extractor.avi;
|
|||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AviHeaderBox extends ResidentBox {
|
||||
public static final int AVIF_HASINDEX = 0x10;
|
||||
private static final int AVIF_HASINDEX = 0x10;
|
||||
private static int AVIF_MUSTUSEINDEX = 0x20;
|
||||
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
|
||||
|
||||
//AVIMAINHEADER
|
||||
|
|
@ -12,19 +13,20 @@ public class AviHeaderBox extends ResidentBox {
|
|||
super(type, size, byteBuffer);
|
||||
}
|
||||
|
||||
boolean hasIndex() {
|
||||
return (getFlags() & AVIF_HASINDEX) > 0;
|
||||
}
|
||||
|
||||
int getMicroSecPerFrame() {
|
||||
return byteBuffer.getInt(0);
|
||||
}
|
||||
|
||||
//4 = dwMaxBytesPerSec
|
||||
//Always 0, but should be 2
|
||||
// int getPaddingGranularity() {
|
||||
// return byteBuffer.getInt(8);
|
||||
// }
|
||||
//8 = dwPaddingGranularity - Always 0, but should be 2
|
||||
|
||||
public boolean hasIndex() {
|
||||
return (getFlags() & AVIF_HASINDEX) == AVIF_HASINDEX;
|
||||
}
|
||||
|
||||
public boolean mustUseIndex() {
|
||||
return (getFlags() & AVIF_MUSTUSEINDEX) == AVIF_MUSTUSEINDEX;
|
||||
}
|
||||
|
||||
int getFlags() {
|
||||
return byteBuffer.getInt(12);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.google.android.exoplayer2.extractor.avi;
|
||||
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.NonNull;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
|
@ -13,9 +15,6 @@ import java.util.Arrays;
|
|||
public class AviTrack {
|
||||
final int id;
|
||||
|
||||
@NonNull
|
||||
final TrackOutput trackOutput;
|
||||
|
||||
@NonNull
|
||||
final StreamHeaderBox streamHeaderBox;
|
||||
|
||||
|
|
@ -26,24 +25,26 @@ public class AviTrack {
|
|||
*/
|
||||
boolean allKeyFrames;
|
||||
|
||||
@NonNull
|
||||
TrackOutput trackOutput;
|
||||
|
||||
/**
|
||||
* Key is frame number value is offset
|
||||
*/
|
||||
@Nullable
|
||||
int[] keyFrames;
|
||||
|
||||
transient int chunkSize;
|
||||
transient int chunkRemaining;
|
||||
|
||||
/**
|
||||
* Current frame in the stream
|
||||
* This needs to be updated on seek
|
||||
* TODO: Should be offset from StreamHeaderBox.getStart()
|
||||
*/
|
||||
transient int frame;
|
||||
int frame;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param trackOutput
|
||||
*/
|
||||
AviTrack(int id, @NonNull TrackOutput trackOutput, @NonNull StreamHeaderBox streamHeaderBox) {
|
||||
AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) {
|
||||
this.id = id;
|
||||
this.trackOutput = trackOutput;
|
||||
this.streamHeaderBox = streamHeaderBox;
|
||||
|
|
@ -67,11 +68,11 @@ public class AviTrack {
|
|||
}
|
||||
|
||||
public long getUs() {
|
||||
return frame * usPerSample;
|
||||
return getUs(getUsFrame());
|
||||
}
|
||||
|
||||
public void advance() {
|
||||
frame++;
|
||||
public long getUs(final int myFrame) {
|
||||
return myFrame * usPerSample;
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
|
|
@ -81,4 +82,45 @@ public class AviTrack {
|
|||
public boolean isAudio() {
|
||||
return streamHeaderBox.isAudio();
|
||||
}
|
||||
|
||||
public void advance() {
|
||||
frame++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frame number used to calculate the timeUs
|
||||
* @return
|
||||
*/
|
||||
int getUsFrame() {
|
||||
return frame;
|
||||
}
|
||||
|
||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
||||
final int remaining = size - trackOutput.sampleData(input, size, false);
|
||||
if (remaining == 0) {
|
||||
done(size);
|
||||
return true;
|
||||
} else {
|
||||
chunkSize = size;
|
||||
chunkRemaining = remaining;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean resume(ExtractorInput input) throws IOException {
|
||||
chunkRemaining -= trackOutput.sampleData(input, chunkRemaining, false);
|
||||
if (chunkRemaining == 0) {
|
||||
done(chunkSize);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void done(final int size) {
|
||||
trackOutput.sampleMetadata(
|
||||
getUs(), (isKeyFrame() ? C.BUFFER_FLAG_KEY_FRAME : 0), size, 0, null);
|
||||
//Log.d(AviExtractor.TAG, "Frame: " + (isVideo()? 'V' : 'A') + " us=" + getUs() + " size=" + size + " frame=" + frame + " usFrame=" + getUsFrame());
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.google.android.exoplayer2.extractor.avi;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Mp4vAviTrack extends AviTrack {
|
||||
private static final byte SEQUENCE_START_CODE = (byte)0xb0;
|
||||
private static final int LAYER_START_CODE = 0x20;
|
||||
private static final float[] ASPECT_RATIO = {0f, 1f, 12f/11f, 10f/11f, 16f/11f, 40f/33f};
|
||||
private static final int Extended_PAR = 0xf;
|
||||
private final Format.Builder formatBuilder;
|
||||
private float pixelWidthHeightRatio = 1f;
|
||||
|
||||
Mp4vAviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput,
|
||||
@NonNull Format.Builder formatBuilder) {
|
||||
super(id, streamHeaderBox, trackOutput);
|
||||
this.formatBuilder = formatBuilder;
|
||||
}
|
||||
|
||||
private void processLayerStart(byte[] peek, int offset) {
|
||||
final ParsableNalUnitBitArray in = new ParsableNalUnitBitArray(peek, offset, peek.length);
|
||||
in.skipBit(); // random_accessible_vol
|
||||
in.skipBits(8); // video_object_type_indication
|
||||
boolean is_object_layer_identifier = in.readBit();
|
||||
if (is_object_layer_identifier) {
|
||||
in.skipBits(7); // video_object_layer_verid, video_object_layer_priority
|
||||
}
|
||||
int aspect_ratio_info = in.readBits(4);
|
||||
final float aspectRatio;
|
||||
if (aspect_ratio_info == Extended_PAR) {
|
||||
float par_width = (float)in.readBits(8);
|
||||
float par_height = (float)in.readBits(8);
|
||||
aspectRatio = par_width / par_height;
|
||||
} else {
|
||||
aspectRatio = ASPECT_RATIO[aspect_ratio_info];
|
||||
}
|
||||
if (aspectRatio != pixelWidthHeightRatio) {
|
||||
trackOutput.format(formatBuilder.setPixelWidthHeightRatio(aspectRatio).build());
|
||||
pixelWidthHeightRatio = aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
private void seekLayerStart(ExtractorInput input) throws IOException {
|
||||
byte[] peek = new byte[128];
|
||||
input.peekFully(peek, 0, peek.length);
|
||||
for (int i = 4;i<peek.length - 4;i++) {
|
||||
if (peek[i] == 0 && peek[i+1] == 0 && peek[i+2] == 1 && (peek[i+3] & 0xf0) == LAYER_START_CODE) {
|
||||
processLayerStart(peek, i+4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean newChunk(int tag, int size, ExtractorInput input) throws IOException {
|
||||
final byte[] peek = new byte[4];
|
||||
input.peekFully(peek, 0, peek.length);
|
||||
if (peek[0] == 0 && peek[1] == 0 && peek[2] == 1 && peek[3] == SEQUENCE_START_CODE) {
|
||||
seekLayerStart(input);
|
||||
}
|
||||
return super.newChunk(tag, size, input);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue