news 2026/8/2 20:02:57

FFmpeg之二 摄像头录制保存视频, API编解码从理论到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FFmpeg之二 摄像头录制保存视频, API编解码从理论到实战

这篇文章,详细讲解了用FFmpeg的API代码的方式,如何把摄像头的录制的视频,保存为MP4、YUV格式,会详细介绍视频的相关知识,已经遇到的问题

文章目录

  • YUV
    • YUV采样格式
    • 与RGB比较
    • YUV格式间的转换
  • 视频的比特率(bit_rate)、帧率(framerate)、分辨率
  • I、B、P帧
  • time_base
    • 三种时间基 tbr、tbn 和 tbc
  • PTS、DTS
    • PTS的转换
      • 对frame的pts,进行解码器->编码器的时间基转换
      • 编码后,对packet,进行编码器->容器的时间基转换
  • 录制视频的代码:

YUV

YUV中 “Y” 表示明亮度( Luminance 或 Luma ),也就是灰阶值;而 “U” 和 “V” 表示的则是色度( Chrominance 或 Chroma ),作用是描述影像色彩及饱和度,用于指定像素的颜色。

早期技术不发达的时候,照片打印和电视播放都只能实现黑白的灰度显示,所以Y数据就成了标准。之后才有了彩色打印和彩色电视,为了兼容之前的黑白数据,发明了UV数据。

YUV一起就可以实现彩色,只用Y数据,可实现黑白画面,这样一套数据格式就同时兼容了黑白设备和彩色设备。

人眼对亮点信息更敏感。也就是说,可以压缩UV数据,而人眼难以发现。把RGB数据转换成YUV数据。对Y少压缩一点,对UV多压缩一点,以平衡图像效果和压缩率。

YUV采样格式

#NV12NV21的存储格式为Y平面,UV打包一个平面,共两个平面,即:先连续存储Y,然后连续交叉存储UV# 不同点在于UV的排列顺序,SPSemi-Planar的缩写 #NV12:先是 w*h 长度的Y,后面跟 w*h*0.5长度的UV(交叉存储),总长度为 w*h*1.5#NV21:先是 w*h 长度的Y,后面跟 w*h*0.5长度的VU(交叉存储),总长度为 w*h*1.5NV12:YYYYYYYYUVUV=>YUV420SP# iOS 平台常用NV12NV21:YYYYYYYYVUVU=>YUV420SP#Android平台常用NV21#I420YV12三个分量均为平面格式,共三个平面,即:先连续存储Y,然后连续存储U,最后连续存储V# 不同点在于UV的排列顺序,PPlanar的缩写 #I420:先是 w*h 长度的Y,后面跟 w*h*0.25长度的U, 最后是 w*h*0.25长度的V,总长度为 w*h*1.5#YV12:先是 w*h 长度的Y,后面跟 w*h*0.25长度的V, 最后是 w*h*0.25长度的U,总长度为 w*h*1.5I420:YYYYYYYYUUVV=>YUV420P#Android平台常用I420YV12:YYYYYYYYVVUU=>YUV420P


按照颜色可找到Y对应的UV值,以上图的NV12为例, 每个像素点都有一个Y值,像素1、2、7、8位置的Y值是Y1、Y2,、Y7、Y8(蓝色) 对应的UV是U1,V1(蓝色),
像素1=Y1U1V1, 像素2=Y2U1V1,以此类推。

知道了存储格式,就可以把上面二维的YUV值(上图只是为了方便理解),连续存储了,如下图:

与RGB比较

一帧width*height的图像

  • RGB格式,R/G/B各需要1byte(8bit),总存储空间=width*height*3byte,
  • YUV420P格式,Y存储空间 = width*height*1byte ,一个U用于4个像素,平均到单个像素用到1/4个U,U存储空间 = width*height*1/4byte,同理V存储空间 = width*height*1/4byte,总存储空间= width*height*(1byte + 1/4byte + 1/4byte),占用空间比RGB减少了一半。

YUV格式间的转换

例如:NV12 转 YUV420P

#include<stdint.h>#include<string.h>voidnv12_to_yuv420p(uint8_t*src_yuv,uint8_t*dst_yuv,intwidth,intheight){inty_size=width*height;intuv_size=y_size/2;// NV12 UV plane size (interleaved)intu_size=y_size/4;// YUV420P U or V plane sizeinti;// Copy Y plane directly (identical in both formats)memcpy(dst_yuv,src_yuv,y_size);// Pointers to U and V planes in YUV420Puint8_t*dst_u=dst_yuv+y_size;// U plane starts after Yuint8_t*dst_v=dst_yuv+y_size+u_size;// V plane starts after U// Pointer to NV12 UV planeuint8_t*src_uv=src_yuv+y_size;// Deinterleave NV12 UV plane into separate U and V planesfor(i=0;i<uv_size/2;i++){dst_u[i]=src_uv[2*i];// U values are at even indicesdst_v[i]=src_uv[2*i+1];// V values are at odd indices}}

从转换代码可以看出,YUV只有像素的数据,所有YUV格式保存到文件,没有包装头,读取后可直接对YUV操作,所以视频的原始数据就是YUV

视频的比特率(bit_rate)、帧率(framerate)、分辨率

  • 帧率

每秒中视频帧数量 ,单位为 fps(frame per second), 帧率越高 , 视频越流畅 ;

在ffmpeg中,为编码器设置帧率

// 设置帧率encoder_codec_ctx->time_base=(AVRational){1,30};encoder_codec_ctx->framerate=(AVRational){30,1};

framerate 和 time_base 值互为倒数,time_base 是每一帧视频的持续时间,此处是1/30s,framerate 是30fps。 后续需要根据解码器 time_base和编码器的time_base,来计算pts

  • 分辨率

分辨率 = 宽度的像素点数* 高度的像素点数

  • 比特率

每秒的bit数量,单位为 bps(bit per second)。 一个文件的比特率 =文件大小(字节)X8/ 文件时长(秒))

分辨率越大、帧率越大,都会使比特率增大。

为了保证视频的观看体验,这三者关系如下:

I、B、P帧

想起了大学时做的一款桌面远程控制软件,把桌面一张一张截图,发送过来,那个延时,适合树獭用。视频也是一个个帧,连续播放的效果,其中两帧之间,大部分时间差异很小,所以可以通过算法来提取一下相邻两帧之间的不变的画面,减少重复数据量。这个算法比较流行的就是H.264,经过压缩后,帧分为 I、B、P帧

通俗理解:
I帧 就是完整的一帧图片,
P帧 是与它前面I帧或P帧的差异部分,可以与前帧结合,还原出一帧图。
B帧 需要与前后的I帧或P帧,进行解析,还原出一帧图。

I帧:帧内编码帧(intra picture),又称全帧压缩编码帧。I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访问的参考点,可以当成静态图像。

P帧:前向预测编码帧(predictive-frame),通常将图像序列中前面已经编码帧的冗余信息去除,达到压缩传输数据量的效果,也称为预测帧。

B帧:双向预测内插编码帧(bi-directional interpolated prediction frame),既考虑源图像序列前面的已编码帧,又顾及源图像序列后面的已编码帧之间的冗余信息,来压缩传输数据量的编码图像,也称为双向预测帧。


以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。

采集顺序指图像传感器采集原始信号得到图像帧的顺序。
编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。
传输顺序指编码后的流在网络中传输过程中图像帧的顺序。
解码顺序指解码器解码图像帧的顺序。
显示顺序指图像帧在显示器上显示的顺序。
采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。

  • 直播时,不使用B帧,因为解析B帧,需要先接收到更后面的I/P帧,降低了实时性。直播秒开,也可以通过设置更小的GOP(一般是5),能更快的找到I帧,显示出画面
  • 在没有B帧的情况下,存放帧的顺序和显示帧的顺序就是一样的,PTS和DTS的值也是一样的。

time_base

在 FFmpeg 中,时间基(time_base)是时间戳(timestamp)的单位,时间戳值乘以时间基,可以得到实际的时刻值(以秒等为单位)。

例如,如果一个视频帧的 dts 是 40,pts 是 160,其 time_base 是 1/1000 秒,那么可以计算出此视频帧的解码时刻是 40 毫秒(40/1000),显示时刻是 160 毫秒(160/1000)。

三种时间基 tbr、tbn 和 tbc

使用ffmpeg 、ffplay 操作视频时,经常会看到视频的这些参数,它们是不同的时间基

不同的封装格式具有不同的时间基。在 FFmpeg 处理音视频过程中的不同阶段,也会采用不同的时间基。

FFmepg 中有三种时间基,命令行中 tbr、tbn 和 tbc 的打印值就是这三种时间基的倒数:
tbn:对应容器中的时间基。值是 AVStream.time_base 的倒数
tbc:对应编解码器中的时间基。值是 AVCodecContext.time_base 的倒数
tbr:从视频流中猜算得到,可能是帧率或场率(帧率的 2 倍)

PTS、DTS

PTS的转换

编码器、解码器、要写入的容器(例如写到MP4的容器) 都有自己的time_base,上面说过,pts、dts的值的单位是time_base,那么如果time_base不相同,pts还是原样复制过去,播放时就会有问题。如果是录制视频,pts的起点不是从0开始,如不处理,会导致录制完的视频时间特别长。

下面以摄像头录制文件为例,介绍pts的处理:

对frame的pts,进行解码器->编码器的时间基转换

staticvoidprocess_frame_use_decode(AVPacket*pkt,AVFrame*decoded_frame,AVFrame*converted_frame,AVCodecContext*av_decoder_ctx,AVCodecContext*h264_encoder_ctx,AVFormatContext*ofmtCtx,structSwsContext*sws_ctx,intframeIndex){//av_read_frame 接收到的是原始数据,也可以解码后再发送给编码器。如果接收到是编码后的数据packet,需要解码后再发送给编码器intret=avcodec_send_packet(av_decoder_ctx,pkt);if(ret<0){printf("Decode error.\n");return;}if(avcodec_receive_frame(av_decoder_ctx,decoded_frame)>=0){// frame的格式,转换成YUV420Psws_scale(sws_ctx,decoded_frame->data,decoded_frame->linesize,0,av_decoder_ctx->height,converted_frame->data,converted_frame->linesize);// Store the first PTS valueif(first_pts==AV_NOPTS_VALUE){first_pts=decoded_frame->pts;}//打印av_decoder_ctx->time_base、 h264_encoder_ctx->time_baseprintf("av_decoder_ctx time_base: %d/%d\n",av_decoder_ctx->time_base.num,av_decoder_ctx->time_base.den);printf("h264_encoder_ctx time_base: %d/%d\n",h264_encoder_ctx->time_base.num,h264_encoder_ctx->time_base.den);// 得到相对于起始时间first_pts的PTS,pts从0开始,保证了视频时长的正确性converted_frame->pts=decoded_frame->pts-first_pts;// 将 decoded_frame 的 PTS 进行时间基转换,传递给 converted_frameconverted_frame->pts=av_rescale_q(converted_frame->pts,av_decoder_ctx->time_base,h264_encoder_ctx->time_base);// 打印pts,pkt_dts,// frame 的pkt_dts 是取自packet的dts,此处打印pkt_dts,意义不大,只是想看看// 经过h264编码后,会自动设置packet的dtsprintf("decoded_frame pts: %ld, pkt_dts: %ld\n",decoded_frame->pts,decoded_frame->pkt_dts);printf("converted_frame pts: %ld, pkt_dts: %ld\n",converted_frame->pts,converted_frame->pkt_dts);encode_and_write_frame(h264_encoder_ctx,converted_frame,ofmtCtx,pkt,frameIndex);}}

转换后的一系列 converted_frame->pts,是0、1、2、3、、、,说明转换对了

编码后,对packet

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 20:01:51

【JavaScript】Javascript—APIs—Day03

Javascript——Day0801.事件流、事件捕获、事件冒泡以及阻止冒泡02.事件解绑、mouseover和mouseenter的区别03.事件委托04.阻止元素默认行为05.页面加载事件和页面滚动事件06.client家族和offset家族01.事件流、事件捕获、事件冒泡以及阻止冒泡 事件流&#xff1a; 事件流经历…

作者头像 李华
网站建设 2026/8/2 20:00:14

Unity物理范围检测:从原理到实战,优化游戏交互性能

1. 项目概述&#xff1a;为什么范围检测是游戏交互的基石在Unity里做游戏&#xff0c;尤其是涉及到战斗、解谜、交互这些核心玩法时&#xff0c;有一个问题你几乎绕不开&#xff1a;如何判断一个物体是否进入了另一个物体的“势力范围”&#xff1f;比如&#xff0c;敌人如何发…

作者头像 李华
网站建设 2026/8/2 19:57:25

如何快速掌握SmartCode:3个实用技巧与完整代码生成指南

如何快速掌握SmartCode&#xff1a;3个实用技巧与完整代码生成指南 【免费下载链接】SmartCode SmartCode IDataSource -> IBuildTask -> IOutput > Build Everything!!! 项目地址: https://gitcode.com/gh_mirrors/smar/SmartCode 还在为重复的CRUD代码编写而…

作者头像 李华
网站建设 2026/8/2 19:55:28

UniGetUI完整指南:如何用统一界面管理Windows所有软件包管理器

UniGetUI完整指南&#xff1a;如何用统一界面管理Windows所有软件包管理器 【免费下载链接】UniGetUI UniGetUI: The Graphical Interface for your package managers. Could be terribly described as a package manager manager to manage your package managers 项目地址:…

作者头像 李华
网站建设 2026/8/2 19:44:52

hexo-theme-inside PWA实战:实现沉浸式设计与离线访问的秘诀

hexo-theme-inside PWA实战&#xff1a;实现沉浸式设计与离线访问的秘诀 【免费下载链接】hexo-theme-inside &#x1f308; SPA, Flat and clean theme for Hexo https://blog.oniuo.com/theme-inside 项目地址: https://gitcode.com/gh_mirrors/hex/hexo-theme-inside …

作者头像 李华