Add file chooser to UI, Fixed timing issues on H264, Fixed PAR on XVID and H264

This commit is contained in:
Dustin 2022-01-21 22:25:32 -07:00
parent d9afe5105b
commit 4c76bf1a9d
7 changed files with 382 additions and 74 deletions

View file

@ -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)",

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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);
}
}