协议-ACLLite-ffmpeg

是什么?

  • FFmpeg是一个开源的多媒体处理工具包,它集成了多种功能,包括音视频的录制、转换和流式传输处理。
  • FFmpeg由一系列的库和工具组成,其中最核心的是libavcodec和libavformat库。
    • libavcodec是一个领先的音频/视频编解码器库,支持多种音频和视频格式的编码和解码操作。
    • libavformat库则用于处理各种不同的多媒体容器格式,如MP4、AVI、MKV等。它能够解析提取其中的音频和视频流,以便进行进一步的处理

为什么?

为什么有GStreamer的前提下,还要使用FFmpeg?

  • 在某些情况下,GStreamer 会比 FFmpeg 更适合特定的需求
    • FFmpeg 是一个功能强大的单体框架,主要专注于媒体的编解码、转码和流处理
    • GStreamer适合构建复杂的媒体处理管道,如视频会议、实时流媒体处理和视频编辑
功能 FFmpeg GStreamer
核心架构 Monolithic 模块化,基于管道线
易用性 使用 CLI 更容易完成简单的媒体任务 需要更多设置,但管道灵活
编解码器支持 广泛的编解码器支持,几乎涵盖所有编解码器 功能强大,但通常需要额外的插件
使用案例 媒体转换、流媒体 转码、实时流媒体、媒体应用
模块化 模块化程度低,围绕特定命令构建 模块化程度高,可定制组件
插件系统 有限的插件系统,主要集中于编解码器 广泛的插件系统,用于自定义处理
实时处理 可以,但不直观 专为实时流媒体和处理而设计
跨平台 是(Linux、Windows、macOS 等) 是(Linux、Windows、macOS 等)
复杂性 对于简单的任务来说更简单 对于复杂的自定义媒体工作流程来说更好
许可 LGPL 或 GPL(取决于配置) LGPL
开发者社区 庞大、活跃、被广泛采用 活跃,但规模小于 FFmpeg

怎么做?

  • 官网Download FFmpeg,选择安装包Windows builds from gyan.dev
  • 找到release bulids部分,选择 ffmpeg-release-essentials.zip
  • 解压文件并检查目录结构
  • 配置环境变量 并检验 ffmpeg -version

核心本质

本质就四个名词

  • Demuxer:拆解多媒体文件,提取音频和视频流。
  • Decoder:将编码后的音频或视频数据解码为原始数据。
  • Encoder:将原始音频或视频数据编码为特定格式。
  • Muxer:将音频和视频流重新封装为多媒体文件。
  • Demuxer:拆解多媒体文件,提取音频和视频流。
  • 解码器接收音频、视频、 或字幕基本流,并将它们解码为原始帧( 视频的像素,音频的 PCM)
  • 编码器接收原始音频、视频或字幕帧并进行编码 它们被编码为数据包
  • Muxer:将音频和视频流重新封装为多媒体文件

大局观总览

  • 库总览

真正有用三库

  • libavcodec库包含多种音频、视频和字幕流的解码器和编码器,以及多种位流过滤器。
  • libavformat库用于将音频、视频和字幕流多路复用和解复用
  • libavfilter库用于处理音频/视频数据,例如进行视频的缩放、裁剪、旋转,音频的混音、音量调整

辅助类型库

  • libavutil包含了一些安全的、可移植的字符串函数、随机数生成器、数据结构、额外的数学函数、密码学和与多媒体相关的功能(如枚举像素和采样格式)

  • libswscale是一个用于图像缩放和颜色空间以及像素格式转换的高效库。

  • libswresample是一个高度优化的音频重采样、重矩阵和采样格式转换库。

  • libavdevice是一个通用的框架,用于抓取和渲染许多常见的多媒体输入/输出设备。

  • 命令总览

  • 功能总览

转换格式流程图

  • 输入文件拆解多媒体文件,提取音频和视频流 ;将编码后的音频或视频数据解码为原始数据;将原始音频或视频数据编码为特定格式;将音频和视频流重新封装为多媒体文件;
c 复制代码
ffmpeg -i INPUT.mkv -map 0:v -map 0:a -c:v libx264 -c:a copy OUTPUT.mp4

简单的 filtergraph

  • 将编码后的音频或视频数据解码为原始数据;并对原始数据进行处理;将原始音频或视频数据编码为特定格式;

复杂 filtergraph 它可能有多个输入,多个输出,可能是不同类型的(音频或 video)

  • 第二个输入的帧将叠加在来自第一个输入的帧上。第三个 input 被重新缩放,然后被复制到两个相同的流中。其中之一 它们叠加在组合的前两个输入上,显示为 FilterGraph 的第一个输出。另一个是 filterGraph 的第二个输出

基本命令

c 复制代码
ffmpeg -y -c:a libfdk_aac -c:v libx264 -i input.mp4 -c:v libvpx-vp9 -c:a libvorbis output.webm 
  • -y:不经过确认,输出时直接覆盖同名文件
  • -c:指定编码器
    • -c copy:直接复制,不经过重新编码(这样比较快)
    • -c:v:指定视频编码器
    • -c:a:指定音频编码器
  • -i:指定输入文件
  • -an:去除音频流
  • -vn: 去除视频流
  • -preset:指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
  • -f:fmt指定格式(音频或视频格式)
  • -t:duration 记录时长为t
  • -acodec: 音频选项, 一般后面加copy表示拷贝
  • -vcodec:视频选项,一般后面加copy表示拷贝
  • h264: 表示输出的是h264的视频裸流
  • mp4: 表示输出的是mp4的视频
  • mpegts: 表示ts视频流
c++ 复制代码
ffmpeg -r 1 -i racing.mp4 -c:v libx264 -profile:v high -level:v 5.1 -c:a copy -r 60 racingoutput.mp4
  • -r 强制输入文件的帧速率(仅对 Raw 格式有效)为 1 fps,并且 输出文件的帧速率为 24 fps
  • -profile:v -level 设置H.264画质级别
c 复制代码
ffmpeg -i input.avi output.mp4
  • 通过重新编码媒体流,将输入媒体文件转换为其他格式
c 复制代码
ffmpeg -i input.avi -b:v 64k -bufsize 64k output.mp4
  • -b:v 64k 设置输出文件的视频码率为 64 kbit/s

Streamcopy 流复制 -map 的使用

c 复制代码
ffmpeg -i INPUT.mkv -map 0:1 -c copy OUTPUT.mp4
c 复制代码
ffmpeg -i INPUT0.mkv -i INPUT1.aac -map 0:0 -map 1:0 -c copy OUTPUT.mp4
c 复制代码
ffmpeg -i INPUT.mkv -map 0:0 -c copy OUTPUT0.mp4 -map 0:1 -c copy OUTPUT1.mp4

推拉流RTSP命令

UDP推流

C 复制代码
ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
  • -re 为以流的方式读取

TCP推流

C 复制代码
ffmpeg -re -i input.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/stream

循环推流

C 复制代码
ffmpeg -re -stream_loop -1 -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
  • -stream_loop 为循环读取视频源的次数,-1为无限循环

拉流

c 复制代码
ffplay rtsp://127.0.0.1:8554/stream

FFmpeg拉流保存成视频

c 复制代码
ffmpeg -stimeout 30000000 -i rtsp://127.0.0.1:8554/stream -c copy output.mp4
  • -stimeout 30000000 为等待RTSP 流连接的时间,单位为us微秒,等待 30 秒如果连接失败则退出

过滤器(fliter)命令

  • 过滤器就是实现某一种视频功能的工具,FFmpeg自带开发了很多种filter用于实现不同的功能

  • 源视频宽度扩大两倍

c 复制代码
ffmpeg -i jidu.mp4 -t 10 -vf pad=2 * iw output.mp4
  • 源视频水平翻转
c 复制代码
ffmpeg -i jidu.mp4 -t 10 -vf hflip output2.mp4
  • 水平翻转视频覆盖output.mp4
c 复制代码
ffmpeg -i output.mp4 -i output2.mp4 -filter_complex overlay=w compare.mp4

核心结构体

  • 围绕解协议,解封装,解码
  • libavcodec 的核心是 AVCodec 和 AVCodecContext

AVIOContext,URLProtocol,URLContext,AVFormatContext

解协议(http,rtsp,rtmp,mms)

  • AVIOContext,URLProtocol,URLContext
    • AVIOContext是FFMPEG管理输入输出数据的结构体,内含指针指向URLContext结构体
    • URLContext结构体中包含结构体URLProtocol
      • URLContext存储音频/视频使用的协议的类型以及状态
      • URLProtocol存储音频/视频使用的协议(rtp,rtmp,file等)操作函数接口

解封装(flv,avi,rmvb,mp4)

  • AVFormatContext存储音频/视频封装包含的数据和信息
  • AVInputFormat表示音频/视频输入格式

AVCodec AVCodecContext AVStream

架构类似于昇腾的device conent steam

解码(h264,mpeg2,aac,mp3)

  • AVCodecContext,存储该音视频流使用解码器的相关数据(所需的上下文环境)
  • AVCodec (该音视频的解码器)(h264 mpeg2 AAC mp3)
  • 实际使用时有可能会有多个 AVCodecContext 关联同一个 AVCodec 的情况。尤其是我们解码音频的时候。

音频文件时 5.1声道的 AVCodec AVCodecContext AVStream关系

  • 通常会对应 3 个 AVStream

    • 左右声道在一个 AVStream
    • 环绕声在一个 AVStream
    • 最后低音在另一个AVStream
  • 3 个AVStream的编码可能是相同的

  • 解码这个音频文件时就应该建立 3 个 AVCodecContext ,分别对应三个 AVStream。然后只需要有 1 个 AVCodec 。每个 AVCodecContext 都利用这一个 AVCodec 来解码。


AVPacket - AVFrame

  • 所谓解码就是把一个 AVPacket 中的数据解成 AVFrame
    • AVPacket是 编码压缩之后的数据
    • AVFrame 是原始的,没有编码、没有压缩的数据 对视频来说是YUV RGB,对音频来说是PCM

I帧 P帧 B帧 的影响

  • 我们会遇到前几个 AVPacket 解不出数据。

    • 到了某个 AVPacket ,可以连续解出多个 AVFrame 来的情况。
    • 这时这多个 AVFrame 就包括前面积压的 AVPacket 里的数据
  • avcodec_send_packet() 调用一次将一个 packet 推给Codec,

  • avcodec_receive_frame() 调用一次或多次来获得 frame


华为昇腾ACLLite库封装ffmpeg案例

什么是华为昇腾ACLLite库?

  • ACLLite库是对CANN提供的ACL接口进行的高阶封装,简化用户调用流程,为用户提供一组简易的公共接口。当前主要针对边缘场景设计

ACLLite\Media\CameraRead.cpp

c 复制代码
void CameraRead::DecodeFrameThread(void* decoderSelf)

对照核心结构体 理解 具体解码流程

c++ 复制代码
void CameraRead::DecodeFrameThread(void* decoderSelf)
{
    CameraRead* thisPtr = (CameraRead*)decoderSelf;
    int videoStreamIndex = -1;
    for (int i = 0; i < thisPtr->formatContext_->nb_streams; ++i) {
        if (thisPtr->formatContext_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
        }
    }
    if (videoStreamIndex == -1) {
        LOG_PRINT("[ERROR] usb camera %s index is -1", thisPtr->streamName_.c_str());
        thisPtr->isOpened_ = false;
        return;
    }
    AVCodecParameters* codecParameters = thisPtr->formatContext_->streams[videoStreamIndex]->codecpar;
    AVCodec* codec = avcodec_find_decoder(codecParameters->codec_id);
    if (codec == nullptr) {
        LOG_PRINT("[ERROR] Could not find ffmpeg decoder.");
        thisPtr->isOpened_ = false;
        return;
    }
    AVCodecContext* codecContext = avcodec_alloc_context3(codec);
    if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
        LOG_PRINT("[ERROR] Could not create decoder context.");
        thisPtr->isOpened_ = false;
        return;
    }
    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        LOG_PRINT("[ERROR] Could not open decoder context.");
        thisPtr->isOpened_ = false;
        return;
    }
    AVFrame* frame = av_frame_alloc();
    AVPacket packet;

    while (av_read_frame(thisPtr->formatContext_, &packet) >= 0 && !thisPtr->isStop_) {
        if (packet.stream_index == videoStreamIndex) {
            int response = avcodec_send_packet(codecContext, &packet);
            if (response < 0 || response == AVERROR(EAGAIN)) {
                continue;
            }
            while (response >= 0) {
                response = avcodec_receive_frame(codecContext, frame);
                if (response == AVERROR(EAGAIN)) {
                    break;
                } else if (response < 0) {
                    LOG_PRINT("[ERROR] Receive false frame from ffmpeg.");
                    thisPtr->isOpened_ = false;
                    return;
                }
                bool ret = thisPtr->SendFrame(packet.data, packet.size);
                if (!ret) {
                    thisPtr->isOpened_ = false;
                    LOG_PRINT("[ERROR] Send single frame from ffmpeg failed.");
                    return;
                }
            }
        }
        av_packet_unref(&packet);
    }
    av_frame_free(&frame);
    avcodec_close(codecContext);
    avformat_close_input(&thisPtr->formatContext_);
    thisPtr->isOpened_ = false;
    return;
}
  • avcodec_find_decoder 根据编解码参数的编解码器ID查找对应的编解码器
  • avcodec_alloc_context3 分配编解码器上下文
  • avcodec_open2 打开编解码器上下文
  • av_frame_alloc 分配帧结构体
  • av_read_frame 从输入文件中读取一个数据包(AVPacket),并将其存储到 packet 中
  • avcodec_send_packet 将一个数据包发送到解码器
  • avcodec_receive_frame 从解码器中接收一个解码后的帧
  • SendFrame 用于将解码后的帧发送到其他地方(例如显示或进一步处理)
  • av_frame_free avcodec_close avformat_close_input 最后释放资源

ACLLite\DVPPLite\src\VideoRead.cpp

c 复制代码
void FFmpegDecoder::Decode(FrameProcessCallBack callback,void *callbackParam)

对照核心结构体 理解 具体解码流程

c 复制代码
void FFmpegDecoder::Decode(FrameProcessCallBack callback,
                           void *callbackParam)
{
    LOG_PRINT("[INFO] Start ffmpeg decode video %s ...", streamName_.c_str());
    avformat_network_init(); // init network

    AVFormatContext* avFormatContext = avformat_alloc_context();

    // check open video result
    if (!OpenVideo(avFormatContext)) {
        return;
    }

    int videoIndex = GetVideoIndex(avFormatContext);
    if (videoIndex == kInvalidVideoIndex) { // check video index is valid
        LOG_PRINT("[ERROR] Rtsp %s index is -1", streamName_.c_str());
        return;
    }

    AVBSFContext* bsfCtx = nullptr;
    // check initialize video parameters result
    if (!InitVideoParams(videoIndex, avFormatContext, bsfCtx)) {
        return;
    }

    LOG_PRINT("[INFO] Start decode frame of video %s ...", streamName_.c_str());

    AVPacket avPacket;
    int processOk = true;
    // loop to get every frame from video stream
    while ((av_read_frame(avFormatContext, &avPacket) == 0) && processOk && !isStop_) {
        if (avPacket.stream_index == videoIndex) { // check current stream is video
          // send video packet to ffmpeg
            if (av_bsf_send_packet(bsfCtx, &avPacket)) {
                LOG_PRINT("[ERROR] Fail to call av_bsf_send_packet, channel id:%s",
                    streamName_.c_str());
            }

            // receive single frame from ffmpeg
            while ((av_bsf_receive_packet(bsfCtx, &avPacket) == 0) && !isStop_) {
                int ret = callback(callbackParam, avPacket.data, avPacket.size);
                if (ret != 0) {
                    processOk = false;
                    break;
                }
            }
        }
        av_packet_unref(&avPacket);
    }

    av_bsf_free(&bsfCtx); // free AVBSFContext pointer
    avformat_close_input(&avFormatContext); // close input video

    isFinished_ = true;
    LOG_PRINT("[INFO] Ffmpeg decoder %s finished", streamName_.c_str());
}
  • av_read_frame 从输入文件中读取一个数据包(AVPacket)
  • av_bsf_send_packet 数据包发送到解码器
  • av_bsf_receive_packet 从解码器中接收解码后的帧
  • callback 进一步处理
  • av_packet_unref av_bsf_free avformat_close_input 最后释放资源

根据对应类型初始化过滤器

c 复制代码
void FFmpegDecoder::InitVideoStreamFilter(const AVBitStreamFilter*& videoFilter)
{
    if (videoType_ == AV_CODEC_ID_H264) { // check video type is h264
        videoFilter = av_bsf_get_by_name("h264_mp4toannexb"); // 目的是从Avcodec库中获取一个名为"h264_mp4toannexb"的视频过滤器。
    } else { // the video type is h265
        videoFilter = av_bsf_get_by_name("hevc_mp4toannexb");
    }
}

附录-h264基础概念

H264

为什么诞生

  • ⼀段分辨率为 1920 * 1080,每个像素点为 RGB 占⽤3 个字节,帧率是 25 的视频,对于传输带宽的要求是

  • 换成 bps 则意味着视频每秒带宽为 1186.523Mbps,这样的速率对于⽹络存储是不可接受的

  • H264 采⽤了 16 * 16 的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码

  • 一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了

H264有两种封装

  • H.264码流分Annex-B和mp4两种格式。

  • ⼀种是 annexb 模式,传统模式

  • ⼀种是 mp4 模式,⼀般 mp4 mkv 都是 mp4 模式

  • 很多解码器只⽀持 annexb 这种模式,因此需要将 mp4 做转换:

  • 在 ffmpeg 中⽤h264_mp4toannexb_filter 可以做转换

c++ 复制代码
ffmpeg -i INPUT.mp4 -codec copy -bsf:v h264_mp4toannexb OUTPUT.ts
  • -bsf:v h264_mp4toannexb:指定视频过滤器为h264_mp4toannexb,这个过滤器的作用是将H.264流从长度前缀模式转换为开始代码前缀模式。

H.264有四种画质级别,分别是baseline, extended, main, high:

  • Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
  • Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;(用的少)
  • Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced), 也支持CAVLC 和CABAC 的支持;
  • High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
c++ 复制代码
ffmpeg -i input.mp4 -profile:v baseline -level 3.0 output.mp4
ffmpeg -i input.mp4 -profile:v main -level 4.2 output.mp4
ffmpeg -i input.mp4 -profile:v high -level 5.1 output.mp4

GOP - I帧 P帧 B帧

  • 编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures )

  • 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示

  • GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成

  • I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)

简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。

  • I帧表示关键帧,你可以理解为经过适度地压缩这一帧画面的完整保留
  • P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别
  • B帧记录的是本帧与前后帧的差别
  • B帧传送的是它与前面的I帧或P帧和后面的P帧之间的预测误差及运动矢量

有了 I帧,P帧, 为什么需要B帧

  • 因为B帧记录的是前后帧的差别,比P帧能节约更多的空间

IDR 图像(立即刷新图像

  • 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像
  • H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,
    • 将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列
  • 如果前一个序列出现重大错误,在这里可以获得重新同步的机会
    • IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码

DTS PTS

为什么会有PTS和DTS的概念

  • P帧需要参考前面的I帧或P帧才可以生成一张完整的图片

  • B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片

  • 这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?

PTS和DTS的概念

  • DTS(Decoding Time Stamp):即解码时间戳
    • 这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
  • PTS(Presentation Time Stamp):即显示时间戳
    • 这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

文档链接说明

相关推荐
沐风_ZTL5 小时前
ubuntu中 使用C++ FFmpeg拉取RTSP视频流
c++·ubuntu·ffmpeg
Johnny_Hu20165 小时前
基于Qt开发FFMpeg遇到的编译错误问题
java·qt·ffmpeg
程序猿玖月柒6 小时前
音频文件格式——AAC、OGG和FLAC
ffmpeg·音视频·aac·ogg·flac
学习嵌入式的小羊~1 天前
音视频流媒体中的实 时 流式 传 输 (Realtime (progressive streaming)。streaming )和 顺 序 流式 传 输
ffmpeg·音视频
去往火星1 天前
【ffmpeg 到Qt的图片格式转换】精彩的像素:深入解析 AVFrame 到 QImage 的转换
开发语言·qt·ffmpeg
Leon_Chenl2 天前
FFmpeg 头文件完美翻译之 libavdevice 模块
ffmpeg·音视频·c·编解码·libavdevice
EasyGBS3 天前
设备通过国标GB28181接入EasyCVR,显示在线但视频无法播放的原因排查
网络·ffmpeg·音视频
belldeep3 天前
python:如何播放 .spx 声音文件
python·ffmpeg·pyaudio
skywalk81635 天前
Windows下怎么安装FFFmpeg呢?
windows·ffmpeg