javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換爲YUV、BGR24或RGB24等圖像像素數據

javacpp-ffmpeg系列:java

javacpp-FFmpeg系列之1:視頻拉流解碼成YUVJ420P,並保存爲jpg圖片python

javacpp-FFmpeg系列之2:通用拉流解碼器,支持視頻拉流解碼並轉換爲YUV、BGR24或RGB24等圖像像素數據網絡

javacpp-FFmpeg系列之3: 圖像數據轉換(BGR與BufferdImage互轉,RGB與BufferdImage互轉)app

前言:ide

第一篇中視頻解碼成YUVJ420P圖像像素數據(如下簡稱YUV或YUV數據),只是YUV在流媒體協議中用的較多(數據少,節省流量帶寬),在圖像處理應用較多的是BGR和RGB像素數據。咱們已經獲取到了YUV數據,那麼把YUV轉成BGR或者RGB須要再進行一次轉換,顯然性能上的表現並非很好,因此本篇會經過編寫一個通用轉換器來介紹如何使用ffmpeg解碼轉出BGR、RGB、YUV等像素數據。性能

補充:ui

(1)爲何暫時沒有用python實現,主要是先熟悉ffmpeg的API,後面會出的this

(2)爲何要轉成BGR、RGB像素數據,由於有了這倆其中一個就能夠直接生成java的BufferImage了啊,最重要的是咱們本意不是要轉成BufferImage,而是直接編碼成base64的圖像數據啊編碼

(3)演示demo見下一章url

1、功能設計

第一篇寫的很簡略(其實是那一大坨代碼,本身實在看不下去了qaq),直接參考ffmpeg原生C的API,不符合java語言編寫習慣,因此本篇會對上篇代碼進行一些簡單的封裝複用。

功能上,會支持多種格式的像素數據(BGR、RGB、YUV等等);代碼上,會對各個流程進行闡述分析。

2、功能實現

(1)初始化

加載ffmpeg的網絡庫和編解碼庫,不初始化就無法用,適合放在靜態塊中進行加載

static {
        // Register all formats and codecs
        av_register_all();
        avformat_network_init();
    }

(2)打開視頻流

初始化AVFormatContext,主要就是根據url建立InputStream,而且會根據不一樣協議(rtmp/rtsp/hls/文件等)嘗試讀取一些文件頭數據(流媒體頭數據)。

補充:FileNotOpenException是繼承自RuntimeException的自定義異常類,只是加個名字方便標識異常而已,下面還會有幾個異常,都是繼承自RuntimeException的自定義異常類,如下不會再提

/**
     * 打開視頻流
     * @param url -url
     * @return
     * @throws FileNotOpenException
     */
    protected AVFormatContext openInput(String url) throws FileNotOpenException{
        AVFormatContext pFormatCtx = new AVFormatContext(null);
        if(avformat_open_input(pFormatCtx, url, null, null)==0) {
            return pFormatCtx;
        }
        throw new FileNotOpenException("Didn't open video file");
    }

(3)檢索流信息

上一步得到了AVFormatContext,這一步繼續根據AVFormatContext讀取一部分視音頻數據而且得到一些相關的信息

/**
     * 檢索流信息
     * @param pFormatCtx
     * @return
     */
    protected AVFormatContext findStreamInfo(AVFormatContext pFormatCtx) throws StreamInfoNotFoundException{
        if (avformat_find_stream_info(pFormatCtx, (PointerPointer<?>) null)>= 0) {
            return pFormatCtx;
        }
        throw new StreamInfoNotFoundException("Didn't retrieve stream information");
    }

(3)獲取視頻幀

上面兩步肯定了媒體文件/流的上下文,這一步嘗試讀取一幀視頻幀。

分紅兩個方法,先獲取視頻幀位置,而後根據位置獲取視頻幀,固然也能夠合成一個方法使用。

/**
     * 獲取第一幀視頻位置
     * @param pFormatCtx
     * @return
     */
    protected int findVideoStreamIndex(AVFormatContext pFormatCtx) {
        int i = 0, videoStream = -1;
        for (i = 0; i < pFormatCtx.nb_streams(); i++) {
            AVStream stream=pFormatCtx.streams(i);
            AVCodecContext codec=stream.codec();
            if (codec.codec_type() == AVMEDIA_TYPE_VIDEO) {
                videoStream = i;
                break;
            }
        }
        return videoStream;
    }

/**
     * 指定視頻幀位置獲取對應視頻幀
     * @param pFormatCtx
     * @param videoStream
     * @return
     */
    protected AVCodecContext findVideoStream(AVFormatContext pFormatCtx ,int videoStream)throws StreamNotFoundException {
        if(videoStream >=0) {
            // Get a pointer to the codec context for the video stream
            AVStream stream=pFormatCtx.streams(videoStream);
            AVCodecContext pCodecCtx = stream.codec();
            return pCodecCtx;
        }
        throw new StreamNotFoundException("Didn't open video file");
    }

(4)查找編解碼器

其實底層調用的仍是find_encdec(),遍歷AVCodec鏈表查找有沒有對應的編解碼器,若是沒有找到直接拋出異常,若是已經肯定編解碼,也能夠指定codec_id

/**
     * 查找並嘗試打開解碼器
     * @return
     */
    protected AVCodecContext findAndOpenCodec(AVCodecContext pCodecCtx) {
        // Find the decoder for the video stream
        AVCodec pCodec = avcodec_find_decoder(pCodecCtx.codec_id());
        if (pCodec == null) {
            System.err.println("Codec not found");
            throw new CodecNotFoundExpception("Codec not found");
        }
        AVDictionary optionsDict = null;
        // Open codec
        if (avcodec_open2(pCodecCtx, pCodec, optionsDict) < 0) {
            System.err.println("Could not open codec");
            throw new CodecNotFoundExpception("Could not open codec"); // Could not open codec
        }
        return pCodecCtx;
    }

(5.1)循環讀取視頻幀並解碼成yuv

這個沒什麼好講的了,前面的準備任務作完,就是一直循環讀取視頻幀,最後解碼出來的圖像幀都是yuv像素數據,這個顯然不是咱們想要的,因此要對這裏進行改動

// Allocate video frame
AVFrame pFrame = av_frame_alloc();

AVPacket packet = new AVPacket();

// Read frames and save first five frames to disk
            while (av_read_frame(pFormatCtx, packet) >= 0) {
                // Is this a packet from the video stream?
                if (packet.stream_index() == videoStream) {
                    // Decode video frame
                    avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet);
                    // Did we get a video frame?
                    if (frameFinished != null&&frameFinished[0] != 0) {

                          //ffmpeg默認解碼出來的是yuv數據
                          System.err.println(pFrame.data());
                    }
                }
                // Free the packet that was allocated by av_read_frame
                av_free_packet(packet);
            }

(5.2)循環讀取視頻幀並轉換成RGB或BGR圖像像素數據

// Allocate video frame
        AVFrame pFrame = av_frame_alloc();
        //Allocate an AVFrame structure
        AVFrame pFrameRGB = av_frame_alloc();

        width = pCodecCtx.width();
        height = pCodecCtx.height();
        pFrameRGB.width(width);
        pFrameRGB.height(height);
        pFrameRGB.format(fmt);

        // Determine required buffer size and allocate buffer
        int numBytes = avpicture_get_size(fmt, width, height);

        SwsContext sws_ctx = sws_getContext(width, height, pCodecCtx.pix_fmt(), width, height,fmt, SWS_BILINEAR, null, null, (DoublePointer) null);

        BytePointer buffer = new BytePointer(av_malloc(numBytes));
        // Assign appropriate parts of buffer to image planes in pFrameRGB
        // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
        // of AVPicture
        avpicture_fill(new AVPicture(pFrameRGB), buffer, fmt, width, height);
        AVPacket packet = new AVPacket();
        int[] frameFinished = new int[1];
       
            // Read frames and save first five frames to disk
            while (av_read_frame(pFormatCtx, packet) >= 0) {
                // Is this a packet from the video stream?
                if (packet.stream_index() == videoStream) {
                    // Decode video frame
                    avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet);
                    // Did we get a video frame?
                    if (frameFinished != null&&frameFinished[0] != 0) {
                        // Convert the image from its native format to BGR
                        sws_scale(sws_ctx, pFrame.data(), pFrame.linesize(), 0, height, pFrameRGB.data(),pFrameRGB.linesize());
                        //Convert BGR to ByteBuffer

//保存RGB或BGR數據
                        return saveFrame(pFrameRGB, width, height);
                    }
                }
                // Free the packet that was allocated by av_read_frame
                av_free_packet(packet);
            }

3、完整代碼

package cc.eguid.cv.corelib.videoimageshot.grabber;

import static org.bytedeco.javacpp.avcodec.*;
import static org.bytedeco.javacpp.avformat.*;
import static org.bytedeco.javacpp.avutil.*;
import static org.bytedeco.javacpp.swscale.*;


import java.io.IOException;
import java.nio.ByteBuffer;

import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.DoublePointer;
import org.bytedeco.javacpp.PointerPointer;

import cc.eguid.cv.corelib.videoimageshot.exception.CodecNotFoundExpception;
import cc.eguid.cv.corelib.videoimageshot.exception.FileNotOpenException;
import cc.eguid.cv.corelib.videoimageshot.exception.StreamInfoNotFoundException;
import cc.eguid.cv.corelib.videoimageshot.exception.StreamNotFoundException;

public abstract class GrabberTmplate {

    static {
        // Register all formats and codecs
        av_register_all();
        avformat_network_init();
    }
    //保留,暫不用
    protected int width;//寬度
    protected int height;//高度
    
    /**
     * 打開視頻流
     * @param url -url
     * @return
     * @throws FileNotOpenException
     */
    protected AVFormatContext openInput(String url) throws FileNotOpenException{
        AVFormatContext pFormatCtx = new AVFormatContext(null);
        if(avformat_open_input(pFormatCtx, url, null, null)==0) {
            return pFormatCtx;
        }
        throw new FileNotOpenException("Didn't open video file");
    }
    
    /**
     * 檢索流信息
     * @param pFormatCtx
     * @return
     */
    protected AVFormatContext findStreamInfo(AVFormatContext pFormatCtx) throws StreamInfoNotFoundException{
        if (avformat_find_stream_info(pFormatCtx, (PointerPointer<?>) null)>= 0) {
            return pFormatCtx;
        }
        throw new StreamInfoNotFoundException("Didn't retrieve stream information");
    }
    
    /**
     * 獲取第一幀視頻位置
     * @param pFormatCtx
     * @return
     */
    protected int findVideoStreamIndex(AVFormatContext pFormatCtx) {
        int i = 0, videoStream = -1;
        for (i = 0; i < pFormatCtx.nb_streams(); i++) {
            AVStream stream=pFormatCtx.streams(i);
            AVCodecContext codec=stream.codec();
            if (codec.codec_type() == AVMEDIA_TYPE_VIDEO) {
                videoStream = i;
                break;
            }
        }
        return videoStream;
    }
    
    /**
     * 指定視頻幀位置獲取對應視頻幀
     * @param pFormatCtx
     * @param videoStream
     * @return
     */
    protected AVCodecContext findVideoStream(AVFormatContext pFormatCtx ,int videoStream)throws StreamNotFoundException {
        if(videoStream >=0) {
            // Get a pointer to the codec context for the video stream
            AVStream stream=pFormatCtx.streams(videoStream);
            AVCodecContext pCodecCtx = stream.codec();
            return pCodecCtx;
        }
        throw new StreamNotFoundException("Didn't open video file");
    }
    
    /**
     * 查找並嘗試打開解碼器
     * @return
     */
    protected AVCodecContext findAndOpenCodec(AVCodecContext pCodecCtx) {
        // Find the decoder for the video stream
        AVCodec pCodec = avcodec_find_decoder(pCodecCtx.codec_id());
        if (pCodec == null) {
            System.err.println("Codec not found");
            throw new CodecNotFoundExpception("Codec not found");
        }
        AVDictionary optionsDict = null;
        // Open codec
        if (avcodec_open2(pCodecCtx, pCodec, optionsDict) < 0) {
            System.err.println("Could not open codec");
            throw new CodecNotFoundExpception("Could not open codec"); // Could not open codec
        }
        return pCodecCtx;
    }
    

   /**
     * 抓取視頻幀(默認跳過音頻幀和空幀)
     * @param url -視頻源(rtsp/rtmp/hls/文件等等)
     * @param fmt - 像素格式,好比AV_PIX_FMT_BGR24
     * @return
     * @throws IOException
     */
    public ByteBuffer grabVideoFrame(String url,int fmt) throws IOException {
        // Open video file
        AVFormatContext pFormatCtx=openInput(url);

        // Retrieve stream information
        pFormatCtx=findStreamInfo(pFormatCtx);

        // Dump information about file onto standard error
        //av_dump_format(pFormatCtx, 0, url, 0);

        //Find a video stream
        int videoStream=findVideoStreamIndex(pFormatCtx);
        AVCodecContext pCodecCtx =findVideoStream(pFormatCtx,videoStream);
        
        // Find the decoder for the video stream
        pCodecCtx= findAndOpenCodec(pCodecCtx);

        // Allocate video frame
        AVFrame pFrame = av_frame_alloc();
        //Allocate an AVFrame structure
        AVFrame pFrameRGB = av_frame_alloc();

        width = pCodecCtx.width();
        height = pCodecCtx.height();
        pFrameRGB.width(width);
        pFrameRGB.height(height);
        pFrameRGB.format(fmt);

        // Determine required buffer size and allocate buffer
        int numBytes = avpicture_get_size(fmt, width, height);

        SwsContext sws_ctx = sws_getContext(width, height, pCodecCtx.pix_fmt(), width, height,fmt, SWS_BILINEAR, null, null, (DoublePointer) null);

        BytePointer buffer = new BytePointer(av_malloc(numBytes));
        // Assign appropriate parts of buffer to image planes in pFrameRGB
        // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
        // of AVPicture
        avpicture_fill(new AVPicture(pFrameRGB), buffer, fmt, width, height);
        AVPacket packet = new AVPacket();
        int[] frameFinished = new int[1];
        try {
            // Read frames and save first five frames to disk
            while (av_read_frame(pFormatCtx, packet) >= 0) {
                // Is this a packet from the video stream?
                if (packet.stream_index() == videoStream) {
                    // Decode video frame
                    avcodec_decode_video2(pCodecCtx, pFrame, frameFinished, packet);
                    // Did we get a video frame?
                    if (frameFinished != null&&frameFinished[0] != 0) {
                        // Convert the image from its native format to BGR
                        sws_scale(sws_ctx, pFrame.data(), pFrame.linesize(), 0, height, pFrameRGB.data(),pFrameRGB.linesize());
                        //Convert BGR to ByteBuffer
                        return saveFrame(pFrameRGB, width, height);
                    }
                }
                // Free the packet that was allocated by av_read_frame
                av_free_packet(packet);
            }
            return null;
        }finally {
            //Don't free buffer
//            av_free(buffer);
            av_free(pFrameRGB);// Free the RGB image
            av_free(pFrame);// Free the YUV frame
            sws_freeContext(sws_ctx);//Free SwsContext
            avcodec_close(pCodecCtx);// Close the codec
            avformat_close_input(pFormatCtx);// Close the video file
        }
    }
    
    /**
     * BGR圖像幀轉字節緩衝區(BGR結構)
     *
     * @param pFrame
     *            -bgr圖像幀
     * @param width
     *            -寬度
     * @param height
     *            -高度
     * @return
     * @throws IOException
     */
    abstract ByteBuffer saveFrame(AVFrame pFrameRGB, int width, int height);
}

4、小結

本章主要詳解ffmpeg拉流解碼的各個流程,能夠經過本章代碼能夠輕鬆實現不限於RGB/BGR/YUV的FFmpeg所支持的多種像素格式轉換

相關文章
相關標籤/搜索