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.5.0"; } public sealed class SyncVideoConfig { public const string DefaultLobbyName = "Sync Video Lobby"; public const bool LogBusMessages = false; private const int ConfigVersion = 5; private const string ConfigVersionMarker = "SyncVideoConfigVersion"; public ConfigEntry TvObjectName { get; } public ConfigEntry ScreenMaterialTextureName { get; } public ConfigEntry HostBeaconInterval { get; } public ConfigEntry EnableOfflineMode { get; } public ConfigEntry HostStateResendInterval { get; } public ConfigEntry DriftToleranceSeconds { get; } public ConfigEntry HardSeekThresholdSeconds { get; } public ConfigEntry AutoAttachToTVsOnStageLoad { get; } public ConfigEntry ShowScreenPositionMenu { get; } public ConfigEntry ShowRefreshScreensButton { get; } public ConfigEntry HideNativeLobbyUi { get; } public ConfigEntry HostAutoplay { get; } public ConfigEntry DefaultVolume { get; } public ConfigEntry VideoRenderResolution { get; } public ConfigEntry YouTubeStreamResolution { get; } public ConfigEntry UseFFmpeg { get; } public ConfigEntry EnableMkvSupport { get; } public ConfigEntry SuppressAFK { get; } public ConfigEntry MuteMusicAndAmbient { get; } public ConfigEntry UseUnityAudioSource { get; } public ConfigEntry EnableMkvFfmpegConversion { get; } public ConfigEntry MkvTranscodeToH264 { get; } public ConfigEntry YouTubeVolumeScale { get; } public ConfigEntry SubtitleFontSize { get; } public ConfigEntry StaticTVs { get; } public string PluginDirectory { get; } public string AdvancedConfigPath { get; } public SyncVideoConfig(ConfigFile config, ConfigFile advancedConfig, string pluginLocation) { bool flag = Migrate(config); bool flag2 = Migrate(advancedConfig); PluginDirectory = Path.GetDirectoryName(pluginLocation) ?? string.Empty; AdvancedConfigPath = advancedConfig.ConfigFilePath; EnableOfflineMode = config.Bind("Offline Mode", "EnableOfflineMode", false, "Disable all online functionality when enabled. Create local lobbies for personal use."); HideNativeLobbyUi = config.Bind("ACN", "Hide Lobby UI", true, "Hide ACN's lobby UI by default."); SuppressAFK = config.Bind("ACN", "Suppress AFK", true, "Hide AFK animations for yourself and other players while in a Sync Video lobby."); HostAutoplay = config.Bind("Video", "Host Autoplay", true, "If hosting a video lobby, automatically start playback after loading your video URL."); VideoRenderResolution = config.Bind("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("Video", "YouTube Resolution", "1280x720", "Resolution used when streaming YouTube videos. Options: 1920x1080, 1280x720, 960x540, 854x480, 640x360, 426x240."); UseFFmpeg = config.Bind("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("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("Video", "Subtitle Font Size", 34f, "Font size for MKV subtitles displayed on the TV screen. Default is 34."); DefaultVolume = config.Bind("Volume", "Default Volume", 90, "Starting volume level (0–100). Automatically rounds to increments of 10 in-game."); MuteMusicAndAmbient = config.Bind("Volume", "Mute Music and Ambient SFX", true, "Mute the game's music and ambient sounds while in a Sync Video lobby."); StaticTVs = config.Bind("World Props", "Static TVs", true, "Make TVs stay in place, so people can't kick them and disrupt your watch party."); ShowScreenPositionMenu = config.Bind("World Props", "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."); TvObjectName = advancedConfig.Bind("Debug", "TV Object Name", "TV", "Scene object name to bind video screens to."); ScreenMaterialTextureName = advancedConfig.Bind("Debug", "Screen Material Texture Name", "_MainTex", "Texture slot used when drawing video to a renderer."); AutoAttachToTVsOnStageLoad = advancedConfig.Bind("Debug", "Auto Attach To TVs On Load", true, "Automatically bind screens to matching TV objects when a map loads."); ShowRefreshScreensButton = advancedConfig.Bind("Debug", "Refresh Screens Button", false, "Show the Refresh Screens button in the phone app. Rebinds screens to objects."); YouTubeVolumeScale = advancedConfig.Bind("Debug", "YouTube Volume Scale", 1f, "Volume multiplier applied to YouTube videos (0.0-1.0). Makes YouTube videos not kill your ears."); UseUnityAudioSource = advancedConfig.Bind("Debug", "Unity AudioSource", false, "Use Unity AudioSource output for video audio instead of Direct output. Slightly higher latency, but can prevent video/audio offset."); EnableMkvFfmpegConversion = advancedConfig.Bind("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("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("Networking", "Host Beacon Interval", 2f, "Seconds between lobby beacons."); HostStateResendInterval = advancedConfig.Bind("Networking", "Host State Resend Interval", 0.5f, "Seconds between host sync state broadcasts."); DriftToleranceSeconds = advancedConfig.Bind("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("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."); if (flag) { SaveCleanConfig(config); } else { config.Save(); } if (flag2) { SaveCleanConfig(advancedConfig); } else { advancedConfig.Save(); } } private bool Migrate(ConfigFile config) { int num = ReadConfigVersion(config.ConfigFilePath); if (num >= 5) { return false; } bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { config.Clear(); ClearOrphanedEntries(config); try { File.Delete(config.ConfigFilePath); } catch { } } finally { config.SaveOnConfigSet = saveOnConfigSet; } return true; } private int ReadConfigVersion(string path) { try { if (!File.Exists(path)) { return 0; } string a = string.Empty; string[] array = File.ReadAllLines(path); foreach (string text in array) { string text2 = text.Trim(); if (text2.StartsWith("## SyncVideoConfigVersion", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("# SyncVideoConfigVersion", StringComparison.OrdinalIgnoreCase)) { int num = text2.IndexOf('='); if (num >= 0 && int.TryParse(text2.Substring(num + 1).Trim(), out var result)) { return result; } } if (text2.Length > 2 && text2[0] == '[' && text2[text2.Length - 1] == ']') { a = text2.Substring(1, text2.Length - 2).Trim(); } else if (string.Equals(a, "Config", StringComparison.OrdinalIgnoreCase) && text2.StartsWith("Version", StringComparison.OrdinalIgnoreCase)) { int num2 = text2.IndexOf('='); if (num2 >= 0 && int.TryParse(text2.Substring(num2 + 1).Trim(), out var result2)) { return Math.Min(result2, 4); } } } } catch { } return 0; } private void SaveCleanConfig(ConfigFile config) { try { string directoryName = Path.GetDirectoryName(config.ConfigFilePath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } List list = new List(); Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (ConfigDefinition key in config.Keys) { if (key == (ConfigDefinition)null) { continue; } ConfigEntryBase val = null; try { val = config[key]; } catch { } if (val != null) { string text = key.Section ?? string.Empty; if (!dictionary.TryGetValue(text, out var value)) { value = (dictionary[text] = new List()); list.Add(text); } value.Add(val); } } using StreamWriter streamWriter = new StreamWriter(config.ConfigFilePath, append: false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); streamWriter.WriteLine("## SyncVideoConfigVersion = " + 5.ToString(CultureInfo.InvariantCulture)); streamWriter.WriteLine(); foreach (string item in list) { streamWriter.WriteLine("[" + item + "]"); foreach (ConfigEntryBase item2 in dictionary[item]) { ConfigDescription description = item2.Description; string text2 = ((description != null) ? description.Description : null); if (!string.IsNullOrEmpty(text2)) { string[] array = text2.Replace("\r\n", "\n").Split(new char[1] { '\n' }); foreach (string text3 in array) { streamWriter.WriteLine("## " + text3); } } streamWriter.WriteLine(item2.Definition.Key + " = " + GetConfigValueString(item2)); streamWriter.WriteLine(); } } } catch { config.Save(); } } private string GetConfigValueString(ConfigEntryBase entry) { try { if (entry.BoxedValue == null) { return string.Empty; } if (entry.BoxedValue is IFormattable formattable) { return formattable.ToString(null, CultureInfo.InvariantCulture); } return entry.BoxedValue.ToString(); } catch { return string.Empty; } } private void ClearOrphanedEntries(ConfigFile config) { BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; try { PropertyInfo[] properties = ((object)config).GetType().GetProperties(bindingAttr); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.Name.IndexOf("orphan", StringComparison.OrdinalIgnoreCase) >= 0) { try { object value = propertyInfo.GetValue(config, null); value?.GetType().GetMethod("Clear", bindingAttr)?.Invoke(value, null); } catch { } } } FieldInfo[] fields = ((object)config).GetType().GetFields(bindingAttr); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.Name.IndexOf("orphan", StringComparison.OrdinalIgnoreCase) >= 0) { try { object value2 = fieldInfo.GetValue(config); value2?.GetType().GetMethod("Clear", bindingAttr)?.Invoke(value2, null); } catch { } } } } catch { } } } [BepInPlugin("transrights.SyncVideo", "Sync Video", "1.5.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 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 SyncPacketReceived; public SyncVideoTransport(ManualLogSource logger) { _logger = logger; ClientController.RegisterCustomPacketHandler("syncvideo.state", (Action)OnRawState); ClientController.RegisterCustomPacketHandler("syncvideo.time", (Action)OnRawTime); ClientController.RegisterCustomPacketHandler("syncvideo.state_request", (Action)OnRawStateRequest); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_advertise", (Action)OnRawLobbyAdvertise); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_join", (Action)OnRawLobbyJoin); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_leave", (Action)OnRawLobbyLeave); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_members", (Action)OnRawLobbyMembers); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_closed", (Action)OnRawLobbyClosed); ClientController.RegisterCustomPacketHandler("syncvideo.screen_transform", (Action)OnRawScreenTransform); ClientController.RegisterCustomPacketHandler("syncvideo.suggestion", (Action)OnRawSuggestion); ClientController.RegisterCustomPacketHandler("syncvideo.suggestions_open", (Action)OnRawSuggestionsOpen); ClientController.RegisterCustomPacketHandler("syncvideo.suggestion_ack", (Action)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 int SeekRevision; 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(); } if (reader.BaseStream.Position < reader.BaseStream.Length) { SeekRevision = 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); writer.Write(SeekRevision); } } 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 readonly bool _useAudioSourceOutput; private readonly List _audioSources = new List(); private string _lastOriginalUrl = string.Empty; private string _subtitleSourceUrl = string.Empty; private string _lastVideoId = string.Empty; private readonly object _audioTrackLock = new object(); private readonly List _audioTracks = new List(); 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 double DurationSeconds => _player.isPrepared ? _player.length : 0.0; 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_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Expected O, but got Unknown //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_0241: Expected O, but got Unknown //IL_024f: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Expected O, but got Unknown //IL_0267: Unknown result type (might be due to invalid IL or missing references) //IL_0271: Expected O, but got Unknown _logger = Logger.CreateLogSource("SyncVideo.DirectUrlVideoBackend"); _subtitleManager = new SubtitleManager(_logger); _useAudioSourceOutput = SyncVideoPlugin.Settings == null || SyncVideoPlugin.Settings.UseUnityAudioSource.Value; _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(); _player.playOnAwake = false; _player.isLooping = false; _player.renderMode = (VideoRenderMode)2; _player.targetTexture = _texture; _player.aspectRatio = (VideoAspectRatio)3; _player.audioOutputMode = (VideoAudioOutputMode)(_useAudioSourceOutput ? 1 : 2); if (_useAudioSourceOutput) { EnsureAudioSourceCount(1); } _player.EnableAudioTrack((ushort)0, true); _player.skipOnDrop = true; _player.waitForFirstFrame = true; _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}."); _logger.LogInfo((object)("Using video audio output mode " + (_useAudioSourceOutput ? "AudioSource" : "Direct") + ".")); } 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(); } StopAudioSources(); _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) { bool isPlaying = _player.isPlaying; if (isPlaying) { _player.Pause(); } _player.time = Math.Max(0.0, seconds); _lastKnownTimeSeconds = Math.Max(0.0, seconds); _subtitleManager.ResetSearchHint(); if (isPlaying) { _player.Play(); } else { 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 + "\nScanning...") : ("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(); } StopAudioSources(); _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 (_useAudioSourceOutput) { if (HasCachedAudioTracks()) { List cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot(); EnsureAudioSourceCount(cachedAudioTracksSnapshot.Count); for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++) { int unityTrackIndex = cachedAudioTracksSnapshot[i].UnityTrackIndex; if (unityTrackIndex >= 0 && unityTrackIndex < _audioSources.Count) { _audioSources[unityTrackIndex].volume = ((i == num3) ? num : 0f); } } } else { EnsureAudioSourceCount(num2); for (int j = 0; j < num2 && j < _audioSources.Count; j++) { _audioSources[j].volume = ((j == num3) ? num : 0f); } } return; } if (HasCachedAudioTracks()) { List cachedAudioTracksSnapshot2 = GetCachedAudioTracksSnapshot(); for (int k = 0; k < cachedAudioTracksSnapshot2.Count; k++) { ushort num4 = (ushort)cachedAudioTracksSnapshot2[k].UnityTrackIndex; try { _player.SetDirectAudioVolume(num4, (k == 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(); StopAudioSources(); 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(); StopAudioSources(); 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() { if (_useAudioSourceOutput) { return; } try { _player.audioOutputMode = (VideoAudioOutputMode)0; _player.audioOutputMode = (VideoAudioOutputMode)2; } catch { } } private void EnsureAudioSourceCount(int count) { if (_useAudioSourceOutput) { count = Math.Max(1, count); while (_audioSources.Count < count) { AudioSource val = _root.AddComponent(); val.playOnAwake = false; val.loop = false; val.spatialBlend = 0f; val.volume = 0f; _audioSources.Add(val); } } } private void StopAudioSources() { if (!_useAudioSourceOutput) { return; } for (int i = 0; i < _audioSources.Count; i++) { try { _audioSources[i].Stop(); } catch { } } } private void ConfigureAudioTracks() { int num = Math.Max(1, _knownAudioTrackCount); int num2 = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, num - 1)); if (HasCachedAudioTracks()) { List cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot(); try { _player.controlledAudioTrackCount = (ushort)cachedAudioTracksSnapshot.Count; } catch { } EnsureAudioSourceCount(cachedAudioTracksSnapshot.Count); for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++) { ushort num3 = (ushort)cachedAudioTracksSnapshot[i].UnityTrackIndex; try { _player.EnableAudioTrack(num3, i == num2); } catch { } if (_useAudioSourceOutput && num3 < _audioSources.Count) { try { _player.SetTargetAudioSource(num3, _audioSources[num3]); } catch { } } } AudioTrackInfo audioTrackInfo = ((num2 >= 0 && num2 < cachedAudioTracksSnapshot.Count) ? cachedAudioTracksSnapshot[num2] : null); if (audioTrackInfo != null) { return; } } try { _player.controlledAudioTrackCount = (ushort)num; } catch { } EnsureAudioSourceCount(num); for (int j = 0; j < num; j++) { try { _player.EnableAudioTrack((ushort)j, j == num2); } catch { } if (_useAudioSourceOutput && j < _audioSources.Count) { try { _player.SetTargetAudioSource((ushort)j, _audioSources[j]); } 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 GetCachedAudioTracksSnapshot() { lock (_audioTrackLock) { return new List(_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 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 ProbeAudioTracks(string url, string ffprobePath, CancellationToken token) { List result = new List(); 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 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 ProbeAudioTracksFromFfmpegStderr(string url, string ffmpegPath, CancellationToken token) { List list = new List(); 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; private static float _ambientSFXRestoreTimer; private const float AmbientSFXRestoreTimerSec = 2f; 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: Off", HudMode.UI => "Hide HUD: UI", HudMode.UIChat => "Hide HUD: UI+Chat", HudMode.UIAll => "Hide HUD: Everything", _ => "Hide HUD", }; } public static void Cycle() { _currentMode = (HudMode)((int)(_currentMode + 1) % 4); Apply(); } public static void Tick() { if (_ambientSFXRestoreTimer > 0f) { _ambientSFXRestoreTimer -= Time.unscaledDeltaTime; if (_ambientSFXRestoreTimer <= 0f) { _ambientSFXRestoreTimer = 0f; RestoreAudio(); } } 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(); _ambientSFXRestoreTimer = 0f; 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; if (_musicMuted) { _ambientSFXRestoreTimer = 2f; } } 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(); if ((Object)(object)_gameplayCanvasGroup == (Object)null) { _gameplayCanvasGroup = val3.gameObject.AddComponent(); } } _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 _lobbyNamesBuffer = new HashSet(StringComparer.Ordinal); private static readonly HashSet _hiddenObjects = new HashSet(); 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(); } 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(); if ((Object)(object)_cachedCanvas == (Object)null) { ClearCache(); return false; } _cachedCanvasGroup = ((Component)val).GetComponent(); if ((Object)(object)_cachedCanvasGroup == (Object)null) { _cachedCanvasGroup = ((Component)val).gameObject.AddComponent(); } _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(); 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() : 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 _tracks = new List(); private volatile List _entries = new List(); 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> _trackCache = new ConcurrentDictionary>(StringComparer.Ordinal); private static readonly ConcurrentDictionary _subtitleSrtCache = new ConcurrentDictionary(StringComparer.Ordinal); private static readonly ConcurrentDictionary> _subtitleEntryCache = new ConcurrentDictionary>(StringComparer.Ordinal); private static readonly ConcurrentDictionary _subtitleCompleteCache = new ConcurrentDictionary(StringComparer.Ordinal); private static readonly ConcurrentDictionary> _subtitleExtractTasks = new ConcurrentDictionary>(StringComparer.Ordinal); private static readonly ConcurrentDictionary _subtitleProcesses = new ConcurrentDictionary(StringComparer.Ordinal); private static readonly ConcurrentDictionary>>> _subtitleProgressListeners = new ConcurrentDictionary>>>(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 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(); _entries = new List(); _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(value); onComplete?.Invoke(); return; } _isProbing = true; _sessionCts = new CancellationTokenSource(); CancellationToken token = _sessionCts.Token; ThreadPool.QueueUserWorkItem(delegate { try { if (!token.IsCancellationRequested) { List list = ProbeSubtitleTracks(url, ffmpegPath, token); if (!token.IsCancellationRequested) { _tracks = list; if (list != null && list.Count > 0) { _trackCache[url] = new List(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(); _searchHint = 0; _hasLoggedSubtitleReady = false; List 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> progressListener = delegate(List 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 orStartSubtitleExtraction = GetOrStartSubtitleExtraction(cacheKey, url, streamIndex, ffmpegPath, sessionToken); orStartSubtitleExtraction.ContinueWith(delegate(Task 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(); _searchHint = 0; _hasLoggedSubtitleReady = false; } internal void EagerExtractAllTracksAsync(string url, string ffmpegPath) { List 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 pendingKeys = new List(tracks.Count); List pendingIndexes = new List(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(); _entries = new List(); _selectedTrack = -1; _searchHint = 0; _hasLoggedSubtitleReady = false; } public string GetActiveSubtitle(double time) { List 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] Subtitles ready: Sub Track #{0} pre-loaded, {1} Entries Loaded. {2}", _selectedTrack, loadedEntryCount, arg)); } } } public void ResetSearchHint() { _searchHint = 0; } internal static List ParseSrt(string content) { List list = new List(); if (string.IsNullOrWhiteSpace(content)) { return list; } StringReader stringReader = new StringReader(content); StringBuilder stringBuilder = new StringBuilder(256); while (true) { string text = null; string text2; while ((text2 = stringReader.ReadLine()) != null) { if (text2.IndexOf("-->", StringComparison.Ordinal) >= 0) { text = text2; break; } } if (text == null) { break; } string[] array = text.Split(new string[1] { "-->" }, StringSplitOptions.None); if (array.Length < 2 || !TryParseSrtTime(array[0].Trim(), out var seconds) || !TryParseSrtTime(array[1].Trim(), out var seconds2) || seconds2 <= seconds) { continue; } stringBuilder.Clear(); while ((text2 = stringReader.ReadLine()) != null && text2.Length > 0) { if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(text2); } if (stringBuilder.Length != 0) { string text3 = CleanSubtitleText(stringBuilder.ToString()); if (!string.IsNullOrWhiteSpace(text3)) { list.Add(new SubtitleEntry { StartTime = seconds, EndTime = seconds2, Text = text3 }); } } } stringReader.Dispose(); list.Sort((SubtitleEntry a, SubtitleEntry b) => a.StartTime.CompareTo(b.StartTime)); return list; } private static string CleanSubtitleText(string text) { if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } text = _ssaOverrideTagRx.Replace(text, string.Empty); text = _htmlTagRx.Replace(text, string.Empty); return text.Trim(); } private static bool TryParseSrtTime(string s, out double seconds) { seconds = 0.0; s = s.Replace(',', '.').Trim(); int num = s.IndexOf(':'); int num2 = s.IndexOf(':', num + 1); if (num < 0 || num2 < 0) { return false; } if (!int.TryParse(s.Substring(0, num), out var result)) { return false; } if (!int.TryParse(s.Substring(num + 1, num2 - num - 1), out var result2)) { return false; } if (!double.TryParse(s.Substring(num2 + 1), NumberStyles.Any, CultureInfo.InvariantCulture, out var result3)) { return false; } seconds = (double)result * 3600.0 + (double)result2 * 60.0 + result3; return true; } private static List ProbeSubtitleTracks(string url, string ffmpegPath, CancellationToken token) { List list = new List(); string text = FindFfprobePath(ffmpegPath); if (!string.IsNullOrWhiteSpace(text)) { try { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "-v error -probesize 10000000 -analyzeduration 10000000 -show_entries stream=index,codec_type:stream_tags=language,title -select_streams s -of default=noprint_wrappers=0 \"" + EscapeArg(url) + "\"", RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; Process ffprobeProc = new Process { StartInfo = startInfo }; try { CancellationTokenRegistration cancellationTokenRegistration = default(CancellationTokenRegistration); try { ffprobeProc.Start(); cancellationTokenRegistration = token.Register(delegate { KillProcess(ffprobeProc); }); string output = ffprobeProc.StandardOutput.ReadToEnd(); ffprobeProc.WaitForExit(15000); if (token.IsCancellationRequested) { return list; } ParseFfprobeSubtitleStreams(output, list, token); if (list.Count > 0) { return list; } } finally { cancellationTokenRegistration.Dispose(); } } finally { if (ffprobeProc != null) { ((IDisposable)ffprobeProc).Dispose(); } } } catch { } } ProcessStartInfo startInfo2 = new ProcessStartInfo { FileName = ffmpegPath, Arguments = "-probesize 10000000 -analyzeduration 10000000 -i \"" + EscapeArg(url) + "\" -hide_banner", RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; Process proc = new Process { StartInfo = startInfo2 }; try { CancellationTokenRegistration cancellationTokenRegistration2 = default(CancellationTokenRegistration); string text2; try { proc.Start(); cancellationTokenRegistration2 = token.Register(delegate { KillProcess(proc); }); text2 = proc.StandardError.ReadToEnd(); proc.WaitForExit(15000); } finally { cancellationTokenRegistration2.Dispose(); } if (token.IsCancellationRequested) { return list; } SubtitleTrackInfo subtitleTrackInfo = null; int num = 0; string[] array = text2.Split(new char[1] { '\n' }); foreach (string text3 in array) { if (token.IsCancellationRequested) { break; } string text4 = text3.TrimEnd(new char[1] { '\r' }); if (text4.IndexOf("Stream #", StringComparison.Ordinal) >= 0) { if (subtitleTrackInfo != null) { list.Add(subtitleTrackInfo); subtitleTrackInfo = null; } Match match = _streamSubRx.Match(text4); if (match.Success) { subtitleTrackInfo = new SubtitleTrackInfo { StreamIndex = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture), Language = StripOuterBrackets(match.Groups[2].Value.Trim()) }; num++; } } else if (subtitleTrackInfo != null) { Match match2 = _titleLineRx.Match(text4); if (match2.Success) { subtitleTrackInfo.Title = StripOuterBrackets(match2.Groups[1].Value.Trim()); } } } if (subtitleTrackInfo != null && !token.IsCancellationRequested) { list.Add(subtitleTrackInfo); } } finally { if (proc != null) { ((IDisposable)proc).Dispose(); } } return list; } private List ParseAndCacheSubtitleEntries(string cacheKey, string srt) { List list = ParseSrt(srt); if (list == null) { list = new List(); } _subtitleEntryCache[cacheKey] = list; _subtitleCompleteCache[cacheKey] = 1; return list; } private static bool TryGetParsedSubtitleEntries(string cacheKey, out List entries) { if (_subtitleCompleteCache.ContainsKey(cacheKey) && _subtitleEntryCache.TryGetValue(cacheKey, out entries) && entries != null) { return true; } entries = null; return false; } private static bool TryGetPartialSubtitleEntries(string cacheKey, out List entries) { if (_subtitleEntryCache.TryGetValue(cacheKey, out entries) && entries != null) { return true; } entries = null; return false; } private static void AddSubtitleProgressListener(string cacheKey, Action> listener) { if (string.IsNullOrWhiteSpace(cacheKey) || listener == null) { return; } List>> orAdd = _subtitleProgressListeners.GetOrAdd(cacheKey, (string _) => new List>>()); lock (orAdd) { orAdd.Add(listener); } } private static void RemoveSubtitleProgressListener(string cacheKey, Action> listener) { if (string.IsNullOrWhiteSpace(cacheKey) || listener == null || !_subtitleProgressListeners.TryGetValue(cacheKey, out var value) || value == null) { return; } lock (value) { value.Remove(listener); } } private static void PublishSubtitleProgress(string cacheKey, List entries) { if (string.IsNullOrWhiteSpace(cacheKey) || entries == null || entries.Count == 0) { return; } List list = new List(entries); list.Sort((SubtitleEntry a, SubtitleEntry b) => a.StartTime.CompareTo(b.StartTime)); _subtitleEntryCache[cacheKey] = list; if (!_subtitleProgressListeners.TryGetValue(cacheKey, out var value) || value == null) { return; } Action>[] array; lock (value) { array = value.ToArray(); } for (int i = 0; i < array.Length; i++) { try { array[i]?.Invoke(list); } catch { } } } private static bool TryParseSrtBlock(List lines, out SubtitleEntry entry) { entry = null; if (lines == null || lines.Count == 0) { return false; } int num = -1; for (int i = 0; i < lines.Count; i++) { if (lines[i].IndexOf("-->", StringComparison.Ordinal) >= 0) { num = i; break; } } if (num < 0) { return false; } string[] array = lines[num].Split(new string[1] { "-->" }, StringSplitOptions.None); if (array.Length < 2) { return false; } if (!TryParseSrtTime(array[0].Trim(), out var seconds)) { return false; } if (!TryParseSrtTime(array[1].Trim(), out var seconds2)) { return false; } if (seconds2 <= seconds) { return false; } StringBuilder stringBuilder = new StringBuilder(256); for (int j = num + 1; j < lines.Count; j++) { if (!string.IsNullOrWhiteSpace(lines[j])) { if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(lines[j]); } } if (stringBuilder.Length == 0) { return false; } string text = CleanSubtitleText(stringBuilder.ToString()); if (string.IsNullOrWhiteSpace(text)) { return false; } entry = new SubtitleEntry { StartTime = seconds, EndTime = seconds2, Text = text }; return true; } private List LoadAndParseSubtitleEntriesFromDisk(string cacheKey) { if (!TryGetCachedSubtitleSrt(cacheKey, out var srt) || string.IsNullOrWhiteSpace(srt)) { return null; } return ParseAndCacheSubtitleEntries(cacheKey, srt); } private bool TryLoadSubtitleEntriesFromDiskCache(string cacheKey, out List entries) { if (TryGetParsedSubtitleEntries(cacheKey, out entries)) { return true; } entries = LoadAndParseSubtitleEntriesFromDisk(cacheKey); return entries != null; } private Task GetOrStartSubtitleExtraction(string cacheKey, string url, int subtitleStreamIndex, string ffmpegPath, CancellationToken sessionToken) { return _subtitleExtractTasks.GetOrAdd(cacheKey, (string _) => Task.Run(delegate { try { if (TryGetCachedSubtitleSrt(cacheKey, out var srt) && !string.IsNullOrWhiteSpace(srt)) { ParseAndCacheSubtitleEntries(cacheKey, srt); return srt; } string text = ExtractSubtitleSrt(cacheKey, url, subtitleStreamIndex, ffmpegPath, sessionToken); if (string.IsNullOrWhiteSpace(text) || sessionToken.IsCancellationRequested) { return null; } _subtitleSrtCache[cacheKey] = text; List list = ParseAndCacheSubtitleEntries(cacheKey, text); QueueDiskCacheWrite(cacheKey, text, subtitleStreamIndex, list?.Count ?? 0, _logger); return text; } finally { _subtitleExtractTasks.TryRemove(cacheKey, out var _); } }, sessionToken)); } private static void QueueDiskCacheWrite(string cacheKey, string srt, int subtitleStreamIndex, int entryCount, ManualLogSource logger) { if (!string.IsNullOrWhiteSpace(cacheKey) && !string.IsNullOrWhiteSpace(srt)) { ThreadPool.QueueUserWorkItem(delegate { StoreCachedSubtitleSrt(cacheKey, srt, subtitleStreamIndex, entryCount, logger); }); } } private static string ExtractSubtitleSrt(string cacheKey, string url, int subtitleStreamIndex, string ffmpegPath, CancellationToken token) { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffmpegPath, Arguments = $"-nostdin -probesize 10000000 -analyzeduration 1000000 -i \"{EscapeArg(url)}\" -vn -an -dn -map 0:{subtitleStreamIndex} -c:s srt -f srt pipe:1 -hide_banner -loglevel error", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; Process proc = new Process { StartInfo = startInfo }; try { CancellationTokenRegistration cancellationTokenRegistration = default(CancellationTokenRegistration); try { proc.Start(); _subtitleProcesses[cacheKey] = proc; cancellationTokenRegistration = token.Register(delegate { KillProcess(proc); }); proc.BeginErrorReadLine(); StringBuilder stringBuilder = new StringBuilder(65536); List list = new List(8); List list2 = new List(); Stopwatch stopwatch = Stopwatch.StartNew(); bool flag = false; string text; while (!token.IsCancellationRequested && (text = proc.StandardOutput.ReadLine()) != null) { stringBuilder.AppendLine(text); if (text.Length == 0) { if (TryParseSrtBlock(list, out var entry)) { list2.Add(entry); if (!flag || stopwatch.ElapsedMilliseconds >= 50) { PublishSubtitleProgress(cacheKey, list2); stopwatch.Restart(); flag = true; } } list.Clear(); } else { list.Add(text); } } if (token.IsCancellationRequested) { return null; } if (TryParseSrtBlock(list, out var entry2)) { list2.Add(entry2); } if (list2.Count > 0) { PublishSubtitleProgress(cacheKey, list2); } if (!proc.WaitForExit(60000)) { KillProcess(proc); return null; } if (token.IsCancellationRequested) { return null; } string text2 = stringBuilder.ToString(); return string.IsNullOrWhiteSpace(text2) ? null : text2; } finally { cancellationTokenRegistration.Dispose(); _subtitleProcesses.TryRemove(cacheKey, out var _); } } finally { if (proc != null) { ((IDisposable)proc).Dispose(); } } } private static void KillActiveSubtitleProcesses() { KeyValuePair[] array = _subtitleProcesses.ToArray(); foreach (KeyValuePair keyValuePair in array) { KillProcess(keyValuePair.Value); } _subtitleProcesses.Clear(); } private static void KillProcess(Process proc) { if (proc == null) { return; } try { if (!proc.HasExited) { proc.Kill(); } } catch { } } private static string BuildSubtitleCacheKey(string url, int subtitleStreamIndex) { return (url ?? string.Empty) + "|" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture); } private static bool TryGetCachedSubtitleSrt(string cacheKey, out string srt) { if (_subtitleSrtCache.TryGetValue(cacheKey, out srt) && !string.IsNullOrWhiteSpace(srt)) { return true; } string subtitleCachePath = GetSubtitleCachePath(cacheKey); try { if (File.Exists(subtitleCachePath)) { srt = File.ReadAllText(subtitleCachePath); if (!string.IsNullOrWhiteSpace(srt)) { _subtitleSrtCache[cacheKey] = srt; return true; } } } catch { } srt = null; return false; } private static void StoreCachedSubtitleSrt(string cacheKey, string srt, int subtitleStreamIndex, int entryCount, ManualLogSource logger) { if (string.IsNullOrWhiteSpace(cacheKey) || string.IsNullOrWhiteSpace(srt)) { return; } _subtitleSrtCache[cacheKey] = srt; string subtitleCacheDirectory = GetSubtitleCacheDirectory(); if (string.IsNullOrWhiteSpace(subtitleCacheDirectory)) { return; } try { Directory.CreateDirectory(subtitleCacheDirectory); string subtitleCachePath = GetSubtitleCachePath(cacheKey); string text = subtitleCachePath + ".tmp"; File.WriteAllText(text, srt); if (File.Exists(subtitleCachePath)) { File.Delete(subtitleCachePath); } File.Move(text, subtitleCachePath); if (logger != null) { logger.LogInfo((object)string.Format(CultureInfo.InvariantCulture, "[SubtitleManager] Subtitle track cached! Sub Track #{0} complete, {1} Entries loaded. Cache Path: {2}", subtitleStreamIndex, entryCount, subtitleCachePath)); } } catch (Exception ex) { if (logger != null) { logger.LogError((object)("[SubtitleManager] Failed to cache subtitle track " + subtitleStreamIndex + ": " + ex.Message)); } } } private static string GetSubtitleCacheDirectory() { string text = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } return Path.Combine(text, "SubtitleCache"); } private static string GetSubtitleCachePath(string cacheKey) { string subtitleCacheDirectory = GetSubtitleCacheDirectory(); uint num = 2166136261u; string text = cacheKey ?? string.Empty; foreach (char c in text) { num = (num ^ c) * 16777619; } return Path.Combine(subtitleCacheDirectory, string.Format(CultureInfo.InvariantCulture, "sub_{0:x8}.srt", num)); } private static string EscapeArg(string s) { return s?.Replace("\"", "\\\"") ?? string.Empty; } private static void ParseFfprobeSubtitleStreams(string output, List result, CancellationToken token) { if (string.IsNullOrWhiteSpace(output)) { return; } string[] array = output.Replace("\r\n", "\n").Split(new string[2] { "[STREAM]", "[/STREAM]" }, StringSplitOptions.RemoveEmptyEntries); string[] array2 = array; foreach (string text in array2) { if (token.IsCancellationRequested) { break; } string text2 = text.Trim(); if (text2.Length == 0) { continue; } string text3 = string.Empty; string language = string.Empty; string title = string.Empty; string[] array3 = text2.Split(new char[1] { '\n' }); foreach (string text4 in array3) { string text5 = text4.Trim(); if (text5.StartsWith("codec_type=", StringComparison.OrdinalIgnoreCase)) { text3 = text5.Substring("codec_type=".Length).Trim(); } else if (text5.StartsWith("TAG:language=", StringComparison.OrdinalIgnoreCase)) { string text6 = StripOuterBrackets(text5.Substring("TAG:language=".Length).Trim()); if (!string.IsNullOrWhiteSpace(text6) && !text6.Equals("N/A", StringComparison.OrdinalIgnoreCase)) { language = text6; } } else if (text5.StartsWith("TAG:title=", StringComparison.OrdinalIgnoreCase)) { string text7 = StripOuterBrackets(text5.Substring("TAG:title=".Length).Trim()); if (!string.IsNullOrWhiteSpace(text7) && !text7.Equals("N/A", StringComparison.OrdinalIgnoreCase)) { title = text7; } } } if (text3.Equals("subtitle", StringComparison.OrdinalIgnoreCase)) { result.Add(new SubtitleTrackInfo { StreamIndex = result.Count, Language = language, Title = title }); } } } public static List ProbeAudioTracks(string url, string ffmpegPath, ManualLogSource logger = null) { List list = new List(); string text = FindFfprobePath(ffmpegPath); if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(text)) { return list; } try { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "-v error -probesize 100M -analyzeduration 100M -select_streams a -show_entries stream=index,codec_type,codec_name:stream_tags=language,title -of default=noprint_wrappers=0 \"" + EscapeArg(url) + "\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using Process process = new Process { StartInfo = startInfo }; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(30000); ParseFfprobeAudioStreams(output, list); } catch (Exception ex) { if (logger != null) { logger.LogWarning((object)("[SubtitleManager] Audio probe error: " + ex.Message)); } } for (int i = 0; i < list.Count; i++) { list[i].AudioIndex = i; if (logger != null) { logger.LogInfo((object)("[SubtitleManager] Audio stream " + (i + 1) + ": streamIndex=" + list[i].StreamIndex + ", codec=" + (list[i].CodecName ?? string.Empty) + ", language=" + (list[i].Language ?? string.Empty) + ", title=" + (list[i].Title ?? string.Empty))); } } return list; } private static void ParseFfprobeAudioStreams(string output, List result) { if (string.IsNullOrWhiteSpace(output)) { return; } AudioTrackInfo audioTrackInfo = null; string[] array = output.Replace("\r\n", "\n").Split(new char[1] { '\n' }); foreach (string text in array) { 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, AudioIndex = 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, AudioIndex = result.Count }; } if (text2.StartsWith("codec_name=", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.CodecName = text2.Substring("codec_name=".Length).Trim(); } else if (text2.StartsWith("TAG:language=", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.Language = text2.Substring("TAG:language=".Length).Trim(); } else if (text2.StartsWith("TAG:title=", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.Title = text2.Substring("TAG:title=".Length).Trim(); } } if (audioTrackInfo != null && audioTrackInfo.StreamIndex >= 0) { result.Add(audioTrackInfo); } } public static string GetMkvAudioTrackCachedOutputPath(string sourceUrl, string playableUrl, int streamIndex) { string path = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; string text = Path.Combine(path, "cache"); try { Directory.CreateDirectory(text); } catch { } string text2 = (sourceUrl ?? string.Empty) + "|" + (playableUrl ?? string.Empty) + "|audio-vlc-v1|" + streamIndex.ToString(CultureInfo.InvariantCulture); uint num = 2166136261u; string text3 = text2; foreach (char c in text3) { num = (num ^ c) * 16777619; } return Path.Combine(text, $"mkv_audio_{num:x8}.mp4"); } public static void PrepareMkvAudioTrackFileAsync(string sourceUrl, string playableUrl, int streamIndex, string ffmpegPath, CancellationToken token, Action onComplete) { ThreadPool.QueueUserWorkItem(delegate { bool flag = false; string text = null; try { text = GetMkvAudioTrackCachedOutputPath(sourceUrl, playableUrl, streamIndex); if (File.Exists(text) && new FileInfo(text).Length > 0) { flag = true; } else { if (File.Exists(text)) { try { File.Delete(text); } catch { } } ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffmpegPath, Arguments = "-i \"" + EscapeArg(playableUrl) + "\" -i \"" + EscapeArg(sourceUrl) + "\" " + $"-map 0:v:0 -map 1:{streamIndex} -map_metadata 1 -map_chapters -1 -sn -dn " + "-c:v copy -c:a aac -ac 2 -ar 48000 -b:a 192k -af aresample=async=1:first_pts=0 -shortest -movflags +faststart -f mp4 -y \"" + EscapeArg(text) + "\" -hide_banner -loglevel error", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (Process process = new Process { StartInfo = startInfo }) { process.Start(); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate { }; while (!process.WaitForExit(500)) { if (token.IsCancellationRequested) { try { process.Kill(); return; } catch { return; } } } flag = process.ExitCode == 0 && File.Exists(text) && new FileInfo(text).Length > 0; } if (!flag) { try { if (File.Exists(text)) { File.Delete(text); } return; } catch { return; } } } } catch { flag = false; try { if (text != null && File.Exists(text)) { File.Delete(text); } } catch { } } finally { if (!token.IsCancellationRequested) { onComplete?.Invoke(flag, flag ? text : null); } } }); } 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; } public static string FindFfmpegPath() { string text = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; if (!string.IsNullOrEmpty(text)) { string[] array = new string[2] { "ffmpeg.exe", "ffmpeg" }; foreach (string path in array) { string text2 = Path.Combine(text, path); if (File.Exists(text2)) { return text2; } } } string text3 = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; string[] array2 = text3.Split(new char[1] { Path.PathSeparator }); foreach (string text4 in array2) { string[] array3 = new string[2] { "ffmpeg.exe", "ffmpeg" }; foreach (string path2 in array3) { try { string text5 = Path.Combine(text4.Trim(), path2); if (File.Exists(text5)) { return text5; } } catch { } } } return null; } public static string GetMkvCachedOutputPath(string inputUrl, bool transcode) { string path = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; string text = Path.Combine(path, "cache"); try { Directory.CreateDirectory(text); } catch { } uint num = 2166136261u; string text2 = inputUrl ?? string.Empty; foreach (char c in text2) { num = (num ^ c) * 16777619; } if (transcode) { num ^= 0xDEADBEEFu; } string arg = (transcode ? "tc_aacall_v2" : "rm_aacall_v2"); return Path.Combine(text, $"mkv_{num:x8}_{arg}.mp4"); } public static void ConvertMkvAutoAsync(string inputUrl, string ffmpegPath, bool userTranscode, CancellationToken token, Action onComplete) { ThreadPool.QueueUserWorkItem(delegate { bool flag = false; string text = null; try { if (!token.IsCancellationRequested) { bool flag2 = userTranscode; if (!flag2) { string a = ProbeVideoCodec(inputUrl, ffmpegPath, token); if (!token.IsCancellationRequested && (string.Equals(a, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "vp9", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "av1", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "mpeg4", StringComparison.OrdinalIgnoreCase))) { flag2 = true; } } if (!token.IsCancellationRequested) { string mkvCachedOutputPath = GetMkvCachedOutputPath(inputUrl, flag2); if (File.Exists(mkvCachedOutputPath) && new FileInfo(mkvCachedOutputPath).Length > 0) { flag = true; text = mkvCachedOutputPath; } else { if (File.Exists(mkvCachedOutputPath)) { try { File.Delete(mkvCachedOutputPath); } catch { } } string encodingArgs = (flag2 ? "-map 0:v:0 -map 0:a? -map_metadata 0 -map_chapters 0 -sn -dn -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -c:a aac -ac 2 -b:a 192k -movflags +faststart -f mp4" : "-map 0:v:0 -map 0:a? -map_metadata 0 -map_chapters 0 -sn -dn -c:v copy -c:a aac -ac 2 -ar 48000 -b:a 192k -movflags +faststart -f mp4"); flag = RunFfmpegConvert(ffmpegPath, inputUrl, mkvCachedOutputPath, encodingArgs, token); if (!flag || !File.Exists(mkvCachedOutputPath) || new FileInfo(mkvCachedOutputPath).Length <= 0) { flag = false; try { if (File.Exists(mkvCachedOutputPath)) { File.Delete(mkvCachedOutputPath); } return; } catch { return; } } text = mkvCachedOutputPath; } } } } catch { flag = false; try { if (text != null && File.Exists(text)) { File.Delete(text); } } catch { } } finally { if (!token.IsCancellationRequested) { onComplete?.Invoke(flag, text); } } }); } public static void ConvertMkvAsync(string inputUrl, string ffmpegPath, string outputPath, bool transcodeToH264, CancellationToken token, Action onComplete) { ThreadPool.QueueUserWorkItem(delegate { bool flag = false; string arg = null; try { if (File.Exists(outputPath)) { try { File.Delete(outputPath); } catch { } } string encodingArgs = (transcodeToH264 ? "-map 0:v:0 -map 0:a? -map_metadata 0 -map_chapters 0 -sn -dn -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -c:a aac -ac 2 -b:a 192k -movflags +faststart -f mp4" : "-map 0:v:0 -map 0:a? -map_metadata 0 -map_chapters 0 -sn -dn -c:v copy -c:a aac -ac 2 -ar 48000 -b:a 192k -movflags +faststart -f mp4"); flag = RunFfmpegConvert(ffmpegPath, inputUrl, outputPath, encodingArgs, token); if (flag && File.Exists(outputPath) && new FileInfo(outputPath).Length > 0) { arg = outputPath; } else { flag = false; try { if (File.Exists(outputPath)) { File.Delete(outputPath); } } catch { } } } catch { flag = false; try { if (File.Exists(outputPath)) { File.Delete(outputPath); } } catch { } } if (!token.IsCancellationRequested) { onComplete?.Invoke(flag, arg); } }); } private static string ProbeVideoCodec(string url, string ffmpegPath, CancellationToken token) { string text = FindFfprobePath(ffmpegPath); if (!string.IsNullOrWhiteSpace(text)) { try { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "-v error -probesize 10000000 -analyzeduration 10000000 -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 \"" + EscapeArg(url) + "\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using Process process = new Process { StartInfo = startInfo }; process.Start(); string text2 = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(10000); if (token.IsCancellationRequested) { return string.Empty; } if (!string.IsNullOrEmpty(text2)) { return text2; } } catch { } } try { ProcessStartInfo startInfo2 = new ProcessStartInfo { FileName = ffmpegPath, Arguments = "-probesize 10000000 -analyzeduration 10000000 -i \"" + EscapeArg(url) + "\" -hide_banner", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using Process process2 = new Process { StartInfo = startInfo2 }; process2.Start(); string input = process2.StandardError.ReadToEnd(); process2.WaitForExit(10000); if (token.IsCancellationRequested) { return string.Empty; } Match match = _videoCodecRx.Match(input); return match.Success ? match.Groups[1].Value.ToLowerInvariant() : string.Empty; } catch { return string.Empty; } } private static bool RunFfmpegConvert(string ffmpegPath, string inputUrl, string outputPath, string encodingArgs, CancellationToken token) { ProcessStartInfo processStartInfo = new ProcessStartInfo(); processStartInfo.FileName = ffmpegPath; processStartInfo.Arguments = "-i \"" + EscapeArg(inputUrl) + "\" " + encodingArgs + " -y \"" + EscapeArg(outputPath) + "\" -hide_banner -loglevel error"; processStartInfo.RedirectStandardOutput = true; processStartInfo.RedirectStandardError = true; processStartInfo.UseShellExecute = false; processStartInfo.CreateNoWindow = true; ProcessStartInfo startInfo = processStartInfo; try { using Process process = new Process { StartInfo = startInfo }; process.Start(); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate { }; while (!process.WaitForExit(500)) { if (token.IsCancellationRequested) { try { process.Kill(); } catch { } return false; } } return process.ExitCode == 0; } catch { return false; } } } public sealed class SyncVideoController : IDisposable { private readonly ManualLogSource _logger; private readonly VideoLobbyManager _lobbyManager; private readonly DirectUrlVideoBackend _backend; private readonly ConcurrentQueue _mainThreadActions = new ConcurrentQueue(); private int _lastAppliedRevision = -1; private int _lastAppliedSeekRevision; private bool _restartAfterPrepare; private bool _restartAutoPlay; private bool _autoplayAfterPrepare; private bool _lastRemotePlaying; private float _viewerCommandSyncingTimer; private int _viewerSeekDirection; private float _seekCooldownTimer; private double _lastSeekTarget = double.MinValue; private float _hostCommandCooldown; private string _lastLoadedLobbyUrl = string.Empty; private string _lastLoadedVideoId = string.Empty; private string _pendingLoadLobbyUrl = string.Empty; private string _pendingLoadVideoId = string.Empty; private bool _loadInProgress; private int _activeLoadRequestId; private CancellationTokenSource _mkvConversionCts; private int _lastAppliedAudioTrack = int.MinValue; private int _lastAppliedSubtitleTrack = int.MinValue; private bool _viewerLocalTrackOverride = false; public IVideoBackend Backend => _backend; public float LocalVolume => _backend.LocalVolume; public bool IsMuted => _backend.IsMuted; public bool IsViewerCommandSyncing => _viewerCommandSyncingTimer > 0f; public int ViewerSeekDirection => _viewerSeekDirection; public double VideoDurationSeconds => _backend.DurationSeconds; public event Action StateChanged; public SyncVideoController(ManualLogSource logger, VideoLobbyManager lobbyManager) { _logger = logger; _lobbyManager = lobbyManager; _backend = new DirectUrlVideoBackend(); _lobbyManager.ActiveStateChanged += OnActiveStateChanged; _backend.Prepared += OnPrepared; _backend.Ended += OnEnded; _backend.AudioTrackSwitchCompleted += OnAudioTrackSwitchCompleted; _backend.AudioTracksChanged += OnAudioTracksChanged; } public void Dispose() { _lobbyManager.ActiveStateChanged -= OnActiveStateChanged; _backend.Prepared -= OnPrepared; _backend.Ended -= OnEnded; _backend.AudioTrackSwitchCompleted -= OnAudioTrackSwitchCompleted; _backend.AudioTracksChanged -= OnAudioTracksChanged; _backend.Dispose(); } public bool IsCurrentMkv() { return _backend?.IsCurrentMkv ?? false; } public bool ShouldShowMkvSettings() { if (!(SyncVideoPlugin.Settings?.EnableMkvSupport?.Value).GetValueOrDefault()) { return false; } string empty = string.Empty; if (_loadInProgress && !string.IsNullOrWhiteSpace(_pendingLoadLobbyUrl)) { empty = _pendingLoadLobbyUrl; } else { empty = _lobbyManager.CurrentLobby?.CurrentUrl ?? string.Empty; if (string.IsNullOrWhiteSpace(empty)) { empty = _backend?.CurrentDirectUrl ?? string.Empty; } } return UrlNormalizer.IsMkvUrl(empty); } public bool ShouldShowFfmpegSyncingStatus() { return _backend?.ShouldShowFfmpegSyncingStatus ?? false; } public int GetMkvAudioTrackCount() { return _backend?.AudioTrackCount ?? 0; } public int GetMkvSelectedAudioTrack() { return _backend?.SelectedAudioTrack ?? 0; } public string GetMkvAudioTrackLabel(int trackIndex) { return _backend?.GetAudioTrackLabel(trackIndex) ?? ("Audio Track " + (trackIndex + 1)); } public bool SelectMkvAudioTrack(int trackIndex) { DirectUrlVideoBackend backend = _backend; if (backend == null) { return false; } bool flag; if (_lobbyManager.IsHost) { flag = backend.SelectAudioTrack(trackIndex); } else { VideoLobby currentLobby = _lobbyManager.CurrentLobby; double value = ((currentLobby != null) ? GetExpectedTime(currentLobby) : backend.CurrentTimeSeconds); bool value2 = currentLobby?.IsPlaying ?? false; flag = backend.SelectAudioTrack(trackIndex, value, value2); } if (flag && _lobbyManager.IsHost) { int subtitleTrack = _lobbyManager.CurrentLobby?.SelectedSubtitleTrack ?? (-1); _lobbyManager.SetMkvTrackSelection(trackIndex, subtitleTrack); } else if (flag && !_lobbyManager.IsHost) { _viewerLocalTrackOverride = true; } return flag; } public int GetMkvSubtitleTrackCount() { return _backend?.SubtitleTrackCount ?? 0; } public int GetMkvSelectedSubtitleTrack() { return _backend?.SelectedSubtitleTrack ?? (-1); } public string GetMkvSubtitleTrackLabel(int trackIndex) { return _backend?.GetSubtitleTrackLabel(trackIndex) ?? ("Subtitle Track " + (trackIndex + 1)); } public bool IsMkvSubtitleProbing() { return _backend?.IsSubtitleProbing ?? false; } public bool IsMkvSubtitleExtracting() { return _backend?.IsSubtitleExtracting ?? false; } public bool IsMkvAudioSwitching() { return _backend?.IsAudioSwitching ?? false; } public bool IsMkvAudioProbing() { return _backend?.IsAudioProbing ?? false; } public void SelectMkvSubtitleTrack(int trackIndex, Action onComplete) { DirectUrlVideoBackend backend = _backend; if (backend == null) { onComplete?.Invoke(); } else if (trackIndex < 0) { backend.DisableSubtitles(); if (_lobbyManager.IsHost) { int audioTrack = _lobbyManager.CurrentLobby?.SelectedAudioTrack ?? 0; _lobbyManager.SetMkvTrackSelection(audioTrack, -1); } else { _viewerLocalTrackOverride = true; } onComplete?.Invoke(); } else { if (_lobbyManager.IsHost) { int audioTrack2 = _lobbyManager.CurrentLobby?.SelectedAudioTrack ?? 0; _lobbyManager.SetMkvTrackSelection(audioTrack2, trackIndex); } else { _viewerLocalTrackOverride = true; } backend.SelectSubtitleTrack(trackIndex, onComplete); } } public string GetCurrentSubtitleText() { DirectUrlVideoBackend backend = _backend; return backend?.GetCurrentSubtitleText(backend.CurrentTimeSeconds); } public void Tick(float deltaTime) { Action result; while (_mainThreadActions.TryDequeue(out result)) { result?.Invoke(); } _backend.Tick(deltaTime); if (_viewerCommandSyncingTimer > 0f) { _viewerCommandSyncingTimer = Math.Max(0f, _viewerCommandSyncingTimer - deltaTime); } if (_seekCooldownTimer > 0f) { _seekCooldownTimer = Math.Max(0f, _seekCooldownTimer - deltaTime); } if (_hostCommandCooldown > 0f) { _hostCommandCooldown = Math.Max(0f, _hostCommandCooldown - deltaTime); } VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby == null) { _viewerCommandSyncingTimer = 0f; return; } if (_lobbyManager.IsHost) { _viewerCommandSyncingTimer = 0f; if (currentLobby.IsPlaying) { _lobbyManager.SetObservedPlaybackTime(_backend.CurrentTimeSeconds); } _lastRemotePlaying = currentLobby.IsPlaying; return; } bool flag = currentLobby.IsPlaying && !_lastRemotePlaying; bool flag2 = !currentLobby.IsPlaying && _lastRemotePlaying; _lastRemotePlaying = currentLobby.IsPlaying; if (_loadInProgress || !_backend.IsPrepared) { return; } if (flag) { double num = GetExpectedTime(currentLobby); if (num <= 0.5) { num = 0.0; } bool flag3 = Math.Abs(num - _lastSeekTarget) > 5.0; if (_seekCooldownTimer <= 0f || flag3) { _seekCooldownTimer = 5f; _lastSeekTarget = num; _backend.Seek(num); } if (!_backend.IsPlaying) { _backend.Play(); } return; } if (flag2) { _viewerCommandSyncingTimer = 0f; if (_backend.IsPlaying) { _backend.Pause(); } } else if (currentLobby.IsPlaying && !_backend.IsPlaying) { _backend.Play(); } double expectedTime = GetExpectedTime(currentLobby); double currentTimeSeconds = _backend.CurrentTimeSeconds; double value = expectedTime - currentTimeSeconds; double num2 = Math.Abs(value); if (!currentLobby.IsPlaying) { _viewerCommandSyncingTimer = 0f; if (num2 >= 0.03 && (num2 < 1.0 || _seekCooldownTimer <= 0f)) { if (num2 >= 1.0) { _seekCooldownTimer = 3f; _lastSeekTarget = expectedTime; } _backend.Seek(expectedTime); } _backend.NudgeToward(currentTimeSeconds, 0.0); return; } float num3 = Time.unscaledTime - currentLobby.LastSeenSeconds; if (num3 > 4f) { return; } if (num2 >= Math.Max(1.5, SyncVideoPlugin.Settings.HardSeekThresholdSeconds.Value)) { bool flag4 = Math.Abs(expectedTime - _lastSeekTarget) > 5.0; if (_seekCooldownTimer > 0f && !flag4) { _viewerCommandSyncingTimer = ((num2 > 0.75) ? 0.75f : 0f); return; } _viewerCommandSyncingTimer = ((num2 > 0.75) ? 0.75f : 0f); _seekCooldownTimer = 5f; _lastSeekTarget = expectedTime; _backend.Seek(expectedTime); } else if (num2 > (double)SyncVideoPlugin.Settings.DriftToleranceSeconds.Value) { _viewerCommandSyncingTimer = ((num2 > 0.5) ? 0.35f : 0f); _backend.NudgeToward(expectedTime, num2); } else if (num2 <= 0.03) { _viewerCommandSyncingTimer = 0f; _viewerSeekDirection = 0; _backend.NudgeToward(currentTimeSeconds, 0.0); } else { _backend.NudgeToward(currentTimeSeconds, 0.0); } } public void HostSetUrl(string rawInput) { if (_lobbyManager.IsHost) { string videoId; string directPlayableUrl; string text = UrlNormalizer.Normalize(rawInput, out videoId, out directPlayableUrl); if (UrlNormalizer.ValidateSubmissionUrl(text, videoId, directPlayableUrl, out var _)) { CancelPendingLoadAndPlayback(); _autoplayAfterPrepare = SyncVideoPlugin.Settings.HostAutoplay.Value; _lobbyManager.SetVideo(text, videoId); LoadUrlWithResolution(text, videoId, directPlayableUrl); SyncVideoPlugin.ScreenManager?.OnVideoChanged(); } } } public void HostPlay() { if (_lobbyManager.IsHost && !(_hostCommandCooldown > 0f)) { _hostCommandCooldown = 0.5f; VideoLobby currentLobby = _lobbyManager.CurrentLobby; double num = _backend.CurrentTimeSeconds; if (currentLobby != null && !currentLobby.IsPlaying && (currentLobby.HasEnded || num <= 0.05)) { num = 0.0; } _backend.Seek(num); _backend.Play(); _lobbyManager.SetObservedPlaybackTime(num); _lobbyManager.SetPlayback(playing: true); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: true, num); } } public void HostPause() { if (_lobbyManager.IsHost && !(_hostCommandCooldown > 0f)) { _hostCommandCooldown = 0.5f; _backend.Pause(); _lobbyManager.SetPlayback(playing: false); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: false, _backend.CurrentTimeSeconds); } } public void HostSeekRelative(double seconds) { if (_lobbyManager.IsHost && !(_hostCommandCooldown > 0f)) { _hostCommandCooldown = 0.5f; VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby == null || !currentLobby.HasEnded) { double seconds2 = Math.Max(0.0, _backend.CurrentTimeSeconds + seconds); _backend.Seek(seconds2); _lobbyManager.SeekRelative(seconds); bool playing = _lobbyManager.CurrentLobby != null && _lobbyManager.CurrentLobby.IsPlaying; SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing, _backend.CurrentTimeSeconds); } } } public void HostSeekToTime(double seconds) { if (_lobbyManager.IsHost && !(_hostCommandCooldown > 0f)) { _hostCommandCooldown = 0.5f; VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby == null || !currentLobby.HasEnded) { _backend.Seek(seconds); _lobbyManager.SeekToAbsolute(seconds); bool playing = _lobbyManager.CurrentLobby != null && _lobbyManager.CurrentLobby.IsPlaying; SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing, seconds); } } } public void HostRestart() { if (!_lobbyManager.IsHost) { return; } VideoLobby currentLobby = _lobbyManager.CurrentLobby; bool flag = currentLobby?.HasEnded ?? false; bool flag2 = (currentLobby?.IsPlaying ?? false) || flag; if (!_backend.IsPrepared) { if (!string.IsNullOrWhiteSpace(_backend.CurrentDirectUrl)) { _restartAfterPrepare = true; _restartAutoPlay = flag2; _backend.ReloadCurrent(); } return; } _backend.Seek(0.0); _lobbyManager.RestartFromBeginning(flag2); if (flag2) { _backend.Play(); } else { _backend.Pause(); } SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(flag2, 0.0); this.StateChanged?.Invoke(MakeState(_lobbyManager.CurrentLobby)); } public void EnqueueMainThreadAction(Action action) { if (action != null) { _mainThreadActions.Enqueue(action); } } public void AdjustLocalVolume(float delta) { _backend.AdjustVolume(delta); } public void ToggleMute() { _backend.ToggleMute(); } public void StopForMissingScreen() { if (_backend.IsPrepared || _backend.IsPlaying) { double currentTimeSeconds = _backend.CurrentTimeSeconds; _backend.Stop(); if (_lobbyManager.IsHost && _lobbyManager.CurrentLobby != null) { _lobbyManager.SetObservedPlaybackTime(currentTimeSeconds); _lobbyManager.SetPlayback(playing: false); } SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: false, currentTimeSeconds); } } private void SeekAndSetDirection(double target) { _viewerSeekDirection = ((target > _backend.CurrentTimeSeconds) ? 1 : (-1)); _backend.Seek(target); } private int CancelPendingLoadAndPlayback() { _activeLoadRequestId++; _mkvConversionCts?.Cancel(); _mkvConversionCts = null; _restartAfterPrepare = false; _restartAutoPlay = false; _autoplayAfterPrepare = false; _loadInProgress = false; _pendingLoadLobbyUrl = string.Empty; _pendingLoadVideoId = string.Empty; _seekCooldownTimer = 0f; _lastSeekTarget = double.MinValue; _hostCommandCooldown = 0f; _lastAppliedAudioTrack = int.MinValue; _lastAppliedSubtitleTrack = int.MinValue; _viewerLocalTrackOverride = false; YouTube.CancelAllPendingRequests(); _backend.Stop(); return _activeLoadRequestId; } private void LoadUrlWithResolution(string originalUrl, string videoId, string directPlayableUrl = null) { int requestId = _activeLoadRequestId; _loadInProgress = true; _pendingLoadLobbyUrl = originalUrl ?? string.Empty; _pendingLoadVideoId = videoId ?? string.Empty; if (!string.IsNullOrEmpty(videoId) && string.IsNullOrWhiteSpace(directPlayableUrl)) { if (YouTube.IsFfmpegAvailable()) { _backend.SetDownloadingStatus(); } else { _backend.SetResolvingStatus(); } YouTube.ClearAllCache(); YouTube.ResolveAsync(videoId, originalUrl, delegate(string resolvedUrl) { _mainThreadActions.Enqueue(delegate { if (requestId == _activeLoadRequestId) { _backend.Load(resolvedUrl, originalUrl, videoId); } }); }, delegate(string errorMessage) { _mainThreadActions.Enqueue(delegate { if (requestId == _activeLoadRequestId) { _loadInProgress = false; _pendingLoadLobbyUrl = string.Empty; _pendingLoadVideoId = string.Empty; _backend.SetErrorStatus(errorMessage); } }); }); } else if (IsMkvConversionEnabled(directPlayableUrl ?? originalUrl)) { bool valueOrDefault = (SyncVideoPlugin.Settings?.MkvTranscodeToH264?.Value).GetValueOrDefault(); string inputUrl = directPlayableUrl ?? originalUrl; string ffmpegPath = SubtitleManager.FindFfmpegPath(); _backend.SetConvertingStatus(); string mkvCachedOutputPath = SubtitleManager.GetMkvCachedOutputPath(inputUrl, transcode: false); string mkvCachedOutputPath2 = SubtitleManager.GetMkvCachedOutputPath(inputUrl, transcode: true); string text = (File.Exists(mkvCachedOutputPath2) ? mkvCachedOutputPath2 : (File.Exists(mkvCachedOutputPath) ? mkvCachedOutputPath : null)); if (text != null) { if (requestId == _activeLoadRequestId) { _backend.Load(MakeFileUrl(text), originalUrl, videoId ?? string.Empty); } return; } _mkvConversionCts = new CancellationTokenSource(); CancellationToken token = _mkvConversionCts.Token; SubtitleManager.ConvertMkvAutoAsync(inputUrl, ffmpegPath, valueOrDefault, token, delegate(bool success, string resultPath) { _mainThreadActions.Enqueue(delegate { if (requestId == _activeLoadRequestId) { _loadInProgress = false; _pendingLoadLobbyUrl = string.Empty; _pendingLoadVideoId = string.Empty; if (success && !string.IsNullOrEmpty(resultPath)) { _backend.Load(MakeFileUrl(resultPath), originalUrl, videoId ?? string.Empty); } else { _backend.SetErrorStatus("MKV conversion failed."); } } }); }); } else if (requestId == _activeLoadRequestId) { _backend.Load(directPlayableUrl ?? originalUrl, originalUrl, videoId ?? string.Empty); } } private void OnActiveStateChanged(VideoLobby lobby) { if (lobby == null) { CancelPendingLoadAndPlayback(); _lastAppliedRevision = -1; _lastAppliedSeekRevision = 0; _lastRemotePlaying = false; _viewerCommandSyncingTimer = 0f; _seekCooldownTimer = 0f; _lastSeekTarget = double.MinValue; _viewerSeekDirection = 0; _lastLoadedLobbyUrl = string.Empty; _lastLoadedVideoId = string.Empty; YouTube.ClearAllCache(); this.StateChanged?.Invoke(null); } else if (_lobbyManager.IsHost) { this.StateChanged?.Invoke(MakeState(lobby)); } else { if (lobby.Revision == _lastAppliedRevision) { return; } _lastAppliedRevision = lobby.Revision; double expectedTime = GetExpectedTime(lobby); string text = lobby.CurrentUrl ?? string.Empty; string text2 = lobby.CurrentVideoId ?? string.Empty; bool flag = !string.IsNullOrWhiteSpace(text); bool flag2 = string.Equals(_lastLoadedLobbyUrl, text, StringComparison.Ordinal) && string.Equals(_lastLoadedVideoId, text2, StringComparison.Ordinal); bool flag3 = _loadInProgress && string.Equals(_pendingLoadLobbyUrl, text, StringComparison.Ordinal) && string.Equals(_pendingLoadVideoId, text2, StringComparison.Ordinal); bool flag4 = flag && !_backend.IsPrepared && !_backend.IsPlaying && !lobby.HasEnded && !flag3 && !_backend.IsAudioSwitching; if (flag && (!flag2 || flag4) && !flag3) { _lastLoadedLobbyUrl = text; _lastLoadedVideoId = text2; _lastAppliedSeekRevision = lobby.SeekRevision; _viewerSeekDirection = 0; _viewerCommandSyncingTimer = 0f; CancelPendingLoadAndPlayback(); LoadUrlWithResolution(text, text2); _lastRemotePlaying = lobby.IsPlaying; SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(lobby.IsPlaying, lobby.IsPlaying ? expectedTime : expectedTime); this.StateChanged?.Invoke(MakeState(lobby)); return; } bool flag5 = lobby.SeekRevision > _lastAppliedSeekRevision; if (lobby.HasEnded && !lobby.IsPlaying) { _viewerCommandSyncingTimer = 0f; _backend.ShowEndedState(expectedTime); } else if (!lobby.IsPlaying) { _viewerCommandSyncingTimer = 0f; if (_backend.IsPlaying) { _backend.Pause(); } if (Math.Abs(_backend.CurrentTimeSeconds - expectedTime) >= 0.03) { bool flag6 = Math.Abs(expectedTime - _lastSeekTarget) > 5.0; if (_seekCooldownTimer <= 0f || flag6) { _seekCooldownTimer = 3f; _lastSeekTarget = expectedTime; if (flag5) { SeekAndSetDirection(expectedTime); } else { _backend.Seek(expectedTime); } } } } else { double num = Math.Abs(_backend.CurrentTimeSeconds - expectedTime); if (!_backend.IsPlaying) { _backend.Play(); } if (num >= 1.5) { bool flag7 = Math.Abs(expectedTime - _lastSeekTarget) > 5.0; if (_seekCooldownTimer <= 0f || flag7) { _seekCooldownTimer = 5f; _lastSeekTarget = expectedTime; if (flag5) { SeekAndSetDirection(expectedTime); } else { _backend.Seek(expectedTime); } } _viewerCommandSyncingTimer = ((num >= 0.75) ? 0.6f : 0f); } else if (!_backend.IsPlaying && expectedTime <= 0.5) { _backend.Seek(0.0); _viewerCommandSyncingTimer = 0f; } else if (num > 0.2) { _backend.NudgeToward(expectedTime, num); _viewerCommandSyncingTimer = ((num >= 0.5) ? 0.35f : 0f); } else { _viewerCommandSyncingTimer = 0f; } } if (flag5) { _lastAppliedSeekRevision = lobby.SeekRevision; } _lastRemotePlaying = lobby.IsPlaying; SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(lobby.IsPlaying, lobby.IsPlaying ? expectedTime : expectedTime); ApplyLobbyTrackSelection(lobby); this.StateChanged?.Invoke(MakeState(lobby)); } } private void OnPrepared() { _loadInProgress = false; _pendingLoadLobbyUrl = string.Empty; _pendingLoadVideoId = string.Empty; VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby != null) { _lastLoadedLobbyUrl = currentLobby.CurrentUrl ?? string.Empty; _lastLoadedVideoId = currentLobby.CurrentVideoId ?? string.Empty; } if (_restartAfterPrepare && _lobbyManager.IsHost) { _restartAfterPrepare = false; _backend.Seek(0.0); _lobbyManager.RestartFromBeginning(_restartAutoPlay); if (_restartAutoPlay) { _backend.Play(); } else { _backend.Pause(); } SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(_restartAutoPlay, 0.0); this.StateChanged?.Invoke(MakeState(_lobbyManager.CurrentLobby)); return; } if (_autoplayAfterPrepare && _lobbyManager.IsHost) { _autoplayAfterPrepare = false; _backend.Seek(0.0); _backend.Play(); _lobbyManager.RestartFromBeginning(playing: true); SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: true, 0.0); this.StateChanged?.Invoke(MakeState(_lobbyManager.CurrentLobby)); return; } VideoLobby currentLobby2 = _lobbyManager.CurrentLobby; if (currentLobby2 == null) { return; } double expectedTime = GetExpectedTime(currentLobby2); _lastAppliedSeekRevision = currentLobby2.SeekRevision; _viewerSeekDirection = 0; if (currentLobby2.HasEnded && !currentLobby2.IsPlaying) { _backend.ShowEndedState(expectedTime); } else { double num = (currentLobby2.IsPlaying ? GetExpectedTime(currentLobby2) : expectedTime); if (num < 0.5) { num = 0.0; } if (num > 0.0) { _seekCooldownTimer = 5f; _lastSeekTarget = num; } _backend.Seek(num); if (currentLobby2.IsPlaying) { currentLobby2.MediaTimeSeconds = num; currentLobby2.LastSeenSeconds = Time.unscaledTime; _backend.Play(); } else { _backend.Pause(); } } _lastRemotePlaying = currentLobby2.IsPlaying; SyncVideoPlugin.ScreenManager?.OnVideoChanged(); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(currentLobby2.IsPlaying, expectedTime); ApplyLobbyTrackSelection(currentLobby2); } private void OnAudioTracksChanged() { this.StateChanged?.Invoke(MakeState(_lobbyManager.CurrentLobby)); } private void OnAudioTrackSwitchCompleted() { if (!_lobbyManager.IsHost) { ForceImmediateViewerResync(); } } private void ForceImmediateViewerResync() { VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby == null) { return; } double expectedTime = GetExpectedTime(currentLobby); double currentTimeSeconds = _backend.CurrentTimeSeconds; double value = expectedTime - currentTimeSeconds; double num = Math.Abs(value); if (currentLobby.IsPlaying) { if (!_backend.IsPlaying) { _backend.Play(); } if (num >= (double)SyncVideoPlugin.Settings.HardSeekThresholdSeconds.Value) { _backend.Seek(expectedTime); } _viewerCommandSyncingTimer = ((num >= 0.25) ? 0.35f : 0f); _backend.NudgeToward(expectedTime, num); } else { if (_backend.IsPlaying) { _backend.Pause(); } _backend.Seek(expectedTime); _backend.NudgeToward(currentTimeSeconds, 0.0); _viewerCommandSyncingTimer = 0f; } SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(currentLobby.IsPlaying, expectedTime); } private void OnEnded() { VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby == null) { return; } if (_lobbyManager.IsHost) { _lobbyManager.NotifyPlaybackEnded(_backend.CurrentTimeSeconds); SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: false, _backend.CurrentTimeSeconds); this.StateChanged?.Invoke(MakeState(_lobbyManager.CurrentLobby)); if (_lobbyManager.PlaylistModeEnabled && _lobbyManager.TryDequeueNextSuggestion(out var _, out var suggestion)) { HostSetUrl(suggestion.Url); } } else { SyncVideoPlugin.ScreenManager?.OnPlaybackStateChanged(playing: false, _backend.CurrentTimeSeconds); } } private double GetExpectedTime(VideoLobby lobby) { if (lobby == null) { return 0.0; } double num = Math.Max(0.0, lobby.MediaTimeSeconds); if (!lobby.IsPlaying || lobby.LastSeenSeconds <= 0f) { return num; } float num2 = Time.unscaledTime - lobby.LastSeenSeconds; if (num2 <= 0f || num2 > 30f) { return num; } return num + (double)num2; } private static bool IsMkvConversionEnabled(string url) { if (!UrlNormalizer.IsMkvUrl(url)) { return false; } return (SyncVideoPlugin.Settings?.EnableMkvFfmpegConversion?.Value).GetValueOrDefault(); } private static string MakeFileUrl(string localPath) { return "file:///" + (localPath ?? string.Empty).Replace('\\', '/'); } internal void ReapplyLobbyTrackSelection() { if (!_lobbyManager.IsHost && !_viewerLocalTrackOverride) { _lastAppliedAudioTrack = int.MinValue; _lastAppliedSubtitleTrack = int.MinValue; VideoLobby currentLobby = _lobbyManager.CurrentLobby; if (currentLobby != null) { ApplyLobbyTrackSelection(currentLobby); } } } private void ApplyLobbyTrackSelection(VideoLobby lobby) { if (_lobbyManager.IsHost || lobby == null || !_backend.IsPrepared || _viewerLocalTrackOverride) { return; } DirectUrlVideoBackend backend = _backend; if (backend == null) { return; } int selectedAudioTrack = lobby.SelectedAudioTrack; int selectedSubtitleTrack = lobby.SelectedSubtitleTrack; if (selectedAudioTrack != _lastAppliedAudioTrack) { _lastAppliedAudioTrack = selectedAudioTrack; backend.SelectAudioTrack(selectedAudioTrack); } if (selectedSubtitleTrack != _lastAppliedSubtitleTrack) { if (selectedSubtitleTrack < 0) { _lastAppliedSubtitleTrack = selectedSubtitleTrack; backend.DisableSubtitles(); } else if (SubtitleManager.FindFfmpegPath() != null && !backend.IsSubtitleProbing) { _lastAppliedSubtitleTrack = selectedSubtitleTrack; backend.SelectSubtitleTrack(selectedSubtitleTrack, null); } } } private VideoSyncState MakeState(VideoLobby lobby) { if (lobby == null) { return null; } return new VideoSyncState { Url = lobby.CurrentUrl, VideoId = lobby.CurrentVideoId, IsPlaying = lobby.IsPlaying, MediaTimeSeconds = lobby.MediaTimeSeconds, HostUnixMilliseconds = lobby.HostUnixMilliseconds, Revision = lobby.Revision, HasEnded = lobby.HasEnded }; } } public static class UrlNormalizer { private static readonly Regex YoutubeWatch = new Regex("(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([\\w\\-]{6,})", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex YoutubeShorts = new Regex("youtube\\.com\\/shorts\\/([\\w\\-]{6,})", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly string[] AllowedDirectMediaExtensions = new string[6] { ".mp4", ".webm", ".m4v", ".mov", ".avi", ".mkv" }; public const int MaxDirectSuggestionUrlLength = 180; public static bool IsMkvUrl(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } try { if (Uri.TryCreate(value, UriKind.Absolute, out Uri result)) { return (result.AbsolutePath ?? string.Empty).EndsWith(".mkv", StringComparison.OrdinalIgnoreCase); } } catch { } return value.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase); } public static bool IsMkvSubmissionAllowed(string normalizedUrl, string videoId, string directPlayableUrl, out string error) { error = string.Empty; if (!string.IsNullOrWhiteSpace(videoId)) { return true; } string value = ((!string.IsNullOrWhiteSpace(directPlayableUrl)) ? directPlayableUrl : normalizedUrl); if (!IsMkvUrl(value)) { return true; } if ((SyncVideoPlugin.Settings?.EnableMkvSupport?.Value).GetValueOrDefault()) { return true; } error = "MKV Not Supported\n\nPlease enable experimental features in config."; return false; } public static bool IsDirectSuggestionUrlTooLong(string normalizedUrl, string videoId, string directPlayableUrl, out string error) { error = string.Empty; if (!string.IsNullOrWhiteSpace(videoId)) { return false; } string text = ((!string.IsNullOrWhiteSpace(directPlayableUrl)) ? directPlayableUrl : normalizedUrl); if (string.IsNullOrWhiteSpace(text)) { return false; } if (text.Length > 180) { error = "MP4 URL length too long!"; return true; } return false; } public static bool ValidateSubmissionUrl(string normalizedUrl, string videoId, string directPlayableUrl, out string error) { error = string.Empty; string text = ((!string.IsNullOrWhiteSpace(directPlayableUrl)) ? directPlayableUrl : normalizedUrl); if (!IsMkvSubmissionAllowed(normalizedUrl, videoId, directPlayableUrl, out error)) { return false; } if (!Uri.TryCreate(text ?? string.Empty, UriKind.Absolute, out Uri result)) { error = "URL Error!"; return false; } if (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps) { error = "Only http/https URLs are supported."; return false; } if (IsBlockedHost(result.Host)) { error = "Local or private network URLs are not allowed."; return false; } if (!string.IsNullOrWhiteSpace(videoId)) { return true; } string text2 = (result.AbsolutePath ?? string.Empty).ToLowerInvariant(); for (int i = 0; i < AllowedDirectMediaExtensions.Length; i++) { if (text2.EndsWith(AllowedDirectMediaExtensions[i], StringComparison.Ordinal)) { return true; } } error = "Direct links must point to a supported video file."; return false; } private static bool IsBlockedHost(string host) { if (string.IsNullOrWhiteSpace(host)) { return true; } if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase)) { return true; } if (!IPAddress.TryParse(host, out IPAddress address)) { return false; } if (IPAddress.IsLoopback(address)) { return true; } byte[] addressBytes = address.GetAddressBytes(); if (address.AddressFamily == AddressFamily.InterNetwork && addressBytes.Length == 4) { if (addressBytes[0] == 10 || addressBytes[0] == 127) { return true; } if (addressBytes[0] == 169 && addressBytes[1] == 254) { return true; } if (addressBytes[0] == 192 && addressBytes[1] == 168) { return true; } if (addressBytes[0] == 172 && addressBytes[1] >= 16 && addressBytes[1] <= 31) { return true; } } if (address.AddressFamily == AddressFamily.InterNetworkV6) { if (address.IsIPv6LinkLocal || address.IsIPv6SiteLocal) { return true; } if (addressBytes.Length == 16 && (addressBytes[0] & 0xFE) == 252) { return true; } } return false; } public static string Normalize(string raw, out string videoId, out string directPlayableUrl) { raw = (raw ?? string.Empty).Trim(); videoId = string.Empty; directPlayableUrl = raw; if (string.IsNullOrEmpty(raw)) { return raw; } Match match = YoutubeShorts.Match(raw); if (match.Success) { videoId = match.Groups[1].Value; directPlayableUrl = string.Empty; return "https://www.youtube.com/watch?v=" + videoId; } Match match2 = YoutubeWatch.Match(raw); if (match2.Success) { videoId = match2.Groups[1].Value; directPlayableUrl = string.Empty; return "https://www.youtube.com/watch?v=" + videoId; } return raw; } } public sealed class VideoLobbyManager : IDisposable { private sealed class SyncShadow { public bool IsSyncVideoLobby; public string Url = string.Empty; public string VideoId = string.Empty; public bool IsPlaying; public double MediaTimeSeconds; public long HostUnixMilliseconds; public int Revision; public int SeekRevision; public bool HasEnded; public bool IsOpen = true; public bool SuggestionsOpen; public int SelectedAudioTrack = 0; public int SelectedSubtitleTrack = -1; public SyncShadow Clone() { return (SyncShadow)MemberwiseClone(); } } public struct VideoSuggestion { public string Url; public string Title; public string ChannelName; public string GetButtonLabel() { string text = (string.IsNullOrWhiteSpace(Title) ? Url : Title); if (text.Length > 42) { text = text.Substring(0, 40) + "..."; } return "" + text + ""; } } private const int SyncMagic = 827742547; private const byte SyncVersion = 5; private const string OfflineLobbyId = "offline"; private static readonly Regex _sanitizeSpriteRx = new Regex("]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex _sanitizeTagRx = new Regex("<[^>]+>", RegexOptions.Compiled); private readonly ManualLogSource _logger; private readonly SyncVideoTransport _transport; private readonly List _visibleLobbies = new List(); private readonly Dictionary _pendingStatePackets = new Dictionary(StringComparer.Ordinal); private SyncShadow _hostShadow = new SyncShadow { IsSyncVideoLobby = true }; private float _stateTimer; private float _pushCooldownTimer; private string _lastCurrentLobbyId; private bool _offlineLobbyActive; private bool _leaveInProgress; private string _requestedStateLobbyId; private bool _receivedFreshStateForCurrentLobby; private float _stateRequestRetryTimer; private bool _syncVideoHostActive; private readonly Dictionary _refreshPreviousById = new Dictionary(StringComparer.Ordinal); private readonly List _refreshLobbyKeys = new List(); private readonly Dictionary _suggestions = new Dictionary(StringComparer.Ordinal); private readonly HashSet _hostReceivedSuggestionKeys = new HashSet(StringComparer.Ordinal); private bool _suggestionsOpen; private readonly HashSet _pendingSuggestionMetadataLookups = new HashSet(StringComparer.Ordinal); private readonly List _suggestionOrder = new List(); private bool _playlistModeEnabled; internal bool HasPendingSuggestion => false; internal string PendingSuggestionUrl => string.Empty; internal string PendingSuggestionTitle => string.Empty; internal string PendingSuggestionPlayerName => string.Empty; public VideoLobby HostedLobby => IsHost ? CurrentLobby : null; public VideoLobby JoinedLobby => (!IsHost) ? CurrentLobby : null; public IReadOnlyCollection Lobbies => _visibleLobbies; public VideoLobby CurrentLobby { get; private set; } private ClientLobbyManager NativeLobbyManager => ((Object)(object)ClientController.Instance != (Object)null) ? ClientController.Instance.ClientLobbyManager : null; public bool InLobby => CurrentLobby != null; public bool IsHost => CurrentLobby != null && (_offlineLobbyActive || CurrentLobby.HostId == _transport.LocalPlayerId); public bool OfflineModeEnabled => SyncVideoPlugin.Settings.EnableOfflineMode.Value; public bool IsOfflineLobby => _offlineLobbyActive; public bool LeaveInProgress => _leaveInProgress; public bool CanAcceptNewMembers => CurrentLobby != null && CurrentLobby.IsOpen; public bool IsLobbyOpen => (CurrentLobby != null) ? CurrentLobby.IsOpen : _hostShadow.IsOpen; public bool SuggestionsOpen => _suggestionsOpen; public bool PlaylistModeEnabled => _playlistModeEnabled; public event Action LobbiesChanged; public event Action ActiveLobbyChanged; public event Action ActiveStateChanged; public event Action SuggestionsChanged; public void SetLobbyOpen(bool isOpen) { if (IsHost && CurrentLobby != null) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.IsOpen = isOpen; _hostShadow.Revision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.IsOpen = isOpen; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: true, notifyActiveStateChanged: true); return; } PushHostShadowToNativeLobby(); BroadcastStatePacket(); this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); } } public void ToggleLobbyOpen() { SetLobbyOpen(!IsLobbyOpen); } public Dictionary GetSuggestions() { return new Dictionary(_suggestions, StringComparer.Ordinal); } public List> GetOrderedSuggestions() { List> list = new List>(_suggestions.Count); for (int i = 0; i < _suggestionOrder.Count; i++) { string key = _suggestionOrder[i]; if (_suggestions.TryGetValue(key, out var value)) { list.Add(new KeyValuePair(key, value)); } } if (list.Count < _suggestions.Count) { HashSet hashSet = new HashSet(_suggestionOrder, StringComparer.Ordinal); foreach (KeyValuePair suggestion in _suggestions) { if (!hashSet.Contains(suggestion.Key)) { list.Add(suggestion); } } } return list; } public void TogglePlaylistMode() { _playlistModeEnabled = !_playlistModeEnabled; this.SuggestionsChanged?.Invoke(); } public bool QueueHostUrl(string rawInput, string title = null) { if (!IsHost) { return false; } string videoId; string directPlayableUrl; string text = UrlNormalizer.Normalize(rawInput, out videoId, out directPlayableUrl); if (UrlNormalizer.IsDirectSuggestionUrlTooLong(text, videoId, directPlayableUrl, out var error) || !UrlNormalizer.ValidateSubmissionUrl(text, videoId, directPlayableUrl, out error)) { return false; } AddOrUpdateSuggestion(_transport.LocalPlayerId, string.Empty, text, string.IsNullOrWhiteSpace(title) ? text : title, "Host queue"); return true; } public bool TryDequeueNextSuggestion(out string suggestionEntryKey, out VideoSuggestion suggestion) { for (int i = 0; i < _suggestionOrder.Count; i++) { string text = _suggestionOrder[i]; if (_suggestions.TryGetValue(text, out suggestion)) { suggestionEntryKey = text; _suggestions.Remove(text); _suggestionOrder.RemoveAt(i); this.SuggestionsChanged?.Invoke(); return true; } } suggestionEntryKey = string.Empty; suggestion = default(VideoSuggestion); return false; } private static string BuildStoredSuggestionEntryKey(ushort senderPlayerId, string url) { return senderPlayerId + "|" + BuildSuggestionKey(url); } private static string BuildSuggestionKey(string url) { string videoId; string directPlayableUrl; string text = UrlNormalizer.Normalize(url ?? string.Empty, out videoId, out directPlayableUrl); if (!string.IsNullOrWhiteSpace(videoId)) { return "Y:" + videoId; } return "U:" + (text ?? string.Empty); } private static bool HasResolvedSuggestionMetadata(VideoSuggestion suggestion) { if (!string.IsNullOrWhiteSpace(suggestion.ChannelName)) { return true; } if (string.IsNullOrWhiteSpace(suggestion.Title) || string.IsNullOrWhiteSpace(suggestion.Url)) { return false; } if (string.Equals(suggestion.Title, "Loading suggestion...", StringComparison.Ordinal)) { return false; } return !string.Equals(suggestion.Title, suggestion.Url, StringComparison.Ordinal); } private static bool IsFallbackSuggestionTitle(string title, string normalizedUrl, string videoId) { return string.IsNullOrWhiteSpace(title) || string.Equals(title, normalizedUrl, StringComparison.Ordinal) || (!string.IsNullOrWhiteSpace(videoId) && string.Equals(title, videoId, StringComparison.Ordinal)); } private static string GetPendingSuggestionTitle(string normalizedUrl, string videoId, string incomingTitle) { if (!string.IsNullOrWhiteSpace(videoId) && IsFallbackSuggestionTitle(incomingTitle, normalizedUrl, videoId)) { return "Loading suggestion..."; } return string.IsNullOrWhiteSpace(incomingTitle) ? normalizedUrl : incomingTitle.Trim(); } private void SendSuggestionConfirmation(ushort recipientPlayerId, string url) { if (!IsHost || CurrentLobby == null || !_transport.Connected || string.IsNullOrWhiteSpace(url)) { return; } try { _transport.SendToPlayer(new SyncVideoSuggestionAckPacket { RecipientPlayerId = recipientPlayerId, LobbyId = CurrentLobby.LobbyId, SuggestionKey = BuildSuggestionKey(url) }, recipientPlayerId); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Failed to send suggestion confirmation: " + ex.Message)); } } private void AddOrUpdateSuggestion(ushort senderPlayerId, string playerName, string url, string title, string source) { if (!IsHost || CurrentLobby == null || string.IsNullOrWhiteSpace(url)) { return; } string videoId; string directPlayableUrl; string text = UrlNormalizer.Normalize(url, out videoId, out directPlayableUrl); string pendingSuggestionTitle = GetPendingSuggestionTitle(text, videoId, title); string text2 = BuildStoredSuggestionEntryKey(senderPlayerId, text); bool flag = _suggestionOrder.Count == 0; VideoSuggestion value; bool flag2 = _suggestions.TryGetValue(text2, out value); bool flag3 = flag2 && HasResolvedSuggestionMetadata(value); bool flag4 = IsFallbackSuggestionTitle(pendingSuggestionTitle, text, videoId) || string.Equals(pendingSuggestionTitle, "Loading suggestion...", StringComparison.Ordinal); string text3 = ((flag3 && flag4) ? value.Title : pendingSuggestionTitle); string text4 = (flag3 ? value.ChannelName : string.Empty); if (!flag2 || !string.Equals(value.Url, text, StringComparison.Ordinal) || !string.Equals(value.Title, text3, StringComparison.Ordinal) || !string.Equals(value.ChannelName, text4, StringComparison.Ordinal)) { string playerDisplayName = GetPlayerDisplayName(senderPlayerId); _logger.LogInfo((object)("Received suggestion via " + source + " from " + playerDisplayName + " for lobby " + CurrentLobby.LobbyId + ": " + text3)); _suggestions[text2] = new VideoSuggestion { Url = text, Title = text3, ChannelName = text4 }; if (!flag2) { _suggestionOrder.Add(text2); } this.SuggestionsChanged?.Invoke(); if (!flag2 && flag && _playlistModeEnabled && SyncVideoPlugin.Settings.HostAutoplay.Value && CurrentLobby != null && CurrentLobby.HasEnded) { RemoveSuggestion(text2); SyncVideoPlugin.SyncController?.HostSetUrl(text); } else { TryResolveSuggestionMetadata(senderPlayerId, text2, text); } } } private void TryResolveSuggestionMetadata(ushort senderPlayerId, string suggestionEntryKey, string url) { string videoId; string directPlayableUrl; string originalUrl = UrlNormalizer.Normalize(url, out videoId, out directPlayableUrl); if (string.IsNullOrWhiteSpace(videoId)) { return; } string lookupKey = suggestionEntryKey + "|" + videoId; if (!_pendingSuggestionMetadataLookups.Add(lookupKey)) { return; } YouTube.ResolveTitleAndUploaderAsync(originalUrl, videoId, delegate(string resolvedTitle, string resolvedUploader) { SyncVideoPlugin.SyncController.EnqueueMainThreadAction(delegate { _pendingSuggestionMetadataLookups.Remove(lookupKey); if (_suggestions.TryGetValue(suggestionEntryKey, out var value) && string.Equals(value.Url, url, StringComparison.Ordinal)) { string text = (string.IsNullOrWhiteSpace(resolvedTitle) ? value.Title : resolvedTitle); string text2 = (string.IsNullOrWhiteSpace(resolvedUploader) ? value.ChannelName : resolvedUploader); if (!string.Equals(value.Title, text, StringComparison.Ordinal) || !string.Equals(value.ChannelName, text2, StringComparison.Ordinal)) { value.Title = text; value.ChannelName = text2; _suggestions[suggestionEntryKey] = value; this.SuggestionsChanged?.Invoke(); } } }); }); } public bool RemoveSuggestion(string suggestionEntryKey) { if (string.IsNullOrWhiteSpace(suggestionEntryKey)) { return false; } bool flag = _suggestions.Remove(suggestionEntryKey); if (flag) { _suggestionOrder.Remove(suggestionEntryKey); this.SuggestionsChanged?.Invoke(); } return flag; } public void ClearSuggestions() { _suggestions.Clear(); _suggestionOrder.Clear(); _hostReceivedSuggestionKeys.Clear(); _pendingSuggestionMetadataLookups.Clear(); this.SuggestionsChanged?.Invoke(); } public void SetSuggestionsOpen(bool open) { if (IsHost && CurrentLobby != null) { _suggestionsOpen = open; _hostShadow.SuggestionsOpen = open; if (CurrentLobby != null) { CurrentLobby.SuggestionsOpen = open; } this.SuggestionsChanged?.Invoke(); if (CurrentLobby.Members.Count > 1) { _transport.BroadcastToLobby(new SyncVideoSuggestionsOpenPacket { LobbyId = CurrentLobby.LobbyId, IsOpen = open }); } } } public void SendSuggestion(string url, string title) { if (!IsHost && CurrentLobby != null && CurrentLobby.HostId != 0) { string text = url ?? string.Empty; string text2 = (string.IsNullOrEmpty(title) ? text : title); string videoId; string directPlayableUrl; string normalizedUrl = UrlNormalizer.Normalize(text, out videoId, out directPlayableUrl); if (UrlNormalizer.IsDirectSuggestionUrlTooLong(normalizedUrl, videoId, directPlayableUrl, out var error) || !UrlNormalizer.ValidateSubmissionUrl(normalizedUrl, videoId, directPlayableUrl, out error)) { _logger.LogWarning((object)("[SyncVideo] Rejected unsafe suggestion URL before send: " + error)); return; } _logger.LogInfo((object)("[SyncVideo] Sending suggestion for lobby " + CurrentLobby.LobbyId + ": " + text2)); _transport.SendToPlayer(new SyncVideoSuggestionPacket { LobbyId = CurrentLobby.LobbyId, Url = text, Title = text2, PlayerName = string.Empty }, CurrentLobby.HostId); } } public void RequestSuggestionScan() { } private void ClearSuggestionState() { _suggestions.Clear(); _suggestionOrder.Clear(); _suggestionsOpen = false; _hostReceivedSuggestionKeys.Clear(); this.SuggestionsChanged?.Invoke(); } public VideoLobbyManager(ManualLogSource logger, SyncVideoTransport transport) { _logger = logger; _transport = transport; ClientLobbyManager.LobbiesUpdated = (Action)Delegate.Combine(ClientLobbyManager.LobbiesUpdated, new Action(OnNativeLobbiesUpdated)); ClientLobbyManager.LobbyChanged = (Action)Delegate.Combine(ClientLobbyManager.LobbyChanged, new Action(OnNativeLobbyChanged)); _transport.SyncPacketReceived += OnSyncPacketReceived; RefreshFromNative(); } public void Dispose() { ClientLobbyManager.LobbiesUpdated = (Action)Delegate.Remove(ClientLobbyManager.LobbiesUpdated, new Action(OnNativeLobbiesUpdated)); ClientLobbyManager.LobbyChanged = (Action)Delegate.Remove(ClientLobbyManager.LobbyChanged, new Action(OnNativeLobbyChanged)); _transport.SyncPacketReceived -= OnSyncPacketReceived; } public void RequestStateFromHost() { RequestFreshStateForCurrentLobby(force: true); } private void RequestFreshStateForCurrentLobby(bool force = false) { if (_offlineLobbyActive || !_transport.Connected || CurrentLobby == null || IsHost || string.IsNullOrWhiteSpace(CurrentLobby.LobbyId) || CurrentLobby.HostId == 0 || (!force && string.Equals(_requestedStateLobbyId, CurrentLobby.LobbyId, StringComparison.Ordinal) && _receivedFreshStateForCurrentLobby) || (!force && string.Equals(_requestedStateLobbyId, CurrentLobby.LobbyId, StringComparison.Ordinal) && _stateRequestRetryTimer > 0f && _stateRequestRetryTimer < 0.95f)) { return; } _requestedStateLobbyId = CurrentLobby.LobbyId; _stateRequestRetryTimer = 0f; try { _transport.SendToPlayer(new SyncVideoStateRequestPacket { LobbyId = CurrentLobby.LobbyId }, CurrentLobby.HostId); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Failed to request fresh SyncVideo state: " + ex.Message)); } } private void ResetFreshStateTracking(string lobbyId) { _requestedStateLobbyId = lobbyId; _receivedFreshStateForCurrentLobby = false; _stateRequestRetryTimer = 0f; } public void Tick(float deltaTime) { if (!IsHost) { if (!_offlineLobbyActive && CurrentLobby != null && _transport.Connected && !_receivedFreshStateForCurrentLobby) { _stateRequestRetryTimer += deltaTime; if (_stateRequestRetryTimer >= 0.5f) { _stateRequestRetryTimer = 0f; RequestFreshStateForCurrentLobby(force: true); } } } else { if (CurrentLobby == null) { return; } if (_offlineLobbyActive) { if (CurrentLobby.IsPlaying) { _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); } } else if (_transport.Connected) { if (_pushCooldownTimer > 0f) { _pushCooldownTimer = Math.Max(0f, _pushCooldownTimer - deltaTime); } _stateTimer += deltaTime; if (_stateTimer >= SyncVideoPlugin.Settings.HostStateResendInterval.Value) { _stateTimer = 0f; BroadcastTimePacket(); } } } } public void HostLobby(string lobbyName = null) { _leaveInProgress = false; _syncVideoHostActive = true; if (!SyncVideoPlugin.ScreenManager.HasAnyScreensInMap()) { return; } if (OfflineModeEnabled) { CreateOfflineLobby(lobbyName); return; } ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager != null && nativeLobbyManager.CanJoinLobby()) { if (nativeLobbyManager.CurrentLobby != null) { nativeLobbyManager.LeaveLobby(); } _offlineLobbyActive = false; _hostShadow = new SyncShadow { IsSyncVideoLobby = true }; HudManager.OnLobbyEnter(); GamemodeSettings gamemodeSettings = GamemodeFactory.GetGamemodeSettings((GamemodeIDs)3); nativeLobbyManager.CreateLobby((GamemodeIDs)3, gamemodeSettings); } } public void JoinLobby(string lobbyId) { _leaveInProgress = false; if (OfflineModeEnabled || _offlineLobbyActive) { return; } ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager == null || !SyncVideoPlugin.ScreenManager.HasAnyScreensInMap()) { return; } VideoLobby videoLobby = null; for (int i = 0; i < _visibleLobbies.Count; i++) { VideoLobby videoLobby2 = _visibleLobbies[i]; if (videoLobby2 != null && string.Equals(videoLobby2.LobbyId, lobbyId, StringComparison.Ordinal)) { videoLobby = videoLobby2; break; } } if ((videoLobby == null || videoLobby.IsOpen) && uint.TryParse(lobbyId, out var result)) { HudManager.OnLobbyEnter(); nativeLobbyManager.JoinLobby(result); } } public void LeaveLobby() { if (_offlineLobbyActive) { string lastCurrentLobbyId = _lastCurrentLobbyId; _syncVideoHostActive = false; _offlineLobbyActive = false; CurrentLobby = null; _lastCurrentLobbyId = null; _hostShadow = new SyncShadow { IsSyncVideoLobby = true }; _stateTimer = 0f; _leaveInProgress = false; ResetFreshStateTracking(null); if (!string.Equals(lastCurrentLobbyId, _lastCurrentLobbyId, StringComparison.Ordinal)) { this.ActiveLobbyChanged?.Invoke(CurrentLobby); } ClearSuggestionState(); this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); return; } ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager != null) { string lastCurrentLobbyId2 = _lastCurrentLobbyId; _syncVideoHostActive = false; _leaveInProgress = true; CurrentLobby = null; _lastCurrentLobbyId = null; _hostShadow = new SyncShadow { IsSyncVideoLobby = true }; _stateTimer = 0f; ResetFreshStateTracking(null); if (!string.Equals(lastCurrentLobbyId2, _lastCurrentLobbyId, StringComparison.Ordinal)) { this.ActiveLobbyChanged?.Invoke(CurrentLobby); } ClearSuggestionState(); this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); nativeLobbyManager.LeaveLobby(); } } public void SetVideo(string url, string videoId) { if (IsHost && CurrentLobby != null) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.Url = url ?? string.Empty; _hostShadow.VideoId = videoId ?? string.Empty; _hostShadow.MediaTimeSeconds = 0.0; _hostShadow.IsPlaying = false; _hostShadow.Revision++; _hostShadow.SeekRevision = 0; _hostShadow.HasEnded = false; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); _hostShadow.SelectedAudioTrack = 0; _hostShadow.SelectedSubtitleTrack = -1; CurrentLobby.MediaTimeSeconds = 0.0; CurrentLobby.IsPlaying = false; CurrentLobby.HasEnded = false; CurrentLobby.SeekRevision = _hostShadow.SeekRevision; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: true, notifyActiveStateChanged: true); return; } PushHostShadowToNativeLobby(); BroadcastStatePacket(); RefreshFromNative(); this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); } } public void SetPlayback(bool playing) { if (IsHost && CurrentLobby != null) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.IsPlaying = playing; if (playing) { _hostShadow.HasEnded = false; } _hostShadow.Revision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.IsPlaying = _hostShadow.IsPlaying; CurrentLobby.HasEnded = _hostShadow.HasEnded; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); return; } BroadcastStatePacket(); PushHostShadowToNativeLobby(); this.ActiveStateChanged?.Invoke(CurrentLobby); } } public void SeekRelative(double seconds) { if (IsHost && CurrentLobby != null && !CurrentLobby.HasEnded && !_hostShadow.HasEnded) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.MediaTimeSeconds = Math.Max(0.0, _hostShadow.MediaTimeSeconds + seconds); _hostShadow.HasEnded = false; _hostShadow.Revision++; _hostShadow.SeekRevision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.MediaTimeSeconds = _hostShadow.MediaTimeSeconds; CurrentLobby.HasEnded = _hostShadow.HasEnded; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.SeekRevision = _hostShadow.SeekRevision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); return; } BroadcastStatePacket(); PushHostShadowToNativeLobby(); this.ActiveStateChanged?.Invoke(CurrentLobby); } } public void SeekToAbsolute(double seconds) { if (IsHost && CurrentLobby != null && !CurrentLobby.HasEnded && !_hostShadow.HasEnded) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.MediaTimeSeconds = Math.Max(0.0, seconds); _hostShadow.HasEnded = false; _hostShadow.Revision++; _hostShadow.SeekRevision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.MediaTimeSeconds = _hostShadow.MediaTimeSeconds; CurrentLobby.HasEnded = _hostShadow.HasEnded; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.SeekRevision = _hostShadow.SeekRevision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); return; } BroadcastStatePacket(); PushHostShadowToNativeLobby(); this.ActiveStateChanged?.Invoke(CurrentLobby); } } public void NotifyPlaybackEnded(double seconds) { if (IsHost && CurrentLobby != null) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.IsPlaying = false; _hostShadow.HasEnded = true; _hostShadow.MediaTimeSeconds = Math.Max(0.0, seconds); _hostShadow.Revision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.IsPlaying = false; CurrentLobby.HasEnded = true; CurrentLobby.MediaTimeSeconds = _hostShadow.MediaTimeSeconds; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); return; } BroadcastStatePacket(); PushHostShadowToNativeLobby(); this.ActiveStateChanged?.Invoke(CurrentLobby); } } public void RestartFromBeginning(bool playing) { if (IsHost && CurrentLobby != null) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.MediaTimeSeconds = 0.0; _hostShadow.IsPlaying = playing; _hostShadow.HasEnded = false; _hostShadow.Revision++; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); CurrentLobby.MediaTimeSeconds = 0.0; CurrentLobby.IsPlaying = playing; CurrentLobby.HasEnded = false; CurrentLobby.Revision = _hostShadow.Revision; CurrentLobby.HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: false, notifyActiveStateChanged: true); return; } BroadcastStatePacket(); PushHostShadowToNativeLobby(); this.ActiveStateChanged?.Invoke(CurrentLobby); } } public void SetMkvTrackSelection(int audioTrack, int subtitleTrack) { if (IsHost && CurrentLobby != null) { _hostShadow.SelectedAudioTrack = audioTrack; _hostShadow.SelectedSubtitleTrack = subtitleTrack; _hostShadow.Revision++; CurrentLobby.SelectedAudioTrack = audioTrack; CurrentLobby.SelectedSubtitleTrack = subtitleTrack; CurrentLobby.Revision = _hostShadow.Revision; if (_offlineLobbyActive) { UpdateOfflineLobbySnapshot(notifyLobbiesChanged: true, notifyActiveStateChanged: true); return; } PushHostShadowToNativeLobby(); BroadcastStatePacket(); } } public void SetObservedPlaybackTime(double seconds) { if (IsHost && CurrentLobby != null) { _hostShadow.MediaTimeSeconds = Math.Max(0.0, seconds); } } private void CreateOfflineLobby(string lobbyName) { string lastCurrentLobbyId = _lastCurrentLobbyId; HudManager.OnLobbyEnter(); _offlineLobbyActive = true; _hostShadow = new SyncShadow { IsSyncVideoLobby = true, HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; CurrentLobby = BuildOfflineSnapshot(lobbyName); _lastCurrentLobbyId = CurrentLobby?.LobbyId; _stateTimer = 0f; ResetFreshStateTracking(_lastCurrentLobbyId); if (!string.Equals(lastCurrentLobbyId, _lastCurrentLobbyId, StringComparison.Ordinal)) { this.ActiveLobbyChanged?.Invoke(CurrentLobby); } this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); } private void UpdateOfflineLobbySnapshot(bool notifyLobbiesChanged, bool notifyActiveStateChanged) { if (_offlineLobbyActive) { CurrentLobby = BuildOfflineSnapshot(CurrentLobby?.LobbyName); _lastCurrentLobbyId = CurrentLobby?.LobbyId; if (notifyActiveStateChanged) { this.ActiveStateChanged?.Invoke(CurrentLobby); } if (notifyLobbiesChanged) { this.LobbiesChanged?.Invoke(); } } } private VideoLobby BuildOfflineSnapshot(string lobbyName) { ushort num = ((_transport.LocalPlayerId != 0) ? _transport.LocalPlayerId : ushort.MaxValue); string lobbyName2 = (string.IsNullOrWhiteSpace(lobbyName) ? "Sync Video Lobby (Offline)" : lobbyName); VideoLobby videoLobby = new VideoLobby { LobbyId = "offline", HostId = num, LobbyName = lobbyName2, CurrentUrl = _hostShadow.Url, CurrentVideoId = _hostShadow.VideoId, IsPlaying = _hostShadow.IsPlaying, MediaTimeSeconds = _hostShadow.MediaTimeSeconds, HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds, Revision = _hostShadow.Revision, SeekRevision = _hostShadow.SeekRevision, HasEnded = _hostShadow.HasEnded, IsOpen = _hostShadow.IsOpen, SelectedAudioTrack = _hostShadow.SelectedAudioTrack, SelectedSubtitleTrack = _hostShadow.SelectedSubtitleTrack }; videoLobby.Members.Add(num); return videoLobby; } private void OnNativeLobbiesUpdated() { if (!_offlineLobbyActive) { string lastCurrentLobbyId = _lastCurrentLobbyId; string b = CurrentLobby?.CurrentUrl; int num = CurrentLobby?.Revision ?? int.MinValue; bool flag = CurrentLobby?.IsPlaying ?? false; bool flag2 = CurrentLobby?.HasEnded ?? false; RefreshFromNative(); bool flag3 = !string.Equals(lastCurrentLobbyId, _lastCurrentLobbyId, StringComparison.Ordinal); if (flag3) { ResetFreshStateTracking(_lastCurrentLobbyId); this.ActiveLobbyChanged?.Invoke(CurrentLobby); } TryApplyPendingStateForCurrentLobby(); RequestFreshStateForCurrentLobby(); if (flag3 || CurrentLobby == null || CurrentLobby.Revision != num || !string.Equals(CurrentLobby.CurrentUrl, b, StringComparison.Ordinal) || CurrentLobby.IsPlaying != flag || CurrentLobby.HasEnded != flag2) { this.ActiveStateChanged?.Invoke(CurrentLobby); } this.LobbiesChanged?.Invoke(); MaybeBroadcastHostState(); } } private void OnNativeLobbyChanged() { if (_offlineLobbyActive) { return; } EnsureHostLobbyIsTagged(); string lastCurrentLobbyId = _lastCurrentLobbyId; string b = CurrentLobby?.CurrentUrl; int num = CurrentLobby?.Revision ?? int.MinValue; bool flag = CurrentLobby?.IsPlaying ?? false; bool flag2 = CurrentLobby?.HasEnded ?? false; RefreshFromNative(); bool flag3 = !string.Equals(lastCurrentLobbyId, _lastCurrentLobbyId, StringComparison.Ordinal); if (flag3) { ResetFreshStateTracking(_lastCurrentLobbyId); this.ActiveLobbyChanged?.Invoke(CurrentLobby); if (CurrentLobby != null) { HudManager.OnLobbyEnter(); } } TryApplyPendingStateForCurrentLobby(); RequestFreshStateForCurrentLobby(); if (flag3 || CurrentLobby == null || CurrentLobby.Revision != num || !string.Equals(CurrentLobby.CurrentUrl, b, StringComparison.Ordinal) || CurrentLobby.IsPlaying != flag || CurrentLobby.HasEnded != flag2) { this.ActiveStateChanged?.Invoke(CurrentLobby); } this.LobbiesChanged?.Invoke(); MaybeBroadcastHostState(); } private void OnSyncPacketReceived(SyncVideoPacketBase packet) { if (_offlineLobbyActive || packet == null) { return; } if (packet is SyncVideoStateRequestPacket syncVideoStateRequestPacket) { if (IsHost && CurrentLobby != null && string.Equals(CurrentLobby.LobbyId, syncVideoStateRequestPacket.LobbyId, StringComparison.Ordinal)) { _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); SendStatePacketToPlayer(packet.SenderPlayerId); } return; } if (packet is SyncVideoSuggestionPacket syncVideoSuggestionPacket) { if (IsHost && CurrentLobby != null && string.Equals(CurrentLobby.LobbyId, syncVideoSuggestionPacket.LobbyId, StringComparison.Ordinal)) { AddOrUpdateSuggestion(syncVideoSuggestionPacket.SenderPlayerId, syncVideoSuggestionPacket.PlayerName, syncVideoSuggestionPacket.Url, syncVideoSuggestionPacket.Title, "suggestion packet"); SendSuggestionConfirmation(syncVideoSuggestionPacket.SenderPlayerId, syncVideoSuggestionPacket.Url); } return; } if (packet is SyncVideoSuggestionsOpenPacket syncVideoSuggestionsOpenPacket) { if (!IsHost && CurrentLobby != null && string.Equals(CurrentLobby.LobbyId, syncVideoSuggestionsOpenPacket.LobbyId, StringComparison.Ordinal)) { _suggestionsOpen = syncVideoSuggestionsOpenPacket.IsOpen; this.SuggestionsChanged?.Invoke(); } return; } SyncVideoSuggestionAckPacket syncVideoSuggestionAckPacket = packet as SyncVideoSuggestionAckPacket; if (syncVideoSuggestionAckPacket != null) { return; } if (packet is SyncVideoTimePacket syncVideoTimePacket) { if (!IsHost && !string.IsNullOrWhiteSpace(syncVideoTimePacket.LobbyId) && CurrentLobby != null && string.Equals(CurrentLobby.LobbyId, syncVideoTimePacket.LobbyId, StringComparison.Ordinal) && (CurrentLobby.HostId == 0 || syncVideoTimePacket.SenderPlayerId == 0 || syncVideoTimePacket.SenderPlayerId == CurrentLobby.HostId)) { CurrentLobby.MediaTimeSeconds = Math.Max(0.0, syncVideoTimePacket.MediaTimeSeconds); CurrentLobby.IsPlaying = syncVideoTimePacket.IsPlaying; CurrentLobby.LastSeenSeconds = Time.unscaledTime; if (syncVideoTimePacket.HostSentMilliseconds > 0) { CurrentLobby.HostUnixMilliseconds = syncVideoTimePacket.HostSentMilliseconds; } } } else { if (!(packet is SyncVideoStatePacket syncVideoStatePacket) || IsHost || string.IsNullOrWhiteSpace(syncVideoStatePacket.LobbyId)) { return; } _pendingStatePackets[syncVideoStatePacket.LobbyId] = CloneStatePacket(syncVideoStatePacket); if (!syncVideoStatePacket.IsOpen && (CurrentLobby == null || !string.Equals(CurrentLobby.LobbyId, syncVideoStatePacket.LobbyId, StringComparison.Ordinal))) { return; } if (CurrentLobby == null || !string.Equals(CurrentLobby.LobbyId, syncVideoStatePacket.LobbyId, StringComparison.Ordinal)) { VideoLobby videoLobby = TryCreateCurrentLobbyFromPacket(syncVideoStatePacket); if (videoLobby == null) { return; } string lastCurrentLobbyId = _lastCurrentLobbyId; CurrentLobby = videoLobby; _lastCurrentLobbyId = CurrentLobby.LobbyId; if (!string.Equals(lastCurrentLobbyId, _lastCurrentLobbyId, StringComparison.Ordinal)) { this.ActiveLobbyChanged?.Invoke(CurrentLobby); } } if ((CurrentLobby.HostId != 0 && syncVideoStatePacket.SenderPlayerId != 0 && syncVideoStatePacket.SenderPlayerId != CurrentLobby.HostId) || (syncVideoStatePacket.HostUnixMilliseconds > 0 && CurrentLobby.HostUnixMilliseconds > 0 && syncVideoStatePacket.HostUnixMilliseconds < CurrentLobby.HostUnixMilliseconds)) { return; } _requestedStateLobbyId = CurrentLobby.LobbyId; _receivedFreshStateForCurrentLobby = true; _stateRequestRetryTimer = 0f; if (syncVideoStatePacket.Revision > CurrentLobby.Revision || !string.Equals(CurrentLobby.CurrentUrl, syncVideoStatePacket.Url, StringComparison.Ordinal) || !string.Equals(CurrentLobby.CurrentVideoId, syncVideoStatePacket.VideoId, StringComparison.Ordinal) || CurrentLobby.IsPlaying != syncVideoStatePacket.IsPlaying || CurrentLobby.HasEnded != syncVideoStatePacket.HasEnded || CurrentLobby.IsOpen != syncVideoStatePacket.IsOpen || CurrentLobby.SelectedAudioTrack != syncVideoStatePacket.SelectedAudioTrack || CurrentLobby.SelectedSubtitleTrack != syncVideoStatePacket.SelectedSubtitleTrack) { CurrentLobby.CurrentUrl = syncVideoStatePacket.Url ?? string.Empty; CurrentLobby.CurrentVideoId = syncVideoStatePacket.VideoId ?? string.Empty; CurrentLobby.IsPlaying = syncVideoStatePacket.IsPlaying; CurrentLobby.HasEnded = syncVideoStatePacket.HasEnded; CurrentLobby.IsOpen = syncVideoStatePacket.IsOpen; CurrentLobby.SuggestionsOpen = syncVideoStatePacket.SuggestionsOpen; CurrentLobby.Revision = syncVideoStatePacket.Revision; CurrentLobby.SeekRevision = syncVideoStatePacket.SeekRevision; CurrentLobby.SelectedAudioTrack = syncVideoStatePacket.SelectedAudioTrack; CurrentLobby.SelectedSubtitleTrack = syncVideoStatePacket.SelectedSubtitleTrack; CurrentLobby.MediaTimeSeconds = Math.Max(0.0, syncVideoStatePacket.MediaTimeSeconds); CurrentLobby.HostUnixMilliseconds = syncVideoStatePacket.HostUnixMilliseconds; CurrentLobby.LastSeenSeconds = Time.unscaledTime; if (!IsHost && _suggestionsOpen != syncVideoStatePacket.SuggestionsOpen) { _suggestionsOpen = syncVideoStatePacket.SuggestionsOpen; this.SuggestionsChanged?.Invoke(); } this.ActiveStateChanged?.Invoke(CurrentLobby); this.LobbiesChanged?.Invoke(); } } } private void TryApplyPendingStateForCurrentLobby() { if (CurrentLobby == null || string.IsNullOrWhiteSpace(CurrentLobby.LobbyId) || !_pendingStatePackets.TryGetValue(CurrentLobby.LobbyId, out var value) || value == null || (CurrentLobby.HostId != 0 && value.SenderPlayerId != 0 && value.SenderPlayerId != CurrentLobby.HostId) || (value.HostUnixMilliseconds > 0 && CurrentLobby.HostUnixMilliseconds > 0 && value.HostUnixMilliseconds < CurrentLobby.HostUnixMilliseconds)) { return; } _requestedStateLobbyId = CurrentLobby.LobbyId; _receivedFreshStateForCurrentLobby = true; _stateRequestRetryTimer = 0f; if (value.Revision > CurrentLobby.Revision || !string.Equals(CurrentLobby.CurrentUrl, value.Url, StringComparison.Ordinal) || !string.Equals(CurrentLobby.CurrentVideoId, value.VideoId, StringComparison.Ordinal) || CurrentLobby.IsPlaying != value.IsPlaying || CurrentLobby.HasEnded != value.HasEnded || CurrentLobby.IsOpen != value.IsOpen || CurrentLobby.SelectedAudioTrack != value.SelectedAudioTrack || CurrentLobby.SelectedSubtitleTrack != value.SelectedSubtitleTrack) { CurrentLobby.CurrentUrl = value.Url ?? string.Empty; CurrentLobby.CurrentVideoId = value.VideoId ?? string.Empty; CurrentLobby.IsPlaying = value.IsPlaying; CurrentLobby.HasEnded = value.HasEnded; CurrentLobby.IsOpen = value.IsOpen; CurrentLobby.SuggestionsOpen = value.SuggestionsOpen; CurrentLobby.Revision = value.Revision; CurrentLobby.SeekRevision = value.SeekRevision; CurrentLobby.SelectedAudioTrack = value.SelectedAudioTrack; CurrentLobby.SelectedSubtitleTrack = value.SelectedSubtitleTrack; CurrentLobby.MediaTimeSeconds = Math.Max(0.0, value.MediaTimeSeconds); CurrentLobby.HostUnixMilliseconds = value.HostUnixMilliseconds; CurrentLobby.LastSeenSeconds = Time.unscaledTime; if (!IsHost && _suggestionsOpen != value.SuggestionsOpen) { _suggestionsOpen = value.SuggestionsOpen; this.SuggestionsChanged?.Invoke(); } } } private static SyncVideoStatePacket CloneStatePacket(SyncVideoStatePacket source) { return new SyncVideoStatePacket { LobbyId = (source.LobbyId ?? string.Empty), Url = (source.Url ?? string.Empty), VideoId = (source.VideoId ?? string.Empty), IsPlaying = source.IsPlaying, MediaTimeSeconds = source.MediaTimeSeconds, HostUnixMilliseconds = source.HostUnixMilliseconds, Revision = source.Revision, SeekRevision = source.SeekRevision, HasEnded = source.HasEnded, IsOpen = source.IsOpen, SuggestionsOpen = source.SuggestionsOpen, SenderPlayerId = source.SenderPlayerId, SelectedAudioTrack = source.SelectedAudioTrack, SelectedSubtitleTrack = source.SelectedSubtitleTrack }; } private void MaybeBroadcastHostState() { if (!_offlineLobbyActive && IsHost && CurrentLobby != null && _transport.Connected) { _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (_pushCooldownTimer <= 0f) { PushHostShadowToNativeLobby(); } BroadcastStatePacket(); } } private VideoLobby TryCreateCurrentLobbyFromPacket(SyncVideoStatePacket statePacket) { ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager == null || nativeLobbyManager.CurrentLobby == null || nativeLobbyManager.CurrentLobby.LobbyState == null) { return null; } LobbyState lobbyState = nativeLobbyManager.CurrentLobby.LobbyState; if (!string.Equals(lobbyState.Id.ToString(), statePacket.LobbyId, StringComparison.Ordinal)) { return null; } string hostName = GetHostName(lobbyState.HostId); VideoLobby videoLobby = new VideoLobby { LobbyId = statePacket.LobbyId, HostId = lobbyState.HostId, LobbyName = (string.IsNullOrWhiteSpace(hostName) ? "Host Lobby" : (hostName + " Lobby")), CurrentUrl = (statePacket.Url ?? string.Empty), CurrentVideoId = (statePacket.VideoId ?? string.Empty), IsPlaying = statePacket.IsPlaying, HasEnded = statePacket.HasEnded, MediaTimeSeconds = Math.Max(0.0, statePacket.MediaTimeSeconds), HostUnixMilliseconds = statePacket.HostUnixMilliseconds, Revision = statePacket.Revision, SeekRevision = statePacket.SeekRevision, IsOpen = statePacket.IsOpen, LastSeenSeconds = Time.unscaledTime, SelectedAudioTrack = statePacket.SelectedAudioTrack, SelectedSubtitleTrack = statePacket.SelectedSubtitleTrack }; if (lobbyState.Players != null) { foreach (KeyValuePair player in lobbyState.Players) { videoLobby.Members.Add(player.Key); } } return videoLobby; } private void EnsureHostLobbyIsTagged() { if (!_syncVideoHostActive) { return; } ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager != null && nativeLobbyManager.CurrentLobby != null && nativeLobbyManager.CurrentLobby.LobbyState != null && nativeLobbyManager.CurrentLobby.LobbyState.HostId == _transport.LocalPlayerId) { ParseSyncShadow(nativeLobbyManager.CurrentLobby.LobbyState.GamemodeSettings, out var isSyncVideo); if (!isSyncVideo) { _hostShadow.IsSyncVideoLobby = true; _hostShadow.HostUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); PushHostShadowToNativeLobby(); } } } private void RefreshFromNative() { _refreshPreviousById.Clear(); if (CurrentLobby != null && !string.IsNullOrWhiteSpace(CurrentLobby.LobbyId)) { _refreshPreviousById[CurrentLobby.LobbyId] = CurrentLobby; } for (int i = 0; i < _visibleLobbies.Count; i++) { VideoLobby videoLobby = _visibleLobbies[i]; if (videoLobby != null && !string.IsNullOrWhiteSpace(videoLobby.LobbyId) && !_refreshPreviousById.ContainsKey(videoLobby.LobbyId)) { _refreshPreviousById[videoLobby.LobbyId] = videoLobby; } } _visibleLobbies.Clear(); if (_offlineLobbyActive) { CurrentLobby = BuildOfflineSnapshot(CurrentLobby?.LobbyName); if (CurrentLobby != null && _refreshPreviousById.TryGetValue(CurrentLobby.LobbyId, out var value)) { CurrentLobby.LastSeenSeconds = value.LastSeenSeconds; } _lastCurrentLobbyId = ((CurrentLobby != null) ? CurrentLobby.LobbyId : null); return; } ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager != null) { _refreshLobbyKeys.Clear(); foreach (uint key in nativeLobbyManager.Lobbies.Keys) { _refreshLobbyKeys.Add(key); } _refreshLobbyKeys.Sort(); foreach (uint refreshLobbyKey in _refreshLobbyKeys) { if (!nativeLobbyManager.Lobbies.TryGetValue(refreshLobbyKey, out var value2)) { continue; } VideoLobby videoLobby2 = BuildSnapshot(value2, allowCurrentUnmarked: false); if (videoLobby2 != null) { if (_refreshPreviousById.TryGetValue(videoLobby2.LobbyId, out var value3)) { videoLobby2.LastSeenSeconds = value3.LastSeenSeconds; } if (videoLobby2.IsOpen) { _visibleLobbies.Add(videoLobby2); } } } if (_leaveInProgress) { CurrentLobby = null; if (nativeLobbyManager.CurrentLobby == null) { _leaveInProgress = false; } } else { CurrentLobby = ((nativeLobbyManager.CurrentLobby != null) ? BuildSnapshot(nativeLobbyManager.CurrentLobby, allowCurrentUnmarked: true) : null); if (CurrentLobby != null && _refreshPreviousById.TryGetValue(CurrentLobby.LobbyId, out var value4)) { CurrentLobby.LastSeenSeconds = value4.LastSeenSeconds; if (value4.MediaTimeSeconds > 0.0) { CurrentLobby.MediaTimeSeconds = value4.MediaTimeSeconds; CurrentLobby.HostUnixMilliseconds = value4.HostUnixMilliseconds; } if (value4.Revision > CurrentLobby.Revision) { CurrentLobby.Revision = value4.Revision; } } } } else { CurrentLobby = null; _leaveInProgress = false; } _lastCurrentLobbyId = ((CurrentLobby != null) ? CurrentLobby.LobbyId : null); if (!IsHost && CurrentLobby != null) { _suggestionsOpen = CurrentLobby.SuggestionsOpen; } } private VideoLobby BuildSnapshot(Lobby nativeLobby, bool allowCurrentUnmarked) { if (nativeLobby == null || nativeLobby.LobbyState == null) { return null; } LobbyState lobbyState = nativeLobby.LobbyState; bool isSyncVideo; SyncShadow syncShadow = ParseSyncShadow(lobbyState.GamemodeSettings, out isSyncVideo); if (!isSyncVideo) { if (!allowCurrentUnmarked) { return null; } if (lobbyState.HostId != _transport.LocalPlayerId || !_syncVideoHostActive) { return null; } syncShadow = _hostShadow.Clone(); } string hostName = GetHostName(lobbyState.HostId); VideoLobby videoLobby = new VideoLobby { LobbyId = lobbyState.Id.ToString(), HostId = lobbyState.HostId, LobbyName = (string.IsNullOrWhiteSpace(hostName) ? "Host Lobby" : (hostName + " Lobby")), CurrentUrl = syncShadow.Url, CurrentVideoId = syncShadow.VideoId, IsPlaying = syncShadow.IsPlaying, MediaTimeSeconds = syncShadow.MediaTimeSeconds, HostUnixMilliseconds = syncShadow.HostUnixMilliseconds, Revision = syncShadow.Revision, SeekRevision = syncShadow.SeekRevision, HasEnded = syncShadow.HasEnded, IsOpen = syncShadow.IsOpen, SuggestionsOpen = syncShadow.SuggestionsOpen, SelectedAudioTrack = syncShadow.SelectedAudioTrack, SelectedSubtitleTrack = syncShadow.SelectedSubtitleTrack }; if (lobbyState.Players != null) { foreach (KeyValuePair player in lobbyState.Players) { videoLobby.Members.Add(player.Key); } } return videoLobby; } private static string SanitizeLobbyDisplayName(string rawName) { if (string.IsNullOrWhiteSpace(rawName)) { return string.Empty; } string input = _sanitizeSpriteRx.Replace(rawName, string.Empty); input = _sanitizeTagRx.Replace(input, string.Empty); return input.Replace("\n", " ").Replace("\r", " ").Trim(); } public string SanitizeDisplayNameForUi(string rawName) { return SanitizeLobbyDisplayName(rawName); } private string GetHostName(ushort hostId) { try { if ((Object)(object)ClientController.Instance != (Object)null && ClientController.Instance.Players != null && ClientController.Instance.Players.TryGetValue(hostId, out var value)) { return SanitizeLobbyDisplayName(MPUtility.GetPlayerDisplayName(value.ClientState)); } } catch { } return string.Empty; } public string GetPlayerDisplayName(ushort playerId) { try { if ((Object)(object)ClientController.Instance != (Object)null && ClientController.Instance.Players != null && ClientController.Instance.Players.TryGetValue(playerId, out var value)) { string text = SanitizeLobbyDisplayName(MPUtility.GetPlayerDisplayName(value.ClientState)); if (!string.IsNullOrWhiteSpace(text)) { return text; } } } catch { } return $"Player {playerId}"; } public bool KickPlayer(ushort playerId) { if (!IsHost || CurrentLobby == null || playerId == 0 || playerId == _transport.LocalPlayerId || playerId == CurrentLobby.HostId) { return false; } try { Type type = Type.GetType("BombRushMP.Common.Packets.ClientLobbyKick, BombRushMP.Common"); if (type == null) { return false; } object? obj = Activator.CreateInstance(type); Packet val = (Packet)((obj is Packet) ? obj : null); if (val == null) { return false; } bool flag = false; string[] array = new string[5] { "PlayerId", "TargetPlayerId", "KickedPlayerId", "ClientId", "Id" }; string[] array2 = array; foreach (string name in array2) { FieldInfo field = type.GetField(name); if (field != null && (field.FieldType == typeof(ushort) || field.FieldType == typeof(int))) { if (field.FieldType == typeof(ushort)) { field.SetValue(val, playerId); } else { field.SetValue(val, (int)playerId); } flag = true; break; } PropertyInfo property = type.GetProperty(name); if (property != null && property.CanWrite && (property.PropertyType == typeof(ushort) || property.PropertyType == typeof(int))) { if (property.PropertyType == typeof(ushort)) { property.SetValue(val, playerId, null); } else { property.SetValue(val, (int)playerId, null); } flag = true; break; } } if (!flag) { FieldInfo[] fields = type.GetFields(); foreach (FieldInfo fieldInfo in fields) { if ((fieldInfo.FieldType == typeof(ushort) || fieldInfo.FieldType == typeof(int)) && fieldInfo.Name.IndexOf("player", StringComparison.OrdinalIgnoreCase) >= 0) { if (fieldInfo.FieldType == typeof(ushort)) { fieldInfo.SetValue(val, playerId); } else { fieldInfo.SetValue(val, (int)playerId); } flag = true; break; } } } if (!flag) { return false; } ClientController.Instance.SendPacket(val, (SendModes)2, (NetChannels)2); return true; } catch (Exception ex) { _logger.LogWarning((object)("Failed to kick player from lobby: " + ex.Message)); return false; } } private void PushHostShadowToNativeLobby() { //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown _pushCooldownTimer = 3f; ClientLobbyManager nativeLobbyManager = NativeLobbyManager; if (nativeLobbyManager == null || nativeLobbyManager.CurrentLobby == null) { return; } try { GamemodeSettings gamemodeSettings = GamemodeFactory.GetGamemodeSettings((GamemodeIDs)3); byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8); gamemodeSettings.Write(binaryWriter); WriteSyncShadow(binaryWriter, _hostShadow); binaryWriter.Flush(); array = memoryStream.ToArray(); } ClientController.Instance.SendPacket((Packet)new ClientLobbySetGamemode((GamemodeIDs)3, array), (SendModes)2, (NetChannels)2); } catch (Exception ex) { _logger.LogError((object)("Failed to push SyncVideo state into native lobby settings: " + ex)); } } private void BroadcastTimePacket() { if (_offlineLobbyActive || !_transport.Connected || CurrentLobby == null || CurrentLobby.Members.Count <= 1) { return; } try { long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); _hostShadow.HostUnixMilliseconds = num; _transport.BroadcastToLobby(new SyncVideoTimePacket { LobbyId = (CurrentLobby.LobbyId ?? string.Empty), MediaTimeSeconds = _hostShadow.MediaTimeSeconds, IsPlaying = _hostShadow.IsPlaying, HostSentMilliseconds = num }); } catch (Exception ex) { _logger.LogError((object)("Failed to broadcast SyncVideo time packet: " + ex)); } } private void BroadcastStatePacket(bool skipMemberCheck = false) { if (_offlineLobbyActive || !_transport.Connected || CurrentLobby == null || (!skipMemberCheck && CurrentLobby.Members.Count <= 1)) { return; } try { _transport.BroadcastToLobby(new SyncVideoStatePacket { LobbyId = (CurrentLobby.LobbyId ?? string.Empty), Url = (_hostShadow.Url ?? string.Empty), VideoId = (_hostShadow.VideoId ?? string.Empty), IsPlaying = _hostShadow.IsPlaying, MediaTimeSeconds = _hostShadow.MediaTimeSeconds, HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds, Revision = _hostShadow.Revision, SeekRevision = _hostShadow.SeekRevision, HasEnded = _hostShadow.HasEnded, IsOpen = _hostShadow.IsOpen, SuggestionsOpen = _hostShadow.SuggestionsOpen, SelectedAudioTrack = _hostShadow.SelectedAudioTrack, SelectedSubtitleTrack = _hostShadow.SelectedSubtitleTrack }); } catch (Exception ex) { _logger.LogWarning((object)("Failed to broadcast SyncVideo host state: " + ex.Message)); } } private void SendStatePacketToPlayer(ushort targetPlayerId) { if (_offlineLobbyActive || !_transport.Connected || CurrentLobby == null) { return; } try { _transport.SendToPlayer(new SyncVideoStatePacket { LobbyId = (CurrentLobby.LobbyId ?? string.Empty), Url = (_hostShadow.Url ?? string.Empty), VideoId = (_hostShadow.VideoId ?? string.Empty), IsPlaying = _hostShadow.IsPlaying, MediaTimeSeconds = _hostShadow.MediaTimeSeconds, HostUnixMilliseconds = _hostShadow.HostUnixMilliseconds, Revision = _hostShadow.Revision, SeekRevision = _hostShadow.SeekRevision, HasEnded = _hostShadow.HasEnded, IsOpen = _hostShadow.IsOpen, SuggestionsOpen = _hostShadow.SuggestionsOpen, SelectedAudioTrack = _hostShadow.SelectedAudioTrack, SelectedSubtitleTrack = _hostShadow.SelectedSubtitleTrack }, targetPlayerId); } catch (Exception ex) { _logger.LogWarning((object)("Failed to send SyncVideo host state to player " + targetPlayerId + ": " + ex.Message)); } } private SyncShadow ParseSyncShadow(byte[] settingsBytes, out bool isSyncVideo) { SyncShadow syncShadow = new SyncShadow(); isSyncVideo = false; if (settingsBytes == null || settingsBytes.Length < 9) { return syncShadow; } try { for (int num = settingsBytes.Length - 4; num >= 0; num--) { if (BitConverter.ToInt32(settingsBytes, num) == 827742547) { using MemoryStream memoryStream = new MemoryStream(settingsBytes); using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8); memoryStream.Position = num; int num2 = binaryReader.ReadInt32(); byte b = binaryReader.ReadByte(); if (num2 == 827742547 && b >= 1 && b <= 5) { syncShadow.IsSyncVideoLobby = binaryReader.ReadBoolean(); syncShadow.Url = binaryReader.ReadString(); syncShadow.VideoId = binaryReader.ReadString(); syncShadow.IsPlaying = binaryReader.ReadBoolean(); syncShadow.MediaTimeSeconds = binaryReader.ReadDouble(); syncShadow.HostUnixMilliseconds = binaryReader.ReadInt64(); syncShadow.Revision = binaryReader.ReadInt32(); syncShadow.HasEnded = b >= 2 && memoryStream.Position < memoryStream.Length && binaryReader.ReadBoolean(); syncShadow.IsOpen = b < 3 || memoryStream.Position >= memoryStream.Length || binaryReader.ReadBoolean(); syncShadow.SuggestionsOpen = b >= 4 && memoryStream.Position < memoryStream.Length && binaryReader.ReadBoolean(); syncShadow.SelectedAudioTrack = ((b >= 5 && memoryStream.Position < memoryStream.Length) ? binaryReader.ReadInt32() : 0); syncShadow.SelectedSubtitleTrack = ((b >= 5 && memoryStream.Position < memoryStream.Length) ? binaryReader.ReadInt32() : (-1)); syncShadow.SeekRevision = ((memoryStream.Position < memoryStream.Length) ? binaryReader.ReadInt32() : 0); isSyncVideo = syncShadow.IsSyncVideoLobby; return syncShadow; } } } } catch (Exception ex) { _logger.LogWarning((object)("Failed to parse SyncVideo lobby metadata: " + ex.Message)); } return syncShadow; } private void WriteSyncShadow(BinaryWriter writer, SyncShadow shadow) { writer.Write(827742547); writer.Write((byte)5); writer.Write(shadow.IsSyncVideoLobby); writer.Write(shadow.Url ?? string.Empty); writer.Write(shadow.VideoId ?? string.Empty); writer.Write(shadow.IsPlaying); writer.Write(shadow.MediaTimeSeconds); writer.Write(shadow.HostUnixMilliseconds); writer.Write(shadow.Revision); writer.Write(shadow.HasEnded); writer.Write(shadow.IsOpen); writer.Write(shadow.SuggestionsOpen); writer.Write(shadow.SelectedAudioTrack); writer.Write(shadow.SelectedSubtitleTrack); writer.Write(shadow.SeekRevision); } } public sealed class VideoScreenManager : IDisposable { public struct ScreenTransformData { public float PosX; public float PosY; public float PosZ; public float ScaleX; public float ScaleY; } private sealed class StaticTvAnchor : MonoBehaviour { private Transform _target; private Vector3 _localPosition; private Quaternion _localRotation; private Vector3 _localScale; private Rigidbody[] _rigidbodies; public void Capture(Transform target) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) _target = target; if (!((Object)(object)_target == (Object)null)) { _localPosition = _target.localPosition; _localRotation = _target.localRotation; _localScale = _target.localScale; _rigidbodies = ((Component)_target).GetComponentsInChildren(true); } } public void FreezeRigidbodies() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) if (_rigidbodies == null) { return; } for (int i = 0; i < _rigidbodies.Length; i++) { Rigidbody val = _rigidbodies[i]; if (!((Object)(object)val == (Object)null)) { val.velocity = Vector3.zero; val.angularVelocity = Vector3.zero; val.useGravity = false; val.isKinematic = true; val.constraints = (RigidbodyConstraints)126; } } } private void LateUpdate() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_target == (Object)null)) { if (_target.localPosition != _localPosition) { _target.localPosition = _localPosition; } if (_target.localRotation != _localRotation) { _target.localRotation = _localRotation; } if (_target.localScale != _localScale) { _target.localScale = _localScale; } } } } private sealed class ScreenInstance : IDisposable { public readonly GameObject Root; public readonly MeshRenderer Renderer; public readonly GameObject StatusBackdrop; public readonly GameObject StatusCanvas; public readonly TextMeshProUGUI StatusText; public readonly TextMeshProUGUI SubtitleText; public readonly RectTransform[] SubtitleBgPanels; public readonly Vector2 SubtitleTextCenter; public readonly TextMeshProUGUI AudioLoadingText; public Texture LastAppliedTexture; public bool FontSettingsApplied; public ScreenInstance(GameObject root, MeshRenderer renderer, GameObject statusBackdrop, GameObject statusCanvas, TextMeshProUGUI statusText, TextMeshProUGUI subtitleText, RectTransform[] subtitleBgPanels, Vector2 subtitleTextCenter, TextMeshProUGUI audioLoadingText) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) Root = root; Renderer = renderer; StatusBackdrop = statusBackdrop; StatusCanvas = statusCanvas; StatusText = statusText; SubtitleText = subtitleText; SubtitleBgPanels = subtitleBgPanels; SubtitleTextCenter = subtitleTextCenter; AudioLoadingText = audioLoadingText; } public void Dispose() { if ((Object)(object)Root != (Object)null) { Object.Destroy((Object)(object)Root); } } } private const string RootObjectName = "SyncVideoPlayerInstance"; private const float TvStatusFontSize = 70f; private const float SubtitleFontSize = 34f; private const float DefaultPosX = 0f; private const float DefaultPosY = 0.33f; private const float DefaultPosZ = 0.22f; private const float DefaultScaleX = 0.52f; private const float DefaultScaleY = 0.293f; private const float SubtitleBgPadH = 12f; private const float SubtitleBgPadV = 5f; private readonly ManualLogSource _logger; private readonly SyncVideoController _controller; private readonly List _screens = new List(); private float _posX = 0f; private float _posY = 0.33f; private float _posZ = 0.22f; private float _scaleX = 0.52f; private float _scaleY = 0.293f; private float _loadingAnimTimer; private int _loadingAnimStep; private static TMP_FontAsset _cachedGameFont; private string _lastStatusInput; private bool _lastStatusPrepared; private bool _lastStatusPlaying; private int _lastStatusAnimStep = -1; private int _lastStatusRevision = int.MinValue; private bool _lastStatusInLobby; private bool _lastStatusIsHost; private bool _lastStatusIsViewerSyncing; private bool _lastStatusShouldShowFfmpeg; private bool _lastLobbyHasMediaTime; private string _cachedFinalStatus = string.Empty; private bool _cachedShowOverlay; private float _tickAccumulator; private bool _ffmpegMode; private float _ffmpegModeCheckTimer; private const float FfmpegModeCheckInterval = 10f; public VideoScreenManager(ManualLogSource logger, SyncVideoController controller) { _logger = logger; _controller = controller; } public void Dispose() { Clear(); } public void Tick(float deltaTime) { UpdateLoadingAnimation(deltaTime); _ffmpegModeCheckTimer -= deltaTime; if (_ffmpegModeCheckTimer <= 0f) { _ffmpegModeCheckTimer = 10f; _ffmpegMode = YouTube.IsFfmpegAvailable(); } _tickAccumulator += deltaTime; float num = (_ffmpegMode ? (1f / 60f) : (1f / 30f)); if (!(_tickAccumulator < num)) { _tickAccumulator -= num; PruneDestroyedScreens(); if (_screens.Count == 0) { _controller.StopForMissingScreen(); } ApplyBackendTexture(); ApplyStatusOverlay(); ApplySubtitles(); } } private void PruneDestroyedScreens() { for (int num = _screens.Count - 1; num >= 0; num--) { ScreenInstance screenInstance = _screens[num]; if (screenInstance == null || (Object)(object)screenInstance.Root == (Object)null) { _screens.RemoveAt(num); } } } public bool HasAnyScreensInMap() { Junk[] array = Object.FindObjectsOfType(true); foreach (Junk val in array) { if ((Object)(object)val != (Object)null && string.Equals(((Object)val).name, SyncVideoPlugin.Settings.TvObjectName.Value, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public string GetCurrentTransformSummary() { return $"X: {_posX:0.000} | Y: {_posY:0.000} | Z: {_posZ:0.000}\nW: {_scaleX:0.000} | H: {_scaleY:0.000}"; } public ScreenTransformData GetCurrentTransformData() { ScreenTransformData result = default(ScreenTransformData); result.PosX = _posX; result.PosY = _posY; result.PosZ = _posZ; result.ScaleX = _scaleX; result.ScaleY = _scaleY; return result; } public void ApplyTransformData(ScreenTransformData data) { _posX = data.PosX; _posY = data.PosY; _posZ = data.PosZ; _scaleX = Mathf.Max(0.1f, data.ScaleX); _scaleY = Mathf.Max(0.1f, data.ScaleY); Respawn(); } public void Rebind() { SpawnPlayersForActiveScene(); } public void AdjustX(float delta) { _posX += delta; Respawn(); } public void AdjustY(float delta) { _posY += delta; Respawn(); } public void AdjustZ(float delta) { _posZ += delta; Respawn(); } public void AdjustSize(float delta) { _scaleX = Mathf.Max(0.1f, _scaleX + delta); _scaleY = _scaleX / 1.7777778f; Respawn(); } public void AdjustAspect(float deltaX, float deltaY) { _scaleX = Mathf.Max(0.1f, _scaleX + deltaX); _scaleY = Mathf.Max(0.1f, _scaleY + deltaY); Respawn(); } private static TMP_FontAsset TryGetGameFont() { if ((Object)(object)_cachedGameFont != (Object)null) { return _cachedGameFont; } try { TextMeshProUGUI[] array = Resources.FindObjectsOfTypeAll(); foreach (TextMeshProUGUI val in array) { if (!((Object)(object)val == (Object)null) && !((Object)(object)((TMP_Text)val).font == (Object)null) && ((Object)val).name == "HeaderLabel") { _cachedGameFont = ((TMP_Text)val).font; return _cachedGameFont; } } foreach (TextMeshProUGUI val2 in array) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)((TMP_Text)val2).font == (Object)null)) { string hierarchyPath = GetHierarchyPath(((TMP_Text)val2).transform); if (hierarchyPath.IndexOf("UIRoot", StringComparison.OrdinalIgnoreCase) >= 0 || hierarchyPath.IndexOf("Phone", StringComparison.OrdinalIgnoreCase) >= 0 || hierarchyPath.IndexOf("Overlay", StringComparison.OrdinalIgnoreCase) >= 0) { _cachedGameFont = ((TMP_Text)val2).font; return _cachedGameFont; } } } foreach (TextMeshProUGUI val3 in array) { if ((Object)(object)val3 != (Object)null && (Object)(object)((TMP_Text)val3).font != (Object)null) { _cachedGameFont = ((TMP_Text)val3).font; return _cachedGameFont; } } } catch { } return null; } private static string GetHierarchyPath(Transform transform) { if ((Object)(object)transform == (Object)null) { return string.Empty; } string text = ((Object)transform).name; while ((Object)(object)transform.parent != (Object)null) { transform = transform.parent; text = ((Object)transform).name + "/" + text; } return text; } private static void ApplyGameFontToStatusText(TMP_Text text) { if (!((Object)(object)text == (Object)null)) { TMP_FontAsset val = TryGetGameFont(); if ((Object)(object)val != (Object)null && (Object)(object)text.font != (Object)(object)val) { text.font = val; } text.enableAutoSizing = false; text.fontSize = 70f; } } public void ResetScreenTransform() { _posX = 0f; _posY = 0.33f; _posZ = 0.22f; _scaleX = 0.52f; _scaleY = 0.293f; Respawn(); } private void Respawn() { SpawnPlayersForActiveScene(); } public void SpawnPlayersForActiveScene() { Clear(); Junk[] array = Object.FindObjectsOfType(true); int num = 0; foreach (Junk val in array) { if (!((Object)(object)val == (Object)null) && string.Equals(((Object)val).name, SyncVideoPlugin.Settings.TvObjectName.Value, StringComparison.OrdinalIgnoreCase)) { DestroyExistingSyncVideoChildren(((Component)val).transform); MakeTvStatic(val); ScreenInstance screenInstance = CreateScreenForTv(((Component)val).transform); if (screenInstance != null) { _screens.Add(screenInstance); num++; } if (num == 0) { _logger.LogWarning((object)"SyncVideo found no TV/screen objects in the current map."); _controller.StopForMissingScreen(); } _logger.LogInfo((object)$"SyncVideo spawned {num} visual TV screen(s)."); ApplyBackendTexture(); ApplyStatusOverlay(); } } } public void OnVideoChanged() { ApplyBackendTexture(); ApplyStatusOverlay(); } public void OnPlaybackStateChanged(bool playing, double timeSeconds) { ApplyBackendTexture(); ApplyStatusOverlay(); } private void MakeTvStatic(Junk junk) { if ((Object)(object)junk == (Object)null || !SyncVideoPlugin.Settings.StaticTVs.Value) { return; } try { StaticTvAnchor staticTvAnchor = ((Component)junk).GetComponent(); if ((Object)(object)staticTvAnchor == (Object)null) { staticTvAnchor = ((Component)junk).gameObject.AddComponent(); } staticTvAnchor.Capture(((Component)junk).transform); staticTvAnchor.FreezeRigidbodies(); if (((Behaviour)junk).enabled) { ((Behaviour)junk).enabled = false; } } catch (Exception ex) { _logger.LogWarning((object)("SyncVideo could not make TV static: " + ex.Message)); } } private void DestroyExistingSyncVideoChildren(Transform tvTransform) { for (int num = tvTransform.childCount - 1; num >= 0; num--) { Transform child = tvTransform.GetChild(num); if ((Object)(object)child != (Object)null && ((Object)child).name == "SyncVideoPlayerInstance") { Object.Destroy((Object)(object)((Component)child).gameObject); } } } private ScreenInstance CreateScreenForTv(Transform tvTransform) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_08c6: Unknown result type (might be due to invalid IL or missing references) //IL_08cd: Expected O, but got Unknown //IL_08f7: Unknown result type (might be due to invalid IL or missing references) //IL_090e: Unknown result type (might be due to invalid IL or missing references) //IL_091b: Unknown result type (might be due to invalid IL or missing references) //IL_0928: Unknown result type (might be due to invalid IL or missing references) //IL_094b: Unknown result type (might be due to invalid IL or missing references) //IL_0977: Unknown result type (might be due to invalid IL or missing references) //IL_0992: Unknown result type (might be due to invalid IL or missing references) //IL_09ff: Unknown result type (might be due to invalid IL or missing references) //IL_09e0: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01ac: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_023c: Unknown result type (might be due to invalid IL or missing references) //IL_0246: Expected O, but got Unknown //IL_0262: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_02a1: Expected O, but got Unknown //IL_02cb: Unknown result type (might be due to invalid IL or missing references) //IL_02dd: Unknown result type (might be due to invalid IL or missing references) //IL_032e: Unknown result type (might be due to invalid IL or missing references) //IL_03a6: Unknown result type (might be due to invalid IL or missing references) //IL_03b6: Unknown result type (might be due to invalid IL or missing references) //IL_03bd: Expected O, but got Unknown //IL_03dd: Unknown result type (might be due to invalid IL or missing references) //IL_03ea: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_0418: Unknown result type (might be due to invalid IL or missing references) //IL_043b: Unknown result type (might be due to invalid IL or missing references) //IL_0467: Unknown result type (might be due to invalid IL or missing references) //IL_0482: Unknown result type (might be due to invalid IL or missing references) //IL_04a3: Unknown result type (might be due to invalid IL or missing references) //IL_04aa: Expected O, but got Unknown //IL_04d4: Unknown result type (might be due to invalid IL or missing references) //IL_04e6: Unknown result type (might be due to invalid IL or missing references) //IL_0537: Unknown result type (might be due to invalid IL or missing references) //IL_05af: Unknown result type (might be due to invalid IL or missing references) //IL_05fe: Unknown result type (might be due to invalid IL or missing references) //IL_0605: Unknown result type (might be due to invalid IL or missing references) //IL_0619: Unknown result type (might be due to invalid IL or missing references) //IL_0620: Unknown result type (might be due to invalid IL or missing references) //IL_0637: Unknown result type (might be due to invalid IL or missing references) //IL_063e: Unknown result type (might be due to invalid IL or missing references) //IL_0652: Unknown result type (might be due to invalid IL or missing references) //IL_0659: Unknown result type (might be due to invalid IL or missing references) //IL_069d: Unknown result type (might be due to invalid IL or missing references) //IL_06a4: Expected O, but got Unknown //IL_06ce: Unknown result type (might be due to invalid IL or missing references) //IL_06e5: Unknown result type (might be due to invalid IL or missing references) //IL_06f2: Unknown result type (might be due to invalid IL or missing references) //IL_06ff: Unknown result type (might be due to invalid IL or missing references) //IL_0729: Unknown result type (might be due to invalid IL or missing references) //IL_0767: Unknown result type (might be due to invalid IL or missing references) //IL_076e: Expected O, but got Unknown //IL_078e: Unknown result type (might be due to invalid IL or missing references) //IL_0798: Unknown result type (might be due to invalid IL or missing references) //IL_07a2: Unknown result type (might be due to invalid IL or missing references) //IL_07ac: Unknown result type (might be due to invalid IL or missing references) //IL_07cc: Unknown result type (might be due to invalid IL or missing references) //IL_07f8: Unknown result type (might be due to invalid IL or missing references) //IL_0813: Unknown result type (might be due to invalid IL or missing references) //IL_08a5: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("SyncVideoPlayerInstance"); val.transform.SetParent(tvTransform, false); val.transform.localPosition = new Vector3(_posX, _posY, _posZ); val.transform.localRotation = Quaternion.identity; val.transform.localScale = Vector3.one; GameObject val2 = GameObject.CreatePrimitive((PrimitiveType)5); ((Object)val2).name = "SyncVideoScreenQuad"; val2.transform.SetParent(val.transform, false); val2.transform.localPosition = Vector3.zero; val2.transform.localRotation = Quaternion.Euler(0f, 180f, 0f); val2.transform.localScale = new Vector3(_scaleX, _scaleY, 1f); Collider component = val2.GetComponent(); if ((Object)(object)component != (Object)null) { Object.Destroy((Object)(object)component); } MeshRenderer component2 = val2.GetComponent(); if ((Object)(object)component2 == (Object)null) { Object.Destroy((Object)(object)val); return null; } Shader val3 = Shader.Find("Unlit/Texture") ?? Shader.Find("Sprites/Default"); ((Renderer)component2).material = new Material(val3); ((Renderer)component2).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)component2).receiveShadows = false; TryMakeMaterialOneSided(((Renderer)component2).material); GameObject val4 = GameObject.CreatePrimitive((PrimitiveType)5); ((Object)val4).name = "SyncVideoStatusBackdrop"; val4.transform.SetParent(val2.transform, false); val4.transform.localPosition = new Vector3(0f, 0f, -0.0012f); val4.transform.localRotation = Quaternion.identity; val4.transform.localScale = new Vector3(1f, 1f, 1f); Collider component3 = val4.GetComponent(); if ((Object)(object)component3 != (Object)null) { Object.Destroy((Object)(object)component3); } MeshRenderer component4 = val4.GetComponent(); if ((Object)(object)component4 == (Object)null) { Object.Destroy((Object)(object)val); return null; } Shader val5 = Shader.Find("Unlit/Color") ?? Shader.Find("Sprites/Default"); ((Renderer)component4).material = new Material(val5); ((Renderer)component4).material.color = new Color(0f, 0f, 0f, 0.7f); ((Renderer)component4).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)component4).receiveShadows = false; TryMakeMaterialOneSided(((Renderer)component4).material); val4.SetActive(false); GameObject val6 = new GameObject("SyncVideoStatusCanvas"); val6.transform.SetParent(val2.transform, false); val6.transform.localPosition = new Vector3(0f, 0f, -0.0016f); val6.transform.localRotation = Quaternion.identity; Canvas val7 = val6.AddComponent(); val7.renderMode = (RenderMode)2; val7.overrideSorting = true; val7.sortingOrder = 500; val7.pixelPerfect = false; RectTransform component5 = ((Component)val7).GetComponent(); component5.sizeDelta = new Vector2(1600f, 900f); float num = Mathf.Min(_scaleX / 1600f, _scaleY / 900f); float num2 = ((_scaleX > 0.0001f) ? (num / _scaleX) : 1f); float num3 = ((_scaleY > 0.0001f) ? (num / _scaleY) : 1f); val6.transform.localScale = new Vector3(num2, num3, 1f); GameObject val8 = new GameObject("SyncVideoStatusText"); val8.transform.SetParent(val6.transform, false); RectTransform val9 = val8.AddComponent(); val9.anchorMin = Vector2.zero; val9.anchorMax = Vector2.one; val9.offsetMin = new Vector2(-80f, 50f); val9.offsetMax = new Vector2(80f, -50f); TextMeshProUGUI val10 = val8.AddComponent(); ((TMP_Text)val10).text = string.Empty; ((Graphic)val10).color = Color.white; ((TMP_Text)val10).alignment = (TextAlignmentOptions)514; ((TMP_Text)val10).enableWordWrapping = true; ((TMP_Text)val10).overflowMode = (TextOverflowModes)0; ((TMP_Text)val10).margin = Vector4.zero; ((Graphic)val10).raycastTarget = false; val8.transform.localScale = Vector3.one; ApplyGameFontToStatusText((TMP_Text)(object)val10); val6.SetActive(false); GameObject val11 = new GameObject("SyncVideoSubtitleCanvas"); val11.transform.SetParent(val2.transform, false); val11.transform.localPosition = new Vector3(0f, 0f, -0.002f); val11.transform.localRotation = Quaternion.identity; Canvas val12 = val11.AddComponent(); val12.renderMode = (RenderMode)2; val12.overrideSorting = true; val12.sortingOrder = 510; val12.pixelPerfect = false; RectTransform component6 = ((Component)val12).GetComponent(); component6.sizeDelta = new Vector2(1600f, 900f); float num4 = Mathf.Min(_scaleX / 1600f, _scaleY / 900f); float num5 = ((_scaleX > 0.0001f) ? (num4 / _scaleX) : 1f); float num6 = ((_scaleY > 0.0001f) ? (num4 / _scaleY) : 1f); val11.transform.localScale = new Vector3(num5, num6, 1f); Vector2 val13 = default(Vector2); ((Vector2)(ref val13))..ctor(0.1f, 0f); Vector2 val14 = default(Vector2); ((Vector2)(ref val14))..ctor(0.9f, 0.2f); Vector2 val15 = default(Vector2); ((Vector2)(ref val15))..ctor(0f, 30f); Vector2 val16 = default(Vector2); ((Vector2)(ref val16))..ctor(0f, 0f); float num7 = (val13.x + val14.x) * 0.5f * 1600f + (val15.x + val16.x) * 0.5f - 800f; float num8 = (val13.y + val14.y) * 0.5f * 900f + (val15.y + val16.y) * 0.5f - 450f; Vector2 subtitleTextCenter = default(Vector2); ((Vector2)(ref subtitleTextCenter))..ctor(num7, num8); RectTransform[] array = (RectTransform[])(object)new RectTransform[3]; for (int i = 0; i < 3; i++) { GameObject val17 = new GameObject("SyncVideoSubtitleBg" + i); val17.transform.SetParent(val11.transform, false); RectTransform val18 = val17.AddComponent(); val18.anchorMin = new Vector2(0.5f, 0.5f); val18.anchorMax = new Vector2(0.5f, 0.5f); val18.anchoredPosition = Vector2.zero; val18.sizeDelta = Vector2.zero; Image val19 = val17.AddComponent(); ((Graphic)val19).color = new Color(0f, 0f, 0f, 0.55f); ((Graphic)val19).raycastTarget = false; val17.SetActive(false); array[i] = val18; } GameObject val20 = new GameObject("SyncVideoSubtitleText"); val20.transform.SetParent(val11.transform, false); RectTransform val21 = val20.AddComponent(); val21.anchorMin = val13; val21.anchorMax = val14; val21.offsetMin = val15; val21.offsetMax = val16; TextMeshProUGUI val22 = val20.AddComponent(); ((TMP_Text)val22).text = string.Empty; ((Graphic)val22).color = Color.white; ((TMP_Text)val22).alignment = (TextAlignmentOptions)514; ((TMP_Text)val22).enableWordWrapping = true; ((TMP_Text)val22).overflowMode = (TextOverflowModes)0; ((TMP_Text)val22).margin = Vector4.zero; ((Graphic)val22).raycastTarget = false; val20.transform.localScale = Vector3.one; float valueOrDefault = (SyncVideoPlugin.Settings?.SubtitleFontSize?.Value).GetValueOrDefault(34f); ((TMP_Text)val22).enableAutoSizing = false; ((TMP_Text)val22).fontSize = valueOrDefault; ((TMP_Text)val22).lineSpacing = 26f; try { Material fontMaterial = ((TMP_Text)val22).fontMaterial; fontMaterial.SetFloat(ShaderUtilities.ID_OutlineWidth, 0.3f); fontMaterial.SetColor(ShaderUtilities.ID_OutlineColor, Color.black); } catch { } val11.SetActive(false); GameObject val23 = new GameObject("SyncVideoAudioLoadingText"); val23.transform.SetParent(val11.transform, false); RectTransform val24 = val23.AddComponent(); val24.anchorMin = new Vector2(0.1f, 0.21f); val24.anchorMax = new Vector2(0.9f, 0.29f); val24.offsetMin = Vector2.zero; val24.offsetMax = Vector2.zero; TextMeshProUGUI val25 = val23.AddComponent(); ((TMP_Text)val25).text = string.Empty; ((Graphic)val25).color = Color.white; ((TMP_Text)val25).alignment = (TextAlignmentOptions)514; ((TMP_Text)val25).enableWordWrapping = false; ((TMP_Text)val25).overflowMode = (TextOverflowModes)0; ((TMP_Text)val25).margin = Vector4.zero; ((Graphic)val25).raycastTarget = false; val23.transform.localScale = Vector3.one; ((TMP_Text)val25).enableAutoSizing = false; ((TMP_Text)val25).fontSize = valueOrDefault; ((TMP_Text)val25).lineSpacing = 26f; try { Material fontMaterial2 = ((TMP_Text)val25).fontMaterial; fontMaterial2.SetFloat(ShaderUtilities.ID_OutlineWidth, 0.3f); fontMaterial2.SetColor(ShaderUtilities.ID_OutlineColor, Color.black); } catch { } return new ScreenInstance(val, component2, val4, val6, val10, val22, array, subtitleTextCenter, val25); } private static void TryMakeMaterialOneSided(Material material) { if (!((Object)(object)material == (Object)null)) { TrySetInt(material, "_Cull", 2); TrySetInt(material, "_CullMode", 2); TrySetInt(material, "_Culling", 2); } } private static void TrySetInt(Material material, string propertyName, int value) { if ((Object)(object)material != (Object)null && material.HasProperty(propertyName)) { material.SetInt(propertyName, value); } } private void UpdateLoadingAnimation(float deltaTime) { VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; string text = _controller.Backend.StatusOverlayText ?? string.Empty; bool flag = (string.IsNullOrWhiteSpace(text) && !_controller.Backend.IsPrepared && !_controller.Backend.IsPlaying) || (lobbyManager.InLobby && !lobbyManager.IsHost && (string.Equals(text, "No Video Loaded!", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "Video URL Error!", StringComparison.OrdinalIgnoreCase))); bool flag2 = (lobbyManager.InLobby && !lobbyManager.IsHost && lobbyManager.CurrentLobby != null && !string.IsNullOrWhiteSpace(lobbyManager.CurrentLobby?.CurrentUrl) && (!_controller.Backend.IsPrepared || !_controller.Backend.IsPlaying)) || _controller.IsViewerCommandSyncing; if (!flag && !flag2) { _loadingAnimTimer = 0f; _loadingAnimStep = 0; return; } _loadingAnimTimer += deltaTime; if (_loadingAnimTimer >= 0.35f) { _loadingAnimTimer = 0f; _loadingAnimStep = (_loadingAnimStep + 1) % 4; } } private string GetAnimatedLoadingText() { return _loadingAnimStep switch { 1 => "Loading.", 2 => "Loading..", 3 => "Loading...", _ => "Loading", }; } private string GetAnimatedSyncingText() { if (_controller.ShouldShowFfmpegSyncingStatus()) { return _loadingAnimStep switch { 1 => "FFmpeg enabled!\nDownloading and Syncing.", 2 => "FFmpeg enabled!\nDownloading and Syncing..", 3 => "FFmpeg enabled!\nDownloading and Syncing...", _ => "FFmpeg enabled!\nDownloading and Syncing", }; } string text = string.Empty; int viewerSeekDirection = _controller.ViewerSeekDirection; if (viewerSeekDirection > 0) { text = "\n\nSeeking forward!"; } else if (viewerSeekDirection < 0) { text = "\n\nSeeking backward!"; } return _loadingAnimStep switch { 1 => "Syncing." + text, 2 => "Syncing.." + text, 3 => "Syncing..." + text, _ => "Syncing" + text, }; } private void ApplyBackendTexture() { object outputTexture = _controller.Backend.OutputTexture; Texture val = (Texture)((outputTexture is Texture) ? outputTexture : null); if ((Object)(object)val == (Object)null) { return; } for (int i = 0; i < _screens.Count; i++) { ScreenInstance screenInstance = _screens[i]; if (screenInstance != null && !((Object)(object)screenInstance.Renderer == (Object)null) && !((Object)(object)screenInstance.LastAppliedTexture == (Object)(object)val)) { ((Renderer)screenInstance.Renderer).material.mainTexture = val; screenInstance.LastAppliedTexture = val; } } } private string GetDisplayStatusText(string backendMessage, bool isLoading) { string text = backendMessage ?? string.Empty; VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; VideoLobby currentLobby = lobbyManager.CurrentLobby; string text2 = ((text.IndexOf('\r') >= 0) ? text.Replace("\r\n", "\n").Replace("\r", "\n").Trim() : text.Trim()); if (!lobbyManager.InLobby && string.Equals(text, "No Video Loaded!", StringComparison.OrdinalIgnoreCase)) { return "No Video Loaded!\nOpen the Sync Video app!"; } if (lobbyManager.InLobby && !lobbyManager.IsHost && currentLobby != null) { bool flag = !string.IsNullOrWhiteSpace(currentLobby.CurrentUrl); bool flag2 = flag && !currentLobby.IsPlaying && !currentLobby.HasEnded; bool flag3 = flag && !_controller.Backend.IsPrepared && !_controller.Backend.IsPlaying; if (currentLobby.HasEnded) { return "Video Ended!"; } if (!flag && (isLoading || string.Equals(text, "No Video Loaded!", StringComparison.OrdinalIgnoreCase) || string.Equals(text, "Video URL Error!", StringComparison.OrdinalIgnoreCase))) { return _loadingAnimStep switch { 1 => "Host is Setting Up Video.", 2 => "Host is Setting Up Video..", 3 => "Host is Setting Up Video...", _ => "Host is Setting Up Video", }; } if (!string.IsNullOrWhiteSpace(currentLobby.CurrentUrl) && currentLobby.CurrentUrl.EndsWith(".mkv", StringComparison.OrdinalIgnoreCase) && !SyncVideoPlugin.Settings.EnableMkvSupport.Value) { return "Host is playing an MKV File!\nEnable MKV Support in your Config"; } if (flag && string.Equals(text, "Video URL Error!", StringComparison.OrdinalIgnoreCase)) { return "Video could not be loaded!\nTry rejoining the lobby!"; } if (_controller.IsViewerCommandSyncing || flag3) { return GetAnimatedSyncingText(); } if (flag2) { if (currentLobby.MediaTimeSeconds > 0.05) { return "Video Paused!\nWaiting on Host!"; } if (string.Equals(text2, "Video Loaded!\nPress Play!", StringComparison.OrdinalIgnoreCase) || string.Equals(text2, "Paused", StringComparison.OrdinalIgnoreCase) || string.Equals(text2, "Video Paused!", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(text2)) { return "Video Loaded!\nWaiting on Host!"; } } if (string.Equals(text2, "Video Ended!", StringComparison.OrdinalIgnoreCase)) { return "Video Ended!"; } if (string.Equals(text2, "Video Loaded!\nPress Play!", StringComparison.OrdinalIgnoreCase)) { return "Video Loaded!\nWaiting on Host!"; } if (string.Equals(text2, "Paused", StringComparison.OrdinalIgnoreCase) || string.Equals(text2, "Video Paused!", StringComparison.OrdinalIgnoreCase)) { return "Video Paused!\nWaiting on Host!"; } } if (isLoading) { return GetAnimatedLoadingText(); } return text; } private void ApplyStatusOverlay() { string text = _controller.Backend.StatusOverlayText ?? string.Empty; bool isPrepared = _controller.Backend.IsPrepared; bool isPlaying = _controller.Backend.IsPlaying; VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; VideoLobby currentLobby = lobbyManager.CurrentLobby; bool inLobby = lobbyManager.InLobby; bool isHost = lobbyManager.IsHost; bool isViewerCommandSyncing = _controller.IsViewerCommandSyncing; bool flag = _controller.ShouldShowFfmpegSyncingStatus(); int num = currentLobby?.Revision ?? int.MinValue; bool flag2 = currentLobby != null && currentLobby.MediaTimeSeconds > 0.05; if (!string.Equals(_lastStatusInput, text, StringComparison.Ordinal) || _lastStatusPrepared != isPrepared || _lastStatusPlaying != isPlaying || _lastStatusAnimStep != _loadingAnimStep || _lastStatusInLobby != inLobby || _lastStatusIsHost != isHost || _lastStatusIsViewerSyncing != isViewerCommandSyncing || _lastStatusShouldShowFfmpeg != flag || _lastStatusRevision != num || _lastLobbyHasMediaTime != flag2) { _lastStatusInput = text; _lastStatusPrepared = isPrepared; _lastStatusPlaying = isPlaying; _lastStatusAnimStep = _loadingAnimStep; _lastStatusInLobby = inLobby; _lastStatusIsHost = isHost; _lastStatusIsViewerSyncing = isViewerCommandSyncing; _lastStatusShouldShowFfmpeg = flag; _lastStatusRevision = num; _lastLobbyHasMediaTime = flag2; bool isLoading = string.IsNullOrWhiteSpace(text) && !isPrepared && !isPlaying; _cachedFinalStatus = GetDisplayStatusText(text, isLoading); _cachedShowOverlay = !string.IsNullOrWhiteSpace(_cachedFinalStatus); } string cachedFinalStatus = _cachedFinalStatus; bool cachedShowOverlay = _cachedShowOverlay; for (int i = 0; i < _screens.Count; i++) { ScreenInstance screenInstance = _screens[i]; if (screenInstance == null) { continue; } if ((Object)(object)screenInstance.StatusText != (Object)null) { if (!screenInstance.FontSettingsApplied) { ApplyGameFontToStatusText((TMP_Text)(object)screenInstance.StatusText); if ((Object)(object)_cachedGameFont != (Object)null) { screenInstance.FontSettingsApplied = true; } } if (!string.Equals(((TMP_Text)screenInstance.StatusText).text, cachedFinalStatus, StringComparison.Ordinal)) { ((TMP_Text)screenInstance.StatusText).text = cachedFinalStatus; ((TMP_Text)screenInstance.StatusText).ForceMeshUpdate(true, true); } if (((Component)screenInstance.StatusText).gameObject.activeSelf != cachedShowOverlay) { ((Component)screenInstance.StatusText).gameObject.SetActive(cachedShowOverlay); } } if ((Object)(object)screenInstance.StatusBackdrop != (Object)null && screenInstance.StatusBackdrop.activeSelf != cachedShowOverlay) { screenInstance.StatusBackdrop.SetActive(cachedShowOverlay); } if ((Object)(object)screenInstance.StatusCanvas != (Object)null && screenInstance.StatusCanvas.activeSelf != cachedShowOverlay) { screenInstance.StatusCanvas.SetActive(cachedShowOverlay); } } } private void ApplySubtitles() { VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; bool flag = lobbyManager.InLobby && !lobbyManager.IsHost; bool isPlaying = _controller.Backend.IsPlaying; bool flag2 = !string.IsNullOrWhiteSpace(_controller.Backend.StatusOverlayText); bool flag3 = flag && _controller.IsMkvAudioSwitching(); bool flag4 = _controller.IsMkvSubtitleExtracting(); string text = null; if (flag4) { text = "Subtitles Loading..."; } else if (isPlaying && !flag2) { text = _controller.GetCurrentSubtitleText(); } bool flag5 = !string.IsNullOrEmpty(text); bool flag6 = flag3; for (int i = 0; i < _screens.Count; i++) { ScreenInstance screenInstance = _screens[i]; if (screenInstance == null || (Object)(object)screenInstance.SubtitleText == (Object)null) { continue; } Transform parent = ((TMP_Text)screenInstance.SubtitleText).transform.parent; GameObject val = ((parent != null) ? ((Component)parent).gameObject : null); if (!(flag5 || flag6)) { if ((Object)(object)val != (Object)null && val.activeSelf) { val.SetActive(false); } if (!string.IsNullOrEmpty(((TMP_Text)screenInstance.SubtitleText).text)) { ((TMP_Text)screenInstance.SubtitleText).text = string.Empty; } UpdateSubtitleBgPanels(screenInstance, show: false); if ((Object)(object)screenInstance.AudioLoadingText != (Object)null) { if (!string.IsNullOrEmpty(((TMP_Text)screenInstance.AudioLoadingText).text)) { ((TMP_Text)screenInstance.AudioLoadingText).text = string.Empty; } if (((Component)screenInstance.AudioLoadingText).gameObject.activeSelf) { ((Component)screenInstance.AudioLoadingText).gameObject.SetActive(false); } } continue; } if ((Object)(object)val != (Object)null && !val.activeSelf) { val.SetActive(true); } bool flag7 = !string.Equals(((TMP_Text)screenInstance.SubtitleText).text, text ?? string.Empty, StringComparison.Ordinal); if (flag7) { ((TMP_Text)screenInstance.SubtitleText).text = text ?? string.Empty; ((TMP_Text)screenInstance.SubtitleText).ForceMeshUpdate(true, true); } bool activeSelf = ((Component)screenInstance.SubtitleText).gameObject.activeSelf; if (activeSelf != flag5) { ((Component)screenInstance.SubtitleText).gameObject.SetActive(flag5); } if (flag7 || activeSelf != flag5) { UpdateSubtitleBgPanels(screenInstance, flag5); } if ((Object)(object)screenInstance.AudioLoadingText != (Object)null) { if (!string.Equals(((TMP_Text)screenInstance.AudioLoadingText).text, flag6 ? "Audio channel loading..." : string.Empty, StringComparison.Ordinal)) { ((TMP_Text)screenInstance.AudioLoadingText).text = (flag6 ? "Audio channel loading..." : string.Empty); } if (((Component)screenInstance.AudioLoadingText).gameObject.activeSelf != flag6) { ((Component)screenInstance.AudioLoadingText).gameObject.SetActive(flag6); } } } } private void UpdateSubtitleBgPanels(ScreenInstance screen, bool show) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) RectTransform[] subtitleBgPanels = screen.SubtitleBgPanels; if (subtitleBgPanels == null) { return; } if (!show || (Object)(object)screen.SubtitleText == (Object)null) { for (int i = 0; i < subtitleBgPanels.Length; i++) { if ((Object)(object)subtitleBgPanels[i] != (Object)null && ((Component)subtitleBgPanels[i]).gameObject.activeSelf) { ((Component)subtitleBgPanels[i]).gameObject.SetActive(false); } } return; } TMP_TextInfo textInfo = ((TMP_Text)screen.SubtitleText).textInfo; int num = textInfo?.lineCount ?? 0; Vector2 subtitleTextCenter = screen.SubtitleTextCenter; for (int j = 0; j < subtitleBgPanels.Length; j++) { RectTransform val = subtitleBgPanels[j]; if ((Object)(object)val == (Object)null) { continue; } if (j >= num || textInfo == null) { if (((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(false); } continue; } TMP_LineInfo val2 = textInfo.lineInfo[j]; if (val2.characterCount == 0) { if (((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(false); } continue; } float num2 = val2.lineExtents.max.x - val2.lineExtents.min.x; float num3 = val2.ascender - val2.descender; if (num2 < 1f || num3 < 1f) { if (((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(false); } continue; } float num4 = (val2.lineExtents.min.x + val2.lineExtents.max.x) * 0.5f; float num5 = (val2.ascender + val2.descender) * 0.5f; val.anchoredPosition = subtitleTextCenter + new Vector2(num4, num5); val.sizeDelta = new Vector2(num2 + 24f, num3 + 10f); if (!((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(true); } } } private void Clear() { for (int i = 0; i < _screens.Count; i++) { _screens[i]?.Dispose(); } _screens.Clear(); } } public static class YouTube { private sealed class PendingRequest { public readonly List> OnResolved = new List>(); public readonly List> OnError = new List>(); } private readonly struct StreamCacheEntry { public readonly string Url; public readonly DateTime Expiry; public StreamCacheEntry(string url, DateTime expiry) { Url = url; Expiry = expiry; } } private sealed class LocalFileServer { private HttpListener _listener; private int _port; private bool _started; private readonly object _lock = new object(); public string Serve(string filePath) { EnsureStarted(); string arg = Uri.EscapeDataString(Path.GetFileName(filePath)); return $"http://127.0.0.1:{_port}/{arg}"; } private void EnsureStarted() { lock (_lock) { if (!_started) { _listener = new HttpListener(); _port = GetFreePort(); _listener.Prefixes.Add($"http://127.0.0.1:{_port}/"); _listener.Start(); _started = true; Logger.LogInfo((object)$"[YouTube] Local file server started on port {_port}"); ThreadPool.QueueUserWorkItem(delegate { AcceptLoop(); }); } } } private static int GetFreePort() { TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0); tcpListener.Start(); int port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; tcpListener.Stop(); return port; } private void AcceptLoop() { while (true) { HttpListener listener = _listener; if (listener != null && listener.IsListening) { HttpListenerContext ctx; try { ctx = _listener.GetContext(); } catch { break; } ThreadPool.QueueUserWorkItem(delegate { HandleRequest(ctx); }); continue; } break; } } private void HandleRequest(HttpListenerContext ctx) { try { string stringToUnescape = ctx.Request.Url.AbsolutePath.TrimStart(new char[1] { '/' }); string path = Uri.UnescapeDataString(stringToUnescape); string cacheDirectory = GetCacheDirectory(); string text = Path.Combine(cacheDirectory, path); if (!File.Exists(text)) { Logger.LogWarning((object)("[YouTube] Server: file not found: " + text)); ctx.Response.StatusCode = 404; ctx.Response.Close(); return; } long length = new FileInfo(text).Length; ctx.Response.ContentType = "video/mp4"; ctx.Response.AddHeader("Accept-Ranges", "bytes"); string text2 = ctx.Request.Headers["Range"]; long num = 0L; long num2 = length - 1; if (!string.IsNullOrEmpty(text2) && text2.StartsWith("bytes=")) { string text3 = text2.Substring("bytes=".Length); int num3 = text3.IndexOf('-'); if (num3 >= 0) { string s = text3.Substring(0, num3); string s2 = text3.Substring(num3 + 1); long result; bool flag = long.TryParse(s, out result); long result2; bool flag2 = long.TryParse(s2, out result2); if (flag && flag2) { num = result; num2 = result2; } else if (flag) { num = result; } else if (flag2) { num = Math.Max(0L, length - result2); } } num2 = Math.Min(num2, length - 1); num = Math.Max(0L, Math.Min(num, num2)); ctx.Response.StatusCode = 206; ctx.Response.AddHeader("Content-Range", $"bytes {num}-{num2}/{length}"); } else { ctx.Response.StatusCode = 200; } long num4 = num2 - num + 1; ctx.Response.ContentLength64 = num4; using FileStream fileStream = new FileStream(text, FileMode.Open, FileAccess.Read, FileShare.Read); fileStream.Seek(num, SeekOrigin.Begin); byte[] array = new byte[65536]; long num5 = num4; while (num5 > 0) { int count = (int)Math.Min(array.Length, num5); int num6 = fileStream.Read(array, 0, count); if (num6 == 0) { break; } ctx.Response.OutputStream.Write(array, 0, num6); num5 -= num6; } } catch (Exception ex) { if (ex.Message.IndexOf("forcibly closed by the remote host", StringComparison.OrdinalIgnoreCase) >= 0) { Logger.LogInfo((object)"[YouTube] Server request closed by client."); } else { Logger.LogError((object)("[YouTube] Server request error: " + ex.Message)); } try { ctx.Response.StatusCode = 500; } catch { } } finally { try { ctx.Response.Close(); } catch { } } } } private const int DefaultTargetHeight = 480; private static readonly TimeSpan StreamUrlCacheDuration = TimeSpan.FromHours(4.0); private static readonly ManualLogSource Logger = Logger.CreateLogSource("SyncVideo.YouTube"); private static readonly Dictionary StreamUrlCache = new Dictionary(StringComparer.Ordinal); private static readonly object CacheLock = new object(); private static readonly Dictionary PendingRequests = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary ActiveProcesses = new Dictionary(StringComparer.Ordinal); private static readonly HashSet CancelledRequests = new HashSet(StringComparer.Ordinal); private static readonly LocalFileServer FileServer = new LocalFileServer(); private static readonly HashSet _activeDownloadPaths = new HashSet(StringComparer.OrdinalIgnoreCase); private static string _cachedFfmpegPath; private static bool _ffmpegPathSearched; private static string _cachedYtDlpPath; private static bool _ytDlpPathSearched; private static float _lastCacheClearTime = -999f; private const float CacheClearCooldown = 2f; public static void ResolveAsync(string videoId, string originalUrl, Action onResolved, Action onError) { if (string.IsNullOrEmpty(videoId)) { onError?.Invoke("No video ID provided!"); return; } int configTargetResolutionHeight = GetConfigTargetResolutionHeight(); string text = ((SyncVideoPlugin.Settings?.UseFFmpeg?.Value).GetValueOrDefault() ? FindFfmpeg() : null); if (text != null) { DownloadAndCacheAsync(videoId, originalUrl, configTargetResolutionHeight, text, onResolved, onError); } else { StreamUrlResolveAsync(videoId, originalUrl, configTargetResolutionHeight, onResolved, onError); } } public static void ValidateSuggestionAsync(string originalUrl, string videoId, string directPlayableUrl, Action onSuccess, Action onError) { if (string.IsNullOrWhiteSpace(originalUrl)) { onError?.Invoke("URL Error!"); return; } if (!UrlNormalizer.ValidateSubmissionUrl(originalUrl, videoId, directPlayableUrl, out var error)) { onError?.Invoke(error); return; } ThreadPool.QueueUserWorkItem(delegate { try { if (string.IsNullOrEmpty(videoId)) { if (!Uri.TryCreate(directPlayableUrl ?? originalUrl, UriKind.Absolute, out Uri result) || (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps)) { onError?.Invoke("URL Error!"); } else { onSuccess?.Invoke(originalUrl); } } else { string text = FindYtDlp(); if (text == null) { onError?.Invoke("Video not supported!"); } else { if (!IsLivestream(text, originalUrl)) { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "--no-playlist --no-warnings --skip-download --print title -- \"" + originalUrl + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; using Process process = Process.Start(startInfo); StringBuilder stderrCapture = new StringBuilder(); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stderrCapture.AppendLine(e.Data); } }; string multiline = process.StandardOutput.ReadToEnd(); string stderr = stderrCapture.ToString(); process.WaitForExit(20000); if (process.ExitCode != 0) { if (IsUrlError(stderr)) { onError?.Invoke("URL Error!"); } else if (IsAgeRestrictedError(stderr)) { onError?.Invoke("Video not supported!\nVideo is age-restricted."); } else { onError?.Invoke("Video not supported!"); } } else { string text2 = FirstNonEmpty(multiline); if (string.IsNullOrWhiteSpace(text2)) { onError?.Invoke("Video not supported!"); } else { onSuccess?.Invoke(text2); } } return; } onError?.Invoke("Video not supported!"); } } } catch { onError?.Invoke("Video not supported!"); } }); } public static bool IsFfmpegAvailable() { return (SyncVideoPlugin.Settings?.UseFFmpeg?.Value).GetValueOrDefault() && FindFfmpeg() != null; } public static void ClearAllCache() { string cacheDirectory = GetCacheDirectory(); if (!Directory.Exists(cacheDirectory)) { return; } string[] files = Directory.GetFiles(cacheDirectory, "*.mp4"); if (files.Length == 0) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup - _lastCacheClearTime < 2f) { return; } _lastCacheClearTime = realtimeSinceStartup; lock (CacheLock) { string[] array = files; foreach (string text in array) { if (_activeDownloadPaths.Contains(text)) { Logger.LogInfo((object)("[YouTube] Skipping active download during cache clear: " + Path.GetFileName(text))); continue; } try { File.Delete(text); } catch { } } } Logger.LogInfo((object)"[YouTube] Cache cleared."); } public static void InvalidateCache(string videoId) { if (string.IsNullOrEmpty(videoId)) { return; } lock (CacheLock) { List list = new List(); foreach (string key in StreamUrlCache.Keys) { if (key.StartsWith(videoId + "@", StringComparison.Ordinal)) { list.Add(key); } } foreach (string item in list) { StreamUrlCache.Remove(item); } } string cacheDirectory = GetCacheDirectory(); if (!Directory.Exists(cacheDirectory)) { return; } string[] files = Directory.GetFiles(cacheDirectory, SanitiseId(videoId) + "_*.mp4"); foreach (string path in files) { try { File.Delete(path); } catch { } } } private static void DownloadAndCacheAsync(string videoId, string originalUrl, int targetHeight, string ffmpegPath, Action onResolved, Action onError) { string cachedFile = GetCachedFilePath(videoId, targetHeight); if (File.Exists(cachedFile)) { Logger.LogInfo((object)$"[YouTube] Video already in cache folder: {videoId} @ {targetHeight}p"); onResolved?.Invoke(FileServer.Serve(cachedFile)); return; } string requestKey = $"download:{videoId}@{targetHeight}"; if (!TryBeginPendingRequest(requestKey, onResolved, onError)) { return; } lock (CacheLock) { _activeDownloadPaths.Add(cachedFile); } ThreadPool.QueueUserWorkItem(delegate { try { string text = FindYtDlp(); if (text == null) { CompletePendingError(requestKey, "yt-dlp.exe not found!"); } else if (IsLivestream(text, originalUrl)) { Logger.LogInfo((object)("[YouTube] Blocked download of livestream: " + videoId)); CompletePendingError(requestKey, "Livestreams are not supported!"); } else { Directory.CreateDirectory(GetCacheDirectory()); string text2 = Path.GetDirectoryName(ffmpegPath) ?? string.Empty; Logger.LogInfo((object)$"[YouTube] Downloading {videoId} @ {targetHeight}p (ffmpeg merge)..."); ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "--no-playlist --no-warnings --ffmpeg-location \"" + text2 + "\" -f \"" + BuildFormatWithFfmpeg(targetHeight) + "\" --merge-output-format mp4 --postprocessor-args \"ffmpeg:-movflags +faststart\" -o \"" + cachedFile + "\" -- \"" + originalUrl + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; bool flag = false; string text3 = null; for (int i = 1; i <= 2 && !flag; i++) { CleanupPartialDownloadFiles(videoId, targetHeight); using (Process process = Process.Start(startInfo)) { RegisterActiveProcess(requestKey, process); if (IsRequestCancelled(requestKey)) { try { if (!process.HasExited) { process.Kill(); } return; } catch { return; } } process.BeginOutputReadLine(); process.OutputDataReceived += delegate { }; string multiline = process.StandardError.ReadToEnd(); process.WaitForExit(300000); if (process.ExitCode != 0 || !WaitForFileReady(cachedFile, 2000)) { TryDelete(cachedFile); text3 = FirstNonEmpty(multiline) ?? "Unknown yt-dlp error."; Logger.LogError((object)$"[YouTube] Download failed ({process.ExitCode}) attempt {i}: {text3}"); if (i >= 2 || !IsRetryableDownloadError(text3)) { CompletePendingError(requestKey, IsAgeRestrictedError(text3) ? "Video not supported!\nVideo is age-restricted." : ("Download failed: " + text3)); return; } Thread.Sleep(300); continue; } flag = true; } break; } if (!flag) { CompletePendingError(requestKey, IsAgeRestrictedError(text3) ? "Video not supported!\nVideo is age-restricted." : ("Download failed: " + (text3 ?? "Unknown yt-dlp error."))); } else { Logger.LogInfo((object)$"[YouTube] Download complete: {videoId} @ {targetHeight}p"); CompletePendingSuccess(requestKey, FileServer.Serve(cachedFile)); } } } catch (Exception ex) { Logger.LogError((object)$"[YouTube] Exception: {ex}"); CompletePendingError(requestKey, "YouTube exception: " + ex.Message); } finally { lock (CacheLock) { _activeDownloadPaths.Remove(cachedFile); } } }); } private static void StreamUrlResolveAsync(string videoId, string originalUrl, int targetHeight, Action onResolved, Action onError) { string cacheKey = $"{videoId}@{targetHeight}"; lock (CacheLock) { if (StreamUrlCache.TryGetValue(cacheKey, out var value) && DateTime.UtcNow < value.Expiry) { Logger.LogInfo((object)$"[YouTube] URL cache hit: {videoId} @ {targetHeight}p"); onResolved?.Invoke(value.Url); return; } } string requestKey = "stream:" + cacheKey; if (!TryBeginPendingRequest(requestKey, onResolved, onError)) { return; } ThreadPool.QueueUserWorkItem(delegate { try { string text = FindYtDlp(); if (text == null) { CompletePendingError(requestKey, "yt-dlp.exe not found!"); return; } Logger.LogInfo((object)$"[YouTube] Loading Video URL: {videoId} @ {targetHeight}p"); ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "--no-playlist --no-warnings -f \"" + BuildFormatWithoutFfmpeg(targetHeight) + "\" --get-url -- \"" + originalUrl + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; using Process process = Process.Start(startInfo); RegisterActiveProcess(requestKey, process); if (IsRequestCancelled(requestKey)) { try { if (!process.HasExited) { process.Kill(); } return; } catch { return; } } StringBuilder stderrCapture = new StringBuilder(); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stderrCapture.AppendLine(e.Data); } }; string text2 = process.StandardOutput.ReadToEnd(); string multiline = stderrCapture.ToString(); process.WaitForExit(30000); if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(text2)) { string text3 = FirstNonEmpty(multiline) ?? "No output from yt-dlp!"; Logger.LogError((object)$"[YouTube] yt-dlp error ({process.ExitCode}): {text3}"); CompletePendingError(requestKey, IsAgeRestrictedError(text3) ? "Video not supported!\nVideo is age-restricted." : ("yt-dlp error: " + text3)); } else { string text4 = FirstNonEmpty(text2); if (string.IsNullOrEmpty(text4)) { CompletePendingError(requestKey, "yt-dlp returned an empty URL."); } else { Logger.LogInfo((object)("[YouTube] Loaded " + videoId + ": " + text4.Substring(0, Math.Min(80, text4.Length)) + "...")); lock (CacheLock) { StreamUrlCache[cacheKey] = new StreamCacheEntry(text4, DateTime.UtcNow + StreamUrlCacheDuration); } CompletePendingSuccess(requestKey, text4); } } } catch (Exception ex) { Logger.LogError((object)$"[YouTube] Exception: {ex}"); CompletePendingError(requestKey, "YouTube Exception: " + ex.Message); } }); } public static void CancelAllPendingRequests() { List list = null; lock (CacheLock) { foreach (string item in new List(PendingRequests.Keys)) { CancelledRequests.Add(item); } PendingRequests.Clear(); HashSet hashSet = new HashSet(); list = new List(ActiveProcesses.Count); foreach (Process value in ActiveProcesses.Values) { if (value != null && hashSet.Add(value)) { list.Add(value); } } ActiveProcesses.Clear(); } foreach (Process item2 in list) { try { if (!item2.HasExited) { item2.Kill(); } } catch { } } } private static void RegisterActiveProcess(string requestKey, Process process) { if (process == null) { return; } lock (CacheLock) { ActiveProcesses[requestKey] = process; } } private static void UnregisterActiveProcess(string requestKey, Process process = null) { lock (CacheLock) { if (ActiveProcesses.TryGetValue(requestKey, out var value) && (process == null || value == process)) { ActiveProcesses.Remove(requestKey); } } } private static bool IsRequestCancelled(string requestKey) { lock (CacheLock) { return CancelledRequests.Contains(requestKey); } } private static bool TryBeginPendingRequest(string requestKey, Action onResolved, Action onError) { lock (CacheLock) { if (PendingRequests.TryGetValue(requestKey, out var value)) { value.OnResolved.Add(onResolved); value.OnError.Add(onError); Logger.LogInfo((object)("[YouTube] Joining pending request: " + requestKey)); return false; } CancelledRequests.Remove(requestKey); value = new PendingRequest(); value.OnResolved.Add(onResolved); value.OnError.Add(onError); PendingRequests[requestKey] = value; return true; } } private static void CompletePendingSuccess(string requestKey, string resolvedUrl) { PendingRequest value = null; bool flag; lock (CacheLock) { ActiveProcesses.Remove(requestKey); flag = CancelledRequests.Remove(requestKey); if (PendingRequests.TryGetValue(requestKey, out value)) { PendingRequests.Remove(requestKey); } } if (value == null || flag) { return; } foreach (Action item in value.OnResolved) { item?.Invoke(resolvedUrl); } } private static void CompletePendingError(string requestKey, string errorMessage) { PendingRequest value = null; bool flag; lock (CacheLock) { ActiveProcesses.Remove(requestKey); flag = CancelledRequests.Remove(requestKey); if (PendingRequests.TryGetValue(requestKey, out value)) { PendingRequests.Remove(requestKey); } } if (value == null || flag) { return; } foreach (Action item in value.OnError) { item?.Invoke(errorMessage); } } private static bool IsUrlError(string stderr) { string text = (stderr ?? string.Empty).ToLowerInvariant(); return text.Contains("unsupported url") || text.Contains("invalid url") || text.Contains("bad url") || text.Contains("no such file") || text.Contains("unable to download webpage") || text.Contains("unable to extract") || text.Contains("not a valid url"); } private static bool IsAgeRestrictedError(string stderr) { string text = (stderr ?? string.Empty).ToLowerInvariant(); return text.Contains("age-restricted") || text.Contains("age restricted") || text.Contains("sign in to confirm your age") || text.Contains("this video may be inappropriate for some users"); } private static bool WaitForFileReady(string path, int timeoutMs) { int num = Environment.TickCount + timeoutMs; while (Environment.TickCount < num) { try { if (File.Exists(path)) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); if (fileStream.Length > 0) { return true; } } } catch { } Thread.Sleep(100); } return File.Exists(path); } private static bool IsRetryableDownloadError(string error) { if (string.IsNullOrWhiteSpace(error)) { return false; } return error.IndexOf("WinError 2", StringComparison.OrdinalIgnoreCase) >= 0 || error.IndexOf("cannot find the file specified", StringComparison.OrdinalIgnoreCase) >= 0 || error.IndexOf("Unable to rename file", StringComparison.OrdinalIgnoreCase) >= 0; } private static void CleanupPartialDownloadFiles(string videoId, int targetHeight) { string cacheDirectory = GetCacheDirectory(); if (!Directory.Exists(cacheDirectory)) { return; } string text = $"{SanitiseId(videoId)}_{targetHeight}"; string[] files = Directory.GetFiles(cacheDirectory, text + "*"); foreach (string text2 in files) { if (!string.Equals(text2, GetCachedFilePath(videoId, targetHeight), StringComparison.OrdinalIgnoreCase)) { try { File.Delete(text2); } catch { } } } } private static string BuildFormatWithFfmpeg(int height) { return $"bestvideo[height<={height}][ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]" + $"/bestvideo[height<={height}][ext=mp4][vcodec^=avc]+bestaudio[acodec^=mp4a]" + $"/bestvideo[height<={height}][ext=mp4]+bestaudio[ext=m4a]" + $"/best[height<={height}][ext=mp4]" + "/best[ext=mp4]"; } private static string BuildFormatWithoutFfmpeg(int height) { if (height >= 720) { return "22/45/best[height<=720][fps>30][ext=mp4][protocol=https]/best[height<=720][fps>30][protocol=https]/best[height<=720][ext=mp4][protocol=https]/best[height<=720][protocol=https]/18/43/best[protocol=https]"; } if (height >= 480) { return "59/78/44" + $"/best[height<={height}][fps>30][ext=mp4][protocol=https]" + $"/best[height<={height}][fps>30][protocol=https]" + $"/best[height<={height}][ext=mp4][protocol=https]" + $"/best[height<={height}][protocol=https]" + "/18/43/best[protocol=https]"; } if (height >= 360) { return "18/43/best[height<=360][ext=mp4][protocol=https]/best[height<=360][protocol=https]/best[protocol=https]"; } return $"best[height<={height}][ext=mp4][protocol=https]" + $"/best[height<={height}][protocol=https]" + "/best[protocol=https]"; } internal static string GetCacheDirectory() { string path = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; return Path.Combine(path, "cache"); } private static string GetCachedFilePath(string videoId, int height) { return Path.Combine(GetCacheDirectory(), $"{SanitiseId(videoId)}_{height}.mp4"); } private static string SanitiseId(string videoId) { StringBuilder stringBuilder = new StringBuilder(); foreach (char c in videoId) { if (char.IsLetterOrDigit(c) || c == '-' || c == '_') { stringBuilder.Append(c); } } return (stringBuilder.Length > 0) ? stringBuilder.ToString() : "unknown"; } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } private static int GetConfigTargetResolutionHeight() { string text = SyncVideoPlugin.Settings?.YouTubeStreamResolution?.Value; if (!string.IsNullOrEmpty(text)) { string text2 = text.Trim().ToLowerInvariant().Replace('×', 'x'); int num = text2.IndexOf('x'); if (num >= 0 && int.TryParse(text2.Substring(num + 1), out var result) && result > 0) { return result; } } return 480; } public static void ResolveTitleAsync(string originalUrl, string videoId, Action onTitle) { if (string.IsNullOrEmpty(videoId)) { onTitle?.Invoke(originalUrl ?? string.Empty); return; } ThreadPool.QueueUserWorkItem(delegate { try { string text = FindYtDlp(); if (text == null) { onTitle?.Invoke(originalUrl); return; } ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "--no-playlist --no-warnings --print title -- \"" + originalUrl + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; using Process process = Process.Start(startInfo); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate { }; string text2 = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(15000); onTitle?.Invoke(string.IsNullOrEmpty(text2) ? originalUrl : text2); } catch { onTitle?.Invoke(originalUrl); } }); } public static void ResolveTitleAndUploaderAsync(string originalUrl, string videoId, Action onResolved) { if (string.IsNullOrEmpty(videoId)) { onResolved?.Invoke(originalUrl ?? string.Empty, string.Empty); return; } ThreadPool.QueueUserWorkItem(delegate { try { string text = FindYtDlp(); if (text == null) { onResolved?.Invoke(originalUrl ?? string.Empty, string.Empty); return; } ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = "--no-playlist --no-warnings --skip-download --print title --print uploader -- \"" + originalUrl + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; using Process process = Process.Start(startInfo); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate { }; string text2 = process.StandardOutput.ReadToEnd(); process.WaitForExit(15000); string arg = originalUrl ?? string.Empty; string arg2 = string.Empty; int num = 0; string[] array = text2.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text3 in array) { string text4 = text3.Trim(); if (!string.IsNullOrEmpty(text4)) { if (num == 0) { arg = text4; } else { arg2 = text4; } if (++num == 2) { break; } } } onResolved?.Invoke(arg, arg2); } catch { onResolved?.Invoke(originalUrl ?? string.Empty, string.Empty); } }); } private static bool IsLivestream(string ytDlpPath, string url) { try { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ytDlpPath, Arguments = "--no-playlist --no-warnings --print is_live -- \"" + url + "\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true }; using Process process = Process.Start(startInfo); process.BeginErrorReadLine(); process.ErrorDataReceived += delegate { }; string a = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(15000); return string.Equals(a, "True", StringComparison.OrdinalIgnoreCase); } catch { return false; } } private static string FindYtDlp() { lock (CacheLock) { if (_ytDlpPathSearched) { return _cachedYtDlpPath; } _ytDlpPathSearched = true; string text = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; if (!string.IsNullOrEmpty(text)) { string[] array = new string[2] { "yt-dlp.exe", "yt-dlp" }; foreach (string path in array) { string text2 = Path.Combine(text, path); if (File.Exists(text2)) { _cachedYtDlpPath = text2; return text2; } } } _cachedYtDlpPath = null; return null; } } private static string FindFfmpeg() { lock (CacheLock) { if (_ffmpegPathSearched) { return _cachedFfmpegPath; } _ffmpegPathSearched = true; string text = SyncVideoPlugin.Settings?.PluginDirectory ?? string.Empty; if (!string.IsNullOrEmpty(text)) { string[] array = new string[2] { "ffmpeg.exe", "ffmpeg" }; foreach (string path in array) { string text2 = Path.Combine(text, path); if (File.Exists(text2)) { _cachedFfmpegPath = text2; return text2; } } } string text3 = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; string[] array2 = text3.Split(new char[1] { Path.PathSeparator }); foreach (string text4 in array2) { string[] array3 = new string[2] { "ffmpeg.exe", "ffmpeg" }; foreach (string path2 in array3) { try { string text5 = Path.Combine(text4.Trim(), path2); if (File.Exists(text5)) { _cachedFfmpegPath = text5; return text5; } } catch { } } } _cachedFfmpegPath = null; return null; } } private static string FirstNonEmpty(string multiline) { if (string.IsNullOrEmpty(multiline)) { return null; } string[] array = multiline.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { string text2 = text.Trim(); if (!string.IsNullOrEmpty(text2)) { return text2; } } return null; } } } namespace SyncVideo.Phone { public sealed class AppSyncVideo : CustomApp { internal static Sprite _iconSprite; private bool _built; private bool _lastInLobbyState; private bool _pendingLobbyOpen; public static void Initialize() { string text = Path.Combine(SyncVideoPlugin.Settings.PluginDirectory, "syncvideo_icon.png"); _iconSprite = (File.Exists(text) ? TextureUtility.LoadSprite(text) : null); PhoneAPI.RegisterApp("sync video", _iconSprite); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Sync Video", _iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); if (AppSyncVideoLobby.ReopenRequested && SyncVideoPlugin.LobbyManager.InLobby) { AppSyncVideoLobby.ReopenRequested = false; ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); return; } AppSyncVideoLobby.ReopenRequested = false; _lastInLobbyState = SyncVideoPlugin.LobbyManager.InLobby; BuildButtons(); _built = true; } public override void OnAppDisable() { ((App)this).OnAppDisable(); _built = false; _pendingLobbyOpen = false; } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); bool inLobby = SyncVideoPlugin.LobbyManager.InLobby; if (_pendingLobbyOpen && inLobby && SyncVideoPlugin.LobbyManager.CurrentLobby != null) { _pendingLobbyOpen = false; ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); } else if (!_built || inLobby != _lastInLobbyState) { _lastInLobbyState = inLobby; BuildButtons(); _built = true; } } public override void OnPressLeft() { ((App)this).OnPressLeft(); } private static string GetLobbyUiToggleLabel() { return SyncVideoPlugin.Settings.HideNativeLobbyUi.Value ? "Lobby UI: Hidden" : "Lobby UI: Visible"; } private static void TrySetButtonLabel(object button, string label) { if (button == null || string.IsNullOrEmpty(label)) { return; } Type type = button.GetType(); try { MethodInfo method = type.GetMethod("SetText", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method != null) { method.Invoke(button, new object[1] { label }); return; } MethodInfo method2 = type.GetMethod("UpdateTextLabel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method2 != null) { method2.Invoke(button, new object[1] { label }); return; } FieldInfo fieldInfo = type.GetField("textLabel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("label", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (fieldInfo != null) { object? value = fieldInfo.GetValue(button); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if (val != null) { val.text = label; return; } object? value2 = fieldInfo.GetValue(button); GameObject val2 = (GameObject)((value2 is GameObject) ? value2 : null); if (val2 != null) { TMP_Text componentInChildren = val2.GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.text = label; return; } } } Component val3 = (Component)((button is Component) ? button : null); if (val3 != null) { TMP_Text componentInChildren2 = val3.GetComponentInChildren(true); if ((Object)(object)componentInChildren2 != (Object)null) { componentInChildren2.text = label; } } } catch { } } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } base.ScrollView.RemoveAllButtons(); if (!SyncVideoPlugin.ScreenManager.HasAnyScreensInMap()) { SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("No Screens!"); base.ScrollView.AddButton((PhoneButton)(object)val); SimplePhoneButton val2 = PhoneUIUtility.CreateSimpleButton("Re-scan Map"); ((PhoneButton)val2).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val2).OnConfirm, (Action)delegate { SyncVideoPlugin.ScreenManager.Rebind(); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val2); return; } VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; if (!lobbyManager.InLobby) { SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton(lobbyManager.OfflineModeEnabled ? "Host Offline" : "Host Lobby"); ((PhoneButton)val3).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val3).OnConfirm, (Action)delegate { _pendingLobbyOpen = true; lobbyManager.HostLobby(); if (SyncVideoPlugin.LobbyManager.InLobby && SyncVideoPlugin.LobbyManager.CurrentLobby != null) { _pendingLobbyOpen = false; ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); } }); base.ScrollView.AddButton((PhoneButton)(object)val3); if (!lobbyManager.OfflineModeEnabled) { SimplePhoneButton val4 = PhoneUIUtility.CreateSimpleButton("Join Lobby"); ((PhoneButton)val4).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val4).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoPublicLobbies)); }); base.ScrollView.AddButton((PhoneButton)(object)val4); } else { SimplePhoneButton val5 = PhoneUIUtility.CreateSimpleButton("Offline Mode Enabled"); base.ScrollView.AddButton((PhoneButton)(object)val5); } } else { SimplePhoneButton val6 = PhoneUIUtility.CreateSimpleButton("Current Lobby"); ((PhoneButton)val6).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val6).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); }); base.ScrollView.AddButton((PhoneButton)(object)val6); string text = (lobbyManager.IsHost ? "Close Lobby" : "Leave Lobby"); SimplePhoneButton val7 = PhoneUIUtility.CreateSimpleButton(text); ((PhoneButton)val7).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val7).OnConfirm, (Action)delegate { HudManager.Reset(); SyncVideoPlugin.LobbyManager.LeaveLobby(); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val7); } if (SyncVideoPlugin.Settings.ShowRefreshScreensButton.Value) { SimplePhoneButton val8 = PhoneUIUtility.CreateSimpleButton("Refresh Screens"); ((PhoneButton)val8).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val8).OnConfirm, (Action)delegate { SyncVideoPlugin.ScreenManager.Rebind(); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val8); } } } public sealed class AppSyncVideoLobby : CustomApp { public static bool ReopenRequested; private bool _showingWaitingState; private bool _reopenAfterPromptCancel; private object _playPauseButton; private string _lastPlayPauseLabel; private object _suggestionsToggleButton; private string _lastSuggestionsToggleLabel; private object _viewSuggestionsButton; private bool _lastSuggestionsOpen; private string _lastViewSuggestionsLabel; private object _viewerSuggestButton; private bool _lastViewerSuggestionsOpen; private object _playlistModeButton; private string _lastPlaylistModeLabel; private object _setUrlButton; private string _lastSetUrlLabel; private object _mkvSettingsButton; private bool _lastShowMkvSettings; private int _suggestionValidationRequestId; private bool _suggestionValidationInProgress; public override bool Available => false; private static bool ShouldBlockInput => UrlPromptOverlay.IsVisible && !UrlPromptOverlay.IsConfirmation; public static void Initialize() { PhoneAPI.RegisterApp("sync video lobby", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Video Lobby", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); BuildButtons(); } public override void OnPressLeft() { if (UrlPromptOverlay.IsVisible) { if (UrlPromptOverlay.IsConfirmation) { UrlPromptOverlay.Hide(); return; } _reopenAfterPromptCancel = true; UrlPromptOverlay.Hide(); } else { _reopenAfterPromptCancel = false; ((App)this).OnPressLeft(); } } public override void OnPressRight() { if (UrlPromptOverlay.IsVisible && UrlPromptOverlay.IsConfirmation) { UrlPromptOverlay.Hide(); } else if (!ShouldBlockInput) { ((CustomApp)this).OnPressRight(); } } public override void OnPressUp() { if (UrlPromptOverlay.IsVisible && UrlPromptOverlay.IsConfirmation) { UrlPromptOverlay.Hide(); } else if (!ShouldBlockInput) { ((CustomApp)this).OnPressUp(); } } public override void OnPressDown() { if (UrlPromptOverlay.IsVisible && UrlPromptOverlay.IsConfirmation) { UrlPromptOverlay.Hide(); } else if (!ShouldBlockInput) { ((CustomApp)this).OnPressDown(); } } public override void OnHoldUp() { if (!ShouldBlockInput) { ((CustomApp)this).OnHoldUp(); } } public override void OnHoldDown() { if (!ShouldBlockInput) { ((CustomApp)this).OnHoldDown(); } } public override void OnReleaseUp() { if (!ShouldBlockInput) { ((CustomApp)this).OnReleaseUp(); } } public override void OnReleaseDown() { if (!ShouldBlockInput) { ((CustomApp)this).OnReleaseDown(); } } public override void OnReleaseRight() { if (!ShouldBlockInput) { ((CustomApp)this).OnReleaseRight(); } } public override void OnAppDisable() { ReopenRequested = _reopenAfterPromptCancel && SyncVideoPlugin.LobbyManager.InLobby && !SyncVideoPlugin.LobbyManager.LeaveInProgress; _reopenAfterPromptCancel = false; ((App)this).OnAppDisable(); UrlPromptOverlay.Hide(); _playPauseButton = null; _lastPlayPauseLabel = null; _suggestionsToggleButton = null; _lastSuggestionsToggleLabel = null; _viewSuggestionsButton = null; _lastViewSuggestionsLabel = null; _viewerSuggestButton = null; _playlistModeButton = null; _lastPlaylistModeLabel = null; _setUrlButton = null; _lastSetUrlLabel = null; _mkvSettingsButton = null; _lastShowMkvSettings = false; _suggestionValidationInProgress = false; _suggestionValidationRequestId++; } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if (!SyncVideoPlugin.LobbyManager.InLobby) { if (_showingWaitingState) { VideoLobby currentLobby = SyncVideoPlugin.LobbyManager.CurrentLobby; if (currentLobby != null) { BuildButtons(); } } else { HudManager.Reset(); ReopenRequested = false; ((App)this).MyPhone.CloseCurrentApp(); } return; } if (_showingWaitingState && SyncVideoPlugin.LobbyManager.CurrentLobby != null) { BuildButtons(); } if (_playPauseButton != null) { string playPauseLabel = GetPlayPauseLabel(); if (playPauseLabel != _lastPlayPauseLabel) { _lastPlayPauseLabel = playPauseLabel; TrySetButtonLabel(_playPauseButton, playPauseLabel); } } if (_suggestionsToggleButton != null) { string suggestionsToggleLabel = GetSuggestionsToggleLabel(); if (suggestionsToggleLabel != _lastSuggestionsToggleLabel) { _lastSuggestionsToggleLabel = suggestionsToggleLabel; TrySetButtonLabel(_suggestionsToggleButton, suggestionsToggleLabel); } } if (_viewSuggestionsButton != null) { bool suggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen; string viewSuggestionsLabel = GetViewSuggestionsLabel(suggestionsOpen); if (suggestionsOpen != _lastSuggestionsOpen || viewSuggestionsLabel != _lastViewSuggestionsLabel) { _lastSuggestionsOpen = suggestionsOpen; _lastViewSuggestionsLabel = viewSuggestionsLabel; TrySetButtonLabel(_viewSuggestionsButton, viewSuggestionsLabel); } } if (_viewerSuggestButton != null) { bool suggestionsOpen2 = SyncVideoPlugin.LobbyManager.SuggestionsOpen; if (suggestionsOpen2 != _lastViewerSuggestionsOpen) { _lastViewerSuggestionsOpen = suggestionsOpen2; TrySetButtonLabel(_viewerSuggestButton, suggestionsOpen2 ? "Suggest Video" : "Suggest Video"); } } if (_playlistModeButton != null) { string playlistModeLabel = GetPlaylistModeLabel(); if (playlistModeLabel != _lastPlaylistModeLabel) { _lastPlaylistModeLabel = playlistModeLabel; TrySetButtonLabel(_playlistModeButton, playlistModeLabel); } } if (_setUrlButton != null) { string hostSubmitLabel = GetHostSubmitLabel(); if (hostSubmitLabel != _lastSetUrlLabel) { _lastSetUrlLabel = hostSubmitLabel; TrySetButtonLabel(_setUrlButton, hostSubmitLabel); } } bool flag = SyncVideoPlugin.SyncController.ShouldShowMkvSettings(); if (flag != _lastShowMkvSettings) { _lastShowMkvSettings = flag; BuildButtons(); } else if (_suggestionValidationInProgress && !UrlPromptOverlay.IsVisible) { _suggestionValidationInProgress = false; _suggestionValidationRequestId++; } } private static string GetPlayPauseLabel() { VideoLobby currentLobby = SyncVideoPlugin.LobbyManager.CurrentLobby; bool flag = currentLobby != null && !string.IsNullOrEmpty(currentLobby.CurrentUrl); bool flag2 = currentLobby?.IsPlaying ?? false; bool flag3 = currentLobby?.HasEnded ?? false; return (flag && !flag2 && !flag3 && currentLobby != null && currentLobby.MediaTimeSeconds > 0.05) ? "Play / Pause" : "Play / Pause"; } private static string GetVolumeBar() { if (SyncVideoPlugin.SyncController.IsMuted) { return "\n[MUTED]"; } float localVolume = SyncVideoPlugin.SyncController.LocalVolume; int num = Mathf.Clamp(Mathf.RoundToInt(localVolume * 10f), 0, 10); int value = num * 10; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("["); for (int i = 0; i < 10; i++) { if (i < num) { string value2 = VolumeGradientHex((float)i / 9f); stringBuilder.Append("#"); } else { stringBuilder.Append("-"); } } stringBuilder.Append("] ").Append(value).Append("%"); return "\n" + stringBuilder?.ToString() + ""; } private static string VolumeGradientHex(float t) { int num; int num2; if (t <= 0.5f) { num = Mathf.RoundToInt(t * 2f * 255f); num2 = 255; } else { num = 255; num2 = Mathf.RoundToInt((1f - t) * 2f * 255f); } return num.ToString("X2") + num2.ToString("X2") + "00"; } private static string GetVolumeDownLabel() { string volumeBar = GetVolumeBar(); return (SyncVideoPlugin.SyncController.LocalVolume <= 0f) ? ("No Volume" + volumeBar) : ("Volume Down (-)" + volumeBar); } private static string GetVolumeUpLabel() { string volumeBar = GetVolumeBar(); return (SyncVideoPlugin.SyncController.LocalVolume >= 1f) ? ("Max Volume" + volumeBar) : ("Volume Up (+)" + volumeBar); } private static string GetLobbyUiToggleLabel() { return SyncVideoPlugin.Settings.HideNativeLobbyUi.Value ? "Lobby UI: Hidden" : "Lobby UI: Visible"; } private static string GetLobbyOpenToggleLabel() { VideoLobby currentLobby = SyncVideoPlugin.LobbyManager.CurrentLobby; return (currentLobby != null && currentLobby.IsOpen) ? "Lobby: Open" : "Lobby: Closed"; } private static string GetAutoplayToggleLabel() { return SyncVideoPlugin.Settings.HostAutoplay.Value ? "Autoplay: On" : "Autoplay: Off"; } private static string GetSuggestionsToggleLabel() { return SyncVideoPlugin.LobbyManager.SuggestionsOpen ? "Suggestions: Open" : "Suggestions: Closed"; } private static string GetPlaylistModeLabel() { return SyncVideoPlugin.LobbyManager.PlaylistModeEnabled ? "Playlist Mode: On" : "Playlist Mode: Off"; } private static string GetHostSubmitLabel() { return SyncVideoPlugin.LobbyManager.PlaylistModeEnabled ? "Add URL to Queue" : "Set URL"; } private static string GetViewSuggestionsLabel(bool suggestionsOpen) { if (SyncVideoPlugin.LobbyManager.PlaylistModeEnabled) { return "Playlist Queue"; } return suggestionsOpen ? "View Suggestions" : "View Suggestions"; } private static string GetMuteLabel() { return SyncVideoPlugin.SyncController.IsMuted ? "Mute / Unmute" : "Mute / Unmute"; } private static string FormatVideoTime(double totalSeconds) { int num = (int)Math.Max(0.0, totalSeconds); int num2 = num / 3600; int num3 = num % 3600 / 60; int num4 = num % 60; return (num2 > 0) ? $"{num2}:{num3:D2}:{num4:D2}" : $"{num3}:{num4:D2}"; } private static bool TryParseTimeInput(string input, out double seconds) { seconds = 0.0; if (string.IsNullOrWhiteSpace(input)) { return false; } string[] array = input.Trim().Split(new char[1] { ':' }); try { if (array.Length == 1) { if (!double.TryParse(array[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var result) || result < 0.0) { return false; } seconds = result; return true; } if (array.Length == 2) { if (!int.TryParse(array[0], out var result2) || result2 < 0) { return false; } if (!double.TryParse(array[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var result3) || result3 < 0.0) { return false; } seconds = (double)result2 * 60.0 + result3; return true; } if (array.Length == 3) { if (!int.TryParse(array[0], out var result4) || result4 < 0) { return false; } if (!int.TryParse(array[1], out var result5) || result5 < 0) { return false; } if (!double.TryParse(array[2], NumberStyles.Any, CultureInfo.InvariantCulture, out var result6) || result6 < 0.0) { return false; } seconds = (double)result4 * 3600.0 + (double)result5 * 60.0 + result6; return true; } } catch { } return false; } private static void TrySetButtonLabel(object button, string label) { if (button == null || string.IsNullOrEmpty(label)) { return; } Type type = button.GetType(); try { MethodInfo method = type.GetMethod("SetText", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method != null) { method.Invoke(button, new object[1] { label }); } MethodInfo method2 = type.GetMethod("UpdateTextLabel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method2 != null) { method2.Invoke(button, new object[1] { label }); } FieldInfo fieldInfo = type.GetField("textLabel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? type.GetField("label", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (fieldInfo != null) { object? value = fieldInfo.GetValue(button); TMP_Text val = (TMP_Text)((value is TMP_Text) ? value : null); if (val != null) { val.richText = true; val.text = label; return; } object? value2 = fieldInfo.GetValue(button); GameObject val2 = (GameObject)((value2 is GameObject) ? value2 : null); if (val2 != null) { TMP_Text componentInChildren = val2.GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.richText = true; componentInChildren.text = label; return; } } } Component val3 = (Component)((button is Component) ? button : null); if (val3 != null) { TMP_Text componentInChildren2 = val3.GetComponentInChildren(true); if ((Object)(object)componentInChildren2 != (Object)null) { componentInChildren2.richText = true; componentInChildren2.text = label; } } } catch { } } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } VideoLobby currentLobby = SyncVideoPlugin.LobbyManager.CurrentLobby; base.ScrollView.RemoveAllButtons(); if (currentLobby == null) { _showingWaitingState = true; SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("Waiting for Lobby..."); base.ScrollView.AddButton((PhoneButton)(object)val); } _showingWaitingState = false; if (SyncVideoPlugin.LobbyManager.IsHost) { _setUrlButton = null; _lastSetUrlLabel = GetHostSubmitLabel(); SimplePhoneButton val2 = (SimplePhoneButton)(_setUrlButton = PhoneUIUtility.CreateSimpleButton(_lastSetUrlLabel)); ((PhoneButton)val2).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val2).OnConfirm, (Action)delegate { if (!_suggestionValidationInProgress) { UrlPromptOverlay.Show(delegate(string value) { string videoId2; string directPlayableUrl2; string normalized2 = UrlNormalizer.Normalize(value, out videoId2, out directPlayableUrl2); if (!UrlNormalizer.ValidateSubmissionUrl(normalized2, videoId2, directPlayableUrl2, out var error2)) { UrlPromptOverlay.ShowConfirmation(error2 + "\n\nPress left arrow key to go back."); } else if (SyncVideoPlugin.LobbyManager.PlaylistModeEnabled) { if (UrlNormalizer.IsDirectSuggestionUrlTooLong(normalized2, videoId2, directPlayableUrl2, out error2)) { UrlPromptOverlay.ShowConfirmation(error2 + "\n\nPress left arrow key to go back."); } else { _suggestionValidationInProgress = true; int validationToken2 = ++_suggestionValidationRequestId; UrlPromptOverlay.ShowConfirmation("Checking video...\n\nPress left arrow key to go back."); YouTube.ValidateSuggestionAsync(normalized2, videoId2, directPlayableUrl2, delegate(string title) { SyncVideoPlugin.SyncController.EnqueueMainThreadAction(delegate { if (validationToken2 == _suggestionValidationRequestId) { _suggestionValidationInProgress = false; if (SyncVideoPlugin.LobbyManager.QueueHostUrl(normalized2, string.IsNullOrWhiteSpace(title) ? normalized2 : title)) { UrlPromptOverlay.ShowConfirmation("Added to queue!\n\nPress left arrow key to go back."); } else { UrlPromptOverlay.ShowConfirmation("URL Error!\n\nPress left arrow key to go back."); } } }); }, delegate(string errorMessage) { SyncVideoPlugin.SyncController.EnqueueMainThreadAction(delegate { if (validationToken2 == _suggestionValidationRequestId) { _suggestionValidationInProgress = false; UrlPromptOverlay.ShowConfirmation(errorMessage + "\n\nPress left arrow key to go back."); } }); }); } } else { SyncVideoPlugin.SyncController.HostSetUrl(normalized2); } }, showAsViewerSuggestion: true); } }); base.ScrollView.AddButton((PhoneButton)(object)val2); _playPauseButton = null; SimplePhoneButton playPause = PhoneUIUtility.CreateSimpleButton(GetPlayPauseLabel()); _playPauseButton = playPause; _lastPlayPauseLabel = GetPlayPauseLabel(); SimplePhoneButton obj = playPause; ((PhoneButton)obj).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj).OnConfirm, (Action)delegate { VideoLobby currentLobby3 = SyncVideoPlugin.LobbyManager.CurrentLobby; if (currentLobby3 != null && currentLobby3.IsPlaying) { SyncVideoPlugin.SyncController.HostPause(); } else { SyncVideoPlugin.SyncController.HostPlay(); } TrySetButtonLabel(playPause, GetPlayPauseLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)playPause); SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton("Restart Video"); ((PhoneButton)val3).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val3).OnConfirm, (Action)delegate { VideoLobby currentLobby2 = SyncVideoPlugin.LobbyManager.CurrentLobby; if (currentLobby2 == null || string.IsNullOrEmpty(currentLobby2.CurrentUrl)) { UrlPromptOverlay.ShowConfirmation("No Video Loaded!\n\nPress left arrow key to go back."); } else { SyncVideoPlugin.SyncController.HostRestart(); } }); base.ScrollView.AddButton((PhoneButton)(object)val3); _lastShowMkvSettings = SyncVideoPlugin.SyncController.ShouldShowMkvSettings(); if (_lastShowMkvSettings) { SimplePhoneButton val4 = (SimplePhoneButton)(_mkvSettingsButton = PhoneUIUtility.CreateSimpleButton("MKV Settings")); ((PhoneButton)val4).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val4).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoMkvSettings)); }); base.ScrollView.AddButton((PhoneButton)(object)val4); } } if (!SyncVideoPlugin.LobbyManager.IsHost && (_lastShowMkvSettings = SyncVideoPlugin.SyncController.ShouldShowMkvSettings())) { SimplePhoneButton val5 = (SimplePhoneButton)(_mkvSettingsButton = PhoneUIUtility.CreateSimpleButton("MKV Settings")); ((PhoneButton)val5).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val5).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoMkvSettings)); }); base.ScrollView.AddButton((PhoneButton)(object)val5); } SimplePhoneButton volumeDown = PhoneUIUtility.CreateSimpleButton(GetVolumeDownLabel()); SimplePhoneButton volumeUp = PhoneUIUtility.CreateSimpleButton(GetVolumeUpLabel()); SimplePhoneButton mute = PhoneUIUtility.CreateSimpleButton(GetMuteLabel()); SimplePhoneButton obj2 = volumeDown; ((PhoneButton)obj2).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj2).OnConfirm, (Action)delegate { if (SyncVideoPlugin.SyncController.IsMuted) { SyncVideoPlugin.SyncController.ToggleMute(); } SyncVideoPlugin.SyncController.AdjustLocalVolume(-0.1f); TrySetButtonLabel(volumeDown, GetVolumeDownLabel()); TrySetButtonLabel(volumeUp, GetVolumeUpLabel()); TrySetButtonLabel(mute, GetMuteLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)volumeDown); SimplePhoneButton obj3 = volumeUp; ((PhoneButton)obj3).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj3).OnConfirm, (Action)delegate { if (SyncVideoPlugin.SyncController.IsMuted) { SyncVideoPlugin.SyncController.ToggleMute(); } SyncVideoPlugin.SyncController.AdjustLocalVolume(0.1f); TrySetButtonLabel(volumeUp, GetVolumeUpLabel()); TrySetButtonLabel(volumeDown, GetVolumeDownLabel()); TrySetButtonLabel(mute, GetMuteLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)volumeUp); SimplePhoneButton obj4 = mute; ((PhoneButton)obj4).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj4).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.ToggleMute(); TrySetButtonLabel(mute, GetMuteLabel()); TrySetButtonLabel(volumeDown, GetVolumeDownLabel()); TrySetButtonLabel(volumeUp, GetVolumeUpLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)mute); if (SyncVideoPlugin.LobbyManager.IsHost) { SimplePhoneButton val6 = PhoneUIUtility.CreateSimpleButton("Seek << 5 sec"); ((PhoneButton)val6).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val6).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.HostSeekRelative(-5.0); }); base.ScrollView.AddButton((PhoneButton)(object)val6); SimplePhoneButton val7 = PhoneUIUtility.CreateSimpleButton("Seek >> 5 sec"); ((PhoneButton)val7).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val7).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.HostSeekRelative(5.0); }); base.ScrollView.AddButton((PhoneButton)(object)val7); SimplePhoneButton val8 = PhoneUIUtility.CreateSimpleButton("Seek to Time..."); ((PhoneButton)val8).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val8).OnConfirm, (Action)delegate { string text3 = FormatVideoTime(SyncVideoPlugin.SyncController.Backend.CurrentTimeSeconds); UrlPromptOverlay.Show(delegate(string input) { if (!TryParseTimeInput(input, out var seconds)) { UrlPromptOverlay.ShowConfirmation("Invalid time!\n\nPress left arrow key to go back."); } else { double videoDurationSeconds = SyncVideoPlugin.SyncController.VideoDurationSeconds; if (videoDurationSeconds <= 0.0 || seconds < 0.0 || seconds > videoDurationSeconds - 3.0) { string message = ((!(videoDurationSeconds > 0.0)) ? "No video loaded!\n\nPress left arrow key to go back." : ("Invalid time!\n\nValid range: 0:00 - " + FormatVideoTime(videoDurationSeconds - 3.0) + "\n\nPress left arrow key to go back.")); UrlPromptOverlay.ShowConfirmation(message); } else { SyncVideoPlugin.SyncController.HostSeekToTime(seconds); } } }, showAsViewerSuggestion: false, "Seek to Time", "Enter the specific time you want to seek to.\nType \"4:20\" to seek four minutes and twenty seconds in.\n\nPress left arrow key to cancel.", "Current Video Time: " + text3); }); base.ScrollView.AddButton((PhoneButton)(object)val8); } if (SyncVideoPlugin.Settings.ShowScreenPositionMenu.Value) { SimplePhoneButton val9 = PhoneUIUtility.CreateSimpleButton("Adjust Screen"); ((PhoneButton)val9).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val9).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoScreenOptions)); }); base.ScrollView.AddButton((PhoneButton)(object)val9); } SimplePhoneButton lobbyUiToggle = PhoneUIUtility.CreateSimpleButton(GetLobbyUiToggleLabel()); SimplePhoneButton obj5 = lobbyUiToggle; ((PhoneButton)obj5).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj5).OnConfirm, (Action)delegate { SyncVideoPlugin.Settings.HideNativeLobbyUi.Value = !SyncVideoPlugin.Settings.HideNativeLobbyUi.Value; TrySetButtonLabel(lobbyUiToggle, GetLobbyUiToggleLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)lobbyUiToggle); if (SyncVideoPlugin.LobbyManager.IsHost) { SimplePhoneButton hideHud2 = PhoneUIUtility.CreateSimpleButton(HudManager.GetLabel()); SimplePhoneButton obj6 = hideHud2; ((PhoneButton)obj6).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj6).OnConfirm, (Action)delegate { HudManager.Cycle(); TrySetButtonLabel(hideHud2, HudManager.GetLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)hideHud2); SimplePhoneButton autoplayToggle = PhoneUIUtility.CreateSimpleButton(GetAutoplayToggleLabel()); SimplePhoneButton obj7 = autoplayToggle; ((PhoneButton)obj7).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj7).OnConfirm, (Action)delegate { SyncVideoPlugin.Settings.HostAutoplay.Value = !SyncVideoPlugin.Settings.HostAutoplay.Value; TrySetButtonLabel(autoplayToggle, GetAutoplayToggleLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)autoplayToggle); SimplePhoneButton lobbyOpenToggle = PhoneUIUtility.CreateSimpleButton(GetLobbyOpenToggleLabel()); TrySetButtonLabel(lobbyOpenToggle, GetLobbyOpenToggleLabel()); SimplePhoneButton obj8 = lobbyOpenToggle; ((PhoneButton)obj8).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj8).OnConfirm, (Action)delegate { SyncVideoPlugin.LobbyManager.ToggleLobbyOpen(); TrySetButtonLabel(lobbyOpenToggle, GetLobbyOpenToggleLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)lobbyOpenToggle); _playlistModeButton = null; SimplePhoneButton playlistMode = PhoneUIUtility.CreateSimpleButton(GetPlaylistModeLabel()); _playlistModeButton = playlistMode; _lastPlaylistModeLabel = GetPlaylistModeLabel(); SimplePhoneButton obj9 = playlistMode; ((PhoneButton)obj9).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj9).OnConfirm, (Action)delegate { SyncVideoPlugin.LobbyManager.TogglePlaylistMode(); TrySetButtonLabel(playlistMode, GetPlaylistModeLabel()); TrySetButtonLabel(_setUrlButton, GetHostSubmitLabel()); TrySetButtonLabel(_viewSuggestionsButton, GetViewSuggestionsLabel(SyncVideoPlugin.LobbyManager.SuggestionsOpen)); }); base.ScrollView.AddButton((PhoneButton)(object)playlistMode); _suggestionsToggleButton = null; _viewSuggestionsButton = null; SimplePhoneButton val10 = (SimplePhoneButton)(_suggestionsToggleButton = PhoneUIUtility.CreateSimpleButton(GetSuggestionsToggleLabel())); _lastSuggestionsToggleLabel = GetSuggestionsToggleLabel(); ((PhoneButton)val10).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val10).OnConfirm, (Action)delegate { SyncVideoPlugin.LobbyManager.SetSuggestionsOpen(!SyncVideoPlugin.LobbyManager.SuggestionsOpen); }); base.ScrollView.AddButton((PhoneButton)(object)val10); _lastViewSuggestionsLabel = GetViewSuggestionsLabel(_lastSuggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen); SimplePhoneButton val11 = (SimplePhoneButton)(_viewSuggestionsButton = PhoneUIUtility.CreateSimpleButton(_lastViewSuggestionsLabel)); ((PhoneButton)val11).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val11).OnConfirm, (Action)delegate { if (SyncVideoPlugin.LobbyManager.PlaylistModeEnabled || SyncVideoPlugin.LobbyManager.SuggestionsOpen) { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoSuggestions)); } }); base.ScrollView.AddButton((PhoneButton)(object)val11); SimplePhoneButton val12 = PhoneUIUtility.CreateSimpleButton("Kick Viewers"); ((PhoneButton)val12).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val12).OnConfirm, (Action)delegate { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobbyKick)); }); base.ScrollView.AddButton((PhoneButton)(object)val12); } else { SimplePhoneButton hideHud = PhoneUIUtility.CreateSimpleButton(HudManager.GetLabel()); SimplePhoneButton obj10 = hideHud; ((PhoneButton)obj10).OnConfirm = (Action)Delegate.Combine(((PhoneButton)obj10).OnConfirm, (Action)delegate { HudManager.Cycle(); TrySetButtonLabel(hideHud, HudManager.GetLabel()); }); base.ScrollView.AddButton((PhoneButton)(object)hideHud); _viewerSuggestButton = null; string text = ((_lastViewerSuggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen) ? "Suggest Video" : "Suggest Video"); SimplePhoneButton val13 = (SimplePhoneButton)(_viewerSuggestButton = PhoneUIUtility.CreateSimpleButton(text)); ((PhoneButton)val13).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val13).OnConfirm, (Action)delegate { if (SyncVideoPlugin.LobbyManager.SuggestionsOpen && !_suggestionValidationInProgress) { _suggestionValidationInProgress = true; int validationToken = ++_suggestionValidationRequestId; UrlPromptOverlay.Show(delegate(string url) { string videoId; string directPlayableUrl; string normalized = UrlNormalizer.Normalize(url, out videoId, out directPlayableUrl); string error = string.Empty; if (string.IsNullOrWhiteSpace(videoId)) { _suggestionValidationInProgress = false; UrlPromptOverlay.ShowConfirmation("Video not suggested!\n\nOnly YouTube links allowed!\n\nPress left arrow key to go back."); } else if (UrlNormalizer.IsDirectSuggestionUrlTooLong(normalized, videoId, directPlayableUrl, out error)) { _suggestionValidationInProgress = false; UrlPromptOverlay.ShowConfirmation(error + "\n\nPress left arrow key to go back."); } else if (string.IsNullOrWhiteSpace(normalized) || !UrlNormalizer.ValidateSubmissionUrl(normalized, videoId, directPlayableUrl, out error)) { _suggestionValidationInProgress = false; UrlPromptOverlay.ShowConfirmation(error + "\n\nPress left arrow key to go back."); } else { UrlPromptOverlay.ShowConfirmation("Checking video...\n\nPress left arrow key to cancel."); YouTube.ValidateSuggestionAsync(normalized, videoId, directPlayableUrl, delegate(string title) { SyncVideoPlugin.SyncController.EnqueueMainThreadAction(delegate { if (validationToken == _suggestionValidationRequestId) { _suggestionValidationInProgress = false; SyncVideoPlugin.LobbyManager.SendSuggestion(normalized, string.IsNullOrWhiteSpace(title) ? normalized : title); UrlPromptOverlay.ShowConfirmation("Video submitted!\n\nPress left arrow to go back."); } }); }, delegate(string errorMessage) { SyncVideoPlugin.SyncController.EnqueueMainThreadAction(delegate { if (validationToken == _suggestionValidationRequestId) { _suggestionValidationInProgress = false; UrlPromptOverlay.ShowConfirmation(errorMessage + "\n\nPress left arrow key to go back."); } }); }); } }, showAsViewerSuggestion: true); } }); base.ScrollView.AddButton((PhoneButton)(object)val13); } string text2 = (SyncVideoPlugin.LobbyManager.IsHost ? "Close Lobby" : "Leave Lobby"); SimplePhoneButton val14 = PhoneUIUtility.CreateSimpleButton(text2); ((PhoneButton)val14).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val14).OnConfirm, (Action)delegate { HudManager.Reset(); SyncVideoPlugin.LobbyManager.LeaveLobby(); ((App)this).MyPhone.CloseCurrentApp(); }); base.ScrollView.AddButton((PhoneButton)(object)val14); } } public sealed class AppSyncVideoLobbyKick : CustomApp { public override bool Available => false; public static void Initialize() { PhoneAPI.RegisterApp("sync video kick viewers", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Kick Viewers", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); BuildButtons(); } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if (!SyncVideoPlugin.LobbyManager.InLobby || !SyncVideoPlugin.LobbyManager.IsHost) { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); } } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } base.ScrollView.RemoveAllButtons(); VideoLobby currentLobby = SyncVideoPlugin.LobbyManager.CurrentLobby; if (currentLobby == null || !SyncVideoPlugin.LobbyManager.IsHost) { SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("No Viewers"); base.ScrollView.AddButton((PhoneButton)(object)val); return; } ushort num = (ushort)((SyncVideoPlugin.Transport != null) ? SyncVideoPlugin.Transport.LocalPlayerId : 0); bool flag = false; foreach (ushort memberId in currentLobby.Members) { if (memberId == 0 || memberId == num || memberId == currentLobby.HostId) { continue; } string playerName = GetPlayerName(memberId); SimplePhoneButton val2 = PhoneUIUtility.CreateSimpleButton(playerName); ((PhoneButton)val2).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val2).OnConfirm, (Action)delegate { if ((Object)(object)ClientController.Instance != (Object)null && ClientController.Instance.ClientLobbyManager != null) { ClientController.Instance.ClientLobbyManager.KickPlayer(memberId); } BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val2); flag = true; } if (!flag) { SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton("No Viewers to Kick"); base.ScrollView.AddButton((PhoneButton)(object)val3); } } private static string GetPlayerName(ushort playerId) { try { string playerDisplayName = SyncVideoPlugin.LobbyManager.GetPlayerDisplayName(playerId); if (!string.IsNullOrWhiteSpace(playerDisplayName)) { return playerDisplayName; } } catch { } return "Player " + playerId; } } public sealed class AppSyncVideoMkvSettings : CustomApp { private int _lastTrackCount = -1; private int _lastSelectedTrack = -1; private int _lastSubCount = -2; private int _lastSelectedSub = -2; private bool _lastAudioProbing = false; private bool _lastSubProbing = false; private bool _lastSubExtracting = false; public override bool Available => false; public static void Initialize() { PhoneAPI.RegisterApp("sync video mkv settings", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("MKV Settings", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); _lastTrackCount = -1; _lastSelectedTrack = -1; _lastSubCount = -2; _lastSelectedSub = -2; _lastAudioProbing = false; _lastSubProbing = false; _lastSubExtracting = false; BuildButtons(); } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if (!SyncVideoPlugin.LobbyManager.InLobby || !(SyncVideoPlugin.Settings?.EnableMkvSupport?.Value).GetValueOrDefault() || !SyncVideoPlugin.SyncController.IsCurrentMkv()) { AppSyncVideoLobby.ReopenRequested = false; ((App)this).MyPhone.CloseCurrentApp(); return; } int mkvAudioTrackCount = SyncVideoPlugin.SyncController.GetMkvAudioTrackCount(); int mkvSelectedAudioTrack = SyncVideoPlugin.SyncController.GetMkvSelectedAudioTrack(); int mkvSubtitleTrackCount = SyncVideoPlugin.SyncController.GetMkvSubtitleTrackCount(); int mkvSelectedSubtitleTrack = SyncVideoPlugin.SyncController.GetMkvSelectedSubtitleTrack(); bool flag = SyncVideoPlugin.SyncController.IsMkvAudioProbing(); bool flag2 = SyncVideoPlugin.SyncController.IsMkvSubtitleProbing(); bool flag3 = SyncVideoPlugin.SyncController.IsMkvSubtitleExtracting(); if (mkvAudioTrackCount != _lastTrackCount || mkvSelectedAudioTrack != _lastSelectedTrack || flag != _lastAudioProbing || mkvSubtitleTrackCount != _lastSubCount || mkvSelectedSubtitleTrack != _lastSelectedSub || flag2 != _lastSubProbing || flag3 != _lastSubExtracting) { BuildButtons(); } } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } base.ScrollView.RemoveAllButtons(); int mkvAudioTrackCount = SyncVideoPlugin.SyncController.GetMkvAudioTrackCount(); int mkvSelectedAudioTrack = SyncVideoPlugin.SyncController.GetMkvSelectedAudioTrack(); bool lastAudioProbing = SyncVideoPlugin.SyncController.IsMkvAudioProbing(); _lastTrackCount = mkvAudioTrackCount; _lastSelectedTrack = mkvSelectedAudioTrack; _lastAudioProbing = lastAudioProbing; SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("--- Audio Tracks ---"); base.ScrollView.AddButton((PhoneButton)(object)val); if (mkvAudioTrackCount <= 0) { SimplePhoneButton val2 = PhoneUIUtility.CreateSimpleButton("No audio tracks found."); base.ScrollView.AddButton((PhoneButton)(object)val2); } else { for (int i = 0; i < mkvAudioTrackCount; i++) { int trackIndex = i; string text = SyncVideoPlugin.SyncController.GetMkvAudioTrackLabel(trackIndex); if (trackIndex == mkvSelectedAudioTrack) { text = "" + text + ""; } SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton(text); ((PhoneButton)val3).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val3).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.SelectMkvAudioTrack(trackIndex); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val3); } } int mkvSubtitleTrackCount = SyncVideoPlugin.SyncController.GetMkvSubtitleTrackCount(); int mkvSelectedSubtitleTrack = SyncVideoPlugin.SyncController.GetMkvSelectedSubtitleTrack(); bool flag = SyncVideoPlugin.SyncController.IsMkvSubtitleProbing(); bool flag2 = SyncVideoPlugin.SyncController.IsMkvSubtitleExtracting(); _lastSubCount = mkvSubtitleTrackCount; _lastSelectedSub = mkvSelectedSubtitleTrack; _lastSubProbing = flag; _lastSubExtracting = flag2; SimplePhoneButton val4 = PhoneUIUtility.CreateSimpleButton("--- Subtitle Tracks ---"); base.ScrollView.AddButton((PhoneButton)(object)val4); if (SubtitleManager.FindFfmpegPath() == null) { SimplePhoneButton val5 = PhoneUIUtility.CreateSimpleButton("Subtitles: Requires FFmpeg"); base.ScrollView.AddButton((PhoneButton)(object)val5); return; } if (flag) { SimplePhoneButton val6 = PhoneUIUtility.CreateSimpleButton("Subtitles: Scanning..."); base.ScrollView.AddButton((PhoneButton)(object)val6); return; } if (flag2) { SimplePhoneButton val7 = PhoneUIUtility.CreateSimpleButton("Subtitles: Loading track..."); base.ScrollView.AddButton((PhoneButton)(object)val7); return; } if (mkvSubtitleTrackCount <= 0) { SimplePhoneButton val8 = PhoneUIUtility.CreateSimpleButton("Subtitles: None found"); base.ScrollView.AddButton((PhoneButton)(object)val8); return; } string text2 = "None"; if (mkvSelectedSubtitleTrack < 0) { text2 = "None"; } SimplePhoneButton val9 = PhoneUIUtility.CreateSimpleButton(text2); ((PhoneButton)val9).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val9).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.SelectMkvSubtitleTrack(-1, BuildButtons); }); base.ScrollView.AddButton((PhoneButton)(object)val9); for (int j = 0; j < mkvSubtitleTrackCount; j++) { int subIndex = j; string text3 = SyncVideoPlugin.SyncController.GetMkvSubtitleTrackLabel(subIndex); if (subIndex == mkvSelectedSubtitleTrack) { text3 = "" + text3 + ""; } SimplePhoneButton val10 = PhoneUIUtility.CreateSimpleButton(text3); ((PhoneButton)val10).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val10).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.SelectMkvSubtitleTrack(subIndex, BuildButtons); }); base.ScrollView.AddButton((PhoneButton)(object)val10); } } } public sealed class AppSyncVideoPublicLobbies : CustomApp { private bool _pendingJoin; private string _pendingLobbyId = string.Empty; public override bool Available => false; public static void Initialize() { PhoneAPI.RegisterApp("sync video lobbies", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Video Lobbies", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); if (SyncVideoPlugin.LobbyManager.InLobby) { ((App)this).MyPhone.CloseCurrentApp(); } else { BuildButtons(); } } public override void OnAppDisable() { ((App)this).OnAppDisable(); _pendingJoin = false; _pendingLobbyId = string.Empty; } public override void OnPressLeft() { _pendingJoin = false; _pendingLobbyId = string.Empty; ((App)this).OnPressLeft(); } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if (_pendingJoin && SyncVideoPlugin.LobbyManager.InLobby && SyncVideoPlugin.LobbyManager.CurrentLobby != null && string.Equals(SyncVideoPlugin.LobbyManager.CurrentLobby.LobbyId, _pendingLobbyId, StringComparison.Ordinal)) { _pendingJoin = false; _pendingLobbyId = string.Empty; ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); } } private void RefreshLobbyButtons() { BuildButtons(); } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } base.ScrollView.RemoveAllButtons(); SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("Refresh"); ((PhoneButton)val).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val).OnConfirm, new Action(RefreshLobbyButtons)); base.ScrollView.AddButton((PhoneButton)(object)val); if (SyncVideoPlugin.LobbyManager.OfflineModeEnabled) { SimplePhoneButton val2 = PhoneUIUtility.CreateSimpleButton("Offline Mode Enabled"); base.ScrollView.AddButton((PhoneButton)(object)val2); } if (!SyncVideoPlugin.ScreenManager.HasAnyScreensInMap()) { SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton("No Screens!"); base.ScrollView.AddButton((PhoneButton)(object)val3); return; } VideoLobby[] array = SyncVideoPlugin.LobbyManager.Lobbies.OrderBy((VideoLobby x) => x.LobbyName).ToArray(); if (array.Length == 0) { SimplePhoneButton val4 = PhoneUIUtility.CreateSimpleButton("No Sync Video Lobbies Found"); base.ScrollView.AddButton((PhoneButton)(object)val4); return; } foreach (VideoLobby lobby in array) { SimplePhoneButton val5 = PhoneUIUtility.CreateSimpleButton(lobby.LobbyName); ((PhoneButton)val5).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val5).OnConfirm, (Action)delegate { _pendingJoin = true; _pendingLobbyId = lobby.LobbyId ?? string.Empty; SyncVideoPlugin.LobbyManager.JoinLobby(_pendingLobbyId); }); base.ScrollView.AddButton((PhoneButton)(object)val5); } } } public sealed class AppSyncVideoScreenOptions : CustomApp { private PhoneButton _summaryButton; public override bool Available => false; public static void Initialize() { PhoneAPI.RegisterApp("sync video screen", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Screen Position", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); BuildButtons(); } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if ((Object)(object)_summaryButton != (Object)null) { TrySetButtonLabel(_summaryButton, SyncVideoPlugin.ScreenManager.GetCurrentTransformSummary()); } } private void BuildButtons() { base.ScrollView.RemoveAllButtons(); AddAdjustButton("X -", delegate { SyncVideoPlugin.ScreenManager.AdjustX(-0.01f); }); AddAdjustButton("X +", delegate { SyncVideoPlugin.ScreenManager.AdjustX(0.01f); }); AddAdjustButton("Y -", delegate { SyncVideoPlugin.ScreenManager.AdjustY(-0.01f); }); AddAdjustButton("Y +", delegate { SyncVideoPlugin.ScreenManager.AdjustY(0.01f); }); AddAdjustButton("Z -", delegate { SyncVideoPlugin.ScreenManager.AdjustZ(-0.005f); }); AddAdjustButton("Z +", delegate { SyncVideoPlugin.ScreenManager.AdjustZ(0.005f); }); AddAdjustButton("Size -", delegate { SyncVideoPlugin.ScreenManager.AdjustSize(-0.02f); }); AddAdjustButton("Size +", delegate { SyncVideoPlugin.ScreenManager.AdjustSize(0.02f); }); AddAdjustButton("Wider", delegate { SyncVideoPlugin.ScreenManager.AdjustAspect(0.02f, 0f); }); AddAdjustButton("Taller", delegate { SyncVideoPlugin.ScreenManager.AdjustAspect(0f, 0.02f); }); AddAdjustButton("Reset to Default", delegate { SyncVideoPlugin.ScreenManager.ResetScreenTransform(); }); _summaryButton = (PhoneButton)(object)PhoneUIUtility.CreateSimpleButton(SyncVideoPlugin.ScreenManager.GetCurrentTransformSummary()); base.ScrollView.AddButton(_summaryButton); MakeButtonTextOneSizeSmaller(_summaryButton); } private void AddAdjustButton(string label, Action onConfirm) { SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton(label); ((PhoneButton)val).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val).OnConfirm, (Action)delegate { onConfirm(); }); base.ScrollView.AddButton((PhoneButton)(object)val); } private static void TrySetButtonLabel(PhoneButton button, string label) { if ((Object)(object)button == (Object)null || string.IsNullOrEmpty(label)) { return; } Type type = ((object)button).GetType(); try { MethodInfo method = type.GetMethod("SetText", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method != null) { method.Invoke(button, new object[1] { label }); return; } MethodInfo method2 = type.GetMethod("UpdateTextLabel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method2 != null) { method2.Invoke(button, new object[1] { label }); return; } TMP_Text val = ((button != null) ? ((Component)button).GetComponentInChildren(true) : null); if ((Object)(object)val != (Object)null) { val.text = label; } } catch { } } private static void MakeButtonTextOneSizeSmaller(PhoneButton button) { try { TMP_Text val = ((button != null) ? ((Component)button).GetComponentInChildren(true) : null); if ((Object)(object)val != (Object)null) { val.fontSize = Mathf.Max(1f, val.fontSize - 1f); } } catch { } } } public sealed class AppSyncVideoSuggestions : CustomApp { private int _lastSuggestionCount = -1; private bool _lastSuggestionsOpen; private string _lastSuggestionSignature = string.Empty; public override bool Available => false; public static void Initialize() { PhoneAPI.RegisterApp("sync video suggestions", (Sprite)null); } public override void OnAppInit() { ((CustomApp)this).OnAppInit(); ((CustomApp)this).CreateTitleBar("Suggestions", AppSyncVideo._iconSprite, 80f); base.ScrollView = PhoneScrollView.Create((CustomApp)(object)this, 275f, 1600f); } public override void OnAppEnable() { ((App)this).OnAppEnable(); ((CustomApp)this).CreateTitleBar(SyncVideoPlugin.LobbyManager.PlaylistModeEnabled ? "Playlist Queue" : "Suggestions", AppSyncVideo._iconSprite, 80f); _lastSuggestionCount = -1; _lastSuggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen; _lastSuggestionSignature = string.Empty; SyncVideoPlugin.LobbyManager.RequestSuggestionScan(); BuildButtons(); } public override void OnAppUpdate() { ((App)this).OnAppUpdate(); if (!SyncVideoPlugin.LobbyManager.InLobby || !SyncVideoPlugin.LobbyManager.IsHost) { ((App)this).MyPhone.OpenApp(typeof(AppSyncVideoLobby)); return; } List> orderedSuggestions = SyncVideoPlugin.LobbyManager.GetOrderedSuggestions(); int count = orderedSuggestions.Count; bool suggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen; string text = BuildSuggestionSignature(orderedSuggestions); if (count != _lastSuggestionCount || suggestionsOpen != _lastSuggestionsOpen || text != _lastSuggestionSignature) { BuildButtons(); } } private static string BuildSuggestionSignature(IReadOnlyList> suggestions) { if (suggestions == null || suggestions.Count == 0) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < suggestions.Count; i++) { KeyValuePair keyValuePair = suggestions[i]; stringBuilder.Append(keyValuePair.Key).Append('|').Append(keyValuePair.Value.Url ?? string.Empty) .Append('|') .Append(keyValuePair.Value.Title ?? string.Empty) .Append('|') .Append(keyValuePair.Value.ChannelName ?? string.Empty) .Append(';'); } return stringBuilder.ToString(); } private void BuildButtons() { if ((Object)(object)base.ScrollView == (Object)null) { return; } base.ScrollView.RemoveAllButtons(); if (!SyncVideoPlugin.LobbyManager.IsHost || SyncVideoPlugin.LobbyManager.CurrentLobby == null) { return; } List> orderedSuggestions = SyncVideoPlugin.LobbyManager.GetOrderedSuggestions(); _lastSuggestionCount = orderedSuggestions.Count; _lastSuggestionsOpen = SyncVideoPlugin.LobbyManager.SuggestionsOpen; _lastSuggestionSignature = BuildSuggestionSignature(orderedSuggestions); SimplePhoneButton val = PhoneUIUtility.CreateSimpleButton("Clear All"); ((PhoneButton)val).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val).OnConfirm, (Action)delegate { SyncVideoPlugin.LobbyManager.ClearSuggestions(); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val); if (orderedSuggestions.Count == 0) { SimplePhoneButton val2 = PhoneUIUtility.CreateSimpleButton(SyncVideoPlugin.LobbyManager.PlaylistModeEnabled ? "No videos added yet." : "No suggestions yet."); base.ScrollView.AddButton((PhoneButton)(object)val2); return; } foreach (KeyValuePair item in orderedSuggestions) { string suggestionEntryKey = item.Key; VideoLobbyManager.VideoSuggestion suggestion = item.Value; string buttonLabel = suggestion.GetButtonLabel(); SimplePhoneButton val3 = PhoneUIUtility.CreateSimpleButton(buttonLabel); ((PhoneButton)val3).OnConfirm = (Action)Delegate.Combine(((PhoneButton)val3).OnConfirm, (Action)delegate { SyncVideoPlugin.SyncController.HostSetUrl(suggestion.Url); SyncVideoPlugin.LobbyManager.RemoveSuggestion(suggestionEntryKey); BuildButtons(); }); base.ScrollView.AddButton((PhoneButton)(object)val3); } } } public sealed class UrlPromptOverlay : MonoBehaviour { private static UrlPromptOverlay _instance; private static TMP_FontAsset _cachedGameFont; private TMP_InputField _input; private TextMeshProUGUI _confirmationText; private TextMeshProUGUI _titleText; private TextMeshProUGUI _helpText; private GameObject _panelGo; private GameObject _inputGo; private GameObject _submitButtonGo; private GameObject _cancelButtonGo; private Action _onSubmit; private bool _fontApplied; private static float _suppressPhoneNavigationUntil = -1f; private static bool _showAsViewerSuggestion; private bool _cancelInputWasDown; private string _SeekToTitle; private string _SeekToHelpText; private string _SeekToPlaceholder; public static bool IsVisible => (Object)(object)_instance != (Object)null && ((Component)_instance).gameObject.activeSelf; public static bool IsConfirmation { get; private set; } public static bool ShouldSuppressPhoneNavigation => Time.unscaledTime <= _suppressPhoneNavigationUntil; private static void EnableMouseForOverlay() { Cursor.visible = true; Cursor.lockState = (CursorLockMode)0; try { GameInput gameInput = Core.Instance.GameInput; if (gameInput != null) { gameInput.EnableControllerMaps(BaseModule.IN_GAME_INPUT_MAPS, 0); gameInput.EnableControllerMaps(BaseModule.MENU_INPUT_MAPS, 0); } } catch { } } private static void RestoreMouseAfterOverlay() { Cursor.visible = false; Cursor.lockState = (CursorLockMode)1; try { GameInput gameInput = Core.Instance.GameInput; if (gameInput != null) { gameInput.DisableAllControllerMaps(0); gameInput.EnableControllerMaps(BaseModule.IN_GAME_INPUT_MAPS, 0); gameInput.EnableControllerMaps(BaseModule.MENU_INPUT_MAPS, 0); } } catch { } } private static bool IsCurrentUserHost() { try { return SyncVideoPlugin.LobbyManager != null && SyncVideoPlugin.LobbyManager.IsHost; } catch { return false; } } public static void Show(Action onSubmit, bool showAsViewerSuggestion = false, string customTitle = null, string customHelpText = null, string customPlaceholder = null) { EnsureInstance(); IsConfirmation = false; _showAsViewerSuggestion = showAsViewerSuggestion && !IsCurrentUserHost(); _instance._onSubmit = onSubmit; _instance._cancelInputWasDown = true; _instance._SeekToTitle = customTitle; _instance._SeekToHelpText = customHelpText; _instance._SeekToPlaceholder = customPlaceholder; ((Component)_instance).gameObject.SetActive(true); EnableMouseForOverlay(); _instance.ApplyPromptLayout(); _instance.UpdateHelpText(); _instance.ApplyGameFontToOverlay(); _instance._input.text = ((customPlaceholder != null) ? string.Empty : GetClipboardText()); _instance._input.caretPosition = _instance._input.text.Length; ((Selectable)_instance._input).Select(); _instance._input.ActivateInputField(); } public static void ShowConfirmation(string message) { EnsureInstance(); IsConfirmation = true; _showAsViewerSuggestion = false; _instance._onSubmit = null; _instance._cancelInputWasDown = true; ((Component)_instance).gameObject.SetActive(true); RestoreMouseAfterOverlay(); _instance.ApplyConfirmationLayout(message); _instance.ApplyGameFontToOverlay(); if ((Object)(object)EventSystem.current != (Object)null) { EventSystem.current.SetSelectedGameObject((GameObject)null); } } public static void Hide() { if (!((Object)(object)_instance == (Object)null)) { IsConfirmation = false; _showAsViewerSuggestion = false; _instance._SeekToTitle = null; _instance._SeekToHelpText = null; _instance._SeekToPlaceholder = null; _suppressPhoneNavigationUntil = Time.unscaledTime + 0.2f; _instance._cancelInputWasDown = false; RestoreMouseAfterOverlay(); ((Component)_instance).gameObject.SetActive(false); if ((Object)(object)EventSystem.current != (Object)null) { EventSystem.current.SetSelectedGameObject((GameObject)null); } } } private static void EnsureInstance() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)_instance != (Object)null)) { GameObject val = new GameObject("SyncVideoUrlPrompt"); val.transform.SetParent(((Component)Core.Instance.UIManager).transform, false); _instance = val.AddComponent(); _instance.Build(); } } private static string GetClipboardText() { try { Type type = Type.GetType("UnityEngine.GUIUtility, UnityEngine.IMGUIModule") ?? Type.GetType("UnityEngine.GUIUtility, UnityEngine"); if (type != null) { PropertyInfo property = type.GetProperty("systemCopyBuffer", BindingFlags.Static | BindingFlags.Public); if (property != null) { return (property.GetValue(null) as string) ?? string.Empty; } } } catch { } return string.Empty; } private static TMP_FontAsset TryGetGameFont() { if ((Object)(object)_cachedGameFont != (Object)null) { return _cachedGameFont; } try { TextMeshProUGUI[] array = Resources.FindObjectsOfTypeAll(); foreach (TextMeshProUGUI val in array) { if (!((Object)(object)val == (Object)null) && !((Object)(object)((TMP_Text)val).font == (Object)null) && ((Object)val).name == "HeaderLabel") { _cachedGameFont = ((TMP_Text)val).font; return _cachedGameFont; } } foreach (TextMeshProUGUI val2 in array) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)((TMP_Text)val2).font == (Object)null)) { string hierarchyPath = GetHierarchyPath(((TMP_Text)val2).transform); if (hierarchyPath.IndexOf("UIRoot", StringComparison.OrdinalIgnoreCase) >= 0 || hierarchyPath.IndexOf("Phone", StringComparison.OrdinalIgnoreCase) >= 0 || hierarchyPath.IndexOf("Overlay", StringComparison.OrdinalIgnoreCase) >= 0) { _cachedGameFont = ((TMP_Text)val2).font; return _cachedGameFont; } } } foreach (TextMeshProUGUI val3 in array) { if ((Object)(object)val3 != (Object)null && (Object)(object)((TMP_Text)val3).font != (Object)null) { _cachedGameFont = ((TMP_Text)val3).font; return _cachedGameFont; } } } catch { } return null; } private static string GetHierarchyPath(Transform transform) { if ((Object)(object)transform == (Object)null) { return string.Empty; } string text = ((Object)transform).name; while ((Object)(object)transform.parent != (Object)null) { transform = transform.parent; text = ((Object)transform).name + "/" + text; } return text; } private void ApplyGameFontToOverlay() { TMP_FontAsset val = TryGetGameFont(); if ((Object)(object)val == (Object)null) { return; } TMP_Text[] componentsInChildren = ((Component)this).GetComponentsInChildren(true); foreach (TMP_Text val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null) && (Object)(object)val2.font != (Object)(object)val) { val2.font = val; } } _fontApplied = true; } private static bool TryGetAxisRaw(string axisName, out float value) { value = 0f; try { value = Input.GetAxisRaw(axisName); return true; } catch { return false; } } private bool IsCancelInputPressedThisFrame() { bool flag = Input.GetKey((KeyCode)276) || Input.GetKey((KeyCode)27) || Input.GetKey((KeyCode)331); bool result = flag && !_cancelInputWasDown; _cancelInputWasDown = flag; return result; } private void Update() { if (!((Component)this).gameObject.activeSelf) { return; } if (!_fontApplied) { ApplyGameFontToOverlay(); } if (IsConfirmation) { Cursor.visible = false; Cursor.lockState = (CursorLockMode)1; if (IsCancelInputPressedThisFrame()) { Hide(); } return; } Cursor.visible = true; Cursor.lockState = (CursorLockMode)0; if (IsCancelInputPressedThisFrame()) { Cancel(); return; } if ((Object)(object)_input != (Object)null && !_input.isFocused) { _input.ActivateInputField(); } if (Input.GetKeyDown((KeyCode)13) || Input.GetKeyDown((KeyCode)271)) { SubmitCurrentValue(); } } private void SubmitCurrentValue() { string obj = (((Object)(object)_input != (Object)null) ? _input.text : string.Empty); RestoreMouseAfterOverlay(); ((Component)this).gameObject.SetActive(false); _onSubmit?.Invoke(obj); if ((Object)(object)EventSystem.current != (Object)null) { EventSystem.current.SetSelectedGameObject((GameObject)null); } } private void Cancel() { if ((Object)(object)_input != (Object)null) { _input.DeactivateInputField(false); } if ((Object)(object)EventSystem.current != (Object)null) { EventSystem.current.SetSelectedGameObject((GameObject)null); } RestoreMouseAfterOverlay(); Hide(); } private void UpdateHelpText() { if (!((Object)(object)_helpText == (Object)null)) { if ((Object)(object)_titleText != (Object)null) { ((TMP_Text)_titleText).text = _SeekToTitle ?? "Submit URL"; ((Component)_titleText).gameObject.SetActive(true); } if (_SeekToHelpText != null) { ((TMP_Text)_helpText).text = _SeekToHelpText; } else { ((TMP_Text)_helpText).text = (_showAsViewerSuggestion ? "Paste your YouTube link, then press Enter to suggest.\nPress left arrow key to cancel.\n\nOnly YouTube links are supported for suggestions!" : "Paste your video link, then press Enter to load.\nPress left arrow key to cancel.\n\nSupports YouTube, MP4, WebM, AVI, MOV, M4V, and most MKVs."); } } } private void ApplyPromptLayout() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_panelGo == (Object)null)) { RectTransform component = _panelGo.GetComponent(); component.anchorMin = new Vector2(0.2f, 0.32f); component.anchorMax = new Vector2(0.8f, 0.68f); component.offsetMin = Vector2.zero; component.offsetMax = Vector2.zero; if ((Object)(object)_titleText != (Object)null) { ((TMP_Text)_titleText).text = _SeekToTitle ?? "Submit URL"; ((Component)_titleText).gameObject.SetActive(true); } if ((Object)(object)_helpText != (Object)null) { ((Component)_helpText).gameObject.SetActive(true); } if ((Object)(object)_inputGo != (Object)null) { _inputGo.SetActive(true); } if ((Object)(object)_submitButtonGo != (Object)null) { _submitButtonGo.SetActive(true); } if ((Object)(object)_cancelButtonGo != (Object)null) { _cancelButtonGo.SetActive(true); } if ((Object)(object)_confirmationText != (Object)null) { ((Component)_confirmationText).gameObject.SetActive(false); } TMP_InputField input = _input; Graphic obj = ((input != null) ? input.placeholder : null); TextMeshProUGUI val = (TextMeshProUGUI)(object)((obj is TextMeshProUGUI) ? obj : null); if (val != null) { ((TMP_Text)val).text = _SeekToPlaceholder ?? "https://www.youtube.com/watch?v=k9jNqmC211c"; } } } private void ApplyConfirmationLayout(string message) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_panelGo == (Object)null)) { RectTransform component = _panelGo.GetComponent(); component.anchorMin = new Vector2(0.34f, 0.42f); component.anchorMax = new Vector2(0.66f, 0.58f); component.offsetMin = Vector2.zero; component.offsetMax = Vector2.zero; if ((Object)(object)_titleText != (Object)null) { ((Component)_titleText).gameObject.SetActive(false); } if ((Object)(object)_helpText != (Object)null) { ((Component)_helpText).gameObject.SetActive(false); } if ((Object)(object)_inputGo != (Object)null) { _inputGo.SetActive(false); } if ((Object)(object)_submitButtonGo != (Object)null) { _submitButtonGo.SetActive(false); } if ((Object)(object)_cancelButtonGo != (Object)null) { _cancelButtonGo.SetActive(false); } if ((Object)(object)_confirmationText != (Object)null) { ((TMP_Text)_confirmationText).text = message; RectTransform rectTransform = ((TMP_Text)_confirmationText).rectTransform; rectTransform.anchorMin = new Vector2(0.08f, 0.18f); rectTransform.anchorMax = new Vector2(0.92f, 0.82f); rectTransform.offsetMin = Vector2.zero; rectTransform.offsetMax = Vector2.zero; ((Component)_confirmationText).gameObject.SetActive(true); } } } private void Build() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Expected O, but got Unknown //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_0239: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_025d: Unknown result type (might be due to invalid IL or missing references) //IL_026a: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Unknown result type (might be due to invalid IL or missing references) //IL_0285: Expected O, but got Unknown //IL_02b1: Unknown result type (might be due to invalid IL or missing references) //IL_02dd: Unknown result type (might be due to invalid IL or missing references) //IL_02f4: Unknown result type (might be due to invalid IL or missing references) //IL_0301: Unknown result type (might be due to invalid IL or missing references) //IL_030e: Unknown result type (might be due to invalid IL or missing references) //IL_032f: Unknown result type (might be due to invalid IL or missing references) //IL_0336: Expected O, but got Unknown //IL_0367: Unknown result type (might be due to invalid IL or missing references) //IL_0386: Unknown result type (might be due to invalid IL or missing references) //IL_0393: Unknown result type (might be due to invalid IL or missing references) //IL_03aa: Unknown result type (might be due to invalid IL or missing references) //IL_03c1: Unknown result type (might be due to invalid IL or missing references) //IL_0409: Unknown result type (might be due to invalid IL or missing references) //IL_0410: Expected O, but got Unknown //IL_0462: Unknown result type (might be due to invalid IL or missing references) //IL_0492: Unknown result type (might be due to invalid IL or missing references) //IL_049f: Unknown result type (might be due to invalid IL or missing references) //IL_04b6: Unknown result type (might be due to invalid IL or missing references) //IL_04cd: Unknown result type (might be due to invalid IL or missing references) //IL_0502: Unknown result type (might be due to invalid IL or missing references) //IL_0539: Unknown result type (might be due to invalid IL or missing references) //IL_0559: Unknown result type (might be due to invalid IL or missing references) //IL_0560: Expected O, but got Unknown //IL_059d: Unknown result type (might be due to invalid IL or missing references) //IL_05df: Unknown result type (might be due to invalid IL or missing references) //IL_05f6: Unknown result type (might be due to invalid IL or missing references) //IL_0603: Unknown result type (might be due to invalid IL or missing references) //IL_0610: Unknown result type (might be due to invalid IL or missing references) Canvas val = ((Component)this).gameObject.AddComponent(); val.renderMode = (RenderMode)0; ((Component)this).gameObject.AddComponent(); _panelGo = new GameObject("Panel"); _panelGo.transform.SetParent(((Component)this).transform, false); Image val2 = _panelGo.AddComponent(); ((Graphic)val2).color = new Color(0f, 0f, 0f, 0.85f); RectTransform rectTransform = ((Graphic)val2).rectTransform; rectTransform.anchorMin = new Vector2(0.2f, 0.32f); rectTransform.anchorMax = new Vector2(0.8f, 0.68f); rectTransform.offsetMin = Vector2.zero; rectTransform.offsetMax = Vector2.zero; GameObject val3 = new GameObject("TitleText"); val3.transform.SetParent(_panelGo.transform, false); _titleText = val3.AddComponent(); ((TMP_Text)_titleText).text = "Submit URL"; ((TMP_Text)_titleText).fontSize = 28f; ((Graphic)_titleText).color = Color.white; ((TMP_Text)_titleText).alignment = (TextAlignmentOptions)514; ((TMP_Text)_titleText).enableWordWrapping = false; RectTransform rectTransform2 = ((TMP_Text)_titleText).rectTransform; rectTransform2.anchorMin = new Vector2(0.07f, 0.84f); rectTransform2.anchorMax = new Vector2(0.93f, 0.94f); rectTransform2.offsetMin = Vector2.zero; rectTransform2.offsetMax = Vector2.zero; val3.SetActive(false); GameObject val4 = new GameObject("HelpText"); val4.transform.SetParent(_panelGo.transform, false); _helpText = val4.AddComponent(); ((TMP_Text)_helpText).text = "Paste your video link, then press Enter to load.\nPress left arrow key to cancel.\n\nSupports YouTube, MP4, WebM, AVI, MOV, M4V, and most MKVs."; ((TMP_Text)_helpText).fontSize = 22f; ((Graphic)_helpText).color = Color.white; ((TMP_Text)_helpText).alignment = (TextAlignmentOptions)514; ((TMP_Text)_helpText).enableWordWrapping = true; RectTransform rectTransform3 = ((TMP_Text)_helpText).rectTransform; rectTransform3.anchorMin = new Vector2(0.07f, 0.58f); rectTransform3.anchorMax = new Vector2(0.93f, 0.82f); rectTransform3.offsetMin = Vector2.zero; rectTransform3.offsetMax = Vector2.zero; _inputGo = new GameObject("Input"); _inputGo.transform.SetParent(_panelGo.transform, false); Image val5 = _inputGo.AddComponent(); ((Graphic)val5).color = Color.white; _inputGo.AddComponent(); RectTransform rectTransform4 = ((Graphic)val5).rectTransform; rectTransform4.anchorMin = new Vector2(0.07f, 0.4f); rectTransform4.anchorMax = new Vector2(0.93f, 0.5f); rectTransform4.offsetMin = Vector2.zero; rectTransform4.offsetMax = Vector2.zero; _input = _inputGo.AddComponent(); GameObject val6 = new GameObject("Text"); val6.transform.SetParent(_inputGo.transform, false); TextMeshProUGUI val7 = val6.AddComponent(); ((TMP_Text)val7).fontSize = 20f; ((Graphic)val7).color = Color.black; ((TMP_Text)val7).enableWordWrapping = false; RectTransform rectTransform5 = ((TMP_Text)val7).rectTransform; rectTransform5.anchorMin = Vector2.zero; rectTransform5.anchorMax = Vector2.one; rectTransform5.offsetMin = new Vector2(12f, 6f); rectTransform5.offsetMax = new Vector2(-12f, -6f); _input.textComponent = (TMP_Text)(object)val7; _input.lineType = (LineType)0; ((UnityEvent)(object)_input.onSubmit).AddListener((UnityAction)delegate { SubmitCurrentValue(); }); GameObject val8 = new GameObject("Placeholder"); val8.transform.SetParent(_inputGo.transform, false); TextMeshProUGUI val9 = val8.AddComponent(); ((TMP_Text)val9).text = "https://www.youtube.com/watch?v=k9jNqmC211c"; ((TMP_Text)val9).fontSize = 20f; ((Graphic)val9).color = new Color(0f, 0f, 0f, 0.4f); ((TMP_Text)val9).alignment = (TextAlignmentOptions)4097; ((TMP_Text)val7).alignment = (TextAlignmentOptions)4097; RectTransform rectTransform6 = ((TMP_Text)val9).rectTransform; rectTransform6.anchorMin = Vector2.zero; rectTransform6.anchorMax = Vector2.one; rectTransform6.offsetMin = new Vector2(12f, 2f); rectTransform6.offsetMax = new Vector2(-12f, -2f); _input.placeholder = (Graphic)(object)val9; _submitButtonGo = MakeButton(_panelGo.transform, "Submit", new Vector2(0.24f, 0.14f), SubmitCurrentValue); _cancelButtonGo = MakeButton(_panelGo.transform, "Cancel", new Vector2(0.58f, 0.14f), Cancel); GameObject val10 = new GameObject("ConfirmationText"); val10.transform.SetParent(_panelGo.transform, false); _confirmationText = val10.AddComponent(); ((TMP_Text)_confirmationText).fontSize = 28f; ((Graphic)_confirmationText).color = Color.white; ((TMP_Text)_confirmationText).alignment = (TextAlignmentOptions)514; ((TMP_Text)_confirmationText).enableWordWrapping = true; RectTransform rectTransform7 = ((TMP_Text)_confirmationText).rectTransform; rectTransform7.anchorMin = new Vector2(0.08f, 0.18f); rectTransform7.anchorMax = new Vector2(0.92f, 0.82f); rectTransform7.offsetMin = Vector2.zero; rectTransform7.offsetMax = Vector2.zero; val10.SetActive(false); UpdateHelpText(); ApplyGameFontToOverlay(); ((Component)this).gameObject.SetActive(false); } private GameObject MakeButton(Transform parent, string label, Vector2 anchorMin, Action onClick) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Expected O, but got Unknown //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(label + "Button"); val.transform.SetParent(parent, false); Image val2 = val.AddComponent(); ((Graphic)val2).color = new Color(1f, 1f, 1f, 0.95f); Button val3 = val.AddComponent