Currently video playback is handled this way:
- the video is set via a url and prepared
- when finished preparing we call VideoPlayer.Play()
- when the frame is ready and the video is playing we call VideoPlayer.Pause()
- then from fixed update we’re calling SetTargetTime(), which updates the Video Player via VideoPlayer.time
We’re playing the video this way to be able to smoothly scrub in either direction and control playback with more precision.
Originally in 2020.1.16 this was working like a charm, but since updating to 2020.3.25 the video playback is choppy and lagging. Upon further inspection it seems that VideoPlayer.time is not actually being updated when set, and only updates its value after several frames.
I also noticed that the VideoPlayer values (e.g. frameRate, length, width, height) which used to be populated after VideoPlayer.prepareCompleted are all zero after prepareCompleted. I found a workaround by using a callback for onSeekCompleted, and this has worked for getting a reference to the correct video values, but I wasn’t sure if this was at all related to the laggy playback.
Any help is much appreciated. Code is below:
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using TMPro;
public class VideoPlayback_Unity : MonoBehaviour, IVideoPlaybackSource
{
private Action<IVideoPlaybackSource, VideoPlaybackState> _stateChangeCallback;
private VideoPlayer _player;
private Vector2 _size;
private double _time;
private double _startTime;
private double _frameTime;
private double _frameRate;
private float _length;
private float _masterVolume;
private long _frameCount;
private int _frame;
private bool _started;
private const int MinFramesBeforeFinish = 20;
public bool FrameReady { get; private set; }
public Texture Texture { get { return _player.texture; } }
public string Id { get { return _player != null ? System.IO.Path.GetFileNameWithoutExtension( _player.url ) : "None Loaded"; } }
public void Create(Action<IVideoPlaybackSource, VideoPlaybackState> stateChangeCallback)
{
_stateChangeCallback = stateChangeCallback;
_player = gameObject.AddComponent<VideoPlayer>();
_player.isLooping = false;
_player.playOnAwake = false;
_player.sendFrameReadyEvents = true;
_player.audioOutputMode = VideoAudioOutputMode.None;
_player.controlledAudioTrackCount = 0;
_player.prepareCompleted += OnPlayerPrepareCompleted;
_player.frameReady += OnFrameReady;
_player.started += OnPlayerStarted;
_player.loopPointReached += OnPlayerLoopPointReached;
_player.errorReceived += OnPlayerErrorReceived;
}
public void SetTargetTime(double time)
{
time = Math.Min(Math.Max(time, 0.0), _length);
_player.time = _time = time;
_frame = Mathf.Clamp((int)Math.Round((_frameRate * time) + 1), 1, (int)_frameCount);
}
public bool CheckForFinish()
{
bool finished = false;
if (_started)
{
finished = _player.frame <= MinFramesBeforeFinish ||
Mathf.Abs((long)_player.frameCount - _player.frame) <= MinFramesBeforeFinish;
}
return finished;
}
public void Load(string filename, double time = 0.0, float duration = 0.0f)
{
Debug.LogFormat("Looking for clip: {0}", filename);
_startTime = time;
_length = duration;
_player.source = VideoSource.Url;
_player.url = filename + ".mp4";
_player.audioOutputMode = VideoAudioOutputMode.None;
_player.controlledAudioTrackCount = 0;
_player.sendFrameReadyEvents = true;
FrameReady = false;
_started = false;
_player.seekCompleted += OnSeekCompleted;
_player.Prepare();
}
public Vector2 GetSize()
{
return _size;
}
public int GetFrame()
{
return _frame;
}
public double GetFrameTime()
{
return (_frame - 1) / _frameRate;
}
public long GetTotalFrames()
{
return _frameCount;
}
public bool IsPrepared()
{
return _player.isPrepared;
}
public bool IsPlaying()
{
return _player.isPlaying;
}
public void Play()
{
_player.Play();
}
public void Pause()
{
_player.Pause();
}
public void Stop()
{
_player.Stop();
}
public void SetVolume(float volume)
{
_masterVolume = volume;
_player.SetDirectAudioVolume(0, _masterVolume);
}
private void OnPlayerPrepareCompleted(VideoPlayer source)
{
_player.time = _time = _startTime;
}
private void OnSeekCompleted(VideoPlayer source)
{
if (_length == 0.0f)
{
_length = (float)_player.length;
}
_frameRate = _player.frameRate;
_frameCount = (long)Math.Round(_length * _frameRate);
_size = new Vector2(_player.texture.width, _player.texture.height);
if (_player.pixelAspectRatioNumerator != 1.0f || _player.pixelAspectRatioDenominator != 1.0f)
{
Debug.LogWarningFormat("Warning: Loaded Video {0} does not use square pixels ({1}:{2})", _player.url, _player.pixelAspectRatioNumerator, _player.pixelAspectRatioDenominator);
}
_stateChangeCallback?.Invoke(this, VideoPlaybackState.Prepared);
source.seekCompleted -= OnSeekCompleted;
}
private void OnPlayerStarted(VideoPlayer source)
{
_stateChangeCallback?.Invoke(this, VideoPlaybackState.Playing);
_started = true;
}
private void OnPlayerLoopPointReached(VideoPlayer source)
{
_stateChangeCallback?.Invoke(this, VideoPlaybackState.Finished);
}
private void OnPlayerErrorReceived(VideoPlayer source, string message)
{
_stateChangeCallback?.Invoke(this, VideoPlaybackState.Error);
}
private void OnFrameReady(VideoPlayer source, long frameIndex)
{
FrameReady = true;
_player.sendFrameReadyEvents = false;
}
}
