Android音视频-FFmpeg命令行工具使用

我们这篇主要了解使用FFmpeg命令行如何配置。在编译FFmpeg的时候,使用了参数-disable-ffmpeg,这样不会生成FFmpeg工具,如果生成了在Android应用也用不了,但是我们可以通过jni对代码做一些修改,间接的使用命令行工具。这个工具真的非常强大,例如
本示例接着上一篇的应用下面,所以不用引入libffmpeg.so和前面一些ndk开发的配置,具体的环境和配置信息见Android音视频-FFmpeg编译为单个so与测试调用,本篇的配置也是基于Cmake来进行配置的,感觉它比ndk-build好用的多。

Mac电脑引入FFmpeg命令行工具

当我们要直接进行视频的各种转换的时候,又不想下载别的软件,可以选择在电脑里面安装一个ffmpeg工具,安装步骤很简单:

  • 安装HomeBrew
  • 安装ffmpeg:brew install ffmpeg
  • 等待安装完成,最后在我的电脑里面找到安装的命令行工具位置为:/usr/local/Cellar/ffmpeg/3.4.2
  • 在上面的ffmpeg根目录进入到bin目录下面就可以执行ffmpeg命令了例如执行一个转换视频格式命令:ffmpeg -i input.mp4 output.avi注意这个input.mp4文件放到bin目录下面,否则找不到

引入命令行工具相关代码

我们以前已经把FFmpeg的相关头文件和libffmpeg.so引入了。下面只要引入如下文件:

复制ffmpeg相关文件

下面把根目录ffmpeg-3.3.6文件下面的ffmpeg.h,cmdutils.h,cmdutils_common_opts.h,config.h,ffmpeg.c, ffmpeg_opt.c, ffmpeg_filter.c,cmdutils.c, 文件引入进来,引入后cpp文件夹项目结构如下:



修改引入文件代码

修改ffmpeg.c和ffmpeg.h

  • ffmpeg.c
    把int main(int argc, char **argv)改为int run(int argc, char **argv)
  • ffmpeg.h
    在文件末尾添加函数声明:
    int run(int argc, char **argv);

修改cmdutils.c

找到cmdutils.c中的exit_program函数,注释到 exit(ret), 添加 return ret;
并修改函数的返回类型为int, 找到cmdutils.h中exit_program的申明,也把返回类型修改为int。
看的一篇文章说到这里就可以但是,但是当实际运行多次执行指令的时候有问题,因为FFmpeg每次执行完会调用ffmpeg_cleanup函数清理内存,并且调用exit(0)结束当前进程,但是我们把exit方法删掉了,怕下次执行没有清理掉有问题,于是简单的把一些变量置空:
把如下代码加到修改的run方法的return之前:

nb_filtergraphs = 0;
     progress_avio = NULL;

     input_streams = NULL;
     nb_input_streams = 0;
     input_files = NULL;
     nb_input_files = 0;

     output_streams = NULL;
     nb_output_streams = 0;
     output_files = NULL;
     nb_output_files = 0;

这个参考自这里
我没有去试会不会出现这个问题,这里留个笔记,怕以后遇到。

编写调用命令行代码

这里我们还是和以前一样,通过Java应用层声明native方法并且实现c文件以及它们之间的调用。

声明Java应用层代码

声明一个命令行工具类FFmpegKit.java,它调用底层的实现c代码

package com.lyman.ffmpeg_cmake_single.utils;

import java.util.ArrayList;

/**
 * Author: lyman
 * Email: [email protected]
 * Date: 2018/3/9
 * Description:
 */

public class FFmpegKit {
    private ArrayList<String> commands;
    static {
        System.loadLibrary("ffmpeg");
        System.loadLibrary("ffmpeginvoke");
    }


    public FFmpegKit() {
        this.commands = new ArrayList<String>();
        this.commands.add("ffmpeg");
    }

    public native static int run(String[] commands);
}

在Activity里面调用工具类方法:

package com.lyman.ffmpeg_cmake_single;

import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

import com.lyman.ffmpeg_cmake_single.utils.FFmpegKit;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CommandLineActivity extends AppCompatActivity {
    private static final String TAG = "CommandLineActivity";
    private ProgressBar mProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_command_line);
        mProgressBar = findViewById(R.id.progressBar2);
        if (!getMoveFile("input_video.mp4").exists()
                || !getMoveFile("input_audio.acc").exists()) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    copyFilesFromRaw(CommandLineActivity.this, R.raw.input_video,
                            "input_video.mp4",
                            getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
                    copyFilesFromRaw(CommandLineActivity.this, R.raw.input_audio,
                            "input_audio.acc",
                            getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath());
                }
            }).start();
        }
    }

    public void onClickStartCommand(View view) {
        mProgressBar.setVisibility(View.VISIBLE);
        new Thread(new Runnable() {
            @Override
            public void run() {
                String base = getMoveFile("input_video.mp4").getParent();
                Log.e("PATH", base);
                String[] commands = new String[9];
                commands[0] = "ffmpeg";
                commands[1] = "-i";
                commands[2] = base + "/input_video.mp4";
                commands[3] = "-i";
                commands[4] = base + "/input_audio.acc";
                commands[5] = "-strict";
                commands[6] = "-2";
                commands[7] = "-y";
                commands[8] = base + "/merge.mp4";
                int result = FFmpegKit.run(commands);
                Log.e("RESULT", result + "**********************");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mProgressBar.setVisibility(View.GONE);
                    }
                });
            }
        }).start();
    }

    private File getMoveFile(String fileName) {
        File rootFile = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
        // Create the storage directory if it does not exist
        if (!rootFile.exists()) {
            if (!rootFile.mkdirs()) {
                Log.d(TAG, "failed to create directory");
                return null;
            }
        }

        String path = rootFile.getAbsolutePath() + File.separator + fileName;
        return new File(path);
    }


    private void copyFilesFromRaw(Context context, int id, String fileName, String storagePath) {
        Log.e(TAG, "copyFilesFromRaw: " + fileName + "start");
        InputStream inputStream = context.getResources().openRawResource(id);
        File file = new File(storagePath);
        if (!file.exists()) {//如果文件夹不存在,则创建新的文件夹
            file.mkdirs();
        }
        readInputStream(storagePath + File.separator + fileName, inputStream);
    }

    /**
     * 读取输入流中的数据写入输出流
     *
     * @param storagePath 目标文件路径
     * @param inputStream 输入流
     */
    private void readInputStream(String storagePath, InputStream inputStream) {
        File file = new File(storagePath);
        try {
            if (!file.exists()) {
                // 1.建立通道对象
                FileOutputStream fos = new FileOutputStream(file);
                // 2.定义存储空间
                byte[] buffer = new byte[inputStream.available()];
                // 3.开始读文件
                int lenght = 0;
                while ((lenght = inputStream.read(buffer)) != -1) {// 循环从输入流读取buffer字节
                    // 将Buffer中的数据写到outputStream对象中
                    fos.write(buffer, 0, lenght);
                }
                fos.flush();// 刷新缓冲区
                // 4.关闭流
                fos.close();
                inputStream.close();
                Log.e(TAG, "readInputStream: " + storagePath);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里我吧raw下面的两个文件拷贝到了手机存储目录上面,便于底层C代码找路径。

实现底层C代码

这里我们实现的是FFmpegKit.java里面的run方法,建立ffmpeginvoke.c文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#include "ffmpeg.h"
#include <android/log.h>
/* Header for class com_lyman_ffmpeg_cmake_single_utils_FFmpegKit */

#ifndef _Included_com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
#define _Included_com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_lyman_ffmpeg_cmake_single_utils_FFmpegKit
 * Method:    run
 * Signature: ([Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_lyman_ffmpeg_1cmake_1single_utils_FFmpegKit_run
        (JNIEnv *env, jclass obj, jobjectArray commands) {
    int argc = (*env)->GetArrayLength(env, commands);
    char *argv[argc];

    __android_log_print(ANDROID_LOG_ERROR, "Kit", "argc %d\n", argc);
    int i;
    for (i = 0; i < argc; i++) {
        jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
        argv[i] = (char *) (*env)->GetStringUTFChars(env, js, 0);
        __android_log_print(ANDROID_LOG_ERROR, "Kit", "argv %s\n", argv[i]);
    }
    return run(argc, argv);
}

#ifdef __cplusplus
}
#endif
#endif

配置CmakeLists.txt文件

这里的配置真的非常关键

add_library( # Sets the name of the library.
             ffmpeginvoke
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/include/cmdutils.c
             src/main/cpp/include/ffmpeg.c
             src/main/cpp/include/ffmpeg_filter.c
             src/main/cpp/include/ffmpeg_opt.c
             src/main/cpp/ffmpeginvoke.c)
target_link_libraries(
                       ffmpeginvoke
                       libffmpeg
                       ${log-lib})

OK,我以为万事具备了,点击build等待libffmpeginvoke.so生成出来。一点击如下报错:




看到这个错误头文件找不到,我是这样处理的:
对于libavresample/avresample.h这个头文件是和libavresample.so相关的吧,我开始并没有生成这个so,也没去查它是干嘛的,我直接把它注释掉了,并且下面的文件也没有出错;
对于其他文件头文件找不到,libavutil包下面的拷贝进来,到哪里拷贝: ffmpeg-3.3.6也就是下载的源码路径下面去找到还是列出我拷贝了那些文件进来吧:

  • libavcodec/mathops.h
  • libavformat/os_support.h
  • libavutil/internal.h
  • libavutil/libm.h
  • libavutil/reverse.h
  • libavutil/timer.h
    OK就这么多。然后build成功生成了so文件。但是之前我配置
target_link_libraries(
                       ffmpeginvoke
                       libffmpeg
                       ${log-lib})

的时候把libffmpeg没有链接进去,导致很多方法没有定义,几百个错误,找了整整一天发现问题。。。
开始测试功能是否可用:我上面的FFmpeg指令是合并音频和视频文件为一个mp4文件,我取的之前使用MediaMuxer合并的时候用的文件,视频15兆左右,音频5兆左右,执行以后速度非常的缓慢,运行了大概一两分钟完成了最后的合成文件,打开文件,可以播放。欣喜不已。看看手机文件目录:



参考链接:
编译Android下可执行命令的FFmpeg

项目完整代码:查看

猜你喜欢

转载自blog.csdn.net/lyman_ye/article/details/79493748