-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRecorder.cs
158 lines (128 loc) · 5.42 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
// Copyright (c) 2021 Eliah Kagan
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Timers;
using SharpAvi;
using SharpAvi.Codecs;
using SharpAvi.Output;
namespace VidDraw {
/// <summary>Captures frames from a bitmap as AVI video.</summary>
/// <remarks>This class is single-threaded.</remarks>
internal sealed class Recorder : IDisposable {
internal Recorder(Bitmap bitmap,
ISynchronizeInvoke synchronizingObject)
{
_bitmap = bitmap;
_rectangle = new(Point.Empty, bitmap.Size);
_buffer = new byte[_rectangle.Width * _rectangle.Height * 4];
_timer = new(interval: IntervalInMilliseconds) {
AutoReset = true,
Enabled = false,
SynchronizingObject = synchronizingObject,
};
_timer.Elapsed += (_, _) => CaptureFrame();
}
public void Dispose()
{
if (IsRunning) Finish();
_timer.Dispose();
}
internal bool IsRunning => _job is not null;
internal event EventHandler<RecordedEventArgs>? Recorded;
internal void Start(Stream outputStream,
Codec codec,
string? name = null)
{
if (IsRunning) {
throw new InvalidOperationException(
"Can't start: already recording");
}
var aviWriter = CreateAviWriter(outputStream);
_job = new(AviWriter: aviWriter,
VideoStream: CreateVideoStream(aviWriter, codec),
Codec: codec,
Name: name ?? (outputStream as FileStream)?.Name);
CaptureFrame(); // Ensure we always get an initial frame.
_timer.Enabled = true;
}
internal void Finish()
{
var job = _job ?? throw new InvalidOperationException(
"Can't finish: not recording");
_timer.Enabled = false;
_job = null;
job.AviWriter.Close();
Recorded?.Invoke(this, new(job.Name, job.Codec));
}
private sealed record Job(AviWriter AviWriter,
IAviVideoStream VideoStream,
Codec Codec,
string? Name);
private const int IntervalInMilliseconds = 30;
private static AviWriter CreateAviWriter(Stream outputStream)
=> new(outputStream, leaveOpen: false) {
FramesPerSecond = 1000m / IntervalInMilliseconds,
EmitIndex1 = true,
};
// TODO: Let the user set/adjust the quality of Motion JPEG and H.264.
private IAviVideoStream CreateVideoStream(AviWriter aviWriter,
Codec codec)
{
Debug.Assert(aviWriter is not null);
return codec switch {
Codec.Raw
=> aviWriter.AddVideoStream(
width: _rectangle.Width,
height: _rectangle.Height),
Codec.Uncompressed
=> aviWriter.AddUncompressedVideoStream(
width: _rectangle.Width,
height: _rectangle.Height),
Codec.MotionJpeg
=> aviWriter.AddMotionJpegVideoStream(
width: _rectangle.Width,
height: _rectangle.Height,
quality: 100),
Codec.H264
=> aviWriter.AddMpeg4VideoStream(
width: _rectangle.Width,
height: _rectangle.Height,
fps: 1000.0 / IntervalInMilliseconds,
codec: KnownFourCCs.Codecs.X264),
_ => throw new InvalidOperationException(
"Unrecognized codec enumerator"),
};
}
private void CaptureFrame()
{
if (_job is null) return;
using (var lb = new LockedBits(_bitmap, _rectangle)) {
if (_job.Codec is Codec.Raw) {
lb.UpsideDownCopyTo(_buffer);
} else {
lb.CopyTo(_buffer);
}
}
_job.VideoStream.WriteFrame(true, _buffer, 0, _buffer.Length);
}
private readonly Bitmap _bitmap;
private readonly Rectangle _rectangle;
private readonly byte[] _buffer;
private readonly Timer _timer;
private Job? _job;
}
}