From b19360ac75a068d28e1023b81044c1e93fc6630c Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 17 Oct 2014 14:27:33 -0700 Subject: [PATCH 1/5] Suppress deprecation warnings for MediaCodecList APIs --- src/com/limelight/binding/video/MediaCodecDecoderRenderer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 05715090..03f6e7e3 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -26,6 +26,9 @@ import android.media.MediaCodec.BufferInfo; import android.os.Build; 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 { private ByteBuffer[] videoDecoderInputBuffers; From fa847ef2fc5886438ccf52f4ce98531cdf6d1149 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 17 Oct 2014 14:45:58 -0700 Subject: [PATCH 2/5] Ensure that no input buffers will ever be submitted with the same timestamp per SDK docs --- .../binding/video/MediaCodecDecoderRenderer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 03f6e7e3..a6411fa5 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -39,6 +39,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { private boolean adaptivePlayback; private int initialWidth, initialHeight; + private long lastTimestampUs; private long totalTimeMs; private long decoderTimeMs; private int totalFrames; @@ -463,6 +464,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { totalFrames++; } + long timestampUs = currentTime * 1000; + if (timestampUs == lastTimestampUs) { + // We can't submit multiple buffers with the same timestamp + // so bump it up by one before queuing + timestampUs = lastTimestampUs + 1; + } + lastTimestampUs = timestampUs; + // Clear old input data buf.clear(); @@ -521,7 +530,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { try { videoDecoder.queueInputBuffer(inputBufferIndex, 0, buf.position(), - currentTime * 1000, codecFlags); + timestampUs, codecFlags); } catch (Exception e) { throw new RendererException(this, e, buf, codecFlags); } @@ -542,7 +551,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { try { videoDecoder.queueInputBuffer(inputBufferIndex, 0, decodeUnit.getDataLength(), - currentTime * 1000, codecFlags); + timestampUs, codecFlags); } catch (Exception e) { throw new RendererException(this, e, buf, codecFlags); } From ac03f73cf98be668942d22ce2bde40878110d08a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 17 Oct 2014 15:51:50 -0700 Subject: [PATCH 3/5] Add new experimental decoder code for Lollipop's asynchronous MediaCodec capability --- .../video/ConfigurableDecoderRenderer.java | 2 +- .../video/MediaCodecDecoderRenderer.java | 392 +++++++----------- .../binding/video/MediaCodecHelper.java | 236 +++++++++++ 3 files changed, 379 insertions(+), 251 deletions(-) create mode 100644 src/com/limelight/binding/video/MediaCodecHelper.java diff --git a/src/com/limelight/binding/video/ConfigurableDecoderRenderer.java b/src/com/limelight/binding/video/ConfigurableDecoderRenderer.java index 856192a8..74024dae 100644 --- a/src/com/limelight/binding/video/ConfigurableDecoderRenderer.java +++ b/src/com/limelight/binding/video/ConfigurableDecoderRenderer.java @@ -25,7 +25,7 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer { public void initializeWithFlags(int drFlags) { if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && - MediaCodecDecoderRenderer.findProbableSafeDecoder() != null)) { + MediaCodecHelper.findProbableSafeDecoder() != null)) { decoderRenderer = new MediaCodecDecoderRenderer(); } else { diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index a6411fa5..6281f27c 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -1,8 +1,6 @@ package com.limelight.binding.video; import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; import java.util.Locale; import java.util.concurrent.locks.LockSupport; @@ -18,17 +16,12 @@ import com.limelight.nvstream.av.video.VideoDepacketizer; import android.annotation.TargetApi; import android.media.MediaCodec; 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.MediaCodec.BufferInfo; +import android.media.MediaCodec.CodecException; import android.os.Build; 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 { private ByteBuffer[] videoDecoderInputBuffers; @@ -49,44 +42,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { private int numPpsIn; private int numIframeIn; - public static final List preferredDecoders; - - public static final List blacklistedDecoderPrefixes; - public static final List spsFixupBitstreamFixupDecoderPrefixes; - public static final List whitelistedAdaptiveResolutionPrefixes; - - static { - preferredDecoders = new LinkedList(); - } - - static { - blacklistedDecoderPrefixes = new LinkedList(); - - // Software decoders that don't support H264 high profile - blacklistedDecoderPrefixes.add("omx.google"); - blacklistedDecoderPrefixes.add("AVCDecoder"); - } - - static { - spsFixupBitstreamFixupDecoderPrefixes = new LinkedList(); - spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); - spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); - spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); - - whitelistedAdaptiveResolutionPrefixes = new LinkedList(); - whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia"); - whitelistedAdaptiveResolutionPrefixes.add("omx.qcom"); - whitelistedAdaptiveResolutionPrefixes.add("omx.sec"); - whitelistedAdaptiveResolutionPrefixes.add("omx.TI"); - } + private static final boolean ENABLE_ASYNC_RENDERER = true; @TargetApi(Build.VERSION_CODES.KITKAT) public MediaCodecDecoderRenderer() { //dumpDecoders(); - MediaCodecInfo decoder = findProbableSafeDecoder(); + MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder(); if (decoder == null) { - decoder = findFirstDecoder(); + decoder = MediaCodecHelper.findFirstDecoder(); } if (decoder == null) { // This case is handled later in setup() @@ -95,182 +59,15 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { decoderName = decoder.getName(); - // Possibly enable adaptive playback on KitKat and above - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - try { - 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); + // Set decoder-specific attributes + adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); + needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); if (needsSpsBitstreamFixup) { LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup"); } } - private static boolean isDecoderInList(List 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; - } - - 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) + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { this.initialWidth = width; @@ -298,6 +95,52 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { 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.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); @@ -306,9 +149,34 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { 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() { rendererThread = new Thread() { + @SuppressWarnings("deprecation") @Override public void run() { BufferInfo info = new BufferInfo(); @@ -319,19 +187,24 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { // 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. if (inputIndex == -1 && du == null) { - for (int i = 0; i < 5; i++) { - inputIndex = videoDecoder.dequeueInputBuffer(0); - du = depacketizer.pollNextDecodeUnit(); + try { + for (int i = 0; i < 5; i++) { + inputIndex = videoDecoder.dequeueInputBuffer(0); + du = depacketizer.pollNextDecodeUnit(); - // Stop if we can't get a DU or input buffer - if (du == null || inputIndex == -1) { - break; + // Stop if we can't get a DU or input buffer + if (du == null || inputIndex == -1) { + break; + } + + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); + + du = null; + inputIndex = -1; } - - submitDecodeUnit(du, inputIndex); - - du = null; + } catch (Exception e) { inputIndex = -1; + handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); } } @@ -348,7 +221,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { // just see if we can get one immediately. inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); } 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 // submit now. Otherwise, we wait until we have one. if (du != null && inputIndex >= 0) { - submitDecodeUnit(du, inputIndex); + submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); // DU and input buffer have both been consumed du = null; @@ -412,7 +286,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { } } } 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(); } + @SuppressWarnings("deprecation") @Override public boolean start(VideoDepacketizer depacketizer) { this.depacketizer = depacketizer; // Start the decoder videoDecoder.start(); - videoDecoderInputBuffers = videoDecoder.getInputBuffers(); - // Start the rendering thread - startRendererThread(); + // 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(); + } return true; } @Override public void stop() { - // Halt the rendering thread - rendererThread.interrupt(); - try { - rendererThread.join(); - } catch (InterruptedException e) { } + if (rendererThread != null) { + // Halt the rendering thread + rendererThread.interrupt(); + try { + rendererThread.join(); + } catch (InterruptedException e) { } + } // Stop the decoder videoDecoder.stop(); @@ -453,10 +332,31 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { videoDecoder.release(); } } + + private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) { + // 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); + } + } - private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) { - ByteBuffer buf = videoDecoderInputBuffers[inputBufferIndex]; - + @SuppressWarnings("deprecation") + private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { long currentTime = System.currentTimeMillis(); long delta = currentTime-decodeUnit.getReceiveTimestamp(); if (delta >= 0 && delta < 300) { @@ -527,13 +427,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { // Write the modified SPS to the input buffer sps.write(buf); - try { - videoDecoder.queueInputBuffer(inputBufferIndex, - 0, buf.position(), - timestampUs, codecFlags); - } catch (Exception e) { - throw new RendererException(this, e, buf, codecFlags); - } + queueInputBuffer(inputBufferIndex, + 0, buf.position(), + timestampUs, codecFlags); depacketizer.freeDecodeUnit(decodeUnit); return; @@ -548,13 +444,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { buf.put(desc.data, desc.offset, desc.length); } - try { - videoDecoder.queueInputBuffer(inputBufferIndex, - 0, decodeUnit.getDataLength(), - timestampUs, codecFlags); - } catch (Exception e) { - throw new RendererException(this, e, buf, codecFlags); - } + queueInputBuffer(inputBufferIndex, + 0, decodeUnit.getDataLength(), + timestampUs, codecFlags); depacketizer.freeDecodeUnit(decodeUnit); return; @@ -622,7 +514,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { str += "Full decoder dump:\n"; try { - str += dumpDecoders(); + str += MediaCodecHelper.dumpDecoders(); } catch (Exception e) { str += e.getMessage(); } diff --git a/src/com/limelight/binding/video/MediaCodecHelper.java b/src/com/limelight/binding/video/MediaCodecHelper.java new file mode 100644 index 00000000..f26a8be9 --- /dev/null +++ b/src/com/limelight/binding/video/MediaCodecHelper.java @@ -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 preferredDecoders; + + public static final List blacklistedDecoderPrefixes; + public static final List spsFixupBitstreamFixupDecoderPrefixes; + public static final List whitelistedAdaptiveResolutionPrefixes; + + static { + preferredDecoders = new LinkedList(); + } + + static { + blacklistedDecoderPrefixes = new LinkedList(); + + // Software decoders that don't support H264 high profile + blacklistedDecoderPrefixes.add("omx.google"); + blacklistedDecoderPrefixes.add("AVCDecoder"); + } + + static { + spsFixupBitstreamFixupDecoderPrefixes = new LinkedList(); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); + + whitelistedAdaptiveResolutionPrefixes = new LinkedList(); + whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia"); + whitelistedAdaptiveResolutionPrefixes.add("omx.qcom"); + whitelistedAdaptiveResolutionPrefixes.add("omx.sec"); + whitelistedAdaptiveResolutionPrefixes.add("omx.TI"); + } + + private static boolean isDecoderInList(List 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 getMediaCodecList() { + LinkedList infoList = new LinkedList(); + + 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; + } +} From 332960922a57dd223e3832fb3e9a83734e472651 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 17 Oct 2014 15:54:07 -0700 Subject: [PATCH 4/5] Small addendum to the timestamp fix --- src/com/limelight/binding/video/MediaCodecDecoderRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 6281f27c..174a7196 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -365,7 +365,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { } long timestampUs = currentTime * 1000; - if (timestampUs == lastTimestampUs) { + if (timestampUs <= lastTimestampUs) { // We can't submit multiple buffers with the same timestamp // so bump it up by one before queuing timestampUs = lastTimestampUs + 1; From 7b1f6ee48362a496906fdcad9524b84413da7a93 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 17 Oct 2014 22:49:36 -0700 Subject: [PATCH 5/5] Update common and disable the new renderer for now --- libs/limelight-common.jar | Bin 423898 -> 423963 bytes .../video/MediaCodecDecoderRenderer.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/limelight-common.jar b/libs/limelight-common.jar index 2e1ef63955a0dfd4b2769ef276869eabc80b94f6..e218e4d27ad3a4a1b4461d8ec6f602628ed44dc7 100644 GIT binary patch delta 17057 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 CM1NBN delta 16951 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 diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index 174a7196..cf91d802 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -42,7 +42,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { private int numPpsIn; private int numIframeIn; - private static final boolean ENABLE_ASYNC_RENDERER = true; + private static final boolean ENABLE_ASYNC_RENDERER = false; @TargetApi(Build.VERSION_CODES.KITKAT) public MediaCodecDecoderRenderer() {