阅读 2473

从零开始仿写一个抖音App——跨平台视频编辑SDK项目搭建

本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。微信号:a1018998632,交流qq群:859640274

GitHub地址

不知不觉已经到了2019年,本系列的文章也更新到了8篇。很庆幸笔者能坚持下来,从我司的代码中学习到了很多东西。当然更庆幸的是收获了众多读者的鼓励和支持。从本篇文章开始,我们将接触短视频 app 中比较核心的功能——视频编辑,笔者在我司的日常工作中,也经常对这个模块进行开发,可以说对这部分功能比较熟悉了。所以最近的几篇文章,我会从零开始完善一个视频编辑 sdk 的各种功能,最后集成到我们之前的 MyTiktok 项目中。注:本文以 android 平台为例子,ios 因为不会,所以暂时不涉及。

本文分为以下章节,读者可按需阅读:

  • 1.项目建立——新建一个跨平台视频编辑项目
  • 2.基础 lib 集成——将 ffmpeg、protobuf 这些必须使用的三方库集成到项目中
  • 3.基础数据结构——定义和讲解一些视频编辑流程中需要使用到的数据结构

一、项目建立

1.方法论

我想看本文的人有很大一部分都是 android 工程师,所以在讲干货之前,我需要讲一讲方法论

  • 1.当我们在使用 IDE 开发 App 的时候,我们在干什么?
    • 1.Android 工程师平时使用 Android Studio 来开发 App,ios 工程师使用 XCode。那么我们平时在使用 IDE 的时候,我们到底在干什么呢?
    • 2.这里我以 Android 来举例子:
      • 1.首先我们会使用 AS 来新建一个项目,项目会有很多可选的参数。
      • 2.在项目的 gradle 文件中添加依赖库,然后写代码
      • 3.打包成 APK,运行 App
    • 3.上面就是我们平时开发的流程了,那么我们能不使用 IDE 来开发一个 App 吗?理论上来说是可以的,有下面这些步骤。
      • 1.创建一个文件夹,模仿 AS 生成的项目,向文件夹里面加文件
      • 2.在 gradle 文件中添加依赖库,然后写代码。使用命令行来 sync gradle。
      • 3.命令行运行 gradle 来打包 APK,运行 App
    • 4.其实我们不需要 AS 就能进行 Android 开发(当然没有人那么傻)。我们需要的只是一个项目管理的工具——gradle。放在不同的开发者那里,只是项目管理的工具不同:写 java 的用 maven、写 python 的用 conda/pip、写前端的用 npm、写 c/c++ 的用 CMake。
    • 5.所以当一个项目中既要写 c++ 又要写 android 还要写 ios 的时候,我们只需要三个项目管理工具就行了,IDE 对我们来说只是一个文件编辑器+文件搜索器+文件浏览器。
    • 6.以上就是我作为一个 android 工程师,在使用了各种不同语言构建不同项目之后思维上的转变。当你能看清和思考一个东西的本质的时候你能走的更远。

2.项目搭建

那么废话不多说,就开始搭建我们的项目吧注意:目前 MyTiktokVideoEditor 已经上传到了 github 上面了,建议结合项目食用,

  • 1.首先我们新建一个文件夹,然后进入文件夹中。在其中创建下面这些东西,如图1。里面的东西我一个个来讲解
    • 1.首先 LICENSSE 和 README.md 就不用说了。
    • 2.android:下面是一个完整的 android 工程,android 工程的内部也会引用到外部的文件,这个后面再说。
    • 3.ios:下会是一个完整的 ios 工程,当然我目前还不会 ios,所以先略过
    • 4.buildtools:里面会存放一些项目运行时的脚本,比如我们在 上一篇文章 中用到的编译 FFmpeg 的脚本等等
    • 5.docs:内部存放一些项目文档
    • 6.sharedcode:里面存放 android 和 ios 共享的代码,如 c/c++ 代码等等,还有就是 protobuf 生成的代码。
    • 7.sharedproto:里面存放 android、ios、c++ 三端共享的 protobuf 代码,可以使用 buildtools 里面的脚本一键生成三端的代码
    • 8.third_part:可以以 git submodule 的形式,引用其他的三方库的源代码与 android 和 ios 项目一起编译,目前是空的。

图1:根目录

  • 2.介绍好了项目构成,我们开始配置 android 项目吧。
    • 1.首先,我们需要使用 AS 来创建一个支持 C++ 的工程,注意目录需要选在上面提到的 android 目录下面。
    • 2.创建好了之后,我们需要创建一个 android library 作为视频编辑 sdk 的载体。这个 module 将会整合所有的,共享 cpp 代码、.so 文件、.a 文件,然后通过 java 代码被外部调用。在项目中我将这个 module 命名为了 mttvideoeditorsdk
    • 3.至于 app module 可以引用 mttvideoeditorsdk module 便于平时调试 sdk。
    • 4.我们再来看 mttvideoeditorsdk 的结构如图2,其实比较简单
      • 1.多了 jni.editorsdk 目录,这个目录用来存放 jni 文件,相当于是 c/c++ 和 java 的中间层。
      • 2.然后是 CMakeLists.txt 文件,其用于管理 android 项目需要引入的 c/c++ 代码。
    • 5.我们再来看看 gradle 文件是怎么配置的如图3。
      • 1.首先 externalNativeBuild.cmake 里面配置了一些参数,这里只要知道我们使用的是 c++11 就好了
      • 2.externalNativeBuild.ndk 里面我们只生成一种 so 文件就是 armeabi,本来是应该使用 arm64-v8a,这样才是最佳适配,现在就先凑合着用吧
      • 3.再看外面的 externalNativeBuild.cmake,这里设置了 CMake 的路径,注意这里是以当前 gradle 文件为初始路径的。

图2:mttvideoeditorsdk目录

图3:mttvideoeditorsdk的gradle文件

二、基础 lib 集成

上面讲了如何搭建项目,这一章就来讲讲如何集成一些基础库吧。

首先我们都知道,在 android 中我们可以使用 gradle 向远程中央仓库拉取我们需要的库。像 java 的 maven、js 的 npm、ios 的 pods都有这个能力。但是在 c/c++ 上的项目管理工具 CMake 就没有这个能力,它只能在本地搜索和集成你已经安装好的库或者源码,而且 c/c++ 又不具有跨平台能力。所以最终就导致了我们如果想使用 ffmpeg、protobuf 这样大型的开源项目都需要自己去 clone 源码然后自己编译出不同平台的库。

1.FFmpeg 集成

  • 1.说到 FFmpeg 的集成,其实我在这里,已经提到过一些了。我这里就简单讲讲。
  • 2.首先我们需要编译 FFmpeg 的代码获取 so 库和 头文件,我的这个项目与上次不同,现在已经能编译出一个单独的 libffmpeg.so 的文件了,大家可以之前拿过来用。
  • 3.然后我们在 android 项目下面新建一个目录用来储存这些东西,如图4。
  • 4.最后我们看代码块1,这里都有注释比较简单,就是将 libffmpeg.so 和他的头文件链接到整个项目中

图4:android_ffmpeg目录

----代码块1,本文发自简书、掘金:何时夕-----
cmake_minimum_required(VERSION 3.4.1)

# 当前文件存在的目录
set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
# MyTiktokVideoEditor 的根目录
set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../..)
# ffmpeg 的目录
set(FFMPEG_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../android_ffmpeg)
# protobuf 头文件与静态库的目录
set(PROTOBUF_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../android_protobuf)
# android 专用 c++ 代码的目录
set(EDITORSDK_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/editorsdk)
# c++ 共享代码的目录
set(SHARED_CODE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../sharedcode)
# c++ 的版本
set(CMAKE_CXX_STANDARD 11)

# 找到 android ndk 的 log 库
find_library(log-lib log)

# 将 libffmpeg.so 添加到 libffmpeg 这个 动态 library中
add_library(libffmpeg SHARED IMPORTED)
set_target_properties(libffmpeg PROPERTIES IMPORTED_LOCATION
        ${FFMPEG_LIB_DIR}/armeabi/libffmpeg.so)

# 将 libprotobuf.a 添加到 libprotobuf-lite 这个 静态 library 中
add_library(libprotobuf-lite STATIC IMPORTED)
set_target_properties(libprotobuf-lite PROPERTIES IMPORTED_LOCATION
        ${PROTOBUF_LIB_DIR}/armeabi/libprotobuf-lite.a)

aux_source_directory(${SOURCE_DIR} SOURCE_DIR_ROOT)
# 将所有自己写的 c++ 代码添加到 mttvideoeditorsdkjni 这个 动态 library 中
list(APPEND SOURCE_DIR_ROOT
        ${EDITORSDK_JNI_DIR}/native-lib.cc
        ${EDITORSDK_JNI_DIR}/ffmpeg_sample_six.cpp)
list(APPEND SOURCE_DIR_ROOT
        ${SHARED_CODE_DIR}/editorsdk/base/av_utils.cc
        ${SHARED_CODE_DIR}/editorsdk/generated_protobuf/editor_model.pb.cc)
add_library(mttvideoeditorsdkjni
            SHARED
            ${SOURCE_DIR_ROOT})
# 将所有头文件添加到一个列表中,在最后一起链接
list(APPEND SOURCE_DIR_INCLUDE
        ${SHARED_CODE_DIR}/editorsdk/base/av_utils.h
        ${SHARED_CODE_DIR}/editorsdk/base/blocking_queue.h
        ${SHARED_CODE_DIR}/editorsdk/generated_protobuf/editor_model.pb.h
        ${PROTOBUF_LIB_DIR}/include # 将 protobuf 的头文件放入一个列表中
        ${FFMPEG_LIB_DIR}/include) # 将 ffmpeg 的头文件放入一个列表中

target_include_directories(mttvideoeditorsdkjni PRIVATE ${SOURCE_DIR_INCLUDE}) # 连接列表中所有的头文件

list(APPEND LINK_LIBRARIES
        mttvideoeditorsdkjni
        -landroid
        libprotobuf-lite
        libffmpeg) # 将所有的库添加到一个列表中,最后一起链接
target_compile_options(mttvideoeditorsdkjni PUBLIC -D_LIBCPP_HAS_THREAD_SAFETY_ANNOTATIONS -Wthread-safety -Werror -Wall -Wno-documentation -Wno-shorten-64-to-32 -Wno-nullability-completeness)
target_link_libraries(${LINK_LIBRARIES} ${log-lib}) # 链接所有库

复制代码

2.protobuf 集成

  • 1.先上脚本看代码块2,里面主要是 clone protobuf 的源码,然后编译,然后根据我们前面建立项目的时候 sharedproto 文件夹里面的 proto 文件来生成 java c++ 的代码,最后移动到 android 项目和 sharedcode 文件夹下。每次更新了 proto 文件就可以运行一下这个脚本。
  • 2.当然还得将 protobuf c++ 的库集成到项目中,如图5我们新建一个 android_protobuf 的目录,然后将刚刚编译生成的 .a 文件与头文件拷贝到里面去,这里与 ffmpeg 的集成类似。不同的地方在于,protobuf 生成的是 .a 文件,这里需要将其作为静态链接库,添加到项目中。详细的在代码块1中已经说明了。
----代码块2,本文发自简书、掘金:何时夕-----
#!/bin/bash
show_msg() {
  echo -e "\033[36m$1\033[0m"
}

show_err() {
  echo -e "\033[31m$1\033[0m"
}
# protobuf 的版本
v3_0_0="v3.0.0"
# 当前的目录
script_path=$(cd `dirname $0`; pwd)
# protoc 是 protobuf 编译之后生成的可执行文件,可以用来根据 proto 文件生成 java、c++等等代码
protoc_path=$script_path/tools/protoc
# protobuf 的源码地址
protoc_src=$script_path/protobuf
# 生成的 java 文件需要移动到的位置
java_target_path="$script_path/../android/mttvideoeditorsdk/src/main"
# 生成的 c++ 文件需要移动的位置
cpp_target_path="$script_path/../sharedcode/editorsdk/generated_protobuf"

# 本方法用于执行 protobuf 源码的脚本进行编译
build_protobuf() {
  mkdir -p $protoc_src/host
  mkdir -p $protoc_path/$1
  cd $protoc_src/host
  ../configure --prefix=$protoc_path/$1 && make -j8 && make install

  if test $? != 0; then
    show_err "Build protobuf failed"
    exit 1
  fi

  cd $script_path
  rm -rf $protoc_src/host
}

# 本方法用于 clone protobuf 的源码,然后 checkout 到3.0.0的版本,然后调用 build_protobuf 进行编译
build() {
  git clone https://github.com/google/protobuf.git

  show_msg "Building android protobuff source code"
  cd protobuf
  git checkout $v3_0_0
  git cherry-pick bba446b  # fix issue https://github.com/google/protobuf/issues/2063
  ./autogen.sh
  build_protobuf $v3_0_0

  show_msg "Build protobuf complete"
  cd $script_path
  rm -rf protobuf
}

# 如果 protoc 不存在,那么就去 clone protobuf 的源码,然后编译
if [ ! -x "$protoc_path/$v3_0_0/bin/protoc" ]; then
  build
fi

# 删除之前已经生成的 java c++ 文件
rm $java_target_path/java/com/whensunset/mttvideoeditorsdk/model/protobuf/*.java
rm $cpp_target_path/*.pb.cc $cpp_target_path/*.pb.h

cd $script_path/../sharedproto

mkdir -p java cpp

# 用 protoc 生成 java c++ 文件
$protoc_path/$v3_0_0/bin/protoc *.proto --java_out=java --cpp_out=cpp

# 将生成的 java c++ 文件移动到对应的文件夹下
cp -r java $java_target_path
mkdir -p $cpp_target_path
cp cpp/* $cpp_target_path
rm -rf java cpp
复制代码

图5:android_protobuf目录

三、基础数据结构

最后一章我们来定义一下在一个视频编辑过程中,需要用到的数据结构。

  • 1.大家可以看见在 sharedproto 文件夹下面有个 editor_model.proto 文件,里面定义了一些我们在未来整个视频编辑功能开发过程中需要用到的数据结构,如代码块3
  • 2.前面的几行初始化代码就不讲了,我就按照定义的一个个数据结构来进行讲解
    • 1.TimeRange:这个顾名思义,用于保存一段时间,单位是秒。是最基础的数据结构,比如特效出现的时间段、视频被剪裁的段落、贴纸出现的时间段等等都需要用到它。
    • 2.MediaStreamHolder:我们都知道(如果不知道可以去看看我之前的文章),FFmpeg 解封装了一个视频文件之后会得到好几个不同的 stream,每一个 stream 可能是一个视频数据流可能是一个音频数据流,而这个数据结构就是为了储存视频数据流的信息。
    • 3.FileHolder:我们解析了一个多媒体文件的时候,也需要把这个文件的一些信息存下来,比如:文件的后缀名、文件中每个 stream 的信息、文件中最优的视频流和音频流的 index等等。这个时候就要用到这个数据结构了。
    • 4.Color、AssetType:两个工具数据结构
    • 5.VideoAsset:表示一个视频素材,里面除了有个 FileHolder 来储存视频被解析后的信息,还有视频被剪裁的时间段、视频的音量、视频的速度之类的信息。
    • 6.AudioAsset:与 VideoAsset 类似,表示一个音频素材。
    • 7.VideoWorkspace:表示一次视频编辑的数据结构,里面有复数个视频素材和音频素材,以及一些其他参数。
    • 8.VideoEncoderType:表示当前视频编辑的过程中,视频用到的编解码方式。目前只有使用 FFmpeg 编解码与使用 android 的 mediaCodec 编解码这两种方式。
    • 9.这里的数据结构随着 sdk 开发的进行会不断的增加和修改,目前这里定义的只是最的简单版本,大家有想法可以在评论区和我交流。
syntax = "proto3";
package sharedcode;
option optimize_for = LITE_RUNTIME;
option java_package = "com.whensunset.mttvideoeditorsdk.model.protobuf";

// 用于保存一段时间,单位是秒
message TimeRange {
    double start = 1;
    double duration = 2;
    uint64 id = 3;
}

// 一个多媒体文件的一个多媒体数据流的信息
message MediaStreamHolder {
    // 视频的长和宽
    int32 width = 1;
    int32 height = 2;
    // 编解码器的名称
    string codec_type = 3;
    // 视频的旋转角度
    int32 rotation = 4;
    // 视频像素的格式
    int32 pix_format = 5;
    // 视频的色彩空间,rgb、yuv 等等
    int32 color_space = 6;
    // 视频的色彩范围
    int32 color_range = 7;
    // 视频的 bit 流
    int64 bit_rate = 8;
}

// 储存一个多媒体文件的信息,减少反复解析的性能消耗
message FileHolder {
    string path = 1;
    // 文件的后缀名
    string format_name = 2;
    int32 probe_score = 3;
    // 文件中的多媒体数据流的数量
    int32 num_streams = 4;
    // 文件中的多媒体数据流的信息列表
    repeated MediaStreamHolder streams = 5;
    // 文件中多媒体信息流中最优的视频流
    int32 video_strema_index = 6;
    // 文件中多媒体信息流中最优的音频流
    int32 audio_strema_index = 7;
    string video_comment = 8;
}

message Color {
    float red = 1;
    float green = 2;
    float blue = 3;
    float alpha = 4;
}

// 素材的种类
enum AssetType {
    ASSET_TYPE_VIDEO = 0;
    ASSET_TYPE_AUDIO = 1;
}
// 表示一个视频素材
message VideoAsset {
    // 相同表示当前素材是同样的
    uint64 asset_id = 1;
    string asset_path = 2;
    FileHolder asset_video_file_hodler = 3;
    // 当前素材被剪裁的时间区域
    repeated TimeRange clipped_time_range = 4;
    // 视频的速度
    double speed = 5;
    // 视频声音大小
    double volume = 6;
    bool is_reversed = 7;
}

// 表示一个音频的素材
message AudioAsset {
    uint64 asset_id = 1;
    string asset_path = 2;
    FileHolder asset_audio_file_holder = 3;
    repeated TimeRange clipped_time_range = 4;
    double speed = 5;
    double volume = 6;
    bool is_repeat = 7;
}

// 表示一次视频编辑的流程
message VideoWorkspace {
    int64 work_space_id = 1;
    repeated VideoAsset video_asset = 2;
    repeated AudioAsset audio_asset = 3;
    repeated TimeRange clipped_ranges = 4;
    int32 workspace_output_width = 5;
    int32 workspace_output_height = 6;
    VideoEncoderType video_encoder_type = 7;
}

// 当前视频编辑流程使用的编解码方式
enum VideoEncoderType {
    VIDEO_ENCODER_TYPE_FFMPEG_MJPEG = 0;
    VIDEO_ENCODER_TYPE_MEDIACODEC = 1;
}
复制代码

四、结束

不知不觉又水了一篇文章^_^,最近的两篇文章都是代码多而文字少。不知道大家是不是喜欢这种方式呢?(感觉以前废话太多了,哈哈)大家有什么建议或者意见希望能在评论区提出来。如果文章问题可以指出是哪里,方便我进行修改(手动@上篇文章中说我文章有错别字的哥们)。最近点赞关注有点少啊,希望大家看完能随手点个赞和关注,谢谢啦!

连载文章

不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是我的微信公众号:世界上有意思的事,干货多多等你来看。

世界上有意思的事

关注下面的标签,发现更多相似文章
评论