-
Notifications
You must be signed in to change notification settings - Fork 408
/
VideoTestPatternSource.cs
executable file
·294 lines (255 loc) · 11.9 KB
/
VideoTestPatternSource.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
//-----------------------------------------------------------------------------
// Filename: VideoTestPatternSource.cs
//
// Description: Implements a video test pattern source based on a static
// I420 file.
//
// Author(s):
// Aaron Clauson (aaron@sipsorcery.com)
//
// History:
// 04 Sep 2020 Aaron Clauson Created, Dublin, Ireland.
// 05 Nov 2020 Aaron Clauson Added video encoder parameter.
//
// License:
// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file.
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SIPSorceryMedia.Abstractions;
namespace SIPSorcery.Media
{
public class VideoTestPatternSource : IVideoSource, IDisposable
{
public const string TEST_PATTERN_RESOURCE_PATH = "SIPSorcery.media.testpattern.i420";
public const int TEST_PATTERN_WIDTH = 640;
public const int TEST_PATTERN_HEIGHT = 480;
private const int VIDEO_SAMPLING_RATE = 90000;
private const int MAXIMUM_FRAMES_PER_SECOND = 60; // Note the Threading.Timer's maximum callback rate is approx 60/s so allowing higher has no effect.
private const int DEFAULT_FRAMES_PER_SECOND = 30;
private const int MINIMUM_FRAMES_PER_SECOND = 1;
private const int STAMP_BOX_SIZE = 20;
private const int STAMP_BOX_PADDING = 10;
private const int TIMER_DISPOSE_WAIT_MILLISECONDS = 1000;
private const int VP8_SUGGESTED_FORMAT_ID = 96;
private const int H264_SUGGESTED_FORMAT_ID = 100;
public static ILogger logger = Sys.Log.Logger;
public static readonly List<VideoFormat> SupportedFormats = new List<VideoFormat>
{
new VideoFormat(VideoCodecsEnum.VP8, VP8_SUGGESTED_FORMAT_ID, VIDEO_SAMPLING_RATE),
new VideoFormat(VideoCodecsEnum.H264, H264_SUGGESTED_FORMAT_ID, VIDEO_SAMPLING_RATE, "packetization-mode=1")
};
private int _frameSpacing;
private byte[] _testI420Buffer;
private Timer _sendTestPatternTimer;
private bool _isStarted;
private bool _isPaused;
private bool _isClosed;
private bool _isMaxFrameRate;
private int _frameCount;
private IVideoEncoder _videoEncoder;
private MediaFormatManager<VideoFormat> _formatManager;
/// <summary>
/// Unencoded test pattern samples.
/// </summary>
public event RawVideoSampleDelegate OnVideoSourceRawSample;
#pragma warning disable CS0067
public event RawVideoSampleFasterDelegate OnVideoSourceRawSampleFaster;
#pragma warning restore CS0067
/// <summary>
/// If a video encoder has been set then this event contains the encoded video
/// samples.
/// </summary>
public event EncodedSampleDelegate OnVideoSourceEncodedSample;
public event SourceErrorDelegate OnVideoSourceError;
public VideoTestPatternSource(IVideoEncoder encoder = null)
{
if (encoder != null)
{
_videoEncoder = encoder;
_formatManager = new MediaFormatManager<VideoFormat>(SupportedFormats);
}
var assem = typeof(VideoTestPatternSource).GetTypeInfo().Assembly;
var testPatternStm = assem.GetManifestResourceStream(TEST_PATTERN_RESOURCE_PATH);
if (testPatternStm == null)
{
OnVideoSourceError?.Invoke($"Test pattern embedded resource could not be found, {TEST_PATTERN_RESOURCE_PATH}.");
}
else
{
_testI420Buffer = new byte[TEST_PATTERN_WIDTH * TEST_PATTERN_HEIGHT * 3 / 2];
testPatternStm.Read(_testI420Buffer, 0, _testI420Buffer.Length);
testPatternStm.Close();
_sendTestPatternTimer = new Timer(GenerateTestPattern, null, Timeout.Infinite, Timeout.Infinite);
_frameSpacing = 1000 / DEFAULT_FRAMES_PER_SECOND;
}
}
public void RestrictFormats(Func<VideoFormat, bool> filter) => _formatManager.RestrictFormats(filter);
public List<VideoFormat> GetVideoSourceFormats() => _formatManager.GetSourceFormats();
public void SetVideoSourceFormat(VideoFormat videoFormat) => _formatManager.SetSelectedFormat(videoFormat);
public List<VideoFormat> GetVideoSinkFormats() => _formatManager.GetSourceFormats();
public void SetVideoSinkFormat(VideoFormat videoFormat) => _formatManager.SetSelectedFormat(videoFormat);
public void ForceKeyFrame() => _videoEncoder?.ForceKeyFrame();
public bool HasEncodedVideoSubscribers() => OnVideoSourceEncodedSample != null;
public void ExternalVideoSourceRawSample(uint durationMilliseconds, int width, int height, byte[] sample, VideoPixelFormatsEnum pixelFormat) =>
throw new NotImplementedException("The test pattern video source does not offer any encoding services for external sources.");
public void ExternalVideoSourceRawSampleFaster(uint durationMilliseconds, RawImage rawImage) =>
throw new NotImplementedException("The test pattern video source does not offer any encoding services for external sources.");
public Task<bool> InitialiseVideoSourceDevice() =>
throw new NotImplementedException("The test pattern video source does not use a device.");
public bool IsVideoSourcePaused() => _isPaused;
public void SetFrameRate(int framesPerSecond)
{
if (framesPerSecond < MINIMUM_FRAMES_PER_SECOND || framesPerSecond > MAXIMUM_FRAMES_PER_SECOND)
{
logger.LogWarning($"Frames per second not in the allowed range of {MINIMUM_FRAMES_PER_SECOND} to {MAXIMUM_FRAMES_PER_SECOND}, ignoring.");
}
else
{
_frameSpacing = 1000 / framesPerSecond;
if (_isStarted)
{
_sendTestPatternTimer.Change(0, _frameSpacing);
}
}
}
/// <summary>
/// If this gets set the frames will be generated in a loop with no pause. Ideally this would
/// only ever be done in load test scenarios.
/// </summary>
public void SetMaxFrameRate(bool isMaxFrameRate)
{
if (_isMaxFrameRate != isMaxFrameRate)
{
_isMaxFrameRate = isMaxFrameRate;
if (_isStarted)
{
if (_isMaxFrameRate)
{
_sendTestPatternTimer.Change(Timeout.Infinite, Timeout.Infinite);
GenerateMaxFrames();
}
else
{
_sendTestPatternTimer.Change(0, _frameSpacing);
}
}
}
}
public Task PauseVideo()
{
_isPaused = true;
_sendTestPatternTimer.Change(Timeout.Infinite, Timeout.Infinite);
return Task.CompletedTask;
}
public Task ResumeVideo()
{
_isPaused = false;
_sendTestPatternTimer.Change(0, _frameSpacing);
return Task.CompletedTask;
}
public Task StartVideo()
{
if (!_isStarted)
{
_isStarted = true;
if (_isMaxFrameRate)
{
GenerateMaxFrames();
}
else
{
_sendTestPatternTimer.Change(0, _frameSpacing);
}
}
return Task.CompletedTask;
}
public Task CloseVideo()
{
if (!_isClosed)
{
_isClosed = true;
ManualResetEventSlim mre = new ManualResetEventSlim();
_sendTestPatternTimer?.Dispose(mre.WaitHandle);
return Task.Run(() => mre.Wait(TIMER_DISPOSE_WAIT_MILLISECONDS));
}
return Task.CompletedTask;
}
private void GenerateMaxFrames()
{
DateTime lastGenerateTime = DateTime.Now;
while (!_isClosed && _isMaxFrameRate)
{
_frameSpacing = Convert.ToInt32(DateTime.Now.Subtract(lastGenerateTime).TotalMilliseconds);
GenerateTestPattern(null);
lastGenerateTime = DateTime.Now;
}
}
private void GenerateTestPattern(object state)
{
lock (_sendTestPatternTimer)
{
if (!_isClosed && (OnVideoSourceRawSample != null || OnVideoSourceEncodedSample != null))
{
_frameCount++;
StampI420Buffer(_testI420Buffer, TEST_PATTERN_WIDTH, TEST_PATTERN_HEIGHT, _frameCount);
if (OnVideoSourceRawSample != null)
{
GenerateRawSample(TEST_PATTERN_WIDTH, TEST_PATTERN_HEIGHT, _testI420Buffer);
}
if (_videoEncoder != null && OnVideoSourceEncodedSample != null && !_formatManager.SelectedFormat.IsEmpty())
{
var encodedBuffer = _videoEncoder.EncodeVideo(TEST_PATTERN_WIDTH, TEST_PATTERN_HEIGHT, _testI420Buffer, VideoPixelFormatsEnum.I420, _formatManager.SelectedFormat.Codec);
if (encodedBuffer != null)
{
uint fps = (_frameSpacing > 0) ? 1000 / (uint)_frameSpacing : DEFAULT_FRAMES_PER_SECOND;
uint durationRtpTS = VIDEO_SAMPLING_RATE / fps;
OnVideoSourceEncodedSample.Invoke(durationRtpTS, encodedBuffer);
}
}
if (_frameCount == int.MaxValue)
{
_frameCount = 0;
}
}
}
}
/// <summary>
/// Consumers subscribing to the <seealso cref="OnVideoSourceRawSample"/> will most likely want bitmap samples.
/// This method takes the I420 buffer for the test patten frame, converts it to BGR and fire the event.
/// </summary>
/// <param name="i420Buffer">The I420 buffer representing the test pattern.</param>
private void GenerateRawSample(int width, int height, byte[] i420Buffer)
{
var bgr = PixelConverter.I420toBGR(i420Buffer, width, height, out _);
OnVideoSourceRawSample?.Invoke((uint)_frameSpacing, width, height, bgr, VideoPixelFormatsEnum.Bgr);
}
/// <summary>
/// TODO: Add something better for a dynamic stamp on an I420 buffer. This is useful to provide
/// a visual indication to the receiver that the video stream has not stalled.
/// </summary>
public static void StampI420Buffer(byte[] i420Buffer, int width, int height, int frameNumber)
{
// Draws a varying grey scale square in the bottom right corner on the base I420 buffer.
int startX = width - STAMP_BOX_SIZE - STAMP_BOX_PADDING;
int startY = height - STAMP_BOX_SIZE - STAMP_BOX_PADDING;
for (int y = startY; y < startY + STAMP_BOX_SIZE; y++)
{
for (int x = startX; x < startX + STAMP_BOX_SIZE; x++)
{
i420Buffer[y * width + x] = (byte)(frameNumber % 255);
}
}
}
public void Dispose()
{
_isClosed = true;
_sendTestPatternTimer?.Dispose();
_videoEncoder?.Dispose();
}
}
}