python - example synchronized player (#70)
authorSaveliy <saveliy.m.yusufov@gmail.com>
Wed, 30 Jan 2019 22:27:49 +0000 (17:27 -0500)
committerOlivier Aubert <contact@olivieraubert.net>
Wed, 30 Jan 2019 22:27:49 +0000 (23:27 +0100)
Example python-based player allowing synchronized playing

examples/video_sync/README.md [new file with mode: 0644]
examples/video_sync/figure.png [new file with mode: 0644]
examples/video_sync/main.py [new file with mode: 0644]
examples/video_sync/mini_player.py [new file with mode: 0644]
examples/video_sync/network.py [new file with mode: 0644]

diff --git a/examples/video_sync/README.md b/examples/video_sync/README.md
new file mode 100644 (file)
index 0000000..c3792d8
--- /dev/null
@@ -0,0 +1,11 @@
+## Video Synchronization with python-vlc
+
+<img src="figure.png" width="500" align="right">
+
+Each video player is launched as a separate process. `main.py` is the entry point into the program. It creates the "main" video player with all of its respective controls. The main video player is started alongside a multithreaded server that "broadcasts" signals (e.g. play, pause, stop, and etc.) and the current time of the video player, to all client/slave videos. Specifically, this is accomplished by putting the current time or the signal of the main video player into a `queue` (FIFO). The multithreaded server gets the value from the queue and sends the value to each connected client.
+
+In a somewhat similar fashion, each "slave" video player is launched as a separate process, along with a client. The client has a `data_receiver` thread that continuously listens for data, receives data, parses the data, and puts the data into a `queue` that is specific to that "slave" video player. The "slave" video player gets the value from the queue and updates itself in accordance with that value.
+
+In order to facilitate interprocess communication, UNIX domain sockets are used to send and receive data. For the time being, Windows users have to use TCP/IP sockets.
+
+Note: for the sake of clarity, the figure on the right only shows the case of 3 client/slave videos.
diff --git a/examples/video_sync/figure.png b/examples/video_sync/figure.png
new file mode 100644 (file)
index 0000000..f0feb1d
Binary files /dev/null and b/examples/video_sync/figure.png differ
diff --git a/examples/video_sync/main.py b/examples/video_sync/main.py
new file mode 100644 (file)
index 0000000..f3ed9c0
--- /dev/null
@@ -0,0 +1,396 @@
+#
+# PyQt5-based video-sync example for VLC Python bindings
+# Copyright (C) 2009-2010 the VideoLAN team
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+#
+"""
+A VLC python bindings player implemented with PyQt5 that is meant to be utilized
+as the "master" player that controls all "slave players".
+
+Author: Saveliy Yusufov, Columbia University, sy2685@columbia.edu
+Date: 25 January 2019
+"""
+
+import platform
+import queue
+import os
+import subprocess
+import sys
+
+from PyQt5 import QtWidgets, QtGui, QtCore
+import vlc
+from network import Server
+
+
+class Player(QtWidgets.QMainWindow):
+    """A "master" Media Player using VLC and Qt
+    """
+
+    def __init__(self, master=None):
+        QtWidgets.QMainWindow.__init__(self, master)
+        self.setWindowTitle("Main Media Player")
+
+        # Create a basic vlc instance
+        self.instance = vlc.Instance()
+
+        self.media = None
+
+        # Create an empty vlc media player
+        self.mediaplayer = self.instance.media_player_new()
+
+        self.create_ui()
+        self.data_queue = queue.Queue()
+        self.is_paused = False
+
+        self.timer = QtCore.QTimer(self)
+        self.timer.setInterval(100)
+        self.timer.timeout.connect(self.update_ui)
+        self.timer.timeout.connect(self.update_time_label)
+
+    def create_ui(self):
+        """Set up the user interface, signals & slots
+        """
+        self.widget = QtWidgets.QWidget(self)
+        self.setCentralWidget(self.widget)
+
+        # In this widget, the video will be drawn
+        if platform.system() == "Darwin":  # for MacOS
+            self.videoframe = QtWidgets.QMacCocoaViewContainer(0)
+        else:
+            self.videoframe = QtWidgets.QFrame()
+
+        self.palette = self.videoframe.palette()
+        self.palette.setColor(QtGui.QPalette.Window, QtGui.QColor(0, 0, 0))
+        self.videoframe.setPalette(self.palette)
+        self.videoframe.setAutoFillBackground(True)
+
+        # Create the time display
+        self.timelabel = QtWidgets.QLabel("00:00:00", self)
+
+        # Create the position slider (QSlider)
+        self.positionslider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
+        self.positionslider.setToolTip("Position")
+        self.positionslider.setMaximum(1000)
+        self.positionslider.sliderMoved.connect(self.set_position)
+        # self.positionslider.sliderPressed.connect(self.set_position)
+        self.positionslider.sliderMoved.connect(self.update_time_label)
+
+        # Create the "previous frame" button
+        self.previousframe = QtWidgets.QPushButton()
+        self.previousframe.setFixedWidth(25)
+        self.previousframe.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaSkipBackward))
+        self.previousframe.clicked.connect(self.on_previous_frame)
+
+        # Create the play button and connect it to the play/pause function
+        self.playbutton = QtWidgets.QPushButton()
+        self.playbutton.setFixedWidth(40)
+        self.playbutton.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay))
+        self.playbutton.clicked.connect(self.play_pause)
+
+        # Create the "next frame" button
+        self.nextframe = QtWidgets.QPushButton()
+        self.nextframe.setFixedWidth(25)
+        self.nextframe.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaSkipForward))
+        self.nextframe.clicked.connect(self.on_next_frame)
+
+        # Create the "Playback rate" label
+        self.pb_rate_label = QtWidgets.QLabel("Playback rate: {}x".format(self.mediaplayer.get_rate()), self)
+
+        # Create the "decrease playback rate" button
+        self.decr_pb_rate = QtWidgets.QPushButton()
+        self.decr_pb_rate.setFixedWidth(30)
+        self.decr_pb_rate.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaSeekBackward))
+        self.decr_pb_rate.clicked.connect(self.decr_mov_play_rate)
+
+        # Create the stop button and connect it to the stop function
+        self.stopbutton = QtWidgets.QPushButton()
+        self.stopbutton.setFixedWidth(30)
+        self.stopbutton.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaStop))
+        self.stopbutton.clicked.connect(self.stop)
+
+        # Create the "increase playback rate" button
+        self.incr_pb_rate = QtWidgets.QPushButton()
+        self.incr_pb_rate.setFixedWidth(30)
+        self.incr_pb_rate.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaSeekForward))
+        self.incr_pb_rate.clicked.connect(self.incr_mov_play_rate)
+
+        self.top_control_box = QtWidgets.QHBoxLayout()
+
+        # Add the time and position slider to the 1st controls layout
+        self.top_control_box.addWidget(self.timelabel)
+        self.top_control_box.addWidget(self.positionslider)
+
+        self.bottom_control_box = QtWidgets.QHBoxLayout()
+
+        # Add the buttons to the 2nd controls layout
+        self.bottom_control_box.addWidget(self.previousframe)
+        self.bottom_control_box.addWidget(self.playbutton)
+        self.bottom_control_box.addWidget(self.nextframe)
+        self.bottom_control_box.addWidget(self.pb_rate_label)
+        self.bottom_control_box.addWidget(self.decr_pb_rate)
+        self.bottom_control_box.addWidget(self.stopbutton)
+        self.bottom_control_box.addWidget(self.incr_pb_rate)
+
+        self.vboxlayout = QtWidgets.QVBoxLayout()
+
+        self.vboxlayout.addWidget(self.videoframe)
+        self.vboxlayout.addLayout(self.top_control_box)
+        self.vboxlayout.addLayout(self.bottom_control_box)
+
+        self.widget.setLayout(self.vboxlayout)
+
+        menu_bar = self.menuBar()
+
+        # File menu
+        file_menu = menu_bar.addMenu("File")
+
+        # Create submenu to start new processes from file menu
+        new_menu = QtWidgets.QMenu("Launch", self)
+        file_menu.addMenu(new_menu)
+        new_video_action = QtWidgets.QAction("New Video", self)
+        new_menu.addAction(new_video_action)
+        new_video_action.triggered.connect(on_new_video)
+
+        # Create actions to load a new media file and to close the app
+        open_action = QtWidgets.QAction("Load Video", self)
+        close_action = QtWidgets.QAction("Close App", self)
+        file_menu.addAction(open_action)
+        file_menu.addAction(close_action)
+        open_action.triggered.connect(self.open_file)
+        close_action.triggered.connect(sys.exit)
+
+    def play_pause(self):
+        """Toggle play/pause status
+        """
+        if self.mediaplayer.is_playing():
+            signal = 'p'
+            self.mediaplayer.pause()
+            self.playbutton.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay))
+            self.is_paused = True
+            self.timer.stop()
+        else:
+            if self.mediaplayer.play() == -1:
+                self.open_file()
+                return
+
+            signal = 'P'
+            self.mediaplayer.play()
+            self.playbutton.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPause))
+            self.timer.start()
+            self.is_paused = False
+
+        # Reset the queue & send the appropriate signal, i.e., play/pause
+        self.data_queue.queue.clear()
+        self.data_queue.put('d')
+        self.data_queue.put(signal)
+
+    def stop(self):
+        """Stop player
+        """
+        self.mediaplayer.stop()
+        self.playbutton.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay))
+
+        # Reset the time label back to 00:00:00
+        time = QtCore.QTime(0, 0, 0, 0)
+        self.timelabel.setText(time.toString())
+
+        # Reset the queue
+        self.data_queue.queue.clear()
+        self.data_queue.put('d')
+        self.data_queue.put('S')
+
+        # Reset the media position slider
+        self.positionslider.setValue(0)
+
+        self.timer.stop()
+
+    def on_next_frame(self):
+        """Go forward one frame.
+
+            The Python VLC binding next_frame function causes a:
+
+            "direct3d11 vout display error: SetThumbNailClip failed"
+
+            error when next_frame is called while the video is playing,
+            so we are using our own fucntion to get the next frame.
+        """
+        # self.mediaplayer.next_frame()
+        next_frame_time = self.mediaplayer.get_time() + self.mspf()
+
+        # Reset the queue & put the next frame's time into the queue
+        self.data_queue.queue.clear()
+        self.data_queue.put('d')
+        self.data_queue.put(next_frame_time)
+        self.update_time_label()
+        self.mediaplayer.set_time(next_frame_time)
+
+    def on_previous_frame(self):
+        """Go backward one frame"""
+        next_frame_time = self.mediaplayer.get_time() - self.mspf()
+
+        # Reset the queue & put the next frame's time into the queue
+        self.data_queue.queue.clear()
+        self.data_queue.put('d')
+        self.data_queue.put(next_frame_time)
+        self.update_time_label()
+        self.mediaplayer.set_time(next_frame_time)
+
+    def mspf(self):
+        """Milliseconds per frame"""
+        return int(1000 // (self.mediaplayer.get_fps() or 25))
+
+    def incr_mov_play_rate(self):
+        """Increase the movie play rate by a factor of 2."""
+        if self.mediaplayer.get_rate() >= 64:
+            return
+
+        rate = self.mediaplayer.get_rate() * 2
+        result = self.mediaplayer.set_rate(rate)
+        if result == 0:
+            self.data_queue.queue.clear()
+            self.data_queue.put('d')
+            self.data_queue.put('>')
+            self.update_pb_rate_label()
+
+    def decr_mov_play_rate(self):
+        """Decrease the movie play rate by a factor of 2."""
+        if self.mediaplayer.get_rate() <= 0.125:
+            return
+
+        rate = self.mediaplayer.get_rate() * 0.5
+        result = self.mediaplayer.set_rate(rate)
+        if result == 0:
+            self.data_queue.queue.clear()
+            self.data_queue.put('d')
+            self.data_queue.put('<')
+            self.update_pb_rate_label()
+
+    def open_file(self):
+        """Open a media file in a MediaPlayer
+        """
+        dialog_txt = "Choose Media File"
+        filename = QtWidgets.QFileDialog.getOpenFileName(self, dialog_txt, os.path.expanduser('~'))
+        if not filename[0]:
+            return
+
+        # getOpenFileName returns a tuple, so use only the actual file name
+        self.media = self.instance.media_new(filename[0])
+
+        # Put the media in the media player
+        self.mediaplayer.set_media(self.media)
+
+        # Parse the metadata of the file
+        self.media.parse()
+
+        # Set the title of the track as window title
+        self.setWindowTitle("Main Media Player: {}".format(self.media.get_meta(0)))
+
+        # The media player has to be 'connected' to the QFrame (otherwise the
+        # video would be displayed in it's own window). This is platform
+        # specific, so we must give the ID of the QFrame (or similar object) to
+        # vlc. Different platforms have different functions for this.
+        if platform.system() == "Linux":  # for Linux using the X Server
+            self.mediaplayer.set_xwindow(int(self.videoframe.winId()))
+        elif platform.system() == "Windows":  # for Windows
+            self.mediaplayer.set_hwnd(int(self.videoframe.winId()))
+        elif platform.system() == "Darwin":  # for MacOS
+            self.mediaplayer.set_nsobject(int(self.videoframe.winId()))
+
+        self.play_pause()
+
+    def set_position(self):
+        """Set the movie position according to the position slider.
+
+        The vlc MediaPlayer needs a float value between 0 and 1, Qt uses
+        integer variables, so you need a factor; the higher the factor,
+        the more precise are the results (1000 should suffice).
+        """
+        # Set the media position to where the slider was dragged
+        self.timer.stop()
+        pos = self.positionslider.value()
+
+        if pos >= 0:
+            self.data_queue.queue.clear()
+            self.data_queue.put('d')
+            current_time = self.mediaplayer.get_time()
+
+            # If the player is stopped, do not attempt to send a -1!!!
+            if current_time == -1:
+                self.timer.start()
+                return
+            self.data_queue.put(current_time)
+
+        self.mediaplayer.set_position(pos * .001)
+        self.timer.start()
+
+    def update_ui(self):
+        """Updates the user interface"""
+
+        # Set the slider's position to its corresponding media position
+        # Note that the setValue function only takes values of type int,
+        # so we must first convert the corresponding media position.
+        media_pos = int(self.mediaplayer.get_position() * 1000)
+        self.positionslider.setValue(media_pos)
+
+        if media_pos >= 0 and self.mediaplayer.is_playing():
+            current_time = self.mediaplayer.get_time()
+            self.data_queue.put(current_time)
+        else:
+            self.data_queue.queue.clear()
+
+        # No need to call this function if nothing is played
+        if not self.mediaplayer.is_playing():
+            self.timer.stop()
+
+            # After the video finished, the play button stills shows "Pause",
+            # which is not the desired behavior of a media player.
+            # This fixes that "bug".
+            if not self.is_paused:
+                self.stop()
+
+    def update_time_label(self):
+        mtime = QtCore.QTime(0, 0, 0, 0)
+        self.time = mtime.addMSecs(self.mediaplayer.get_time())
+        self.timelabel.setText(self.time.toString())
+
+    def update_pb_rate_label(self):
+        self.pb_rate_label.setText("Playback rate: {}x".format(str(self.mediaplayer.get_rate())))
+
+
+def on_new_video():
+    """Launches a new PyQt5-based "mini" media player
+    """
+    if platform.system() == "Windows":
+        subprocess.Popen(["python", "mini_player.py"], shell=True)
+    else:
+        subprocess.Popen(["python", "mini_player.py"])
+
+
+def main():
+    """Entry point for our simple vlc player
+    """
+    app = QtWidgets.QApplication(sys.argv)
+    player = Player()
+
+    _ = Server("localhost", 10000, player.data_queue)
+
+    player.show()
+    player.resize(640, 480)
+    sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/video_sync/mini_player.py b/examples/video_sync/mini_player.py
new file mode 100644 (file)
index 0000000..a65719d
--- /dev/null
@@ -0,0 +1,169 @@
+#
+# PyQt5-based video-sync example for VLC Python bindings
+# Copyright (C) 2009-2010 the VideoLAN team
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+#
+"""
+This module contains a bare-bones VLC player class to play videos.
+
+Author: Saveliy Yusufov, Columbia University, sy2685@columbia.edu
+Date: 25 January 2019
+"""
+
+import os
+import sys
+import queue
+import platform
+
+from PyQt5 import QtWidgets, QtGui, QtCore
+import vlc
+from network import Client
+
+
+class MiniPlayer(QtWidgets.QMainWindow):
+    """Stripped-down PyQt5-based media player class to sync with "master" video.
+    """
+
+    def __init__(self, data_queue, master=None):
+        QtWidgets.QMainWindow.__init__(self, master)
+        self.setWindowTitle("Mini Player")
+        self.statusbar = self.statusBar()
+        self.statusbar.showMessage("Ready")
+
+        # Create a basic vlc instance
+        self.instance = vlc.Instance()
+
+        self.media = None
+
+        # Create an empty vlc media player
+        self.mediaplayer = self.instance.media_player_new()
+
+        self.init_ui()
+        self.open_file()
+
+        self.timer = QtCore.QTimer(self)
+        self.timer.setInterval(10)
+        self.timer.timeout.connect(self.update_ui)
+
+        self.data_queue = data_queue
+        self.timer.start()
+
+    def init_ui(self):
+        """Set up the user interface
+        """
+        self.widget = QtWidgets.QWidget(self)
+        self.setCentralWidget(self.widget)
+
+        # In this widget, the video will be drawn
+        if platform.system() == "Darwin":  # for MacOS
+            self.videoframe = QtWidgets.QMacCocoaViewContainer(0)
+        else:
+            self.videoframe = QtWidgets.QFrame()
+
+        self.palette = self.videoframe.palette()
+        self.palette.setColor(QtGui.QPalette.Window, QtGui.QColor(0, 0, 0))
+        self.videoframe.setPalette(self.palette)
+        self.videoframe.setAutoFillBackground(True)
+
+        self.vboxlayout = QtWidgets.QVBoxLayout()
+        self.vboxlayout.addWidget(self.videoframe)
+        self.widget.setLayout(self.vboxlayout)
+
+    def open_file(self):
+        """Open a media file in a MediaPlayer
+        """
+        dialog_txt = "Choose Media File"
+        filename = QtWidgets.QFileDialog.getOpenFileName(self, dialog_txt, os.path.expanduser('~'))
+        if not filename[0]:
+            return
+
+        # getOpenFileName returns a tuple, so use only the actual file name
+        self.media = self.instance.media_new(filename[0])
+
+        # Put the media in the media player
+        self.mediaplayer.set_media(self.media)
+
+        # Parse the metadata of the file
+        self.media.parse()
+
+        # Set the title of the track as the window title
+        self.setWindowTitle("{}".format(self.media.get_meta(0)))
+
+        # The media player has to be 'connected' to the QFrame (otherwise the
+        # video would be displayed in it's own window). This is platform
+        # specific, so we must give the ID of the QFrame (or similar object) to
+        # vlc. Different platforms have different functions for this
+        if platform.system() == "Linux":  # for Linux using the X Server
+            self.mediaplayer.set_xwindow(int(self.videoframe.winId()))
+        elif platform.system() == "Windows":  # for Windows
+            self.mediaplayer.set_hwnd(int(self.videoframe.winId()))
+        elif platform.system() == "Darwin":  # for MacOS
+            self.mediaplayer.set_nsobject(int(self.videoframe.winId()))
+
+        # Start playing the video as soon as it loads
+        self.mediaplayer.play()
+
+    def update_ui(self):
+        self.update_statusbar()
+
+        try:
+            val = self.data_queue.get(block=False)
+        except queue.Empty:
+            return
+
+        if val == '<':
+            self.mediaplayer.set_rate(self.mediaplayer.get_rate() * 0.5)
+            return
+        if val == '>':
+            self.mediaplayer.set_rate(self.mediaplayer.get_rate() * 2)
+            return
+        if val == 'P':
+            self.mediaplayer.play()
+            return
+        if val == 'p':
+            self.mediaplayer.pause()
+            return
+        if val == 'S':
+            self.mediaplayer.stop()
+            return
+
+        val = int(val)
+        if val != self.mediaplayer.get_time():
+            self.mediaplayer.set_time(val)
+
+    def update_statusbar(self):
+        mtime = QtCore.QTime(0, 0, 0, 0)
+        time = mtime.addMSecs(self.mediaplayer.get_time())
+        self.statusbar.showMessage(time.toString())
+
+
+def main():
+    """Entry point for our simple vlc player
+    """
+    app = QtWidgets.QApplication(sys.argv)
+
+    data_queue = queue.Queue()
+
+    player = MiniPlayer(data_queue)
+    player.show()
+    player.resize(480, 480)
+
+    _ = Client("localhost", 10000, data_queue)
+    sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/video_sync/network.py b/examples/video_sync/network.py
new file mode 100644 (file)
index 0000000..d657764
--- /dev/null
@@ -0,0 +1,175 @@
+#
+# PyQt5-based video-sync example for VLC Python bindings
+# Copyright (C) 2009-2010 the VideoLAN team
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+#
+"""
+The client/server classes that keep multiple VLC python bindings players
+synchronized.
+
+Author: Saveliy Yusufov, Columbia University, sy2685@columbia.edu
+Date: 25 January 2019
+"""
+
+import os
+import platform
+import socket
+import threading
+import sys
+import logging
+from concurrent import futures
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+logger.setLevel(logging.DEBUG)
+
+# create console handler and set level to debug
+ch = logging.StreamHandler()
+ch.setLevel(logging.DEBUG)
+
+# create formatter
+formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+
+# add formatter to ch
+ch.setFormatter(formatter)
+
+# add ch to logger
+logger.addHandler(ch)
+
+
+class Server:
+    """Data sender server"""
+
+    def __init__(self, host, port, data_queue):
+
+        if platform.system() == "Windows":
+
+            # Create a TCP/IP socket
+            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+            # Bind the socket to the port
+            logger.info("Server started on %s port %s", host, port)
+            self.sock.bind((host, port))
+
+        else:
+            server_address = "./uds_socket"
+
+            # Make sure the socket does not already exist
+            try:
+                os.unlink(server_address)
+            except OSError:
+                if os.path.exists(server_address):
+                    raise
+
+            # Create a UDS socket
+            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+
+            # Bind the socket to the address
+            logger.info("Server starting up on %s", server_address)
+            self.sock.bind(server_address)
+
+        # Listen for incoming connections
+        self.sock.listen(5)
+
+        self.clients = set()
+        self.data_queue = data_queue
+        listener_thread = threading.Thread(target=self.listen_for_clients, args=())
+        listener_thread.daemon = True
+        listener_thread.start()
+
+    def listen_for_clients(self):
+        logger.info("Now listening for clients")
+        t = threading.Thread(target=self.data_sender, args=())
+        t.daemon = True
+        t.start()
+
+        while True:
+            client, _ = self.sock.accept()
+            logger.info("Accepted Connection from: %s", client)
+            self.clients.add(client)
+
+    def data_sender(self):
+        while True:
+            data = "{},".format(self.data_queue.get())
+
+            with futures.ThreadPoolExecutor(max_workers=5) as ex:
+                for client in self.clients:
+                    ex.submit(self.sendall, client, data.encode())
+
+    def sendall(self, client, data):
+        """Wraps socket module's `sendall` function"""
+        try:
+            client.sendall(data)
+        except socket.error:
+            logger.exception("Connection to client: %s was broken!", client)
+            client.close()
+            self.clients.remove(client)
+
+
+class Client:
+    """Data receiver client"""
+
+    def __init__(self, address, port, data_queue):
+        self.data_queue = data_queue
+
+        if platform.system() == "Windows":
+
+            # Create a TCP/IP socket
+            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+            # Connect the socket to the port where the server is listening
+            logger.info("Connecting to %s port %s", address, port)
+            self.sock.connect((address, port))
+        else:
+
+            # Create a UDS socket
+            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+
+            # Connect the socket to the port where the server is listening
+            server_address = "./uds_socket"
+            logger.info("New client connecting to %s", server_address)
+
+            try:
+                self.sock.connect(server_address)
+            except socket.error:
+                logger.exception()
+                sys.exit(1)
+
+        thread = threading.Thread(target=self.data_receiver, args=())
+        thread.daemon = True
+        thread.start()
+
+    def data_receiver(self):
+        """Handles receiving, parsing, and queueing data"""
+        logger.info("New data receiver thread started.")
+
+        try:
+            while True:
+                data = self.sock.recv(4096)
+                if data:
+                    data = data.decode()
+
+                    for char in data.split(','):
+                        if char:
+                            if char == 'd':
+                                self.data_queue.queue.clear()
+                            else:
+                                self.data_queue.put(char)
+        except:
+            logger.exception("Closing socket: %s", self.sock)
+            self.sock.close()
+            return