I was interested in a python based screen sharing set of scripts. I did not care to write the low-level socket code. I recently discovered an interesting messaging broker/server called mosquitto (https://mosquitto.org/) In short, you make a connection to the server and subscribe to topics. When the broker receives a message on the topic you are subscribed to it will send you that message.
Here are two scripts that connect to the mosquitto broker. One script listens for a request for a screen grab. The other script requests screen grabs and displays them.
These scripts rely on image processing modules to do the heavy lifting
The process is
- client requests screen
- server is notified that there is a message on a topic for a screen grab
- server grabs screen with mss
- server converts screen to numpy
- server base 64 encodes a compressed pickled numpy image
- server does a delta with the last image if possible
- server publishes the base 64 string to the screen grab topic
- client is notified that there is a message on the screen grab topic
- client reverses the process
- client displays the screen
- client goes back to step 1
Quit the server with a command line message
C:\Program Files\mosquitto>mosquitto_pub.exe -h "127.0.0.1" -t "server/quit" -m "0"
This implementation uses delta refreshes. It uses numpy to xor the current and
last screen. This really increases the compression ratio. It demonstrates that an offsite server can be used and connected to by many clients who may be interested in a live stream of what is going on on a certain machine. These scripts are definitely not production quality and only serve as a POC.
script 1 - the server
import paho.mqtt.client as mqtt
import time
import uuid
import cv2
import mss
from mss.tools import zlib
import numpy
import base64
import io
import pickle
monitor = 0 # all monitors
quit = False
capture = False
def on_connect(client, userdata, flags, rc):
print("Connected flags " + str(flags) + " ,result code=" + str(rc))
def on_disconnect(client, userdata, flags, rc):
print("Disconnected flags " + str(flags) + " ,result code=" + str(rc))
def on_message(client, userdata, message):
global quit
global capture
global last_image
if message.topic == "server/size":
with mss.mss() as sct:
sct_img = sct.grab(sct.monitors[monitor])
size = sct_img.size
client.publish("client/size", str(size.width) + "|" + str(size.height))
if message.topic == "server/update/first":
with mss.mss() as sct:
b64img = BuildPayload(False)
client.publish("client/update/first", b64img)
if message.topic == "server/update/next":
with mss.mss() as sct:
b64img = BuildPayload()
client.publish("client/update/next", b64img)
if message.topic == "server/quit":
quit = True
def BuildPayload(NextFrame = True):
global last_image
with mss.mss() as sct:
sct_img = sct.grab(sct.monitors[monitor])
image = numpy.array(sct_img)
if NextFrame == True:
# subsequent image - delta that brings much better compression ratio as unchanged RGBA quads will XOR to 0,0,0,0
xor_image = image ^ last_image
b64img = base64.b64encode(zlib.compress(pickle.dumps(xor_image), 9))
else:
# first image - less compression than delta
b64img = base64.b64encode(zlib.compress(pickle.dumps(image), 9))
print("Source Image Size=" + str(len(sct_img.rgb)))
last_image = image
print("Compressed Image Size=" + str(len(b64img)) + " bytes")
return b64img
myid = str(uuid.uuid4()) + str(time.time())
print("Client Id = " + myid)
client = mqtt.Client(myid, False)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
try:
client.connect("127.0.0.1")
client.loop_start()
client.subscribe("server/size")
client.subscribe("server/update/first")
client.subscribe("server/update/next")
client.subscribe("server/quit")
while not quit:
time.sleep(5)
continue
client.publish("client/quit")
time.sleep(5)
client.loop_stop()
client.disconnect()
except:
print("Could not connect to the Mosquito server")
script 2 - the client
import paho.mqtt.client as mqtt
import time
import uuid
import cv2
import mss
from mss.tools import zlib
import numpy
import base64
import io
import pickle
quit = False
size = False
capture = False
width = 0
height = 0
last_image = None
first = False
def on_connect(client, userdata, flags, rc):
print("Connected flags " + str(flags) + " ,result code=" + str(rc))
def on_message(client, userdata, message):
global quit
global size
global capture
global width
global height
global last_image
global first
if message.topic == "client/size":
if width == 0 and height == 0:
strsize = message.payload.decode("utf-8")
strlist = strsize.split("|")
width = int(strlist[0])
height = int(strlist[1])
size = True
if message.topic == "client/update/first":
# stay synchronized with other connected clients
if size == True:
DecodeAndShowPayload(message, False)
first = True
if message.topic == "client/update/next":
# stay synchronized with other connected clients
if size == True and first == True:
DecodeAndShowPayload(message)
if message.topic == "client/quit":
quit = True
def DecodeAndShowPayload(message, NextFrame = True):
global last_image
global capture
global quit
if NextFrame == True:
# subsequent image - delta that brings much better compression ratio as unchanged RGBA quads will XOR to 0,0,0,0
xor_image = pickle.loads(zlib.decompress(base64.b64decode(message.payload.decode("utf-8")), 15, 65535))
image = last_image ^ xor_image
else:
# first image - less compression than delta
image = pickle.loads(zlib.decompress(base64.b64decode(message.payload.decode("utf-8")), 15, 65535))
last_image = image
cv2.imshow("Server", image)
if cv2.waitKeyEx(25) == 113:
quit = True
capture = False
myid = str(uuid.uuid4()) + str(time.time())
print("Client Id = " + myid)
client = mqtt.Client(myid, False)
client.on_connect = on_connect
client.on_message = on_message
try:
client.connect("127.0.0.1")
client.loop_start()
client.subscribe("client/size")
client.subscribe("client/update/first")
client.subscribe("client/update/next")
client.subscribe("client/quit")
# ask once and retain in case client starts before server
asksize = False
while not size:
if not asksize:
client.publish("server/size", "1", 0, True)
asksize = True
time.sleep(1)
first_image = True
while not quit:
if capture == False:
capture = True
if first_image:
client.publish("server/update/first")
first_image = False
else:
client.publish("server/update/next")
time.sleep(.1)
cv2.destroyAllWindows()
client.loop_stop()
client.disconnect()
except:
print("Could not connect to the Mosquito server")
Sample output showing compression
ex: Source is 18,662,400 Bytes (3 screens)
A compressed image is as small as 35,588 Bytes which is 524 to 1
