├── LICENSE ├── README.md └── source ├── Audio.meta ├── Audio ├── Editor.meta ├── Editor │ ├── AudioInput.cs │ ├── AudioInput.cs.meta │ ├── AudioInputSettings.cs │ ├── AudioInputSettings.cs.meta │ ├── AudioInputSettingsEditor.cs │ └── AudioInputSettingsEditor.cs.meta └── Engine.meta ├── Recorder.meta └── Recorder ├── Editor.meta └── Editor ├── MediaRecorder.cs ├── MediaRecorder.cs.meta ├── MediaRecorderEditor.cs ├── MediaRecorderEditor.cs.meta ├── MediaRecorderSettings.cs └── MediaRecorderSettings.cs.meta /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Unity Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GenericRecorder-MovieRecorderPlugin 2 | 3 | ### Brief 4 | 5 | FrameRecorder recorder plugin to record mp4 and webm movies with audio using Unity APIs only. 6 | 7 | It uses the same native APIs as Unity's video transcoding implementation, made available through the UnityEditor.MediaEncoder C# class. 8 | 9 | ### Features 10 | 11 | * webm container using the VP8 video codec and Vorbis audio codec. 12 | * mp4 container using the H.264 video codec and AAC audio codec. 13 | * Embedded alpha in webm container using [Google's specification](http://wiki.webmproject.org/alpha-channel) 14 | * Seamless non-realtime audio capture from Unity, allowing perfect capture even when frames cannot render in real time. 15 | 16 | See the [GenericFrameRecorder](https://github.com/Unity-Technologies/GenericFrameRecorder) project for the framework doc and source. 17 | 18 | ### Current limitations 19 | 20 | * Only available in editor. 21 | 22 | * Only supports constant frame rate. 23 | -------------------------------------------------------------------------------- /source/Audio.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9c5b01e0a69a84297bc9fec89e4159ff 3 | folderAsset: yes 4 | timeCreated: 1502429657 5 | licenseType: Pro 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /source/Audio/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7682926e3d3e8412ca8b99a853746ea3 3 | folderAsset: yes 4 | timeCreated: 1502429657 5 | licenseType: Pro 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInput.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | using System; 3 | using UnityEngine; 4 | #if UNITY_EDITOR 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEditorInternal; 8 | 9 | #endif 10 | #if UNITY_2018_1_OR_NEWER 11 | using Unity.Collections; 12 | #else 13 | using UnityEngine.Collections; 14 | #endif 15 | using UnityEngine.Recorder; 16 | 17 | namespace UnityEditor.Recorder.Input 18 | { 19 | class AudioRenderer 20 | { 21 | private static MethodInfo m_StartMethod; 22 | private static MethodInfo m_StopMethod; 23 | private static MethodInfo m_GetSampleCountForCaptureFrameMethod; 24 | private static MethodInfo m_RenderMethod; 25 | 26 | static AudioRenderer() 27 | { 28 | var className = "UnityEngine.AudioRenderer"; 29 | var dllName = "UnityEngine"; 30 | var audioRecorderType = Type.GetType(className + ", " + dllName); 31 | if (audioRecorderType == null) 32 | { 33 | Debug.Log("AudioInput could not find " + className + " type in " + dllName); 34 | return; 35 | } 36 | m_StartMethod = audioRecorderType.GetMethod("Start"); 37 | m_StopMethod = audioRecorderType.GetMethod("Stop"); 38 | m_GetSampleCountForCaptureFrameMethod = 39 | audioRecorderType.GetMethod("GetSampleCountForCaptureFrame"); 40 | m_RenderMethod = audioRecorderType.GetMethod("Render"); 41 | } 42 | 43 | static public void Start() 44 | { 45 | m_StartMethod.Invoke(null, null); 46 | } 47 | 48 | static public void Stop() 49 | { 50 | m_StopMethod.Invoke(null, null); 51 | } 52 | 53 | static public uint GetSampleCountForCaptureFrame() 54 | { 55 | var count = (int)m_GetSampleCountForCaptureFrameMethod.Invoke(null, null); 56 | return (uint)count; 57 | } 58 | 59 | static public void Render(NativeArray buffer) 60 | { 61 | m_RenderMethod.Invoke(null, new object[] { buffer }); 62 | } 63 | } 64 | 65 | public class AudioInput : RecorderInput 66 | { 67 | private class BufferManager : IDisposable 68 | { 69 | private NativeArray[] m_Buffers; 70 | 71 | public BufferManager(ushort bufferCount, uint sampleFrameCount, ushort channelCount) 72 | { 73 | m_Buffers = new NativeArray[bufferCount]; 74 | for (int i = 0; i < m_Buffers.Length; ++i) 75 | m_Buffers[i] = new NativeArray((int)sampleFrameCount * (int)channelCount, Allocator.Temp); 76 | } 77 | 78 | public NativeArray GetBuffer(int index) 79 | { 80 | return m_Buffers[index]; 81 | } 82 | 83 | public void Dispose() 84 | { 85 | foreach (var a in m_Buffers) 86 | a.Dispose(); 87 | } 88 | } 89 | 90 | public ushort channelCount { get { return m_ChannelCount; } } 91 | private ushort m_ChannelCount; 92 | public int sampleRate { get { return AudioSettings.outputSampleRate; } } 93 | public NativeArray mainBuffer { get { return m_BufferManager.GetBuffer(0); } } 94 | public NativeArray GetMixerGroupBuffer(int n) 95 | { return m_BufferManager.GetBuffer(n + 1); } 96 | private BufferManager m_BufferManager; 97 | 98 | public AudioInputSettings audioSettings 99 | { get { return (AudioInputSettings)settings; } } 100 | 101 | public override void BeginRecording(RecordingSession session) 102 | { 103 | m_ChannelCount = new Func(() => { 104 | switch (AudioSettings.speakerMode) 105 | { 106 | case AudioSpeakerMode.Mono: return 1; 107 | case AudioSpeakerMode.Stereo: return 2; 108 | case AudioSpeakerMode.Quad: return 4; 109 | case AudioSpeakerMode.Surround: return 5; 110 | case AudioSpeakerMode.Mode5point1: return 6; 111 | case AudioSpeakerMode.Mode7point1: return 7; 112 | case AudioSpeakerMode.Prologic: return 2; 113 | default: return 1; 114 | } 115 | })(); 116 | 117 | if (Verbose.enabled) 118 | Debug.Log(string.Format( 119 | "AudioInput.BeginRecording for capture frame rate {0}", Time.captureFramerate)); 120 | 121 | if (audioSettings.m_PreserveAudio) 122 | AudioRenderer.Start(); 123 | } 124 | 125 | public override void NewFrameReady(RecordingSession session) 126 | { 127 | if (!audioSettings.m_PreserveAudio) 128 | return; 129 | 130 | var sampleFrameCount = (uint)AudioRenderer.GetSampleCountForCaptureFrame(); 131 | if (Verbose.enabled) 132 | Debug.Log(string.Format("AudioInput.NewFrameReady {0} audio sample frames @ {1} ch", 133 | sampleFrameCount, m_ChannelCount)); 134 | 135 | ushort bufferCount = 136 | #if RECORD_AUDIO_MIXERS 137 | (ushort)(audioSettings.m_AudioMixerGroups.Length + 1) 138 | #else 139 | 1 140 | #endif 141 | ; 142 | 143 | m_BufferManager = new BufferManager(bufferCount, sampleFrameCount, m_ChannelCount); 144 | var mainBuffer = m_BufferManager.GetBuffer(0); 145 | 146 | #if RECORD_AUDIO_MIXERS 147 | for (int n = 1; n < bufferCount; n++) 148 | { 149 | var group = audioSettings.m_AudioMixerGroups[n - 1]; 150 | if (group.m_MixerGroup == null) 151 | continue; 152 | 153 | var buffer = m_BufferManager.GetBuffer(n); 154 | AudioRenderer.AddMixerGroupRecorder(group.m_MixerGroup, buffer, group.m_Isolate); 155 | } 156 | #endif 157 | 158 | AudioRenderer.Render(mainBuffer); 159 | } 160 | 161 | public override void FrameDone(RecordingSession session) 162 | { 163 | if (!audioSettings.m_PreserveAudio) 164 | return; 165 | 166 | m_BufferManager.Dispose(); 167 | m_BufferManager = null; 168 | } 169 | 170 | public override void EndRecording(RecordingSession session) 171 | { 172 | if (audioSettings.m_PreserveAudio) 173 | AudioRenderer.Stop(); 174 | } 175 | } 176 | } 177 | #endif -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInput.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d53962d7bf5a4415ba204aea5e80e746 3 | timeCreated: 1502429657 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInputSettings.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | using System; 3 | using System.Collections.Generic; 4 | using UnityEngine.Audio; 5 | using UnityEngine.Recorder; 6 | 7 | namespace UnityEditor.Recorder.Input 8 | { 9 | public class AudioInputSettings : RecorderInputSetting 10 | { 11 | public bool m_PreserveAudio = true; 12 | 13 | #if RECORD_AUDIO_MIXERS 14 | [System.Serializable] 15 | public struct MixerGroupRecorderListItem 16 | { 17 | [SerializeField] 18 | public AudioMixerGroup m_MixerGroup; 19 | 20 | [SerializeField] 21 | public bool m_Isolate; 22 | } 23 | public MixerGroupRecorderListItem[] m_AudioMixerGroups; 24 | #endif 25 | 26 | public override Type inputType 27 | { 28 | get { return typeof(AudioInput); } 29 | } 30 | 31 | public override bool ValidityCheck(List errors) 32 | { 33 | return true; 34 | } 35 | } 36 | } 37 | #endif -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInputSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fae41d0797227451ea4b10db1c086647 3 | timeCreated: 1502429657 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInputSettingsEditor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | using UnityEditorInternal; 3 | using UnityEngine; 4 | using UnityEngine.Recorder; 5 | using UnityEngine.Recorder.Input; 6 | using UnityEngine.UI; 7 | 8 | namespace UnityEditor.Recorder.Input 9 | { 10 | [CustomEditor(typeof(AudioInputSettings))] 11 | public class AudioInputSettingsEditor : InputEditor 12 | { 13 | SerializedProperty m_PreserveAudio; 14 | #if RECORD_AUDIO_MIXERS 15 | SerializedProperty m_AudioMixerGroups; 16 | ReorderableList m_AudioMixerGroupsList; 17 | #endif 18 | 19 | protected void OnEnable() 20 | { 21 | if (target == null) 22 | return; 23 | 24 | var pf = new PropertyFinder(serializedObject); 25 | m_PreserveAudio = pf.Find(w => w.m_PreserveAudio); 26 | 27 | #if RECORD_AUDIO_MIXERS 28 | m_AudioMixerGroups = serializedObject.FindProperty(x => x.m_AudioMixerGroups); 29 | m_AudioMixerGroupsList = new ReorderableList(serializedObject, m_AudioMixerGroups, true, true, true, true); 30 | m_AudioMixerGroupsList.drawElementCallback = 31 | (Rect rect, int index, bool isActive, bool isFocused) => 32 | { 33 | var element = m_AudioMixerGroupsList.serializedProperty.GetArrayElementAtIndex(index); 34 | rect.y += 2; 35 | EditorGUI.PropertyField( 36 | new Rect(rect.x - 25, rect.y, rect.width - 90, EditorGUIUtility.singleLineHeight), 37 | element.FindPropertyRelative("m_MixerGroup"), GUIContent.none); 38 | EditorGUI.PropertyField( 39 | new Rect(rect.x + rect.width - 85, rect.y, 20, EditorGUIUtility.singleLineHeight), 40 | element.FindPropertyRelative("m_Isolate"), GUIContent.none); 41 | EditorGUI.LabelField( 42 | new Rect(rect.x + rect.width - 65, rect.y, 60, EditorGUIUtility.singleLineHeight), 43 | new GUIContent ("Isolate", "Isolate group from mix")); 44 | }; 45 | 46 | m_AudioMixerGroupsList.drawHeaderCallback = (Rect rect) => 47 | { 48 | EditorGUI.LabelField(rect, "Audio Mixer Groups"); 49 | }; 50 | #endif 51 | } 52 | 53 | public override void OnInspectorGUI() 54 | { 55 | EditorGUILayout.PropertyField(m_PreserveAudio, new GUIContent("Capture audio")); 56 | 57 | #if RECORD_AUDIO_MIXERS 58 | if (m_AudioMixerGroups != null) 59 | { 60 | serializedObject.Update(); 61 | m_AudioMixerGroupsList.DoLayoutList(); 62 | } 63 | #endif 64 | 65 | serializedObject.ApplyModifiedProperties(); 66 | } 67 | } 68 | } 69 | 70 | #endif -------------------------------------------------------------------------------- /source/Audio/Editor/AudioInputSettingsEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db4f1a6c0bab84e29b9a12b4db3909c0 3 | timeCreated: 1502429657 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /source/Audio/Engine.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7676ffe4555ae4321a895f96a7ef2d57 3 | folderAsset: yes 4 | timeCreated: 1502429657 5 | licenseType: Pro 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /source/Recorder.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9513f369a97374649befbe2a0ab40c8f 3 | folderAsset: yes 4 | timeCreated: 1502736060 5 | licenseType: Pro 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /source/Recorder/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a792ed034f3d1459bbebef185d07ac1c 3 | folderAsset: yes 4 | timeCreated: 1501796122 5 | licenseType: Pro 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorder.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using UnityEngine; 6 | using UnityEngine.Audio; 7 | #if UNITY_2018_1_OR_NEWER 8 | using Unity.Collections; 9 | #else 10 | using UnityEngine.Collections; 11 | #endif 12 | using UnityEngine.Recorder.Input; 13 | using UnityEditor; 14 | using UnityEditor.Media; 15 | using UnityEditor.Recorder.Input; 16 | using UnityEngine.Recorder; 17 | 18 | namespace UnityEditor.Recorder 19 | { 20 | #if RECORD_AUDIO_MIXERS 21 | class WavWriter 22 | { 23 | BinaryWriter binwriter; 24 | 25 | // Use this for initialization 26 | public void Start (string filename) 27 | { 28 | var stream = new FileStream (filename, FileMode.Create); 29 | binwriter = new BinaryWriter (stream); 30 | for(int n = 0; n < 44; n++) 31 | binwriter.Write ((byte)0); 32 | } 33 | 34 | public void Stop() 35 | { 36 | var closewriter = binwriter; 37 | binwriter = null; 38 | int subformat = 3; // float 39 | int numchannels = 2; 40 | int numbits = 32; 41 | int samplerate = AudioSettings.outputSampleRate; 42 | Debug.Log ("Closing file"); 43 | long pos = closewriter.BaseStream.Length; 44 | closewriter.Seek (0, SeekOrigin.Begin); 45 | closewriter.Write ((byte)'R'); closewriter.Write ((byte)'I'); closewriter.Write ((byte)'F'); closewriter.Write ((byte)'F'); 46 | closewriter.Write ((uint)(pos - 8)); 47 | closewriter.Write ((byte)'W'); closewriter.Write ((byte)'A'); closewriter.Write ((byte)'V'); closewriter.Write ((byte)'E'); 48 | closewriter.Write ((byte)'f'); closewriter.Write ((byte)'m'); closewriter.Write ((byte)'t'); closewriter.Write ((byte)' '); 49 | closewriter.Write ((uint)16); 50 | closewriter.Write ((ushort)subformat); 51 | closewriter.Write ((ushort)numchannels); 52 | closewriter.Write ((uint)samplerate); 53 | closewriter.Write ((uint)((samplerate * numchannels * numbits) / 8)); 54 | closewriter.Write ((ushort)((numchannels * numbits) / 8)); 55 | closewriter.Write ((ushort)numbits); 56 | closewriter.Write ((byte)'d'); closewriter.Write ((byte)'a'); closewriter.Write ((byte)'t'); closewriter.Write ((byte)'a'); 57 | closewriter.Write ((uint)(pos - 36)); 58 | closewriter.Seek ((int)pos, SeekOrigin.Begin); 59 | closewriter.Flush (); 60 | } 61 | 62 | public void Feed(NativeArray data) 63 | { 64 | Debug.Log ("Writing wav chunk " + data.Length); 65 | 66 | if (binwriter == null) 67 | return; 68 | 69 | for(int n = 0; n < data.Length; n++) 70 | binwriter.Write (data[n]); 71 | } 72 | } 73 | #endif 74 | 75 | [Recorder(typeof(MediaRecorderSettings), "Video", "Unity/Movie")] 76 | public class MediaRecorder : GenericRecorder 77 | { 78 | private MediaEncoder m_Encoder; 79 | #if RECORD_AUDIO_MIXERS 80 | private WavWriter[] m_WavWriters; 81 | #endif 82 | private Texture2D m_ReadBackTexture; 83 | 84 | public override bool BeginRecording(RecordingSession session) 85 | { 86 | if (!base.BeginRecording(session)) 87 | return false; 88 | 89 | try 90 | { 91 | m_Settings.m_DestinationPath.CreateDirectory(); 92 | } 93 | catch (Exception) 94 | { 95 | Debug.LogError(string.Format( "Movie recorder output directory \"{0}\" could not be created.", m_Settings.m_DestinationPath.GetFullPath())); 96 | return false; 97 | } 98 | 99 | int width; 100 | int height; 101 | if (m_Inputs[0] is ScreenCaptureInput) 102 | { 103 | var input = (ScreenCaptureInput)m_Inputs[0]; 104 | width = input.outputWidth; 105 | height = input.outputHeight; 106 | } 107 | else 108 | { 109 | var input = (BaseRenderTextureInput)m_Inputs[0]; 110 | if (input == null) 111 | { 112 | if (Verbose.enabled) 113 | Debug.Log("MediaRecorder could not find input."); 114 | return false; 115 | } 116 | width = input.outputWidth; 117 | height = input.outputHeight; 118 | } 119 | 120 | if (width <= 0 || height <= 0) 121 | { 122 | if (Verbose.enabled) 123 | Debug.Log(string.Format( 124 | "MovieRecorder got invalid input resolution {0} x {1}.", width, height)); 125 | return false; 126 | } 127 | 128 | if (width > 4096 || height > 2160 && m_Settings.m_OutputFormat == MediaRecorderOutputFormat.MP4) 129 | { 130 | Debug.LogError("Mp4 format does not support requested resolution."); 131 | } 132 | 133 | var cbRenderTextureInput = m_Inputs[0] as CBRenderTextureInput; 134 | 135 | bool includeAlphaFromTexture = cbRenderTextureInput != null && cbRenderTextureInput.cbSettings.m_AllowTransparency; 136 | if (includeAlphaFromTexture && m_Settings.m_OutputFormat == MediaRecorderOutputFormat.MP4) 137 | { 138 | Debug.LogWarning("Mp4 format does not support alpha."); 139 | includeAlphaFromTexture = false; 140 | } 141 | 142 | var videoAttrs = new VideoTrackAttributes() 143 | { 144 | frameRate = RationalFromDouble(session.settings.m_FrameRate), 145 | width = (uint)width, 146 | height = (uint)height, 147 | #if UNITY_2018_1_OR_NEWER 148 | includeAlpha = includeAlphaFromTexture, 149 | bitRateMode = (VideoBitrateMode)m_Settings.m_VideoBitRateMode 150 | #else 151 | includeAlpha = includeAlphaFromTexture 152 | #endif 153 | }; 154 | 155 | if (Verbose.enabled) 156 | Debug.Log( 157 | string.Format( 158 | "MovieRecorder starting to write video {0}x{1}@[{2}/{3}] fps into {4}", 159 | width, height, videoAttrs.frameRate.numerator, 160 | videoAttrs.frameRate.denominator, m_Settings.m_DestinationPath.GetFullPath())); 161 | 162 | var audioInput = (AudioInput)m_Inputs[1]; 163 | var audioAttrsList = new List(); 164 | var audioAttrs = 165 | new UnityEditor.Media.AudioTrackAttributes() 166 | { 167 | sampleRate = new MediaRational 168 | { 169 | numerator = audioInput.sampleRate, 170 | denominator = 1 171 | }, 172 | channelCount = audioInput.channelCount, 173 | language = "" 174 | }; 175 | audioAttrsList.Add(audioAttrs); 176 | 177 | if (Verbose.enabled) 178 | Debug.Log( string.Format( "MovieRecorder starting to write audio {0}ch @ {1}Hz", audioAttrs.channelCount, audioAttrs.sampleRate.numerator)); 179 | 180 | #if RECORD_AUDIO_MIXERS 181 | var audioSettings = input.audioSettings; 182 | m_WavWriters = new WavWriter [audioSettings.m_AudioMixerGroups.Length]; 183 | 184 | for (int n = 0; n < m_WavWriters.Length; n++) 185 | { 186 | if (audioSettings.m_AudioMixerGroups[n].m_MixerGroup == null) 187 | continue; 188 | 189 | var path = Path.Combine( 190 | m_Settings.m_DestinationPath, 191 | "recording of " + audioSettings.m_AudioMixerGroups[n].m_MixerGroup.name + ".wav"); 192 | if (Verbose.enabled) 193 | Debug.Log("Starting wav recording into file " + path); 194 | m_WavWriters[n].Start(path); 195 | } 196 | #endif 197 | 198 | try 199 | { 200 | var fileName = m_Settings.m_BaseFileName.BuildFileName( session, recordedFramesCount, width, height, m_Settings.m_OutputFormat.ToString().ToLower()); 201 | var path = m_Settings.m_DestinationPath.GetFullPath() + "/" + fileName; 202 | 203 | m_Encoder = new UnityEditor.Media.MediaEncoder( path, videoAttrs, audioAttrsList.ToArray() ); 204 | return true; 205 | } 206 | catch 207 | { 208 | if (Verbose.enabled) 209 | Debug.LogError("MovieRecorder unable to create MovieEncoder."); 210 | } 211 | 212 | return false; 213 | } 214 | 215 | public override void RecordFrame(RecordingSession session) 216 | { 217 | if (m_Inputs.Count != 2) 218 | throw new Exception("Unsupported number of sources"); 219 | 220 | int width; 221 | int height; 222 | if (m_Inputs[0] is ScreenCaptureInput) 223 | { 224 | var input = (ScreenCaptureInput)m_Inputs[0]; 225 | width = input.outputWidth; 226 | height = input.outputHeight; 227 | m_Encoder.AddFrame(input.image); 228 | } 229 | else 230 | { 231 | var input = (BaseRenderTextureInput)m_Inputs[0]; 232 | width = input.outputWidth; 233 | height = input.outputHeight; 234 | 235 | if (!m_ReadBackTexture) 236 | m_ReadBackTexture = new Texture2D(width, height, TextureFormat.RGBA32, false); 237 | var backupActive = RenderTexture.active; 238 | RenderTexture.active = input.outputRT; 239 | m_ReadBackTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0, false); 240 | m_Encoder.AddFrame(m_ReadBackTexture); 241 | RenderTexture.active = backupActive; 242 | } 243 | 244 | var audioInput = (AudioInput)m_Inputs[1]; 245 | if (!audioInput.audioSettings.m_PreserveAudio) 246 | return; 247 | 248 | #if RECORD_AUDIO_MIXERS 249 | for (int n = 0; n < m_WavWriters.Length; n++) 250 | if (m_WavWriters[n] != null) 251 | m_WavWriters[n].Feed(audioInput.mixerGroupAudioBuffer(n)); 252 | #endif 253 | 254 | m_Encoder.AddSamples(audioInput.mainBuffer); 255 | } 256 | 257 | public override void EndRecording(RecordingSession session) 258 | { 259 | base.EndRecording(session); 260 | if (m_Encoder != null) 261 | { 262 | m_Encoder.Dispose(); 263 | m_Encoder = null; 264 | } 265 | 266 | // When adding a file to Unity's assets directory, trigger a refresh so it is detected. 267 | if (m_Settings.m_DestinationPath.root == OutputPath.ERoot.AssetsPath ) 268 | AssetDatabase.Refresh(); 269 | } 270 | 271 | // https://stackoverflow.com/questions/26643695/converting-decimal-to-fraction-c 272 | static long GreatestCommonDivisor(long a, long b) 273 | { 274 | if (a == 0) 275 | return b; 276 | 277 | if (b == 0) 278 | return a; 279 | 280 | return (a < b) ? GreatestCommonDivisor(a, b % a) : GreatestCommonDivisor(b, a % b); 281 | } 282 | 283 | static MediaRational RationalFromDouble(double value) 284 | { 285 | double integral = Math.Floor(value); 286 | double frac = value - integral; 287 | 288 | const long precision = 10000000; 289 | 290 | long gcd = GreatestCommonDivisor((long)Math.Round(frac * precision), precision); 291 | long denom = precision / gcd; 292 | 293 | return new MediaRational() 294 | { 295 | numerator = (int)((long)integral * denom + ((long)Math.Round(frac * (double)precision)) / gcd), 296 | denominator = (int)denom 297 | }; 298 | } 299 | } 300 | } 301 | #endif -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 81bb4b24c44c943b49c402cbdad20d9c 3 | timeCreated: 1501796122 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorderEditor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using UnityEditor.Recorder.Input; 6 | using UnityEngine; 7 | using UnityEngine.Recorder; 8 | using UnityEngine.Recorder.Input; 9 | 10 | namespace UnityEditor.Recorder 11 | { 12 | [CustomEditor(typeof(MediaRecorderSettings))] 13 | public class MediaRecorderEditor : RecorderEditor 14 | { 15 | SerializedProperty m_OutputFormat; 16 | #if UNITY_2018_1_OR_NEWER 17 | SerializedProperty m_EncodingBitRateMode; 18 | #endif 19 | SerializedProperty m_FlipVertical; 20 | RTInputSelector m_RTInputSelector; 21 | 22 | [MenuItem("Window/Recorder/Video")] 23 | static void ShowRecorderWindow() 24 | { 25 | RecorderWindow.ShowAndPreselectCategory("Video"); 26 | } 27 | 28 | protected override void OnEnable() 29 | { 30 | base.OnEnable(); 31 | 32 | if (target == null) 33 | return; 34 | 35 | var pf = new PropertyFinder(serializedObject); 36 | m_OutputFormat = pf.Find(w => w.m_OutputFormat); 37 | #if UNITY_2018_1_OR_NEWER 38 | m_EncodingBitRateMode = pf.Find(w => w.m_VideoBitRateMode); 39 | #endif 40 | } 41 | 42 | #if UNITY_2018_1_OR_NEWER 43 | protected override void OnEncodingGui() 44 | { 45 | AddProperty(m_EncodingBitRateMode, () => EditorGUILayout.PropertyField(m_EncodingBitRateMode, new GUIContent("Bitrate Mode"))); 46 | } 47 | #else 48 | protected override void OnEncodingGroupGui() 49 | { 50 | // hiding this group by not calling parent class's implementation. 51 | } 52 | #endif 53 | 54 | protected override void OnOutputGui() 55 | { 56 | AddProperty(m_OutputFormat, () => EditorGUILayout.PropertyField(m_OutputFormat, new GUIContent("Output format"))); 57 | 58 | base.OnOutputGui(); 59 | } 60 | 61 | protected override EFieldDisplayState GetFieldDisplayState(SerializedProperty property) 62 | { 63 | if (property.name == "m_FlipVertical" || property.name == "m_CaptureEveryNthFrame" ) 64 | return EFieldDisplayState.Hidden; 65 | if (property.name == "m_FrameRateMode" ) 66 | return EFieldDisplayState.Disabled; 67 | 68 | if (property.name == "m_AllowTransparency") 69 | { 70 | return (target as MediaRecorderSettings).m_OutputFormat == MediaRecorderOutputFormat.MP4 ? EFieldDisplayState.Disabled : EFieldDisplayState.Enabled; 71 | } 72 | 73 | return base.GetFieldDisplayState(property); 74 | } 75 | 76 | 77 | } 78 | } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorderEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 856c53159e929467abccd8747fc0fbbb 3 | timeCreated: 1501796122 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorderSettings.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2017_3_OR_NEWER 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using UnityEditor.Recorder.Input; 6 | using UnityEngine; 7 | using UnityEngine.Recorder; 8 | using UnityEngine.Recorder.Input; 9 | namespace UnityEditor.Recorder 10 | { 11 | 12 | public enum MediaRecorderOutputFormat 13 | { 14 | MP4, 15 | WEBM 16 | } 17 | 18 | [ExecuteInEditMode] 19 | public class MediaRecorderSettings : RecorderSettings 20 | { 21 | public MediaRecorderOutputFormat m_OutputFormat = MediaRecorderOutputFormat.MP4; 22 | #if UNITY_2018_1_OR_NEWER 23 | public UnityEditor.VideoBitrateMode m_VideoBitRateMode = UnityEditor.VideoBitrateMode.High; 24 | #endif 25 | public bool m_AppendSuffix = false; 26 | 27 | MediaRecorderSettings() 28 | { 29 | m_BaseFileName.pattern = "movie."; 30 | } 31 | 32 | public override List GetDefaultInputSettings() 33 | { 34 | return new List() 35 | { 36 | NewInputSettingsObj("Pixels"), 37 | NewInputSettingsObj("Audio") 38 | }; 39 | } 40 | 41 | public override bool ValidityCheck( List errors ) 42 | { 43 | var ok = base.ValidityCheck(errors); 44 | 45 | if( string.IsNullOrEmpty(m_DestinationPath.GetFullPath() )) 46 | { 47 | ok = false; 48 | errors.Add("Missing destination path."); 49 | } 50 | if( string.IsNullOrEmpty(m_BaseFileName.pattern)) 51 | { 52 | ok = false; 53 | errors.Add("missing file name"); 54 | } 55 | 56 | return ok; 57 | } 58 | 59 | public override RecorderInputSetting NewInputSettingsObj(Type type, string title) 60 | { 61 | var obj = base.NewInputSettingsObj(type, title); 62 | if (type == typeof(CBRenderTextureInputSettings)) 63 | { 64 | (obj as CBRenderTextureInputSettings).m_ForceEvenSize = true; 65 | (obj as CBRenderTextureInputSettings).m_FlipFinalOutput = Application.platform == RuntimePlatform.OSXEditor; 66 | } 67 | if (type == typeof(RenderTextureSamplerSettings)) 68 | { 69 | (obj as RenderTextureSamplerSettings).m_ForceEvenSize = true; 70 | } 71 | if (type == typeof(ScreenCaptureInputSettings)) 72 | { 73 | (obj as ScreenCaptureInputSettings).m_ForceEvenSize = true; 74 | } 75 | 76 | return obj ; 77 | } 78 | 79 | public override List GetInputGroups() 80 | { 81 | return new List() 82 | { 83 | new InputGroupFilter() 84 | { 85 | title = "Pixels", 86 | typesFilter = new List() 87 | { 88 | new TInputFilter("Game View"), 89 | new TInputFilter("Specific Camera(s)"), 90 | #if UNITY_2018_1_OR_NEWER 91 | new TInputFilter("360 View (feature preview)"), 92 | #endif 93 | new TInputFilter("Sampling (off screen)"), 94 | new TInputFilter("Render Texture Asset"), 95 | } 96 | }, 97 | new InputGroupFilter() 98 | { 99 | title = "Sound", 100 | typesFilter = new List() 101 | { 102 | new TInputFilter("Audio"), 103 | } 104 | } 105 | }; 106 | } 107 | 108 | public override bool SelfAdjustSettings() 109 | { 110 | if (inputsSettings.Count == 0 ) 111 | return false; 112 | 113 | var adjusted = false; 114 | 115 | if (inputsSettings[0] is ImageInputSettings) 116 | { 117 | var iis = (ImageInputSettings)inputsSettings[0]; 118 | var maxRes = m_OutputFormat == MediaRecorderOutputFormat.MP4 ? EImageDimension.x2160p_4K : EImageDimension.x4320p_8K; 119 | if (iis.maxSupportedSize != maxRes) 120 | { 121 | iis.maxSupportedSize = maxRes; 122 | adjusted = true; 123 | } 124 | } 125 | 126 | return adjusted; 127 | } 128 | 129 | } 130 | } 131 | 132 | #endif -------------------------------------------------------------------------------- /source/Recorder/Editor/MediaRecorderSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6fde0a8ac3e6b482c95fa602e65ab045 3 | timeCreated: 1501796122 4 | licenseType: Pro 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | --------------------------------------------------------------------------------