Poor Man's Screen Sharing

Remember when I about how was letting take the lead in features as simple as screen sharing?

Well, until they come to their senses and make (or the built-in server) as simple to use as the screen sharing feature, I decided to have a go at it myself.

I wanted an approach that:

  • Let me share a screen with anybody, on any platform
  • Required the least possible amount of software at either end (no complex servers and no funky clients to install)
  • Wasn't too sophisticated (the more features, the harder things are to use)
  • Would work remotely, provided you have direct Internet access

And what comes with pretty much every OS on the planet? Well, a browser, of course.

What about ?

At first I considered setting up an HTML page on my and have it serve the applet, but the built-in server is utterly brain-dead in terms of image encodings and crashed most of the applet versions I tried.

Then I realized that applets aren't the best thing to ensure cross-platform support (modern Windows and require you to find and download a JVM, which severely raises the bar and kills the "instant sharing" approach).

Plus I started running across issues such as screen scaling (which not all clients do properly, let alone the ones), pre-configuring the applets to do shared connections, etc., etc.

Furthermore, trying another server like OSXvnc would be cheating, since it requires extra software to be installed on the serving Mac.

KISS Is Best

As usual, taking down complexity a peg makes for a better solution.

Browsers can render images, and images are readily obtainable on the using the screencapture command. I didn't need interactivity at all, so a solution that let me see a 's screen in a browser as an image was fine with me.

All that remained was the refresh interval, and I settled on 10 seconds because it's a nice compromise between the pace of slideshow presentations and live demos (where you need to see some sort of mouse movement and interaction).

Incidentally, people doing online/LAN-based presentations might want to take a look at - it helps people keep track of the cursor.

So I decided to write a small script to serve screenshots via , taking some care to ensure that the screenshots were evenly spaced in time and that a bunch of simultaneous requests wouldn't result in undue load on the serving and several nearly-identical screenshots being served to clients.

What Your Audience Sees

The end result looks like this:

Can't be much simpler, huh? And the code (without any dependencies whatsoever, runnable on just about any ) is small enough to include verbatim right here, server and all.

Code

import os, socket, datetime, SimpleHTTPServer, SocketServer, StringIO

IMAGE_PATH = '/tmp/screen.png'
INTERVAL = 10
HTTP_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S %Z"

def screenCapture(path):
  """Take screenshot if necessary"""
  try:
    # See if the current screenshot is stale
    t = os.stat(path)
    if((datetime.datetime.now() - datetime.datetime.fromtimestamp(t.st_mtime)) < datetime.timedelta(seconds=INTERVAL)):
      return
  except OSError:
    # File was not found
    pass
  os.system('screencapture -xC ' + path)
  os.system('sips -Z 800 ' + path)

def formatPage():
  """Render HTML Page"""
  return """<html>
<head>
<title>Screen Sharing: %s</title>
<script language="JavaScript">
  counter = 0;
  function update() {
    document.getElementById("screenshot").src = "/screen.png?" + counter;
    self.setTimeout("update()",%s);
    counter++;
  }
</script>
<style>
body { font-family: Lucida, Arial, "MS Trebuchet", sans-serif; }
.info { background-color: #DDD; padding: 4px; }
</style>
</head>
<body onload="update();">
<center>
<img id="screenshot" src="/screen.png"><br><p class="info">Updated every %s seconds</p>
</center>
</body>
</html>""" % (socket.gethostname(), INTERVAL * 1000, INTERVAL)

class LocalRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  """Custom HTTP Request Handler"""
  def send_head(self):
    """Common handler for GET and HEAD requests"""
    if self.path == "/":
      self.send_response(200)
      self.send_header("Content-type","text/html")
      self.end_headers()
      return StringIO.StringIO(formatPage())

    if self.path[:11] == "/screen.png":
      try:
        screenCapture(IMAGE_PATH)
        f = open(IMAGE_PATH,'rb')
        self.send_response(200)
        self.send_header("Content-type","image/png")
        self.send_header("Content-Length", str(os.fstat(f.fileno()).st_size))
        mtime = datetime.datetime.fromtimestamp(os.fstat(f.fileno()).st_mtime)
        self.send_header("Last-Modified", mtime.strftime(HTTP_DATE_FORMAT))
        expires =  mtime + datetime.timedelta(seconds=INTERVAL)
        self.send_header("Expires", expires.strftime(HTTP_DATE_FORMAT))
        self.end_headers()
        return f
      except IOError:
        pass

    self.send_error(404, "File not found")
    return None

if __name__=='__main__':
  screenCapture(IMAGE_PATH)
  httpd = SocketServer.TCPServer(('',2000),LocalRequestHandler)
  httpd.serve_forever()

Usage

To use this, just save the above as, say, present.py, type python present.py at the prompt, and access your via port 2000 like the URL you see above (hostname:2000).

Obviously, you'll have to figure out and tell other people your IP address, open any firewalls, etc., etc.

If you're a complete networking newbie, please don't ask me how to find your own IP address. Learn the basics first...

Possible Enhancements

And, of course, this can be enhanced in all sorts of ways:

  • Taking parameters such as the port number, refresh interval and screen size
  • Auto-detecting the most useful interface (usually en0 or en1 in s) and outputting the IP address upon startup
  • It can be re-packaged as an application bundle
  • It can use PyObjC to perform the actual screenshot-taking and resizing
  • It can be modified to advertise itself on the LAN via
  • handling can be improved a bit (dealing with HEAD and If-Modified-Since)
  • etc., etc.

But as it is, it works just fine with my s and s (I have not tried , but I assume it will work), and might be just the thing for anyone wanting a very simple view-only screen sharing trick.

The choice of port 2000 is entirely arbitrary, and my guess is that a more sensible default would be 8000, 8080, 8090, etc., since those are typically open in corporate firewalls in the outbound direction (i.e., you can share your screen with somebody behind one).

This page is referenced in: