38

小窗播放视频的原理和实现(上)

 6 years ago
source link: http://mp.weixin.qq.com/s/W3fS3SeWbvGnRYYn276bFQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

小窗播放视频的原理和实现(上)

Original larryycliu 腾讯音乐技术团队 2017-12-29 07:11 Posted on

本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移动、缩放等效果,很有技术深度。文中几种方案的对比,以及SurfaceViewGLSurfaceViewTextureView相关知识点的讲解,非常实用,值得收藏。

— 责任编辑 junyihan

由于文章篇幅较长,将分为上、下两篇。上篇主要介绍小窗播放视频的原理,下篇主要介绍小窗播放视频的实现。

目前很多视频类App都有小窗播放功能,比如Youtube(如图1)、Facebook(如图2)等,不过它们的实现方式却不同。Youtube 是将视频播放View内嵌到应用内,优点是交互好;Facebook则是通过WindowManager添加视频播放View,同时支持应用内部和外部播放。

小窗播放视频功能在小窗和大屏之间切换时,视频类App通常一边执行交互动作一边播放视频。交互动作包括移动、缩放或者动画;这些App在播放时期望给用户平滑的过渡体验,流畅加载视频,不能有明显的卡顿。

Image
Image
(图1 Youtube小窗播放视频)(图2 Facebook小窗播放视频)

二、SurfaceView 和 GLSurfaceView

Android 中使用 MediaPlayer 播放视频时,一般采用SurfaceViewGLSurfaceViewTextureView。Youtube、Facebook用不同技术方案实现了小窗播放视频功能,它们共同点是都可以使用SurfaceViewGLSurfaceViewTextureView来播放视频。接下来分析三个视图用于小窗播放视频的原理。

SurfaceView继承自类View,但与其他View的子类不同的是它有独立的Surface,如下源码可以看出它包含一个Surface属性,即它不与宿主窗口共享同一个绘图表面。因此SurfaceView的UI可以在一个独立的线程中进行绘制。由于不会占用主线程资源,因此SurfaceView可以实现复杂而高效的UI。GLSurfaceView继承SurfaceView,作为SurfaceView的补充,加入了EGL的管理,并自带了渲染线程。它用于小窗播放时效果和SurfaceView类似。本文以下内容以SurfaceView为例分析独立的Surface对小窗播放会有什么影响。

public class SurfaceView extends View {   
    final Surface mSurface = new Surface();   }

2.1、什么是Surface

Handle onto a raw buffer that is being managed by the screen compositor.[1]

通过Surface的类注释可以知道,Surface处理一块由Screen compositor管理的Raw buffer。而Screen compositor其实就是SurfaceFlinger服务。Surface字面意思就是绘图表面,可以理解为是UI的画布。

Android应用程序窗口需要请求SurfaceFlinger服务创建绘图表面(也就是Surface对象),同时窗口还需要被WindowManagerService管理;所以实际上窗口的绘图表面是通过两个Surface对象来描述,一个是应用程序进程创建的,另一个是由WindowManagerService创建的,这两个Surface对象对应于SurfaceFlinger服务的同一个Layer对象。

在应用程序进程这一侧,每一个应用程序窗口,如Activity,都有一个Surface对象,就是在ViewRootImpl对象的mSurface属性,这个Surface用来绘制应用程序窗口的UI,如下ViewRootImpl源码所示。当界面需要刷新时,窗口调用draw方法,向Surface请求canvas,执行绘画操作后再次提交给Surface完成屏幕显示。

在WindowManagerService服务这一侧,每一个窗口,都有一个对应的WindowState对象。其有一个属性mSurface,它负责设置窗口的位置、大小属性。例如,一个窗口的Z轴坐标大小要考虑到它的窗口类型,以及它与系统中的其它窗口的关系[2]。

public final class ViewRootImpl {
    private final Surface mSurface = new Surface();
    private void draw(boolean fullRedrawNeeded) {
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        }
    }
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo,……) {
        inal Canvas canvas;
        canvas = mSurface.lockCanvas(dirty);
        surface.unlockCanvasAndPost(canvas);
    }
}

SurfaceView有独立的绘图表面,那么SurfaceView又是怎么绘制在宿主窗口的呢?

2.2、SurfaceView的Surface的创建过程

上面说到每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。同时每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面[3]。

Image

(图3 SurfaceView的Surface的创建过程)

接下来了解Surface创建过程,如图3的时序图所示,每当一个窗口需要刷新UI时,就会调用ViewRootImpl类的performTraversals方法。如果当前窗口的Surface还没有创建,或者已经失效,SurfaceView就会请求WindowManagerService服务创建一个新的Surface,它最终会调用updateWindow来完成Surface的创建。

public class SurfaceView extends View {
    final Surface mSurface = new Surface();
    MyWindow mWindow;
    int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;

    private void updateWindow(boolean force, boolean redrawNeeded) {
        if (mWindow == null) {
            mSession.addWithoutInputChannel(mWindow, mLayout,……);
        }
        ……
        mSurfaceLock.lock();
        try {
            ……
            final int relayoutResult = mSession.relayout(mWindow, mLayout,……, mSurface);
            ……
        } finally {
            mSurfaceLock.unlock();
    }
}

如SurfaceView源码所示,它有一个mSurface属性,相比之下TextView或者Button等普通View会共用ViewRootImpl的Surface。

SurfaceView类的属性mWindowType描述的是SurfaceView的窗口类型,它的默认值等于TYPE_APPLICATION_MEDIA,用来显示多媒体的,如视频。SurfaceView还有另外一个类型TYPE_APPLICATION_MEDIA_OVERLAY,它是在视频上面显示Overlay的,它可以显示视字幕等信息。宿主窗口会遮挡这两个类型的SurfaceView,如果窗口嵌入这两类SurfaceView,那么它们的Z轴位置会低于该窗口的Z轴位置,显示在该窗口下面。如果mWindow等于null的话,那么就说明该SurfaceView还没有添加到WindowManagerService服务中去,然后调用addWithoutInputChannel添加到WindowManagerService服务中。

mSession.relayout()请求WindowManagerService服务对SurfaceView的UI进行布局。如果宿主窗口的绘制表面还未创建,或者需要重新创建,那么就会请求SurfaceFlinger服务为它创建一个新的Surface。由于这一步可能会修改SurfaceView的Surface,所以添加了mSurfaceLock锁,避免其它线程同时修改该Surface的内容。

执行完成上述步骤之后,SurfaceView的Surface的创建完成了。但是mWindowType为TYPE_APPLICATION_MEDIATYPE_APPLICATION_MEDIA_OVERLAY的SurfaceView会被宿主窗口挡住,如何解决这个问题,这就要了解SurfaceView“挖洞”原理。

2.3、SurfaceView“挖洞”原理

当SurfaceView附加宿主窗口时,它的onAttachedToWindow会被调用。这个方法调用requestTransparentRegion请求在宿主窗口上设置透明区域,即请求在宿主窗口上挖洞,其实就是设置ViewRootImpl中Surface的透明度。而每当其宿主窗口刷新自己的UI的时候,就会调用ViewGroup的gatherTransparentRegion将所有嵌入在它里面的SurfaceView所设置的透明区域收集起来。然后再通知WindowManagerService为SurfaceView的gatherTransparentRegion方法设置一个总的透明区域。这就是SurfaceViewd的“挖洞”原理,如图4时序图所示。

Image

(图4 SurfaceView“挖洞”原理)

接下来结合ViewRootImpl类的requestTransparentRegion源码,来分析请求在宿主窗口上设置透明区域的过程。

public final class ViewRootImpl implements ……{    
   public void requestTransparentRegion(View child) {      
    if (mView == child) {            mView.mPrivateFlags |= View.REQUEST_TRANSPARENT_REGIONS;            mWindowAttributesChanged = true;            requestLayout();        }    } }

当mView等于子View时,将mPrivateFlags的View.REQUEST_TRANSPARENT_REGIONS位设置为1,表示该窗口被设置了一块透明区域。当一个窗口被请求设置了一块透明区域之后,它的窗口属性就发生了变化,这时候除了要将与它所关联的ViewRootImpl对象的mWindowAttributesChanged值设置为true之外,还要调用该ViewRootImpl对象的requestLayout方法对窗口的UI进行重新布局和绘制。requestLayout最终会调用到另外一个方法performTraversals来实际执行刷新窗口UI的操作。

public final class ViewRootImpl implements ……{
    private void performTraversals() {
        final View host = mView;
        if (didLayout) {
            host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
            if (……);
                host.gatherTransparentRegion(mTransparentRegion);
                if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                    mPreviousTransparentRegion.set(mTransparentRegion);
                    try {
                        sWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
                    } catch (RemoteException e) {
                    }
                }
            }
        }
        boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();
        if (!cancelDraw && !newSurface) {
            draw(fullRedrawNeeded);
        } 
    }
}

ViewRootImpl类的方法performTraversals是用来收集嵌入在它里面的SurfaceView所设置的透明区域的。它处于窗口的UI布局完成之后,窗口的UI绘制之前。这是因为窗口的UI布局完成之后,各个子视图的大小和位置才能确定下来,进而才能确定SurfaceView的透明区域的位置和大小。从顶层视图开始,从上到下收集每一个子视图所要设置的区域,最终收集到的总透明区域并保存在ViewRootImpl类的成员变量mTransparentRegion中。其中host是DecorView,它的gatherTransparentRegion方法重载了父类ViewGroup的gatherTransparentRegion方法。

public abstract class ViewGroup extends View …… {
    @Override
    public boolean gatherTransparentRegion(Region region) {
        //检测mPrivateFlags的REQUEST_TRANSPARENT_REGIONS位是否为1,因为如果当前视图不透明,子视图都不可能设置有透明区域。
        final boolean meOpaque = (mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) == 0;
        if (meOpaque && region == null) {
            return true;
        }
        super.gatherTransparentRegion(region);
        ……
        boolean noneOfTheChildrenAreTransparent = true;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK)==VISIBLE||child.getAnimation() != null){
                if (!child.gatherTransparentRegion(region)) {
                    noneOfTheChildrenAreTransparent = false;
                }
            }
        }
        return meOpaque || noneOfTheChildrenAreTransparent;
    }
}

ViewGroup的gatherTransparentRegion方法中,检测到有透明区域时,调用父类View的方法gatherTransparentRegion来检查当前视图容器是否需要绘制。如果需要绘制,说明当前视图的前景需要绘制,就会将它所占据的区域从参数region所占据的区域移除,以便可以显示当前视图的前景。然后调用每一个子视图的成员函数gatherTransparentRegion来继续往下收集透明区域。

public class SurfaceView extends View {
    @Override
    public boolean gatherTransparentRegion(Region region) {
        if (mWindowType == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
            return super.gatherTransparentRegion(region);
        }
        boolean opaque = true;
        if ((mPrivateFlags & SKIP_DRAW) == 0) {
            // this view draws, remove it from the transparent region
            opaque = super.gatherTransparentRegion(region);
        } else if (region != null) {
            int w = getWidth();
            int h = getHeight();
            if (w>0 && h>0) {
                getLocationInWindow(mLocation);
                int l = mLocation[0];
                int t = mLocation[1];
                region.op(l, t, l+w, t+h, Region.Op.UNION);
            }
        }
        if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
            opaque = false;
        }
        return opaque;
    }
    ……
}

SurfaceView类的方法gatherTransparentRegion中,先检测是否用作窗口面板以及mPrivateFlags的SKIP_DRAW位是不是1。如果都是,将它所占据的区域从参数region所描述的区域移除,region中剩下的就是透明区域。最后判断Surface的像素格式是否设置有透明值。如果有,返回false给ViewRootImpl,然后ViewRootImpl调用sWindowSession.setTransparentRegion(mWindow, mTransparentRegion);设置窗口为透明。这样就能看到SurfaceView了。

2.4、SurfaceView的绘制

SurfaceView虽然具有独立的Surface,不过它仍然是宿主窗口的视图结构中的一个结点,因此,它仍然是可以参与到宿主窗口的绘制流程中去的。

public class SurfaceView extends View {
    @Override
    public void draw(Canvas canvas) {
        if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
            // draw() is not called when SKIP_DRAW is set
            if ((mPrivateFlags & SKIP_DRAW) == 0) {
                // punch a whole in the view-hierarchy below us
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
        }
        super.draw(canvas);
    }
    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
            // if SKIP_DRAW is cleared, draw() has already punched a hole
            if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
                // punch a whole in the view-hierarchy below us
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
        }
        // reposition ourselves where the surface is 
        mHaveFrame = true;
        updateWindow(false, false);
        super.dispatchDraw(canvas);
    }
}

在SurfaceView的draw和dispatchDraw方法中,参数canvas是建立在宿主窗口的Surface上的画布,因此在这块画布上绘制任何UI都是出现在宿主窗口的Surface上的。但如果当前正在处理的SurfaceView不是用作宿主窗口面板的时候,即属性mWindowType的值不等于TYPE_APPLICATION_PANEL的时候,SurfaceView的这两个方法只是简单地将它所占据的区域绘制为黑色。另外dispatchDraw还会调用另外一个方法updateWindow更新的UI,绘制自己的Sueface。

2.5、小结SurfaceView的分析

通过了解SurfaceView的创建过程、“挖洞”原理和绘制过程,可以了解到Surface对小窗播放视频的影响如下:

1)SurfaceView在宿主窗口下面,通过“挖洞”原理显示Surface。SurfaceView在做旋转时,画面不会跟随SurfaceView旋转。

2)同理,设置透明度或者执行透明值动画时,SurfaceView显示有问题。

3)SurfaceView绘制时会先绘制黑边,所以在移动或者缩放过程,在更新不及时时会看到黑边。

4)SurfaceView具有独立的Surface,它的UI绘制可以在独立的线程中进行,可以进行复杂的UI绘制。

三、Android N上SurfaceView新特性

Note: Starting in platform version N, SurfaceView’s window position is updated synchronously with other View rendering. This means that translating and scaling a SurfaceView on screen will not cause rendering artifacts. Such artifacts may occur on previous versions of the platform when its window is positioned asynchronously.[4]

由于SurfaceView不在View hierarchy中,View的一些缩放,透明度变化等方法无法使用。要实现这些功能就得使用TextureView,但TextureView有个缺点就是性能低耗电高。Android N对SurfaceView进行了更改,它对SurfaceView自身和它的内容改变做了同步处理,播放视频时不会出现之前难看的黑色条。SurfaceView因这个新特性不会出现黑色条,但它旋转时画面仍然不会跟随旋转,仍然不支持透明度。

四、TextureView

如下源码所示,TextureView继承于View,并重载了View的draw()方法,它与其它的View一样在View hierarchy中管理与绘制。draw()方法中主要把SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer中。SurfaceTexture.OnFrameAvailableListener用于通知TextureView有新数据。

public class TextureView extends View {
    private HardwareLayer mLayer;
    private SurfaceTexture mSurface;
    private SurfaceTextureListener mListener;
    private Canvas mCanvas;
    private long mNativeWindow;
      //……
      public final void draw(Canvas canvas) {
        // NOTE: Maintain this carefully (see View.java)
        mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        applyUpdate();
        applyTransformMatrix();
    }
    private void applyUpdate() {
        if (mLayer == null) {
            return;
        }
        synchronized (mLock) {
            if (mUpdateLayer) {
                mUpdateLayer = false;
            } else {
                return;
            }
        }
        mLayer.prepare(getWidth(), getHeight(), mOpaque);
        mLayer.updateSurfaceTexture();
        if (mListener != null) {
            mListener.onSurfaceTextureUpdated(mSurface);
        }
    }
}

根据以上信息,可以知道TextureView不同于SurfaceView,没有单独创建Surface,而是作为View hierarchy中的一个普通View,来进行移动,旋转,缩放,动画等,没有SurfaceView执行旋转、缩放时的缺点。值得注意的是TextureView必须在硬件加速的窗口中,通过HardwareLayer更新视图。它需要硬件加速层,这使得TextureView比SurfaceView更耗性能。 Android N上SurfaceView新特性的说明上,官方也推荐在不执行旋转、透明度、缩放时使用SurfaceView。

SurfaceView有独立的Surface,通过“挖洞”原理显示它。以致它在执行旋转时,画面不会跟随旋转;同时设置透明度或者执行透明值动画时,显示有问题。Android N以上的SurfaceView在视频进行缩放旋转时会同步变化,不会看到黑色边,官方推荐使用SurfaceView。TextureView作为普通View在View hierarchy中管理与绘制,更适用于小窗播放视频功能。但TextureView需要硬件加速层,使得TextureView比SurfaceView和GLSurfaceView更耗性能。

[1]、Surface(https://developer.android.com/reference/android/view/Surface.html)

[2]、Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析 (http://blog.csdn.net/luoshengyang/article/details/8303098)

[3]、Android视图SurfaceView的实现原理分析(http://blog.csdn.net/luoshengyang/article/details/8661317)

[4]、SurfaceView(https://developer.android.com/reference/android/view/SurfaceView.html)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK