使用Native(以Android為例)播放器構建Flutter播放外掛

Fluency發表於2018-08-21

Flutter官方提供了一系列的外掛的外掛

使用Native(以Android為例)播放器構建Flutter播放外掛

來為Flutter提供眾多原生系統級API呼叫,包括感測器、檔案讀寫、資料庫、輕量儲存等等,這些外掛大都是以原生、Dart間通過MethodChannelEventChannel相互通訊實現的。但google還提拱了一個Video_Player,專門用於視訊播放的外掛。外掛原理是通過Native提供播放器能力,dart層通過Channel呼叫,以及雙方通訊控制播放,但畫面是如何渲染到FlutterView(Android繼承自SurfaceView)上的呢? 在此之前我們先回顧下Android上的檢視都是如何渲染到螢幕上的。

在Android 中負責繪製UI的服務是SurfaceFlingerSurfaceFlinger服務執行在Android系統的System程式中,它負責管理Android系統的幀緩衝區(Frame Buffer)。Android應用程式為了能夠將自己的UI繪製在系統的幀緩衝區上,它們就必須要與SurfaceFlinger服務進行通訊,由於Android應用程式與SurfaceFlinger服務是執行在不同的程式中的,因此,它們採用Binder程式間通訊機制來進行通訊。

使用Native(以Android為例)播放器構建Flutter播放外掛

Android應用程式在通知SurfaceFlinger服務來繪製自己的UI的時候,需要將大量的UI相關資料傳遞給SurfaceFlinger服務(如要繪製UI的區域、位置等資訊)。一個Android應用程式可能會有很多個Window,而每一個Window都有自己的UI後設資料,為了解決客戶端與服務端通訊效率問題,每一個客戶端利用Android系統的匿名共享記憶體機制(Anonymous Shared Memory)與SurfaceFlinger服務程式開闢一塊共享記憶體。

使用Native(以Android為例)播放器構建Flutter播放外掛

並對該共享記憶體進行封裝,成為一個ShareClient。ShareClient中儲存著ShareBufferStack,可以簡單的理解為對應的Surface

使用Native(以Android為例)播放器構建Flutter播放外掛

Android 應用程式通過Canvas 繪製其UI並儲存進FramBuffer中,而Canvas是其Surface 的成員,所以SurfaceFlinger可以讀取FramBuffer資料並繪製渲染UI。

簡而言之,Android是通過SurfaceFlinger服務進行繪製UI,而FlutterApplication中通過FlutterView(SurfaceView)將渲染UI的工作交給了Flutter ,dart->組裝LayerTree->Skia(SurfaceFlinger服務底層繪製二維影像同樣使用開源的Skia)

使用Native(以Android為例)播放器構建Flutter播放外掛

其實我們可以發現Android的繪製流程和Flutter的回執流程大體相當,但是否可以在Dart層將繪製能力返還給Android呢(Java),由於Surface持有Canvas,我們只要能夠得到Surface就可以將Surface交由播放器繪製了,事實上FlutterSDK也提供了相關的API。下面我們來用一個簡單的例子看下如何實現。

首先建立一個FlutterPlugin:MySurfaceTestPlugin

package com.example.darttest;

import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Build;
import android.view.Surface;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.view.TextureRegistry;

public class MySurfaceTestPlugin implements MethodChannel.MethodCallHandler {
    private final Registrar flutterRegistrar;
    private TextureRegistry textureRegistry;
    private TextureRegistry.SurfaceTextureEntry surfaceTextureEntry;
    private Surface surface;
    Random random;

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {

        String method = methodCall.method;

        switch (method) {
            
            case "init":
                //flutterRegistrar的textures方法獲取textureRegistry,textureRegistry是包含了當前Flutter應用所有SurfaceTexture的登記表
                //同時也可以建立一個新的surfaceTexture
                textureRegistry = flutterRegistrar.textures();
                surfaceTextureEntry = textureRegistry.createSurfaceTexture();
                //通過一個剛剛建立的SurfaceTexure作為引數建立一個Surface
                surface = new Surface(surfaceTextureEntry.surfaceTexture());
                Map<String, Long> reply = new HashMap<>();
                long textureId = surfaceTextureEntry.id();
                //textureId回傳給Flutter 用來建立Texure控制元件
                reply.put("textureId", textureId);
                result.success(reply);
                break;

            case "render":
                Canvas canvas = surface.lockCanvas(null);
                //這裡的canvas 寬和高只有1個畫素 因為surface得建立不是surfaceView拖管的,所以不能夠draw實際內容,但是仍然可以繪製背景色
                //int height = canvas.getHeight();
                //int width = canvas.getWidth();
                canvas.drawColor(Color.argb(255, random.nextInt(255), random.nextInt(255), random.nextInt(255)));
                surface.unlockCanvasAndPost(canvas);
                result.success(null);
                break;
                
            default:
                break;
        }
    }

    public static void registerWith(Registrar registrar) {
        final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter.io/SurfaceTest");
        channel.setMethodCallHandler(new MySurfaceTestPlugin(registrar));
    }


    private MySurfaceTestPlugin(Registrar registrar) {
        this.flutterRegistrar = registrar;
        random = new Random();

    }


}


複製程式碼

可以看到通過FlutterSdk提供的方法獲取了一個SurfaceTexure,通過SurfaceTexure我們new了一個Surface,官方文件對於Surface有如下描述:

SurfaceTexture物件中建立的Surface類物件可被用作下述類的輸出目標: android.hardware.camera2, MediaCodec, MediaPlayer, 和 Allocation APIs。

也就是說Surface可以直接交由MediaPlayer、或者更低一級的MediaCodec作為輸出物件使用。通過**new Surface(SurfaceTexture texture) 方式建立的Surface.LockCanvas(DirtyRect rect)**獲取到的畫布只有1px,顯然私自建立的Surface官方意圖並非讓我們拿來繪製,而是交由MediaCodec, MediaPlayer輸出,但我們仍然可以drawColor背景色來確認我們的java層與dart建立了關聯。

接下來我們註冊該外掛

MySurfaceTestPlugin.registerWith(getFlutterView().getPluginRegistry().registrarFor("flutter.io/SurfaceTest"));
複製程式碼

註冊完畢我們就可以在Dart中使用該外掛了,我們建立一個簡單的DartApp 上面是通過java層渲染過的"播放視窗",下面是一個控制渲染的按鈕。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

main() async {
  await _initVideoPlugin();
  runApp(new MyApp());
}

final MethodChannel _channel = const MethodChannel('flutter.io/SurfaceTest');
int _textureId;

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Test',
      home: new Scaffold(
          body: Column(
        children: <Widget>[
          AspectRatio(child: new VideoView(), aspectRatio: 16 / 9),
          Expanded(
              child: Center(
                  child: new FlatButton(
                      padding: EdgeInsets.only(
                          left: 25.0, right: 25.0, top: 15.0, bottom: 15.0),
                      onPressed: _render,
                      child: new Text("render"))))
        ],
      )
          ),
    );
  }
}

Future<void> _render() async {
  _channel.invokeMethod(
    'render',
    <String, dynamic>{},
  );
}

class VideoView extends StatefulWidget {
  @override
  State createState() {
    return new VideoState();
  }
}

_initVideoPlugin() async {
  final Map<dynamic, dynamic> response = await _channel.invokeMethod('init');
  _textureId = response['textureId'];
}

class VideoState extends State<VideoView> {
  @override
  Widget build(BuildContext context) {
	//這裡注意Texure是dart提供的控制元件 引數必須是java Plugin傳過來的texureId
    return new Texture(textureId: _textureId);
  }
}

複製程式碼

需要注意的是播放視窗實際上是一個Dart控制元件Texture,這個Texure就是Plugin裡通過flutterRegistrar.textures()獲取到的textureRegistry所管理的SurfaceTexture 一一對應的。執行看一下效果

使用Native(以Android為例)播放器構建Flutter播放外掛

可見由java層繪製背景已經可以渲染到螢幕上,接下來我們只要將Surface交於播放器即可實現視訊播放。如官方的Video_Player使用的ExoPlayer,我們也用ExoPlayer快速實現視訊播放。

case "init":
                //flutterRegistrar的textures方法獲取textureRegistry,textureRegistry是包含了當前Flutter應用所有SurfaceTexture的登記表
                //同時也可以建立一個新的surfaceTexture
                textureRegistry = flutterRegistrar.textures();
                surfaceTextureEntry = textureRegistry.createSurfaceTexture();
                //通過一個剛剛建立的SurfaceTexure作為引數建立一個Surface
                surface = new Surface(surfaceTextureEntry.surfaceTexture());
                initPlayer();
                Map<String, Long> reply = new HashMap<>();
                long textureId = surfaceTextureEntry.id();
                //textureId回傳給Flutter 用來建立Texure控制元件
                reply.put("textureId", textureId);
                result.success(reply);
                break;

            case "render":
//                Canvas canvas = surface.lockCanvas(null);
//                canvas.drawColor(Color.argb(255, random.nextInt(255), random.nextInt(255), random.nextInt(255)));
//                surface.unlockCanvasAndPost(canvas);
                exoPlayer.setPlayWhenReady(true);
                exoPlayer.setRepeatMode(REPEAT_MODE_ALL);
                result.success(null);
                break;
複製程式碼

這裡 ,我們在init中執行initPlayer() 其中將Surface直接交給播放器,render中執行播放

private void initPlayer() {
        DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(flutterRegistrar.activeContext(), "ExoPlayer");
        MediaSource mediaSource = new ExtractorMediaSource(Uri.parse("asset:///flutter_assets/assets/Butterfly.mp4"), dataSourceFactory, new DefaultExtractorsFactory(), null, null);
        exoPlayer = ExoPlayerFactory.newSimpleInstance(flutterRegistrar.activeContext(), new DefaultTrackSelector());
        exoPlayer.setVideoSurface(surface);
        exoPlayer.prepare(mediaSource);
    }
複製程式碼

ok 現在我們開下效果

使用Native(以Android為例)播放器構建Flutter播放外掛

參考:

source.android.com/devices/gra… blog.csdn.net/luoshengyan…

相關文章