FFmpeg

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

本文在 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  // 尖括号指定位置
#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
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 
#include 
#include 

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 
#include 

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 
#include 
#include 

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
  • 退出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)