之前在博文《FFmpeg支持QUIC》中介绍了FFmpeg支持谷歌QUIC协议的方法,简单说就是在Chromium中自定义一个模块,把QUIC的接口封装成FFmpeg Protocol插件需要的接口形式。但是这种办法归根结底只是实验性质,实际用到生产环境中,往往QUIC(基于UDP)有可能不通,需要一定的回退策略。
本文将介绍FFmpeg集成Chromium自带网络库Cronet的方法,在Cronet中有使用QUIC的完整策略,可以参考我的博文《Chromium QUIC逻辑》。当然使用Cronet并不仅仅可以使用QUIC,它还支持HTTP/HTTP2,甚至可以支持WebSocket,如果要防止DNS劫持,可以启用其基于异步DNS的DOH(DNS Over HTTP)功能。FFmpeg内部实现的HTTP协议仅支持HTTP/1.1,如果考虑升级FFmpeg的HTTP协议,跨平台的Cronet是一个比较好的选择。
目前Cronet已经在Google的YouTube、Google App等产品中被大量使用,此外国内的各大厂商如腾讯、新浪微博、百度、哔哩哔哩等都陆续使用了Cronet。这段时间主要就是在折腾这个库,我们的产品也已经全面上线Cronet。
FFmepg代码:https://github.com/sonysuqin/FFmpeg-Quic-Cronet
上图描述了Cronet网络库的基本层次结构,在Chromium内核的基础上封装了Cronet Native层,针对不同的平台分别封装了C层、Android、iOS的接口。具体可以参考谷歌官方Cronet接口参考。
上图是Cronet网络库接口的基本工作流程,适用于所有平台。
接口基本组件包括:
序号 | 组件 | 功能 |
---|---|---|
1 | CronetEngine | Cronet引擎,存储Cronet的一些全局数据,例如代理配置、HTTP缓存、DNS缓存等。每个HTTP请求都基于CronetEngine这个上下文,不同的HTTP请求可以通过同一个CronetEngine共享各种缓存。一个APP最好只创建一个CronetEngine。 |
2 | CronetRequest | 一次Cronet请求,封装该请求的方法、数据、状态等。 |
3 | CronetExecutor | CronetRequest的运行环境,由APP实现,是Cronet底层与APP交互的通道,通常是一个线程。 |
4 | CronetCallback | Cronet异步接口的回调对象,每个CronetRequest必须绑定一个CronetCallback对象才能获得通知。 |
工作流程:
基于C层的接口,适用于在所有平台上开发C/C++的程序,Android和iOS版本的开发本文不涉及。
Cronet_EnginePtr CreateCronetEngine() {
// 创建Cronet_Engine对象, 每个APP最好只创建一个Cronet_Engine对象.
Cronet_EnginePtr cronet_engine = Cronet_Engine_Create();
// 创建Cronet_EngineParams对象.
Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create();
// 设置User agent.
Cronet_EngineParams_user_agent_set(engine_params, "CronetTest/1");
// 使能HTTP2.
Cronet_EngineParams_enable_http2_set(engine_params, true);
// 使能HTTP3/QUIC.
Cronet_EngineParams_enable_quic_set(engine_params, true);
// 设置QUIC的空闲超时,在已经协商成功的QUIC通道上请求数据,如果
// 超过5秒未收到响应,则超时并自动回滚到HTTP,如果在接收QUIC数
// 据的过程中UDP不可用,则5秒后上报超时失败。注意QUIC的参数通过
// JSON串设置,对全平台适用。
Cronet_EngineParams_experimental_options_set(
engine_params,
"{\"QUIC\":{\"idle_connection_timeout_seconds\":5}}");
// 启动Cronet_Engine.
Cronet_Engine_StartWithParams(cronet_engine, engine_params);
// 销毁Cronet_EngineParams对象.
Cronet_EngineParams_Destroy(engine_params);
return cronet_engine;
}
Cronet_UrlRequestCallback是Cronet所有数据、结果上报的唯一方式,因为Cronet Native层只提供了异步接口,每个Cronet_UrlRequest请求必须绑定一个Cronet_UrlRequestCallback对象才能获得通知。
原型:
CRONET_EXPORT Cronet_UrlRequestCallbackPtr Cronet_UrlRequestCallback_CreateWith(
Cronet_UrlRequestCallback_OnRedirectReceivedFunc OnRedirectReceivedFunc,
Cronet_UrlRequestCallback_OnResponseStartedFunc OnResponseStartedFunc,
Cronet_UrlRequestCallback_OnReadCompletedFunc OnReadCompletedFunc,
Cronet_UrlRequestCallback_OnSucceededFunc OnSucceededFunc,
Cronet_UrlRequestCallback_OnFailedFunc OnFailedFunc,
Cronet_UrlRequestCallback_OnCanceledFunc OnCanceledFunc,
Cronet_UrlRequestCallback_OnMetricsCollectedFunc OnMetricsCollectedFunc);
回调 | 作用 |
---|---|
Cronet_UrlRequestCallback_OnRedirectReceivedFunc | 接收到重定向的通知,APP可以决定进行重定向或者取消请求。 |
Cronet_UrlRequestCallback_OnResponseStartedFunc | 开始接收数据的通知,APP可以在此回调中获取HTTP响应头,并开始读取响应数据。 |
Cronet_UrlRequestCallback_OnReadCompletedFunc | 一次读取结束的通知,APP可以获取一次读取的数据,并发起下一次读取操作。 |
Cronet_UrlRequestCallback_OnSucceededFunc | 一次请求成功结束的通知,但是不能代表一次业务请求的成功,APP需要判断响应码并处理响应的数据。 |
Cronet_UrlRequestCallback_OnFailedFunc | 一次请求失败的通知,可能原因是网络错误,并不会因为服务端返回400、500错误调用。 |
Cronet_UrlRequestCallback_OnCanceledFunc | 一次请求被成功取消的通知。 |
Cronet_UrlRequestCallback_OnMetricsCollectedFunc | 一次请求的度量通知,以JSON格式上报HTTP请求的各个阶段:包括DNS请求、连接、握手、收发数据等阶段消耗的时间。 |
该方法需要传入若干回调,为了与某个APP的对象建立关系,可以调用下面的方法:
CRONET_EXPORT void Cronet_UrlRequestCallback_SetClientContext(Cronet_UrlRequestCallbackPtr self, Cronet_ClientContext client_context);
可以看到上述每个回调都会携带Cronet_UrlRequestCallbackPtr self参数,APP可以从回调本身获得绑定的APP的对象,调用下面的方法:
CRONET_EXPORT Cronet_ClientContext Cronet_UrlRequestCallback_GetClientContext(Cronet_UrlRequestCallbackPtr self);
当然从每个回调携带的Cronet_UrlRequestPtr request参数也获得了每个绑定的HTTP请求。
注意:这些回调都不是直接在底层的线程直接调用到APP,而是通过Cronet_Executor来调用。
Cronet_Executor是Cronet底层与APP交互的通道,Cronet底层的某些任务(例如回调)会通过Cronet_Executor调用,Cronet_Executor可以选择在自己的线程中调用这些任务,达到与底层线程的隔离。
CRONET_EXPORT Cronet_ExecutorPtr Cronet_Executor_CreateWith(Cronet_Executor_ExecuteFunc ExecuteFunc);
上面的方法创建一个Cronet_Executor,需要传入一个回调:
typedef void (*Cronet_Executor_ExecuteFunc)(Cronet_ExecutorPtr self, Cronet_RunnablePtr command);
该回调会在底层线程调用,通知APP有一个Cronet_RunnablePtr任务需要调度执行,通常APP需要将任务Cronet_RunnablePtr放到独立的线程执行,最后APP负责删除该Cronet_RunnablePtr 任务。
可以通过以下两个方法设置、获取Cronet_Executor绑定的某个APP的自定义对象。
CRONET_EXPORT void Cronet_Executor_SetClientContext(Cronet_ExecutorPtr self, Cronet_ClientContext client_context);
CRONET_EXPORT Cronet_ClientContext Cronet_Executor_GetClientContext(Cronet_ExecutorPtr self);
不需要每个请求都创建一个Cronet_Executor,例如,可以只创建一个Cronet_Executor,所有请求都共享一个Cronet_Executor。
Cronet_UrlRequestPtr PerformRequest(
const std::string& url, // url.
Cronet_EnginePtr cronet_engine, // Cronet_Engine对象.
Cronet_ExecutorPtr executor, // Cronet_Executor对象.
Cronet_UrlRequestCallbackPtr callback) { // Cronet_UrlRequestCallback对象.
// 创建Cronet_UrlRequest对象.
Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create();
// 创建Cronet_UrlRequestParams对象.
Cronet_UrlRequestParamsPtr request_params = Cronet_UrlRequestParams_Create();
// 设置GET方法.
Cronet_UrlRequestParams_http_method_set(request_params, "GET");
// 用上述参数初始化请求.
Cronet_RESULT ret = Cronet_UrlRequest_InitWithParams(
request, // Cronet_UrlRequest对象.
cronet_engine, // Cronet_Engine对象.
url.c_str(), // url.
request_params, // Cronet_UrlRequestParams对象.
callback, // Cronet_UrlRequestCallback对象.
executor); // Cronet_Executor对象.
// 销毁Cronet_UrlRequestParams对象.
Cronet_UrlRequestParams_Destroy(request_params);
// 判断Cronet_UrlRequest_InitWithParams结果.
if (ret != Cronet_RESULT_SUCCESS) {
std::cout << "Cronet_UrlRequest_InitWithParams error:" << ret;
return NULL;
}
// 启动Cronet_UrlRequest请求.
ret = Cronet_UrlRequest_Start(request);
// 判断Cronet_UrlRequest_Start结果.
if (ret != Cronet_RESULT_SUCCESS) {
std::cout << "Cronet_UrlRequest_Start error:" << ret;
return NULL;
}
return request;
}
APP必须保证在Cronet_UrlRequestCallback_OnSucceededFunc、Cronet_UrlRequestCallback_OnFailedFunc、Cronet_UrlRequestCallback_OnCanceledFunc这些回调调用之后才调用以下方法销毁请求对象以保证安全。
CRONET_EXPORT void Cronet_UrlRequest_Destroy(Cronet_UrlRequestPtr self);
在请求还没有结束前的任何时刻,都可以调用Cronet_UrlRequest_Cancel方法来取消一个请求,然后在Cronet_UrlRequestCallback_OnCanceledFunc回调中调用Cronet_UrlRequest_Destroy释放请求。
将所有调用都POST到Cronet_Executor线程中执行可以保证线程安全性。
跟《FFmpeg支持QUIC》中提到的方法一样,这里在FFmpeg内部增加了一个协议cronet,实现FFmpeg协议要求的基本方法:
const URLProtocol ff_cronet_protocol = {
.name = "cronet",
.url_open = cronet_open,
.url_close = cronet_close,
.url_read = cronet_read,
.url_write = cronet_write,
.url_seek = cronet_seek,
.priv_data_size = sizeof(CronetContext),
.priv_data_class = &cronet_context_class,
.flags = URL_PROTOCOL_FLAG_NETWORK,
.default_whitelist = "cronet,cronets"
};
调用上一节介绍的C层接口,并将Cronet异步接口转成FFmpeg要求的同步接口。
在Android下面,APP层(Java)和Native层FFmpeg依赖同一个Cronet动态库,在iOS下,APP层(OC)和Native层FFmpeg依赖同一个Cronet.framework。
具体细节请直接获取、阅读、测试代码:
git clone https://github.com/sonysuqin/FFmpeg-Quic-Cronet.git
git checkout -b 4.1.cronet remotes/origin/4.1.cronet
按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。
在chromium/src下执行:
gn gen out/Debug --args="is_debug=true is_component_build=false target_cpu=\"x86\""
这里可以决定是产生Debug版还是Release版。
在chromium/src下执行:
ninja -C out\Debug cronet
在chromium/src/out/Debug目录下会生成:
按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。
在chromium/src下执行:
./components/cronet/tools/cr_cronet.py gn --release --out_dir=out/Release
这里可以决定是产生Debug版还是Release版。
在chromium/src下执行:
ninja -C out\Release cronet_package
在chromium/src/out/Release目录下会生成cronet目录:
cronet
|-- api_version.txt
|-- AUTHORS
|-- cronet_api.jar
|-- cronet_api-src.jar
|-- cronet_impl_common_java.jar
|-- cronet_impl_common_java-src.jar
|-- cronet_impl_common_proguard.cfg
|-- cronet_impl_native_java.jar
|-- cronet_impl_native_java-src.jar
|-- cronet_impl_native_proguard.cfg
|-- cronet_impl_platform_java.jar
|-- cronet_impl_platform_java-src.jar
|-- cronet_impl_platform_proguard.cfg
|-- cronet-sample-src.jar
|-- javadoc
|-- libs
|-- LICENSE
|-- README.md.html
|-- res
|-- symbols
|-- test
`-- VERSION
jar包为Android使用的库文件,lib目录下为Native动态库,symbols目录下为Native动态库对应的符号文件。
按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。
在chromium/src下执行:
./components/cronet/tools/cr_cronet.py -i gn --release --out_dir=out/Release
这里可以决定是产生Debug版还是Release版。
在chromium/src下执行:
ninja -C out\Release cronet_package
在chromium/src/out/Release目录下会生成:
git clone https://github.com/sonysuqin/FFmpeg-Quic-Cronet.git
git checkout -b 4.1.cronet remotes/origin/4.1.cronet
需要安装mingw32/msys2环境,然后安装GCC、SDL2等FFmpeg通常需要依赖的工具,主要参考了以下这些网页:
《msys2和SDL2环境搭建》
《windows下编译FFMPEG篇》
《Windows10平台编译ffmpeg 4.0.2,生成ffplay》
使用mingw32,在chromium/src/out/Debug目录下,执行:
gendef cronet.73.0.3683.75.dll
dlltool --kill-at -d cronet.73.0.3683.75.def --dllname cronet.73.0.3683.75.dll -l cronet.73.0.3683.75.a
注意自己修改cronet的版本号。
在FFmpeg目录下,执行:
mkdir build_debug
cd build_debug
../configure --prefix=/d/log/ffmpeg_debug --disable-static --enable-shared \
--enable-debug=3 --disable-optimizations --disable-stripping \
--enable-gpl --enable-version3 --enable-sdl --disable-mmx \
--arch=x86 --enable-libcronet \
--extra-cflags="-I/d/work/google/chromium/src/components/cronet/native/include -I/d/work/google/chromium/src/components/cronet/native/generated" \
--extra-ldflags=-L/d/work/google/chromium/src/out/Debug \
--extra-libs=-lcronet.73.0.3683.75
make && make install
软件 | 版本 |
---|---|
Ubuntu | 16.04 |
NDK | r17c |
在FFmpeg目录下,执行:
./build_android.sh
可以在脚本中自行指定输出目录,默认在当前目录的android目录下。
软件 | 版本 |
---|---|
macos | 10.14 |
xcode | 10.3 |
参考FFmpeg-iOS-build-script,安装gas-preprocessor和yasm。
在FFmpeg目录下,执行:
./build_ios.sh
可以在脚本中自行指定输出目录,默认在当前上级目录的thin目录下。
Windows端可以使用ffplay直接进行测试,用mingw32进入FFmpeg的输出目录,执行:
./ffplay cronets://roblin.cn/video/mao.mp4
Debug版需要同时拷贝mingw环境的依赖库到FFmpeg输出目录下,例如/mingw32/bin下的dll。
bin
|-- avcodec-58.dll
|-- avcodec.lib
|-- avdevice-58.dll
|-- avdevice.lib
|-- avfilter-7.dll
|-- avfilter.lib
|-- avformat-58.dll
|-- avformat.lib
|-- avutil-56.dll
|-- avutil.lib
|-- cronet.73.0.3683.75.dll
|-- ffmpeg.exe
|-- ffplay.exe
|-- ffprobe.exe
|-- libatomic-1.dll
|-- libbz2-1.dll
|-- libcharset-1.dll
|-- libgcc_s_dw2-1.dll
|-- libgmp-10.dll
|-- libgmpxx-4.dll
|-- libgomp-1.dll
|-- libiconv-2.dll
|-- liblsmash-2.dll
|-- liblzma-5.dll
|-- libminizip-1.dll
|-- libopenal-1.dll
|-- libquadmath-0.dll
|-- libssp-0.dll
|-- libstdc++-6.dll
|-- libvulkan-1.dll
|-- libwinpthread-1.dll
|-- libx264-157.dll
|-- postproc-55.dll
|-- postproc.lib
|-- SDL2.dll
|-- swresample-3.dll
|-- swresample.lib
|-- swscale-5.dll
|-- swscale.lib
`-- zlib1.dll
其他平台的测试需要编写程序,调用FFmpeg的avformat API,传入URL:cronets://roblin.cn/video/mao.mp4。
如果想用浏览器测试QUIC可以直接点播放。
目前在roblin.cn上部署了一个我们自己开发的支持QUIC的Nginx,代码地址:https://github.com/evansun922/nginx-quic。