using System; using System.ComponentModel; using Unity.Profiling; using UnityEditor.Media; using UnityEngine; using UnityEngine.LowLevel; namespace UnityEditor.Recorder { /// /// A class that represents a Recorder session, with support for processing incoming data and preparing as /// well as cleaning up resources. /// public class RecordingSession : IDisposable { /// /// The Recorder associated with this session. /// public Recorder recorder; internal GameObject recorderGameObject; internal _FrameRequestComponent recorderComponent; static bool frameRateCapped; // Used to have only 1 Recorder cap framerate when multiple are trying to do. Unity.Profiling.ProfilerMarker capFPSMarker = new ProfilerMarker("Unity.Recorder.CapFPS"); int m_SubFrameIndex = 0; int m_FrameIndex = 0; int m_InitialFrame = 0; int m_FirstRecordedFrameCount = -1; float m_FPSTimeStart; float m_FPSNextTimeStart; int m_FPSNextFrameCount; internal double currentFrameStartTS { get; private set; } internal double recordingStartTS { get; private set; } internal DateTime sessionStartTS { get; private set; } /// /// The settings of the Recorder. /// /// /// /// /// /// /// public RecorderSettings settings { get { return recorder.settings; } } internal bool isRecording { get { return recorder.Recording; } } /// /// The index of the current frame being processed. /// public int frameIndex { get { return m_FrameIndex; } } /// /// The index of the current sub frame being processed. /// internal int subFrameIndex { get { return m_SubFrameIndex; } } internal int RecordedFrameSpan { get { return m_FirstRecordedFrameCount == -1 ? 0 : Time.renderedFrameCount - m_FirstRecordedFrameCount; } } /// /// The time (in seconds) of the current frame. /// public float recorderTime { get { return (float)(currentFrameStartTS - settings.StartTime); } } static void AllowInBackgroundMode() { if (!Application.runInBackground) { Application.runInBackground = true; if (RecorderOptions.VerboseMode) Debug.Log("Recording sessions is enabling Application.runInBackground!"); } } bool ValidateFrameRate(float frameRate) { if (frameRate == 0F) { Debug.LogErrorFormat("{0} does not support 0 fps frame rate.", recorder.GetType().Name); return false; } if (frameRate < 0F) { Debug.LogErrorFormat( "{0} does not support negative frame rate ({1}).", recorder.GetType().Name, frameRate); return false; } if (float.IsNaN(frameRate) || float.IsInfinity(frameRate)) { Debug.LogErrorFormat( "{0} does not support invalid floating point value for frame rate ({1}).", recorder.GetType().Name, frameRate); return false; } return true; } internal bool SessionCreated() { try { AllowInBackgroundMode(); recordingStartTS = (Time.time / (Mathf.Approximately(Time.timeScale, 0f) ? 1f : Time.timeScale)); sessionStartTS = DateTime.Now; recorder.SessionCreated(this); return true; } catch (Exception ex) { Debug.LogException(ex); return false; } } internal void RecreateRecorder() { if (settings != null && recorder == null) { recorder = RecordersInventory.CreateDefaultRecorder(settings); } } internal bool BeginRecording() { try { if (!settings.IsPlatformSupported) { Debug.LogError(string.Format("Recorder {0} does not support current platform", recorder.GetType().Name)); return false; } AllowInBackgroundMode(); if (!ValidateFrameRate(recorder.settings.FrameRate)) return false; recordingStartTS = (Time.time / (Mathf.Approximately(Time.timeScale, 0f) ? 1f : Time.timeScale)); recorder.SignalInputsOfStage(ERecordingSessionStage.BeginRecording, this); // This must be after the above call otherwise GameView will not be initialized if (!recorder.BeginRecording(this)) { recorder.CleanupFailedRecording(); UnityHelpers.Destroy(recorderComponent); // so that the time scale is immediately reset (REC-834) return false; } m_InitialFrame = Time.renderedFrameCount; m_FPSTimeStart = Time.unscaledTime; return true; } catch (Exception ex) { Debug.LogException(ex); return false; } } internal void EndRecording() { try { recorder.SignalInputsOfStage(ERecordingSessionStage.EndRecording, this); recorder.EndRecording(this); } catch (Exception ex) { Debug.LogException(ex); } } internal void RecordFrame() { try { if (!recorder.SkipFrame(this)) { recorder.SignalInputsOfStage(ERecordingSessionStage.NewFrameReady, this); recorder.RecordSubFrame(this); if (!recorder.SkipSubFrame(this)) { #if DEBUG_RECORDER_TIMING Debug.LogFormat("Session::RecordFrame() index # {0} sub # {1} Out at frame # {2} - {3} - {4} ", m_FrameIndex, m_SubFrameIndex, Time.renderedFrameCount, Time.time, Time.deltaTime); #endif recorder.RecordFrame(this); recorder.RecordedFramesCount++; if (recorder.RecordedFramesCount == 1) m_FirstRecordedFrameCount = Time.renderedFrameCount; } recorder.SignalInputsOfStage(ERecordingSessionStage.FrameDone, this); } else { recorder.SignalInputsOfStage(ERecordingSessionStage.SkipFrame, this); } } catch (Exception ex) { Debug.LogException(ex); } using (capFPSMarker.Auto()) { // Note: This is not great when multiple recorders are simultaneously active... if (settings.FrameRatePlayback == FrameRatePlayback.Variable || settings.FrameRatePlayback == FrameRatePlayback.Constant && recorder.settings.CapFrameRate) { var frameCount = Time.renderedFrameCount - m_InitialFrame; var frameLen = 1.0f / recorder.settings.FrameRate; // seconds var elapsed = Time.unscaledTime - m_FPSTimeStart; var target = frameLen * (frameCount + 1); var sleepSec = Math.Min(target - elapsed, 1F); if (sleepSec > 0.002F && !frameRateCapped) { frameRateCapped = true; var curRealTime = Time.realtimeSinceStartup; var endWaitTime = curRealTime + sleepSec; if (RecorderOptions.VerboseMode) Debug.Log(string.Format( "Recording session info => dT: {0:F1}s, Target dT: {1:F1}s, Retarding: {2}s, fps: {3:F1}", elapsed, target, sleepSec, frameCount / elapsed)); // We have a delta between the expected and actual current time. We'll sleep part of this delta, and // use a busy loop for the rest. The idea is that typically, OS-level sleep calls are imprecise and // may return "some time after the wanted duration". var sleepMS = (int)(sleepSec * 1000) - 3; if (sleepMS > 0) System.Threading.Thread.Sleep(sleepMS); // Do the rest of the wait using a busy loop to maximize chances of keeping the main thread running // on the CPU. while (Time.realtimeSinceStartup < endWaitTime) ; } else if (sleepSec < -frameLen) { m_InitialFrame--; } else if (RecorderOptions.VerboseMode) Debug.Log(string.Format("Recording session info => fps: {0:F1}", frameCount / elapsed)); // reset every 30 frames if (frameCount % 50 == 49) { m_FPSNextTimeStart = Time.unscaledTime; m_FPSNextFrameCount = Time.renderedFrameCount; } if (frameCount % 100 == 99) { m_FPSTimeStart = m_FPSNextTimeStart; m_InitialFrame = m_FPSNextFrameCount; } } } if (!recorder.SkipSubFrame(this)) { // This is a 'full' frame: increment the current full frame counter m_FrameIndex++; } } // Don't start incrementing the subframe count until accumulation has been activated. void IncrementSubFrameCount() { #if HDRP_AVAILABLE if (settings.IsAccumulationSupported() && settings is IAccumulation accumulation && accumulation.GetAccumulationSettings().CaptureAccumulation) { switch (settings.RecordMode) { case RecordMode.FrameInterval: case RecordMode.SingleFrame: if (m_FrameIndex >= settings.StartFrame) { m_SubFrameIndex++; } break; case RecordMode.TimeInterval: if (currentFrameStartTS >= settings.StartTime) { m_SubFrameIndex++; } break; case RecordMode.Manual: m_SubFrameIndex++; break; default: throw new InvalidEnumArgumentException($"Unexpected record mode {settings.RecordMode}"); } } #endif } internal void PrepareNewFrame() { try { frameRateCapped = false; AllowInBackgroundMode(); currentFrameStartTS = (Time.time / (Mathf.Approximately(Time.timeScale, 0f) ? 1f : Time.timeScale)) - recordingStartTS; IncrementSubFrameCount(); if (!recorder.SkipFrame(this)) { recorder.SignalInputsOfStage(ERecordingSessionStage.NewFrameStarting, this); recorder.PrepareNewFrame(this); } } catch (Exception ex) { Debug.LogException(ex); } } /// /// Cleans up the object's resources. /// public void Dispose() { if (recorder != null) { EndRecording(); UnityHelpers.Destroy(recorder); } } } }