写点什么

从零开始仿写一个抖音 App——视频编辑 SDK 开发 (一),安卓 kotlin 逆向

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:5388 字

    阅读完需:约 18 分钟

本章我将介绍 WsVideoEditor 项目的基本结构、组织方式以及运行方式。需要大家把项目 clone 下来跟着我一步步来做。

1.基本结构


我们看着图 1,一个个来讲:


  • 1.android:顾名思义,这个目录下是一个 Android 项目,去掉 .gradle、build、.idea 等等 ignore 的文件,我们主要关注下面这几个文件夹。



  • 1.ffmpeg-cpp:如图 2,这个文件夹中有 FFMPEG 的头文件与 .so 文件,我们需要将这个库集成到我们的 SDK 中,我们的 编辑 SDK 需要有解码视频的能力,解码分为硬解和软解,ffmpeg 就是用于软解的最强开源库。至于如何得到这些东西,我之前写过一篇 FFMPE食用指南 有兴趣的读者可以看看。

  • 2.protobuf-cpp:这个文件夹与 ffmpeg-cpp 类似,里面有 Protobuf For Cpp 的头文件与 .a 文件,因为我们 Native 与 Android/iOS/Linux 的通信方式使用的是 Protobuf,所以我们也需要将 Cpp 层的 Protobuf 集成到我们的 SDK 中。



  • 3.wsvideoeditor-sdk:如图 3,这个文件夹是一个 Android Library 项目,我们的 编辑 SDK 在 Android 端会以一个独立的 jar 包形式存在。这个目录下的东西比较多,例如 src 目录下是 Java 层的一些封装代码。jni 目录下是一些使用了 Android Native Api 的 Cpp 代码。更详细的解析,会在后面几章。

  • 4.wsvideoeditor-test:这个文件夹则是一个 Android Application 项目,主要是用于编写一些测试 编辑 SDK 的代码。



  • 2.buildtools:如图 4,这个目录下主要存放一些工具脚本,例如目前 build_proto.sh 用于生成 Java 与 Cpp 层的 Protobuf 代码。

  • 3.ios、linux:因为我给 编辑 SDK 的定义是一个跨平台的视频编辑 SDK,所以未来的想法是 iOS 和 Linux 端也能接入我们的 编辑 SDK,目前这两个目录里还啥也没有:-D。



  • 4.sharedcpp:如图 5,这个目录里面主要存放与平台无关的 Cpp 代码,因为我们要做的是一个跨平台的视频编辑 SDK,所以尽量多的将与平台无关的代码进行共用,是一个明智的选择。可以看见里面的 prebuilt_protobuf 目录下就有我们使用 build_proto.sh 生成的 Cpp 文件,这些文件就是可以共用的。

  • 5.sharedproto:这里存放着我们定义的 Protobuf 文件。

  • 6.thirdparty:这里存放着一些包含源码的与平台无关的三方库,例如 libyuv。

  • 7.CMakeLists.txt:这个文件主要是为了让 Clion 能够识别我们这个整个项目。

2.如何运行项目

  • 1.git clone https://github.com/TheGodsThemselves/WsVideoEditor.git

  • 2.NDK 环境需要准备好

  • 3.用 Android Studio 打开 WsVideoEditor/android 目录

  • 4.在手机中准备 /sdcard/test.mp4 视频文件

  • 5.运行 wsvideoeditor-test 项目

二、SDK 功能介绍

这一章我们来介绍一下 编辑 SDK 目前有的以及未来会有的功能。编辑 SDK 的最终形态会和抖音的视频编辑功能接近,有其他想法的读者也可以在评论区留言或者提 issue。

1.目前有的功能

  • 1.开始播放

  • 2.暂停播放

  • 3.视频音量调整

  • 4.单段视频播放

  • 5.多段视频播放

  • 6.视频 Seek

  • 7.视频边缘模糊填充

2.规划中的功能

  • 1.视频类:

  • 1.按时间轴添加额外的声音

  • 2.按时间轴添加滤镜

  • 3.按时间轴添加静态贴纸、动态贴纸

  • 4.多段视频间转场

  • 2.图片类:

  • 1.添加声音

  • 2.多张图片间的转场

  • 3.照片电影

  • 3.工具类:

  • 1.视频缩略图截取

  • 2.视频元数据读取

  • 4.编码类:

  • 1.导出不同格式的视频

  • 2.更改视频的分辨率、


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


帧率


  • 3.视频转 gif

  • 5.技术类:

  • 1.多进程编解码视频

  • 2.多进程播放视频

  • 3.多进程视频缩略图截取

三、SDK 架构以及运行机制介绍

这一章我来介绍一下目前 编辑 SDK 的整体架构以及运行机制。

1.编辑 SDK 架构


图 6 是 编辑 SDK 的架构图,这一节我会照着这张图来介绍。

(1).基础 API

先从底部看起,底部是整个 SDK 依赖的底层 API 库。


  • 1.FFMPEG:前面简单介绍过,是一个开源的视频库,在我们的项目中主要用于软编解码

  • 2.MediaCodec:是 Android 中的硬编解码 API,相应的 iOS 也有自己的硬编解码方式。

  • 3.OpenGL:是一个开源的图形库,Android 和 iOS 中都有内置 OpenGL ES 作为默认图形库。在我们的项目中主要用于将视频解码后的视频帧绘制到屏幕上去。当然也可以对这些图像做一些效果的变化,例如滤镜、视频/图片转场等等。

  • 4.Libyuv:是 Google 开源的实现各种 YUV 与 RGB 之间相互转换、旋转、缩放的库。

  • 5.Protobuf:是 Google 开源的一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议。在我们的项目中主要用于 Cpp 与 Java、OC、Dart 之间的数据通信。

(2).SDK 主体

接着我们再看图片中的主体部分,因为目前只有 Android 端的实现,所以主体部分的上层实现我使用 Android 来代替。


  • 1.Android 层架构

  • 1.WSMediaPlayerView:继承于 TextureView,所以其可以提供一个具有 Open GL 环境的线程。对 Surface 家族不了解的同学可以看看这两篇文章:Android绘制机制以及Surface家族源码全解析相机/OpenGL/视频/Flutter和SurfaceView

  • 2.WSMediaPlayer:这个是一个代理了 Native 的 NativeWSMediaPlayer 的 Java 类。该类具有一个播放器应该有的各种 API,例如 play、pause、seek 等等。其实很多 Android 中的系统类都是以这种形式存在的,例如 Bitmap、Surface、Canvas 等等。说到底 Java 只是 Android 系统方便开发者开发 App 的上层语言,系统中大部分的功能最终都会走到 Native 中去,所以读者需要习惯这种代码逻辑

  • 3.AudioPlayer:这个类是基于 Android 中的 AudioTrack 封装的能够播放音频帧的 Java 类。我们在 Native 层也有一个 AudioPlayer。这里与 WSMediaPlayer 相反 Native 层的 AudioPlayer 是一个空壳,Java 层的 AudioPlayer 反向代理了 Native 层的 AudioPlayer,因为在这里 Java 层的 AudioPlayer 才是真正播放音频的东西。

  • 2.Native 层架构:这里我们自底向上来剖析,Native 层的架构

  • 1.AudioDecodeService:它负责使用 FFMPEG/MediaCodec,来从视频/音频中解码出某个时间点的音频帧,并且存储在一个音频帧队列中。最终被外部取出音频帧交给音频播放器播放。

  • 2.VideoDecodeService:它和 AudioDecodeService 类似,是使用 FFMPEG/MediaCodec 来从视频中解码出某个时间点的视频帧并且存储在一个视频帧队列中。最终被外部取出视频帧交给 OpenGL 绘制到屏幕上。

  • 3.VideoFramePool:它负责响应外部的 seek 事件,然后使用 FFMPEG/MediaCodec 来从视频中解码出当前时间点的视频帧,然后存储到一个 LruCache 中同时返回 seek 时间点的视频帧。

  • 4.AudioPlayer:前面说过,这个是 Java 层的 AudioPlayer 代理类,主要用于播放 AudioDecodeService 解码出来音频帧。

  • 5.FrameRenderer:这个东西是一个渲染器,在视频播放时用于渲染 VideoDecodeService 不断解码出的视频帧,在视频 seek 的时用于向 VideoDecoderPool 发送 seek 请求,然后渲染返回的视频帧。

  • 6.NativeWSMediaPlayer:用于同步 AudioPlayer 和 FrameRenderer 的音视频播放。即我们一般认为的视频播放器实体,被 Java 层的 WSMediaPlayer 代理着。

2.编辑 SDK 运行机制


上一节讲解了 编辑 SDK 的架构,这一节在来基于图 7 讲讲 编辑 SDK 的运行机制。


  • 1.经过上一节的介绍,我们都知道了 WSMediaPlayerView 是整个 编辑 SDK 的顶级类。所以我们由 WSMediaPlayerView 入手,先看图片最上面。

  • 1.可以看见 WSMediaPlayerView 中会维护一个 30ms 的定时循环,这个循环中会不断的调用 draw frame 来驱动 WSMediaPlayer/NativeWSMediaPlayer 进行视频/音频的播放。

  • 2.与此同时,最左边的用户会通过 play、pause、seek 等 API 来更新 NativeWSMediaPlayer 的状态。

  • 3.需要注意的是,WSMediaPlayerView 的定时循环不会被用户的 play、pause、seek 等操作所中断的。

  • 2.再来看看图片左边,这是 WSMediaPlayer 的内部播放机制。要点为 三个循环,两个播放,我们还是自底向上解析。

  • 1.VideoDecodeService:它内部维护了一个可阻塞循环与一个先进先出队列——BlockingQueue,当我们开始播放视频或者 seek 视频到某个时间点的时候,VideoDecodeService 会记录这个开始的时间点,然后不断的解码当前时间点之后的每一帧,每解码出一帧便把这一帧放入 BlockingQueue 中。当队列中的元素达到最大值时,当前的循环就会被阻塞,直到外部将 BlockingQueue 中的 Top 帧消费了,那么循环又会被启动继续解码。需要注意的是:VideoDecodeService 只在视频播放的时候提供视频帧,因为在这个情况下 BlockingQueue 中的视频帧的顺序就是视频真正播放的顺序。

  • 2.VideoFramePool:它内部维护了一个可阻塞请求循环与一个 LruCachePool。一般情况下 VideoFramePool 的循环是处于阻塞状态的。当外部 seek 视频的时候,循环会接收到一个请求并开始处理这个请求,如果 LruCachePool 中有 Cache 被命中了,那么就直接返回 Cache,否则将会立即从视频中解码出这个请求中时间点的视频帧存到 LruCachePool 中然后再返回。需要注意的是:VideoFramePool 只在视频 seek 的时候提供视频帧,因为我们的 seek 操作是随机的,所以在这个情况下 VideoDecodeService 无法使用。

  • 3.AudioDecodeService:它与 VideoDecodeService 类似,也维护了一个可阻塞循环先进先出队列,内部的其他行为也类似,只是将视频帧换成了音频帧。

  • 4.FrameRenderer

  • 1.当视频 seek 的时候,其会从 VideoFramePool 中取出 seek 时刻的视频帧绘制它。

  • 2.当视频处于 playing 状态时,它的 drawFrame 方法就会不断被 WSMediaPlayerView 通过定时循环调用并从 VideoDecodeService 中取出当前帧通过 Open GL 绘制它。

  • 5.AudioPlayer:当视频处于 playing 状态时,它也会不断被 WSMediaPlayerView 通过定时循环驱动着从 AudioDecodeService 中取出当前的音频帧,然后通过反向代理将音频帧交给 Java 层的 AudioPlayer 进行播放。

四、VideoDecodeService 解析

上一章大概的讲了讲整个 编辑 SDK 的整体架构和运行机制,但其实整个 编辑 SDK 内部的每一个部分的细节都非常多,所以这一章我会先讲解 VideoDecodeService 的内部细节。其他各个部分则放在后面几篇文章中讲解。与此同时,WsVideoEditor 中的代码也会随着讲解的进行而不断更新。最终形成一个可用的 编辑 SDK

1.API 讲解

-----代码块 1----- VideoDecodeService.javaprivate native long newNative(int bufferCapacity);


private native void releaseNative(long nativeAddress);


private native void setProjectNative(long nativeAddress, double startTime, byte[] projectData);


private native void startNative(long nativeAddress);


private native String getRenderFrameNative(long nativeAddress, double renderTime);


private native void updateProjectNative(long nativeAddress, byte[] projectData);


private native void seekNative(long nativeAddress, double seekTime);


private native void stopNative(long nativeAddress);


private native boolean endedNative(long nativeAddress);


private native boolean stoppedNative(long nativeAddress);


private native int getBufferedFrameCountNative(long nativeAddress);


如代码块 1 所示,我们先来讲讲 VideoDecodeService 的 API


  • 1.newNative:由前面几章的讲解我们知道,VideoDecoderService 内部有一个先进先出的阻塞队列,这个方法的入参 bufferCapacity 就是用于设置这个阻塞队列的长度。这个方法调用之后 Native 层会创建一个与 Java 层同名的 VideoDecodeService.cpp 对象。然后返回一个 long 表示这个 Cpp 对象的地址。我们会将其记录在 Java 层,后续要调用其他方法时需要通过这个地址找到相应的对象。

  • 2.releaseNative:因为 Cpp 没有垃圾回收机制,所以 Cpp 对象都是需要手动释放的,所以这个方法就是用于释放 VideoDecodeService.cpp 对象。

  • 3.setProjectNative:因为 Protobuf 是高效的跨平台通信协议,所以 Java 与 Cpp 层的通信方式使用的就是 Protobuf,我们可以看 ws_video_editor_sdk.proto 这个文件,里面定义的 EditorProject 就是两端一起使用的数据结构。这个方法的入参 nativeAddress 就是我们在 1 中获取到的对象地址。入参 startTime 表示起始的解码点,单位是秒。入参 projectData 就是 EditorProject 序列化之后的字节流。

  • 4.startNative:这个方法表示开始解码。

  • 5.getRenderFrameNative:这个方法表示获取 renderTime 这一时刻的帧数据,目前返回到 Java 层的是一个 String,在 Cpp 层后续我们主要就是使用这个方法获取到的帧数据使用 OpenGL 绘制到屏幕上。

  • 6.updateProjectNative:这个方法和 setProjectNative 类似,用于更新 EditorProject。

  • 7.seekNative:我们在看视频的时候,将进度条拖动到某一时刻的操作被称为 seek,在 VideoDecodeService 中的体现就是这个方法,这个方法会将当前的解码时间点设置为 seekTime

  • 8.stopNative:这个方法表示暂停解码。

最后

在这里我和身边一些朋友特意整理了一份快速进阶为 Android 高级工程师的系统且全面的学习资料。涵盖了 Android 初级——Android 高级架构师进阶必备的一些学习技能。


附上:我们之前因为秋招收集的二十套一二线互联网公司 Android 面试真题(含 BAT、小米、华为、美团、滴滴)和我自己整理 Android 复习笔记(包含 Android 基础知识点、Android 扩展知识点、Android 源码解析、设计模式汇总、Gradle 知识点、常见算法题汇总。)



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
从零开始仿写一个抖音App——视频编辑SDK开发(一),安卓kotlin逆向