挺久没有写博客了,这是2020年的第一篇博客,先说说写这篇博客的原因吧。
去年下半年负责一个新项目,项目里面有需要播放大量视频的需求,由于Demo时间比较紧急就没有下功夫去做这块。
问题1:改变视频进度时进度条出现回滚。
问题2:改变视频进度时出现音画不一致问题,音频较视频有延迟。
问题3:开发周期小,时间紧急,代码不规范。
马上就要春节放假了,赶在了放假之前把关卡都完成了,今天有空就重构了一下视频播放的代码,公司工作嗨不起来,回家一边嗨一边重构代码吧。
Demo代码 https://github.com/wuxiaomu/VideoPlayerDemo
我用的是unity自带的视频播放器组件VideoPlayer,很多人觉得VideoPlayer贼难用,确实!因为unity没有把视频播放器的组件整合起来,只是提供了显示视频的组件,不会用的人当然会说不好用啦,毕竟unity主要是用来做游戏开发的,视频播放的需求没有那么的重要。
猫小帅的视频播放功能
VideoPlayer介绍
Source:代表播放源的来源,有Video Clip,URL两个选项,前者需要直接手动引用视频资源,后者的既可以在栏URL直接填链接地址,也可以在代码中指定,网络链接和本地地址皆可,这里用的就是URL源。
Play On Awake:字面意思,组件激活时就直接播放。
Loop:字面意思,循环播放。
Playback Speed:字面意思,播放速度。
Render Mode:字面意思,渲染模式,这个需要详细介绍一下,包含5个选项,
- CameraFarPlane(基于摄像机的渲染,渲染在摄像机的远平面上,需要设置用于渲染的摄像机,同时可以修改alpha通道的值做透明效果,可用于背景播放器,我用这种渲染模式)
- CameraNearPlane(基于摄像机的渲染,渲染在摄像机的近平面上,需要设置用于渲染的摄像机,同时可以修改alpha通道的值做透明效果,可用作前景播放器)
- RenderTexture(渲染在RenderTexture上,可以用来做基于UGUI的播放器)
- MaterialOverride(将视频画面复制给所选Render的Material。需要选择具有Render组件的物体,可以选择赋值的材质属性。可制作360全景视频和VR视频)
- APIOnly
AspectRatio:屏幕长宽比适应。
AudioOutputMode:音频输出模式,
- None:不播放声音
- AudioSource:用AudioSource播放使用
- ControlledTracks:控制音轨,填需要的数量,再把对应AudioSource节点拖上去引用
VideoController视频控制器
新建视频控制器脚本“VideoController”,主要是提供了各种控制视频的属性和方法,和UI层分开。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Video;
public class VideoController : MonoBehaviour
{
[SerializeField]
private bool startAfterPreparation = true;
[Header("Optional")]
[SerializeField]
private VideoPlayer videoPlayer;
[SerializeField]
private AudioSource audioSource;
[Header("Events")]
[SerializeField]
private UnityEvent onPrepared = new UnityEvent();
[SerializeField]
private UnityEvent onStartedPlaying = new UnityEvent();
[SerializeField]
private UnityEvent onFinishedPlaying = new UnityEvent();
#region Properties
public bool StartAfterPreparation
{
get { return startAfterPreparation; }
set { startAfterPreparation = value; }
}
public UnityEvent OnPrepared
{
get { return onPrepared; }
}
public UnityEvent OnStartedPlaying
{
get { return onStartedPlaying; }
}
public UnityEvent OnFinishedPlaying
{
get { return onFinishedPlaying; }
}
public ulong Time
{
get { return (ulong)videoPlayer.time; }
}
public bool IsPlaying
{
get { return videoPlayer.isPlaying; }
}
public bool IsPrepared
{
get { return videoPlayer.isPrepared; }
}
public float NormalizedTime
{
get { return (float)(videoPlayer.time / Duration); }
}
public ulong Duration
{
get {
return videoPlayer.frameCount / (ulong)videoPlayer.frameRate;
}
}
public float Volume
{
get { return audioSource == null ? videoPlayer.GetDirectAudioVolume(0) : audioSource.volume; }
set
{
if (audioSource == null)
videoPlayer.SetDirectAudioVolume(0, value);
else
audioSource.volume = value;
}
}
#endregion
#region Unity Methods
// Start is called before the first frame update
void Start()
{
if (videoPlayer == null)
{
SubscribeToVideoPlayerEvents();
}
videoPlayer.playOnAwake = false;
}
// Update is called once per frame
void Update()
{
}
private void OnEnable()
{
SubscribeToVideoPlayerEvents();
}
private void OnDisable()
{
UnsubscribeFromVideoPlayerEvents();
}
#endregion
#region Public Methods
public void PrepareForUrl(string url)
{
Debug.Log("PrepareForUrl");
videoPlayer.source = VideoSource.Url;
videoPlayer.url = url;
videoPlayer.Prepare();
}
public void PrepareForClip(VideoClip clip)
{
videoPlayer.source = VideoSource.VideoClip;
videoPlayer.clip = clip;
videoPlayer.Prepare();
}
public void Play()
{
Debug.Log("Play");
if (!IsPrepared)
{
videoPlayer.Prepare();
return;
}
videoPlayer.Play();
}
public void Pause()
{
videoPlayer.Pause();
}
public void TogglePlayPause()
{
if (IsPlaying)
{
Pause();
}
else
{
Play();
}
}
public void Seek(float time)
{
time = Mathf.Clamp(time, 0, 1);
XMDebug.Log(time, Duration, time * Duration);
videoPlayer.time = time * Duration;
}
#endregion
#region Private Methods
private void OnPrepareCompleted(VideoPlayer source)
{
Debug.Log("OnPrepareCompleted");
onPrepared.Invoke();
SetupAudio();
if (StartAfterPreparation)
Play();
}
private void OnStarted(VideoPlayer source)
{
onStartedPlaying.Invoke();
}
private void OnFinished(VideoPlayer source)
{
onFinishedPlaying.Invoke();
}
private void OnError(VideoPlayer source, string message)
{
Debug.LogError("OnError " + message);
}
private void SetupAudio()
{
Debug.Log("SetupAudio");
if (videoPlayer.audioTrackCount <= 0)
return;
if (audioSource == null && videoPlayer.canSetDirectAudioVolume)
{
videoPlayer.audioOutputMode = VideoAudioOutputMode.Direct;
}
else
{
videoPlayer.audioOutputMode = VideoAudioOutputMode.AudioSource;
videoPlayer.SetTargetAudioSource(0, audioSource);
}
videoPlayer.controlledAudioTrackCount = 1;
videoPlayer.EnableAudioTrack(0, true);
}
private void SubscribeToVideoPlayerEvents()
{
if (videoPlayer == null)
return;
videoPlayer.errorReceived += OnError;
videoPlayer.prepareCompleted += OnPrepareCompleted;
videoPlayer.started += OnStarted;
videoPlayer.loopPointReached += OnFinished;
}
private void UnsubscribeFromVideoPlayerEvents()
{
if (videoPlayer == null)
return;
videoPlayer.errorReceived -= OnError;
videoPlayer.prepareCompleted -= OnPrepareCompleted;
videoPlayer.started -= OnStarted;
videoPlayer.loopPointReached -= OnFinished;
}
#endregion
}
UIVideoPanel视频播放器界面
新建视频播放器界面脚本“UIVideoPanel”。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using UnityEngine.Video;
[Serializable]
public class FloatEvent : UnityEvent<float> { }
public class UIVideoPanel : MonoBehaviour
{
private VideoController controller;
[SerializeField]
private VideoClip clip;
[SerializeField]
private GameObject StartPanel;
[SerializeField]
private Button PlayBtn;
[SerializeField]
private Button PlayPauseBtn;
[SerializeField]
private Text PlayBtnTxt;
[SerializeField]
private Slider PositionSlider;
[SerializeField]
private Slider PreviewSlider;
[SerializeField]
private FloatEvent onSeeked = new FloatEvent();
// Start is called before the first frame update
void Start()
{
controller = GetComponent<VideoController>();
PlayBtn.onClick.AddListener(PlayVideo);
PlayPauseBtn.onClick.AddListener(ToggleIsPlaying);
PositionSlider.onValueChanged.AddListener(SliderValueChanged);
StartPanel.SetActive(true);
}
private void OnDestroy()
{
PlayBtn.onClick.RemoveListener(PlayVideo);
PlayPauseBtn.onClick.RemoveListener(ToggleIsPlaying);
PositionSlider.onValueChanged.RemoveListener(SliderValueChanged);
}
// Update is called once per frame
void Update()
{
if (controller.IsPlaying)
{
PreviewSlider.value = controller.NormalizedTime;
}
}
private void PlayVideo()
{
StartPanel.SetActive(false);
controller.PrepareForUrl("C:/Users/4399/AppData/LocalLow/haizileyuan/猫小帅学英语/Android/step/test1/l1t7-2.mp4");
PlayBtnTxt.text = "Pause";
}
private void ToggleIsPlaying()
{
if (controller.IsPlaying)
{
controller.Pause();
PlayBtnTxt.text = "Play";
}
else
{
controller.Play();
PlayBtnTxt.text = "Pause";
}
}
private void SliderValueChanged(float value)
{
onSeeked.Invoke(value);
}
}
注:以上代码是视频播放器demo的代码,非项目代码。
视频播放器需要Video Player和Audio Source,一个播放视频,一个播放声音。通过SetTargetAudioSource设置音频。
videoPlayer.SetTargetAudioSource(0, audioSource);
进度条需要一个用来控制进度(PositionSlider),一个用来显示进度(PreviewSlider)。
切换进度条是利用Seek()方法控制视频的videoPlayer.time 。