挺久没有写博客了,这是2020年的第一篇博客,先说说写这篇博客的原因吧。

去年下半年负责一个新项目,项目里面有需要播放大量视频的需求,由于Demo时间比较紧急就没有下功夫去做这块。

问题1:改变视频进度时进度条出现回滚。

问题2:改变视频进度时出现音画不一致问题,音频较视频有延迟。

问题3:开发周期小,时间紧急,代码不规范。

马上就要春节放假了,赶在了放假之前把关卡都完成了,今天有空就重构了一下视频播放的代码,公司工作嗨不起来,回家一边嗨一边重构代码吧。

Demo代码  https://github.com/wuxiaomu/VideoPlayerDemo

我用的是unity自带的视频播放器组件VideoPlayer,很多人觉得VideoPlayer贼难用,确实!因为unity没有把视频播放器的组件整合起来,只是提供了显示视频的组件,不会用的人当然会说不好用啦,毕竟unity主要是用来做游戏开发的,视频播放的需求没有那么的重要。


unity UMP 播放监控 Android unity 播放器_VideoPlayer

猫小帅的视频播放功能

 

VideoPlayer介绍

unity UMP 播放监控 Android unity 播放器_ide_02

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 。

unity UMP 播放监控 Android unity 播放器_Source_03

unity UMP 播放监控 Android unity 播放器_VideoPlayer_04