-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathRecorder.cs
357 lines (305 loc) · 10.6 KB
/
Recorder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
using System;
using System.Diagnostics;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;
using CSCore.SoundIn;
using CSCore.Codecs.WAV;
using CSCore;
class Recorder
{
// Public Settings
public Rectangle region;
public string status = "Idle";
public float fps = 30;
public bool drawCursor = true;
// Public Information
public float averageFps { get; private set; }
public float duration { get; private set; }
public int frames { get { return _frames; } }
public string tempPath { get; private set; }
public bool isRecording { get; private set; }
public bool isPaused { get; private set; }
// Image Capturing
private ImageFormat imageFormat = ImageFormat.Png;
private string imageExtension = ".png";
int _frames;
// Timers
uint durationTimerId;
uint captureTimerId;
PInvoke.MMTimerProc durationTimerDelegate;
PInvoke.MMTimerProc captureTimerDelegate;
// Audio Capturing
private bool recordAudio;
private WasapiLoopbackCapture audioSource;
private WaveWriter audioFile;
public static bool ForceStereoAudio = true;
/// <summary>
/// Constructor
/// </summary>
/// <param name="imageFormat">Image Format to save images as</param>
public Recorder(ImageFormat imageFormat = null)
{
if (imageFormat == null)
return;
this.imageFormat = imageFormat;
this.imageExtension = "." + imageFormat.ToString().ToLower();
}
/// <summary>
/// Start Recording
/// </summary>
/// <param name="recordAudio">Record Audio</param>
/// <returns>Successful</returns>
public bool Start(bool recordAudio = false)
{
if (isRecording)
return false;
// Reset
status = "Pending";
_frames = 0;
// Create Temporary Directory
CreateTemporaryPath();
// Setup Audio Recording
if (recordAudio)
{
this.recordAudio = recordAudio;
audioSource = new WasapiLoopbackCapture();
audioSource.DataAvailable += new EventHandler<DataAvailableEventArgs>(WriteAudio);
try
{
audioSource.Initialize();
}
catch (COMException exception)
{
if (exception.Message.Contains("0x88890008") && audioSource.WaveFormat.Channels > 2)
{
//this specific exception is most likely caused by "Headphone Virtualization" enabled in device control panel properties
var waveFormatTag = (audioSource.WaveFormat.WaveFormatTag == AudioEncoding.Extensible) ? AudioEncoding.IeeeFloat : audioSource.WaveFormat.WaveFormatTag;
var waveFormat = new WaveFormat(audioSource.WaveFormat.SampleRate, audioSource.WaveFormat.BitsPerSample, Math.Min(audioSource.WaveFormat.Channels, 2), waveFormatTag);
audioSource = new WasapiLoopbackCapture(0, waveFormat);
audioSource.DataAvailable += new EventHandler<DataAvailableEventArgs>(WriteAudio);
audioSource.Initialize();
}
else
throw exception;
}
audioFile = new WaveWriter(Path.Combine(tempPath, "audio.wav"), audioSource.WaveFormat);
audioSource.Start();
}
// Start Timers
StartTimers();
status = "Recording";
isRecording = true;
return isRecording;
}
/// <summary>
/// Stop Recording
/// </summary>
/// <returns>Successful</returns>
public bool Stop()
{
if (!isRecording)
return false;
// Stop Capturing Audio
if (recordAudio)
{
audioSource.Stop();
if (audioSource != null)
{
audioSource.Dispose();
audioSource = null;
}
if (audioFile != null)
{
audioFile.Dispose();
audioFile = null;
}
}
// Kill Timers
StopTimers();
status = "Idle";
isRecording = false;
return isRecording;
}
/// <summary>
/// Toggle recording to pause or resume if paused
/// </summary>
/// <returns>Successful</returns>
public bool Pause()
{
if (!isRecording)
return false;
// Paused; needs to be resumed
if (isPaused)
{
// Resume Audio
if (recordAudio)
audioSource.Start();
StartTimers();
status = "Recording";
}
// Recording; needs to be paused
else
{
// Pause Audio
if (recordAudio)
audioSource.Stop();
StopTimers();
status = "Paused";
}
isPaused = !isPaused;
return true;
}
/// <summary>
/// Redirects to Pause()
/// </summary>
/// <returns>Successful</returns>
public bool Resume()
{
return Pause();
}
/// <summary>
/// Start Recording Timers
/// </summary>
private void StartTimers()
{
// Enable Capture Timer
durationTimerDelegate = new PInvoke.MMTimerProc(DurationTick);
durationTimerId = PInvoke.timeSetEvent(100, 0, durationTimerDelegate, 0, 1);
// Enable Duration Timer
captureTimerDelegate = new PInvoke.MMTimerProc(CaptureTick);
captureTimerId = PInvoke.timeSetEvent((uint)(1000 / fps), 0, captureTimerDelegate, 0, 1);
}
/// <summary>
/// Stop Recording Timers
/// </summary>
private void StopTimers()
{
// Kill Timers
PInvoke.timeKillEvent(captureTimerId);
PInvoke.timeKillEvent(durationTimerId);
// Nullify
captureTimerId = 0;
captureTimerDelegate = null;
durationTimerId = 0;
durationTimerDelegate = null;
}
/// <summary>
/// Tick once a second second, update duration and current FPS
/// </summary>
private void DurationTick(uint timerid, uint msg, IntPtr user, uint dw1, uint dw2)
{
duration += (float).1;
averageFps = frames / duration;
}
/// <summary>
/// Capture and save file every (1000 / fps)
/// </summary>
private void CaptureTick(uint timerid, uint msg, IntPtr user, uint dw1, uint dw2)
{
// TODO: Make this write to a single file, rather than multiple little ones
// as the little ones will always be slower than a single file. After the recording,
// the single file can be expanded into multiple (when FS performance is less needed) for ffmpeg
Task.Run(() =>
{
//Ignore after stop has been requested.
if (captureTimerDelegate == null)
return;
var bmp = Capture();
int frame = Interlocked.Increment(ref _frames);
string outputPath = Path.Combine(tempPath, "_" + frame + imageExtension);
Debug.Assert(!File.Exists(outputPath));
bmp.Save(outputPath, imageFormat);
bmp.Dispose();
});
}
/// <summary>
/// Write captured Audio to file
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void WriteAudio(object sender, DataAvailableEventArgs e)
{
if (audioFile == null)
return;
audioFile.Write(e.Data, 0, e.ByteCount);
//audioFile.flu();
}
/// <summary>
/// Capture screenshot
/// </summary>
/// <returns>Captured bitmap of region area</returns>
public Bitmap Capture()
{
/*
* For future attempts,
* - Average capture w/ cursor is 100ms
* - Writing 300 images to a filestream was 14 seconds than 300 images with .Save() method
* - Draw cursor adds roughly 30ms to a capture
* - Locking bmp bits and copying them to byte[] did not work on any attempt I tried
*
* Method below is most concise and is *possibly* the most efficient
*/
// Create Bitmap and drawing surface from the bmp variable
// this will allow CopyFromScreen to "copy" the screen to the variable's address
var bmp = new Bitmap(region.Width - 2, region.Height - 2, PixelFormat.Format32bppRgb);
var graphics = Graphics.FromImage(bmp);
// Capture the Screenshot and save it to bmp
graphics.CopyFromScreen(region.X, region.Y, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
// Draw Cursor
if (drawCursor)
{
PInvoke.CursorInfo cursorInfo;
cursorInfo.cbSize = Marshal.SizeOf(typeof(PInvoke.CursorInfo));
if (PInvoke.GetCursorInfo(out cursorInfo))
if (cursorInfo.flags == 0x0001)
{
var hdc = graphics.GetHdc();
PInvoke.DrawIconEx(hdc, cursorInfo.ptScreenPos.X - region.X,
cursorInfo.ptScreenPos.Y - region.Y, cursorInfo.hCursor,
0, 0, 0, IntPtr.Zero, 0x0003);
graphics.ReleaseHdc();
}
}
// Release
graphics.Dispose();
return bmp;
}
/// <summary>
/// Create temporary directory to store the images
/// </summary>
public void CreateTemporaryPath()
{
string tempPath = Path.GetTempFileName() + "_WebMCam";
Directory.CreateDirectory(tempPath);
this.tempPath = tempPath;
}
/// <summary>
/// Delete Temp Folder
/// </summary>
/// <returns>Successful</returns>
public bool Flush()
{
Directory.Delete(tempPath, true);
return !Directory.Exists(tempPath);
}
}
public class PInvoke
{
public delegate void MMTimerProc(uint timerid, uint msg, IntPtr user, uint dw1, uint dw2);
[DllImport("winmm.dll")]
public static extern uint timeSetEvent(uint uDelay, uint uResolution, [MarshalAs(UnmanagedType.FunctionPtr)] MMTimerProc lpTimeProc, uint dwUser, int fuEvent);
[DllImport("winmm.dll")]
public static extern uint timeKillEvent(uint uTimerID);
[StructLayout(LayoutKind.Sequential)]
public struct CursorInfo { public int cbSize; public int flags; public IntPtr hCursor; public Point ptScreenPos; }
[DllImport("user32.dll")]
public static extern bool GetCursorInfo(out CursorInfo pci);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
}