• javacpp-FFmpeg系列之2:通用拉流解码器,支持视频拉流解码并转换为YUV、BGR24或RGB24等图像像素数据


    javacpp-ffmpeg系列:

    javacpp-FFmpeg系列之1:视频拉流解码成YUVJ420P,并保存为jpg图片

    javacpp-FFmpeg系列之2:通用拉流解码器,支持视频拉流解码并转换为YUV、BGR24或RGB24等图像像素数据

    javacpp-FFmpeg系列之3: 图像数据转换(BGR与BufferdImage互转,RGB与BufferdImage互转)

    前言:

    第一篇中视频解码成YUVJ420P图像像素数据(以下简称YUV或YUV数据),只是YUV在流媒体协议中用的较多(数据少,节省流量带宽),在图像处理应用较多的是BGR和RGB像素数据。我们已经获取到了YUV数据,那么把YUV转成BGR或者RGB需要再进行一次转换,显然性能上的表现并不是很好,所以本篇会通过编写一个通用转换器来介绍如何使用ffmpeg解码转出BGR、RGB、YUV等像素数据。

    补充:

    (1)为什么暂时没有用python实现,主要是先熟悉ffmpeg的API,后面会出的

    (2)为什么要转成BGR、RGB像素数据,因为有了这俩其中一个就可以直接生成java的BufferImage了啊,最重要的是我们本意不是要转成BufferImage,而是直接编码成base64的图像数据啊

    (3)演示demo见下一章

    一、功能设计

    第一篇写的很简略(实际上是那一大坨代码,自己实在看不下去了qaq),直接参考ffmpeg原生C的API,不符合java语言编写习惯,所以本篇会对上篇代码进行一些简单的封装复用。

    功能上,会支持多种格式的像素数据(BGR、RGB、YUV等等);代码上,会对各个流程进行阐述分析。

    二、功能实现

    (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);
                }

    三、完整代码

    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);
    }

    四、小结

    本章主要详解ffmpeg拉流解码的各个流程,可以通过本章代码可以轻松实现不限于RGB/BGR/YUV的FFmpeg所支持的多种像素格式转换

  • 相关阅读:
    Redis 错误摘记篇
    搭建备份到业务迁移---mysql
    业务迁移---web
    业务迁移---redis
    redis 编译安装错误问题
    location 匹配规则 (NGINX)
    nginx虚拟目录实现两个后台使用
    零基础学python-19.10 生成器是单迭代器
    零基础学python-19.9 生成器函数与生成器表达式
    零基础学python-19.8 生成器表达式:当迭代器遇上列表解析
  • 原文地址:https://www.cnblogs.com/eguid/p/9667171.html
Copyright © 2020-2023  润新知