视频播放基础控件与无缝技术介绍

视频播放基础控件与无缝技术介绍

概述

   前阵子经手了项目里 Feed 流短视频播放的工作,虽然技术上难度不算大,但实现还是花了不少功。花了点时间稍微总结了下相关的技术点,分享给有需要的人。

SurfaceView与TextureView区别

   目前视频画面帧的展示控件常用的有两种 SurfaceView 及TextureView ,这节简单的介绍 SurfaceView 与 TextureView 区别。

Android图形渲染

   Android 框架提供了各种用于 2D 和 3D 图形渲染的 API,可与制造商的图形驱动程序实现方法交互。

在这里插入图片描述
图1 Surface 如何被渲染

   无论开发者使用什么渲染 API,一切内容都会渲染到“Surface”。Surface 表示缓冲队列(BufferQueue)中的生产方,而缓冲队列通常会被 SurfaceFlinger 消耗。在 Android 平台上创建的每个窗口都由 Surface 提供支持。所有被渲染的可见 Surface 都被 SurfaceFlinger 合成到显示部分,也就是说 Surface 对象使应用能够渲染要在屏幕上显示的图像。

SurfaceView

   这小节通过 SurfaceView 概述、SurfaceView 双缓冲区、SurfaceView 使用、SurfaceView 优缺点等知识点来了解 SurfaceView。

SurfaceView概述

   SurfaceView 是一个组件,其继承自View,所以它本质也是一个 View 组件,可用于在 View 层次结构中嵌入其他合成层。SurfaceView 采用与其他 View 相同的布局参数,因此可以像对待其他任何 View 一样对其进行操作,但 SurfaceView 的内容是透明的。

   但与普通 View 不同的是,它有自己的 Surface,在 WMS 中有对应的 WindowState,在 SurfaceFlinger 中有 Layer, 如下图。

在这里插入图片描述

   也正是如此,虽然在 App 端它仍在 View Hierachy 中,但在 Server 端(WMS 和 SF)中,它与宿主窗口是分离的。这样的好处是对这个 Surface的渲染可以放到单独线程去做,渲染时可以有自己的 GL Context,所以它不会影响主线程对事件的响应。

   当有画面请求时,SurfaceView 通过调用 SurfaceHolder 接口(内部封装了获取Surface的接口)将这块图形绘冲区的 UI 数据提交给 SurfaceFlinger 服务来处理,SurfaceFlinger 服务可以在合适的时候将该图形缓冲区合成到屏幕上去显示,这样就可以将对应的 SurfaceView 的 UI 展现出来了。

SurfaceView双缓冲区

   SurfaceView 在更新视图时用到了两张 Canvas对应于两个 Surface,一张 frontCanvas 和一张 backCanvas,每次实际显示的是 frontCanvas,backCanvas 存储的是上一次更改前的视图,当使用 lockCanvas() 获取画布时,得到的实际上是 backCanvas 而不是正在显示的 frontCanvas,之后你在获取到的 backCanvas 上绘制新视图,再 unlockCanvasAndPost(Canvas) 此视图,那么上传的这张 Canvas将替换原来的 frontCanvas 作为新的 frontCanvas,原来的 frontCanvas 将切换到后台作为 backCanvas。例如,如果你已经先后两次绘制了视图 A 和 B,那么你再调用 lockCanvas() 获取视图,获得的将是 A 而不是正在显示的 B,之后你将重绘的 C 视图上传,那么 C 将取代 B 作为新的 frontCanvas 显示在 SurfaceView 上,原来的 B 则转换为 backCanvas。

SurfaceView优缺点
  • 优点: 使用双缓冲机制,可以在一个独立的线程中进行绘制,不会影响主线程,播放视频时画面更流畅。
  • 缺点:Surface 不在 View hierachy中,它的显示也不受 View 的属性控制,SurfaceView 不能嵌套使用。在 7.0 版本之前不能进行平移,缩放等变换,也不能放在其它 ViewGroup 中,在 7.0 版本之后可以进行平移,缩放等变换。
SurfaceView使用

   一般为了扩展性,会写一个子类继承SurfaceView,这样有更好的扩展性。SurfaceView 的使用与普通的 View 最大区别是,需要在设置 SurfaceHolder.Callback 来监听其创建、销毁、状态改变,以便在一些场景下恢复画面,而且视频画面通过回调的 SurfaceHolder来绑定指定的MediaPlayer,以下是使用示例。

    ExtentSurfaceView surfaceView = new ExtentSurfaceView(context);
    LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
            LayoutParams.MATCH_PARENT);
    params.gravity = Gravity.CENTER;
    surfaceView.setLayoutParams(params);
    rootView.addView(surfaceView);
    
    public class ExtentSurfaceView extends SurfaceView {

        public ExtentSurfaceView(Context context, AttributeSet attrs,
                                int defStyle) {
            super(context, attrs, defStyle);
            init();
        }

        public ExtentSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }

        public ExtentSurfaceView(Context context) {
            super(context);
            init();
        }

        private void init() {
            getHolder().addCallback(mSHCallback);
        }

        private SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() {
            public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
                Log.i(TAG, "surfaceChanged!!!");
            }

            public void surfaceCreated(SurfaceHolder holder) {
                Log.i(TAG, "surfaceCreated");
                // 设置SurfaceHolder,绑定mediaplayer
                mMediaPlayer.setDisplay(holder);
            }

            public void surfaceDestroyed(SurfaceHolder holder) {
                Log.i(TAG, "surface destroyed!!!");
            }
        };
    }

TextureVeiw

   这小节通过 TextureView 概述、TextureView 使用、TextureView 优缺点等知识点来了解 TextureView。

TextureView概述

   在 4.0(API level 14) 中引入,与 SurfaceView 一样继承 View,但它结合了 View 与 SurfaceTexture,所以它可以将内容流直接投影到 View 中,TextureView 重载了 draw() 方法,其中主要工作是 SurfaceTexture 中收到的图像数据作为纹理更新到对应的 HardwareLayer 中。

   与 SurfaceView 不同,它不会在 WMS 中单独创建窗口,而是作为 View hierachy 中的一个普通 View,因此可以和其它普通 View 一样进行移动,旋转,缩放,动画等变化。值得注意的是 TextureView 必须在硬件加速的窗口中。

TextureView优缺点
  • 优点:支持移动、旋转、缩放等动画,支持截图。
  • 缺点:必须在硬件加速的窗口中使用,占用内存比 SurfaceView 高,在 5.0 以前在主线程渲染,5.0 以后有单独的渲染线程,性能相比 SurfaceView 较差些。
TextureView使用

   一般为了扩展性,会写一个子类继承TextureView,这样有更好的扩展性。TextureView 的使用与普通的 View 最大区别是,需要在设置 TextureView.SurfaceTextureListener 来监听其内部 SurfaceTexture 创建、销毁、状态改变,并且根据回调回来的 SurfaceTexture 创建 Surface 与指定的MediaPlayer绑定,以下是使用示例。

        TextureView textureView = new TextureView(getContext());
        textureView.setSurfaceTextureListener(mSurfaceTextureListener);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        params.gravity = Gravity.CENTER;
        addView(textureView, params);
        
    TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureAvailable" + " width = " + width + " height = " + height);
            Surface surface = new Surface(surfaceTexture);
            //创建surface并绑定mediaplayer
            mMediaPlayer.setSurface(surface);
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureSizeChanged" + " width = " + width + " height = " + height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
            LogUtils.d(TAG, "onSurfaceTextureDestroyed");
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surface) {
            // do nothing
        }
    };

如何选择?

谷歌官方:在 API 24 及更高版本中,建议实现 SurfaceView 而不是 TextureView。

   根据谷歌官方的建议,api 24及以上的版本使用 SurfaceView 而在其他版本的话可以根据二者的特性选择对应的控件。下表为二者正常情况下一些参数的对比情况(仅供参考)。

在这里插入图片描述

PlayerBase框架

概述

   PlayerBase是一种将解码器和播放视图组件化处理的解决方案框架。您需要什么解码器实现框架定义的抽象引入即可,对于视图,无论是播放器内的控制视图还是业务视图,均可以做到组件化处理。将播放器的开发变得清晰简单,更利于产品的迭代。

   对于PlayerBase其介绍文档已经很详细了,这里不在赘述,有兴趣的可以直接看PlayerBase介绍

无缝播放技术介绍

概述

   先上几个图。

无缝转场播放
在这里插入图片描述

列表无缝旋转播放

在这里插入图片描述

   从以上图片的演示可以看出,我们所说的无缝播放技术,其实就是利用 Android 中的动画实现特定的播放效果,但同时保证播放不中断的能力。

以无缝转场播放为例

   关于Activity等共享元素过渡动画的介绍,谷歌官方文档上已经做了比较详细的介绍,直接看共享元素过渡动画

   在 PlayerBase 中,过渡动画实现如下:

Intent intent = new Intent(this, ShareAnimationActivityB.class);
intent.putExtra(ShareAnimationActivityB.KEY_DATA, mData);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
    // 过渡动画,mLayoutContainer表示共享元素
    ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
            this, mLayoutContainer, "videoShare");
    ActivityCompat.startActivity(this, intent, options.toBundle());
}else{
    startActivity(intent);
}

public class ShareAnimationActivityB extends AppCompatActivity {

    public static final String KEY_DATA = "data_source";

    @BindView(R.id.top_container)
    RelativeLayout mTopContainer;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share_animation_b);
        ButterKnife.bind(this);

        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        DataSource dataSource = (DataSource) getIntent().getSerializableExtra(KEY_DATA);

        DataSource useData = null;
        DataSource playData = ShareAnimationPlayer.get().getDataSource();
        boolean dataChange = playData!=null && !playData.getData().equals(dataSource.getData());
        if(!ShareAnimationPlayer.get().isInPlaybackState() || dataChange){
            useData = dataSource;
        }
       //  动画结束后,将播放器绑定到新界面上的viewgroup,实现播放。
        ShareAnimationPlayer.get().play(mTopContainer, useData);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ShareAnimationPlayer.get().destroy();
    }
}


   看完过渡动画,那是如何保证播放的不中断呢?我们一起来看看。

   以 TextureView 为例。在前面,我们介绍过,TextureView 其内部是通过封装 SurfaceTexture 来实现内容呈现的。下面我们再看看其监听方法。

    private SurfaceTexture mOldSurfaceTexture;
    private TextureView vTextureView;

    TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            // 当TextureView的画面出现时会回调
            LogUtils.d(TAG, "onSurfaceTextureAvailable" + " width = " + width + " height = " + height);
            if (mOldSurfaceTexture == null) {
                mOldSurfaceTexture = surfaceTexture;
                Surface surface = new Surface(mOldSurfaceTexture);
                //创建surface并绑定mediaplayer
                mMediaPlayer.setSurface(surface);
            } else {
                vTextureView.setSurfaceTexture(mOldSurfaceTexture);
                // 开始播放
                mMediaPlayer.start();
            }

        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
            LogUtils.d(TAG, "onSurfaceTextureSizeChanged" + " width = " + width + " height = " + height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
            LogUtils.d(TAG, "onSurfaceTextureDestroyed");
            // 当TextureView的画面丢失时会回调,此时保存原来的画面
            mOldSurfaceTexture = surface;
            return false;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surface) {
            // do nothing
        }
    };

   根据上面的注释说明,首次绑定 Mediaplayer 时,会将 SurfaceTexture 创建的 Surface 与其绑定,这样就能正常展示播放内容。而在进行转场过渡页面时,播放器会重新绑定到新的父 View 上。此时 SurfaceTexture 会回调 onSurfaceTextureDestroyed 并且把与当前画面绑定的 SurfaceTexture 回调出来,而再重新绑定到父 View 后又会回调 onSurfaceTextureAvailable。

   正是利用这个实现,PlayerBase 将上一个画面的 SurfaceTexture 保存,再下一次 onSurfaceTextureAvailable 时重新设置给了 TextureView, 而此时 Mediaplayer 只是暂停而没有释放,重新启动播放就可以实现无缝衔接播放了。

   总结起来,就是不同的渲染视图使用同一个解码实例即可。可以简单比作一个 MediaPlayer 去不断设置不同的 Surface 呈现播放,但这个过程如果自己实现就比较复杂。PlayerBase 中的 RelationAssist 就是为了简化这个过程而设计的,在不同的页面或视图切换播放时,只需要提供并传入对应位置的视图容器(ViewGroup类型)即可。内部复杂的设置项和关联由 RelationAssist 完成。

几个无缝转场实现方案

Activity

   PlayerBase 中的无缝转场是使用的 Activity 的实现方式,该方法实现较为方便,但存在如下问题:

  1. 受限于Activity生命周期,可能会有耗时影响;
  2. 共享动画能力支持弱;
  3. 堆栈管理弱。
Fragment

   爱奇艺,好看等视频软件的列表播放是使用 Fragment 来实现共享元素跳转,Fragment的实现成本可能相对较高,比Activity灵活,但动画能力也比较弱。

View

   西瓜视频的实现是通过 View 来实现的,他们提供了一个开源的页面导航和组合框架,方法实现这种复杂的过渡动画等,有兴趣的可以去看看相关文档。

参考文献

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值