FFmpeg支持Cronet(Chromium网络库)

FFmpeg支持Cronet

  • 1 背景
  • 2 代码
  • 3 Cronet使用介绍
    • 3.1 接口
    • 3.2 组件及工作流程
    • 3.3 Native开发
      • 3.3.1 创建并配置Cronet_Engine
      • 3.3.2 创建Cronet_UrlRequestCallback
      • 3.3.3 创建Cronet_Executor
      • 3.3.4 创建并发起Cronet_UrlRequest请求
      • 3.3.5 结束、销毁请求
  • 4 FFmpeg集成Cronet
  • 5 编译Cronet
    • 5.1 Windows
      • 5.1.1 下载Chromium
      • 5.1.2 生成工程
      • 5.1.3 编译
    • 5.2 Android
      • 5.2.1 下载Chromium
      • 5.2.2 生成工程
      • 5.2.3 编译
    • 5.3 iOS
      • 5.3.1 下载Chromium
      • 5.3.2 生成工程
      • 5.3.3 编译
  • 6 编译FFmpeg
    • 6.1 获取支持Cronet的FFmpeg代码
    • 6.2 编译
      • 6.2.1 Windows
        • 6.2.1.1 安装依赖
        • 6.2.1.2 处理cronet库的符号
        • 6.2.1.3 编译
      • 6.2.2 Android
        • 6.2.2.1 编译环境
        • 6.2.2.2 编译
      • 6.2.3 iOS
        • 6.2.3.1 编译环境
        • 6.2.3.2 安装gas-preprocessor和yasm
        • 6.2.3.3 编译
  • 7 测试
  • 8 关于QUIC服务端

1 背景

之前在博文《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。

2 代码

FFmepg代码:https://github.com/sonysuqin/FFmpeg-Quic-Cronet

3 Cronet使用介绍

3.1 接口

FFmpeg支持Cronet(Chromium网络库)_第1张图片
上图描述了Cronet网络库的基本层次结构,在Chromium内核的基础上封装了Cronet Native层,针对不同的平台分别封装了C层、Android、iOS的接口。具体可以参考谷歌官方Cronet接口参考。

3.2 组件及工作流程

FFmpeg支持Cronet(Chromium网络库)_第2张图片
上图是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对象才能获得通知。

工作流程:

  • APP创建一个全局的CronetEngine;
  • APP创建一个全局的CronetExecutor,用于执行各种Task;
  • APP为一次请求创建一个CronetCallback;
  • APP从CronetEngine、CronetExecutor、CronetCallback创建一个CronetRequest;
  • APP发起CronetRequest请求,CronetRequest请求内部的所有Task都在CronetExecutor中执行;
  • CronetRequest的结果通过绑定的CronetCallback回调上报到APP。

3.3 Native开发

基于C层的接口,适用于在所有平台上开发C/C++的程序,Android和iOS版本的开发本文不涉及。

3.3.1 创建并配置Cronet_Engine

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;
}

3.3.2 创建Cronet_UrlRequestCallback

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来调用。

3.3.3 创建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。

3.3.4 创建并发起Cronet_UrlRequest请求

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;
}

3.3.5 结束、销毁请求

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线程中执行可以保证线程安全性。

4 FFmpeg集成Cronet

跟《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

5 编译Cronet

5.1 Windows

5.1.1 下载Chromium

按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。

5.1.2 生成工程

在chromium/src下执行:

gn gen out/Debug --args="is_debug=true is_component_build=false target_cpu=\"x86\""

这里可以决定是产生Debug版还是Release版。

5.1.3 编译

在chromium/src下执行:

ninja -C out\Debug cronet

在chromium/src/out/Debug目录下会生成:

  • cronet.73.0.3683.75.dll
  • cronet.73.0.3683.75.dll.lib
  • cronet.73.0.3683.75.dll.pdb

5.2 Android

5.2.1 下载Chromium

按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。

5.2.2 生成工程

在chromium/src下执行:

./components/cronet/tools/cr_cronet.py gn --release --out_dir=out/Release

这里可以决定是产生Debug版还是Release版。

5.2.3 编译

在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动态库对应的符号文件。

5.3 iOS

5.3.1 下载Chromium

按照chromium的官方编译文档,配置环境并下载chromium代码。注意checkout到较新的稳定版tag。

5.3.2 生成工程

在chromium/src下执行:

./components/cronet/tools/cr_cronet.py -i gn --release --out_dir=out/Release

这里可以决定是产生Debug版还是Release版。

5.3.3 编译

在chromium/src下执行:

ninja -C out\Release cronet_package

在chromium/src/out/Release目录下会生成:

  • Cronet.framework,动态库;
  • Static/Cronet.framework,静态库;
  • Cronet.dSYM,用于线上崩溃问题的堆栈解析。

6 编译FFmpeg

6.1 获取支持Cronet的FFmpeg代码

git clone https://github.com/sonysuqin/FFmpeg-Quic-Cronet.git
git checkout -b 4.1.cronet remotes/origin/4.1.cronet

6.2 编译

6.2.1 Windows

6.2.1.1 安装依赖

需要安装mingw32/msys2环境,然后安装GCC、SDL2等FFmpeg通常需要依赖的工具,主要参考了以下这些网页:
《msys2和SDL2环境搭建》
《windows下编译FFMPEG篇》
《Windows10平台编译ffmpeg 4.0.2,生成ffplay》

6.2.1.2 处理cronet库的符号

使用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的版本号。

6.2.1.3 编译

在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

6.2.2 Android

6.2.2.1 编译环境

软件 版本
Ubuntu 16.04
NDK r17c

6.2.2.2 编译

在FFmpeg目录下,执行:

./build_android.sh

可以在脚本中自行指定输出目录,默认在当前目录的android目录下。

6.2.3 iOS

6.2.3.1 编译环境

软件 版本
macos 10.14
xcode 10.3

6.2.3.2 安装gas-preprocessor和yasm

参考FFmpeg-iOS-build-script,安装gas-preprocessor和yasm。

6.2.3.3 编译

在FFmpeg目录下,执行:

./build_ios.sh

可以在脚本中自行指定输出目录,默认在当前上级目录的thin目录下。

7 测试

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支持Cronet(Chromium网络库)_第3张图片
其他平台的测试需要编写程序,调用FFmpeg的avformat API,传入URL:cronets://roblin.cn/video/mao.mp4。
如果想用浏览器测试QUIC可以直接点播放。
FFmpeg支持Cronet(Chromium网络库)_第4张图片

8 关于QUIC服务端

目前在roblin.cn上部署了一个我们自己开发的支持QUIC的Nginx,代码地址:https://github.com/evansun922/nginx-quic。

你可能感兴趣的:(QUIC,网络,播放器开发)