首页 > 代码库 > Chromium为视频标签<video>全屏播放的过程分析
Chromium为视频标签<video>全屏播放的过程分析
在Chromium中,<video>标签有全屏和非全屏两种播放模式。在非全屏模式下,<video>标签播放的视频嵌入在网页中显示,也就是视频画面作为网页的一部分显示。在全屏模式下,我们是看不到网页其它内容的,因此<video>标签播放的视频可以在一个独立的全屏窗口中显示。这两种截然不同的播放模式,导致Chromium使用不同的方式渲染视频画面。本文接下来就详细分析<video>标签全屏播放的过程。
从前面Chromium为视频标签<video>渲染视频画面的过程分析一文可以知道,在Android平台上,<video>标签指定的视频是通过系统提供的MediaPlayer进行播放的。MediaPlayer提供了一个setSurface接口,用来给MediaPlayer设置一个Surface。Surface内部有一个GPU缓冲区队列,以后MediaPlayer会将解码出来的视频画面写入到这个队列中去。
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合成显示在屏幕中。
简单来说,在非全屏模式下,<video>标签的视频画面经过MediaPlayer->Chromium->SurfaceFlinger显示在屏幕中,而在全屏模式下,经过MediaPlayer->SurfaceView->SurfaceFlinger显示在屏幕中。
Chromium支持<video>标签在全屏和非全屏模式之间无缝切换,也就是从一个模式切换到另外一个模式的时候,不需要重新创建MediaPlayer,只需要给原先使用的MediaPlayer设置一个新的Surface即可。图1描述的是<video>标签从非全屏模式切换为全屏模式的示意图,如下所示:
图1 <video>标签从非全屏模式切换为全屏模式
当<video>标签从非全屏模式切换为全屏模式时,Chromium会为它创建一个全屏的SurfaceView,并且将这个SurfaceView内部的Surface设置给MediaPlayer。以后MediaPlayer就不会再将解码出来的视频画面通过原先设置的SurfaceTexture交给Chromium处理,而是通过后面设置的Surface交给SurfaceView处理。
接下来,我们就结合源代码,从<video>标签进入全屏模式开始,分析<video>标签全屏播放视频的过程。从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,在WebKit中,<video>标签是通过HTMLMediaElement类描述的。当<video>标签进入全屏模式时,HTMLMediaElement类的成员函数enterFullscreen就会被调用,它的实现如下所示:
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获得当前正在处理的<video>标签所属的网页,然后再通过调用FullscreenElementStack类的静态成员函数from获得这个网页所对应的FullscreenElementStack对象。有了这个FullscreenElementStack对象之后,就可以调用它的成员函数requestFullScreenForElement请求将当前正在处理的<video>标签设置为全屏模式。
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通过<iframe>标签加载了Doc2,后者里面有一个<video>标签。当Doc2里面的<video>标签被设置为全屏模式时,Doc1的Stack会被压入一个<iframe>标签和一个<video>标签,其中,<iframe>标签代表的是Doc1,Doc2的Stack会被压入一个<video>标签。
注释中提到,Mozilla定义的Fullscreen API没有Fullscreen Element Stack的概念。没有Fullscreen Element Stack,意味着网页的标签在任意情况下都可以设置为全屏模式。不过,非Mozilla定义的Fullscreen API是要求Fullscreen Element Stack的。Fullscreen Element Stack用来限制一个标签是否可以设置为全屏模式:
1. 当Stack为空时,任意标签均可设置为全屏模式。
2. 当Stack非空时,栈顶标签的子标签才可以设置为全屏模式。
FullscreenElementStack类的成员函数requestFullScreenForElement就是根据上述逻辑判断参数element描述的标签是否可以设置为全屏模式的。如果可以,那么就会更新与它相关的Stack,并且在最后调用在WebKit Glue层创建一个WebViewImpl对象的成员函数enterFullScreenForElement,用来通知WebKit Glue层有一个标签要进入全屏模式。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。WebKit Glue层的作用,可以参考前面Chromium网页加载过程简要介绍和学习计划一文。
接下来,我们就继续分析WebViewImpl类的成员函数enterFullScreenForElement的实现,如下所示:
void WebViewImpl::enterFullScreenForElement(WebCore::Element* element) { m_fullscreenController->enterFullScreenForElement(element); }这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。
WebViewImpl类的成员变量m_fullscreenController指向的是一个FullscreenController对象。WebViewImpl类的成员函数enterFullScreenForElement调用这个FullscreenController对象的成员函数enterFullScreenForElement,用来通知它将参数element描述的标签设置为全屏模式。
FullscreenController类的成员函数enterFullScreenForElement的实现如下所示:
void FullscreenController::enterFullScreenForElement(WebCore::Element* element) { // We are already transitioning to fullscreen for a different element. if (m_provisionalFullScreenElement) { m_provisionalFullScreenElement = element; return; } // We are already in fullscreen mode. if (m_fullScreenFrame) { m_provisionalFullScreenElement = element; willEnterFullScreen(); didEnterFullScreen(); return; } // We need to transition to fullscreen mode. if (WebViewClient* client = m_webViewImpl->client()) { if (client->enterFullScreen()) m_provisionalFullScreenElement = element; } }这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。
如果参数element描述的标签所在的网页已经有标签处于全屏模式,那么FullscreenController类的成员变量m_provisionalFullScreenElement就会指向该标签,并且FullscreenController类的另外一个成员变量m_fullScreenFrame会指向一个LocalFrame对象。这个LocalFrame对象描述的就是处于全屏模式的标签所在的网页。
我们假设参数element描述的标签所在的网页还没有标签被设置为全屏模式。这时候FullscreenController类的成员函数enterFullScreenForElement会调用成员变量m_webViewImpl指向的一个WebViewImpl对象的成员函数client获得一个WebViewClient接口,然后再调用这个WebViewClient接口的成员函数enterFullScreen,用来通知它进入全屏模式。
上述WebViewClient接口是由WebKit的使用者设置的。在我们这个情景中,WebKit的使用者即Render进程中的Content模块,它设置的WebViewClient接口指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文,它描述的是当前正在处理的网页所加载在的一个RenderView控件。这一步实际上是通知Chromium的Content层进入全屏模式。
FullscreenController类的成员函数enterFullScreenForElement通知了Content层进入全屏模式之后,会将引发Content层进入全屏模式的标签记录在成员变量m_provisionalFullScreenElement中。在我们这个情景中,这个标签即为一个<video>标签。
接下来我们继续分析Chromium的Content层进入全屏模式的过程,也就是RenderViewImpl类的成员函数enterFullScreen的实现,如下所示:
bool RenderViewImpl::enterFullScreen() { Send(new ViewHostMsg_ToggleFullscreen(routing_id_, true)); return true; }这个函数定义在文件external/chromium_org/content/renderer/render_view_impl.cc。
RenderViewImpl类的成员函数enterFullScreen向Browser进程发送一个类型为ViewHostMsg_ToggleFullscreen的IPC消息,用来通知它将Routing ID为routing_id_的网页所加载在的Tab设置为全屏模式。
Browser进程通过RenderViewHostImpl类的成员函数OnMessageReceived接收类型为ViewHostMsg_ToggleFullscreen的IPC消息,如下所示:
bool RenderViewHostImpl::OnMessageReceived(const IPC::Message& msg) { ...... bool handled = true; IPC_BEGIN_MESSAGE_MAP(RenderViewHostImpl, msg) ...... IPC_MESSAGE_HANDLER(ViewHostMsg_ToggleFullscreen, OnToggleFullscreen) ...... IPC_MESSAGE_UNHANDLED( handled = RenderWidgetHostImpl::OnMessageReceived(msg)) IPC_END_MESSAGE_MAP() return handled; }这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。
RenderViewHostImpl类的成员函数OnMessageReceived将类型为ViewHostMsg_ToggleFullscreen的IPC消息分发给另外一个成员函数OnToggleFullscreen处理,如下所示:
void RenderViewHostImpl::OnToggleFullscreen(bool enter_fullscreen) { DCHECK_CURRENTLY_ON(BrowserThread::UI); delegate_->ToggleFullscreenMode(enter_fullscreen); // We need to notify the contents that its fullscreen state has changed. This // is done as part of the resize message. WasResized(); }这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_view_host_impl.cc中。
RenderViewHostImpl类的成员函数OnToggleFullscreen首先通过成员变量delegate_描述的一个RenderViewHostDelegate委托接口将浏览器窗口设置为全屏模式,然后再调用另外一个成员函数WasResized通知Render进程,浏览器窗口大小已经发生了变化,也就是它进入了全屏模式。
RenderViewHostImpl类的成员变量delegate_描述的RenderViewHostDelegate委托接口指向的是一个WebContentsImpl对象。这个WebContentsImpl对象是Content层提供给外界的一个接口,用来描述当前正在加载的一个网页。外界通过这个接口就可以访问当前正在加载的网页。
RenderViewHostImpl类的成员函数WasResized是从父类RenderWidgetHostImpl继承下来的,它的实现如下所示:
void RenderWidgetHostImpl::WasResized() { ...... is_fullscreen_ = IsFullscreen(); ...... ViewMsg_Resize_Params params; ...... params.is_fullscreen = is_fullscreen_; if (!Send(new ViewMsg_Resize(routing_id_, params))) { resize_ack_pending_ = false; } else { last_requested_size_ = new_size; } }这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_widget_host_impl.cc中。
RenderWidgetHostImpl类的成员函数WasResized主要是向Render进程发送一个类型为ViewMsg_Resize的IPC消息。这个ViewMsg_Resize的IPC消息。携带了一个is_fullscreen信息,用来告知Render进程当前正在处理的网页是否已经进入了全屏模式。
Render进程通过RenderWidget类的成员函数OnMessageReceived接收类型为ViewMsg_Resize的IPC消息,如下所示:
bool RenderWidget::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(RenderWidget, message) ...... IPC_MESSAGE_HANDLER(ViewMsg_Resize, OnResize) ...... IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; }这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
RenderWidget类的成员函数OnMessageReceived将类型为ViewMsg_Resize的IPC消息分发给另外一个成员函数OnResize处理,如下所示:
void RenderWidget::OnResize(const ViewMsg_Resize_Params& params) { ...... Resize(params.new_size, params.physical_backing_size, params.overdraw_bottom_height, params.visible_viewport_size, params.resizer_rect, params.is_fullscreen, SEND_RESIZE_ACK); ...... }这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
RenderWidget类的成员函数OnResize又调用另外一个成员函数Resize处理类型为ViewMsg_Resize的IPC消息,如下所示:
void RenderWidget::Resize(const gfx::Size& new_size, const gfx::Size& physical_backing_size, float overdraw_bottom_height, const gfx::Size& visible_viewport_size, const gfx::Rect& resizer_rect, bool is_fullscreen, ResizeAck resize_ack) { ...... if (compositor_) { compositor_->setViewportSize(new_size, physical_backing_size); ...... } ...... // NOTE: We may have entered fullscreen mode without changing our size. bool fullscreen_change = is_fullscreen_ != is_fullscreen; if (fullscreen_change) WillToggleFullscreen(); is_fullscreen_ = is_fullscreen; if (size_ != new_size) { size_ = new_size; // When resizing, we want to wait to paint before ACK‘ing the resize. This // ensures that we only resize as fast as we can paint. We only need to // send an ACK if we are resized to a non-empty rect. webwidget_->resize(new_size); } ...... if (fullscreen_change) DidToggleFullscreen(); ...... }这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
RenderWidget类的成员函数Resize一方面会通知网页的UI合成器,它负责渲染的网页的大小发生了变化,以便它修改网页UI的Viewport大小。这是通过调用成员变量compositor_指向的一个RenderWidgetCompositor对象的成员函数setViewportSize实现的。这个RenderWidgetCompositor对象的创建过程可以参考前面Chromium网页Layer Tree创建过程分析一文。
另一方面,RenderWidget类的成员函数Resize又会通知WebKit,它当前正在加载网页的大小发生了变化。这是通过调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数resize实现的。这个WebViewImpl对象的创建过程可以参考前面Chromium网页Frame Tree创建过程分析一文。
此外,RenderWidget类的成员函数Resize还会判断网页是否从全屏模式退出,或者进入全屏模式。如果是的话,那么就在通知WebKit修改网页的大小前后,RenderWidget类的成员函数Resize还会分别调用另外两个成员函数WillToggleFullscreen和DidToggleFullscreen,用来通知WebKit网页进入或者退出了全屏模式。
在我们这个情景中,网页是进入了全屏模式。接下来我们就先分析RenderWidget类的成员函数WillToggleFullscreen的实现,然后再分析另外一个成员函数DidToggleFullscreen的实现。
RenderWidget类的成员函数WillToggleFullscreen的实现如下所示:
void RenderWidget::WillToggleFullscreen() { ...... if (is_fullscreen_) { webwidget_->willExitFullScreen(); } else { webwidget_->willEnterFullScreen(); } }这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
从前面的分析可以知道,RenderWidget类的成员函数WillToggleFullscreen是在通知WebKit进入全屏模式之前被调用的。这时候RenderWidget类的成员变量is_fullscreen_的值为false。因此,接下来RenderWidget类的成员函数WillToggleFullscreen会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数willEnterFullScreen,用来通知WebKit它即将要进入全屏模式。
WebViewImpl类的成员函数willEnterFullScreen的实现如下所示:
void WebViewImpl::willEnterFullScreen() { m_fullscreenController->willEnterFullScreen(); }这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。
WebViewImpl类的成员函数willEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数willEnterFullScreen,用来通知WebKit即将要进入全屏模式,如下所示:
void FullscreenController::willEnterFullScreen() { if (!m_provisionalFullScreenElement) return; // Ensure that this element‘s document is still attached. Document& doc = m_provisionalFullScreenElement->document(); if (doc.frame()) { ...... m_fullScreenFrame = doc.frame(); } m_provisionalFullScreenElement.clear(); }这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。
从前面的分析可以知道,此时FullscreenController类的成员变量m_provisionalFullScreenElement的值不等于NULL,它指向了一个<video>标签。FullscreenController类的成员函数willEnterFullScreen找到这个<video>标签所在的Document,并且获得与这个Document关联的一个LocalFrame对象,保存在成员变量m_fullScreenFrame中,表示当前处于全屏模式的网页。
这一步执行完成后,回到前面分析的RenderWidget类的成员函数Resize,它在通知了WebKit修改当前正在加载的网页的大小之后,会调用另外一个成员函数DidToggleFullscreen,用来通知WebKit已经进入了全屏模式,如下所示:
void RenderWidget::DidToggleFullscreen() { ...... if (is_fullscreen_) { webwidget_->didEnterFullScreen(); } else { webwidget_->didExitFullScreen(); } }这个函数定义在文件external/chromium_org/content/renderer/render_widget.cc中。
从前面的分析可以知道,此时RenderWidget类的成员变量is_fullscreen_的值已经被设置为true。因此,RenderWidget类的成员函数DidToggleFullscreen接下来就会调用成员变量webwidget_指向的一个WebViewImpl对象的成员函数didEnterFullScreen,用来通知WebKit它已经进入全屏模式,如下所示:
void WebViewImpl::didEnterFullScreen() { m_fullscreenController->didEnterFullScreen(); }
这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebViewImpl.cpp中。
WebViewImpl类的成员函数didEnterFullScreen调用成员变量m_fullscreenController指向的一个FullscreenController对象的成员函数didEnterFullScreen,用来通知WebKit已经进入全屏模式,如下所示:
void FullscreenController::didEnterFullScreen() { if (!m_fullScreenFrame) return; if (Document* doc = m_fullScreenFrame->document()) { if (FullscreenElementStack::isFullScreen(*doc)) { ...... if (RuntimeEnabledFeatures::overlayFullscreenVideoEnabled()) { Element* element = FullscreenElementStack::currentFullScreenElementFrom(*doc); ASSERT(element); if (isHTMLMediaElement(*element)) { HTMLMediaElement* mediaElement = toHTMLMediaElement(element); if (mediaElement->webMediaPlayer() && mediaElement->webMediaPlayer()->canEnterFullscreen() // FIXME: There is no embedder-side handling in layout test mode. && !isRunningLayoutTest()) { mediaElement->webMediaPlayer()->enterFullscreen(); } ....... } } } } }这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/FullscreenController.cpp中。
从前面的分析可以知道,FullscreenController类的成员变量m_fullScreenFrame的值不等于NULL。它指向了一个LocalFrame对象。这个LocalFrame对象描述的就是当前被设置为全屏模式的标签所在的网页。通过这个LocalFrame对象可以获得一个Document对象。
有了这个Document对象之后,FullscreenController类的成员函数didEnterFullScreen就会检查它的Fullscreen Element Stack栈顶标签。如果这是一个<video>标签,并且已经为这个<video>标签创建过MediaPlayer接口,以及这个MediaPlayer接口允许进入全屏模式,那么FullscreenController类的成员函数didEnterFullScreen就会调用这个MediaPlayer接口的成员函数enenterFullScreen,让其进入全屏模式。
从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebKit为<video>标签创建的MediaPlayer接口指向的是一个WebMediaPlayerAndroid对象。因此,FullscreenController类的成员函数didEnterFullScreen调用的是WebMediaPlayerAndroid类的成员函数enenterFullScreen让<video>标签的播放器进入全屏模式。
WebMediaPlayerAndroid类的成员函数enenterFullScreen的实现如下所示:
void WebMediaPlayerAndroid::enterFullscreen() { if (player_manager_->CanEnterFullscreen(frame_)) { player_manager_->EnterFullscreen(player_id_, frame_); ...... } }这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
从前面Chromium为视频标签<video>创建播放器的过程分析一文可以知道,WebMediaPlayerAndroid类的成员变量player_manager_指向的是一个RendererMediaPlayerManager对象。这个RendererMediaPlayerManager对象负责管理为当前正在处理的网页中的所有<video>标签创建的播放器实例。
WebMediaPlayerAndroid类的成员函数enenterFullScreen首先调用上述RendererMediaPlayerManager对象的成员函数CanEnterFullscreen检查与当前正在处理的播放器实例关联的<video>标签所在的网页是否已经进入了全屏模式。如果已经进入,那么就会继续调用RendererMediaPlayerManager对象的成员函数EnterFullscreen使得当前正在处理的播放器实例进入全屏模式。
从前面的分析可以知道,与当前正在处理的播放器实例关联的<video>标签所在的网页已经进入了全屏模式。因此,接下来RendererMediaPlayerManager类的成员函数EnterFullscreen就会被调用,如下所示:
void RendererMediaPlayerManager::EnterFullscreen(int player_id, blink::WebFrame* frame) { pending_fullscreen_frame_ = frame; Send(new MediaPlayerHostMsg_EnterFullscreen(routing_id(), player_id)); }这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。
RendererMediaPlayerManager类的成员函数EnterFullscreen主要是向Browser进程发送一个类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,通知它将ID为player_id的播放器设置为全屏模式。
Browser进程通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:
bool MediaWebContentsObserver::OnMediaPlayerMessageReceived( const IPC::Message& msg, RenderFrameHost* render_frame_host) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg) IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_EnterFullscreen, GetMediaPlayerManager(render_frame_host), BrowserMediaPlayerManager::OnEnterFullscreen) ...... IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; }这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。
MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived首先调用成员函数GetMediaPlayerManager获得一个BrowserMediaPlayerManager对象,然后调用这个BrowserMediaPlayerManager对象的成员函数OnEnterFullscreen处理类型为MediaPlayerHostMsg_EnterFullscreen的IPC消息,如下所示:
void BrowserMediaPlayerManager::OnEnterFullscreen(int player_id) { ...... if (video_view_.get()) { fullscreen_player_id_ = player_id; video_view_->OpenVideo(); return; } else if (!ContentVideoView::GetInstance()) { ...... video_view_.reset(new ContentVideoView(this)); base::android::ScopedJavaLocalRef<jobject> j_content_video_view = video_view_->GetJavaObject(base::android::AttachCurrentThread()); if (!j_content_video_view.is_null()) { fullscreen_player_id_ = player_id; return; } } ..... }这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。
当BrowserMediaPlayerManager类的成员变量video_view_的值不等于NULL时,它指向一个ContentVideoView对象。这个ContentVideoView对象描述的是一个全屏播放器窗口。
在Browser进程中,一个网页对应有一个BrowserMediaPlayerManager对象。同一个网页中的所有<video>标签在设置为全屏模式时,使用的是同一个全屏播放器窗口。不同的网页的<video>标签在设置为全屏模式时,使用不同的全屏播放器窗口。在同一时刻,Browser进程只能存在一个全屏播放器窗口。
BrowserMediaPlayerManager类的成员函数OnEnterFullscreen的执行逻辑如下所示:
1. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值不等于NULL,也就是它指向了一个ContentVideoView对象,那么就说明Browser进程已经为当前正在处理的BrowserMediaPlayerManager对象创建过全屏播放器窗口了。这时候就会将参数player_id的值保存在另外一个成员变量fullscreen_player_id_中,表示当前正在处理的BrowserMediaPlayerManager对象所管理的全屏播放器窗口正在被ID为player_id的播放器使用。同时,会调用上述ContentVideoView对象的成员函数OpenVideo,让其全屏播放当前被设置为全屏模式的<video>标签的视频。
2. 如果当前正在处理的BrowserMediaPlayerManager对象的成员变量video_view_的值NULL,并且Browser进程当前没有为其它网页的<video>标签全屏播放视频(这时候调用ContentVideoView类的静态成员函数GetInstance获得的返回值为NULL),那么就会为当前正在处理的BrowserMediaPlayerManager对象创建一个全屏播放窗口,也就是创建一个ContentVideoView对象(这时候调用ContentVideoView类的静态成员函数GetInstance将会返回该ContentVideoView对象),并且保存在其成员变量video_view_中。
C++层ContentVideoView对象在创建的过程中,又会在Java层创建一个对应的ContentVideoView对象。这个Java层ContentVideoView对象如果能成功创建,那么就可以通过调用其对应的C++层ContentVideoView对象的成员函数GetJavaObject获得。这时候说明Browser进程成功创建了一个全屏播放器窗口。这个全屏播放器窗口实际上就是一个SurfaceView对象。
一个SurfaceView对象刚创建出来的时候,它描述的窗口没有创建出来。Browser进程需要等等它描述的窗口创建出来之后,才使用它全屏播放当前被设置为全屏模式的<video>标签的视频。因此,BrowserMediaPlayerManager类的成员函数OnEnterFullscreen在创建了一个SurfaceView对象之后,只做了一件简单的事情,就是记录当前是哪一个播放器进入了全屏模式,即将参数player_id的值保存在当前正在处理的BrowserMediaPlayerManager对象的成员变量fullscreen_player_id_中。
接下来,我们就继续分析全屏播放器窗口的创建过程,以及Browser进程使用它来全屏播放当前设置为全屏模式的<video>标签的视频的过程。
全屏播放器窗口是在C++层ContentVideoView对象的创建过程当中创建出来的,因此,我们就从C++层ContentVideoView类的构造函数开始,分析全屏播放器窗口的创建过程,如下所示:
ContentVideoView::ContentVideoView( BrowserMediaPlayerManager* manager) : manager_(manager), weak_factory_(this) { DCHECK(!g_content_video_view); j_content_video_view_ = CreateJavaObject(); g_content_video_view = this; ...... }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
C++层ContentVideoView类的构造函数会调用另外一个成员函数CreateJavaObject在Java层创建一个对应的ContentVideoView对象,并且保存在成员变量j_content_video_view_中。
与此同时,C++层ContentVideoView类的构造函数会将当前正在创建的ContentVideoView对象保存一个静态成员变量g_content_video_view_中,表示Browser进程当前已经存在一个全屏窗口。根据我们前面的分析,这样就会阻止其它网页全屏播放它们的<video>标签的视频。这个静态成员变量g_content_video_view_的值可以通过调用前面提到的C++层ContentVideoView类的静态成员函数GetInstance获得,如下所示:
ContentVideoView* ContentVideoView::GetInstance() { return g_content_video_view; }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
回到C++层的ContentVideoView类的构造函数中,我们主要关注Java层的ContentVideoView对象的创建过程,因此接下来我们继续分析C++层的ContentVideoView类的成员函数CreateJavaObject的实现,如下所示:
JavaObjectWeakGlobalRef ContentVideoView::CreateJavaObject() { ContentViewCoreImpl* content_view_core = manager_->GetContentViewCore(); JNIEnv* env = AttachCurrentThread(); bool legacyMode = CommandLine::ForCurrentProcess()->HasSwitch( switches::kDisableOverlayFullscreenVideoSubtitle); return JavaObjectWeakGlobalRef( env, Java_ContentVideoView_createContentVideoView( env, content_view_core->GetContext().obj(), reinterpret_cast<intptr_t>(this), content_view_core->GetContentVideoViewClient().obj(), legacyMode).obj()); }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
C++层的ContentVideoView类的成员函数CreateJavaObject主要是通过JNI接口Java_ContentVideoView_createContentVideoView调用Java层的ContentVideoView类的静态成员函数createContentVideoView创建一个Java层的ContentVideoView对象,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... @CalledByNative private static ContentVideoView createContentVideoView( Context context, long nativeContentVideoView, ContentVideoViewClient client, boolean legacy) { ...... ContentVideoView videoView = null; if (legacy) { videoView = new ContentVideoViewLegacy(context, nativeContentVideoView, client); } else { videoView = new ContentVideoView(context, nativeContentVideoView, client); } if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) { return videoView; } return null; } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
从前面的调用过程可以知道,当Browser进程设置了kDisableOverlayFullscreenVideoSubtitle(disable-overlay-fullscreen-video-subtitle)启动选项时,参数legacy的值就会等于true时,表示禁止在全屏播放器窗口上面显示播放控制控件以及字幕。这时候全屏播放器窗口通过一个ContentVideoViewLegacy对象描述。另一方面,如果参数legacy的值等于false,那么全屏播放器窗口将会通过一个ContentVideoView对象描述。
一个ContentVideoViewLegacy对象或者一个ContentVideoView对象描述的全屏播放器窗口实际上是一个SurfaceView。这个SurfaceView只有添加到浏览器窗口之后,才能显示在屏幕中。为了将该全屏播放器窗口显示出来,ContentVideoView类的静态成员函数createContentVideoView将会通过调用前面创建的ContentVideoViewLegacy对象或者ContentVideoView对象的成员函数getContentVideoViewClient获得一个ContentVideoViewClient接口。有了这个ContentVideoViewClient接口之后,就可以调用它的成员函数onShowCustomView将刚才创建出来的全屏播放器窗口显示出来了。
我们假设参数legacy的值等于false。这意味着全屏播放器窗口是通过一个ContentVideoView对象描述的。接下来我们就继续分析这个ContentVideoView对象的创建过程,即ContentVideoView类的构造函数的实现,以便了解它内部的SurfaceView的创建过程,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... // This view will contain the video. private VideoSurfaceView mVideoSurfaceView; ...... protected ContentVideoView(Context context, long nativeContentVideoView, ContentVideoViewClient client) { super(context); ...... mVideoSurfaceView = new VideoSurfaceView(context); showContentVideoView(); ...... } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的构造函数会创建一个VideoSurfaceView对象,并且保存在成员变量mVideoSurfaceView中。
VideoSurfaceView类是从SurfaceView类继承下来的,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... private class VideoSurfaceView extends SurfaceView { ...... } ...... }这个类定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的构造函数在创建了一个VideoSurfaceView对象,接下来会调用另外一个成员函数showContentVideoView将它作为当前正在创建的ContentVideoView对象的子View,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... protected void showContentVideoView() { mVideoSurfaceView.getHolder().addCallback(this); this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); ...... } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的成员函数showContentVideoView将前面创建的VideoSurfaceView对象作为当前正在创建的ContentVideoView对象的子View之外,还会使用后者监前者的surfaceCreated事件,也就是它描述的窗口创建完成事件。
当前正在创建的ContentVideoView对象实现了SurfaceHolder.Callback接口,一旦前面创建的VideoSurfaceView对象发出surfaceCreated事件通知,那么前者的成员函数surfaceCreated就会被调用,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... @Override public void surfaceCreated(SurfaceHolder holder) { mSurfaceHolder = holder; openVideo(); } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的成员函数surfaceCreated会将参数holder描述的一个SurfaceHolder对象保存在成员变量mSurfaceHolder中。后面可以通过这个SurfaceHolder对象获得前面创建的VideoSurfaceView对象内部的一个Surface。这个Surface将会用来接收MediaPlayer的解码输出。
接下来,ContentVideoView类的成员函数surfaceCreated就会调用另外一个成员函数openVideo将上述Surface设置为MediaPlayer的解码输出,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... @CalledByNative protected void openVideo() { if (mSurfaceHolder != null) { mCurrentState = STATE_IDLE; if (mNativeContentVideoView != 0) { nativeRequestMediaMetadata(mNativeContentVideoView); nativeSetSurface(mNativeContentVideoView, mSurfaceHolder.getSurface()); } } } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的成员变量mNativeContentVideoView描述的是前面在C++层创建的一个ContentVideoView对象。ContentVideoView类的成员函数openVideo主要是做两件事情:
1. 获取要播放的视频的元数据,以便用来初始化全屏播放器窗口。
2. 将前面创建的VideoSurfaceView对象内部维护的Surface设置为MediaPlayer的解码输出。
这两件事情分别是通过调用成员函数nativeRequestMediaMetadata和nativeSetSurface完成的。当它们完成之后,<video>标签的视频可以开始全屏播放了。接下来我们继续分析它们的实现。
ContentVideoView类的成员函数nativeRequestMediaMetadata是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata实现,如下所示:
void Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata(JNIEnv* env, jobject jcaller, jlong nativeContentVideoView) { ContentVideoView* native = reinterpret_cast<ContentVideoView*>(nativeContentVideoView); CHECK_NATIVE_PTR(env, jcaller, native, "RequestMediaMetadata"); return native->RequestMediaMetadata(env, jcaller); }这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。
从前面的调用过程可以知道,参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeRequestMediaMetadata调用它的成员函数RequestMediaMetadata获取要播放的视频的元数据,如下所示:
void ContentVideoView::RequestMediaMetadata(JNIEnv* env, jobject obj) { base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&ContentVideoView::UpdateMediaMetadata, weak_factory_.GetWeakPtr())); }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
ContentVideoView类的成员函数RequestMediaMetadata向当前线程的消息队列发送一个Task。这个Task绑定了ContentVideoView类的另外一个成员函数UpdateMediaMetadata。这意味着接下来ContentVideoView类的成员函数UpdateMediaMetadata会在当前线程中调用,用来获取要播放的视频的元数据。
ContentVideoView类的成员函数UpdateMediaMetadata的实现如下所示:
void ContentVideoView::UpdateMediaMetadata() { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef<jobject> content_video_view = GetJavaObject(env); if (content_video_view.is_null()) return; media::MediaPlayerAndroid* player = manager_->GetFullscreenPlayer(); if (player && player->IsPlayerReady()) { Java_ContentVideoView_onUpdateMediaMetadata( env, content_video_view.obj(), player->GetVideoWidth(), player->GetVideoHeight(), static_cast<int>(player->GetDuration().InMilliseconds()), player->CanPause(),player->CanSeekForward(), player->CanSeekBackward()); } }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
ContentVideoView类的成员函数UpdateMediaMetadata首先调用另外一个成员函数GetJavaObject获得与当前正在处理的C++层ContentVideoView对象对应的Java层ContentVideoView对象。
ContentVideoView类的成员函数UpdateMediaMetadata接下来又通过调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数GetFullscreenPlayer获得当前处于全屏模式的播放器,如下所示:
MediaPlayerAndroid* BrowserMediaPlayerManager::GetFullscreenPlayer() { return GetPlayer(fullscreen_player_id_); }这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。
从前面的分析可以知道,BrowserMediaPlayerManager类的成员变量fullscreen_player_id_记录了当前处于全屏模式的播放器的ID。有了这个ID之后,就可以调用另外一个成员函数GetPlayer获得一个对应的MediaPlayerBridge对象。这个MediaPlayerBridge对象描述的就是当前处于全屏模式的播放器。
回到ContentVideoView类的成员函数UpdateMediaMetadata中,它获得的全屏播放器之后,实际上就是<video>标签进入全屏模式之前所使用的那个播放器。这意味着<video>标签在全屏和非全屏模式下,使用的是同一个播放器,区别只在于播放器在两种模式下使用的UI不一样。这个播放器之前已经获得了要播放的视频的元数据,并且保存在了内部,因此这里就不需要从网络上重新获取。
有了要播放的视频的元数据之后,ContentVideoView类的成员函数UpdateMediaMetadata就通过JNI接口Java_ContentVideoView_onUpdateMediaMetadata调用前面获得的Java层ContentVideoView对象的成员函数onUpdateMediaMetadata,让其初始化全屏播放器窗口,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... @CalledByNative protected void onUpdateMediaMetadata( int videoWidth, int videoHeight, int duration, boolean canPause, boolean canSeekBack, boolean canSeekForward) { mDuration = duration; ..... onVideoSizeChanged(videoWidth, videoHeight); } ...... }
这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
参数duration描述的就是要播放的视频的总时长,它会被记录在ContentVideoView类的成员变量mDuration中。
另外两个参数videoWidth和videoHeight描述的是要播放的视频的宽和高,ContentVideoView类的成员函数onUpdateMediaMetadata将它们传递给另外一个成员函数onVideoSizeChanged,用来初始化全屏播放器窗口,如下所示:
public class ContentVideoView extends FrameLayout implements SurfaceHolder.Callback, ViewAndroidDelegate { ...... @CalledByNative private void onVideoSizeChanged(int width, int height) { mVideoWidth = width; mVideoHeight = height; // This will trigger the SurfaceView.onMeasure() call. mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); } ...... }这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java中。
ContentVideoView类的成员函数onVideoSizeChanged将视频的宽和度分别记录在成员变量mVideoWidth和mVideoHeight中,然后再将它们设置为用来显示视频画面的VideoSurfaceView的大小。
这一步执行完成后,全屏播放器窗口就初始化完毕。回到前面分析的ContentVideoView类的成员函数openVideo中,它接下来将上述VideoSurfaceView内部使用的Surface设置为MediaPlayer的解码输出。这是通过调用ContentVideoView类的成员函数nativeSetSurface实现的。
ContentVideoView类的成员函数nativeSetSurface是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface实现,如下所示:
void Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface(JNIEnv* env, jobject jcaller, jlong nativeContentVideoView, jobject surface) { ContentVideoView* native = reinterpret_cast<ContentVideoView*>(nativeContentVideoView); CHECK_NATIVE_PTR(env, jcaller, native, "SetSurface"); return native->SetSurface(env, jcaller, surface); }这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentVideoView_jni.h中。
参数nativeContentVideoView描述的是一个C++层ContentVideoView对象,另外一个参数surface描述的就是前面提到的用来显示视频画面的VideoSurfaceView内部使用的Surface。
函数Java_com_android_org_chromium_content_browser_ContentVideoView_nativeSetSurface参数nativeContentVideoView描述的C++层ContentVideoView对象的成员函数SetSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:
void ContentVideoView::SetSurface(JNIEnv* env, jobject obj, jobject surface) { manager_->SetVideoSurface( gfx::ScopedJavaSurface::AcquireExternalSurface(surface)); }这个函数定义在文件external/chromium_org/content/browser/android/content_video_view.cc中。
ContentVideoView类的成员函数SetSurface调用成员变量manager_指向的一个BrowserMediaPlayerManager对象的成员函数SetVideoSurface重新给当前处于全屏模式的播放器设置一个Surface,如下所示:
void BrowserMediaPlayerManager::SetVideoSurface( gfx::ScopedJavaSurface surface) { MediaPlayerAndroid* player = GetFullscreenPlayer(); ...... player->SetVideoSurface(surface.Pass()); ...... }这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。
BrowserMediaPlayerManager类的成员函数SetVideoSurface首先调用另外一个成员函数GetFullscreenPlayer获得一个MediaPlayerBridge对象。这个MediaPlayerBridge对象被一个MediaPlayerAndroid指针引用(MediaPlayerAndroid是MediaPlayerBridge的父类),它描述的便是当前处于全屏模式的播放器。
获得了当前处于全屏模式的播放器之后,就可以调用它的成员函数SetVideoSurface将参数surface描述的一个Surface设置为它的解码输出。这个设置过程,也就是MediaPlayerBridge类的成员函数SetVideoSurface的实现,可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。它实际上就是给在Java层创建的一个MediaPlayer重新设置一个Surface作为解码输出。这个Surface是由一个VideoSurfaceView提供的。这个VideoSurfaceView实际上就是一个SurfaceView。SurfaceView会自已将MediaPlayer的解码输出交给系统渲染。因此就不再需要Chromium参与这个渲染过程。
回忆<video>标签在进入全屏模式之前,Chromium会为它会创建一个纹理,然后用这个纹理创建一个SurfaceTexture。这个SurfaceTexture最终又会封装在一个Surface中。这个Surface就设置为MediaPlayer的解码输出。这时候MediaPlayer的解码输出需要由Chromium来处理,也就是渲染在浏览器窗口中。当浏览器窗口被系统渲染在屏幕上时,我们就可以看到MediaPlayer输出的视频画面了。这个过程可以参考前面Chromium为视频标签<video>渲染视频画面的过程分析一文。
这样,我们就分析完成<video>标签全屏播放视频的过程了。从这个分析过程我们就可以知道,<video>标签在全屏模式和非全屏模式下使用的都是相同的MediaPlayer,区别只在于这个MediaPlayer将视频画面渲染在不同的Surface之上。因此,<video>标签可以在全屏模式和非全屏模式之间进行无缝的播放切换。
至此,我们也分析完成了video标签在Chromium中的实现。视频在互联网中将扮演着越来越重要的角色。以前浏览器主要是通过Flash插件来支持视频播放。Flash插件有着臭名昭著的安全问题和Crash问题。因此,随着HTML5的出现,浏览器逐步转向使用<video>标签来支持视频播放。这不仅在产品上带来更好的体验(无需用户安装插件),而且在技术上也更加稳定。基于上述理由,理解<video>标签的实现原理就显得尤为重要。重新学习可以参考前面Chromium视频标签<video>简要介绍和学习计划一文。
Chromium为视频标签<video>全屏播放的过程分析