diff --git a/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java index 440148a556..9d8bf0e43e 100644 --- a/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java @@ -15,9 +15,17 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; + /** * Contains information about a media decoder. */ +@TargetApi(16) public final class DecoderInfo { /** @@ -36,13 +44,120 @@ public final class DecoderInfo { */ public final boolean adaptive; + private final CodecCapabilities capabilities; + /** * @param name The name of the decoder. - * @param adaptive Whether the decoder is adaptive. */ - /* package */ DecoderInfo(String name, boolean adaptive) { + /* package */ DecoderInfo(String name) { this.name = name; - this.adaptive = adaptive; + this.adaptive = false; + this.capabilities = null; + } + + /** + * @param name The name of the decoder. + * @param capabilities The capabilities of the decoder. + */ + /* package */ DecoderInfo(String name, CodecCapabilities capabilities) { + this.name = name; + this.capabilities = capabilities; + adaptive = isAdaptive(capabilities); + } + + /** + * The profile levels supported by the decoder. + * + * @return The profile levels supported by the decoder. + */ + public CodecProfileLevel[] getProfileLevels() { + return capabilities == null || capabilities.profileLevels == null ? new CodecProfileLevel[0] + : capabilities.profileLevels; + } + + /** + * Whether the decoder supports video with a specified width and height. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @return Whether the decoder supports video with the given width and height. + */ + @TargetApi(21) + public boolean isVideoSizeSupportedV21(int width, int height) { + if (capabilities == null) { + return false; + } + MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + return videoCapabilities != null && videoCapabilities.isSizeSupported(width, height); + } + + /** + * Whether the decoder supports video with a given width, height and frame rate. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @param frameRate Frame rate in frames per second. + * @return Whether the decoder supports video with the given width, height and frame rate. + */ + @TargetApi(21) + public boolean isVideoSizeAndRateSupportedV21(int width, int height, double frameRate) { + if (capabilities == null) { + return false; + } + MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + return videoCapabilities != null && videoCapabilities.areSizeAndRateSupported(width, height, + frameRate); + } + + /** + * Whether the decoder supports audio with a given sample rate. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param sampleRate The sample rate in Hz. + * @return Whether the decoder supports audio with the given sample rate. + */ + @TargetApi(21) + public boolean isAudioSampleRateSupportedV21(int sampleRate) { + if (capabilities == null) { + return false; + } + MediaCodecInfo.AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); + return audioCapabilities != null && audioCapabilities.isSampleRateSupported(sampleRate); + } + + /** + * Whether the decoder supports audio with a given channel count. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param channelCount The channel count. + * @return Whether the decoder supports audio with the given channel count. + */ + @TargetApi(21) + public boolean isAudioChannelCountSupportedV21(int channelCount) { + if (capabilities == null) { + return false; + } + MediaCodecInfo.AudioCapabilities audioCapabilities = capabilities.getAudioCapabilities(); + return audioCapabilities != null && audioCapabilities.getMaxInputChannelCount() >= channelCount; + } + + private static boolean isAdaptive(CodecCapabilities capabilities) { + if (Util.SDK_INT >= 19) { + return isAdaptiveV19(capabilities); + } else { + return false; + } + } + + @TargetApi(19) + private static boolean isAdaptiveV19(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } } diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java index 311764f0ac..7b01c82d28 100644 --- a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java @@ -21,8 +21,8 @@ package com.google.android.exoplayer; public final class DummyTrackRenderer extends TrackRenderer { @Override - protected boolean handlesTrack(MediaFormat mediaFormat) throws ExoPlaybackException { - return false; + protected int supportsFormat(MediaFormat mediaFormat) throws ExoPlaybackException { + return TrackRenderer.FORMAT_UNSUPPORTED_TYPE; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 03c842a59d..546473427e 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -328,7 +328,7 @@ import java.util.concurrent.atomic.AtomicInteger; MediaFormat adaptiveTrackFormat = null; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { MediaFormat trackFormat = source.getTrackGroup(groupIndex).getFormat(trackIndex); - if (renderer.handlesTrack(trackFormat)) { + if (renderer.supportsFormat(trackFormat) == TrackRenderer.FORMAT_HANDLED) { adaptiveTrackIndices[adaptiveTrackIndexCount++] = trackIndex; if (adaptiveTrackFormat == null) { adaptiveTrackFormat = trackFormat.copyAsAdaptive("auto"); @@ -345,7 +345,7 @@ import java.util.concurrent.atomic.AtomicInteger; } for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { MediaFormat trackFormat = source.getTrackGroup(groupIndex).getFormat(trackIndex); - if (renderer.handlesTrack(trackFormat)) { + if (renderer.supportsFormat(trackFormat) == TrackRenderer.FORMAT_HANDLED) { rendererTrackGroups[rendererTrackCount] = groupIndex; rendererTrackIndices[rendererTrackCount] = new int[] {trackIndex}; rendererTrackFormats[rendererTrackCount++] = trackFormat; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index fbef69b660..44ecc38082 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.AudioManager; @@ -179,12 +180,34 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem } @Override - protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) throws DecoderQueryException { String mimeType = mediaFormat.mimeType; - return MimeTypes.isAudio(mimeType) && (MimeTypes.AUDIO_UNKNOWN.equals(mimeType) - || (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderName() != null) - || mediaCodecSelector.getDecoderInfo(mediaFormat, false) != null); + if (!MimeTypes.isAudio(mimeType)) { + return TrackRenderer.FORMAT_UNSUPPORTED_TYPE; + } + if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderName() != null) { + return TrackRenderer.FORMAT_HANDLED; + } + // TODO[REFACTOR]: Propagate requiresSecureDecoder to this point. Note that we need to check + // that the drm session can make use of a secure decoder, as well as that a secure decoder + // exists. + DecoderInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mediaFormat.mimeType, false); + if (decoderInfo == null) { + return TrackRenderer.FORMAT_UNSUPPORTED_TYPE; + } + if (Util.SDK_INT >= 21) { + // Note: We assume support in the case that the sampleRate or channelCount is unknown. + if (mediaFormat.sampleRate != MediaFormat.NO_VALUE + && !decoderInfo.isAudioSampleRateSupportedV21(mediaFormat.sampleRate)) { + return TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES; + } + if (mediaFormat.channelCount != MediaFormat.NO_VALUE + && !decoderInfo.isAudioChannelCountSupportedV21(mediaFormat.channelCount)) { + return TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES; + } + } + return TrackRenderer.FORMAT_HANDLED; } @Override @@ -194,7 +217,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem String passthroughDecoderName = mediaCodecSelector.getPassthroughDecoderName(); if (passthroughDecoderName != null) { passthroughEnabled = true; - return new DecoderInfo(passthroughDecoderName, false); + return new DecoderInfo(passthroughDecoderName); } } passthroughEnabled = false; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecSelector.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecSelector.java index 40378b40fd..e1f073c97e 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecSelector.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecSelector.java @@ -27,7 +27,7 @@ public interface MediaCodecSelector { /** * Default implementation of {@link MediaCodecSelector}. */ - public static final MediaCodecSelector DEFAULT = new MediaCodecSelector() { + MediaCodecSelector DEFAULT = new MediaCodecSelector() { /** * The name for the raw (passthrough) decoder OMX component. @@ -35,9 +35,9 @@ public interface MediaCodecSelector { private static final String RAW_DECODER_NAME = "OMX.google.raw.decoder"; @Override - public DecoderInfo getDecoderInfo(MediaFormat format, boolean requiresSecureDecoder) + public DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) throws DecoderQueryException { - return MediaCodecUtil.getDecoderInfo(format.mimeType, requiresSecureDecoder); + return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder); } @Override @@ -49,15 +49,15 @@ public interface MediaCodecSelector { }; /** - * Selects a decoder to instantiate for a given format. + * Selects a decoder to instantiate for a given mime type. * - * @param format The format for which a decoder is required. + * @param mimeType The mime type for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. * @return A {@link DecoderInfo} describing the decoder to instantiate, or null if no suitable * decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ - DecoderInfo getDecoderInfo(MediaFormat format, boolean requiresSecureDecoder) + DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) throws DecoderQueryException; /** diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 34279f1493..25aa68d309 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -262,23 +262,40 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer } @Override - protected final boolean handlesTrack(MediaFormat mediaFormat) throws ExoPlaybackException { + protected final int supportsAdaptive(String mimeType) throws ExoPlaybackException { + if (mimeType == null) { + return TrackRenderer.ADAPTIVE_NOT_SEAMLESS; + } try { - return handlesTrack(mediaCodecSelector, mediaFormat); + // TODO[REFACTOR]: Propagate requiresSecureDecoder to this point. + DecoderInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); + return decoderInfo != null && decoderInfo.adaptive ? TrackRenderer.ADAPTIVE_SEAMLESS + : TrackRenderer.ADAPTIVE_NOT_SEAMLESS; + } catch (DecoderQueryException e) { + throw new ExoPlaybackException(e); + } + } + + @Override + protected final int supportsFormat(MediaFormat mediaFormat) throws ExoPlaybackException { + try { + return supportsFormat(mediaCodecSelector, mediaFormat); } catch (DecoderQueryException e) { throw new ExoPlaybackException(e); } } /** - * Returns whether this renderer is capable of handling the provided track. + * Returns the extent to which the renderer is capable of rendering a given format. * * @param mediaCodecSelector The decoder selector. - * @param mediaFormat The format of the track. - * @return True if the renderer can handle the track, false otherwise. - * @throws DecoderQueryException Thrown if there was an error querying decoders. + * @param mediaFormat The format. + * @return The extent to which the renderer is capable of rendering the given format. One of + * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES} and + * {@link #FORMAT_UNSUPPORTED_TYPE}. + * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract boolean handlesTrack(MediaCodecSelector mediaCodecSelector, + protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) throws DecoderQueryException; /** @@ -293,7 +310,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer */ protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat, boolean requiresSecureDecoder) throws DecoderQueryException { - return mediaCodecSelector.getDecoderInfo(format, requiresSecureDecoder); + return mediaCodecSelector.getDecoderInfo(format.mimeType, requiresSecureDecoder); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 69efa88c22..8ad988447b 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; @@ -26,7 +25,6 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import java.util.HashMap; @@ -52,29 +50,12 @@ public final class MediaCodecUtil { private static final String TAG = "MediaCodecUtil"; - private static final HashMap> codecs = new HashMap<>(); + private static final HashMap codecs = new HashMap<>(); private MediaCodecUtil() {} /** - * Get information about the decoder that will be used for a given mime type. - * - * @param mimeType The mime type. - * @param secure Whether the decoder is required to support secure decryption. Always pass false - * unless secure decryption really is required. - * @return Information about the decoder that will be used, or null if no decoder exists. - */ - public static DecoderInfo getDecoderInfo(String mimeType, boolean secure) - throws DecoderQueryException { - Pair info = getMediaCodecInfo(mimeType, secure); - if (info == null) { - return null; - } - return new DecoderInfo(info.first, isAdaptive(info.second)); - } - - /** - * Optional call to warm the codec cache for a given mime type. + * Optional call to pre-populate cached decoder information for a given mime type. *

* Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}. * @@ -82,9 +63,9 @@ public final class MediaCodecUtil { * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. */ - public static synchronized void warmCodec(String mimeType, boolean secure) { + public static synchronized void warmDecoderInfoCache(String mimeType, boolean secure) { try { - getMediaCodecInfo(mimeType, secure); + getDecoderInfo(mimeType, secure); } catch (DecoderQueryException e) { // Codec warming is best effort, so we can swallow the exception. Log.e(TAG, "Codec warming failed", e); @@ -92,42 +73,41 @@ public final class MediaCodecUtil { } /** - * Returns the name of the best decoder and its capabilities for the given mimeType. + * Returns a {@link DecoderInfo} describing the most suitable decoder for a given mime type. * * @param mimeType The mime type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * @return The name of the best decoder and its capabilities for the given mimeType, or null if - * no decoder exists. + * @return Information about the decoder, or null if no suitable decoder exists. */ - public static synchronized Pair getMediaCodecInfo( - String mimeType, boolean secure) throws DecoderQueryException { + public static synchronized DecoderInfo getDecoderInfo(String mimeType, boolean secure) + throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); if (codecs.containsKey(key)) { return codecs.get(key); } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - Pair codecInfo = getMediaCodecInfo(key, mediaCodecList); - if (secure && codecInfo == null && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { + DecoderInfo info = getDecoderInfo(key, mediaCodecList); + if (secure && info == null && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. // TODO: Verify that the issue cannot occur on API levels 22 and 23, and tighten this block // to execute on API level 21 only if confirmed. mediaCodecList = new MediaCodecListCompatV16(); - codecInfo = getMediaCodecInfo(key, mediaCodecList); - if (codecInfo != null) { + info = getDecoderInfo(key, mediaCodecList); + if (info != null) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType - + ". Assuming: " + codecInfo.first); + + ". Assuming: " + info.name); } } - return codecInfo; + return info; } - private static Pair getMediaCodecInfo(CodecKey key, - MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + private static DecoderInfo getDecoderInfo(CodecKey key, MediaCodecListCompat mediaCodecList) + throws DecoderQueryException { try { - return getMediaCodecInfoInternal(key, mediaCodecList); + return getDecoderInfoInternal(key, mediaCodecList); } catch (Exception e) { // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException // or an IllegalArgumentException here. @@ -135,7 +115,7 @@ public final class MediaCodecUtil { } } - private static Pair getMediaCodecInfoInternal(CodecKey key, + private static DecoderInfo getDecoderInfoInternal(CodecKey key, MediaCodecListCompat mediaCodecList) { String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); @@ -154,16 +134,16 @@ public final class MediaCodecUtil { if (!secureDecodersExplicit) { // Cache variants for both insecure and (if we think it's supported) secure playback. codecs.put(key.secure ? new CodecKey(mimeType, false) : key, - Pair.create(codecName, capabilities)); + new DecoderInfo(codecName, capabilities)); if (secure) { codecs.put(key.secure ? key : new CodecKey(mimeType, true), - Pair.create(codecName + ".secure", capabilities)); + new DecoderInfo(codecName + ".secure", capabilities)); } } else { // Only cache this variant. If both insecure and secure decoders are available, they // should both be listed separately. codecs.put(key.secure == secure ? key : new CodecKey(mimeType, secure), - Pair.create(codecName, capabilities)); + new DecoderInfo(codecName, capabilities)); } if (codecs.containsKey(key)) { return codecs.get(key); @@ -233,79 +213,23 @@ public final class MediaCodecUtil { return true; } - private static boolean isAdaptive(CodecCapabilities capabilities) { - if (Util.SDK_INT >= 19) { - return isAdaptiveV19(capabilities); - } else { - return false; - } - } - - @TargetApi(19) - private static boolean isAdaptiveV19(CodecCapabilities capabilities) { - return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); - } - /** - * Tests whether the device advertises it can decode video of a given type at a specified width - * and height. - *

- * Must not be called if the device SDK version is less than 21. + * Whether the default H264 decoder supports the specified profile at the specified level. * - * @param mimeType The mime type. - * @param secure Whether the decoder is required to support secure decryption. Always pass false - * unless secure decryption really is required. - * @param width Width in pixels. - * @param height Height in pixels. - * @return Whether the decoder advertises support of the given size. - */ - @TargetApi(21) - public static boolean isSizeSupportedV21(String mimeType, boolean secure, int width, - int height) throws DecoderQueryException { - Assertions.checkState(Util.SDK_INT >= 21); - MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure); - return videoCapabilities != null && videoCapabilities.isSizeSupported(width, height); - } - - /** - * Tests whether the device advertises it can decode video of a given type at a specified - * width, height, and frame rate. - *

- * Must not be called if the device SDK version is less than 21. - * - * @param mimeType The mime type. - * @param secure Whether the decoder is required to support secure decryption. Always pass false - * unless secure decryption really is required. - * @param width Width in pixels. - * @param height Height in pixels. - * @param frameRate Frame rate in frames per second. - * @return Whether the decoder advertises support of the given size and frame rate. - */ - @TargetApi(21) - public static boolean isSizeAndRateSupportedV21(String mimeType, boolean secure, - int width, int height, double frameRate) throws DecoderQueryException { - Assertions.checkState(Util.SDK_INT >= 21); - MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure); - return videoCapabilities != null - && videoCapabilities.areSizeAndRateSupported(width, height, frameRate); - } - - /** - * @param profile An AVC profile constant from {@link CodecProfileLevel}. - * @param level An AVC profile level from {@link CodecProfileLevel}. + * @param profile A profile constant from {@link CodecProfileLevel}. + * @param level A profile level from {@link CodecProfileLevel}. * @return Whether the specified profile is supported at the specified level. */ public static boolean isH264ProfileSupported(int profile, int level) throws DecoderQueryException { - Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false); + DecoderInfo info = getDecoderInfo(MimeTypes.VIDEO_H264, false); if (info == null) { return false; } - CodecCapabilities capabilities = info.second; - for (int i = 0; i < capabilities.profileLevels.length; i++) { - CodecProfileLevel profileLevel = capabilities.profileLevels[i]; - if (profileLevel.profile == profile && profileLevel.level >= level) { + CodecProfileLevel[] profileLevels = info.getProfileLevels(); + for (int i = 0; i < profileLevels.length; i++) { + if (profileLevels[i].profile == profile && profileLevels[i].level >= level) { return true; } } @@ -314,35 +238,27 @@ public final class MediaCodecUtil { } /** - * @return the maximum frame size for an H264 stream that can be decoded on the device. + * Returns the maximum frame size supported by the default H264 decoder. + * + * @return The maximum frame size for an H264 stream that can be decoded on the device. */ public static int maxH264DecodableFrameSize() throws DecoderQueryException { - Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false); + DecoderInfo info = getDecoderInfo(MimeTypes.VIDEO_H264, false); if (info == null) { return 0; } int maxH264DecodableFrameSize = 0; - CodecCapabilities capabilities = info.second; - for (int i = 0; i < capabilities.profileLevels.length; i++) { - CodecProfileLevel profileLevel = capabilities.profileLevels[i]; - maxH264DecodableFrameSize = Math.max( - avcLevelToMaxFrameSize(profileLevel.level), maxH264DecodableFrameSize); + CodecProfileLevel[] profileLevels = info.getProfileLevels(); + for (int i = 0; i < profileLevels.length; i++) { + CodecProfileLevel profileLevel = profileLevels[i]; + maxH264DecodableFrameSize = Math.max(avcLevelToMaxFrameSize(profileLevel.level), + maxH264DecodableFrameSize); } return maxH264DecodableFrameSize; } - @TargetApi(21) - private static MediaCodecInfo.VideoCapabilities getVideoCapabilitiesV21(String mimeType, - boolean secure) throws DecoderQueryException { - Pair info = getMediaCodecInfo(mimeType, secure); - if (info == null) { - return null; - } - return info.second.getVideoCapabilities(); - } - /** * Conversion values taken from: https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC. * 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 dc976f57fa..0a4e2c4be4 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -213,11 +213,38 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) throws DecoderQueryException { String mimeType = mediaFormat.mimeType; - return MimeTypes.isVideo(mimeType) && (MimeTypes.VIDEO_UNKNOWN.equals(mimeType) - || mediaCodecSelector.getDecoderInfo(mediaFormat, false) != null); + if (!MimeTypes.isVideo(mimeType)) { + return TrackRenderer.FORMAT_UNSUPPORTED_TYPE; + } + // TODO[REFACTOR]: Propagate requiresSecureDecoder to this point. Note that we need to check + // that the drm session can make use of a secure decoder, as well as that a secure decoder + // exists. + DecoderInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mediaFormat.mimeType, false); + if (decoderInfo == null) { + return TrackRenderer.FORMAT_UNSUPPORTED_TYPE; + } + if (mediaFormat.width > 0 && mediaFormat.height > 0) { + if (Util.SDK_INT >= 21) { + // TODO[REFACTOR]: Propagate frame rate to this point. + // if (mediaFormat.frameRate > 0) { + // return decoderInfo.isSizeAndRateSupportedV21(mediaFormat.width, mediaFormat.height, + // mediaFormat.frameRate) ? TrackRenderer.TRACK_HANDLED + // : TrackRenderer.TRACK_NOT_HANDLED_EXCEEDS_CAPABILITIES; + // } else { + return decoderInfo.isVideoSizeSupportedV21(mediaFormat.width, mediaFormat.height) + ? TrackRenderer.FORMAT_HANDLED : TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES; + // } + } + // TODO[REFACTOR]: We should probably assume that we can decode at least the resolution of + // the display, or the camera, as a sanity check? + if (mediaFormat.width * mediaFormat.height > MediaCodecUtil.maxH264DecodableFrameSize()) { + return TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES; + } + } + return TrackRenderer.FORMAT_HANDLED; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/TrackGroup.java b/library/src/main/java/com/google/android/exoplayer/TrackGroup.java index b06004f063..d1083c255b 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackGroup.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackGroup.java @@ -46,11 +46,11 @@ public final class TrackGroup { } /** - * @param supportsAdaptive Whether it's possible to adapt between multiple tracks in the group. + * @param adaptive Whether it's possible to adapt between multiple tracks in the group. * @param formats The track formats. */ - public TrackGroup(boolean supportsAdaptive, MediaFormat... formats) { - this.adaptive = supportsAdaptive; + public TrackGroup(boolean adaptive, MediaFormat... formats) { + this.adaptive = adaptive; this.formats = formats; length = formats.length; } diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index 72ebb26ee2..ed828b7b53 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.SampleSource.TrackStream; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; import java.io.IOException; @@ -34,6 +35,42 @@ import java.io.IOException; */ public abstract class TrackRenderer implements ExoPlayerComponent { + /** + * The {@link TrackRenderer} can seamlessly adapt between formats. + */ + public static final int ADAPTIVE_SEAMLESS = 0; + /** + * The {@link TrackRenderer} can adapt between formats, but may suffer a brief discontinuity + * (~50-100ms) when adaptation occurs. + */ + public static final int ADAPTIVE_NOT_SEAMLESS = 1; + /** + * The {@link TrackRenderer} does not support adaptation between formats. + */ + public static final int ADAPTIVE_NOT_SUPPORTED = 2; + + /** + * The {@link TrackRenderer} is capable of rendering the format. + */ + public static final int FORMAT_HANDLED = 0; + /** + * The {@link TrackRenderer} is capable of rendering formats with the same mimeType, but the + * properties of the format exceed the renderer's capability. + *

+ * Example: The {@link TrackRenderer} is capable of rendering H264 and the format's mimeType is + * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported + * by the underlying H264 decoder. + */ + public static final int FORMAT_EXCEEDS_CAPABILITIES = 1; + /** + * The {@link TrackRenderer} is not capable of rendering the format, or any other format with the + * same mimeType. + *

+ * Example: The {@link TrackRenderer} is only capable of rendering video and the track has an + * audio mimeType. + */ + public static final int FORMAT_UNSUPPORTED_TYPE = 2; + /** * The renderer is idle. */ @@ -75,13 +112,31 @@ public abstract class TrackRenderer implements ExoPlayerComponent { } /** - * Returns whether this renderer is capable of handling the provided track. + * Returns the extent to which the renderer is capable of consuming from a {@link TrackStream} + * that adapts between multiple supported formats of a given mimeType, or of differing mimeTypes + * if {@code mimeType == null} is passed. * - * @param mediaFormat The format of the track. - * @return True if the renderer can handle the track, false otherwise. + * @param mimeType The mimeType, or null to query the extent to which the renderer is capable of + * adapting between formats with different mimeTypes. + * @return The extent to which the renderer supports the adaptation. One of + * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and + * {@link #ADAPTIVE_NOT_SUPPORTED}. * @throws ExoPlaybackException If an error occurs. */ - protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws ExoPlaybackException; + protected int supportsAdaptive(String mimeType) throws ExoPlaybackException { + return ADAPTIVE_NOT_SUPPORTED; + } + + /** + * Returns the extent to which the renderer is capable of rendering a given format. + * + * @param mediaFormat The format. + * @return The extent to which the renderer is capable of rendering the given format. One of + * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES} and + * {@link #FORMAT_UNSUPPORTED_TYPE}. + * @throws ExoPlaybackException If an error occurs. + */ + protected abstract int supportsFormat(MediaFormat mediaFormat) throws ExoPlaybackException; /** * Enable the renderer to consume from the specified {@link TrackStream}. diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java b/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java deleted file mode 100644 index 84717b9c79..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.chunk; - -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.Util; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.Point; -import android.view.Display; -import android.view.WindowManager; - -import java.util.ArrayList; -import java.util.List; - -/** - * Selects from possible video formats. - */ -public final class VideoFormatSelectorUtil { - - /** - * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the - * corresponding viewport dimension, then the video is considered as filling the viewport (in that - * dimension). - */ - private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f; - - /** - * Chooses a suitable subset from a number of video formats, to be rendered on the device's - * default display. - * - * @param context A context. - * @param formatWrappers Wrapped formats from which to select. - * @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all - * mime types. - * @param filterHdFormats True to filter HD formats. False otherwise. - * @return An array holding the indices of the selected formats. - * @throws DecoderQueryException Thrown if there was an error querying decoders. - */ - public static int[] selectVideoFormatsForDefaultDisplay(Context context, - List formatWrappers, String[] allowedContainerMimeTypes, - boolean filterHdFormats) throws DecoderQueryException { - Point viewportSize = getViewportSize(context); - return selectVideoFormats(formatWrappers, allowedContainerMimeTypes, filterHdFormats, true, - viewportSize.x, viewportSize.y); - } - - /** - * Chooses a suitable subset from a number of video formats. - *

- * A format is filtered (i.e. not selected) if: - *

- * - * @param formatWrappers Wrapped formats from which to select. - * @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all - * mime types. - * @param filterHdFormats True to filter HD formats. False otherwise. - * @param orientationMayChange True if the video's orientation may change with respect to the - * viewport during playback. - * @param viewportWidth The width in pixels of the viewport within which the video will be - * displayed. If the viewport size may change, this should be set to the maximum possible - * width. -1 if selection should not be constrained by a viewport. - * @param viewportHeight The height in pixels of the viewport within which the video will be - * displayed. If the viewport size may change, this should be set to the maximum possible - * height. -1 if selection should not be constrained by a viewport. - * @return An array holding the indices of the selected formats. - * @throws DecoderQueryException - */ - public static int[] selectVideoFormats(List formatWrappers, - String[] allowedContainerMimeTypes, boolean filterHdFormats, boolean orientationMayChange, - int viewportWidth, int viewportHeight) throws DecoderQueryException { - int maxVideoPixelsToRetain = Integer.MAX_VALUE; - ArrayList selectedIndexList = new ArrayList<>(); - int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize(); - - // First pass to filter out formats that individually fail to meet the selection criteria. - int formatWrapperCount = formatWrappers.size(); - for (int i = 0; i < formatWrapperCount; i++) { - Format format = formatWrappers.get(i).getFormat(); - if (isFormatPlayable(format, allowedContainerMimeTypes, filterHdFormats, - maxDecodableFrameSize)) { - // Select the format for now. It may still be filtered in the second pass below. - selectedIndexList.add(i); - // Keep track of the number of pixels of the selected format whose resolution is the - // smallest to exceed the maximum size at which it can be displayed within the viewport. - // We'll discard formats of higher resolution in a second pass. - if (format.width > 0 && format.height > 0 && viewportWidth > 0 && viewportHeight > 0) { - Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange, - viewportWidth, viewportHeight, format.width, format.height); - int videoPixels = format.width * format.height; - if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN) - && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN) - && videoPixels < maxVideoPixelsToRetain) { - maxVideoPixelsToRetain = videoPixels; - } - } - } - } - - // Second pass to filter out formats that exceed maxVideoPixelsToRetain. These formats are have - // unnecessarily high resolution given the size at which the video will be displayed within the - // viewport. - if (maxVideoPixelsToRetain != Integer.MAX_VALUE) { - for (int i = selectedIndexList.size() - 1; i >= 0; i--) { - Format format = formatWrappers.get(selectedIndexList.get(i)).getFormat(); - if (format.width > 0 && format.height > 0 - && format.width * format.height > maxVideoPixelsToRetain) { - selectedIndexList.remove(i); - } - } - } - - return Util.toArray(selectedIndexList); - } - - /** - * Determines whether an individual format is playable, given an array of allowed container types, - * whether HD formats should be filtered and a maximum decodable frame size in pixels. - */ - private static boolean isFormatPlayable(Format format, String[] allowedContainerMimeTypes, - boolean filterHdFormats, int maxDecodableFrameSize) throws DecoderQueryException { - if (allowedContainerMimeTypes != null - && !Util.contains(allowedContainerMimeTypes, format.mimeType)) { - // Filtering format based on its container mime type. - return false; - } - if (filterHdFormats && (format.width >= 1280 || format.height >= 720)) { - // Filtering format because it's HD. - return false; - } - if (format.width > 0 && format.height > 0) { - String videoMediaMimeType = MimeTypes.getVideoMediaMimeType(format.codecs); - if (Util.SDK_INT >= 21 && !MimeTypes.VIDEO_UNKNOWN.equals(videoMediaMimeType)) { - if (format.frameRate > 0) { - return MediaCodecUtil.isSizeAndRateSupportedV21(videoMediaMimeType, false, format.width, - format.height, format.frameRate); - } else { - return MediaCodecUtil.isSizeSupportedV21(videoMediaMimeType, false, format.width, - format.height); - } - } - //Assuming that the media is H.264 - if (format.width * format.height > maxDecodableFrameSize) { - // Filtering stream that device cannot play - return false; - } - } - return true; - } - - /** - * Given viewport dimensions and video dimensions, computes the maximum size of the video as it - * will be rendered to fit inside of the viewport. - */ - private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth, - int viewportHeight, int videoWidth, int videoHeight) { - if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) { - // Rotation is allowed, and the video will be larger in the rotated viewport. - int tempViewportWidth = viewportWidth; - viewportWidth = viewportHeight; - viewportHeight = tempViewportWidth; - } - - if (videoWidth * viewportHeight >= videoHeight * viewportWidth) { - // Horizontal letter-boxing along top and bottom. - return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth)); - } else { - // Vertical letter-boxing along edges. - return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight); - } - } - - private static Point getViewportSize(Context context) { - // Before API 23 the platform Display object does not provide a way to identify Android TVs that - // can show 4k resolution in a SurfaceView, so check for supported devices here. - // See also https://developer.sony.com/develop/tvs/android-tv/design-guide/. - if (Util.SDK_INT < 23 && Util.MODEL != null && Util.MODEL.startsWith("BRAVIA") - && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { - return new Point(3840, 2160); - } - - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return getDisplaySize(windowManager.getDefaultDisplay()); - } - - private static Point getDisplaySize(Display display) { - Point displaySize = new Point(); - if (Util.SDK_INT >= 23) { - getDisplaySizeV23(display, displaySize); - } else if (Util.SDK_INT >= 17) { - getDisplaySizeV17(display, displaySize); - } else if (Util.SDK_INT >= 16) { - getDisplaySizeV16(display, displaySize); - } else { - getDisplaySizeV9(display, displaySize); - } - return displaySize; - } - - @TargetApi(23) - private static void getDisplaySizeV23(Display display, Point outSize) { - Display.Mode mode = display.getMode(); - outSize.x = mode.getPhysicalWidth(); - outSize.y = mode.getPhysicalHeight(); - } - - @TargetApi(17) - private static void getDisplaySizeV17(Display display, Point outSize) { - display.getRealSize(outSize); - } - - @TargetApi(16) - private static void getDisplaySizeV16(Display display, Point outSize) { - display.getSize(outSize); - } - - @SuppressWarnings("deprecation") - private static void getDisplaySizeV9(Display display, Point outSize) { - outSize.x = display.getWidth(); - outSize.y = display.getHeight(); - } - - private VideoFormatSelectorUtil() {} - -} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java index 0337a76ca7..c3ca46d5b6 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataTrackRenderer.java @@ -86,8 +86,9 @@ public final class MetadataTrackRenderer extends SampleSourceTrackRenderer im } @Override - protected boolean handlesTrack(MediaFormat mediaFormat) { - return metadataParser.canParse(mediaFormat.mimeType); + protected int supportsFormat(MediaFormat mediaFormat) { + return metadataParser.canParse(mediaFormat.mimeType) ? TrackRenderer.FORMAT_HANDLED + : TrackRenderer.FORMAT_UNSUPPORTED_TYPE; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 48296d2c88..a361ba409a 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -155,8 +155,9 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement } @Override - protected boolean handlesTrack(MediaFormat mediaFormat) { - return getParserIndex(mediaFormat) != -1; + protected int supportsFormat(MediaFormat mediaFormat) { + return getParserIndex(mediaFormat) != -1 ? TrackRenderer.FORMAT_HANDLED + : TrackRenderer.FORMAT_UNSUPPORTED_TYPE; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java index ae871d62c7..ca1421dfc1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -86,8 +86,9 @@ public final class Eia608TrackRenderer extends SampleSourceTrackRenderer impleme } @Override - protected boolean handlesTrack(MediaFormat mediaFormat) { - return eia608Parser.canParse(mediaFormat.mimeType); + protected int supportsFormat(MediaFormat mediaFormat) { + return eia608Parser.canParse(mediaFormat.mimeType) ? TrackRenderer.FORMAT_HANDLED + : TrackRenderer.FORMAT_UNSUPPORTED_TYPE; } @Override