Decompiled source of MP4Export v1.0.1

CWMp4Export.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyCompany("U-FESNOS")]
[assembly: AssemblyProduct("MP4Export")]
[assembly: AssemblyTitle("MP4Export")]
[assembly: AssemblyCopyright("U-FESNOS")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyVersion("1.0.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace CWMp4Export
{
	[ContentWarningPlugin("U-FESNOS.MP4Export", "1.0.1", true)]
	[BepInPlugin("U-FESNOS.MP4Export", "CWMp4Export", "1.0.1")]
	public class Plugin : BaseUnityPlugin
	{
		private sealed class Job
		{
			public string FFmpeg = string.Empty;

			public string Input = string.Empty;

			public string Encoder = "libx264";

			public int Quality = 20;

			public string Preset = "veryfast";

			public string AudioBitrate = "160k";

			public bool DeleteInput;
		}

		public const string Guid = "U-FESNOS.MP4Export";

		public const string Name = "CWMp4Export";

		public const string Version = "1.0.1";

		internal static ManualLogSource Log = null;

		private static string pluginDir = string.Empty;

		private static ConfigEntry<string> ffmpegPath = null;

		private static ConfigEntry<bool> deleteWebm = null;

		private static ConfigEntry<string> videoEncoder = null;

		private static ConfigEntry<int> quality = null;

		private static ConfigEntry<string> x264Preset = null;

		private static ConfigEntry<string> audioBitrate = null;

		private static ConfigEntry<bool> showStatus = null;

		private static readonly object StatusLock = new object();

		private static string statusText = string.Empty;

		private static DateTime statusUntil = DateTime.MinValue;

		private void Awake()
		{
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			pluginDir = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? Paths.PluginPath;
			ffmpegPath = ((BaseUnityPlugin)this).Config.Bind<string>("General", "FFmpegPath", string.Empty, "ffmpeg.exe のフルパス。空欄なら自動検索 (このModのフォルダ -> PATH -> winget -> choco -> scoop)");
			deleteWebm = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DeleteWebmAfterConvert", false, "変換に成功したら元の .webm を削除する");
			showStatus = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ShowStatus", true, "画面左上に変換の状態を表示する");
			videoEncoder = ((BaseUnityPlugin)this).Config.Bind<string>("Encode", "VideoEncoder", "libx264", "映像エンコーダ。libx264 (CPU, 標準) / h264_nvenc (NVIDIA GPU) など");
			quality = ((BaseUnityPlugin)this).Config.Bind<int>("Encode", "Quality", 20, "画質。小さいほど高画質で容量が大きい (libx264 は CRF, nvenc は CQ。目安 18-28)");
			x264Preset = ((BaseUnityPlugin)this).Config.Bind<string>("Encode", "X264Preset", "veryfast", "libx264 の速度プリセット (ultrafast, veryfast, fast, medium, slow ...)");
			audioBitrate = ((BaseUnityPlugin)this).Config.Bind<string>("Encode", "AudioBitrate", "160k", "AAC 音声のビットレート");
			MethodInfo methodInfo = AccessTools.Method("CameraRecording:SaveToDesktop", (Type[])null, (Type[])null);
			if (methodInfo == null)
			{
				Log.LogError((object)"CameraRecording.SaveToDesktop が見つかりません。ゲームの更新で構造が変わった可能性があります。");
				return;
			}
			new Harmony("U-FESNOS.MP4Export").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "AfterSaveToDesktop", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			string text = FindFFmpeg();
			if (text == null)
			{
				Log.LogWarning((object)"ffmpeg.exe が見つかりません。インストールするか、設定の FFmpegPath を指定してください。");
			}
			else
			{
				Log.LogInfo((object)("ffmpeg: " + text));
			}
		}

		private static void AfterSaveToDesktop(bool __result, ref string __0)
		{
			if (!__result || string.IsNullOrEmpty(__0))
			{
				return;
			}
			string text = (Path.IsPathRooted(__0) ? __0 : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), __0));
			if (!File.Exists(text))
			{
				Log.LogWarning((object)("保存されたファイルが見つかりません: " + text));
				return;
			}
			string text2 = FindFFmpeg();
			if (text2 == null)
			{
				Log.LogError((object)"ffmpeg.exe が見つからないため変換できません。");
				SetStatus("MP4: ffmpeg not found (see LogOutput.log)", 8.0);
				return;
			}
			Job job = new Job
			{
				FFmpeg = text2,
				Input = text,
				Encoder = videoEncoder.Value.Trim(),
				Quality = quality.Value,
				Preset = x264Preset.Value.Trim(),
				AudioBitrate = audioBitrate.Value.Trim(),
				DeleteInput = deleteWebm.Value
			};
			Thread thread = new Thread((ThreadStart)delegate
			{
				Convert(job);
			});
			thread.IsBackground = true;
			thread.Name = "CWMp4Export";
			thread.Start();
		}

		private static void Convert(Job job)
		{
			string text = Path.ChangeExtension(job.Input, ".mp4");
			string text2 = Path.Combine(Path.GetDirectoryName(text) ?? string.Empty, Path.GetFileNameWithoutExtension(text) + ".converting.mp4");
			SetStatus("MP4: converting...", 600.0);
			Log.LogInfo((object)("Converting: " + job.Input));
			try
			{
				if (File.Exists(text2))
				{
					File.Delete(text2);
				}
				string text3;
				int exitCode;
				using (Process process = Process.Start(new ProcessStartInfo
				{
					FileName = job.FFmpeg,
					Arguments = BuildArguments(job, text2),
					UseShellExecute = false,
					CreateNoWindow = true,
					RedirectStandardError = true
				}))
				{
					if (process == null)
					{
						throw new Exception("ffmpeg を起動できませんでした。");
					}
					try
					{
						process.PriorityClass = ProcessPriorityClass.BelowNormal;
					}
					catch
					{
					}
					text3 = process.StandardError.ReadToEnd();
					process.WaitForExit();
					exitCode = process.ExitCode;
				}
				if (exitCode != 0 || !File.Exists(text2) || new FileInfo(text2).Length == 0L)
				{
					throw new Exception($"ffmpeg exit code {exitCode}: {Tail(text3)}");
				}
				if (File.Exists(text))
				{
					File.Delete(text);
				}
				File.Move(text2, text);
				if (job.DeleteInput)
				{
					File.Delete(job.Input);
				}
				Log.LogInfo((object)("Converted: " + text));
				SetStatus("MP4: saved " + Path.GetFileName(text), 6.0);
			}
			catch (Exception ex)
			{
				Log.LogError((object)("Conversion failed: " + ex.Message));
				try
				{
					if (File.Exists(text2))
					{
						File.Delete(text2);
					}
				}
				catch
				{
				}
				SetStatus("MP4: conversion failed (see LogOutput.log)", 8.0);
			}
		}

		private static string BuildArguments(Job job, string output)
		{
			string text = ((job.Encoder.IndexOf("nvenc", StringComparison.OrdinalIgnoreCase) >= 0) ? $"-c:v {job.Encoder} -preset p5 -rc vbr -cq {job.Quality} -b:v 0" : ((!job.Encoder.Equals("libx264", StringComparison.OrdinalIgnoreCase)) ? ("-c:v " + job.Encoder) : $"-c:v libx264 -preset {job.Preset} -crf {job.Quality}"));
			return "-hide_banner -loglevel error -nostdin -y -i \"" + job.Input + "\" -vf \"scale=trunc(iw/2)*2:trunc(ih/2)*2\" " + text + " -pix_fmt yuv420p -c:a aac -b:a " + job.AudioBitrate + " -movflags +faststart \"" + output + "\"";
		}

		private static string? FindFFmpeg()
		{
			string text = (ffmpegPath.Value ?? string.Empty).Trim().Trim('"');
			if (text.Length > 0)
			{
				if (File.Exists(text))
				{
					return text;
				}
				Log.LogWarning((object)("FFmpegPath に指定されたファイルがありません: " + text));
				return null;
			}
			List<string> list = new List<string> { Path.Combine(pluginDir, "ffmpeg.exe") };
			string[] array = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim().Trim('"');
				if (text2.Length > 0)
				{
					list.Add(text2 + Path.DirectorySeparatorChar + "ffmpeg.exe");
				}
			}
			string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
			string folderPath2 = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
			list.Add(Path.Combine(folderPath, "Microsoft", "WinGet", "Links", "ffmpeg.exe"));
			list.Add("C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe");
			list.Add(Path.Combine(folderPath2, "scoop", "shims", "ffmpeg.exe"));
			foreach (string item in list)
			{
				try
				{
					if (File.Exists(item))
					{
						return item;
					}
				}
				catch
				{
				}
			}
			return null;
		}

		private static string Tail(string text)
		{
			text = (text ?? string.Empty).Trim();
			if (text.Length > 400)
			{
				return text.Substring(text.Length - 400);
			}
			return text;
		}

		private static void SetStatus(string text, double seconds)
		{
			lock (StatusLock)
			{
				statusText = text;
				statusUntil = DateTime.UtcNow.AddSeconds(seconds);
			}
		}

		private void OnGUI()
		{
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			if (!showStatus.Value)
			{
				return;
			}
			string text;
			lock (StatusLock)
			{
				if (DateTime.UtcNow > statusUntil)
				{
					return;
				}
				text = statusText;
			}
			GUI.Box(new Rect(10f, 10f, 460f, 26f), text);
		}
	}
}