-
Notifications
You must be signed in to change notification settings - Fork 0
/
VideoPlayer.cs
772 lines (685 loc) · 25.5 KB
/
VideoPlayer.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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
// Modified AForge Controls Library
// by EVision, 2017/08/13
//
// ===============================
// AForge Controls Library in AForge.NET framework
// http://www.aforgenet.com/framework/
//
// Copyright © AForge.NET, 2005-2012
// contacts@aforgenet.com
//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Data;
using System.Text;
using System.Windows.Forms;
using AForge.Video;
namespace EVision.Video
{
using Point = System.Drawing.Point;
using System.Drawing.Drawing2D;
/// <summary>
/// Video source player control.
/// </summary>
///
/// <remarks><para>The control is aimed to play video sources, which implement
/// <see cref="AForge.Video.IVideoSource"/> interface. To start playing a video
/// the <see cref="VideoSource"/> property should be initialized first and then
/// <see cref="Start"/> method should be called. In the case if user needs to
/// perform some sort of image processing with video frames before they are displayed,
/// the <see cref="NewFrame"/> event may be used.</para>
///
/// <para>Sample usage:</para>
/// <code>
/// // set new frame event handler if we need processing of new frames
/// playerControl.NewFrame += new VideoSourcePlayer.NewFrameHandler( this.playerControl_NewFrame );
///
/// // create video source
/// IVideoSource videoSource = new ...
/// // start playing it
/// playerControl.VideoSource = videoSource;
/// playerControl.Start( );
/// ...
///
/// // new frame event handler
/// private void playerControl_NewFrame( object sender, ref Bitmap image )
/// {
/// // process new frame somehow ...
///
/// // Note: it may be even changed, so the control will display the result
/// // of image processing done here
/// }
/// </code>
/// </remarks>
///
public partial class VideoPlayer: Control
{
// video source to play
private IVideoSource videoSource = null;
// last received frame from the video source
private Bitmap currentFrame = null;
// converted version of the current frame (in the case if current frame is a 16 bpp
// per color plane image, then the converted image is its 8 bpp version for rendering)
private Bitmap convertedFrame = null;
// last error message provided by video source
private string lastMessage = null;
// controls border color
private Color borderColor = Color.Black;
private Pen borderPen = null;
private bool hasBorder = false;
private ControlStyles controlStyles = ControlStyles.UserPaint;
private Size frameSize = new Size( 320, 240 );
private bool autosize = false;
private bool keepRatio = false;
private bool needSizeUpdate = false;
private bool firstFrameNotProcessed = true;
private volatile bool requestedToStop = false;
// parent of the control
private Control parent = null;
// dummy object to lock for synchronization
private object sync = new object( );
/// <summary>
/// Auto size control or not.
/// </summary>
///
/// <remarks><para>The property specifies if the control should be autosized or not.
/// If the property is set to <see langword="true"/>, then the control will change its size according to
/// video size and control will change its position automatically to be in the center
/// of parent's control.</para>
///
/// <para><note>Setting the property to <see langword="true"/> has no effect if
/// <see cref="Control.Dock"/> property is set to <see cref="DockStyle.Fill"/>.</note></para>
/// </remarks>
///
[DefaultValue( false )]
public bool AutoSizeControl
{
get { return autosize; }
set
{
autosize = value;
UpdatePosition( );
}
}
/// <summary>
/// Gets or sets whether the player should keep the aspect ratio of the images being shown.
/// </summary>
///
[Category("Appearance")]
[DefaultValue( true )]
public bool KeepAspectRatio
{
get { return keepRatio; }
set
{
keepRatio = value;
Invalidate( );
}
}
/// <summary>
/// Control's border color.
/// </summary>
///
/// <remarks><para>Specifies color of the border drawn around video frame.</para></remarks>
///
[Category("Appearance")]
[DefaultValue( typeof( Color ), "Black" )]
public Color BorderColor
{
get { return borderColor; }
set
{
borderColor = value;
UpdateBorderPen();
}
}
[Category("Appearance")]
[DefaultValue(true)]
public bool HasBorder
{
get { return hasBorder; }
set
{
hasBorder = value;
UpdateBorderPen();
}
}
[DefaultValue(CompositingMode.SourceCopy)]
public CompositingMode CompositingMode { get; set; }
[DefaultValue(InterpolationMode.Low)]
public InterpolationMode InterpolationMode { get; set; }
[DefaultValue(SmoothingMode.None)]
public SmoothingMode SmoothingMode { get; set; }
[DefaultValue(ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint |
ControlStyles.DoubleBuffer)]
public ControlStyles ControlStyles
{
get { return controlStyles; }
set
{
controlStyles = value;
SetStyle(controlStyles, true);
}
}
/// <summary>
/// Video source to play.
/// </summary>
///
/// <remarks><para>The property sets the video source to play. After setting the property the
/// <see cref="Start"/> method should be used to start playing the video source.</para>
///
/// <para><note>Trying to change video source while currently set video source is still playing
/// will generate an exception. Use <see cref="IsRunning"/> property to check if current video
/// source is still playing or <see cref="Stop"/> or <see cref="SignalToStop"/> and <see cref="WaitForStop"/>
/// methods to stop current video source.</note></para>
/// </remarks>
///
/// <exception cref="Exception">Video source can not be changed while current video source is still running.</exception>
///
[Browsable( false )]
public IVideoSource VideoSource
{
get { return videoSource; }
set
{
CheckForCrossThreadAccess( );
// detach events
if ( videoSource != null )
{
videoSource.NewFrame -= new NewFrameEventHandler( OnNewFrame );
videoSource.VideoSourceError -= new VideoSourceErrorEventHandler( videoSource_VideoSourceError );
videoSource.PlayingFinished -= new PlayingFinishedEventHandler( videoSource_PlayingFinished );
}
lock ( sync )
{
if ( currentFrame != null )
{
currentFrame.Dispose( );
currentFrame = null;
}
}
videoSource = value;
// atach events
if ( videoSource != null )
{
videoSource.NewFrame += new NewFrameEventHandler( OnNewFrame );
videoSource.VideoSourceError += new VideoSourceErrorEventHandler( videoSource_VideoSourceError );
videoSource.PlayingFinished += new PlayingFinishedEventHandler( videoSource_PlayingFinished );
}
else
{
frameSize = new Size( 320, 240 );
}
lastMessage = null;
needSizeUpdate = true;
firstFrameNotProcessed = true;
// update the control
Invalidate( );
}
}
/// <summary>
/// State of the current video source.
/// </summary>
///
/// <remarks><para>Current state of the current video source object - running or not.</para></remarks>
///
[Browsable( false )]
public bool IsRunning
{
get
{
CheckForCrossThreadAccess( );
return ( videoSource != null ) ? videoSource.IsRunning : false;
}
}
protected Rectangle videoArea = new Rectangle();
[Browsable(false)]
public Rectangle VideoArea { get => videoArea; }
/// <summary>
/// Delegate to notify about new frame.
/// </summary>
///
/// <param name="sender">Event sender.</param>
/// <param name="image">New frame.</param>
///
public delegate void NewFrameHandler( object sender, ref Bitmap image );
/// <summary>
/// New frame event.
/// </summary>
///
/// <remarks><para>The event is fired on each new frame received from video source. The
/// event is fired right after receiving and before displaying, what gives user a chance to
/// perform some image processing on the new frame and/or update it.</para>
///
/// <para><note>Users should not keep references of the passed to the event handler image.
/// If user needs to keep the image, it should be cloned, since the original image will be disposed
/// by the control when it is required.</note></para>
/// </remarks>
///
public event NewFrameHandler NewFrame;
/// <summary>
/// Playing finished event.
/// </summary>
///
/// <remarks><para>The event is fired when/if video playing finishes. The reason of video
/// stopping is provided as an argument to the event handler.</para></remarks>
///
public event PlayingFinishedEventHandler PlayingFinished;
/// <summary>
/// Initializes a new instance of the <see cref="VideoSourcePlayer"/> class.
/// </summary>
public VideoPlayer()
{
this.HasBorder = true;
this.KeepAspectRatio = true;
this.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
this.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
this.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
this.videoSource = null;
this.ForeColor = DefaultForeColor;
InitializeComponent( );
// update control style
this.ControlStyles = ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint |
ControlStyles.DoubleBuffer;
}
public void UpdateBorderPen(Pen pen = null)
{
if (hasBorder)
{
if (borderPen != null && pen != null && pen != borderPen)
{
borderPen.Dispose();
borderPen = null;
}
if (pen != null)
{
borderPen = pen;
}
else if (borderPen == null)
{
borderPen = new Pen(borderColor, 1);
}
else if (borderPen.Color != borderColor)
{
borderPen.Color = borderColor;
}
}
else if (borderPen != null)
{
borderPen.Dispose();
borderPen = null;
}
Invalidate();
}
// Check if the control is accessed from a none UI thread
protected void CheckForCrossThreadAccess()
{
// force handle creation, so InvokeRequired() will use it instead of searching through parent's chain
if ( !IsHandleCreated )
{
CreateControl( );
// if the control is not Visible, then CreateControl() will not be enough
if ( !IsHandleCreated )
{
CreateHandle( );
}
}
if ( InvokeRequired )
{
throw new InvalidOperationException( "Cross thread access to the control is not allowed." );
}
}
/// <summary>
/// Start video source and displaying its frames.
/// </summary>
public void Start( )
{
CheckForCrossThreadAccess( );
requestedToStop = false;
if ( videoSource != null )
{
firstFrameNotProcessed = true;
videoSource.Start( );
Invalidate( );
}
}
/// <summary>
/// Stop video source.
/// </summary>
///
/// <remarks><para>The method stops video source by calling its <see cref="AForge.Video.IVideoSource.Stop"/>
/// method, which abourts internal video source's thread. Use <see cref="SignalToStop"/> and
/// <see cref="WaitForStop"/> for more polite video source stopping, which gives a chance for
/// video source to perform proper shut down and clean up.
/// </para></remarks>
///
public void Stop( )
{
CheckForCrossThreadAccess( );
requestedToStop = true;
if ( videoSource != null )
{
videoSource.Stop( );
if ( currentFrame != null )
{
currentFrame.Dispose( );
currentFrame = null;
}
Invalidate( );
}
}
/// <summary>
/// Signal video source to stop.
/// </summary>
///
/// <remarks><para>Use <see cref="WaitForStop"/> method to wait until video source
/// stops.</para></remarks>
///
public void SignalToStop( )
{
CheckForCrossThreadAccess( );
requestedToStop = true;
if ( videoSource != null )
{
videoSource.SignalToStop( );
}
}
/// <summary>
/// Wait for video source has stopped.
/// </summary>
///
/// <remarks><para>Waits for video source stopping after it was signaled to stop using
/// <see cref="SignalToStop"/> method. If <see cref="SignalToStop"/> was not called, then
/// it will be called automatically.</para></remarks>
///
public void WaitForStop( )
{
CheckForCrossThreadAccess( );
if ( !requestedToStop )
{
SignalToStop( );
}
if ( videoSource != null )
{
videoSource.WaitForStop( );
if ( currentFrame != null )
{
currentFrame.Dispose( );
currentFrame = null;
}
Invalidate( );
}
}
protected int rotation = 0;
public void setRotation(int rotate)
{
rotation = rotate;
}
/// <summary>
/// Get clone of current video frame displayed by the control.
/// </summary>
///
/// <returns>Returns copy of the video frame, which is currently displayed
/// by the control - the last video frame received from video source. If the
/// control did not receive any video frames yet, then the method returns
/// <see langword="null"/>.</returns>
///
public Bitmap GetCurrentVideoFrame( )
{
lock ( sync )
{
return ( currentFrame == null ) ? null : AForge.Imaging.Image.Clone( currentFrame );
}
}
protected const int WM_ERASEBKGND = 20;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_ERASEBKGND)
{
m.Result = IntPtr.Zero;
}
else
{
base.WndProc(ref m);
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
}
// Paint control
protected override void OnPaint( PaintEventArgs e )
{
if ( !Visible )
{
return;
}
// is it required to update control's size/position
if ( ( needSizeUpdate ) || ( firstFrameNotProcessed ) )
{
UpdatePosition( );
needSizeUpdate = false;
}
Graphics g = e.Graphics;
Rectangle rect = this.ClientRectangle;
if (borderPen != null)
{
// draw rectangle
g.DrawRectangle(borderPen, rect.X, rect.Y, rect.Width - 1, rect.Height - 1);
}
if (videoSource == null)
{
using (SolidBrush drawBrush = new SolidBrush(Color.Black))
{
g.Clear(this.BackColor);
string curMessage = lastMessage;
g.DrawString(string.IsNullOrEmpty(curMessage) ? "Not connected" : curMessage, Font ?? DefaultFont, drawBrush, new Point(5, 5));
}
videoArea = this.ClientRectangle;
base.OnPaint(e);
return;
}
Bitmap frame = null;
lock ( sync )
{
if ((currentFrame != null) && (lastMessage == null))
{
frame = (convertedFrame != null) ? convertedFrame : currentFrame;
frame = frame.Clone() as Bitmap;
firstFrameNotProcessed = false;
}
}
if (frame != null)
{
int a = hasBorder ? 1 : 0, b = hasBorder ? 2 : 0;
g.CompositingMode = CompositingMode;
g.InterpolationMode = InterpolationMode;
g.SmoothingMode = SmoothingMode;
if (keepRatio || rotation != 0)
{
double ratio = (double)frame.Width / frame.Height;
if (rotation != 0)
{
ratio = 1;
a = b = 0;
}
int w = rect.Width - b, h = rect.Height - b;
if (w < h * ratio)
{
h = (int)(w / ratio);
}
else
{
w = (int)(h * ratio);
}
videoArea.X = (rect.Width - w) / 2 + a;
videoArea.Y = (rect.Height - h) / 2 + a;
videoArea.Width = w;
videoArea.Height = h;
}
else
{
videoArea.X = rect.X + a;
videoArea.Y = rect.Y + a;
videoArea.Width = rect.Width - b;
videoArea.Height = rect.Height - b;
}
// draw current frame
g.DrawImage(frame, videoArea);
frame.Dispose();
}
else
{
videoArea = this.ClientRectangle;
using (SolidBrush drawBrush = new SolidBrush(this.ForeColor))
{
g.Clear(this.BackColor);
string curMessage = lastMessage;
g.DrawString(string.IsNullOrEmpty(curMessage) ? "Connecting ..." : curMessage, Font ?? DefaultFont, drawBrush, new Point(5, 5));
}
}
base.OnPaint(e);
}
// Update controls size and position
public void UpdatePosition( )
{
if ( ( autosize ) && ( this.Dock != DockStyle.Fill ) && ( this.Parent != null ) )
{
Rectangle rc = this.Parent.ClientRectangle;
int width = frameSize.Width;
int height = frameSize.Height;
// update controls size and location
this.SuspendLayout( );
this.Location = new Point( ( rc.Width - width - 2 ) / 2, ( rc.Height - height - 2 ) / 2 );
this.Size = new Size( width + 2, height + 2 );
this.ResumeLayout( );
}
}
// On new frame ready
public void OnNewFrame( object _sender, NewFrameEventArgs eventArgs )
{
if ( !requestedToStop )
{
Bitmap newFrame = (Bitmap) eventArgs.Frame.Clone( );
// let user process the frame first
if ( NewFrame != null )
{
NewFrame( this, ref newFrame );
}
if (rotation != 0)
{
Bitmap bmp2 = new Bitmap(480, 480);
using (Graphics g = Graphics.FromImage(bmp2))
{
if (rotation == 90)
{
g.TranslateTransform(0, -160);
g.RotateTransform(90);
g.DrawImage(newFrame, 0, -480);
}
else
{
g.TranslateTransform(0, 80);
g.RotateTransform(-90);
g.DrawImage(newFrame, -480, 0);
}
}
newFrame.Dispose();
newFrame = bmp2;
}
// now update current frame of the control
Bitmap old = null, oldC = null;
lock ( sync )
{
// dispose previous frame
if ( currentFrame != null )
{
if ( currentFrame.Size != eventArgs.Frame.Size )
{
needSizeUpdate = true;
}
old = currentFrame;
currentFrame = null;
}
oldC = convertedFrame;
convertedFrame = null;
currentFrame = newFrame;
frameSize = currentFrame.Size;
lastMessage = null;
// check if conversion is required to lower bpp rate
if ( ( currentFrame.PixelFormat == PixelFormat.Format16bppGrayScale ) ||
( currentFrame.PixelFormat == PixelFormat.Format48bppRgb ) ||
( currentFrame.PixelFormat == PixelFormat.Format64bppArgb ) )
{
convertedFrame = AForge.Imaging.Image.Convert16bppTo8bpp( currentFrame );
}
}
if (old != null)
{
old.Dispose();
}
if (oldC != null)
{
oldC.Dispose();
}
// update control
Invalidate( );
}
}
// Error occured in video source
private void videoSource_VideoSourceError( object sender, VideoSourceErrorEventArgs eventArgs )
{
lastMessage = eventArgs.Description;
Invalidate( );
}
// Video source has finished playing video
private void videoSource_PlayingFinished( object sender, ReasonToFinishPlaying reason )
{
switch ( reason )
{
case ReasonToFinishPlaying.EndOfStreamReached:
lastMessage = "Video has finished";
break;
case ReasonToFinishPlaying.StoppedByUser:
lastMessage = "Video was stopped";
break;
case ReasonToFinishPlaying.DeviceLost:
lastMessage = "Video device was unplugged";
break;
case ReasonToFinishPlaying.VideoSourceError:
lastMessage = "Video has finished because of error in video source";
break;
default:
lastMessage = "Video has finished for unknown reason";
break;
}
Invalidate( );
// notify users
if ( PlayingFinished != null )
{
PlayingFinished( this, reason );
}
}
// Parent Changed event handler
private void VideoSourcePlayer_ParentChanged( object sender, EventArgs e )
{
if ( parent != null )
{
parent.SizeChanged -= new EventHandler( parent_SizeChanged );
}
parent = this.Parent;
// set handler for Size Changed parent's event
if ( parent != null )
{
parent.SizeChanged += new EventHandler( parent_SizeChanged );
}
}
// Parent control has changed its size
private void parent_SizeChanged( object sender, EventArgs e )
{
UpdatePosition( );
}
}
}