mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +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",
|
"name": "Misc",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "AVI",
|
"name": "User File",
|
||||||
"uri": "https://drive.google.com/u/0/uc?id=1K6oLKCS56WFbhz33TgilTJBqfMYFTeUd&?export=download",
|
"uri": "content://user"
|
||||||
"extension": "avi"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dizzy (MP4)",
|
"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.checkNotNull;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
@ -40,6 +41,8 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
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 TAG = "SampleChooserActivity";
|
||||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
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 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 String[] uris;
|
||||||
private boolean useExtensionRenderers;
|
private boolean useExtensionRenderers;
|
||||||
|
|
@ -80,6 +84,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
private SampleAdapter sampleAdapter;
|
private SampleAdapter sampleAdapter;
|
||||||
private MenuItem preferExtensionDecodersMenuItem;
|
private MenuItem preferExtensionDecodersMenuItem;
|
||||||
private ExpandableListView sampleListView;
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -223,13 +234,25 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
prefEditor.apply();
|
prefEditor.apply();
|
||||||
|
|
||||||
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
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 intent = new Intent(this, PlayerActivity.class);
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
IntentUtil.addToIntent(mediaItems, intent);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
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
|
// private long indexOffset; //Usually chunkStart
|
||||||
|
|
||||||
//If partial read
|
//If partial read
|
||||||
private transient AviTrack sampleTrack;
|
private transient AviTrack chunkHandler;
|
||||||
private transient int sampleRemaining;
|
|
||||||
private transient int sampleSize;
|
|
||||||
|
|
||||||
public AviExtractor() {
|
public AviExtractor() {
|
||||||
this(0);
|
this(0);
|
||||||
|
|
@ -213,14 +211,6 @@ public class AviExtractor implements Extractor {
|
||||||
i++;
|
i++;
|
||||||
if (streamHeader.isVideo()) {
|
if (streamHeader.isVideo()) {
|
||||||
final VideoFormat videoFormat = streamFormat.getVideoFormat();
|
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 TrackOutput trackOutput = output.track(streamId, C.TRACK_TYPE_VIDEO);
|
||||||
final Format.Builder builder = new Format.Builder();
|
final Format.Builder builder = new Format.Builder();
|
||||||
builder.setWidth(videoFormat.getWidth());
|
builder.setWidth(videoFormat.getWidth());
|
||||||
|
|
@ -228,17 +218,30 @@ public class AviExtractor implements Extractor {
|
||||||
builder.setFrameRate(streamHeader.getFrameRate());
|
builder.setFrameRate(streamHeader.getFrameRate());
|
||||||
final String mimeType = streamHeader.getMimeType();
|
final String mimeType = streamHeader.getMimeType();
|
||||||
builder.setSampleMimeType(mimeType);
|
builder.setSampleMimeType(mimeType);
|
||||||
if (MimeTypes.VIDEO_H263.equals(mimeType)) {
|
// final StreamDataBox codecBox = (StreamDataBox) peekNext(streamChildren, i, StreamDataBox.STRD);
|
||||||
builder.setSelectionFlags(C.SELECTION_FLAG_FORCED);
|
// final List<byte[]> codecData;
|
||||||
}
|
// if (codecBox != null) {
|
||||||
//builder.setCodecs(streamHeader.getCodec());
|
// codecData = Collections.singletonList(codecBox.getData());
|
||||||
if (codecData != null) {
|
// i++;
|
||||||
builder.setInitializationData(codecData);
|
// } 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());
|
trackOutput.format(builder.build());
|
||||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24),
|
idTrackMap.put('0' | (('0' + streamId) << 8) | ('d' << 16) | ('c' << 24), aviTrack);
|
||||||
new AviTrack(streamId, trackOutput,
|
|
||||||
streamHeader));
|
|
||||||
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
|
durationUs = streamHeader.getUsPerSample() * streamHeader.getLength();
|
||||||
} else if (streamHeader.isAudio()) {
|
} else if (streamHeader.isAudio()) {
|
||||||
final AudioFormat audioFormat = streamFormat.getAudioFormat();
|
final AudioFormat audioFormat = streamFormat.getAudioFormat();
|
||||||
|
|
@ -262,7 +265,7 @@ public class AviExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
trackOutput.format(builder.build());
|
trackOutput.format(builder.build());
|
||||||
idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24),
|
idTrackMap.put('0' | (('0' + streamId) << 8) | ('w' << 16) | ('b' << 24),
|
||||||
new AviTrack(streamId, trackOutput, streamHeader));
|
new AviTrack(streamId, streamHeader, trackOutput));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamId++;
|
streamId++;
|
||||||
|
|
@ -374,20 +377,20 @@ public class AviExtractor implements Extractor {
|
||||||
final int[] keyFrames = keyFrameList.array;
|
final int[] keyFrames = keyFrameList.array;
|
||||||
videoTrack.setKeyFrames(keyFrames);
|
videoTrack.setKeyFrames(keyFrames);
|
||||||
|
|
||||||
|
//Correct the timings
|
||||||
|
durationUs = videoTrack.usPerSample * videoTrack.frame;
|
||||||
|
|
||||||
final SparseArray<int[]> idFrameArray = new SparseArray<>();
|
final SparseArray<int[]> idFrameArray = new SparseArray<>();
|
||||||
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
for (Map.Entry<Integer, UnboundedIntArray> entry : audioIdFrameMap.entrySet()) {
|
||||||
entry.getValue().pack();
|
entry.getValue().pack();
|
||||||
idFrameArray.put(entry.getKey(), entry.getValue().array);
|
idFrameArray.put(entry.getKey(), entry.getValue().array);
|
||||||
final AviTrack aviTrack = idTrackMap.get(entry.getKey());
|
final AviTrack aviTrack = idTrackMap.get(entry.getKey());
|
||||||
//If the index isn't sparse, double check the audio length
|
//Sometimes this value is way off
|
||||||
if (videoTrack.frame == videoTrack.streamHeaderBox.getLength()) {
|
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
||||||
//Sometimes this value is way off
|
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
||||||
long calcUsPerSample = (getDuration()/aviTrack.frame);
|
if (deltaPercent >.01) {
|
||||||
float deltaPercent = Math.abs(calcUsPerSample - aviTrack.usPerSample) / (float)aviTrack.usPerSample;
|
aviTrack.usPerSample = getDuration()/aviTrack.frame;
|
||||||
if (deltaPercent >.01) {
|
Log.d(TAG, "Frames act=" + getDuration() + " calc=" + (aviTrack.usPerSample * aviTrack.frame));
|
||||||
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,
|
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 {
|
int readSamples(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||||
if (sampleRemaining != 0) {
|
if (chunkHandler != null) {
|
||||||
sampleRemaining -= sampleTrack.trackOutput.sampleData(input, sampleRemaining, false);
|
if (chunkHandler.resume(input)) {
|
||||||
|
chunkHandler = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ByteBuffer byteBuffer = allocate(8);
|
ByteBuffer byteBuffer = allocate(8);
|
||||||
final byte[] bytes = byteBuffer.array();
|
final byte[] bytes = byteBuffer.array();
|
||||||
|
|
@ -415,33 +420,26 @@ public class AviExtractor implements Extractor {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
input.readFully(bytes, 1, 7);
|
input.readFully(bytes, 1, 7);
|
||||||
int id = byteBuffer.getInt();
|
final int id = byteBuffer.getInt();
|
||||||
sampleSize = byteBuffer.getInt();
|
final int size = byteBuffer.getInt();
|
||||||
sampleTrack = idTrackMap.get(id);
|
AviTrack sampleTrack = idTrackMap.get(id);
|
||||||
if (sampleTrack == null) {
|
if (sampleTrack == null) {
|
||||||
if (id == ListBox.LIST) {
|
if (id == ListBox.LIST) {
|
||||||
seekPosition.position = input.getPosition() + 4;
|
seekPosition.position = input.getPosition() + 4;
|
||||||
} else {
|
} else {
|
||||||
seekPosition.position = input.getPosition() + sampleSize;
|
seekPosition.position = input.getPosition() + size;
|
||||||
if (id != JUNK) {
|
if (id != JUNK) {
|
||||||
Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8)
|
Log.w(TAG, "Unknown tag=" + toString(id) + " pos=" + (input.getPosition() - 8)
|
||||||
+ " size=" + sampleSize + " moviEnd=" + moviEnd);
|
+ " size=" + size + " moviEnd=" + moviEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RESULT_SEEK;
|
return RESULT_SEEK;
|
||||||
} else {
|
} else {
|
||||||
//sampleOffset = (int)(input.getPosition() - 8 - moviOffset);
|
if (!sampleTrack.newChunk(id, size, input)) {
|
||||||
sampleRemaining = sampleSize - sampleTrack.trackOutput.sampleData(input, sampleSize, false);
|
chunkHandler = sampleTrack;
|
||||||
//Log.d(TAG, "Sample pos=" + (input.getPosition() - 8) + " size=" + sampleSize + " video=" + sampleTrack.isVideo());
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ package com.google.android.exoplayer2.extractor.avi;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public class AviHeaderBox extends ResidentBox {
|
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);
|
static final int AVIH = 'a' | ('v' << 8) | ('i' << 16) | ('h' << 24);
|
||||||
|
|
||||||
//AVIMAINHEADER
|
//AVIMAINHEADER
|
||||||
|
|
@ -12,19 +13,20 @@ public class AviHeaderBox extends ResidentBox {
|
||||||
super(type, size, byteBuffer);
|
super(type, size, byteBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasIndex() {
|
|
||||||
return (getFlags() & AVIF_HASINDEX) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getMicroSecPerFrame() {
|
int getMicroSecPerFrame() {
|
||||||
return byteBuffer.getInt(0);
|
return byteBuffer.getInt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//4 = dwMaxBytesPerSec
|
//4 = dwMaxBytesPerSec
|
||||||
//Always 0, but should be 2
|
//8 = dwPaddingGranularity - Always 0, but should be 2
|
||||||
// int getPaddingGranularity() {
|
|
||||||
// return byteBuffer.getInt(8);
|
public boolean hasIndex() {
|
||||||
// }
|
return (getFlags() & AVIF_HASINDEX) == AVIF_HASINDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustUseIndex() {
|
||||||
|
return (getFlags() & AVIF_MUSTUSEINDEX) == AVIF_MUSTUSEINDEX;
|
||||||
|
}
|
||||||
|
|
||||||
int getFlags() {
|
int getFlags() {
|
||||||
return byteBuffer.getInt(12);
|
return byteBuffer.getInt(12);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package com.google.android.exoplayer2.extractor.avi;
|
package com.google.android.exoplayer2.extractor.avi;
|
||||||
|
|
||||||
import android.util.SparseIntArray;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
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.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,9 +15,6 @@ import java.util.Arrays;
|
||||||
public class AviTrack {
|
public class AviTrack {
|
||||||
final int id;
|
final int id;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
final TrackOutput trackOutput;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
final StreamHeaderBox streamHeaderBox;
|
final StreamHeaderBox streamHeaderBox;
|
||||||
|
|
||||||
|
|
@ -26,24 +25,26 @@ public class AviTrack {
|
||||||
*/
|
*/
|
||||||
boolean allKeyFrames;
|
boolean allKeyFrames;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
TrackOutput trackOutput;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key is frame number value is offset
|
* Key is frame number value is offset
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
int[] keyFrames;
|
int[] keyFrames;
|
||||||
|
|
||||||
|
transient int chunkSize;
|
||||||
|
transient int chunkRemaining;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current frame in the stream
|
* Current frame in the stream
|
||||||
* This needs to be updated on seek
|
* This needs to be updated on seek
|
||||||
* TODO: Should be offset from StreamHeaderBox.getStart()
|
* TODO: Should be offset from StreamHeaderBox.getStart()
|
||||||
*/
|
*/
|
||||||
transient int frame;
|
int frame;
|
||||||
|
|
||||||
/**
|
AviTrack(int id, @NonNull StreamHeaderBox streamHeaderBox, @NonNull TrackOutput trackOutput) {
|
||||||
*
|
|
||||||
* @param trackOutput
|
|
||||||
*/
|
|
||||||
AviTrack(int id, @NonNull TrackOutput trackOutput, @NonNull StreamHeaderBox streamHeaderBox) {
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.trackOutput = trackOutput;
|
this.trackOutput = trackOutput;
|
||||||
this.streamHeaderBox = streamHeaderBox;
|
this.streamHeaderBox = streamHeaderBox;
|
||||||
|
|
@ -67,11 +68,11 @@ public class AviTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUs() {
|
public long getUs() {
|
||||||
return frame * usPerSample;
|
return getUs(getUsFrame());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void advance() {
|
public long getUs(final int myFrame) {
|
||||||
frame++;
|
return myFrame * usPerSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVideo() {
|
public boolean isVideo() {
|
||||||
|
|
@ -81,4 +82,45 @@ public class AviTrack {
|
||||||
public boolean isAudio() {
|
public boolean isAudio() {
|
||||||
return streamHeaderBox.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