From 22ede34997ed240276335c5db001c8c85e144409 Mon Sep 17 00:00:00 2001 From: rd91 Date: Wed, 30 Nov 2016 00:57:14 +0200 Subject: [PATCH] Added window capture ability --- Sources/Video/Video.csproj | 1 + Sources/Video/WindowCaptureStream.cs | 340 +++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 Sources/Video/WindowCaptureStream.cs diff --git a/Sources/Video/Video.csproj b/Sources/Video/Video.csproj index 360d6de2..06a2c18a 100644 --- a/Sources/Video/Video.csproj +++ b/Sources/Video/Video.csproj @@ -72,6 +72,7 @@ + diff --git a/Sources/Video/WindowCaptureStream.cs b/Sources/Video/WindowCaptureStream.cs new file mode 100644 index 00000000..afac3efc --- /dev/null +++ b/Sources/Video/WindowCaptureStream.cs @@ -0,0 +1,340 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Threading; + +namespace AForge.Video +{ + /// + /// Window capture video source. + /// + /// + /// The video source constantly captures the target window. + /// + /// Sample usage: + /// + /// // set NewFrame event handler + /// stream.NewFrame += new NewFrameEventHandler( video_NewFrame ); + /// + /// // start the video source + /// stream.Start( ); + /// + /// // ... + /// // signal to stop + /// stream.SignalToStop( ); + /// // ... + /// + /// private void video_NewFrame( object sender, NewFrameEventArgs eventArgs ) + /// { + /// // get new frame + /// Bitmap bitmap = eventArgs.Frame; + /// // process the frame + /// } + /// + /// + /// + public class WindowCaptureStream : IVideoSource + { + private IntPtr hwnd; + private Rectangle region; + + // frame interval in milliseconds + private int frameInterval = 100; + + private Thread thread = null; + private ManualResetEvent stopEvent = null; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Window to capture. + /// + public WindowCaptureStream(IntPtr hWnd) + { + hwnd = hWnd; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Window to capture. + /// Window's rectangle to capture + /// + public WindowCaptureStream(IntPtr hWnd, Rectangle region) : this(hWnd) + { + this.region = region; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Window to capture. + /// Window's rectangle to capture + /// Time interval between making screen shots, ms. + /// + public WindowCaptureStream(IntPtr hWnd, Rectangle region, int frameInterval) : this(hWnd, region) + { + this.frameInterval = frameInterval; + } + + /// + /// Received bytes count. + /// + /// + /// The property is not implemented for this video source and always returns 0. + /// + public long BytesReceived { get { return 0; } } + + private int framesReceived; + /// + /// Received frames count. + /// + /// + /// Number of frames the video source provided from the moment of the last + /// access to the property. + /// + /// + public int FramesReceived + { + get + { + int frames = framesReceived; + framesReceived = 0; + return frames; + } + } + + /// + /// State of the video source. + /// + /// + /// Current state of video source object - running or not. + /// + public bool IsRunning + { + get + { + if (thread != null) + { + // check thread status + if (thread.Join(0) == false) + return true; + + // the thread is not running, free resources + Free(); + } + return false; + } + } + + /// + /// Video source. + /// + /// + public string Source { get { return "Window Capture"; } } + + /// + /// New frame event. + /// + /// + /// Notifies clients about new available frame from video source. + /// + /// Since video source may have multiple clients, each client is responsible for + /// making a copy (cloning) of the passed video frame, because the video source disposes its + /// own original copy after notifying of clients. + /// + /// + public event NewFrameEventHandler NewFrame; + + /// + /// Video playing finished event. + /// + /// + /// This event is used to notify clients that the video playing has finished. + /// + /// + public event PlayingFinishedEventHandler PlayingFinished; + + /// + /// Video source error event. + /// + /// + /// This event is used to notify clients about any type of errors occurred in + /// video source object, for example internal exceptions. + /// + public event VideoSourceErrorEventHandler VideoSourceError; + + /// + /// Signal video source to stop its work. + /// + /// + /// Signals video source to stop its background thread, stop to + /// provide new frames and free resources. + /// + public void SignalToStop() + { + // stop thread + if (thread != null) + { + // signal to stop + stopEvent.Set(); + } + } + + /// + /// Start video source. + /// + /// + /// Starts video source and return execution to caller. Video source + /// object creates background thread and notifies about new frames with the + /// help of event. + /// + /// Video source is not specified. + /// + public void Start() + { + if (!IsRunning) + { + framesReceived = 0; + + // create events + stopEvent = new ManualResetEvent(false); + + // create and start new thread + thread = new Thread(new ThreadStart(WorkerThread)); + thread.Name = Source; // mainly for debugging + thread.Start(); + } + } + + /// + /// Stop video source. + /// + /// + /// Stops video source aborting its thread. + /// + /// Since the method aborts background thread, its usage is highly not preferred + /// and should be done only if there are no other options. The correct way of stopping camera + /// is signaling it stop and then + /// waiting for background thread's completion. + /// + /// + public void Stop() + { + if (this.IsRunning) + { + //stopEvent.Set(); + //thread.Abort(); + SignalToStop(); + WaitForStop(); + } + } + + /// + /// Wait for video source has stopped. + /// + /// + /// Waits for source stopping after it was signalled to stop using + /// method. + /// + public void WaitForStop() + { + if (thread != null) + { + // wait for thread stop + thread.Join(); + + Free(); + } + } + + /// + /// Free resource. + /// + /// + private void Free() + { + thread = null; + + // release events + stopEvent.Close(); + stopEvent = null; + } + + private void WorkerThread() + { + int width = region.Width; + int height = region.Height; + int x = region.Location.X; + int y = region.Location.Y; + + Bitmap screenshot = new Bitmap(width, height); + + var fromHwnd = Graphics.FromHwnd(hwnd); + var fromImage = Graphics.FromImage(screenshot); + + // download start time and duration + DateTime start; + TimeSpan span; + + while (!stopEvent.WaitOne(0, false)) + { + // set dowbload start time + start = DateTime.Now; + + try + { + IntPtr hdc_screen = fromHwnd.GetHdc(); + IntPtr hdc_screenshot = fromImage.GetHdc(); + + BitBlt(hdc_screenshot, 0, 0, width, height, hdc_screen, x, y, (int)CopyPixelOperation.SourceCopy); + + fromImage.ReleaseHdc(hdc_screenshot); + fromHwnd.ReleaseHdc(hdc_screen); + + NewFrame?.Invoke(this, new NewFrameEventArgs(screenshot)); + + // wait for a while ? + if (frameInterval > 0) + { + // get download duration + span = DateTime.Now.Subtract(start); + + // miliseconds to sleep + int msec = frameInterval - (int)span.TotalMilliseconds; + + if ((msec > 0) && (stopEvent.WaitOne(msec, false))) + break; + } + } + catch (ThreadAbortException) + { + break; + } + catch (Exception exception) + { + // provide information to clients + VideoSourceError?.Invoke(this, new VideoSourceErrorEventArgs(exception.Message)); + // wait for a while before the next try + Thread.Sleep(250); + } + + // need to stop ? + if (stopEvent.WaitOne(0, false)) + break; + } + + screenshot.Dispose(); + fromHwnd.Dispose(); + fromImage.Dispose(); + + PlayingFinished?.Invoke(this, ReasonToFinishPlaying.StoppedByUser); + } + + [DllImport("gdi32.dll")] + static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hObjectSource, int nXSrc, int nYSrc, int dwRop); + } +}