上次成功通過FFmpeg採集攝像頭的YUV資料,這次針對上一次的程式進行了改造,使用H264編碼採集後的資料。
(傳送門) JavaCV FFmpeg採集攝像頭YUV資料
採集攝像頭資料是一個解碼過程,而將採集後的資料進行H264編碼則是編碼過程,如圖:
從上圖可以看出,編碼過程,資料流是從AVFrame流向AVPacket,而解碼過程正好相反,資料流是從AVPacket流向AVFrame。
javacpp-ffmpeg依賴:
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
</dependency>
FFmpeg編碼的過程是解碼的逆過程,不過主線流程是類似的,如下圖:
基本上主要的步驟都是:
- 查詢編碼/解碼器
- 開啟編碼/解碼器
- 進行編碼/解碼
在FFmpeg的demo流程中其實還有建立流avformat_new_stream()
,寫入頭部資訊avformat_write_header()
和尾部資訊av_write_trailer()
等操作,這裡只是將YUV資料編碼成H264裸流,所以可以暫時不需要考慮這些操作。
將採集視訊流資料進行H264編碼的整體流程主要有以下幾個步驟:
- 採集視訊幀
- 將視訊幀轉化為YUV420P格式
- 構建H264編碼器
- 對視訊幀進行編碼
採集視訊幀
採集視訊流中的視訊幀在上一次採集YUV資料的時候已經實現了,主要是從AVFormatContext中用av_read_frame()
讀取視訊資料並進行解碼(avcodec_decode_video2()
),實現程式碼如下:
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;
}
這樣通過grab()方法就可以獲取到視訊流中的視訊幀了。
將視訊幀轉化為YUV420P格式
在進行H264編碼之前一定要確保視訊幀是YUV420P格式的,所以必須對採集到的視訊幀做一次轉化,用到的是FFmpeg的SwsContext
元件,下面的VideoConverter
是對SwsContext
封裝的元件,內部實現了AVFrame的填充及SwsContext
的初始化,使用方式如下:
// 1. 建立VideoConverter,指定轉化格式為AV_PIX_FMT_YUV420P
videoConverter = VideoConverter.create(videoWidth, videoHeight, pCodecCtx.pix_fmt(),
videoWidth, videoHeight, AV_PIX_FMT_YUV420P);
// 2. 對視訊幀進行轉化
videoConverter.scale(pFrame);
VideoConvert的scale方式,實際上也是呼叫了SwsContext的scale方法:
sws_scale(swsContext, new PointerPointer<>(pFrame), pFrame.linesize(),
0, srcSliceH, new PointerPointer<>(avFrame), avFrame.linesize());
構建H264編碼器
進行H264編碼之前需要構建H264編碼器,根據上面的流程圖利用avcodec_find_encoder()
和avcodec_alloc_context3()
實現編碼器的建立和引數配置,最後用avcodec_open()
開啟編碼器,完整的初始化程式碼如下:
public static VideoH264Encoder create(int width, int height, int fps, Map<String, String> opts)
throws FFmpegException {
VideoH264Encoder h = new VideoH264Encoder();
// 查詢H264編碼器
h.pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (h.pCodec == null) {
throw new FFmpegException("初始化 AV_CODEC_ID_H264 編碼器失敗");
}
// 初始化編碼器資訊
h.pCodecCtx = avcodec_alloc_context3(h.pCodec);
h.pCodecCtx.codec_id(AV_CODEC_ID_H264);
h.pCodecCtx.codec_type(AVMEDIA_TYPE_VIDEO);
h.pCodecCtx.pix_fmt(AV_PIX_FMT_YUV420P);
h.pCodecCtx.width(width);
h.pCodecCtx.height(height);
h.pCodecCtx.time_base().num(1);
h.pCodecCtx.time_base().den(fps);
// 其他引數設定
AVDictionary dictionary = new AVDictionary();
opts.forEach((k, v) -> {
avutil.av_dict_set(dictionary, k, v, 0);
});
h.ret = avcodec_open2(h.pCodecCtx, h.pCodec, dictionary);
if (h.ret < 0) {
throw new FFmpegException(h.ret, "avcodec_open2 編碼器開啟失敗");
}
h.pkt = new AVPacket();
return h;
}
引數說明
width:視訊的寬度
height:視訊的高度
fps:視訊的幀率
opts:編碼器的其他引數設定
對視訊幀進行編碼
編碼器構建完成後就可以對視訊幀進行編碼了,入參為AVFrame,出參為byte[](這裡也可以是AVPacket,由於需要將H264裸流寫入檔案,這裡直接返回byte陣列)
public byte[] encode(AVFrame avFrame) throws FFmpegException {
if (avFrame == null) {
return null;
}
byte[] bf = null;
try {
avFrame.format(pCodecCtx.pix_fmt());
avFrame.width(pCodecCtx.width());
avFrame.height(pCodecCtx.height());
ret = avcodec_encode_video2(pCodecCtx, pkt, avFrame, got);
if (ret < 0) {
throw new FFmpegException(ret, "avcodec_encode_video2 編碼失敗");
}
if (got[0] != 0) {
bf = new byte[pkt.size()];
pkt.data().get(bf);
}
av_packet_unref(pkt);
} catch (Exception e) {
throw new FFmpegException(e.getMessage());
}
return bf;
}
最後只需要調整一下上一次的主程式,將讀取YUV資料的部分,調整為將AVFrame丟進編碼器,拉取byte陣列即可。
public static void main(String[] args) throws FFmpegException, IOException, InterruptedException {
int fps = 25;
avdevice_register_all();
av_register_all();
VideoGrabber g = new VideoGrabber();
g.open("Integrated Camera");
VideoH264Encoder encoder = VideoH264Encoder.create(g.getVideoWidth(), g.getVideoHeight(), fps);
OutputStream fos = new FileOutputStream("yuv420p.h264");
for (int i = 0; i < 200; i++) {
AVFrame avFrame = g.grab();
byte[] buf = encoder.encode(avFrame);
if (buf != null) {
fos.write(buf);
}
Thread.sleep(1000 / fps);
}
fos.flush();
fos.close();
encoder.release();
g.close();
}
最終採集效果(H264裸流)可以用VLC播放:
這裡對比一下,同樣的200幀YUV資料和H264資料的大小,相差還是很大的。
=========================================================
H264編碼原始碼可關注公眾號 “HiIT青年” 傳送 “ffmpeg-h264” 獲取。
關注公眾號,閱讀更多文章。