Creating a Screenshot Server Part 1

I’ve been doing some testing recently that requires me to leave an application running for long periods of time being interacted with through an AutoHotkey script. The nature of these tests meant that RDPing into the machine would invalidate them. With RDP out as an option, I started thinking about how I could remotely monitor the system in a simple way.

I through together an application to take screenshots at fixed intervals and dump the files into a shared folder. I could then access the shared folder remotely and see what was going on at any given time. As a bonus, I had a history of what had been happening. This worked rather well for my needs so I thought I’d share how it works.

In part 1, we’ll create the library to periodically capture screenshots and add a simple console application to consume it and give us feedback. Next, in part 2, we’ll create a webpage running on a local server to show us the images without the need for hacky folder shares. Let’s get started!

Creating the Solution and Projects

Open Visual Studio and create a new Class Library project named ScreenServist. This will be the core library we’ll consume from our console application.

Creating the new project

Add a second project of type Console Application named ScreenServistConsole and set it as the startup project. We’re all set now to start adding some code!

The Screenshot Class

The Screenshot class will represent a single screen grab. It will expose some metadata about the screenshot and do the actual capture. Here’s what the final class will look like.

/// <summary>
/// Represents a single screen shot.
/// </summary>
public class Screenshot
{
    private readonly string _fullSavePath;
    private readonly int _xScreenCoordinate;
    private readonly int _yScreenCoordinate;
    private readonly Size _screenSize;

    /// <summary>
    /// Initializes a new <see cref="Screenshot"/>.
    /// </summary>
    /// <param name="fullSavePath">Full path at which to save the image.</param>
    /// <param name="xScreenCoordinate">X coordinate of the screen to capture.</param>
    /// <param name="yScreenCoordinate">Y coordinate of the screen to capture.</param>
    /// <param name="screenSize">Size of the area to capture.</param>
    public Screenshot(string fullSavePath,
        int xScreenCoordinate,
        int yScreenCoordinate,
        Size screenSize)
    {
        _xScreenCoordinate = xScreenCoordinate;
        _yScreenCoordinate = yScreenCoordinate;
        _fullSavePath = fullSavePath;
        _screenSize = screenSize;
    }

    /// <summary>
    /// Path to the image file.
    /// </summary>
    public string Path => _fullSavePath;

    /// <summary>
    /// Time at which the image file was created.
    /// </summary>
    public DateTime CreationTime { get; private set; }

    /// <summary>
    /// Save the screenshot to disk.
    /// </summary>
    public void Save()
    {
        using (Bitmap screenshot = new Bitmap(_screenSize.Width, _screenSize.Height, PixelFormat.Format24bppRgb))
        {
            using (Graphics screenGraph = Graphics.FromImage(screenshot))
            {
                screenGraph.CopyFromScreen(_xScreenCoordinate,
                    _yScreenCoordinate,
                    0,
                    0,
                    _screenSize,
                    CopyPixelOperation.SourceCopy);

                screenshot.Save(_fullSavePath, ImageFormat.Jpeg);
            }
        }

        UpdateFromFileInfo();
    }

    private void UpdateFromFileInfo()
    {
        FileInfo info = new FileInfo(_fullSavePath);
        CreationTime = info.CreationTime;
    }
}

The Save method is responsible for saving the capture of the screen to disk. First, a new Bitmap is created in a using statement, after all we don’t want to forget to dispose it now do we? We give the bitmap a size and a PixelFormat. The size will match the size of your desktop. You can play around with the PixelFormat a bit to find something that works for you, compromising size for quality.

Next, also in a using, we create the Graphics object. Graphics in the System.Drawing namespace is a powerful tool for drawing. It’s all but forgotten in today’s world of WPF development for the desktop, but it’s the only way I found to capture the screen without using native code . Next, we simply call CopyFromScreen and pass in the screen coordinates of the top left (x, y), the size, and how we want the pixels copied. This transfers the pixels of the source (the screen) to the destination (our Bitmap).

After creating the Bitmap in memory, we call Save, giving it a path and ImageFormat. Pay careful attention to the ImageFormat. I first used ImageFormat.Png and on my dual monitor system, the screenshots were around 1.2MB. Not bad, but we’re going to generate a lot of these. Changing to ImageFormat.Jpeg reduced the size significantly, to around 460KB. The quality of the jpegs was perfectly acceptable for my needs.

JPEG screenshot

Lastly, we call UpdateFromFileInfo to get some metadata about the file we just created. You can doctor that up a bit and get more information if you wanted, like the file name, size etc. Since I’m using GUIDs for names, CreationTime was a more descriptive way to tell one image from another.

The ScreenCaptureService Class

The ScreenCaptureService class will create Screenshots on a timer and raise an event to let consumers know when a new capture is saved.

/// <summary>
/// Service to capture screen shots.
/// </summary>
public class ScreenCaptureService
{
    private const string CAPTURE_SUBDIRECTORY = "Captures";
    private const int CAPTURE_FREQUENCY_MS = 2000;
    private readonly Timer _captureTimer;

    /// <summary>
    /// Event fired when a new capture has been saved.
    /// </summary>
    public event EventHandler<CaptureSavedEventArgs> CaptureSaved;

    /// <summary>
    /// Initializes a new <see cref="ScreenCaptureService"/>.
    /// </summary>
    public ScreenCaptureService()
    {
        _captureTimer = new Timer(CAPTURE_FREQUENCY_MS);
        _captureTimer.Elapsed += (s, e) => { Capture(); };
    }

    /// <summary>
    /// Start capturing images.
    /// </summary>
    public void StartCapturing()
    {
        CreateSubDirectoryIfNeeded();
        _captureTimer.Start();
    }

    /// <summary>
    /// Stop capturing images.
    /// </summary>
    public void StopCapturing()
    {
        _captureTimer.Stop();
    }

    /// <summary>
    /// Capture a snapshot of the desktop screen.
    /// </summary>
    private void Capture()
    {
        try
        {
            string imageName = CreateImageName();
            Screenshot screenshot = new Screenshot(imageName,
                SystemInformation.VirtualScreen.X,
                SystemInformation.VirtualScreen.Y,
                SystemInformation.VirtualScreen.Size);

            screenshot.Save();

            OnCaptureSaved(new CaptureSavedEventArgs(screenshot));
        }
        catch (System.IO.IOException ex)
        {
            Debug.WriteLine(ex);
        }
    }

    private string CreateImageName()
    {
        string path = System.Reflection.Assembly.GetExecutingAssembly().Location;
        string name = Path.Combine(
            Path.GetDirectoryName(path),
            CAPTURE_SUBDIRECTORY,
            $"{Guid.NewGuid()}.jpg");
        return name;
    }

    private void CreateSubDirectoryIfNeeded()
    {
        if (!Directory.Exists(CAPTURE_SUBDIRECTORY))
        {
            Directory.CreateDirectory(CAPTURE_SUBDIRECTORY);
        }
    }

    private void OnCaptureSaved(CaptureSavedEventArgs e)
    {
        CaptureSaved?.Invoke(this, e);
    }
}

This is a little more mundane. We create a timer, I chose two-seconds as the interval, but that can be increased or decreased as needed. I could also have passed that in as a parameter to the service’s constructor to make it more useful and reusable. When the timer elapses, the service creates a new Screenshot instance, saves the file and raises the CaptureSaved event to communicate the new image to consumers. The CreateImageName function generates a unique image name by appending a GUID to the desired directory. Here, I created a subdirectory named “Captures” in the same directory as the executable. You could change that to any directory to which the application has permissions though.

Consuming ScreenCaptureService in a Console Application

Switching over to the ScreenServistConsole project, open up Program.cs. This is the main entry point for our application. In main, we’ll create a new instance of ScreenCaptureService, subscribe to the CaptureSaved event and call StartCapturing to begin taking screenshots.

To prevent the application from exiting, I added a while loop that can be exited by pressing “q” followed by Enter. There are, of course, other ways to accomplish the same thing.

class Program
{
    static void Main(string[] args)
    {
        ScreenCaptureService service = new ScreenCaptureService();
        service.CaptureSaved += Service_CaptureSaved;
        service.StartCapturing();

        while (Console.ReadLine() != "q")
        {

        }
    }

    private static void Service_CaptureSaved(object sender,
        CaptureSavedEventArgs e)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"Captured at {e.Screenshot.CreationTime.ToLongTimeString()}");
    }
}

In the event handler for CaptureSaved, we print out CreationTime from the provided Screenshot to give us some feedback that it’s working. Build and run the solution, you should see times printing out in your console window and new jpeg files being created in the “Captures” sub-directory.

console

In the next part, we’ll create a method to view these images from a browser and avoid the shared folders business.

The complete code for part 1 is available on github https://github.com/chrishibler/screen-servist-part1

This entry was posted in Programming, Tech. Bookmark the permalink.

Leave a Reply