项目实训—基于AI的智能视频剪辑器(六)GIF片段合成视频

文章目录

  • 前言
  • 初次尝试
  • 实现GIF片段的合并
  • GIF 格式转换为视频格式


前言

在用户上传视频和目标人物图片后,前端此时陷入等待,不断请求,直到后端调用算法完毕,将所有包含目标人物的主要 GIF 片段返回。在返回后,前端需要下载这些动图片段,并将其保存在文件夹下,而后对这些片段进行拼接,得到初步剪辑的视频,进入剪辑页面,再由用户根据需要进行细剪。

在本周,我主要实现了读取特定文件夹下的 gif 片段,并将其拼接,合成视频的功能。


初次尝试

因为在签名的视频文件传输功能上,分片视频的合并就是通过分片文件流拼接实现的。所以这里初步的想法是将视频片段看做普通文件,将文件流简单合并在一起,再保存为相应的格式

  • 首先遍历文件夹,获取所有gif片段的路径
  • 而后根据路径读取文件,获取文件流,将其拼接
    public void getUrlList(String strPath) {
        File dir = new File(strPath);//文件夹dir
        File[] files = dir.listFiles();//文件夹下的所有文件或文件夹
        if (files != null){
            for (int i = 0; i < files.length; i++) {
                PageLog.dTag(TAG,files[i].getAbsolutePath());
                UrlList.add(files[i].getAbsolutePath());//对于文件才把它的路径加到filelist中
            }
        }
    }

    public void uniteFiles() {
        try {
            VideoUrl = base_path + "/Video/" + fileName + ".gif";
            File unitedFile = new File(VideoUrl);
            unitedFile.createNewFile();
            PageLog.dTag(TAG,VideoUrl);
            FileOutputStream fos = new FileOutputStream(unitedFile);
            RandomAccessFile ra = null;
            for (int i = 0; i < UrlList.size(); i++) {
                ra = new RandomAccessFile(UrlList.get(i), "r");
                if (i != 0) {
                    ra.seek(6+7+7); //gif的文件头
                }
                byte[] buffer = new byte[1024 * 8];
                int len = 0;
                while ((len = ra.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            }
            ra.close();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

但是这样直接拼接,虽然最终保存的动图可以播放,但是其只能播放第一个动图的内容。因此不顾 gif 文件的特殊格式,直接以文件形式合并并不可行

实现GIF片段的合并

第二个想法是,首先合并gif片段,再进行格式转换,转换为视频的mp4等格式
这部分网上资源较少,我的思路是首先获取到每个小 gif 的每一帧,将其保存在数组中,最后根据所有帧,合并为一个大的 gif

获取所有帧

    public void getBitmapArrayByGif(String strPath) {
        // 所有 gif 片段的路径
        ArrayList<String> urlList = new ArrayList<>();
        File dir = new File(strPath);//文件夹dir
        File[] files = dir.listFiles();//文件夹下的所有文件或文件夹
        if (files != null){
            for (int i = 0; i < files.length; i++) {
                PageLog.dTag(TAG,files[i].getAbsolutePath());
                urlList.add(files[i].getAbsolutePath());//对于文件才把它的路径加到filelist中
            }
        }
        try {
            // 所有 gif 中的每一帧
            ArrayList<Bitmap> allFrames = new ArrayList<>();
            for(String path:urlList){
                GifDrawable gifFromAssets = new GifDrawable(path);
                int totalCount = gifFromAssets.getNumberOfFrames();
                for (int i=0; i<totalCount; i++)
                {
                    allFrames.add(gifFromAssets.seekToFrameAndGet(i));
                }
            }
            PageLog.dTag(TAG, "all frames add finish");
            createGif(allFrames);
        } catch (Exception e) {
            PageLog.dTag(TAG, e.toString());
        }
    }

将所有帧合并为 GIF

    private void createGif(ArrayList<Bitmap> allFrames) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        AnimatedGifEncoder encoder = new AnimatedGifEncoder();
        encoder.start(baos);
        for(int i = 0; i<allFrames.size(); i++){
            encoder.addFrame(allFrames.get(i));
        }
        encoder.finish();
        try {
            VideoUrl = base_path + "/Video/" + fileName + ".gif";
            File unitedFile = new File(VideoUrl);
            unitedFile.createNewFile();
            PageLog.dTag(TAG,VideoUrl);
            FileOutputStream fos = new FileOutputStream(unitedFile);
            baos.writeTo(fos);
            baos.flush();
            fos.flush();
            baos.close();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这样顺利的合成了可以正常播放的总GIF片段,但是其耗时较长。在一开始,我将这一方法放到了主线程,结果报了 “ANR” 的错误。而后将其作为一个异步任务 AsyncTask,在后台执行,前端显示进度条。

GIF 格式转换为视频格式

在视频格式转换上,我采用了 ffmpeg,其本身是一个命令行工具,可以提供许多媒体处理相关的功能。ffmpeg 使用 c++ 实现,可以集成到安卓项目中,来通过命令执行媒体处理工作,这一配置过程非常繁琐,这里简单描述一下:

1、首先需要将 ffmpeg 编译成 android 可以使用的 so 文件
FFmpeg是一个autoconf项目,无法使用cmake的编译方法,因为autoconf只支持Unix-like的系统,如果想要在Windows系统下编译autoconf项目,可以装一个Linux虚拟机,或者使用MSYS2

2、项目目录下新建 cpp 文件夹,将编译好的.so文件、头文件以及部分 ffmpeg 源码拷贝到项目目录,拷贝后的目录结构如图:
项目实训—基于AI的智能视频剪辑器(六)GIF片段合成视频_第1张图片
3、配置项目的 ndk,使用 CMakeLists.txt 进行第三方库链接
由于项目初始未选择 Native C++ 架构,因此这部分需要手动配置
CMakeLists.txt 的代码如下:

cmake_minimum_required(VERSION 3.4.1)
#add libavcodec
add_library(libffmpeg
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        libffmpeg
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libffmpeg.so
)

include_directories(ffmpeg/)

add_library(
        ffmpeg-cmd
        SHARED
        ffmpeg/ffmpeg-cmd.cpp ffmpeg/ffmpeg.c ffmpeg/cmdutils.c ffmpeg/ffmpeg_filter.c ffmpeg/ffmpeg_hw.c ffmpeg/ffmpeg_opt.c
)

find_library( # Sets the name of the path variable.
        log-lib

        log)

target_link_libraries( # Specifies the target library.
        ffmpeg-cmd

        libffmpeg
        ${log-lib})

4、编写JNI接口和C++代码
新建一个类 FFmpegCmd 专门负责执行 ffmpeg 命令

public class FFmpegCmd {
    static {
        System.loadLibrary("ffmpeg");
        System.loadLibrary("ffmpeg-cmd");
    }

    //执行FFmpeg命令
    private static native int run(String[] cmd);

    public int ffmpeg_cmd(String cmd) {
        return cmdRun(cmd);
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                cmdRun(cmd);
//            }
//        }).start();
    }

    private int cmdRun(String cmd) {
        PageLog.dTag("FFmpeg", cmd);
        String regulation = "[ \\t]+";
        final String[] split = cmd.split(regulation);
        return run(split);
    }

}

新建 ffmpeg.cpp 文件,调用 ffmpeg 以执行命令

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "ffmpeg-cmd", __VA_ARGS__)

extern "C"{
#include "ffmpeg.h"
#include "libavcodec/jni.h"
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_xuexiang_easycut_utils_FFmpegCmd_run(JNIEnv *env, jclass obj, jobjectArray commands) {
    JavaVM *jvm = NULL;
    env->GetJavaVM(&jvm);
    av_jni_set_java_vm(jvm, NULL);
    int argc = env->GetArrayLength(commands);
    char *argv[argc];
    jstring buf[argc];

    for (int i = 0; i < argc; i++) {
        buf[i] = static_cast<jstring>(env->GetObjectArrayElement(commands, i));
        char *string = const_cast<char *>(env->GetStringUTFChars(buf[i], JNI_FALSE));;
        argv[i] = string;
    }

    int retCode = ffmpeg_exec(argc, argv);
    LOGD("ffmpeg-invoke: retCode=%d",retCode);

    return retCode;
}

这样可以直接在代码中调用 ffmpeg 命令,以实现音视频操作

你可能感兴趣的:(项目实训,android,java)