一文入门 FFmpeg 开发

注意: 本博客/笔记并不适合新手, 适合有一定的开发经验, 快速上手开发的老油条.

2018-11-15

[[N_OSI 模型.TCP 协议.封包]]
[[Z_Cpp]]

FFmpeg

官方 4.1 API 文档
官方wiki 页面
Github FFmpeg
官方教程 (很旧的版本)
linux平台 release下载
linux平台 dev下载
win平台 dev下载 //shard 包含dll/exe文件 //dev 包含lib/def/a和include头文件//
FFmpeg-Builds - git
雷神博客 目录
雷神博客 视音频编解码技术零基础学习方法

命令行工具

命令行说明文档
ffmpeg.exe 的源码

另外命令行工具默认是使用CPU 编解码处理的;

硬件支持

ffmpeg -hwaccels 可以列出可用的硬件方法:

λ ffmpeg  -hwaccels                      

Hardware acceleration methods:          
cuda                                     
dxva2                                    
qsv                                      
d3d11va                                  
opencl
vulkan

H265/264 硬件编解码器

h264_cuvid: h264硬件解码器
h264_nvenc: h264硬件编码器

hevc_cuvid: h265 硬件解码器
hevc_nvenc: h265 硬件编码器

转为H265编码要装 CUDA Toolkit

ffmpeg -i input.mp4 -vcodec hevc_nvenc ouput_h265.mp4


# 仅仅转为h265编码, 尽可能保持原视频质量

软转 使用CPU

ffmpeg -i input.mp4 -vcodec libx265 _hevc.mp4

访问本地摄像头

ffplay -x 800 -y 600 -f dshow -i video="RMONCAM A2 1080P"

访问本地摄像头 推rtmp

ffmpeg -f dshow -i video="RMONCAM A2 1080P" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -rtsp_transport tcp -f flv rtmp://127.0.0.1/live/wcl/

视频抽帧

ffmpeg -i in_video.mp4 ./ouput_path/frame_%d.png

每隔一秒截一张图

ffmpeg -i out.mp4 -f image2 -vf fps=fps=1 out%d.png

每隔20秒截一张图

ffmpeg -i out.mp4 -f image2 -vf fps=fps=1/20 out%d.png

合成视频

图片合成视频

ffmpeg -framerate 30 -i frame%d.png -c:v libx264 -r 30 output.mp4

合成webp

ffmpeg -framerate 3 -i  ./frame_%d.png -loop 0 -c:v libwebp  output.webp

framerate : 帧率
loop : 0=无限循环

抽取音频

提取音频数据 - 重新编码

ffmpeg -i input.mp4 -vn -c:a libmp3lame -q:a 1 output.mp3

单声道wav

ffmpeg -i input.mp4 -vn -f wav output.wav

本地视频 推 RTSP

将本地视频 推送到 rtsp 服务端

ffmpeg -re -stream_loop -1 -i C:\Users\yang\Videos\时间录屏20250213172059.mp4 -c copy -f rtsp rtsp://192.168.20.130:554/abc/mystream

模块lib说明

libavcodec provides implementation of a wider range of codecs.//提供编解码器的实现
libavformat implements streaming protocols, container formats and basic I/O access.//提供流协议, 格式容器和基本IO访问
libavutil includes hashers, decompressors and miscellaneous utility functions.//untils
libavfilter provides a mean to alter decoded Audio and Video through chain of filters.//滤镜?
libavdevice provides an abstraction to access capture and playback devices.//抽象访问设备
libswresample implements audio mixing and resampling routines.//实现音频混合和重新采样例程
libswscale implements color conversion and scaling routines.//实现图像 颜色转换和缩放

解/编码流程

FFmpeg源代码结构图 - 编码
FFmpeg源代码结构图 - 解码

解码

1. 打开输入

  • avformat_open_input
char *rtspUrl = "rtsp://admin:abc123COM@112.91.85.154:503/h264/ch0/sub/av_stream"
AVFormatContext *ifmt_ctx = nullptr //大概与封装格式对应的结构体
AVDictionary* options = nullptr;//一些配置
av_dict_set(&options, "rtsp_transport", "tcp", 0);//over tcp
av_dict_set(&options, "segment_list_flags", "+live ", 0); 

if((avformat_open_input(&ifmt_ctx,rtspUrl,0,&options)) != 0){
          qDebug()<<"打开输入 失败";
    throw new std::runtime_error("Could not open input stream? ");
}

2. 查找输入的视频信息

//获取信息
if((avformat_find_stream_info(ifmt_ctx,0))!=0){
        qDebug()<<"获取输入信息 失败";
    throw new std::runtime_error("Failed to retrieve input stream information?");
}

3. 获取得视频通道

一般两个; 音频/视频

int videoIndex = -1;//视频流 索引
int audioIndex = -1;//音频流 索引
 //流通道 一般两个; 音频/视频
for(unsigned int i=0; i<inAvFormatCtx->nb_streams; i++) {
    if(inAvFormatCtx->streams[i]->codecpar->codec_type==AVMediaType::AVMEDIA_TYPE_VIDEO){
        videoIndex = i;
    }else if(inAvFormatCtx->streams[i]->codecpar->codec_type==AVMediaType::AVMEDIA_TYPE_AUDIO){
        audioIndex  = i;
    }
}

4. 初始化解码器上下文

查找解码器/初始化

//根据输入流ID找
//AVCodec *codec = avcodec_find_decoder(inAvFormatCtx->streams[videoIndex]->codecpar->codec_id); 
//H264 解码器
AVCodec *codec = avcodec_find_decoder(AVCodecID::AV_CODEC_ID_H264);
AVCodecContext *coder_ctx  = avcodec_alloc_context3(codec);
//复制解码器设置 
//avcodec_copy_context( this->coderCtx, inAvFormatCtx->streams[videoIndex]->codec); 旧API已弃用
avcodec_parameters_to_context(coder_ctx, inAvFormatCtx->streams[videoIndex]->codecpar );
//重要! 打开一下
if (avcodec_open2(coder_ctx, codec,nullptr) < 0){
    qDebug()<<"avcodec_open2 失败";
    throw new std::runtime_error("avcodec_open2 ");
}

旧API avcodec_copy_context

avcodec_copy_context 函数(因语义不明确 被禁用?) 使用int avcodec_parameters_to_context(AVCodecContext *codec,const AVCodecParameters *par);代替 (或使用 avcodec_parameters_copy?)
如果反过来的 还有这个函数: int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);

4. 读取编码包(AVPacket) 解码(AVFrame)

AVPacket *avpkt = av_packet_alloc();//AVPacket 包含不大于一帧的原始数据
AVFrame *pFrame = av_frame_alloc();//AVFrame 解码后的一帧图片
while(true) {
  ret = av_read_frame(ifmt_ctx, avpkt);//读包 AVPacket
  if (ret < 0){
    qDebug()<<"没有编码包";
    continue;
  }
 char buf[1024];
  avcodec_send_packet(coder_ctx, avpkt);//发送 AVPacket 解码
  while (avcodec_receive_frame(coder_ctx, pFrame) == 0) {//接受解码输出 AVFrame, 注意要循环有可能不止一次输出(例如结尾的时候)
    qDebug()<<"解帧"<<i;
    snprintf(buf, sizeof(buf), "f:/test/QT_TM/fb%d.jpg",i);
    SaveFrame( pFrame, buf);
  }
  av_packet_unref(avpkt);//释放AVPacket 数据引用 
}

解码 H264帧包 (Javacv)

解码H264包含NALU的数据.

注意: 发送给解码器前, 要设置完 SPS/PPS!
avCodecContext.extradata(desc);

SPS/PPS

NALU 头部格式(1字节基本格式)

NALU 头部格式(1字节基本格式)

位位置 字段名 位数 描述
7 ​F​ 1 禁止位(Forbidden Zero Bit),必须为0,否则表示非法数据
6-5 ​NRI​ 2 NAL重要性指示(NAL Ref Idc),表示NAL单元的重要性(0-3)
4-0 ​Type​ 5 NAL单元类型(0-31),标识NAL单元的内容类型

SPS : 序列参数集 (Sequence Paramater Set) 记录了编码的 Profilelevel、图像宽高等
PPS: 图像参数集 (Picture Paramater Set) 每一帧编码后数据所依赖的参数保存于 PPS 中

一般情况 SPS 和 PPS 的 NAL Unit 通常位于整个码流的起始位置。 封装文件一般进保存一次,位于文件头部
in short 0X67 是SPS; 0X68 是PPS; 0X65 是I帧;

[[RTSP 协议]]

NAL单元类型 描述 常见场景
​未指定​ 0 保留值,未使用 -
​非IDR图像的编码条带​ 1 非关键帧的条带数据(P帧或B帧) 普通视频帧
​编码条带数据分割块A​ 2 包含宏块头和运动向量的数据块 数据分割流
​编码条带数据分割块B​ 3 包含帧内编码块模式和帧内系数的数据块 数据分割流
​编码条带数据分割块C​ 4 包含帧间编码块模式和帧间系数的数据块 数据分割流
​IDR图像的编码条带​ 5 关键帧数据(I帧),解码器从此开始重建图像 关键帧/场景切换
​SEI​ 6 补充增强信息(如时间码、字幕、编码参数等) 元数据嵌入
​SPS​ 7 序列参数集(包含视频分辨率、帧率、档次级别等全局参数) 视频流开头
​PPS​ 8 图像参数集(包含熵编码模式、切片组等帧级参数) SPS之后
​访问单元分隔符​ 9 标识视频帧的开始 流同步
​序列结尾​ 10 表示序列结束 视频流结尾
​码流结尾​ 11 表示码流结束 码流结尾
​填充数据​ 12 用于填充的数据 字节对齐
​保留​ 13-23 保留值 -
​未指定​ 24-31 保留值

profile 主要参数

java 代码片段

注意, 有很多是参数/ 结构体没有释放的,仅示例

  byte[] H264
  /**
   * 构建待解码包
   */
  AVPacket pkt = new AVPacket();
  if (av_new_packet(pkt, H264.length) < 0) {
    System.err.println(" av_new_packet err");;
  }
  BytePointer data = new BytePointer(H264);
  pkt = pkt.data( data);
  pkt.pts(AV_NOPTS_VALUE);
  pkt.dts(AV_NOPTS_VALUE);
/**
* 函数原型: 
* int avcodec_send_packet( AVCodecContext * 	avct, const AVPacket * 	Awpkt )<br>
* https://www.ffmpeg.org/doxygen/4.1/group__lavc__decoding.html#ga58bc4bf1e0ac59e27362597e467efff3
* <br>
* <br>
* 成功时为0, 
* 否则为负错误代码: AVERROR(EAGAIN): 当前状态下不接受输入 - 
* 用户必须使用avcodec_receive_frame()读取输出(读取所有输出后, 应重新发送数据包, 并且呼叫不会失败与EAGAIN); 
* <br>
* AVERROR_EOF: 已刷新解码器, 并且不会向其发送新数据包(如果发送的数据包超过1个, 也会返回)A
* VERROR(EINVAL): 编解码器未打开, 编码器或需要刷新
* AVERROR(ENOMEM): 无法将数据包添加到内部队列, 或类似的其他错误: 合法解码错误
* 
*/
int stat = avcodec_send_packet(avCodecContext,pkt);
BytePointer desc = new BytePointer(new byte[1024]);//面向过程 , 创建中间对象以接受错误描述, 指定缓冲大小, 也可以.. 		Pointer data = BytePointer.malloc(H264.length);
if(stat != 0) {
    av_strerror(stat, desc, 1024);//解析错误描述
    System.err.println("avcodec_send_packet:"+stat+", desc: "+desc.getString());
}
/**
* 函数原型: 
* int avcodec_receive_frame(AVCodecContext * 	avctx, AVFrame * 	frame )<br>
* 	https://www.ffmpeg.org/doxygen/4.1/group__lavc__decoding.html#ga11e6542c4e66d3028668788a1a74217c
* <br>
* <br>
* 0: 成功, 返回帧 
* AVERROR(EAGAIN): 输出在此状态下不可用 - 用户必须尝试发送新输入
* AVERROR_EOF: 解码器已完全刷新, 并且将不再有输出帧
* AVERROR(EINVAL): 编解码器未打开, 或者它是编码器其他负值: 合法解码错误
*/
AVFrame avFrame = av_frame_alloc() ;
int rec = avcodec_receive_frame(avCodecContext, avFrame );
if(rec != 0) {
    av_strerror(rec, desc, 1024);//解析错误描述
    System.err.println("avcodec_receive_frame:"+rec+", desc: "+desc.getString());
}
System.out.println("解码后信息: capacity="+avFrame.capacity() 
+", width="+avFrame.width()+", height="+avFrame.height()
+", 帧类型="+avFrame.pict_type()+", 图像类型"+avFrame.format());

AVFrame 转 Opencv的 Mat对象 (色彩/分辨率转换)

FFmpeg 库里面的 sws_scaleh函数: 可以在一个函数里面同时实现: 1.图像色彩空间转换; 2.分辨率缩放; 3.前后图像滤波处理;

  • 首先需要转换为Mat(opencv的一个图片对象), 以显示
void  (AVFrame avFrame_dec) throws IOException {
    int width = avFrame_dec.width();
    int height = avFrame_dec.height();
    /**
    * 待转换到 rgb帧数据的缓存
    * av_frame_alloc 并没有为AVFrame的像素数据分配空间; 
    * 因此AVFrame中的像素数据的空间需要自行分配空间, 例如使用 avpicture_fill(), av_image_fill_arrays()等函数; 
    */
    AVFrame avFrameRGB = av_frame_alloc();
    if (avFrameRGB == nullptr) {
        System.err.println("Can not pFrameRGB ");
        System.exit(-1);
    }
    /** 
      * 确定内存大小,再分配内存
      *  Determine required buffer size and allocate buffer
      */
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, 
        width, height, 1);
    BytePointer rgbData=new BytePointer(av_malloc(numBytes));
    av_image_fill_arrays(avFrameRGB.data(), avFrameRGB.linesize(),
        rgbData, AV_PIX_FMT_BGR24, width, height, 1);
    /**
      * avCodecContext.pix_fmt()
      * 初始化转换上下文, 同一个转换, SwsContext 上下文可复用.
      * 参数定义
      * 1~3 定义输入图像信息(寬, 高, 颜色空间(像素格式))
      * 3~6 定义输出图像信息寬, 高, 颜色空间(像素格式))
      * 参数int flags选择缩放算法(只有当输入输出图像大小不同时有效)
      * 参数SwsFilter *srcFilter, SwsFilter *dstFilter分别定义输入/输出图像滤波器信息, 如果不做前后图像滤波, 输入nullptr
      * 参数const double *param定义特定缩放算法需要的参数(?), 默认为nullptr
      * 参考 :https://www.cnblogs.com/yongdaimi/p/10715830.html
      */
    SwsContext  swsCtx  = sws_getContext(
        width, height, avCodecContext.pix_fmt(),
        width, height, AV_PIX_FMT_BGR24,//RGB & BGR 傻傻分不清!
        SWS_BILINEAR, nullptr, nullptr, (DoublePointer)nullptr);

Mat image=new Mat(height, width, CV_8UC3);//opencv的一个图片对象
int b = 0;
int g = 1;
int r = 2;
/**
  * 转换
  * 参数struct SwsContext *c, 为上面sws_getContext函数返回值
  * 参数const uint8_t *const srcSlice[], const int srcStride[]定义输入图像信息(当前处理区域的每个通道数据指针, 每个通道行字节数)
  *  stride定义下一行的起始位置; stride和width不一定相同, 这是因为: 
  *   1.由于数据帧存储的对齐, 有可能会向每行后面增加一些填充字节这样 stride = width + N;  
  *   2.packet色彩空间下, 每个像素几个通道数据混合在一起, 例如RGB24, 每个像素3字节连续存放, 因此下一行的位置需要跳过3*width字节;  
  * 参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域, srcSliceY是起始位置, srcSliceH是处理多少行; 如果srcSliceY=0, srcSliceH=height, 表示一次性处理完整个图像; 
  * 这种设置是为了多线程并行, 例如可以创建两个线程, 第一个线程处理 [0, h/2-1]行, 第二个线程处理 [h/2, h-1]行; 并行处理加快速度; 
  * 参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个通道数据指针, 每个通道行字节数)
  * 
  * 另:
  * // 释放sws_scale
  * void sws_freeContext(struct SwsContext *swsContext);
  */
sws_scale(swsCtx, avFrame_dec.data(), avFrame_dec.linesize(), 0, height, avFrameRGB.data(), avFrameRGB.linesize());
image.data(avFrameRGB.data(0));

  • 接上代码, 创建opencv hei_gui 窗口绘制 image
	/**
     * opencv hei_gui
     *  创建一个窗口
     * void namedWindow(const string & winname, int flags = WINDOW_AUTOSIZE)
     * 第一个参数为窗口的名称;
     * 第二个参数为窗口的标识 
     * WINDOW_NORMAL,设置这个值, 用户可以改变窗口的大小
     * WINDOW_AUTOSIZE,自适应改变大小, 用户不能改变
     * WINDOW_OPENGL支持openGL
     */
    final String winName = "haha";
    namedWindow(winName, WINDOW_NORMAL);
    resizeWindow(winName, 1080/2,1920/2);
    /**
     *显示图片,
      * 没有的话,以这个名称创建一个窗口
      * ,无限循环调用 	imshow(winName, image); 绘制视频
      */
    imshow(winName, image);
    /**
     * 后续事件
     * javacv 有 CanvasFrame 这个类, 它继承了JFrame, 但是绘制图片: https://blog.csdn.net/tryflys/article/details/78906722
     * 还是需要转成 java 的 BufferImage,
     * 
     * 出于性能效率的考虑,  opencv的窗口可设置事件回调, 所以可以绘制用 opencv的窗口, 其他控制按钮,用java swing..
     * OK: https://stackoverflow.com/questions/15161454/using-cvsetmousecallback-in-javacv
     * 为这个窗口注册事件
     */
      CvMouseCallback on_mouse = new CvMouseCallback() {
            @Override
            public void call(int event, int x, int y, int flags, Pointer param) {
                System.out.println("point = (" + x + ", " + y + "), event="+event);
            }
        };
      cvSetMouseCallback(winName, on_mouse, nullptr);
      
    if(waitKey(300000)==27){
      System.exit(0);
    }
    }
}

opencv Mat 转 BufferedImage

绘制在swing

package org.yang.javacv;

import static org.bytedeco.javacpp.opencv_highgui.imshow;
import static org.bytedeco.javacpp.opencv_highgui.waitKey;
import static org.bytedeco.javacpp.opencv_imgcodecs.imread;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.Buffer;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

import org.bytedeco.javacpp.opencv_core.Mat;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameConverter;
/**
 * 
 * @author yangfh
 * 2019年4月16日
 */
public class MatCv {
    private static Java2DFrameConverter frameConverter = new Java2DFrameConverter();
    private static OpenCVFrameConverter.ToMat toMatConverter = new OpenCVFrameConverter.ToMat();
	/**
	 * ref  Java2DFrameUtils 
	 * OpenCVFrameConverter
	 * @param args
	 * @author yangfh
	 * 2019年4月16日
	 */
	public static void main(String[] args) {
		String winname ="tttttttttttttttttttttt0";
		Mat mat = imread("f:/test/big_img.png");
		long time = System.currentTimeMillis();
		org.bytedeco.javacv.Frame frame = toMatConverter.convert(mat);
		BufferedImage bufimage = frameConverter.getBufferedImage(frame);
//		byte[] byteArray = new byte[ub.capacity()];
		System.out.println( System.currentTimeMillis() - time);
//		imshow(winname, mat);
//		waitKey(0);
		ImagePanel ip = new ImagePanel();
		ip.setSourceImage(bufimage);
		JFrame jFrame = new JFrame("draw geom");
		jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		jFrame.setSize(1024, 768);
		jFrame.add(ip);
		jFrame.setVisible(true);
	}

	public static class ImagePanel extends JPanel {
	    private static final long serialVersionUID = 1L;
	    private BufferedImage sourceImage;
	    private BufferedImage destImage;
	    public ImagePanel()
	    {
	        // do nothing
	    }
	    @Override
	    protected void paintComponent(Graphics g) {
	        Graphics2D g2d = (Graphics2D) g;
	        g2d.clearRect(0, 0, this.getWidth(), 
	                    this.getHeight());
	        if(sourceImage != nullptr)
	        {
	            g2d.drawImage(sourceImage, 0, 0, 
	            sourceImage.getWidth(), 
	            sourceImage.getHeight(), nullptr);
	        }
	    }

	    public void setSourceImage(BufferedImage sourceImage) {
	        this.sourceImage = sourceImage;
	    }
	}
}


编码

int width = pFrame->width;
int height = pFrame->height;
AVCodecContext *pCodeCtx = nullptr;

AVFormatContext *pFormatCtx = avformat_alloc_context();
// 1. 设置输出格式 可用: ffmpeg -formats 获取
pFormatCtx->oformat = av_guess_format("mjpeg", nullptr, nullptr);

// 2. 创建并初始化输出AVIOContext
if (avio_open(&pFormatCtx->pb, out_name, AVIO_FLAG_READ_WRITE) < 0) {
    printf("Couldn't open output file.");
    return -1;
}

// 3. 构建一个新stream
AVStream *pAVStream = avformat_new_stream(pFormatCtx, 0);
if (pAVStream == nullptr) {
    return -1;
}
/// 指定视频输出参数
AVCodecParameters *parameters = pAVStream->codecpar;
parameters->codec_id = pFormatCtx->oformat->video_codec;
parameters->codec_type = AVMEDIA_TYPE_VIDEO;
parameters->format = AV_PIX_FMT_YUVJ420P;
parameters->width = pFrame->width;
parameters->height = pFrame->height;
// 4. 找到编码器
AVCodec *pCodec = avcodec_find_encoder(pAVStream->codecpar->codec_id);
if (!pCodec) {
    printf("Could not find encoder\n");
    return -1;
}
// 5. 分配 codeCtx
pCodeCtx = avcodec_alloc_context3(pCodec);
if (!pCodeCtx) {
    fprintf(stderr, "Could not allocate video codec context\n");
    exit(1);
}
// 这是 复制一些参数 到 decoder context?
if ((avcodec_parameters_to_context(pCodeCtx, pAVStream->codecpar)) < 0) {
    fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n",
            av_get_media_type_string(AVMEDIA_TYPE_VIDEO));
    return -1;
}
 //基准帧率
pCodeCtx->time_base = (AVRational) {1, 25};

// 6. 打开输出 
if (avcodec_open2(pCodeCtx, pCodec, nullptr) < 0) {
    printf("Could not open codec.");
    return -1;
}

// 7. 写入头部
int ret = avformat_write_header(pFormatCtx, nullptr);
if (ret < 0) {
    printf("write_header fail\n");
    return -1;
}
int y_size = width * height;
//Encode
// 给AVPacket分配足够大的空间
AVPacket pkt;
av_new_packet(&pkt, y_size * 3);

// 8. 发送编码帧数据
ret = avcodec_send_frame(pCodeCtx, pFrame);
if (ret < 0) {
    printf("Could not avcodec_send_frame.");
    return -1;
}

// 9. 得到帧编码后的数据
ret = avcodec_receive_packet(pCodeCtx, &pkt);
if (ret < 0) {
    printf("Could not avcodec_receive_packet");
    return -1;
}
// 10. 写入帧
ret = av_write_frame(pFormatCtx, &pkt);

if (ret < 0) {
    printf("Could not av_write_frame");
    return -1;
}
av_packet_unref(&pkt);

//10. 写尾
av_write_trailer(pFormatCtx);

//11. 释放
avcodec_close(pCodeCtx);
avio_close(pFormatCtx->pb);
avformat_free_context(pFormatCtx);

编解码的 API

解码使用avcodec_send_packet()和avcodec_receive_frame()两个函数;

关于avcodec_send_packet()avcodec_receive_frame()的使用说明:

  • 按dts递增的顺序向解码器送入编码帧packet, 解码器按pts递增的顺序输出原始帧frame, 实际上解码器不关注输入packet的dts(错值都没关系), 它只管依次处理收到的packet, 按需缓冲和解码

  • avcodec_receive_frame()输出frame时, 会根据各种因素设置好frame->best_effort_timestamp(文档明确说明), 实测frame->pts也会被设置(通常直接拷贝自对应的packet.pts, 文档未明确说明)用户应确保avcodec_send_packet()发送的packet具有正确的pts, 编码帧packet与原始帧frame间的对应关系通过pts确定

  • avcodec_receive_frame()输出frame时, frame->pkt_dts拷贝自当前avcodec_send_packet()发送的packet中的dts,

如果当前packet为nullptr(flush packet), 解码器进入flush模式, 当前及剩余的frame->pkt_dts值总为AV_NOPTS_VALUE; 因为解码器中有缓存帧, 当前输出的frame并不是由当前输入的packet解码得到的, 所以这个frame->pkt_dts没什么实际意义, 可以不必关注末尾缓存帧的情况

  • avcodec_send_packet()发送第一个nullptr 会返回成功, 后续的nullptr 会返回 AVERROR_EOF;

  • avcodec_send_packet()多次发送nullptr并不会导致解码器中缓存的帧丢失, 使用avcodec_flush_buffers()可以立即丢掉解码器中缓存帧; 因此播放完毕时应avcodec_send_packet(nullptr)来取完缓存的帧, 而SEEK操作或切换流时应调用avcodec_flush_buffers()来直接丢弃缓存帧;

  • 解码器通常的冲洗方法: 调用一次avcodec_send_packet(nullptr)(返回成功), 然后不停调用avcodec_receive_frame()直到其返回AVERROR_EOF, 取出所有缓存帧, avcodec_receive_frame()返回AVERROR_EOF这一次是没有有效数据的, 仅仅获取到一个结束标志;

编码使用avcodec_send_frame()和avcodec_receive_packet()两个函数;

  • pts递增的顺序向编码器送入原始帧frame, 编码器按dts递增的顺序输出编码帧packet, 实际上编码器关注输入frame的pts不关注其dts, 它只管依次处理收到的frame, 按需缓冲和编码

  • avcodec_receive_packet()输出packet时, 会设置packet.dts, 从0开始, 每次输出的packet的dts加1, 这是视频层的dts, 用户写输出前应将其转换为容器层的dts

  • avcodec_receive_packet()输出packet时, packet.pts拷贝自对应的frame.pts, 这是视频层的pts, 用户写输出前应将其转换为容器层的pts

  • avcodec_send_frame()发送nullptr frame时, 编码器进入flush模式

  • avcodec_send_frame()发送第一个nullptr会返回成功, 后续的nullptr会返回AVERROR_EOF

  • avcodec_send_frame()多次发送nullptr并不会导致编码器中缓存的帧丢失, 使用avcodec_flush_buffers()可以立即丢掉编码器中缓存帧; 因此编码完毕时应使用avcodec_send_frame(nullptr)来取完缓存的帧, 而SEEK操作或切换流时应调用avcodec_flush_buffers()来直接丢弃缓存帧;

  • 编码器通常的冲洗方法: 调用一次avcodec_send_frame(nullptr)(返回成功), 然后不停调用avcodec_receive_packet()直到其返回AVERROR_EOF, 取出所有缓存帧, avcodec_receive_packet()返回AVERROR_EOF这一次是没有有效数据的, 仅仅获取到一个结束标志;

  • 对音频来说, 如果AV_CODEC_CAP_VARIABLE_FRAME_SIZE(在AVCodecContext.codec.capabilities变量中, 只读)标志有效, 表示编码器支持可变尺寸音频帧, 送入编码器的音频帧可以包含任意数量的采样点; 如果此标志无效, 则每一个音频帧的采样点数目(frame->nb_samples)必须等于编码器设定的音频帧尺寸(avctx->frame_size), 最后一帧除外, 最后一帧音频帧采样点数可以小于avctx->frame_size

编解码API详解

av_guess_format

av_guess_format 中支持的 short_name 格式可以通过下面命令获取

ffmpeg -formats

图片色彩转换

关于YUV

YUV 4:4:4
YUV 4:4:4 表示 Y, U, V 三分量采样率相同, 即每个像素的三分量信息完整, 都是 8bit, 每个像素占用 3 个字节;

四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
采样的码流为: Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
映射出的像素点为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]

YUV 4:2:2
YUV 4:2:2 表示 UV 分量的采样率是 Y 分量的一半;

四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
采样的码流为: Y0 U0 Y1 V1 Y2 U2 Y3 U3
映射出的像素点为: [Y0 U0 V1], [Y1 U0 V1], [Y2 U2 V3], [Y3 U2 V3]

一文理解 YUV

sws_scale

FFmpeg里面的sws_scale库可以在一个函数里面同时实现: 1.图像色彩空间转换; 2.分辨率缩放; 3.前后图像滤波处理;
参考下JavaCV的代码 [AVFrame 转换 opencv Mat(色彩/分辨率转换)]

实例 保存AVFrame为图片

新版API

使用 AVCodecParameters

int FFmpegDecoder::SaveFrame( AVFrame *pFrame, char *out_name )
{
	int	width = pFrame->width,height = pFrame->height, ret = -1;
	AVCodecContext	*pCodeCtx	= nullptr;
    /* AVFormatContext 初始化, mjpeg*/
	AVFormatContext *pFormatCtx = avformat_alloc_context();
	pFormatCtx->oformat = av_guess_format( "mjpeg", nullptr, nullptr );
	if ( avio_open( &pFormatCtx->pb, out_name, AVIO_FLAG_READ_WRITE ) < 0 ){
        std::cout<<"Couldn't open output file.";return(-1);return(-1);
	}
	/* 构建一个新stream , 设置编解码器参数*/
	AVStream *pAVStream = avformat_new_stream( pFormatCtx, 0 );
	AVCodecParameters *parameters = pAVStream->codecpar;
	parameters->codec_id	= pFormatCtx->oformat->video_codec;
	parameters->codec_type	= AVMEDIA_TYPE_VIDEO;
	parameters->format	= AV_PIX_FMT_YUVJ420P;
	parameters->width	= pFrame->width;
	parameters->height	= pFrame->height;
	AVCodec *pCodec = avcodec_find_encoder( pAVStream->codecpar->codec_id );
	pCodeCtx = avcodec_alloc_context3( pCodec );
	avcodec_parameters_to_context( pCodeCtx, pAVStream->codecpar ) ;
	pCodeCtx->time_base = (AVRational) { 1, 25 };
    //AVFormatContext 写头 
	if ( avcodec_open2( pCodeCtx, pCodec, nullptr ) < 0 )
	int ret = avformat_write_header( pFormatCtx, nullptr );
	int y_size = width * height;
	/* encode 给AVPacket分配足够大的空间 */
	AVPacket pkt;
	av_new_packet( &pkt, y_size * 3 );
	/* 使用编码器 AVFrame ->  AVPacket*/
	ret = avcodec_send_frame( pCodeCtx, pFrame );
	ret = avcodec_receive_packet( pCodeCtx, &pkt );
    if(ret<0){
        char * errMesg = new char[512];
        av_strerror(ret,errMesg,512);
        std::cout<<"ffmpeg err:"<<errMesg<<std::endl;
        delete []errMesg; return ret;
    }
	ret = av_write_frame( pFormatCtx, &pkt );
	av_packet_unref( &pkt );
	//AVFormatContext 写尾 
	av_write_trailer( pFormatCtx );
	avcodec_close( pCodeCtx );
	avio_close( pFormatCtx->pb );
	avformat_free_context( pFormatCtx );
	return(0);
}

旧版API

使用 AVCodecContext

int save_frame_as_jpeg(AVCodecContext *pCodecCtx, AVFrame *pFrame, int FrameNo) {
    AVCodec *jpegCodec = avcodec_find_encoder(AV_CODEC_ID_JPEG2000);//AV_CODEC_ID_MJPEG
    //设置 AVCodecContext
    AVCodecContext *jpegContext = avcodec_alloc_context3(jpegCodec);
    jpegContext->pix_fmt = pCodecCtx->pix_fmt;
    jpegContext->height = pFrame->height;
    jpegContext->width = pFrame->width;
    if (avcodec_open2(jpegContext, jpegCodec, NULL)  0) {
        return -1;
    }
    FILE *JPEGFile;
    char JPEGFName[256];
    AVPacket packet = {.data = NULL, .size = 0};
    av_init_packet(&packet);
    int gotFrame;
    //encode 
    if (avcodec_encode_video2(jpegContext, &packet, pFrame, &gotFrame) < 0) {
        return -1;
    }
    sprintf(JPEGFName, "dvr-%06d.jpg", FrameNo);
    JPEGFile = fopen(JPEGFName, "wb");
    fwrite(packet.data, 1, packet.size, JPEGFile);
    fclose(JPEGFile);
    av_free_packet(&packet);
    avcodec_close(jpegContext);
    return 0;
}

实例 不解码, 转换封装格式

HLS 时间分片问题

AVDictionary HLS选项
在写头的时候设置!!!!

AVDictionary* options = nullptr;//格式
if( this->type  == 1){//hls直播 切片参数
    av_dict_set_int(&options, "hls_time", 2, 0); //hls
    av_dict_set_int(&options, "hls_wrap", 5, 0); //hls
    av_dict_set(&options, "hls_flags", "delete_segments", 0); //hls
    av_dict_set(&options, "segment_list_flags", "+live ", 0); //hls
    // av_dict_set_int(&options, "hls_list_size", 5, 0); //hls
}
ret = avformat_write_header(avFormatCtx, &options);//写头 

踩坑: 转封装格式,保存封装格式,某些无法写头, 原因是遍历复制包括音频通道, 而写的时候没有音频(目前只要视频),从而错误..

参考 雷神博客 封装格式转换器(无编解码)

RTSP to RTMP 使用新API (5.0)

新API 使用 AVCodecParameters

关键是新API没有 AVStream 没有了 AVCodecContext 属性, 需要手动创建.

    AVFormatContext *ofmt_ctx = nullptr;
    ret = avformat_alloc_output_context2(&ofmt_ctx,nullptr,"flv", toRtmpUrl.toStdString().data());
    if (avio_open(&ofmt_ctx->pb, toRtmpUrl.toStdString().data(),AVIO_FLAG_WRITE) < 0) {
        std::cout<<"Couldn't open output file.";return(-1);
    }
    AVStream *in_stream = ifmt_ctx->streams[videoIndex];
    AVStream *out_stream = avformat_new_stream(ofmt_ctx, nullptr);
    if (!out_stream) {
        qDebug()<<"创建输出流通道出错";
        throw new std::runtime_error("Failed allocating output stream");
    }
    const AVCodec *in_codec = avcodec_find_encoder(in_stream->codecpar->codec_id);
    AVCodecContext *out_codec_ctx = avcodec_alloc_context3(in_codec);
    ret = avcodec_parameters_copy( out_stream->codecpar, in_stream->codecpar);
    printFFmpegError(ret);

实例 H264 包 转AVPacket

QString path = QString("F:/test/stream/h264/farme%1.dat").arg(var);
QFile frame(path);
frame.open(QIODevice::ReadOnly);
QByteArray array = frame.readAll();
char* buff = array.data();

///// 把h264 转成 AVPacket
    AVPacket opkt;
    av_new_packet(&opkt, array.size() );
    memcpy(opkt.data ,buff, array.size());
    //转换时间戳
    AVRational time_base = outputStream->time_base;
    opkt.pts = opkt.dts = dts++;
    opkt.pts = av_rescale_q(opkt.pts, AVRational{1,25}, time_base);
    opkt.dts = av_rescale_q(opkt.dts, AVRational{1,25}, time_base);
    opkt.duration = av_rescale_q(1, AVRational{1,25}, time_base);

参考 树莓派的一个贴


DTS/PTS 转换

DTS(Decoding Time Stamp, 解码时间戳)
PTS(Presentation Time Stamp, 显示时间戳)

ffmpeg提供av_rescale_q函数用于time_base之间转换, av_rescale_q(a,b,c)作用相当于执行a*b/c, 通过设置b,c的值, 可以很方便的实现time_base之间转换;

///不同封装格式具有不同的时间基, 在转封装(将一种封装格式转换为另一种封装格式)过程中, 时间基转换相关代码如下: 
pPacket.pts = av_rescale_q_rnd(pPacket.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pPacket.dts = av_rescale_q_rnd(pPacket.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pPacket.duration = av_rescale_q(pPacket.duration, in_stream->time_base, out_stream->time_base);

// 基于 packet 数据转换 具有和上面代码相同的效果: 
av_read_frame(ipacket, &pkt);
av_packet_rescale_ts(ipacket, sctx->i_stream->time_base, sctx->o_codec_ctx->time_base);

///如果手动换算
if(vc->timebase.den > 0 && vs->timebase.den > 0)
pkt.pts = pkt.pts * (vc->timebase.num/vc->timebase.den)/(vs->timebase.num/vs->timebase.den)


几个结构中的 base_time

AVStream
AVStream->base_time 单位为秒, 通过 avpriv_set_pts_info(st, 33, 1, 90000) 函数, 设置 AVStream->time_base为1/90000; 为什么是90000?因为mpeg的pts, dts都是以90kHz来采样的, 所以采样间隔为1/90000秒;

AVPacket
AVPacket 下的pts和dts以AVStream->time_base为单位(数值比较大); 这也很容易理解, 根据mpeg的协议, 压缩后或解压前的数据, pts和dts是90kHz时钟的采样值, 时间间隔就是AVStream->time_base;

AVFrame
AVFrame里面的pkt_pts和pkt_dts是拷贝自AVPacket,同样以AVStream->time_base为单位; 而pts是为输出(显示)准备的, 以AVCodecContex->time_base为单位);

AVCodecContext
AVCodecContext->time_base单位同样为秒, 不过精度没有AVStream->time_base高, 大小为1/framerate;

手动计算每帧的 (PTS)

在 FFmpeg 中, 时间基(time_base)是时间戳(timestamp)的单位, 时间戳值乘以时间基, 可以得到实际的时刻值(以秒等为单位);
例如, 如果一个视频帧的 dts 是 40,pts 是 160, 其 time_base 是 1/1000 秒, 那么可以计算出此视频帧的解码时刻是 40 毫秒(40/1000),显示时刻是 160 毫秒(160/1000);
FFmpeg 中时间戳(pts/dts)的类型是 int64_t 类型, 把一个 time_base 看作一个时钟脉冲, 则可把 dts/pts 看作时钟脉冲的计数;

例: 一个mp4文件的 time_base=90k pAVStream->time_base = (AVRational) { 1, 90000 };, 即每秒的差值~=90000; 30帧的视频, 每帧的PTS增量则: 90000/30 = 3000

FFmpeg时间戳详解
音视频录入的pts和dts问题

AVfilter

使用 AVfilter 绘制, 实时绘制目标识别的框?

drawbox
Draw a colored box on the input image.
It accepts the following parameters:
x y

The expressions which specify the top left corner coordinates of the box. It defaults to 0.
width, w
height, h

The expressions which specify the width and height of the box; if 0 they are interpreted as the input width and height. It defaults to 0.

...
嗯... 过滤器貌似做不到实时绘制框

avfilter_get_by_name 有哪些名称可以在文档中找到
参考雷神博客
FFmpeg filter简介

posted @ 2025-11-15 14:56  daidaidaiyu  阅读(28)  评论(0)    收藏  举报