因为在工作中,接触到了视频相关的开发工作;同时,大多数android处理音视频多半都是有C++工程师提供处理库,所以,在这里记录一下我自己在工作中遇到的问题。
主要功能:
采集Android摄像头数据,实时编码H264,发送至指定地址(RTP/RTSP/RTMP等等,还有很多其他封装格式,可自行研究)。
本文使用了javacv来处理音视频数据,javacv是一套java封装的jni库,可以适用于本地多媒体(音视频)调用以及音视频,图片等文件后期操作(图片修改,音视频解码剪辑等等功能)。
javacv官方github维护地址:https://github.com/bytedeco/javacv
实现步骤:
- Android视频采集
- 视频编码为H264
- 建立管道流
PipedInputStream与PipedOutputStream建立连接,将PipedInputStream作为视频源,传给javacv。
PipedInputStream的大小默认为1024,大小视情况来定。(javacv内部读取视频数据是4096)
/**
* 开启编码与javacv录制的通道
*/
private Runnable sendRunnable = new Runnable() {
@Override
public void run() {
//管道流,编码数据与javacv录制建立
PipedInputStream pipedInputStream = null;
try {
pipedInputStream = new PipedInputStream(1024 * 4);
pipedOutputStream = new PipedOutputStream(pipedInputStream);
} catch (IOException e) {
e.printStackTrace();
}
String mTag;
if (m_CameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
mTag = "front";
} else {
mTag = "back";
}
javaCvHelper = new JavaCvHelper(pipedInputStream, m_outPath, CameraHelper.this, mTag);
}
};
- 在编码输出位置,将H264数据写入管道流
在这个地方需要注意一下。javacv会默认发送第一帧信息帧(SPS+PPS),需要在这里将信息帧保留,并不发送第一帧,在后续的I帧前,发送信息帧(方便播放端快速解码显示)。
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
//存储SPS/PPS
if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
configbyte = outData;
}
//是关键帧,发送sps\pps
if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
LogUtil.e(TAG, "KEY_FRAME !");
mCount = 0;
if (!isFirstFrame) {
sendData(configbyte);
}
}
mCount++;
// LogUtil.e(TAG, "_FRAME " + mCount);
if (!isFirstFrame) {
sendData(outData);
}
isFirstFrame = false;
mEncoder.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, 12000);
}
/**
* 发送数据至javacv
*
* @param outData
*/
private void sendData(final byte[] outData) {
writeExecutor.execute(new Runnable() {
@Override
public void run() {
if (pipedOutputStream != null) {
try {
pipedOutputStream.write(outData);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
- javacv的处理
FFmpegFrameGrabberPlus是重写的FFmpegFrameGrabber;
修改了当视频源为InputStream是,注释了inputStream = new BufferedInputStream(inputStream);因为在测试过程中发现,BufferedInputStream会占用很多的缓存,导致内存暴涨。注释掉,就没什么问题了。
FFmpegFrameRecorderPlus是重写的FFmpegFrameRecorder;
增加了在recordPacket时,pts的同步计算问题。持续使用时间达2小时左右,可能出现视频播放延时60s,经分析,是pts和dts导致的。
如果是rtp地址, recorder.setFormat(“rtp”)即可;
如果是rtmp地址, recorder.setFormat(“flv”)即可;
如果是rtsp地址, recorder.setFormat(“rtsp”)即可;
…
/**
* 数据封装与发送
*
* @throws FrameGrabber.Exception
* @throws FFmpegFrameRecorder.Exception
*/
private void recordStream() throws FrameGrabber.Exception, FFmpegFrameRecorderPlus.Exception {
this.isRunning = true;
//数据采集
FFmpegFrameGrabberPlus grabber = new FFmpegFrameGrabberPlus(inStream);
grabber.setVideoOption("vcodec", "copy");
grabber.start();
LogUtil.d(TAG, "视频宽" + grabber.getImageWidth());
LogUtil.d(TAG, "视频高" + grabber.getImageHeight());
LogUtil.d(TAG, "视频帧率:" + grabber.getVideoFrameRate());
LogUtil.d(TAG, "视频比特率:" + grabber.getVideoBitrate());
//数据录制(推送)
FFmpegFrameRecorderPlus recorder = new FFmpegFrameRecorderPlus(outUrl, grabber.getImageWidth(), grabber.getImageHeight(), 0);
recorder.setFormat("rtp");
recorder.setVideoOption("preset", "ultrafast");
recorder.setVideoOption("tune", "zerolatency");
recorder.start(grabber.getFormatContext());
AVPacket avPacket = null;
int error_times = 0;
while (isRunning && error_times < 1) {
avPacket = grabber.grabPacket();
if (avPacket == null) {
LogUtil.d(TAG, "- Null AVPacket -");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
if (avPacket.flags() == avcodec.AV_PKT_FLAG_KEY) {
LogUtil.d(TAG, "- AVPacket -");
}
pts = System.nanoTime() / 1000;
avPacket.pts(pts);
avPacket.dts(pts);
error_times += (recorder.recordPacket(avPacket) ? 0 : 1);
avcodec.av_free_packet(avPacket);
avcodec.av_packet_unref(avPacket);
avutil.av_freep(avPacket);
}
recorder.stop();
grabber.stop();
mCloseLisener.onJavacvClosed(mTag);
LogUtil.d(TAG, "- Stop AVPacket -");
}
- 关于javacv内存释放的问题
avcodec.av_free_packet(avPacket);avcodec.av_packet_unref(avPacket);
avutil.av_freep(avPacket);。
因为有些时候只调用avcodec.av_free_packet(avPacket);是起作用的。都调用,也没什么影响。可以自己选择。
以上是我在工作中碰到的一些问题和解决方案,记录一下。