Decompiled source of SyncVideo v1.1.0
plugins/SyncVideo/SyncVideo.dll
Decompiled 4 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BombRushMP.Common; using BombRushMP.Common.Networking; using BombRushMP.Common.Packets; using BombRushMP.Plugin; using BombRushMP.Plugin.Gamemodes; using CommonAPI; using CommonAPI.Phone; using HarmonyLib; using Microsoft.CodeAnalysis; using Reptile; using Reptile.Phone; using SyncVideo.Model; using SyncVideo.Phone; using SyncVideo.Runtime; using SyncVideo.Transport; using SyncVideo.Transport.Packets; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.Rendering; using UnityEngine.UI; using UnityEngine.Video; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace SyncVideo { internal static class PluginInfo { public const string PLUGIN_GUID = "transrights.SyncVideo"; public const string PLUGIN_NAME = "Sync Video"; public const string PLUGIN_VERSION = "1.0.0"; } public sealed class SyncVideoConfig { public const string DefaultLobbyName = "Sync Video Lobby"; public const bool LogBusMessages = false; public ConfigEntry<string> TvObjectName { get; } public ConfigEntry<string> ScreenMaterialTextureName { get; } public ConfigEntry<float> HostBeaconInterval { get; } public ConfigEntry<bool> EnableOfflineMode { get; } public ConfigEntry<float> HostStateResendInterval { get; } public ConfigEntry<float> DriftToleranceSeconds { get; } public ConfigEntry<float> HardSeekThresholdSeconds { get; } public ConfigEntry<bool> AutoAttachToTVsOnStageLoad { get; } public ConfigEntry<bool> ShowScreenPositionMenu { get; } public ConfigEntry<bool> ShowRefreshScreensButton { get; } public ConfigEntry<bool> HideNativeLobbyUi { get; } public ConfigEntry<bool> HostAutoplay { get; } public ConfigEntry<int> DefaultVolume { get; } public ConfigEntry<string> VideoRenderResolution { get; } public ConfigEntry<string> YouTubeStreamResolution { get; } public ConfigEntry<bool> UseFFmpeg { get; } public ConfigEntry<bool> EnableMkvSupport { get; } public ConfigEntry<bool> SuppressAFK { get; } public ConfigEntry<bool> MuteMusicAndAmbient { get; } public ConfigEntry<bool> EnableMkvFfmpegConversion { get; } public ConfigEntry<bool> MkvTranscodeToH264 { get; } public ConfigEntry<float> YouTubeVolumeScale { get; } public ConfigEntry<float> SubtitleFontSize { get; } public string PluginDirectory { get; } public string AdvancedConfigPath { get; } public SyncVideoConfig(ConfigFile config, ConfigFile advancedConfig, string pluginLocation) { PluginDirectory = Path.GetDirectoryName(pluginLocation) ?? string.Empty; AdvancedConfigPath = advancedConfig.ConfigFilePath; EnableOfflineMode = config.Bind<bool>("OfflineMode", "EnableOfflineMode", false, "Disable all online functionality when enabled. Create local lobbies for personal use."); HideNativeLobbyUi = config.Bind<bool>("ACN", "Hide Lobby UI", true, "Hide ACN's lobby UI by default."); SuppressAFK = config.Bind<bool>("ACN", "Suppress AFK", true, "Hide AFK animations for yourself and other players while in a Sync Video lobby."); HostAutoplay = config.Bind<bool>("Video", "Host Autoplay", true, "If hosting a video lobby, automatically start playback loading your video URL."); VideoRenderResolution = config.Bind<string>("Video", "Video Render Resolution", "854x480", "Render resolution used for MP4 video playback. Higher resolutions will cause lag. Options: 1920x1080, 1280x720, 960x540, 854x480, 640x360, 426x240."); YouTubeStreamResolution = config.Bind<string>("Video", "YouTube Resolution", "1280x720", "Resolution used when streaming YouTube videos. Options: 1920x1080, 1280x720, 960x540, 854x480, 640x360, 426x240."); UseFFmpeg = config.Bind<bool>("Video", "Use FFmpeg", false, "Enable FFmpeg for higher quality YouTube video playback. Requires ffmpeg.exe to be placed in the plugin folder, next to SyncVideo.dll. When disabled, stream videos without downloading."); EnableMkvSupport = config.Bind<bool>("Video", "MKV Support", true, "Enable experimental MKV playback support. An MKV Settings menu for the host will appear in the app when an MKV file is loaded."); SubtitleFontSize = config.Bind<float>("Video", "Subtitle Font Size", 34f, "Font size for MKV subtitles displayed on the TV screen. Default is 34."); ShowScreenPositionMenu = config.Bind<bool>("Video", "Show Screen Position Menu", false, "Show the Screen Position menu in the phone app. Lets you move the screen around. Does not sync with viewers if host."); DefaultVolume = config.Bind<int>("Volume", "Default Volume", 90, "Starting volume level (0–100). Automatically rounds to increments of 10 in-game."); MuteMusicAndAmbient = config.Bind<bool>("Volume", "Mute Music and Ambient SFX", true, "Mute the game's music and ambient sounds while in a Sync Video lobby."); TvObjectName = advancedConfig.Bind<string>("Debug", "TV Object Name", "TV", "Scene object name to bind video screens to."); ScreenMaterialTextureName = advancedConfig.Bind<string>("Debug", "Screen Material Texture Name", "_MainTex", "Texture slot used when drawing video to a renderer."); AutoAttachToTVsOnStageLoad = advancedConfig.Bind<bool>("Debug", "Auto Attach To TVs On Load", true, "Automatically bind screens to matching TV objects when a map loads."); ShowRefreshScreensButton = advancedConfig.Bind<bool>("Debug", "Refresh Screens Button", false, "Show the Refresh Screens button in the phone app. Rebinds screens to objects."); YouTubeVolumeScale = advancedConfig.Bind<float>("Debug", "YouTube Volume Scale", 1f, "Volume multiplier applied to YouTube videos (0.0-1.0). Makes YouTube videos not kill your ears."); EnableMkvFfmpegConversion = advancedConfig.Bind<bool>("Experimental", "MKV To MP4 Conversion", false, "When enabled, MKV files are converted into MP4 with FFmpeg. This re-muxes the MKV to fix container issues for MKVs using the H.264 codec."); MkvTranscodeToH264 = advancedConfig.Bind<bool>("Experimental", "Transcode MKV to H.264", false, "Transcodes the video to H.264 instead of using the MKV's codec. Required for H.265/HEVC, VP9, AV1, and 10-bit H.264 sources. Will be very slow."); HostBeaconInterval = advancedConfig.Bind<float>("Networking", "Host Beacon Interval", 2f, "Seconds between lobby beacons."); HostStateResendInterval = advancedConfig.Bind<float>("Networking", "Host State Resend Interval", 0.5f, "Seconds between host sync state broadcasts."); DriftToleranceSeconds = advancedConfig.Bind<float>("Sync", "Drift Tolerance", 0.16f, "Amount of time difference between host and viewer, in seconds. If drift exceeds this, the viewer nudges toward host time."); HardSeekThresholdSeconds = advancedConfig.Bind<float>("Sync", "Hard Seek Threshold", 0.6f, "Maximum amount of time difference between host and viewer, in seconds. If drift exceeds this, the viewer performs a hard seek."); advancedConfig.Save(); } } [BepInPlugin("transrights.SyncVideo", "Sync Video", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class SyncVideoPlugin : BaseUnityPlugin { private Harmony _harmony; public static SyncVideoPlugin Instance { get; private set; } public static SyncVideoConfig Settings { get; private set; } public static VideoLobbyManager LobbyManager { get; private set; } public static SyncVideoController SyncController { get; private set; } public static VideoScreenManager ScreenManager { get; private set; } public static SyncVideoTransport Transport { get; private set; } public static LobbyUiOverrideManager LobbyUiOverride { get; private set; } public static event Action<bool> LobbyStateChanged; private void Awake() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Expected O, but got Unknown Instance = this; string text = Path.Combine(Paths.ConfigPath, "transrights.SyncVideoAdvanced.cfg"); ConfigFile advancedConfig = new ConfigFile(text, true); Settings = new SyncVideoConfig(((BaseUnityPlugin)this).Config, advancedConfig, ((BaseUnityPlugin)this).Info.Location); SyncVideoPacketRegistry.RegisterPackets(((BaseUnityPlugin)this).Logger); Transport = new SyncVideoTransport(((BaseUnityPlugin)this).Logger); LobbyManager = new VideoLobbyManager(((BaseUnityPlugin)this).Logger, Transport); LobbyManager.ActiveLobbyChanged += delegate(VideoLobby lobby) { SyncVideoPlugin.LobbyStateChanged?.Invoke(lobby != null); }; SyncController = new SyncVideoController(((BaseUnityPlugin)this).Logger, LobbyManager); ScreenManager = new VideoScreenManager(((BaseUnityPlugin)this).Logger, SyncController); LobbyUiOverride = new LobbyUiOverrideManager(((BaseUnityPlugin)this).Logger, LobbyManager); _harmony = new Harmony("transrights.SyncVideo"); _harmony.PatchAll(); AppSyncVideo.Initialize(); AppSyncVideoLobby.Initialize(); AppSyncVideoPublicLobbies.Initialize(); AppSyncVideoScreenOptions.Initialize(); AppSyncVideoLobbyKick.Initialize(); AppSyncVideoSuggestions.Initialize(); AppSyncVideoMkvSettings.Initialize(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Sync Video loaded."); } private void Update() { LobbyManager.Tick(Time.unscaledDeltaTime); SyncController.Tick(Time.unscaledDeltaTime); ScreenManager.Tick(Time.unscaledDeltaTime); HudManager.Tick(); } private void LateUpdate() { LobbyUiOverride?.Tick(Time.unscaledDeltaTime); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } LobbyUiOverride?.Dispose(); ScreenManager?.Dispose(); SyncController?.Dispose(); LobbyManager?.Dispose(); Transport?.Dispose(); } } } namespace SyncVideo.Transport { public static class SyncVideoPacketRegistry { public static void RegisterPackets(ManualLogSource logger) { } } public sealed class SyncVideoTransport : IDisposable { private readonly ManualLogSource _logger; private bool _disposed; public ushort LocalPlayerId => (ushort)(((Object)(object)ClientController.Instance != (Object)null) ? ClientController.Instance.LocalID : 0); public bool Connected => (Object)(object)ClientController.Instance != (Object)null && ClientController.Instance.Connected; public event Action<SyncVideoPacketBase> SyncPacketReceived; public SyncVideoTransport(ManualLogSource logger) { _logger = logger; ClientController.RegisterCustomPacketHandler("syncvideo.state", (Action<ushort, byte[]>)OnRawState); ClientController.RegisterCustomPacketHandler("syncvideo.time", (Action<ushort, byte[]>)OnRawTime); ClientController.RegisterCustomPacketHandler("syncvideo.state_request", (Action<ushort, byte[]>)OnRawStateRequest); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_advertise", (Action<ushort, byte[]>)OnRawLobbyAdvertise); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_join", (Action<ushort, byte[]>)OnRawLobbyJoin); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_leave", (Action<ushort, byte[]>)OnRawLobbyLeave); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_members", (Action<ushort, byte[]>)OnRawLobbyMembers); ClientController.RegisterCustomPacketHandler("syncvideo.lobby_closed", (Action<ushort, byte[]>)OnRawLobbyClosed); ClientController.RegisterCustomPacketHandler("syncvideo.screen_transform", (Action<ushort, byte[]>)OnRawScreenTransform); ClientController.RegisterCustomPacketHandler("syncvideo.suggestion", (Action<ushort, byte[]>)OnRawSuggestion); ClientController.RegisterCustomPacketHandler("syncvideo.suggestions_open", (Action<ushort, byte[]>)OnRawSuggestionsOpen); ClientController.RegisterCustomPacketHandler("syncvideo.suggestion_ack", (Action<ushort, byte[]>)OnRawSuggestionAck); } public void Dispose() { _disposed = true; } public void BroadcastToLobby(SyncVideoPacketBase packet) { if (_disposed || !Connected || packet == null) { return; } try { ClientController.Instance.BroadcastCustomPacketToCurrentLobby(packet.Serialize(), packet.PacketId, (SendModes)2); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] BroadcastToLobby(" + packet.PacketId + ") failed: " + ex.Message)); } } public void SendToPlayer(SyncVideoPacketBase packet, ushort targetPlayerId) { if (_disposed || !Connected || packet == null || targetPlayerId == 0) { return; } try { ClientController.Instance.SendCustomPacketToPlayer(packet.Serialize(), packet.PacketId, targetPlayerId, (SendModes)2); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] SendToPlayer(" + packet.PacketId + " → " + targetPlayerId + ") failed: " + ex.Message)); } } private void Dispatch(SyncVideoPacketBase packet) { if (!_disposed && packet != null) { this.SyncPacketReceived?.Invoke(packet); } } private void OnRawState(ushort fromId, byte[] data) { try { Dispatch(SyncVideoStatePacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise State failed: " + ex.Message)); } } private void OnRawTime(ushort fromId, byte[] data) { try { Dispatch(SyncVideoTimePacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise Time failed: " + ex.Message)); } } private void OnRawStateRequest(ushort fromId, byte[] data) { try { Dispatch(SyncVideoStateRequestPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise StateRequest failed: " + ex.Message)); } } private void OnRawLobbyAdvertise(ushort fromId, byte[] data) { try { Dispatch(SyncVideoLobbyAdvertisePacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise LobbyAdvertise failed: " + ex.Message)); } } private void OnRawLobbyJoin(ushort fromId, byte[] data) { try { Dispatch(SyncVideoLobbyJoinPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise LobbyJoin failed: " + ex.Message)); } } private void OnRawLobbyLeave(ushort fromId, byte[] data) { try { Dispatch(SyncVideoLobbyLeavePacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise LobbyLeave failed: " + ex.Message)); } } private void OnRawLobbyMembers(ushort fromId, byte[] data) { try { Dispatch(SyncVideoLobbyMembersPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise LobbyMembers failed: " + ex.Message)); } } private void OnRawLobbyClosed(ushort fromId, byte[] data) { try { Dispatch(SyncVideoLobbyClosedPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise LobbyClosed failed: " + ex.Message)); } } private void OnRawScreenTransform(ushort fromId, byte[] data) { try { Dispatch(SyncVideoScreenTransformPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise ScreenTransform failed: " + ex.Message)); } } private void OnRawSuggestion(ushort fromId, byte[] data) { try { Dispatch(SyncVideoSuggestionPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise Suggestion failed: " + ex.Message)); } } private void OnRawSuggestionsOpen(ushort fromId, byte[] data) { try { Dispatch(SyncVideoSuggestionsOpenPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise SuggestionsOpen failed: " + ex.Message)); } } private void OnRawSuggestionAck(ushort fromId, byte[] data) { try { Dispatch(SyncVideoSuggestionAckPacket.Deserialize(fromId, data)); } catch (Exception ex) { _logger.LogWarning((object)("[SyncVideo] Deserialise SuggestionAck failed: " + ex.Message)); } } } } namespace SyncVideo.Transport.Packets { public sealed class SyncVideoLobbyAdvertisePacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public ushort HostId; public string LobbyName = string.Empty; public int MemberCount; public string CurrentUrl = string.Empty; public string CurrentVideoId = string.Empty; public bool IsPlaying; public double MediaTimeSeconds; public long HostUnixMilliseconds; public int Revision; public override string PacketId => "syncvideo.lobby_advertise"; public static SyncVideoLobbyAdvertisePacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoLobbyAdvertisePacket syncVideoLobbyAdvertisePacket = new SyncVideoLobbyAdvertisePacket { SenderPlayerId = senderPlayerId }; syncVideoLobbyAdvertisePacket.PopulateFrom(data); return syncVideoLobbyAdvertisePacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); HostId = reader.ReadUInt16(); LobbyName = SyncVideoPacketSerialization.ReadString(reader); MemberCount = reader.ReadInt32(); CurrentUrl = SyncVideoPacketSerialization.ReadString(reader); CurrentVideoId = SyncVideoPacketSerialization.ReadString(reader); IsPlaying = reader.ReadBoolean(); MediaTimeSeconds = reader.ReadDouble(); HostUnixMilliseconds = reader.ReadInt64(); Revision = reader.ReadInt32(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(HostId); SyncVideoPacketSerialization.WriteString(writer, LobbyName); writer.Write(MemberCount); SyncVideoPacketSerialization.WriteString(writer, CurrentUrl); SyncVideoPacketSerialization.WriteString(writer, CurrentVideoId); writer.Write(IsPlaying); writer.Write(MediaTimeSeconds); writer.Write(HostUnixMilliseconds); writer.Write(Revision); } } public sealed class SyncVideoLobbyClosedPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public override string PacketId => "syncvideo.lobby_closed"; public static SyncVideoLobbyClosedPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoLobbyClosedPacket syncVideoLobbyClosedPacket = new SyncVideoLobbyClosedPacket { SenderPlayerId = senderPlayerId }; syncVideoLobbyClosedPacket.PopulateFrom(data); return syncVideoLobbyClosedPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); } } public sealed class SyncVideoLobbyJoinPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public ushort PlayerId; public override string PacketId => "syncvideo.lobby_join"; public static SyncVideoLobbyJoinPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoLobbyJoinPacket syncVideoLobbyJoinPacket = new SyncVideoLobbyJoinPacket { SenderPlayerId = senderPlayerId }; syncVideoLobbyJoinPacket.PopulateFrom(data); return syncVideoLobbyJoinPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); PlayerId = reader.ReadUInt16(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(PlayerId); } } public sealed class SyncVideoLobbyLeavePacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public ushort PlayerId; public override string PacketId => "syncvideo.lobby_leave"; public static SyncVideoLobbyLeavePacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoLobbyLeavePacket syncVideoLobbyLeavePacket = new SyncVideoLobbyLeavePacket { SenderPlayerId = senderPlayerId }; syncVideoLobbyLeavePacket.PopulateFrom(data); return syncVideoLobbyLeavePacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); PlayerId = reader.ReadUInt16(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(PlayerId); } } public sealed class SyncVideoLobbyMembersPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public ushort[] MemberIds = new ushort[0]; public override string PacketId => "syncvideo.lobby_members"; public static SyncVideoLobbyMembersPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoLobbyMembersPacket syncVideoLobbyMembersPacket = new SyncVideoLobbyMembersPacket { SenderPlayerId = senderPlayerId }; syncVideoLobbyMembersPacket.PopulateFrom(data); return syncVideoLobbyMembersPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); MemberIds = SyncVideoPacketSerialization.ReadUShortArray(reader); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); SyncVideoPacketSerialization.WriteUShortArray(writer, MemberIds); } } public abstract class SyncVideoPacketBase { public ushort SenderPlayerId; public abstract string PacketId { get; } public byte[] Serialize() { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8); WritePayload(binaryWriter); binaryWriter.Flush(); return memoryStream.ToArray(); } protected void PopulateFrom(byte[] data) { using MemoryStream input = new MemoryStream(data); using BinaryReader reader = new BinaryReader(input, Encoding.UTF8); ReadPayload(reader); } protected abstract void WritePayload(BinaryWriter writer); protected abstract void ReadPayload(BinaryReader reader); } public static class SyncVideoPacketIds { public const string LobbyAdvertise = "syncvideo.lobby_advertise"; public const string LobbyJoin = "syncvideo.lobby_join"; public const string LobbyLeave = "syncvideo.lobby_leave"; public const string LobbyMembers = "syncvideo.lobby_members"; public const string State = "syncvideo.state"; public const string LobbyClosed = "syncvideo.lobby_closed"; public const string StateRequest = "syncvideo.state_request"; public const string ScreenTransform = "syncvideo.screen_transform"; public const string Suggestion = "syncvideo.suggestion"; public const string SuggestionsOpen = "syncvideo.suggestions_open"; public const string SuggestionAck = "syncvideo.suggestion_ack"; public const string Time = "syncvideo.time"; } public static class SyncVideoPacketSerialization { public static void WriteString(BinaryWriter writer, string value) { writer.Write(value ?? string.Empty); } public static string ReadString(BinaryReader reader) { return reader.ReadString(); } public static void WriteUShortArray(BinaryWriter writer, ushort[] values) { if (values == null) { writer.Write(0); return; } writer.Write(values.Length); for (int i = 0; i < values.Length; i++) { writer.Write(values[i]); } } public static ushort[] ReadUShortArray(BinaryReader reader) { int num = reader.ReadInt32(); if (num <= 0) { return new ushort[0]; } ushort[] array = new ushort[num]; for (int i = 0; i < num; i++) { array[i] = reader.ReadUInt16(); } return array; } } public sealed class SyncVideoScreenTransformPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public float PosX; public float PosY; public float PosZ; public float ScaleX; public float ScaleY; public int Revision; public override string PacketId => "syncvideo.screen_transform"; public static SyncVideoScreenTransformPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoScreenTransformPacket syncVideoScreenTransformPacket = new SyncVideoScreenTransformPacket { SenderPlayerId = senderPlayerId }; syncVideoScreenTransformPacket.PopulateFrom(data); return syncVideoScreenTransformPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); PosX = reader.ReadSingle(); PosY = reader.ReadSingle(); PosZ = reader.ReadSingle(); ScaleX = reader.ReadSingle(); ScaleY = reader.ReadSingle(); Revision = reader.ReadInt32(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(PosX); writer.Write(PosY); writer.Write(PosZ); writer.Write(ScaleX); writer.Write(ScaleY); writer.Write(Revision); } } public sealed class SyncVideoStatePacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public string Url = string.Empty; public string VideoId = string.Empty; public bool IsPlaying; public double MediaTimeSeconds; public long HostUnixMilliseconds; public int Revision; public bool HasEnded; public bool IsOpen = true; public bool SuggestionsOpen; public int SelectedAudioTrack = 0; public int SelectedSubtitleTrack = -1; public override string PacketId => "syncvideo.state"; public static SyncVideoStatePacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoStatePacket syncVideoStatePacket = new SyncVideoStatePacket { SenderPlayerId = senderPlayerId }; syncVideoStatePacket.PopulateFrom(data); return syncVideoStatePacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); Url = SyncVideoPacketSerialization.ReadString(reader); VideoId = SyncVideoPacketSerialization.ReadString(reader); IsPlaying = reader.ReadBoolean(); MediaTimeSeconds = reader.ReadDouble(); HostUnixMilliseconds = reader.ReadInt64(); Revision = reader.ReadInt32(); HasEnded = reader.ReadBoolean(); IsOpen = reader.ReadBoolean(); SuggestionsOpen = reader.ReadBoolean(); if (reader.BaseStream.Position < reader.BaseStream.Length) { SelectedAudioTrack = reader.ReadInt32(); } if (reader.BaseStream.Position < reader.BaseStream.Length) { SelectedSubtitleTrack = reader.ReadInt32(); } } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); SyncVideoPacketSerialization.WriteString(writer, Url); SyncVideoPacketSerialization.WriteString(writer, VideoId); writer.Write(IsPlaying); writer.Write(MediaTimeSeconds); writer.Write(HostUnixMilliseconds); writer.Write(Revision); writer.Write(HasEnded); writer.Write(IsOpen); writer.Write(SuggestionsOpen); writer.Write(SelectedAudioTrack); writer.Write(SelectedSubtitleTrack); } } public sealed class SyncVideoStateRequestPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public override string PacketId => "syncvideo.state_request"; public static SyncVideoStateRequestPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoStateRequestPacket syncVideoStateRequestPacket = new SyncVideoStateRequestPacket { SenderPlayerId = senderPlayerId }; syncVideoStateRequestPacket.PopulateFrom(data); return syncVideoStateRequestPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); } } public sealed class SyncVideoSuggestionAckPacket : SyncVideoPacketBase { public ushort RecipientPlayerId; public string LobbyId = string.Empty; public string SuggestionKey = string.Empty; public override string PacketId => "syncvideo.suggestion_ack"; public static SyncVideoSuggestionAckPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoSuggestionAckPacket syncVideoSuggestionAckPacket = new SyncVideoSuggestionAckPacket { SenderPlayerId = senderPlayerId }; syncVideoSuggestionAckPacket.PopulateFrom(data); return syncVideoSuggestionAckPacket; } protected override void ReadPayload(BinaryReader reader) { RecipientPlayerId = reader.ReadUInt16(); LobbyId = SyncVideoPacketSerialization.ReadString(reader); SuggestionKey = SyncVideoPacketSerialization.ReadString(reader); } protected override void WritePayload(BinaryWriter writer) { writer.Write(RecipientPlayerId); SyncVideoPacketSerialization.WriteString(writer, LobbyId); SyncVideoPacketSerialization.WriteString(writer, SuggestionKey); } } public sealed class SyncVideoSuggestionPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public string Url = string.Empty; public string Title = string.Empty; public string PlayerName = string.Empty; public override string PacketId => "syncvideo.suggestion"; public static SyncVideoSuggestionPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoSuggestionPacket syncVideoSuggestionPacket = new SyncVideoSuggestionPacket { SenderPlayerId = senderPlayerId }; syncVideoSuggestionPacket.PopulateFrom(data); return syncVideoSuggestionPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); Url = SyncVideoPacketSerialization.ReadString(reader); Title = SyncVideoPacketSerialization.ReadString(reader); PlayerName = ((reader.BaseStream.Position < reader.BaseStream.Length) ? SyncVideoPacketSerialization.ReadString(reader) : string.Empty); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); SyncVideoPacketSerialization.WriteString(writer, Url); SyncVideoPacketSerialization.WriteString(writer, Title); SyncVideoPacketSerialization.WriteString(writer, PlayerName); } } public sealed class SyncVideoSuggestionsOpenPacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public bool IsOpen; public override string PacketId => "syncvideo.suggestions_open"; public static SyncVideoSuggestionsOpenPacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoSuggestionsOpenPacket syncVideoSuggestionsOpenPacket = new SyncVideoSuggestionsOpenPacket { SenderPlayerId = senderPlayerId }; syncVideoSuggestionsOpenPacket.PopulateFrom(data); return syncVideoSuggestionsOpenPacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); IsOpen = reader.ReadBoolean(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(IsOpen); } } public sealed class SyncVideoTimePacket : SyncVideoPacketBase { public string LobbyId = string.Empty; public double MediaTimeSeconds; public bool IsPlaying; public long HostSentMilliseconds; public override string PacketId => "syncvideo.time"; public static SyncVideoTimePacket Deserialize(ushort senderPlayerId, byte[] data) { SyncVideoTimePacket syncVideoTimePacket = new SyncVideoTimePacket { SenderPlayerId = senderPlayerId }; syncVideoTimePacket.PopulateFrom(data); return syncVideoTimePacket; } protected override void ReadPayload(BinaryReader reader) { LobbyId = SyncVideoPacketSerialization.ReadString(reader); MediaTimeSeconds = reader.ReadDouble(); IsPlaying = reader.ReadBoolean(); HostSentMilliseconds = reader.ReadInt64(); } protected override void WritePayload(BinaryWriter writer) { SyncVideoPacketSerialization.WriteString(writer, LobbyId); writer.Write(MediaTimeSeconds); writer.Write(IsPlaying); writer.Write(HostSentMilliseconds); } } } namespace SyncVideo.Runtime { public sealed class DirectUrlVideoBackend : IDisposable, IVideoBackend { private sealed class AudioTrackInfo { public int StreamIndex; public int UnityTrackIndex; public string Language = string.Empty; public string Title = string.Empty; public string Codec = string.Empty; public int Channels; } private const string NoVideoLoadedMessage = "No Video Loaded!"; private const string LoadedReadyMessage = "Video Loaded!\nPress Play!"; private const string UrlErrorMessage = "Video URL Error!"; private const string PausedMessage = "Paused!"; private const string VideoEndedMessage = "Video Ended!"; private const string ResolvingBaseMessage = "Loading Youtube URL!\nPlease wait"; private const string DownloadingBaseMessage = "FFmpeg enabled!\nDownloading HD Video!\nPlease wait"; private readonly ManualLogSource _logger; private readonly GameObject _root; private readonly VideoPlayer _player; private readonly RenderTexture _texture; private readonly int _renderWidth; private readonly int _renderHeight; private readonly SubtitleManager _subtitleManager; private string _lastOriginalUrl = string.Empty; private string _subtitleSourceUrl = string.Empty; private string _lastVideoId = string.Empty; private readonly object _audioTrackLock = new object(); private readonly List<AudioTrackInfo> _audioTracks = new List<AudioTrackInfo>(); private CancellationTokenSource _audioProbeCts; private int _knownAudioTrackCount = 1; private int _selectedAudioTrack = 0; private bool _isAudioProbing; private bool _isMuted; private float _volume; private double _lastKnownTimeSeconds; private bool _isResolving; private float _resolvingAnimTimer; private int _resolvingAnimStep; private string _resolvingCurrentBase = string.Empty; private bool _isAudioSwitching; private double _audioSwitchPendingSeek; private bool _audioSwitchPendingWasPlaying; private static readonly Regex _audioStreamRx = new Regex("Stream #0:(\\d+)(?:\\(([^)]+)\\))?[^:]*: Audio", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex _audioTitleLineRx = new Regex("^\\s+title\\s*:\\s*(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public string CurrentDirectUrl { get; private set; } = string.Empty; public bool HasPreparedUrl => !string.IsNullOrWhiteSpace(CurrentDirectUrl); public string StatusOverlayText { get; private set; } = "No Video Loaded!"; public bool IsPrepared => _player.isPrepared; public bool IsPlaying => _player.isPlaying; public double CurrentTimeSeconds => (_player.isPrepared || _player.isPlaying) ? _player.time : _lastKnownTimeSeconds; public object OutputTexture => _texture; public float LocalVolume => _volume; public bool IsMuted => _isMuted; public bool IsCurrentMkv => UrlNormalizer.IsMkvUrl(CurrentDirectUrl) || UrlNormalizer.IsMkvUrl(_lastOriginalUrl); public bool ShouldShowFfmpegSyncingStatus => !string.IsNullOrEmpty(_lastVideoId) && YouTube.IsFfmpegAvailable(); public int AudioTrackCount => Math.Max(1, _knownAudioTrackCount); public int SelectedAudioTrack => Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, AudioTrackCount - 1)); public int SubtitleTrackCount => _subtitleManager.TrackCount; public int SelectedSubtitleTrack => _subtitleManager.SelectedTrack; public bool IsSubtitleProbing => _subtitleManager.IsProbing; public bool IsSubtitleExtracting => _subtitleManager.IsExtracting; public bool IsAudioSwitching => _isAudioSwitching; public bool IsAudioProbing => _isAudioProbing; public event Action Prepared; public event Action Ended; public event Action AudioTrackSwitchCompleted; public event Action AudioTracksChanged; public DirectUrlVideoBackend() { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Expected O, but got Unknown //IL_01f0: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Expected O, but got Unknown //IL_0208: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Expected O, but got Unknown //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Expected O, but got Unknown _logger = Logger.CreateLogSource("SyncVideo.DirectUrlVideoBackend"); _subtitleManager = new SubtitleManager(_logger); _volume = Mathf.Clamp01((float)((SyncVideoPlugin.Settings != null) ? SyncVideoPlugin.Settings.DefaultVolume.Value : 90) / 100f); _root = new GameObject("SyncVideoBackend"); Object.DontDestroyOnLoad((Object)(object)_root); ParseRenderResolution((SyncVideoPlugin.Settings != null) ? SyncVideoPlugin.Settings.VideoRenderResolution.Value : null, out _renderWidth, out _renderHeight); _texture = new RenderTexture(_renderWidth, _renderHeight, 0, (RenderTextureFormat)0); _texture.useMipMap = false; _texture.autoGenerateMips = false; _texture.antiAliasing = 1; _texture.Create(); ClearRenderTexture(); _player = _root.AddComponent<VideoPlayer>(); _player.playOnAwake = false; _player.isLooping = false; _player.renderMode = (VideoRenderMode)2; _player.targetTexture = _texture; _player.aspectRatio = (VideoAspectRatio)3; _player.audioOutputMode = (VideoAudioOutputMode)2; _player.EnableAudioTrack((ushort)0, true); _player.skipOnDrop = true; _player.waitForFirstFrame = false; _player.prepareCompleted += new EventHandler(OnPrepareCompleted); _player.errorReceived += new ErrorEventHandler(OnErrorReceived); _player.loopPointReached += new EventHandler(OnLoopPointReached); _logger.LogInfo((object)$"Using video render texture {_renderWidth}x{_renderHeight}."); } private static void ParseRenderResolution(string configuredValue, out int width, out int height) { width = 854; height = 480; switch (string.IsNullOrWhiteSpace(configuredValue) ? string.Empty : configuredValue.Trim().ToLowerInvariant().Replace('x', 'x')) { case "1920x1080": width = 1920; height = 1080; break; case "1280x720": width = 1280; height = 720; break; case "960x540": width = 960; height = 540; break; case "854x480": width = 854; height = 480; break; case "640x360": width = 640; height = 360; break; case "426x240": width = 426; height = 240; break; } } public string GetSubtitleTrackLabel(int index) { return _subtitleManager.GetTrackLabel(index); } public void SelectSubtitleTrack(int trackIndex, Action onComplete) { if (string.IsNullOrWhiteSpace(_subtitleSourceUrl)) { return; } string text = SubtitleManager.FindFfmpegPath(); if (text != null) { _subtitleManager.SelectTrack(trackIndex, _subtitleSourceUrl, text, delegate { SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(onComplete); }); } } public void DisableSubtitles() { _subtitleManager.DisableSubtitles(); } public string GetCurrentSubtitleText(double time) { return _subtitleManager.GetActiveSubtitle(time); } public void SetResolvingStatus() { if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _lastKnownTimeSeconds = 0.0; CurrentDirectUrl = string.Empty; ClearRenderTexture(); _isResolving = true; _resolvingAnimTimer = 0f; _resolvingAnimStep = 0; _resolvingCurrentBase = "Loading Youtube URL!\nPlease wait"; SetStatusOverlay("Loading Youtube URL!\nPlease wait"); } public void SetDownloadingStatus() { if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _lastKnownTimeSeconds = 0.0; CurrentDirectUrl = string.Empty; ClearRenderTexture(); _isResolving = true; _resolvingAnimTimer = 0f; _resolvingAnimStep = 0; _resolvingCurrentBase = "FFmpeg enabled!\nDownloading HD Video!\nPlease wait"; SetStatusOverlay("FFmpeg enabled!\nDownloading HD Video!\nPlease wait"); } public void SetConvertingStatus() { if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _lastKnownTimeSeconds = 0.0; CurrentDirectUrl = string.Empty; ClearRenderTexture(); _isResolving = true; _resolvingAnimTimer = 0f; _resolvingAnimStep = 0; _resolvingCurrentBase = "FFmpeg: Converting MKV!\nPlease wait"; SetStatusOverlay("FFmpeg: Converting MKV!\nPlease wait"); } public void SetErrorStatus(string message) { _isResolving = false; ClearRenderTexture(); int num = (message ?? string.Empty).IndexOf('\n'); string statusOverlay = ((num >= 0) ? ("Error: Video not supported!" + message.Substring(num)) : "Error: Video not supported!"); SetStatusOverlay(statusOverlay); } public void Dispose() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown _player.prepareCompleted -= new EventHandler(OnPrepareCompleted); _player.errorReceived -= new ErrorEventHandler(OnErrorReceived); _player.loopPointReached -= new EventHandler(OnLoopPointReached); CancelAudioProbe(); _subtitleManager.Clear(); if ((Object)(object)_texture != (Object)null) { _texture.Release(); } if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); } if (_logger != null) { Logger.Sources.Remove((ILogSource)(object)_logger); } } public void Load(string directPlayableUrl, string originalUrl, string videoId) { _lastOriginalUrl = originalUrl ?? string.Empty; _subtitleSourceUrl = ResolveSubtitleSourceUrl(directPlayableUrl, _lastOriginalUrl); _lastVideoId = videoId ?? string.Empty; CurrentDirectUrl = directPlayableUrl ?? string.Empty; _lastKnownTimeSeconds = 0.0; ResetAudioTrackCache(); _selectedAudioTrack = 0; _isAudioSwitching = false; _audioSwitchPendingSeek = 0.0; _audioSwitchPendingWasPlaying = false; Stop(); if (string.IsNullOrWhiteSpace(directPlayableUrl)) { SetStatusOverlay("Video URL Error!"); return; } ProbeAudioTracksIfSupported(_subtitleSourceUrl); ProbeSubtitlesIfSupported(_subtitleSourceUrl); SetStatusOverlay(string.Empty); _player.source = (VideoSource)1; _player.url = directPlayableUrl; ResetAudioRouting(); ConfigureAudioTracks(); _player.Prepare(); } public void ReloadCurrent() { if (!string.IsNullOrWhiteSpace(CurrentDirectUrl)) { Load(CurrentDirectUrl, _lastOriginalUrl, _lastVideoId); } } public void Play() { if (_player.isPrepared) { SetStatusOverlay(string.Empty); _player.Play(); } } public void Pause() { if (_player.isPrepared) { _player.Pause(); UpdatePausedOverlay(); } } public void Stop() { if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _lastKnownTimeSeconds = 0.0; _isResolving = false; SetStatusOverlay("No Video Loaded!"); ClearRenderTexture(); CancelAudioProbe(); ResetAudioTrackCache(); _subtitleManager.Clear(); } public void Seek(double seconds) { if (_player.isPrepared) { _player.time = Math.Max(0.0, seconds); _lastKnownTimeSeconds = Math.Max(0.0, seconds); _subtitleManager.ResetSearchHint(); if (!_player.isPlaying) { UpdatePausedOverlay(); } } } public void NudgeToward(double seconds, double driftSeconds) { if (!_player.isPrepared) { return; } double num = seconds - _player.time; double num2 = Math.Abs(num); if (num2 < 0.015 || driftSeconds <= 0.0) { _player.playbackSpeed = 1f; return; } float num3 = Mathf.Clamp((float)(num * 0.14), -0.08f, 0.08f); if (Math.Abs(num3) < 0.012f) { num3 = ((num > 0.0) ? 0.012f : (-0.012f)); } _player.playbackSpeed = Mathf.Clamp(1f + num3, 0.92f, 1.08f); } public void Tick(float deltaTime) { if (_isResolving) { _resolvingAnimTimer += deltaTime; if (_resolvingAnimTimer >= 0.35f) { _resolvingAnimTimer = 0f; _resolvingAnimStep = (_resolvingAnimStep + 1) % 4; switch (_resolvingAnimStep) { case 1: SetStatusOverlay(_resolvingCurrentBase + "."); break; case 2: SetStatusOverlay(_resolvingCurrentBase + ".."); break; case 3: SetStatusOverlay(_resolvingCurrentBase + "..."); break; default: SetStatusOverlay(_resolvingCurrentBase); break; } } } if (_player.isPrepared || _player.isPlaying) { _lastKnownTimeSeconds = Math.Max(0.0, _player.time); } if (!_player.isPlaying && Math.Abs(_player.playbackSpeed - 1f) > 0.001f) { _player.playbackSpeed = 1f; } } public void AdjustVolume(float delta) { _volume = Mathf.Clamp01(_volume + delta); ApplyAudioState(); } public void ToggleMute() { _isMuted = !_isMuted; ApplyAudioState(); } public string GetAudioTrackLabel(int trackIndex) { AudioTrackInfo cachedAudioTrack = GetCachedAudioTrack(trackIndex); int num = trackIndex + 1; if (cachedAudioTrack == null) { return _isAudioProbing ? ("Audio Track " + num + "\n<color=yellow>Scanning...</color>") : ("Audio Track " + num); } string text = BuildAudioTrackDetail(cachedAudioTrack); if (string.IsNullOrWhiteSpace(text)) { return "Audio Track " + num; } return "Audio Track " + num + "\n(" + text + ")"; } public bool SelectAudioTrack(int trackIndex) { return SelectAudioTrack(trackIndex, null, null); } public bool SelectAudioTrack(int trackIndex, double? resumeTimeOverride, bool? resumePlayingOverride) { if (string.IsNullOrWhiteSpace(CurrentDirectUrl)) { return false; } int audioTrackCount = AudioTrackCount; if (audioTrackCount <= 0) { return false; } int num = Mathf.Clamp(trackIndex, 0, Math.Max(0, audioTrackCount - 1)); if (num == _selectedAudioTrack) { return true; } _selectedAudioTrack = num; _audioSwitchPendingSeek = resumeTimeOverride ?? CurrentTimeSeconds; _audioSwitchPendingWasPlaying = resumePlayingOverride ?? _player.isPlaying; _isAudioSwitching = true; if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _player.source = (VideoSource)1; _player.url = CurrentDirectUrl; ResetAudioRouting(); ConfigureAudioTracks(); _player.Prepare(); return true; } public void ShowEndedState(double seconds) { if (_player.isPlaying || _player.isPrepared) { _player.Stop(); } _player.playbackSpeed = 1f; _lastKnownTimeSeconds = Math.Max(_lastKnownTimeSeconds, seconds); ClearRenderTexture(); SetStatusOverlay("Video Ended!"); } private void ApplyAudioState() { float num = (_isMuted ? 0f : _volume); if (!string.IsNullOrEmpty(_lastVideoId) && SyncVideoPlugin.Settings != null) { num *= Mathf.Clamp01(SyncVideoPlugin.Settings.YouTubeVolumeScale.Value); } int num2 = Math.Max(1, _knownAudioTrackCount); int num3 = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, num2 - 1)); if (HasCachedAudioTracks()) { List<AudioTrackInfo> cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot(); for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++) { ushort num4 = (ushort)cachedAudioTracksSnapshot[i].UnityTrackIndex; try { _player.SetDirectAudioVolume(num4, (i == num3) ? num : 0f); } catch { } } return; } for (ushort num5 = 0; num5 < (ushort)num2; num5++) { try { _player.SetDirectAudioVolume(num5, (num5 == num3) ? num : 0f); } catch { } } } private void OnPrepareCompleted(VideoPlayer source) { source.playbackSpeed = 1f; _lastKnownTimeSeconds = 0.0; if (_isAudioSwitching) { int val = Math.Max(1, (int)source.audioTrackCount); if (!HasCachedAudioTracks()) { _knownAudioTrackCount = Math.Max(_knownAudioTrackCount, val); } _selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1)); ResetAudioRouting(); ConfigureAudioTracks(); ApplyAudioState(); _isAudioSwitching = false; double audioSwitchPendingSeek = _audioSwitchPendingSeek; bool audioSwitchPendingWasPlaying = _audioSwitchPendingWasPlaying; _audioSwitchPendingSeek = 0.0; _audioSwitchPendingWasPlaying = false; if (audioSwitchPendingSeek > 0.05) { source.time = Math.Max(0.0, audioSwitchPendingSeek); _lastKnownTimeSeconds = Math.Max(0.0, audioSwitchPendingSeek); } _subtitleManager.ResetSearchHint(); if (audioSwitchPendingWasPlaying) { source.Play(); } else { UpdatePausedOverlay(); } this.AudioTrackSwitchCompleted?.Invoke(); } else { if (!HasCachedAudioTracks()) { _knownAudioTrackCount = Math.Max(1, (int)source.audioTrackCount); } _selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1)); ResetAudioRouting(); ConfigureAudioTracks(); ApplyAudioState(); ClearRenderTexture(); SetStatusOverlay("Video Loaded!\nPress Play!"); this.Prepared?.Invoke(); } } private void OnErrorReceived(VideoPlayer source, string message) { _logger.LogError((object)("SyncVideo URL Video backend error: " + message)); source.Stop(); source.playbackSpeed = 1f; SetStatusOverlay("Video URL Error!"); ClearRenderTexture(); } private void OnLoopPointReached(VideoPlayer source) { double num = source.time; if (num <= 0.0 && source.frameCount != 0 && source.frameRate > 0f) { num = (float)source.frameCount / source.frameRate; } _lastKnownTimeSeconds = Math.Max(_lastKnownTimeSeconds, num); source.Stop(); source.playbackSpeed = 1f; ClearRenderTexture(); SetStatusOverlay("Video Ended!"); this.Ended?.Invoke(); } private void UpdatePausedOverlay() { if (_player.isPrepared) { SetStatusOverlay((_player.time <= 0.05) ? "Video Loaded!\nPress Play!" : "Paused!"); } } private void SetStatusOverlay(string message) { StatusOverlayText = message ?? string.Empty; } private void ResetAudioRouting() { try { _player.audioOutputMode = (VideoAudioOutputMode)0; _player.audioOutputMode = (VideoAudioOutputMode)2; } catch { } } private void ConfigureAudioTracks() { int num = Math.Max(1, _knownAudioTrackCount); int num2 = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, num - 1)); if (HasCachedAudioTracks()) { List<AudioTrackInfo> cachedAudioTracksSnapshot = GetCachedAudioTracksSnapshot(); try { _player.controlledAudioTrackCount = (ushort)cachedAudioTracksSnapshot.Count; } catch { } for (int i = 0; i < cachedAudioTracksSnapshot.Count; i++) { ushort num3 = (ushort)cachedAudioTracksSnapshot[i].UnityTrackIndex; try { _player.EnableAudioTrack(num3, i == num2); } catch { } } AudioTrackInfo audioTrackInfo = ((num2 >= 0 && num2 < cachedAudioTracksSnapshot.Count) ? cachedAudioTracksSnapshot[num2] : null); if (audioTrackInfo != null) { return; } } try { _player.controlledAudioTrackCount = (ushort)num; } catch { } for (int j = 0; j < num; j++) { try { _player.EnableAudioTrack((ushort)j, j == num2); } catch { } } } private void ResetAudioTrackCache() { CancelAudioProbe(); lock (_audioTrackLock) { _audioTracks.Clear(); } _knownAudioTrackCount = 1; _isAudioProbing = false; } private void CancelAudioProbe() { try { _audioProbeCts?.Cancel(); } catch { } try { _audioProbeCts?.Dispose(); } catch { } _audioProbeCts = null; } private bool HasCachedAudioTracks() { lock (_audioTrackLock) { return _audioTracks.Count > 0; } } private AudioTrackInfo GetCachedAudioTrack(int trackIndex) { lock (_audioTrackLock) { if (trackIndex < 0 || trackIndex >= _audioTracks.Count) { return null; } return _audioTracks[trackIndex]; } } private List<AudioTrackInfo> GetCachedAudioTracksSnapshot() { lock (_audioTrackLock) { return new List<AudioTrackInfo>(_audioTracks); } } private void ProbeAudioTracksIfSupported(string url) { if (string.IsNullOrWhiteSpace(url) || !UrlNormalizer.IsMkvUrl(url)) { return; } string ffmpegPath = SubtitleManager.FindFfmpegPath(); if (ffmpegPath == null) { return; } string ffprobePath = FindFfprobePath(ffmpegPath); CancelAudioProbe(); CancellationTokenSource cts = new CancellationTokenSource(); _audioProbeCts = cts; _isAudioProbing = true; this.AudioTracksChanged?.Invoke(); Task.Run(delegate { List<AudioTrackInfo> detected = null; try { if (ffprobePath != null) { detected = ProbeAudioTracks(url, ffprobePath, cts.Token); } if ((detected == null || detected.Count == 0) && !cts.IsCancellationRequested) { detected = ProbeAudioTracksFromFfmpegStderr(url, ffmpegPath, cts.Token); } } catch (Exception ex) { _logger.LogWarning((object)("[MKV Audio] Probe error: " + ex.Message)); } if (!cts.IsCancellationRequested) { SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(delegate { if (!cts.IsCancellationRequested) { _isAudioProbing = false; if (detected != null && detected.Count > 0) { lock (_audioTrackLock) { _audioTracks.Clear(); _audioTracks.AddRange(detected); } _knownAudioTrackCount = detected.Count; _selectedAudioTrack = Mathf.Clamp(_selectedAudioTrack, 0, Math.Max(0, _knownAudioTrackCount - 1)); ConfigureAudioTracks(); ApplyAudioState(); _logger.LogInfo((object)$"[MKV Audio] Cached {detected.Count} audio track(s)!"); } this.AudioTracksChanged?.Invoke(); } }); } }, cts.Token); } private static List<AudioTrackInfo> ProbeAudioTracks(string url, string ffprobePath, CancellationToken token) { List<AudioTrackInfo> result = new List<AudioTrackInfo>(); ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffprobePath, Arguments = "-v error -probesize 100M -analyzeduration 100M -select_streams a -show_entries stream=index,codec_name,channels:stream_tags=language,title -of default=noprint_wrappers=0 " + QuoteArg(url), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; using (Process process = new Process { StartInfo = startInfo }) { process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(15000); if (!process.HasExited) { try { process.Kill(); } catch { } return result; } ParseFfprobeAudioStreams(output, result, token); } return result; } private static void ParseFfprobeAudioStreams(string output, List<AudioTrackInfo> result, CancellationToken token) { AudioTrackInfo audioTrackInfo = null; string[] array = (output ?? string.Empty).Replace("\r\n", "\n").Split(new char[1] { '\n' }); foreach (string text in array) { if (token.IsCancellationRequested) { break; } string text2 = text.Trim(); if (text2.Length == 0) { continue; } if (text2.StartsWith("index=", StringComparison.OrdinalIgnoreCase)) { if (audioTrackInfo != null && audioTrackInfo.StreamIndex >= 0) { result.Add(audioTrackInfo); } audioTrackInfo = new AudioTrackInfo { StreamIndex = -1, UnityTrackIndex = result.Count }; if (int.TryParse(text2.Substring("index=".Length).Trim(), out var result2)) { audioTrackInfo.StreamIndex = result2; } continue; } if (audioTrackInfo == null) { audioTrackInfo = new AudioTrackInfo { StreamIndex = -1, UnityTrackIndex = result.Count }; } if (text2.StartsWith("codec_name=", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.Codec = text2.Substring("codec_name=".Length).Trim(); } else if (text2.StartsWith("channels=", StringComparison.OrdinalIgnoreCase)) { if (int.TryParse(text2.Substring("channels=".Length).Trim(), out var result3)) { audioTrackInfo.Channels = result3; } } else if (text2.StartsWith("TAG:language=", StringComparison.OrdinalIgnoreCase)) { string text3 = text2.Substring("TAG:language=".Length).Trim().Trim('[', ']'); if (!string.IsNullOrWhiteSpace(text3) && !text3.Equals("N/A", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.Language = text3; } } else if (text2.StartsWith("TAG:title=", StringComparison.OrdinalIgnoreCase)) { string text4 = text2.Substring("TAG:title=".Length).Trim(); if (!string.IsNullOrWhiteSpace(text4) && !text4.Equals("N/A", StringComparison.OrdinalIgnoreCase)) { audioTrackInfo.Title = text4; } } } if (audioTrackInfo != null && audioTrackInfo.StreamIndex >= 0) { result.Add(audioTrackInfo); } } private static List<AudioTrackInfo> ProbeAudioTracksFromFfmpegStderr(string url, string ffmpegPath, CancellationToken token) { List<AudioTrackInfo> list = new List<AudioTrackInfo>(); ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffmpegPath, Arguments = "-probesize 10000000 -analyzeduration 10000000 -i " + QuoteArg(url) + " -hide_banner", RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using (Process process = new Process { StartInfo = startInfo }) { process.Start(); string text = process.StandardError.ReadToEnd(); process.WaitForExit(15000); if (!process.HasExited) { try { process.Kill(); } catch { } return list; } if (token.IsCancellationRequested) { return list; } AudioTrackInfo audioTrackInfo = null; string[] array = text.Split(new char[1] { '\n' }); foreach (string text2 in array) { if (token.IsCancellationRequested) { break; } string text3 = text2.TrimEnd(new char[1] { '\r' }); if (text3.IndexOf("Stream #", StringComparison.Ordinal) >= 0) { if (audioTrackInfo != null) { list.Add(audioTrackInfo); audioTrackInfo = null; } Match match = _audioStreamRx.Match(text3); if (match.Success) { int.TryParse(match.Groups[1].Value, out var result); string text4 = match.Groups[2].Value.Trim().Trim('[', ']'); if (text4.Equals("und", StringComparison.OrdinalIgnoreCase)) { text4 = string.Empty; } audioTrackInfo = new AudioTrackInfo { StreamIndex = result, UnityTrackIndex = list.Count, Language = text4 }; } } else if (audioTrackInfo != null) { Match match2 = _audioTitleLineRx.Match(text3); if (match2.Success) { audioTrackInfo.Title = match2.Groups[1].Value.Trim(); } } } if (audioTrackInfo != null && !token.IsCancellationRequested) { list.Add(audioTrackInfo); } } return list; } private static string BuildAudioTrackDetail(AudioTrackInfo info) { string text = CleanTrackText(info.Title); string text2 = FormatLanguage(info.Language); string text3 = (string.IsNullOrWhiteSpace(info.Codec) ? string.Empty : info.Codec.Trim().ToUpperInvariant()); string text4 = FormatChannels(info.Channels); string text5 = text; if (string.IsNullOrWhiteSpace(text5)) { if (!string.IsNullOrWhiteSpace(text2)) { text5 = text2; } if (!string.IsNullOrWhiteSpace(text4)) { text5 = (string.IsNullOrWhiteSpace(text5) ? text4 : (text5 + " " + text4)); } if (!string.IsNullOrWhiteSpace(text3)) { text5 = (string.IsNullOrWhiteSpace(text5) ? text3 : (text5 + " " + text3)); } } if (!string.IsNullOrWhiteSpace(text2) && text5.IndexOf(text2, StringComparison.OrdinalIgnoreCase) < 0) { text5 = text5 + " - [" + text2 + "]"; } return text5.Trim(); } private static string CleanTrackText(string value) { return (value ?? string.Empty).Replace("_", " ").Trim(); } private static string FormatChannels(int channels) { return channels switch { 1 => "1.0", 2 => "2.0", 6 => "5.1", 8 => "7.1", _ => (channels > 0) ? (channels + "ch") : string.Empty, }; } private static string FormatLanguage(string language) { if (string.IsNullOrWhiteSpace(language) || language.Equals("und", StringComparison.OrdinalIgnoreCase)) { return string.Empty; } switch (language.Trim().ToLowerInvariant()) { case "en": case "eng": return "English"; case "ja": case "jpn": case "jp": return "Japanese"; case "es": case "spa": return "Spanish"; case "fr": case "fre": case "fra": return "French"; case "de": case "ger": case "deu": return "German"; case "it": case "ita": return "Italian"; case "pt": case "por": return "Portuguese"; default: return language.Trim(); } } private static string FindFfprobePath(string ffmpegPath) { try { if (!string.IsNullOrWhiteSpace(ffmpegPath)) { string path = Path.GetDirectoryName(ffmpegPath) ?? string.Empty; string[] array = new string[2] { "ffprobe.exe", "ffprobe" }; foreach (string path2 in array) { string text = Path.Combine(path, path2); if (File.Exists(text)) { return text; } } } } catch { } string text2 = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; string[] array2 = text2.Split(new char[1] { Path.PathSeparator }); foreach (string text3 in array2) { string[] array3 = new string[2] { "ffprobe.exe", "ffprobe" }; foreach (string path3 in array3) { try { string text4 = Path.Combine(text3.Trim(), path3); if (File.Exists(text4)) { return text4; } } catch { } } } return null; } private static string QuoteArg(string value) { return "\"" + (value ?? string.Empty).Replace("\"", "\\\"") + "\""; } private static string ResolveSubtitleSourceUrl(string directPlayableUrl, string originalUrl) { if (!string.IsNullOrWhiteSpace(originalUrl) && UrlNormalizer.IsMkvUrl(originalUrl)) { return originalUrl; } return directPlayableUrl ?? string.Empty; } private void ProbeSubtitlesIfSupported(string url) { _subtitleManager.Clear(); if (string.IsNullOrWhiteSpace(url) || !SubtitleManager.IsSubtitleProbeSupported(url)) { return; } string ffmpegPath = SubtitleManager.FindFfmpegPath(); if (ffmpegPath == null) { return; } _subtitleManager.ProbeAsync(url, ffmpegPath, delegate { _subtitleManager.EagerExtractAllTracksAsync(url, ffmpegPath); SyncVideoPlugin.SyncController?.EnqueueMainThreadAction(delegate { SyncVideoPlugin.SyncController?.ReapplyLobbyTrackSelection(); }); }); } private void ClearRenderTexture() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_texture == (Object)null)) { RenderTexture active = RenderTexture.active; RenderTexture.active = _texture; GL.Clear(true, true, Color.black); RenderTexture.active = active; } } } public enum HudMode { Off, UI, UIChat, UIAll } public static class HudManager { private static HudMode _currentMode; private static bool _savedShowChat; private static bool _savedShowNamePlates; private static bool _savedShowAFKEffects; private static bool _savedAfkMessages; private static bool _hasSavedSettings; private static bool _suppressAfkForLobby; private static float _afkTickTimer; private const float AfkTickInterval = 0.5f; private static CanvasGroup _gameplayCanvasGroup; private static bool _musicMuted; public static HudMode CurrentMode => _currentMode; private static object GetMusicPlayer() { try { if ((Object)(object)Core.Instance == (Object)null) { return null; } Traverse val = Traverse.Create((object)Core.Instance); object obj = val.Property("AudioManager", (object[])null).GetValue() ?? val.Property("audioManager", (object[])null).GetValue() ?? val.Field("audioManager").GetValue() ?? val.Field("AudioManager").GetValue(); if (obj == null) { return null; } Traverse val2 = Traverse.Create(obj); return val2.Property("musicPlayer", (object[])null).GetValue() ?? val2.Property("MusicPlayer", (object[])null).GetValue() ?? val2.Field("musicPlayer").GetValue() ?? val2.Field("MusicPlayer").GetValue(); } catch { return null; } } private static object GetAudioManager() { try { if ((Object)(object)Core.Instance == (Object)null) { return null; } Traverse val = Traverse.Create((object)Core.Instance); return val.Property("AudioManager", (object[])null).GetValue() ?? val.Property("audioManager", (object[])null).GetValue() ?? val.Field("audioManager").GetValue() ?? val.Field("AudioManager").GetValue(); } catch { return null; } } public static string GetLabel() { return _currentMode switch { HudMode.Off => "Hide HUD: <color=green>Off</color>", HudMode.UI => "Hide HUD: <color=yellow>UI</color>", HudMode.UIChat => "Hide HUD: <color=yellow>UI+Chat</color>", HudMode.UIAll => "Hide HUD: <color=yellow>Everything</color>", _ => "Hide HUD", }; } public static void Cycle() { _currentMode = (HudMode)((int)(_currentMode + 1) % 4); Apply(); } public static void Tick() { if (_currentMode < HudMode.UI) { if (!_suppressAfkForLobby) { return; } VideoLobbyManager lobbyManager = SyncVideoPlugin.LobbyManager; if (lobbyManager == null || !lobbyManager.InLobby) { return; } } _afkTickTimer += Time.unscaledDeltaTime; if (_afkTickTimer < 0.5f) { return; } _afkTickTimer = 0f; try { PlayerComponent local = PlayerComponent.GetLocal(); if (local != null) { local.StopAFK(); } } catch { } try { MPSettings instance = MPSettings.Instance; if (instance != null) { if (instance.ShowAFKEffects) { instance.ShowAFKEffects = false; } if (instance.AFKMessages) { instance.AFKMessages = false; } } } catch { } } public static void OnLobbyEnter() { MPSettings instance = MPSettings.Instance; if (instance != null && !_hasSavedSettings) { _savedShowChat = instance.ShowChat; _savedShowNamePlates = instance.ShowNamePlates; _savedShowAFKEffects = instance.ShowAFKEffects; _savedAfkMessages = instance.AFKMessages; _hasSavedSettings = true; } SyncVideoConfig settings = SyncVideoPlugin.Settings; _suppressAfkForLobby = settings != null && (settings.SuppressAFK?.Value).GetValueOrDefault(); SyncVideoConfig settings2 = SyncVideoPlugin.Settings; if (settings2 != null && (settings2.MuteMusicAndAmbient?.Value).GetValueOrDefault()) { MuteForLobby(); } Apply(); } public static void Reset() { _currentMode = HudMode.Off; _suppressAfkForLobby = false; _afkTickTimer = 0f; ApplyGameplayUiHide(hide: false); MPSettings instance = MPSettings.Instance; if (instance != null) { instance.ShowChat = !_hasSavedSettings || _savedShowChat; instance.ShowNamePlates = !_hasSavedSettings || _savedShowNamePlates; instance.ShowAFKEffects = !_hasSavedSettings || _savedShowAFKEffects; instance.AFKMessages = !_hasSavedSettings || _savedAfkMessages; } _hasSavedSettings = false; RestoreAudio(); } private static void MuteForLobby() { if (_musicMuted) { return; } object audioManager = GetAudioManager(); if (audioManager == null) { _musicMuted = true; return; } Type type = audioManager.GetType(); BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; try { type.GetMethod("MuteMusic", bindingAttr)?.Invoke(audioManager, null); } catch { } try { if (type.GetField("ambienceAudioSources", bindingAttr)?.GetValue(audioManager) is AudioSource[] array) { AudioSource[] array2 = array; foreach (AudioSource val in array2) { if ((Object)(object)val != (Object)null) { val.mute = true; } } } } catch { } try { type.GetMethod("PauseAllGameplayLoopingSfx", bindingAttr)?.Invoke(audioManager, null); } catch { } _musicMuted = true; } private static void RestoreAudio() { if (!_musicMuted) { return; } object audioManager = GetAudioManager(); BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; try { audioManager?.GetType().GetMethod("UnMuteMusic", bindingAttr)?.Invoke(audioManager, null); } catch { } try { if (audioManager != null && audioManager.GetType().GetField("ambienceAudioSources", bindingAttr)?.GetValue(audioManager) is AudioSource[] array) { AudioSource[] array2 = array; foreach (AudioSource val in array2) { if ((Object)(object)val != (Object)null) { val.mute = false; } } } } catch { } try { audioManager?.GetType().GetMethod("ResumeAllGameplayLoopingSfx", bindingAttr)?.Invoke(audioManager, null); } catch { } _musicMuted = false; } private static void Apply() { bool hide = _currentMode >= HudMode.UI; bool flag = _currentMode >= HudMode.UI || _suppressAfkForLobby; bool flag2 = _currentMode >= HudMode.UIChat; bool flag3 = _currentMode >= HudMode.UIAll; MPSettings instance = MPSettings.Instance; if (instance != null && !_hasSavedSettings) { _savedShowChat = instance.ShowChat; _savedShowNamePlates = instance.ShowNamePlates; _savedShowAFKEffects = instance.ShowAFKEffects; _savedAfkMessages = instance.AFKMessages; _hasSavedSettings = true; } ApplyGameplayUiHide(hide); if (instance != null) { instance.ShowChat = !flag2 && (!_hasSavedSettings || _savedShowChat); instance.ShowNamePlates = !flag3 && (!_hasSavedSettings || _savedShowNamePlates); instance.ShowAFKEffects = !flag && (!_hasSavedSettings || _savedShowAFKEffects); instance.AFKMessages = !flag && (!_hasSavedSettings || _savedAfkMessages); } } private static void ApplyGameplayUiHide(bool hide) { try { Core instance = Core.Instance; UIManager val = ((instance != null) ? instance.UIManager : null); if ((Object)(object)val == (Object)null) { return; } if ((Object)(object)_gameplayCanvasGroup == (Object)null) { Traverse val2 = Traverse.Create((object)val); object value = val2.Field("gameplay").GetValue(); Component val3 = (Component)((value is Component) ? value : null); if ((Object)(object)val3 == (Object)null) { return; } _gameplayCanvasGroup = val3.GetComponent<CanvasGroup>(); if ((Object)(object)_gameplayCanvasGroup == (Object)null) { _gameplayCanvasGroup = val3.gameObject.AddComponent<CanvasGroup>(); } } _gameplayCanvasGroup.alpha = (hide ? 0f : 1f); _gameplayCanvasGroup.interactable = !hide; _gameplayCanvasGroup.blocksRaycasts = !hide; } catch { _gameplayCanvasGroup = null; } } } public interface IVideoBackend { bool IsPrepared { get; } bool IsPlaying { get; } double CurrentTimeSeconds { get; } object OutputTexture { get; } string StatusOverlayText { get; } void Load(string directPlayableUrl, string originalUrl, string videoId); void Play(); void Pause(); void Stop(); void Seek(double seconds); void NudgeToward(double seconds, double driftSeconds); void Tick(float deltaTime); } public sealed class LobbyUiOverrideManager : IDisposable { private readonly ManualLogSource _logger; private readonly VideoLobbyManager _lobbyManager; private LobbyUI _cachedLobbyUi; private Canvas _cachedCanvas; private CanvasGroup _cachedCanvasGroup; private TextMeshProUGUI _cachedLobbyName; private GameObject _cachedLobbySettings; private GameObject _cachedGameplayUi; private string _originalLobbyName; private bool _originalLobbySettingsActive; private bool _originalGameplayUiActive; private float _inviteRewriteTimer; private float _publicLobbyFilterTimer; private TextMeshProUGUI[] _cachedTexts; private float _textsRefreshTimer; private const float TextsRefreshInterval = 3f; private bool _isPublicLobbyAppOpen; private float _appOpenCheckTimer; private const float AppOpenCheckInterval = 0.5f; private bool? _lastHideAll; private float _tickAccumulator; private const float TickInterval = 1f / 30f; private readonly HashSet<string> _lobbyNamesBuffer = new HashSet<string>(StringComparer.Ordinal); private static readonly HashSet<GameObject> _hiddenObjects = new HashSet<GameObject>(); public LobbyUiOverrideManager(ManualLogSource logger, VideoLobbyManager lobbyManager) { _logger = logger; _lobbyManager = lobbyManager; } public void Dispose() { } public void Tick(float deltaTime) { _tickAccumulator += deltaTime; if (_tickAccumulator < 1f / 30f) { return; } float tickAccumulator = _tickAccumulator; _tickAccumulator = 0f; try { ApplyOverride(tickAccumulator); } catch (Exception ex) { _logger.LogWarning((object)("Failed to override All City Network lobby UI: " + ex.Message)); ClearCache(); } } private void ApplyOverride(float deltaTime) { bool flag = _lobbyManager != null && _lobbyManager.InLobby; if (_isPublicLobbyAppOpen || !flag) { _appOpenCheckTimer -= deltaTime; if (_appOpenCheckTimer <= 0f) { _appOpenCheckTimer = 0.5f; _isPublicLobbyAppOpen = IsBombRushMpPublicLobbyAppOpen(); } } bool flag2 = !flag && (_lobbyManager?.Lobbies.Count ?? 0) > 0; if (flag2 || _isPublicLobbyAppOpen) { _textsRefreshTimer -= deltaTime; if (_textsRefreshTimer <= 0f) { _textsRefreshTimer = 3f; _cachedTexts = Resources.FindObjectsOfTypeAll<TextMeshProUGUI>(); } if (flag2) { _inviteRewriteTimer -= deltaTime; if (_inviteRewriteTimer <= 0f) { _inviteRewriteTimer = 0.75f; RewriteInviteTexts(_cachedTexts); } } if (_isPublicLobbyAppOpen) { _publicLobbyFilterTimer -= deltaTime; if (_publicLobbyFilterTimer <= 0f) { _publicLobbyFilterTimer = 1.5f; HideSyncVideoLobbiesFromMultiplayerMenu(_cachedTexts); } } } if (!flag) { if ((Object)(object)_cachedCanvas != (Object)null) { RestoreOverriddenElements(); if (_lastHideAll != false) { SetCanvasHidden(hidden: false); } ClearCache(); _lastHideAll = null; } } else if (TryResolveReferences()) { bool flag3 = _lobbyManager.LeaveInProgress || SyncVideoPlugin.Settings.HideNativeLobbyUi.Value; if ((Object)(object)_cachedLobbySettings != (Object)null && _cachedLobbySettings.activeSelf) { _cachedLobbySettings.SetActive(false); } if ((Object)(object)_cachedGameplayUi != (Object)null && _cachedGameplayUi.activeSelf) { _cachedGameplayUi.SetActive(false); } if ((Object)(object)_cachedLobbyName != (Object)null && ((TMP_Text)_cachedLobbyName).text != "Sync Video Lobby" && ((TMP_Text)_cachedLobbyName).text != "Sync Video Watch Party") { ((TMP_Text)_cachedLobbyName).text = "Sync Video Lobby"; } if (_lastHideAll != flag3) { _lastHideAll = flag3; SetCanvasHidden(flag3); } } } private bool TryResolveReferences() { LobbyUI instance = LobbyUI.Instance; if ((Object)(object)instance == (Object)null) { ClearCache(); return false; } if ((Object)(object)_cachedLobbyUi == (Object)(object)instance && (Object)(object)_cachedCanvas != (Object)null) { return true; } _cachedLobbyUi = instance; Transform transform = ((Component)instance).transform; Transform val = FindDeepChild(transform, "Canvas"); if ((Object)(object)val == (Object)null) { ClearCache(); return false; } _cachedCanvas = ((Component)val).GetComponent<Canvas>(); if ((Object)(object)_cachedCanvas == (Object)null) { ClearCache(); return false; } _cachedCanvasGroup = ((Component)val).GetComponent<CanvasGroup>(); if ((Object)(object)_cachedCanvasGroup == (Object)null) { _cachedCanvasGroup = ((Component)val).gameObject.AddComponent<CanvasGroup>(); } _cachedLobbyName = FindDeepTMP(transform, "Lobby Name"); Transform val2 = FindDeepChild(transform, "Lobby Settings"); _cachedLobbySettings = (((Object)(object)val2 != (Object)null) ? ((Component)val2).gameObject : null); Transform val3 = FindDeepChild(transform, "Gameplay UI"); _cachedGameplayUi = (((Object)(object)val3 != (Object)null) ? ((Component)val3).gameObject : null); _originalLobbyName = (((Object)(object)_cachedLobbyName != (Object)null) ? ((TMP_Text)_cachedLobbyName).text : null); _originalLobbySettingsActive = (Object)(object)_cachedLobbySettings != (Object)null && _cachedLobbySettings.activeSelf; _originalGameplayUiActive = (Object)(object)_cachedGameplayUi != (Object)null && _cachedGameplayUi.activeSelf; return true; } private void HideSyncVideoLobbiesFromMultiplayerMenu(TextMeshProUGUI[] texts) { if (_lobbyManager == null || !_isPublicLobbyAppOpen) { return; } _lobbyNamesBuffer.Clear(); foreach (VideoLobby lobby in _lobbyManager.Lobbies) { if (lobby != null && !string.IsNullOrWhiteSpace(lobby.LobbyName)) { _lobbyNamesBuffer.Add(lobby.LobbyName); } } if (_lobbyNamesBuffer.Count == 0 || texts == null) { return; } foreach (TextMeshProUGUI val in texts) { if (!((Object)(object)val == (Object)null)) { string text = ((TMP_Text)val).text; if (!string.IsNullOrWhiteSpace(text) && _lobbyNamesBuffer.Contains(text.Trim())) { HideContainingLobbyButton(val); } } } } private static bool IsBombRushMpPublicLobbyAppOpen() { try { Type type = Type.GetType("Reptile.Core, Assembly-CSharp"); if (type == null) { return false; } PropertyInfo property = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public); object obj = ((property != null) ? property.GetValue(null, null) : null); if (obj == null) { return false; } PropertyInfo property2 = type.GetProperty("UIManager", BindingFlags.Instance | BindingFlags.Public); object obj2 = ((property2 != null) ? property2.GetValue(obj, null) : null); if (obj2 == null) { return false; } Type type2 = obj2.GetType(); PropertyInfo property3 = type2.GetProperty("MyPhone", BindingFlags.Instance | BindingFlags.Public); object obj3 = ((property3 != null) ? property3.GetValue(obj2, null) : null); if (obj3 == null) { return false; } Type type3 = obj3.GetType(); FieldInfo field = type3.GetField("m_CurrentApp", BindingFlags.Instance | BindingFlags.NonPublic); object obj4 = ((field != null) ? field.GetValue(obj3) : null); if (obj4 == null) { PropertyInfo property4 = type3.GetProperty("CurrentApp", BindingFlags.Instance | BindingFlags.Public); obj4 = ((property4 != null) ? property4.GetValue(obj3, null) : null); } return obj4 != null && string.Equals(obj4.GetType().Name, "AppMultiplayerPublicLobbies", StringComparison.Ordinal); } catch { return false; } } private static void HideContainingLobbyButton(TextMeshProUGUI tmp) { if ((Object)(object)tmp == (Object)null) { return; } Transform val = ((TMP_Text)tmp).transform; int num = 0; while ((Object)(object)val != (Object)null && num < 8) { if (!((Object)(object)val == (Object)null)) { GameObject gameObject = ((Component)val).gameObject; if (_hiddenObjects.Contains(gameObject)) { break; } Component[] components = ((Component)val).GetComponents<Component>(); foreach (Component val2 in components) { if ((Object)(object)val2 == (Object)null) { continue; } string name = ((object)val2).GetType().Name; if (name.IndexOf("PhoneScrollButton", StringComparison.OrdinalIgnoreCase) >= 0 || name.Equals("Button", StringComparison.OrdinalIgnoreCase)) { if (gameObject.activeSelf) { gameObject.SetActive(false); _hiddenObjects.Add(gameObject); } return; } } } num++; val = val.parent; } } private static void RewriteInviteTexts(TextMeshProUGUI[] texts) { if (texts == null) { return; } foreach (TextMeshProUGUI val in texts) { if ((Object)(object)val == (Object)null) { continue; } string text = ((TMP_Text)val).text; if (!string.IsNullOrEmpty(text) && text.IndexOf("Has invited you to their", StringComparison.OrdinalIgnoreCase) >= 0) { string text2 = text.Replace("Pro Skater Score Battle", "Sync Video").Replace("Pro Skater Battle", "Sync Video"); if (!string.Equals(text, text2, StringComparison.Ordinal)) { ((TMP_Text)val).text = text2; } } } } private void RestoreOverriddenElements() { if ((Object)(object)_cachedLobbySettings != (Object)null) { _cachedLobbySettings.SetActive(_originalLobbySettingsActive); } if ((Object)(object)_cachedGameplayUi != (Object)null) { _cachedGameplayUi.SetActive(_originalGameplayUiActive); } if ((Object)(object)_cachedLobbyName != (Object)null && _originalLobbyName != null) { ((TMP_Text)_cachedLobbyName).text = _originalLobbyName; } } private void SetCanvasHidden(bool hidden) { if (!((Object)(object)_cachedCanvas == (Object)null) && !((Object)(object)_cachedCanvasGroup == (Object)null)) { ((Behaviour)_cachedCanvas).enabled = !hidden; _cachedCanvasGroup.alpha = (hidden ? 0f : 1f); _cachedCanvasGroup.interactable = !hidden; _cachedCanvasGroup.blocksRaycasts = !hidden; } } private void ClearCache() { _lastHideAll = null; _cachedLobbyUi = null; _cachedCanvas = null; _cachedCanvasGroup = null; _cachedLobbyName = null; _cachedLobbySettings = null; _cachedGameplayUi = null; _originalLobbyName = null; _originalLobbySettingsActive = false; _originalGameplayUiActive = false; } private static Transform FindDeepChild(Transform root, string name) { if ((Object)(object)root == (Object)null) { return null; } if (((Object)root).name == name) { return root; } for (int i = 0; i < root.childCount; i++) { Transform val = FindDeepChild(root.GetChild(i), name); if ((Object)(object)val != (Object)null) { return val; } } return null; } private static TextMeshProUGUI FindDeepTMP(Transform root, string name) { Transform val = FindDeepChild(root, name); return ((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<TextMeshProUGUI>() : null; } } public sealed class SubtitleTrackInfo { public int StreamIndex; public string Language; public string Title; } public sealed class AudioTrackInfo { public int StreamIndex; public int AudioIndex; public string Language; public string Title; public string CodecName; public string GetMenuLabel() { string text = ((!string.IsNullOrWhiteSpace(Title)) ? Title.Trim() : string.Empty); string text2 = ((!string.IsNullOrWhiteSpace(Language) && !Language.Equals("und", StringComparison.OrdinalIgnoreCase)) ? Language.Trim() : string.Empty); if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2)) { return text + " - [" + text2 + "]"; } if (!string.IsNullOrWhiteSpace(text)) { return text; } if (!string.IsNullOrWhiteSpace(text2)) { return "[" + text2 + "]"; } if (!string.IsNullOrWhiteSpace(CodecName)) { return CodecName; } return string.Empty; } } public sealed class SubtitleEntry { public double StartTime; public double EndTime; public string Text; } public sealed class SubtitleManager { private readonly ManualLogSource _logger; private volatile List<SubtitleTrackInfo> _tracks = new List<SubtitleTrackInfo>(); private volatile List<SubtitleEntry> _entries = new List<SubtitleEntry>(); private int _selectedTrack = -1; private int _searchHint = 0; private volatile bool _isProbing = false; private volatile bool _isExtracting = false; private volatile bool _hasLoggedSubtitleReady = false; private CancellationTokenSource _sessionCts; private CancellationTokenSource _selectionCts; private static readonly Regex _htmlTagRx = new Regex("<[^>]+>", RegexOptions.Compiled); private static readonly Regex _ssaOverrideTagRx = new Regex("\\{\\\\[^}]*\\}", RegexOptions.Compiled); private static readonly Regex _titleLineRx = new Regex("^\\s+title\\s*:\\s*(.+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex _videoCodecRx = new Regex("Stream #0:\\d+[^:]*: Video: (\\w+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly ConcurrentDictionary<string, List<SubtitleTrackInfo>> _trackCache = new ConcurrentDictionary<string, List<SubtitleTrackInfo>>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, string> _subtitleSrtCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, List<SubtitleEntry>> _subtitleEntryCache = new ConcurrentDictionary<string, List<SubtitleEntry>>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, byte> _subtitleCompleteCache = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, Task<string>> _subtitleExtractTasks = new ConcurrentDictionary<string, Task<string>>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, Process> _subtitleProcesses = new ConcurrentDictionary<string, Process>(StringComparer.Ordinal); private static readonly ConcurrentDictionary<string, List<Action<List<SubtitleEntry>>>> _subtitleProgressListeners = new ConcurrentDictionary<string, List<Action<List<SubtitleEntry>>>>(StringComparer.Ordinal); private static readonly Regex _streamSubRx = new Regex("Stream #0:(\\d+)(?:\\(([^)]+)\\))?[^:]*: Subtitle", RegexOptions.IgnoreCase | RegexOptions.Compiled); public int TrackCount => _tracks.Count; public int SelectedTrack => _selectedTrack; public bool IsProbing => _isProbing; public bool IsExtracting => _isExtracting; public SubtitleManager(ManualLogSource logger) { _logger = logger; } public string GetTrackLabel(int index) { List<SubtitleTrackInfo> tracks = _tracks; if (index < 0 || index >= tracks.Count) { return "Subtitle Track " + (index + 1); } SubtitleTrackInfo subtitleTrackInfo = tracks[index]; string text = "Subtitle Track " + (index + 1); string text2 = StripOuterBrackets(subtitleTrackInfo.Title); if (!string.IsNullOrWhiteSpace(text2)) { text = text + " (" + text2 + ")"; } else { string text3 = FormatLanguage(StripOuterBrackets(subtitleTrackInfo.Language)); if (!string.IsNullOrWhiteSpace(text3)) { text = text + " (" + text3 + ")"; } } return text; } private static string StripOuterBrackets(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } string text = value.Trim(); if (text.Length >= 2 && text[0] == '[' && text[text.Length - 1] == ']') { return text.Substring(1, text.Length - 2).Trim(); } return text; } private static string FormatLanguage(string language) { if (string.IsNullOrWhiteSpace(language) || language.Equals("und", StringComparison.OrdinalIgnoreCase)) { return string.Empty; } switch (language.Trim().ToLowerInvariant()) { case "en": case "eng": return "English"; case "ja": case "jpn": case "jp": return "Japanese"; case "es": case "spa": return "Spanish"; case "fr": case "fre": case "fra": return "French"; case "de": case "ger": case "deu": return "German"; case "it": case "ita": return "Italian"; case "pt": case "por": return "Portuguese"; case "zh": case "zho": case "chi": return "Chinese"; case "ko": case "kor": return "Korean"; case "ru": case "rus": return "Russian"; case "ar": case "ara": return "Arabic"; default: return language.Trim(); } } public static bool IsSubtitleProbeSupported(string url) { if (string.IsNullOrWhiteSpace(url)) { return false; } string text = url; int num = text.IndexOf('?'); if (num >= 0) { text = text.Substring(0, num); } string extension = Path.GetExtension(text); if (string.IsNullOrWhiteSpace(extension)) { return false; } switch (extension.ToLowerInvariant()) { case ".mkv": case ".mp4": case ".m4v": case ".mov": case ".webm": case ".avi": return true; default: return false; } } public void ProbeAsync(string url, string ffmpegPath, Action onComplete) { CancelAllWork(); _tracks = new List<SubtitleTrackInfo>(); _entries = new List<SubtitleEntry>(); _selectedTrack = -1; _searchHint = 0; _hasLoggedSubtitleReady = false; if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(ffmpegPath)) { onComplete?.Invoke(); return; } if (_trackCache.TryGetValue(url, out var value)) { _tracks = new List<SubtitleTrackInfo>(value); onComplete?.Invoke(); return; } _isProbing = true; _sessionCts = new CancellationTokenSource(); CancellationToken token = _sessionCts.Token; ThreadPool.QueueUserWorkItem(delegate { try { if (!token.IsCancellationRequested) { List<SubtitleTrackInfo> list = ProbeSubtitleTracks(url, ffmpegPath, token); if (!token.IsCancellationRequested) { _tracks = list; if (list != null && list.Count > 0) { _trackCache[url] = new List<SubtitleTrackInfo>(list); } } } } catch (Exception ex) { if (_logger != null) { _logger.LogWarning((object)("[SubtitleManager] Probe error: " + ex.Message)); } } finally { _isProbing = false; if (!token.IsCancellationRequested) { onComplete?.Invoke(); } } }); } public void SelectTrack(int trackIndex, string url, string ffmpegPath, Action onComplete) { CancelSelectionOnly(); _entries = new List<SubtitleEntry>(); _searchHint = 0; _hasLoggedSubtitleReady = false; List<SubtitleTrackInfo> tracks = _tracks; if (trackIndex < 0 || trackIndex >= tracks.Count) { _selectedTrack = -1; onComplete?.Invoke(); return; } _selectedTrack = trackIndex; _isExtracting = true; _selectionCts = new CancellationTokenSource(); CancellationToken selectionToken = _selectionCts.Token; CancellationToken sessionToken = ((_sessionCts != null) ? _sessionCts.Token : CancellationToken.None); int streamIndex = tracks[trackIndex].StreamIndex; string cacheKey = BuildSubtitleCacheKey(url, streamIndex); if (TryGetParsedSubtitleEntries(cacheKey, out var entries)) { _entries = entries; LogSubtitlesReady(entries?.Count ?? 0, stillLoading: false); _isExtracting = false; onComplete?.Invoke(); return; } if (TryLoadSubtitleEntriesFromDiskCache(cacheKey, out entries)) { _entries = entries; LogSubtitlesReady(entries?.Count ?? 0, stillLoading: false); _isExtracting = false; onComplete?.Invoke(); return; } int firstProgressNotified = 0; Action<List<SubtitleEntry>> progressListener = delegate(List<SubtitleEntry> partialEntries) { if (!selectionToken.IsCancellationRequested && partialEntries != null && partialEntries.Count != 0) { _entries = partialEntries; LogSubtitlesReady(partialEntries.Count, stillLoading: true); if (Interlocked.Exchange(ref firstProgressNotified, 1) == 0) { _isExtracting = false; onComplete?.Invoke(); } } }; AddSubtitleProgressListener(cacheKey, progressListener); if (TryGetPartialSubtitleEntries(cacheKey, out var entries2) && entries2.Count > 0) { _entries = entries2; LogSubtitlesReady(entries2.Count, stillLoading: true); } Task<string> orStartSubtitleExtraction = GetOrStartSubtitleExtraction(cacheKey, url, streamIndex, ffmpegPath, sessionToken); orStartSubtitleExtraction.ContinueWith(delegate(Task<string> t) { RemoveSubtitleProgressListener(cacheKey, progressListener); if (selectionToken.IsCancellationRequested) { return; } try { if (t.Status == TaskStatus.RanToCompletion && t.Result != null) { if (TryGetParsedSubtitleEntries(cacheKey, out var entries3)) { _entries = entries3; LogSubtitlesReady(entries3?.Count ?? 0, stillLoading: false); } else { _entries = ParseAndCacheSubtitleEntries(cacheKey, t.Result); LogSubtitlesReady((_entries != null) ? _entries.Count : 0, stillLoading: false); } } } catch (Exception ex) { if (_logger != null) { _logger.LogWarning((object)("[SubtitleManager] Extract error: " + ex.Message)); } } finally { bool flag = Interlocked.Exchange(ref firstProgressNotified, 1) == 0; _isExtracting = false; if (flag && !selectionToken.IsCancellationRequested) { onComplete?.Invoke(); } } }, TaskScheduler.Default); } public void DisableSubtitles() { Cancel(); _selectedTrack = -1; _entries = new List<SubtitleEntry>(); _searchHint = 0; _hasLoggedSubtitleReady = false; } internal void EagerExtractAllTracksAsync(string url, string ffmpegPath) { List<SubtitleTrackInfo> tracks = _tracks; if (tracks == null || tracks.Count == 0 || string.IsNullOrWhiteSpace(url)) { return; } CancellationToken sessionToken = ((_sessionCts != null) ? _sessionCts.Token : CancellationToken.None); int selectedTrack = _selectedTrack; if (selectedTrack < 0 || selectedTrack >= tracks.Count) { return; } int selectedStreamIndex = tracks[selectedTrack].StreamIndex; string selectedCacheKey = BuildSubtitleCacheKey(url, selectedStreamIndex); List<string> pendingKeys = new List<string>(tracks.Count); List<int> pendingIndexes = new List<int>(tracks.Count); for (int i = 0; i < tracks.Count; i++) { int streamIndex = tracks[i].StreamIndex; string text = BuildSubtitleCacheKey(url, streamIndex); if (!TryGetParsedSubtitleEntries(text, out var entries) && !TryLoadSubtitleEntriesFromDiskCache(text, out entries) && streamIndex != selectedStreamIndex) { pendingKeys.Add(text); pendingIndexes.Add(streamIndex); } } ThreadPool.QueueUserWorkItem(delegate { try { if (!sessionToken.IsCancellationRequested && !TryGetParsedSubtitleEntries(selectedCacheKey, out var entries2)) { GetOrStartSubtitleExtraction(selectedCacheKey, url, selectedStreamIndex, ffmpegPath, sessionToken).Wait(); } for (int j = 0; j < pendingKeys.Count; j++) { if (sessionToken.IsCancellationRequested) { break; } string cacheKey = pendingKeys[j]; int subtitleStreamIndex = pendingIndexes[j]; if (!TryGetParsedSubtitleEntries(cacheKey, out entries2)) { GetOrStartSubtitleExtraction(cacheKey, url, subtitleStreamIndex, ffmpegPath, sessionToken).Wait(); } } } catch { } }); } public void Cancel() { CancelSelectionOnly(); } private void CancelSelectionOnly() { if (_selectionCts != null) { _selectionCts.Cancel(); _selectionCts = null; } _isExtracting = false; } private void CancelAllWork() { CancelSelectionOnly(); if (_sessionCts != null) { _sessionCts.Cancel(); _sessionCts = null; } KillActiveSubtitleProcesses(); _isProbing = false; } public void Clear() { if ((_isExtracting || (_entries != null && _entries.Count > 0)) && _selectedTrack >= 0) { string arg = (_isExtracting ? "Was extracting." : "Extraction finished."); ManualLogSource logger = _logger; if (logger != null) { logger.LogWarning((object)$"[SubtitleManager] Lobby shutting down, subtitles abandoned! Track #{_selectedTrack} has {_entries.Count} Entries. {arg}"); } } CancelAllWork(); _tracks = new List<SubtitleTrackInfo>(); _entries = new List<SubtitleEntry>(); _selectedTrack = -1; _searchHint = 0; _hasLoggedSubtitleReady = false; } public string GetActiveSubtitle(double time) { List<SubtitleEntry> entries = _entries; if (entries == null || entries.Count == 0) { return null; } int count = entries.Count; if (_searchHint >= count) { _searchHint = 0; } SubtitleEntry subtitleEntry = entries[_searchHint]; if (time >= subtitleEntry.StartTime && time < subtitleEntry.EndTime) { return subtitleEntry.Text; } if (time >= subtitleEntry.EndTime) { for (int i = _searchHint + 1; i < count; i++) { SubtitleEntry subtitleEntry2 = entries[i]; if (time < subtitleEntry2.StartTime) { _searchHint = i; return null; } if (time < subtitleEntry2.EndTime) { _searchHint = i; return subtitleEntry2.Text; } } _searchHint = count - 1; return null; } int num = 0; int num2 = count - 1; while (num <= num2) { int num3 = num + num2 >> 1; SubtitleEntry subtitleEntry3 = entries[num3]; if (time < subtitleEntry3.StartTime) { num2 = num3 - 1; continue; } if (time >= subtitleEntry3.EndTime) { num = num3 + 1; continue; } _searchHint = num3; return subtitleEntry3.Text; } return null; } private void LogSubtitlesReady(int loadedEntryCount, bool stillLoading) { if (!_hasLoggedSubtitleReady && loadedEntryCount > 0) { _hasLoggedSubtitleReady = true; if (_logger != null) { string arg = (stillLoading ? "Subtitles loading in background..." : "Subtitles complete!"); _logger.LogInfo((object)string.Format(CultureInfo.InvariantCulture, "[SubtitleManager] Subtitle