Fix rotation handling as far as is possible.

Issue: #91
This commit is contained in:
Oliver Woodman 2015-08-13 11:18:15 +01:00
parent 8db1331021
commit d3995eaa7a
12 changed files with 152 additions and 61 deletions

View File

@ -76,8 +76,10 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + pixelWidthHeightRatio + "]"); float pixelWidthHeightRatio) {
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees
+ ", " + pixelWidthHeightRatio + "]");
} }
// DemoPlayer.InfoListener // DemoPlayer.InfoListener

View File

@ -341,7 +341,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
} }
@Override @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); shutterView.setVisibility(View.GONE);
videoFrame.setAspectRatio( videoFrame.setAspectRatio(
height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); height == 0 ? 1 : (width * pixelWidthAspectRatio) / height);

View File

@ -89,7 +89,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public interface Listener { public interface Listener {
void onStateChanged(boolean playWhenReady, int playbackState); void onStateChanged(boolean playWhenReady, int playbackState);
void onError(Exception e); 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 @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) { for (Listener listener : listeners) {
listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
} }
} }

View File

@ -91,7 +91,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
public void testMaxVideoDimensions() { public void testMaxVideoDimensions() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO,
null, null, null); 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); format = chunkSource.getWithMaxVideoDimensions(format);
assertEquals(WIDE_WIDTH, format.maxWidth); assertEquals(WIDE_WIDTH, format.maxWidth);
@ -121,7 +121,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2); Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2);
DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2); 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); format = chunkSource.getWithMaxVideoDimensions(format);
assertEquals(WIDE_WIDTH, format.maxWidth); assertEquals(WIDE_WIDTH, format.maxWidth);

View File

@ -77,8 +77,9 @@ public final class Mp4ExtractorTest extends TestCase {
+ "000000000000000000000000000003"); + "000000000000000000000000000003");
/** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */ /** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */
private static final byte[] TKHD_PAYLOAD = private static final byte[] TKHD_PAYLOAD = getByteArray(
getByteArray("0000000000000000000000000000000000000000FFFFFFFF"); "00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100"
+ "0000000000000000000000000000010000000000000000000000000000400000000780000004380000");
/** Video frame timestamps in time units. */ /** Video frame timestamps in time units. */
private static final int[] SAMPLE_TIMESTAMPS = {0, 2, 3, 5, 6, 7}; 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. */ /** Indices of key-frames. */
private static final boolean[] SAMPLE_IS_SYNC = {true, false, false, false, true, true}; private static final boolean[] SAMPLE_IS_SYNC = {true, false, false, false, true, true};
/** Indices of video frame chunk offsets. */ /** 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. */ /** Numbers of video frames in each chunk. */
private static final int[] SAMPLES_IN_CHUNK = {2, 2, 1, 1}; 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. */ /** 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_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()), atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco())))))), 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. */ /** 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_stsc, getStsc()),
atom(Atom.TYPE_stsz, getStsz()), atom(Atom.TYPE_stsz, getStsz()),
atom(Atom.TYPE_stco, getStco())))))), 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) { private static Mp4Atom atom(int type, Mp4Atom... containedMp4Atoms) {

View File

@ -302,6 +302,7 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE); int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH); int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); 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 channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
ArrayList<byte[]> initializationData = new ArrayList<>(); ArrayList<byte[]> initializationData = new ArrayList<>();
@ -314,9 +315,9 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
} }
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, MediaFormat.NO_VALUE, return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees,
channelCount, sampleRate, language, initializationData, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, channelCount, sampleRate, language, initializationData,
MediaFormat.NO_VALUE); MediaFormat.NO_VALUE, MediaFormat.NO_VALUE);
} }
@TargetApi(16) @TargetApi(16)

View File

@ -26,6 +26,7 @@ import android.media.MediaCrypto;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
import android.view.TextureView;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -59,11 +60,19 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
* *
* @param width The video width in pixels. * @param width The video width in pixels.
* @param height The video height 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 * @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 * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* content. * 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 * 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 long droppedFrameAccumulationStartTimeMs;
private int droppedFrameCount; private int droppedFrameCount;
private int pendingRotationDegrees;
private float pendingPixelWidthHeightRatio;
private int currentWidth; private int currentWidth;
private int currentHeight; private int currentHeight;
private int currentUnappliedRotationDegrees;
private float currentPixelWidthHeightRatio; private float currentPixelWidthHeightRatio;
private float pendingPixelWidthHeightRatio;
private int lastReportedWidth; private int lastReportedWidth;
private int lastReportedHeight; private int lastReportedHeight;
private int lastReportedUnappliedRotationDegrees;
private float lastReportedPixelWidthHeightRatio; private float lastReportedPixelWidthHeightRatio;
/** /**
@ -374,6 +386,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
super.onInputFormatChanged(holder); super.onInputFormatChanged(holder);
pendingPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1 pendingPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1
: holder.format.pixelWidthHeightRatio; : 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(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
: outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT); : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT);
currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; 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 @Override
@ -520,22 +548,26 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
private void maybeNotifyVideoSizeChanged() { private void maybeNotifyVideoSizeChanged() {
if (eventHandler == null || eventListener == null if (eventHandler == null || eventListener == null
|| (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight
&& lastReportedUnappliedRotationDegrees == currentUnappliedRotationDegrees
&& lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) {
return; return;
} }
// Make final copies to ensure the runnable reports the correct values. // Make final copies to ensure the runnable reports the correct values.
final int currentWidth = this.currentWidth; final int currentWidth = this.currentWidth;
final int currentHeight = this.currentHeight; final int currentHeight = this.currentHeight;
final int currentUnappliedRotationDegrees = this.currentUnappliedRotationDegrees;
final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio;
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); eventListener.onVideoSizeChanged(currentWidth, currentHeight,
currentUnappliedRotationDegrees, currentPixelWidthHeightRatio);
} }
}); });
// Update the last reported values. // Update the last reported values.
lastReportedWidth = currentWidth; lastReportedWidth = currentWidth;
lastReportedHeight = currentHeight; lastReportedHeight = currentHeight;
lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;
lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
} }

View File

@ -44,6 +44,7 @@ public final class MediaFormat {
public final int width; public final int width;
public final int height; public final int height;
public final int rotationDegrees;
public final float pixelWidthHeightRatio; public final float pixelWidthHeightRatio;
public final int channelCount; public final int channelCount;
@ -64,19 +65,21 @@ public final class MediaFormat {
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, List<byte[]> initializationData) { int height, List<byte[]> initializationData) {
return createVideoFormat( 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, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, List<byte[]> initializationData) { int width, int height, int rotationDegrees, List<byte[]> initializationData) {
return createVideoFormat( 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, public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, long durationUs,
int width, int height, float pixelWidthHeightRatio, List<byte[]> initializationData) { int width, int height, int rotationDegrees, float pixelWidthHeightRatio,
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio, List<byte[]> initializationData) {
NO_VALUE, NO_VALUE, null, initializationData, NO_VALUE, NO_VALUE); 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, 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, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, long durationUs,
int channelCount, int sampleRate, List<byte[]> initializationData) { int channelCount, int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, 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) { 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) { public static MediaFormat createTextFormat(String mimeType, String language, long durationUs) {
return new MediaFormat(mimeType, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, 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) { public static MediaFormat createFormatForMimeType(String mimeType) {
@ -106,17 +109,19 @@ public final class MediaFormat {
public static MediaFormat createFormatForMimeType(String mimeType, long durationUs) { public static MediaFormat createFormatForMimeType(String mimeType, long durationUs) {
return new MediaFormat(mimeType, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, NO_VALUE, 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, /* package */ MediaFormat(String mimeType, int maxInputSize, long durationUs, int width,
int height, float pixelWidthHeightRatio, int channelCount, int sampleRate, String language, int height, int rotationDegrees, float pixelWidthHeightRatio, int channelCount,
List<byte[]> initializationData, int maxWidth, int maxHeight) { int sampleRate, String language, List<byte[]> initializationData, int maxWidth,
int maxHeight) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.maxInputSize = maxInputSize; this.maxInputSize = maxInputSize;
this.durationUs = durationUs; this.durationUs = durationUs;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.rotationDegrees = rotationDegrees;
this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.channelCount = channelCount; this.channelCount = channelCount;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
@ -128,8 +133,9 @@ public final class MediaFormat {
} }
public MediaFormat copyWithMaxVideoDimension(int maxWidth, int maxHeight) { public MediaFormat copyWithMaxVideoDimension(int maxWidth, int maxHeight) {
return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, pixelWidthHeightRatio, return new MediaFormat(mimeType, maxInputSize, durationUs, width, height, rotationDegrees,
channelCount, sampleRate, language, initializationData, maxWidth, maxHeight); 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_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); 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_WIDTH, maxWidth);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
@ -163,8 +170,8 @@ public final class MediaFormat {
@Override @Override
public String toString() { public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+ pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + language + ", " + rotationDegrees + ", " + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate
+ durationUs + ", " + maxWidth + ", " + maxHeight + ")"; + ", " + language + ", " + durationUs + ", " + maxWidth + ", " + maxHeight + ")";
} }
@Override @Override
@ -175,6 +182,7 @@ public final class MediaFormat {
result = 31 * result + maxInputSize; result = 31 * result + maxInputSize;
result = 31 * result + width; result = 31 * result + width;
result = 31 * result + height; result = 31 * result + height;
result = 31 * result + rotationDegrees;
result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + (int) durationUs; result = 31 * result + (int) durationUs;
result = 31 * result + maxWidth; result = 31 * result + maxWidth;
@ -213,6 +221,7 @@ public final class MediaFormat {
private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) { private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) {
if (maxInputSize != other.maxInputSize || width != other.width || height != other.height if (maxInputSize != other.maxInputSize || width != other.width || height != other.height
|| rotationDegrees != other.rotationDegrees
|| pixelWidthHeightRatio != other.pixelWidthHeightRatio || pixelWidthHeightRatio != other.pixelWidthHeightRatio
|| (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight)) || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight))
|| channelCount != other.channelCount || sampleRate != other.sampleRate || channelCount != other.channelCount || sampleRate != other.sampleRate

View File

@ -50,9 +50,8 @@ import java.util.List;
return null; return null;
} }
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
int id = header.first; long duration = tkhdData.duration;
long duration = header.second;
long movieTimescale = parseMvhd(mvhd.data); long movieTimescale = parseMvhd(mvhd.data);
long durationUs; long durationUs;
if (duration == -1) { if (duration == -1) {
@ -64,10 +63,10 @@ import java.util.List;
.getContainerAtomOfType(Atom.TYPE_stbl); .getContainerAtomOfType(Atom.TYPE_stbl);
Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); Pair<Long, String> mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data);
StsdDataHolder stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs, StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, durationUs,
mdhdData.second); tkhdData.rotationDegrees, mdhdData.second);
return stsdData.mediaFormat == null ? null 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); stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength);
} }
@ -268,19 +267,17 @@ import java.util.List;
/** /**
* Parses a tkhd atom (defined in 14496-12). * Parses a tkhd atom (defined in 14496-12).
* *
* @return A {@link Pair} consisting of the track id and duration (in the timescale indicated in * @return An object containing the parsed data.
* the movie header box). The duration is set to -1 if the duration is unspecified.
*/ */
private static Pair<Integer, Long> parseTkhd(ParsableByteArray tkhd) { private static TkhdData parseTkhd(ParsableByteArray tkhd) {
tkhd.setPosition(Atom.HEADER_SIZE); tkhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tkhd.readInt(); int fullAtom = tkhd.readInt();
int version = Atom.parseFullAtomVersion(fullAtom); int version = Atom.parseFullAtomVersion(fullAtom);
tkhd.skipBytes(version == 0 ? 8 : 16); tkhd.skipBytes(version == 0 ? 8 : 16);
int trackId = tkhd.readInt(); int trackId = tkhd.readInt();
tkhd.skipBytes(4);
tkhd.skipBytes(4);
boolean durationUnknown = true; boolean durationUnknown = true;
int durationPosition = tkhd.getPosition(); int durationPosition = tkhd.getPosition();
int durationByteCount = version == 0 ? 4 : 8; int durationByteCount = version == 0 ? 4 : 8;
@ -298,7 +295,27 @@ import java.util.List;
duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); 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); 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) { String language) {
stsd.setPosition(Atom.FULL_HEADER_SIZE); stsd.setPosition(Atom.FULL_HEADER_SIZE);
int numberOfEntries = stsd.readInt(); int numberOfEntries = stsd.readInt();
StsdDataHolder holder = new StsdDataHolder(numberOfEntries); StsdData out = new StsdData(numberOfEntries);
for (int i = 0; i < numberOfEntries; i++) { for (int i = 0; i < numberOfEntries; i++) {
int childStartPosition = stsd.getPosition(); int childStartPosition = stsd.getPosition();
int childAtomSize = stsd.readInt(); int childAtomSize = stsd.readInt();
@ -347,25 +369,26 @@ import java.util.List;
|| childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v
|| childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1
|| childAtomType == Atom.TYPE_s263) { || 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 } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
|| childAtomType == Atom.TYPE_ac_3) { || childAtomType == Atom.TYPE_ac_3) {
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs, parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs,
holder, i); out, i);
} else if (childAtomType == Atom.TYPE_TTML) { } else if (childAtomType == Atom.TYPE_TTML) {
holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, language, out.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TTML, language,
durationUs); durationUs);
} else if (childAtomType == Atom.TYPE_tx3g) { } else if (childAtomType == Atom.TYPE_tx3g) {
holder.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, language, out.mediaFormat = MediaFormat.createTextFormat(MimeTypes.APPLICATION_TX3G, language,
durationUs); durationUs);
} }
stsd.setPosition(childStartPosition + childAtomSize); stsd.setPosition(childStartPosition + childAtomSize);
} }
return holder; return out;
} }
private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size, 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.setPosition(position + Atom.HEADER_SIZE);
parent.skipBytes(24); parent.skipBytes(24);
@ -428,7 +451,7 @@ import java.util.List;
} }
out.mediaFormat = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, durationUs, 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) { 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, 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.setPosition(position + Atom.HEADER_SIZE);
parent.skipBytes(16); parent.skipBytes(16);
int channelCount = parent.readUnsignedShort(); int channelCount = parent.readUnsignedShort();
@ -702,23 +725,43 @@ import java.util.List;
// Prevent instantiation. // 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. * 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 final TrackEncryptionBox[] trackEncryptionBoxes;
public MediaFormat mediaFormat; public MediaFormat mediaFormat;
public int nalUnitLengthFieldLength; public int nalUnitLengthFieldLength;
public StsdDataHolder(int numberOfEntries) { public StsdData(int numberOfEntries) {
trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
nalUnitLengthFieldLength = -1; nalUnitLengthFieldLength = -1;
} }
} }
/**
* Holds data parsed from an AvcC atom.
*/
private static final class AvcCData { private static final class AvcCData {
public final List<byte[]> initializationData; public final List<byte[]> initializationData;

View File

@ -211,7 +211,7 @@ import java.util.List;
// Construct and output the format. // Construct and output the format.
output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, 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)); parsedSpsData.pixelWidthAspectRatio, initializationData));
hasOutputFormat = true; hasOutputFormat = true;
} }

View File

@ -306,7 +306,7 @@ import java.util.Collections;
} }
output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H265, MediaFormat.NO_VALUE, 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))); Collections.singletonList(csd)));
hasOutputFormat = true; hasOutputFormat = true;
} }

View File

@ -1127,7 +1127,7 @@ public final class WebmExtractor implements Extractor {
sampleRate, initializationData); sampleRate, initializationData);
} else if (MimeTypes.isVideo(mimeType)) { } else if (MimeTypes.isVideo(mimeType)) {
return MediaFormat.createVideoFormat(mimeType, maxInputSize, durationUs, pixelWidth, return MediaFormat.createVideoFormat(mimeType, maxInputSize, durationUs, pixelWidth,
pixelHeight, initializationData); pixelHeight, 0, initializationData);
} else { } else {
throw new ParserException("Unexpected MIME type."); throw new ParserException("Unexpected MIME type.");
} }