diff options
Diffstat (limited to 'android/sdl_android/src/main/java/com/smartdevicelink/encoder')
3 files changed, 751 insertions, 0 deletions
diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/encoder/EncoderUtils.java b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/EncoderUtils.java new file mode 100644 index 000000000..362564da8 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/EncoderUtils.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017, Xevo Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.smartdevicelink.encoder; + +import android.annotation.TargetApi; +import android.media.MediaFormat; +import android.os.Build; +import android.util.Log; + +import java.nio.ByteBuffer; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public final class EncoderUtils { + private final static String TAG = "EncoderUtils"; + + /** + * Extracts codec-specific data from MediaFormat instance + * + * Currently, only AVC is supported. + * + * @param format MediaFormat instance retrieved from MediaCodec + * @return byte array containing codec-specific data, or null if an error occurred + */ + public static byte[] getCodecSpecificData(MediaFormat format) { + if (format == null) { + return null; + } + + String name = format.getString(MediaFormat.KEY_MIME); + if (name == null) { + return null; + } + + // same as MediaFormat.MIMETYPE_VIDEO_AVC but it requires API level 21 + if (name.equals("video/avc")) { + return getAVCCodecSpecificData(format); + } else { + Log.w(TAG, "Retrieving codec-specific data for " + name + " is not supported"); + return null; + } + } + + /** + * Extracts H.264 codec-specific data (SPS and PPS) from MediaFormat instance + * + * The codec-specific data is in byte-stream format; 4-byte start codes (0x00 0x00 0x00 0x01) + * are added in front of SPS and PPS NAL units. + * + * @param format MediaFormat instance retrieved from MediaCodec + * @return byte array containing codec-specific data, or null if an error occurred + */ + private static byte[] getAVCCodecSpecificData(MediaFormat format) { + // For H.264, "csd-0" contains SPS and "csd-1" contains PPS. Refer to the documentation + // of MediaCodec. + if (!(format.containsKey("csd-0") && format.containsKey("csd-1"))) { + Log.w(TAG, "H264 codec specific data not found"); + return null; + } + + ByteBuffer sps = format.getByteBuffer("csd-0"); + int spsLen = sps.remaining(); + ByteBuffer pps = format.getByteBuffer("csd-1"); + int ppsLen = pps.remaining(); + + byte[] output = new byte[spsLen + ppsLen]; + try { + sps.get(output, 0, spsLen); + pps.get(output, spsLen, ppsLen); + } catch (Exception e) { + // should not happen + Log.w(TAG, "Error while copying H264 codec specific data: " + e); + return null; + } + + return output; + } + + private EncoderUtils() {} +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/encoder/SdlEncoder.java b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/SdlEncoder.java new file mode 100644 index 000000000..6c9198cb4 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/SdlEncoder.java @@ -0,0 +1,241 @@ +package com.smartdevicelink.encoder; + +import java.io.IOException; +import java.io.PipedOutputStream; +import java.nio.ByteBuffer; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Build; +import android.util.Log; +import android.view.Surface; + +import com.smartdevicelink.proxy.interfaces.IVideoStreamListener; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) +public class SdlEncoder { + + private static final String TAG = "SdlEncoder"; + + // parameters for the encoder + private static final String _MIME_TYPE = "video/avc"; // H.264/AVC video + private static final long KEEPALIVE_INTERVAL_MSEC = 100; + + // private static final String MIME_TYPE = "video/mp4v-es"; //MPEG4 video + private int frameRate = 30; + private int frameInterval = 5; + private int frameWidth = 800; + private int frameHeight = 480; + private int bitrate = 6000000; + + // encoder state + private MediaCodec mEncoder; + private PipedOutputStream mOutputStream; + private IVideoStreamListener mOutputListener; + private long mLastEmittedFrameTimestamp; + + // allocate one of these up front so we don't need to do it every time + private MediaCodec.BufferInfo mBufferInfo; + + // Codec-specific data (SPS and PPS) + private byte[] mH264CodecSpecificData = null; + + public SdlEncoder () { + } + public void setFrameRate(int iVal){ + frameRate = iVal; + } + public void setFrameInterval(int iVal){ + frameInterval = iVal; + } + public void setFrameWidth(int iVal){ + frameWidth = iVal; + } + public void setFrameHeight(int iVal){ + frameHeight = iVal; + } + public void setBitrate(int iVal){ + bitrate = iVal; + } + public void setOutputStream(PipedOutputStream mStream){ + mOutputStream = mStream; + } + public void setOutputListener(IVideoStreamListener listener) { + mOutputListener = listener; + } + public Surface prepareEncoder () { + + mBufferInfo = new MediaCodec.BufferInfo(); + + MediaFormat format = MediaFormat.createVideoFormat(_MIME_TYPE, frameWidth, + frameHeight); + + // Set some properties. Failing to specify some of these can cause the + // MediaCodec + // configure() call to throw an unhelpful exception. + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); + format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, frameInterval); + + // Create a MediaCodec encoder, and configure it with our format. Get a + // Surface + // we can use for input and wrap it with a class that handles the EGL + // work. + // + // If you want to have two EGL contexts -- one for display, one for + // recording -- + // you will likely want to defer instantiation of CodecInputSurface + // until after the + // "display" EGL context is created, then modify the eglCreateContext + // call to + // take eglGetCurrentContext() as the share_context argument. + try { + mEncoder = MediaCodec.createEncoderByType(_MIME_TYPE); + } catch (Exception e) {e.printStackTrace();} + + if(mEncoder != null) { + mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + return mEncoder.createInputSurface(); + } else { + return null; + } + } + + public void startEncoder () { + if(mEncoder != null) { + mEncoder.start(); + } + } + + /** + * Releases encoder resources. + */ + public void releaseEncoder() { + if (mEncoder != null) { + mEncoder.stop(); + mEncoder.release(); + mEncoder = null; + } + if (mOutputStream != null) { + try { + mOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + mOutputStream = null; + } + mH264CodecSpecificData = null; + } + + /** + * Extracts all pending data from the encoder + * <p> + * If endOfStream is not set, this returns when there is no more data to + * drain. If it is set, we send EOS to the encoder, and then iterate until + * we see EOS on the output. Calling this with endOfStream set should be + * done once, right before stopping the muxer. + */ + public void drainEncoder(boolean endOfStream) { + final int TIMEOUT_USEC = 10000; + + if(mEncoder == null || (mOutputStream == null && mOutputListener == null)) { + return; + } + if (endOfStream) { + mEncoder.signalEndOfInputStream(); + } + + ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers(); + while (true) { + int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, + TIMEOUT_USEC); + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + // no output available yet + if (!endOfStream) { + trySendVideoKeepalive(); + break; // out of while + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + // not expected for an encoder + encoderOutputBuffers = mEncoder.getOutputBuffers(); + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + if (mH264CodecSpecificData == null) { + MediaFormat format = mEncoder.getOutputFormat(); + mH264CodecSpecificData = EncoderUtils.getCodecSpecificData(format); + } else { + Log.w(TAG, "Output format change notified more than once, ignoring."); + } + } else if (encoderStatus < 0) { + } else { + if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // If we already retrieve codec specific data via OUTPUT_FORMAT_CHANGED event, + // we do not need this data. + if (mH264CodecSpecificData != null) { + mBufferInfo.size = 0; + } else { + Log.i(TAG, "H264 codec specific data not retrieved yet."); + } + } + + if (mBufferInfo.size != 0) { + ByteBuffer encoderOutputBuffer = encoderOutputBuffers[encoderStatus]; + byte[] dataToWrite = null; + int dataOffset = 0; + + // append SPS and PPS in front of every IDR NAL unit + if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 + && mH264CodecSpecificData != null) { + dataToWrite = new byte[mH264CodecSpecificData.length + mBufferInfo.size]; + System.arraycopy(mH264CodecSpecificData, 0, + dataToWrite, 0, mH264CodecSpecificData.length); + dataOffset = mH264CodecSpecificData.length; + } else { + dataToWrite = new byte[mBufferInfo.size]; + } + + try { + encoderOutputBuffer.position(mBufferInfo.offset); + encoderOutputBuffer.limit(mBufferInfo.offset + mBufferInfo.size); + + encoderOutputBuffer.get(dataToWrite, dataOffset, mBufferInfo.size); + + emitFrame(dataToWrite); + } catch (Exception e) {} + } + + mEncoder.releaseOutputBuffer(encoderStatus, false); + + if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; // out of while + } + } + } + } + + private void trySendVideoKeepalive() { + if (mH264CodecSpecificData == null) { + return; + } + + try { + long timeSinceLastEmitted = System.currentTimeMillis() - mLastEmittedFrameTimestamp; + if (timeSinceLastEmitted >= KEEPALIVE_INTERVAL_MSEC) { + emitFrame(mH264CodecSpecificData); + } + } catch (IOException e) {} + } + + private void emitFrame(final byte[] dataToWrite) throws IOException { + if (mOutputStream != null) { + mOutputStream.write(dataToWrite, 0, mBufferInfo.size); + } else if (mOutputListener != null) { + mOutputListener.sendFrame( + dataToWrite, 0, dataToWrite.length, mBufferInfo.presentationTimeUs); + } + mLastEmittedFrameTimestamp = System.currentTimeMillis(); + } +}
\ No newline at end of file diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java new file mode 100644 index 000000000..6b7f2a686 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/encoder/VirtualDisplayEncoder.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2017 Livio, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following + * disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of the Livio Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.smartdevicelink.encoder; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Build; +import android.util.Log; +import android.view.Display; +import android.view.Surface; + +import com.smartdevicelink.proxy.interfaces.IVideoStreamListener; +import com.smartdevicelink.proxy.rpc.ImageResolution; +import com.smartdevicelink.proxy.rpc.VideoStreamingFormat; +import com.smartdevicelink.proxy.rpc.enums.VideoStreamingCodec; +import com.smartdevicelink.streaming.video.VideoStreamingParameters; + +import java.nio.ByteBuffer; + +@TargetApi(19) +@SuppressWarnings("NullableProblems") +public class VirtualDisplayEncoder { + private static final String TAG = "VirtualDisplayEncoder"; + private VideoStreamingParameters streamingParams = new VideoStreamingParameters(); + private DisplayManager mDisplayManager; + private volatile MediaCodec mVideoEncoder = null; + private volatile MediaCodec.BufferInfo mVideoBufferInfo = null; + private volatile Surface inputSurface = null; + private volatile VirtualDisplay virtualDisplay = null; + private IVideoStreamListener mOutputListener; + private Boolean initPassed = false; + private final Object STREAMING_LOCK = new Object(); + + // Codec-specific data (SPS and PPS) + private byte[] mH264CodecSpecificData = null; + + //For older (<21) OS versions + private Thread encoderThread; + + + /** + * Initialization method for VirtualDisplayEncoder object. MUST be called before start() or shutdown() + * Will overwrite previously set videoWeight and videoHeight + * @param context to create the virtual display + * @param outputListener the listener that the video frames will be sent through + * @param streamingParams parameters to create the virtual display and encoder + * @throws Exception if the API level is <19 or supplied parameters were null + */ + public void init(Context context, IVideoStreamListener outputListener, VideoStreamingParameters streamingParams) throws Exception { + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + Log.e(TAG, "API level of 19 required for VirtualDisplayEncoder"); + throw new Exception("API level of 19 required"); + } + + if (context == null || outputListener == null || streamingParams == null || streamingParams.getResolution() == null || streamingParams.getFormat() == null) { + Log.e(TAG, "init parameters cannot be null for VirtualDisplayEncoder"); + throw new Exception("init parameters cannot be null"); + } + + this.mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + + this.streamingParams.update(streamingParams); + + mOutputListener = outputListener; + + initPassed = true; + } + + @SuppressWarnings("unused") + public VideoStreamingParameters getStreamingParams(){ + return this.streamingParams; + } + + @SuppressWarnings("unused") + public void setStreamingParams(int displayDensity, ImageResolution resolution, int frameRate, int bitrate, int interval, VideoStreamingFormat format) { + this.streamingParams = new VideoStreamingParameters(displayDensity, frameRate, bitrate, interval, resolution, format); + } + + @SuppressWarnings("unused") + public void setStreamingParams(VideoStreamingParameters streamingParams) { + this.streamingParams = streamingParams; + } + + /** + * NOTE: Must call init() without error before calling this method. + * Prepares the encoder and virtual display. + */ + public void start() throws Exception { + if (!initPassed) { + Log.e(TAG, "VirtualDisplayEncoder was not properly initialized with the init() method."); + return; + } + if (streamingParams == null || streamingParams.getResolution() == null || streamingParams.getFormat() == null) { + return; + } + + synchronized (STREAMING_LOCK) { + + try { + inputSurface = prepareVideoEncoder(); + + // Create a virtual display that will output to our encoder. + virtualDisplay = mDisplayManager.createVirtualDisplay(TAG, + streamingParams.getResolution().getResolutionWidth(), streamingParams.getResolution().getResolutionHeight(), + streamingParams.getDisplayDensity(), inputSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION); + + startEncoder(); + + } catch (Exception ex) { + Log.e(TAG, "Unable to create Virtual Display."); + throw new RuntimeException(ex); + } + } + } + + public void shutDown() { + if (!initPassed) { + Log.e(TAG, "VirtualDisplayEncoder was not properly initialized with the init() method."); + return; + } + try { + if (encoderThread != null) { + encoderThread.interrupt(); + encoderThread = null; + } + + if (mVideoEncoder != null) { + mVideoEncoder.stop(); + mVideoEncoder.release(); + mVideoEncoder = null; + } + + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + if (inputSurface != null) { + inputSurface.release(); + inputSurface = null; + } + } catch (Exception ex) { + Log.e(TAG, "shutDown() failed"); + } + } + + private Surface prepareVideoEncoder() { + + if (streamingParams == null || streamingParams.getResolution() == null || streamingParams.getFormat() == null) { + return null; + } + + if (mVideoBufferInfo == null) { + mVideoBufferInfo = new MediaCodec.BufferInfo(); + } + + String videoMimeType = getMimeForFormat(streamingParams.getFormat()); + + MediaFormat format = MediaFormat.createVideoFormat(videoMimeType, streamingParams.getResolution().getResolutionWidth(), streamingParams.getResolution().getResolutionHeight()); + + // Set some required properties. The media codec may fail if these aren't defined. + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, streamingParams.getBitrate()); + format.setInteger(MediaFormat.KEY_FRAME_RATE, streamingParams.getFrameRate()); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, streamingParams.getInterval()); // seconds between I-frames + + + // Create a MediaCodec encoder and configure it. Get a Surface we can use for recording into. + try { + mVideoEncoder = MediaCodec.createEncoderByType(videoMimeType); + mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + Surface surface = mVideoEncoder.createInputSurface(); //prepared + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mVideoEncoder.setCallback(new MediaCodec.Callback() { + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + // nothing to do here + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { + try { + ByteBuffer encodedData = codec.getOutputBuffer(index); + if (encodedData != null) { + if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + info.size = 0; + } + if (info.size != 0) { + byte[] dataToWrite;// = new byte[info.size]; + int dataOffset = 0; + + // append SPS and PPS in front of every IDR NAL unit + if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 && mH264CodecSpecificData != null) { + dataToWrite = new byte[mH264CodecSpecificData.length + info.size]; + System.arraycopy(mH264CodecSpecificData, 0, dataToWrite, 0, mH264CodecSpecificData.length); + dataOffset = mH264CodecSpecificData.length; + } else { + dataToWrite = new byte[info.size]; + } + + encodedData.position(info.offset); + encodedData.limit(info.offset + info.size); + + encodedData.get(dataToWrite, dataOffset, info.size); + if (mOutputListener != null) { + mOutputListener.sendFrame(dataToWrite, 0, dataToWrite.length, info.presentationTimeUs); + } + } + + codec.releaseOutputBuffer(index, false); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + @Override + public void onError(MediaCodec codec, MediaCodec.CodecException e) { + e.printStackTrace(); + } + + @Override + public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + mH264CodecSpecificData = EncoderUtils.getCodecSpecificData(format); + } + }); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + //Implied from previous if check that this has to be older than Build.VERSION_CODES.LOLLIPOP + encoderThread = new Thread(new EncoderCompat()); + + } else { + Log.e(TAG, "Unable to start encoder. Android OS version " + Build.VERSION.SDK_INT + "is not supported"); + } + + return surface; + + } catch (Exception ex) { + ex.printStackTrace(); + } + return null; + } + + private void startEncoder() { + if (mVideoEncoder != null) { + mVideoEncoder.start(); + } + if (encoderThread != null) { + encoderThread.start(); + } + } + + public Display getVirtualDisplay() { + return virtualDisplay.getDisplay(); + } + + private String getMimeForFormat(VideoStreamingFormat format) { + if (format != null) { + VideoStreamingCodec codec = format.getCodec(); + if (codec != null) { + switch (codec) { //MediaFormat constants are only available in Android 21+ + case VP8: + return "video/x-vnd.on2.vp8"; //MediaFormat.MIMETYPE_VIDEO_VP8 + case VP9: + return "video/x-vnd.on2.vp9"; //MediaFormat.MIMETYPE_VIDEO_VP9 + case H264: //Fall through + default: + return "video/avc"; // MediaFormat.MIMETYPE_VIDEO_AVC + } + } + } + return null; + } + + private class EncoderCompat implements Runnable { + + @Override + public void run() { + try { + drainEncoder(false); + } catch (Exception e) { + Log.w(TAG, "Error attempting to drain encoder"); + } finally { + mVideoEncoder.release(); + } + } + + @SuppressWarnings("deprecation") + void drainEncoder(boolean endOfStream) { + if (mVideoEncoder == null || mOutputListener == null) { + return; + } + + if (endOfStream) { + mVideoEncoder.signalEndOfInputStream(); + } + + ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers(); + Thread currentThread = Thread.currentThread(); + while (!currentThread.isInterrupted()) { + int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, -1); + if(encoderStatus < 0){ + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + // no output available yet + if (!endOfStream) { + break; // out of while + } + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + // not expected for an encoder + encoderOutputBuffers = mVideoEncoder.getOutputBuffers(); + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + if (mH264CodecSpecificData == null) { + MediaFormat format = mVideoEncoder.getOutputFormat(); + mH264CodecSpecificData = EncoderUtils.getCodecSpecificData(format); + } else { + Log.w(TAG, "Output format change notified more than once, ignoring."); + } + } + } else { + if ((mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // If we already retrieve codec specific data via OUTPUT_FORMAT_CHANGED event, + // we do not need this data. + if (mH264CodecSpecificData != null) { + mVideoBufferInfo.size = 0; + } else { + Log.i(TAG, "H264 codec specific data not retrieved yet."); + } + } + + if (mVideoBufferInfo.size != 0) { + ByteBuffer encoderOutputBuffer = encoderOutputBuffers[encoderStatus]; + byte[] dataToWrite; + int dataOffset = 0; + + // append SPS and PPS in front of every IDR NAL unit + if ((mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 && mH264CodecSpecificData != null) { + dataToWrite = new byte[mH264CodecSpecificData.length + mVideoBufferInfo.size]; + System.arraycopy(mH264CodecSpecificData, 0, dataToWrite, 0, mH264CodecSpecificData.length); + dataOffset = mH264CodecSpecificData.length; + } else { + dataToWrite = new byte[mVideoBufferInfo.size]; + } + + try { + encoderOutputBuffer.position(mVideoBufferInfo.offset); + encoderOutputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size); + + encoderOutputBuffer.get(dataToWrite, dataOffset, mVideoBufferInfo.size); + + if (mOutputListener != null) { + mOutputListener.sendFrame(dataToWrite, 0, dataToWrite.length, mVideoBufferInfo.presentationTimeUs); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + mVideoEncoder.releaseOutputBuffer(encoderStatus, false); + + if ((mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; // out of while + } + } + } + } + } +} |