Support anamorphic video content.

This commit is contained in:
Oliver Woodman 2014-09-11 16:34:35 +01:00
parent 6c3ae7f175
commit ec90eac301
9 changed files with 95 additions and 32 deletions

View File

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

View File

@ -260,9 +260,10 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
} }
@Override @Override
public void onVideoSizeChanged(int width, int height) { public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) {
shutterView.setVisibility(View.GONE); shutterView.setVisibility(View.GONE);
surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); surfaceView.setVideoWidthHeightRatio(
height == 0 ? 1 : (width * pixelWidthAspectRatio) / height);
} }
// User controls // User controls

View File

@ -93,7 +93,7 @@ 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); void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
} }
/** /**
@ -377,9 +377,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onVideoSizeChanged(int width, int height) { public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onVideoSizeChanged(width, height); listener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
} }
} }

View File

@ -231,8 +231,9 @@ public class SimplePlayerActivity extends Activity implements SurfaceHolder.Call
// MediaCodecVideoTrackRenderer.Listener // MediaCodecVideoTrackRenderer.Listener
@Override @Override
public void onVideoSizeChanged(int width, int height) { public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height); surfaceView.setVideoWidthHeightRatio(
height == 0 ? 1 : (pixelWidthHeightRatio * width) / height);
} }
@Override @Override

View File

@ -79,18 +79,19 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
} }
/** /**
* Value of {@link #sourceState} when the source is not ready. * Value returned by {@link #getSourceState()} when the source is not ready.
*/ */
protected static final int SOURCE_STATE_NOT_READY = 0; protected static final int SOURCE_STATE_NOT_READY = 0;
/** /**
* Value of {@link #sourceState} when the source is ready and we're able to read from it. * Value returned by {@link #getSourceState()} when the source is ready and we're able to read
* from it.
*/ */
protected static final int SOURCE_STATE_READY = 1; protected static final int SOURCE_STATE_READY = 1;
/** /**
* Value of {@link #sourceState} when the source is ready but we might not be able to read from * Value returned by {@link #getSourceState()} when the source is ready but we might not be able
* it. We transition to this state when an attempt to read a sample fails despite the source * to read from it. We transition to this state when an attempt to read a sample fails despite the
* reporting that samples are available. This can occur when the next sample to be provided by * source reporting that samples are available. This can occur when the next sample to be provided
* the source is for another renderer. * by the source is for another renderer.
*/ */
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2; protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
@ -620,7 +621,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @param formatHolder Holds the new format. * @param formatHolder Holds the new format.
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}. * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
*/ */
private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
MediaFormat oldFormat = format; MediaFormat oldFormat = format;
format = formatHolder.format; format = formatHolder.format;
drmInitData = formatHolder.drmInitData; drmInitData = formatHolder.drmInitData;

View File

@ -58,8 +58,11 @@ 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 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); void onVideoSizeChanged(int width, int height, 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
@ -98,8 +101,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
private int currentWidth; private int currentWidth;
private int currentHeight; private int currentHeight;
private float currentPixelWidthHeightRatio;
private int lastReportedWidth; private int lastReportedWidth;
private int lastReportedHeight; private int lastReportedHeight;
private float lastReportedPixelWidthHeightRatio;
/** /**
* @param source The upstream source from which the renderer obtains samples. * @param source The upstream source from which the renderer obtains samples.
@ -208,8 +213,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
joiningDeadlineUs = -1; joiningDeadlineUs = -1;
currentWidth = -1; currentWidth = -1;
currentHeight = -1; currentHeight = -1;
currentPixelWidthHeightRatio = -1;
lastReportedWidth = -1; lastReportedWidth = -1;
lastReportedHeight = -1; lastReportedHeight = -1;
lastReportedPixelWidthHeightRatio = -1;
} }
@Override @Override
@ -272,8 +279,10 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
super.onDisabled(); super.onDisabled();
currentWidth = -1; currentWidth = -1;
currentHeight = -1; currentHeight = -1;
currentPixelWidthHeightRatio = -1;
lastReportedWidth = -1; lastReportedWidth = -1;
lastReportedHeight = -1; lastReportedHeight = -1;
lastReportedPixelWidthHeightRatio = -1;
} }
@Override @Override
@ -315,6 +324,14 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
codec.setVideoScalingMode(videoScalingMode); codec.setVideoScalingMode(videoScalingMode);
} }
@Override
protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException {
super.onInputFormatChanged(holder);
// TODO: Ideally this would be read in onOutputFormatChanged, but there doesn't seem
// to be a way to pass a custom key/value pair value through to the output format.
currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio;
}
@Override @Override
protected void onOutputFormatChanged(android.media.MediaFormat format) { protected void onOutputFormatChanged(android.media.MediaFormat format) {
boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
@ -394,10 +411,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight) { if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight
|| lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) {
lastReportedWidth = currentWidth; lastReportedWidth = currentWidth;
lastReportedHeight = currentHeight; lastReportedHeight = currentHeight;
notifyVideoSizeChanged(currentWidth, currentHeight); lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
notifyVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio);
} }
TraceUtil.beginSection("renderVideoBuffer"); TraceUtil.beginSection("renderVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, true); codec.releaseOutputBuffer(bufferIndex, true);
@ -409,12 +428,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
} }
} }
private void notifyVideoSizeChanged(final int width, final int height) { private void notifyVideoSizeChanged(final int width, final int height,
final float pixelWidthHeightRatio) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onVideoSizeChanged(width, height); eventListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio);
} }
}); });
} }

View File

@ -31,6 +31,9 @@ import java.util.List;
*/ */
public class MediaFormat { public class MediaFormat {
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
public static final int NO_VALUE = -1; public static final int NO_VALUE = -1;
public final String mimeType; public final String mimeType;
@ -38,6 +41,7 @@ public class MediaFormat {
public final int width; public final int width;
public final int height; public final int height;
public final float pixelWidthHeightRatio;
public final int channelCount; public final int channelCount;
public final int sampleRate; public final int sampleRate;
@ -59,14 +63,19 @@ public 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 new MediaFormat(mimeType, maxInputSize, width, height, NO_VALUE, NO_VALUE, return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
initializationData); }
public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
NO_VALUE, initializationData);
} }
public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount, public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
int sampleRate, List<byte[]> initializationData) { int sampleRate, List<byte[]> initializationData) {
return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, channelCount, sampleRate, return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
initializationData); sampleRate, initializationData);
} }
@TargetApi(16) @TargetApi(16)
@ -78,6 +87,7 @@ public class MediaFormat {
height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
pixelWidthHeightRatio = getOptionalFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
initializationData = new ArrayList<byte[]>(); initializationData = new ArrayList<byte[]>();
for (int i = 0; format.containsKey("csd-" + i); i++) { for (int i = 0; format.containsKey("csd-" + i); i++) {
ByteBuffer buffer = format.getByteBuffer("csd-" + i); ByteBuffer buffer = format.getByteBuffer("csd-" + i);
@ -90,12 +100,14 @@ public class MediaFormat {
maxHeight = NO_VALUE; maxHeight = NO_VALUE;
} }
private MediaFormat(String mimeType, int maxInputSize, int width, int height, int channelCount, private MediaFormat(String mimeType, int maxInputSize, int width, int height,
int sampleRate, List<byte[]> initializationData) { float pixelWidthHeightRatio, int channelCount, int sampleRate,
List<byte[]> initializationData) {
this.mimeType = mimeType; this.mimeType = mimeType;
this.maxInputSize = maxInputSize; this.maxInputSize = maxInputSize;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.channelCount = channelCount; this.channelCount = channelCount;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList() this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
@ -128,6 +140,7 @@ public 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 + Float.floatToRawIntBits(pixelWidthHeightRatio);
result = 31 * result + maxWidth; result = 31 * result + maxWidth;
result = 31 * result + maxHeight; result = 31 * result + maxHeight;
result = 31 * result + channelCount; result = 31 * result + channelCount;
@ -163,6 +176,7 @@ public 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
|| 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
|| !Util.areEqual(mimeType, other.mimeType) || !Util.areEqual(mimeType, other.mimeType)
@ -179,8 +193,9 @@ public class MediaFormat {
@Override @Override
public String toString() { public String toString() {
return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " + return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
channelCount + ", " + sampleRate + ", " + maxWidth + ", " + maxHeight + ")"; + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + maxWidth + ", "
+ maxHeight + ")";
} }
/** /**
@ -196,6 +211,7 @@ public class MediaFormat {
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate); maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
} }
@ -221,9 +237,21 @@ public class MediaFormat {
} }
@TargetApi(16) @TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format, private static final void maybeSetFloatV16(android.media.MediaFormat format, String key,
String key) { float value) {
if (value != NO_VALUE) {
format.setFloat(key, value);
}
}
@TargetApi(16)
private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : NO_VALUE; return format.containsKey(key) ? format.getInteger(key) : NO_VALUE;
} }
@TargetApi(16)
private static final float getOptionalFloatV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getFloat(key) : NO_VALUE;
}
} }

View File

@ -53,6 +53,7 @@ import java.util.ArrayList;
public static final int TYPE_saiz = 0x7361697A; public static final int TYPE_saiz = 0x7361697A;
public static final int TYPE_uuid = 0x75756964; public static final int TYPE_uuid = 0x75756964;
public static final int TYPE_senc = 0x73656E63; public static final int TYPE_senc = 0x73656E63;
public static final int TYPE_pasp = 0x70617370;
public final int type; public final int type;

View File

@ -106,6 +106,7 @@ public final class FragmentedMp4Extractor implements Extractor {
parsedAtoms.add(Atom.TYPE_saiz); parsedAtoms.add(Atom.TYPE_saiz);
parsedAtoms.add(Atom.TYPE_uuid); parsedAtoms.add(Atom.TYPE_uuid);
parsedAtoms.add(Atom.TYPE_senc); parsedAtoms.add(Atom.TYPE_senc);
parsedAtoms.add(Atom.TYPE_pasp);
PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms); PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
} }
@ -529,6 +530,7 @@ public final class FragmentedMp4Extractor implements Extractor {
parent.skip(24); parent.skip(24);
int width = parent.readUnsignedShort(); int width = parent.readUnsignedShort();
int height = parent.readUnsignedShort(); int height = parent.readUnsignedShort();
float pixelWidthHeightRatio = 1;
parent.skip(50); parent.skip(50);
List<byte[]> initializationData = null; List<byte[]> initializationData = null;
@ -543,12 +545,14 @@ public final class FragmentedMp4Extractor implements Extractor {
initializationData = parseAvcCFromParent(parent, childStartPosition); initializationData = parseAvcCFromParent(parent, childStartPosition);
} else if (childAtomType == Atom.TYPE_sinf) { } else if (childAtomType == Atom.TYPE_sinf) {
trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize); trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize);
} else if (childAtomType == Atom.TYPE_pasp) {
pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition);
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
width, height, initializationData); width, height, pixelWidthHeightRatio, initializationData);
return Pair.create(format, trackEncryptionBox); return Pair.create(format, trackEncryptionBox);
} }
@ -643,6 +647,13 @@ public final class FragmentedMp4Extractor implements Extractor {
return trackEncryptionBox; return trackEncryptionBox;
} }
private static float parsePaspFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + ATOM_HEADER_SIZE);
int hSpacing = parent.readUnsignedIntToInt();
int vSpacing = parent.readUnsignedIntToInt();
return (float) hSpacing / vSpacing;
}
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
int size) { int size) {
int childPosition = position + ATOM_HEADER_SIZE; int childPosition = position + ATOM_HEADER_SIZE;