XOPlayer 阶段性总结——使用 Unity 的 VideoPlayer 组件开发全景播放器

项目地址

现在还只是一个简陋的单 Scene 的应用,但已具备基本的视频播放和播放控制能力:

  • 本地视频播放
  • 进度显示、跳转
  • 暂停、恢复、停止
  • 音量调节
  • 视频源类型切换

播放界面如下:
播放左右格式 3D 视频

0. 开发、运行环境

  • VS2019 Community。安装时务必勾选 Visual Studio Tools for Unity
  • Unity 2018.4.14f1。运行时修改菜单 Edit-Preferences 的 External Tools 选项页中的 External Script Editor 项值,关联到 VS2019,方能在 VS2019 中正确识别 Unity 工程和源码。
  • AMD Ryzen7 1700 + GTX 1070 + DDR4 16GB

1. UI 布局一览

UI 布局

模块化的组合功能均以 Prefab 实现(蓝色组件)。Prefab 既提供了一种可复用手段(2次出现的 Slider 以同一 Prefab 实例化),又契合了高内聚,低耦合的设计理念。我的理解是:相比于直接在 Scene 中布局复杂界面,Prefab 是更优选择,能用则用。

注意 VideoPlayer 所在位置,出现在根 Canvas 之外,原因参考 2. VideoPlayer

2. VideoPlayer 相关

以流媒体角度解读 VideoPlayer 的话,VideoPlayer 是一个 demuxers,decoders 和 renders 的组合。注意这里说的渲染,非指渲染到 UI,而是以某种标准格式渲染到内存或显存,从内存/显存渲染到 UI 是用户职责。所以,VideoPlayer 被设计为非 UI 组件,这也是上文提到 VideoPlayer 为什么出现在根 Canvas 之外的原因。

VideoPlayer 提供了多种 Render Mode,适配不同的渲染场景,XOPlayer 只用到了 Render Texture 一种。此纹理须在脚本中动态创建并关联到 VideoPlayer :

1
2
3
4
5
6
7
8
9
10
// make sure: mPlayer.prepareCompleted += onPrepareCompleted.
// width & height are available until prepared.
private void onPrepareCompleted(VideoPlayer videoPlayer)
{
mVideoTexture = new RenderTexture((int)mPlayer.width, (int)mPlayer.height, 0, RenderTextureFormat.ARGB32);
mPlayer.targetTexture = mVideoTexture;
// attaching texture to material
// attaching material to some kind of UI controls, e.g., Image, global Skybox.
mPlayer.Play();
}

XOPlayer 支持普通视频和全景视频的播放,两者在渲染到 UI 时大不相同:

  • 普通视频渲染到 Image 组件
  • 全景视频渲染到全局 Skybox 或 Sphere

此逻辑应补充在 RenderTexture 创建后、视频播放前:

1
2
3
4
5
6
7
8
9
10
11
12
// attaching texture to material
// attaching material to some kind of UI controls, e.g., Image, global Skybox.
if (mMode == PlayMode.kNormal) // normal videos
{
video2DMaterial.mainTexture = mVideoTexture;
normalPlayer.GetComponent<Image>().material = video2DMaterial;
}
else // panoramic videos
{
videoPanoramicMaterial.mainTexture = mVideoTexture;
RenderSettings.skybox = videoPanoramicMaterial;
}
  • video2DMaterial 是一个预先定义的材质,Shader 采用 “Unlit/Texture”
  • videoPanoramicMaterial 是一个预先定义的材质,Shader 采用 “Skybox/Panoramic”

全景视频的视角又分为 180° 和 360°,可以通过预先定义 2 个不同的材质,Image Type 分别设置为 180 Degrees 和 360 Degrees 实现;也可以只创建一个材质,脚本中动态切换 Image Type :

1
2
3
4
5
6
7
8
9
if (mMode == PlayMode.kPanoramic180) // 180 degrees panoramic videos
{
videoPanoramicMaterial.SetFloat("_ImageType", 1f);

}
else // 360 degrees panoramic videos
{
videoPanoramicMaterial.SetFloat("_ImageType", 0f);
}

3. 其它

3.1 全景视频的视角旋转

PC 上通过鼠标拖拽驱动视角旋转。天空盒内,只需要将 Main Camera 按其自身 position 沿 X/Y 轴旋转即可:

1
2
3
4
5
6
7
8
9
10
11
12
private float rotateSpeed = 2.0f;
// commonly, we track Input status in Update loop.
void Update()
{
if (Input.GetMouseButton(0)) // if left mouse button pressed down
{
// rotate about x asix
transform.RotateAround(transform.position, Vector3.down, rotateSpeed * Input.GetAxis("Mouse X"));
// rotate about y asix
transform.RotateAround(transform.position, transform.right, rotateSpeed * Input.GetAxis("Mouse Y"));
}
}

3.2 鼠标点击任意处显示/隐藏工具栏

因为绝大部分逻辑均在 VideoPlayer 组件的脚本 PlayerManager 内实现,所以第一反应是通过 PlayerManager 实现 IPointerDownHandler, IPointerUpHandler 接口即可。

结论是不可以。上文已经提及,VideoPlayer 不是 UI 组件,没有 Rect Transform,所以它其实是不可能接受鼠标事件的。

所以需要在根 Canvas 上添加脚本,并实现 IPointerDownHandler, IPointerUpHandler :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PointerHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
// attaching to PlayerManager script.
public PlayerManager playerManager;

public void OnPointerDown(PointerEventData eventData)
{
}

public void OnPointerUp(PointerEventData eventData)
{
playerManager.OnPointerUp(); // we do actions in PlayerManager.
}
}

3.3 普通视频播放相关

普通视频的调用链是这样的:

VideoPlayer——RenderTexture——video2DMaterial——normalPlayer

其中 RenderTexture 是动态创建的,直接导致 normalPlayer.GetComponent<Image>().material 属性不能通过 Inspector 面板关联(指 video2DMaterial——normalPlayer),甚至在 Start() 中赋值也不行。Image 画面不会随播放更新。没有找到相关资料支持,但是我反推的结论是:

1
2
3
mVideoTexture = new RenderTexture((int)mPlayer.width, (int)mPlayer.height, 0, RenderTextureFormat.ARGB32);
video2DMaterial.mainTexture = mVideoTexture;
normalPlayer.GetComponent<Image>().material = video2DMaterial;

这三句的调用顺序是不能变动的。即需要先完备材质信息,才能将材质赋值给 Image (或其它 UI 组件 ?)。

停止播放时存在类似问题:

1
2
3
mPlayer.Stop();
// 这句赋空是必须的,否则 Image 变为不可重入,再播放视频时画面不能更新。
normalPlayer.GetComponent<Image>().material = null;

结合创建时的三句代码反推,结论是:再次播放时 VideoTexture 是重新创建的,但是 video2DMaterial 不是,所以 normalPlayer 跟踪不到这个间接变化。

得出一个不知道对错的结论:一个调用链上的对象,最好要么全部静态创建,要么全部动态创建。

4. Issues

  • 基本功能缺失:快进、快退、循环
  • 180° 全景视频的旋转角度是 360°
  • 进度条 Slider 不能拖动(OnValueChanged 死循环)
  • 全景视频变糊(存疑)

评论