Decompiled source of SyncVideo v1.1.0

plugins/SyncVideo/SyncVideo.dll

Decompiled 4 hours ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using BombRushMP.Common;
using BombRushMP.Common.Networking;
using BombRushMP.Common.Packets;
using BombRushMP.Plugin;
using BombRushMP.Plugin.Gamemodes;
using CommonAPI;
using CommonAPI.Phone;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Reptile;
using Reptile.Phone;
using SyncVideo.Model;
using SyncVideo.Phone;
using SyncVideo.Runtime;
using SyncVideo.Transport;
using SyncVideo.Transport.Packets;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.Rendering;
using UnityEngine.UI;
using UnityEngine.Video;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 SyncVideo
{
	internal static class PluginInfo
	{
		public const string PLUGIN_GUID = "transrights.SyncVideo";

		public const string PLUGIN_NAME = "Sync Video";

		public const string PLUGIN_VERSION = "1.0.0";
	}
	public sealed class SyncVideoConfig
	{
		public const string DefaultLobbyName = "Sync Video Lobby";

		public const bool LogBusMessages = false;

		public ConfigEntry<string> TvObjectName { get; }

		public ConfigEntry<string> ScreenMaterialTextureName { get; }

		public ConfigEntry<float> HostBeaconInterval { get; }

		public ConfigEntry<bool> EnableOfflineMode { get; }

		public ConfigEntry<float> HostStateResendInterval { get; }

		public ConfigEntry<float> DriftToleranceSeconds { get; }

		public ConfigEntry<float> HardSeekThresholdSeconds { get; }

		public ConfigEntry<bool> AutoAttachToTVsOnStageLoad { get; }

		public ConfigEntry<bool> ShowScreenPositionMenu { get; }

		public ConfigEntry<bool> ShowRefreshScreensButton { get; }

		public ConfigEntry<bool> HideNativeLobbyUi { get; }

		public ConfigEntry<bool> HostAutoplay { get; }

		public ConfigEntry<int> DefaultVolume { get; }

		public ConfigEntry<string> VideoRenderResolution { get; }

		public ConfigEntry<string> YouTubeStreamResolution { get; }

		public ConfigEntry<bool> UseFFmpeg { get; }

		public ConfigEntry<bool> EnableMkvSupport { get; }

		public ConfigEntry<bool> SuppressAFK { get; }

		public ConfigEntry<bool> MuteMusicAndAmbient { get; }

		public ConfigEntry<bool> EnableMkvFfmpegConversion { get; }

		public ConfigEntry<bool> MkvTranscodeToH264 { get; }

		public ConfigEntry<float> YouTubeVolumeScale { get; }

		public ConfigEntry<float> SubtitleFontSize { get; }

		public string PluginDirectory { get; }

		public string AdvancedConfigPath { get; }

		public SyncVideoConfig(ConfigFile config, ConfigFile advancedConfig, string pluginLocation)
		{
			PluginDirectory = Path.GetDirectoryName(pluginLocation) ?? string.Empty;
			AdvancedConfigPath = advancedConfig.ConfigFilePath;
			EnableOfflineMode = config.Bind<bool>("OfflineMode", "EnableOfflineMode", false, "Disable all online functionality when enabled. Create local lobbies for personal use.");
			HideNativeLobbyUi = config.Bind<bool>("ACN", "Hide Lobby UI", true, "Hide ACN's lobby UI by default.");
			SuppressAFK = config.Bind<bool>("ACN", "Suppress AFK", true, "Hide AFK animations for yourself and other players while in a Sync Video lobby.");
			HostAutoplay = config.Bind<bool>("Video", "Host Autoplay", true, "If hosting a video lobby, automatically start playback loading your video URL.");
			VideoRenderResolution = config.Bind<string>("Video", "Video Render Resolution", "854x480", "Render resolution used for MP4 video playback. Higher resolutions will cause lag. Options: 1920x1080, 1280x720, 960x540, 854x480, 640x360, 426x240.");
			YouTubeStreamResolution = config.Bind<string>("Video", "YouTube Resolution", "1280x720", "Resolution used when streaming YouTube videos. Options: 1920x1080, 1280x720, 960x540, 854x480, 640x360, 426x240.");
			UseFFmpeg = config.Bind<bool>("Video", "Use FFmpeg", false, "Enable FFmpeg for higher quality YouTube video playback. Requires ffmpeg.exe to be placed in the plugin folder, next to SyncVideo.dll. When disabled, stream videos without downloading.");
			EnableMkvSupport = config.Bind<bool>("Video", "MKV Support", true, "Enable experimental MKV playback support. An MKV Settings menu for the host will appear in the app when an MKV file is loaded.");
			SubtitleFontSize = config.Bind<float>("Video", "Subtitle Font Size", 34f, "Font size for MKV subtitles displayed on the TV screen. Default is 34.");
			ShowScreenPositionMenu = config.Bind<bool>("Video", "Show Screen Position Menu", false, "Show the Screen Position menu in the phone app. Lets you move the screen around. Does not sync with viewers if host.");
			DefaultVolume = config.Bind<int>("Volume", "Default Volume", 90, "Starting volume level (0–100). Automatically rounds to increments of 10 in-game.");
			MuteMusicAndAmbient = config.Bind<bool>("Volume", "Mute Music and Ambient SFX", true, "Mute the game's music and ambient sounds while in a Sync Video lobby.");
			TvObjectName = advancedConfig.Bind<string>("Debug", "TV Object Name", "TV", "Scene object name to bind video screens to.");
			ScreenMaterialTextureName = advancedConfig.Bind<string>("Debug", "Screen Material Texture Name", "_MainTex", "Texture slot used when drawing video to a renderer.");
			AutoAttachToTVsOnStageLoad = advancedConfig.Bind<bool>("Debug", "Auto Attach To TVs On Load", true, "Automatically bind screens to matching TV objects when a map loads.");
			ShowRefreshScreensButton = advancedConfig.Bind<bool>("Debug", "Refresh Screens Button", false, "Show the Refresh Screens button in the phone app. Rebinds screens to objects.");
			YouTubeVolumeScale = advancedConfig.Bind<float>("Debug", "YouTube Volume Scale", 1f, "Volume multiplier applied to YouTube videos (0.0-1.0). Makes YouTube videos not kill your ears.");
			EnableMkvFfmpegConversion = advancedConfig.Bind<bool>("Experimental", "MKV To MP4 Conversion", false, "When enabled, MKV files are converted into MP4 with FFmpeg. This re-muxes the MKV to fix container issues for MKVs using the H.264 codec.");
			MkvTranscodeToH264 = advancedConfig.Bind<bool>("Experimental", "Transcode MKV to H.264", false, "Transcodes the video to H.264 instead of using the MKV's codec. Required for H.265/HEVC, VP9, AV1, and 10-bit H.264 sources. Will be very slow.");
			HostBeaconInterval = advancedConfig.Bind<float>("Networking", "Host Beacon Interval", 2f, "Seconds between lobby beacons.");
			HostStateResendInterval = advancedConfig.Bind<float>("Networking", "Host State Resend Interval", 0.5f, "Seconds between host sync state broadcasts.");
			DriftToleranceSeconds = advancedConfig.Bind<float>("Sync", "Drift Tolerance", 0.16f, "Amount of time difference between host and viewer, in seconds. If drift exceeds this, the viewer nudges toward host time.");
			HardSeekThresholdSeconds = advancedConfig.Bind<float>("Sync", "Hard Seek Threshold", 0.6f, "Maximum amount of time difference between host and viewer, in seconds. If drift exceeds this, the viewer performs a hard seek.");
			advancedConfig.Save();
		}
	}
	[BepInPlugin("transrights.SyncVideo", "Sync Video", "1.0.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class SyncVideoPlugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		public static SyncVideoPlugin Instance { get; private set; }

		public static SyncVideoConfig Settings { get; private set; }

		public static VideoLobbyManager LobbyManager { get; private set; }

		public static SyncVideoController SyncController { get; private set; }

		public static VideoScreenManager ScreenManager { get; private set; }

		public static SyncVideoTransport Transport { get; private set; }

		public static LobbyUiOverrideManager LobbyUiOverride { get; private set; }

		public static event Action<bool> LobbyStateChanged;

		private void Awake()
		{
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Expected O, but got Unknown
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ec: Expected O, but got Unknown
			Instance = this;
			string text = Path.Combine(Paths.ConfigPath, "transrights.SyncVideoAdvanced.cfg");
			ConfigFile advancedConfig = new ConfigFile(text, true);
			Settings = new SyncVideoConfig(((BaseUnityPlugin)this).Config, advancedConfig, ((BaseUnityPlugin)this).Info.Location);
			SyncVideoPacketRegistry.RegisterPackets(((BaseUnityPlugin)this).Logger);
			Transport = new SyncVideoTransport(((BaseUnityPlugin)this).Logger);
			LobbyManager = new VideoLobbyManager(((BaseUnityPlugin)this).Logger, Transport);
			LobbyManager.ActiveLobbyChanged += delegate(VideoLobby lobby)
			{
				SyncVideoPlugin.LobbyStateChanged?.Invoke(lobby != null);
			};
			SyncController = new SyncVideoController(((BaseUnityPlugin)this).Logger, LobbyManager);
			ScreenManager = new VideoScreenManager(((BaseUnityPlugin)this).Logger, SyncController);
			LobbyUiOverride = new LobbyUiOverrideManager(((BaseUnityPlugin)this).Logger, LobbyManager);
			_harmony = new Harmony("transrights.SyncVideo");
			_harmony.PatchAll();
			AppSyncVideo.Initialize();
			AppSyncVideoLobby.Initialize();
			AppSyncVideoPublicLobbies.Initialize();
			AppSyncVideoScreenOptions.Initialize();
			AppSyncVideoLobbyKick.Initialize();
			AppSyncVideoSuggestions.Initialize();
			AppSyncVideoMkvSettings.Initialize();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Sync Video loaded.");
		}

		private void Update()
		{
			LobbyManager.Tick(Time.unscaledDeltaTime);
			SyncController.Tick(Time.unscaledDeltaTime);
			ScreenManager.Tick(Time.unscaledDeltaTime);
			HudManager.Tick();
		}

		private void LateUpdate()
		{
			LobbyUiOverride?.Tick(Time.unscaledDeltaTime);
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			LobbyUiOverride?.Dispose();
			ScreenManager?.Dispose();
			SyncController?.Dispose();
			LobbyManager?.Dispose();
			Transport?.Dispose();
		}
	}
}
namespace SyncVideo.Transport
{
	public static class SyncVideoPacketRegistry
	{
		public static void RegisterPackets(ManualLogSource logger)
		{
		}
	}
	public sealed class SyncVideoTransport : IDisposable
	{
		private readonly ManualLogSource _logger;

		private bool _disposed;

		public ushort LocalPlayerId => (ushort)(((Object)(object)ClientController.Instance != (Object)null) ? ClientController.Instance.LocalID : 0);

		public bool Connected => (Object)(object)ClientController.Instance != (Object)null && ClientController.Instance.Connected;

		public event Action<SyncVideoPacketBase> SyncPacketReceived;

		public SyncVideoTransport(ManualLogSource logger)
		{
			_logger = logger;
			ClientController.RegisterCustomPacketHandler("syncvideo.state", (Action<ushort, byte[]>)OnRawState);
			ClientController.RegisterCustomPacketHandler("syncvideo.time", (Action<ushort, byte[]>)OnRawTime);
			ClientController.RegisterCustomPacketHandler("syncvideo.state_request", (Action<ushort, byte[]>)OnRawStateRequest);
			ClientController.RegisterCustomPacketHandler("syncvideo.lobby_advertise", (Action<ushort, byte[]>)OnRawLobbyAdvertise);
			ClientController.RegisterCustomPacketHandler("syncvideo.lobby_join", (Action<ushort, byte[]>)OnRawLobbyJoin);
			ClientController.RegisterCustomPacketHandler("syncvideo.lobby_leave", (Action<ushort, byte[]>)OnRawLobbyLeave);
			ClientController.RegisterCustomPacketHandler("syncvideo.lobby_members", (Action<ushort, byte[]>)OnRawLobbyMembers);
			ClientController.RegisterCustomPacketHandler("syncvideo.lobby_closed", (Action<ushort, byte[]>)OnRawLobbyClosed);
			ClientController.RegisterCustomPacketHandler("syncvideo.screen_transform", (Action<ushort, byte[]>)OnRawScreenTransform);
			ClientController.RegisterCustomPacketHandler("syncvideo.suggestion", (Action<ushort, byte[]>)OnRawSuggestion);
			ClientController.RegisterCustomPacketHandler("syncvideo.suggestions_open", (Action<ushort, byte[]>)OnRawSuggestionsOpen);
			ClientController.RegisterCustomPacketHandler("syncvideo.suggestion_ack", (Action<ushort, byte[]>)OnRawSuggestionAck);
		}

		public void Dispose()
		{
			_disposed = true;
		}

		public void BroadcastToLobby(SyncVideoPacketBase packet)
		{
			if (_disposed || !Connected || packet == null)
			{
				return;
			}
			try
			{
				ClientController.Instance.BroadcastCustomPacketToCurrentLobby(packet.Serialize(), packet.PacketId, (SendModes)2);
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] BroadcastToLobby(" + packet.PacketId + ") failed: " + ex.Message));
			}
		}

		public void SendToPlayer(SyncVideoPacketBase packet, ushort targetPlayerId)
		{
			if (_disposed || !Connected || packet == null || targetPlayerId == 0)
			{
				return;
			}
			try
			{
				ClientController.Instance.SendCustomPacketToPlayer(packet.Serialize(), packet.PacketId, targetPlayerId, (SendModes)2);
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] SendToPlayer(" + packet.PacketId + " → " + targetPlayerId + ") failed: " + ex.Message));
			}
		}

		private void Dispatch(SyncVideoPacketBase packet)
		{
			if (!_disposed && packet != null)
			{
				this.SyncPacketReceived?.Invoke(packet);
			}
		}

		private void OnRawState(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoStatePacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise State failed: " + ex.Message));
			}
		}

		private void OnRawTime(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoTimePacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise Time failed: " + ex.Message));
			}
		}

		private void OnRawStateRequest(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoStateRequestPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise StateRequest failed: " + ex.Message));
			}
		}

		private void OnRawLobbyAdvertise(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoLobbyAdvertisePacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise LobbyAdvertise failed: " + ex.Message));
			}
		}

		private void OnRawLobbyJoin(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoLobbyJoinPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise LobbyJoin failed: " + ex.Message));
			}
		}

		private void OnRawLobbyLeave(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoLobbyLeavePacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise LobbyLeave failed: " + ex.Message));
			}
		}

		private void OnRawLobbyMembers(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoLobbyMembersPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise LobbyMembers failed: " + ex.Message));
			}
		}

		private void OnRawLobbyClosed(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoLobbyClosedPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise LobbyClosed failed: " + ex.Message));
			}
		}

		private void OnRawScreenTransform(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoScreenTransformPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise ScreenTransform failed: " + ex.Message));
			}
		}

		private void OnRawSuggestion(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoSuggestionPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise Suggestion failed: " + ex.Message));
			}
		}

		private void OnRawSuggestionsOpen(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoSuggestionsOpenPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise SuggestionsOpen failed: " + ex.Message));
			}
		}

		private void OnRawSuggestionAck(ushort fromId, byte[] data)
		{
			try
			{
				Dispatch(SyncVideoSuggestionAckPacket.Deserialize(fromId, data));
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("[SyncVideo] Deserialise SuggestionAck failed: " + ex.Message));
			}
		}
	}
}
namespace SyncVideo.Transport.Packets
{
	public sealed class SyncVideoLobbyAdvertisePacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public ushort HostId;

		public string LobbyName = string.Empty;

		public int MemberCount;

		public string CurrentUrl = string.Empty;

		public string CurrentVideoId = string.Empty;

		public bool IsPlaying;

		public double MediaTimeSeconds;

		public long HostUnixMilliseconds;

		public int Revision;

		public override string PacketId => "syncvideo.lobby_advertise";

		public static SyncVideoLobbyAdvertisePacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoLobbyAdvertisePacket syncVideoLobbyAdvertisePacket = new SyncVideoLobbyAdvertisePacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoLobbyAdvertisePacket.PopulateFrom(data);
			return syncVideoLobbyAdvertisePacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			HostId = reader.ReadUInt16();
			LobbyName = SyncVideoPacketSerialization.ReadString(reader);
			MemberCount = reader.ReadInt32();
			CurrentUrl = SyncVideoPacketSerialization.ReadString(reader);
			CurrentVideoId = SyncVideoPacketSerialization.ReadString(reader);
			IsPlaying = reader.ReadBoolean();
			MediaTimeSeconds = reader.ReadDouble();
			HostUnixMilliseconds = reader.ReadInt64();
			Revision = reader.ReadInt32();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(HostId);
			SyncVideoPacketSerialization.WriteString(writer, LobbyName);
			writer.Write(MemberCount);
			SyncVideoPacketSerialization.WriteString(writer, CurrentUrl);
			SyncVideoPacketSerialization.WriteString(writer, CurrentVideoId);
			writer.Write(IsPlaying);
			writer.Write(MediaTimeSeconds);
			writer.Write(HostUnixMilliseconds);
			writer.Write(Revision);
		}
	}
	public sealed class SyncVideoLobbyClosedPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public override string PacketId => "syncvideo.lobby_closed";

		public static SyncVideoLobbyClosedPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoLobbyClosedPacket syncVideoLobbyClosedPacket = new SyncVideoLobbyClosedPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoLobbyClosedPacket.PopulateFrom(data);
			return syncVideoLobbyClosedPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
		}
	}
	public sealed class SyncVideoLobbyJoinPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public ushort PlayerId;

		public override string PacketId => "syncvideo.lobby_join";

		public static SyncVideoLobbyJoinPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoLobbyJoinPacket syncVideoLobbyJoinPacket = new SyncVideoLobbyJoinPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoLobbyJoinPacket.PopulateFrom(data);
			return syncVideoLobbyJoinPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			PlayerId = reader.ReadUInt16();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(PlayerId);
		}
	}
	public sealed class SyncVideoLobbyLeavePacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public ushort PlayerId;

		public override string PacketId => "syncvideo.lobby_leave";

		public static SyncVideoLobbyLeavePacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoLobbyLeavePacket syncVideoLobbyLeavePacket = new SyncVideoLobbyLeavePacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoLobbyLeavePacket.PopulateFrom(data);
			return syncVideoLobbyLeavePacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			PlayerId = reader.ReadUInt16();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(PlayerId);
		}
	}
	public sealed class SyncVideoLobbyMembersPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public ushort[] MemberIds = new ushort[0];

		public override string PacketId => "syncvideo.lobby_members";

		public static SyncVideoLobbyMembersPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoLobbyMembersPacket syncVideoLobbyMembersPacket = new SyncVideoLobbyMembersPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoLobbyMembersPacket.PopulateFrom(data);
			return syncVideoLobbyMembersPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			MemberIds = SyncVideoPacketSerialization.ReadUShortArray(reader);
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			SyncVideoPacketSerialization.WriteUShortArray(writer, MemberIds);
		}
	}
	public abstract class SyncVideoPacketBase
	{
		public ushort SenderPlayerId;

		public abstract string PacketId { get; }

		public byte[] Serialize()
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8);
			WritePayload(binaryWriter);
			binaryWriter.Flush();
			return memoryStream.ToArray();
		}

		protected void PopulateFrom(byte[] data)
		{
			using MemoryStream input = new MemoryStream(data);
			using BinaryReader reader = new BinaryReader(input, Encoding.UTF8);
			ReadPayload(reader);
		}

		protected abstract void WritePayload(BinaryWriter writer);

		protected abstract void ReadPayload(BinaryReader reader);
	}
	public static class SyncVideoPacketIds
	{
		public const string LobbyAdvertise = "syncvideo.lobby_advertise";

		public const string LobbyJoin = "syncvideo.lobby_join";

		public const string LobbyLeave = "syncvideo.lobby_leave";

		public const string LobbyMembers = "syncvideo.lobby_members";

		public const string State = "syncvideo.state";

		public const string LobbyClosed = "syncvideo.lobby_closed";

		public const string StateRequest = "syncvideo.state_request";

		public const string ScreenTransform = "syncvideo.screen_transform";

		public const string Suggestion = "syncvideo.suggestion";

		public const string SuggestionsOpen = "syncvideo.suggestions_open";

		public const string SuggestionAck = "syncvideo.suggestion_ack";

		public const string Time = "syncvideo.time";
	}
	public static class SyncVideoPacketSerialization
	{
		public static void WriteString(BinaryWriter writer, string value)
		{
			writer.Write(value ?? string.Empty);
		}

		public static string ReadString(BinaryReader reader)
		{
			return reader.ReadString();
		}

		public static void WriteUShortArray(BinaryWriter writer, ushort[] values)
		{
			if (values == null)
			{
				writer.Write(0);
				return;
			}
			writer.Write(values.Length);
			for (int i = 0; i < values.Length; i++)
			{
				writer.Write(values[i]);
			}
		}

		public static ushort[] ReadUShortArray(BinaryReader reader)
		{
			int num = reader.ReadInt32();
			if (num <= 0)
			{
				return new ushort[0];
			}
			ushort[] array = new ushort[num];
			for (int i = 0; i < num; i++)
			{
				array[i] = reader.ReadUInt16();
			}
			return array;
		}
	}
	public sealed class SyncVideoScreenTransformPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public float PosX;

		public float PosY;

		public float PosZ;

		public float ScaleX;

		public float ScaleY;

		public int Revision;

		public override string PacketId => "syncvideo.screen_transform";

		public static SyncVideoScreenTransformPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoScreenTransformPacket syncVideoScreenTransformPacket = new SyncVideoScreenTransformPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoScreenTransformPacket.PopulateFrom(data);
			return syncVideoScreenTransformPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			PosX = reader.ReadSingle();
			PosY = reader.ReadSingle();
			PosZ = reader.ReadSingle();
			ScaleX = reader.ReadSingle();
			ScaleY = reader.ReadSingle();
			Revision = reader.ReadInt32();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(PosX);
			writer.Write(PosY);
			writer.Write(PosZ);
			writer.Write(ScaleX);
			writer.Write(ScaleY);
			writer.Write(Revision);
		}
	}
	public sealed class SyncVideoStatePacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public string Url = string.Empty;

		public string VideoId = string.Empty;

		public bool IsPlaying;

		public double MediaTimeSeconds;

		public long HostUnixMilliseconds;

		public int Revision;

		public bool HasEnded;

		public bool IsOpen = true;

		public bool SuggestionsOpen;

		public int SelectedAudioTrack = 0;

		public int SelectedSubtitleTrack = -1;

		public override string PacketId => "syncvideo.state";

		public static SyncVideoStatePacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoStatePacket syncVideoStatePacket = new SyncVideoStatePacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoStatePacket.PopulateFrom(data);
			return syncVideoStatePacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			Url = SyncVideoPacketSerialization.ReadString(reader);
			VideoId = SyncVideoPacketSerialization.ReadString(reader);
			IsPlaying = reader.ReadBoolean();
			MediaTimeSeconds = reader.ReadDouble();
			HostUnixMilliseconds = reader.ReadInt64();
			Revision = reader.ReadInt32();
			HasEnded = reader.ReadBoolean();
			IsOpen = reader.ReadBoolean();
			SuggestionsOpen = reader.ReadBoolean();
			if (reader.BaseStream.Position < reader.BaseStream.Length)
			{
				SelectedAudioTrack = reader.ReadInt32();
			}
			if (reader.BaseStream.Position < reader.BaseStream.Length)
			{
				SelectedSubtitleTrack = reader.ReadInt32();
			}
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			SyncVideoPacketSerialization.WriteString(writer, Url);
			SyncVideoPacketSerialization.WriteString(writer, VideoId);
			writer.Write(IsPlaying);
			writer.Write(MediaTimeSeconds);
			writer.Write(HostUnixMilliseconds);
			writer.Write(Revision);
			writer.Write(HasEnded);
			writer.Write(IsOpen);
			writer.Write(SuggestionsOpen);
			writer.Write(SelectedAudioTrack);
			writer.Write(SelectedSubtitleTrack);
		}
	}
	public sealed class SyncVideoStateRequestPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public override string PacketId => "syncvideo.state_request";

		public static SyncVideoStateRequestPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoStateRequestPacket syncVideoStateRequestPacket = new SyncVideoStateRequestPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoStateRequestPacket.PopulateFrom(data);
			return syncVideoStateRequestPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
		}
	}
	public sealed class SyncVideoSuggestionAckPacket : SyncVideoPacketBase
	{
		public ushort RecipientPlayerId;

		public string LobbyId = string.Empty;

		public string SuggestionKey = string.Empty;

		public override string PacketId => "syncvideo.suggestion_ack";

		public static SyncVideoSuggestionAckPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoSuggestionAckPacket syncVideoSuggestionAckPacket = new SyncVideoSuggestionAckPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoSuggestionAckPacket.PopulateFrom(data);
			return syncVideoSuggestionAckPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			RecipientPlayerId = reader.ReadUInt16();
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			SuggestionKey = SyncVideoPacketSerialization.ReadString(reader);
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			writer.Write(RecipientPlayerId);
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			SyncVideoPacketSerialization.WriteString(writer, SuggestionKey);
		}
	}
	public sealed class SyncVideoSuggestionPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public string Url = string.Empty;

		public string Title = string.Empty;

		public string PlayerName = string.Empty;

		public override string PacketId => "syncvideo.suggestion";

		public static SyncVideoSuggestionPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoSuggestionPacket syncVideoSuggestionPacket = new SyncVideoSuggestionPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoSuggestionPacket.PopulateFrom(data);
			return syncVideoSuggestionPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			Url = SyncVideoPacketSerialization.ReadString(reader);
			Title = SyncVideoPacketSerialization.ReadString(reader);
			PlayerName = ((reader.BaseStream.Position < reader.BaseStream.Length) ? SyncVideoPacketSerialization.ReadString(reader) : string.Empty);
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			SyncVideoPacketSerialization.WriteString(writer, Url);
			SyncVideoPacketSerialization.WriteString(writer, Title);
			SyncVideoPacketSerialization.WriteString(writer, PlayerName);
		}
	}
	public sealed class SyncVideoSuggestionsOpenPacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public bool IsOpen;

		public override string PacketId => "syncvideo.suggestions_open";

		public static SyncVideoSuggestionsOpenPacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoSuggestionsOpenPacket syncVideoSuggestionsOpenPacket = new SyncVideoSuggestionsOpenPacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoSuggestionsOpenPacket.PopulateFrom(data);
			return syncVideoSuggestionsOpenPacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			IsOpen = reader.ReadBoolean();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(IsOpen);
		}
	}
	public sealed class SyncVideoTimePacket : SyncVideoPacketBase
	{
		public string LobbyId = string.Empty;

		public double MediaTimeSeconds;

		public bool IsPlaying;

		public long HostSentMilliseconds;

		public override string PacketId => "syncvideo.time";

		public static SyncVideoTimePacket Deserialize(ushort senderPlayerId, byte[] data)
		{
			SyncVideoTimePacket syncVideoTimePacket = new SyncVideoTimePacket
			{
				SenderPlayerId = senderPlayerId
			};
			syncVideoTimePacket.PopulateFrom(data);
			return syncVideoTimePacket;
		}

		protected override void ReadPayload(BinaryReader reader)
		{
			LobbyId = SyncVideoPacketSerialization.ReadString(reader);
			MediaTimeSeconds = reader.ReadDouble();
			IsPlaying = reader.ReadBoolean();
			HostSentMilliseconds = reader.ReadInt64();
		}

		protected override void WritePayload(BinaryWriter writer)
		{
			SyncVideoPacketSerialization.WriteString(writer, LobbyId);
			writer.Write(MediaTimeSeconds);
			writer.Write(IsPlaying);
			writer.Write(HostSentMilliseconds);
		}
	}
}
namespace SyncVideo.Runtime
{
	public sealed class DirectUrlVideoBackend : IDisposable, IVideoBackend
	{
		private sealed class AudioTrackInfo
		{
			public int StreamIndex;

			public int UnityTrackIndex;

			public string Language = string.Empty;

			public string Title = string.Empty;

			public string Codec = string.Empty;

			public int Channels;
		}

		private const string NoVideoLoadedMessage = "No Video Loaded!";

		private const string LoadedReadyMessage = "Video Loaded!\nPress Play!";

		private const string UrlErrorMessage = "Video URL Error!";

		private const string PausedMessage = "Paused!";

		private const string VideoEndedMessage = "Video Ended!";

		private const string ResolvingBaseMessage = "Loading Youtube URL!\nPlease wait";

		private const string DownloadingBaseMessage = "FFmpeg enabled!\nDownloading HD Video!\nPlease wait";

		private readonly ManualLogSource _logger;

		private readonly GameObject _root;

		private readonly VideoPlayer _player;

		private readonly RenderTexture _texture;

		private readonly int _renderWidth;

		private readonly int _renderHeight;

		private readonly SubtitleManager _subtitleManager;

		private string _lastOriginalUrl = string.Empty;

		private string _subtitleSourceUrl = string.Empty;

		private string _lastVideoId = string.Empty;

		private readonly object _audioTrackLock = new object();

		private readonly List<AudioTrackInfo> _audioTracks = new List<AudioTrackInfo>();

		private CancellationTokenSource _audioProbeCts;

		private int _knownAudioTrackCount = 1;

		private int _selectedAudioTrack = 0;

		private bool _isAudioProbing;

		private bool _isMuted;

		private float _volume;

		private double _lastKnownTimeSeconds;

		private bool _isResolving;

		private float _resolvingAnimTimer;

		private int _resolvingAnimStep;

		private string _resolvingCurrentBase = string.Empty;

		private bool _isAudioSwitching;

		private double _audioSwitchPendingSeek;

		private bool _audioSwitchPendingWasPlaying;

		private static readonly Regex _audioStreamRx = new Regex("Stream #0:(\\d+)(?:\\(([^)]+)\\))?[^:]*: Audio", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static readonly Regex _audioTitleLineRx = new Regex("^\\s+title\\s*:\\s*(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		public string CurrentDirectUrl { get; private set; } = string.Empty;


		public bool HasPreparedUrl => !string.IsNullOrWhiteSpace(CurrentDirectUrl);

		public string StatusOverlayText { get; private set; } = "No Video Loaded!";


		public bool IsPrepared => _player.isPrepared;

		public bool IsPlaying => _player.isPlaying;

		public double CurrentTimeSeconds => (_player.isPrepared || _player.isPlaying) ? _player.time : _lastKnownTimeSeconds;

		public object OutputTexture => _texture;

		public float LocalVolume => _volume;

		public bool IsMuted => _isMuted;

		public bool IsCurrentMkv => UrlNormalizer.IsMkvUrl(CurrentDirectUrl) || UrlNormalizer.IsMkvUrl(_lastOriginalUrl);

		public bool ShouldShowFfmpegSyncingStatus => !string.IsNullOrEmpty(_lastVideoId) && YouTube.IsFfmpegAvailable();

		public int AudioTrackCount => Math.Max(1, _knownAudioTrackCount);

		public int SelectedAudioTrack => Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, AudioTrackCount - 1));

		public int SubtitleTrackCount => _subtitleManager.TrackCount;

		public int SelectedSubtitleTrack => _subtitleManager.SelectedTrack;

		public bool IsSubtitleProbing => _subtitleManager.IsProbing;

		public bool IsSubtitleExtracting => _subtitleManager.IsExtracting;

		public bool IsAudioSwitching => _isAudioSwitching;

		public bool IsAudioProbing => _isAudioProbing;

		public event Action Prepared;

		public event Action Ended;

		public event Action AudioTrackSwitchCompleted;

		public event Action AudioTracksChanged;

		public DirectUrlVideoBackend()
		{
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Expected O, but got Unknown
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			//IL_011d: Expected O, but got Unknown
			//IL_01f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fa: Expected O, but got Unknown
			//IL_0208: Unknown result type (might be due to invalid IL or missing references)
			//IL_0212: Expected O, but got Unknown
			//IL_0220: Unknown result type (might be due to invalid IL or missing references)
			//IL_022a: Expected O, but got Unknown
			_logger = Logger.CreateLogSource("SyncVideo.DirectUrlVideoBackend");
			_subtitleManager = new SubtitleManager(_logger);
			_volume = Mathf.Clamp01((float)((SyncVideoPlugin.Settings != null) ? SyncVideoPlugin.Settings.DefaultVolume.Value : 90) / 100f);
			_root = new GameObject("SyncVideoBackend");
			Object.DontDestroyOnLoad((Object)(object)_root);
			ParseRenderResolution((SyncVideoPlugin.Settings != null) ? SyncVideoPlugin.Settings.VideoRenderResolution.Value : null, out _renderWidth, out _renderHeight);
			_texture = new RenderTexture(_renderWidth, _renderHeight, 0, (RenderTextureFormat)0);
			_texture.useMipMap = false;
			_texture.autoGenerateMips = false;
			_texture.antiAliasing = 1;
			_texture.Create();
			ClearRenderTexture();
			_player = _root.AddComponent<VideoPlayer>();
			_player.playOnAwake = false;
			_player.isLooping = false;
			_player.renderMode = (VideoRenderMode)2;
			_player.targetTexture = _texture;
			_player.aspectRatio = (VideoAspectRatio)3;
			_player.audioOutputMode = (VideoAudioOutputMode)2;
			_player.EnableAudioTrack((ushort)0, true);
			_player.skipOnDrop = true;
			_player.waitForFirstFrame = false;
			_player.prepareCompleted += new EventHandler(OnPrepareCompleted);
			_player.errorReceived += new ErrorEventHandler(OnErrorReceived);
			_player.loopPointReached += new EventHandler(OnLoopPointReached);
			_logger.LogInfo((object)$"Using video render texture {_renderWidth}x{_renderHeight}.");
		}

		private static void ParseRenderResolution(string configuredValue, out int width, out int height)
		{
			width = 854;
			height = 480;
			switch (string.IsNullOrWhiteSpace(configuredValue) ? string.Empty : configuredValue.Trim().ToLowerInvariant().Replace('x', 'x'))
			{
			case "1920x1080":
				width = 1920;
				height = 1080;
				break;
			case "1280x720":
				width = 1280;
				height = 720;
				break;
			case "960x540":
				width = 960;
				height = 540;
				break;
			case "854x480":
				width = 854;
				height = 480;
				break;
			case "640x360":
				width = 640;
				height = 360;
				break;
			case "426x240":
				width = 426;
				height = 240;
				break;
			}
		}

		public string GetSubtitleTrackLabel(int index)
		{
			return _subtitleManager.GetTrackLabel(index);
		}

		public void SelectSubtitleTrack(int trackIndex, Action onComplete)
		{
			if (string.IsNullOrWhiteSpace(_subtitleSourceUrl))
			{
				return;
			}
			string text = SubtitleManager.FindFfmpegPath();
			if (text != null)
			{
				_subtitleManager.SelectTrack(trackIndex, _subtitleSourceUrl, text, delegate
				{
					SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(onComplete);
				});
			}
		}

		public void DisableSubtitles()
		{
			_subtitleManager.DisableSubtitles();
		}

		public string GetCurrentSubtitleText(double time)
		{
			return _subtitleManager.GetActiveSubtitle(time);
		}

		public void SetResolvingStatus()
		{
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_lastKnownTimeSeconds = 0.0;
			CurrentDirectUrl = string.Empty;
			ClearRenderTexture();
			_isResolving = true;
			_resolvingAnimTimer = 0f;
			_resolvingAnimStep = 0;
			_resolvingCurrentBase = "Loading Youtube URL!\nPlease wait";
			SetStatusOverlay("Loading Youtube URL!\nPlease wait");
		}

		public void SetDownloadingStatus()
		{
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_lastKnownTimeSeconds = 0.0;
			CurrentDirectUrl = string.Empty;
			ClearRenderTexture();
			_isResolving = true;
			_resolvingAnimTimer = 0f;
			_resolvingAnimStep = 0;
			_resolvingCurrentBase = "FFmpeg enabled!\nDownloading HD Video!\nPlease wait";
			SetStatusOverlay("FFmpeg enabled!\nDownloading HD Video!\nPlease wait");
		}

		public void SetConvertingStatus()
		{
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_lastKnownTimeSeconds = 0.0;
			CurrentDirectUrl = string.Empty;
			ClearRenderTexture();
			_isResolving = true;
			_resolvingAnimTimer = 0f;
			_resolvingAnimStep = 0;
			_resolvingCurrentBase = "FFmpeg: Converting MKV!\nPlease wait";
			SetStatusOverlay("FFmpeg: Converting MKV!\nPlease wait");
		}

		public void SetErrorStatus(string message)
		{
			_isResolving = false;
			ClearRenderTexture();
			int num = (message ?? string.Empty).IndexOf('\n');
			string statusOverlay = ((num >= 0) ? ("Error: Video not supported!" + message.Substring(num)) : "Error: Video not supported!");
			SetStatusOverlay(statusOverlay);
		}

		public void Dispose()
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected O, but got Unknown
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Expected O, but got Unknown
			_player.prepareCompleted -= new EventHandler(OnPrepareCompleted);
			_player.errorReceived -= new ErrorEventHandler(OnErrorReceived);
			_player.loopPointReached -= new EventHandler(OnLoopPointReached);
			CancelAudioProbe();
			_subtitleManager.Clear();
			if ((Object)(object)_texture != (Object)null)
			{
				_texture.Release();
			}
			if ((Object)(object)_root != (Object)null)
			{
				Object.Destroy((Object)(object)_root);
			}
			if (_logger != null)
			{
				Logger.Sources.Remove((ILogSource)(object)_logger);
			}
		}

		public void Load(string directPlayableUrl, string originalUrl, string videoId)
		{
			_lastOriginalUrl = originalUrl ?? string.Empty;
			_subtitleSourceUrl = ResolveSubtitleSourceUrl(directPlayableUrl, _lastOriginalUrl);
			_lastVideoId = videoId ?? string.Empty;
			CurrentDirectUrl = directPlayableUrl ?? string.Empty;
			_lastKnownTimeSeconds = 0.0;
			ResetAudioTrackCache();
			_selectedAudioTrack = 0;
			_isAudioSwitching = false;
			_audioSwitchPendingSeek = 0.0;
			_audioSwitchPendingWasPlaying = false;
			Stop();
			if (string.IsNullOrWhiteSpace(directPlayableUrl))
			{
				SetStatusOverlay("Video URL Error!");
				return;
			}
			ProbeAudioTracksIfSupported(_subtitleSourceUrl);
			ProbeSubtitlesIfSupported(_subtitleSourceUrl);
			SetStatusOverlay(string.Empty);
			_player.source = (VideoSource)1;
			_player.url = directPlayableUrl;
			ResetAudioRouting();
			ConfigureAudioTracks();
			_player.Prepare();
		}

		public void ReloadCurrent()
		{
			if (!string.IsNullOrWhiteSpace(CurrentDirectUrl))
			{
				Load(CurrentDirectUrl, _lastOriginalUrl, _lastVideoId);
			}
		}

		public void Play()
		{
			if (_player.isPrepared)
			{
				SetStatusOverlay(string.Empty);
				_player.Play();
			}
		}

		public void Pause()
		{
			if (_player.isPrepared)
			{
				_player.Pause();
				UpdatePausedOverlay();
			}
		}

		public void Stop()
		{
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_lastKnownTimeSeconds = 0.0;
			_isResolving = false;
			SetStatusOverlay("No Video Loaded!");
			ClearRenderTexture();
			CancelAudioProbe();
			ResetAudioTrackCache();
			_subtitleManager.Clear();
		}

		public void Seek(double seconds)
		{
			if (_player.isPrepared)
			{
				_player.time = Math.Max(0.0, seconds);
				_lastKnownTimeSeconds = Math.Max(0.0, seconds);
				_subtitleManager.ResetSearchHint();
				if (!_player.isPlaying)
				{
					UpdatePausedOverlay();
				}
			}
		}

		public void NudgeToward(double seconds, double driftSeconds)
		{
			if (!_player.isPrepared)
			{
				return;
			}
			double num = seconds - _player.time;
			double num2 = Math.Abs(num);
			if (num2 < 0.015 || driftSeconds <= 0.0)
			{
				_player.playbackSpeed = 1f;
				return;
			}
			float num3 = Mathf.Clamp((float)(num * 0.14), -0.08f, 0.08f);
			if (Math.Abs(num3) < 0.012f)
			{
				num3 = ((num > 0.0) ? 0.012f : (-0.012f));
			}
			_player.playbackSpeed = Mathf.Clamp(1f + num3, 0.92f, 1.08f);
		}

		public void Tick(float deltaTime)
		{
			if (_isResolving)
			{
				_resolvingAnimTimer += deltaTime;
				if (_resolvingAnimTimer >= 0.35f)
				{
					_resolvingAnimTimer = 0f;
					_resolvingAnimStep = (_resolvingAnimStep + 1) % 4;
					switch (_resolvingAnimStep)
					{
					case 1:
						SetStatusOverlay(_resolvingCurrentBase + ".");
						break;
					case 2:
						SetStatusOverlay(_resolvingCurrentBase + "..");
						break;
					case 3:
						SetStatusOverlay(_resolvingCurrentBase + "...");
						break;
					default:
						SetStatusOverlay(_resolvingCurrentBase);
						break;
					}
				}
			}
			if (_player.isPrepared || _player.isPlaying)
			{
				_lastKnownTimeSeconds = Math.Max(0.0, _player.time);
			}
			if (!_player.isPlaying && Math.Abs(_player.playbackSpeed - 1f) > 0.001f)
			{
				_player.playbackSpeed = 1f;
			}
		}

		public void AdjustVolume(float delta)
		{
			_volume = Mathf.Clamp01(_volume + delta);
			ApplyAudioState();
		}

		public void ToggleMute()
		{
			_isMuted = !_isMuted;
			ApplyAudioState();
		}

		public string GetAudioTrackLabel(int trackIndex)
		{
			AudioTrackInfo cachedAudioTrack = GetCachedAudioTrack(trackIndex);
			int num = trackIndex + 1;
			if (cachedAudioTrack == null)
			{
				return _isAudioProbing ? ("Audio Track " + num + "\n<color=yellow>Scanning...</color>") : ("Audio Track " + num);
			}
			string text = BuildAudioTrackDetail(cachedAudioTrack);
			if (string.IsNullOrWhiteSpace(text))
			{
				return "Audio Track " + num;
			}
			return "Audio Track " + num + "\n(" + text + ")";
		}

		public bool SelectAudioTrack(int trackIndex)
		{
			return SelectAudioTrack(trackIndex, null, null);
		}

		public bool SelectAudioTrack(int trackIndex, double? resumeTimeOverride, bool? resumePlayingOverride)
		{
			if (string.IsNullOrWhiteSpace(CurrentDirectUrl))
			{
				return false;
			}
			int audioTrackCount = AudioTrackCount;
			if (audioTrackCount <= 0)
			{
				return false;
			}
			int num = Mathf.Clamp(trackIndex, 0, Math.Max(0, audioTrackCount - 1));
			if (num == _selectedAudioTrack)
			{
				return true;
			}
			_selectedAudioTrack = num;
			_audioSwitchPendingSeek = resumeTimeOverride ?? CurrentTimeSeconds;
			_audioSwitchPendingWasPlaying = resumePlayingOverride ?? _player.isPlaying;
			_isAudioSwitching = true;
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_player.source = (VideoSource)1;
			_player.url = CurrentDirectUrl;
			ResetAudioRouting();
			ConfigureAudioTracks();
			_player.Prepare();
			return true;
		}

		public void ShowEndedState(double seconds)
		{
			if (_player.isPlaying || _player.isPrepared)
			{
				_player.Stop();
			}
			_player.playbackSpeed = 1f;
			_lastKnownTimeSeconds = Math.Max(_lastKnownTimeSeconds, seconds);
			ClearRenderTexture();
			SetStatusOverlay("Video Ended!");
		}

		private void ApplyAudioState()
		{
			float num = (_isMuted ? 0f : _volume);
			if (!string.IsNullOrEmpty(_lastVideoId) && SyncVideoPlugin.Settings != null)
			{
				num *= Mathf.Clamp01(SyncVideoPlugin.Settings.YouTubeVolumeScale.Value);
			}
			int num2 = Math.Max(1, _knownAudioTrackCount);
			int num3 = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, num2 - 1));
			if (HasCachedAudioTracks())
			{
				List<AudioTrackInfo> cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot();
				for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++)
				{
					ushort num4 = (ushort)cachedAudioTracksSnapshot[i].UnityTrackIndex;
					try
					{
						_player.SetDirectAudioVolume(num4, (i == num3) ? num : 0f);
					}
					catch
					{
					}
				}
				return;
			}
			for (ushort num5 = 0; num5 < (ushort)num2; num5++)
			{
				try
				{
					_player.SetDirectAudioVolume(num5, (num5 == num3) ? num : 0f);
				}
				catch
				{
				}
			}
		}

		private void OnPrepareCompleted(VideoPlayer source)
		{
			source.playbackSpeed = 1f;
			_lastKnownTimeSeconds = 0.0;
			if (_isAudioSwitching)
			{
				int val = Math.Max(1, (int)source.audioTrackCount);
				if (!HasCachedAudioTracks())
				{
					_knownAudioTrackCount = Math.Max(_knownAudioTrackCount, val);
				}
				_selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1));
				ResetAudioRouting();
				ConfigureAudioTracks();
				ApplyAudioState();
				_isAudioSwitching = false;
				double audioSwitchPendingSeek = _audioSwitchPendingSeek;
				bool audioSwitchPendingWasPlaying = _audioSwitchPendingWasPlaying;
				_audioSwitchPendingSeek = 0.0;
				_audioSwitchPendingWasPlaying = false;
				if (audioSwitchPendingSeek > 0.05)
				{
					source.time = Math.Max(0.0, audioSwitchPendingSeek);
					_lastKnownTimeSeconds = Math.Max(0.0, audioSwitchPendingSeek);
				}
				_subtitleManager.ResetSearchHint();
				if (audioSwitchPendingWasPlaying)
				{
					source.Play();
				}
				else
				{
					UpdatePausedOverlay();
				}
				this.AudioTrackSwitchCompleted?.Invoke();
			}
			else
			{
				if (!HasCachedAudioTracks())
				{
					_knownAudioTrackCount = Math.Max(1, (int)source.audioTrackCount);
				}
				_selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1));
				ResetAudioRouting();
				ConfigureAudioTracks();
				ApplyAudioState();
				ClearRenderTexture();
				SetStatusOverlay("Video Loaded!\nPress Play!");
				this.Prepared?.Invoke();
			}
		}

		private void OnErrorReceived(VideoPlayer source, string message)
		{
			_logger.LogError((object)("SyncVideo URL Video backend error: " + message));
			source.Stop();
			source.playbackSpeed = 1f;
			SetStatusOverlay("Video URL Error!");
			ClearRenderTexture();
		}

		private void OnLoopPointReached(VideoPlayer source)
		{
			double num = source.time;
			if (num <= 0.0 && source.frameCount != 0 && source.frameRate > 0f)
			{
				num = (float)source.frameCount / source.frameRate;
			}
			_lastKnownTimeSeconds = Math.Max(_lastKnownTimeSeconds, num);
			source.Stop();
			source.playbackSpeed = 1f;
			ClearRenderTexture();
			SetStatusOverlay("Video Ended!");
			this.Ended?.Invoke();
		}

		private void UpdatePausedOverlay()
		{
			if (_player.isPrepared)
			{
				SetStatusOverlay((_player.time <= 0.05) ? "Video Loaded!\nPress Play!" : "Paused!");
			}
		}

		private void SetStatusOverlay(string message)
		{
			StatusOverlayText = message ?? string.Empty;
		}

		private void ResetAudioRouting()
		{
			try
			{
				_player.audioOutputMode = (VideoAudioOutputMode)0;
				_player.audioOutputMode = (VideoAudioOutputMode)2;
			}
			catch
			{
			}
		}

		private void ConfigureAudioTracks()
		{
			int num = Math.Max(1, _knownAudioTrackCount);
			int num2 = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, num - 1));
			if (HasCachedAudioTracks())
			{
				List<AudioTrackInfo> cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot();
				try
				{
					_player.controlledAudioTrackCount = (ushort)cachedAudioTracksSnapshot.Count;
				}
				catch
				{
				}
				for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++)
				{
					ushort num3 = (ushort)cachedAudioTracksSnapshot[i].UnityTrackIndex;
					try
					{
						_player.EnableAudioTrack(num3, i == num2);
					}
					catch
					{
					}
				}
				AudioTrackInfo audioTrackInfo = ((num2 >= 0 && num2 < cachedAudioTracksSnapshot.Count) ? cachedAudioTracksSnapshot[num2] : null);
				if (audioTrackInfo != null)
				{
					return;
				}
			}
			try
			{
				_player.controlledAudioTrackCount = (ushort)num;
			}
			catch
			{
			}
			for (int j = 0; j < num; j++)
			{
				try
				{
					_player.EnableAudioTrack((ushort)j, j == num2);
				}
				catch
				{
				}
			}
		}

		private void ResetAudioTrackCache()
		{
			CancelAudioProbe();
			lock (_audioTrackLock)
			{
				_audioTracks.Clear();
			}
			_knownAudioTrackCount = 1;
			_isAudioProbing = false;
		}

		private void CancelAudioProbe()
		{
			try
			{
				_audioProbeCts?.Cancel();
			}
			catch
			{
			}
			try
			{
				_audioProbeCts?.Dispose();
			}
			catch
			{
			}
			_audioProbeCts = null;
		}

		private bool HasCachedAudioTracks()
		{
			lock (_audioTrackLock)
			{
				return _audioTracks.Count > 0;
			}
		}

		private AudioTrackInfo GetCachedAudioTrack(int trackIndex)
		{
			lock (_audioTrackLock)
			{
				if (trackIndex < 0 || trackIndex >= _audioTracks.Count)
				{
					return null;
				}
				return _audioTracks[trackIndex];
			}
		}

		private List<AudioTrackInfo> GetCachedAudioTracksSnapshot()
		{
			lock (_audioTrackLock)
			{
				return new List<AudioTrackInfo>(_audioTracks);
			}
		}

		private void ProbeAudioTracksIfSupported(string url)
		{
			if (string.IsNullOrWhiteSpace(url) || !UrlNormalizer.IsMkvUrl(url))
			{
				return;
			}
			string ffmpegPath = SubtitleManager.FindFfmpegPath();
			if (ffmpegPath == null)
			{
				return;
			}
			string ffprobePath = FindFfprobePath(ffmpegPath);
			CancelAudioProbe();
			CancellationTokenSource cts = new CancellationTokenSource();
			_audioProbeCts = cts;
			_isAudioProbing = true;
			this.AudioTracksChanged?.Invoke();
			Task.Run(delegate
			{
				List<AudioTrackInfo> detected = null;
				try
				{
					if (ffprobePath != null)
					{
						detected = ProbeAudioTracks(url, ffprobePath, cts.Token);
					}
					if ((detected == null || detected.Count == 0) && !cts.IsCancellationRequested)
					{
						detected = ProbeAudioTracksFromFfmpegStderr(url, ffmpegPath, cts.Token);
					}
				}
				catch (Exception ex)
				{
					_logger.LogWarning((object)("[MKV Audio] Probe error: " + ex.Message));
				}
				if (!cts.IsCancellationRequested)
				{
					SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(delegate
					{
						if (!cts.IsCancellationRequested)
						{
							_isAudioProbing = false;
							if (detected != null && detected.Count > 0)
							{
								lock (_audioTrackLock)
								{
									_audioTracks.Clear();
									_audioTracks.AddRange(detected);
								}
								_knownAudioTrackCount = detected.Count;
								_selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1));
								ConfigureAudioTracks();
								ApplyAudioState();
								_logger.LogInfo((object)$"[MKV Audio] Cached {detected.Count} audio track(s)!");
							}
							this.AudioTracksChanged?.Invoke();
						}
					});
				}
			}, cts.Token);
		}

		private static List<AudioTrackInfo> ProbeAudioTracks(string url, string ffprobePath, CancellationToken token)
		{
			List<AudioTrackInfo> result = new List<AudioTrackInfo>();
			ProcessStartInfo startInfo = new ProcessStartInfo
			{
				FileName = ffprobePath,
				Arguments = "-v error -probesize 100M -analyzeduration 100M -select_streams a -show_entries stream=index,codec_name,channels:stream_tags=language,title -of default=noprint_wrappers=0 " + QuoteArg(url),
				UseShellExecute = false,
				RedirectStandardOutput = true,
				RedirectStandardError = true,
				CreateNoWindow = true
			};
			using (Process process = new Process
			{
				StartInfo = startInfo
			})
			{
				process.Start();
				string output = process.StandardOutput.ReadToEnd();
				process.WaitForExit(15000);
				if (!process.HasExited)
				{
					try
					{
						process.Kill();
					}
					catch
					{
					}
					return result;
				}
				ParseFfprobeAudioStreams(output, result, token);
			}
			return result;
		}

		private static void ParseFfprobeAudioStreams(string output, List<AudioTrackInfo> result, CancellationToken token)
		{
			AudioTrackInfo audioTrackInfo = null;
			string[] array = (output ?? string.Empty).Replace("\r\n", "\n").Split(new char[1] { '\n' });
			foreach (string text in array)
			{
				if (token.IsCancellationRequested)
				{
					break;
				}
				string text2 = text.Trim();
				if (text2.Length == 0)
				{
					continue;
				}
				if (text2.StartsWith("index=", StringComparison.OrdinalIgnoreCase))
				{
					if (audioTrackInfo != null && audioTrackInfo.StreamIndex >= 0)
					{
						result.Add(audioTrackInfo);
					}
					audioTrackInfo = new AudioTrackInfo
					{
						StreamIndex = -1,
						UnityTrackIndex = result.Count
					};
					if (int.TryParse(text2.Substring("index=".Length).Trim(), out var result2))
					{
						audioTrackInfo.StreamIndex = result2;
					}
					continue;
				}
				if (audioTrackInfo == null)
				{
					audioTrackInfo = new AudioTrackInfo
					{
						StreamIndex = -1,
						UnityTrackIndex = result.Count
					};
				}
				if (text2.StartsWith("codec_name=", StringComparison.OrdinalIgnoreCase))
				{
					audioTrackInfo.Codec = text2.Substring("codec_name=".Length).Trim();
				}
				else if (text2.StartsWith("channels=", StringComparison.OrdinalIgnoreCase))
				{
					if (int.TryParse(text2.Substring("channels=".Length).Trim(), out var result3))
					{
						audioTrackInfo.Channels = result3;
					}
				}
				else if (text2.StartsWith("TAG:language=", StringComparison.OrdinalIgnoreCase))
				{
					string text3 = text2.Substring("TAG:language=".Length).Trim().Trim('[', ']');
					if (!string.IsNullOrWhiteSpace(text3) && !text3.Equals("N/A", StringComparison.OrdinalIgnoreCase))
					{
						audioTrackInfo.Language = text3;
					}
				}
				else if (text2.StartsWith("TAG:title=", StringComparison.OrdinalIgnoreCase))
				{
					string text4 = text2.Substring("TAG:title=".Length).Trim();
					if (!string.IsNullOrWhiteSpace(text4) && !text4.Equals("N/A", StringComparison.OrdinalIgnoreCase))
					{
						audioTrackInfo.Title = text4;
					}
				}
			}
			if (audioTrackInfo != null && audioTrackInfo.StreamIndex >= 0)
			{
				result.Add(audioTrackInfo);
			}
		}

		private static List<AudioTrackInfo> ProbeAudioTracksFromFfmpegStderr(string url, string ffmpegPath, CancellationToken token)
		{
			List<AudioTrackInfo> list = new List<AudioTrackInfo>();
			ProcessStartInfo startInfo = new ProcessStartInfo
			{
				FileName = ffmpegPath,
				Arguments = "-probesize 10000000 -analyzeduration 10000000 -i " + QuoteArg(url) + " -hide_banner",
				RedirectStandardError = true,
				RedirectStandardOutput = true,
				UseShellExecute = false,
				CreateNoWindow = true
			};
			using (Process process = new Process
			{
				StartInfo = startInfo
			})
			{
				process.Start();
				string text = process.StandardError.ReadToEnd();
				process.WaitForExit(15000);
				if (!process.HasExited)
				{
					try
					{
						process.Kill();
					}
					catch
					{
					}
					return list;
				}
				if (token.IsCancellationRequested)
				{
					return list;
				}
				AudioTrackInfo audioTrackInfo = null;
				string[] array = text.Split(new char[1] { '\n' });
				foreach (string text2 in array)
				{
					if (token.IsCancellationRequested)
					{
						break;
					}
					string text3 = text2.TrimEnd(new char[1] { '\r' });
					if (text3.IndexOf("Stream #", StringComparison.Ordinal) >= 0)
					{
						if (audioTrackInfo != null)
						{
							list.Add(audioTrackInfo);
							audioTrackInfo = null;
						}
						Match match = _audioStreamRx.Match(text3);
						if (match.Success)
						{
							int.TryParse(match.Groups[1].Value, out var result);
							string text4 = match.Groups[2].Value.Trim().Trim('[', ']');
							if (text4.Equals("und", StringComparison.OrdinalIgnoreCase))
							{
								text4 = string.Empty;
							}
							audioTrackInfo = new AudioTrackInfo
							{
								StreamIndex = result,
								UnityTrackIndex = list.Count,
								Language = text4
							};
						}
					}
					else if (audioTrackInfo != null)
					{
						Match match2 = _audioTitleLineRx.Match(text3);
						if (match2.Success)
						{
							audioTrackInfo.Title = match2.Groups[1].Value.Trim();
						}
					}
				}
				if (audioTrackInfo != null && !token.IsCancellationRequested)
				{
					list.Add(audioTrackInfo);
				}
			}
			return list;
		}

		private static string BuildAudioTrackDetail(AudioTrackInfo info)
		{
			string text = CleanTrackText(info.Title);
			string text2 = FormatLanguage(info.Language);
			string text3 = (string.IsNullOrWhiteSpace(info.Codec) ? string.Empty : info.Codec.Trim().ToUpperInvariant());
			string text4 = FormatChannels(info.Channels);
			string text5 = text;
			if (string.IsNullOrWhiteSpace(text5))
			{
				if (!string.IsNullOrWhiteSpace(text2))
				{
					text5 = text2;
				}
				if (!string.IsNullOrWhiteSpace(text4))
				{
					text5 = (string.IsNullOrWhiteSpace(text5) ? text4 : (text5 + " " + text4));
				}
				if (!string.IsNullOrWhiteSpace(text3))
				{
					text5 = (string.IsNullOrWhiteSpace(text5) ? text3 : (text5 + " " + text3));
				}
			}
			if (!string.IsNullOrWhiteSpace(text2) && text5.IndexOf(text2, StringComparison.OrdinalIgnoreCase) < 0)
			{
				text5 = text5 + " - [" + text2 + "]";
			}
			return text5.Trim();
		}

		private static string CleanTrackText(string value)
		{
			return (value ?? string.Empty).Replace("_", " ").Trim();
		}

		private static string FormatChannels(int channels)
		{
			return channels switch
			{
				1 => "1.0", 
				2 => "2.0", 
				6 => "5.1", 
				8 => "7.1", 
				_ => (channels > 0) ? (channels + "ch") : string.Empty, 
			};
		}

		private static string FormatLanguage(string language)
		{
			if (string.IsNullOrWhiteSpace(language) || language.Equals("und", StringComparison.OrdinalIgnoreCase))
			{
				return string.Empty;
			}
			switch (language.Trim().ToLowerInvariant())
			{
			case "en":
			case "eng":
				return "English";
			case "ja":
			case "jpn":
			case "jp":
				return "Japanese";
			case "es":
			case "spa":
				return "Spanish";
			case "fr":
			case "fre":
			case "fra":
				return "French";
			case "de":
			case "ger":
			case "deu":
				return "German";
			case "it":
			case "ita":
				return "Italian";
			case "pt":
			case "por":
				return "Portuguese";
			default:
				return language.Trim();
			}
		}

		private static string FindFfprobePath(string ffmpegPath)
		{
			try
			{
				if (!string.IsNullOrWhiteSpace(ffmpegPath))
				{
					string path = Path.GetDirectoryName(ffmpegPath) ?? string.Empty;
					string[] array = new string[2] { "ffprobe.exe", "ffprobe" };
					foreach (string path2 in array)
					{
						string text = Path.Combine(path, path2);
						if (File.Exists(text))
						{
							return text;
						}
					}
				}
			}
			catch
			{
			}
			string text2 = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
			string[] array2 = text2.Split(new char[1] { Path.PathSeparator });
			foreach (string text3 in array2)
			{
				string[] array3 = new string[2] { "ffprobe.exe", "ffprobe" };
				foreach (string path3 in array3)
				{
					try
					{
						string text4 = Path.Combine(text3.Trim(), path3);
						if (File.Exists(text4))
						{
							return text4;
						}
					}
					catch
					{
					}
				}
			}
			return null;
		}

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

		private static string ResolveSubtitleSourceUrl(string directPlayableUrl, string originalUrl)
		{
			if (!string.IsNullOrWhiteSpace(originalUrl) && UrlNormalizer.IsMkvUrl(originalUrl))
			{
				return originalUrl;
			}
			return directPlayableUrl ?? string.Empty;
		}

		private void ProbeSubtitlesIfSupported(string url)
		{
			_subtitleManager.Clear();
			if (string.IsNullOrWhiteSpace(url) || !SubtitleManager.IsSubtitleProbeSupported(url))
			{
				return;
			}
			string ffmpegPath = SubtitleManager.FindFfmpegPath();
			if (ffmpegPath == null)
			{
				return;
			}
			_subtitleManager.ProbeAsync(url, ffmpegPath, delegate
			{
				_subtitleManager.EagerExtractAllTracksAsync(url, ffmpegPath);
				SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(delegate
				{
					SyncVideoPlugin.SyncController?.ReapplyLobbyTrackSelection();
				});
			});
		}

		private void ClearRenderTexture()
		{
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)_texture == (Object)null))
			{
				RenderTexture active = RenderTexture.active;
				RenderTexture.active = _texture;
				GL.Clear(true, true, Color.black);
				RenderTexture.active = active;
			}
		}
	}
	public enum HudMode
	{
		Off,
		UI,
		UIChat,
		UIAll
	}
	public static class HudManager
	{
		private static HudMode _currentMode;

		private static bool _savedShowChat;

		private static bool _savedShowNamePlates;

		private static bool _savedShowAFKEffects;

		private static bool _savedAfkMessages;

		private static bool _hasSavedSettings;

		private static bool _suppressAfkForLobby;

		private static float _afkTickTimer;

		private const float AfkTickInterval = 0.5f;

		private static CanvasGroup _gameplayCanvasGroup;

		private static bool _musicMuted;

		public static HudMode CurrentMode => _currentMode;

		private static object GetMusicPlayer()
		{
			try
			{
				if ((Object)(object)Core.Instance == (Object)null)
				{
					return null;
				}
				Traverse val = Traverse.Create((object)Core.Instance);
				object obj = val.Property("AudioManager", (object[])null).GetValue() ?? val.Property("audioManager", (object[])null).GetValue() ?? val.Field("audioManager").GetValue() ?? val.Field("AudioManager").GetValue();
				if (obj == null)
				{
					return null;
				}
				Traverse val2 = Traverse.Create(obj);
				return val2.Property("musicPlayer", (object[])null).GetValue() ?? val2.Property("MusicPlayer", (object[])null).GetValue() ?? val2.Field("musicPlayer").GetValue() ?? val2.Field("MusicPlayer").GetValue();
			}
			catch
			{
				return null;
			}
		}

		private static object GetAudioManager()
		{
			try
			{
				if ((Object)(object)Core.Instance == (Object)null)
				{
					return null;
				}
				Traverse val = Traverse.Create((object)Core.Instance);
				return val.Property("AudioManager", (object[])null).GetValue() ?? val.Property("audioManager", (object[])null).GetValue() ?? val.Field("audioManager").GetValue() ?? val.Field("AudioManager").GetValue();
			}
			catch
			{
				return null;
			}
		}

		public static string GetLabel()
		{
			return _currentMode switch
			{
				HudMode.Off => "Hide HUD: <color=green>Off</color>", 
				HudMode.UI => "Hide HUD: <color=yellow>UI</color>", 
				HudMode.UIChat => "Hide HUD: <color=yellow>UI+Chat</color>", 
				HudMode.UIAll => "Hide HUD: <color=yellow>Everything</color>", 
				_ => "Hide HUD", 
			};
		}

		public static void Cycle()
		{
			_currentMode = (HudMode)((int)(_currentMode + 1) % 4);
			Apply();
		}

		public static void Tick()
		{
			if (_currentMode < HudMode.UI)
			{
				if (!_suppressAfkForLobby)
				{
					return;
				}
				VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager;
				if (lobbyManager == null || !lobbyManager.InLobby)
				{
					return;
				}
			}
			_afkTickTimer += Time.unscaledDeltaTime;
			if (_afkTickTimer < 0.5f)
			{
				return;
			}
			_afkTickTimer = 0f;
			try
			{
				PlayerComponent local = PlayerComponent.GetLocal();
				if (local != null)
				{
					local.StopAFK();
				}
			}
			catch
			{
			}
			try
			{
				MPSettings instance = MPSettings.Instance;
				if (instance != null)
				{
					if (instance.ShowAFKEffects)
					{
						instance.ShowAFKEffects = false;
					}
					if (instance.AFKMessages)
					{
						instance.AFKMessages = false;
					}
				}
			}
			catch
			{
			}
		}

		public static void OnLobbyEnter()
		{
			MPSettings instance = MPSettings.Instance;
			if (instance != null && !_hasSavedSettings)
			{
				_savedShowChat = instance.ShowChat;
				_savedShowNamePlates = instance.ShowNamePlates;
				_savedShowAFKEffects = instance.ShowAFKEffects;
				_savedAfkMessages = instance.AFKMessages;
				_hasSavedSettings = true;
			}
			SyncVideoConfig settings = SyncVideoPlugin.Settings;
			_suppressAfkForLobby = settings != null && (settings.SuppressAFK?.Value).GetValueOrDefault();
			SyncVideoConfig settings2 = SyncVideoPlugin.Settings;
			if (settings2 != null && (settings2.MuteMusicAndAmbient?.Value).GetValueOrDefault())
			{
				MuteForLobby();
			}
			Apply();
		}

		public static void Reset()
		{
			_currentMode = HudMode.Off;
			_suppressAfkForLobby = false;
			_afkTickTimer = 0f;
			ApplyGameplayUiHide(hide: false);
			MPSettings instance = MPSettings.Instance;
			if (instance != null)
			{
				instance.ShowChat = !_hasSavedSettings || _savedShowChat;
				instance.ShowNamePlates = !_hasSavedSettings || _savedShowNamePlates;
				instance.ShowAFKEffects = !_hasSavedSettings || _savedShowAFKEffects;
				instance.AFKMessages = !_hasSavedSettings || _savedAfkMessages;
			}
			_hasSavedSettings = false;
			RestoreAudio();
		}

		private static void MuteForLobby()
		{
			if (_musicMuted)
			{
				return;
			}
			object audioManager = GetAudioManager();
			if (audioManager == null)
			{
				_musicMuted = true;
				return;
			}
			Type type = audioManager.GetType();
			BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			try
			{
				type.GetMethod("MuteMusic", bindingAttr)?.Invoke(audioManager, null);
			}
			catch
			{
			}
			try
			{
				if (type.GetField("ambienceAudioSources", bindingAttr)?.GetValue(audioManager) is AudioSource[] array)
				{
					AudioSource[] array2 = array;
					foreach (AudioSource val in array2)
					{
						if ((Object)(object)val != (Object)null)
						{
							val.mute = true;
						}
					}
				}
			}
			catch
			{
			}
			try
			{
				type.GetMethod("PauseAllGameplayLoopingSfx", bindingAttr)?.Invoke(audioManager, null);
			}
			catch
			{
			}
			_musicMuted = true;
		}

		private static void RestoreAudio()
		{
			if (!_musicMuted)
			{
				return;
			}
			object audioManager = GetAudioManager();
			BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			try
			{
				audioManager?.GetType().GetMethod("UnMuteMusic", bindingAttr)?.Invoke(audioManager, null);
			}
			catch
			{
			}
			try
			{
				if (audioManager != null && audioManager.GetType().GetField("ambienceAudioSources", bindingAttr)?.GetValue(audioManager) is AudioSource[] array)
				{
					AudioSource[] array2 = array;
					foreach (AudioSource val in array2)
					{
						if ((Object)(object)val != (Object)null)
						{
							val.mute = false;
						}
					}
				}
			}
			catch
			{
			}
			try
			{
				audioManager?.GetType().GetMethod("ResumeAllGameplayLoopingSfx", bindingAttr)?.Invoke(audioManager, null);
			}
			catch
			{
			}
			_musicMuted = false;
		}

		private static void Apply()
		{
			bool hide = _currentMode >= HudMode.UI;
			bool flag = _currentMode >= HudMode.UI || _suppressAfkForLobby;
			bool flag2 = _currentMode >= HudMode.UIChat;
			bool flag3 = _currentMode >= HudMode.UIAll;
			MPSettings instance = MPSettings.Instance;
			if (instance != null && !_hasSavedSettings)
			{
				_savedShowChat = instance.ShowChat;
				_savedShowNamePlates = instance.ShowNamePlates;
				_savedShowAFKEffects = instance.ShowAFKEffects;
				_savedAfkMessages = instance.AFKMessages;
				_hasSavedSettings = true;
			}
			ApplyGameplayUiHide(hide);
			if (instance != null)
			{
				instance.ShowChat = !flag2 && (!_hasSavedSettings || _savedShowChat);
				instance.ShowNamePlates = !flag3 && (!_hasSavedSettings || _savedShowNamePlates);
				instance.ShowAFKEffects = !flag && (!_hasSavedSettings || _savedShowAFKEffects);
				instance.AFKMessages = !flag && (!_hasSavedSettings || _savedAfkMessages);
			}
		}

		private static void ApplyGameplayUiHide(bool hide)
		{
			try
			{
				Core instance = Core.Instance;
				UIManager val = ((instance != null) ? instance.UIManager : null);
				if ((Object)(object)val == (Object)null)
				{
					return;
				}
				if ((Object)(object)_gameplayCanvasGroup == (Object)null)
				{
					Traverse val2 = Traverse.Create((object)val);
					object value = val2.Field("gameplay").GetValue();
					Component val3 = (Component)((value is Component) ? value : null);
					if ((Object)(object)val3 == (Object)null)
					{
						return;
					}
					_gameplayCanvasGroup = val3.GetComponent<CanvasGroup>();
					if ((Object)(object)_gameplayCanvasGroup == (Object)null)
					{
						_gameplayCanvasGroup = val3.gameObject.AddComponent<CanvasGroup>();
					}
				}
				_gameplayCanvasGroup.alpha = (hide ? 0f : 1f);
				_gameplayCanvasGroup.interactable = !hide;
				_gameplayCanvasGroup.blocksRaycasts = !hide;
			}
			catch
			{
				_gameplayCanvasGroup = null;
			}
		}
	}
	public interface IVideoBackend
	{
		bool IsPrepared { get; }

		bool IsPlaying { get; }

		double CurrentTimeSeconds { get; }

		object OutputTexture { get; }

		string StatusOverlayText { get; }

		void Load(string directPlayableUrl, string originalUrl, string videoId);

		void Play();

		void Pause();

		void Stop();

		void Seek(double seconds);

		void NudgeToward(double seconds, double driftSeconds);

		void Tick(float deltaTime);
	}
	public sealed class LobbyUiOverrideManager : IDisposable
	{
		private readonly ManualLogSource _logger;

		private readonly VideoLobbyManager _lobbyManager;

		private LobbyUI _cachedLobbyUi;

		private Canvas _cachedCanvas;

		private CanvasGroup _cachedCanvasGroup;

		private TextMeshProUGUI _cachedLobbyName;

		private GameObject _cachedLobbySettings;

		private GameObject _cachedGameplayUi;

		private string _originalLobbyName;

		private bool _originalLobbySettingsActive;

		private bool _originalGameplayUiActive;

		private float _inviteRewriteTimer;

		private float _publicLobbyFilterTimer;

		private TextMeshProUGUI[] _cachedTexts;

		private float _textsRefreshTimer;

		private const float TextsRefreshInterval = 3f;

		private bool _isPublicLobbyAppOpen;

		private float _appOpenCheckTimer;

		private const float AppOpenCheckInterval = 0.5f;

		private bool? _lastHideAll;

		private float _tickAccumulator;

		private const float TickInterval = 1f / 30f;

		private readonly HashSet<string> _lobbyNamesBuffer = new HashSet<string>(StringComparer.Ordinal);

		private static readonly HashSet<GameObject> _hiddenObjects = new HashSet<GameObject>();

		public LobbyUiOverrideManager(ManualLogSource logger, VideoLobbyManager lobbyManager)
		{
			_logger = logger;
			_lobbyManager = lobbyManager;
		}

		public void Dispose()
		{
		}

		public void Tick(float deltaTime)
		{
			_tickAccumulator += deltaTime;
			if (_tickAccumulator < 1f / 30f)
			{
				return;
			}
			float tickAccumulator = _tickAccumulator;
			_tickAccumulator = 0f;
			try
			{
				ApplyOverride(tickAccumulator);
			}
			catch (Exception ex)
			{
				_logger.LogWarning((object)("Failed to override All City Network lobby UI: " + ex.Message));
				ClearCache();
			}
		}

		private void ApplyOverride(float deltaTime)
		{
			bool flag = _lobbyManager != null && _lobbyManager.InLobby;
			if (_isPublicLobbyAppOpen || !flag)
			{
				_appOpenCheckTimer -= deltaTime;
				if (_appOpenCheckTimer <= 0f)
				{
					_appOpenCheckTimer = 0.5f;
					_isPublicLobbyAppOpen = IsBombRushMpPublicLobbyAppOpen();
				}
			}
			bool flag2 = !flag && (_lobbyManager?.Lobbies.Count ?? 0) > 0;
			if (flag2 || _isPublicLobbyAppOpen)
			{
				_textsRefreshTimer -= deltaTime;
				if (_textsRefreshTimer <= 0f)
				{
					_textsRefreshTimer = 3f;
					_cachedTexts = Resources.FindObjectsOfTypeAll<TextMeshProUGUI>();
				}
				if (flag2)
				{
					_inviteRewriteTimer -= deltaTime;
					if (_inviteRewriteTimer <= 0f)
					{
						_inviteRewriteTimer = 0.75f;
						RewriteInviteTexts(_cachedTexts);
					}
				}
				if (_isPublicLobbyAppOpen)
				{
					_publicLobbyFilterTimer -= deltaTime;
					if (_publicLobbyFilterTimer <= 0f)
					{
						_publicLobbyFilterTimer = 1.5f;
						HideSyncVideoLobbiesFromMultiplayerMenu(_cachedTexts);
					}
				}
			}
			if (!flag)
			{
				if ((Object)(object)_cachedCanvas != (Object)null)
				{
					RestoreOverriddenElements();
					if (_lastHideAll != false)
					{
						SetCanvasHidden(hidden: false);
					}
					ClearCache();
					_lastHideAll = null;
				}
			}
			else if (TryResolveReferences())
			{
				bool flag3 = _lobbyManager.LeaveInProgress || SyncVideoPlugin.Settings.HideNativeLobbyUi.Value;
				if ((Object)(object)_cachedLobbySettings != (Object)null && _cachedLobbySettings.activeSelf)
				{
					_cachedLobbySettings.SetActive(false);
				}
				if ((Object)(object)_cachedGameplayUi != (Object)null && _cachedGameplayUi.activeSelf)
				{
					_cachedGameplayUi.SetActive(false);
				}
				if ((Object)(object)_cachedLobbyName != (Object)null && ((TMP_Text)_cachedLobbyName).text != "Sync Video Lobby" && ((TMP_Text)_cachedLobbyName).text != "Sync Video Watch Party")
				{
					((TMP_Text)_cachedLobbyName).text = "Sync Video Lobby";
				}
				if (_lastHideAll != flag3)
				{
					_lastHideAll = flag3;
					SetCanvasHidden(flag3);
				}
			}
		}

		private bool TryResolveReferences()
		{
			LobbyUI instance = LobbyUI.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				ClearCache();
				return false;
			}
			if ((Object)(object)_cachedLobbyUi == (Object)(object)instance && (Object)(object)_cachedCanvas != (Object)null)
			{
				return true;
			}
			_cachedLobbyUi = instance;
			Transform transform = ((Component)instance).transform;
			Transform val = FindDeepChild(transform, "Canvas");
			if ((Object)(object)val == (Object)null)
			{
				ClearCache();
				return false;
			}
			_cachedCanvas = ((Component)val).GetComponent<Canvas>();
			if ((Object)(object)_cachedCanvas == (Object)null)
			{
				ClearCache();
				return false;
			}
			_cachedCanvasGroup = ((Component)val).GetComponent<CanvasGroup>();
			if ((Object)(object)_cachedCanvasGroup == (Object)null)
			{
				_cachedCanvasGroup = ((Component)val).gameObject.AddComponent<CanvasGroup>();
			}
			_cachedLobbyName = FindDeepTMP(transform, "Lobby Name");
			Transform val2 = FindDeepChild(transform, "Lobby Settings");
			_cachedLobbySettings = (((Object)(object)val2 != (Object)null) ? ((Component)val2).gameObject : null);
			Transform val3 = FindDeepChild(transform, "Gameplay UI");
			_cachedGameplayUi = (((Object)(object)val3 != (Object)null) ? ((Component)val3).gameObject : null);
			_originalLobbyName = (((Object)(object)_cachedLobbyName != (Object)null) ? ((TMP_Text)_cachedLobbyName).text : null);
			_originalLobbySettingsActive = (Object)(object)_cachedLobbySettings != (Object)null && _cachedLobbySettings.activeSelf;
			_originalGameplayUiActive = (Object)(object)_cachedGameplayUi != (Object)null && _cachedGameplayUi.activeSelf;
			return true;
		}

		private void HideSyncVideoLobbiesFromMultiplayerMenu(TextMeshProUGUI[] texts)
		{
			if (_lobbyManager == null || !_isPublicLobbyAppOpen)
			{
				return;
			}
			_lobbyNamesBuffer.Clear();
			foreach (VideoLobby lobby in _lobbyManager.Lobbies)
			{
				if (lobby != null && !string.IsNullOrWhiteSpace(lobby.LobbyName))
				{
					_lobbyNamesBuffer.Add(lobby.LobbyName);
				}
			}
			if (_lobbyNamesBuffer.Count == 0 || texts == null)
			{
				return;
			}
			foreach (TextMeshProUGUI val in texts)
			{
				if (!((Object)(object)val == (Object)null))
				{
					string text = ((TMP_Text)val).text;
					if (!string.IsNullOrWhiteSpace(text) && _lobbyNamesBuffer.Contains(text.Trim()))
					{
						HideContainingLobbyButton(val);
					}
				}
			}
		}

		private static bool IsBombRushMpPublicLobbyAppOpen()
		{
			try
			{
				Type type = Type.GetType("Reptile.Core, Assembly-CSharp");
				if (type == null)
				{
					return false;
				}
				PropertyInfo property = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public);
				object obj = ((property != null) ? property.GetValue(null, null) : null);
				if (obj == null)
				{
					return false;
				}
				PropertyInfo property2 = type.GetProperty("UIManager", BindingFlags.Instance | BindingFlags.Public);
				object obj2 = ((property2 != null) ? property2.GetValue(obj, null) : null);
				if (obj2 == null)
				{
					return false;
				}
				Type type2 = obj2.GetType();
				PropertyInfo property3 = type2.GetProperty("MyPhone", BindingFlags.Instance | BindingFlags.Public);
				object obj3 = ((property3 != null) ? property3.GetValue(obj2, null) : null);
				if (obj3 == null)
				{
					return false;
				}
				Type type3 = obj3.GetType();
				FieldInfo field = type3.GetField("m_CurrentApp", BindingFlags.Instance | BindingFlags.NonPublic);
				object obj4 = ((field != null) ? field.GetValue(obj3) : null);
				if (obj4 == null)
				{
					PropertyInfo property4 = type3.GetProperty("CurrentApp", BindingFlags.Instance | BindingFlags.Public);
					obj4 = ((property4 != null) ? property4.GetValue(obj3, null) : null);
				}
				return obj4 != null && string.Equals(obj4.GetType().Name, "AppMultiplayerPublicLobbies", StringComparison.Ordinal);
			}
			catch
			{
				return false;
			}
		}

		private static void HideContainingLobbyButton(TextMeshProUGUI tmp)
		{
			if ((Object)(object)tmp == (Object)null)
			{
				return;
			}
			Transform val = ((TMP_Text)tmp).transform;
			int num = 0;
			while ((Object)(object)val != (Object)null && num < 8)
			{
				if (!((Object)(object)val == (Object)null))
				{
					GameObject gameObject = ((Component)val).gameObject;
					if (_hiddenObjects.Contains(gameObject))
					{
						break;
					}
					Component[] components = ((Component)val).GetComponents<Component>();
					foreach (Component val2 in components)
					{
						if ((Object)(object)val2 == (Object)null)
						{
							continue;
						}
						string name = ((object)val2).GetType().Name;
						if (name.IndexOf("PhoneScrollButton", StringComparison.OrdinalIgnoreCase) >= 0 || name.Equals("Button", StringComparison.OrdinalIgnoreCase))
						{
							if (gameObject.activeSelf)
							{
								gameObject.SetActive(false);
								_hiddenObjects.Add(gameObject);
							}
							return;
						}
					}
				}
				num++;
				val = val.parent;
			}
		}

		private static void RewriteInviteTexts(TextMeshProUGUI[] texts)
		{
			if (texts == null)
			{
				return;
			}
			foreach (TextMeshProUGUI val in texts)
			{
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				string text = ((TMP_Text)val).text;
				if (!string.IsNullOrEmpty(text) && text.IndexOf("Has invited you to their", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					string text2 = text.Replace("Pro Skater Score Battle", "Sync Video").Replace("Pro Skater Battle", "Sync Video");
					if (!string.Equals(text, text2, StringComparison.Ordinal))
					{
						((TMP_Text)val).text = text2;
					}
				}
			}
		}

		private void RestoreOverriddenElements()
		{
			if ((Object)(object)_cachedLobbySettings != (Object)null)
			{
				_cachedLobbySettings.SetActive(_originalLobbySettingsActive);
			}
			if ((Object)(object)_cachedGameplayUi != (Object)null)
			{
				_cachedGameplayUi.SetActive(_originalGameplayUiActive);
			}
			if ((Object)(object)_cachedLobbyName != (Object)null && _originalLobbyName != null)
			{
				((TMP_Text)_cachedLobbyName).text = _originalLobbyName;
			}
		}

		private void SetCanvasHidden(bool hidden)
		{
			if (!((Object)(object)_cachedCanvas == (Object)null) && !((Object)(object)_cachedCanvasGroup == (Object)null))
			{
				((Behaviour)_cachedCanvas).enabled = !hidden;
				_cachedCanvasGroup.alpha = (hidden ? 0f : 1f);
				_cachedCanvasGroup.interactable = !hidden;
				_cachedCanvasGroup.blocksRaycasts = !hidden;
			}
		}

		private void ClearCache()
		{
			_lastHideAll = null;
			_cachedLobbyUi = null;
			_cachedCanvas = null;
			_cachedCanvasGroup = null;
			_cachedLobbyName = null;
			_cachedLobbySettings = null;
			_cachedGameplayUi = null;
			_originalLobbyName = null;
			_originalLobbySettingsActive = false;
			_originalGameplayUiActive = false;
		}

		private static Transform FindDeepChild(Transform root, string name)
		{
			if ((Object)(object)root == (Object)null)
			{
				return null;
			}
			if (((Object)root).name == name)
			{
				return root;
			}
			for (int i = 0; i < root.childCount; i++)
			{
				Transform val = FindDeepChild(root.GetChild(i), name);
				if ((Object)(object)val != (Object)null)
				{
					return val;
				}
			}
			return null;
		}

		private static TextMeshProUGUI FindDeepTMP(Transform root, string name)
		{
			Transform val = FindDeepChild(root, name);
			return ((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<TextMeshProUGUI>() : null;
		}
	}
	public sealed class SubtitleTrackInfo
	{
		public int StreamIndex;

		public string Language;

		public string Title;
	}
	public sealed class AudioTrackInfo
	{
		public int StreamIndex;

		public int AudioIndex;

		public string Language;

		public string Title;

		public string CodecName;

		public string GetMenuLabel()
		{
			string text = ((!string.IsNullOrWhiteSpace(Title)) ? Title.Trim() : string.Empty);
			string text2 = ((!string.IsNullOrWhiteSpace(Language) && !Language.Equals("und", StringComparison.OrdinalIgnoreCase)) ? Language.Trim() : string.Empty);
			if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2))
			{
				return text + " - [" + text2 + "]";
			}
			if (!string.IsNullOrWhiteSpace(text))
			{
				return text;
			}
			if (!string.IsNullOrWhiteSpace(text2))
			{
				return "[" + text2 + "]";
			}
			if (!string.IsNullOrWhiteSpace(CodecName))
			{
				return CodecName;
			}
			return string.Empty;
		}
	}
	public sealed class SubtitleEntry
	{
		public double StartTime;

		public double EndTime;

		public string Text;
	}
	public sealed class SubtitleManager
	{
		private readonly ManualLogSource _logger;

		private volatile List<SubtitleTrackInfo> _tracks = new List<SubtitleTrackInfo>();

		private volatile List<SubtitleEntry> _entries = new List<SubtitleEntry>();

		private int _selectedTrack = -1;

		private int _searchHint = 0;

		private volatile bool _isProbing = false;

		private volatile bool _isExtracting = false;

		private volatile bool _hasLoggedSubtitleReady = false;

		private CancellationTokenSource _sessionCts;

		private CancellationTokenSource _selectionCts;

		private static readonly Regex _htmlTagRx = new Regex("<[^>]+>", RegexOptions.Compiled);

		private static readonly Regex _ssaOverrideTagRx = new Regex("\\{\\\\[^}]*\\}", RegexOptions.Compiled);

		private static readonly Regex _titleLineRx = new Regex("^\\s+title\\s*:\\s*(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static readonly Regex _videoCodecRx = new Regex("Stream #0:\\d+[^:]*: Video: (\\w+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static readonly ConcurrentDictionary<string, List<SubtitleTrackInfo>> _trackCache = new ConcurrentDictionary<string, List<SubtitleTrackInfo>>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, string> _subtitleSrtCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, List<SubtitleEntry>> _subtitleEntryCache = new ConcurrentDictionary<string, List<SubtitleEntry>>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, byte> _subtitleCompleteCache = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, Task<string>> _subtitleExtractTasks = new ConcurrentDictionary<string, Task<string>>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, Process> _subtitleProcesses = new ConcurrentDictionary<string, Process>(StringComparer.Ordinal);

		private static readonly ConcurrentDictionary<string, List<Action<List<SubtitleEntry>>>> _subtitleProgressListeners = new ConcurrentDictionary<string, List<Action<List<SubtitleEntry>>>>(StringComparer.Ordinal);

		private static readonly Regex _streamSubRx = new Regex("Stream #0:(\\d+)(?:\\(([^)]+)\\))?[^:]*: Subtitle", RegexOptions.IgnoreCase | RegexOptions.Compiled);

		public int TrackCount => _tracks.Count;

		public int SelectedTrack => _selectedTrack;

		public bool IsProbing => _isProbing;

		public bool IsExtracting => _isExtracting;

		public SubtitleManager(ManualLogSource logger)
		{
			_logger = logger;
		}

		public string GetTrackLabel(int index)
		{
			List<SubtitleTrackInfo> tracks = _tracks;
			if (index < 0 || index >= tracks.Count)
			{
				return "Subtitle Track " + (index + 1);
			}
			SubtitleTrackInfo subtitleTrackInfo = tracks[index];
			string text = "Subtitle Track " + (index + 1);
			string text2 = StripOuterBrackets(subtitleTrackInfo.Title);
			if (!string.IsNullOrWhiteSpace(text2))
			{
				text = text + " (" + text2 + ")";
			}
			else
			{
				string text3 = FormatLanguage(StripOuterBrackets(subtitleTrackInfo.Language));
				if (!string.IsNullOrWhiteSpace(text3))
				{
					text = text + " (" + text3 + ")";
				}
			}
			return text;
		}

		private static string StripOuterBrackets(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return string.Empty;
			}
			string text = value.Trim();
			if (text.Length >= 2 && text[0] == '[' && text[text.Length - 1] == ']')
			{
				return text.Substring(1, text.Length - 2).Trim();
			}
			return text;
		}

		private static string FormatLanguage(string language)
		{
			if (string.IsNullOrWhiteSpace(language) || language.Equals("und", StringComparison.OrdinalIgnoreCase))
			{
				return string.Empty;
			}
			switch (language.Trim().ToLowerInvariant())
			{
			case "en":
			case "eng":
				return "English";
			case "ja":
			case "jpn":
			case "jp":
				return "Japanese";
			case "es":
			case "spa":
				return "Spanish";
			case "fr":
			case "fre":
			case "fra":
				return "French";
			case "de":
			case "ger":
			case "deu":
				return "German";
			case "it":
			case "ita":
				return "Italian";
			case "pt":
			case "por":
				return "Portuguese";
			case "zh":
			case "zho":
			case "chi":
				return "Chinese";
			case "ko":
			case "kor":
				return "Korean";
			case "ru":
			case "rus":
				return "Russian";
			case "ar":
			case "ara":
				return "Arabic";
			default:
				return language.Trim();
			}
		}

		public static bool IsSubtitleProbeSupported(string url)
		{
			if (string.IsNullOrWhiteSpace(url))
			{
				return false;
			}
			string text = url;
			int num = text.IndexOf('?');
			if (num >= 0)
			{
				text = text.Substring(0, num);
			}
			string extension = Path.GetExtension(text);
			if (string.IsNullOrWhiteSpace(extension))
			{
				return false;
			}
			switch (extension.ToLowerInvariant())
			{
			case ".mkv":
			case ".mp4":
			case ".m4v":
			case ".mov":
			case ".webm":
			case ".avi":
				return true;
			default:
				return false;
			}
		}

		public void ProbeAsync(string url, string ffmpegPath, Action onComplete)
		{
			CancelAllWork();
			_tracks = new List<SubtitleTrackInfo>();
			_entries = new List<SubtitleEntry>();
			_selectedTrack = -1;
			_searchHint = 0;
			_hasLoggedSubtitleReady = false;
			if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(ffmpegPath))
			{
				onComplete?.Invoke();
				return;
			}
			if (_trackCache.TryGetValue(url, out var value))
			{
				_tracks = new List<SubtitleTrackInfo>(value);
				onComplete?.Invoke();
				return;
			}
			_isProbing = true;
			_sessionCts = new CancellationTokenSource();
			CancellationToken token = _sessionCts.Token;
			ThreadPool.QueueUserWorkItem(delegate
			{
				try
				{
					if (!token.IsCancellationRequested)
					{
						List<SubtitleTrackInfo> list = ProbeSubtitleTracks(url, ffmpegPath, token);
						if (!token.IsCancellationRequested)
						{
							_tracks = list;
							if (list != null && list.Count > 0)
							{
								_trackCache[url] = new List<SubtitleTrackInfo>(list);
							}
						}
					}
				}
				catch (Exception ex)
				{
					if (_logger != null)
					{
						_logger.LogWarning((object)("[SubtitleManager] Probe error: " + ex.Message));
					}
				}
				finally
				{
					_isProbing = false;
					if (!token.IsCancellationRequested)
					{
						onComplete?.Invoke();
					}
				}
			});
		}

		public void SelectTrack(int trackIndex, string url, string ffmpegPath, Action onComplete)
		{
			CancelSelectionOnly();
			_entries = new List<SubtitleEntry>();
			_searchHint = 0;
			_hasLoggedSubtitleReady = false;
			List<SubtitleTrackInfo> tracks = _tracks;
			if (trackIndex < 0 || trackIndex >= tracks.Count)
			{
				_selectedTrack = -1;
				onComplete?.Invoke();
				return;
			}
			_selectedTrack = trackIndex;
			_isExtracting = true;
			_selectionCts = new CancellationTokenSource();
			CancellationToken selectionToken = _selectionCts.Token;
			CancellationToken sessionToken = ((_sessionCts != null) ? _sessionCts.Token : CancellationToken.None);
			int streamIndex = tracks[trackIndex].StreamIndex;
			string cacheKey = BuildSubtitleCacheKey(url, streamIndex);
			if (TryGetParsedSubtitleEntries(cacheKey, out var entries))
			{
				_entries = entries;
				LogSubtitlesReady(entries?.Count ?? 0, stillLoading: false);
				_isExtracting = false;
				onComplete?.Invoke();
				return;
			}
			if (TryLoadSubtitleEntriesFromDiskCache(cacheKey, out entries))
			{
				_entries = entries;
				LogSubtitlesReady(entries?.Count ?? 0, stillLoading: false);
				_isExtracting = false;
				onComplete?.Invoke();
				return;
			}
			int firstProgressNotified = 0;
			Action<List<SubtitleEntry>> progressListener = delegate(List<SubtitleEntry> partialEntries)
			{
				if (!selectionToken.IsCancellationRequested && partialEntries != null && partialEntries.Count != 0)
				{
					_entries = partialEntries;
					LogSubtitlesReady(partialEntries.Count, stillLoading: true);
					if (Interlocked.Exchange(ref firstProgressNotified, 1) == 0)
					{
						_isExtracting = false;
						onComplete?.Invoke();
					}
				}
			};
			AddSubtitleProgressListener(cacheKey, progressListener);
			if (TryGetPartialSubtitleEntries(cacheKey, out var entries2) && entries2.Count > 0)
			{
				_entries = entries2;
				LogSubtitlesReady(entries2.Count, stillLoading: true);
			}
			Task<string> orStartSubtitleExtraction = GetOrStartSubtitleExtraction(cacheKey, url, streamIndex, ffmpegPath, sessionToken);
			orStartSubtitleExtraction.ContinueWith(delegate(Task<string> t)
			{
				RemoveSubtitleProgressListener(cacheKey, progressListener);
				if (selectionToken.IsCancellationRequested)
				{
					return;
				}
				try
				{
					if (t.Status == TaskStatus.RanToCompletion && t.Result != null)
					{
						if (TryGetParsedSubtitleEntries(cacheKey, out var entries3))
						{
							_entries = entries3;
							LogSubtitlesReady(entries3?.Count ?? 0, stillLoading: false);
						}
						else
						{
							_entries = ParseAndCacheSubtitleEntries(cacheKey, t.Result);
							LogSubtitlesReady((_entries != null) ? _entries.Count : 0, stillLoading: false);
						}
					}
				}
				catch (Exception ex)
				{
					if (_logger != null)
					{
						_logger.LogWarning((object)("[SubtitleManager] Extract error: " + ex.Message));
					}
				}
				finally
				{
					bool flag = Interlocked.Exchange(ref firstProgressNotified, 1) == 0;
					_isExtracting = false;
					if (flag && !selectionToken.IsCancellationRequested)
					{
						onComplete?.Invoke();
					}
				}
			}, TaskScheduler.Default);
		}

		public void DisableSubtitles()
		{
			Cancel();
			_selectedTrack = -1;
			_entries = new List<SubtitleEntry>();
			_searchHint = 0;
			_hasLoggedSubtitleReady = false;
		}

		internal void EagerExtractAllTracksAsync(string url, string ffmpegPath)
		{
			List<SubtitleTrackInfo> tracks = _tracks;
			if (tracks == null || tracks.Count == 0 || string.IsNullOrWhiteSpace(url))
			{
				return;
			}
			CancellationToken sessionToken = ((_sessionCts != null) ? _sessionCts.Token : CancellationToken.None);
			int selectedTrack = _selectedTrack;
			if (selectedTrack < 0 || selectedTrack >= tracks.Count)
			{
				return;
			}
			int selectedStreamIndex = tracks[selectedTrack].StreamIndex;
			string selectedCacheKey = BuildSubtitleCacheKey(url, selectedStreamIndex);
			List<string> pendingKeys = new List<string>(tracks.Count);
			List<int> pendingIndexes = new List<int>(tracks.Count);
			for (int i = 0; i < tracks.Count; i++)
			{
				int streamIndex = tracks[i].StreamIndex;
				string text = BuildSubtitleCacheKey(url, streamIndex);
				if (!TryGetParsedSubtitleEntries(text, out var entries) && !TryLoadSubtitleEntriesFromDiskCache(text, out entries) && streamIndex != selectedStreamIndex)
				{
					pendingKeys.Add(text);
					pendingIndexes.Add(streamIndex);
				}
			}
			ThreadPool.QueueUserWorkItem(delegate
			{
				try
				{
					if (!sessionToken.IsCancellationRequested && !TryGetParsedSubtitleEntries(selectedCacheKey, out var entries2))
					{
						GetOrStartSubtitleExtraction(selectedCacheKey, url, selectedStreamIndex, ffmpegPath, sessionToken).Wait();
					}
					for (int j = 0; j < pendingKeys.Count; j++)
					{
						if (sessionToken.IsCancellationRequested)
						{
							break;
						}
						string cacheKey = pendingKeys[j];
						int subtitleStreamIndex = pendingIndexes[j];
						if (!TryGetParsedSubtitleEntries(cacheKey, out entries2))
						{
							GetOrStartSubtitleExtraction(cacheKey, url, subtitleStreamIndex, ffmpegPath, sessionToken).Wait();
						}
					}
				}
				catch
				{
				}
			});
		}

		public void Cancel()
		{
			CancelSelectionOnly();
		}

		private void CancelSelectionOnly()
		{
			if (_selectionCts != null)
			{
				_selectionCts.Cancel();
				_selectionCts = null;
			}
			_isExtracting = false;
		}

		private void CancelAllWork()
		{
			CancelSelectionOnly();
			if (_sessionCts != null)
			{
				_sessionCts.Cancel();
				_sessionCts = null;
			}
			KillActiveSubtitleProcesses();
			_isProbing = false;
		}

		public void Clear()
		{
			if ((_isExtracting || (_entries != null && _entries.Count > 0)) && _selectedTrack >= 0)
			{
				string arg = (_isExtracting ? "Was extracting." : "Extraction finished.");
				ManualLogSource logger = _logger;
				if (logger != null)
				{
					logger.LogWarning((object)$"[SubtitleManager] Lobby shutting down, subtitles abandoned! Track #{_selectedTrack} has {_entries.Count} Entries. {arg}");
				}
			}
			CancelAllWork();
			_tracks = new List<SubtitleTrackInfo>();
			_entries = new List<SubtitleEntry>();
			_selectedTrack = -1;
			_searchHint = 0;
			_hasLoggedSubtitleReady = false;
		}

		public string GetActiveSubtitle(double time)
		{
			List<SubtitleEntry> entries = _entries;
			if (entries == null || entries.Count == 0)
			{
				return null;
			}
			int count = entries.Count;
			if (_searchHint >= count)
			{
				_searchHint = 0;
			}
			SubtitleEntry subtitleEntry = entries[_searchHint];
			if (time >= subtitleEntry.StartTime && time < subtitleEntry.EndTime)
			{
				return subtitleEntry.Text;
			}
			if (time >= subtitleEntry.EndTime)
			{
				for (int i = _searchHint + 1; i < count; i++)
				{
					SubtitleEntry subtitleEntry2 = entries[i];
					if (time < subtitleEntry2.StartTime)
					{
						_searchHint = i;
						return null;
					}
					if (time < subtitleEntry2.EndTime)
					{
						_searchHint = i;
						return subtitleEntry2.Text;
					}
				}
				_searchHint = count - 1;
				return null;
			}
			int num = 0;
			int num2 = count - 1;
			while (num <= num2)
			{
				int num3 = num + num2 >> 1;
				SubtitleEntry subtitleEntry3 = entries[num3];
				if (time < subtitleEntry3.StartTime)
				{
					num2 = num3 - 1;
					continue;
				}
				if (time >= subtitleEntry3.EndTime)
				{
					num = num3 + 1;
					continue;
				}
				_searchHint = num3;
				return subtitleEntry3.Text;
			}
			return null;
		}

		private void LogSubtitlesReady(int loadedEntryCount, bool stillLoading)
		{
			if (!_hasLoggedSubtitleReady && loadedEntryCount > 0)
			{
				_hasLoggedSubtitleReady = true;
				if (_logger != null)
				{
					string arg = (stillLoading ? "Subtitles loading in background..." : "Subtitles complete!");
					_logger.LogInfo((object)string.Format(CultureInfo.InvariantCulture, "[SubtitleManager] Subtitle