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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 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() {