1576 lines
51 KiB
C#
1576 lines
51 KiB
C#
|
|
|||
|
#define USE_SERVER_TIME_MS // Uses GetServerTimeMilliseconds instead of the server datetime which in theory is less reliable
|
|||
|
|
|||
|
using JetBrains.Annotations;
|
|||
|
using UdonSharp;
|
|||
|
using UnityEngine;
|
|||
|
using VRC.SDK3.Components.Video;
|
|||
|
using VRC.SDKBase;
|
|||
|
using TMPro;
|
|||
|
|
|||
|
namespace UdonSharp.Video
|
|||
|
{
|
|||
|
[AddComponentMenu("Udon Sharp/Video/USharp Video Player")]
|
|||
|
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
|
|||
|
public class USharpVideoPlayer : UdonSharpBehaviour
|
|||
|
{
|
|||
|
// Video player references
|
|||
|
VideoPlayerManager _videoPlayerManager;
|
|||
|
|
|||
|
public name_gen _name_gen;
|
|||
|
|
|||
|
|
|||
|
[Tooltip("Whether to allow video seeking with the progress bar on the video")]
|
|||
|
[PublicAPI]
|
|||
|
public bool allowSeeking = true;
|
|||
|
|
|||
|
[Tooltip("If enabled defaults to unlocked so anyone can put in a URL")]
|
|||
|
[SerializeField]
|
|||
|
bool defaultUnlocked = true;
|
|||
|
|
|||
|
|
|||
|
|
|||
|
[Tooltip("If enabled allows the instance creator to always control the video player regardless of if they are master or not")]
|
|||
|
[PublicAPI]
|
|||
|
public bool allowInstanceCreatorControl = true;
|
|||
|
|
|||
|
[Tooltip("How often the video player should check if it is more than Sync Threshold out of sync with the video time")]
|
|||
|
[PublicAPI]
|
|||
|
public float syncFrequency = 8.0f;
|
|||
|
[Tooltip("How many seconds desynced from the owner the client needs to be to trigger a resync")]
|
|||
|
[PublicAPI]
|
|||
|
public float syncThreshold = 0.85f;
|
|||
|
|
|||
|
[Range(0f, 1f)]
|
|||
|
[Tooltip("The default volume for the volume slider on the video player")]
|
|||
|
[SerializeField]
|
|||
|
float defaultVolume = 0.5f;
|
|||
|
|
|||
|
#pragma warning disable CS0414
|
|||
|
[Tooltip("The max range of the audio sources on this video player")]
|
|||
|
[SerializeField]
|
|||
|
float audioRange = 40f;
|
|||
|
|
|||
|
#pragma warning restore CS0414
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Local offset from the network time to sync the video
|
|||
|
/// Can be used for things like making a video player sync up with someone singing
|
|||
|
/// </summary>
|
|||
|
[PublicAPI, System.NonSerialized]
|
|||
|
public float localSyncOffset = 0f;
|
|||
|
|
|||
|
[Tooltip("List of urls to play automatically when the world is loaded until someone puts in another URL")]
|
|||
|
public VRCUrl[] playlist = new VRCUrl[0];
|
|||
|
|
|||
|
[Tooltip("Should default to the stream player? This is usually used when you want to put a live stream in the default playlist.")]
|
|||
|
[SerializeField]
|
|||
|
bool defaultStreamMode = false;
|
|||
|
|
|||
|
[Tooltip("If the default playlist should loop")]
|
|||
|
[PublicAPI]
|
|||
|
public bool loopPlaylist = false;
|
|||
|
|
|||
|
[Tooltip("If the default playlist should be shuffled upon world load")]
|
|||
|
public bool shufflePlaylist = false;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// The URL that we should currently be playing and that other people are playing
|
|||
|
/// </summary>
|
|||
|
[UdonSynced]
|
|||
|
VRCUrl _syncedURL = VRCUrl.Empty;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// The video sequence identifier, gets incremented whenever a new video is put in. Used to determine in combination with _currentVideoIdx if we need to load the new URL
|
|||
|
/// </summary>
|
|||
|
[UdonSynced]
|
|||
|
int _syncedVideoIdx;
|
|||
|
int _currentVideoIdx;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// If we're locked so only the master may put in URLs
|
|||
|
/// </summary>
|
|||
|
[UdonSynced]
|
|||
|
bool _isMasterOnly = true;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
int _nextPlaylistIndex = 0;
|
|||
|
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
[UdonSynced]
|
|||
|
int _networkTimeVideoStart;
|
|||
|
int _localNetworkTimeStart;
|
|||
|
#else
|
|||
|
[UdonSynced]
|
|||
|
double _videoStartNetworkTime;
|
|||
|
double _localVideoStartTime;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
long _networkTimeStart;
|
|||
|
System.DateTime _localNetworkTimeStart;
|
|||
|
#endif
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
bool _ownerPlaying;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
bool _ownerPaused;
|
|||
|
bool _locallyPaused;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
bool _loopVideo = false;
|
|||
|
bool _localLoopVideo = false;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
int _shuffleSeed;
|
|||
|
|
|||
|
// The last unpaused time in the video
|
|||
|
float _lastVideoTime;
|
|||
|
|
|||
|
VideoControlHandler[] _registeredControlHandlers;
|
|||
|
VideoScreenHandler[] _registeredScreenHandlers;
|
|||
|
UdonSharpBehaviour[] _registeredCallbackReceivers;
|
|||
|
|
|||
|
// Video loading state
|
|||
|
const int MAX_RETRY_COUNT = 4;
|
|||
|
const float DEFAULT_RETRY_TIMEOUT = 35.0f;
|
|||
|
const float RATE_LIMIT_RETRY_TIMEOUT = 5.5f;
|
|||
|
const float VIDEO_ERROR_RETRY_TIMEOUT = 5f;
|
|||
|
const float PLAYLIST_ERROR_RETRY_COUNT = 4;
|
|||
|
|
|||
|
bool _loadingVideo = false;
|
|||
|
float _currentLoadingTime = 0f; // Counts down to 0 while loading
|
|||
|
int _currentRetryCount = 0;
|
|||
|
float _videoTargetStartTime = 0f;
|
|||
|
int _playlistErrorCount = 0;
|
|||
|
|
|||
|
bool _waitForSync = false;
|
|||
|
|
|||
|
// Player mode tracking
|
|||
|
const int PLAYER_MODE_UNITY = 0;
|
|||
|
const int PLAYER_MODE_AVPRO = 1;
|
|||
|
|
|||
|
[UdonSynced]
|
|||
|
int currentPlayerMode = PLAYER_MODE_UNITY;
|
|||
|
int _localPlayerMode = PLAYER_MODE_UNITY;
|
|||
|
|
|||
|
bool _videoSync = true;
|
|||
|
|
|||
|
bool _ranInit = false;
|
|||
|
|
|||
|
void Start()
|
|||
|
{
|
|||
|
|
|||
|
if (_ranInit)
|
|||
|
return;
|
|||
|
|
|||
|
_ranInit = true;
|
|||
|
|
|||
|
_videoPlayerManager = GetVideoManager();
|
|||
|
_videoPlayerManager.Start();
|
|||
|
|
|||
|
if (_registeredControlHandlers == null)
|
|||
|
_registeredControlHandlers = new VideoControlHandler[0];
|
|||
|
|
|||
|
if (_registeredCallbackReceivers == null)
|
|||
|
_registeredCallbackReceivers = new UdonSharpBehaviour[0];
|
|||
|
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
{
|
|||
|
if (defaultUnlocked)
|
|||
|
_isMasterOnly = false;
|
|||
|
|
|||
|
if (defaultStreamMode)
|
|||
|
{
|
|||
|
SetPlayerMode(PLAYER_MODE_AVPRO);
|
|||
|
_nextPlaylistIndex = 0; // SetPlayerMode sets this to -1, but we want to be able to keep it intact so reset to 0
|
|||
|
}
|
|||
|
|
|||
|
_shuffleSeed = Random.Range(0, 10000);
|
|||
|
}
|
|||
|
|
|||
|
_lastMasterLocked = _isMasterOnly;
|
|||
|
|
|||
|
SetUILocked(_isMasterOnly);
|
|||
|
|
|||
|
#if !USE_SERVER_TIME_MS
|
|||
|
_networkTimeStart = Networking.GetNetworkDateTime().Ticks;
|
|||
|
_localNetworkTimeStart = new System.DateTime(_networkTimeStart, System.DateTimeKind.Utc);
|
|||
|
#endif
|
|||
|
|
|||
|
PlayNextVideoFromPlaylist();
|
|||
|
|
|||
|
SetVolume(defaultVolume);
|
|||
|
|
|||
|
// Serialize the default setup state from the master once regardless of if a video has played
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
public override void OnVideoReady()
|
|||
|
{
|
|||
|
ResetVideoLoad();
|
|||
|
_playlistErrorCount = 0;
|
|||
|
|
|||
|
if (IsUsingAVProPlayer())
|
|||
|
{
|
|||
|
float duration = _videoPlayerManager.GetDuration();
|
|||
|
|
|||
|
if (duration == float.MaxValue || float.IsInfinity(duration) || IsRTSPStream())
|
|||
|
_videoSync = false;
|
|||
|
else
|
|||
|
_videoSync = true;
|
|||
|
}
|
|||
|
else
|
|||
|
_videoSync = true;
|
|||
|
|
|||
|
if (_videoSync)
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
{
|
|||
|
_waitForSync = false;
|
|||
|
_videoPlayerManager.Play();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if (_ownerPlaying)
|
|||
|
{
|
|||
|
_waitForSync = false;
|
|||
|
_locallyPaused = false;
|
|||
|
_videoPlayerManager.Play();
|
|||
|
|
|||
|
SyncVideo();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
if (_networkTimeVideoStart == 0)
|
|||
|
#else
|
|||
|
if (_videoStartNetworkTime == 0f || _videoStartNetworkTime > GetNetworkTime() - _videoPlayerManager.GetDuration()) // Todo: remove the 0f check and see how this actually gets set to 0 while the owner is playing
|
|||
|
#endif
|
|||
|
{
|
|||
|
_waitForSync = true;
|
|||
|
SetStatusText("Waiting for owner sync...");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_waitForSync = false;
|
|||
|
SyncVideo();
|
|||
|
SetStatusText("");
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
LogMessage($"Loaded into world with complete video, duration: {_videoPlayerManager.GetDuration()}, start net time: {_networkTimeVideoStart}");
|
|||
|
#else
|
|||
|
LogMessage($"Loaded into world with complete video, duration: {_videoPlayerManager.GetDuration()}, start net time: {_videoStartNetworkTime}, subtracted net time {GetNetworkTime() - _videoPlayerManager.GetDuration()}");
|
|||
|
#endif
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else // Live streams should start asap
|
|||
|
{
|
|||
|
_waitForSync = false;
|
|||
|
_videoPlayerManager.Play();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public override void OnVideoStart()
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
{
|
|||
|
SetPausedInternal(false, false);
|
|||
|
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_networkTimeVideoStart = Networking.GetServerTimeInMilliseconds() - (int)(_videoTargetStartTime * 1000f);
|
|||
|
#else
|
|||
|
_videoStartNetworkTime = GetNetworkTime() - _videoTargetStartTime;
|
|||
|
#endif
|
|||
|
|
|||
|
_videoPlayerManager.SetTime(_videoTargetStartTime);
|
|||
|
|
|||
|
_ownerPlaying = true;
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
LogMessage($"Started video: {_syncedURL}");
|
|||
|
}
|
|||
|
else if (!_ownerPlaying) // Watchers pause and wait for sync from owner
|
|||
|
{
|
|||
|
_videoPlayerManager.Pause();
|
|||
|
_waitForSync = true;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
SetPausedInternal(_ownerPaused, false);
|
|||
|
SyncVideo();
|
|||
|
LogMessage($"Started video: {_syncedURL}");
|
|||
|
}
|
|||
|
|
|||
|
SetStatusText("");
|
|||
|
|
|||
|
_videoTargetStartTime = 0f;
|
|||
|
|
|||
|
SetUIPaused(_locallyPaused);
|
|||
|
|
|||
|
UpdateRenderTexture();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoPlay");
|
|||
|
}
|
|||
|
|
|||
|
bool IsRTSPStream()
|
|||
|
{
|
|||
|
string urlStr = _syncedURL.ToString();
|
|||
|
|
|||
|
return IsUsingAVProPlayer() &&
|
|||
|
_videoPlayerManager.GetDuration() == 0f &&
|
|||
|
IsRTSPURL(urlStr);
|
|||
|
}
|
|||
|
|
|||
|
bool IsRTSPURL(string urlStr)
|
|||
|
{
|
|||
|
return urlStr.StartsWith("rtsp://", System.StringComparison.OrdinalIgnoreCase) ||
|
|||
|
urlStr.StartsWith("rtmp://", System.StringComparison.OrdinalIgnoreCase) || // RTMP isn't really supported in VRC's context and it's probably never going to be, but we'll just be safe here
|
|||
|
urlStr.StartsWith("rtspt://", System.StringComparison.OrdinalIgnoreCase) || // rtsp over TCP
|
|||
|
urlStr.StartsWith("rtspu://", System.StringComparison.OrdinalIgnoreCase); // rtsp over UDP
|
|||
|
}
|
|||
|
|
|||
|
public override void OnVideoEnd()
|
|||
|
{
|
|||
|
// VRC falsely throws OnVideoEnd instantly on RTSP streams since they report 0 length
|
|||
|
if (!IsRTSPStream())
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
{
|
|||
|
_ownerPlaying = false;
|
|||
|
_ownerPaused = _locallyPaused = false;
|
|||
|
|
|||
|
SetStatusText("");
|
|||
|
SetUIPaused(false);
|
|||
|
|
|||
|
PlayNextVideoFromPlaylist();
|
|||
|
QueueSerialize();
|
|||
|
}
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoEnd");
|
|||
|
|
|||
|
UpdateRenderTexture();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Workaround for bug that needs to be addressed in U# where calling built in methods with parameters will get the parameters overwritten when called from other UdonBehaviours
|
|||
|
public void _OnVideoErrorCallback(VideoError videoError)
|
|||
|
{
|
|||
|
OnVideoError(videoError);
|
|||
|
}
|
|||
|
|
|||
|
public override void OnVideoError(VideoError videoError)
|
|||
|
{
|
|||
|
if (videoError == VideoError.RateLimited)
|
|||
|
{
|
|||
|
SetStatusText("Rate limited, retrying...");
|
|||
|
LogWarning("Rate limited, retrying...");
|
|||
|
_currentLoadingTime = RATE_LIMIT_RETRY_TIMEOUT;
|
|||
|
return;
|
|||
|
}
|
|||
|
else if (videoError == VideoError.PlayerError)
|
|||
|
{
|
|||
|
SetStatusText("Video error, retrying...");
|
|||
|
LogError("Video player error when trying to load " + _syncedURL);
|
|||
|
_loadingVideo = true; // Apparently OnVideoReady gets fired erroneously??
|
|||
|
_currentLoadingTime = VIDEO_ERROR_RETRY_TIMEOUT;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
ResetVideoLoad();
|
|||
|
_videoTargetStartTime = 0f;
|
|||
|
|
|||
|
_videoPlayerManager.Stop();
|
|||
|
|
|||
|
LogError($"Video '{_syncedURL}' failed to play with error {videoError}");
|
|||
|
|
|||
|
switch (videoError)
|
|||
|
{
|
|||
|
case VideoError.InvalidURL:
|
|||
|
SetStatusText("Invalid URL");
|
|||
|
break;
|
|||
|
case VideoError.AccessDenied:
|
|||
|
SetStatusText("Video blocked, enabled untrusted URLs");
|
|||
|
break;
|
|||
|
default:
|
|||
|
SetStatusText("Failed to load video");
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
++_playlistErrorCount;
|
|||
|
PlayNextVideoFromPlaylist();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoError");
|
|||
|
}
|
|||
|
|
|||
|
public override void OnVideoPause() { }
|
|||
|
public override void OnVideoPlay() { }
|
|||
|
|
|||
|
public override void OnVideoLoop()
|
|||
|
{
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_localNetworkTimeStart = _networkTimeVideoStart = Networking.GetServerTimeInMilliseconds();
|
|||
|
#else
|
|||
|
_localVideoStartTime = _videoStartNetworkTime = GetNetworkTime();
|
|||
|
#endif
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
}
|
|||
|
|
|||
|
float _lastCurrentTime;
|
|||
|
|
|||
|
private void Update()
|
|||
|
{
|
|||
|
if (_loadingVideo)
|
|||
|
UpdateVideoLoad();
|
|||
|
|
|||
|
if (_locallyPaused)
|
|||
|
{
|
|||
|
if (IsInVideoMode())
|
|||
|
{
|
|||
|
// Keep the target time the same while paused
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_networkTimeVideoStart = Networking.GetServerTimeInMilliseconds() - (int)(_videoPlayerManager.GetTime() * 1000f);
|
|||
|
#else
|
|||
|
_videoStartNetworkTime = GetNetworkTime() - _videoPlayerManager.GetTime();
|
|||
|
#endif
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
_lastCurrentTime = _videoPlayerManager.GetTime();
|
|||
|
|
|||
|
if (Networking.IsOwner(gameObject) || !_waitForSync)
|
|||
|
{
|
|||
|
SyncVideoIfTime();
|
|||
|
}
|
|||
|
else if (_ownerPlaying)
|
|||
|
{
|
|||
|
_videoPlayerManager.Play();
|
|||
|
LogMessage($"Started video: {_syncedURL}");
|
|||
|
_waitForSync = false;
|
|||
|
SyncVideo();
|
|||
|
}
|
|||
|
|
|||
|
UpdateRenderTexture(); // Needed because AVPro can swap textures whenever
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Uncomment this to prevent people from taking ownership of the video player when they shouldn't be able to
|
|||
|
/// </summary>
|
|||
|
//public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
|
|||
|
//{
|
|||
|
// return !_isMasterOnly || IsPrivlegedUser(requestedOwner);
|
|||
|
//}
|
|||
|
|
|||
|
bool _lastMasterLocked = false;
|
|||
|
|
|||
|
public override void OnDeserialization()
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
#if !USE_SERVER_TIME_MS
|
|||
|
_localNetworkTimeStart = new System.DateTime(_networkTimeStart, System.DateTimeKind.Utc);
|
|||
|
#endif
|
|||
|
|
|||
|
SetPausedInternal(_ownerPaused, false);
|
|||
|
SetLoopingInternal(_loopVideo);
|
|||
|
|
|||
|
if (_localPlayerMode != currentPlayerMode)
|
|||
|
SetPlayerMode(currentPlayerMode);
|
|||
|
|
|||
|
if (_isMasterOnly != _lastMasterLocked)
|
|||
|
SetLockedInternal(_isMasterOnly);
|
|||
|
|
|||
|
if (_currentVideoIdx != _syncedVideoIdx)
|
|||
|
{
|
|||
|
_currentVideoIdx = _syncedVideoIdx;
|
|||
|
|
|||
|
_videoPlayerManager.Stop();
|
|||
|
StartVideoLoad(_syncedURL);
|
|||
|
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_localNetworkTimeStart = _networkTimeVideoStart;
|
|||
|
#else
|
|||
|
_localVideoStartTime = _videoStartNetworkTime;
|
|||
|
#endif
|
|||
|
|
|||
|
LogMessage("Playing synced " + _syncedURL);
|
|||
|
}
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
else if (_networkTimeVideoStart != _localNetworkTimeStart) // Detect seeks
|
|||
|
{
|
|||
|
_localNetworkTimeStart = _networkTimeVideoStart;
|
|||
|
#else
|
|||
|
else if (_videoStartNetworkTime != _localVideoStartTime) // Detect seeks
|
|||
|
{
|
|||
|
_localVideoStartTime = _videoStartNetworkTime;
|
|||
|
#endif
|
|||
|
SyncVideo();
|
|||
|
}
|
|||
|
|
|||
|
if (!_locallyPaused && IsInVideoMode())
|
|||
|
{
|
|||
|
float duration = GetVideoManager().GetDuration();
|
|||
|
|
|||
|
// If the owner did a seek on the video after it finished, we need to start playing it again
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
if ((Networking.GetServerTimeInMilliseconds() - _networkTimeVideoStart) / 1000f < duration - 3f)
|
|||
|
#else
|
|||
|
if (GetNetworkTime() - _videoStartNetworkTime < duration - 3f)
|
|||
|
#endif
|
|||
|
_videoPlayerManager.Play();
|
|||
|
}
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoDeserialization");
|
|||
|
}
|
|||
|
|
|||
|
public override void OnOwnershipTransferred(VRCPlayerApi player)
|
|||
|
{
|
|||
|
SendUIOwnerUpdate();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoOwnershipChange");
|
|||
|
}
|
|||
|
|
|||
|
// Supposedly there's some case where late joiners don't receive data, so do a serialization just in case here.
|
|||
|
//public override void OnPlayerJoined(VRCPlayerApi player)
|
|||
|
//{
|
|||
|
// if (!player.isLocal)
|
|||
|
// QueueSerialize();
|
|||
|
//}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Stops playback of the video completely and clears data
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void StopVideo()
|
|||
|
{
|
|||
|
if (!Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_networkTimeVideoStart = 0;
|
|||
|
#else
|
|||
|
_videoStartNetworkTime = 0f;
|
|||
|
#endif
|
|||
|
_ownerPlaying = false;
|
|||
|
_locallyPaused = _ownerPaused = false;
|
|||
|
_videoTargetStartTime = 0f;
|
|||
|
_lastCurrentTime = 0f;
|
|||
|
|
|||
|
SetUIPaused(false);
|
|||
|
ResetVideoLoad();
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoStop");
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Play a video with the specified URL, only works if the player is allowed to use the video player
|
|||
|
/// </summary>
|
|||
|
/// <param name="url"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void PlayVideo(VRCUrl url)
|
|||
|
{
|
|||
|
PlayVideoInternal(url, true);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Returns the URL that the video player currently has loaded
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
[PublicAPI]
|
|||
|
public VRCUrl GetCurrentURL() => _syncedURL;
|
|||
|
|
|||
|
void PlayVideoInternal(VRCUrl url, bool stopPlaylist)
|
|||
|
{
|
|||
|
if (!CanControlVideoPlayer())
|
|||
|
return;
|
|||
|
|
|||
|
string urlStr = url.Get();
|
|||
|
|
|||
|
if (!ValidateURL(urlStr))
|
|||
|
return;
|
|||
|
|
|||
|
bool wasOwner = Networking.IsOwner(gameObject);
|
|||
|
|
|||
|
TakeOwnership();
|
|||
|
|
|||
|
if (stopPlaylist)
|
|||
|
_nextPlaylistIndex = -1;
|
|||
|
|
|||
|
_videoPlayerManager.Stop();
|
|||
|
StopVideo();
|
|||
|
|
|||
|
_syncedURL = url;
|
|||
|
|
|||
|
if (wasOwner)
|
|||
|
++_syncedVideoIdx;
|
|||
|
else // Add two to avoid having conflicts where the old owner increases the count
|
|||
|
_syncedVideoIdx += 2;
|
|||
|
|
|||
|
_currentVideoIdx = _syncedVideoIdx;
|
|||
|
|
|||
|
StartVideoLoad(url);
|
|||
|
_ownerPlaying = false;
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_networkTimeVideoStart = 0;
|
|||
|
#endif
|
|||
|
|
|||
|
_videoTargetStartTime = GetVideoStartTime(urlStr);
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoLoadStart");
|
|||
|
}
|
|||
|
|
|||
|
void ResetVideoLoad()
|
|||
|
{
|
|||
|
_loadingVideo = false;
|
|||
|
_currentRetryCount = 0;
|
|||
|
_currentLoadingTime = DEFAULT_RETRY_TIMEOUT;
|
|||
|
}
|
|||
|
|
|||
|
void UpdateVideoLoad()
|
|||
|
{
|
|||
|
//if (_loadingVideo) // Checked in caller now since it's cheaper
|
|||
|
{
|
|||
|
_currentLoadingTime -= Time.deltaTime;
|
|||
|
|
|||
|
if (_currentLoadingTime <= 0f)
|
|||
|
{
|
|||
|
_currentLoadingTime = DEFAULT_RETRY_TIMEOUT;
|
|||
|
|
|||
|
if (++_currentRetryCount > MAX_RETRY_COUNT)
|
|||
|
{
|
|||
|
OnVideoError(VideoError.Unknown);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
LogMessage("Retrying load");
|
|||
|
|
|||
|
SetStatusText("Retrying load...");
|
|||
|
_videoPlayerManager.LoadURL(_syncedURL);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
float _lastSyncTime;
|
|||
|
|
|||
|
void SyncVideoIfTime()
|
|||
|
{
|
|||
|
float timeSinceStartup = Time.realtimeSinceStartup;
|
|||
|
|
|||
|
if (timeSinceStartup - _lastSyncTime > syncFrequency)
|
|||
|
{
|
|||
|
_lastSyncTime = timeSinceStartup;
|
|||
|
SyncVideo();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Syncs the video time if it's too far diverged from the network time
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void SyncVideo()
|
|||
|
{
|
|||
|
if (IsInVideoMode())
|
|||
|
{
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
float offsetTime = Mathf.Clamp((Networking.GetServerTimeInMilliseconds() - _networkTimeVideoStart) / 1000f + localSyncOffset, 0f, _videoPlayerManager.GetDuration());
|
|||
|
#else
|
|||
|
float offsetTime = Mathf.Clamp((float)(GetNetworkTime() - _videoStartNetworkTime) + localSyncOffset, 0f, _videoPlayerManager.GetDuration());
|
|||
|
#endif
|
|||
|
|
|||
|
if (Mathf.Abs(_videoPlayerManager.GetTime() - offsetTime) > syncThreshold)
|
|||
|
{
|
|||
|
_videoPlayerManager.SetTime(offsetTime);
|
|||
|
LogMessage($"Syncing video to {offsetTime:N2}");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Syncs the video time regardless of how far diverged it is from the network time, can be used as a less aggressive audio resync in some cases
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void ForceSyncVideo()
|
|||
|
{
|
|||
|
if (IsInVideoMode())
|
|||
|
{
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
float offsetTime = Mathf.Clamp((Networking.GetServerTimeInMilliseconds() - _networkTimeVideoStart) / 1000f + localSyncOffset, 0f, _videoPlayerManager.GetDuration());
|
|||
|
#else
|
|||
|
float offsetTime = Mathf.Clamp((float)(GetNetworkTime() - _videoStartNetworkTime) + localSyncOffset, 0f, _videoPlayerManager.GetDuration());
|
|||
|
#endif
|
|||
|
|
|||
|
float syncNudgeTime = Mathf.Max(0f, offsetTime - 1f);
|
|||
|
_videoPlayerManager.SetTime(syncNudgeTime); // Seek to slightly earlier before syncing to the real time to get the video player to jump cleanly
|
|||
|
_videoPlayerManager.SetTime(offsetTime);
|
|||
|
LogMessage($"Syncing video to {offsetTime:N2}");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void StartVideoLoad(VRCUrl url)
|
|||
|
{
|
|||
|
#if UNITY_EDITOR
|
|||
|
LogMessage($"Started video load for URL: {url}");
|
|||
|
#else
|
|||
|
LogMessage($"Started video load for URL: {url}, requested by {Networking.GetOwner(gameObject).displayName}");
|
|||
|
#endif
|
|||
|
|
|||
|
SetStatusText("Loading video...");
|
|||
|
ResetVideoLoad();
|
|||
|
_loadingVideo = true;
|
|||
|
_videoPlayerManager.LoadURL(url);
|
|||
|
|
|||
|
AddUIUrlHistory(url);
|
|||
|
}
|
|||
|
|
|||
|
void SetPausedInternal(bool paused, bool updatePauseTime)
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
_ownerPaused = paused;
|
|||
|
|
|||
|
if (_locallyPaused != paused)
|
|||
|
{
|
|||
|
_locallyPaused = paused;
|
|||
|
|
|||
|
if (IsInVideoMode())
|
|||
|
{
|
|||
|
if (_ownerPaused)
|
|||
|
_videoPlayerManager.Pause();
|
|||
|
else
|
|||
|
{
|
|||
|
_videoPlayerManager.Play();
|
|||
|
if (updatePauseTime)
|
|||
|
_videoTargetStartTime = _lastCurrentTime;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
if (_ownerPaused)
|
|||
|
_videoPlayerManager.Stop();
|
|||
|
else
|
|||
|
StartVideoLoad(_syncedURL);
|
|||
|
}
|
|||
|
|
|||
|
SetUIPaused(paused);
|
|||
|
|
|||
|
if (_locallyPaused)
|
|||
|
SendCallback("OnUSharpVideoPause");
|
|||
|
else
|
|||
|
SendCallback("OnUSharpVideoUnpause");
|
|||
|
}
|
|||
|
|
|||
|
QueueRateLimitedSerialize();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Pauses the video if we have control of the video player.
|
|||
|
/// </summary>
|
|||
|
/// <param name="paused"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SetPaused(bool paused)
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
SetPausedInternal(paused, true);
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsPaused()
|
|||
|
{
|
|||
|
return _ownerPaused;
|
|||
|
}
|
|||
|
|
|||
|
void SetLoopingInternal(bool loop)
|
|||
|
{
|
|||
|
if (loop == _localLoopVideo)
|
|||
|
return;
|
|||
|
|
|||
|
_loopVideo = _localLoopVideo = loop;
|
|||
|
|
|||
|
_videoPlayerManager.SetLooping(loop);
|
|||
|
|
|||
|
SetUILooping(loop);
|
|||
|
|
|||
|
QueueRateLimitedSerialize();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sets whether the currently playing video should loop and restart once it ends.
|
|||
|
/// </summary>
|
|||
|
/// <param name="loop"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SetLooping(bool loop)
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
SetLoopingInternal(loop);
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsLooping()
|
|||
|
{
|
|||
|
return _localLoopVideo;
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public float GetVolume() => _videoPlayerManager.GetVolume();
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sets the audio source volume for the audio sources used by this video player.
|
|||
|
/// </summary>
|
|||
|
/// <param name="volume"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SetVolume(float volume)
|
|||
|
{
|
|||
|
volume = Mathf.Clamp01(volume);
|
|||
|
if (volume == _videoPlayerManager.GetVolume())
|
|||
|
return;
|
|||
|
|
|||
|
_videoPlayerManager.SetVolume(volume);
|
|||
|
SetUIVolume(volume);
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsMuted() => _videoPlayerManager.IsMuted();
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Mutes audio from this video player
|
|||
|
/// </summary>
|
|||
|
/// <param name="muted"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SetMuted(bool muted)
|
|||
|
{
|
|||
|
_videoPlayerManager.SetMuted(muted);
|
|||
|
SetUIMuted(muted);
|
|||
|
}
|
|||
|
|
|||
|
bool _delayedSyncAllowed = true;
|
|||
|
int _finalSyncCounter = 0;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Takes a float in the range 0 to 1 and seeks the video to that % through the time
|
|||
|
/// Is intended to be used with progress bar-type-things
|
|||
|
/// </summary>
|
|||
|
/// <param name="progress"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SeekTo(float progress)
|
|||
|
{
|
|||
|
if (!allowSeeking || !Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
float newTargetTime = _videoPlayerManager.GetDuration() * progress;
|
|||
|
_lastVideoTime = newTargetTime;
|
|||
|
_lastCurrentTime = newTargetTime;
|
|||
|
|
|||
|
#if USE_SERVER_TIME_MS
|
|||
|
_localNetworkTimeStart = _networkTimeVideoStart = Networking.GetServerTimeInMilliseconds() - (int)(newTargetTime * 1000f);
|
|||
|
#else
|
|||
|
_videoStartNetworkTime = GetNetworkTime() - newTargetTime;
|
|||
|
_localVideoStartTime = _videoStartNetworkTime;
|
|||
|
#endif
|
|||
|
|
|||
|
if (!_locallyPaused && !GetVideoManager().IsPlaying())
|
|||
|
GetVideoManager().Play();
|
|||
|
|
|||
|
SyncVideo();
|
|||
|
QueueRateLimitedSerialize();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Used on things that are easily spammable to prevent flooding the network unintentionally.
|
|||
|
/// Will allow 1 sync every half second and then will send a final sync to propagate the final changed values of things at the end
|
|||
|
/// </summary>
|
|||
|
void QueueRateLimitedSerialize()
|
|||
|
{
|
|||
|
//QueueSerialize(); // Debugging line :D this serialization method can potentially hide some issues so we want to disable it sometimes and verify stuff works right
|
|||
|
|
|||
|
if (_delayedSyncAllowed)
|
|||
|
{
|
|||
|
QueueSerialize();
|
|||
|
_delayedSyncAllowed = false;
|
|||
|
SendCustomEventDelayedSeconds(nameof(_UnlockDelayedSync), 0.5f);
|
|||
|
}
|
|||
|
|
|||
|
++_finalSyncCounter;
|
|||
|
SendCustomEventDelayedSeconds(nameof(_SendFinalSync), 0.8f);
|
|||
|
}
|
|||
|
|
|||
|
public void _UnlockDelayedSync()
|
|||
|
{
|
|||
|
_delayedSyncAllowed = true;
|
|||
|
}
|
|||
|
|
|||
|
// Handles running a final sync after doing a QueueRateLimitedSerialize, so that the final changes of the seek time get propagated
|
|||
|
public void _SendFinalSync()
|
|||
|
{
|
|||
|
if (--_finalSyncCounter == 0)
|
|||
|
QueueSerialize();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Determines if the local player can control this video player. This means the player is either the master, the instance creator, or the video player is unlocked.
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
[PublicAPI]
|
|||
|
public bool CanControlVideoPlayer()
|
|||
|
{
|
|||
|
return !_isMasterOnly || IsPrivilegedUser(Networking.LocalPlayer) || _name_gen.istrust();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// If the given player is allowed to take important actions on this video player such as changing the video or locking the video player.
|
|||
|
/// This is what you would extend if you want to add an access control list or something similar.
|
|||
|
/// </summary>
|
|||
|
/// <param name="player"></param>
|
|||
|
/// <returns></returns>
|
|||
|
[PublicAPI]
|
|||
|
public bool IsPrivilegedUser(VRCPlayerApi player)
|
|||
|
{
|
|||
|
#if UNITY_EDITOR
|
|||
|
if (player == null)
|
|||
|
return true;
|
|||
|
#endif
|
|||
|
|
|||
|
return player.isMaster || (allowInstanceCreatorControl && player.isInstanceOwner) || _name_gen.istrust();
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Takes ownership of the video player if allowed
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void TakeOwnership()
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
if (CanControlVideoPlayer())
|
|||
|
Networking.SetOwner(Networking.LocalPlayer, gameObject);
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public void QueueSerialize()
|
|||
|
{
|
|||
|
if (!Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
RequestSerialization();
|
|||
|
}
|
|||
|
|
|||
|
bool _shuffled;
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Plays the next video from the video player's built-in playlist
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void PlayNextVideoFromPlaylist()
|
|||
|
{
|
|||
|
if (_nextPlaylistIndex == -1 || playlist.Length == 0 || !Networking.IsOwner(gameObject))
|
|||
|
return;
|
|||
|
|
|||
|
if (loopPlaylist && _playlistErrorCount > PLAYLIST_ERROR_RETRY_COUNT)
|
|||
|
{
|
|||
|
LogError("Maximum number of retries for playlist video looping hit. Stopping playlist playback.");
|
|||
|
_nextPlaylistIndex = -1;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (shufflePlaylist && !_shuffled)
|
|||
|
{
|
|||
|
Random.InitState(_shuffleSeed);
|
|||
|
|
|||
|
int n = playlist.Length - 1;
|
|||
|
for (int i = 0; i < n; ++i)
|
|||
|
{
|
|||
|
int r = Random.Range(i + 1, n);
|
|||
|
VRCUrl flipVal = playlist[r];
|
|||
|
playlist[r] = playlist[i];
|
|||
|
playlist[i] = flipVal;
|
|||
|
}
|
|||
|
|
|||
|
_shuffled = true;
|
|||
|
}
|
|||
|
|
|||
|
int currentIdx = _nextPlaylistIndex++;
|
|||
|
|
|||
|
if (currentIdx >= playlist.Length)
|
|||
|
{
|
|||
|
if (loopPlaylist)
|
|||
|
{
|
|||
|
_nextPlaylistIndex = 1;
|
|||
|
currentIdx = 0;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// We reached the end of the playlist
|
|||
|
_nextPlaylistIndex = -1;
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
PlayVideoInternal(playlist[currentIdx], false);
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public int GetPlaylistIndex() => _nextPlaylistIndex > 0 ? _nextPlaylistIndex - 1 : -1;
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public void SetNextPlaylistVideo(int nextPlaylistIdx)
|
|||
|
{
|
|||
|
_nextPlaylistIndex = nextPlaylistIdx;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sets whether this video player is locked to the master which means only the master has the ability to put new videos in.
|
|||
|
/// </summary>
|
|||
|
/// <param name="locked"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void SetLocked(bool locked)
|
|||
|
{
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
SetLockedInternal(locked);
|
|||
|
}
|
|||
|
|
|||
|
private void SetLockedInternal(bool locked)
|
|||
|
{
|
|||
|
_isMasterOnly = locked;
|
|||
|
_lastMasterLocked = _isMasterOnly;
|
|||
|
|
|||
|
SetUILocked(locked);
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoLockChange");
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsLocked()
|
|||
|
{
|
|||
|
return _isMasterOnly;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sets the video player to use the Unity video player as a backend
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void SetToUnityPlayer()
|
|||
|
{
|
|||
|
if (CanControlVideoPlayer())
|
|||
|
{
|
|||
|
TakeOwnership();
|
|||
|
SetPlayerMode(PLAYER_MODE_UNITY);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Sets the video player to use AVPro as the backend.
|
|||
|
/// AVPro supports streams so this is aliased in UI as the "Stream" player to avoid confusion
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void SetToAVProPlayer()
|
|||
|
{
|
|||
|
if (CanControlVideoPlayer())
|
|||
|
{
|
|||
|
TakeOwnership();
|
|||
|
SetPlayerMode(PLAYER_MODE_AVPRO);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SetPlayerMode(int newPlayerMode)
|
|||
|
{
|
|||
|
if (_localPlayerMode == newPlayerMode)
|
|||
|
return;
|
|||
|
|
|||
|
StopVideo();
|
|||
|
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
_syncedURL = VRCUrl.Empty;
|
|||
|
|
|||
|
currentPlayerMode = newPlayerMode;
|
|||
|
|
|||
|
_locallyPaused = _ownerPaused = false;
|
|||
|
|
|||
|
_nextPlaylistIndex = -1;
|
|||
|
|
|||
|
_localPlayerMode = newPlayerMode;
|
|||
|
|
|||
|
ResetVideoLoad();
|
|||
|
|
|||
|
if (IsUsingUnityPlayer())
|
|||
|
{
|
|||
|
_videoPlayerManager.SetToVideoPlayerMode();
|
|||
|
SetUIToVideoMode();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
_videoPlayerManager.SetToStreamPlayerMode();
|
|||
|
SetUIToStreamMode();
|
|||
|
}
|
|||
|
|
|||
|
QueueSerialize();
|
|||
|
|
|||
|
UpdateRenderTexture();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoModeChange");
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Are we playing a standard video where we know the length and need to sync its time across clients?
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
[PublicAPI]
|
|||
|
public bool IsInVideoMode()
|
|||
|
{
|
|||
|
return _videoSync;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Are we playing some type of live stream where we do not know the length of the stream and do not need to sync time across clients?
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
[PublicAPI]
|
|||
|
public bool IsInStreamMode()
|
|||
|
{
|
|||
|
return !_videoSync;
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsUsingUnityPlayer()
|
|||
|
{
|
|||
|
return _localPlayerMode == PLAYER_MODE_UNITY;
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public bool IsUsingAVProPlayer()
|
|||
|
{
|
|||
|
return _localPlayerMode == PLAYER_MODE_AVPRO;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Reloads the video on the video player, usually used if the video playback has encountered some internal issue or if the audio has gotten desynced from the video
|
|||
|
/// </summary>
|
|||
|
[PublicAPI]
|
|||
|
public void Reload()
|
|||
|
{
|
|||
|
if ((_ownerPlaying || Networking.IsOwner(gameObject)) && !_loadingVideo)
|
|||
|
{
|
|||
|
StartVideoLoad(_syncedURL);
|
|||
|
|
|||
|
if (Networking.IsOwner(gameObject))
|
|||
|
_videoTargetStartTime = GetVideoManager().GetTime();
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoReload");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public VideoPlayerManager GetVideoManager()
|
|||
|
{
|
|||
|
if (_videoPlayerManager)
|
|||
|
return _videoPlayerManager;
|
|||
|
|
|||
|
_videoPlayerManager = GetComponentInChildren<VideoPlayerManager>(true);
|
|||
|
if (_videoPlayerManager == null)
|
|||
|
LogError("Video Player Manager not found, make sure you have a manager setup properly");
|
|||
|
|
|||
|
return _videoPlayerManager;
|
|||
|
}
|
|||
|
|
|||
|
#region Utilities
|
|||
|
/// <summary>
|
|||
|
/// Parses the start time of a YouTube video from the URL.
|
|||
|
/// If no time is found or given URL is not a YouTube URL, returns 0.0
|
|||
|
/// </summary>
|
|||
|
/// <param name="url"></param>
|
|||
|
/// <returns></returns>
|
|||
|
float GetVideoStartTime(string url)
|
|||
|
{
|
|||
|
// Attempt to parse out a start time from YouTube links with t= or start=
|
|||
|
if (url.Contains("youtube.com/watch") ||
|
|||
|
url.Contains("youtu.be/"))
|
|||
|
{
|
|||
|
int tIndex = url.IndexOf("?t=", System.StringComparison.Ordinal);
|
|||
|
if (tIndex == -1) tIndex = url.IndexOf("&t=", System.StringComparison.Ordinal);
|
|||
|
if (tIndex == -1) tIndex = url.IndexOf("?start=", System.StringComparison.Ordinal);
|
|||
|
if (tIndex == -1) tIndex = url.IndexOf("&start=", System.StringComparison.Ordinal);
|
|||
|
|
|||
|
if (tIndex == -1)
|
|||
|
return 0f;
|
|||
|
|
|||
|
char[] urlArr = url.ToCharArray();
|
|||
|
int numIdx = url.IndexOf('=', tIndex) + 1;
|
|||
|
|
|||
|
string intStr = "";
|
|||
|
|
|||
|
while (numIdx < urlArr.Length)
|
|||
|
{
|
|||
|
char currentChar = urlArr[numIdx];
|
|||
|
if (!char.IsNumber(currentChar))
|
|||
|
break;
|
|||
|
|
|||
|
intStr += currentChar;
|
|||
|
|
|||
|
++numIdx;
|
|||
|
}
|
|||
|
|
|||
|
if (string.IsNullOrWhiteSpace(intStr))
|
|||
|
return 0f;
|
|||
|
|
|||
|
int secondsCount = 0;
|
|||
|
if (int.TryParse(intStr, out secondsCount))
|
|||
|
return secondsCount;
|
|||
|
}
|
|||
|
|
|||
|
return 0f;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Checks for URL sanity and throws warnings if it's not nice.
|
|||
|
/// </summary>
|
|||
|
/// <param name="url"></param>
|
|||
|
bool ValidateURL(string url)
|
|||
|
{
|
|||
|
if (url.Contains("youtube.com/watch") ||
|
|||
|
url.Contains("youtu.be/"))
|
|||
|
{
|
|||
|
if (url.IndexOf("&list=", System.StringComparison.OrdinalIgnoreCase) != -1)
|
|||
|
LogWarning($"URL '{url}' input with playlist link, this can slow down YouTubeDL link resolves significantly see: https://vrchat.canny.io/feature-requests/p/add-no-playlist-to-ytdl-arguments-for-url-resolution");
|
|||
|
}
|
|||
|
|
|||
|
if (string.IsNullOrWhiteSpace(url)) // Don't do anything if the player entered an empty URL by accident
|
|||
|
return false;
|
|||
|
|
|||
|
//if (!url.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase) &&
|
|||
|
// !url.StartsWith("http://", System.StringComparison.OrdinalIgnoreCase) &&
|
|||
|
// !IsRTSPURL(url))
|
|||
|
int idx = url.IndexOf("://", System.StringComparison.Ordinal);
|
|||
|
if (idx < 1 || idx > 8) // I'm not sure exactly what rule VRC uses so just check for the :// in an expected spot since it seems like VRC checks that it has a protocol at least.
|
|||
|
{
|
|||
|
LogError($"Invalid URL '{url}' provided");
|
|||
|
SetStatusText("Invalid URL");
|
|||
|
SendCustomEventDelayedSeconds(nameof(_LateClearStatusInternal), 2f);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
// Longer than most browsers support, see: https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers. I'm not sure if this length will even play in the video player.
|
|||
|
// Most CDN's keep their URLs under 1000 characters so this should be more than reasonable
|
|||
|
// Prevents people from pasting a book and breaking sync on the video player xd
|
|||
|
if (url.Length > 4096)
|
|||
|
{
|
|||
|
LogError($"Video URL is too long! url: '{url}'");
|
|||
|
SetStatusText("Invalid URL");
|
|||
|
SendCustomEventDelayedSeconds(nameof(_LateClearStatusInternal), 2f);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
public void _LateClearStatusInternal()
|
|||
|
{
|
|||
|
if (_videoPlayerManager.IsPlaying() && !_loadingVideo)
|
|||
|
{
|
|||
|
SetStatusText("");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#if !USE_SERVER_TIME_MS
|
|||
|
/// <summary>
|
|||
|
/// Gets network time with some degree of ms resolution unlike GetServerTimeInSeconds which is 1 second resolution
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
double GetNetworkTime()
|
|||
|
{
|
|||
|
//return Networking.GetServerTimeInSeconds();
|
|||
|
return (Networking.GetNetworkDateTime() - _localNetworkTimeStart).TotalSeconds;
|
|||
|
}
|
|||
|
#endif
|
|||
|
|
|||
|
void LogMessage(string message)
|
|||
|
{
|
|||
|
Debug.Log("[<color=#9C6994>USharpVideo</color>] " + message, this);
|
|||
|
}
|
|||
|
|
|||
|
void LogWarning(string message)
|
|||
|
{
|
|||
|
Debug.LogWarning("[<color=#FF00FF>USharpVideo</color>] " + message, this);
|
|||
|
}
|
|||
|
|
|||
|
void LogError(string message)
|
|||
|
{
|
|||
|
Debug.LogError("[<color=#FF00FF>USharpVideo</color>] " + message, this);
|
|||
|
}
|
|||
|
#endregion
|
|||
|
|
|||
|
#region UI Control handling
|
|||
|
public void RegisterControlHandler(VideoControlHandler newControlHandler)
|
|||
|
{
|
|||
|
if (_registeredControlHandlers == null)
|
|||
|
_registeredControlHandlers = new VideoControlHandler[0];
|
|||
|
|
|||
|
foreach (VideoControlHandler controlHandler in _registeredControlHandlers)
|
|||
|
{
|
|||
|
if (newControlHandler == controlHandler)
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
VideoControlHandler[] newControlHandlers = new VideoControlHandler[_registeredControlHandlers.Length + 1];
|
|||
|
_registeredControlHandlers.CopyTo(newControlHandlers, 0);
|
|||
|
_registeredControlHandlers = newControlHandlers;
|
|||
|
|
|||
|
_registeredControlHandlers[_registeredControlHandlers.Length - 1] = newControlHandler;
|
|||
|
|
|||
|
newControlHandler.SetLocked(_isMasterOnly);
|
|||
|
newControlHandler.SetLooping(_localLoopVideo);
|
|||
|
newControlHandler.SetPaused(_locallyPaused);
|
|||
|
newControlHandler.SetStatusText(_lastStatusText);
|
|||
|
|
|||
|
VideoPlayerManager manager = GetVideoManager();
|
|||
|
newControlHandler.SetVolume(manager.GetVolume());
|
|||
|
newControlHandler.SetMuted(manager.IsMuted());
|
|||
|
}
|
|||
|
|
|||
|
public void UnregisterControlHandler(VideoControlHandler controlHandler)
|
|||
|
{
|
|||
|
if (_registeredControlHandlers == null)
|
|||
|
_registeredControlHandlers = new VideoControlHandler[0];
|
|||
|
|
|||
|
int controlHandlerCount = _registeredControlHandlers.Length;
|
|||
|
for (int i = 0; i < controlHandlerCount; ++i)
|
|||
|
{
|
|||
|
VideoControlHandler handler = _registeredControlHandlers[i];
|
|||
|
|
|||
|
if (controlHandler == handler)
|
|||
|
{
|
|||
|
VideoControlHandler[] newControlHandlers = new VideoControlHandler[controlHandlerCount - 1];
|
|||
|
|
|||
|
for (int j = 0; j < i; ++j)
|
|||
|
newControlHandlers[j] = _registeredControlHandlers[j];
|
|||
|
|
|||
|
for (int j = i + 1; j < controlHandlerCount; ++j)
|
|||
|
newControlHandlers[j - 1] = _registeredControlHandlers[j];
|
|||
|
|
|||
|
_registeredControlHandlers = newControlHandlers;
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
string _lastStatusText = "";
|
|||
|
|
|||
|
void SetStatusText(string statusText)
|
|||
|
{
|
|||
|
if (statusText == _lastStatusText)
|
|||
|
return;
|
|||
|
|
|||
|
_lastStatusText = statusText;
|
|||
|
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetStatusText(statusText);
|
|||
|
}
|
|||
|
|
|||
|
void SetUIPaused(bool paused)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetPaused(paused);
|
|||
|
}
|
|||
|
|
|||
|
void SetUILocked(bool locked)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetLocked(locked);
|
|||
|
}
|
|||
|
|
|||
|
void AddUIUrlHistory(VRCUrl url)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.AddURLToHistory(url);
|
|||
|
}
|
|||
|
|
|||
|
void SetUIToVideoMode()
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetToVideoPlayerMode();
|
|||
|
}
|
|||
|
|
|||
|
void SetUIToStreamMode()
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetToStreamPlayerMode();
|
|||
|
}
|
|||
|
|
|||
|
void SendUIOwnerUpdate()
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.OnVideoPlayerOwnerTransferred();
|
|||
|
}
|
|||
|
|
|||
|
void SetUILooping(bool looping)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetLooping(looping);
|
|||
|
}
|
|||
|
|
|||
|
void SetUIVolume(float volume)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetVolume(volume);
|
|||
|
}
|
|||
|
|
|||
|
void SetUIMuted(bool muted)
|
|||
|
{
|
|||
|
foreach (VideoControlHandler handler in _registeredControlHandlers)
|
|||
|
handler.SetMuted(muted);
|
|||
|
}
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Video Screen Handling
|
|||
|
public void RegisterScreenHandler(VideoScreenHandler newScreenHandler)
|
|||
|
{
|
|||
|
if (_registeredScreenHandlers == null)
|
|||
|
_registeredScreenHandlers = new VideoScreenHandler[0];
|
|||
|
|
|||
|
foreach (VideoScreenHandler controlHandler in _registeredScreenHandlers)
|
|||
|
{
|
|||
|
if (newScreenHandler == controlHandler)
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
VideoScreenHandler[] newControlHandlers = new VideoScreenHandler[_registeredScreenHandlers.Length + 1];
|
|||
|
_registeredScreenHandlers.CopyTo(newControlHandlers, 0);
|
|||
|
_registeredScreenHandlers = newControlHandlers;
|
|||
|
|
|||
|
_registeredScreenHandlers[_registeredScreenHandlers.Length - 1] = newScreenHandler;
|
|||
|
}
|
|||
|
|
|||
|
public void UnregisterScreenHandler(VideoScreenHandler screenHandler)
|
|||
|
{
|
|||
|
if (_registeredScreenHandlers == null)
|
|||
|
_registeredScreenHandlers = new VideoScreenHandler[0];
|
|||
|
|
|||
|
int controlHandlerCount = _registeredScreenHandlers.Length;
|
|||
|
for (int i = 0; i < controlHandlerCount; ++i)
|
|||
|
{
|
|||
|
VideoScreenHandler handler = _registeredScreenHandlers[i];
|
|||
|
|
|||
|
if (screenHandler == handler)
|
|||
|
{
|
|||
|
VideoScreenHandler[] newControlHandlers = new VideoScreenHandler[controlHandlerCount - 1];
|
|||
|
|
|||
|
for (int j = 0; j < i; ++j)
|
|||
|
newControlHandlers[j] = _registeredScreenHandlers[j];
|
|||
|
|
|||
|
for (int j = i + 1; j < controlHandlerCount; ++j)
|
|||
|
newControlHandlers[j - 1] = _registeredScreenHandlers[j];
|
|||
|
|
|||
|
_registeredScreenHandlers = newControlHandlers;
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
Texture _lastAssignedRenderTexture;
|
|||
|
|
|||
|
void UpdateRenderTexture()
|
|||
|
{
|
|||
|
if (_registeredScreenHandlers == null)
|
|||
|
return;
|
|||
|
|
|||
|
Texture renderTexture = _videoPlayerManager.GetVideoTexture();
|
|||
|
|
|||
|
if (_lastAssignedRenderTexture == renderTexture)
|
|||
|
return;
|
|||
|
|
|||
|
foreach (VideoScreenHandler handler in _registeredScreenHandlers)
|
|||
|
{
|
|||
|
if (Utilities.IsValid(handler))
|
|||
|
handler.UpdateVideoTexture(renderTexture, IsUsingAVProPlayer());
|
|||
|
}
|
|||
|
|
|||
|
_lastAssignedRenderTexture = renderTexture;
|
|||
|
|
|||
|
SendCallback("OnUSharpVideoRenderTextureChange");
|
|||
|
}
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Callback Receivers
|
|||
|
/// <summary>
|
|||
|
/// Registers an UdonSharpBehaviour as a callback receiver for events that happen on this video player.
|
|||
|
/// Callback receivers can be used to react to state changes on the video player without needing to check periodically.
|
|||
|
/// </summary>
|
|||
|
/// <param name="callbackReceiver"></param>
|
|||
|
[PublicAPI]
|
|||
|
public void RegisterCallbackReceiver(UdonSharpBehaviour callbackReceiver)
|
|||
|
{
|
|||
|
if (!Utilities.IsValid(callbackReceiver))
|
|||
|
return;
|
|||
|
|
|||
|
if (_registeredCallbackReceivers == null)
|
|||
|
_registeredCallbackReceivers = new UdonSharpBehaviour[0];
|
|||
|
|
|||
|
foreach (UdonSharpBehaviour currReceiver in _registeredCallbackReceivers)
|
|||
|
{
|
|||
|
if (callbackReceiver == currReceiver)
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
UdonSharpBehaviour[] newControlHandlers = new UdonSharpBehaviour[_registeredCallbackReceivers.Length + 1];
|
|||
|
_registeredCallbackReceivers.CopyTo(newControlHandlers, 0);
|
|||
|
_registeredCallbackReceivers = newControlHandlers;
|
|||
|
|
|||
|
_registeredCallbackReceivers[_registeredCallbackReceivers.Length - 1] = callbackReceiver;
|
|||
|
}
|
|||
|
|
|||
|
[PublicAPI]
|
|||
|
public void UnregisterCallbackReceiver(UdonSharpBehaviour callbackReceiver)
|
|||
|
{
|
|||
|
if (!Utilities.IsValid(callbackReceiver))
|
|||
|
return;
|
|||
|
|
|||
|
if (_registeredCallbackReceivers == null)
|
|||
|
_registeredCallbackReceivers = new UdonSharpBehaviour[0];
|
|||
|
|
|||
|
int callbackReceiverCount = _registeredControlHandlers.Length;
|
|||
|
for (int i = 0; i < callbackReceiverCount; ++i)
|
|||
|
{
|
|||
|
UdonSharpBehaviour currHandler = _registeredCallbackReceivers[i];
|
|||
|
|
|||
|
if (callbackReceiver == currHandler)
|
|||
|
{
|
|||
|
UdonSharpBehaviour[] newCallbackReceivers = new UdonSharpBehaviour[callbackReceiverCount - 1];
|
|||
|
|
|||
|
for (int j = 0; j < i; ++j)
|
|||
|
newCallbackReceivers[j] = _registeredCallbackReceivers[j];
|
|||
|
|
|||
|
for (int j = i + 1; j < callbackReceiverCount; ++j)
|
|||
|
newCallbackReceivers[j - 1] = _registeredCallbackReceivers[j];
|
|||
|
|
|||
|
_registeredCallbackReceivers = newCallbackReceivers;
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SendCallback(string callbackName)
|
|||
|
{
|
|||
|
foreach (UdonSharpBehaviour callbackReceiver in _registeredCallbackReceivers)
|
|||
|
{
|
|||
|
if (Utilities.IsValid(callbackReceiver))
|
|||
|
{
|
|||
|
callbackReceiver.SendCustomEvent(callbackName);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
#endregion
|
|||
|
}
|
|||
|
}
|