using System; using System.IO; using UnityEditor; using UnityEngine; namespace Streamingle.Editor { public sealed class AICameraABRenderWindow : EditorWindow { private string candidateADirectory = string.Empty; private string candidateBDirectory = string.Empty; private string outputRoot = string.Empty; private int startFrame; private int endFrameExclusive = 1; private AICameraABRenderCodec codec = AICameraABRenderCodec.MP4; private int width = 1920; private int height = 1080; private Vector2 scrollPosition; [MenuItem("Tools/Streamingle/Timeline/Render AI Camera A-B")] public static void Open() { GetWindow( "AI Camera A-B Render"); } private void OnEnable() { if (string.IsNullOrWhiteSpace(outputRoot)) { outputRoot = Path.GetFullPath(Path.Combine( Application.dataPath, "..", "Recordings", "CWAI_AB")); } } private void OnInspectorUpdate() { Repaint(); } private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.LabelField( "AI Camera A-B Movie Render", EditorStyles.boldLabel); EditorGUILayout.LabelField( "Renders two generated camera candidates against the same " + "saved Scene, Timeline frame range, and Timeline audio.", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(); DrawFolderField( "Candidate A", ref candidateADirectory, "Select generated camera candidate A"); DrawFolderField( "Candidate B", ref candidateBDirectory, "Select generated camera candidate B"); DrawFolderField( "Output Root", ref outputRoot, "Select A-B movie output directory"); EditorGUILayout.Space(); EditorGUILayout.LabelField("Frame Range", EditorStyles.boldLabel); startFrame = EditorGUILayout.IntField( "Start Frame", startFrame); endFrameExclusive = EditorGUILayout.IntField( "End Frame (Exclusive)", endFrameExclusive); EditorGUILayout.Space(); EditorGUILayout.LabelField("Movie", EditorStyles.boldLabel); codec = (AICameraABRenderCodec)EditorGUILayout.EnumPopup( "Codec", codec); width = EditorGUILayout.IntField("Width", width); height = EditorGUILayout.IntField("Height", height); EditorGUILayout.HelpBox( "MP4 uses H.264 and requires even dimensions. WebM uses VP8. " + "Both candidates use MainCamera and include Timeline audio.", MessageType.Info); EditorGUILayout.Space(); DrawActiveJob(); EditorGUILayout.Space(); var hasActive = AICameraABRenderRunner.TryGetActiveStatus( out _, out var phase, out _); var nonTerminalJob = hasActive && !AICameraABRenderUtility.IsTerminalPhase(phase); using (new EditorGUI.DisabledScope(nonTerminalJob)) { if (GUILayout.Button("Start A-B Render", GUILayout.Height(30))) { StartRender(); } } EditorGUILayout.EndScrollView(); } private void DrawActiveJob() { if (!AICameraABRenderRunner.TryGetActiveStatus( out var jobId, out var phase, out var manifestPath)) { EditorGUILayout.LabelField("No active A-B render job."); return; } EditorGUILayout.LabelField("Current Job", EditorStyles.boldLabel); EditorGUILayout.SelectableLabel( jobId, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight)); EditorGUILayout.LabelField("Phase", phase); EditorGUILayout.LabelField( "Manifest", manifestPath, EditorStyles.wordWrappedLabel); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Reveal Manifest") && !string.IsNullOrWhiteSpace(manifestPath)) { EditorUtility.RevealInFinder(manifestPath); } using (new EditorGUI.DisabledScope( AICameraABRenderUtility.IsTerminalPhase(phase))) { if (GUILayout.Button("Cancel")) { AICameraABRenderRunner.CancelABRenderForCli(jobId); } } } } private void StartRender() { try { var request = new AICameraABRenderRequest { candidateADirectory = candidateADirectory, candidateBDirectory = candidateBDirectory, outputRoot = outputRoot, startFrame = startFrame, endFrameExclusive = endFrameExclusive, codec = codec, width = width, height = height }; var started = AICameraABRenderRunner.Start(request); Debug.Log( $"CW-AI A/B render started. Manifest: " + $"{started.manifestPath}"); } catch (Exception exception) { Debug.LogException(exception); EditorUtility.DisplayDialog( "Unable to start A-B render", exception.Message, "OK"); } } private static void DrawFolderField( string label, ref string value, string dialogTitle) { using (new EditorGUILayout.HorizontalScope()) { value = EditorGUILayout.TextField(label, value); if (GUILayout.Button("Browse", GUILayout.Width(70))) { var initialDirectory = Directory.Exists(value) ? value : Directory.GetParent(Application.dataPath)?.FullName ?? string.Empty; var selected = EditorUtility.OpenFolderPanel( dialogTitle, initialDirectory, string.Empty); if (!string.IsNullOrWhiteSpace(selected)) { value = selected; } } } } } }