Skip to content

Instantly share code, notes, and snippets.

@selvan
Created March 20, 2021 01:38
Show Gist options
  • Save selvan/6de9aebfb5a13772d43e19763d22379b to your computer and use it in GitHub Desktop.
Save selvan/6de9aebfb5a13772d43e19763d22379b to your computer and use it in GitHub Desktop.
rtmppublisher - Drain encoder via call back + Set bitrate on the fly
package com.takusemba.rtmppublisher;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
class VideoEncoder implements Encoder {
// H.264 Advanced Video Coding
private static final String MIME_TYPE = "video/avc";
// 5 seconds between I-frames
private static final int IFRAME_INTERVAL = 5;
private boolean isEncoding = false;
private static final int TIMEOUT_USEC = 10000;
private Surface inputSurface;
private MediaCodec encoder;
private MediaCodec.BufferInfo bufferInfo;
private VideoHandler.OnVideoEncoderStateListener listener;
private long lastFrameEncodedAt = 0;
private long startStreamingAt = 0;
void setOnVideoEncoderStateListener(VideoHandler.OnVideoEncoderStateListener listener) {
this.listener = listener;
}
long getLastFrameEncodedAt() {
return lastFrameEncodedAt;
}
Surface getInputSurface() {
return inputSurface;
}
/**
* prepare the Encoder. call this before start the encoder.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
void prepare(int width, int height, int bitRate, int frameRate, long _startStreamingAt)
throws IOException {
startStreamingAt = _startStreamingAt;
this.bufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
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, IFRAME_INTERVAL);
encoder = MediaCodec.createEncoderByType(MIME_TYPE);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void drainViaCallback() {
encoder.setCallback(new MediaCodec.Callback() {
public void onError(MediaCodec codec, MediaCodec.CodecException exception) {
}
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
Log.d("VideoEncoder", ":: INFO_OUTPUT_FORMAT_CHANGED :::");
ByteBuffer sps = format.getByteBuffer("csd-0");
ByteBuffer pps = format.getByteBuffer("csd-1");
byte[] config = new byte[sps.limit() + pps.limit()];
sps.get(config, 0, sps.limit());
pps.get(config, sps.limit(), pps.limit());
listener.onVideoDataEncoded(config, config.length, 0);
}
public void onInputBufferAvailable(MediaCodec codec, int index) {
}
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
ByteBuffer encodedData = codec.getOutputBuffer(index);
if (encodedData == null) {
Log.d("VideoEncoder", "Encodeddata is null");
return;
}
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
info.size = 0;
encoder.releaseOutputBuffer(index, false);
return;
}
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
long currentTime = System.currentTimeMillis();
int timestamp = (int) (currentTime - startStreamingAt);
byte[] data = new byte[info.size];
encodedData.get(data, 0, info.size);
encodedData.position(info.offset);
listener.onVideoDataEncoded(data, info.size, timestamp);
lastFrameEncodedAt = currentTime;
encoder.releaseOutputBuffer(index, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
release();
}
}
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void start() {
drainViaCallback();
encoder.start();
isEncoding = true;
// drain();
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void setVideoBitrateOnFly(int bitrate) {
Bundle bundle = new Bundle();
bundle.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
encoder.setParameters(bundle);
}
@Override
public void stop() {
if (isEncoding()) {
encoder.signalEndOfInputStream();
}
}
@Override
public boolean isEncoding() {
return encoder != null && isEncoding;
}
void drain() {
HandlerThread handlerThread = new HandlerThread("VideoEncoder-drain");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
// keep running... so use a different thread.
while (isEncoding) {
if (encoder == null) return;
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
int inputBufferId = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
if (inputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.d("VideoEncoder", ":: INFO_OUTPUT_FORMAT_CHANGED :::");
MediaFormat newFormat = encoder.getOutputFormat();
ByteBuffer sps = newFormat.getByteBuffer("csd-0");
ByteBuffer pps = newFormat.getByteBuffer("csd-1");
byte[] config = new byte[sps.limit() + pps.limit()];
sps.get(config, 0, sps.limit());
pps.get(config, sps.limit(), pps.limit());
listener.onVideoDataEncoded(config, config.length, 0);
} else {
if (inputBufferId > 0) {
ByteBuffer encodedData = encoderOutputBuffers[inputBufferId];
if (encodedData == null) {
continue;
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0;
}
if (bufferInfo.size != 0) {
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
long currentTime = System.currentTimeMillis();
int timestamp = (int) (currentTime - startStreamingAt);
byte[] data = new byte[bufferInfo.size];
encodedData.get(data, 0, bufferInfo.size);
encodedData.position(bufferInfo.offset);
listener.onVideoDataEncoded(data, bufferInfo.size, timestamp);
lastFrameEncodedAt = currentTime;
}
encoder.releaseOutputBuffer(inputBufferId, false);
} else if (inputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
continue;
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
release();
}
});
}
private void release() {
if (encoder != null) {
isEncoding = false;
encoder.stop();
encoder.release();
encoder = null;
}
}
}
package com.takusemba.rtmppublisher;
import android.graphics.SurfaceTexture;
import android.opengl.EGLContext;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.io.IOException;
class VideoHandler implements CameraSurfaceRenderer.OnRendererStateChangedListener {
private static final int FRAME_RATE = 30;
/**
* note that to use {@link VideoEncoder} and {@link VideoRenderer} from handler.
*/
private Handler handler;
private VideoEncoder videoEncoder;
private VideoRenderer videoRenderer;
interface OnVideoEncoderStateListener {
void onVideoDataEncoded(byte[] data, int size, int timestamp);
}
void setOnVideoEncoderStateListener(OnVideoEncoderStateListener listener) {
videoEncoder.setOnVideoEncoderStateListener(listener);
}
VideoHandler() {
this.videoRenderer = new VideoRenderer();
this.videoEncoder = new VideoEncoder();
HandlerThread handlerThread = new HandlerThread("VideoHandler");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
void start(final int width, final int height, final int bitRate,
final EGLContext sharedEglContext, final long startStreamingAt) {
handler.post(new Runnable() {
@Override
public void run() {
try {
videoEncoder.prepare(width, height, bitRate, FRAME_RATE, startStreamingAt);
videoEncoder.start();
videoRenderer.initialize(sharedEglContext, videoEncoder.getInputSurface());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Log.d("Trace", "Adaptive bitrate is set to 200");
// videoEncoder.setVideoBitrateOnFly(200);
}
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
});
}
void stop() {
handler.post(new Runnable() {
@Override
public void run() {
if (videoEncoder.isEncoding()) {
videoEncoder.stop();
}
if (videoRenderer.isInitialized()) {
videoRenderer.release();
}
}
});
}
@Override
public void onSurfaceCreated(SurfaceTexture surfaceTexture) {
// no-op`
}
@Override
public void onFrameDrawn(final int textureId, final float[] transform, final long timestamp) {
handler.post(new Runnable() {
@Override
public void run() {
long elapsedTime = System.currentTimeMillis() - videoEncoder.getLastFrameEncodedAt();
if (!videoEncoder.isEncoding() || !videoRenderer.isInitialized()
|| elapsedTime < getFrameInterval()) {
return;
}
videoRenderer.draw(textureId, transform, timestamp);
}
});
}
private long getFrameInterval() {
return 1000 / FRAME_RATE;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment