blob: 5f6606bb547f13b43fbd059c7068bc79dd24f2db [file] [log] [blame]
<html devsite><head>
<title>TextureView</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>我们在 Android 4.0 中引入了 TextureView 类,它结合了 View 与 SurfaceTexture,是我们在此讨论的最复杂的 View 对象。</p>
<h2 id="render_gles">使用 GLES 呈现</h2>
<p>我们已经知道,SurfaceTexture 是一个“GL 消费者”,它会占用图形数据的缓冲区,并将它们作为纹理进行提供。TextureView 会对 SurfaceTexture 进行封装,并接管对回调做出响应以及获取新缓冲区的责任。新缓冲区的就位会导致 TextureView 发出 View 失效请求。当被要求进行绘图时,TextureView 会使用最近收到的缓冲区的内容作为数据源,并根据 View 状态的指示,以相应的方式在相应的位置进行呈现。</p>
<p>您可以使用 GLES 在 TextureView 上呈现内容,就像在 SurfaceView 上一样。只需将 SurfaceTexture 传递到 EGL 窗口创建调用即可。不过,这样做会导致潜在问题。</p>
<p>在我们看到的大部分内容中,BufferQueue 是在不同进程之间传递缓冲区。当使用 GLES 呈现到 TextureView 时,生产者和消费者处于同一进程中,它们甚至可能会在单个线程上得到处理。假设我们以快速连续的方式从界面线程提交多个缓冲区。EGL 缓冲区交换调用需要使一个缓冲区从 BufferQueue 出列,而在有可用的缓冲区之前,它将处于暂停状态。只有当消费者获取一个缓冲区用于呈现时才会有可用的缓冲区,但是这一过程也会发生在界面线程上…因此我们陷入了困境。</p>
<p>解决方案是让 BufferQueue 确保始终有一个可用的缓冲区能够出列,以使缓冲区交换始终不会暂停。要保证能够实现这一点,一种方法是让 BufferQueue 在新缓冲区加入队列时舍弃之前加入队列的缓冲区的内容,并对最小缓冲区计数和最大获取缓冲区计数施加限制(如果您的队列有三个缓冲区,而所有这三个缓冲区均被消费者获取,那么就没有可以出列的缓冲区,缓冲区交换调用必然会暂停或失败。因此我们需要防止消费者一次获取两个以上的缓冲区)。丢弃缓冲区通常是不可取的,因此仅允许在特定情况下发生,例如生产者和消费者处于同一进程中时。</p>
<h2 id="surface_or_texture">SurfaceView 还是 TextureView?</h2>SurfaceView 和 TextureView 扮演的角色类似,但是拥有截然不同的实现。要作出最合适的选择,则需要了解它们各自的利弊。<p></p>
<p>因为 TextureView 是 View 层次结构的固有成员,所以其行为与其他所有 View 一样,可以与其他元素相互叠加。您可以执行任意转换,并通过简单的 API 调用将内容检索为位图。</p>
<p>影响 TextureView 的主要因素是合成步骤的表现。使用 SurfaceView 时,内容可以写到 SurfaceFlinger(理想情况下使用叠加层)合成的独立分层中。使用 TextureView 时,View 合成往往使用 GLES 执行,并且对其内容进行的更新也可能会导致其他 View 元素重绘(例如,如果它们位于 TextureView 上方)。View 呈现完成后,应用界面层必须由 SurfaceFlinger 与其他分层合成,以便您可以高效地将每个可见像素合成两次。对于全屏视频播放器,或任何其他相当于位于视频上方的界面元素的应用,SurfaceView 可以带来更好的效果。</p>
<p>如之前所述,受 DRM 保护的视频只能在叠加平面上呈现。支持受保护内容的视频播放器必须使用 SurfaceView 进行实现。</p>
<h2 id="grafika">案例研究:Grafika 的视频播放 (TextureView)</h2>
<p>Grafika 包括一对视频播放器,一个用 TextureView 实现,另一个用 SurfaceView 实现。对于这两个视频播放器来说,仅将帧从 MediaCodec 发送到 Surface 的视频解码部分是一样的。这两种实现之间最有趣的区别是呈现正确宽高比所需的步骤。</p>
<p>SurfaceView 需要 FrameLayout 的自定义实现,而要重新调整 SurfaceTexture 的大小,只需使用 <code>TextureView#setTransform()</code> 配置转换矩阵即可。对于前者,您会通过 WindowManager 向 SurfaceFlinger 发送新的窗口位置和大小值;对于后者,您仅仅是在以不同的方式呈现它。</p>
<p>否则,两种实现均遵循相同的模式。创建 Surface 后,系统会启用播放。点击“播放”时,系统会启动视频解码线程,并将 Surface 作为输出目标。之后,应用代码不需要执行任何操作,SurfaceFlinger(适用于 SurfaceView)或 TextureView 会处理合成和显示。</p>
<h2 id="decode">案例研究:Grafika 的双重解码</h2>
<p>此操作组件演示了在 TextureView 中对 SurfaceTexture 的操控。</p>
<p>此操作组件的基本结构是一对显示两个并排播放的不同视频的 TextureView。为了模拟视频会议应用的需求,我们希望在操作组件因屏幕方向发生变化而暂停和恢复时,MediaCodec 解码器能保持活动状态。原因在于,如果不对 MediaCodec 解码器使用的 Surface 进行完全重新配置,就无法更改它,而这是成本相当高的操作;因此我们希望 Surface 保持活动状态。Surface 只是 SurfaceTexture 的 BufferQueue 中生产者界面的句柄,而 SurfaceTexture 由 TextureView 管理;因此我们还需要 SurfaceTexture 保持活动状态。那么我们如何处理 TextureView 被关闭的情况呢?</p>
<p>TextureView 提供的 <code>setSurfaceTexture()</code> 调用正好能够满足我们的需求。我们从 TextureView 获取对 SurfaceTexture 的引用,并将它们保存在静态字段中。当操作组件被关闭时,我们从 <code>onSurfaceTextureDestroyed()</code> 回调返回“false”,以防止 SurfaceTexture 被销毁。当操作组件重新启动时,我们将原来的 SurfaceTexture 填充到新的 TextureView 中。TextureView 类负责创建和破坏 EGL 上下文。</p>
<p>每个视频解码器都是从单独的线程驱动的。乍一看,我们似乎需要每个线程的本地 EGL 上下文;但请注意,具有解码输出的缓冲区实际上是从 mediaserver 发送给我们的 BufferQueue 消费者 (SurfaceTexture)。TextureView 会为我们处理呈现,并在界面线程上执行。</p>
<p>使用 SurfaceView 实现该操作组件可能较为困难。我们不能只创建一对 SurfaceView 并将输出引导至它们,因为 Surface 在屏幕方向改变期间会被销毁。此外,这样做会增加两个层,而由于可用叠加层的数量限制,我们不得不尽量将层数量减到最少。与上述方法不同,我们希望创建一对 SurfaceTexture,以从视频解码器接收输出,然后在应用中执行呈现,使用 GLES 将两个纹理间隙呈现到 SurfaceView 的 Surface。</p>
</body></html>