音视频开发实战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_AVDEVICEavdevice_register_all();//老版本还有很多需要注册的,包括编码器,解码器,解复用等,新版的不需要处理了
#endifavformat_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库里面查看

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

26.JavaWeb-SpringSecurity安全框架

1.SpringSecurity安全框架 Spring Security是一个功能强大且灵活的安全框架&#xff0c;它专注于为Java应用程序提供身份验证&#xff08;Authentication&#xff09;、授权&#xff08;Authorization&#xff09;和其他安全功能。Spring Security可以轻松地集成到Spring框架中…

MySQL数据库(五)

目录 一、数据库的约束 1.1 约束类型 1.1.1 null约束 1.1.2unique约束 1.1.3default默认值约束 1.1.4primary key主键约束 1.1.5foreign key外键约束 二、内容重点总结 一、数据库的约束 1.1 约束类型 not null - 指示某列不能存储 null值。unique - 保证某列的每行必须有唯一…

上市公司Git分支管理规范

Git分支管理策略 主分支Master 首先&#xff0c;代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本&#xff0c;都在这个主分支上发布。 Git主分支的名字&#xff0c;默认叫做Master。它是自动建立的&#xff0c;版本库初始化以后&#xff0c;默认就是在主…

采集传感器的物联网网关怎么采集数据?

随着工业4.0和智能制造的快速发展&#xff0c;物联网&#xff08;IoT&#xff09;技术的应用越来越广泛&#xff0c;传感器在整个物联网系统中使用非常普遍&#xff0c;如温度传感器、湿度传感器、光照传感器等&#xff0c;对于大部分物联网应用来说&#xff0c;采集传感器都非…

Ubuntu学习笔记(二)——文件属性与权限

文章目录 前言一、用户与用户组1.用户&#xff08;文件拥有者&#xff09;2.用户组3.其他人 二、Linux用户身份与用户组记录文件1. /etc/passwd2. /etc/shadow3. /etc/group 三、文件属性与权限1. 查看文件属性的方法&#xff08;ls&#xff09;2.文件属性详细介绍2.1 权限2.2 …

MacOS触控板缩放暂时失灵问题解决

我的系统版本为Monterey 12.5.1&#xff0c;亲测有效 直接创建脚本xxx.sh&#xff0c;并在终端执行脚本bash xxx.sh即可解决此问题&#xff0c;脚本内容如下&#xff1a; #!/bin/bashkillall Finder #kill Finder如不需要可以删除 killall Dock #kill Dock 如不需要可以删…

【wxWidgets】使用布局控件进行窗口布局

使用布局控件进行窗口布局 窗口布局基础 为了在各种环境中都能使窗口拥有合适的位置和大小&#xff0c;可能需要在OnSize事件中计算每一个窗口的大小并设置新位置&#xff0c;当然使用窗口布局控件可以更方便地实现 如果选择使用布局控件&#xff0c;可以通过自己编写或者使用…

【汉诺塔问题分析】

一、背景 汉诺塔问题是一种经典的递归问题&#xff0c;它由法国数学家Huygens在1665年发现&#xff0c;也是一道有趣的数学难题。这道问题的主要目的是将三根柱子上的一堆盘子移动到另一根柱子上&#xff0c;移动过程中每次只能移动一个盘子&#xff0c;并且大盘子不能放在小盘…

[QT编程系列-10]:C++图形用户界面编程,QT框架快速入门培训 - 4- QT画图与动画

目录 4. QT画图与动画 4.1 QT的绘图系统 4.2 案例目标 4.3 绘制过程 4.4 更换控件的icon 4.5 案例2 4.6 坐标轴 4. QT画图与动画 4.1 QT的绘图系统 QT&#xff08;也称为Qt Framework&#xff09;是一种流行的跨平台应用程序开发框架&#xff0c;它提供了丰富的图形用户…

集群基础1——集群概念、LVS负载均衡

文章目录 一、基本了解二、LVS负载均衡2.1 基本了解2.2 工作模式2.2.1 NAT模式2.2.2 DR模式2.2.3 LVS-TUN模式2.2.4 LVS-FULLNAT模式 三、调度器算法四、ipvsadm命令 一、基本了解 什么是集群&#xff1f; 多台服务器做同一件事情。 集群扩展方式&#xff1a; scale up&#xf…

2023年7月北京/广州/深圳制造业产品经理NPDP认证招生

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

C# 移除链表元素

203 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5] 示例 2&#x…

2023-07-14:讲一讲Kafka与RocketMQ中存储设计的异同?

2023-07-14&#xff1a;讲一讲Kafka与RocketMQ中存储设计的异同&#xff1f; 答案2023-07-14&#xff1a; 在Kafka中&#xff0c;文件的布局采用了Topic/Partition的方式&#xff0c;每个分区对应一个物理文件夹&#xff0c;且在分区文件级别上实现了顺序写入。然而&#xff0…

WIN无法访问linux开启的SAMBA服务器

WIN无法访问linux开启的SAMBA服务器 打开搜索框“管理Windows凭据” 点击编辑

Camtasia Studio 2023保存为mp4格式的视频的详细教程,Camtasia的视频导出功能

很多用户刚接触Camtasia Studio&#xff0c;不熟悉如何保存mp4格式的视频。在今天的文章中小编为大家带来了Camtasia Studio 2023保存为mp4格式的视频的详细教程介绍。 1、 打开Camtasia Studio。 Camtasia Studio- 2023 win&#xff1a; https://souurl.cn/1JFEsn Camtasia …

06_本地方法接口+07_本地方法栈

一、本地方法&#xff1f; 本地方法就是Java调用非Java代码的接口。 本地方法的作用是融合不同的编程语言为Java所用&#xff0c;它的初衷是融合 C、C程序 二、为什么要使用Native Method? 三、本地方法栈 Java虚拟机栈用于管理Java方法的调用&#xff0c;而本地方法栈用于…

【Linux】Docker 基本管理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 Docker 基本管理 Docker 概述Docker 核心概念Docker 安装部署Docker 镜像操作Docker 容器操作 Docker 概述 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵…

spring复习:(40)全注解的spring AOP

零、需要的依赖&#xff1a; <dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>1.8.9</version></dependency><dependency><groupId>org.aspectj</groupId><arti…

AHB协议理解

从小父亲就教育我&#xff0c;做一个对社会有用的人&#xff01; 目录 Chapter1 AHB Block Diagram Ginput signal lnput signals Output Signal Chapter3 Transfers AHB接口Overview Chapter6 Data Buses HWDATA HRDATA Chapter1 Introduction AHB: Advanced High-performanc…

奇迹MU架设教程:SQL Server 2008数据库的安装教程

不管是搭建什么游戏&#xff0c;都是有数据库的&#xff0c;奇迹MU用的是SQL 数据库&#xff0c;根据服务器系统选择SQL server版本&#xff0c;我比较喜欢用Windows server 2008R2系统&#xff0c;所以我安装的是SQL server 2008。作为架设奇迹很重要的数据库程序&#xff0c;…