Chromium为视频标签<video>全屏播放的过程分析

Android社区 收藏文章

在Chromium中,

从前面Chromium为视频标签一文可以知道,在Android平台上,

Surface有两种获取方式。第一种方式是通过SurfaceTexture构造一个新的Surface。第二种方式是从SurfaceView内部获得。在非全屏模式下,Chromium就是通过第一种方式构造一个Surface,然后设置给MediaPlayer的。在全屏模式下,Chromium将会直接创建一个全屏的SurfaceView,然后再从这个SurfaceView内部获得一个Surface,并且设置给MediaPlayer。

在Android平台上,SurfaceView的本质是一个窗口。既然是窗口,那么它的UI就是由系统(SurfaceFlinger)合成在屏幕上显示的。它的UI就来源于它内部的Surface描述的GPU缓冲区队列。因此,当MediaPlayer将解码出来的视频画面写入到SurfaceView内部的Surface描述的GPU缓冲区队列去时,SurfaceFlinger就会从该GPU缓冲区队列中将新写入的视频画面提取出来,并且合成在屏幕上显示。关于SurfaceView的更多知识,可以参考前面Android视图SurfaceView的实现原理分析一文。

Surface描述的GPU缓冲区队列,是一个生产者/消息者模型。在我们这个情景中,生产者便是MediaPlayer。如果Surface是通过SurfaceTexture构造的,那么SurfaceTexture的所有者,也就是Chromium,就是消费者。消费者有责任将视频画面从GPU缓冲区队列中提取出来,并且进行渲染。渲染完成后,再交给SurfaceFlinger合成显示在屏幕中。如果Surface是从SurfaceView内部获取的,那么SurfaceView就是消费者,然后再交给SurfaceFlinger合成显示在屏幕中。

简单来说,在非全屏模式下,

Chromium支持

图1

接下来,我们就结合源代码,从

void HTMLMediaElement::enterFullscreen()
    {
        WTF_LOG(Media, "HTMLMediaElement::enterFullscreen");

        FullscreenElementStack::from(document()).requestFullScreenForElement(this, 0, FullscreenElementStack::ExemptIFrameAllowFullScreenRequirement);
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

在WebKit中,网页的每一个标签都可以进入全屏模式。每一个网页都对应有一个FullscreenElementStack对象。这个FullscreenElementStack对象内部有一个栈,用来记录它对应的网页有哪些标签进入了全屏模式。

HTMLMediaElement类的成员函数enterFullscreen首先调用成员函数document获得当前正在处理的

FullscreenElementStack类的成员函数requestFullScreenForElement的实现如下所示:

void FullscreenElementStack::requestFullScreenForElement(Element* element, unsigned short flags, FullScreenCheckType checkType)
    {
        ......

        // The Mozilla Full Screen API <https://wiki.mozilla.org/Gecko:FullScreenAPI> has different requirements
        // for full screen mode, and do not have the concept of a full screen element stack.
        bool inLegacyMozillaMode = (flags & Element::LEGACY_MOZILLA_REQUEST);

        do {
            ......

            // 1. If any of the following conditions are true, terminate these steps and queue a task to fire
            // an event named fullscreenerror with its bubbles attribute set to true on the context object's
            // node document:
            ......

            // The context object's node document fullscreen element stack is not empty and its top element
            // is not an ancestor of the context object. (NOTE: Ignore this requirement if the request was
            // made via the legacy Mozilla-style API.)
            if (!m_fullScreenElementStack.isEmpty() && !inLegacyMozillaMode) {
                Element* lastElementOnStack = m_fullScreenElementStack.last().get();
                if (lastElementOnStack == element || !lastElementOnStack->contains(element))
                    break;
            }

            // A descendant browsing context's document has a non-empty fullscreen element stack.
            bool descendentHasNonEmptyStack = false;
            for (Frame* descendant = document()->frame() ? document()->frame()->tree().traverseNext() : 0; descendant; descendant = descendant->tree().traverseNext()) {
                ......
                if (fullscreenElementFrom(*toLocalFrame(descendant)->document())) {
                    descendentHasNonEmptyStack = true;
                    break;
                }
            }
            if (descendentHasNonEmptyStack && !inLegacyMozillaMode)
                break;

            ......

            // 2. Let doc be element's node document. (i.e. "this")
            Document* currentDoc = document();

            // 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
            Deque<Document*> docs;

            do {
                docs.prepend(currentDoc);
                currentDoc = currentDoc->ownerElement() ? ¤tDoc->ownerElement()->document() : 0;
            } while (currentDoc);

            // 4. For each document in docs, run these substeps:
            Deque<Document*>::iterator current = docs.begin(), following = docs.begin();

            do {
                ++following;

                // 1. Let following document be the document after document in docs, or null if there is no
                // such document.
                Document* currentDoc = *current;
                Document* followingDoc = following != docs.end() ? *following : 0;

                // 2. If following document is null, push context object on document's fullscreen element
                // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
                // set to true on the document.
                if (!followingDoc) {
                    from(*currentDoc).pushFullscreenElementStack(element);
                    addDocumentToFullScreenChangeEventQueue(currentDoc);
                    continue;
                }

                // 3. Otherwise, if document's fullscreen element stack is either empty or its top element
                // is not following document's browsing context container,
                Element* topElement = fullscreenElementFrom(*currentDoc);
                if (!topElement || topElement != followingDoc->ownerElement()) {
                    // ...push following document's browsing context container on document's fullscreen element
                    // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
                    // set to true on document.
                    from(*currentDoc).pushFullscreenElementStack(followingDoc->ownerElement());
                    addDocumentToFullScreenChangeEventQueue(currentDoc);
                    continue;
                }

                // 4. Otherwise, do nothing for this document. It stays the same.
            } while (++current != docs.end());

            // 5. Return, and run the remaining steps asynchronously.
            // 6. Optionally, perform some animation.
            ......
            document()->frameHost()->chrome().client().enterFullScreenForElement(element);

            // 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
            return;
        } while (0);

        ......
    }

这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/dom/FullscreenElementStack.cpp中。

FullscreenElementStack类的成员函数requestFullScreenForElement主要是用来为网页中的每一个Document建立一个Stack。这个Stack记录了Document中所有请求设置为全屏模式的标签。我们通过图2所示的例子说明FullscreenElementStack类的成员函数requestFullScreenForElement的实现:

图2 Fullscreen Stack for Document

图2所示的网页包含了两个Document:Doc1和Doc2。其中,Doc1通过