腾讯教育 App Flutter 跨端点播组件实践

本文对比分析了Flutter中PlatformView和TextureWidget两种视频渲染方案,详细介绍了各自的实现方式和性能表现,探讨了在教育场景下的应用及优化。

点击“开发者技术前线”,选择“星标?”

13:21 在看|星标|留言,  真爱

640?wx_fmt=png 来自:腾讯在线教育

腾讯教育从去年开始接入Flutter,今年上半年重构腾讯课堂和企鹅辅导iPad端,80%代码都采用Flutter实现,对于教育最重要的点播功能同样也需要迁移到Flutter上进行渲染。

目前正在研究的实现渲染的方案主要有2种形式PlatformView和Texture Widget。下面文章就先大概讲述一下Flutter的渲染框架原理和实现,然后会对这两种方案进行对比分析。

Flutter渲染框架和原理

Flutter的框架主要包括Framework和Engine两层,应用是基于Framework层开发的,Framework负责渲染中的Build,Layout,Paint,生成Layer等。Engine层是C++实现的渲染引擎,负责把Framework生成的Layer组合,生成纹理,然后通过OpenGL接口向GPU提交渲染数据。

640?wx_fmt=png

Flutter:Framework的最底层,提供工具类和方法 Painting :封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的接口 Animation :动画相关的类 Gesture :提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器 Rendering:渲染库,Flutter的控件树在实际显示时会转换成对应的渲染对象(RenderObject)树来实现布局和绘制操作

渲染原理

当GPU发出Vsync信号时,会执行Dart代码绘制新UI,Dart会被执行为Layer Tree,然后经过Compositor合成后交由Skia引擎渲染处理为GPU数据,最后通过GL/Vulkan发给GPU,具体流程如下:

640?wx_fmt=png

当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候通知Framework,Framework进行animations,build,layout,compositing,paint,最后生成layer提交给Engine。Engine再把layer进行组合,生成纹理,最后通过OpenGL接口提交数据给GPU,具体流程如下:

640?wx_fmt=png


接下来分别分析一下两个方案的各自的特点以及使用的方式。

PlatformView

PlatformView是Flutter官方在1.0版本推出的组件,以解决开发者想在Flutter中嵌入Android和iOS平台原生View的Widget。例如想嵌入地图、视频播放器等原生组件,对于想尝试Flutter,但是又想低成本的迁移复杂组件的团队,可以尝试PlatformView,在 Dart 中的类对应到 iOS 和 Android 平台分别是UIKitView和AndroidView。

640?wx_fmt=png

那么PlatformView在点播功能中应该怎么实现,如下图所示:

640?wx_fmt=png

其中的ARMPlatformView代表业务View。

Dart层
1.创建关联类

关联类的作用是Native和Dart侧的桥梁,其中id需要和Native获取对应。

class VodPlayerController {	
    VodPlayerController._(int id)	
    : _channel = MethodChannel('ARMFlutterVodPlayerView_$id');	
    final MethodChannel _channel;	
    Future<void> play(String url) async {	
        return _channel.invokeMethod('play', url);	
    }	
    Future<void> stop() async {	
        return _channel.invokeMethod('stop');	
    }	
}
2.创建Callback
typedef void VodPlayerViewWidgetCreatedCallback(VodPlayerController controller);
3.创建Widget布局
class VodVideoWidget extends StatefulWidget {	
    final VodPlayerViewWidgetCreatedCallback callback;	
    final x;	
    final y;	
    final width;	
    final height;	
    VodVideoWidget({	
        Key key,	
        @required this.callback,	
        @required this.x,	
        @required this.y,	
        @required this.width,	
        @required this.height,	
    });	
    @override	
    _VodVideoWidgetState createState() => _VodVideoWidgetState();	
}	
class _VodVideoWidgetState extends State<VodVideoWidget> {	
    @override	
    Widget build(BuildContext context) {	
        return UiKitView(	
        viewType: 'ARMFlutterVodPlayerView',	
        onPlatformViewCreated: _onPlatformViewCreated,	
        creationParams: <String,dynamic>{	
            'x': widget.x,	
            'y': widget.y,	
            'width': widget.width,	
            'height': widget.height,	
        },	
        creationParamsCodec: new StandardMessageCodec(),	
        );	
    }	
    void _onPlatformViewCreated(int id){	
        if(widget.callback == null) {	
            return;	
        }	
        widget.callback(VodPlayerController._(id));	
    }	
}
Native层
1.注册ViewFactory
@implementation ARMFlutterVodPlugin	
+ (void)registerWithRegistrar:(nonnull NSObject<FlutterPluginRegistrar> *)registrar {	
    ARMFlutterVodPlayerFactory* vodFactory =	
    [[ARMFlutterVodPlayerFactory alloc] initWithMessenger:registrar.messenger];	
    [registrar registerViewFactory:vodFactory withId:@"ARMFlutterVodPlayerView"];	
}	
@end
2.注册Plugin
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {	
    [ARMFlutterVodPlugin registerWithRegistrar:[registry registrarForPlugin:@"ARMFlutterVodPlugin"]];	
}	
3.ViewFactory实现
@implementation ARMFlutterVodPlayerFactory	
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager {	
    self = [super init];	
    if (self) {	
        _messenger = messager;	
    }	
    return self;	
}	
- (NSObject<FlutterMessageCodec> *)createArgsCodec {	
    return [FlutterStandardMessageCodec sharedInstance];	
}	
- (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args {	
    return [[ARMFlutterVodPlayerView alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:self.messenger];	
}	
@end
4.View实现
@implementation ARMFlutterVodPlayerView	
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{	
    if (self = [super init]) {	
        NSDictionary *dic = args;	
        CGFloat x = [dic[@"x"] floatValue];	
        CGFloat y = [dic[@"y"] floatValue];	
        CGFloat width = [dic[@"width"] floatValue];	
        CGFloat height = [dic[@"height"] floatValue];	
        ARMFlutterVodManager.shareInstance.mainView.frame = CGRectMake(x, y, width, height);	
        NSString* channelName = [NSString stringWithFormat:@"ARMFlutterVodPlayerView_%lld", viewId];	
        _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];	
        __weak __typeof__(self) weakSelf = self;	
        [_channel setMethodCallHandler:^(FlutterMethodCall *  call, FlutterResult  result) {	
        [weakSelf onMethodCall:call result:result];	
        }];	
    }	
    return self;	
}	
- (nonnull UIView *)view {	
    return ARMFlutterVodManager.shareInstance.mainView;	
}	
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{	
    if ([[call method] isEqualToString:@"play"]) {	
        NSString *url = [call arguments];	
        [ARMFlutterVodManager.shareInstance play:url];	
    } else {	
        result(FlutterMethodNotImplemented);	
    }	
}	
@end

Texture Widget

基于纹理实现视频渲染,Flutter官方提供的video_player则是通过这种方式实现的,以iOS为例,Native需要提供一个CVPixelBufferRef给Texture Widget,具体实现流程如下图所示: 640?wx_fmt=png其中的ARMTexture是业务提供CVPixelBufferRef,具体实现步骤主要是 1.继承FlutterTexture 2.管理已注册textures集合
textures = [self.registrar textures];
3.获得textureId
self.textureId = [textures registerTexture:self];
4.重写
- (CVPixelBufferRef _Nullable)copyPixelBuffer
返回CVPixelBufferRef 5.通知Texture获取CVPixelBufferRef
- (void)onDisplayLink {	
    [textures textureFrameAvailable:self.textureId];	
}
Dart层
MethodChannel _globalChannel = MethodChannel("ARMFlutterTextureVodPlayer");	
class _ARMPlugin {	
    MethodChannel get channel => MethodChannel("ARMFlutterTextureVodPlayer/$textureId");	
    int textureId;	
    _ARMPlugin(this.textureId);	
    Future<void> play() async {	
        await channel.invokeMethod("play");	
    }	
    Future<void> pause() async {	
        await channel.invokeMethod("pause");	
    }	
    Future<void> stop() async {	
        await channel.invokeMethod("stop");	
    }	
    Future<void> setNetworkDataSource(	
        {String uri, Map<String, String> headers = const {}}) async {	
        await channel.invokeMethod("setNetworkDataSource", <String, dynamic>{	
        "uri": uri,	
        "headers": headers,	
        });	
    }	
}
Native层
1.注册Plugin
@implementation ARMFlutterTextureVodPlugin	
- (instancetype)initWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {	
    self = [super init];	
    if (self) {	
        self.registrar = registrar;	
    }	
    return self;	
}	
+ (instancetype)pluginWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {	
    return [[self alloc] initWithRegistrar:registrar];	
}	
+ (void)registerWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {	
    FlutterMethodChannel *channel = [FlutterMethodChannel	
        methodChannelWithName:@"ARMFlutterTextureVodPlayer"	
        binaryMessenger:[registrar messenger]];	
    ARMFlutterTextureVodPlugin *instance = [ARMFlutterTextureVodPlugin pluginWithRegistrar:registrar];	
    [registrar addMethodCallDelegate:instance channel:channel];	
}	
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {	
}	
@end
2.管理Texture/获取TextureId
+ (instancetype)armWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {	
    return [[self alloc] initWithRegistrar:registrar];	
}	
- (instancetype)initWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {	
    if (self = [super init]) {	
        self.registrar = registrar;	
        textures = [self.registrar textures];	
        self.textureId = [textures registerTexture:self];	
        NSString *channelName = [NSString stringWithFormat:@"ARMFlutterTextureVodPlayer/%lli", self.textureId];	
        channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:[registrar messenger]];	
        __weak typeof(&*self) weakSelf = self;	
        [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {	
        [weakSelf handleMethodCall:call result:result];	
        }];	
    }	
    return self;	
}
3.重写copyPixelBuffer
- (CVPixelBufferRef _Nullable)copyPixelBuffer {	
    CVPixelBufferRef newBuffer = [self.vodPlayer framePixelbuffer];	
    if (newBuffer) {	
        CFRetain(newBuffer);	
        CVPixelBufferRef pixelBuffer = latestPixelBuffer;	
        while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, newBuffer, (void **) &latestPixelBuffer)) {	
            pixelBuffer = latestPixelBuffer;	
        }	
        return pixelBuffer;	
    }	
    return NULL;	
}
4.调用textureFrameAvailable
这里是需要主动调用的,告诉TextureRegistry更新画面。
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink)];	
displayLink.frameInterval = 1;	
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- (void)onDisplayLink {	
    [textures textureFrameAvailable:self.textureId];	
}

性能对比

播放同一段MP4视频PlatformView和Texture Widget性能对比, Texture Widget 性能相对差一些,分析主要原因是因为CVPixelBufferRef 提供给Flutter的Texture,Native到Flutter会经过GPU->CPU->GPU的拷贝过程,1.0版本数据对比如下: 640?wx_fmt=png 640?wx_fmt=png
遇到的问题
Flutter 播放 器接入到课堂iPad中采用的是Texure的方案,在实现PlatformView和 Tex ture Widget 两个方案的时候,主要遇到了以下几个问题。
PlatformView内存增长问题
课堂在连续播放视频之后,出现内存暴增问题,主要原因是OpenGL 操作都需要设置[EAGLContext setCurrentContext:context_] 在IOSGLRenderTarget析构的时候,没有设置context上下文。
课堂直播场景退出之后,前面Flutter页面出现黑屏
课堂直播课退出之后回到上一个Flutter页面出现页面黑屏,直播视频渲染也是采用OpenGL,当直播退出的时候不仅需要设置context,还需要清空帧缓冲区,重置纹理,销毁代码如下:
EAGLContext *prevContext = [EAGLContext currentContext];	
[EAGLContext setCurrentContext:_context];	
_renderer = nil;	
glBindTexture(GL_TEXTURE_2D, 0);	
glBindBuffer(GL_ARRAY_BUFFER, 0);	
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);	
if (_framebuffer) {	
     glDeleteFramebuffers(1, &_framebuffer);	
     _framebuffer = 0;	
}	
if (_renderbuffer) {	
     glDeleteRenderbuffers(1, &_renderbuffer);	
      _renderbuffer = 0;	
}	
if (_program) {	
     glDeleteProgram(_program);	
     _program = 0;	
}	
_context = nil;	
[EAGLContext setCurrentContext:prevContext];
Texture Widget内存相比PlatformView性能相对差一些
主要原因是因为CVPixelBufferRef 提供给Flutter的Texture,Native到Flutter会经过GPU->CPU->GPU的拷贝过程,所以我们将Native生成TextureID->拷贝生成PixelBuffer->生成新的TextureID改为直接通过Native生成TextureID->渲染,减少多次拷贝引起的内存问题,经过优化Texture Widget 的整体性能优于PlatformView。2.0优化后数据对比如下: 640?wx_fmt=png 640?wx_fmt=png
总结
目前基于教育自研的播放器ARMPlayer的Flutter播放器Plugin已经在腾讯课堂iPad中使用,采用优化后的Texture Widget方案,Texture Widget是官方推荐的方式,不管是视频,还是图片都可以用Texture,方便扩展,同时通过纹理形式贴到LayerTree上保证平台无关,多端可复用,优化后的Texture Widget性能也优于PlatformView,Flutter播放器Plugin是教育客户端中台(大前端和点播)结合的一个新的尝试。

---END---


选择”开发者技术前线 “星标?,内容一触即达。点击原文更多惊喜!

开发者技术前线 汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。


历史推荐

为什么我推荐你用 Ubuntu 开发?

支付宝 App 启动性能优化

美团基于跨平台 Flutter 的动态化平台建设

640?

点个在看,解锁更多惊喜!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值