mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-20 11:33:06 +00:00
Add new experimental decoder code for Lollipop's asynchronous MediaCodec capability
This commit is contained in:
parent
fa847ef2fc
commit
ac03f73cf9
@ -25,7 +25,7 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
public void initializeWithFlags(int drFlags) {
|
public void initializeWithFlags(int drFlags) {
|
||||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||||
MediaCodecDecoderRenderer.findProbableSafeDecoder() != null)) {
|
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
@ -18,17 +16,12 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
|
||||||
import android.media.MediaCodecList;
|
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.media.MediaCodec.BufferInfo;
|
import android.media.MediaCodec.BufferInfo;
|
||||||
|
import android.media.MediaCodec.CodecException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
// Ignore warnings about deprecated MediaCodecList APIs in API level 21
|
|
||||||
// We don't care about any of the new codec types anyway.
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
|
||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
@ -49,44 +42,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
public static final List<String> preferredDecoders;
|
private static final boolean ENABLE_ASYNC_RENDERER = true;
|
||||||
|
|
||||||
public static final List<String> blacklistedDecoderPrefixes;
|
|
||||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
|
||||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
|
||||||
|
|
||||||
static {
|
|
||||||
preferredDecoders = new LinkedList<String>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
|
||||||
|
|
||||||
// Software decoders that don't support H264 high profile
|
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public MediaCodecDecoderRenderer() {
|
public MediaCodecDecoderRenderer() {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
MediaCodecInfo decoder = findProbableSafeDecoder();
|
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
decoder = findFirstDecoder();
|
decoder = MediaCodecHelper.findFirstDecoder();
|
||||||
}
|
}
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
// This case is handled later in setup()
|
// This case is handled later in setup()
|
||||||
@ -95,182 +59,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
decoderName = decoder.getName();
|
decoderName = decoder.getName();
|
||||||
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Set decoder-specific attributes
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||||
try {
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||||
if (decoder.getCapabilitiesForType("video/avc").
|
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
|
||||||
{
|
|
||||||
// This will make getCapabilities() return that adaptive playback is supported
|
|
||||||
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
|
|
||||||
adaptivePlayback = true;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Tolerate buggy codecs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!adaptivePlayback) {
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
|
||||||
adaptivePlayback = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
|
||||||
if (needsSpsBitstreamFixup) {
|
if (needsSpsBitstreamFixup) {
|
||||||
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
for (String badPrefix : decoderList) {
|
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
|
||||||
if (prefix.equalsIgnoreCase(badPrefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String dumpDecoders() throws Exception {
|
|
||||||
String str = "";
|
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
|
||||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
|
||||||
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
str += "Decoder: "+codecInfo.getName()+"\n";
|
|
||||||
for (String type : codecInfo.getSupportedTypes()) {
|
|
||||||
str += "\t"+type+"\n";
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
|
|
||||||
|
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
|
||||||
str += "\t\t"+profile.profile+" "+profile.level+"\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaCodecInfo findPreferredDecoder() {
|
|
||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
|
||||||
// because we want to evaluate the decoders in our list's order
|
|
||||||
// rather than MediaCodecList's order
|
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
|
||||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
|
||||||
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for preferred decoders
|
|
||||||
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
|
||||||
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaCodecInfo findFirstDecoder() {
|
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
|
||||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
|
||||||
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports H.264
|
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
|
||||||
if (mime.equalsIgnoreCase("video/avc")) {
|
|
||||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MediaCodecInfo findProbableSafeDecoder() {
|
|
||||||
// First look for a preferred decoder by name
|
|
||||||
MediaCodecInfo info = findPreferredDecoder();
|
|
||||||
if (info != null) {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now look for decoders we know are safe
|
|
||||||
try {
|
|
||||||
// If this function completes, it will determine if the decoder is safe
|
|
||||||
return findKnownSafeDecoder();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Some buggy devices seem to throw exceptions
|
|
||||||
// from getCapabilitiesForType() so we'll just assume
|
|
||||||
// they're okay and go with the first one we find
|
|
||||||
return findFirstDecoder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We declare this method as explicitly throwing Exception
|
|
||||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
|
||||||
// and we want to be sure all callers are handling this possibility
|
|
||||||
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
|
||||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
|
||||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
|
||||||
|
|
||||||
// Skip encoders
|
|
||||||
if (codecInfo.isEncoder()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports H.264 high profile
|
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
|
||||||
if (mime.equalsIgnoreCase("video/avc")) {
|
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
|
||||||
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
|
||||||
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
|
||||||
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
@ -298,6 +95,52 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
||||||
|
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
videoDecoder.setCallback(new MediaCodec.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + format);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
||||||
|
BufferInfo info) {
|
||||||
|
try {
|
||||||
|
// FIXME: It looks like we can't frameskip here
|
||||||
|
codec.releaseOutputBuffer(index, true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||||
|
try {
|
||||||
|
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// What do we do here?
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(MediaCodec codec, CodecException e) {
|
||||||
|
if (e.isTransient()) {
|
||||||
|
LimeLog.warning(e.getDiagnosticInfo());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.severe(e.getDiagnosticInfo());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
@ -306,9 +149,34 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (e instanceof CodecException) {
|
||||||
|
CodecException codecExc = (CodecException) e;
|
||||||
|
|
||||||
|
if (codecExc.isTransient()) {
|
||||||
|
// We'll let transient exceptions go
|
||||||
|
LimeLog.warning(codecExc.getDiagnosticInfo());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.severe(codecExc.getDiagnosticInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf != null || codecFlags != 0) {
|
||||||
|
throw new RendererException(dr, e, buf, codecFlags);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new RendererException(dr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
@ -319,6 +187,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// In order to get as much data to the decoder as early as possible,
|
// In order to get as much data to the decoder as early as possible,
|
||||||
// try to submit up to 5 decode units at once without blocking.
|
// try to submit up to 5 decode units at once without blocking.
|
||||||
if (inputIndex == -1 && du == null) {
|
if (inputIndex == -1 && du == null) {
|
||||||
|
try {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
@ -328,11 +197,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, inputIndex);
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
|
||||||
du = null;
|
du = null;
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
inputIndex = -1;
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab an input buffer if we don't have one already.
|
// Grab an input buffer if we don't have one already.
|
||||||
@ -348,7 +221,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// just see if we can get one immediately.
|
// just see if we can get one immediately.
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RendererException(MediaCodecDecoderRenderer.this, e);
|
inputIndex = -1;
|
||||||
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +234,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// If we've got both a decode unit and an input buffer, we'll
|
// If we've got both a decode unit and an input buffer, we'll
|
||||||
// submit now. Otherwise, we wait until we have one.
|
// submit now. Otherwise, we wait until we have one.
|
||||||
if (du != null && inputIndex >= 0) {
|
if (du != null && inputIndex >= 0) {
|
||||||
submitDecodeUnit(du, inputIndex);
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
du = null;
|
du = null;
|
||||||
@ -412,7 +286,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RendererException(MediaCodecDecoderRenderer.this, e);
|
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,26 +296,31 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public boolean start(VideoDepacketizer depacketizer) {
|
public boolean start(VideoDepacketizer depacketizer) {
|
||||||
this.depacketizer = depacketizer;
|
this.depacketizer = depacketizer;
|
||||||
|
|
||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
|
||||||
|
|
||||||
// Start the rendering thread
|
// On devices pre-Lollipop, we'll use a rendering thread
|
||||||
|
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||||
startRendererThread();
|
startRendererThread();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
|
if (rendererThread != null) {
|
||||||
// Halt the rendering thread
|
// Halt the rendering thread
|
||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException e) { }
|
||||||
|
}
|
||||||
|
|
||||||
// Stop the decoder
|
// Stop the decoder
|
||||||
videoDecoder.stop();
|
videoDecoder.stop();
|
||||||
@ -454,9 +333,30 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) {
|
private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
||||||
ByteBuffer buf = videoDecoderInputBuffers[inputBufferIndex];
|
// Try 25 times to submit the input buffer before throwing a real exception
|
||||||
|
int i;
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
|
for (i = 0; i < 25; i++) {
|
||||||
|
try {
|
||||||
|
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||||
|
0, length,
|
||||||
|
timestampUs, codecFlags);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(this, e, null, codecFlags);
|
||||||
|
lastException = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 25) {
|
||||||
|
throw new RendererException(this, lastException, null, codecFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
||||||
if (delta >= 0 && delta < 300) {
|
if (delta >= 0 && delta < 300) {
|
||||||
@ -527,13 +427,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// Write the modified SPS to the input buffer
|
// Write the modified SPS to the input buffer
|
||||||
sps.write(buf);
|
sps.write(buf);
|
||||||
|
|
||||||
try {
|
queueInputBuffer(inputBufferIndex,
|
||||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
|
||||||
0, buf.position(),
|
0, buf.position(),
|
||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
depacketizer.freeDecodeUnit(decodeUnit);
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
return;
|
return;
|
||||||
@ -548,13 +444,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
buf.put(desc.data, desc.offset, desc.length);
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
queueInputBuffer(inputBufferIndex,
|
||||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
|
||||||
0, decodeUnit.getDataLength(),
|
0, decodeUnit.getDataLength(),
|
||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RendererException(this, e, buf, codecFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
depacketizer.freeDecodeUnit(decodeUnit);
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
return;
|
return;
|
||||||
@ -622,7 +514,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
str += "Full decoder dump:\n";
|
str += "Full decoder dump:\n";
|
||||||
try {
|
try {
|
||||||
str += dumpDecoders();
|
str += MediaCodecHelper.dumpDecoders();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
str += e.getMessage();
|
str += e.getMessage();
|
||||||
}
|
}
|
||||||
|
236
src/com/limelight/binding/video/MediaCodecHelper.java
Normal file
236
src/com/limelight/binding/video/MediaCodecHelper.java
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecList;
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
|
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MediaCodecHelper {
|
||||||
|
|
||||||
|
public static final List<String> preferredDecoders;
|
||||||
|
|
||||||
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
|
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
|
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
|
|
||||||
|
static {
|
||||||
|
preferredDecoders = new LinkedList<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
|
// Software decoders that don't support H264 high profile
|
||||||
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
||||||
|
|
||||||
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||||
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
|
for (String badPrefix : decoderList) {
|
||||||
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
|
String prefix = decoderName.substring(0, badPrefix.length());
|
||||||
|
if (prefix.equalsIgnoreCase(badPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
try {
|
||||||
|
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||||
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
|
{
|
||||||
|
// This will make getCapabilities() return that adaptive playback is supported
|
||||||
|
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Tolerate buggy codecs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||||
|
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
|
for (MediaCodecInfo info : mcl.getCodecInfos()) {
|
||||||
|
infoList.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
||||||
|
infoList.add(MediaCodecList.getCodecInfoAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String dumpDecoders() throws Exception {
|
||||||
|
String str = "";
|
||||||
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
// Skip encoders
|
||||||
|
if (codecInfo.isEncoder()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
str += "Decoder: "+codecInfo.getName()+"\n";
|
||||||
|
for (String type : codecInfo.getSupportedTypes()) {
|
||||||
|
str += "\t"+type+"\n";
|
||||||
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
|
||||||
|
|
||||||
|
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||||
|
str += "\t\t"+profile.profile+" "+profile.level+"\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaCodecInfo findPreferredDecoder() {
|
||||||
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
|
// because we want to evaluate the decoders in our list's order
|
||||||
|
// rather than MediaCodecList's order
|
||||||
|
|
||||||
|
for (String preferredDecoder : preferredDecoders) {
|
||||||
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
// Skip encoders
|
||||||
|
if (codecInfo.isEncoder()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for preferred decoders
|
||||||
|
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
||||||
|
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaCodecInfo findFirstDecoder() {
|
||||||
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
// Skip encoders
|
||||||
|
if (codecInfo.isEncoder()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for explicitly blacklisted decoders
|
||||||
|
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
||||||
|
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a decoder that supports H.264
|
||||||
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
|
if (mime.equalsIgnoreCase("video/avc")) {
|
||||||
|
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaCodecInfo findProbableSafeDecoder() {
|
||||||
|
// First look for a preferred decoder by name
|
||||||
|
MediaCodecInfo info = findPreferredDecoder();
|
||||||
|
if (info != null) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now look for decoders we know are safe
|
||||||
|
try {
|
||||||
|
// If this function completes, it will determine if the decoder is safe
|
||||||
|
return findKnownSafeDecoder();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Some buggy devices seem to throw exceptions
|
||||||
|
// from getCapabilitiesForType() so we'll just assume
|
||||||
|
// they're okay and go with the first one we find
|
||||||
|
return findFirstDecoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We declare this method as explicitly throwing Exception
|
||||||
|
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||||
|
// and we want to be sure all callers are handling this possibility
|
||||||
|
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||||
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
|
// Skip encoders
|
||||||
|
if (codecInfo.isEncoder()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for explicitly blacklisted decoders
|
||||||
|
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
||||||
|
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a decoder that supports H.264 high profile
|
||||||
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
|
if (mime.equalsIgnoreCase("video/avc")) {
|
||||||
|
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||||
|
|
||||||
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||||
|
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||||
|
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
||||||
|
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
||||||
|
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user