FFmpeg

作者: simplehych | 来源:发表于2019-09-26 09:45 被阅读0次

    从开发小白到音视频专家 七牛云

    本文在 Mac 系统下操作

    安装

    下载

    git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
    

    配置

    ./configure --prefix=/usr/local/Cellar/ffmpeg --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --enable-debug --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags= --host-ldflags=
    

    报错进行安装

    Q: nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.

    A: brew install yasm

    Q:ERROR: libfdk_aac not found

    A: brew install fdk-aac

    编译安装

    make && make install 
    

    Q:mkdir: /user/local/ffmpeg/lib: Permission denied
    make: *** [install-libavdevice-static] Error 1

    A: 修改--prefix=/usr/local/Cellar/ffmpeg路径,之前是/usr/local/ffmpeg

    Q: speex not found using pkg-config / x265 not found using pkg-config
    A1: 强制指定

    ./configure --prefix=/usr/local/Cellar/ffmpeg --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --enable-debug --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags= --host-ldflags=\
    --extra-ldflags="-L/usr/local/Cellar/speex/1.2.0/lib -L/usr/local/Cellar/x265/3.0/lib" \
    --extra-cflags="-I/usr/local/Cellar/speex/1.2.0/include -I/usr/local/Cellar/x265/3.0/include"
    

    亲测结果为:speex强制指定有效,x265无效仍报错

    A2: 如果没有安装x265,先安装brew install x265;如果已经安装,pkg-config --list-all 查看所有关联的包是否有x265,如果没有重新安装x265 brew reinstall x265。检查一下环境变量的配置 PKG_CONFIG_PATH

    编译

    gcc/clang -g -O2 -o test test.c -I ... -L ... -lxxx
    -g:输出文件中的调试信息
    -O:对输出文件做指令优化,-O2编译器优化,-O1不优化
    -o:输出文件
    -I:指定头文件位置
    -L:指定库文件的位置
    -l:指定使用哪个库

    编译

    vi add.h // 头文件声明
    #ifndef __MY_LIBRARY__
    #define __MY_LIBRARY__
    int add(int a, int b);
    #endif
    
    vi add.c
    #ifndef __MY_LIBRARY__
    #define __MY_LIBRARY__
    int add(int a, int b){
            return (a+b);
    }
    #endif //__MY_LIBRARAY__
    
    clang -g -c add.c  // -c 编译生成 add.o
    libtool -static -o libmylib.a add.o // 输出生成libmylib.a第三方库
    
    vi test_lib.c
    #include <stdio.h> // 尖括号指定位置
    #include "add.h" //引入第三方库,双引号,优先在本地目录搜索
    int main(int argc, char* argv[])
    {
            printf("add=%d\n",add(1,3));
            return 0;
    }
    
    clang -g -o test_lib test_lib.c -I . -L . -lmylib
    ./test_lib // 执行程序
    

    调试

    命令 gdb lldb
    设置断点 breakpoint b b
    运行程序 run r r
    单步执行 next n n
    跳入函数 step s s
    挑出函数 finish finish
    打印内容print p p
    继续执行完 continue c c
    查看断点 break list break list
    退出 quit quit
    查看指针 x/6d(s) xxxx x/6d(s) xxxx

    test_lib.dSYM/Contents/Resources/DWARF

    调试信息:指令地址、对应源代码及行号

    dwarfdump test_lib
    

    代码结构

    描述
    libavcodec 编码器的实现
    libavformate 实现在流协议,容器格式及其本 IO 访问
    libavutil 包括了 hash 器,解码器和各种工具函数
    libavfilter 提供各种音视频过滤器
    libavdevice 提供了访问捕获设备和回访设备的接口
    libswresample 实现了混音和重采样
    libswscale 实现了色彩转换和缩放功能

    日志系统

    步骤

    include <libav/log.h>
    ac_log_set_level(AV_LOG_DEBUG) 设置阈值
    av_log(NULL, AV_LOG_INFO, "%s \n", op)

    日志级别

    AV_LOG_ERROR
    AV_LOG_WARNING
    AV_LOG_INFO
    AV_LOG_DEBUG

    文件的删除和重命名

    avpriv_io_delete()
    avpriv_io_move(src, dst)

    // 设置 ffmpeg的 pkgconfig的环境变量 PKG_CONFIG_PATH
    // export PKG_CONFIG_PATH=/usr/local/Cellar/ffmpeg/lib/pkgconfig 命令行单独使用只本次使用有效
    
    pkg-config --cflags --libs libavformat
    -I/usr/local/Cellar/ffmpeg/include -L/usr/local/Cellar/ffmpeg/lib -lavformat
    
    clang -g -o ffmpeg_del ffmpeg_file.c `pkg-config --cflags --libs libavformat`
    

    操作目录重要函数

    avio_open_dir()
    avio_read_dir()
    avio_close_dir()

    AVIODirContext
    操作目录的上下文

    AVIODirEntry
    目录项,用于存放文件名,文件大小等属性信息

    #include <stdio.h>
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc,char* argv[])
    {
            int ret;
    
            AVIODirContext *ctx = NULL;
            AVIODirEntry *entry = NULL;
    
            av_log_set_level(AV_LOG_INFO);
    
            ret = avio_open_dir(&ctx, "./", NULL);
            if(ret < 0){
                    av_log(NULL, AV_LOG_ERROR,"Cant open dir: %s\n", av_err2str(ret));
                    goto __fail;
            }
    
            while(1){
                    ret = avio_read_dir(ctx, &entry);
                    if(ret < 0){
                            av_log(NULL, AV_LOG_ERROR, "Cant read dir: %s \n", av_err2str(ret));
                            goto __fail;
                    }
                    if(!entry){
                            break;
                    }
                    av_log(NULL,AV_LOG_INFO, "%12"PRId64" %s \n", entry->size, entry -> name);
                    avio_free_directory_entry(&entry);
            }
    
            __fail:
            avio_close_dir(&ctx);
            return 0;
    }
    

    多媒体文件的基本概念

    • 多媒体文件其实是个容器

    • 在容器里有很多流/轨(Stream/Track),不交叉

    • 每种流是由不同的编码器编码的

    • 从流中读出的数据称为包

    • 在一个包中包含着一个或多个帧

    几个重要的结构体

    AVFormatContext
    AVStream
    AVPacket

    操作流数据的基本步骤

    解复用 -> 获取流 -> 读数据包 ->释放资源

    实战打印音视频信息

    • av_register_all()

    • avformat_open_input()/avformat_close_input()

    • av_dump_format()

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc, char* argv[])
    {
        AVFormatContext *fmt_ctx = NULL;
        int ret = 0;
    
        av_log_set_level(AV_LOG_INFO);
    
        av_register_all();
    
        ret = avformat_open_input(&fmt_ctx, "./test.mp4", NULL, NULL);
        if(ret < 0){
            av_log(NULL, AV_LOG_ERROR, "Can't open file: %s \n", av_err2str(ret));
            return -1;
        }
    
        av_dump_format(fmt_ctx, 0, "./test.mp4", 0); // 第四个参数 0/1 输入/输出
    
        avformat_close_input(&fmt_ctx);
    
        return 0;
    }
    
    Input #0, mov,mp4,m4a,3gp,3g2,mj2, from './test.mp4':
      Metadata:
        major_brand     : mp42
        minor_version   : 512
        compatible_brands: isomiso2avc1mp41
        creation_time   : 2019-04-16T01:08:00.000000Z
      Duration: 00:00:15.30, bitrate: N/A
        Stream #0:0(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, 2 channels, 2 kb/s (default)
        Metadata:
          creation_time   : 2019-04-16T01:08:00.000000Z
        Stream #0:1(und): Video: h264 (avc1 / 0x31637661), none(bt709), 1920x1080, 5736 kb/s, 30 fps, 30 tbr, 3k tbn (default)
        Metadata:
          creation_time   : 2019-04-16T01:08:00.000000Z
          encoder         : JVT/AVC Coding
    

    实战抽取音频数据

    • av_init_packet()
    • av_find_best_stream()
    • av_read_frame() / av_packet_unref()admin
    #include <stdio.h>
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc, char* argv[])
    {
        int ret;
        int audio_index;
        int len;
        char* src = NULL;
        char* dst = NULL;
    
        AVPacket pkt;
        AVFormatContext *fmt_ctx = NULL;
    
        // 日志级别
        av_log_set_level(AV_LOG_INFO);
    
        // 注册所有的编解码器和协议 register all format and codec
        av_register_all();
    
        // 1. read two params from console
        if(argc < 3){
            av_log(NULL, AV_LOG_ERROR, "the count of params should be more that three!\n");
            return -1;
        }
    
        src = argv[1];
        dst = argv[2];
    
        if(!src || !dst){
            av_log(NULL, AV_LOG_ERROR, "src or dst is null!\n");
            return -1;
        }
    
    
        ret = avformat_open_input(&fmt_ctx, src, NULL, NULL);
        if(ret < 0){
            av_log(NULL, AV_LOG_ERROR, "Can't open file: %s \n", av_err2str(ret));
            return -1;
        }
    
        // 输出 meta 信息
        av_dump_format(fmt_ctx, 0, src, 0);
    
        // 创建输出文件
        FILE* dst_fd = fopen(dst, "wb");
        if(!dst_fd){
            av_log(NULL, AV_LOG_ERROR, "Can't open out file!\n");
            avformat_close_input(&fmt_ctx);
            return -1;
        }
    
        // 2. get Stream
        ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
        if(ret < 0){
            av_log(NULL, AV_LOG_ERROR, "Cant't find the best stream!\n");
            avformat_close_input(&fmt_ctx);
            fclose(dst_fd);
            return -1;
        }
    
        audio_index = ret;
    
        // 初始化包
        av_init_packet(&pkt);
    
        // 获取每个包
        while(av_read_frame(fmt_ctx, &pkt) >= 0){
    
            if(pkt.stream_index == audio_index){
    
                // 3. write audio data to aac file
                len = fwrite(pkt.data, 1, pkt.size, dst_fd);
                if(len != pkt.size){
                    av_log(NULL, AV_LOG_WARNING, "waning, length of data is not equal size of pkt!\n");
                }
            }
            av_packet_unref(&pkt);
        }
    
        avformat_close_input(&fmt_ctx);
    
        if(dst_fd){
            fclose(dst_fd);
        }
    
        return 0;
    }
    

    抽取视频数据

    • Start code 特征码
      区分视频帧

    开始 + 视频帧的长度

    开始 + 特征码

    • SPS / PPS
      作用:解码视频参数,例如视频帧分辨率、帧率
      参数放在哪?
    1. 一般需要一个 SPS / PPS,每次解码都有相同的

    2. 当分辨率发生变化时,需要更新 SPS / PPS,一般有多个。

    3. 直播流里,网络的原因丢数据,切换分辨率丢帧,在每一帧添加 SPS / PPS,不会增加流量,只有几个字节很小,没有任何负担

    切换分辨率等变换 SPS / PPS,数据丢包在每一帧添加 SPS / PPS

    • codec -> extradata

    从编码器 codec 的扩展数据 extradata 中##获取 SPS / PPS##,不是在正常的存储数据包,

    
    

    将 MP4 转成 FLV 格式

    • avformat_alloc_output_context2() / avformat_free_context()
    • avformat_new_stream()
    • avcodec_parameters_copy()
    • avformat_write_header()
    • av_write_frame() / av_interleaved_write_frame()
    • av_write_trailer()

    从 MP4 截取一段视频

    • av_seek_frame()

    FFmpeg 中级开发

    H264解码
    H264编码
    AAC 解码
    AAC 编码

    FFmpeg H264解码

    添加头文件

    • libavcodec / avcodec.h

    常用数据结构

    • AVCodec 编码器结构体
    • AVCodecContext 编码器上下文
    • AVFrame 解码后的帧

    结构体内存的分配与释放

    • av_frame_alloc() / av_frame_free()
    • avcodec_alloc_context3()
    • avcodec_free_context()

    解码步骤

    • 查找解码器(avcodec_find_decoder)
    • 打开解码器(avcodec_open2)
    • 解码(avcodec_decode_video2)

    FFmpeg H264编码

    H264编码流程

    • 查找编码器(avcodec_find_encoder_by_name) 解码通过 id,编码通过 name
    • 设置编码参数,并打开编码器(avcodec_open2)
    • 编码(avcodec_encode_video2)

    SDL

    介绍

    • SDL:Simple DirectMedia Layer
    • C语言实现的跨平台的媒体开源库
    • 多用于开发游戏/模拟器/媒体播放器等多媒体应用领域

    SDL编译与安装

    • 下载SDL的源码 http://www.libsdl.org/
    • 生成Makefile, ./configure --prefix=/usr/local/Cell
    • 安装 sudo make -j 8 && make install

    SDL使用步骤

    • 添加头文件 #include<SDL.h>
    • 初始化SDL
    • 退出SDL

    SDL渲染窗口

    • SDL_Init/SDL_Quit()
    • SDL_CreateWindow() / SDL_DestroyWindow()
    • SDL_CreateRender() / SDL_DestroyRender()
    • SDL_RenderClear()
    • SDL_RenderPresent

    SDL 事件基本原理

    • SDL将所有的事件都存放在一个队列中
    • 所有对事件的操作,其实就是对队列的操作
    SDL事件种类
    • SDL_WindowEvent:窗口事件
    • SDL_KeyboardEvent:键盘事件
    • SDL_MouseMotionEvent:鼠标事件
    SDL事件处理
    • SDL_PollEvent
    • SDL_WaitEvent
    • SDL_WaitEventTimeout

    纹理渲染

    纹理

    SDL渲染基本原理

    SDL渲染基本原理

    SDL纹理相关的API

    • SDL_CreateTexture()
      format:YUV, RGB
      access: Texture类型,Target,Stream
    • SDL_DestroyTexture()

    SDL渲染相关API

    • SDL_SetRenderTarget()
    • SDL_RenderClear()
    • SDL_RenderCopy()
    • SDL_RenderPresent()

    YUV 视频播放器

    创建线程

    • SDL_CreateThread()
      fn: 线程执行函数
      name: 线程名
      data: 执行函数参数

    SDL_更新纹理

    • SDL_UpdateTexture()
    • SDL_UpdateYUVTexture() 效率更高

    SDL播放音频

    播放音频基本流程

    播放音频基本流程

    播放音频的基本原则

    • 声卡向你要数据而不是你主动推给声卡
    • 数据的多少由音频参数决定的

    SDL 音频 API

    • SDL_OpenAudio/SDL_CloseAudio
    • SDL_PauseAudio
    • SDL_MixAudio 混音 API

    最简单的播放器

    • 该播放器只实现视频播放
    • 将 FFmpeg 与 SDL 结合到一起
    • 通过 FFmpeg 解码视频数据
    • 通过 SDL 渲染

    多线程与锁

    为啥要用多线程

    • 多线程的好处,管理充分利用 CPU
    • 多线程带来的问题

    线程的互斥与同步

    • 互斥
    • 同步

    锁与信号量

    • 锁的种类
      • 读写锁
      • 自旋锁
      • 可重入锁
    • 通过信号进行同步

    SDL 线程的创建

    • SDL_CreateThread
    • SDL_WaitThread

    SDL锁

    • SDL_CreateMutex / SDL_DestroyMutex
    • SDL_LockMutex / SDL_UnlockMutex

    SDL条件变量

    • SDL_CreateCond / SDL_DestroyCond
    • SDL_CondWait / SDL_CondSignal

    播放器线程模型

    播放器线程模型

    音视频同步

    时间戳

    • PTS:Presentation timestamp 渲染
    • DTS:Decoding timestamp 解码
    • I intra 关键帧 帧内压缩/ B bidirectional 前后参考帧 前一帧有 则后一帧不带 帧间压缩 /P predicted 向前参考帧 帧间压缩

    时间戳顺序

    实际帧顺序:I B B P
    存放帧顺序:I P B B
    解码时间戳:1 4 2 3
    展示时间戳:1 2 3 4

    从哪获得 PTS

    • AVPacket 中的 PTS
    • AVFrame 中的 PTS
    • av_frame_get_best_effort_timestamp()

    时间基

    • tbs:time base rate 帧率
    • tbn:time base of stream 流的时间基
    • tbc:time base of codec 解码的时间基

    计算当前帧的 PTS

    • PTS = PTS * av_q2d(video_stream -> time_base)
    • av_q2d(AVRotional a) {return a.num/(double)a.den;}

    计算下一帧的 PTS

    • video_clock: 预测的下一帧视频的 PTS
    • frame_delay: 1/tbr
    • audio_clock: 音频当前播放的时间戳

    音视频同步方式

    • 视频同步到音频
    • 音频同步到视频
    • 音频和视频都同步到系统时钟

    视频播放的基本思路

    一般的做法,展示第一针视频帧之后,获取要显示的下一视频帧的 PTS,然后设置一个定时器,当定时器超时后,刷新新的视频帧,如此反复操作。

    相关文章

      网友评论

        本文标题:FFmpeg

        本文链接:https://www.haomeiwen.com/subject/pfeuwqtx.html