diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index d0201fbc61..ff7ceecc98 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -76,8 +76,10 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener } @Override - public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { - Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + pixelWidthHeightRatio + "]"); + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees + + ", " + pixelWidthHeightRatio + "]"); } // DemoPlayer.InfoListener diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 0e6289cc13..d201891b0f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -341,7 +341,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, } @Override - public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) { + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthAspectRatio) { shutterView.setVisibility(View.GONE); videoFrame.setAspectRatio( height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index b05f7f4b9d..1482657e44 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -89,7 +89,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public interface Listener { void onStateChanged(boolean playWhenReady, int playbackState); void onError(Exception e); - void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio); } /** @@ -449,9 +450,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { for (Listener listener : listeners) { - listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); + listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java index a1f329481e..0c7d7c5e29 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java @@ -91,7 +91,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { public void testMaxVideoDimensions() { DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, null, null, null); - MediaFormat format = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null); + MediaFormat format = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, 1, null); format = chunkSource.getWithMaxVideoDimensions(format); assertEquals(WIDE_WIDTH, format.maxWidth); @@ -121,7 +121,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2); DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2); - MediaFormat format = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null); + MediaFormat format = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, 1, null); format = chunkSource.getWithMaxVideoDimensions(format); assertEquals(WIDE_WIDTH, format.maxWidth); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java index d3480164e2..7b2d9acf0b 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java @@ -77,8 +77,9 @@ public final class Mp4ExtractorTest extends TestCase { + "000000000000000000000000000003"); /** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */ - private static final byte[] TKHD_PAYLOAD = - getByteArray("0000000000000000000000000000000000000000FFFFFFFF"); + private static final byte[] TKHD_PAYLOAD = getByteArray( + "00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100" + + "0000000000000000000000000000010000000000000000000000000000400000000780000004380000"); /** Video frame timestamps in time units. */ private static final int[] SAMPLE_TIMESTAMPS = {0, 2, 3, 5, 6, 7}; @@ -87,7 +88,7 @@ public final class Mp4ExtractorTest extends TestCase { /** Indices of key-frames. */ private static final boolean[] SAMPLE_IS_SYNC = {true, false, false, false, true, true}; /** Indices of video frame chunk offsets. */ - private static final int[] CHUNK_OFFSETS = {1080, 2000, 3000, 4000}; + private static final int[] CHUNK_OFFSETS = {1200, 2120, 3120, 4120}; /** Numbers of video frames in each chunk. */ private static final int[] SAMPLES_IN_CHUNK = {2, 2, 1, 1}; /** The mdat box must be large enough to avoid reading chunk sample data out of bounds. */ @@ -399,7 +400,7 @@ public final class Mp4ExtractorTest extends TestCase { atom(Atom.TYPE_stsc, getStsc()), atom(Atom.TYPE_stsz, getStsz()), atom(Atom.TYPE_stco, getStco())))))), - atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 1048 : 1038, !mp4vFormat))); + atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 1168 : 1158, !mp4vFormat))); } /** Gets a valid MP4 file with audio/video tracks and without a synchronization table. */ @@ -435,7 +436,7 @@ public final class Mp4ExtractorTest extends TestCase { atom(Atom.TYPE_stsc, getStsc()), atom(Atom.TYPE_stsz, getStsz()), atom(Atom.TYPE_stco, getStco())))))), - atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 992 : 982, !mp4vFormat))); + atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 1112 : 1102, !mp4vFormat))); } private static Mp4Atom atom(int type, Mp4Atom... containedMp4Atoms) { diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 103747ae1d..6f485c75e4 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -302,6 +302,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE); int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH); int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); + int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees"); int channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); ArrayList initializationData = new ArrayList<>(); @@ -314,9 +315,9 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe } long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; - return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, MediaFormat.NO_VALUE, - channelCount, sampleRate, language, initializationData, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE); + return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees, + MediaFormat.NO_VALUE, channelCount, sampleRate, language, initializationData, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); } @TargetApi(16) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index eba2e4985c..f25d357cd0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -26,6 +26,7 @@ import android.media.MediaCrypto; import android.os.Handler; import android.os.SystemClock; import android.view.Surface; +import android.view.TextureView; import java.nio.ByteBuffer; @@ -59,11 +60,19 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * * @param width The video width in pixels. * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link TextureView} can apply the rotation by + * calling {@link TextureView#setTransform}. Applications that do not expect to encounter + * rotated videos can safely ignore this parameter. * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic * content. */ - void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio); /** * Invoked when a frame is rendered to a surface for the first time following that surface @@ -129,12 +138,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private long droppedFrameAccumulationStartTimeMs; private int droppedFrameCount; + private int pendingRotationDegrees; + private float pendingPixelWidthHeightRatio; private int currentWidth; private int currentHeight; + private int currentUnappliedRotationDegrees; private float currentPixelWidthHeightRatio; - private float pendingPixelWidthHeightRatio; private int lastReportedWidth; private int lastReportedHeight; + private int lastReportedUnappliedRotationDegrees; private float lastReportedPixelWidthHeightRatio; /** @@ -374,6 +386,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { super.onInputFormatChanged(holder); pendingPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1 : holder.format.pixelWidthHeightRatio; + pendingRotationDegrees = holder.format.rotationDegrees == MediaFormat.NO_VALUE ? 0 + : holder.format.rotationDegrees; } /** @@ -395,6 +409,20 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT); currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; + if (Util.SDK_INT >= 21) { + // On API level 21 and above the decoder applies the rotation when rendering to the surface. + // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need + // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. + if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) { + int rotatedHeight = currentWidth; + currentWidth = currentHeight; + currentHeight = rotatedHeight; + currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; + } + } else { + // On API level 20 and below the decoder does not apply the rotation. + currentUnappliedRotationDegrees = pendingRotationDegrees; + } } @Override @@ -520,22 +548,26 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private void maybeNotifyVideoSizeChanged() { if (eventHandler == null || eventListener == null || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight + && lastReportedUnappliedRotationDegrees == currentUnappliedRotationDegrees && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { return; } // Make final copies to ensure the runnable reports the correct values. final int currentWidth = this.currentWidth; final int currentHeight = this.currentHeight; + final int currentUnappliedRotationDegrees = this.currentUnappliedRotationDegrees; final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); + eventListener.onVideoSizeChanged(currentWidth, currentHeight, + currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); } }); // Update the last reported values. lastReportedWidth = currentWidth; lastReportedHeight = currentHeight; + lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index cea777eadf..1b1dd45a00 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -44,6 +44,7 @@ public final class MediaFormat { public final int width; public final int height; + public final int rotationDegrees; public final float pixelWidthHeightRatio; public final int channelCount; @@ -64,19 +65,21 @@ public final class MediaFormat { public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, int height, List initializationData) { return createVideoFormat( - mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, initializationData); + mimeType, maxInputSize, C.UNKNOWN_TIME_US, width, height, NO_VALUE, initializationData); } public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs, - int width, int height, List initializationData) { + int width, int height, int rotationDegrees, List initializationData) { return createVideoFormat( - mimeType, maxInputSize, durationUs, width, height, 1, initializationData); + mimeType, maxInputSize, durationUs, width, height, rotationDegrees, NO_VALUE, + initializationData); } public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs, - int width, int height, float pixelWidthHeightRatio, List initializationData) { - return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio, - NO_VALUE, NO_VALUE, null, initializationData, NO_VALUE, NO_VALUE); + int width, int height, int rotationDegrees, float pixelWidthHeightRatio, + List initializationData) { + return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees, + pixelWidthHeightRatio, NO_VALUE, NO_VALUE, null, initializationData, NO_VALUE, NO_VALUE); } public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, @@ -88,7 +91,7 @@ public final class MediaFormat { public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs, int channelCount, int sampleRate, List initializationData) { return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, - channelCount, sampleRate, null, initializationData, NO_VALUE, NO_VALUE); + NO_VALUE, channelCount, sampleRate, null, initializationData, NO_VALUE, NO_VALUE); } public static MediaFormat createTextFormat(String mimeType, String language) { @@ -97,7 +100,7 @@ public final class MediaFormat { public static MediaFormat createTextFormat(String mimeType, String language, long durationUs) { return new MediaFormat(mimeType, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, language, null, NO_VALUE, NO_VALUE); + NO_VALUE, NO_VALUE, NO_VALUE, language, null, NO_VALUE, NO_VALUE); } public static MediaFormat createFormatForMimeType(String mimeType) { @@ -106,17 +109,19 @@ public final class MediaFormat { public static MediaFormat createFormatForMimeType(String mimeType, long durationUs) { return new MediaFormat(mimeType, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, null, null, NO_VALUE, NO_VALUE); + NO_VALUE, NO_VALUE, NO_VALUE, null, null, NO_VALUE, NO_VALUE); } /* package */ MediaFormat(String mimeType, int maxInputSize, long durationUs, int width, - int height, float pixelWidthHeightRatio, int channelCount, int sampleRate, String language, - List initializationData, int maxWidth, int maxHeight) { + int height, int rotationDegrees, float pixelWidthHeightRatio, int channelCount, + int sampleRate, String language, List initializationData, int maxWidth, + int maxHeight) { this.mimeType = mimeType; this.maxInputSize = maxInputSize; this.durationUs = durationUs; this.width = width; this.height = height; + this.rotationDegrees = rotationDegrees; this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.channelCount = channelCount; this.sampleRate = sampleRate; @@ -128,8 +133,9 @@ public final class MediaFormat { } public MediaFormat copyWithMaxVideoDimension(int maxWidth, int maxHeight) { - return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio, - channelCount, sampleRate, language, initializationData, maxWidth, maxHeight); + return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees, + pixelWidthHeightRatio, channelCount, sampleRate, language, initializationData, maxWidth, + maxHeight); } /** @@ -145,6 +151,7 @@ public final class MediaFormat { maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); + maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); @@ -163,8 +170,8 @@ public final class MediaFormat { @Override public String toString() { return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " - + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + language + ", " - + durationUs + ", " + maxWidth + ", " + maxHeight + ")"; + + rotationDegrees + ", " + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + + ", " + language + ", " + durationUs + ", " + maxWidth + ", " + maxHeight + ")"; } @Override @@ -175,6 +182,7 @@ public final class MediaFormat { result = 31 * result + maxInputSize; result = 31 * result + width; result = 31 * result + height; + result = 31 * result + rotationDegrees; result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); result = 31 * result + (int) durationUs; result = 31 * result + maxWidth; @@ -213,6 +221,7 @@ public final class MediaFormat { private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) { if (maxInputSize != other.maxInputSize || width != other.width || height != other.height + || rotationDegrees != other.rotationDegrees || pixelWidthHeightRatio != other.pixelWidthHeightRatio || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight)) || channelCount != other.channelCount || sampleRate != other.sampleRate diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index d978b41c45..757f22ce9e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -50,9 +50,8 @@ import java.util.List; return null; } - Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); - int id = header.first; - long duration = header.second; + TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); + long duration = tkhdData.duration; long movieTimescale = parseMvhd(mvhd.data); long durationUs; if (duration == -1) { @@ -64,10 +63,10 @@ import java.util.List; .getContainerAtomOfType(Atom.TYPE_stbl); Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); - StsdDataHolder stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs, - mdhdData.second); + StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs, + tkhdData.rotationDegrees, mdhdData.second); return stsdData.mediaFormat == null ? null - : new Track(id, trackType, mdhdData.first, durationUs, stsdData.mediaFormat, + : new Track(tkhdData.id, trackType, mdhdData.first, durationUs, stsdData.mediaFormat, stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength); } @@ -268,19 +267,17 @@ import java.util.List; /** * Parses a tkhd atom (defined in 14496-12). * - * @return A {@link Pair} consisting of the track id and duration (in the timescale indicated in - * the movie header box). The duration is set to -1 if the duration is unspecified. + * @return An object containing the parsed data. */ - private static Pair parseTkhd(ParsableByteArray tkhd) { + private static TkhdData parseTkhd(ParsableByteArray tkhd) { tkhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tkhd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); tkhd.skipBytes(version == 0 ? 8 : 16); - int trackId = tkhd.readInt(); - tkhd.skipBytes(4); + tkhd.skipBytes(4); boolean durationUnknown = true; int durationPosition = tkhd.getPosition(); int durationByteCount = version == 0 ? 4 : 8; @@ -298,7 +295,27 @@ import java.util.List; duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); } - return Pair.create(trackId, duration); + tkhd.skipBytes(16); + int a00 = tkhd.readInt(); + int a01 = tkhd.readInt(); + tkhd.skipBytes(4); + int a10 = tkhd.readInt(); + int a11 = tkhd.readInt(); + + int rotationDegrees; + int fixedOne = 65536; + if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) { + rotationDegrees = 90; + } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) { + rotationDegrees = 270; + } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) { + rotationDegrees = 180; + } else { + // Only 0, 90, 180 and 270 are supported. Treat anything else as 0. + rotationDegrees = 0; + } + + return new TkhdData(trackId, duration, rotationDegrees); } /** @@ -333,11 +350,16 @@ import java.util.List; return Pair.create(timescale, language); } - private static StsdDataHolder parseStsd(ParsableByteArray stsd, long durationUs, + /** + * Parses a stsd atom (defined in 14496-12). + * + * @return An object containing the parsed data. + */ + private static StsdData parseStsd(ParsableByteArray stsd, long durationUs, int rotationDegrees, String language) { stsd.setPosition(Atom.FULL_HEADER_SIZE); int numberOfEntries = stsd.readInt(); - StsdDataHolder holder = new StsdDataHolder(numberOfEntries); + StsdData out = new StsdData(numberOfEntries); for (int i = 0; i < numberOfEntries; i++) { int childStartPosition = stsd.getPosition(); int childAtomSize = stsd.readInt(); @@ -347,25 +369,26 @@ import java.util.List; || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 || childAtomType == Atom.TYPE_s263) { - parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, durationUs, holder, i); + parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, durationUs, rotationDegrees, + out, i); } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca || childAtomType == Atom.TYPE_ac_3) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs, - holder, i); + out, i); } else if (childAtomType == Atom.TYPE_TTML) { - holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, language, + out.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, language, durationUs); } else if (childAtomType == Atom.TYPE_tx3g) { - holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, language, + out.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, language, durationUs); } stsd.setPosition(childStartPosition + childAtomSize); } - return holder; + return out; } private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size, - long durationUs, StsdDataHolder out, int entryIndex) { + long durationUs, int rotationDegrees, StsdData out, int entryIndex) { parent.setPosition(position + Atom.HEADER_SIZE); parent.skipBytes(24); @@ -428,7 +451,7 @@ import java.util.List; } out.mediaFormat = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, durationUs, - width, height, pixelWidthHeightRatio, initializationData); + width, height, rotationDegrees, pixelWidthHeightRatio, initializationData); } private static AvcCData parseAvcCFromParent(ParsableByteArray parent, int position) { @@ -556,7 +579,7 @@ import java.util.List; } private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, - int size, long durationUs, StsdDataHolder out, int entryIndex) { + int size, long durationUs, StsdData out, int entryIndex) { parent.setPosition(position + Atom.HEADER_SIZE); parent.skipBytes(16); int channelCount = parent.readUnsignedShort(); @@ -702,23 +725,43 @@ import java.util.List; // Prevent instantiation. } + /** + * Holds data parsed from a tkhd atom. + */ + private static final class TkhdData { + + private final int id; + private final long duration; + private final int rotationDegrees; + + public TkhdData(int id, long duration, int rotationDegrees) { + this.id = id; + this.duration = duration; + this.rotationDegrees = rotationDegrees; + } + + } + /** * Holds data parsed from an stsd atom and its children. */ - private static final class StsdDataHolder { + private static final class StsdData { public final TrackEncryptionBox[] trackEncryptionBoxes; public MediaFormat mediaFormat; public int nalUnitLengthFieldLength; - public StsdDataHolder(int numberOfEntries) { + public StsdData(int numberOfEntries) { trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; nalUnitLengthFieldLength = -1; } } + /** + * Holds data parsed from an AvcC atom. + */ private static final class AvcCData { public final List initializationData; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index 8be76ec411..f588c55b3d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -211,7 +211,7 @@ import java.util.List; // Construct and output the format. output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - C.UNKNOWN_TIME_US, parsedSpsData.width, parsedSpsData.height, + C.UNKNOWN_TIME_US, parsedSpsData.width, parsedSpsData.height, 0, parsedSpsData.pixelWidthAspectRatio, initializationData)); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java index ad651cfea2..77642b66ad 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H265Reader.java @@ -306,7 +306,7 @@ import java.util.Collections; } output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H265, MediaFormat.NO_VALUE, - C.UNKNOWN_TIME_US, picWidthInLumaSamples, picHeightInLumaSamples, pixelWidthHeightRatio, + C.UNKNOWN_TIME_US, picWidthInLumaSamples, picHeightInLumaSamples, 0, pixelWidthHeightRatio, Collections.singletonList(csd))); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index 29687aefe3..e50600f089 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -1127,7 +1127,7 @@ public final class WebmExtractor implements Extractor { sampleRate, initializationData); } else if (MimeTypes.isVideo(mimeType)) { return MediaFormat.createVideoFormat(mimeType, maxInputSize, durationUs, pixelWidth, - pixelHeight, initializationData); + pixelHeight, 0, initializationData); } else { throw new ParserException("Unexpected MIME type."); }