一文入门 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
雷神博客 目录
雷神博客 视音频编解码技术零基础学习方法
命令行工具
另外命令行工具默认是使用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) 记录了编码的 Profile、level、图像宽高等
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
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]
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
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.
...
嗯... 过滤器貌似做不到实时绘制框

浙公网安备 33010602011771号