本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,否则将追究版权责任。交流qq群:859640274
大家好久不见,又有一个多月没有发文章了。不知道还有哪些读者记得我的 从零开始仿写抖音App 的系列文章,这个系列的文章已经很久没有更新了,最后一篇文章是我开始开发 视频编辑SDK 时写的。当时踏入到了一个新的领域里,未知的东西太多了,导致接下来的大半年都没有更新相关的文章。 但是别以为我已经放弃了,今天对于我来说是一个值得纪念的日子,2019年10月28日 我终于将 视频编辑SDK 的最简版本给完成了,我将这个 视频编辑SDK 命名为 WSVideoEditor,接下来的一段时间里我计划更新 4 篇解析该 SDK 的相关文章,WsVideoEditor 中的代码我也会随着文章同步更新。当 SDK 解析完毕之后 从零开始仿写一个抖音App 系列文章将会踏出最关键的一步。
阅读须知:
本文分为以下章节,读者可按需阅读:
本章我将介绍 WsVideoEditor 项目的基本结构、组织方式以及运行方式。需要大家把项目 clone 下来跟着我一步步来做。
图1:总结构
我们看着图1,一个个来讲:
图2:ffmpeg-cpp.png
图3:wsvideoeditor-sdk.png
图4:buildtools.png
图5:sharedcpp.png
这一章我们来介绍一下 编辑SDK 目前有的以及未来会有的功能。编辑SDK 的最终形态会和抖音的视频编辑功能接近,有其他想法的读者也可以在评论区留言或者提 issue。
这一章我来介绍一下目前 编辑SDK 的整体架构以及运行机制。
图6:编辑SDK架构.png
图6是 编辑SDK 的架构图,这一节我会照着这张图来介绍。
先从底部看起,底部是整个 SDK 依赖的底层 API 库。
接着我们再看图片中的主体部分,因为目前只有 Android 端的实现,所以主体部分的上层实现我使用 Android 来代替。
图7:编辑SDK运行机制.png
上一节讲解了 编辑SDK 的架构,这一节在来基于图7讲讲 编辑SDK 的运行机制。
上一章大概的讲了讲整个 编辑SDK 的整体架构和运行机制,但其实整个 编辑SDK 内部的每一个部分的细节都非常多,所以这一章我会先讲解 VideoDecodeService 的内部细节。其他各个部分则放在后面几篇文章中讲解。与此同时,WsVideoEditor 中的代码也会随着讲解的进行而不断更新。最终形成一个可用的 编辑SDK。
-----代码块1----- VideoDecodeService.java
private 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
newNative
:由前面几章的讲解我们知道,VideoDecoderService 内部有一个先进先出的阻塞队列,这个方法的入参 bufferCapacity
就是用于设置这个阻塞队列的长度。这个方法调用之后 Native 层会创建一个与 Java 层同名的 VideoDecodeService.cpp 对象。然后返回一个 long
表示这个 Cpp 对象的地址。我们会将其记录在 Java 层,后续要调用其他方法时需要通过这个地址找到相应的对象。releaseNative
:因为 Cpp 没有垃圾回收机制,所以 Cpp 对象都是需要手动释放的,所以这个方法就是用于释放 VideoDecodeService.cpp 对象。setProjectNative
:因为 Protobuf 是高效的跨平台通信协议,所以 Java 与 Cpp 层的通信方式使用的就是 Protobuf,我们可以看 ws_video_editor_sdk.proto 这个文件,里面定义的 EditorProject 就是两端一起使用的数据结构。这个方法的入参 nativeAddress
就是我们在 1 中获取到的对象地址。入参 startTime
表示起始的解码点,单位是秒。入参 projectData
就是 EditorProject 序列化之后的字节流。startNative
:这个方法表示开始解码。getRenderFrameNative
:这个方法表示获取 renderTime
这一时刻的帧数据,目前返回到 Java 层的是一个 String
,在 Cpp 层后续我们主要就是使用这个方法获取到的帧数据使用 OpenGL 绘制到屏幕上。updateProjectNative
:这个方法和 setProjectNative
类似,用于更新 EditorProject。seekNative
:我们在看视频的时候,将进度条拖动到某一时刻的操作被称为 seek,在 VideoDecodeService 中的体现就是这个方法,这个方法会将当前的解码时间点设置为 seekTime
。stopNative
:这个方法表示暂停解码。endedNative
:返回一个 boolean
表示视频的解码点是否到达了视频的结尾。stoppedNative
:返回了一个 boolean
表示当前是否暂停了解码。getBufferedFrameCountNative
:返回一个 int
,表示当前阻塞队列中有多少个帧,最大不会超过我们在 1 中设置的 bufferCapacity
。这一小节中,我使用一个完整的例子来分析 VideoDecodeService 的源码
initButton
中进行了下面这些操作 newNative
方法。这个方法最终会进入到 video_decode_service.h 中调用 VideoDecodeService.cpp 的构造方法,构造方法则会创建一个 BlockingQueue.cpp 对象 decoded_unit_queue_
,这就是我们一直说的 先进先出阻塞队列 /sdcard/test.mp4
stringBuilder
和 times
是用来记录测试数据的就不说了setProject
方法,进过一系列调用链后会通过 jni 进入到代码块3 buffer
反序列化成 EditorProject.cpp 对象。address
强转 VideoDecodeService.cpp 对象。LoadProject
方法解析出一些数据,例如视频的帧率、宽高等等。有兴趣的读者可以跟进入看看。SetProject
给 VideoDecodeService.cpp 设置 EditorProject.cpp。start
最终也是到代码块3中,调用 Start
方法。我们继续进入 Start
方法中,发现其中是启动了一个线程然后调用 VideoDecodeService::DecodeThreadMain
,这个方法内部则是一个 while
循环,每当使用 FFMPEG 解码出一个视频帧的时候就会将这一帧放到 decoded_unit_queue_
中。当外部没有消费者时,decoded_unit_queue_
的帧数量将会很快达到阈值(我们设置的是10),此时这个线程就会被阻塞。直到外部消费后,帧数量减少了,本线程将会继续开始解码视频帧,如此往复。-----代码块3----- com_whensunset_wsvideoeditorsdk_inner_VideoDecoderService.cc
JNIEXPORT void JNICALL
Java_com_whensunset_wsvideoeditorsdk_inner_VideoDecodeService_setProjectNative
(JNIEnv *env, jobject, jlong address, jdouble render_pos, jbyteArray buffer) {
VideoDecodeService *native_decode_service = reinterpret_cast<VideoDecodeService *>(address);
model::EditorProject project;
jbyte *buffer_elements = env->GetByteArrayElements(buffer, 0);
project.ParseFromArray(buffer_elements, env->GetArrayLength(buffer));
env->ReleaseByteArrayElements(buffer, buffer_elements, 0);
LoadProject(&project);
native_decode_service->SetProject(project, render_pos);
}
JNIEXPORT void JNICALL Java_com_whensunset_wsvideoeditorsdk_inner_VideoDecodeService_startNative
(JNIEnv *, jobject, jlong address) {
VideoDecodeService *native_decode_service = reinterpret_cast<VideoDecodeService *>(address);
native_decode_service->Start();
}
getRenderFrame
方法来从 VideoDecodeService 中消费一个视频帧。然后把帧的信息打印到 TextView 上面。其实这里的代码可以类比为视频的播放,VideoDecodeService 不断地在后台线程进行解码按顺序将视频帧放入到队列中,本线程则不断的从队列中取出一帧进行消费,就像视频帧被渲染到屏幕上一样。终于从零开发仿写一个抖音APP这一系列文章又重新开始更新了,今年以来文章的发表间隔长了很多,写文章的时间也少了很多。但是为了那么多支持、关注我的读者我也不能就这样放弃更新。立一个 flag,今后每个月都要更新一篇文章,希望大家能够多多支持,感谢!!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有