469 lines
11 KiB
C#
469 lines
11 KiB
C#
|
#if AVPROVIDEO_SUPPORT_BUFFERED_DISPLAY
|
|||
|
using System.Collections;
|
|||
|
using System.Collections.Generic;
|
|||
|
using UnityEngine;
|
|||
|
using RenderHeads.Media.AVProVideo;
|
|||
|
|
|||
|
//-----------------------------------------------------------------------------
|
|||
|
// Copyright 2015-2022 RenderHeads Ltd. All rights reserved.
|
|||
|
//-----------------------------------------------------------------------------
|
|||
|
|
|||
|
namespace RenderHeads.Media.AVProVideo.Experimental
|
|||
|
{
|
|||
|
/// <summary>
|
|||
|
/// Syncronise multiple MediaPlayer components (currently Windows ONLY using Media Foundation ONLY)
|
|||
|
/// This feature requires Ultra Edition
|
|||
|
/// </summary>
|
|||
|
[AddComponentMenu("AVPro Video/Media Player Sync (BETA)", -90)]
|
|||
|
[HelpURL("https://www.renderheads.com/products/avpro-video/")]
|
|||
|
public class MediaPlayerSync : MonoBehaviour
|
|||
|
{
|
|||
|
[SerializeField] MediaPlayer _masterPlayer = null;
|
|||
|
[SerializeField] MediaPlayer[] _slavePlayers = null;
|
|||
|
[SerializeField] bool _playOnStart = true;
|
|||
|
[SerializeField] bool _waitAfterPreroll = false;
|
|||
|
[SerializeField] bool _logSyncErrors = false;
|
|||
|
|
|||
|
public MediaPlayer MasterPlayer { get { return _masterPlayer; } set { _masterPlayer = value; } }
|
|||
|
public MediaPlayer[] SlavePlayers { get { return _slavePlayers; } set { _slavePlayers = value; } }
|
|||
|
public bool PlayOnStart { get { return _playOnStart; } set { _playOnStart = value; } }
|
|||
|
public bool WaitAfterPreroll { get { return _waitAfterPreroll; } set { _waitAfterPreroll = value; } }
|
|||
|
public bool LogSyncErrors { get { return _logSyncErrors; } set { _logSyncErrors = value; } }
|
|||
|
|
|||
|
private enum State
|
|||
|
{
|
|||
|
Idle,
|
|||
|
Loading,
|
|||
|
Prerolling,
|
|||
|
Prerolled,
|
|||
|
Playing,
|
|||
|
Finished,
|
|||
|
}
|
|||
|
|
|||
|
private State _state = State.Idle;
|
|||
|
|
|||
|
void Awake()
|
|||
|
{
|
|||
|
#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
|
|||
|
SetupPlayers();
|
|||
|
#else
|
|||
|
Debug.LogError("[AVProVideo] This component only works on the Windows platform");
|
|||
|
this.enabled = false;
|
|||
|
#endif
|
|||
|
}
|
|||
|
|
|||
|
void Start()
|
|||
|
{
|
|||
|
if (_playOnStart)
|
|||
|
{
|
|||
|
StartPlayback();
|
|||
|
_state = State.Loading;
|
|||
|
_playOnStart = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public void OpenMedia(string[] mediaPaths)
|
|||
|
{
|
|||
|
Debug.Assert(mediaPaths.Length == (_slavePlayers.Length + 1));
|
|||
|
|
|||
|
_masterPlayer.MediaSource = MediaSource.Path;
|
|||
|
_masterPlayer.MediaPath = new MediaPath(mediaPaths[0], MediaPathType.AbsolutePathOrURL);
|
|||
|
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
_slavePlayers[i].MediaSource = MediaSource.Path;
|
|||
|
_slavePlayers[i].MediaPath = new MediaPath(mediaPaths[i+1], MediaPathType.AbsolutePathOrURL);
|
|||
|
}
|
|||
|
|
|||
|
StartPlayback();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// This is called when _autoPlay is false and once the MediaPlayers have had their source media set
|
|||
|
/// </summary>
|
|||
|
[ContextMenu("StartPlayback")]
|
|||
|
public void StartPlayback()
|
|||
|
{
|
|||
|
SetupPlayers();
|
|||
|
|
|||
|
if (!IsPrerolled())
|
|||
|
{
|
|||
|
OpenMediaAll();
|
|||
|
_state = State.Loading;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
PlayAll();
|
|||
|
_state = State.Playing;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public void Seek(double time, bool approximate = true)
|
|||
|
{
|
|||
|
if (approximate)
|
|||
|
{
|
|||
|
SeekFastAll(time);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
SeekAll(time);
|
|||
|
}
|
|||
|
|
|||
|
_state = State.Prerolling;
|
|||
|
}
|
|||
|
|
|||
|
public bool IsPrerolled()
|
|||
|
{
|
|||
|
return (_state == State.Prerolled);
|
|||
|
}
|
|||
|
|
|||
|
void SetupPlayers()
|
|||
|
{
|
|||
|
SetupPlayer(_masterPlayer);
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
SetupPlayer(_slavePlayers[i]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SetupPlayer(MediaPlayer player)
|
|||
|
{
|
|||
|
bool isMaster = (player == _masterPlayer);
|
|||
|
player.AutoOpen = false;
|
|||
|
player.AutoStart = false;
|
|||
|
player.AudioMuted = !isMaster;
|
|||
|
player.PlatformOptionsWindows.videoApi = Windows.VideoApi.MediaFoundation;
|
|||
|
player.PlatformOptionsWindows.useLowLatency = true;
|
|||
|
player.PlatformOptionsWindows.pauseOnPrerollComplete = true;
|
|||
|
player.PlatformOptionsWindows.bufferedFrameSelection = isMaster ? BufferedFrameSelectionMode.ElapsedTimeVsynced : BufferedFrameSelectionMode.FromExternalTime;
|
|||
|
}
|
|||
|
|
|||
|
// NOTE: We check on LateUpdate() as MediaPlayer uses Update() to update state and we want to make sure all players have been updated
|
|||
|
void LateUpdate()
|
|||
|
{
|
|||
|
if (_state == State.Idle)
|
|||
|
{
|
|||
|
}
|
|||
|
if (_state == State.Loading)
|
|||
|
{
|
|||
|
UpdateLoading();
|
|||
|
}
|
|||
|
if (_state == State.Prerolling)
|
|||
|
{
|
|||
|
UpdatePrerolling();
|
|||
|
}
|
|||
|
if (_state == State.Prerolled)
|
|||
|
{
|
|||
|
/*if (Input.GetKeyDown(KeyCode.Alpha0))
|
|||
|
{
|
|||
|
StartPlayback();
|
|||
|
}*/
|
|||
|
}
|
|||
|
if (_state == State.Playing)
|
|||
|
{
|
|||
|
UpdatePlaying();
|
|||
|
}
|
|||
|
if (_state == State.Finished)
|
|||
|
{
|
|||
|
}
|
|||
|
|
|||
|
#if UNITY_EDITOR
|
|||
|
if (Input.GetKeyDown(KeyCode.Alpha5))
|
|||
|
{
|
|||
|
Debug.Log("sleep");
|
|||
|
System.Threading.Thread.Sleep(16);
|
|||
|
}
|
|||
|
|
|||
|
/*if (Input.GetKeyDown(KeyCode.Alpha1))
|
|||
|
{
|
|||
|
double time = Random.Range(0f, (float)_masterPlayer.Info.GetDuration());
|
|||
|
Seek(time);
|
|||
|
}
|
|||
|
|
|||
|
long gcMemory = System.GC.GetTotalMemory(false);
|
|||
|
//Debug.Log("GC: " + (gcMemory / 1024) + " " + (gcMemory - lastGcMemory));
|
|||
|
if ((gcMemory - lastGcMemory) < 0)
|
|||
|
{
|
|||
|
Debug.LogWarning("COLLECTION!!! " + (lastGcMemory - gcMemory));
|
|||
|
}
|
|||
|
lastGcMemory = gcMemory;*/
|
|||
|
#endif
|
|||
|
}
|
|||
|
|
|||
|
//long lastGcMemory = 0;
|
|||
|
|
|||
|
void UpdateLoading()
|
|||
|
{
|
|||
|
// Finished loading?
|
|||
|
if (IsAllVideosLoaded())
|
|||
|
{
|
|||
|
// Assign the master and slaves
|
|||
|
_masterPlayer.BufferedDisplay.SetBufferedDisplayMode(BufferedFrameSelectionMode.ElapsedTimeVsynced);
|
|||
|
|
|||
|
IBufferedDisplay[] slaves = new IBufferedDisplay[_slavePlayers.Length];
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
slaves[i] = _slavePlayers[i].BufferedDisplay;
|
|||
|
}
|
|||
|
_masterPlayer.BufferedDisplay.SetSlaves(slaves);
|
|||
|
|
|||
|
//System.Threading.Thread.Sleep(1250);
|
|||
|
|
|||
|
// Begin preroll
|
|||
|
PlayAll();
|
|||
|
|
|||
|
_state = State.Prerolling;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void UpdatePrerolling()
|
|||
|
{
|
|||
|
if (IsAllVideosPaused())
|
|||
|
{
|
|||
|
//System.Threading.Thread.Sleep(250);
|
|||
|
|
|||
|
if (_waitAfterPreroll)
|
|||
|
{
|
|||
|
_state = State.Prerolled;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
PlayAll();
|
|||
|
_state = State.Playing;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void UpdatePlaying()
|
|||
|
{
|
|||
|
if (_masterPlayer.Control.IsPlaying())
|
|||
|
{
|
|||
|
if (_logSyncErrors)
|
|||
|
{
|
|||
|
CheckSync();
|
|||
|
CheckSmoothness();
|
|||
|
}
|
|||
|
|
|||
|
BufferedFramesState state = _masterPlayer.BufferedDisplay.GetBufferedFramesState();
|
|||
|
if (state.bufferedFrameCount < 3)
|
|||
|
{
|
|||
|
//Debug.LogWarning("FORCE SLEEP");
|
|||
|
System.Threading.Thread.Sleep(16);
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Pause slaves
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
MediaPlayer slave = _slavePlayers[i];
|
|||
|
slave.Pause();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Finished?
|
|||
|
if (IsPlaybackFinished(_masterPlayer))
|
|||
|
{
|
|||
|
_state = State.Finished;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private long _lastTimeStamp;
|
|||
|
private int _sameFrameCount;
|
|||
|
|
|||
|
void CheckSmoothness()
|
|||
|
{
|
|||
|
long timeStamp = _masterPlayer.TextureProducer.GetTextureTimeStamp();
|
|||
|
//int frameCount = _masterPlayer.TextureProducer.GetTextureFrameCount();
|
|||
|
long frameDuration = (long)(10000000f / _masterPlayer.Info.GetVideoFrameRate());
|
|||
|
|
|||
|
long vsyncDuration = (long)((QualitySettings.vSyncCount * 10000000f) / (float)Screen.currentResolution.refreshRate);
|
|||
|
float vsyncFrames = (float)vsyncDuration / frameDuration;
|
|||
|
|
|||
|
float fractionalFrames = vsyncFrames - Mathf.FloorToInt(vsyncFrames);
|
|||
|
|
|||
|
if (fractionalFrames == 0f)
|
|||
|
{
|
|||
|
if (QualitySettings.vSyncCount != 0)
|
|||
|
{
|
|||
|
if (!Mathf.Approximately(_sameFrameCount, vsyncFrames))
|
|||
|
{
|
|||
|
Debug.LogWarning("Frame " + timeStamp + " was shown for " + _sameFrameCount + " frames instead of expected " + vsyncFrames);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
long d = (timeStamp - _lastTimeStamp);
|
|||
|
if (d != 0)
|
|||
|
{
|
|||
|
long threshold = 10000;
|
|||
|
if (d > frameDuration + threshold ||
|
|||
|
d < frameDuration - threshold)
|
|||
|
{
|
|||
|
Debug.LogWarning("Possible frame skip, " + timeStamp + " " + d);
|
|||
|
}
|
|||
|
|
|||
|
_sameFrameCount = 1;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_sameFrameCount++;
|
|||
|
}
|
|||
|
|
|||
|
_lastTimeStamp = timeStamp;
|
|||
|
//Debug.Log(frameDuration);
|
|||
|
}
|
|||
|
|
|||
|
void CheckSync()
|
|||
|
{
|
|||
|
long timeStamp = _masterPlayer.TextureProducer.GetTextureTimeStamp();
|
|||
|
|
|||
|
bool inSync = true;
|
|||
|
foreach (MediaPlayer slavePlayer in _slavePlayers)
|
|||
|
{
|
|||
|
if (slavePlayer.TextureProducer.GetTextureTimeStamp() != timeStamp)
|
|||
|
{
|
|||
|
inSync = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!inSync)
|
|||
|
{
|
|||
|
LogSyncState();
|
|||
|
Debug.LogWarning("OUT OF SYNC!!!!!!!");
|
|||
|
//Debug.Break();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
//LogSyncState();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void LogSyncState()
|
|||
|
{
|
|||
|
string text = "Time - Full,Free\t\tRange\n";
|
|||
|
text += LogSyncState(_masterPlayer) + "\n";
|
|||
|
foreach (MediaPlayer slavePlayer in _slavePlayers)
|
|||
|
{
|
|||
|
text += LogSyncState(slavePlayer) + "\n";
|
|||
|
}
|
|||
|
Debug.Log(text);
|
|||
|
}
|
|||
|
|
|||
|
string LogSyncState(MediaPlayer player)
|
|||
|
{
|
|||
|
BufferedFramesState state = player.BufferedDisplay.GetBufferedFramesState();
|
|||
|
long timeStamp = player.TextureProducer.GetTextureTimeStamp();
|
|||
|
string result = string.Format("{4} - {2},{3}\t\t{0}-{1} ({5})", state.minTimeStamp, state.maxTimeStamp, state.bufferedFrameCount, state.freeFrameCount, timeStamp, Time.deltaTime);
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
void OpenMediaAll()
|
|||
|
{
|
|||
|
_masterPlayer.OpenMedia(autoPlay:false);
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
_slavePlayers[i].OpenMedia(autoPlay:false);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void PauseAll()
|
|||
|
{
|
|||
|
_masterPlayer.Pause();
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
_slavePlayers[i].Pause();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void PlayAll()
|
|||
|
{
|
|||
|
_masterPlayer.Play();
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
_slavePlayers[i].Play();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SeekAll(double time)
|
|||
|
{
|
|||
|
_masterPlayer.Control.Seek(time);
|
|||
|
foreach (MediaPlayer player in _slavePlayers)
|
|||
|
{
|
|||
|
player.Control.Seek(time);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SeekFastAll(double time)
|
|||
|
{
|
|||
|
_masterPlayer.Control.SeekFast(time);
|
|||
|
foreach (MediaPlayer player in _slavePlayers)
|
|||
|
{
|
|||
|
player.Control.SeekFast(time);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
bool IsAllVideosLoaded()
|
|||
|
{
|
|||
|
bool result = false;
|
|||
|
if (IsVideoLoaded(_masterPlayer))
|
|||
|
{
|
|||
|
result = true;
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
if (!IsVideoLoaded(_slavePlayers[i]))
|
|||
|
{
|
|||
|
result = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
bool IsAllVideosPaused()
|
|||
|
{
|
|||
|
bool result = false;
|
|||
|
if (IsVideoPaused(_masterPlayer))
|
|||
|
{
|
|||
|
result = true;
|
|||
|
for (int i = 0; i < _slavePlayers.Length; i++)
|
|||
|
{
|
|||
|
if (!IsVideoPaused(_slavePlayers[i]))
|
|||
|
{
|
|||
|
result = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
static bool IsPlaybackFinished(MediaPlayer player)
|
|||
|
{
|
|||
|
bool result = false;
|
|||
|
if (player != null && player.Control != null)
|
|||
|
{
|
|||
|
if (player.Control.IsFinished())
|
|||
|
{
|
|||
|
BufferedFramesState state = player.BufferedDisplay.GetBufferedFramesState();
|
|||
|
if (state.bufferedFrameCount == 0)
|
|||
|
{
|
|||
|
result = true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
static bool IsVideoLoaded(MediaPlayer player)
|
|||
|
{
|
|||
|
return (player != null && player.Control != null && player.Control.HasMetaData() && player.Control.CanPlay());
|
|||
|
}
|
|||
|
static bool IsVideoPaused(MediaPlayer player)
|
|||
|
{
|
|||
|
return (player != null && player.Control != null && player.Control.IsPaused());
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
#endif
|