desc:h265 encode
parent
64a7cafe3f
commit
91054ba593
@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2014 Google Inc. All rights reserved.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.yinuo.library.vlc.encoder;
|
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
|
||||||
import android.media.MediaCodecInfo;
|
|
||||||
import android.media.MediaFormat;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Surface;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class wraps up the core components used for surface-input video encoding.
|
|
||||||
* <p>
|
|
||||||
* Once created, frames are fed to the input surface. Remember to provide the presentation
|
|
||||||
* time stamp, and always call drainEncoder() before swapBuffers() to ensure that the
|
|
||||||
* producer side doesn't get backed up.
|
|
||||||
* <p>
|
|
||||||
* This class is not thread-safe, with one exception: it is valid to use the input surface
|
|
||||||
* on one thread, and drain the output on a different thread.
|
|
||||||
*/
|
|
||||||
public class VideoEncoderCore extends MediaEncoderCore {
|
|
||||||
|
|
||||||
// TODO: these ought to be configurable as well
|
|
||||||
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
|
|
||||||
private static final int FRAME_RATE = 24; // 30fps
|
|
||||||
private static final int IFRAME_INTERVAL = 1; // 5 seconds between I-frames
|
|
||||||
private static final int BIT_RATE = 4000000;
|
|
||||||
|
|
||||||
private Surface mInputSurface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures encoder and muxer state, and prepares the input Surface.
|
|
||||||
*/
|
|
||||||
public VideoEncoderCore(AndroidMuxer muxer, int width, int height) {
|
|
||||||
super(muxer);
|
|
||||||
|
|
||||||
prepareEncoder(width, height);
|
|
||||||
|
|
||||||
// Create a MediaMuxer. We can't add the video track and start() the muxer here,
|
|
||||||
// because our MediaFormat doesn't have the Magic Goodies. These can only be
|
|
||||||
// obtained from the encoder after it has started processing data.
|
|
||||||
//
|
|
||||||
// We're not actually interested in multiplexing audio. We just want to convert
|
|
||||||
// the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
|
|
||||||
// mMuxer = new MediaMuxer(outputFile.toString(),
|
|
||||||
// MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prepareEncoder(int width, int height) {
|
|
||||||
mBufferInfo = new MediaCodec.BufferInfo();
|
|
||||||
|
|
||||||
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
|
|
||||||
|
|
||||||
// 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, BIT_RATE);
|
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
|
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
|
|
||||||
if (VERBOSE) Log.d(TAG, "format: " + format);
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
try {
|
|
||||||
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
|
||||||
mInputSurface = mEncoder.createInputSurface();
|
|
||||||
mEncoder.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the encoder's input surface.
|
|
||||||
*/
|
|
||||||
public Surface getInputSurface() {
|
|
||||||
return mInputSurface;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
drainEncoder(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
drainEncoder(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isSurfaceInput() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.yinuo.library.vlc.encoder;
|
||||||
|
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Surface;
|
||||||
|
|
||||||
|
import com.yinuo.library.vlc.PushHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class wraps up the core components used for surface-input video encoding.
|
||||||
|
* <p>
|
||||||
|
* Once created, frames are fed to the input surface. Remember to provide the presentation
|
||||||
|
* time stamp, and always call drainEncoder() before swapBuffers() to ensure that the
|
||||||
|
* producer side doesn't get backed up.
|
||||||
|
* <p>
|
||||||
|
* This class is not thread-safe, with one exception: it is valid to use the input surface
|
||||||
|
* on one thread, and drain the output on a different thread.
|
||||||
|
*/
|
||||||
|
public class VideoEncoderCoreHevc extends MediaEncoderCore {
|
||||||
|
|
||||||
|
// TODO: these ought to be configurable as well
|
||||||
|
private static final String MIME_TYPE = "video/hevc"; // H.264 Advanced Video Coding
|
||||||
|
private static final int FRAME_RATE = 24; // 30fps
|
||||||
|
private static final int IFRAME_INTERVAL = 1; // 5 seconds between I-frames
|
||||||
|
private static final int BIT_RATE = 4000000;
|
||||||
|
|
||||||
|
private Surface mInputSurface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures encoder and muxer state, and prepares the input Surface.
|
||||||
|
*/
|
||||||
|
public VideoEncoderCoreHevc(AndroidMuxer muxer, int width, int height) {
|
||||||
|
super(muxer);
|
||||||
|
prepareEncoder(width, height);
|
||||||
|
PushHelper.INSTANCE.startStream(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareEncoder(int width, int height) {
|
||||||
|
mBufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
|
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
|
||||||
|
|
||||||
|
// 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, BIT_RATE);
|
||||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
|
||||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
|
||||||
|
if (VERBOSE) Log.d(TAG, "format: " + format);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
try {
|
||||||
|
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
mInputSurface = mEncoder.createInputSurface();
|
||||||
|
mEncoder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoder's input surface.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Surface getInputSurface() {
|
||||||
|
return mInputSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int NAL_I = 19;
|
||||||
|
public static final int NAL_VPS = 32;
|
||||||
|
private byte[] vps_sps_pps_buf;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drainEncoder(boolean endOfStream) {
|
||||||
|
buildKeyFrame();
|
||||||
|
final int TIMEOUT_USEC = 10000;
|
||||||
|
if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
|
||||||
|
|
||||||
|
if (endOfStream && isSurfaceInput()) {
|
||||||
|
if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
|
||||||
|
mEncoder.signalEndOfInputStream();
|
||||||
|
}
|
||||||
|
byte[] mSpsPps = new byte[0];
|
||||||
|
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
|
||||||
|
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
|
||||||
|
|
||||||
|
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||||
|
// no output available yet
|
||||||
|
if (!endOfStream) {
|
||||||
|
} else {
|
||||||
|
if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
|
||||||
|
}
|
||||||
|
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||||
|
// not expected for an encoder
|
||||||
|
encoderOutputBuffers = mEncoder.getOutputBuffers();
|
||||||
|
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||||
|
// should happen before receiving buffers, and should only happen once
|
||||||
|
MediaFormat newFormat = mEncoder.getOutputFormat();
|
||||||
|
Log.d(TAG, "encoder output format changed: " + newFormat);
|
||||||
|
|
||||||
|
// now that we have the Magic Goodies, start the muxer
|
||||||
|
mTrackIndex = mMuxer.addTrack(newFormat);
|
||||||
|
} else if (encoderStatus < 0) {
|
||||||
|
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
|
||||||
|
encoderStatus);
|
||||||
|
// let's ignore it
|
||||||
|
} else {
|
||||||
|
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
|
||||||
|
if (encodedData == null) {
|
||||||
|
throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
|
||||||
|
" was null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mMuxer.isStarted()) {
|
||||||
|
mBufferInfo.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||||
|
// The codec config data was pulled out and fed to the muxer when we got
|
||||||
|
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
|
||||||
|
if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
|
||||||
|
mBufferInfo.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mBufferInfo.size != 0) {
|
||||||
|
// adjust the ByteBuffer values to match BufferInfo (not needed?)
|
||||||
|
encodedData.position(mBufferInfo.offset);
|
||||||
|
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
|
||||||
|
|
||||||
|
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
|
||||||
|
if (VERBOSE) {
|
||||||
|
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
|
||||||
|
mBufferInfo.presentationTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] outData = new byte[mBufferInfo.size];
|
||||||
|
encodedData.get(outData);
|
||||||
|
int offset = 4;
|
||||||
|
if (outData[2] == 0x01) {
|
||||||
|
offset = 3;
|
||||||
|
}
|
||||||
|
int type = (outData[offset] & 0x7E) >> 1;
|
||||||
|
if (type == NAL_VPS) {
|
||||||
|
vps_sps_pps_buf = outData;
|
||||||
|
} else if (type == NAL_I) {
|
||||||
|
byte[] newBuf = new byte[vps_sps_pps_buf.length + outData.length];
|
||||||
|
System.arraycopy(vps_sps_pps_buf, 0, newBuf, 0, vps_sps_pps_buf.length);
|
||||||
|
System.arraycopy(outData, 0, newBuf, vps_sps_pps_buf.length, outData.length);
|
||||||
|
outData = newBuf;
|
||||||
|
}
|
||||||
|
PushHelper.INSTANCE.pushData(outData, outData.length, mBufferInfo.presentationTimeUs / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
mEncoder.releaseOutputBuffer(encoderStatus, false);
|
||||||
|
|
||||||
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||||
|
if (!endOfStream) {
|
||||||
|
Log.w(TAG, "reached end of stream unexpectedly");
|
||||||
|
} else {
|
||||||
|
if (VERBOSE) Log.d(TAG, "end of stream reached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
drainEncoder(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
drainEncoder(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSurfaceInput() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue