From 4c76bf1a9d305fe3d0008db173b71d4a50c1fa55 Mon Sep 17 00:00:00 2001 From: Dustin Date: Fri, 21 Jan 2022 22:25:32 -0700 Subject: [PATCH] Add file chooser to UI, Fixed timing issues on H264, Fixed PAR on XVID and H264 --- demos/main/src/main/assets/media.exolist.json | 5 +- .../demo/SampleChooserActivity.java | 27 ++- .../exoplayer2/extractor/avi/AvcAviTrack.java | 177 ++++++++++++++++++ .../extractor/avi/AviExtractor.java | 92 +++++---- .../extractor/avi/AviHeaderBox.java | 20 +- .../exoplayer2/extractor/avi/AviTrack.java | 68 +++++-- .../extractor/avi/Mp4vAviTrack.java | 67 +++++++ 7 files changed, 382 insertions(+), 74 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 2a295a8e26..e6cb246db1 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -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)", diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index b79a7a62ca..0111210101 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -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 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 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 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) { diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java new file mode 100644 index 0000000000..0c9d88f380 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AvcAviTrack.java @@ -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= 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(); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java index 6c55fff1db..f8797b29b0 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviExtractor.java @@ -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 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 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 idFrameArray = new SparseArray<>(); for (Map.Entry 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; } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java index c6c094f903..08d5ddf9e3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviHeaderBox.java @@ -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); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java index 9aa2937268..fb47cc37cd 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/AviTrack.java @@ -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(); + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java new file mode 100644 index 0000000000..ff0962ece0 --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/avi/Mp4vAviTrack.java @@ -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