接到任务需要把摄像头视频流保存到本地MP4,javacv中整合了ffmpeg和opencv的API,可以在java中很方便的调用。javacv版本为1.5.7,按照资料代码逻辑很简单,如下:
FFmpegLogCallback.set();
String url = "rtmp://10.0.24.23/live/3001";
//创建一个拉流器,url可以是音视频文件或流媒体地址
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
grabber.start();
String localUrl = "/tmp/javacv.mp4";
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(localUrl, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
recorder.setFormat("mp4");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setVideoBitrate(grabber.getVideoBitrate());
recorder.start();
LocalDateTime startTime = LocalDateTime.now();
Frame frame;
while (startTime.plusSeconds(200).compareTo(LocalDateTime.now()) > 0
&& (frame = grabber.grab()) != null) {
recorder.record(frame);
}
recorder.stop();
grabber.stop();
测试视频流是文件、rtsp时都可以正常录制,但使用rtmp时出现了报错:
Caused by: org.bytedeco.javacv.FFmpegFrameRecorder$Exception: avcodec_send_frame() error -541478725: Error sending a video frame for encoding. (For more details, make sure FFmpegLogCallback.set() has been called.)
at org.bytedeco.javacv.FFmpegFrameRecorder.recordImage(FFmpegFrameRecorder.java:1056) ~[javacv-1.5.7.jar:1.5.7]
at org.bytedeco.javacv.FFmpegFrameRecorder.record(FFmpegFrameRecorder.java:961) ~[javacv-1.5.7.jar:1.5.7]
at org.bytedeco.javacv.FFmpegFrameRecorder.record(FFmpegFrameRecorder.java:954) ~[javacv-1.5.7.jar:1.5.7]
at cn.sibat.cvtest.MainThread.run(MainThread.java:70) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771) [spring-boot-2.7.2.jar:2.7.2]
... 5 common frames omitted
实际出错的源码位置:
public synchronized boolean recordImage(int width, int height, int depth, int channels, int stride, int pixelFormat, Buffer ... image) throws Exception {
try (PointerScope scope = new PointerScope()) {
...省略...
/* encode the image */
picture.quality(video_c.global_quality());
/* 此处判断了avcodec_send_frame 函数返回小于0则直接抛出异常 */
if ((ret = avcodec_send_frame(video_c, image == null || image.length == 0 ? null : picture)) < 0
&& image != null && image.length != 0) {
throw new Exception("avcodec_send_frame() error " + ret + ": Error sending a video frame for encoding.");
}
picture.pts(picture.pts() + 1); // magic required by libx264
...省略...
}
avcodec_send_frame() 是ffmpeg的视频编码函数,函数返回-541478725(AVERROR_EOF)的意思是检测到了文件结束,但实际流并没有结束。此处javacv判断我们要在一个已经结束的流中进行编码,于是直接抛出了异常。开始以为是我本地rtmp推流的问题,但是换了一个在网上找的开放的rtmp流(伊拉克 Al Sharqiya 电视台:rtmp://ns8.indexforce.com/home/mystream)问题依旧。简单记录下排查流程。
1. 在github上,提交个issues
issues地址:https://github.com/bytedeco/javacv/issues/1858
javacv作者响应很快,但并没有明确定位到问题。详细信息可以去issues中查看,后续也可能会更新。
2. 尝试降低版本
V1.5.7 --> V1.5.6:没有变化
V1.5.7 --> V1.5.4:出现了新的异常信息:avcodec_encode_video2() error -542398533: Could not encode video packet.
avcodec_encode_video2()是ffmpeg 3.1版本之前用于视频编码的函数,3.1版本以后替换成了avcodec_send_frame()
V1.5.7 --> V1.5.5:没有报错,录制成功!
根据1.5.4版本的报错,发现一个非常类似的情况:https://github.com/bytedeco/javacv/issues/1563
该BUG在1.5.5修复了,猜测这就是1.5.5版本可以成功的原因。但是1.5.6开始,引用的ffmpeg中编码函数改为avcodec_send_frame(),重新引入了1.5.4中类似的问题。
3. 代码优化
虽然1.5.5可以暂时实现功能,但没有找到具体问题点,心里不踏实,所以花了2天时间,在代码继续做一些debug,试图找到什么突破。
while (startTime.plusSeconds(200).compareTo(LocalDateTime.now()) > 0
&& (frame = grabber.grab()) != null) {
System.out.println(frame.getTypes());
recorder.record(frame);
}
[DATA]
[AUDIO]
[VIDEO]
[AUDIO]
[VIDEO]
[AUDIO]
...
将每次拉流后,获取的frame类型打印出来,发现只有第一帧是[DATA]类型,其他帧是[VIDEO]或[AUDIO]类型。
当把第一帧忽略掉后,视频就可以正常录制了!最终代码:
FFmpegLogCallback.set();
String url = "rtmp://10.0.24.23/live/3001";
//创建一个拉流器,url可以是音视频文件或流媒体地址
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
grabber.start();
String localUrl = "/tmp/javacv.mp4";
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(localUrl, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
recorder.setFormat("mp4");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setVideoBitrate(grabber.getVideoBitrate());
recorder.start();
LocalDateTime startTime = LocalDateTime.now();
Frame frame = grabber.grab();
System.out.println("忽略掉第一帧,类型为:" + frame.getTypes());
while (startTime.plusSeconds(200).compareTo(LocalDateTime.now()) > 0
&& (frame = grabber.grab()) != null) {
recorder.record(frame);
}
recorder.stop();
grabber.stop();
由于不了解RTMP协议原理,不清楚第一帧在这个过程中起到的作用,猜测可能就是javacv在处理这一“DATA”帧时出现了判断错误。