音视频开发实战03-FFmpeg命令行工具移植

一,背景

作为一个音视频开发者,在日常工作中经常会使用ffmpeg 命令来做很多事比如转码ffmpeg -y -i test.mov -g 150 -s 1280x720 -codec libx265 -r 25 test_h265.mp4 ,水平翻转视频:ffmpeg -i src.mp4 -vf hflip -acodec copy -vcodec h264 -b 22000000 out.mp4,视频截取:ffmpeg -i input.wmv -ss 00:00:30.0 -c copy -t 00:00:10.0 output.wmv 等等,一个简单的命令就可以解决很多事情,如果通过执行一些命令就能完成日常开发工作,那么能极大的提升我们的开发效率,但是这些命令只能在PC上使用,在移动端是无法直接使用的,这也就引出了这篇文章的所要讲的内容–FFmpeg命令行工具编译

编译好的工程:https://github.com/bookzhan/bzffmpegcmd 想偷懒的可以直接跳过本文,直接使用或者直接看源码,记得给一个Start,不过建议完整看完本文,你收获的会更多

由于ffmpeg命令是一个功能完备且比较独立的模块,因此在开发中我们一般都编译为一个独立的SO,在需要的地方作为动态库引入就好了,话不多说,我们来看看FFmpeg官方在PC上实现ffmpeg命令的过程:

二,FFmpeg实现ffmpeg命令的方式

本文使用的FFmpeg版本为6.0,其它版本大同小异

通过查看源码,不难发现FFmpeg实现ffmpeg命令是通过fftools/ffmpeg.c文件来实现的,通常这种.c都有一个入口函数,也就是我们常见的main函数,在ffmpeg.c的入口函数为int main(int argc, char **argv) 其中argc是args count的缩写,在c函数中传指针都需要指定指针的长度,根据这个长度来防止访问越界,char **argv是一个二级指针,里面存放的是参数,类似于ffmpge, -i , test.mov, out.mp4的字符串
在这里插入图片描述
进一步查看main函数,就可以发现这个函数很短,但是基本流程都包括了,详见下面的注释:

int main(int argc, char **argv)
{
    int ret;
    BenchmarkTimeStamps ti;
    init_dynload();//加载动态库的,用于处理Windows,dll库的
    register_exit(ffmpeg_cleanup);//程序结束的回调
    setvbuf(stderr,NULL,_IONBF,0); /* win32 runtime needs this */
    av_log_set_flags(AV_LOG_SKIP_REPEATED);
    parse_loglevel(argc, argv, options);
#if CONFIG_AVDEVICE
    avdevice_register_all();//老版本还有很多需要注册的,包括编码器,解码器,解复用等,新版的不需要处理了
#endif
    avformat_network_init();//只是需要初始化一次就好了
    show_banner(argc, argv, options);
    /* parse options and open all input/output files */
    ret = ffmpeg_parse_options(argc, argv);
    if (ret < 0)
        exit_program(1);
    if (nb_output_files <= 0 && nb_input_files == 0) {
        show_usage();
        av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
        exit_program(1);
    }
    /* file converter / grab */
    if (nb_output_files <= 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
        exit_program(1);
    }
    current_time = ti = get_benchmark_time_stamps();
    if (transcode() < 0)//核心流程
        exit_program(1);
    if (do_benchmark) {
        int64_t utime, stime, rtime;
        current_time = get_benchmark_time_stamps();
        utime = current_time.user_usec - ti.user_usec;
        stime = current_time.sys_usec  - ti.sys_usec;
        rtime = current_time.real_usec - ti.real_usec;
        av_log(NULL, AV_LOG_INFO,
               "bench: utime=%0.3fs stime=%0.3fs rtime=%0.3fs\n",
               utime / 1000000.0, stime / 1000000.0, rtime / 1000000.0);
    }
    av_log(NULL, AV_LOG_DEBUG, "%"PRIu64" frames successfully decoded, %"PRIu64" decoding errors\n",
           decode_error_stat[0], decode_error_stat[1]);
    if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1])
        exit_program(69);
    exit_program(received_nb_signals ? 255 : main_return_code);
    return main_return_code;
}

三,ffmpeg.c文件编译

如上所示,我们之需要把ffmpeg.c的main函数调用起来就好,听起来是不是很简单[手动狗头],那我们就来编译首先请按照:Android音视频开发实战01-环境搭建 把Native开发的环境搭建起来,包括ffmpeg的include的文件特别是config.h文件,以及ffmpeg so文件,最终的文件结构如下:
在这里插入图片描述

3.1 依赖文件处理

fftools 文件夹里面的文件很多,我们没有必要全部copy进去,先把ffmpeg.h,ffmpeg.c文件copy进去,然后看看哪里有报错,就把报错的文件的文件copy进去,最终需要的文件如下:(里面cpp和ffmpeg_cmd文件是后来新建的,请先忽略)
在这里插入图片描述

3.2 调用main函数

我们可以写一个jni函数把main函数直接调用起来,不会jni的可以参考:音视频开发实战02-JNI,写一个命令然后执行
我们把main函数调用起来之后会发现,命令执行成功了,但是app退出了类似发生crash了,入坑了?
在这里插入图片描述
没得办法只能一步步看源码,此处省略10086个字,最终在这个函数中发现了猫腻,如下:
在这里插入图片描述
没错,ffmpeg.c文件在运行过程中有很多地方调用了这个函数,退出的原因就在于执行了exit函数,exit在Linux系统中的实现就是退出进程,但是Android App运行起来后就一个主进程,退出后整个App就退出了,如果作为电脑的命令行工具那么没有问题,每一次执行都是新开一个进程,执行完后进程释放,但是作为作为Android应用那就不行了,我们注释掉之后,程序能够正常运行,不再退出。

3.3 程序健壮性处理

我们在接入一个陌生库的时候步骤一般如下:

  1. 先看License,看协议是否符合开源规范,常见的开源协议可以参考这篇文章:https://www.cnblogs.com/findumars/p/9874836.html
  2. 导入SDK,成功跑起来
  3. 异常参数调用测试
  4. 重复调用测试
  5. 多线程调用测试
  6. 内存泄漏检查
  7. 代码review确保没有高危代码

1,2,3没什么好说的,我们做后面的测试

3.3.1 重复调用测试

我们在重复调用main函数之后,你会惊奇的发现,程序会crash,FFmpeg会这么坑我,不可能,绝对不可能,接着看代码吧,此处省略10086个字,最终你会发现,ffmpeg.c文件里面的变量都是静态变量,如果是想PC那样作为进程来调用,那么自然没有问题,每次进程起来,这些变量就相当于是初始值,如果是面向对象编程也不存在这样的问题,每次new 一个Class那么这些变量也就恢复初始值了,嗨~吃了没有对象的亏!那么现在只能在每次程序运行完成后把这些变量的值重置。在ffmpeg_cleanup函数中把这些变量重置,如下:

static void ffmpeg_cleanup(int ret) {
   	//...
    progress_avio = NULL;

    input_files = NULL;
    nb_input_files = 0;

    output_files = NULL;
    nb_output_files = 0;

    filtergraphs = NULL;
    nb_filtergraphs = 0;

    ffmpeg_exited = 1;
}

3.3.2 多线程调用测试

在3.3.1中我们知道ffmpeg.c中有很多变量是静态的,那么在我们处理完后单线程调用肯定是没有什么问题的,但是在多线程调用的情况下,那么这些变量的读写就会串掉,随手测试一把就会发现疯狂的crash,加锁!C语言的加锁一般都是使用pthread提供的pthread_mutex_lock,其中cmdLock作为静态变量,全局唯一,如下:

	if (!cmdLockHasInit) {
        pthread_mutex_init(&cmdLock, NULL);//初始化
        cmdLockHasInit = 1;
    }
    pthread_mutex_lock(&cmdLock);
    //...处理逻辑
    pthread_mutex_unlock(&cmdLock);

3.3.3 内存泄漏检查

内存泄漏检查没有太多好说的,重复运行多次后观察内存增长情况就好了,这里经过测试ffmpeg.c没有什么问题
在这里插入图片描述

3.3.4 代码review确保没有高危代码

这一步不可少,这一步是确保代码健壮性的重要保障,即使常规case已经测试过了,这一步也可以提前做,不过我喜欢放在全部run起来之后再做,一开始就review代码很容易劝退。我们这里review代码不需要很仔细,重点要关注流程。
在我review代码的过程中发现ffmpeg.c有很多地方调用了exit_program方法,特别是在状态不对,发生错误的时候,在原先的实现中exit_program是直接把整个进程退出了,那么exit_program之后的代码就不会执行,但是我们不能退出进程,而且要确保exit_program方法执行完,后面的代码不能被调用,因为很多资源都被释放,状态已经不对了,代码往下执行会发生不可预知的问题。
因此我们需要修改调用exit_program的地方,改成retrun exit_program(), 同时让exit_program的返回值改成int,把传入的错误码再返回回去,确保错误码能够被传递到调用方,需要修改的地方很多,具体的请直接查看代码。
在这里插入图片描述

四,程序封装

4.1 支持以字符串的方式调用ffmpeg

我们可以看到ffmpeg.c的main函数的入参是一个二级指针,可以理解为一个二维数组,调用的时候很不方便,我们希望在使用的时候和在PC命令工具里面一样输入一个ffmpeg命令就可以直接使用,那么就涉及到命令的解析,如下:

char *pCommand = (char *) command;
    int stingLen = (int) (strlen(command) + 1);
    char *argv[stingLen];

    char *buffer = NULL;
    int index = 0;
    int isStartYH = 0;
    for (int i = 0; i < stingLen; ++i) {
        char str = *pCommand;
        pCommand++;
        if (NULL == buffer) {
            buffer = malloc(512);
            memset(buffer, 0, 512);
            argv[index++] = buffer;
        }
        //保证引号成对出现
        if (str == '"') {
            if (isStartYH) {
                isStartYH = 0;
            } else {
                isStartYH = 1;
            }
            continue;
        }
        if (str != ' ' || isStartYH) {
            *buffer = str;
            buffer++;
        } else {
            buffer = NULL;
        }
    }
    //手动告诉它结束了,防止出现意外
    argv[index] = 0;
    int ret = exe_ffmpeg_cmd(index, argv, handle, progressCallBack, totalTime);
    for (int i = 0; i < index; ++i) {
        free(argv[i]);
    }

经过这样处理之后,我们输入类似ffmpeg -i src.mp4 out.mp4之后就可以自动解析参数传入main函数了

4.2 支持进度回调

由于FFmpeg的命令一般都是处理音视频的,相对来说耗时较长,如果没有进度的话是很让人抓狂的一件事,ffmpeg处理音视频的流程一般来说很固定,如下:

  1. 读取文件
  2. 读取文件,视频流,音频流元信息
  3. 分配解码器
  4. 初始化输出文件
  5. 添加视频流,音频流
  6. 初始化编码器
  7. 解复用,循环读取音视频信息
  8. 解码
  9. 编码
  10. 复用-写音视频数据
  11. 完成
    我们要做进度回调的话一般都是在第10步去做处理,根据写入的音视频数据的时间戳/视频的总时间,那么就能得到我们想要的视频处理进度了,结合音视频开发实战02-JNI 所讲的回调函数的写法,我们可以很容易的实现
static int write_packet(Muxer *mux, OutputStream *ost, AVPacket *pkt)
{
	//...
    //回调处理
    enum AVMediaType mediaType;
    if (ost->hasVideoStream) {
        mediaType = AVMEDIA_TYPE_VIDEO;
    } else {
        mediaType = AVMEDIA_TYPE_AUDIO;
    }
    if (NULL != ost->st && NULL != pkt && pkt->dts > 0 && ost->duration > 0 &&
        NULL != ost->progressCallBack && mediaType == ost->st->codecpar->codec_type) {
        if (ost->writePacketCount % 2 == 0) {
            int64_t temp = pkt->dts * 1000 * ost->st->time_base.num /
                           ost->st->time_base.den;
            float progress = temp * 1.0f / ost->duration;
            ost->progressCallBack(ost->callBackHandle, 0, progress);
        }
        ost->writePacketCount++;
    }
    //回调处理结束
}

核心代码到这里就结束了,还有一些其他的封装就不再这里讲了,具体的可以去git库里面查看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/42283.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AtcoderABC244场

A - Last LetterA - Last Letter 题目大意 给定一个长度为N的字符串S&#xff0c;由小写英文字母组成&#xff0c;打印出S的最后一个字符。 思路分析 题目要求打印出字符串S的最后一个字符&#xff0c;可以直接通过访问S的最后一个元素来获取该字符。可以使用字符串的back()…

Meta发布升级大模型LLaMA 2:开源可商用

论文地址&#xff1a;https://ai.meta.com/research/publications/llama-2-open-foundation-and-fine-tuned-chat-models/ Github地址&#xff1a;https://github.com/facebookresearch/llama LLaMA 2介绍 Meta之前发布自了半开源的大模型LLaMA&#xff0c;自从LLaMA发布以来…

Apikit 自学日记:如何测试多个关联的 API

肯定会有人好奇&#xff0c;如果有多个关联的 API 如何做测试呢&#xff1f;很简单&#xff01;在 APIkit 中也有测试多个关联 API 的功能。 1、在流程测试用例详情页中&#xff0c;点击“ 添加测试步骤”&#xff0c;选择“从API文档添加API请求” 2、在对应的项目下选择关联的…

STL好难(8):map和set

目录 1.一些概念的理解 &#x1f349;关联式容器和序列式容器 &#x1f349;key模型、key/value模型 &#x1f349;树形结构关联式容器 2.set的介绍 &#x1f349;set文档 &#x1f349;set的使用 &#x1f352;set的模板参数列表 &#x1f352;set的构造 &#x1f3…

从制造到智造,安捷利的云数蝶变

伴随着新一轮科技革命和产业变革的兴起&#xff0c;制造业的数字化转型步入深水区&#xff0c;尤其是在5G、工业互联网、大数据等为代表的新技术推动下&#xff0c;制造业全方位、全链条的升级已是大势所趋。 南沙地处中国的南大门&#xff0c;既是国家面向世界的重要战略平台…

python和django中安装mysqlclient失败的解决方案

在Pychram中和pip中安装mysqlclient都不成功&#xff0c;只能直接下载二进制包进行安装了&#xff0c;下载页面中根据python的版本选择对应WHL包下载&#xff0c;下载地址 mysqlclient PyPIhttps://pypi.org/project/mysqlclient/#files 通过pip命令进行安装 pip install d:\…

传输网络介绍

文章目录 1、通信传输介质有哪些&#xff1f;2、通信网络常见的组网形式有哪些&#xff1f;3、光纤通信常用的复用技术是哪两种&#xff1f;4、SDH的复用技术是什么&#xff1f;5、灰光和彩光的区别在哪里&#xff1f;6、波长的计算公式&#xff1f;7、5G时代&#xff0c;承载网…

esp32-cam红外实时监控报警系统(巴发云和邮箱同时推送)

esp32-cam红外实时监控报警系统 设想-巴发云转折-照片数量限制代码避开巴发云照片限制邮箱的坑同时我的巴发云微信也受到了提醒报警&#xff0c;虽然没有图片显示。 设想-巴发云 我想做一个人体红外传感器发现人体报警&#xff0c;同时给我手机发报警提醒&#xff0c;同时发送…

​​Layui之用户管理实例(对数据的增删改查)

目录 ​编辑一、R工具介绍&#xff08;&#xff09; ​编辑二、数据表的增删改查 ​编辑2.1我们先得从查询数据库的语句入手 2.2优化dao类 2.4UserAction类 2.5前台的页面实现增删改查操作 2.6 userManage页面JS 2.7user新增、修改iframe层js 前言 上一篇我分享了…

【图像处理OpenCV(C++版)】——5.6 图像平滑之联合双边滤波

前言&#xff1a; &#x1f60a;&#x1f60a;&#x1f60a;欢迎来到本博客&#x1f60a;&#x1f60a;&#x1f60a; &#x1f31f;&#x1f31f;&#x1f31f; 本专栏主要结合OpenCV和C来实现一些基本的图像处理算法并详细解释各参数含义&#xff0c;适用于平时学习、工作快…

Orleans 微软基于 Actor 的分布式框架

一、Actor模型工作原理 Actor模型是一种并发编程模型&#xff0c;它基于消息传递实现&#xff0c;是一种轻量级的并发模型。在Actor模型中&#xff0c;每个Actor都是一个独立的执行单元&#xff0c;它可以接收和发送消息&#xff0c;并且可以执行一些本地操作&#xff0c;但是不…

Internet Download Manager IDM 破解版 中文便携版 v6.41.15

Internet Download Manager 介绍 Internet Download Manager&#xff0c;全球最佳下载利器。Internet Download Manager (简称IDM) 是一款Windows 平台功能强大的多线程下载工具&#xff0c;国外非常受欢迎。支持断点续传&#xff0c;支持嗅探视频音频&#xff0c;接管所有浏览…

【Python爬虫开发基础⑭】Scrapy架构(组件介绍、架构组成和工作原理)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;python网络爬虫从基础到实战 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; ⭐️前面比较重要的基础内容&#xff1a; 【Python爬…

【ArcGIS Pro微课1000例】0028:绘制酒店分布热力图(POI数据)

本文讲解在ArcGIS Pro中文版中,基于长沙市酒店宾馆分布矢量点数据(POI数据)绘制酒店分布热力图。 文章目录 一、加载酒店分布数据二、绘制热度图参考阅读: 【GeoDa实用技巧100例】004:绘制长沙市宾馆热度图 【ArcGIS微课1000例】0070:制作宾馆酒店分布热度热力图 一、加载…

从0到1构建证券行业组织级项目管理体系的探索与实践︱东吴证券PMO负责人娄鹏呈

东吴证券股份有限公司信息技术总部PMO负责人娄鹏呈先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;从0到1构建证券行业组织级项目管理体系的探索与实践。大会将于8月12-13日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&a…

联通 Flink 实时计算平台化运维实践

摘要&#xff1a;本文整理自联通数科实时计算团队负责人、Apache StreamPark Committer 穆纯进在 Flink Forward Asia 2022 平台建设专场的分享&#xff0c;本篇内容主要分为四个部分&#xff1a; 实时计算平台背景介绍 Flink 实时作业运维挑战 基于 StreamPark 一体化管理 …

力扣题目解析:生成奇数个字符的字符串的巧妙方法

本篇博客会讲解力扣“1374. 生成每种字符都是奇数个的字符串”的解题思路&#xff0c;这是题目链接。 这道题的解题思路很巧妙&#xff0c;它利用了字符串长度n的奇偶性&#xff1a; 如果n是奇数&#xff0c;那么就把字符串全部填充为’a’&#xff0c;这样每种字符都是奇数个…

基于linux下的高并发服务器开发(第一章)- 目录遍历函数

10 / 目录遍历函数 // 打开一个目录 #include <sys/types.h> #include <dirent.h>DIR *opendir(const char *name); 参数&#xff1a; - name: 需要打开的目录的名称 返回值&#xff1a; DIR * 类型&#xff0c;理解为目录流 错误…

网络安全/黑客技术—学习笔记

一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

韦东山Linux驱动入门实验班(4)LED驱动

前言 &#xff08;1&#xff09;我们学习完hello驱动之后&#xff0c;对驱动程序开发有了一点点认识了之后。现在可以开始对硬件进行实际操作了&#xff0c;本人使用的是i.max6ull开发板&#xff0c;STM32MP157和全志的D1H也会进行讲解。 &#xff08;2&#xff09;如果还有对于…