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 <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,然后设置一个定时器,当定时器超时后,刷新新的视频帧,如此反复操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容