Decompiled source of LethalTubeTV v0.3.1

LethalTubeTV.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Video;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("LethalTubeTV")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.3.1.0")]
[assembly: AssemblyInformationalVersion("0.3.1")]
[assembly: AssemblyProduct("LethalTubeTV")]
[assembly: AssemblyTitle("LethalTubeTV")]
[assembly: AssemblyVersion("0.3.1.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 LethalTubeTV
{
	internal sealed class MediaPipeline
	{
		private readonly struct ProcessResult
		{
			internal readonly int ExitCode;

			internal readonly string StdOut;

			internal readonly string StdErr;

			internal readonly long ElapsedMs;

			internal ProcessResult(int exitCode, string stdout, string stderr, long elapsedMs)
			{
				ExitCode = exitCode;
				StdOut = stdout;
				StdErr = stderr;
				ElapsedMs = elapsedMs;
			}
		}

		private readonly string _pluginDir;

		private readonly string _cacheRoot;

		private readonly ManualLogSource _log;

		private readonly ToolBootstrapper _toolBootstrapper;

		private readonly SemaphoreSlim _mediaGate = new SemaphoreSlim(1, 1);

		internal MediaPipeline(string pluginDir, ManualLogSource log)
		{
			_pluginDir = pluginDir;
			_log = log;
			_cacheRoot = Path.Combine(pluginDir, "cache");
			_toolBootstrapper = new ToolBootstrapper(pluginDir, log);
			Directory.CreateDirectory(_cacheRoot);
		}

		internal ToolSetupResult EnsureTools(Action<string> progress)
		{
			return _toolBootstrapper.EnsureTools(progress);
		}

		internal ToolCheckResult CheckTools()
		{
			string text = ResolveTool("yt-dlp.exe");
			string text2 = ResolveTool("ffmpeg.exe");
			string text3 = ResolveTool("deno.exe");
			if (text == null)
			{
				return ToolCheckResult.Fail("yt-dlp.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요.");
			}
			if (text2 == null)
			{
				return ToolCheckResult.Fail("ffmpeg.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요.");
			}
			if (text3 == null)
			{
				return ToolCheckResult.Fail("deno.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요.");
			}
			return ToolCheckResult.Success();
		}

		internal PipelineResult DownloadAndConvert(string url, int maxHeight, Action<string> progress)
		{
			return DownloadAndConvertInternal(url, maxHeight, "current", progress);
		}

		internal PipelineResult PreloadForQueue(string url, int maxHeight, int queueId, Action<string> progress)
		{
			return DownloadAndConvertInternal(url, maxHeight, "queue_" + queueId, progress);
		}

		internal void DeleteCachedFile(string path)
		{
			if (string.IsNullOrWhiteSpace(path))
			{
				return;
			}
			try
			{
				string fullPath = Path.GetFullPath(path);
				string value = Path.GetFullPath(_cacheRoot) + Path.DirectorySeparatorChar;
				if (fullPath.StartsWith(value, StringComparison.OrdinalIgnoreCase))
				{
					if (File.Exists(fullPath))
					{
						File.Delete(fullPath);
					}
					string path2 = Path.ChangeExtension(fullPath, ".wav");
					if (File.Exists(path2))
					{
						File.Delete(path2);
					}
				}
			}
			catch (Exception ex)
			{
				_log.LogDebug((object)("Cached file cleanup warning: " + ex.Message));
			}
		}

		private PipelineResult DownloadAndConvertInternal(string url, int maxHeight, string cacheKey, Action<string> progress)
		{
			_mediaGate.Wait();
			try
			{
				string text = ResolveTool("yt-dlp.exe");
				string text2 = ResolveTool("ffmpeg.exe");
				string text3 = ResolveTool("deno.exe");
				if (text == null || text2 == null || text3 == null)
				{
					return PipelineResult.Fail("필수 도구를 찾지 못했습니다.");
				}
				Directory.CreateDirectory(_cacheRoot);
				string text4 = SanitizeCacheKey(cacheKey);
				string text5 = Path.Combine(_cacheRoot, "work_" + text4);
				ResetWorkDirectory(text5);
				string text6 = Path.Combine(_cacheRoot, text4 + ".mp4");
				string text7 = Path.Combine(_cacheRoot, text4 + ".wav");
				string text8 = Path.Combine(text5, "title.txt");
				int num = Math.Max(144, maxHeight);
				try
				{
					if (File.Exists(text6))
					{
						File.Delete(text6);
					}
				}
				catch
				{
				}
				try
				{
					if (File.Exists(text7))
					{
						File.Delete(text7);
					}
				}
				catch
				{
				}
				string text9 = "--no-playlist --no-simulate --no-color --encoding utf-8 --js-runtimes " + Q("deno:" + text3) + " --ffmpeg-location " + Q(text2) + " ";
				string empty = string.Empty;
				string text10 = null;
				bool flag = false;
				progress?.Invoke("1/3 YouTube 영상/오디오 받는 중...");
				string value = Path.Combine(text5, "prepared.%(ext)s");
				string value2 = $"bv[ext=mp4][vcodec^=avc1][height<={num}]+ba[ext=m4a]/" + $"b[ext=mp4][vcodec^=avc1][height<={num}]";
				string text11 = text9 + "--print-to-file " + Q("before_dl:%(title)s") + " " + Q(text8) + " -f " + Q(value2) + " --merge-output-format mp4 -o " + Q(value) + " " + Q(url);
				_log.LogInfo((object)("yt-dlp fast-path args [" + text4 + "]: " + RedactUrl(text11)));
				ProcessResult processResult = RunProcess(text, text11, text5, 300000);
				string text12 = ((processResult.ExitCode == 0) ? FindMediaFile(text5, "prepared.*") : null);
				if (processResult.ExitCode == 0 && text12 != null)
				{
					empty = ReadUtf8TextFile(text8);
					if (!string.IsNullOrWhiteSpace(empty))
					{
						_log.LogInfo((object)("YouTube title [" + text4 + "]: " + empty));
					}
					try
					{
						if (File.Exists(text6))
						{
							File.Delete(text6);
						}
					}
					catch
					{
					}
					File.Move(text12, text6);
					text10 = text6;
					flag = true;
					_log.LogInfo((object)($"Fast-path remux ready [{text4}] in {(double)processResult.ElapsedMs / 1000.0:0.00}s. " + "AVC/H.264 video was kept without re-encoding."));
				}
				else
				{
					_log.LogWarning((object)($"Fast-path unavailable [{text4}] after {(double)processResult.ElapsedMs / 1000.0:0.00}s; " + "using compatibility fallback. " + ShortError(processResult.StdErr)));
					ResetWorkDirectory(text5);
					text8 = Path.Combine(text5, "title.txt");
					string value3 = Path.Combine(text5, "video.%(ext)s");
					string value4 = Path.Combine(text5, "audio.%(ext)s");
					progress?.Invoke("1/4 호환 영상 스트림 받는 중...");
					string value5 = $"bv*[height<={num}]/b[height<={num}]";
					string text13 = text9 + "--print-to-file " + Q("before_dl:%(title)s") + " " + Q(text8) + " -f " + Q(value5) + " -o " + Q(value3) + " " + Q(url);
					_log.LogInfo((object)("yt-dlp video args [" + text4 + "]: " + RedactUrl(text13)));
					ProcessResult processResult2 = RunProcess(text, text13, text5, 240000);
					if (processResult2.ExitCode != 0)
					{
						_log.LogError((object)("yt-dlp video stderr [" + text4 + "]: " + processResult2.StdErr));
						return PipelineResult.Fail("영상 다운로드 실패: " + ShortError(processResult2.StdErr));
					}
					string text14 = FindMediaFile(text5, "video.*");
					if (text14 == null)
					{
						return PipelineResult.Fail("영상 다운로드는 완료되었지만 video 파일을 찾지 못했습니다.");
					}
					empty = ReadUtf8TextFile(text8);
					if (!string.IsNullOrWhiteSpace(empty))
					{
						_log.LogInfo((object)("YouTube title [" + text4 + "]: " + empty));
					}
					progress?.Invoke("2/4 YouTube 오디오 스트림 받는 중...");
					string text15 = text9 + "-f " + Q("ba/b") + " -o " + Q(value4) + " " + Q(url);
					_log.LogInfo((object)("yt-dlp audio args [" + text4 + "]: " + RedactUrl(text15)));
					ProcessResult processResult3 = RunProcess(text, text15, text5, 240000);
					if (processResult3.ExitCode != 0)
					{
						_log.LogError((object)("yt-dlp audio stderr [" + text4 + "]: " + processResult3.StdErr));
						return PipelineResult.Fail("오디오 다운로드 실패: " + ShortError(processResult3.StdErr));
					}
					string text16 = FindMediaFile(text5, "audio.*");
					if (text16 == null)
					{
						return PipelineResult.Fail("오디오 다운로드는 완료되었지만 audio 파일을 찾지 못했습니다.");
					}
					progress?.Invoke("3/4 TV용 MP4 만드는 중...");
					string text17 = "-y -hide_banner -loglevel warning -i " + Q(text14) + " -i " + Q(text16) + " -map 0:v:0 -map 1:a:0 -c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p -c:a aac -b:a 160k -ar 48000 -ac 2 -shortest -movflags +faststart " + Q(text6);
					_log.LogInfo((object)("ffmpeg fallback args [" + text4 + "]: " + text17));
					ProcessResult processResult4 = RunProcess(text2, text17, text5, 360000);
					if (processResult4.ExitCode != 0 || !File.Exists(text6))
					{
						_log.LogError((object)("ffmpeg stderr [" + text4 + "]: " + processResult4.StdErr));
						return PipelineResult.Fail("ffmpeg 변환 실패: " + ShortError(processResult4.StdErr));
					}
					_log.LogInfo((object)$"Compatibility transcode [{text4}] completed in {(double)processResult4.ElapsedMs / 1000.0:0.00}s.");
					text10 = text16;
				}
				progress?.Invoke(flag ? "2/3 붐박스/크루저용 오디오 만드는 중..." : "4/4 붐박스/크루저용 오디오 만드는 중...");
				string text18 = "-y -hide_banner -loglevel warning -i " + Q(text10) + " -vn -c:a pcm_s16le -ar 44100 -ac 1 " + Q(text7);
				_log.LogInfo((object)("ffmpeg WAV args [" + text4 + "]: " + text18));
				ProcessResult processResult5 = RunProcess(text2, text18, text5, 180000);
				if (processResult5.ExitCode != 0 || !File.Exists(text7))
				{
					_log.LogError((object)("ffmpeg WAV stderr [" + text4 + "]: " + processResult5.StdErr));
					return PipelineResult.Fail("붐박스/크루저용 오디오 변환 실패: " + ShortError(processResult5.StdErr));
				}
				if (flag)
				{
					progress?.Invoke("3/3 재생 준비 완료!");
				}
				long length = new FileInfo(text6).Length;
				long length2 = new FileInfo(text7).Length;
				_log.LogInfo((object)$"Created TV MP4 [{text4}]: {text6} ({(double)length / 1024.0 / 1024.0:0.00} MiB)");
				_log.LogInfo((object)$"Created synchronized WAV [{text4}]: {text7} ({(double)length2 / 1024.0 / 1024.0:0.00} MiB)");
				return PipelineResult.Success(text6, empty);
			}
			catch (Exception ex)
			{
				return PipelineResult.Fail("미디어 준비 오류: " + ex.Message);
			}
			finally
			{
				try
				{
					string text19 = SanitizeCacheKey(cacheKey);
					string path = Path.Combine(_cacheRoot, "work_" + text19);
					if (Directory.Exists(path))
					{
						Directory.Delete(path, recursive: true);
					}
				}
				catch
				{
				}
				_mediaGate.Release();
			}
		}

		internal TitleResult FetchTitle(string url)
		{
			string text = ResolveTool("yt-dlp.exe");
			string text2 = ResolveTool("deno.exe");
			if (text == null || text2 == null)
			{
				return TitleResult.Fail("yt-dlp 또는 Deno를 찾지 못했습니다.");
			}
			Directory.CreateDirectory(_cacheRoot);
			string text3 = Path.Combine(_cacheRoot, "queue_title_" + Guid.NewGuid().ToString("N") + ".txt");
			try
			{
				string args = "--skip-download --no-playlist --no-color --encoding utf-8 --js-runtimes " + Q("deno:" + text2) + " --print-to-file " + Q("%(title)s") + " " + Q(text3) + " " + Q(url);
				ProcessResult processResult = RunProcess(text, args, _cacheRoot, 60000);
				if (processResult.ExitCode != 0)
				{
					return TitleResult.Fail(ShortError(processResult.StdErr));
				}
				string text4 = ReadUtf8TextFile(text3);
				return string.IsNullOrWhiteSpace(text4) ? TitleResult.Fail("제목이 비어 있습니다.") : TitleResult.Success(text4);
			}
			catch (Exception ex)
			{
				return TitleResult.Fail(ex.Message);
			}
			finally
			{
				try
				{
					if (File.Exists(text3))
					{
						File.Delete(text3);
					}
				}
				catch
				{
				}
			}
		}

		internal static bool IsSupportedYouTubeUrl(string raw, out string normalized, out string reason)
		{
			normalized = string.Empty;
			reason = string.Empty;
			if (string.IsNullOrWhiteSpace(raw))
			{
				reason = "YouTube 링크를 입력해 주세요.";
				return false;
			}
			raw = raw.Trim();
			if (!Uri.TryCreate(raw, UriKind.Absolute, out Uri result))
			{
				reason = "링크 형식이 이상해.";
				return false;
			}
			if (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps)
			{
				reason = "http/https 링크만 지원해.";
				return false;
			}
			string text = result.Host.ToLowerInvariant();
			if (!(text == "youtu.be") && !(text == "youtube.com") && !text.EndsWith(".youtube.com"))
			{
				reason = "v0.1에서는 YouTube 링크만 지원해.";
				return false;
			}
			normalized = result.AbsoluteUri;
			return true;
		}

		private static void ResetWorkDirectory(string path)
		{
			try
			{
				if (Directory.Exists(path))
				{
					Directory.Delete(path, recursive: true);
				}
			}
			catch
			{
			}
			Directory.CreateDirectory(path);
		}

		private static string SanitizeCacheKey(string key)
		{
			if (string.IsNullOrWhiteSpace(key))
			{
				return "media";
			}
			char[] invalid = Path.GetInvalidFileNameChars();
			return new string(key.Select((char c) => (!invalid.Contains(c)) ? c : '_').ToArray());
		}

		private static string FindMediaFile(string directory, string pattern)
		{
			return (from p in Directory.GetFiles(directory, pattern)
				where !p.EndsWith(".part", StringComparison.OrdinalIgnoreCase)
				where !p.EndsWith(".ytdl", StringComparison.OrdinalIgnoreCase)
				orderby new FileInfo(p).Length descending
				select p).FirstOrDefault();
		}

		private static string ReadUtf8TextFile(string path)
		{
			try
			{
				if (!File.Exists(path))
				{
					return string.Empty;
				}
				return File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)).Trim();
			}
			catch
			{
				try
				{
					return File.ReadAllText(path, Encoding.UTF8).Trim();
				}
				catch
				{
					return string.Empty;
				}
			}
		}

		private string ResolveTool(string exeName)
		{
			string text = Path.Combine(_pluginDir, "tools", exeName);
			if (File.Exists(text))
			{
				return text;
			}
			string text2 = Path.Combine(_pluginDir, exeName);
			if (File.Exists(text2))
			{
				return text2;
			}
			string[] array = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator);
			foreach (string text3 in array)
			{
				try
				{
					if (!string.IsNullOrWhiteSpace(text3))
					{
						string text4 = Path.Combine(text3.Trim().Trim('"'), exeName);
						if (File.Exists(text4))
						{
							return text4;
						}
					}
				}
				catch
				{
				}
			}
			try
			{
				string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
				if (!string.IsNullOrWhiteSpace(folderPath))
				{
					string text5 = Path.Combine(folderPath, "Microsoft", "WinGet", "Links", exeName);
					if (File.Exists(text5))
					{
						return text5;
					}
					string path = Path.Combine(folderPath, "Microsoft", "WinGet", "Packages");
					if (Directory.Exists(path))
					{
						string text6 = Directory.EnumerateFiles(path, exeName, SearchOption.AllDirectories).FirstOrDefault();
						if (!string.IsNullOrEmpty(text6) && File.Exists(text6))
						{
							return text6;
						}
					}
				}
			}
			catch (Exception ex)
			{
				_log.LogDebug((object)("WinGet tool lookup failed for " + exeName + ": " + ex.Message));
			}
			return null;
		}

		private static ProcessResult RunProcess(string exe, string args, string workingDir, int timeoutMs)
		{
			ProcessStartInfo processStartInfo = new ProcessStartInfo
			{
				FileName = exe,
				Arguments = args,
				WorkingDirectory = workingDir,
				UseShellExecute = false,
				CreateNoWindow = true,
				RedirectStandardOutput = true,
				RedirectStandardError = true,
				StandardOutputEncoding = Encoding.UTF8,
				StandardErrorEncoding = Encoding.UTF8
			};
			try
			{
				processStartInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
				processStartInfo.EnvironmentVariables["PYTHONUTF8"] = "1";
			}
			catch
			{
			}
			using Process process = new Process
			{
				StartInfo = processStartInfo
			};
			Stopwatch stopwatch = Stopwatch.StartNew();
			process.Start();
			try
			{
				process.PriorityClass = ProcessPriorityClass.BelowNormal;
				process.PriorityBoostEnabled = false;
			}
			catch
			{
			}
			Task<string> task = process.StandardOutput.ReadToEndAsync();
			Task<string> task2 = process.StandardError.ReadToEndAsync();
			if (!process.WaitForExit(timeoutMs))
			{
				try
				{
					process.Kill();
				}
				catch
				{
				}
				stopwatch.Stop();
				return new ProcessResult(-1, task.GetAwaiter().GetResult(), "시간 초과", stopwatch.ElapsedMilliseconds);
			}
			stopwatch.Stop();
			return new ProcessResult(process.ExitCode, task.GetAwaiter().GetResult(), task2.GetAwaiter().GetResult(), stopwatch.ElapsedMilliseconds);
		}

		private static string Q(string value)
		{
			return "\"" + value.Replace("\"", "\\\"") + "\"";
		}

		private static string ShortError(string text)
		{
			if (string.IsNullOrWhiteSpace(text))
			{
				return "알 수 없는 오류";
			}
			string[] array = text.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
			return string.Join(" | ", array.TakeLast(Math.Min(4, array.Length)));
		}

		private static string RedactUrl(string text)
		{
			return text;
		}
	}
	internal readonly struct ToolCheckResult
	{
		internal readonly bool Ok;

		internal readonly string Message;

		private ToolCheckResult(bool ok, string message)
		{
			Ok = ok;
			Message = message;
		}

		internal static ToolCheckResult Success()
		{
			return new ToolCheckResult(ok: true, string.Empty);
		}

		internal static ToolCheckResult Fail(string message)
		{
			return new ToolCheckResult(ok: false, message);
		}
	}
	internal readonly struct TitleResult
	{
		internal readonly bool Ok;

		internal readonly string Title;

		internal readonly string Message;

		private TitleResult(bool ok, string title, string message)
		{
			Ok = ok;
			Title = title ?? string.Empty;
			Message = message ?? string.Empty;
		}

		internal static TitleResult Success(string title)
		{
			return new TitleResult(ok: true, title, string.Empty);
		}

		internal static TitleResult Fail(string message)
		{
			return new TitleResult(ok: false, string.Empty, message);
		}
	}
	internal readonly struct PipelineResult
	{
		internal readonly bool Ok;

		internal readonly string Message;

		internal readonly string OutputPath;

		internal readonly string Title;

		private PipelineResult(bool ok, string message, string outputPath, string title)
		{
			Ok = ok;
			Message = message;
			OutputPath = outputPath;
			Title = title;
		}

		internal static PipelineResult Success(string path, string title)
		{
			return new PipelineResult(ok: true, string.Empty, path, title ?? string.Empty);
		}

		internal static PipelineResult Fail(string message)
		{
			return new PipelineResult(ok: false, message, string.Empty, string.Empty);
		}
	}
	public sealed class Plugin : MonoBehaviour
	{
		private enum NetworkRequestType : byte
		{
			AddUrl = 1,
			Skip,
			Clear,
			RequestSnapshot,
			RemoveQueueItem,
			ReplayHistoryItem,
			RemoveHistoryItem,
			ShuffleQueue
		}

		private sealed class RemoteQueueState
		{
			internal int Id;

			internal string Url = string.Empty;

			internal string Title = string.Empty;
		}

		private sealed class QueuedVideo
		{
			internal int Id;

			internal string Url = string.Empty;

			internal string Title = "제목을 불러오는 중...";

			internal string CachedPath = string.Empty;

			internal string PreloadStatus = "대기";

			internal string PreloadError = string.Empty;

			internal bool PreloadStarted;

			internal bool Ready;

			internal bool Removed;
		}

		private sealed class SyncedAudioDevice
		{
			internal Component Owner;

			internal string Label = string.Empty;

			internal AudioSource VanillaAudio;

			internal AudioSource AuxAudio;

			internal AudioSource CustomAudio;

			internal string LoadedPath = string.Empty;

			internal bool Prepared;

			internal bool VanillaStateCaptured;

			internal bool VanillaMuteBefore;

			internal float VanillaVolumeBefore;

			internal bool AuxStateCaptured;

			internal bool AuxMuteBefore;

			internal float AuxVolumeBefore;
		}

		private sealed class WavData
		{
			internal int Channels;

			internal int SampleRate;

			internal float[] Samples;

			internal int FrameCount;
		}

		private const int NetworkProtocol = 2;

		private const string MsgHello = "gj.lethaltubetv.hello";

		private const string MsgHelloAck = "gj.lethaltubetv.helloAck";

		private const string MsgSnapshot = "gj.lethaltubetv.snapshot";

		private const string MsgRequest = "gj.lethaltubetv.request";

		private const float HandshakeTimeoutSeconds = 5f;

		private const float SnapshotIntervalSeconds = 0.4f;

		private const int MaxNetworkQueueItems = 64;

		private const int MaxNetworkUrlLength = 2048;

		private NetworkManager _networkManager;

		private CustomMessagingManager _customMessaging;

		private bool _networkHandlersRegistered;

		private bool _networkSessionWasActive;

		private float _nextNetworkHelloAt;

		private bool _networkHandshakeAccepted;

		private bool _networkSnapshotReceived;

		private float _nextNetworkSnapshotAt;

		private long _networkSnapshotRevision;

		private readonly Dictionary<ulong, float> _pendingModHandshakes = new Dictionary<ulong, float>();

		private readonly HashSet<ulong> _verifiedModClients = new HashSet<ulong>();

		private string _remoteCurrentUrl = string.Empty;

		private string _remoteCurrentTitle = string.Empty;

		private bool _remotePlaybackRunning;

		private double _remotePlaybackTime;

		private double _remoteSnapshotServerTime;

		private long _remoteSnapshotRevision;

		private string _pendingRemoteCurrentUrl = string.Empty;

		private string _pendingRemoteCurrentTitle = string.Empty;

		private string _networkFailedCurrentUrl = string.Empty;

		private float _networkFailedCurrentRetryAt;

		public const string PluginGuid = "gj.lethaltubetv";

		public const string PluginName = "LethalTubeTV";

		public const string PluginVersion = "0.3.1";

		internal static ManualLogSource Log;

		internal static Plugin Instance;

		private static int _suppressQuickMenuThroughFrame = -1;

		private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();

		private readonly List<QueuedVideo> _queue = new List<QueuedVideo>();

		private readonly List<QueuedVideo> _history = new List<QueuedVideo>();

		private readonly Dictionary<int, SyncedAudioDevice> _boomboxSync = new Dictionary<int, SyncedAudioDevice>();

		private readonly Dictionary<int, SyncedAudioDevice> _cruiserSync = new Dictionary<int, SyncedAudioDevice>();

		private int _nextQueueItemId;

		private float _nextDeviceSyncSearchTime;

		private FieldInfo _cruiserRadioOnField;

		private bool _preloadWorkerRunning;

		private int _waitingForNextReadyId = -1;

		private int _skipWhenReadyId = -1;

		private string _currentLocalPath = string.Empty;

		private AudioClip _syncedAudioClip;

		private string _syncedAudioClipSourcePath = string.Empty;

		private bool _syncedAudioClipLoading;

		private int _syncedAudioClipLoadToken;

		private double _sharedPlaybackTime;

		private float _sharedPlaybackAnchorUnscaledTime;

		private bool _sharedPlaybackRunning;

		private bool _handlingSharedTrackEnd;

		private ConfigEntry<KeyCode> _openKey;

		private ConfigEntry<float> _interactionDistance;

		private ConfigEntry<int> _tvResolution;

		private ConfigEntry<bool> _useVanillaBoombox;

		private ConfigEntry<float> _youtubeVolume;

		private ConfigEntry<float> _boomboxVolume;

		private ConfigEntry<float> _cruiserVolume;

		private ConfigEntry<float> _cruiserMaxDistance;

		private ConfigEntry<bool> _showNowPlayingHud;

		private ConfigEntry<float> _nowPlayingHudDistance;

		private TVScript _tv;

		private float _nextTvSearchTime;

		private bool _showWindow;

		private bool _busy;

		private bool _ownsPlayback;

		private bool _loggedFirstFrame;

		private bool _mediaReady;

		private bool _haveObservedTvPower;

		private bool _lastObservedTvOn;

		private string _url = string.Empty;

		private string _currentUrl = string.Empty;

		private string _currentTitle = string.Empty;

		private int _loadToken;

		private string _status = "준비됨";

		private Rect _windowRect = new Rect(0f, 0f, 720f, 420f);

		private CursorLockMode _previousCursorLock;

		private bool _previousCursorVisible;

		private PlayerControllerB _uiPlayer;

		private FieldInfo _disableLookInputField;

		private bool _previousDisableLookInput;

		private bool _capturedLookInput;

		private FieldInfo _disableMoveInputField;

		private bool _previousDisableMoveInput;

		private bool _capturedMoveInput;

		private bool _previousIsTypingChat;

		private bool _capturedTypingChat;

		private object _gameInputTarget;

		private MethodInfo _deactivateGameInputMethod;

		private MethodInfo _activateGameInputMethod;

		private bool _gameInputDeactivated;

		private bool _gameInputWasActive = true;

		private bool _clearGuiFocusOnNextDraw;

		private bool _focusUrlOnNextRepaint;

		private bool _showQueuePanel;

		private Vector2 _queueScroll;

		private GUIStyle _panelStyle;

		private GUIStyle _headerStyle;

		private GUIStyle _titleStyle;

		private GUIStyle _subtitleStyle;

		private GUIStyle _labelStyle;

		private GUIStyle _statusStyle;

		private GUIStyle _inputStyle;

		private GUIStyle _buttonStyle;

		private GUIStyle _dangerButtonStyle;

		private GUIStyle _smallDangerButtonStyle;

		private GUIStyle _queueItemStyle;

		private GUIStyle _playingQueueItemStyle;

		private GUIStyle _nowPlayingStyle;

		private GUIStyle _nowPlayingShadowStyle;

		private Texture2D _panelTexture;

		private Texture2D _headerTexture;

		private Texture2D _fieldTexture;

		private Texture2D _statusTexture;

		private Texture2D _playingTexture;

		private Texture2D _buttonTexture;

		private Texture2D _buttonHoverTexture;

		private Texture2D _dangerTexture;

		private Texture2D _dangerHoverTexture;

		private GameObject _videoPlayerHost;

		private VideoPlayer _customPlayer;

		private VideoPlayer _originalPlayer;

		private RenderTexture _tvScreenTargetTexture;

		private RenderTexture _vanillaVideoSinkTexture;

		private Texture2D _idleLogoTexture;

		private bool _idleLogoApplied;

		private bool _originalVideoStateCaptured;

		private bool _originalVideoEnabledBefore;

		private RenderTexture _originalVideoTargetBefore;

		private VideoAudioOutputMode _originalVideoAudioModeBefore;

		private long _lastCustomVideoFrame = long.MinValue;

		private float _lastCustomVideoFrameAdvanceTime;

		private float _customVideoSeekGraceUntil;

		private float _nextCustomVideoResyncTime;

		private bool _customVideoAwaitingSeekFrame;

		private long _customVideoFrameBeforeSeek = long.MinValue;

		private int _customVideoSeekRecoveryAttempts;

		private AudioSource _tvAudio;

		private AudioSource _customAudio;

		private bool _vanillaAudioStateCaptured;

		private bool _vanillaAudioMuteBefore;

		private float _vanillaAudioVolumeBefore;

		private FieldInfo _videoField;

		private MethodInfo _setTvScreenMaterialMethod;

		private MediaPipeline _pipeline;

		private bool _toolSetupStarted;

		private bool _toolSetupFinished;

		private string _toolSetupError = string.Empty;

		private bool NetworkSessionAvailable
		{
			get
			{
				if ((Object)(object)_networkManager != (Object)null)
				{
					return _networkManager.IsListening;
				}
				return false;
			}
		}

		private bool IsNetworkClientNonHost
		{
			get
			{
				if (NetworkSessionAvailable && _networkManager.IsClient)
				{
					return !_networkManager.IsHost;
				}
				return false;
			}
		}

		private bool UseRemoteNetworkAuthority
		{
			get
			{
				if (IsNetworkClientNonHost && _networkHandshakeAccepted)
				{
					return _networkSnapshotReceived;
				}
				return false;
			}
		}

		private ManualLogSource Logger => BootstrapPlugin.SharedLog ?? Log;

		private ConfigFile Config => BootstrapPlugin.SharedConfig;

		internal static bool UseVanillaBoombox => Instance?._useVanillaBoombox?.Value == true;

		internal static bool UiOpen
		{
			get
			{
				if ((Object)(object)Instance != (Object)null)
				{
					return Instance._showWindow;
				}
				return false;
			}
		}

		internal static bool BlockGameplayInput => UiOpen;

		private bool TryValidateReceivedYouTubeUrl(string rawUrl, out string normalizedUrl, string source)
		{
			normalizedUrl = string.Empty;
			if (string.IsNullOrWhiteSpace(rawUrl) || rawUrl.Length > 2048)
			{
				Logger.LogWarning((object)("Rejected invalid network URL from " + source + ": empty or too long."));
				return false;
			}
			if (!MediaPipeline.IsSupportedYouTubeUrl(rawUrl, out normalizedUrl, out var reason))
			{
				Logger.LogWarning((object)("Rejected invalid network URL from " + source + ": " + reason));
				return false;
			}
			return true;
		}

		private void UpdateNetworkSync()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			if ((Object)(object)singleton == (Object)null || !singleton.IsListening)
			{
				if ((Object)(object)_networkManager != (Object)null)
				{
					ShutdownNetworkSync();
				}
				if (_networkSessionWasActive)
				{
					_networkSessionWasActive = false;
					ResetForLobbyOrSessionChange("network session ended");
				}
				return;
			}
			if (_networkSessionWasActive && (Object)(object)_networkManager != (Object)null && _networkManager != singleton)
			{
				ResetForLobbyOrSessionChange("network session changed");
				ShutdownNetworkSync();
			}
			_networkSessionWasActive = true;
			if (_networkManager != singleton || !_networkHandlersRegistered)
			{
				InitializeNetworkSync(singleton);
			}
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsListening)
			{
				return;
			}
			if (_networkManager.IsHost)
			{
				EnforceRequiredModHandshake();
				if (Time.unscaledTime >= _nextNetworkSnapshotAt)
				{
					_nextNetworkSnapshotAt = Time.unscaledTime + 0.4f;
					BroadcastAuthoritativeSnapshot();
				}
			}
			else if (_networkManager.IsClient && !_networkHandshakeAccepted && Time.unscaledTime >= _nextNetworkHelloAt)
			{
				SendHelloToHost();
			}
			if (UseRemoteNetworkAuthority)
			{
				TryApplyPendingRemoteCurrent();
			}
		}

		private void InitializeNetworkSync(NetworkManager nm)
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Expected O, but got Unknown
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Expected O, but got Unknown
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Expected O, but got Unknown
			ShutdownNetworkSync();
			_networkManager = nm;
			_customMessaging = nm.CustomMessagingManager;
			if (_customMessaging == null)
			{
				return;
			}
			_customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.hello", new HandleNamedMessageDelegate(OnNetworkHello));
			_customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.helloAck", new HandleNamedMessageDelegate(OnNetworkHelloAck));
			_customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.snapshot", new HandleNamedMessageDelegate(OnNetworkSnapshot));
			_customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.request", new HandleNamedMessageDelegate(OnNetworkRequest));
			_networkHandlersRegistered = true;
			nm.OnClientConnectedCallback += OnNetworkClientConnected;
			nm.OnClientDisconnectCallback += OnNetworkClientDisconnected;
			_nextNetworkHelloAt = Time.unscaledTime;
			_networkHandshakeAccepted = nm.IsHost;
			_networkSnapshotReceived = nm.IsHost;
			_nextNetworkSnapshotAt = Time.unscaledTime + 0.15f;
			if (nm.IsHost)
			{
				_verifiedModClients.Clear();
				_verifiedModClients.Add(nm.LocalClientId);
				foreach (ulong connectedClientsId in nm.ConnectedClientsIds)
				{
					if (connectedClientsId != nm.LocalClientId)
					{
						_pendingModHandshakes[connectedClientsId] = Time.unscaledTime + 5f;
					}
				}
				Logger.LogInfo((object)"Multiplayer sync initialized as HOST. LethalTubeTV is required for every remote client.");
			}
			else if (nm.IsClient)
			{
				SendHelloToHost();
				Logger.LogInfo((object)"Multiplayer sync initialized as CLIENT. Waiting for host handshake.");
			}
		}

		private void ShutdownNetworkSync()
		{
			try
			{
				if (_networkHandlersRegistered && _customMessaging != null)
				{
					_customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.hello");
					_customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.helloAck");
					_customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.snapshot");
					_customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.request");
				}
				if ((Object)(object)_networkManager != (Object)null)
				{
					_networkManager.OnClientConnectedCallback -= OnNetworkClientConnected;
					_networkManager.OnClientDisconnectCallback -= OnNetworkClientDisconnected;
				}
			}
			catch
			{
			}
			_networkManager = null;
			_customMessaging = null;
			_networkHandlersRegistered = false;
			_nextNetworkHelloAt = 0f;
			_networkHandshakeAccepted = false;
			_networkSnapshotReceived = false;
			_networkSnapshotRevision = 0L;
			_remoteSnapshotRevision = 0L;
			_pendingModHandshakes.Clear();
			_verifiedModClients.Clear();
			_remoteCurrentUrl = string.Empty;
			_remoteCurrentTitle = string.Empty;
			_pendingRemoteCurrentUrl = string.Empty;
			_pendingRemoteCurrentTitle = string.Empty;
		}

		private void OnNetworkClientConnected(ulong clientId)
		{
			if ((Object)(object)_networkManager == (Object)null)
			{
				return;
			}
			if (_networkManager.IsHost)
			{
				if (clientId != _networkManager.LocalClientId)
				{
					_pendingModHandshakes[clientId] = Time.unscaledTime + 5f;
					Logger.LogInfo((object)string.Format("Client {0} connected; waiting for LethalTubeTV {1} handshake.", clientId, "0.3.1"));
				}
			}
			else if (_networkManager.IsClient && clientId == _networkManager.LocalClientId)
			{
				SendHelloToHost();
			}
		}

		private void OnNetworkClientDisconnected(ulong clientId)
		{
			_pendingModHandshakes.Remove(clientId);
			_verifiedModClients.Remove(clientId);
			if (!((Object)(object)_networkManager != (Object)null) || _networkManager.IsHost || clientId != _networkManager.LocalClientId)
			{
				return;
			}
			_networkHandshakeAccepted = false;
			_networkSnapshotReceived = false;
			if (_networkSessionWasActive)
			{
				_networkSessionWasActive = false;
				_mainThreadActions.Enqueue(delegate
				{
					ResetForLobbyOrSessionChange("local client disconnected");
				});
			}
		}

		private void EnforceRequiredModHandshake()
		{
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _pendingModHandshakes.Count == 0)
			{
				return;
			}
			float unscaledTime = Time.unscaledTime;
			KeyValuePair<ulong, float>[] array = _pendingModHandshakes.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<ulong, float> keyValuePair = array[i];
				if (unscaledTime < keyValuePair.Value)
				{
					continue;
				}
				ulong key = keyValuePair.Key;
				_pendingModHandshakes.Remove(key);
				if (!_verifiedModClients.Contains(key))
				{
					Logger.LogWarning((object)string.Format("Disconnecting client {0}: LethalTubeTV {1} handshake not received.", key, "0.3.1"));
					try
					{
						_networkManager.DisconnectClient(key, "LethalTubeTV 0.3.1 모드가 필요합니다. 같은 버전을 설치한 뒤 다시 접속해주세요.");
					}
					catch (Exception ex)
					{
						Logger.LogWarning((object)("Failed to disconnect client without LethalTubeTV: " + ex.Message));
					}
				}
			}
		}

		private unsafe void SendHelloToHost()
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || _customMessaging == null || !_networkManager.IsClient || _networkManager.IsHost)
			{
				return;
			}
			try
			{
				FastBufferWriter val = default(FastBufferWriter);
				((FastBufferWriter)(ref val))..ctor(256, (Allocator)2, 2048);
				try
				{
					int num = 2;
					((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe("0.3.1", false);
					_customMessaging.SendNamedMessage("gj.lethaltubetv.hello", 0uL, val, (NetworkDelivery)3);
					_nextNetworkHelloAt = Time.unscaledTime + 1f;
					Logger.LogInfo((object)string.Format("Sent multiplayer hello to host. protocol={0}, version={1}", 2, "0.3.1"));
				}
				finally
				{
					((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
				}
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("Failed to send multiplayer hello: " + ex.Message));
				_nextNetworkHelloAt = Time.unscaledTime + 1f;
			}
		}

		private void OnNetworkHello(ulong senderClientId, FastBufferReader reader)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || senderClientId == _networkManager.LocalClientId)
			{
				return;
			}
			try
			{
				int num = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
				string text = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
				if (num != 2 || !string.Equals(text, "0.3.1", StringComparison.Ordinal))
				{
					_pendingModHandshakes.Remove(senderClientId);
					Logger.LogWarning((object)$"Rejecting client {senderClientId}: incompatible LethalTubeTV protocol/version {num}/{text}.");
					_networkManager.DisconnectClient(senderClientId, "LethalTubeTV 버전이 맞지 않습니다. 방장은 0.3.1 버전을 사용 중입니다.");
				}
				else
				{
					_pendingModHandshakes.Remove(senderClientId);
					_verifiedModClients.Add(senderClientId);
					SendHelloAck(senderClientId);
					SendAuthoritativeSnapshot(senderClientId);
					Logger.LogInfo((object)$"Client {senderClientId} verified for LethalTubeTV multiplayer sync.");
				}
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)$"Invalid LethalTubeTV hello from client {senderClientId}: {ex.Message}");
			}
		}

		private unsafe void SendHelloAck(ulong clientId)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			if (_customMessaging == null)
			{
				return;
			}
			FastBufferWriter val = default(FastBufferWriter);
			((FastBufferWriter)(ref val))..ctor(256, (Allocator)2, 2048);
			try
			{
				int num = 2;
				((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives));
				((FastBufferWriter)(ref val)).WriteValueSafe("0.3.1", false);
				_customMessaging.SendNamedMessage("gj.lethaltubetv.helloAck", clientId, val, (NetworkDelivery)3);
			}
			finally
			{
				((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
			}
		}

		private void OnNetworkHelloAck(ulong senderClientId, FastBufferReader reader)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || _networkManager.IsHost || senderClientId != 0L)
			{
				return;
			}
			try
			{
				int num = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
				string text = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
				if (num != 2 || !string.Equals(text, "0.3.1", StringComparison.Ordinal))
				{
					_status = "방장과 LethalTubeTV 버전이 달라. 방장: " + text + " / 나: 0.3.1";
					Logger.LogWarning((object)_status);
					return;
				}
				_networkHandshakeAccepted = true;
				_status = "멀티 동기화 연결됨 · 방장 재생 상태를 따라가는 중";
				Logger.LogInfo((object)"LethalTubeTV multiplayer handshake accepted by host.");
				SendRequest(NetworkRequestType.RequestSnapshot, string.Empty);
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("Invalid hello acknowledgement from host: " + ex.Message));
			}
		}

		private bool TrySendNetworkAddRequest(string normalizedUrl)
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.AddUrl, normalizedUrl);
			return true;
		}

		private bool TrySendNetworkSkipRequest()
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.Skip, string.Empty);
			return true;
		}

		private bool TrySendNetworkClearRequest()
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.Clear, string.Empty);
			return true;
		}

		private bool TrySendNetworkRemoveQueueItemRequest(int itemId)
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.RemoveQueueItem, itemId.ToString());
			return true;
		}

		private bool TrySendNetworkReplayHistoryRequest(int itemId)
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.ReplayHistoryItem, itemId.ToString());
			return true;
		}

		private bool TrySendNetworkRemoveHistoryRequest(int itemId)
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.RemoveHistoryItem, itemId.ToString());
			return true;
		}

		private bool TrySendNetworkShuffleRequest()
		{
			if (!IsNetworkClientNonHost)
			{
				return false;
			}
			if (!_networkHandshakeAccepted)
			{
				_status = "방장과 멀티 동기화 연결을 기다리는 중입니다.";
				return true;
			}
			SendRequest(NetworkRequestType.ShuffleQueue, string.Empty);
			return true;
		}

		private unsafe void SendRequest(NetworkRequestType requestType, string payload)
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			if (_customMessaging == null || (Object)(object)_networkManager == (Object)null || !_networkManager.IsClient || _networkManager.IsHost)
			{
				return;
			}
			try
			{
				FastBufferWriter val = default(FastBufferWriter);
				((FastBufferWriter)(ref val))..ctor(512, (Allocator)2, 8192);
				try
				{
					byte b = (byte)requestType;
					((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe(payload ?? string.Empty, false);
					_customMessaging.SendNamedMessage("gj.lethaltubetv.request", 0uL, val, (NetworkDelivery)3);
				}
				finally
				{
					((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
				}
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("Failed to send LethalTubeTV request: " + ex.Message));
			}
		}

		private void OnNetworkRequest(ulong senderClientId, FastBufferReader reader)
		{
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || senderClientId == _networkManager.LocalClientId)
			{
				return;
			}
			if (!_verifiedModClients.Contains(senderClientId))
			{
				Logger.LogWarning((object)$"Ignoring LethalTubeTV request from unverified client {senderClientId}.");
				return;
			}
			try
			{
				byte b = default(byte);
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives));
				string text = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
				switch ((NetworkRequestType)b)
				{
				default:
					return;
				case NetworkRequestType.AddUrl:
				{
					if (!TryValidateReceivedYouTubeUrl(text, out var normalizedUrl, $"client {senderClientId} AddUrl request"))
					{
						return;
					}
					SubmitUrl(normalizedUrl);
					break;
				}
				case NetworkRequestType.Skip:
					SkipToNext();
					break;
				case NetworkRequestType.Clear:
					ClearQueueAndReturnToVanilla();
					break;
				case NetworkRequestType.RequestSnapshot:
					SendAuthoritativeSnapshot(senderClientId);
					return;
				case NetworkRequestType.RemoveQueueItem:
				{
					if (!int.TryParse(text, out var result3))
					{
						return;
					}
					RemoveQueuedVideoLocal(result3);
					break;
				}
				case NetworkRequestType.ReplayHistoryItem:
				{
					if (!int.TryParse(text, out var result2))
					{
						return;
					}
					ReplayHistoryVideoLocal(result2);
					break;
				}
				case NetworkRequestType.RemoveHistoryItem:
				{
					if (!int.TryParse(text, out var result))
					{
						return;
					}
					RemoveHistoryVideoLocal(result);
					break;
				}
				case NetworkRequestType.ShuffleQueue:
					ShuffleQueueLocal();
					break;
				}
				BroadcastAuthoritativeSnapshot();
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)$"Failed to process LethalTubeTV request from {senderClientId}: {ex.Message}");
			}
		}

		private void BroadcastAuthoritativeSnapshot()
		{
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _customMessaging == null)
			{
				return;
			}
			ulong[] array = _verifiedModClients.ToArray();
			foreach (ulong num in array)
			{
				if (num != _networkManager.LocalClientId)
				{
					if (!_networkManager.ConnectedClientsIds.Contains(num))
					{
						_verifiedModClients.Remove(num);
					}
					else
					{
						SendAuthoritativeSnapshot(num);
					}
				}
			}
		}

		private unsafe void SendAuthoritativeSnapshot(ulong clientId)
		{
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_014d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0153: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Unknown result type (might be due to invalid IL or missing references)
			//IL_0179: Unknown result type (might be due to invalid IL or missing references)
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_021a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0220: Unknown result type (might be due to invalid IL or missing references)
			//IL_026f: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _customMessaging == null)
			{
				return;
			}
			try
			{
				FastBufferWriter val = default(FastBufferWriter);
				((FastBufferWriter)(ref val))..ctor(4096, (Allocator)2, 262144);
				try
				{
					bool flag = _busy || _ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentUrl);
					double num = (flag ? GetSharedPlaybackTime() : 0.0);
					NetworkTime serverTime = _networkManager.ServerTime;
					double time = ((NetworkTime)(ref serverTime)).Time;
					int num2 = Math.Min(_queue.Count, 64);
					int num3 = Math.Min(_history.Count, 64);
					int num4 = 2;
					((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num4, default(ForPrimitives));
					long num5 = ++_networkSnapshotRevision;
					((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref num5, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<double>(ref time, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref flag, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe(_currentUrl ?? string.Empty, false);
					((FastBufferWriter)(ref val)).WriteValueSafe(_currentTitle ?? string.Empty, false);
					((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref _sharedPlaybackRunning, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<double>(ref num, default(ForPrimitives));
					((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num2, default(ForPrimitives));
					for (int i = 0; i < num2; i++)
					{
						QueuedVideo queuedVideo = _queue[i];
						((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref queuedVideo.Id, default(ForPrimitives));
						((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo.Url ?? string.Empty, false);
						((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo.Title ?? string.Empty, false);
					}
					((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num3, default(ForPrimitives));
					for (int j = 0; j < num3; j++)
					{
						QueuedVideo queuedVideo2 = _history[j];
						((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref queuedVideo2.Id, default(ForPrimitives));
						((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo2.Url ?? string.Empty, false);
						((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo2.Title ?? string.Empty, false);
					}
					_customMessaging.SendNamedMessage("gj.lethaltubetv.snapshot", clientId, val, (NetworkDelivery)4);
				}
				finally
				{
					((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
				}
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)$"Failed to send LethalTubeTV snapshot to client {clientId}: {ex.Message}");
			}
		}

		private void OnNetworkSnapshot(ulong senderClientId, FastBufferReader reader)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0126: Unknown result type (might be due to invalid IL or missing references)
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01db: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_networkManager == (Object)null || _networkManager.IsHost || senderClientId != 0L)
			{
				return;
			}
			try
			{
				int num = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
				if (num != 2)
				{
					return;
				}
				long num2 = default(long);
				((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num2, default(ForPrimitives));
				double remoteSnapshotServerTime = default(double);
				((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref remoteSnapshotServerTime, default(ForPrimitives));
				bool flag = default(bool);
				((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref flag, default(ForPrimitives));
				string rawUrl = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl, false);
				string text = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
				bool remotePlaybackRunning = default(bool);
				((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref remotePlaybackRunning, default(ForPrimitives));
				double val = default(double);
				((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref val, default(ForPrimitives));
				int val2 = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref val2, default(ForPrimitives));
				if (num2 <= _remoteSnapshotRevision)
				{
					return;
				}
				if (flag)
				{
					if (!TryValidateReceivedYouTubeUrl(rawUrl, out var normalizedUrl, "host snapshot current track"))
					{
						_status = "방장으로부터 유효하지 않은 YouTube URL을 받아 무시했습니다.";
						return;
					}
					rawUrl = normalizedUrl;
				}
				else
				{
					rawUrl = string.Empty;
				}
				val2 = Math.Max(0, Math.Min(val2, 64));
				List<RemoteQueueState> list = new List<RemoteQueueState>(val2);
				int num3 = default(int);
				string rawUrl2 = default(string);
				string text2 = default(string);
				for (int i = 0; i < val2; i++)
				{
					((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num3, default(ForPrimitives));
					((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl2, false);
					((FastBufferReader)(ref reader)).ReadValueSafe(ref text2, false);
					if (TryValidateReceivedYouTubeUrl(rawUrl2, out var normalizedUrl2, $"host snapshot queue item {num3}"))
					{
						list.Add(new RemoteQueueState
						{
							Id = num3,
							Url = normalizedUrl2,
							Title = (text2 ?? string.Empty)
						});
					}
				}
				int val3 = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref val3, default(ForPrimitives));
				val3 = Math.Max(0, Math.Min(val3, 64));
				List<RemoteQueueState> list2 = new List<RemoteQueueState>(val3);
				int num4 = default(int);
				string rawUrl3 = default(string);
				string text3 = default(string);
				for (int j = 0; j < val3; j++)
				{
					((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num4, default(ForPrimitives));
					((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl3, false);
					((FastBufferReader)(ref reader)).ReadValueSafe(ref text3, false);
					if (TryValidateReceivedYouTubeUrl(rawUrl3, out var normalizedUrl3, $"host snapshot history item {num4}"))
					{
						list2.Add(new RemoteQueueState
						{
							Id = num4,
							Url = normalizedUrl3,
							Title = (text3 ?? string.Empty)
						});
					}
				}
				_networkHandshakeAccepted = true;
				_networkSnapshotReceived = true;
				_remoteSnapshotRevision = num2;
				_remoteCurrentUrl = rawUrl ?? string.Empty;
				_remoteCurrentTitle = text ?? string.Empty;
				_remotePlaybackRunning = remotePlaybackRunning;
				_remotePlaybackTime = Math.Max(0.0, val);
				_remoteSnapshotServerTime = remoteSnapshotServerTime;
				ApplyRemoteCurrentState(flag, _remoteCurrentUrl, _remoteCurrentTitle);
				ReconcileRemoteQueue(list);
				ReconcileRemoteHistory(list2);
			}
			catch (Exception ex)
			{
				Logger.LogWarning((object)("Failed to read LethalTubeTV host snapshot: " + ex.Message));
			}
		}

		private void ApplyRemoteNetworkClock()
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			if (!UseRemoteNetworkAuthority || (Object)(object)_networkManager == (Object)null)
			{
				return;
			}
			double num = _remotePlaybackTime;
			if (_remotePlaybackRunning)
			{
				NetworkTime serverTime = _networkManager.ServerTime;
				double num2 = ((NetworkTime)(ref serverTime)).Time - _remoteSnapshotServerTime;
				if (num2 > 0.0)
				{
					num += num2;
				}
			}
			_sharedPlaybackTime = Math.Max(0.0, num);
			_sharedPlaybackAnchorUnscaledTime = Time.unscaledTime;
			_sharedPlaybackRunning = _remotePlaybackRunning;
			_handlingSharedTrackEnd = false;
		}

		private void ApplyRemoteCurrentState(bool hasSession, string currentUrl, string currentTitle)
		{
			if (hasSession)
			{
				if (!TryValidateReceivedYouTubeUrl(currentUrl, out var normalizedUrl, "remote current-state apply"))
				{
					_status = "방장으로부터 유효하지 않은 YouTube URL을 받아 무시했습니다.";
					return;
				}
				currentUrl = normalizedUrl;
			}
			if (!hasSession || string.IsNullOrWhiteSpace(currentUrl))
			{
				_pendingRemoteCurrentUrl = string.Empty;
				_pendingRemoteCurrentTitle = string.Empty;
				_networkFailedCurrentUrl = string.Empty;
				if (_busy || _ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentUrl))
				{
					StopPlayback();
				}
				_status = "방장 대기열 비어 있음 · 바닐라 장치 사용 중";
			}
			else if (string.Equals(_currentUrl, currentUrl, StringComparison.Ordinal))
			{
				if (!string.IsNullOrWhiteSpace(currentTitle) && currentTitle != "제목을 불러오는 중...")
				{
					_currentTitle = currentTitle;
				}
			}
			else
			{
				_pendingRemoteCurrentUrl = currentUrl;
				_pendingRemoteCurrentTitle = (string.IsNullOrWhiteSpace(currentTitle) ? "제목을 불러오는 중..." : currentTitle);
				TryApplyPendingRemoteCurrent();
			}
		}

		private void TryApplyPendingRemoteCurrent()
		{
			if (!UseRemoteNetworkAuthority || string.IsNullOrWhiteSpace(_pendingRemoteCurrentUrl))
			{
				return;
			}
			string pendingRemoteCurrentUrl = _pendingRemoteCurrentUrl;
			string pendingRemoteCurrentTitle = _pendingRemoteCurrentTitle;
			if (!TryValidateReceivedYouTubeUrl(pendingRemoteCurrentUrl, out var normalizedUrl, "pending remote track download"))
			{
				_pendingRemoteCurrentUrl = string.Empty;
				_pendingRemoteCurrentTitle = string.Empty;
				_status = "유효하지 않은 원격 YouTube URL을 차단했습니다.";
				return;
			}
			pendingRemoteCurrentUrl = normalizedUrl;
			if (string.Equals(_currentUrl, pendingRemoteCurrentUrl, StringComparison.Ordinal))
			{
				_pendingRemoteCurrentUrl = string.Empty;
				_pendingRemoteCurrentTitle = string.Empty;
				if (!string.IsNullOrWhiteSpace(pendingRemoteCurrentTitle) && pendingRemoteCurrentTitle != "제목을 불러오는 중...")
				{
					_currentTitle = pendingRemoteCurrentTitle;
				}
			}
			else
			{
				if (_busy || (string.Equals(_networkFailedCurrentUrl, pendingRemoteCurrentUrl, StringComparison.Ordinal) && Time.unscaledTime < _networkFailedCurrentRetryAt))
				{
					return;
				}
				_networkFailedCurrentUrl = string.Empty;
				QueuedVideo queuedVideo = null;
				for (int i = 0; i < _queue.Count; i++)
				{
					QueuedVideo queuedVideo2 = _queue[i];
					if (!queuedVideo2.Removed && queuedVideo2.Ready && string.Equals(queuedVideo2.Url, pendingRemoteCurrentUrl, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(queuedVideo2.CachedPath) && File.Exists(queuedVideo2.CachedPath))
					{
						queuedVideo = queuedVideo2;
						break;
					}
				}
				if (_ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentLocalPath))
				{
					ArchiveCurrentTrackForHistory();
					StopCurrentMediaForTransition(preserveCurrentFile: true);
				}
				_mediaReady = false;
				_loggedFirstFrame = false;
				_currentUrl = pendingRemoteCurrentUrl;
				_currentTitle = (string.IsNullOrWhiteSpace(pendingRemoteCurrentTitle) ? "제목을 불러오는 중..." : pendingRemoteCurrentTitle);
				_pendingRemoteCurrentUrl = string.Empty;
				_pendingRemoteCurrentTitle = string.Empty;
				if (queuedVideo != null)
				{
					_queue.Remove(queuedVideo);
					string cachedPath = queuedVideo.CachedPath;
					queuedVideo.CachedPath = string.Empty;
					queuedVideo.Removed = true;
					Logger.LogInfo((object)("Client switching instantly to preloaded host track: " + queuedVideo.Title));
					PlayLocalFile(cachedPath);
					TryStartNextPreload();
				}
				else
				{
					_currentUrl = string.Empty;
					_currentTitle = string.Empty;
					Logger.LogInfo((object)("Client downloading host current track: " + pendingRemoteCurrentUrl));
					StartDownload(pendingRemoteCurrentUrl, closeWindow: false, pendingRemoteCurrentTitle);
				}
			}
		}

		private bool HasDifferentPendingRemoteCurrent(string justDownloadedUrl)
		{
			if (UseRemoteNetworkAuthority && !string.IsNullOrWhiteSpace(_pendingRemoteCurrentUrl))
			{
				return !string.Equals(_pendingRemoteCurrentUrl, justDownloadedUrl, StringComparison.Ordinal);
			}
			return false;
		}

		private void NotifyNetworkCurrentDownloadFailed(string url)
		{
			if (UseRemoteNetworkAuthority && !string.IsNullOrWhiteSpace(url))
			{
				_networkFailedCurrentUrl = url;
				_networkFailedCurrentRetryAt = Time.unscaledTime + 15f;
				_status = "현재 동기화 영상 다운로드에 실패했습니다. 15초 뒤 자동으로 다시 시도합니다.";
			}
		}

		private void ReconcileRemoteQueue(List<RemoteQueueState> incoming)
		{
			if (!UseRemoteNetworkAuthority)
			{
				return;
			}
			Dictionary<int, QueuedVideo> dictionary = _queue.ToDictionary((QueuedVideo q) => q.Id, (QueuedVideo q) => q);
			HashSet<int> hashSet = new HashSet<int>(incoming.Select((RemoteQueueState q) => q.Id));
			QueuedVideo[] array = _queue.ToArray();
			foreach (QueuedVideo queuedVideo in array)
			{
				if (!hashSet.Contains(queuedVideo.Id))
				{
					queuedVideo.Removed = true;
					if (!string.IsNullOrWhiteSpace(queuedVideo.CachedPath))
					{
						_pipeline.DeleteCachedFile(queuedVideo.CachedPath);
					}
				}
			}
			List<QueuedVideo> list = new List<QueuedVideo>(incoming.Count);
			foreach (RemoteQueueState item in incoming)
			{
				if (TryValidateReceivedYouTubeUrl(item.Url, out var normalizedUrl, $"remote queue reconcile item {item.Id}"))
				{
					item.Url = normalizedUrl;
					if (!dictionary.TryGetValue(item.Id, out var value) || !string.Equals(value.Url, item.Url, StringComparison.Ordinal))
					{
						value = new QueuedVideo
						{
							Id = item.Id,
							Url = item.Url,
							Title = (string.IsNullOrWhiteSpace(item.Title) ? "제목을 불러오는 중..." : item.Title),
							PreloadStatus = "미리 받기 대기"
						};
					}
					else if (!string.IsNullOrWhiteSpace(item.Title) && item.Title != "제목을 불러오는 중...")
					{
						value.Title = item.Title;
					}
					value.Removed = false;
					list.Add(value);
				}
			}
			_queue.Clear();
			_queue.AddRange(list);
			if (incoming.Count > 0)
			{
				_nextQueueItemId = Math.Max(_nextQueueItemId, incoming.Max((RemoteQueueState x) => x.Id));
			}
			TryStartNextPreload();
		}

		private void ReconcileRemoteHistory(List<RemoteQueueState> incoming)
		{
			if (!UseRemoteNetworkAuthority)
			{
				return;
			}
			QueuedVideo[] array = _history.ToArray();
			List<QueuedVideo> list = new List<QueuedVideo>(incoming.Count);
			foreach (RemoteQueueState item in incoming)
			{
				if (!TryValidateReceivedYouTubeUrl(item.Url, out var normalizedRemoteUrl, $"remote history reconcile item {item.Id}"))
				{
					continue;
				}
				QueuedVideo queuedVideo = array.FirstOrDefault((QueuedVideo h) => !h.Removed && string.Equals(h.Url, normalizedRemoteUrl, StringComparison.Ordinal));
				if (queuedVideo == null)
				{
					queuedVideo = new QueuedVideo
					{
						Id = item.Id,
						Url = normalizedRemoteUrl,
						Title = (string.IsNullOrWhiteSpace(item.Title) ? "YouTube 영상" : item.Title),
						Ready = false,
						PreloadStarted = true,
						PreloadStatus = "재생 완료"
					};
				}
				else
				{
					queuedVideo.Id = item.Id;
					queuedVideo.Url = normalizedRemoteUrl;
					if (!string.IsNullOrWhiteSpace(item.Title))
					{
						queuedVideo.Title = item.Title;
					}
					queuedVideo.Removed = false;
					queuedVideo.PreloadStatus = "재생 완료";
				}
				list.Add(queuedVideo);
			}
			HashSet<string> hashSet = new HashSet<string>(from x in list
				where !string.IsNullOrWhiteSpace(x.CachedPath)
				select x.CachedPath, StringComparer.OrdinalIgnoreCase);
			QueuedVideo[] array2 = array;
			foreach (QueuedVideo queuedVideo2 in array2)
			{
				if (!string.IsNullOrWhiteSpace(queuedVideo2.CachedPath) && !hashSet.Contains(queuedVideo2.CachedPath) && !string.Equals(queuedVideo2.CachedPath, _currentLocalPath, StringComparison.OrdinalIgnoreCase))
				{
					_pipeline.DeleteCachedFile(queuedVideo2.CachedPath);
				}
			}
			_history.Clear();
			_history.AddRange(list);
			if (incoming.Count > 0)
			{
				_nextQueueItemId = Math.Max(_nextQueueItemId, incoming.Max((RemoteQueueState x) => x.Id));
			}
		}

		internal static bool EnsureCreated()
		{
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			if ((Object)(object)Instance == (Object)null)
			{
				try
				{
					Plugin[] array = Resources.FindObjectsOfTypeAll<Plugin>();
					foreach (Plugin plugin in array)
					{
						if (!((Object)(object)plugin == (Object)null) && !((Object)(object)((Component)plugin).gameObject == (Object)null))
						{
							Instance = plugin;
							break;
						}
					}
				}
				catch
				{
				}
			}
			if ((Object)(object)Instance != (Object)null)
			{
				return false;
			}
			GameObject val = new GameObject("LethalTubeTV.PersistentRuntime");
			Object.DontDestroyOnLoad((Object)val);
			val.AddComponent<Plugin>();
			return true;
		}

		internal static bool TryHandleTvPowerOverride(TVScript tv, bool on)
		{
			Plugin instance = Instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)tv == (Object)null)
			{
				return false;
			}
			try
			{
				instance._tv = tv;
				instance.EnsureCustomPlayer();
				instance.IsolateVanillaVideoPlayer();
				instance.SuppressVanillaTvAudio();
				tv.tvOn = on;
				Plugin plugin = instance;
				if ((object)plugin._setTvScreenMaterialMethod == null)
				{
					plugin._setTvScreenMaterialMethod = typeof(TVScript).GetMethod("SetTVScreenMaterial", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				}
				instance._setTvScreenMaterialMethod?.Invoke(tv, new object[1] { on });
				if (!on)
				{
					instance._idleLogoApplied = false;
					try
					{
						if ((Object)(object)instance._customPlayer != (Object)null && instance._customPlayer.isPlaying)
						{
							instance._customPlayer.Pause();
						}
						if ((Object)(object)instance._customAudio != (Object)null && instance._customAudio.isPlaying)
						{
							instance._customAudio.Pause();
						}
					}
					catch
					{
					}
				}
				else if (!instance._ownsPlayback || !instance._mediaReady)
				{
					instance.ShowIdleLogo(force: true);
				}
				ManualLogSource logger = instance.Logger;
				if (logger != null)
				{
					logger.LogInfo((object)("TV power handled by LethalTubeTV override: " + (on ? "ON" : "OFF")));
				}
				return true;
			}
			catch (Exception ex)
			{
				ManualLogSource logger2 = instance.Logger;
				if (logger2 != null)
				{
					logger2.LogWarning((object)("TV power override warning: " + ex.Message));
				}
				try
				{
					tv.tvOn = on;
					instance._setTvScreenMaterialMethod?.Invoke(tv, new object[1] { on });
				}
				catch
				{
				}
				return true;
			}
		}

		internal static bool ShouldBlockQuickMenu()
		{
			if (!UiOpen)
			{
				return Time.frameCount <= _suppressQuickMenuThroughFrame;
			}
			return true;
		}

		internal static void SuppressQuickMenuForFrames(int frames = 2)
		{
			_suppressQuickMenuThroughFrame = Mathf.Max(_suppressQuickMenuThroughFrame, Time.frameCount + Mathf.Max(1, frames));
		}

		private void Awake()
		{
			//IL_00c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Invalid comparison between Unknown and I4
			//IL_0156: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Expected O, but got Unknown
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Expected O, but got Unknown
			//IL_01f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fd: Expected O, but got Unknown
			//IL_0231: Unknown result type (might be due to invalid IL or missing references)
			//IL_023b: Expected O, but got Unknown
			//IL_026f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0279: Expected O, but got Unknown
			//IL_02ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d8: Expected O, but got Unknown
			//IL_0338: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this)
			{
				ManualLogSource logger = Logger;
				if (logger != null)
				{
					logger.LogWarning((object)"Duplicate LethalTubeTV runtime detected; destroying duplicate.");
				}
				Object.Destroy((Object)(object)((Component)this).gameObject);
				return;
			}
			Instance = this;
			Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
			Log = BootstrapPlugin.SharedLog ?? Log;
			if (Logger == null || Config == null)
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogError((object)"LethalTubeTV runtime started without bootstrap logger/config; destroying runtime.");
				}
				if (Instance == this)
				{
					Instance = null;
				}
				Object.Destroy((Object)(object)((Component)this).gameObject);
				return;
			}
			_openKey = Config.Bind<KeyCode>("General", "OpenKey", (KeyCode)104, "TV / 붐박스 / 크루저 근처에서 YouTube 제어창을 여는 키");
			if ((int)_openKey.Value == 287)
			{
				_openKey.Value = (KeyCode)104;
				Config.Save();
				Logger.LogInfo((object)"Migrated OpenKey from F6 to H.");
			}
			_interactionDistance = Config.Bind<float>("General", "InteractionDistance", 4.5f, "TV / 붐박스 / 크루저 제어창을 열 수 있는 최대 거리");
			_tvResolution = Config.Bind<int>("Video", "TVResolution", 480, new ConfigDescription("TV용 YouTube 영상 해상도입니다. 기본값은 480p이며 360/480/720 중 선택할 수 있습니다.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[3] { 360, 480, 720 }), Array.Empty<object>()));
			_useVanillaBoombox = Config.Bind<bool>("Boombox", "UseVanillaBoombox", false, "기본값(false): 붐박스 배터리 무한, 추가 무게 0kg, 다른 아이템으로 바꿔도 음악이 계속 재생됩니다. true로 변경하면 붐박스 배터리, 무게, 아이템 전환 동작을 바닐라 방식으로 사용합니다.");
			_youtubeVolume = Config.Bind<float>("Audio", "YouTubeVolume", 0.25f, new ConfigDescription("TV에서 재생되는 YouTube 볼륨. 0.0 = 음소거, 1.0 = 최대", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			_boomboxVolume = Config.Bind<float>("Audio", "BoomboxVolume", 0.6f, new ConfigDescription("붐박스에서 재생되는 YouTube 볼륨", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			_cruiserVolume = Config.Bind<float>("Audio", "CruiserVolume", 0.7f, new ConfigDescription("크루저 라디오에서 재생되는 YouTube 볼륨", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			_cruiserMaxDistance = Config.Bind<float>("Audio", "CruiserMaxDistance", 22f, new ConfigDescription("크루저 YouTube 오디오가 들리는 최대 거리. 크루저 오디오는 항상 완전한 3D 거리 감쇠를 사용함.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f), Array.Empty<object>()));
			_showNowPlayingHud = Config.Bind<bool>("UI", "ShowNowPlayingHud", true, "H 메뉴를 닫았을 때 재생 중인 장치 근처에서 현재 YouTube 제목을 표시");
			_nowPlayingHudDistance = Config.Bind<float>("UI", "NowPlayingHudDistance", 10f, new ConfigDescription("현재 재생 HUD가 보이는 장치와의 최대 거리", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 30f), Array.Empty<object>()));
			string pluginDir = BootstrapPlugin.SharedPluginDir ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Paths.PluginPath;
			_pipeline = new MediaPipeline(pluginDir, Logger);
			StartAutomaticToolSetup();
			Logger.LogInfo((object)"LethalTubeTV 0.3.1 runtime initialized (target: Lethal Company v81)");
			Logger.LogInfo((object)$"Press {_openKey.Value} near the TV, a boombox, or the cruiser radio to open the YouTube controls.");
		}

		private void StartAutomaticToolSetup()
		{
			if (_toolSetupStarted || _pipeline == null)
			{
				return;
			}
			_toolSetupStarted = true;
			_toolSetupFinished = false;
			_toolSetupError = string.Empty;
			Task.Run(delegate
			{
				ToolSetupResult result;
				try
				{
					result = _pipeline.EnsureTools(delegate(string message)
					{
						Logger.LogInfo((object)("[ToolSetup] " + message));
						_mainThreadActions.Enqueue(delegate
						{
							if (!_busy && !_ownsPlayback && !_mediaReady)
							{
								_status = message;
							}
						});
					});
				}
				catch (Exception ex)
				{
					result = ToolSetupResult.Fail(ex.Message);
				}
				_mainThreadActions.Enqueue(delegate
				{
					_toolSetupFinished = true;
					if (result.Ok)
					{
						_toolSetupError = string.Empty;
						if (!_busy && !_ownsPlayback && !_mediaReady)
						{
							_status = "필수 도구 준비 완료 · YouTube 링크를 입력해 주세요.";
						}
						Logger.LogInfo((object)"LethalTubeTV automatic tool setup finished successfully.");
					}
					else
					{
						_toolSetupError = result.Message;
						_status = result.Message;
						Logger.LogError((object)("LethalTubeTV automatic tool setup failed: " + result.Message));
					}
				});
			});
		}

		private TVScript FindTvIncludingInactive()
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				TVScript val = Object.FindObjectOfType<TVScript>();
				if ((Object)(object)val != (Object)null)
				{
					return val;
				}
				TVScript[] array = Resources.FindObjectsOfTypeAll<TVScript>();
				foreach (TVScript val2 in array)
				{
					if (!((Object)(object)val2 == (Object)null) && !((Object)(object)((Component)val2).gameObject == (Object)null))
					{
						Scene scene = ((Component)val2).gameObject.scene;
						if (((Scene)(ref scene)).IsValid() && ((Scene)(ref scene)).isLoaded)
						{
							return val2;
						}
					}
				}
			}
			catch (Exception ex)
			{
				Logger.LogDebug((object)("TV discovery warning: " + ex.Message));
			}
			return null;
		}

		private void Update()
		{
			Action result;
			while (_mainThreadActions.TryDequeue(out result))
			{
				try
				{
					result();
				}
				catch (Exception ex)
				{
					Logger.LogError((object)ex);
				}
			}
			UpdateNetworkSync();
			if (_showWindow)
			{
				KeepUiInputCaptured();
			}
			if (Time.unscaledTime >= _nextTvSearchTime)
			{
				_nextTvSearchTime = Time.unscaledTime + 2f;
				if ((Object)(object)_tv == (Object)null)
				{
					_tv = FindTvIncludingInactive();
				}
			}
			if ((Object)(object)_tv != (Object)null)
			{
				try
				{
					EnsureCustomPlayer();
					IsolateVanillaVideoPlayer();
					SuppressVanillaTvAudio();
					if ((Object)(object)_customAudio != (Object)null)
					{
						_customAudio.volume = Mathf.Clamp01(_youtubeVolume.Value);
					}
					if (!_haveObservedTvPower)
					{
						_lastObservedTvOn = _tv.tvOn;
						_haveObservedTvPower = true;
					}
					bool tvOn = _tv.tvOn;
					if (tvOn != _lastObservedTvOn)
					{
						_lastObservedTvOn = tvOn;
						_idleLogoApplied = false;
						Logger.LogInfo((object)("TV power changed by game/player: " + (tvOn ? "ON" : "OFF")));
					}
					if (!_ownsPlayback || !_mediaReady)
					{
						if (tvOn)
						{
							_setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { true });
							ShowIdleLogo();
						}
						else
						{
							_setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { false });
							_idleLogoApplied = false;
						}
					}
					else
					{
						_idleLogoApplied = false;
					}
				}
				catch (Exception ex2)
				{
					Logger.LogDebug((object)("TV presentation maintenance warning: " + ex2.Message));
				}
			}
			UpdateSyncedPlaybackDevices();
		}

		private void UpdateSyncedPlaybackDevices()
		{
			if (!_ownsPlayback || !_mediaReady || string.IsNullOrWhiteSpace(_currentLocalPath))
			{
				if (_sharedPlaybackRunning)
				{
					PauseSharedPlaybackClock();
				}
				return;
			}
			if (Time.unscaledTime >= _nextDeviceSyncSearchTime)
			{
				_nextDeviceSyncSearchTime = Time.unscaledTime + 0.75f;
				DiscoverSyncedDevices();
			}
			bool flag = (Object)(object)_tv != (Object)null && _tv.tvOn;
			bool flag2 = false;
			KeyValuePair<int, SyncedAudioDevice>[] array = _boomboxSync.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<int, SyncedAudioDevice> keyValuePair = array[i];
				SyncedAudioDevice value = keyValuePair.Value;
				if ((Object)(object)value?.Owner == (Object)null)
				{
					_boomboxSync.Remove(keyValuePair.Key);
					continue;
				}
				Component owner = value.Owner;
				BoomboxItem val = (BoomboxItem)(object)((owner is BoomboxItem) ? owner : null);
				if (val != null && val.isPlayingMusic)
				{
					flag2 = true;
				}
			}
			bool flag3 = false;
			array = _cruiserSync.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<int, SyncedAudioDevice> keyValuePair2 = array[i];
				SyncedAudioDevice value2 = keyValuePair2.Value;
				if ((Object)(object)value2?.Owner == (Object)null)
				{
					_cruiserSync.Remove(keyValuePair2.Key);
					continue;
				}
				Component owner2 = value2.Owner;
				VehicleController val2 = (VehicleController)(object)((owner2 is VehicleController) ? owner2 : null);
				if (val2 != null && IsCruiserRadioOn(val2))
				{
					flag3 = true;
				}
			}
			bool flag4 = (Object)(object)_syncedAudioClip != (Object)null && string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase);
			bool num = flag && (Object)(object)_customPlayer != (Object)null && _customPlayer.isPrepared && flag4;
			bool flag5 = flag2 && flag4;
			bool flag6 = flag3 && flag4;
			bool flag7 = num || flag5 || flag6;
			if (UseRemoteNetworkAuthority)
			{
				ApplyRemoteNetworkClock();
			}
			else if (flag7)
			{
				StartSharedPlaybackClock();
			}
			else
			{
				PauseSharedPlaybackClock();
			}
			double sharedPlaybackTime = GetSharedPlaybackTime();
			double currentMediaDuration = GetCurrentMediaDuration();
			if (!UseRemoteNetworkAuthority && _sharedPlaybackRunning && !_handlingSharedTrackEnd && currentMediaDuration > 0.1 && sharedPlaybackTime >= currentMediaDuration - 0.04)
			{
				_handlingSharedTrackEnd = true;
				PauseSharedPlaybackClock();
				_sharedPlaybackTime = currentMediaDuration;
				try
				{
					Logger.LogInfo((object)$"Shared track finished. time={sharedPlaybackTime:0.00}s, duration={currentMediaDuration:0.00}s, queueCount={_queue.Count}");
					HandleVideoFinished();
					return;
				}
				finally
				{
					_handlingSharedTrackEnd = false;
				}
			}
			SyncTvDevice(flag, _sharedPlaybackRunning, sharedPlaybackTime);
			array = _boomboxSync.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<int, SyncedAudioDevice> keyValuePair3 = array[i];
				SyncedAudioDevice value3 = keyValuePair3.Value;
				if ((Object)(object)value3?.Owner == (Object)null)
				{
					_boomboxSync.Remove(keyValuePair3.Key);
					continue;
				}
				Component owner3 = value3.Owner;
				BoomboxItem val3 = (BoomboxItem)(object)((owner3 is BoomboxItem) ? owner3 : null);
				bool desiredOn = (Object)(object)val3 != (Object)null && val3.isPlayingMusic;
				SyncAudioDevice(value3, desiredOn, _sharedPlaybackRunning, sharedPlaybackTime);
			}
			array = _cruiserSync.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<int, SyncedAudioDevice> keyValuePair4 = array[i];
				SyncedAudioDevice value4 = keyValuePair4.Value;
				if ((Object)(object)value4?.Owner == (Object)null)
				{
					_cruiserSync.Remove(keyValuePair4.Key);
					continue;
				}
				Component owner4 = value4.Owner;
				VehicleController cruiser = (VehicleController)(object)((owner4 is VehicleController) ? owner4 : null);
				bool desiredOn2 = IsCruiserRadioOn(cruiser);
				SyncAudioDevice(value4, desiredOn2, _sharedPlaybackRunning, sharedPlaybackTime);
			}
		}

		private double GetSharedPlaybackTime()
		{
			if (!_sharedPlaybackRunning)
			{
				return Math.Max(0.0, _sharedPlaybackTime);
			}
			return Math.Max(0.0, _sharedPlaybackTime + (double)(Time.unscaledTime - _sharedPlaybackAnchorUnscaledTime));
		}

		private void StartSharedPlaybackClock()
		{
			if (!_sharedPlaybackRunning)
			{
				_sharedPlaybackAnchorUnscaledTime = Time.unscaledTime;
				_sharedPlaybackRunning = true;
				Logger.LogInfo((object)$"Shared playback clock START. time={_sharedPlaybackTime:0.00}s");
			}
		}

		private void PauseSharedPlaybackClock()
		{
			if (_sharedPlaybackRunning)
			{
				_sharedPlaybackTime = GetSharedPlaybackTime();
				_sharedPlaybackRunning = false;
				Logger.LogInfo((object)$"Shared playback clock PAUSE. time={_sharedPlaybackTime:0.00}s");
			}
		}

		private void ResetSharedPlaybackClock(double time = 0.0)
		{
			_sharedPlaybackTime = Math.Max(0.0, time);
			_sharedPlaybackAnchorUnscaledTime = Time.unscaledTime;
			_sharedPlaybackRunning = false;
			_handlingSharedTrackEnd = false;
		}

		private double GetCurrentMediaDuration()
		{
			try
			{
				if ((Object)(object)_syncedAudioClip != (Object)null && _syncedAudioClip.length > 0.1f)
				{
					return _syncedAudioClip.length;
				}
			}
			catch
			{
			}
			try
			{
				if ((Object)(object)_customPlayer != (Object)null && _customPlayer.isPrepared && _customPlayer.length > 0.1)
				{
					return _customPlayer.length;
				}
			}
			catch
			{
			}
			return 0.0;
		}

		private void SyncTvDevice(bool desiredOn, bool sharedPlaying, double sharedTime)
		{
			if ((Object)(object)_tv == (Object)null || (Object)(object)_customPlayer == (Object)null)
			{
				return;
			}
			try
			{
				if (!desiredOn)
				{
					if (_customPlayer.isPlaying)
					{
						_customPlayer.Pause();
					}
					if ((Object)(object)_customAudio != (Object)null && _customAudio.isPlaying)
					{
						_customAudio.Pause();
					}
					_customVideoAwaitingSeekFrame = false;
					_customVideoSeekRecoveryAttempts = 0;
					_customVideoSeekGraceUntil = 0f;
					_setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { false });
					return;
				}
				SuppressVanillaTvAudio();
				_setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { true });
				if (!EnsureCustomVideoPlayerActive())
				{
					return;
				}
				if ((Object)(object)_tvScreenTargetTexture != (Object)null && (Object)(object)_customPlayer.targetTexture != (Object)(object)_tvScreenTargetTexture)
				{
					_customPlayer.targetTexture = _tvScreenTargetTexture;
				}
				if (!_customPlayer.isPrepared)
				{
					return;
				}
				if ((Object)(object)_syncedAudioClip == (Object)null || !string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase))
				{
					BeginLoadSyncedAudioClip(_currentLocalPath);
					return;
				}
				if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)(object)_syncedAudioClip)
				{
					_customAudio.Stop();
					_customAudio.clip = _syncedAudioClip;
					_customAudio.loop = false;
					Logger.LogInfo((object)($"TV synchronized AudioClip attached. length={_syncedAudioClip.length:0.00}s, " + $"channels={_syncedAudioClip.channels}, frequency={_syncedAudioClip.frequency}"));
				}
				if ((Object)(object)_customAudio != (Object)null)
				{
					((Behaviour)_customAudio).enabled = true;
					_customAudio.mute = false;
					_customAudio.volume = Mathf.Clamp01(_youtubeVolume.Value);
				}
				double num = Math.Max(0.01, _customPlayer.length);
				double num2 = (((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null) ? Math.Max(0.01, _customAudio.clip.length) : num);
				double num3 = Math.Max(0.0, Math.Min(sharedTime, num - 0.01));
				double num4 = Math.Max(0.0, Math.Min(sharedTime, num2 - 0.01));
				_customPlayer.timeReference = (VideoTimeReference)0;
				_customPlayer.playbackSpeed = 1f;
				if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null && Math.Abs((double)_customAudio.time - num4) > 0.06)
				{
					_customAudio.time = (float)num4;
				}
				if (!sharedPlaying)
				{
					if (_customPlayer.isPlaying)
					{
						_customPlayer.Pause();
					}
					if ((Object)(object)_customAudio != (Object)null && _customAudio.isPlaying)
					{
						_customAudio.Pause();
					}
					return;
				}
				float unscaledTime = Time.unscaledTime;
				if (!_customPlayer.isPlaying)
				{
					if (!EnsureCustomVideoPlayerActive())
					{
						Logger.LogWarning((object)"TV VideoPlayer could not be activated; keeping shared audio playback alive.");
						return;
					}
					_customVideoFrameBeforeSeek = _customPlayer.frame;
					if (_customPlayer.canSetTime)
					{
						_customPlayer.time = num3;
					}
					_customPlayer.Play();
					_customVideoAwaitingSeekFrame = true;
					_customVideoSeekGraceUntil = unscaledTime + 2.5f;
					_nextCustomVideoResyncTime = unscaledTime + 3f;
					_customVideoSeekRecoveryAttempts = 0;
					Logger.LogInfo((object)($"TV video join requested (single seek). target={num3:0.00}s, " + $"actual={_customPlayer.time:0.00}s, frame={_customPlayer.frame}, " + $"isPlaying={_customPlayer.isPlaying}, enabled={((Behaviour)_customPlayer).enabled}, " + $"active={((Component)_customPlayer).gameObject.activeInHierarchy}"));
				}
				long frame = _customPlayer.frame;
				if (frame != _lastCustomVideoFrame)
				{
					_lastCustomVideoFrame = frame;
					_lastCustomVideoFrameAdvanceTime = unscaledTime;
					if (_customVideoAwaitingSeekFrame && frame >= 0 && frame != _customVideoFrameBeforeSeek)
					{
						_customVideoAwaitingSeekFrame = false;
						_customVideoSeekRecoveryAttempts = 0;
						_nextCustomVideoResyncTime = unscaledTime + 3f;
						Logger.LogInfo((object)($"TV video seek caught up. frame={frame}, actual={_customPlayer.time:0.00}s, " + $"target={num3:0.00}s"));
					}
				}
				if (_customVideoAwaitingSeekFrame)
				{
					if (unscaledTime >= _customVideoSeekGraceUntil && unscaledTime >= _nextCustomVideoResyncTime && _customPlayer.canSetTime && _customVideoSeekRecoveryAttempts < 2)
					{
						_customVideoSeekRecoveryAttempts++;
						_customVideoFrameBeforeSeek = _customPlayer.frame;
						_customPlayer.Pause();
						_customPlayer.time = num3;
						_customPlayer.Play();
						_customVideoSeekGraceUntil = unscaledTime + 2.5f;
						_nextCustomVideoResyncTime = unscaledTime + 3f;
						Logger.LogWarning((object)($"TV video seek retry {_customVideoSeekRecoveryAttempts}/2. target={num3:0.00}s, " + $"frame={_customPlayer.frame}, isPlaying={_customPlayer.isPlaying}"));
					}
				}
				else if (unscaledTime >= _nextCustomVideoResyncTime && _customPlayer.canSetTime)
				{
					double num5 = Math.Abs(_customPlayer.time - num3);
					if (num5 > 1.25)
					{
						_customVideoFrameBeforeSeek = _customPlayer.frame;
						_customPlayer.time = num3;
						_customVideoAwaitingSeekFrame = true;
						_customVideoSeekGraceUntil = unscaledTime + 2f;
						_nextCustomVideoResyncTime = unscaledTime + 3f;
						_customVideoSeekRecoveryAttempts = 0;
						Logger.LogInfo((object)($"TV video drift resync requested. drift={num5:0.00}s, " + $"target={num3:0.00}s, actual={_customPlayer.time:0.00}s"));
					}
					else
					{
						_nextCustomVideoResyncTime = unscaledTime + 1f;
					}
				}
				if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null && !_customAudio.isPlaying)
				{
					_customAudio.time = (float)num4;
					_customAudio.Play();
					Logger.LogInfo((object)($"TV AudioClip joined shared clock. target={num4:0.00}s, " + $"actual={_customAudio.time:0.00}s"));
				}
			}
			catch (Exception ex)
			{
				Logger.LogDebug((object)("TV shared sync warning: " + ex.Message));
			}
		}

		private void DiscoverSyncedDevices()
		{
			try
			{
				BoomboxItem[] array = Object.FindObjectsOfType<BoomboxItem>();
				foreach (BoomboxItem val in array)
				{
					if (!((Object)(object)val == (Object)null) && !((Object)(object)val.boomboxAudio == (Object)null))
					{
						int instanceID = ((Object)val).GetInstanceID();
						if (!_boomboxSync.ContainsKey(instanceID))
						{
							_boomboxSync[instanceID] = CreateSyncedAudioDevice((Component)(object)val, val.boomboxAudio, null, "Boombox");
							Logger.LogInfo((object)$"Discovered boombox for synchronized YouTube audio. id={instanceID}");
						}
					}
				}
				VehicleController[] array2 = Object.FindObjectsOfType<VehicleController>();
				foreach (VehicleController val2 in array2)
				{
					if ((Object)(object)val2 == (Object)null || (Object)(object)val2.radioAudio == (Object)null)
					{
						continue;
					}
					int instanceID2 = ((Object)val2).GetInstanceID();
					if (!_cruiserSync.ContainsKey(instanceID2))
					{
						AudioSource auxAudio = null;
						try
						{
							auxAudio = val2.radioInterference;
						}
						catch
						{
						}
						_cruiserSync[instanceID2] = CreateSyncedAudioDevice((Component)(object)val2, val2.radioAudio, auxAudio, "Cruiser radio");
						Logger.LogInfo((object)$"Discovered cruiser radio for synchronized YouTube audio. id={instanceID2}");
					}
				}
			}
			catch (Exception ex)
			{
				Logger.LogDebug((object)("Device discovery warning: " + ex.Message));
			}
		}

		private SyncedAudioDevice CreateSyncedAudioDevice(Component owner, AudioSource vanillaAudio, AudioSource auxAudio, string label)
		{
			SyncedAudioDevice syncedAudioDevice = new SyncedAudioDevice
			{
				Owner = owner,
				Label = label,
				VanillaAudio = vanillaAudio,
				AuxAudio = auxAudio
			};
			GameObject val = (((Object)(object)vanillaAudio != (Object)null) ? ((Component)vanillaAudio).gameObject : owner.gameObject);
			AudioSource val2 = val.AddComponent<AudioSource>();
			val2.playOnAwake = false;
			val2.loop = false;
			val2.mute = true;
			CopyAudioSourceSettings(vanillaAudio, val2);
			if (owner is VehicleController)
			{
				val2.spatialBlend = 1f;
				val2.rolloffMode = (AudioRolloffMode)0;
				val2.minDistance = 2f;
				val2.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value);
				val2.dopplerLevel = 0f;
				val2.spread = 0f;
			}
			val2.volume = GetSyncedDeviceVolume(syncedAudioDevice);
			syncedAudioDevice.CustomAudio = val2;
			Logger.LogInfo((object)("Created " + label + " synchronized AudioSource. host=" + ((Object)val).name + ", " + $"volume={val2.volume:0.###}, spatialBlend={val2.spatialBlend:0.###}, " + $"minDistance={val2.minDistance:0.##}, maxDistance={val2.maxDistance:0.##}"));
			return syncedAudioDevice;
		}

		private static void CopyAudioSourceSettings(AudioSource source, AudioSource destination)
		{
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)source == (Object)null) && !((Object)(object)destination == (Object)null))
			{
				destination.outputAudioMixerGroup = source.outputAudioMixerGroup;
				destination.pitch = source.pitch;
				destination.priority = source.priority;
				destination.spatialBlend = source.spatialBlend;
				destination.dopplerLevel = source.dopplerLevel;
				destination.spread = source.spread;
				destination.rolloffMode = source.rolloffMode;
				destination.minDistance = source.minDistance;
				destination.maxDistance = source.maxDistance;
				destination.bypassEffects = source.bypassEffects;
				destination.bypassListenerEffects = source.bypassListenerEffects;
				destination.bypassReverbZones = source.bypassReverbZones;
			}
		}

		private float GetSyncedDeviceVolume(SyncedAudioDevice target)
		{
			if (target?.Owner is BoomboxItem)
			{
				return Mathf.Clamp01(_boomboxVolume.Value);
			}
			if (target?.Owner is VehicleController)
			{
				return Mathf.Clamp01(_cruiserVolume.Value);
			}
			return Mathf.Clamp01(_youtubeVolume.Value);
		}

		private void ApplySyncedDeviceVolumes()
		{
			foreach (SyncedAudioDevice value in _boomboxSync.Values)
			{
				if ((Object)(object)value?.CustomAudio != (Object)null)
				{
					value.CustomAudio.volume = Mathf.Clamp01(_boomboxVolume.Value);
				}
			}
			foreach (SyncedAudioDevice value2 in _cruiserSync.Values)
			{
				if ((Object)(object)value2?.CustomAudio != (Object)null)
				{
					value2.CustomAudio.volume = Mathf.Clamp01(_cruiserVolume.Value);
					value2.CustomAudio.spatialBlend = 1f;
					value2.CustomAudio.rolloffMode = (AudioRolloffMode)0;
					value2.CustomAudio.minDistance = 2f;
					value2.CustomAudio.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value);
					value2.CustomAudio.dopplerLevel = 0f;
				}
			}
		}

		private bool IsCruiserRadioOn(VehicleController cruiser)
		{
			if ((Object)(object)cruiser == (Object)null)
			{
				return false;
			}
			try
			{
				if ((object)_cruiserRadioOnField == null)
				{
					_cruiserRadioOnField = typeof(VehicleController).GetField("radioOn", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				}
				if (_cruiserRadioOnField != null && _cruiserRadioOnField.GetValue(cruiser) is bool result)
				{
					return result;
				}
			}
			catch
			{
			}
			try
			{
				return (Object)(object)cruiser.radioAudio != (Object)null && cruiser.radioAudio.isPlaying;
			}
			catch
			{
				return false;
			}
		}

		private void SyncAudioDevice(SyncedAudioDevice target, bool desiredOn, bool masterPlaying, double masterTime)
		{
			if (target == null || (Object)(object)target.Owner == (Object)null || (Object)(object)target.CustomAudio == (Object)null)
			{
				return;
			}
			if (!desiredOn)
			{
				if (target.CustomAudio.isPlaying)
				{
					target.CustomAudio.Pause();
				}
				target.CustomAudio.mute = true;
				RestoreDeviceVanillaAudio(target, resumeIfOn: false);
				return;
			}
			SuppressDeviceVanillaAudio(target);
			if (target.Owner is VehicleController)
			{
				target.CustomAudio.spatialBlend = 1f;
				target.CustomAudio.rolloffMode = (AudioRolloffMode)0;
				target.CustomAudio.minDistance = 2f;
				target.CustomAudio.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value);
				target.CustomAudio.dopplerLevel = 0f;
			}
			if ((Object)(object)_syncedAudioClip == (Object)null || !string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase))
			{
				BeginLoadSyncedAudioClip(_currentLocalPath);
				return;
			}
			if (!string.Equals(target.LoadedPath, _currentLocalPath, StringComparison.OrdinalIgnoreCase) || (Object)(object)target.CustomAudio.clip != (Object)(object)_syncedAudioClip)
			{
				PrepareSyncedAudioDevice(target, _currentLocalPath);
			}
			if (!target.Prepared || (Object)(object)target.CustomAudio.clip == (Object)null)
			{
				return;
			}
			((Behaviour)target.CustomAudio).enabled = true;
			target.CustomAudio.volume = GetSyncedDeviceVolume(target);
			if (!masterPlaying)
			{
				if (target.CustomAudio.isPlaying)
				{
					target.CustomAudio.Pause();
				}
				target.CustomAudio.mute = true;
				return;
			}
			try
			{
				target.CustomAudio.mute = false;
				double num = Math.Max(0.01, target.CustomAudio.clip.length);
				double num2 = Math.Max(0.0, Math.Min(masterTime, num - 0.01));
				if (Math.Abs((double)target.CustomAudio.time - num2) > 0.2)
				{
					target.CustomAudio.time = (float)num2;
				}
				if (!target.CustomAudio.isPlaying)
				{
					target.CustomAudio.Play();
					Logger.LogInfo((object)$"{target.Label} synchronized AudioClip playback started. time={num2:0.00}s, clip={((Object)target.CustomAudio.clip).name}");
				}
			}
			catch (Exception ex)
			{
				Logger.LogDebug((object)(target.Label + " sync warning: " + ex.Message));
			}
		}

		private void PrepareSyncedAudioDevice(SyncedAudioDevice target, string filePath)
		{
			if (target == null || (Object)(object)target.CustomAudio == (Object)null || (Object)(object)_syncedAudioClip == (Object)null)
			{
				return;
			}
			try
			{
				target.CustomAudio.Stop();
				target.CustomAudio.clip = _syncedAudioClip;
				target.CustomAudio.loop = false;
				target.CustomAudio.volume = GetSyncedDeviceVolume(target);
				target.CustomAudio.mute = false;
				target.LoadedPath = filePath;
				target.Prepared = true;
				Logger.LogInfo((object)$"{target.Label} synchronized AudioClip prepared. clip={((Object)_syncedAudioClip).name}, length={_syncedAudioClip.length:0.00}s, channels={_syncedAudioClip.channels}, frequency={_syncedAudioClip.frequency}");
			}
			catch (Exception ex)
			{
				target.Prepared = false;
				Logger.LogWarning((object)("Failed to prepare " + target.Label + " AudioClip: " + ex.Message));
			}
		}

		private void ResetSyncedAudioClipForNewTrack()
		{
			_syncedAudioClipLoadToken++;
			_syncedAudioClipLoading = false;
			_syncedAudioClipSourcePath = string.Empty;
			try
			{
				if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip == (Object)(object)_syncedAudioClip)
				{
					_customAudio.Stop();
					_customAudio.clip = null;
				}
				foreach (SyncedAudioDevice item in _boomboxSync.Values.Concat(_cruiserSync.Values))
				{
					if (!((Object)(object)item?.CustomAudio == (Object)null))
					{
						if ((Object)(object)item.CustomAudio.clip == (Object)(object)_syncedAudioClip)
						{
							item.CustomAudio.Stop();
							item.CustomAudio.clip = null;
						}
						item.Prepared = false;
						item.LoadedPath = string.Empty;
					}
				}
				if ((Object)(object)_syncedAudioClip != (Object)null)
				{
					Object.Destroy((Object)(object)_syncedAudioClip);
					_syncedAudioClip = null;
				}
			}
			catch (Exception ex)
			{
				Logger.LogDebug((object)("Synchronized AudioClip reset warning: " + ex.Message));
			}
		}

		private void BeginLoadSyncedAudioClip(string mp4Path)
		{
			if (string.IsNullOrWhiteSpace(mp4Path) || ((Object)(object)_syncedAudioClip != (Object)null && string.Equals(_syncedAudioClipSourcePath, mp4Path, StringComparison.OrdinalI