Decompiled source of YoutubeOnTV v0.3.0

BepInEx/plugins/YoutubeOnTV/YoutubeOnTV.dll

Decompiled 5 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using LethalNetworkAPI;
using LethalNetworkAPI.Utils;
using Microsoft.CodeAnalysis;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Video;
using YoutubeDLSharp;
using YoutubeDLSharp.Options;
using YoutubeOnTV.Patches;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("YoutubeOnTV")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("YoutubeOnTV")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("637d9e92-bc74-4646-8436-67954c0a9f24")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace YoutubeOnTV
{
	public class NetworkHandler : MonoBehaviour
	{
		private const string Prefix = "YoutubeOnTV2_";

		private LNetworkMessage<string> addVideoMessage;

		private LNetworkEvent skipVideoEvent;

		private LNetworkEvent clearQueueEvent;

		private LNetworkMessage<string> removeFromQueueMessage;

		private LNetworkMessage<string> loadVideoMessage;

		private LNetworkMessage<VideoPlayData> playVideoMessage;

		private LNetworkMessage<float> syncPlaybackMessage;

		private LNetworkEvent playFallbackEvent;

		private LNetworkEvent requestTVStateEvent;

		private LNetworkMessage<TVStateData> syncTVStateMessage;

		public static NetworkHandler Instance { get; private set; }

		private static bool IsHost => LNetworkUtils.IsHostOrServer;

		private static ulong[] OtherClients => LNetworkUtils.AllConnectedClients.Where((ulong id) => id != NetworkManager.Singleton.LocalClientId).ToArray();

		private void Awake()
		{
			if ((Object)(object)Instance != (Object)null)
			{
				Object.Destroy((Object)(object)this);
				return;
			}
			Instance = this;
			addVideoMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_AddVideo", (Action<string, ulong>)delegate(string entry, ulong _)
			{
				HostAddVideo(entry);
			}, (Action<string>)OnClientReceivedAddVideo, (Action<string, ulong>)null);
			skipVideoEvent = LNetworkEvent.Connect("YoutubeOnTV2_Skip", (Action<ulong>)delegate
			{
				HostSkip();
			}, (Action)OnClientReceivedSkip, (Action<ulong>)null);
			clearQueueEvent = LNetworkEvent.Connect("YoutubeOnTV2_Clear", (Action<ulong>)delegate
			{
				HostClear();
			}, (Action)OnClientReceivedClear, (Action<ulong>)null);
			removeFromQueueMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_RemoveFromQueue", (Action<string, ulong>)null, (Action<string>)OnClientReceivedRemoveFromQueue, (Action<string, ulong>)null);
			loadVideoMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_LoadVideo", (Action<string, ulong>)null, (Action<string>)delegate(string entry)
			{
				VideoManager.Instance?.OnHostLoadingVideo(entry);
			}, (Action<string, ulong>)null);
			playVideoMessage = LNetworkMessage<VideoPlayData>.Connect("YoutubeOnTV2_PlayVideo", (Action<VideoPlayData, ulong>)null, (Action<VideoPlayData>)delegate(VideoPlayData data)
			{
				VideoManager.Instance?.OnHostPlayingVideo(data.input, data.videoUrl, data.startTime);
			}, (Action<VideoPlayData, ulong>)null);
			syncPlaybackMessage = LNetworkMessage<float>.Connect("YoutubeOnTV2_SyncPlayback", (Action<float, ulong>)null, (Action<float>)delegate(float time)
			{
				VideoManager.Instance?.OnHostPlaybackTime(time);
			}, (Action<float, ulong>)null);
			playFallbackEvent = LNetworkEvent.Connect("YoutubeOnTV2_PlayFallback", (Action<ulong>)null, (Action)delegate
			{
				VideoManager.Instance?.OnHostPlayingFallback();
			}, (Action<ulong>)null);
			requestTVStateEvent = LNetworkEvent.Connect("YoutubeOnTV2_RequestTVState", (Action<ulong>)OnServerReceivedRequestTVState, (Action)null, (Action<ulong>)null);
			syncTVStateMessage = LNetworkMessage<TVStateData>.Connect("YoutubeOnTV2_SyncTVState", (Action<TVStateData, ulong>)null, (Action<TVStateData>)delegate(TVStateData state)
			{
				VideoManager.Instance?.ApplyTVStateFromNetwork(state);
			}, (Action<TVStateData, ulong>)null);
			YoutubeOnTVBase.Log.LogInfo((object)"Network messages registered");
		}

		public void RequestAddVideo(string entry)
		{
			if (IsHost)
			{
				HostAddVideo(entry);
			}
			else
			{
				addVideoMessage.SendServer(entry);
			}
		}

		public void RequestSkipVideo()
		{
			if (IsHost)
			{
				HostSkip();
			}
			else
			{
				skipVideoEvent.InvokeServer();
			}
		}

		public void RequestClearQueue()
		{
			if (IsHost)
			{
				HostClear();
			}
			else
			{
				clearQueueEvent.InvokeServer();
			}
		}

		public void RequestTVState()
		{
			if (!IsHost)
			{
				YoutubeOnTVBase.Log.LogInfo((object)"Requesting TV state from host");
				requestTVStateEvent.InvokeServer();
			}
		}

		private void HostAddVideo(string entry)
		{
			entry = VideoInput.Normalize(entry);
			if (entry != null)
			{
				YoutubeOnTVBase.Log.LogInfo((object)("[Host] Adding to queue: " + entry));
				VideoQueue.Add(entry);
				addVideoMessage.SendClients(entry, OtherClients);
			}
		}

		private void HostSkip()
		{
			YoutubeOnTVBase.Log.LogInfo((object)"[Host] Skip");
			VideoManager.Instance?.OnSkipRequested();
			skipVideoEvent.InvokeClients(OtherClients);
		}

		private void HostClear()
		{
			YoutubeOnTVBase.Log.LogInfo((object)"[Host] Clear queue");
			VideoManager.Instance?.OnClearRequested();
			clearQueueEvent.InvokeClients(OtherClients);
		}

		private void OnServerReceivedRequestTVState(ulong clientId)
		{
			if (!((Object)(object)VideoManager.Instance == (Object)null))
			{
				YoutubeOnTVBase.Log.LogInfo((object)$"[Host] Sending TV state to client {clientId}");
				syncTVStateMessage.SendClient(VideoManager.Instance.GetCurrentTVState(), clientId);
			}
		}

		public void BroadcastRemoveFromQueue(string entry)
		{
			if (IsHost)
			{
				removeFromQueueMessage.SendClients(entry, OtherClients);
			}
		}

		public void BroadcastLoadVideo(string entry)
		{
			if (IsHost)
			{
				loadVideoMessage.SendClients(entry, OtherClients);
			}
		}

		public void BroadcastPlayVideo(string entry, string videoUrl, float startTime)
		{
			if (IsHost)
			{
				YoutubeOnTVBase.Log.LogInfo((object)$"Broadcasting play: {videoUrl} ({entry}) at {startTime:0.0}s");
				playVideoMessage.SendClients(new VideoPlayData
				{
					input = entry,
					videoUrl = videoUrl,
					startTime = startTime
				}, OtherClients);
			}
		}

		public void BroadcastPlaybackTime(float time)
		{
			if (IsHost)
			{
				syncPlaybackMessage.SendClients(time, OtherClients);
			}
		}

		public void BroadcastPlayFallback()
		{
			if (IsHost)
			{
				playFallbackEvent.InvokeClients(OtherClients);
			}
		}

		private void OnClientReceivedAddVideo(string entry)
		{
			YoutubeOnTVBase.Log.LogInfo((object)("[Client] Adding to queue: " + entry));
			VideoQueue.Add(entry);
		}

		private void OnClientReceivedRemoveFromQueue(string entry)
		{
			VideoQueue.RemoveFirstMatch(entry);
		}

		private void OnClientReceivedSkip()
		{
			VideoManager.Instance?.OnSkipRequested();
		}

		private void OnClientReceivedClear()
		{
			VideoManager.Instance?.OnClearRequested();
		}
	}
	[Serializable]
	public struct VideoPlayData
	{
		public string input;

		public string videoUrl;

		public float startTime;
	}
	[Serializable]
	public struct TVStateData
	{
		public bool isTVOn;

		public int mode;

		public string input;

		public string videoUrl;

		public float playbackTime;

		public string[] queue;
	}
	[BepInPlugin("com.roandegraaf.youtubeontv", "YoutubeOnTV", "0.3.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class YoutubeOnTVBase : BaseUnityPlugin
	{
		public const string Guid = "com.roandegraaf.youtubeontv";

		private readonly Harmony harmony = new Harmony("com.roandegraaf.youtubeontv");

		public static YoutubeOnTVBase Instance;

		internal static ManualLogSource Log;

		public static ConfigEntry<int> MaxVideoMinutes;

		public static ConfigEntry<int> MaxCacheMegabytes;

		public static ConfigEntry<bool> PrefetchNextVideo;

		private void Awake()
		{
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			MaxVideoMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Videos", "MaxVideoMinutes", 60, "Longest video that can be queued, in minutes. Videos are downloaded in full before they play, so very long ones take a while and use disk space.");
			MaxCacheMegabytes = ((BaseUnityPlugin)this).Config.Bind<int>("Videos", "MaxCacheMegabytes", 1024, "Size limit of the downloaded-video cache in the plugin folder. The least recently played videos are removed first.");
			PrefetchNextVideo = ((BaseUnityPlugin)this).Config.Bind<bool>("Videos", "PrefetchNextVideo", true, "Download the next queued video while the current one plays, so it starts without waiting.");
			string location = Assembly.GetExecutingAssembly().Location;
			Log.LogInfo((object)$"YoutubeOnTV {((BaseUnityPlugin)this).Info.Metadata.Version} loaded (build {File.GetLastWriteTimeUtc(location):yyyy-MM-dd HH:mm:ss} UTC)");
			harmony.PatchAll(typeof(TVScriptPatch));
			harmony.PatchAll(typeof(TerminalPatch));
			harmony.PatchAll(typeof(SessionPatch));
			SceneManager.sceneLoaded += OnSceneLoaded;
		}

		private void Start()
		{
			if (Chainloader.PluginInfos.ContainsKey("rattenbonkers.TVLoader"))
			{
				Log.LogWarning((object)"TVLoader is installed. Both mods take over the ship TV, so disable one of them; with TVLoader videos present they will fight over the screen.");
			}
		}

		private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Expected O, but got Unknown
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)VideoManager.Instance != (Object)null))
			{
				GameObject val = new GameObject("YoutubeOnTVManagers");
				Object.DontDestroyOnLoad((Object)val);
				val.AddComponent<VideoManager>();
				val.AddComponent<NetworkHandler>();
				Log.LogInfo((object)"Managers created");
			}
		}

		internal static TerminalNode HandleTerminalCommand(string typed)
		{
			string text = (typed ?? "").Trim();
			string text2 = text.ToLowerInvariant();
			string argument = "";
			string command;
			if (text2 == "tv")
			{
				command = "";
			}
			else if (text2.StartsWith("tv "))
			{
				string text3 = text.Substring(3).Trim();
				int num = text3.IndexOf(' ');
				command = ((num < 0) ? text3 : text3.Substring(0, num)).ToLowerInvariant();
				argument = ((num < 0) ? "" : text3.Substring(num + 1).Trim());
			}
			else
			{
				switch (text2)
				{
				case "skip tv":
				case "clear tv":
				case "queue tv":
					break;
				default:
					return null;
				}
				command = text2.Substring(0, text2.Length - 3);
			}
			Log.LogInfo((object)("Terminal command: " + text));
			return Page(RunCommand(command, argument));
		}

		private static string RunCommand(string command, string argument)
		{
			NetworkHandler instance = NetworkHandler.Instance;
			switch (command)
			{
			case "add":
			{
				string text = VideoInput.Normalize(argument);
				if (text == null)
				{
					return "Usage: tv add [url, video id or search]\n\n  tv add dQw4w9WgXcQ\n  tv add https://youtube.com/watch?v=...\n  tv add never gonna give you up\n";
				}
				instance?.RequestAddVideo(text);
				return "Added to queue: " + VideoInput.Describe(text) + "\n\nVideos download before they play, so give it a few seconds.\n";
			}
			case "skip":
				instance?.RequestSkipVideo();
				return "Skipped the current video.\n";
			case "clear":
			{
				int num = VideoQueue.Count();
				instance?.RequestClearQueue();
				return string.Format("Cleared the queue ({0} video{1}) and stopped the current one.\n", num, (num == 1) ? "" : "s");
			}
			case "queue":
				return QueueText();
			default:
				return "TV CONTROLS\n===========\n\n  tv add [url/id/search]  Add a video to the queue\n  tv queue                Show what is playing and queued\n  tv skip                 Skip the current video\n  tv clear                Empty the queue\n";
			}
		}

		private static string QueueText()
		{
			StringBuilder stringBuilder = new StringBuilder();
			VideoManager instance = VideoManager.Instance;
			if (instance != null && instance.CurrentInput != null)
			{
				string text = (instance.IsLoadingVideo ? " (downloading)" : "");
				stringBuilder.Append("Now playing" + text + ": " + VideoInput.Describe(instance.CurrentVideoUrl ?? instance.CurrentInput) + "\n\n");
			}
			else
			{
				stringBuilder.Append("Nothing from the queue is playing.\n\n");
			}
			if (VideoQueue.IsEmpty())
			{
				stringBuilder.Append("Queue is empty.\n");
			}
			else
			{
				stringBuilder.Append($"Videos in queue: {VideoQueue.Count()}\n");
				int num = 1;
				foreach (string item in VideoQueue.Entries.Take(10))
				{
					stringBuilder.Append($"{num++}. {VideoInput.Describe(item)}\n");
				}
				if (VideoQueue.Count() > 10)
				{
					stringBuilder.Append($"...and {VideoQueue.Count() - 10} more\n");
				}
			}
			return stringBuilder.ToString();
		}

		private static TerminalNode Page(string text)
		{
			TerminalNode obj = ScriptableObject.CreateInstance<TerminalNode>();
			obj.displayText = "\n" + text + "\n";
			obj.clearPreviousText = true;
			obj.maxCharactersToType = 500;
			return obj;
		}
	}
	public class TVController : MonoBehaviour
	{
		public VideoPlayer videoPlayer;

		private VideoPlayer vanillaVideoPlayer;

		private RenderTexture renderTexture;

		private AudioSource tvAudioSource;

		private TVScript tvScript;

		private ManualLogSource logger;

		private bool isPreparing;

		private double pendingSeek = -1.0;

		private float prepareStartedAt;

		private int prepareAttempts;

		private const float PREPARE_TIMEOUT = 6f;

		private const int MAX_PREPARE_ATTEMPTS = 3;

		public static TVController Instance { get; private set; }

		public string LoadedPath { get; private set; }

		public AudioSource AudioSource => tvAudioSource;

		public double Time => videoPlayer.time;

		private void Awake()
		{
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_014f: Expected O, but got Unknown
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Expected O, but got Unknown
			//IL_0173: Unknown result type (might be due to invalid IL or missing references)
			//IL_017d: Expected O, but got Unknown
			//IL_018a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0194: Expected O, but got Unknown
			Instance = this;
			logger = YoutubeOnTVBase.Log;
			tvScript = ((Component)this).gameObject.GetComponent<TVScript>();
			if ((Object)(object)tvScript == (Object)null)
			{
				logger.LogError((object)"TVScript not found on this GameObject!");
				return;
			}
			tvAudioSource = tvScript.tvSFX;
			vanillaVideoPlayer = tvScript.video;
			if ((Object)(object)vanillaVideoPlayer != (Object)null)
			{
				renderTexture = vanillaVideoPlayer.targetTexture;
				vanillaVideoPlayer.Stop();
				((Behaviour)vanillaVideoPlayer).enabled = false;
			}
			else
			{
				logger.LogError((object)"Vanilla VideoPlayer not found!");
			}
			videoPlayer = ((Component)this).gameObject.AddComponent<VideoPlayer>();
			videoPlayer.playOnAwake = false;
			videoPlayer.isLooping = false;
			videoPlayer.source = (VideoSource)1;
			videoPlayer.skipOnDrop = true;
			videoPlayer.controlledAudioTrackCount = 1;
			videoPlayer.audioOutputMode = (VideoAudioOutputMode)1;
			videoPlayer.SetTargetAudioSource((ushort)0, tvAudioSource);
			videoPlayer.targetTexture = renderTexture;
			tvScript.video = videoPlayer;
			videoPlayer.prepareCompleted += new EventHandler(OnVideoPrepared);
			videoPlayer.loopPointReached += new EventHandler(OnVideoEnd);
			videoPlayer.errorReceived += new ErrorEventHandler(OnVideoError);
			videoPlayer.seekCompleted += (EventHandler)delegate(VideoPlayer vp)
			{
				logger.LogInfo((object)$"Seek completed at {vp.time:0.0}s");
			};
			ManualLogSource obj = logger;
			RenderTexture obj2 = renderTexture;
			object arg = ((obj2 != null) ? new int?(((Texture)obj2).width) : ((int?)null));
			RenderTexture obj3 = renderTexture;
			obj.LogInfo((object)$"TVController attached (screen {arg}x{((obj3 != null) ? new int?(((Texture)obj3).height) : ((int?)null))})");
		}

		private void OnDestroy()
		{
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		public void PlayFile(string filePath, double startAt = 0.0)
		{
			if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
			{
				logger.LogError((object)("Cannot play video: file not found at " + filePath));
				OnVideoError(videoPlayer, "file not found: " + filePath);
				return;
			}
			logger.LogInfo((object)("Playing " + Path.GetFileName(filePath) + ((startAt > 0.0) ? $" from {startAt:0.0}s" : "")));
			videoPlayer.Stop();
			LoadedPath = filePath;
			pendingSeek = ((startAt > 0.0) ? startAt : (-1.0));
			videoPlayer.url = "file://" + filePath;
			prepareAttempts = 0;
			StartPrepare();
		}

		private void StartPrepare()
		{
			isPreparing = true;
			prepareAttempts++;
			prepareStartedAt = Time.realtimeSinceStartup;
			videoPlayer.Prepare();
		}

		private void Update()
		{
			if (isPreparing && !(Time.realtimeSinceStartup - prepareStartedAt < 6f))
			{
				if (prepareAttempts < 3)
				{
					logger.LogWarning((object)$"Video did not finish preparing in {6f}s, retrying ({prepareAttempts + 1}/{3})");
					string url = videoPlayer.url;
					videoPlayer.Stop();
					videoPlayer.url = url;
					StartPrepare();
				}
				else
				{
					OnVideoError(videoPlayer, $"the video did not finish preparing after {3} attempts");
				}
			}
		}

		private void OnVideoPrepared(VideoPlayer vp)
		{
			if (isPreparing)
			{
				isPreparing = false;
				if (vp.audioTrackCount == 0)
				{
					logger.LogWarning((object)"Video has no audio track");
				}
				logger.LogInfo((object)$"Prepared: length={vp.length:0.0}s frames={vp.frameCount} canSetTime={vp.canSetTime} audioTracks={vp.audioTrackCount}");
				if (pendingSeek > 0.0 && pendingSeek < vp.length - 1.0)
				{
					vp.time = pendingSeek;
				}
				pendingSeek = -1.0;
				vp.Play();
			}
		}

		public void Seek(double time)
		{
			logger.LogInfo((object)$"Seeking from {videoPlayer.time:0.0}s to {time:0.0}s");
			if (isPreparing)
			{
				pendingSeek = time;
			}
			else
			{
				videoPlayer.time = time;
			}
		}

		public void Stop()
		{
			isPreparing = false;
			pendingSeek = -1.0;
			LoadedPath = null;
			videoPlayer.Stop();
		}

		public void Pause()
		{
			if (videoPlayer.isPlaying)
			{
				videoPlayer.Pause();
			}
		}

		public void Resume()
		{
			if (IsPaused())
			{
				videoPlayer.Play();
			}
		}

		public bool IsPaused()
		{
			if (LoadedPath != null && !isPreparing && !videoPlayer.isPlaying)
			{
				return videoPlayer.isPrepared;
			}
			return false;
		}

		public bool IsPlaying()
		{
			return videoPlayer.isPlaying;
		}

		public bool IsBusy()
		{
			if (!isPreparing && !videoPlayer.isPlaying)
			{
				return IsPaused();
			}
			return true;
		}

		public TVScript GetTVScript()
		{
			return tvScript;
		}

		private void OnVideoEnd(VideoPlayer vp)
		{
			logger.LogInfo((object)"Video playback completed");
			LoadedPath = null;
			VideoManager.Instance?.OnVideoFinished();
		}

		private void OnVideoError(VideoPlayer vp, string message)
		{
			isPreparing = false;
			LoadedPath = null;
			logger.LogError((object)("Video playback error: " + message));
			try
			{
				VideoManager.Instance?.OnVideoPlaybackError(message);
			}
			catch (Exception arg)
			{
				logger.LogError((object)$"Error handling playback failure: {arg}");
			}
		}
	}
	public class DownloadResult
	{
		public bool Success;

		public string VideoId;

		public string FilePath;

		public string Error;

		public string CanonicalUrl
		{
			get
			{
				if (VideoId != null)
				{
					return VideoInput.WatchUrl(VideoId);
				}
				return null;
			}
		}
	}
	public class VideoDownloader : MonoBehaviour
	{
		private enum ToolState
		{
			Pending,
			Ready,
			Failed
		}

		private class Request
		{
			public string Key;

			public readonly List<Action<DownloadResult>> Callbacks = new List<Action<DownloadResult>>();

			public readonly CancellationTokenSource Cancellation = new CancellationTokenSource();

			public volatile string VideoIdSeen;
		}

		private static VideoDownloader _instance;

		private const string FormatSelector = "bv*[vcodec^=avc1][height<=480]+ba[acodec^=mp4a]/18/bv*[vcodec^=avc1][height<=720]+ba[acodec^=mp4a]/b[vcodec^=avc1][acodec^=mp4a]/b[ext=mp4]";

		private static readonly TimeSpan YtDlpUpdateInterval = TimeSpan.FromHours(24.0);

		private static readonly TimeSpan DownloadTimeout = TimeSpan.FromMinutes(15.0);

		private ManualLogSource _logger;

		private string _toolsDir;

		private string _cacheDir;

		private string _ytDlpPath;

		private string _ffmpegPath;

		private ToolState _toolState;

		private string _toolError;

		private readonly Dictionary<string, Request> _inFlight = new Dictionary<string, Request>();

		private readonly Dictionary<string, DownloadResult> _completed = new Dictionary<string, DownloadResult>();

		public static VideoDownloader Instance
		{
			get
			{
				//IL_0012: Unknown result type (might be due to invalid IL or missing references)
				//IL_0017: Unknown result type (might be due to invalid IL or missing references)
				//IL_001d: Expected O, but got Unknown
				if ((Object)(object)_instance == (Object)null)
				{
					GameObject val = new GameObject("YoutubeOnTV.VideoDownloader");
					Object.DontDestroyOnLoad((Object)val);
					_instance = val.AddComponent<VideoDownloader>();
				}
				return _instance;
			}
		}

		public string CacheDirectory => _cacheDir;

		private void Awake()
		{
			_logger = YoutubeOnTVBase.Log;
			_toolsDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			_cacheDir = Path.Combine(_toolsDir, "cache");
			_ytDlpPath = Path.Combine(_toolsDir, "yt-dlp.exe");
			_ffmpegPath = Path.Combine(_toolsDir, "ffmpeg.exe");
			Directory.CreateDirectory(_cacheDir);
			CleanCache(removeLeftovers: true);
			((MonoBehaviour)this).StartCoroutine(EnsureTools());
		}

		private IEnumerator EnsureTools()
		{
			_toolState = ToolState.Pending;
			bool ytDlpExists = File.Exists(_ytDlpPath);
			bool flag = ytDlpExists && DateTime.UtcNow - File.GetLastWriteTimeUtc(_ytDlpPath) > YtDlpUpdateInterval;
			if (!ytDlpExists || flag)
			{
				_logger.LogInfo((object)(ytDlpExists ? "Updating yt-dlp.exe..." : "Downloading yt-dlp.exe..."));
				Task task = Utils.DownloadYtDlp(_toolsDir);
				while (!task.IsCompleted)
				{
					yield return null;
				}
				if (task.IsFaulted)
				{
					string text = task.Exception?.GetBaseException().Message;
					if (!ytDlpExists)
					{
						Fail("Could not download yt-dlp.exe: " + text);
						yield break;
					}
					_logger.LogWarning((object)("Could not update yt-dlp.exe, keeping the existing one: " + text));
				}
				else
				{
					_logger.LogInfo((object)"yt-dlp.exe is up to date.");
				}
			}
			if (!File.Exists(_ffmpegPath))
			{
				_logger.LogInfo((object)"Downloading ffmpeg.exe (one-time, about 55 MB)...");
				Task task = Utils.DownloadFFmpeg(_toolsDir);
				while (!task.IsCompleted)
				{
					yield return null;
				}
				if (task.IsFaulted || !File.Exists(_ffmpegPath))
				{
					Fail("Could not download ffmpeg.exe: " + (task.Exception?.GetBaseException().Message ?? "file missing after download"));
					yield break;
				}
				_logger.LogInfo((object)"ffmpeg.exe downloaded.");
			}
			_toolState = ToolState.Ready;
			_logger.LogInfo((object)"Video download tools are ready.");
		}

		private void Fail(string error)
		{
			_toolError = error;
			_toolState = ToolState.Failed;
			_logger.LogError((object)error);
		}

		public void Get(string input, Action<DownloadResult> onDone)
		{
			if (TryGetCompleted(input, out var result))
			{
				onDone?.Invoke(result);
				return;
			}
			if (_inFlight.TryGetValue(input, out var value))
			{
				if (onDone != null)
				{
					value.Callbacks.Add(onDone);
				}
				return;
			}
			Request request = new Request
			{
				Key = input
			};
			if (onDone != null)
			{
				request.Callbacks.Add(onDone);
			}
			_inFlight[input] = request;
			((MonoBehaviour)this).StartCoroutine(Guarded(request));
		}

		public void Prefetch(string input)
		{
			Get(input, null);
		}

		public bool IsReadyOrInFlight(string input)
		{
			DownloadResult result;
			if (!_inFlight.ContainsKey(input))
			{
				return TryGetCompleted(input, out result);
			}
			return true;
		}

		public void Cancel(string input)
		{
			if (input != null && _inFlight.TryGetValue(input, out var value))
			{
				_logger.LogInfo((object)("Cancelling download: " + input));
				value.Cancellation.Cancel();
			}
		}

		public void CancelAll()
		{
			foreach (Request item in _inFlight.Values.Distinct().ToList())
			{
				item.Cancellation.Cancel();
			}
		}

		private IEnumerator Guarded(Request request)
		{
			IEnumerator run = Run(request);
			while (true)
			{
				object current;
				try
				{
					if (!run.MoveNext())
					{
						break;
					}
					current = run.Current;
				}
				catch (Exception ex)
				{
					_logger.LogError((object)$"Download of {request.Key} failed unexpectedly: {ex}");
					if (_inFlight.ContainsValue(request))
					{
						Finish(request, new DownloadResult
						{
							Error = "Unexpected error: " + ex.Message
						});
					}
					break;
				}
				yield return current;
			}
		}

		private IEnumerator Run(Request request)
		{
			if (_toolState == ToolState.Failed)
			{
				yield return EnsureTools();
			}
			while (_toolState == ToolState.Pending)
			{
				yield return null;
			}
			if (_toolState == ToolState.Failed)
			{
				Finish(request, new DownloadResult
				{
					Error = _toolError
				});
				yield break;
			}
			_logger.LogInfo((object)("Downloading: " + request.Key));
			float startedAt = Time.realtimeSinceStartup;
			List<string> stdout = new List<string>();
			StringBuilder stderr = new StringBuilder();
			YoutubeDLProcess val = new YoutubeDLProcess(_ytDlpPath);
			val.OutputReceived += delegate(object o, DataReceivedEventArgs e)
			{
				if (!string.IsNullOrEmpty(e.Data))
				{
					lock (stdout)
					{
						stdout.Add(e.Data.Trim());
					}
					if (request.VideoIdSeen == null && VideoInput.ExtractVideoId(e.Data.Trim()) == e.Data.Trim())
					{
						request.VideoIdSeen = e.Data.Trim();
					}
				}
			};
			val.ErrorReceived += delegate(object o, DataReceivedEventArgs e)
			{
				if (!string.IsNullOrEmpty(e.Data))
				{
					lock (stderr)
					{
						stderr.AppendLine(e.Data);
					}
				}
			};
			OptionSet val2 = new OptionSet();
			val2.Format = "bv*[vcodec^=avc1][height<=480]+ba[acodec^=mp4a]/18/bv*[vcodec^=avc1][height<=720]+ba[acodec^=mp4a]/b[vcodec^=avc1][acodec^=mp4a]/b[ext=mp4]";
			val2.MergeOutputFormat = (DownloadMergeFormat)1;
			val2.FfmpegLocation = _ffmpegPath;
			val2.Output = Path.Combine(_cacheDir, "%(id)s.%(ext)s");
			val2.NoPlaylist = true;
			val2.NoSimulate = true;
			val2.NoProgress = true;
			val2.NoMtime = true;
			val2.Print = new MultiValue<string>(new string[2] { "id", "after_move:filepath" });
			val2.MatchFilters = MultiValue<string>.op_Implicit($"!is_live & duration <=? {YoutubeOnTVBase.MaxVideoMinutes.Value * 60}");
			OptionSet val3 = val2;
			request.Cancellation.CancelAfter(DownloadTimeout);
			Task<int> task;
			try
			{
				task = val.RunAsync(new string[1] { request.Key }, val3, request.Cancellation.Token, (IProgress<DownloadProgress>)null);
			}
			catch (Exception ex)
			{
				Finish(request, new DownloadResult
				{
					Error = "Could not start yt-dlp: " + ex.Message
				});
				yield break;
			}
			bool aliased = false;
			while (!task.IsCompleted)
			{
				if (!aliased && request.VideoIdSeen != null)
				{
					aliased = true;
					string key = VideoInput.WatchUrl(request.VideoIdSeen);
					if (!_inFlight.ContainsKey(key))
					{
						_inFlight[key] = request;
					}
				}
				yield return null;
			}
			string text;
			lock (stderr)
			{
				text = stderr.ToString().Trim();
			}
			List<string> source;
			lock (stdout)
			{
				source = new List<string>(stdout);
			}
			if (task.IsCanceled || request.Cancellation.IsCancellationRequested)
			{
				double num = Time.realtimeSinceStartup - startedAt;
				TimeSpan downloadTimeout = DownloadTimeout;
				bool flag = num >= downloadTimeout.TotalSeconds - 1.0;
				Finish(request, new DownloadResult
				{
					Error = (flag ? "Download timed out" : "Download cancelled")
				});
				yield break;
			}
			if (task.IsFaulted)
			{
				Finish(request, new DownloadResult
				{
					Error = "yt-dlp failed to run: " + task.Exception?.GetBaseException().Message
				});
				yield break;
			}
			string text2 = source.FirstOrDefault((string l) => VideoInput.ExtractVideoId(l) == l);
			string text3 = source.LastOrDefault((string l) => l.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase));
			if (task.Result == 0 && text3 != null && File.Exists(text3))
			{
				Touch(text3);
				if (!string.IsNullOrEmpty(text))
				{
					_logger.LogInfo((object)("yt-dlp warnings: " + text));
				}
				_logger.LogInfo((object)$"Downloaded {text2} in {Time.realtimeSinceStartup - startedAt:0.0}s: {text3}");
				DownloadResult downloadResult = new DownloadResult
				{
					Success = true,
					VideoId = text2,
					FilePath = text3
				};
				_completed[request.Key] = downloadResult;
				if (text2 != null)
				{
					_completed[downloadResult.CanonicalUrl] = downloadResult;
				}
				Finish(request, downloadResult);
				CleanCache(removeLeftovers: false);
			}
			else
			{
				string text4 = ((task.Result != 0) ? $"yt-dlp exited with code {task.Result}" : "No playable file (live stream, longer than the configured limit, or no H.264/AAC format)");
				_logger.LogError((object)(text4 + ": " + request.Key));
				if (!string.IsNullOrEmpty(text))
				{
					_logger.LogError((object)("yt-dlp output: " + text));
				}
				Finish(request, new DownloadResult
				{
					VideoId = text2,
					Error = text4
				});
			}
		}

		private void Finish(Request request, DownloadResult result)
		{
			foreach (string item in (from kv in _inFlight
				where kv.Value == request
				select kv.Key).ToList())
			{
				_inFlight.Remove(item);
			}
			if (!result.Success)
			{
				_logger.LogWarning((object)("Download failed for " + request.Key + ": " + result.Error));
			}
			foreach (Action<DownloadResult> callback in request.Callbacks)
			{
				try
				{
					callback(result);
				}
				catch (Exception arg)
				{
					_logger.LogError((object)$"Download callback threw: {arg}");
				}
			}
		}

		private bool TryGetCompleted(string input, out DownloadResult result)
		{
			if (_completed.TryGetValue(input, out result) && File.Exists(result.FilePath))
			{
				return true;
			}
			string text = (input.StartsWith("https://www.youtube.com/watch?v=") ? VideoInput.ExtractVideoId(input) : null);
			if (text != null)
			{
				string text2 = Path.Combine(_cacheDir, text + ".mp4");
				if (File.Exists(text2))
				{
					Touch(text2);
					result = new DownloadResult
					{
						Success = true,
						VideoId = text,
						FilePath = text2
					};
					_completed[input] = result;
					return true;
				}
			}
			result = null;
			return false;
		}

		private static void Touch(string path)
		{
			try
			{
				File.SetLastWriteTimeUtc(path, DateTime.UtcNow);
			}
			catch (IOException)
			{
			}
			catch (UnauthorizedAccessException)
			{
			}
		}

		private void CleanCache(bool removeLeftovers)
		{
			try
			{
				FileInfo[] files = new DirectoryInfo(_cacheDir).GetFiles();
				if (removeLeftovers)
				{
					foreach (FileInfo item in files.Where((FileInfo fileInfo) => !fileInfo.Name.EndsWith(".mp4") || fileInfo.Name.Count((char c) => c == '.') > 1))
					{
						TryDelete(item);
					}
					files = new DirectoryInfo(_cacheDir).GetFiles("*.mp4");
				}
				long num = (long)YoutubeOnTVBase.MaxCacheMegabytes.Value * 1024L * 1024;
				long num2 = files.Sum((FileInfo fileInfo) => fileInfo.Length);
				TVController instance = TVController.Instance;
				object obj;
				if (instance == null)
				{
					obj = null;
				}
				else
				{
					VideoPlayer videoPlayer = instance.videoPlayer;
					obj = ((videoPlayer != null) ? videoPlayer.url : null);
				}
				if (obj == null)
				{
					obj = "";
				}
				string text = (string)obj;
				foreach (FileInfo f in files.OrderBy((FileInfo fileInfo) => fileInfo.LastWriteTimeUtc))
				{
					if (num2 <= num)
					{
						break;
					}
					if (text.EndsWith(f.Name) || _completed.Values.Any((DownloadResult r) => r.FilePath == f.FullName && IsInUse(r)))
					{
						continue;
					}
					num2 -= f.Length;
					TryDelete(f);
					foreach (string item2 in (from kv in _completed
						where kv.Value.FilePath == f.FullName
						select kv.Key).ToList())
					{
						_completed.Remove(item2);
					}
				}
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("Cache cleanup failed: " + ex.Message));
			}
		}

		private static bool IsInUse(DownloadResult result)
		{
			return VideoQueue.Entries.Any((string e) => e == result.CanonicalUrl);
		}

		private void TryDelete(FileInfo file)
		{
			try
			{
				file.Delete();
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("Could not delete " + file.Name + ": " + ex.Message));
			}
		}
	}
	public static class VideoInput
	{
		public const string WatchUrlPrefix = "https://www.youtube.com/watch?v=";

		public const string SearchPrefix = "ytsearch:";

		private static readonly Regex VideoIdPattern = new Regex("^[a-zA-Z0-9_-]{11}$");

		private static readonly Regex[] UrlPatterns = new Regex[3]
		{
			new Regex("youtube\\.com/.*[?&]v=([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])"),
			new Regex("youtu\\.be/([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])"),
			new Regex("youtube\\.com/(?:embed|v|shorts|live)/([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])")
		};

		public static string Normalize(string input)
		{
			input = (input ?? "").Replace("\"", "").Trim();
			if (input.Length == 0)
			{
				return null;
			}
			string text = ExtractVideoId(input);
			if (text != null)
			{
				return "https://www.youtube.com/watch?v=" + text;
			}
			if (input.StartsWith("http://") || input.StartsWith("https://") || input.StartsWith("ytsearch:"))
			{
				return input;
			}
			return "ytsearch:" + input;
		}

		public static string ExtractVideoId(string input)
		{
			if (string.IsNullOrEmpty(input))
			{
				return null;
			}
			if (VideoIdPattern.IsMatch(input) && !IsLowercaseWord(input))
			{
				return input;
			}
			Regex[] urlPatterns = UrlPatterns;
			for (int i = 0; i < urlPatterns.Length; i++)
			{
				Match match = urlPatterns[i].Match(input);
				if (match.Success)
				{
					return match.Groups[1].Value;
				}
			}
			return null;
		}

		private static bool IsLowercaseWord(string s)
		{
			foreach (char c in s)
			{
				if (c < 'a' || c > 'z')
				{
					return false;
				}
			}
			return true;
		}

		public static string WatchUrl(string videoId)
		{
			return "https://www.youtube.com/watch?v=" + videoId;
		}

		public static string Describe(string entry)
		{
			if (string.IsNullOrEmpty(entry))
			{
				return "";
			}
			if (entry.StartsWith("ytsearch:"))
			{
				return "search: " + entry.Substring("ytsearch:".Length);
			}
			if (entry.StartsWith("https://www.youtube.com/watch?v="))
			{
				return "youtu.be/" + entry.Substring("https://www.youtube.com/watch?v=".Length);
			}
			return entry;
		}
	}
	public class VideoManager : MonoBehaviour
	{
		private enum Mode
		{
			Idle,
			Fallback,
			Video
		}

		private ManualLogSource logger;

		private Mode mode;

		private bool fallbackBroken;

		private int currentRetries;

		private float retryAtTime;

		private float lastSyncTime;

		private float hostTime;

		private float hostTimeReceivedAt;

		private string failedVideoUrl;

		private string loadedVideoUrl;

		private bool? pendingTvOn;

		private const int MAX_ATTEMPTS = 2;

		private const float RETRY_DELAY = 3f;

		private const float SYNC_INTERVAL = 2f;

		private const float SYNC_TOLERANCE = 1.5f;

		public static VideoManager Instance { get; private set; }

		public string CurrentInput { get; private set; }

		public string CurrentVideoUrl { get; private set; }

		public bool IsLoadingVideo { get; private set; }

		public bool IsPlayingFallback => mode == Mode.Fallback;

		private static bool IsHost => LNetworkUtils.IsHostOrServer;

		private static string FallbackVideoPath => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "fallback.mp4");

		public float HostPositionEstimate => EstimatedHostTime();

		private void Awake()
		{
			if ((Object)(object)Instance != (Object)null)
			{
				Object.Destroy((Object)(object)this);
				return;
			}
			Instance = this;
			logger = YoutubeOnTVBase.Log;
			_ = VideoDownloader.Instance;
		}

		public void ResetSession()
		{
			logger.LogInfo((object)"Resetting TV state for a new session");
			VideoDownloader.Instance.CancelAll();
			VideoQueue.Clear();
			TVController.Instance?.Stop();
			mode = Mode.Idle;
			fallbackBroken = false;
			IsLoadingVideo = false;
			CurrentInput = null;
			CurrentVideoUrl = null;
			currentRetries = 0;
			retryAtTime = 0f;
			failedVideoUrl = null;
			loadedVideoUrl = null;
			pendingTvOn = null;
		}

		private void Update()
		{
			TVController instance = TVController.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			if (pendingTvOn.HasValue)
			{
				SetTVPower(pendingTvOn.Value);
				pendingTvOn = null;
			}
			if (!IsTVOn())
			{
				instance.Pause();
				return;
			}
			if (IsHost)
			{
				AdvanceHostPlayback(instance);
				SyncPlaybackToClients(instance);
			}
			else
			{
				FollowHost(instance);
			}
			PrefetchNext();
		}

		private void AdvanceHostPlayback(TVController tv)
		{
			if (IsLoadingVideo)
			{
				return;
			}
			if (mode == Mode.Fallback && !VideoQueue.IsEmpty())
			{
				logger.LogInfo((object)"Switching from fallback to queue video");
				tv.Stop();
				mode = Mode.Idle;
			}
			if (tv.IsBusy())
			{
				return;
			}
			if (CurrentInput != null)
			{
				if (retryAtTime > 0f)
				{
					if (Time.time >= retryAtTime)
					{
						retryAtTime = 0f;
						LoadCurrentVideo();
					}
					return;
				}
				ClearCurrentVideo();
			}
			if (!VideoQueue.IsEmpty())
			{
				PlayNextFromQueue();
			}
			else if (mode != Mode.Fallback && !fallbackBroken)
			{
				PlayFallback(broadcast: true);
			}
		}

		private void PlayNextFromQueue()
		{
			CurrentInput = VideoQueue.Dequeue();
			CurrentVideoUrl = null;
			currentRetries = 0;
			retryAtTime = 0f;
			mode = Mode.Video;
			NetworkHandler.Instance?.BroadcastRemoveFromQueue(CurrentInput);
			NetworkHandler.Instance?.BroadcastLoadVideo(CurrentInput);
			LoadCurrentVideo();
		}

		private void LoadCurrentVideo()
		{
			string input = CurrentInput;
			logger.LogInfo((object)("Loading video: " + input));
			IsLoadingVideo = true;
			VideoDownloader.Instance.Get(input, delegate(DownloadResult result)
			{
				if (!(CurrentInput != input))
				{
					IsLoadingVideo = false;
					if (!result.Success)
					{
						OnCurrentVideoFailed(result.Error);
					}
					else
					{
						CurrentVideoUrl = result.CanonicalUrl ?? input;
						if ((Object)(object)TVController.Instance == (Object)null)
						{
							logger.LogError((object)"TVController not found!");
						}
						else
						{
							TVController.Instance.PlayFile(result.FilePath);
							lastSyncTime = Time.time;
							NetworkHandler.Instance?.BroadcastPlayVideo(input, CurrentVideoUrl, 0f);
						}
					}
				}
			});
		}

		private void OnCurrentVideoFailed(string reason)
		{
			logger.LogError((object)("Video failed: " + CurrentInput + " (" + reason + ")"));
			TVController.Instance?.Stop();
			currentRetries++;
			if (currentRetries < 2)
			{
				logger.LogInfo((object)$"Retrying in {3f}s (attempt {currentRetries + 1}/{2})");
				retryAtTime = Time.time + 3f;
				return;
			}
			logger.LogError((object)$"Giving up on {CurrentInput} after {currentRetries} attempts");
			ShowTip("Video Error", "Could not play " + VideoInput.Describe(CurrentInput) + ". " + reason);
			ClearCurrentVideo();
			mode = Mode.Idle;
		}

		private void ClearCurrentVideo()
		{
			CurrentInput = null;
			CurrentVideoUrl = null;
			currentRetries = 0;
			retryAtTime = 0f;
		}

		private void SyncPlaybackToClients(TVController tv)
		{
			if (mode == Mode.Video && tv.IsPlaying() && !(Time.time - lastSyncTime < 2f))
			{
				lastSyncTime = Time.time;
				NetworkHandler.Instance?.BroadcastPlaybackTime((float)tv.Time);
			}
		}

		public void OnSkipRequested()
		{
			string currentInput = CurrentInput;
			if (currentInput != null && !VideoQueue.Entries.Contains(currentInput))
			{
				VideoDownloader.Instance.Cancel(currentInput);
			}
			ClearCurrentVideo();
			IsLoadingVideo = false;
			mode = Mode.Idle;
			TVController.Instance?.Stop();
		}

		public void OnClearRequested()
		{
			VideoQueue.Clear();
			VideoDownloader.Instance.CancelAll();
			OnSkipRequested();
		}

		private void FollowHost(TVController tv)
		{
			if (mode == Mode.Fallback)
			{
				if (!tv.IsBusy() && !fallbackBroken)
				{
					StartFallbackPlayback();
				}
			}
			else
			{
				if (mode != Mode.Video || CurrentVideoUrl == null || IsLoadingVideo || CurrentVideoUrl == failedVideoUrl)
				{
					return;
				}
				string wanted = CurrentVideoUrl;
				if (loadedVideoUrl == wanted && tv.LoadedPath != null)
				{
					return;
				}
				IsLoadingVideo = true;
				VideoDownloader.Instance.Get(wanted, delegate(DownloadResult result)
				{
					if (!(CurrentVideoUrl != wanted))
					{
						IsLoadingVideo = false;
						if (!result.Success)
						{
							failedVideoUrl = wanted;
							ShowTip("Video Error", "Could not download the host's video. " + result.Error);
						}
						else
						{
							loadedVideoUrl = wanted;
							TVController.Instance?.PlayFile(result.FilePath, EstimatedHostTime());
						}
					}
				});
			}
		}

		private float EstimatedHostTime()
		{
			return hostTime + (Time.realtimeSinceStartup - hostTimeReceivedAt);
		}

		public void OnHostLoadingVideo(string input)
		{
			if (!IsHost)
			{
				VideoDownloader.Instance.Prefetch(input);
			}
		}

		public void OnHostPlayingVideo(string input, string videoUrl, float startTime)
		{
			if (!IsHost)
			{
				logger.LogInfo((object)("Host is playing " + videoUrl + " (" + input + ")"));
				CurrentInput = input;
				CurrentVideoUrl = videoUrl;
				IsLoadingVideo = false;
				mode = Mode.Video;
				hostTime = startTime;
				hostTimeReceivedAt = Time.realtimeSinceStartup;
			}
		}

		public void OnHostPlaybackTime(float time)
		{
			if (!IsHost)
			{
				hostTime = time;
				hostTimeReceivedAt = Time.realtimeSinceStartup;
				TVController instance = TVController.Instance;
				if (!((Object)(object)instance == (Object)null) && instance.IsPlaying() && mode == Mode.Video && Mathf.Abs((float)instance.Time - time) > 1.5f)
				{
					logger.LogInfo((object)$"Resyncing to host: {instance.Time:0.0}s -> {time:0.0}s");
					instance.Seek(time);
				}
			}
		}

		public void OnHostPlayingFallback()
		{
			if (!IsHost)
			{
				mode = Mode.Fallback;
				CurrentInput = null;
				CurrentVideoUrl = null;
				IsLoadingVideo = false;
				TVController.Instance?.Stop();
			}
		}

		private void PrefetchNext()
		{
			if (!IsLoadingVideo && YoutubeOnTVBase.PrefetchNextVideo.Value)
			{
				string text = VideoQueue.Peek();
				if (text != null && !VideoDownloader.Instance.IsReadyOrInFlight(text))
				{
					logger.LogInfo((object)("Prefetching next video: " + text));
					VideoDownloader.Instance.Prefetch(text);
				}
			}
		}

		private void PlayFallback(bool broadcast)
		{
			StartFallbackPlayback();
			if (broadcast)
			{
				NetworkHandler.Instance?.BroadcastPlayFallback();
			}
		}

		private void StartFallbackPlayback()
		{
			if (!((Object)(object)TVController.Instance == (Object)null))
			{
				mode = Mode.Fallback;
				loadedVideoUrl = null;
				TVController.Instance.PlayFile(FallbackVideoPath);
			}
		}

		public void OnVideoFinished()
		{
			if (mode == Mode.Fallback)
			{
				StartFallbackPlayback();
				return;
			}
			if (IsHost)
			{
				ClearCurrentVideo();
			}
			mode = Mode.Idle;
		}

		public void OnVideoPlaybackError(string message)
		{
			if (mode == Mode.Fallback)
			{
				logger.LogError((object)"The fallback video cannot be played; the TV stays dark until a video is queued");
				fallbackBroken = true;
				mode = Mode.Idle;
			}
			else if (!IsHost)
			{
				failedVideoUrl = CurrentVideoUrl;
			}
			else if (CurrentInput != null)
			{
				OnCurrentVideoFailed("playback failed: " + message);
			}
		}

		public void OnTVPoweredOn()
		{
			TVController.Instance?.Resume();
		}

		private bool IsTVOn()
		{
			TVScript val = TVController.Instance?.GetTVScript();
			if ((Object)(object)val != (Object)null)
			{
				return val.tvOn;
			}
			return false;
		}

		private void SetTVPower(bool on)
		{
			TVScript val = TVController.Instance?.GetTVScript();
			if ((Object)(object)val != (Object)null && val.tvOn != on)
			{
				val.TurnTVOnOff(on);
			}
		}

		private static void ShowTip(string header, string body)
		{
			HUDManager instance = HUDManager.Instance;
			if (instance != null)
			{
				instance.DisplayTip(header, body, true, false, "LC_Tip1");
			}
		}

		public TVStateData GetCurrentTVState()
		{
			TVController instance = TVController.Instance;
			return new TVStateData
			{
				isTVOn = IsTVOn(),
				mode = (int)mode,
				input = CurrentInput,
				videoUrl = CurrentVideoUrl,
				playbackTime = (((Object)(object)instance != (Object)null && mode == Mode.Video && CurrentVideoUrl != null) ? ((float)instance.Time) : 0f),
				queue = VideoQueue.Entries.ToArray()
			};
		}

		public void ApplyTVStateFromNetwork(TVStateData state)
		{
			if (IsHost)
			{
				return;
			}
			ManualLogSource obj = logger;
			object[] obj2 = new object[5]
			{
				state.isTVOn,
				(Mode)state.mode,
				state.videoUrl,
				state.playbackTime,
				null
			};
			string[] queue = state.queue;
			obj2[4] = ((queue != null) ? queue.Length : 0);
			obj.LogInfo((object)string.Format("Applying host TV state: on={0} mode={1} video={2} t={3:0.0} queue={4}", obj2));
			VideoQueue.ReplaceWith(state.queue);
			pendingTvOn = state.isTVOn;
			switch ((Mode)state.mode)
			{
			case Mode.Fallback:
				OnHostPlayingFallback();
				return;
			case Mode.Video:
				if (state.videoUrl != null)
				{
					OnHostPlayingVideo(state.input, state.videoUrl, state.playbackTime);
					return;
				}
				break;
			}
			mode = Mode.Idle;
			CurrentInput = state.input;
			CurrentVideoUrl = null;
			if (state.input != null)
			{
				VideoDownloader.Instance.Prefetch(state.input);
			}
		}
	}
	public static class VideoQueue
	{
		private static readonly List<string> _entries = new List<string>();

		public static IReadOnlyList<string> Entries => _entries;

		public static void Add(string entry)
		{
			if (!string.IsNullOrEmpty(entry))
			{
				_entries.Add(entry);
				YoutubeOnTVBase.Log.LogInfo((object)$"[VideoQueue] Added '{entry}'. Queue size: {_entries.Count}");
			}
		}

		public static void Clear()
		{
			_entries.Clear();
		}

		public static void ReplaceWith(IEnumerable<string> entries)
		{
			_entries.Clear();
			if (entries != null)
			{
				_entries.AddRange(entries);
			}
		}

		public static bool IsEmpty()
		{
			return _entries.Count == 0;
		}

		public static int Count()
		{
			return _entries.Count;
		}

		public static string Peek()
		{
			if (_entries.Count != 0)
			{
				return _entries[0];
			}
			return null;
		}

		public static string Dequeue()
		{
			if (_entries.Count == 0)
			{
				return null;
			}
			string text = _entries[0];
			_entries.RemoveAt(0);
			YoutubeOnTVBase.Log.LogInfo((object)$"[VideoQueue] Dequeued '{text}'. Remaining: {_entries.Count}");
			return text;
		}

		public static void RemoveFirstMatch(string entry)
		{
			int num = _entries.IndexOf(entry);
			if (num >= 0)
			{
				_entries.RemoveAt(num);
			}
		}
	}
	public static class PluginInfo
	{
		public const string PLUGIN_GUID = "YoutubeOnTV";

		public const string PLUGIN_NAME = "YoutubeOnTV";

		public const string PLUGIN_VERSION = "1.0.0";
	}
}
namespace YoutubeOnTV.Patches
{
	internal static class SessionPatch
	{
		[HarmonyPatch(typeof(StartOfRound), "Awake")]
		[HarmonyPostfix]
		private static void OnRoundLoaded()
		{
			VideoManager.Instance?.ResetSession();
		}

		[HarmonyPatch(typeof(GameNetworkManager), "Disconnect")]
		[HarmonyPostfix]
		private static void OnDisconnected()
		{
			VideoManager.Instance?.ResetSession();
		}

		[HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")]
		[HarmonyPostfix]
		private static void OnLocalPlayerConnected()
		{
			NetworkHandler.Instance?.RequestTVState();
		}
	}
	[HarmonyPatch(typeof(Terminal))]
	internal class TerminalPatch
	{
		[HarmonyPatch("ParsePlayerSentence")]
		[HarmonyPrefix]
		private static bool ParsePlayerSentencePrefix(Terminal __instance, ref TerminalNode __result)
		{
			string text = __instance.screenText.text;
			int textAdded = __instance.textAdded;
			if (textAdded <= 0 || textAdded > text.Length)
			{
				return true;
			}
			TerminalNode val = YoutubeOnTVBase.HandleTerminalCommand(text.Substring(text.Length - textAdded));
			if ((Object)(object)val == (Object)null)
			{
				return true;
			}
			__result = val;
			return false;
		}

		[HarmonyPatch("TextChanged")]
		[HarmonyPrefix]
		private static bool TextChangedPatch(Terminal __instance, string newText, ref int ___textAdded, ref string ___currentText, ref bool ___modifyingText)
		{
			if ((Object)(object)__instance.currentNode == (Object)null)
			{
				return false;
			}
			if (___modifyingText)
			{
				___modifyingText = false;
				return false;
			}
			___textAdded += newText.Length - ___currentText.Length;
			if (___textAdded < 0)
			{
				__instance.screenText.text = ___currentText;
				___textAdded = 0;
				return false;
			}
			int num = 500;
			if (___textAdded > num)
			{
				__instance.screenText.text = ___currentText;
				___textAdded = num;
				return false;
			}
			___currentText = newText;
			return false;
		}
	}
	[HarmonyPatch(typeof(TVScript))]
	internal class TVScriptPatch
	{
		[HarmonyPatch("Update")]
		[HarmonyPrefix]
		private static bool UpdatePatch(TVScript __instance)
		{
			if ((Object)(object)((Component)__instance).gameObject.GetComponent<TVController>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<TVController>();
				Debug.Log((object)"Attached TVController to TVScript object.");
			}
			return false;
		}

		[HarmonyPatch("TVFinishedClip")]
		[HarmonyPrefix]
		private static bool TVFinishedClipPatch()
		{
			return false;
		}

		[HarmonyPatch("TurnTVOnOff")]
		[HarmonyPrefix]
		private static bool TurnTVOnOffPatch(TVScript __instance, bool on)
		{
			__instance.tvOn = on;
			if (on)
			{
				Debug.Log((object)"TV turned ON - triggering mod video playback");
				__instance.tvSFX.PlayOneShot(__instance.switchTVOn);
				WalkieTalkie.TransmitOneShotAudio(__instance.tvSFX, __instance.switchTVOn, 1f);
				if ((Object)(object)VideoManager.Instance != (Object)null)
				{
					VideoManager.Instance.OnTVPoweredOn();
				}
			}
			else
			{
				Debug.Log((object)"TV turned OFF");
				__instance.tvSFX.PlayOneShot(__instance.switchTVOff);
				WalkieTalkie.TransmitOneShotAudio(__instance.tvSFX, __instance.switchTVOff, 1f);
			}
			typeof(TVScript).GetMethod("SetTVScreenMaterial", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(__instance, new object[1] { on });
			return false;
		}
	}
}