diff --git a/Editor/Sources/BaseTextureRecorder.cs b/Editor/Sources/BaseTextureRecorder.cs index 6958f2f..30d871a 100644 --- a/Editor/Sources/BaseTextureRecorder.cs +++ b/Editor/Sources/BaseTextureRecorder.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using UnityEditor.Recorder.Input; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.Rendering; @@ -11,7 +13,7 @@ namespace UnityEditor.Recorder /// The class implementing the Recorder Settings. public abstract class BaseTextureRecorder : GenericRecorder where T : RecorderSettings { - int m_OngoingAsyncGPURequestsCount; + Dictionary m_OngoingRequests; // live requests and the frame's timestamp bool m_DelayedEncoderDispose; /// @@ -33,7 +35,7 @@ protected internal override bool BeginRecording(RecordingSession session) return false; UseAsyncGPUReadback = SystemInfo.supportsAsyncGPUReadback; - m_OngoingAsyncGPURequestsCount = 0; + m_OngoingRequests = new Dictionary(); m_DelayedEncoderDispose = false; return true; } @@ -53,9 +55,10 @@ protected internal override void RecordFrame(RecordingSession session) if (UseAsyncGPUReadback) { - AsyncGPUReadback.Request( + var request = AsyncGPUReadback.Request( renderTexture, 0, ReadbackTextureFormat, ReadbackDone); - ++m_OngoingAsyncGPURequestsCount; + // ASG: Use the audio time for determining frame timestamps. This guarantees audio always lines up. + m_OngoingRequests.Add(request, ((AudioInputBase) m_Inputs[1]).audioTime); return; } @@ -76,10 +79,10 @@ protected internal override void RecordFrame(RecordingSession session) private void ReadbackDone(AsyncGPUReadbackRequest r) { Profiler.BeginSample("BaseTextureRecorder.ReadbackDone"); - WriteFrame(r); + WriteFrame(r, m_OngoingRequests[r]); Profiler.EndSample(); - --m_OngoingAsyncGPURequestsCount; - if (m_OngoingAsyncGPURequestsCount == 0 && m_DelayedEncoderDispose) + m_OngoingRequests.Remove(r); + if (m_OngoingRequests.Count == 0 && m_DelayedEncoderDispose) DisposeEncoder(); } @@ -87,7 +90,7 @@ private void ReadbackDone(AsyncGPUReadbackRequest r) protected internal override void EndRecording(RecordingSession session) { base.EndRecording(session); - if (m_OngoingAsyncGPURequestsCount > 0) + if (m_OngoingRequests.Count > 0) { Recording = true; m_DelayedEncoderDispose = true; @@ -105,7 +108,7 @@ private Texture2D CreateReadbackTexture(int width, int height) /// Writes the frame from an asynchronous GPU read request. /// /// The asynchronous readback target. - protected virtual void WriteFrame(AsyncGPUReadbackRequest r) + protected virtual void WriteFrame(AsyncGPUReadbackRequest r, double timestamp) { if (m_ReadbackTexture == null) m_ReadbackTexture = CreateReadbackTexture(r.width, r.height); diff --git a/Editor/Sources/RecorderControllerSettings.cs b/Editor/Sources/RecorderControllerSettings.cs index 922ee33..dcea532 100644 --- a/Editor/Sources/RecorderControllerSettings.cs +++ b/Editor/Sources/RecorderControllerSettings.cs @@ -166,7 +166,7 @@ public static RecorderControllerSettings GetGlobalSettings() return LoadOrCreate(globalPath); } - internal void ReleaseRecorderSettings() + public void ReleaseRecorderSettings() { foreach (var recorder in m_RecorderSettings) { diff --git a/Editor/Sources/Recorders/AudioRecorder/AudioRecorder.cs b/Editor/Sources/Recorders/AudioRecorder/AudioRecorder.cs index f42119e..80bc25f 100644 --- a/Editor/Sources/Recorders/AudioRecorder/AudioRecorder.cs +++ b/Editor/Sources/Recorders/AudioRecorder/AudioRecorder.cs @@ -27,7 +27,7 @@ protected internal override bool BeginRecording(RecordingSession session) return false; } - var audioInput = (AudioInput)m_Inputs[0]; + var audioInput = (UnityAudioInput)m_Inputs[0]; var audioAttrsList = new List(); if (audioInput.audioSettings.PreserveAudio) @@ -67,7 +67,7 @@ protected internal override bool BeginRecording(RecordingSession session) protected internal override void RecordFrame(RecordingSession session) { - var audioInput = (AudioInput)m_Inputs[0]; + var audioInput = (UnityAudioInput)m_Inputs[0]; if (!audioInput.audioSettings.PreserveAudio) return; diff --git a/Editor/Sources/Recorders/MovieRecorder/MovieRecorder.cs b/Editor/Sources/Recorders/MovieRecorder/MovieRecorder.cs index c08118f..0fea3ab 100644 --- a/Editor/Sources/Recorders/MovieRecorder/MovieRecorder.cs +++ b/Editor/Sources/Recorders/MovieRecorder/MovieRecorder.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Core.Harness; +using Unity.Collections; using UnityEngine; using UnityEngine.Rendering; using UnityEditor.Recorder; @@ -29,6 +31,19 @@ class MovieRecorder : BaseTextureRecorder /// private bool m_RecordingStartedProperly = false; + /// + /// The frame rate the encoder is set to. This is fixed for the entire recording. Video encoders use rational + /// numbers for frame rates to avoid rounding error. + /// + private MediaRational m_FrameRate; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] + private static void EnterPlayMode() + { + s_ConcurrentCount = 0; + s_WarnedUserOfConcurrentCount = false; + } + protected override TextureFormat ReadbackTextureFormat { get @@ -85,23 +100,26 @@ protected internal override bool BeginRecording(RecordingSession session) return false; } + // In variable frame rate mode, we set the encoder to the frame rate of the current display. + m_FrameRate = RationalFromDouble( + session.settings.FrameRatePlayback == FrameRatePlayback.Variable + ? GameHarness.DisplayFPSTarget + : session.settings.FrameRate); + var videoAttrs = new VideoTrackAttributes { - frameRate = RationalFromDouble(session.settings.FrameRate), width = (uint)width, height = (uint)height, + frameRate = m_FrameRate, includeAlpha = alphaWillBeInImage, bitRateMode = Settings.VideoBitRateMode }; - if (RecorderOptions.VerboseMode) - Debug.Log( - string.Format( - "MovieRecorder starting to write video {0}x{1}@[{2}/{3}] fps into {4}", - width, height, videoAttrs.frameRate.numerator, - videoAttrs.frameRate.denominator, Settings.fileNameGenerator.BuildAbsolutePath(session))); + Debug.Log($"(UnityRecorder/MovieRecorder) Encoding video " + + $"{width}x{height}@[{videoAttrs.frameRate.numerator}/{videoAttrs.frameRate.denominator}] fps into " + + $"{Settings.fileNameGenerator.BuildAbsolutePath(session)}"); - var audioInput = (AudioInput)m_Inputs[1]; + var audioInput = (AudioInputBase) m_Inputs[1]; var audioAttrsList = new List(); if (audioInput.audioSettings.PreserveAudio) @@ -189,7 +207,7 @@ protected internal override void RecordFrame(RecordingSession session) throw new Exception("Unsupported number of sources"); base.RecordFrame(session); - var audioInput = (AudioInput)m_Inputs[1]; + var audioInput = (AudioInputBase) m_Inputs[1]; if (audioInput.audioSettings.PreserveAudio) Settings.m_EncoderManager.AddSamples(m_EncoderHandle, audioInput.mainBuffer); } @@ -225,11 +243,42 @@ protected override void WriteFrame(Texture2D t) WarnOfConcurrentRecorders(); } + private long lastFrame = -1; + #if UNITY_2019_1_OR_NEWER - protected override void WriteFrame(AsyncGPUReadbackRequest r) + protected override void WriteFrame(AsyncGPUReadbackRequest r, double timestamp) { + double currentTime = ((AudioInputBase)m_Inputs[1]).audioTime; + if (currentTime - timestamp > 2) + { + Debug.Log($"(MovieRecorder) Received heavily delayed frame. Requested at [{timestamp}]. Received at [{currentTime}]."); + } + var format = Settings.GetCurrentEncoder().GetTextureFormat(Settings); - Settings.m_EncoderManager.AddFrame(m_EncoderHandle, r.width, r.height, 0, format, r.GetData()); + + if (Settings.FrameRatePlayback == FrameRatePlayback.Variable) + { + // The closest media frame to the actual frame's timestamp. + // Convert m_FrameRate using floating-point division. The overloaded cast-to-double operator uses integer division. + MediaTime time = new MediaTime + { + count = (long) Math.Round(timestamp * m_FrameRate.numerator / m_FrameRate.denominator), + rate = m_FrameRate + }; + + if (time.count > lastFrame) // If two render frames fall on the same encoding frame, ignore. + { + Settings.m_EncoderManager.AddFrame(m_EncoderHandle, r.width, r.height, 0, format, r.GetData(), + time); + } + + lastFrame = time.count; + } + else + { + Settings.m_EncoderManager.AddFrame(m_EncoderHandle, r.width, r.height, 0, format, r.GetData()); + } + WarnOfConcurrentRecorders(); } diff --git a/Editor/Sources/Recorders/MovieRecorder/MovieRecorderSettings.cs b/Editor/Sources/Recorders/MovieRecorder/MovieRecorderSettings.cs index 7a5aaa9..cc6ddd6 100644 --- a/Editor/Sources/Recorders/MovieRecorder/MovieRecorderSettings.cs +++ b/Editor/Sources/Recorders/MovieRecorder/MovieRecorderSettings.cs @@ -230,6 +230,24 @@ public MovieRecorderSettings() RegisterAllEncoders(); } + /// + /// (ASG) Some types in Roslyn can't be loaded via GetTypes. Filter those out. + /// + private Type[] GetValidTypes(Assembly a) + { + Type[] allTypes; + try + { + allTypes = a.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + allTypes = e.Types.Where(t => t != null).ToArray(); + } + + return allTypes; + } + /// /// Find all the encoders by looking at the content of the current assemblies. /// @@ -239,7 +257,7 @@ private void RegisterAllEncoders() // For all assemblies find all MediaEncoderRegister foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies()) { - var allTypes = a.GetTypes(); + var allTypes = GetValidTypes(a); var encoders = allTypes.Where( type => type.IsSubclassOf(typeof(MediaEncoderRegister)) ); @@ -307,9 +325,27 @@ protected internal override bool ValidityCheck(List errors) { var ok = base.ValidityCheck(errors); - if (FrameRatePlayback == FrameRatePlayback.Variable) + // Note(John): Rendering a constant video frame rate with fmod is challenging. Normally, UnityRecorder + // approaches rendering constant framerates by setting Time.captureFrameRate. This forces the entire + // game to run *slower* than the target frame rate, but overrides Time.deltaTime every frame to be fixed. + // + // This lets a game capture device take as long as it likes to render and capture each frame. This is + // normally great, but FMOD audio is on a separate thread, and is totally unaffected by Time.captureFrameRate. + // + // The end result is constant video playback, but where audio runs way ahead of the video, leading to heavy + // desyncs, and hangs within the Unity MediaEncoder. This issue would have to be solved by putting FMOD into + // its synchronous mode, stepping it only for every real frame update. This is possible, but needs work. + // + // On the whole, recording a constant frame rate is weird in VR. InputRecordings only look smooth if they + // are played back with the same frame timings (this is why when you record in fast-forward it looks weird + // when played back at normal speed). So if we want to take a video recording with a constant frame rate, + // the played InputRecording must have been recorded at *exactly that frame rate, with no hiccups*. Possible, + // but a little tricky. Needs further exploration. + if (FrameRatePlayback == FrameRatePlayback.Constant && + AudioInputSettings.InputType == typeof(FmodAudioInput)) { - errors.Add("Movie recorder does not properly support Variable frame rate playback. Please consider using Constant frame rate instead"); + errors.Add("MovieRecorder does not support recording FMOD Audio with a constant video frame rate. " + + "Please use a variable frame rate, instead."); ok = false; } diff --git a/Editor/Sources/Recorders/_Inputs/Audio/AudioInput.cs b/Editor/Sources/Recorders/_Inputs/Audio/AudioInput.cs index d75324d..afe6ca2 100644 --- a/Editor/Sources/Recorders/_Inputs/Audio/AudioInput.cs +++ b/Editor/Sources/Recorders/_Inputs/Audio/AudioInput.cs @@ -1,7 +1,12 @@ using System; +using System.Collections.Generic; using UnityEngine; using System.Reflection; +using FMOD; +using FMODUnity; using Unity.Collections; +using UnityEngine.Assertions; +using Debug = UnityEngine.Debug; namespace UnityEditor.Recorder.Input { @@ -61,7 +66,16 @@ public static void Render(NativeArray buffer) } } - class AudioInput : RecorderInput + internal abstract class AudioInputBase : RecorderInput + { + public abstract ushort channelCount { get; } + public abstract int sampleRate { get; } + public abstract NativeArray mainBuffer { get; } + public abstract AudioInputSettings audioSettings { get; } + public abstract double audioTime { get; } + } + + class UnityAudioInput : AudioInputBase { class BufferManager : IDisposable { @@ -88,29 +102,32 @@ public void Dispose() ushort m_ChannelCount; - public ushort channelCount + public override ushort channelCount { get { return m_ChannelCount; } } - public int sampleRate + public override int sampleRate { get { return AudioSettings.outputSampleRate; } } - public NativeArray mainBuffer + public override NativeArray mainBuffer { get { return s_BufferManager.GetBuffer(0); } } - static AudioInput s_Handler; + static UnityAudioInput s_Handler; static BufferManager s_BufferManager; - public AudioInputSettings audioSettings + public override AudioInputSettings audioSettings { get { return (AudioInputSettings)settings; } } + private long recordedSamples = 0; + public override double audioTime => (double) recordedSamples / sampleRate; + protected internal override void BeginRecording(RecordingSession session) { m_ChannelCount = new Func(() => { @@ -157,6 +174,7 @@ protected internal override void NewFrameReady(RecordingSession session) s_BufferManager = new BufferManager(bufferCount, sampleFrameCount, m_ChannelCount); AudioRenderer.Render(mainBuffer); + recordedSamples += sampleFrameCount; } } @@ -181,4 +199,242 @@ protected internal override void EndRecording(RecordingSession session) AudioRenderer.Stop(); } } + + /// + /// (ASG) An Audio Input for FMOD. This reads the raw audio coming out of the FMOD system and forwards it to Unity Recorder. + /// Implemented as a custom DSP that is added to the end of the FMOD Master Bus. We read audio blocks from the DSP callback. + /// + class FmodAudioInput : AudioInputBase + { + private ushort mChannelCount; + public override ushort channelCount => mChannelCount; + + private int mSampleRate; + public override int sampleRate => mSampleRate; + + // A list of received audio blocks waiting to be encoded. Stores blocks until we send them to Unity Recorder in NewFrameReady. + // These arrays are reused every frame, as blocks are always the same size. + private readonly List mixBlockQueue = new List(); + private int mixBlockQueueSize; + + // If the game is paused, or hung, the audio will continue to accumulate up to this number of blocks, before + // cutting. This keeps audio smooth, even with hiccups in the video feed. + // Note: 512 blocks at 48000Hz is about 10 seconds of audio, and about 15Mb of memory. + private const int MaxBlockQueueSize = 512; + + private NativeArray mMainBuffer; // Allocated temp every frame, with all the unencoded samples + public override NativeArray mainBuffer => mMainBuffer; + + public override AudioInputSettings audioSettings => (AudioInputSettings) settings; + + private long sampleFrames = 0; + public override double audioTime => (double) sampleFrames / sampleRate; + + // Keep a reference to the dsp callback so it doesn't get garbage collected. + private static DSP_READCALLBACK dspCallback; + private DSP dsp; + + protected internal override void BeginRecording(RecordingSession session) + { + var dspName = "RecordSessionVideo(Audio)".ToCharArray(); + Array.Resize(ref dspName, 32); + dspCallback = DspReadCallback; + var dspDescription = new DSP_DESCRIPTION + { + version = 0x00010000, + name = dspName, + numinputbuffers = 1, + numoutputbuffers = 1, + read = dspCallback, + numparameters = 0 + }; + + FMOD.System system = RuntimeManager.CoreSystem; + CheckError(system.getMasterChannelGroup(out ChannelGroup masterGroup)); + CheckError(masterGroup.getDSP(CHANNELCONTROL_DSP_INDEX.TAIL, out DSP masterDspTail)); + CheckError(masterDspTail.getChannelFormat(out CHANNELMASK channelMask, out int numChannels, + out SPEAKERMODE sourceSpeakerMode)); + + if (RecorderOptions.VerboseMode) + { + Debug.Log( + $"(UnityRecorder) Listening to FMOD Audio. Setting DSP to [{channelMask}] [{numChannels}] [{sourceSpeakerMode}]"); + } + + // Create a new DSP with the format of the existing master group. + CheckError(system.createDSP(ref dspDescription, out dsp)); + CheckError(dsp.setChannelFormat(channelMask, numChannels, sourceSpeakerMode)); + CheckError(masterGroup.addDSP(CHANNELCONTROL_DSP_INDEX.TAIL, dsp)); + + // Fill in some basic information for the Unity audio encoder. + mChannelCount = (ushort) numChannels; + CheckError(system.getDriver(out int driverId)); + CheckError(system.getDriverInfo(driverId, out Guid _, out int systemRate, out SPEAKERMODE _, out int _)); + mSampleRate = systemRate; + + if (RecorderOptions.VerboseMode) + Debug.Log($"FmodAudioInput.BeginRecording for capture frame rate {Time.captureFramerate}"); + + if (audioSettings.PreserveAudio) + AudioRenderer.Start(); + } + + protected internal override void NewFrameReady(RecordingSession session) + { + try + { + int totalReadBlocks; + int totalFloats = 0; + lock (mixBlockQueue) + { + totalReadBlocks = mixBlockQueueSize; + for (int i = 0; i < totalReadBlocks; i++) + { + if (totalFloats / channelCount > 1 * sampleRate) + { + // The Unity MediaEncoder hangs when we send hundreds of thousands of audio samples. + // This can happen if the game is paused (ie. the audio continues to queue, but no new + // Updates are happening). Instead, we just cut the audio in those cases. + // This is a workaround for the lag spikes we were seeing when recording. + Debug.Log($"(FmodAudioInput) More than 2 seconds of audio samples [{totalFloats}] " + + $"queued up since the last submission. Only sending [{totalFloats}]. Dropping the rest."); + totalReadBlocks = i; + break; + } + + totalFloats += mixBlockQueue[i].Length; + } + } + + // Allocate a giant buffer with all of the samples, since the last frame. + // This is necessary because the Unity audio encoder expects a single native array. + mMainBuffer = new NativeArray(totalFloats, Allocator.Temp); + + int index = 0; + for (int i = 0; i < totalReadBlocks; i++) + { + NativeArray.Copy(mixBlockQueue[i], 0, mMainBuffer, index, mixBlockQueue[i].Length); + index += mixBlockQueue[i].Length; + } + + Assert.AreEqual(0, totalFloats % channelCount); + sampleFrames += totalFloats / channelCount; + } + finally + { + // Reset the list of blocks, so it can be reused. + lock (mixBlockQueue) + { + mixBlockQueueSize = 0; + } + } + } + + /// + protected internal override void EndRecording(RecordingSession session) + { + base.EndRecording(session); + + CheckError(RuntimeManager.CoreSystem.getMasterChannelGroup(out ChannelGroup master), shouldThrow: false); + CheckError(master.removeDSP(dsp), shouldThrow: false); + + // This may throw an error, if EndRecording is called more than once for a single BeginRecording. + // However, this shouldn't be case, and should be treated as a bug, instead. + CheckError(dsp.release(), shouldThrow: false); + + lock (mixBlockQueue) + { + mixBlockQueue.Clear(); + mixBlockQueueSize = 0; + } + } + + private RESULT DspReadCallback(ref DSP_STATE dspState, IntPtr inBuffer, IntPtr outBuffer, uint samples, + int inChannels, ref int outChannels) + { + try + { + // Debug.Log($"Received buffer of samples: {samples}, channels: {inChannels}"); + Assert.AreEqual(inChannels, outChannels); + + const int sampleSizeBytes = 4; // size of a float + int blockSizeFloats = (int) (samples * inChannels); // size of a float + int blockSizeBytes = blockSizeFloats * sampleSizeBytes; + + // Pass the input through to the output, so we can still hear it. + unsafe + { + Buffer.MemoryCopy(inBuffer.ToPointer(), outBuffer.ToPointer(), + blockSizeBytes, + blockSizeBytes); + } + + // Copy the audio into our buffer queue + lock (mixBlockQueue) + { + // If we've queued up too many blocks, without writing them to the encoder, just stop recording audio. + // This can happen if the game pauses, and game Update()'s stop. + if (mixBlockQueueSize < MaxBlockQueueSize) + { + float[] buffer; + if (mixBlockQueueSize == mixBlockQueue.Count) + { + // Add a new buffer if there are no empty buffers left in the list. + buffer = new float[blockSizeFloats]; + mixBlockQueue.Add(buffer); + } + else + { + // Use the next free buffer. + buffer = mixBlockQueue[mixBlockQueueSize]; + + // Reallocate the buffer if the block size has changed (could happen if the audio device changes). + if (buffer.Length != blockSizeFloats) + { + buffer = new float[blockSizeFloats]; + mixBlockQueue[mixBlockQueueSize] = buffer; + } + } + + mixBlockQueueSize++; + + // Copy the audio to the block list. + unsafe + { + fixed (float* bufferPtr = buffer) + { + Buffer.MemoryCopy(inBuffer.ToPointer(), bufferPtr, + blockSizeBytes, blockSizeBytes); + } + } + } + } + } + catch (Exception e) + { + Debug.LogError("There was an error with DSP Processing."); + Debug.LogException(e); + return RESULT.ERR_DSP_DONTPROCESS; + } + + return RESULT.OK; + } + + // Checks for fmod errors. + public static void CheckError(RESULT result, bool shouldThrow = true) + { + if (result != RESULT.OK) + { + if (shouldThrow) + { + throw new Exception(result.ToString()); + } + else + { + Debug.LogException( + new Exception("Got error from FMOD, but suppressing exception. ERROR: " + result)); + } + } + } + } } diff --git a/Editor/Sources/Recorders/_Inputs/Audio/AudioInputSettings.cs b/Editor/Sources/Recorders/_Inputs/Audio/AudioInputSettings.cs index 8751aed..b8a233d 100644 --- a/Editor/Sources/Recorders/_Inputs/Audio/AudioInputSettings.cs +++ b/Editor/Sources/Recorders/_Inputs/Audio/AudioInputSettings.cs @@ -25,7 +25,7 @@ public bool PreserveAudio /// protected internal override Type InputType { - get { return typeof(AudioInput); } + get { return typeof(FmodAudioInput); } } /// diff --git a/Editor/Sources/RecordersInventory.cs b/Editor/Sources/RecordersInventory.cs index 632a36f..de7faba 100644 --- a/Editor/Sources/RecordersInventory.cs +++ b/Editor/Sources/RecordersInventory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEngine; using System.Linq; +using System.Reflection; using UnityEditor.Recorder.FrameCapturer; namespace UnityEditor.Recorder @@ -20,6 +21,24 @@ static class RecordersInventory static HashSet s_BuiltInRecorderInfos; static HashSet s_LegacyRecorderInfos; + /// + /// (ASG) Some types in Roslyn can't be loaded via GetTypes. Filter those out. + /// + private static Type[] GetValidTypes(Assembly a) + { + Type[] allTypes; + try + { + allTypes = a.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + allTypes = e.Types.Where(t => t != null).ToArray(); + } + + return allTypes; + } + static IEnumerable> FindRecorders() { var attribType = typeof(RecorderSettingsAttribute); @@ -28,7 +47,7 @@ static IEnumerable> FindRecorders() Type[] types; try { - types = a.GetTypes(); + types = GetValidTypes(a); } catch (Exception) { diff --git a/Editor/Sources/RecordingSession.cs b/Editor/Sources/RecordingSession.cs index b6bb3f2..4fea118 100644 --- a/Editor/Sources/RecordingSession.cs +++ b/Editor/Sources/RecordingSession.cs @@ -29,7 +29,7 @@ public RecorderSettings settings internal bool isRecording { - get { return recorder.Recording; } + get { return recorder?.Recording ?? false; } } public int frameIndex @@ -199,6 +199,7 @@ public void Dispose() EndRecording(); UnityHelpers.Destroy(recorder); + recorder = null; } } } diff --git a/Editor/Unity.Recorder.Editor.asmdef b/Editor/Unity.Recorder.Editor.asmdef index 30afc39..28944df 100644 --- a/Editor/Unity.Recorder.Editor.asmdef +++ b/Editor/Unity.Recorder.Editor.asmdef @@ -1,13 +1,21 @@ { "name": "Unity.Recorder.Editor", "references": [ - "Unity.Recorder.Base", - "Unity.Recorder", - "Unity.Timeline" + "Unity.Recorder.Base", + "Unity.Recorder", + "Unity.Timeline", + "Fmod.Runtime", + "Clockwork.Core.Harness" ], "includePlatforms": [ "Editor" ], "excludePlatforms": [], - "allowUnsafeCode": true -} + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file