Android实现FFmpeg命令行功能——在你的手机上使用命令行剪辑视频

1,524 阅读8分钟

Android实现FFmpeg命令行功能——在你的手机上使用命令行剪辑视频

前言

FFmpeg是著名的开源多媒体框架,能够解码、编码、转码、复用、解复用、流式传输、过滤和播放人类和机器创建的几乎所有音视频。ffmpeg是FFmpeg的源代码编译生成的一个命令行工具,用户可以使用FFmpeg命令完成音视频的编辑。本文在Android上实现FFmpeg命令行功能,用户可以在本文的软件音视频编辑器上使用FFmpeg命令操作音视频。本文是Android+FFmpeg+x264重编码压缩你的视频的下集。

ffmpeg.c的使用

ffmpeg.exe是FFmpeg官方提供的Windows命令行工具,功能非常强大,大家在FFmpeg官网即可直接下载。本文项目就是将ffmpeg.exe“迁移”到Android上。ffmpeg.exe的主要逻辑是放在FFmpeg源代码文件ffmpeg.c中。

ffmpeg.c在源代码中的位置

理论上我们将ffmpeg.c中的代码放在Android项目中就可以在Android软件中实现ffmpeg.exe的功能,或者直接把ffmpeg.c及其相关的源代码文件编译成so库,再放在Android项目中使用。本文项目选择将ffmpeg.c及其相关的源代码文件编译成so库使用。ffmpeg.c编译的具体步骤可以参见安卓部署ffmpeg全平台so并实现命令行调用给Android 开发者的 FFmpeg 教程 (三) 集成FFmpeg命令行这两篇文章。
ffmpeg.c有个main函数,是FFmpeg命令行程序的入口。

ffmpeg.c的main函数

argc是传入的参数的个数,argv是传入参数的数组。例如输入 ffmpeg -h,那么argc=2,argv=["ffmpeg","-h"]。
我们在Android项目中的cpp文件声明这个main函数,将FFmpeg命令作为参数传入main函数,main函数就可以完成FFmpeg命令的实现。思路如下:

//声明ffmpeg.c的main函数
extern "C" int main(int argc, char **argv);
...
argc=2;
argv[0]="ffmpeg";
argv[1]="-h";
main(argc, argv);

FFmpeg项目里面输出日志信息的函数是av_log,ffmpeg.c执行命令行的日志都会通过av_log打印出来。我们当然需要获取这些日志信息,恰好FFmpeg提供了一个av_log_set_callback 函数帮助开发者根据自己的需要自定义回调函数来处理av_log输出的日志信息。我们使用av_log_set_callback将日志文件保存在本地文件中。

av_log_set_callback(
    [](void *avcl, int level, const char *fmt, va_list vl){
        ...
        //将待打印的内容(有可能是变参列表)转换成字符串并写入buffer的相关结构体和函数
        AVBPrint part;
        av_bprint_init(&part, 0, 65536);
        av_vbprintf(&part, fmt, vl);
        ...
        //打印日志到Android Studio控制台
        __android_log_print(ANDROID_LOG_INFO, "ffmpeg", "%d,%s",level, part.str);
        //写入本地日志文件
        ffmpeg_log_file.write(part.str, part.len);
        ffmpeg_log_file.flush();
        ...
    }
)

我们看一下能不能在Android上正常调用ffmpeg.c的main函数。 运行测试结果如下:

Android上测试main函数输出日志

看起来是能够正常跑起来的,但是有两个问题。一个问题是按理来说输入ffmpeg -h,应该会输出一大串FFmpeg命令行的使用帮助示例。如下图。

Windows上ffmpeg.exe输入ffmpeg -h的结果

但是在Android上输出"Applying option h (show help) with argument (null)."后就结束了,没有输出更多信息。不过我后面测试了一下诸如

ffmpeg -i video.flv -c:v libx264 -c:a flac video.mkv

这样的音视频编辑命令是可以执行和输出日志的,可以正常使用。
另一个问题是经过我的验证,main函数中会存在参数数组访问越界的行为。输出日志中的"Applying option h (show help) with argument (null)."的(null)就是访问 argv[argc]的结果。我出于安全考虑传参时候多申请一个空间。

    char **argv=new char * [argc+1];
    argv[argc]=NULL;

FFmpeg命令行编辑框

FFmpeg命令直接交给ffmpeg.c执行即可,我们下面实现接受用户输入FFmpeg命令的功能。
本项目的UI由Jetpack Compose构建。Jetpack Compose是一个构建原生Android UI的现代工具包,通过它我们可以使用声明式的代码快速开发Android界面。用户输入FFmpeg命令行的页面由一个提交按钮和一个巨大的文本编辑框组成就行了。
Compose函数TextField可以构建Material Design风格的、具有视觉强调效果的文本编辑框,通常被用在表单和对话框中。我们使用它来生成我们的FFmpeg命令行编辑框。

        //创建可观察变量text接受用户输入
        val text= remember {
            mutableStateOf("")
        }
        //创建FFmpeg命令行编辑框
        TextField(
            modifier = Modifier
                .fillMaxSize(),//占满可用的所有屏幕
            value = text.value,
            onValueChange = {
                text.value = it
            },
            label = { Text("FFmpeg命令行编辑框")) }
        )

在文本编辑框上面添加一个提交按钮。

        Button(onClick = {
            ...//将用户输入分割成一个个参数后传给ffmpeg.c的main函数
        }) {
            Text("确定")
        }

这样我们就完成了一个简易的FFmpeg命令行编辑页面了。

简易的FFmpeg命令行编辑页面

日志输出和日志查看功能

Windows上的ffmpeg.exe在执行命令的同时会在命令行窗口打印日志。如下。

ffmpeg.exe输出执行日志

同样的,我们将用户输入的命令行提交给ffmpeg.c的main函数后,还需要将FFmpeg执行输出的日志同步展示在界面上以便用户了解执行情况。在上文中,我们使用av_log_set_callback将av_log输出的日志信息持续追加到本地日志文件里,现在我们可以持续读取本地日志文件的内容,将日志内容展示在界面上。
我的想法是用户点击确定按钮提交FFmpeg命令行后,立即弹出一个小窗口展示输出日志。可以通过Compose函数AlertDialog构建一个对话框小窗口。

        AlertDialog(
            onDismissRequest ={...//响应点击对话框外位置的代码},
            title ={...//标题},
            text ={...//对话框中展现的内容},
            confirmButton ={...//响应点击确定的代码},
            dismissButton ={...//响应点击取消的代码},
        )   

我们一行行读取日志文件中的内容,将日志加入列表里

val file=File(path)
val reader = FileReader(file)
val bufferedReader = BufferedReader(reader)
...
while(...){
    line=bufferedReader.readLine()
    if(line!=null){
         log_lines.add(line)
         ...
    }
}

然后将列表展现在界面上,这里用到LazyColumn函数,LazyColumn是高效地显示大量或长度未知的列表项的延迟组件。

LazyColumn(...//列表展示属性设置){
 items(
   count = log_lines.size
    ){
      Text(log_lines[it])//it是序号
    }
}

我们需要借助线程保证FFmpeg命令执行和用户界面动态展示日志是并发进行的。我们在一个子线程里调用ffmpeg.c中的main函数执行FFmpeg命令和更新日志文件。

std::thread t1([] {
...
av_log_set_callback(
    [](void *avcl, int level, const char *fmt, va_list vl){
        ...
        //写入本地日志文件
        ffmpeg_log_file.write(part.str, part.len);
        ffmpeg_log_file.flush();
        ...
    }
)
...
main(argc, argv);
...
});
t1.detach();

在前台界面我们使用协程来读取日志和更新列表,协程是一种用户级的轻量级线程,viewModelScope是专门在viewModel中使用的协程作用域。

fun startReadLogFile(){
   viewModelScope.launch{
      ...//读取日志和更新列表
      delay(500)//延迟500毫秒后再更新
   } 
}

这样,我们就完成了日志输出的功能。如下。

日志输出

FFmpeg命令执行结束后用户可能还需要查看日志,我们还需提供查看完整日志的功能,也就是文本文件浏览功能。我们直接将上面那个日志输出页面改改变成能查看完整日志的页面。我们继续用AlertDialog生成小窗口展示日志。还是一行行读取文件,每次读取100行,在小窗口添加一个加载按钮,用户点击再继续加载后面的日志内容。如下。

日志浏览界面

总结

完成上面的开发,我们就得到了一个可以在Android上执行FFmpeg命令的app。在我自己的x86 Android10虚拟机和arm64-v8a Android14的真机上都可以正常运行。我感觉在手机上敲命令行并不是一件很方便的事情,而且我自己也记不住许多FFmpeg命令。FFmpeg命令的输入输出文件最好使用绝对路径,我发现在手机上复制绝对路径还挺麻烦的,我自己手机上的默认文件浏览器和系统文件管理应用都不支持直接复制文件夹和文件的绝对路径。后面我打算添加一些支持可视化操作的页面,用户在页面上确定好各种选项后再生成FFmpeg命令来完成音视频编辑。
大家可以在GitHub上看见本项目的源码并下载编译好的的安卓应用。

音视频编辑器源码

如果不方便访问GitHub可以通过下面的链接下载编译好的的安卓应用。

音视频编辑器下载

本项目代码写的比较粗糙,如有不对请多多指教。有什么问题或者建议可以在评论区、私信或者邮箱联系我。 码字和写代码不易,希望大家能喜欢和关注我的项目,谢谢。

参考

1.安卓部署ffmpeg全平台so并实现命令行调用
2.给Android 开发者的 FFmpeg 教程 (三) 集成FFmpeg命令行
3.Android 音视频开发打怪升级系列文章
4.Jetpack Compose Codelabs Android官方示例项目