package com.limelight.binding.video; import java.nio.ByteBuffer; import java.util.Locale; import java.util.concurrent.locks.LockSupport; import org.jcodec.codecs.h264.H264Utils; import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.h264.io.model.VUIParameters; import com.limelight.LimeLog; import com.limelight.nvstream.av.ByteBufferDescriptor; import com.limelight.nvstream.av.DecodeUnit; import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.preferences.PreferenceConfiguration; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaCodec.BufferInfo; import android.media.MediaCodec.CodecException; import android.os.Build; import android.view.SurfaceHolder; public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { private static final boolean USE_FRAME_RENDER_TIME = false; // Used on versions < 5.0 private ByteBuffer[] legacyInputBuffers; private MediaCodecInfo avcDecoder; private MediaCodecInfo hevcDecoder; private MediaCodec videoDecoder; private Thread rendererThread; private boolean needsSpsBitstreamFixup, isExynos4; private VideoDepacketizer depacketizer; private boolean adaptivePlayback, directSubmit; private boolean constrainedHighProfile; private int initialWidth, initialHeight; private VideoFormat videoFormat; private boolean needsBaselineSpsHack; private SeqParameterSet savedSps; private long lastTimestampUs; private long totalTimeMs; private long decoderTimeMs; private int totalFrames; private int numSpsIn; private int numPpsIn; private int numVpsIn; private int numIframeIn; private MediaCodecInfo findAvcDecoder() { MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); if (decoder == null) { decoder = MediaCodecHelper.findFirstDecoder("video/avc"); } return decoder; } private MediaCodecInfo findHevcDecoder(int videoFormat) { // Don't return anything if H.265 is forced off if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) { return null; } // We don't try the first HEVC decoder. We'd rather fall back to hardware accelerated AVC instead // // We need HEVC Main profile, so we could pass that constant to findProbableSafeDecoder, however // some decoders (at least Qualcomm's Snapdragon 805) don't properly report support // for even required levels of HEVC. MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1); if (decoderInfo != null) { if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) { LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName()); if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) { LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder"); } else { return null; } } } return decoderInfo; } public MediaCodecDecoderRenderer(int videoFormat) { //dumpDecoders(); avcDecoder = findAvcDecoder(); if (avcDecoder != null) { LimeLog.info("Selected AVC decoder: "+avcDecoder.getName()); } else { LimeLog.warning("No AVC decoder found"); } hevcDecoder = findHevcDecoder(videoFormat); if (hevcDecoder != null) { LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName()); } else { LimeLog.info("No HEVC decoder found"); } // Set attributes that are queried in getCapabilities(). This must be done here // because getCapabilities() may be called before setup() in current versions of the common // library. The limitation of this is that we don't know whether we're using HEVC or AVC, so // we just assume AVC. This isn't really a problem because the capabilities are usually // shared between AVC and HEVC decoders on the same device. if (avcDecoder != null) { directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName()); adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder.getName()); if (directSubmit) { LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit"); } } } @Override public boolean isHevcSupported() { return hevcDecoder != null; } @Override public boolean isAvcSupported() { return avcDecoder != null; } @Override public boolean setup(VideoDecoderRenderer.VideoFormat format, int width, int height, int redrawRate, Object renderTarget, int drFlags) { this.initialWidth = width; this.initialHeight = height; this.videoFormat = format; String mimeType; String selectedDecoderName; if (videoFormat == VideoFormat.H264) { mimeType = "video/avc"; selectedDecoderName = avcDecoder.getName(); if (avcDecoder == null) { LimeLog.severe("No available AVC decoder!"); return false; } // These fixups only apply to H264 decoders needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName); constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(selectedDecoderName); isExynos4 = MediaCodecHelper.isExynos4Device(); if (needsSpsBitstreamFixup) { LimeLog.info("Decoder "+selectedDecoderName+" needs SPS bitstream restrictions fixup"); } if (needsBaselineSpsHack) { LimeLog.info("Decoder "+selectedDecoderName+" needs baseline SPS hack"); } if (constrainedHighProfile) { LimeLog.info("Decoder "+selectedDecoderName+" needs constrained high profile"); } if (isExynos4) { LimeLog.info("Decoder "+selectedDecoderName+" is on Exynos 4"); } } else if (videoFormat == VideoFormat.H265) { mimeType = "video/hevc"; selectedDecoderName = hevcDecoder.getName(); if (hevcDecoder == null) { LimeLog.severe("No available HEVC decoder!"); return false; } } else { // Unknown format return false; } // Codecs have been known to throw all sorts of crazy runtime exceptions // due to implementation problems try { videoDecoder = MediaCodec.createByCodecName(selectedDecoderName); } catch (Exception e) { return false; } MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height); // Adaptive playback can also be enabled by the whitelist on pre-KitKat devices // so we don't fill these pre-KitKat if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width); videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Operate at maximum rate to lower latency as much as possible on // some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime) // but that will actually result in the decoder crashing if it can't satisfy // our (ludicrous) operating rate requirement. videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE); } videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() { @Override public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) { long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000); if (delta >= 0 && delta < 1000) { if (USE_FRAME_RENDER_TIME) { totalTimeMs += delta; } } } }, null); } LimeLog.info("Using codec "+selectedDecoderName+" for hardware decoding "+mimeType); return true; } private void handleDecoderException(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()); } } // Only throw if this happens at the beginning of a stream if (totalFrames < 60) { if (buf != null || codecFlags != 0) { throw new RendererException(this, e, buf, codecFlags); } else { throw new RendererException(this, e); } } } private void startDirectSubmitRendererThread() { rendererThread = new Thread() { @Override public void run() { BufferInfo info = new BufferInfo(); while (!isInterrupted()) { try { // Try to output a frame int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000); if (outIndex >= 0) { long presentationTimeUs = info.presentationTimeUs; int lastIndex = outIndex; // Get the last output buffer in the queue while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { videoDecoder.releaseOutputBuffer(lastIndex, false); lastIndex = outIndex; presentationTimeUs = info.presentationTimeUs; } // Render the last buffer videoDecoder.releaseOutputBuffer(lastIndex, true); // Add delta time to the totals (excluding probable outliers) long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000); if (delta >= 0 && delta < 1000) { decoderTimeMs += delta; if (!USE_FRAME_RENDER_TIME) { totalTimeMs += delta; } } } else { switch (outIndex) { case MediaCodec.INFO_TRY_AGAIN_LATER: break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: LimeLog.info("Output format changed"); LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); break; default: break; } } } catch (Exception e) { handleDecoderException(e, null, 0); } } } }; rendererThread.setName("Video - Renderer (MediaCodec)"); rendererThread.setPriority(Thread.NORM_PRIORITY + 2); rendererThread.start(); } private int dequeueInputBuffer(boolean wait, boolean infiniteWait) { int index; long startTime, queueTime; startTime = MediaCodecHelper.getMonotonicMillis(); index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0); if (index < 0) { return index; } queueTime = MediaCodecHelper.getMonotonicMillis(); if (queueTime - startTime >= 20) { LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms"); } return index; } private void startLegacyRendererThread() { rendererThread = new Thread() { @Override public void run() { BufferInfo info = new BufferInfo(); DecodeUnit du = null; int inputIndex = -1; long lastDuDequeueTime = 0; while (!isInterrupted()) { // 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) { try { for (int i = 0; i < 5; i++) { inputIndex = dequeueInputBuffer(false, false); du = depacketizer.pollNextDecodeUnit(); if (du != null) { lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis(); notifyDuReceived(du); } // Stop if we can't get a DU or input buffer if (du == null || inputIndex == -1) { break; } submitDecodeUnit(du, inputIndex); du = null; inputIndex = -1; } } catch (Exception e) { inputIndex = -1; handleDecoderException(e, null, 0); } } // Grab an input buffer if we don't have one already. // This way we can have one ready hopefully by the time // the depacketizer is done with this frame. It's important // that this can timeout because it's possible that we could exhaust // the decoder's input buffers and deadlocks because aren't pulling // frames out of the other end. if (inputIndex == -1) { try { // If we've got a DU waiting to be given to the decoder, // wait a full 3 ms for an input buffer. Otherwise // just see if we can get one immediately. inputIndex = dequeueInputBuffer(du != null, false); } catch (Exception e) { inputIndex = -1; handleDecoderException(e, null, 0); } } // Grab a decode unit if we don't have one already if (du == null) { du = depacketizer.pollNextDecodeUnit(); if (du != null) { lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis(); notifyDuReceived(du); } } // 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) { long submissionTime = MediaCodecHelper.getMonotonicMillis(); if (submissionTime - lastDuDequeueTime >= 20) { LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms"); } submitDecodeUnit(du, inputIndex); // DU and input buffer have both been consumed du = null; inputIndex = -1; } // Try to output a frame try { int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); if (outIndex >= 0) { long presentationTimeUs = info.presentationTimeUs; int lastIndex = outIndex; // Get the last output buffer in the queue while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { videoDecoder.releaseOutputBuffer(lastIndex, false); lastIndex = outIndex; presentationTimeUs = info.presentationTimeUs; } // Render the last buffer videoDecoder.releaseOutputBuffer(lastIndex, true); // Add delta time to the totals (excluding probable outliers) long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000); if (delta >= 0 && delta < 1000) { decoderTimeMs += delta; if (!USE_FRAME_RENDER_TIME) { totalTimeMs += delta; } } } else { switch (outIndex) { case MediaCodec.INFO_TRY_AGAIN_LATER: // Getting an input buffer may already block // so don't park if we still need to do that if (inputIndex >= 0) { LockSupport.parkNanos(1); } break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: LimeLog.info("Output format changed"); LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); break; default: break; } } } catch (Exception e) { handleDecoderException(e, null, 0); } } } }; rendererThread.setName("Video - Renderer (MediaCodec)"); rendererThread.setPriority(Thread.MAX_PRIORITY); rendererThread.start(); } @SuppressWarnings("deprecation") @Override public boolean start(VideoDepacketizer depacketizer) { this.depacketizer = depacketizer; // Start the decoder videoDecoder.start(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { legacyInputBuffers = videoDecoder.getInputBuffers(); } if (directSubmit) { startDirectSubmitRendererThread(); } else { startLegacyRendererThread(); } return true; } @Override public void stop() { if (rendererThread != null) { // Halt the rendering thread rendererThread.interrupt(); try { rendererThread.join(); } catch (InterruptedException ignored) { } } // We could stop the decoder here, but it seems to cause some problems // so we'll just let release take care of it. } @Override public void release() { if (videoDecoder != null) { 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, offset, length, timestampUs, codecFlags); break; } catch (Exception e) { handleDecoderException(e, null, codecFlags); lastException = e; } } if (i == 25) { throw new RendererException(this, lastException, null, codecFlags); } } // Using the new getInputBuffer() API on Lollipop allows // the framework to do some performance optimizations for us private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) { ByteBuffer buf; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { buf = videoDecoder.getInputBuffer(inputBufferIndex); } else { buf = legacyInputBuffers[inputBufferIndex]; // Clear old input data pre-Lollipop buf.clear(); } return buf; } private void doProfileSpecificSpsPatching(SeqParameterSet sps) { // Some devices benefit from setting constraint flags 4 & 5 to make this Constrained // High Profile which allows the decoder to assume there will be no B-frames and // reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't // like it so we only set them on devices that are confirmed to benefit from it. if (sps.profile_idc == 100 && constrainedHighProfile) { LimeLog.info("Setting constraint set flags for constrained high profile"); sps.constraint_set_4_flag = true; sps.constraint_set_5_flag = true; } else { // Force the constraints unset otherwise (some may be set by default) sps.constraint_set_4_flag = false; sps.constraint_set_5_flag = false; } } @SuppressWarnings("deprecation") private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) { long timestampUs = System.nanoTime() / 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; ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex); int codecFlags = 0; int decodeUnitFlags = decodeUnit.getFlags(); if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; } if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) { codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME; numIframeIn++; } boolean needsSpsReplay = false; if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { ByteBufferDescriptor header = decodeUnit.getBufferHead(); // H264 SPS if (header.data[header.offset+4] == 0x67) { numSpsIn++; ByteBuffer spsBuf = ByteBuffer.wrap(header.data); // Skip to the start of the NALU data spsBuf.position(header.offset+5); // The H264Utils.readSPS function safely handles // Annex B NALUs (including NALUs with escape sequences) SeqParameterSet sps = H264Utils.readSPS(spsBuf); // Some decoders rely on H264 level to decide how many buffers are needed // Since we only need one frame buffered, we'll set the level as low as we can // for known resolution combinations if (initialWidth == 1280 && initialHeight == 720) { // Max 5 buffered frames at 1280x720x60 LimeLog.info("Patching level_idc to 32"); sps.level_idc = 32; } else if (initialWidth == 1920 && initialHeight == 1080) { // Max 4 buffered frames at 1920x1080x64 LimeLog.info("Patching level_idc to 42"); sps.level_idc = 42; } else { // Leave the profile alone (currently 5.0) } // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 // also requires this fixup. // // I'm doing this fixup for all devices because I haven't seen any devices that // this causes issues for. At worst, it seems to do nothing and at best it fixes // issues with video lag, hangs, and crashes. LimeLog.info("Patching num_ref_frames in SPS"); sps.num_ref_frames = 1; // GFE 2.5.11 changed the SPS to add additional extensions // Some devices don't like these so we remove them here. sps.vuiParams.video_signal_type_present_flag = false; sps.vuiParams.colour_description_present_flag = false; sps.vuiParams.chroma_loc_info_present_flag = false; if (needsSpsBitstreamFixup || isExynos4) { // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag // or max_dec_frame_buffering which increases decoding latency on Tegra. // GFE 2.5.11 started sending bitstream restrictions if (sps.vuiParams.bitstreamRestriction == null) { LimeLog.info("Adding bitstream restrictions"); sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; } else { LimeLog.info("Patching bitstream restrictions"); } // Some devices throw errors if max_dec_frame_buffering < num_ref_frames sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames; // These values are the defaults for the fields, but they are more aggressive // than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems. sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2; sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1; // log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more // conservative values by GFE 2.5.11. We'll let those values stand. } else { // Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11 // will continue to not receive them now sps.vuiParams.bitstreamRestriction = null; } // If we need to hack this SPS to say we're baseline, do so now if (needsBaselineSpsHack) { LimeLog.info("Hacking SPS to baseline"); sps.profile_idc = 66; savedSps = sps; } // Patch the SPS constraint flags doProfileSpecificSpsPatching(sps); // Write the annex B header buf.put(header.data, header.offset, 5); // The H264Utils.writeSPS function safely handles // Annex B NALUs (including NALUs with escape sequences) ByteBuffer escapedNalu = H264Utils.writeSPS(sps, header.length); buf.put(escapedNalu); queueInputBuffer(inputBufferIndex, 0, buf.position(), timestampUs, codecFlags); depacketizer.freeDecodeUnit(decodeUnit); return; // H264 PPS } else if (header.data[header.offset+4] == 0x68) { numPpsIn++; if (needsBaselineSpsHack) { LimeLog.info("Saw PPS; disabling SPS hack"); needsBaselineSpsHack = false; // Give the decoder the SPS again with the proper profile now needsSpsReplay = true; } } else if (header.data[header.offset+4] == 0x40) { numVpsIn++; } else if (header.data[header.offset+4] == 0x42) { numSpsIn++; } else if (header.data[header.offset+4] == 0x44) { numPpsIn++; } } // Copy data from our buffer list into the input buffer for (ByteBufferDescriptor desc = decodeUnit.getBufferHead(); desc != null; desc = desc.nextDescriptor) { buf.put(desc.data, desc.offset, desc.length); } queueInputBuffer(inputBufferIndex, 0, decodeUnit.getDataLength(), timestampUs, codecFlags); depacketizer.freeDecodeUnit(decodeUnit); if (needsSpsReplay) { replaySps(); } } private void replaySps() { int inputIndex = dequeueInputBuffer(true, true); ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex); // Write the Annex B header inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67}); // Switch the H264 profile back to high savedSps.profile_idc = 100; // Patch the SPS constraint flags doProfileSpecificSpsPatching(savedSps); // The H264Utils.writeSPS function safely handles // Annex B NALUs (including NALUs with escape sequences) ByteBuffer escapedNalu = H264Utils.writeSPS(savedSps, 128); inputBuffer.put(escapedNalu); // No need for the SPS anymore savedSps = null; // Queue the new SPS queueInputBuffer(inputIndex, 0, inputBuffer.position(), System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_CODEC_CONFIG); LimeLog.info("SPS replay complete"); } @Override public int getCapabilities() { int caps = 0; caps |= adaptivePlayback ? VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; caps |= directSubmit ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0; return caps; } @Override public int getAverageDecoderLatency() { if (totalFrames == 0) { return 0; } return (int)(decoderTimeMs / totalFrames); } @Override public int getAverageEndToEndLatency() { if (totalFrames == 0) { return 0; } return (int)(totalTimeMs / totalFrames); } private void notifyDuReceived(DecodeUnit du) { long currentTime = MediaCodecHelper.getMonotonicMillis(); long delta = currentTime-du.getReceiveTimestamp(); if (delta >= 0 && delta < 1000) { totalTimeMs += currentTime-du.getReceiveTimestamp(); totalFrames++; } } @Override public void directSubmitDecodeUnit(DecodeUnit du) { int inputIndex = -1; notifyDuReceived(du); while (!Thread.currentThread().isInterrupted()) { try { inputIndex = dequeueInputBuffer(true, true); break; } catch (Exception e) { handleDecoderException(e, null, 0); } } if (inputIndex >= 0) { submitDecodeUnit(du, inputIndex); } } public class RendererException extends RuntimeException { private static final long serialVersionUID = 8985937536997012406L; private final Exception originalException; private final MediaCodecDecoderRenderer renderer; private ByteBuffer currentBuffer; private int currentCodecFlags; public RendererException(MediaCodecDecoderRenderer renderer, Exception e) { this.renderer = renderer; this.originalException = e; } public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) { this.renderer = renderer; this.originalException = e; this.currentBuffer = currentBuffer; this.currentCodecFlags = currentCodecFlags; } public String toString() { String str = ""; str += "Format: "+renderer.videoFormat+"\n"; str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n"; str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n"; str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n"; str += "Total frames: "+renderer.totalFrames+"\n"; str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n"; str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n"; if (currentBuffer != null) { str += "Current buffer: "; currentBuffer.flip(); while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) { str += String.format((Locale)null, "%02x ", currentBuffer.get()); } str += "\n"; str += "Buffer codec flags: "+currentCodecFlags+"\n"; } str += "Is Exynos 4: "+renderer.isExynos4+"\n"; str += "/proc/cpuinfo:\n"; try { str += MediaCodecHelper.readCpuinfo(); } catch (Exception e) { str += e.getMessage(); } str += "Full decoder dump:\n"; try { str += MediaCodecHelper.dumpDecoders(); } catch (Exception e) { str += e.getMessage(); } str += originalException.toString(); return str; } } }