JavaCV FFmpeg H264編碼


上次成功通過FFmpeg采集攝像頭的YUV數據,這次針對上一次的程序進行了改造,使用H264編碼采集后的數據。

(傳送門) JavaCV FFmpeg采集攝像頭YUV數據

其他關於JavaCV的文章,可以通過下面的鏈接查看:
JavaCV-開發系列文章匯總篇(https://www.cnblogs.com/itqn/p/14696221.html)

采集攝像頭數據是一個解碼過程,而將采集后的數據進行H264編碼則是編碼過程,如圖:

從上圖可以看出,編碼過程,數據流是從AVFrame流向AVPacket,而解碼過程正好相反,數據流是從AVPacket流向AVFrame。

javacpp-ffmpeg依賴:

<dependency>
    <groupId>org.bytedeco.javacpp-presets</groupId>
    <artifactId>ffmpeg</artifactId>
    <version>${ffmpeg.version}</version>
</dependency>

FFmpeg編碼的過程是解碼的逆過程,不過主線流程是類似的,如下圖:

基本上主要的步驟都是:

  1. 查找編碼/解碼器
  2. 打開編碼/解碼器
  3. 進行編碼/解碼

在FFmpeg的demo流程中其實還有創建流avformat_new_stream(),寫入頭部信息avformat_write_header()和尾部信息av_write_trailer()等操作,這里只是將YUV數據編碼成H264裸流,所以可以暫時不需要考慮這些操作。

將采集視頻流數據進行H264編碼的整體流程主要有以下幾個步驟:

  1. 采集視頻幀
  2. 將視頻幀轉化為YUV420P格式
  3. 構建H264編碼器
  4. 對視頻幀進行編碼
采集視頻幀

采集視頻流中的視頻幀在上一次采集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” 獲取。(如果沒有收到回復,可能是你之前取消過關注。)

HiIT青年
關注公眾號,閱讀更多文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM