前陣子使用利用樹莓派搭建了一個視訊監控平臺(傳送門),不過使用的是JavaCV封裝好的OpenCVFrameGrabber
和FFmpegFrameRecorder
。
其實在javacpp
專案集中有提供FFmpeg的JNI封裝,可以直接使用FFmpeg API的來處理音視訊資料,下面是一個簡單的案例,通過FFmpeg API採集攝像頭的YUV資料。
javacpp-ffmpeg依賴:
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
</dependency>
1. 查詢攝像頭裝置
要採集攝像頭的YUV資料,首先得知道攝像頭的裝置名稱,可以通過FFmpeg來查詢攝像頭裝置。
ffmpeg.exe -list_devices true -f dshow -i dummy
在我的電腦上結果顯示如下:
其中 “Integrated Camera” 就是攝像頭的裝置名稱。
2. 利用FFmpeg解碼
採集攝像頭資料即將攝像頭作為視訊流輸入,通過FFmpeg解碼獲取視訊幀,然後將視訊幀轉為YUV格式,最後將資料寫入檔案即可。
下面是FFmpeg解碼的流程:
3. 開發視訊幀採集器
根據FFmpeg的解碼流程,實現視訊幀採集器大概需要經過以下幾個步驟:
FFmpeg初始化
首先需要使用av_register_all()
這個函式完成編碼器和解碼器的初始化,只有初始化了編碼器和解碼器才能正常使用;另外要採集的是裝置,所以還需要呼叫avdevice_register_all()
完成初始化。
分配AVFormatContext
接著需要分配一個AVFormatContext,可以通過avformat_alloc_context()
來分配AVFormatContext。
pFormatCtx = avformat_alloc_context();
開啟視訊流
通過avformat_open_input()
來開啟視訊流,這裡需要注意的是input format要指定為dshow
,可以通過av_find_input_format("dshow")
獲取AVInputFormat物件。
ret = avformat_open_input(pFormatCtx, String.format("video=%s", input), av_find_input_format("dshow"), (AVDictionary) null);
查詢視訊流
需要注意的是,查詢視訊流之前需要呼叫avformat_find_stream_info()
,下面是查詢視訊流的程式碼:
ret = avformat_find_stream_info(pFormatCtx, (AVDictionary) null);
for (int i = 0; i < pFormatCtx.nb_streams(); i++) {
if (pFormatCtx.streams(i).codec().codec_type() == AVMEDIA_TYPE_VIDEO) {
videoIdx = i;
break;
}
}
開啟解碼器
可以通過視訊流來查詢解碼器,然後開啟解碼器,對視訊流進行解碼,Java程式碼如下:
pCodecCtx = pFormatCtx.streams(videoIdx).codec();
pCodec = avcodec_find_decoder(pCodecCtx.codec_id());
if (pCodec == null) {
throw new FFmpegException("沒有找到合適的解碼器:" + pCodecCtx.codec_id());
}
// 開啟解碼器
ret = avcodec_open2(pCodecCtx, pCodec, (AVDictionary) null);
if (ret != 0) {
throw new FFmpegException(ret, "avcodec_open2 解碼器開啟失敗");
}
採集視訊幀
最後就是採集視訊幀了,這裡需要注意的是採集攝像頭的視訊流解碼得到的不一定是YUV格式的視訊幀,所以需要對視訊幀進行轉化一下(videoConverter.scale(pFrame))。
public AVFrame grab() throws FFmpegException {
if (av_read_frame(pFormatCtx, pkt) >= 0 && pkt.stream_index() == videoIdx) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, got, pkt);
if (ret < 0) {
throw new FFmpegException(ret, "avcodec_decode_video2 解碼失敗");
}
if (got[0] != 0) {
return videoConverter.scale(pFrame);
}
av_packet_unref(pkt);
}
return null;
}
4. 將視訊幀資料寫入檔案
通過視訊解碼之後可以得到YUV格式的視訊幀,只需要將視訊幀的資料寫入檔案就可以完成整個攝像頭YUV資料的採集流程,RGB資料是存在AVFrame.data[0]中,而YUV格式的資料分三個地方儲存,Y資料存在AVFrame.data[0],U資料存在AVFrame.data[1],V資料存在AVFrame.data[2],其中U、V的數量是Y的1/4。
所以只需要根據YUV儲存的位置和容量取出資料即可:
int fps = 25;
Yuv420PGrabber g = new Yuv420PGrabber();
g.open("Integrated Camera");
byte[] y = new byte[g.getVideoWidth() * g.getVideoHeight()];
byte[] u = new byte[g.getVideoWidth() * g.getVideoHeight() / 4];
byte[] v = new byte[g.getVideoWidth() * g.getVideoHeight() / 4];
// 1280x720
OutputStream fos = new FileOutputStream("yuv420p.yuv");
for (int i = 0; i < 200; i ++) {
AVFrame avFrame = g.grab();
avFrame.data(0).get(y);
avFrame.data(1).get(u);
avFrame.data(2).get(v);
fos.write(y);
fos.write(u);
fos.write(v);
Thread.sleep(1000 / fps);
}
fos.flush();
fos.close();
g.close();
5. 播放採集的YUV資料
採集的YUV資料可以通過YUV Player Deluxe,效果如下:
也可以通過ffplay來播放,命令如下
ffplay.exe -f rawvideo -video_size 1280x720 yuv420p.yuv
效果如下:
=========================================================
視訊幀採集器原始碼可關注公眾號 “HiIT青年” 傳送 “ffmpeg-yuv” 獲取。
關注公眾號,閱讀更多文章。