Commit 0b631b05 by andrewlewis Committed by Oliver Woodman

Optionally fall back to other decoders if init fails

Codec initialization may fail in creation, configuration or when start()ing the
codec. If codec initialization fails, there may be other codecs available that
could handle the same format, but currently ExoPlayer can only try to use the
first listed codec for the input format and gives up if it fails to initialize.

This change implements support for optionally falling back to alternative
decoders if initialization fails. MediaCodecSelector can now return a list of
decoders to try in priority order, and use the Format when choosing a codec.
With the default implementation, the codecs and order come from MediaCodecList,
and matches the order used internally by MediaCodec.createDecoderByType (which
implements the same kind of fallback though only to the creation step, without
configuring/starting the codec).

This feature is useful for apps that want to play several videos concurrently on
devices that have software decoders (like OMX.google.h264.decoder), as the new
behavior allows new codecs to be created when no hardware-accelerated decoders
are available.

The list of available codecs is queried when initializing the codec after a
format change that requires a new codec to be instantiated. When a decoder fails
to initialize it is removed from the list of available decoders and won't be
tried again until the next format change (or until the renderer is disabled).

Note: this change does not affect the renderer capabilities API, as when
checking format support we don't know which codec will be used.

Issue: #273

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=203242285
parent 93083362
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
`AnalyticsListener` callbacks. This uri is the redirected uri if redirection `AnalyticsListener` callbacks. This uri is the redirected uri if redirection
occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)). occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).
* Improved compatibility with FireOS devices. * Improved compatibility with FireOS devices.
* Allow `MediaCodecSelector`s to return multiple compatible decoders for
`MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that
falls back to less preferred decoders like `MediaCodec.createDecoderByType`
([#273](https://github.com/google/ExoPlayer/issues/273)).
### 2.8.2 ### ### 2.8.2 ###
......
...@@ -45,6 +45,8 @@ import com.google.android.exoplayer2.util.MediaClock; ...@@ -45,6 +45,8 @@ import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
/** /**
* Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}. * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}.
...@@ -262,15 +264,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -262,15 +264,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;
} }
} }
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, List<MediaCodecInfo> decoderInfos =
requiresSecureDecryption); mediaCodecSelector.getDecoderInfos(format, requiresSecureDecryption);
if (decoderInfo == null) { if (decoderInfos.isEmpty()) {
return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null return requiresSecureDecryption
? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; && !mediaCodecSelector
.getDecoderInfos(format, /* requiresSecureDecoder= */ false)
.isEmpty()
? FORMAT_UNSUPPORTED_DRM
: FORMAT_UNSUPPORTED_SUBTYPE;
} }
if (!supportsFormatDrm) { if (!supportsFormatDrm) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} }
// Check capabilities for the first decoder in the list, which takes priority.
MediaCodecInfo decoderInfo = decoderInfos.get(0);
// Note: We assume support for unknown sampleRate and channelCount. // Note: We assume support for unknown sampleRate and channelCount.
boolean decoderCapable = Util.SDK_INT < 21 boolean decoderCapable = Util.SDK_INT < 21
|| ((format.sampleRate == Format.NO_VALUE || ((format.sampleRate == Format.NO_VALUE
...@@ -282,15 +290,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -282,15 +290,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
@Override @Override
protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, protected List<MediaCodecInfo> getDecoderInfos(
Format format, boolean requiresSecureDecoder) throws DecoderQueryException { MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
throws DecoderQueryException {
if (allowPassthrough(format.sampleMimeType)) { if (allowPassthrough(format.sampleMimeType)) {
MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) { if (passthroughDecoderInfo != null) {
return passthroughDecoderInfo; return Collections.singletonList(passthroughDecoderInfo);
} }
} }
return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); return super.getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder);
} }
/** /**
......
...@@ -159,6 +159,11 @@ public final class MediaCodecInfo { ...@@ -159,6 +159,11 @@ public final class MediaCodecInfo {
secure = forceSecure || (capabilities != null && isSecure(capabilities)); secure = forceSecure || (capabilities != null && isSecure(capabilities));
} }
@Override
public String toString() {
return name;
}
/** /**
* The profile levels supported by the decoder. * The profile levels supported by the decoder.
* *
......
...@@ -16,7 +16,10 @@ ...@@ -16,7 +16,10 @@
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import android.media.MediaCodec; import android.media.MediaCodec;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import java.util.Collections;
import java.util.List;
/** /**
* Selector of {@link MediaCodec} instances. * Selector of {@link MediaCodec} instances.
...@@ -24,32 +27,58 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep ...@@ -24,32 +27,58 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep
public interface MediaCodecSelector { public interface MediaCodecSelector {
/** /**
* Default implementation of {@link MediaCodecSelector}. * Default implementation of {@link MediaCodecSelector}, which returns the preferred decoder for
* the given format.
*/ */
MediaCodecSelector DEFAULT = new MediaCodecSelector() { MediaCodecSelector DEFAULT =
new MediaCodecSelector() {
@Override @Override
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) public List<MediaCodecInfo> getDecoderInfos(Format format, boolean requiresSecureDecoder)
throws DecoderQueryException { throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder); List<MediaCodecInfo> decoderInfos =
MediaCodecUtil.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder);
return decoderInfos.isEmpty()
? Collections.<MediaCodecInfo>emptyList()
: Collections.singletonList(decoderInfos.get(0));
} }
@Override @Override
public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
return MediaCodecUtil.getPassthroughDecoderInfo(); return MediaCodecUtil.getPassthroughDecoderInfo();
} }
};
/**
* A {@link MediaCodecSelector} that returns a list of decoders in priority order, allowing
* fallback to less preferred decoders if initialization fails.
*
* <p>Note: if a hardware-accelerated video decoder fails to initialize, this selector may provide
* a software video decoder to use as a fallback. Using software decoding can be inefficient, and
* the decoder may be too slow to keep up with the playback position.
*/
MediaCodecSelector DEFAULT_WITH_FALLBACK =
new MediaCodecSelector() {
@Override
public List<MediaCodecInfo> getDecoderInfos(Format format, boolean requiresSecureDecoder)
throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder);
}
@Override
public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
return MediaCodecUtil.getPassthroughDecoderInfo();
}
}; };
/** /**
* Selects a decoder to instantiate for a given mime type. * Returns a list of decoders that can decode media in the specified format, in priority order.
* *
* @param mimeType The mime type for which a decoder is required. * @param format The format for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required.
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty.
* @throws DecoderQueryException Thrown if there was an error querying decoders. * @throws DecoderQueryException Thrown if there was an error querying decoders.
*/ */
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) List<MediaCodecInfo> getDecoderInfos(Format format, boolean requiresSecureDecoder)
throws DecoderQueryException; throws DecoderQueryException;
/** /**
......
...@@ -114,11 +114,10 @@ public final class MediaCodecUtil { ...@@ -114,11 +114,10 @@ public final class MediaCodecUtil {
/** /**
* Returns information about the preferred decoder for a given mime type. * Returns information about the preferred decoder for a given mime type.
* *
* @param mimeType The mime type. * @param mimeType The MIME type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false * @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required. * unless secure decryption really is required.
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
* exists.
* @throws DecoderQueryException If there was an error querying the available decoders. * @throws DecoderQueryException If there was an error querying the available decoders.
*/ */
public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
...@@ -128,18 +127,18 @@ public final class MediaCodecUtil { ...@@ -128,18 +127,18 @@ public final class MediaCodecUtil {
} }
/** /**
* Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by * Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link
* {@link MediaCodecList}. * MediaCodecList}.
* *
* @param mimeType The mime type. * @param mimeType The MIME type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false * @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required. * unless secure decryption really is required.
* @return A list of all @{link MediaCodecInfo}s for the given mime type, in the order * @return A list of all {@link MediaCodecInfo}s for the given mime type, in the order given by
* given by {@link MediaCodecList}. * {@link MediaCodecList}.
* @throws DecoderQueryException If there was an error querying the available decoders. * @throws DecoderQueryException If there was an error querying the available decoders.
*/ */
public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType, public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean secure)
boolean secure) throws DecoderQueryException { throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure); CodecKey key = new CodecKey(mimeType, secure);
List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key); List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);
if (cachedDecoderInfos != null) { if (cachedDecoderInfos != null) {
......
...@@ -51,6 +51,7 @@ import com.google.android.exoplayer2.util.TraceUtil; ...@@ -51,6 +51,7 @@ import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List;
/** /**
* Decodes and renders video using {@link MediaCodec}. * Decodes and renders video using {@link MediaCodec}.
...@@ -236,15 +237,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -236,15 +237,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;
} }
} }
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, List<MediaCodecInfo> decoderInfos =
requiresSecureDecryption); mediaCodecSelector.getDecoderInfos(format, requiresSecureDecryption);
if (decoderInfo == null) { if (decoderInfos.isEmpty()) {
return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null return requiresSecureDecryption
? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; && !mediaCodecSelector
.getDecoderInfos(format, /* requiresSecureDecoder= */ false)
.isEmpty()
? FORMAT_UNSUPPORTED_DRM
: FORMAT_UNSUPPORTED_SUBTYPE;
} }
if (!supportsFormatDrm(drmSessionManager, drmInitData)) { if (!supportsFormatDrm(drmSessionManager, drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} }
// Check capabilities for the first decoder in the list, which takes priority.
MediaCodecInfo decoderInfo = decoderInfos.get(0);
boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs);
if (decoderCapable && format.width > 0 && format.height > 0) { if (decoderCapable && format.width > 0 && format.height > 0) {
if (Util.SDK_INT >= 21) { if (Util.SDK_INT >= 21) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment