Created
March 20, 2021 01:38
-
-
Save selvan/6de9aebfb5a13772d43e19763d22379b to your computer and use it in GitHub Desktop.
rtmppublisher - Drain encoder via call back + Set bitrate on the fly
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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