在浏览器中,
浏览器是否能够无缝地渲染播放器的输出,取决于播放器是否有良好的设计。一个有良好设计的播放器要有独立的输入和输出。输入就是一个URL或者一个本地文件路径,输出即为一帧一帧的视频画面。播放器都能接受URL或者本地文件路径作为输入,也就是输入这一点都能满足要求。在输出上,它的设计就很有讲究了,有上中下三种策略。
下策是让使用者提供一个窗口作为播放器的输出。这显然是不合适的,因为一般来说,播放器的使用者除了要在窗口显示视频内容之外,还需要显示其它内容,也就是需要在窗口上放其它控件。当然,如果系统支持将一个窗口作为一个控件嵌入在另外一个窗口中显示,这种设计也未尝不可,不过这种设计太不通用了。
中策是让使用者提供一个控件作为播放器的输出。这种方式可以解决下策中提出的问题。然而,有一类特殊的使用者,它们的主UI不是通过系统提供控件设计出来的,而是用自己的方式绘制出来的。例如,在浏览器中,网页中的元素就不是通过系统提供的控件显示出来的,而是用自己的图形渲染引擎绘制出来的。
上策是让使用者提供一个缓冲区作为播放器的输出。这种输出使得使用者以非常灵活的方式将视频画面显示出来。不过缺点就是使用者要多做一些工作,也就是将缓冲区的内容渲染出来的。
将播放器的输出设计为缓冲区时,有一个细节,是非常值得注意的。一般来说,播放器的输出最终要显示在屏幕上。现在流行的系统,渲染基本上都是通过GPU进行的。如果我们提供给播放器的缓冲区,是普通的缓冲区,也就是只有CPU才可以访问的缓冲区,那么使用者在使用GPU渲染的情况下,需要将缓冲区内容上传到GPU去。这就相当于是执行一个纹理上传操作。我们知道,纹理上传是一个非常慢的操作,而视频的数据又很大,分辨率通常达到1080p。因此,理想的设计是让播放器将输出写入到GPU缓冲区中去。不过,这需要系统提供支持。
好消息是Android平台提供了这样的支持。在Android系统上,SurfaceTexture描述的就是GPU缓冲区,并且以纹理的形式进行渲染。SurfaceTexture可以进一步封装在Surface中。Android系统的MediaPlayer提供了一个setSurface接口,参数是一个Surface,用来接收解码输出,也就是视频画面。这意味着Android系统的MediaPlayer支持将解码输出写入在GPU缓冲区中。这是上上策,得益于Android系统本身的良好的设计。
Chromium正是利用了SurfaceTexture作为MediaPlayer的解码输出,如图1所示:
图1 以SurfaceTexture作为MediaPlayer的解码输出
从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,在Chromium的Content层,一个网页被抽象为三个Tree:CC Layer Tree、CC Pending Layer Tree和CC Active Layer Tree。其中,CC Layer Tree由Render进程中的Main线程管理,CC Pending Layer Tree和CC Active Layer Tree由Render进程中的Compositor线程管理。CC Pending Layer Tree由CC Layer Tree同步得到,CC Active Layer Tree由CC Pending Layer Tree激活得到。
Chromium为每一个
接下来,我们就先分析Chromium为
从前面Chromium为视频标签
void WebMediaPlayerAndroid::OnMediaMetadataChanged(
const base::TimeDelta& duration, int width, int height, bool success) {
......
if (success)
OnVideoSizeChanged(width, height);
......
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
当参数success的值等于true的时候,表示成功获取了要播放的视频的元数据,也就是长、宽和持续时间等数据。在这种情况下,WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged就会调用另外一个成员函数OnVideoSizeChanged通知要播放的视频大小发生了变化,如下所示:
void WebMediaPlayerAndroid::OnVideoSizeChanged(int width, int height) {
......
// Lazily allocate compositing layer.
if (!video_weblayer_) {
video_weblayer_.reset(new WebLayerImpl(cc::VideoLayer::Create(this)));
......
}
......
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员变量video_weblayer_描述的是
接下来,我们主要关注VideoLayer对象的创建过程,也就是VideoLayer类的静态成员函数Create的实现,如下所示:
scoped_refptr<VideoLayer> VideoLayer::Create(VideoFrameProvider* provider) {
return make_scoped_refptr(new VideoLayer(provider));
}
这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。
从前面的调用过程可以知道,参数provider指向的是一个WebMediaPlayerAndroid对象。这个WebMediaPlayerAndroid对象描述的是Render进程提供的播放器接口。VideoLayer类的静态成员函数Create使用这个WebMediaPlayerAndroid对象创建了一个VideoPlayer对象,并且返回给调用者。
VideoPlayer对象的创建过程,也就是VideoPlayer类的构造函数的实现,如下所示:
VideoLayer::VideoLayer(VideoFrameProvider* provider) : provider_(provider) {
DCHECK(provider_);
}
这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。
VideoPlayer类的构造函数将参数provider指向的一个WebMediaPlayerAndroid对象保存在成员变量provider_,表示当前正在创建的VideoPlayer对象要渲染的内容由它提供。
从前面Chromium网页Layer Tree同步为Pending Layer Tree的过程分析一文可以知道,当CC Layer Tree同步为CC Pending Layer Tree的时候,CC Layer Tree中的每一个XXXLayer对象都会在CC Pending Layer Tree中有一个对应的XXXLayerImpl对象。对于
scoped_ptr<LayerImpl> VideoLayer::CreateLayerImpl(LayerTreeImpl* tree_impl) {
return VideoLayerImpl::Create(tree_impl, id(), provider_).PassAs<LayerImpl>();
}
这个函数定义在文件external/chromium_org/cc/layers/video_layer.cc中。
参数tree_impl指向的是一个LayerTreeImpl对象。这个LayerTreeImpl对象描述的就是CC Pending Layer Tree。VideoLayer类的成员函数CreateLayerImpl使用这个LayerTreeImpl对象,以及当前正在处理的VideoLayer对象的成员变量provider_指向的一个WebMediaPlayerAndroid对象,创建一个VideoLayerImpl对象,也就是在CC Pending Layer Tree中为
scoped_ptr<VideoLayerImpl> VideoLayerImpl::Create(
LayerTreeImpl* tree_impl,
int id,
VideoFrameProvider* provider) {
scoped_ptr<VideoLayerImpl> layer(new VideoLayerImpl(tree_impl, id));
layer->SetProviderClientImpl(VideoFrameProviderClientImpl::Create(provider));
......
return layer.Pass();
}
这个函数定义在文件external/chromium_org/cc/layers/video_layer_impl.cc中。
VideoPlayerImpl类的静态成员函数Create首先创建了一个VideoLayerImpl对象,接着又调用VideoFrameProviderClientImpl类的静态成员函数Create创建了一个VideoFrameProviderClientImpl对象,如下所示:
scoped_refptr<VideoFrameProviderClientImpl>
VideoFrameProviderClientImpl::Create(
VideoFrameProvider* provider) {
return make_scoped_refptr(
new VideoFrameProviderClientImpl(provider));
}
这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。
VideoFrameProviderClientImpl类的静态成员函数Create使用参数provider指向的一个WebMediaPlayerAndroid对象创建了一个VideoFrameProviderClientImpl对象,如下所示:
VideoFrameProviderClientImpl::VideoFrameProviderClientImpl(
VideoFrameProvider* provider)
: active_video_layer_(NULL), provider_(provider) {
......
provider_->SetVideoFrameProviderClient(this);
......
}
这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。
VideoFrameProviderClientImpl类的构造函数除了将参数provider指向的WebMediaPlayerAndroid对象保存在成员变量provider_中,还会调用这个WebMediaPlayerAndroid对象的成员函数SetVideoFrameProviderClient,将当前正在创建的VideoFrameProviderClientImpl对象作为它的Client。这个Client将会负责接收播放器的解码输出。
WebMediaPlayerAndroid类的成员函数SetVideoFrameProviderClient的实现如下所示:
void WebMediaPlayerAndroid::SetVideoFrameProviderClient(
cc::VideoFrameProvider::Client* client) {
......
video_frame_provider_client_ = client;
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员函数SetVideoFrameProviderClient主要是将参数client指向的一个VideoFrameProviderClientImpl对象保存在成员变量video_frame_provider_client_中。
回到前面分析的VideoLayerImpl类的静态成员函数Create中,它创建了一个VideoFrameProviderClientImpl对象之后,接下来会将这个VideoFrameProviderClientImpl对象设置给前面创建的VideoLayerImpl对象。这是通过调用VideoLayerImpl类的成员函数SetProviderClientImpl实现的,如下所示:
void VideoLayerImpl::SetProviderClientImpl(
scoped_refptr<VideoFrameProviderClientImpl> provider_client_impl) {
provider_client_impl_ = provider_client_impl;
}
这个函数定义在文件external/chromium_org/cc/layers/video_layer_impl.cc中。
VideoLayerImpl类的成员函数SetProviderClientImpl将参数provider_client_impl指向的一个VideoFrameProviderClientImpl对象保存在成员变量provider_client_impl_中。
这一步执行完成之后,Chromium就为
这样,Chromium就为
从前面Chromium为视频标签
void WebMediaPlayerAndroid::play() {
......
TryCreateStreamTextureProxyIfNeeded();
......
if (hasVideo() && needs_establish_peer_ &&
!player_manager_->IsInFullscreen(frame_)) {
EstablishSurfaceTexturePeer();
}
if (paused())
player_manager_->Start(player_id_);
......
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员函数play的详细分析可以参考前面Chromium为视频标签
WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded的实现如下所示:
void WebMediaPlayerAndroid::TryCreateStreamTextureProxyIfNeeded() {
// Already created.
if (stream_texture_proxy_)
return;
......
stream_texture_proxy_.reset(stream_texture_factory_->CreateProxy());
if (stream_texture_proxy_) {
DoCreateStreamTexture();
ReallocateVideoFrame();
if (video_frame_provider_client_) {
stream_texture_proxy_->BindToLoop(
stream_id_, video_frame_provider_client_, compositor_loop_);
}
}
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员变量stream_texture_proxy_指向的是一个StreamTextureProxyImpl对象。这个StreamTextureProxyImpl对象用来在Compositor线程中接收MediaPlayer的解码输出。
WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded首先检查成员变量stream_texture_proxy_是否已经指向了一个StreamTextureProxyImpl对象。如果已经指向,那么就说明Chromium已经为当前正在处理的WebMediaPlayerAndroid创建过了一个SurfaceTexture。在这种情况下,WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded就什么也不做就返回。
另一方面,如果WebMediaPlayerAndroid类的成员变量stream_texture_proxy_还没有指向一个StreamTextureProxyImpl对象,那么WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded就会调用另外一个成员变量stream_texture_factory_指向的一个StreamTextureFactoryImpl对象的成员函数CreateProxy创建一个StreamTextureProxyImpl对象,并且保存在成员变量stream_texture_proxy_中。
WebMediaPlayerAndroid类的成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的创建过程可以参考前面Chromium为视频标签
StreamTextureFactoryImpl类的成员函数CreateProxy的实现如下所示:
StreamTextureProxy* StreamTextureFactoryImpl::CreateProxy() {
DCHECK(channel_.get());
StreamTextureHost* host = new StreamTextureHost(channel_.get());
return new StreamTextureProxyImpl(host);
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
StreamTextureFactoryImpl类的成员变量channel_描述的就是上述的GPU通道。StreamTextureFactoryImpl类的成员函数CreateProxy首先使用这个GPU通道创建一个StreamTextureHost对象,然后再将这个StreamTextureHost对象封装在一个StreamTextureProxyImpl对象中返回给调用者。这个StreamTextureHost对象描述的实际上就是Render进程接下来要求GPU进程创建的SurfaceTexture。
回到WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,它创建了一个StreamTextureProxyImpl对象之后,紧接着又会调用另外一个成员函数DoCreateStreamTexture请求GPU进程创建一个SurfaceTexture,如下所示:
void WebMediaPlayerAndroid::DoCreateStreamTexture() {
......
stream_id_ = stream_texture_factory_->CreateStreamTexture(
kGLTextureExternalOES, &texture_id_, &texture_mailbox_);
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员函数DoCreateStreamTexture调用成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的成员函数CreateStreamTexture请求GPU进程创建一个SurfaceTexture对象,如下所示:
unsigned StreamTextureFactoryImpl::CreateStreamTexture(
unsigned texture_target,
unsigned* texture_id,
gpu::Mailbox* texture_mailbox) {
GLuint stream_id = 0;
gpu::gles2::GLES2Interface* gl = context_provider_->ContextGL();
gl->GenTextures(1, texture_id);
stream_id = gl->CreateStreamTextureCHROMIUM(*texture_id);
gl->GenMailboxCHROMIUM(texture_mailbox->name);
gl->BindTexture(texture_target, *texture_id);
gl->ProduceTextureCHROMIUM(texture_target, texture_mailbox->name);
return stream_id;
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
在Android中,创建一个SurfaceTexture对象,需要指定一个纹理ID。因此,StreamTextureFactoryImpl类的成员函数CreateStreamTexture在请求GPU进程创建SurfaceTexture对象之前,首先会创建一个纹理。这个纹理可以通过调用Chromium为Render进程提供的Command Buffer OpenGL接口的成员函数GenTextures创建。
StreamTextureFactoryImpl类的成员变量context_provider_指向的是一个ContextProviderCommandBuffer对象。调用这个ContextProviderCommandBuffer对象的成员函数ContextGL可以获得一个Command Buffer GL接口。当我们调用这个Command Buffer GL接口的成员函数执行GPU命令时,它实际上是通过Command Buffer将GPU命令发送给GPU进程执行。关于Command Buffer GL的更多知识,可以参考前面Chromium硬件加速渲染机制基础知识简要介绍和学习计划这个系列的文章。
获得了一个纹理ID之后,StreamTextureFactoryImpl类的成员函数CreateStreamTexture就继续调用上述Command Buffer GL接口的成员函数CreateStreamTextureCHROMIUM请求GPU进程创建一个SurfaceTexutre对象。
创建了SurfaceTexutre对象之后,StreamTextureFactoryImpl类的成员函数CreateStreamTexture还会为前面创建出来的纹理创建一个Mailbox。这个Mailbox的作用是将与它关联的纹理发送给Browser进程进行合成,以便显示在浏览器窗口中。这个纹理描述的实际上就是MediaPlayer的解码输出,因此,上述Mailbox是用来将MediaPlayer的解码输出交给Browser进程合成显示在浏览器窗口中。关于Mailbox的更多知识,可以参考前面Chromium硬件加速渲染的UI合成过程分析一文。
接下来我们继续分析Render进程请求GPU进程创建SurfaceTexture对象的过程,也就是Command Buffer GL接口的成员函数CreateStreamTextureCHROMIUM的实现。
从前面Chromium硬件加速渲染机制基础知识简要介绍和学习计划这个系列的文章可以知道,Chromium是通过GLES2Implementation类来描述Command Buffer GL接口的,因此接下来我们分析GLES2Implementation类的成员函数CreateStreamTextureCHROMIUM的实现,如下所示:
GLuint GLES2Implementation::CreateStreamTextureCHROMIUM(GLuint texture) {
......
return gpu_control_->CreateStreamTexture(texture);
}
这个函数定义在文件external/chromium_org/gpu/command_buffer/client/gles2_implementation.cc中。
从前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文可以知道,GLES2Implementation类的成员变量gpu_control_指向的是一个CommandBufferProxyImpl对象。GLES2Implementation类的成员函数CreateStreamTextureCHROMIUM调用这个CommandBufferProxyImpl对象的成员函数CreateStreamTexture请求GPU进程创建一个SurfaceTexture对象,如下所示:
uint32 CommandBufferProxyImpl::CreateStreamTexture(uint32 texture_id) {
......
int32 stream_id = channel_->GenerateRouteID();
bool succeeded;
Send(new GpuCommandBufferMsg_CreateStreamTexture(
route_id_, texture_id, stream_id, &succeeded));
......
return stream_id;
}
这个函数定义在文件external/chromium_org/content/common/gpu/client/command_buffer_proxy_impl.cc中。
CommandBufferProxyImpl类的成员函数CreateStreamTexture向GPU进程发送一个类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息,用来请求创建一个SurfaceTexture对象。
GPU进程通过GpuCommandBufferStub类的成员函数OnMessageReceived接收类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息,如下所示:
bool GpuCommandBufferStub::OnMessageReceived(const IPC::Message& message) {
......
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(GpuCommandBufferStub, message)
......
IPC_MESSAGE_HANDLER(GpuCommandBufferMsg_CreateStreamTexture,
OnCreateStreamTexture)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
.....
return handled;
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。
GpuCommandBufferStub类的成员函数OnMessageReceived将类型为GpuCommandBufferMsg_CreateStreamTexture的IPC消息分发给另外一个成员函数OnCreateStreamTexture处理,如下所示:
void GpuCommandBufferStub::OnCreateStreamTexture(
uint32 texture_id, int32 stream_id, bool* succeeded) {
#if defined(OS_ANDROID)
*succeeded = StreamTexture::Create(this, texture_id, stream_id);
#else
*succeeded = false;
#endif
}
这个函数定义在文件external/chromium_org/content/common/gpu/gpu_command_buffer_stub.cc中。
SurfaceTexture是Android平台特有的接口,因此GpuCommandBufferStub类的成员函数OnCreateStreamTexture只有在Android平台上才会响应请求创建一个SurfaceTexture对象。
这个SurfaceTexture对象是通过调用StreamTexture类的静态成员函数Create创建的,如下所示:
bool StreamTexture::Create(
GpuCommandBufferStub* owner_stub,
uint32 client_texture_id,
int stream_id) {
GLES2Decoder* decoder = owner_stub->decoder();
TextureManager* texture_manager =
decoder->GetContextGroup()->texture_manager();
TextureRef* texture = texture_manager->GetTexture(client_texture_id);
if (texture && (!texture->texture()->target() ||
texture->texture()->target() == GL_TEXTURE_EXTERNAL_OES)) {
// TODO: Ideally a valid image id was returned to the client so that
// it could then call glBindTexImage2D() for doing the following.
scoped_refptr<gfx::GLImage> gl_image(
new StreamTexture(owner_stub, stream_id, texture->service_id()));
gfx::Size size = gl_image->GetSize();
texture_manager->SetTarget(texture, GL_TEXTURE_EXTERNAL_OES);
texture_manager->SetLevelInfo(texture,
GL_TEXTURE_EXTERNAL_OES,
0,
GL_RGBA,
size.width(),
size.height(),
1,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
true);
texture_manager->SetLevelImage(
texture, GL_TEXTURE_EXTERNAL_OES, 0, gl_image);
return true;
}
return false;
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
参数client_texture_id描述的是一个纹理ID,不过这个纹理ID是在Render进程中分配的,称为Client ID。在Chromium中,所有的OpenGL相关的对象都是需要在GPU进程创建的。GPU进程在创建这些对象的时候,会获得一个相应的ID,称为Service ID。Client ID和Service ID是一一对应的。
例如,Render进程需要一个纹理对象时。它就会先在本进程获得一个Client ID,然后该Client ID发送给GPU进程,让GPU进程调用OpenGL函数为其创建一个纹理对象。这个纹理对象与指定的Client ID关联,并且被封装在一个TextureRef对象中,由GPU进程中的一个TextureManager对象进行管理。调用这个TextureRef对象的成员函数service_id可以获得一个Service ID。这个Service ID实际上就是调用OpenGL函数glGenTextures时获得的纹理ID。
StreamTexture类的静态成员函数Create在为参数client_texture_id创建SurfaceTexture之前,首先要确保它已经与一个TextureRef对象关联,也就是GPU进程已经为它创建过纹理对象。此外,StreamTexture类的静态成员函数Create还需要确保这个纹理对象的类型为GL_TEXTURE_EXTERNAL_OES,而不是GL_TEXTURE。类型为GL_TEXTURE的纹理是OpenGL提供的标准纹理,而类型为GL_TEXTURE_EXTERNAL_OES的纹理是由厂商扩展的,它有特殊的用途。在Android平台上,它的特殊用途就是用来创建SurfaceTexture。
一旦上述条件得到满足了,StreamTexture类的静态成员函数Create就会创建一个StreamTexture对象,并且将这个StreamTexture对象交给对应的TextureManager对象管理。这个StreamTexture对象在创建的过程中,同时会创建一个SurfaceTexture对象,如下所示:
StreamTexture::StreamTexture(GpuCommandBufferStub* owner_stub,
int32 route_id,
uint32 texture_id)
: surface_texture_(gfx::SurfaceTexture::Create(texture_id)),
...... {
......
surface_texture_->SetFrameAvailableCallback(base::Bind(
&StreamTexture::OnFrameAvailable, weak_factory_.GetWeakPtr()));
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
StreamTexture类的构造函数通过调用SurfaceTexture类的静态成员函数Create创建一个SurfaceTexture对象,并且保存在成员变量surface_texture_中。SurfaceTexture类的静态成员函数Create的实现可以参考前面Chromium网页CPU光栅化原理分析一文。它实际上是通过JNI在Java层创建了一个由Android SDK提供的SurfaceTexture对象。这个Java层的SurfaceTexture对象会被封装在C++层中的一个SurfaceTexture对象中。这个C++层的SurfaceTexture对象就保存在StreamTexture类的成员变量surface_texture_中。
StreamTexture类的构造函数最后还做的一件事情是调用前面创建的C++层的SurfaceTexture对象的成员函数SetFrameAvailableCallback,用来设置一个Frame Available Callback。这个Frame Available Callback绑定了StreamTexture类的成员函数OnFrameAvailable。这意味着当上述设置的Frame Available Callback被执行时,StreamTexture类的成员函数OnFrameAvailable就会被调用。
C++层的SurfaceTexture类的成员函数SetFrameAvailableCallback又会通过JNI调用Java层的SurfaceTexture类的成员函数setOnFrameAvailableListener给前面在Java层创建的SurfaceTexture对象设置一个Frame Available Listener。一个SurfaceTexture对象描述的实际上是一个GPU缓冲区队列。这是一个生产者/消费者队列。在我们这个情景中,生产者即为Android系统提供的MediaPlayer,消费者即为上述在Java层设置的Frame Available Listener。
每当MediaPlayer解码出一帧视频之后,它都会从设置给它的SurfaceTexture对象中获得一个GPU缓冲区,然后将解码出来的视频帧数据拷贝到该GPU缓冲区中去,并且通知上述在Java层设置的Frame Available Listener,有新的GPU缓冲区可用。Java层的Frame Available Listener又会进一步通过JNI执行上述在C++层设置的Frame Available Callback。这时候StreamTexture类的成员函数OnFrameAvailable就会被调用。
StreamTexture类的成员函数OnFrameAvailable的调用过程我们后面再分析。现在回到前面分析的WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,这时候它就请求GPU进程创建了一个SurfaceTexture对象。
WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded接下来调用成员函数ReallocateVideoFrame将前面用来创建SurfaceTexture的纹理封装在一个VideoFrame对象中,如下所示:
void WebMediaPlayerAndroid::ReallocateVideoFrame() {
if (needs_external_surface_) {
......
} else if (!is_remote_ && texture_id_) {
......
scoped_refptr<VideoFrame> new_frame = VideoFrame::WrapNativeTexture(
make_scoped_ptr(new gpu::MailboxHolder(
texture_mailbox_, texture_target, texture_mailbox_sync_point)),
media::BindToCurrentLoop(base::Bind(
&OnReleaseTexture, stream_texture_factory_, texture_id_ref)),
natural_size_,
gfx::Rect(natural_size_),
natural_size_,
base::TimeDelta(),
VideoFrame::ReadPixelsCB());
SetCurrentFrameInternal(new_frame);
}
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
当Chromium用来实现WebView时,WebMediaPlayerAndroid类的成员变量needs_external_surface_的值会等于true。我们不考虑这一种情况。
当WebMediaPlayerAndroid类的成员变量is_remote_的值等于true时,表示
从前面的调用过程可以知道,WebMediaPlayerAndroid类的成员变量texture_id_描述的是一个纹理ID。这个纹理ID就是前面用来创建SurfaceTexture对象所使用的纹理ID。这个纹理ID同时封装在一个Mailbox中。这个Mailbox由WebMediaPlayerAndroid类的另外一个成员变量texture_mailbox_中。
排除上述的两种情况之后,如果WebMediaPlayerAndroid类的成员变量texture_id_的值不等于0,也就是前面我们成功创建了一个SurfaceTexture对象,那么WebMediaPlayerAndroid类的成员函数ReallocateVideoFrame就会创建一个VideoFrame对象。这个VideoFrame对象封装了WebMediaPlayerAndroid类的成员变量texture_mailbox_所描述的一个Mailbox。这个Mailbox同时又与前面用来创建SurfaceTexture对象的纹理关联。因此,以后通过这里创建出来的VideoFrame对象将可以访问到前面创建出来的SurfaceTexture对象的内容,也就是MediaPlayer的解码输出。
WebMediaPlayerAndroid类的成员函数ReallocateVideoFrame最后调用另外一个成员函数SetCurrentFrameInternal将前面创建出来的VideoFrame保存在内部,如下所示:
void WebMediaPlayerAndroid::SetCurrentFrameInternal(
scoped_refptr<media::VideoFrame>& video_frame) {
base::AutoLock auto_lock(current_frame_lock_);
current_frame_ = video_frame;
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员函数SetCurrentFrameInternal将参数video_frame描述的VideoFrame对象保存在成员变量current_frame_中。后面我们将会看到这个VideoFrame对象是如何使用的。
这一步执行完成后,再回到WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded中,它最后还要做一件事情,就是指定一个对象处理MediaPlayer的解码输出,这个对象就是它的成员变量video_frame_provider_client_所指向的VideoFrameProviderClientImpl对象。
为了方便描述,我们重新列出WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded的实现,如下所示:
void WebMediaPlayerAndroid::TryCreateStreamTextureProxyIfNeeded() {
// Already created.
if (stream_texture_proxy_)
return;
......
stream_texture_proxy_.reset(stream_texture_factory_->CreateProxy());
if (stream_texture_proxy_) {
DoCreateStreamTexture();
ReallocateVideoFrame();
if (video_frame_provider_client_) {
stream_texture_proxy_->BindToLoop(
stream_id_, video_frame_provider_client_, compositor_loop_);
}
}
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员变量video_frame_provider_client_指向的VideoFrameProviderClientImpl对象的创建过程可以参考前面的分析。WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded通过调用前面创建的StreamTextureProxyImpl对象的成员函数BindToLoop指定成员变量video_frame_provider_client_指向的VideoFrameProviderClientImpl对象在Render进程的Compositor线程中处理MediaPlayer的解码输出,如下所示:
void StreamTextureProxyImpl::BindToLoop(
int32 stream_id,
cc::VideoFrameProvider::Client* client,
scoped_refptr<base::MessageLoopProxy> loop) {
DCHECK(loop);
{
base::AutoLock lock(lock_);
DCHECK(!loop_ || (loop == loop_));
loop_ = loop;
client_ = client;
}
if (loop->BelongsToCurrentThread()) {
BindOnThread(stream_id);
return;
}
// Unretained is safe here only because the object is deleted on |loop_|
// thread.
loop->PostTask(FROM_HERE,
base::Bind(&StreamTextureProxyImpl::BindOnThread,
base::Unretained(this),
stream_id));
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
StreamTextureProxyImpl类的成员函数BindToLoop首先将参数client指向的VideoFrameProviderClientImpl对象保存在成员变量client_中,以便以后可以将MediaPlayer的解码输出交给它处理。
另外一个参数loop描述的是Render进程的Compositor线程的消息队列。StreamTextureProxyImpl类的成员函数BindToLoop接下来判断当前正在执行的线程是否就是Compositor线程。如果是的话,那么就直接调用另外一个成员函数BindToThread,用来在Compositor线程中设置一个Route,接收MediaPlayer的解码输出通知。如果不是的话,那么就需要向Compositor线程的消息队列发送一个Task。当这个Task在Compositor线程中执行时,再调用StreamTextureProxyImpl类的成员函数BindToThread中,用来确保在Compositor线程获得MediaPlayer的解码输出通知。
StreamTextureProxyImpl类的成员函数BindOnThread的实现如下所示:
void StreamTextureProxyImpl::BindOnThread(int32 stream_id) {
host_->BindToCurrentThread(stream_id, this);
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
StreamTextureProxyImpl类的成员变量host_指向的是一个StreamTextureHost对象。StreamTextureProxyImpl类的成员函数BindOnThread调用这个StreamTextureHost对象的成员函数BindToCurrentThread让其在Compositor线程中接收MediaPlayer的解码输出通知,如下所示:
bool StreamTextureHost::BindToCurrentThread(int32 stream_id,
Listener* listener) {
listener_ = listener;
if (channel_.get() && stream_id && !stream_id_) {
stream_id_ = stream_id;
channel_->AddRoute(stream_id, weak_ptr_factory_.GetWeakPtr());
channel_->Send(new GpuStreamTextureMsg_StartListening(stream_id));
return true;
}
return false;
}
这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。
从前面的调用过程可以知道,参数listener指向的是一个StreamTextureProxyImpl对象。StreamTextureHost类的成员函数BindToCurrentThread首先将这个StreamTextureProxyImpl对象保存在成员变量listener_中。
StreamTextureHost类的另外一个成员变量channel_描述的是一个GPU通道。这个GPU通道就是前面Render请求GPU进程创建SurfaceTexture所用的GPU通道。StreamTextureHost类的成员函数BindToCurrentThread将当前正在处理的StreamTextureHost对象设置为该GPU通道的一个Route,用来接收从GPU进程发送过来的Routing ID为stream_id的IPC消息,也就是那些与前面创建的SurfaceTexture相关的IPC消息。
StreamTextureHost类的成员函数最后通过成员变量channel_描述的GPU通道向GPU进程发送一个类型为GpuStreamTextureMsg_StartListening的IPC消息,表示Render进程已经准备就绪接收MediaPlayer的解码输出。
GPU进程通过StreamTexture类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_StartListening的IPC消息,如下所示:
bool StreamTexture::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(StreamTexture, message)
IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_StartListening, OnStartListening)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
DCHECK(handled);
return handled;
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
StreamTexture类的成员函数OnMessageReceived将类型为StreamTextureMsg_StartListening的IPC消息分发给另外一个成员函数OnStartListening处理,如下所示:
void StreamTexture::OnStartListening() {
DCHECK(!has_listener_);
has_listener_ = true;
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
StreamTexture类的成员函数OnStartListening将成员变量has_listener_的值设置为true,表示Render进程准备就绪接收MediaPlayer的解码输出。
这一步执行完成之后,Chromium就为
void WebMediaPlayerAndroid::EstablishSurfaceTexturePeer() {
......
if (stream_texture_factory_.get() && stream_id_)
stream_texture_factory_->EstablishPeer(stream_id_, player_id_);
......
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。
WebMediaPlayerAndroid类的成员函数EstablishSurfaceTexturePeer调用成员变量stream_texture_factory_指向的StreamTextureFactoryImpl对象的成员函数EstablishPeer将之前创建的SurfaceTexture对象设置为MediaPlayer的解码输出,如下所示:
void StreamTextureFactoryImpl::EstablishPeer(int32 stream_id, int player_id) {
DCHECK(channel_.get());
channel_->Send(
new GpuStreamTextureMsg_EstablishPeer(stream_id, frame_id_, player_id));
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
StreamTextureFactoryImpl类的成员函数EstablishPeer通过成员变量channel_描述的GPU通道向GPU进程发送一个类型为GpuStreamTextureMsg_EstablishPeer的IPC消息,请求它将之前创建的SurfaceTexture对象设置为MediaPlayer的解码输出。
GPU进程通过StreamTexture类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_EstablishPeer的IPC消息,如下所示:
bool StreamTexture::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(StreamTexture, message)
......
IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_EstablishPeer, OnEstablishPeer)
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
DCHECK(handled);
return handled;
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
StreamTexture类的成员函数OnMessageReceived将类型为GpuStreamTextureMsg_EstablishPeer的IPC消息分发给另外一个成员函数OnEstablishPeer处理,如下所示:
void StreamTexture::OnEstablishPeer(int32 primary_id, int32 secondary_id) {
......
SurfaceTexturePeer::GetInstance()->EstablishSurfaceTexturePeer(
process, surface_texture_, primary_id, secondary_id);
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
从前面的分析可以知道,StreamTexture类的成员变量surface_texture_指向的SurfaceTexture对象就是之前Render进程请求GPU进程创建的SurfaceTexture对象。现在需要将这个SurfaceTexture对象设置为MediaPlayer的解码输出。这是通过调用运行在GPU进程中的一个SurfaceTexturePeer单例对象的成员函数EstablishSurfaceTexturePeer实现的。
SurfaceTexturePeer类的成员函数EstablishSurfaceTexturePeer的实现如下所示:
void SurfaceTexturePeerBrowserImpl::EstablishSurfaceTexturePeer(
base::ProcessHandle render_process_handle,
scoped_refptr<gfx::SurfaceTexture> surface_texture,
int render_frame_id,
int player_id) {
......
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
&SetSurfacePeer, surface_texture, render_process_handle,
render_frame_id, player_id));
}
这个函数定义在文件external/chromium_org/content/browser/android/surface_texture_peer_browser_impl.cc中。
在Android平台中,Chromium的GPU进程与Browser进程实际上是同一个进程。SurfaceTexturePeer类的成员函数EstablishSurfaceTexturePeer于是就在Browser进程的UI线程中执行函数SetSurfacePeer,目的是将参数surface_texture描述的SurfaceTexture对象设置为MediaPlayer的解码输出。
函数SetSurfacePeer的实现如下所示:
static void SetSurfacePeer(
scoped_refptr<gfx::SurfaceTexture> surface_texture,
base::ProcessHandle render_process_handle,
int render_frame_id,
int player_id) {
int render_process_id = 0;
RenderProcessHost::iterator it = RenderProcessHost::AllHostsIterator();
while (!it.IsAtEnd()) {
if (it.GetCurrentValue()->GetHandle() == render_process_handle) {
render_process_id = it.GetCurrentValue()->GetID();
break;
}
it.Advance();
}
......
RenderFrameHostImpl* frame =
RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
......
RenderViewHostImpl* view =
static_cast<RenderViewHostImpl*>(frame->GetRenderViewHost());
BrowserMediaPlayerManager* player_manager =
view->media_web_contents_observer()->GetMediaPlayerManager(frame);
......
media::MediaPlayerAndroid* player = player_manager->GetPlayer(player_id);
......
if (player != player_manager->GetFullscreenPlayer()) {
gfx::ScopedJavaSurface scoped_surface(surface_texture);
player->SetVideoSurface(scoped_surface.Pass());
}
}
这个函数定义在文件external/chromium_org/content/browser/android/surface_texture_peer_browser_impl.cc中。
从前面Chromium为视频标签
函数SetSurfacePeer要做的事情就是在Browser进程找到ID值为player_id的一个MediaPlayerBridge对象,然后将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出。
从前面Chromium为视频标签
为了找到目标MediaPlayerBridge对象,函数SetSurfacePeer首先要找到目标MediaPlayerBridge对象所对应的Render进程。这可以通过参数render_process_handle获得。然而,知道目标MediaPlayerBridge对象所对应的Render进程还不足够,因为一个Render进程可能会同时加载多个网页。Player ID在同一个网页内才是唯一的。这时候需要用到另外一个参数render_frame_id。这个参数render_frame_id描述的是目标MediaPlayerBridge对象所对应的网页。
Browser为同一个网页的
有了这个MediaPlayerBridge对象之后,就可以调用它的成员函数SetVideoSurface将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出了。不过,只有在
这里还有一点需要注意,MediaPlayerBridge类是从MediaPlayerAndroid类继承下来的,因此,函数SetSurfacePeer可以将找到的目标MediaPlayerBridge对象保存在一个MediaPlayerAndroid指针中。
在将参数surface_texture描述的SurfaceTexture对象设置为它在Java层创建的MediaPlayer的解码输出之前,函数SetSurfacePeer需要将这个SurfaceTexture对象封装在一个Surface对象。这是通过ScopedJavaSurface类实现的,如下所示:
ScopedJavaSurface::ScopedJavaSurface(
const SurfaceTexture* surface_texture)
: auto_release_(true),
is_protected_(false) {
JNIEnv* env = base::android::AttachCurrentThread();
RegisterNativesIfNeeded(env);
ScopedJavaLocalRef<jobject> tmp(JNI_Surface::Java_Surface_Constructor(
env, surface_texture->j_surface_texture().obj()));
DCHECK(!tmp.is_null());
j_surface_.Reset(tmp);
}
这个函数定义在文件external/chromium_org/ui/gl/android/scoped_java_surface.cc中。
ScopedJavaSurface类的构造函数首先是调用参数surface_texture指向的一个C++层的SurfaceTexture对象的成员函数j_surface_texture获得它在Java层对应的SurfaceTexture对象,然后再以这个Java层的SurfaceTexture为参数,调用JNI接口JNI_Surface::Java_Surface_Constructor在Java层创建一个Surface对象。Java层Surface对象的创建接口,可以参考Android SDK文档。创建出来的Java层Surface对象,保存在ScopedJavaSurface类的成员变量j_surface_中。
以后通过调用ScopedJavaSurface类的成员函数j_surface可以获得保存在成员变量j_surface_中的Java层Surface对象,如下所示:
class GL_EXPORT ScopedJavaSurface {
......
public:
......
const base::android::JavaRef<jobject>& j_surface() const {
return j_surface_;
}
private:
......
base::android::ScopedJavaGlobalRef<jobject> j_surface_;
};
这个函数定义在文件external/chromium_org/ui/gl/android/scoped_java_surface.h中。
回到前面分析的函数SetSurfacePeer中,它将参数surface_texture描述的SurfaceTexture对象封装在一个Java层Surface对象之后,接下来就调用前面找到的目标MediaPlayerBridge对象的成员函数SetVideoSurface将其设置为Java层MediaPlayer的解码输出,如下所示:
void MediaPlayerBridge::SetVideoSurface(gfx::ScopedJavaSurface surface) {
if (j_media_player_bridge_.is_null()) {
if (surface.IsEmpty())
return;
Prepare();
}
JNIEnv* env = base::android::AttachCurrentThread();
CHECK(env);
is_surface_in_use_ = true;
Java_MediaPlayerBridge_setSurface(
env, j_media_player_bridge_.obj(), surface.j_surface().obj());
}
这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。
从前面Chromium为视频标签
在我们这个情景中,MediaPlayerBridge类的成员变量j_media_player_bridge_指向的Java层MediaPlayerBridge对象已经创建。这时候C++层的MediaPlayerBridge类的成员函数SetVideoSurface就会通过JNI接口Java_MediaPlayerBridge_setSurface调用这个Java层MediaPlayerBridge对象的成员函数setSurface将参数surface描述的Java层Surface设置为它内部创建的MediaPlayer的解码输出。
Java层的MediaPlayerBridge类的成员函数setSurface的实现如下所示:
public class MediaPlayerBridge {
......
@CalledByNative
protected void setSurface(Surface surface) {
getLocalPlayer().setSurface(surface);
}
......
}
这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。
MediaPlayerBridge类的成员函数setSurface首先调用另外一个成员函数getLocalPlayer获得内部创建的一个MediaPlayer对象。这个MediaPlayer对象描述的就是Android系统提供的播放器实例,它的创建过程可以参考前面Chromium为视频标签
从前面的分析可以知道,参数surface描述的Surface对象封装了一个SurfaceTexture对象。当MediaPlayer开始播放视频时,它每解码出来的一帧,都会从上述被封装的SurfaceTexture对象中获得一个GPU缓冲区,并且将视频帧数据写入到该GPU缓冲区中,然后再通过设置给上述被封装的SurfaceTexture对象的Frame Available Listener,调用C++层的StreamTexture类的成员函数OnFrameAvailable,用来通知它MediaPlayer有新的解码输出需要渲染。
C++层的StreamTexture类的成员函数OnFrameAvailable的实现如下所示:
void StreamTexture::OnFrameAvailable() {
has_pending_frame_ = true;
if (has_listener_ && owner_stub_) {
owner_stub_->channel()->Send(
new GpuStreamTextureMsg_FrameAvailable(route_id_));
}
}
这个函数定义在文件external/chromium_org/content/common/gpu/stream_texture_android.cc中。
StreamTexture类的成员函数OnFrameAvailable首先将成员变量has_pending_frame_的值设置为true,表示有一个刚刚解码出来的视频帧待处理。
从前面的分析可以知道,StreamTexture类的成员变量has_listener_已经被设置为true。在这种情况下,如果StreamTexture类的成员变量owner_stub_指向了一个GpuCommandBufferStub对象,那么就说明当前正在处理的StreamTexture对象是为Render进程创建的。这时候就需要向该Render进程发送一个类型为GpuStreamTextureMsg_FrameAvailable的IPC消息,用来通知它MediaPlayer有一个新的解码输出。
Render进程通过StreamTextureHost类的成员函数OnMessageReceived接收类型为GpuStreamTextureMsg_FrameAvailable的IPC消息,如下所示:
bool StreamTextureHost::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(StreamTextureHost, message)
IPC_MESSAGE_HANDLER(GpuStreamTextureMsg_FrameAvailable,
OnFrameAvailable);
......
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
DCHECK(handled);
return handled;
}
这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。
StreamTextureHost类的成员函数OnMessageReceived将类型为GpuStreamTextureMsg_FrameAvailable的IPC消息分发给另外一个成员函数OnFrameAvailable处理,如下所示:
void StreamTextureHost::OnFrameAvailable() {
if (listener_)
listener_->OnFrameAvailable();
}
这个函数定义在文件external/chromium_org/content/renderer/gpu/stream_texture_host_android.cc中。
从前面的分析可以知道,StreamTextureHost类的成员变量listener_指向的是一个StreamTextureProxyImpl对象。 StreamTextureHost类的成员函数OnFrameAvailable调用这个StreamTextureProxyImpl对象的成员函数OnFrameAvailable通知它MediaPlayer有一个新的解码输出,如下所示:
void StreamTextureProxyImpl::OnFrameAvailable() {
base::AutoLock lock(lock_);
if (client_)
client_->DidReceiveFrame();
}
这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。
从前面的分析可以知道,StreamTextureProxyImpl类的成员变量client_指向的是一个VideoFrameProviderClientImpl对象。StreamTextureProxyImpl类的成员函数OnFrameAvailable调用这个VideoFrameProviderClientImpl对象的成员函数DidReceiveFrame通知它MediaPlayer有一个新的解码输出,如下所示:
void VideoFrameProviderClientImpl::DidReceiveFrame() {
......
if (active_video_layer_)
active_video_layer_->SetNeedsRedraw();
}
这个函数定义在文件external/chromium_org/cc/layers/video_frame_provider_client_impl.cc中。
VideoFrameProviderClientImpl类的成员变量active_video_layer_指向的是一个VideoLayerImpl对象。这个VideoLayerImpl对象描述的就是
从前面Chromium硬件加速渲染的UI合成过程分析一文可以知道,当网页的CC Active Layer Tree被渲染时,它里面的每一个Layer的成员函数WillDraw和AppendQuads都会被调用,用来准备要渲染的材料,以便发送给Browser进程进行合成。对于
VideoLayerImpl类的成员函数WillDraw的实现如下所示:
bool VideoLayerImpl::WillDraw(DrawMode draw_mode,
ResourceProvider* resource_provider) {
......
frame_ = provider_client_impl_->AcquireLockAndCurrentFrame();
......
if (!updater_) {
updater_.reset(
%2
Copyright© 2013-2019