├── videos
└── .gitignore
├── .gitchangelog.rc
├── src
└── videowall
│ ├── player
│ ├── player_exceptions.py
│ ├── assets
│ │ └── error.jpg
│ ├── __init__.py
│ ├── player_platforms.py
│ ├── player_server.py
│ └── player_client.py
│ ├── networking
│ ├── networking_exceptions.py
│ ├── __init__.py
│ ├── message_definition.py
│ ├── networking_server.py
│ └── networking_client.py
│ ├── media_manager
│ ├── media_manager_exceptions.py
│ ├── __init__.py
│ ├── media_manager_client.py
│ ├── media_manager.py
│ └── media_manager_server.py
│ ├── gi_version.py
│ ├── __init__.py
│ ├── util.py
│ ├── client.py
│ ├── server.py
│ └── web_server.py
├── web
├── babel.config.js
├── dist
│ ├── favicon.ico
│ ├── img
│ │ └── bigbunny.6802aeac.jpg
│ ├── index.html
│ ├── css
│ │ └── app.f564cdc6.css
│ └── js
│ │ ├── app.7e10ce6b.js
│ │ └── app.7e10ce6b.js.map
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ ├── logo.png
│ │ └── bigbunny.jpg
│ ├── App.vue
│ ├── filters.js
│ ├── components
│ │ ├── NavBar.vue
│ │ ├── Player.vue
│ │ ├── PlayerBar.vue
│ │ ├── PlaylistModal.vue
│ │ └── ScreenGrid.vue
│ └── main.js
├── vue.config.js
├── .gitignore
├── README.md
└── package.json
├── .travis.yml
├── doc
└── example_2monitor.gif
├── requirements.txt
├── setup.bash
├── cfg
└── rpi
│ ├── boot
│ └── config.txt
│ └── etc
│ ├── systemd
│ └── system
│ │ └── videowall.service
│ └── rc.local
├── CHANGELOG.md
├── test
├── test_media_manager_server
├── test_networking_client
└── test_networking_server
├── install_ubuntu_x86.bash
├── scripts
├── player_server
├── client
├── player_client
└── web_server
├── .gitignore
├── install_raspberry_pi_stretch_lite_autostart.bash
└── README.md
/videos/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/.gitchangelog.rc:
--------------------------------------------------------------------------------
1 | output_engine = mustache("markdown")
2 |
--------------------------------------------------------------------------------
/src/videowall/player/player_exceptions.py:
--------------------------------------------------------------------------------
1 | class PlayerException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/web/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/web/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/web/dist/favicon.ico
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: generic
3 | script:
4 | - ./install_ubuntu_x86.bash
5 |
--------------------------------------------------------------------------------
/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/web/public/favicon.ico
--------------------------------------------------------------------------------
/doc/example_2monitor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/doc/example_2monitor.gif
--------------------------------------------------------------------------------
/web/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/web/src/assets/logo.png
--------------------------------------------------------------------------------
/src/videowall/networking/networking_exceptions.py:
--------------------------------------------------------------------------------
1 | class NetworkingException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/web/src/assets/bigbunny.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/web/src/assets/bigbunny.jpg
--------------------------------------------------------------------------------
/src/videowall/media_manager/media_manager_exceptions.py:
--------------------------------------------------------------------------------
1 | class MediaManagerException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/web/dist/img/bigbunny.6802aeac.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/web/dist/img/bigbunny.6802aeac.jpg
--------------------------------------------------------------------------------
/src/videowall/player/assets/error.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reinzor/videowall/HEAD/src/videowall/player/assets/error.jpg
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tqdm==4.26.0
2 | pymediainfo==2.3.0
3 | colorlog==3.1.4
4 | tornado==5.1.1
5 | gitchangelog==3.0.4
6 | pystache==0.5.4
7 |
--------------------------------------------------------------------------------
/src/videowall/networking/__init__.py:
--------------------------------------------------------------------------------
1 | from .networking_client import NetworkingClient
2 | from .networking_server import NetworkingServer
3 |
--------------------------------------------------------------------------------
/src/videowall/media_manager/__init__.py:
--------------------------------------------------------------------------------
1 | from .media_manager_server import MediaManagerServer
2 | from .media_manager_client import MediaManagerClient
3 |
--------------------------------------------------------------------------------
/setup.bash:
--------------------------------------------------------------------------------
1 | export PYTHONPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"/src
2 | export GST_DEBUG=1
3 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/arm-linux-gnueabihf/gstreamer-1.0
4 |
--------------------------------------------------------------------------------
/src/videowall/player/__init__.py:
--------------------------------------------------------------------------------
1 | from .player_client import PlayerClient
2 | from .player_server import PlayerServer
3 | from .player_platforms import PlayerPlatform, get_player_platform_strings, player_platform_from_string
4 |
--------------------------------------------------------------------------------
/cfg/rpi/boot/config.txt:
--------------------------------------------------------------------------------
1 | hdmi_safe=1 # maximum hdmi compatibility and enforce 640x480@60Hz (our playback is max 720p and we want multiple displays so this seems ok)
2 | gpu_mem = 128MB # Not sure whether this really affects anything ...
3 |
--------------------------------------------------------------------------------
/src/videowall/gi_version.py:
--------------------------------------------------------------------------------
1 | import gi
2 |
3 | gi.require_version('GLib', '2.0')
4 | gi.require_version('Gst', '1.0')
5 | gi.require_version('GstNet', '1.0')
6 |
7 | from gi.repository import GLib, Gst, GstNet, GObject
8 |
9 | _ = Gst, GstNet, GObject, GLib
10 |
--------------------------------------------------------------------------------
/web/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | proxy: {
4 | '^/ws': {
5 | ws: true,
6 | target: 'http://localhost:3000'
7 | },
8 | '^/upload': {
9 | target: 'http://localhost:3000'
10 | },
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/cfg/rpi/etc/systemd/system/videowall.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Videowall
3 | After=getty.target
4 |
5 | [Install]
6 | WantedBy=multi-user.target
7 |
8 | [Service]
9 | User=pi
10 | ExecStart=/bin/bash -c "source /home/pi/videowall/setup.bash && sleep 5 && /home/pi/videowall/scripts/client rpi eth0"
11 |
12 |
--------------------------------------------------------------------------------
/src/videowall/media_manager/media_manager_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from .media_manager import MediaManager
4 |
5 | logger = logging.getLogger(__name__)
6 |
7 |
8 | class MediaManagerClient(MediaManager):
9 | def __init__(self, base_path):
10 | super(MediaManagerClient, self).__init__(base_path)
11 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw*
22 |
--------------------------------------------------------------------------------
/web/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
20 |
21 |
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.1 (2019-02-15)
4 |
5 | ### New
6 |
7 | * Added time and ip overlay for testing purposes [Rein Appeldoorn]
8 |
9 | * Web server for distributing video files and starting/stopping videos [Rein Appeldoorn]
10 |
11 | * Videocrop configs per client [Rein Appeldoorn]
12 |
13 | * Synchronized playback on multiple clients with central time server [Rein Appeldoorn]
14 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # videowall
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Run your tests
19 | ```
20 | npm run test
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | npm run lint
26 | ```
27 |
--------------------------------------------------------------------------------
/web/src/filters.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue"
2 |
3 | Vue.filter("hoursMinutesSeconds", function(value) {
4 | let hours = parseInt(Math.floor(value / 3600));
5 | let minutes = parseInt(Math.floor((value - (hours * 3600)) / 60));
6 | let seconds= parseInt((value - ((hours * 3600) + (minutes * 60))) % 60);
7 |
8 | let dHours = (hours > 9 ? hours : '0' + hours);
9 | let dMins = (minutes > 9 ? minutes : '0' + minutes);
10 | let dSecs = (seconds > 9 ? seconds : '0' + seconds);
11 |
12 | return dHours + ":" + dMins + ":" + dSecs;
13 | })
--------------------------------------------------------------------------------
/src/videowall/__init__.py:
--------------------------------------------------------------------------------
1 | import logging.config
2 |
3 | logging.config.dictConfig({
4 | 'version': 1,
5 | 'disable_existing_loggers': False,
6 | 'formatters': {
7 | 'colored': {
8 | '()': 'colorlog.ColoredFormatter',
9 | 'format': "%(log_color)s[%(levelname)s] %(name)s: %(message)s",
10 | }
11 | },
12 | 'handlers': {
13 | 'stream': {
14 | 'class': 'logging.StreamHandler',
15 | 'formatter': 'colored',
16 | },
17 | },
18 | 'root': {
19 | 'handlers': ['stream'],
20 | 'level': 'INFO',
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/test/test_media_manager_server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import logging
3 | from argparse import ArgumentParser
4 |
5 | from videowall.media_manager import MediaManagerServer
6 |
7 |
8 | if __name__ == '__main__':
9 | parser = ArgumentParser()
10 |
11 | parser.add_argument('remotes', nargs="+")
12 |
13 | parser.add_argument('--base_path', default='~/Videos')
14 |
15 | args = parser.parse_args()
16 |
17 | logging.getLogger().setLevel(logging.DEBUG)
18 |
19 | manager = MediaManagerServer(args.base_path)
20 | logging.info("Filenames: %s", manager.get_filenames())
21 | manager.sync(args.remotes)
22 |
--------------------------------------------------------------------------------
/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | videowall
9 |
10 |
11 |
12 | We're sorry but videowall doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/web/src/components/NavBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Video wall
6 |
7 |
8 |
9 | Player
10 |
11 |
12 |
13 |
14 |
15 |
16 |
25 |
27 |
--------------------------------------------------------------------------------
/cfg/rpi/etc/rc.local:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | if [ ! -f /etc/network/mac ]; then
4 | mac=`echo -n 00:60:2F; dd bs=1 count=3 if=/dev/random 2>/dev/null |hexdump -v -e '/1 ":%02X"'`
5 |
6 | echo -e " auto lo\n"\
7 | "iface lo inet loopback\n"\
8 | "\n"\
9 | "allow-hotplug eth0\n"\
10 | "iface eth0 inet dhcp\n"\
11 | " hwaddress ether ${mac}\n" > /etc/network/interfaces
12 |
13 | echo $mac > /etc/network/mac
14 | echo "Changed the MAC address to ${mac} , rebooting ..."
15 |
16 | reboot
17 | fi
18 |
19 | # Print the IP address
20 | _IP=$(hostname -I) || true
21 | if [ "$_IP" ]; then
22 | printf "My IP address is %s\n" "$_IP"
23 | fi
24 |
25 | exit 0
26 |
--------------------------------------------------------------------------------
/web/dist/index.html:
--------------------------------------------------------------------------------
1 | videowall We're sorry but videowall doesn't work properly without JavaScript enabled. Please enable it to continue.
--------------------------------------------------------------------------------
/install_ubuntu_x86.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Install system dependencies
4 | sudo apt-get -y update
5 | sudo apt-get -y install git \
6 | gstreamer1.0-plugins-base-apps \
7 | gstreamer1.0-plugins-good \
8 | gstreamer1.0-plugins-bad \
9 | gstreamer1.0-libav ubuntu-restricted-extras \
10 | libmediainfo-dev \
11 | python-pip
12 |
13 | # Install python dependencies
14 | sudo -H pip install -r requirements.txt
15 |
16 | # Create videos folder and download sample video
17 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
18 | wget https://github.com/reinzor/videowall/releases/download/0/big_buck_bunny_720p_30mb.mp4 -O $SCRIPT_DIR/videos/big_buck_bunny_720p_30mb.mp4
19 |
20 | # Setup paths in bashrc
21 | echo "source $SCRIPT_DIR/setup.bash" >> ~/.bashrc
22 |
--------------------------------------------------------------------------------
/web/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import VueRouter from 'vue-router'
4 | Vue.use(VueRouter)
5 |
6 | import BootstrapVue from 'bootstrap-vue'
7 | Vue.use(BootstrapVue);
8 | import 'bootstrap/dist/css/bootstrap.css'
9 | import 'bootstrap-vue/dist/bootstrap-vue.css'
10 |
11 | import Icon from 'vue-awesome/components/Icon'
12 | import 'vue-awesome/icons'
13 | Vue.component('v-icon', Icon)
14 |
15 | import VueNativeSock from 'vue-native-websocket'
16 | Vue.use(VueNativeSock, "ws://" + location.host + "/ws", {
17 | format: 'json',
18 | reconnection: true
19 | })
20 |
21 | import "./filters"
22 |
23 | import App from './App.vue'
24 | import Player from './components/Player.vue'
25 |
26 | Vue.config.productionTip = false
27 |
28 | const routes = [
29 | { path: '/', component: Player }
30 | ]
31 |
32 | const router = new VueRouter({
33 | routes
34 | })
35 |
36 | new Vue({
37 | router,
38 | render: h => h(App)
39 | }).$mount('#app')
40 |
--------------------------------------------------------------------------------
/src/videowall/player/player_platforms.py:
--------------------------------------------------------------------------------
1 | from .player_exceptions import PlayerException
2 |
3 |
4 | class PlayerPlatform(object):
5 | pass
6 |
7 |
8 | class PlayerPlatformPC(PlayerPlatform):
9 | pass
10 |
11 |
12 | class PlayerPlatformRaspberryPi(PlayerPlatform):
13 | pass
14 |
15 |
16 | _string_player_platform_map = {
17 | "pc": PlayerPlatformPC,
18 | "rpi": PlayerPlatformRaspberryPi
19 | }
20 |
21 | _platform_player_string_map = {
22 | PlayerPlatformPC: "pc",
23 | PlayerPlatformRaspberryPi: "rpi"
24 | }
25 |
26 |
27 | def player_platform_from_string(string):
28 | try:
29 | platform = _string_player_platform_map[string]
30 | except KeyError as e:
31 | raise PlayerException(e)
32 | else:
33 | return platform
34 |
35 |
36 | def string_from_player_platform(player_platform):
37 | try:
38 | string = _platform_player_string_map[player_platform]
39 | except KeyError as e:
40 | raise PlayerException(e)
41 | else:
42 | return string
43 |
44 |
45 | def get_player_platform_strings():
46 | return _string_player_platform_map.keys()
47 |
48 |
49 | def get_player_platforms():
50 | return _string_player_platform_map.values()
51 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videowall",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "bootstrap-vue": "^2.0.0-rc.11",
12 | "debounce": "^1.2.0",
13 | "vue": "^2.5.17",
14 | "vue-awesome": "^3.1.3",
15 | "vue-grid-layout": "^2.2.0",
16 | "vue-native-websocket": "^2.0.12",
17 | "vue-router": "^3.0.1",
18 | "vue2-dropzone": "^3.5.2",
19 | "vuedraggable": "^2.16.0"
20 | },
21 | "devDependencies": {
22 | "@vue/cli-plugin-babel": "^3.0.4",
23 | "@vue/cli-plugin-eslint": "^3.0.4",
24 | "@vue/cli-service": "^3.0.4",
25 | "vue-template-compiler": "^2.5.17"
26 | },
27 | "eslintConfig": {
28 | "root": true,
29 | "env": {
30 | "node": true
31 | },
32 | "extends": [
33 | "plugin:vue/essential",
34 | "eslint:recommended"
35 | ],
36 | "rules": {},
37 | "parserOptions": {
38 | "parser": "babel-eslint"
39 | }
40 | },
41 | "postcss": {
42 | "plugins": {
43 | "autoprefixer": {}
44 | }
45 | },
46 | "browserslist": [
47 | "> 1%",
48 | "last 2 versions",
49 | "not ie <= 8"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/test/test_networking_client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import logging
4 | from argparse import ArgumentParser
5 | from videowall.networking import NetworkingClient
6 | from videowall.util import validate_positive_int_argument
7 |
8 | if __name__ == '__main__':
9 | parser = ArgumentParser()
10 |
11 | parser.add_argument('--ip', default='127.0.0.1')
12 | parser.add_argument('--server_broadcast_port', type=validate_positive_int_argument, default=2000)
13 | parser.add_argument('--server_play_broadcast_port', type=validate_positive_int_argument, default=2001)
14 | parser.add_argument('--client_broadcast_port', type=validate_positive_int_argument, default=3000)
15 | parser.add_argument('--client_broadcast_interval', type=validate_positive_int_argument, default=1)
16 | parser.add_argument('--buffer_size', type=validate_positive_int_argument, default=1024)
17 |
18 | args = parser.parse_args()
19 |
20 | logging.getLogger().setLevel(logging.DEBUG)
21 |
22 | client = NetworkingClient(args.ip, args.server_broadcast_port, args.client_broadcast_port, args.buffer_size)
23 |
24 | client.receive_server_broadcast()
25 |
26 | try:
27 | while True:
28 | client.receive_server_play_broadcast()
29 | except KeyboardInterrupt:
30 | client.close()
31 |
--------------------------------------------------------------------------------
/web/src/components/Player.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/src/videowall/media_manager/media_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | from .media_manager_exceptions import MediaManagerException
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 |
9 | class MediaManager(object):
10 | def __init__(self, base_path, extensions=["mp4"]):
11 | real_base_path = os.path.realpath(os.path.expanduser(base_path))
12 |
13 | if not os.path.isdir(real_base_path):
14 | raise MediaManagerException("Specified base path %s does not exist", base_path)
15 |
16 | self._base_path = real_base_path
17 | self._extensions = extensions
18 |
19 | def _is_valid_filename(self, filename):
20 | return "." in filename and filename.split(".")[-1] in self._extensions and " " not in filename
21 |
22 | def get_media_path(self):
23 | return self._base_path
24 |
25 | def get_full_path(self, filename):
26 | return os.path.join(self._base_path, filename)
27 |
28 | def get_filenames(self):
29 | return [filename for filename in os.listdir(self._base_path) if self._is_valid_filename(filename)]
30 |
31 | def get_extensions(self):
32 | return self._extensions
33 |
34 | def delete_media(self, filename):
35 | path = self.get_full_path(filename)
36 | if os.path.exists(path):
37 | os.remove(path)
38 | else:
39 | raise MediaManagerException("Media filename {} does not exist!".format(filename))
40 |
--------------------------------------------------------------------------------
/scripts/player_server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import logging
4 | import time
5 |
6 | from tqdm import tqdm
7 | from videowall.player import PlayerServer
8 | from videowall.util import validate_positive_int_argument, validate_ip
9 |
10 | if __name__ == '__main__':
11 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
12 | parser.add_argument('filename')
13 | parser.add_argument('--base_time_offset', type=validate_positive_int_argument, default=10)
14 | parser.add_argument('--spin_rate', type=validate_positive_int_argument, default=100)
15 | parser.add_argument('--ip', type=validate_ip, default='127.0.0.1')
16 | parser.add_argument('--port', type=validate_positive_int_argument, default=11111)
17 | parser.add_argument('--verbose', action='store_true')
18 |
19 | args = parser.parse_args()
20 |
21 | if args.verbose:
22 | logging.getLogger().setLevel(logging.DEBUG)
23 |
24 | player = PlayerServer(args.ip, args.port)
25 |
26 | player.play(args.filename, args.base_time_offset)
27 |
28 | logging.info("Playing file %s", args.filename)
29 |
30 | with tqdm(total=player.get_duration(), bar_format='Playing: {l_bar}{bar} | {n_fmt}/{total_fmt}') as progress_bar:
31 | while player.is_playing():
32 | progress_bar.update(player.get_position() - progress_bar.n)
33 | try:
34 | time.sleep(1. / args.spin_rate)
35 | except KeyboardInterrupt:
36 | break
37 |
--------------------------------------------------------------------------------
/web/dist/css/app.f564cdc6.css:
--------------------------------------------------------------------------------
1 | #playlistModal .list-group-item{padding:10px}.playlistVideoButtons{margin:0!important;padding:0!important}.playlistVideoButtons button{padding:0 5px!important}#playlistModal #dropzone{padding:0;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}#playlistModal .dropzone{margin-top:10px;min-height:0}#playlistModal .dz-details{background-color:#17a2b8;border-radius:10px}#playlistModal .dz-image{height:100px}#playlistModal .dz-progress{opacity:.8;margin-top:-2px;width:80px;height:10px}#playlistModal .modal-dialog{max-width:80%}#playerBar{background-color:rgba(0,0,0,.03);border-left:1px solid rgba(0,0,0,.125);border-right:1px solid rgba(0,0,0,.125);border-bottom:1px solid rgba(0,0,0,.125);width:1280px}#playerBar .btn-group{margin:5px}#playerProgress{display:inline-block;width:882px}#playerProgress .progress{height:38px;margin:5px;text-align:center}#playerProgress .progress-bar{padding:15px}#playerDuration{width:100px}#playerDuration,.playerTime{display:inline-block;margin:5px}.playerTime{width:90px}#screenGrid>.card-body{padding:0}#screenGrid>.card-header{padding:5px}#screenGridLayout{background-image:url(../img/bigbunny.6802aeac.jpg);background-repeat:repeat-y}.screen .card{background:none;height:100%;border:1px solid hsla(0,0%,100%,.8)}.screen .card-header{background-color:hsla(0,0%,100%,.5);font-size:10px;color:grey;padding:0}.screen .card-header div{padding:0!important;margin:0!important}.screen .card-header button{padding:0 5px}.screen .card-header span{margin:3px;line-height:23px}.screen .card-footer{background-color:hsla(0,0%,100%,.5);font-size:10px;color:grey;padding:4px 3px}.clientTable{font-size:10px}#videoWall{padding-top:20px}
--------------------------------------------------------------------------------
/scripts/client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import logging
4 | import os
5 | from argparse import ArgumentParser
6 |
7 | from videowall.client import Client
8 | from videowall.player import get_player_platform_strings, player_platform_from_string
9 | from videowall.util import ip_from_ifname, validate_positive_int_argument, validate_positive_float_argument, get_ifnames
10 |
11 | if __name__ == '__main__':
12 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
13 | parser.add_argument('player_platform', choices=get_player_platform_strings())
14 | parser.add_argument('ifname', choices=get_ifnames())
15 | parser.add_argument('--media_path', default=os.path.join(os.path.dirname(os.path.realpath(__file__)), "../videos"))
16 | parser.add_argument('--verbose', action='store_true')
17 | parser.add_argument('--server_broadcast_port', type=validate_positive_int_argument, default=2000)
18 | parser.add_argument('--server_play_broadcast_port', type=validate_positive_int_argument, default=2001)
19 | parser.add_argument('--client_broadcast_port', type=validate_positive_int_argument, default=3000)
20 | parser.add_argument('--client_broadcast_interval', type=validate_positive_float_argument, default=1.)
21 |
22 | args = parser.parse_args()
23 |
24 | if args.verbose:
25 | logging.getLogger().setLevel(logging.DEBUG)
26 |
27 | client = Client(player_platform_from_string(args.player_platform), args.media_path, ip_from_ifname(args.ifname),
28 | args.server_broadcast_port, args.server_play_broadcast_port, args.client_broadcast_port,
29 | args.client_broadcast_interval)
30 | try:
31 | client.run()
32 | except KeyboardInterrupt:
33 | client.close()
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | .idea
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # pyenv
78 | .python-version
79 |
80 | # celery beat schedule file
81 | celerybeat-schedule
82 |
83 | # SageMath parsed files
84 | *.sage.py
85 |
86 | # Environments
87 | .env
88 | .venv
89 | env/
90 | venv/
91 | ENV/
92 | env.bak/
93 | venv.bak/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | .mypy_cache/
107 |
--------------------------------------------------------------------------------
/src/videowall/media_manager/media_manager_server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import multiprocessing
3 | import os
4 | import subprocess
5 | import time
6 | from functools import partial
7 |
8 | from .media_manager import MediaManager
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def _rsync(local_filenames, remote_path):
14 | cmd = 'rsync -avzP {} {}'.format(" ".join(local_filenames), remote_path)
15 | logger.debug('rsync command: %s', cmd)
16 | t_start = time.time()
17 | with open(os.devnull, 'w') as DEVNULL:
18 | subprocess.call(
19 | cmd.split(' '),
20 | stdout=DEVNULL,
21 | stderr=subprocess.STDOUT
22 | )
23 | logger.debug('rsync to remote path %s took %.3f seconds', remote_path, time.time() - t_start)
24 |
25 |
26 | class MediaManagerServer(MediaManager):
27 | def __init__(self, base_path, num_sync_processes=10):
28 | super(MediaManagerServer, self).__init__(base_path)
29 | self._num_sync_processes = num_sync_processes
30 |
31 | def _sync_many(self, remote_paths):
32 | logger.info('rsync to remote paths %s', remote_paths)
33 |
34 | t_start = time.time()
35 | pool = multiprocessing.Pool(processes=self._num_sync_processes)
36 | pool.map(partial(_rsync, [self.get_full_path(f) for f in self.get_filenames()]), remote_paths)
37 | logger.info('rsync to %d remotes with %d processes took %.3f seconds',
38 | len(remote_paths), self._num_sync_processes, time.time() - t_start)
39 |
40 | def sync(self, remote_path):
41 | if isinstance(remote_path, list):
42 | remote_paths = remote_path
43 | else:
44 | remote_paths = [remote_path]
45 |
46 | self._sync_many(remote_paths)
47 |
48 | def add_file(self, path):
49 | pass
50 |
51 | def remove_file(self, name):
52 | pass
53 |
--------------------------------------------------------------------------------
/test/test_networking_server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import json
3 | import logging
4 | import time
5 | from argparse import ArgumentParser
6 | from videowall.networking import NetworkingServer
7 | from videowall.networking.message_definition import ServerPlayBroadcastMessage, ClientConfig, ServerBroadcastMessage
8 | from videowall.util import validate_positive_int_argument
9 |
10 | if __name__ == '__main__':
11 | parser = ArgumentParser()
12 |
13 | parser.add_argument('--filename', default='file.mp4')
14 | parser.add_argument('--ip', default='127.0.0.1')
15 | parser.add_argument('--clock_port', type=validate_positive_int_argument, default=11111)
16 | parser.add_argument('--server_broadcast_port', type=validate_positive_int_argument, default=2000)
17 | parser.add_argument('--server_play_broadcast_port', type=validate_positive_int_argument, default=2001)
18 | parser.add_argument('--server_broadcast_interval', type=float, default=1.0)
19 | parser.add_argument('--client_broadcast_port', type=validate_positive_int_argument, default=3000)
20 | parser.add_argument('--client_config', type=json.loads, default='{}')
21 | parser.add_argument('--buffer_size', type=validate_positive_int_argument, default=1024)
22 |
23 | args = parser.parse_args()
24 |
25 | logging.getLogger().setLevel(logging.DEBUG)
26 |
27 | logging.info("Client config: %s", args.client_config)
28 |
29 | server = NetworkingServer(args.server_broadcast_port, args.client_broadcast_port, args.buffer_size)
30 | try:
31 | while True:
32 | server.send_broadcast(ServerBroadcastMessage(
33 | clock_ip=args.ip,
34 | clock_port=args.clock_port
35 | ))
36 | server.send_play_broadcast(ServerPlayBroadcastMessage(
37 | filename=args.filename,
38 | base_time_nsecs=int(time.time() / 1e-9),
39 | time_overlay=True,
40 | client_config={ip: ClientConfig(**cfg) for ip, cfg in args.client_config.items()}
41 | ))
42 | time.sleep(args.server_broadcast_interval)
43 | except KeyboardInterrupt:
44 | server.close()
45 |
--------------------------------------------------------------------------------
/scripts/player_client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import logging
4 | import time
5 |
6 | from tqdm import tqdm
7 | from videowall.networking.message_definition import VideocropConfig
8 | from videowall.player import PlayerClient, get_player_platform_strings, player_platform_from_string
9 | from videowall.player.player_exceptions import PlayerException
10 | from videowall.util import validate_positive_int_argument
11 |
12 | if __name__ == '__main__':
13 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
14 | parser.add_argument('platform', choices=get_player_platform_strings())
15 | parser.add_argument('filename')
16 | parser.add_argument('base_time', type=validate_positive_int_argument)
17 | parser.add_argument('--spin_rate', type=validate_positive_int_argument, default=100)
18 | parser.add_argument('--ip', default='127.0.0.1')
19 | parser.add_argument('--port', type=validate_positive_int_argument, default=11111)
20 | parser.add_argument('--verbose', action='store_true')
21 | parser.add_argument('--text_overlay', default='')
22 | parser.add_argument('--time_overlay', action='store_true')
23 | parser.add_argument('--use_local_clock', action='store_true')
24 |
25 | args = parser.parse_args()
26 |
27 | if args.verbose:
28 | logging.getLogger().setLevel(logging.DEBUG)
29 |
30 | player = PlayerClient(player_platform_from_string(args.platform), args.ip, args.port, args.use_local_clock)
31 |
32 | try:
33 | player.play(args.filename, args.base_time, VideocropConfig(200, 20, 0, 0), args.text_overlay, args.time_overlay)
34 | except PlayerException as e:
35 | logging.fatal(e)
36 | else:
37 | logging.info("Playing file %s with base time %d", args.filename, args.base_time)
38 |
39 | with tqdm(total=player.get_duration(),
40 | bar_format='Playing: {l_bar}{bar} | {n_fmt}/{total_fmt}') as progress_bar:
41 | while player.is_playing():
42 | progress_bar.update(player.get_position() - progress_bar.n)
43 | try:
44 | time.sleep(1. / args.spin_rate)
45 | except KeyboardInterrupt:
46 | break
47 |
48 | player.close()
49 |
--------------------------------------------------------------------------------
/web/src/components/PlayerBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
44 |
79 |
--------------------------------------------------------------------------------
/install_raspberry_pi_stretch_lite_autostart.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
3 |
4 | # Install system dependencies
5 | sudo apt-get -y update
6 | sudo apt-get -y install git \
7 | libgstreamer-plugins-base1.0-dev \
8 | gstreamer1.0-plugins-base-apps \
9 | gstreamer1.0-tools \
10 | gstreamer1.0-plugins-good \
11 | gstreamer1.0-plugins-bad \
12 | python-pip
13 |
14 | # Install python dependencies
15 | sudo -H pip install -r requirements.txt
16 |
17 | # Build & Install Gstreamer MMAL
18 | git clone https://github.com/reinzor/gst-mmal /tmp/gst-mmal
19 | cd /tmp/gst-mmal
20 | LDFLAGS='-L/opt/vc/lib' CPPFLAGS='-I/opt/vc/include -I/opt/vc/include/interface/vcos/pthreads -I/opt/vc/include/interface/vmcs_host/linux' ./autogen.sh --disable-gtk-doc
21 | make
22 | sudo make install
23 |
24 | # Build & Install Gstreamer OMX
25 | git clone https://github.com/reinzor/gst-omx /tmp/gst-omx
26 | cd /tmp/gst-omx
27 | git checkout 1.10.4
28 | LDFLAGS='-L/opt/vc/lib' CFLAGS='-I/opt/vc/include -I/opt/vc/include/IL -I/opt/vc/include/interface/vcos/pthreads -I/opt/vc/include/interface/vmcs_host/linux -I/opt/vc/include/IL' CPPFLAGS='-I/opt/vc/include -I/opt/vc/include/IL -I/opt/vc/include/interface/vcos/pthreads -I/opt/vc/include/interface/vmcs_host/linux -I/opt/vc/include/IL' ./autogen.sh --disable-gtk-doc --with-omx-target=rpi
29 | make
30 | sudo make install
31 |
32 | # Create videos folder and download sample video
33 | wget https://github.com/reinzor/videowall/releases/download/0/big_buck_bunny_720p_30mb.mp4 -O $SCRIPT_DIR/videos/big_buck_bunny_720p_30mb.mp4
34 |
35 | # Setup paths in bashrc
36 | echo "source $SCRIPT_DIR/setup.bash" >> ~/.bashrc
37 |
38 | # Copy additional raspberry pi system files
39 | sudo rm /etc/network/mac
40 | sudo cp -r $SCRIPT_DIR/cfg/rpi/* /
41 |
42 | # Enable startup run and disable dhcpcd
43 | sudo systemctl daemon-reload
44 | sudo systemctl enable videowall
45 | sudo systemctl disable dhcpcd
46 |
47 | # Show installation done message
48 | echo "======="
49 | echo ""
50 | echo "Installation complete, please source your ~/.bashrc and type the following command:"
51 | echo ""
52 | echo " gst-launch-1.0 -v filesrc location=$SCRIPT_DIR/videos/big_buck_bunny_720p_30mb.mp4 ! qtdemux ! h264parse ! omxh264dec ! videocrop bottom=10 ! mmalvideosink"
53 | echo ""
54 | echo "======"
55 |
--------------------------------------------------------------------------------
/src/videowall/networking/message_definition.py:
--------------------------------------------------------------------------------
1 | import socket
2 |
3 | from videowall.util import to_dict
4 |
5 | from .networking_exceptions import NetworkingException
6 |
7 |
8 | class Message(object):
9 | def to_dict(self):
10 | return to_dict(self)
11 |
12 | def __repr__(self):
13 | return '{class_name}({params})'.format(
14 | class_name=self.__class__.__name__,
15 | params=', '.join('{}={}'.format(k, v) for k, v in vars(self).items()))
16 |
17 |
18 | class VideocropConfig(Message):
19 | def __init__(self, bottom, left, right, top):
20 | self.bottom = int(bottom)
21 | self.left = int(left)
22 | self.right = int(right)
23 | self.top = int(top)
24 |
25 | @staticmethod
26 | def get_default():
27 | return VideocropConfig(0, 0, 0, 0)
28 |
29 |
30 | class ClientConfig(Message):
31 | def __init__(self, videocrop_config):
32 | self.videocrop_config = VideocropConfig(**videocrop_config)
33 |
34 | @staticmethod
35 | def get_default():
36 | return ClientConfig(VideocropConfig.get_default().to_dict())
37 |
38 |
39 | class ServerPlayBroadcastMessage(Message):
40 | def __init__(self, filename, base_time_nsecs, time_overlay, client_config):
41 | try:
42 | self.filename = filename
43 | self.base_time_nsecs = int(base_time_nsecs)
44 | self.time_overlay = time_overlay
45 |
46 | if not isinstance(client_config, dict):
47 | raise NetworkingException("The client config should be a dictionary")
48 |
49 | self.client_config = {}
50 | for ip, cfg in client_config.items():
51 | if not isinstance(cfg, dict):
52 | raise NetworkingException("Client config entry should be of dictionary")
53 | try:
54 | socket.inet_pton(socket.AF_INET, ip)
55 | except socket.error as e:
56 | raise NetworkingException(e)
57 | self.client_config[ip] = ClientConfig(**cfg)
58 | except Exception as e:
59 | raise NetworkingException(e)
60 |
61 |
62 | class ServerBroadcastMessage(Message):
63 | def __init__(self, clock_ip, clock_port):
64 | try:
65 | self.clock_ip = clock_ip
66 | self.clock_port = int(clock_port)
67 | except Exception as e:
68 | raise NetworkingException(e)
69 |
70 |
71 | class ClientBroadcastMessage(Message):
72 | def __init__(self, username, ip, media_path):
73 | self.username = username
74 | self.ip = ip
75 | self.media_path = media_path
76 |
--------------------------------------------------------------------------------
/scripts/web_server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import argparse
3 | import logging
4 |
5 | from videowall.util import validate_positive_int_argument, validate_positive_float_argument, ip_from_ifname, get_ifnames
6 | from videowall.web_server import WebServer
7 |
8 | if __name__ == "__main__":
9 |
10 | parser = argparse.ArgumentParser(description="Videowall web server",
11 | formatter_class=argparse.ArgumentDefaultsHelpFormatter)
12 | parser.add_argument('ifname', choices=get_ifnames())
13 | parser.add_argument('--media_path', help='Local path where the media files are located', default='~/Videos')
14 | parser.add_argument('--base_time_offset', help='Time between play request and the video playback. This gives '
15 | 'the clients some time to load the video',
16 | type=validate_positive_float_argument, default=0.1)
17 | parser.add_argument('--server_web_port', help="Websocket port of the webserver",
18 | type=validate_positive_int_argument, default=3000)
19 | parser.add_argument('--server_broadcast_port', help="Port for broadcasting the server messages",
20 | type=validate_positive_int_argument, default=2000)
21 | parser.add_argument('--server_play_broadcast_port', help="Port for broadcasting server play messages",
22 | type=validate_positive_int_argument, default=2001)
23 | parser.add_argument('--server_clock_port', help="Gstreamer Netclock port", type=validate_positive_int_argument,
24 | default=11111)
25 | parser.add_argument('--server_broadcast_interval', help="How often the server sends a broadcast message",
26 | type=validate_positive_float_argument, default=1)
27 | parser.add_argument('--client_broadcast_port', help="Port for receiving client broadcast messages",
28 | type=validate_positive_int_argument, default=3000)
29 | parser.add_argument('--client_time_overlay', help="Whether the clients should add a time overlay to the video",
30 | action='store_true')
31 |
32 | parser.add_argument("-v", "--verbose", help="Verbose logging", action='store_true')
33 | args = parser.parse_args()
34 |
35 | if args.verbose:
36 | logging.getLogger().setLevel(logging.DEBUG)
37 |
38 | web_server = WebServer(args.server_web_port, args.media_path, args.base_time_offset, ip_from_ifname(args.ifname),
39 | args.server_broadcast_port, args.server_play_broadcast_port, args.server_clock_port,
40 | args.server_broadcast_interval, args.client_broadcast_port)
41 | web_server.run()
42 |
--------------------------------------------------------------------------------
/src/videowall/networking/networking_server.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import socket
4 |
5 | from .message_definition import ServerPlayBroadcastMessage, ClientBroadcastMessage, ServerBroadcastMessage
6 | from .networking_exceptions import NetworkingException
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class NetworkingServer(object):
12 | def __init__(self, server_broadcast_port, server_play_broadcast_port, client_broadcast_port, buffer_size=1024):
13 | self._server_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
14 | self._server_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
15 | self._server_broadcast_port = server_broadcast_port
16 | self._server_play_broadcast_port = server_play_broadcast_port
17 |
18 | self._client_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
19 | self._client_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
20 | self._client_broadcast_socket.bind(("", client_broadcast_port)) # Bind to all
21 | self._client_broadcast_socket.settimeout(1e-4)
22 |
23 | self._server_play_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
24 | self._server_play_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
25 |
26 | self._buffer_size = buffer_size
27 |
28 | def send_play_broadcast(self, msg):
29 | if not isinstance(msg, ServerPlayBroadcastMessage):
30 | raise NetworkingException("msg ({}) is not of type ServerPlayBroadcastMessage".format(msg))
31 |
32 | logger.debug("Sending %s", msg)
33 | self._server_broadcast_socket.sendto(json.dumps(msg.to_dict()).encode('utf-8'),
34 | ('', self._server_play_broadcast_port))
35 |
36 | def send_broadcast(self, msg):
37 | if not isinstance(msg, ServerBroadcastMessage):
38 | raise NetworkingException("msg ({}) is not of type ServerBroadcastMessage".format(msg))
39 |
40 | logger.debug("Sending %s", msg)
41 | self._server_broadcast_socket.sendto(json.dumps(msg.to_dict()).encode('utf-8'),
42 | ('', self._server_broadcast_port))
43 |
44 | def receive_client_broadcast(self):
45 | data, _ = self._client_broadcast_socket.recvfrom(self._buffer_size)
46 |
47 | try:
48 | msg = ClientBroadcastMessage(**json.loads(data.decode("utf-8")))
49 | except Exception as e:
50 | raise NetworkingException(e)
51 | else:
52 | logger.debug("Client broadcast received: %s", msg)
53 | return msg
54 |
--------------------------------------------------------------------------------
/src/videowall/player/player_server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import time
4 |
5 | from pymediainfo import MediaInfo
6 | from videowall.gi_version import Gst, GstNet
7 | from videowall.util import validate_ip_port
8 |
9 | from .player_exceptions import PlayerException
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class PlayerServer(object):
15 | def __init__(self, ip, port):
16 | validate_ip_port(ip, port)
17 |
18 | self._ip = ip
19 | self._port = port
20 | self._base_time_nsecs = None
21 | self._duration = 0
22 | self._start = 0
23 |
24 | self._clock = Gst.SystemClock.obtain()
25 | self._clock_provider = GstNet.NetTimeProvider.new(self._clock, None, self._port)
26 |
27 | logger.debug("__init__(ip=%s, port=%s) constructed", ip, port)
28 |
29 | @staticmethod
30 | def _get_video_duration_from_file(filename):
31 | if not os.path.exists(filename):
32 | raise PlayerException("{} does not exist!".format(filename))
33 |
34 | info = MediaInfo.parse(filename)
35 |
36 | for track in info.tracks:
37 | if track.track_type == 'Video':
38 | video_track = track
39 | break
40 | else:
41 | raise PlayerException("Video does not contain a video track")
42 |
43 | return round(video_track.duration / 1e3, 2)
44 |
45 | def play(self, filename, base_time_offset):
46 | self._start = time.time() + base_time_offset
47 | self._duration = self._get_video_duration_from_file(filename)
48 | self._base_time_nsecs = self._clock.get_time() + base_time_offset * 1e9
49 |
50 | logger.debug("play(start=%.2f, duration=%.2f, base_time_nsecs=%d, clock_time_nsecs=%d)", self._start,
51 | self._duration, self._base_time_nsecs, self._clock.get_time())
52 |
53 | def get_ip(self):
54 | return self._ip
55 |
56 | def get_port(self):
57 | return self._port
58 |
59 | def get_base_time_nsecs(self):
60 | if self._base_time_nsecs is None:
61 | raise PlayerException('No base time available, please first play a file')
62 | return self._base_time_nsecs
63 |
64 | def get_position(self):
65 | return round(min(max(0.0, time.time() - self._start), self._duration), 2)
66 |
67 | def get_duration(self):
68 | return self._duration
69 |
70 | def stop(self):
71 | self._duration = None
72 | self._start = None
73 | self._base_time_nsecs = None
74 | logger.debug("stop")
75 |
76 | def is_playing(self):
77 | if self._duration is None:
78 | return False
79 | return self.get_position() < self._duration
80 |
81 | def close(self):
82 | self.stop()
83 |
--------------------------------------------------------------------------------
/src/videowall/util.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import fcntl
3 | import os
4 | import re
5 | import socket
6 | import string
7 | import struct
8 |
9 |
10 | def to_dict(obj):
11 | """
12 | Convert object to dictionary, source:
13 |
14 | https://stackoverflow.com/questions/1036409/recursively-convert-python-object-graph-to-dictionary
15 |
16 | :param obj: Instance of a class
17 | :return: Object serialized to a dictionary
18 | """
19 | if isinstance(obj, dict):
20 | data = {}
21 | for (k, v) in obj.items():
22 | data[k] = to_dict(v)
23 | return data
24 | elif hasattr(obj, "_ast"):
25 | return to_dict(obj._ast())
26 | elif hasattr(obj, "__iter__") and not isinstance(obj, str):
27 | return [to_dict(v) for v in obj]
28 | elif hasattr(obj, "__dict__"):
29 | data = dict([(key, to_dict(value))
30 | for key, value in obj.__dict__.items()
31 | if not callable(value) and not key.startswith('_')])
32 | return data
33 | else:
34 | return obj
35 |
36 |
37 | def validate_ip(ip):
38 | try:
39 | socket.inet_pton(socket.AF_INET, ip)
40 | except socket.error as e:
41 | raise argparse.ArgumentTypeError(e)
42 | return ip
43 |
44 |
45 | def validate_positive_int_argument(value):
46 | ivalue = int(value)
47 | if ivalue <= 0:
48 | raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value)
49 | return ivalue
50 |
51 |
52 | def validate_positive_float_argument(value):
53 | fvalue = float(value)
54 | if fvalue <= 0:
55 | raise argparse.ArgumentTypeError("%s is an invalid positive float value" % value)
56 | return fvalue
57 |
58 |
59 | def validate_ip_port(ip, port):
60 | validate_ip(ip)
61 | validate_positive_int_argument(port)
62 | return ip, port
63 |
64 |
65 | def validate_positive_or_zero_int_argument(value):
66 | ivalue = int(value)
67 | if ivalue < 0:
68 | raise argparse.ArgumentTypeError("%s is an invalid positive or zero int value" % value)
69 | return ivalue
70 |
71 |
72 | def get_ifnames():
73 | return os.listdir('/sys/class/net/')
74 |
75 |
76 | def ip_from_ifname(ifname):
77 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
78 | return socket.inet_ntoa(fcntl.ioctl(
79 | s.fileno(),
80 | 0x8915, # SIOCGIFADDR
81 | struct.pack('256s', ifname[:15])
82 | )[20:24])
83 |
84 |
85 | def get_unique_filename(path):
86 | valid_chars = "-_/.%s%s" % (string.ascii_letters, string.digits)
87 | path = ''.join(c for c in path if c in valid_chars).replace(' ', '_')
88 |
89 | i = 0
90 | while os.path.exists(path):
91 | name, ext = os.path.splitext(path)
92 | s = re.search('(.*)_(\d+)\.', name)
93 | if s:
94 | name = s.group(1)
95 | i = int(s.group(2)) + 1
96 | path = "{}_{}{}".format(name, i, ext)
97 | return path
98 |
--------------------------------------------------------------------------------
/src/videowall/networking/networking_client.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import socket
4 |
5 | from videowall.util import validate_ip_port, validate_positive_int_argument
6 |
7 | from .message_definition import ServerPlayBroadcastMessage, ServerBroadcastMessage
8 | from .networking_exceptions import NetworkingException
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class NetworkingClient(object):
14 | def __init__(self, ip, server_broadcast_port, server_play_broadcast_port, client_broadcast_port, buffer_size=1024):
15 | validate_ip_port(ip, server_broadcast_port)
16 | validate_positive_int_argument(client_broadcast_port)
17 | self._ip = ip
18 |
19 | self._server_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
20 | self._server_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
21 | self._server_broadcast_socket.bind(("", server_broadcast_port)) # Bind to all
22 | self._server_broadcast_socket.settimeout(5.0)
23 | self._server_play_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
24 | self._server_play_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
25 | self._server_play_broadcast_socket.bind(("", server_play_broadcast_port)) # Bind to all
26 | self._server_play_broadcast_socket.settimeout(5.0)
27 | self._buffer_size = buffer_size
28 |
29 | self._client_broadcast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
30 | self._client_broadcast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
31 | self._client_broadcast_port = client_broadcast_port
32 |
33 | def send_client_broadcast(self, msg):
34 | logger.debug("Broadcasting client message: %s", msg)
35 | self._client_broadcast_socket.sendto(json.dumps(msg.to_dict()).encode('utf-8'),
36 | ('', self._client_broadcast_port))
37 |
38 | def receive_server_broadcast(self):
39 | logger.debug("waiting for server broadcast message ...")
40 |
41 | # May raise a socket.timeout exception
42 | data, _ = self._server_broadcast_socket.recvfrom(self._buffer_size)
43 |
44 | try:
45 | msg = ServerBroadcastMessage(**json.loads(data.decode("utf-8")))
46 | except Exception as e:
47 | raise NetworkingException(e)
48 | else:
49 | logger.debug("Server broadcast received: %s", msg)
50 | return msg
51 |
52 | def receive_server_play_broadcast(self):
53 | logger.debug("waiting for server play broadcast message ...")
54 |
55 | # May raise a socket.timeout exception
56 | data, _ = self._server_play_broadcast_socket.recvfrom(self._buffer_size)
57 |
58 | print(data)
59 |
60 | try:
61 | msg = ServerPlayBroadcastMessage(**json.loads(data.decode("utf-8")))
62 | except Exception as e:
63 | raise NetworkingException(e)
64 | else:
65 | logger.debug("Server play broadcast received: %s", msg)
66 | return msg
67 |
68 | def get_ip(self):
69 | return self._ip
70 |
71 | def close(self):
72 | logger.debug("Closing NetworkingClient ...")
73 |
--------------------------------------------------------------------------------
/src/videowall/client.py:
--------------------------------------------------------------------------------
1 | import getpass
2 | import logging
3 | import socket
4 | import threading
5 | import time
6 |
7 | from videowall.player.player_exceptions import PlayerException
8 |
9 | from .media_manager import MediaManagerClient
10 | from .networking import NetworkingClient
11 | from .networking.message_definition import ClientConfig, ClientBroadcastMessage
12 | from .player import PlayerClient
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | class Client(object):
18 | def __init__(self, player_platform, media_path, ip, server_broadcast_port, server_play_broadcast_port,
19 | client_broadcast_port, client_broadcast_interval):
20 | self._networking = NetworkingClient(ip, server_broadcast_port, server_play_broadcast_port,
21 | client_broadcast_port)
22 | self._player_platform = player_platform
23 | self._player = None
24 | self._media_manager = MediaManagerClient(media_path)
25 |
26 | self._close = False
27 |
28 | self._client_broadcast_interval = client_broadcast_interval
29 | self._client_broadcast_thread = threading.Thread(target=self.send_client_broadcast)
30 | self._client_broadcast_thread.start()
31 |
32 | def send_client_broadcast(self):
33 | while not self._close:
34 | msg = ClientBroadcastMessage(
35 | getpass.getuser(),
36 | self._networking.get_ip(),
37 | self._media_manager.get_media_path()
38 | )
39 | logger.debug("Broadcasting client message: %s", msg)
40 | self._networking.send_client_broadcast(msg)
41 | time.sleep(self._client_broadcast_interval)
42 |
43 | @staticmethod
44 | def _get_client_specific_config(ip, client_config):
45 | if ip in client_config:
46 | return client_config[ip]
47 | logging.warn("%s not present in client_config %s, using default config", ip, client_config)
48 | return ClientConfig.get_default()
49 |
50 | def run(self):
51 | while not self._close:
52 | try:
53 | logger.info("Waiting for server broadcast message ...")
54 | msg = self._networking.receive_server_broadcast()
55 | except socket.timeout:
56 | pass
57 | else:
58 | self._player = PlayerClient(self._player_platform, msg.clock_ip, msg.clock_port)
59 | break
60 |
61 | while not self._close:
62 | try:
63 | msg = self._networking.receive_server_play_broadcast()
64 | except socket.timeout:
65 | pass
66 | else:
67 | logger.info("Received play request: %s", msg)
68 | client_cfg = Client._get_client_specific_config(self._networking.get_ip(), msg.client_config)
69 | try:
70 | self._player.play(self._media_manager.get_full_path(msg.filename), msg.base_time_nsecs,
71 | client_cfg.videocrop_config, self._networking.get_ip(), msg.time_overlay)
72 | except PlayerException as e:
73 | logger.error(e)
74 |
75 | def close(self):
76 | self._networking.close()
77 | self._player.close()
78 |
79 | logger.debug("Closing Client ...")
80 | self._close = True
81 | self._client_broadcast_thread.join()
82 |
--------------------------------------------------------------------------------
/web/src/components/PlaylistModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
85 |
86 |
127 |
--------------------------------------------------------------------------------
/src/videowall/server.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import socket
3 | import time
4 | from collections import namedtuple
5 |
6 | import tornado
7 |
8 | from .media_manager import MediaManagerServer
9 | from .networking import NetworkingServer
10 | from .networking.message_definition import ServerPlayBroadcastMessage, ServerBroadcastMessage
11 | from .player import PlayerServer
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 | RemoteClient = namedtuple('RemoteClient', 'username ip media_path age')
16 |
17 |
18 | class Server(object):
19 | def __init__(self, media_path, base_time_offset, ip, server_broadcast_port, server_play_broadcast_port,
20 | server_clock_port, server_broadcast_interval, client_broadcast_port):
21 | self._networking = NetworkingServer(server_broadcast_port, server_play_broadcast_port, client_broadcast_port)
22 | self._player = PlayerServer(ip, server_clock_port)
23 | self._base_time_offset = base_time_offset
24 | self._media_manager = MediaManagerServer(media_path)
25 | self._media_filename = None
26 |
27 | self._server_broadcast_timer = tornado.ioloop.PeriodicCallback(self._server_broadcast,
28 | server_broadcast_interval * 1e3)
29 | self._server_broadcast_timer.start()
30 |
31 | self._receive_client_broadcast_timer = tornado.ioloop.PeriodicCallback(self._receive_client_broadcast, 10)
32 | self._receive_client_broadcast_timer.start()
33 |
34 | self._check_done_timer = tornado.ioloop.PeriodicCallback(self._check_player_done, 100)
35 | self._check_done_timer.start()
36 |
37 | self._clients = {}
38 | self._client_config = {}
39 |
40 | def _server_broadcast(self):
41 | self._networking.send_broadcast(ServerBroadcastMessage(
42 | clock_ip=self._player.get_ip(),
43 | clock_port=self._player.get_port()
44 | ))
45 |
46 | def _receive_client_broadcast(self):
47 | try:
48 | msg = self._networking.receive_client_broadcast()
49 | except socket.timeout:
50 | pass
51 | except Exception as e:
52 | logger.error(e)
53 | else:
54 | self._clients[msg.ip] = {
55 | "time": time.time(),
56 | "msg": msg
57 | }
58 |
59 | def _check_player_done(self):
60 | if self._media_filename and not self._player.is_playing():
61 | self.play(self._media_filename)
62 |
63 | def get_media_filenames(self):
64 | return self._media_manager.get_filenames()
65 |
66 | def play(self, filename):
67 | self._media_filename = filename
68 | self._player.play(self._media_manager.get_full_path(filename), self._base_time_offset)
69 | self._networking.send_play_broadcast(ServerPlayBroadcastMessage(
70 | filename=filename,
71 | base_time_nsecs=self._player.get_base_time_nsecs(),
72 | time_overlay=True,
73 | client_config=self._client_config
74 | ))
75 |
76 | def set_client_config(self, config):
77 | self._client_config = config
78 |
79 | def get_client_config(self):
80 | return self._client_config
81 |
82 | def is_playing(self):
83 | return self._player.is_playing()
84 |
85 | def delete_media(self, filename):
86 | self._media_manager.delete_media(filename)
87 |
88 | def close(self):
89 | self._player.close()
90 |
91 | def get_duration(self):
92 | return self._player.get_duration()
93 |
94 | def get_position(self):
95 | return self._player.get_position()
96 |
97 | def get_clients(self):
98 | now = time.time()
99 | return [{
100 | "username": c["msg"].username,
101 | "ip": c["msg"].ip,
102 | "media_path": c["msg"].media_path,
103 | "age": now - c["time"]
104 | } for c in self._clients.values()]
105 |
106 | def sync_media(self):
107 | logger.info("Syncing media ..")
108 | self._media_manager.sync(
109 | ["{}@{}:{}".format(c["username"], c["ip"], c["media_path"]) for c in self.get_clients()])
110 | logger.info("Done syncing media")
111 |
112 | def get_media_path(self):
113 | return self._media_manager.get_media_path()
114 |
115 | def get_current_media_filename(self):
116 | return self._media_filename
117 |
118 | def get_state_dict(self):
119 | return {
120 | "player": {
121 | "media_path": self.get_media_path(),
122 | "media_filenames": self.get_media_filenames(),
123 | "current_media_filename": self.get_current_media_filename(),
124 | "is_playing": self.is_playing(),
125 | "duration": self.get_duration(),
126 | "position": self.get_position()
127 | },
128 | "client_config": self.get_client_config(),
129 | "clients": self.get_clients()
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Videowall
2 |
3 | [](https://travis-ci.org/reinzor/videowall)
4 |
5 | 
6 |
7 | Video wall with multiple tiles that enables synchronized video playback, mirrored or tiled.
8 |
9 | ## Demo videos
10 |
11 | | Description | Video |
12 | :-------------------------:|:-------------------------:
13 | 6x RPI Zero - 720p - Big bug bunny | [](https://www.youtube.com/watch?v=f5Dp35RL9q8&t=6s)
14 | 2x RPI Zero - 720p - Big bug bunny | [](https://www.youtube.com/watch?v=J6anLNTHhKU&t=6s)
15 | 2x RPI Zero - 720p - Simpsons | [](https://www.youtube.com/watch?v=LbjiZv7XG90)
16 | 4x RPI Zero + laptop - 720p - Fantastic 4 | [](https://www.youtube.com/watch?v=6yAyf_zFOXs)
17 |
18 | ## Installation
19 |
20 | ### Software
21 |
22 | #### Raspberry PI
23 |
24 | ##### Installation prerequisites
25 |
26 | - Raspbian Stretch Lite
27 | - Raspberry Pi 3 / Raspberry Pi Zero (other Pi's not tested)
28 | - Videowall repository is your current working directory
29 |
30 | ##### Installation from source
31 |
32 | - Install Raspbian Stretch lite on an sd card of at least 4GB
33 | - Place an `ssh` document in the `/boot` partition to enable direct ssh access
34 | - Login to the raspberry pi and start the installation:
35 |
36 | ```
37 | sudo apt-get -y update && \
38 | sudo apt-get -y install git && \
39 | git clone https://github.com/reinzor/videowall.git && cd videowall && \
40 | ./install_raspberry_pi_stretch_lite_autostart.bash
41 | ```
42 |
43 | This installs the videowall software and enables a client service on startup.
44 |
45 | #### Ubuntu x86
46 |
47 | Installation / usage instruction video: https://youtu.be/XLXoX8bj2vA
48 |
49 | ##### Installation prerequisites
50 |
51 | - Ubuntu x86 16.04 LTS (other versions not tested)
52 | - Videowall repository is your current working directory
53 |
54 | ##### Installation
55 |
56 | ```
57 | ./install_ubuntu_x86.bash
58 | ```
59 |
60 | ### Hardware
61 |
62 | #### Raspberry PI
63 |
64 | ##### Components per client without cables
65 |
66 | - [Raspberry PI zero](https://www.adafruit.com/product/2885)
67 | - [Micro USB 2.0 naar Ethernet 10/100 RJ45 Network Lan Adapter Card](https://nl.banggood.com/Micro-USB-2_0-to-Ethernet-10-or-100-RJ45-Network-Lan-Adapter-Card-p-921585.html)
68 | - 4GB+ Micro SD card
69 |
70 | Cost `~15 USD` per client
71 |
72 | ### Quick start
73 |
74 | Make sure you have sources the `setup.bash` from the root directory. This will set the Gstreamer plugin paths and appends the videowall library to your `PYTHONPATH`.
75 |
76 | #### Server
77 |
78 | scripts/web_server
79 |
80 | #### Client
81 |
82 | scripts/client
83 |
84 | This is automatically started on a raspberry pi after installation. Can be manually started on an ubuntu x86 environment.
85 |
86 | ### How to create release image
87 |
88 | - Create a git tag and update release notes
89 | - Ensure new MAC generation on startup
90 |
91 | ```bash
92 | sudo rm -f /etc/network/mac
93 | ```
94 |
95 | - Create `.img` file on Ubuntu host computer (insert sd card):
96 |
97 | ```bash
98 | sudo fdisk -l # Get the disk name of the sd card
99 | sudo dd bs=4M if=/path/to/disk of=/path/to/image.img
100 | ```
101 |
102 | - Shrink the image using [pishrink.sh](https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh)
103 |
104 | ```bash
105 | sudo ./pishrink.sh /path/to/image.img /path/to/shrinked_image.img
106 | ```
107 |
108 | - Create a `tar.gz` from image file:
109 |
110 | ```
111 | Right click, compress, tar.gz
112 | ```
113 |
114 | ## References
115 |
116 | - [Cinder GST Sync Player](https://github.com/patrickFuerst/Cinder-GstVideoSyncPlayer)
117 | - [Override getty1](https://raymii.org/s/tutorials/Run_software_on_tty1_console_instead_of_login_getty.html)
118 | - [Vigsterkr pi-wall](https://github.com/vigsterkr/pi-wall)
119 | - [Gstreamer mmal for smooth video playback on RPI](https://gstreamer.freedesktop.org/data/events/gstreamer-conference/2016/John%20Sadler%20-%20Smooth%20video%20on%20Raspberry%20Pi%20with%20gst-mmal%20(Lightning%20Talk).pdf)
120 | - [Gstreamer sync server for synchronized playback on multiple client with a gstreamer client server set-up](https://github.com/ford-prefect/gst-sync-server)
121 | - [Multicast Video-Streaming on Embedded Linux Environment, Daichi Fukui, Toshiba Corporation, Japan Technical Jamboree 63, Dec 1st, 2017](https://elinux.org/images/3/33/Multicast_jamboree63_fukui.pdf)
122 | - [omxplayer-sync](https://github.com/turingmachine/omxplayer-sync)
123 | - [pwomxplayer](https://github.com/JeffCost/pwomxplayer)
124 | - [dbus vlc](https://wiki.videolan.org/DBus-spec/)
125 | - [dbus tutorial phython for MPRIS](http://amhndu.github.io/Blog/python-dbus-mpris.html)
126 | - [dbus omxplayer](https://github.com/popcornmix/omxplayer)
127 | - [GPU memory 90 degrees omxplayer](https://github.com/popcornmix/omxplayer/issues/467)
128 | - [Remote dbus control](https://stackoverflow.com/questions/10158684/connecting-to-dbus-over-tcp/13275973#13275973)
129 | - [GST OMX](https://github.com/GStreamer/gst-omx)
130 | - [GST MMAL](https://github.com/youviewtv/gst-mmal)
131 | - [Disable Boot text](https://retropie.org.uk/docs/FAQ/#how-do-i-hide-the-boot-text)
132 |
--------------------------------------------------------------------------------
/src/videowall/web_server.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import json
3 | import logging
4 | import os.path
5 | import shutil
6 | from tempfile import NamedTemporaryFile
7 |
8 | import tornado.escape
9 | import tornado.ioloop
10 | import tornado.options
11 | import tornado.web
12 | import tornado.websocket
13 | from pymediainfo import MediaInfo
14 | from videowall.server import Server
15 | from videowall.util import get_unique_filename
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | class WebServer(tornado.web.Application):
21 | def __init__(self, server_web_port, media_path, base_time_offset, ip, server_broadcast_port,
22 | server_play_broadcast_port, server_clock_port,
23 | server_broadcast_interval, client_broadcast_port):
24 | self._server = Server(media_path, base_time_offset, ip, server_broadcast_port, server_play_broadcast_port,
25 | server_clock_port, server_broadcast_interval, client_broadcast_port)
26 |
27 | self._server_web_port = server_web_port
28 |
29 | UploadHandler.path = self._server.get_media_path()
30 | WebSocketHandler.server = self._server
31 | super(WebServer, self).__init__([
32 | ("/upload", UploadHandler),
33 | ("/ws", WebSocketHandler),
34 | ("/(.*)", tornado.web.StaticFileHandler, {
35 | "path": os.path.join(os.path.dirname(__file__), "../../web/dist/"),
36 | "default_filename": "index.html"
37 | }),
38 | ])
39 | self.listen(self._server_web_port)
40 |
41 | def run(self):
42 | tornado.ioloop.IOLoop.current().start()
43 |
44 |
45 | class WebSocketHandler(tornado.websocket.WebSocketHandler):
46 | def __init__(self, application, request, **kwargs):
47 | super(WebSocketHandler, self).__init__(application, request, **kwargs)
48 | self._broadcast_timer = tornado.ioloop.PeriodicCallback(self._broadcast, 100)
49 |
50 | self._command_handlers = {
51 | "play": self._play,
52 | "delete": self._delete,
53 | "set_client_config": self._set_client_config,
54 | "sync_media": self._sync_media
55 | }
56 |
57 | def check_origin(self, origin):
58 | """
59 | Allow external connections, e.g. dev server
60 | """
61 | return True
62 |
63 | def open(self):
64 | logger.info("New websocket connection!")
65 | self._broadcast_timer.start()
66 |
67 | def _broadcast(self):
68 | self.write_message(WebSocketHandler.server.get_state_dict())
69 |
70 | def on_message(self, message):
71 | try:
72 | message = json.loads(message)
73 | except Exception as e:
74 | logger.error("Could not decode command: {}".format(e))
75 |
76 | if "command" not in message or "arguments" not in message:
77 | logger.error("Message should contain a command and arguments key!")
78 | return
79 |
80 | cmd = message["command"]
81 | args = message["arguments"]
82 |
83 | logger.info("Command %s with arguments %s", cmd, args)
84 |
85 | if cmd in self._command_handlers:
86 | f = self._command_handlers[cmd]
87 | try:
88 | f(**args)
89 | except TypeError:
90 | logger.error(
91 | "Invalid args for cmd {}. Received {} expected {}".format(cmd, args.keys(), inspect.getargspec(f)))
92 | else:
93 | logger.warning("No registered handler for command {}".format(message["command"]))
94 |
95 | def _delete(self, filename):
96 | WebSocketHandler.server.delete_media(filename)
97 |
98 | def _play(self, filename):
99 | try:
100 | WebSocketHandler.server.play(filename)
101 | except Exception as e:
102 | logger.error(e)
103 |
104 | def _set_client_config(self, config):
105 | WebSocketHandler.server.set_client_config(config)
106 |
107 | def _sync_media(self):
108 | WebSocketHandler.server.sync_media()
109 |
110 | def on_close(self):
111 | self._broadcast_timer.stop()
112 |
113 |
114 | class UploadHandler(tornado.web.RequestHandler):
115 | @staticmethod
116 | def _validate_720p_mp4_file(filename):
117 | info = MediaInfo.parse(filename)
118 |
119 | for track in info.tracks:
120 | if track.track_type == 'Video':
121 | video_track = track
122 | if video_track.codec != 'AVC':
123 | raise Exception("Video should have an AVC (h264) encoding")
124 | if video_track.width != 1280 or video_track.height != 720:
125 | raise Exception(
126 | "Video should be of size 1280x720, it is {}x{}".format(video_track.width, video_track.height))
127 | break
128 | else:
129 | raise Exception("File does not contain a video track")
130 |
131 | def post(self):
132 | file_info = self.request.files['file'][0]
133 | logger.info("Received upload: (content_type=%s, filename=%s)", file_info['content_type'], file_info['filename'])
134 | filename = get_unique_filename(os.path.join(UploadHandler.path, file_info['filename']))
135 | file_handle = NamedTemporaryFile(delete=False)
136 | file_handle.write(file_info['body'])
137 | file_handle.close()
138 | try:
139 | self._validate_720p_mp4_file(file_handle.name)
140 | except Exception as e:
141 | logger.error(e)
142 | self.set_status(400)
143 | self.finish({"reason": str(e)})
144 | else:
145 | logger.info("Writing to %s", filename)
146 | shutil.copyfile(file_handle.name, filename)
147 | logger.info("Wrote to %s", filename)
148 | self.finish("Uploaded {}".format(filename))
149 |
--------------------------------------------------------------------------------
/src/videowall/player/player_client.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | import logging
4 | import os.path
5 | import threading
6 | import time
7 |
8 | from videowall.gi_version import GstNet, Gst, GObject
9 | from videowall.util import validate_ip_port
10 |
11 | from .player_exceptions import PlayerException
12 | from .player_platforms import PlayerPlatform, PlayerPlatformPC, PlayerPlatformRaspberryPi, get_player_platforms
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | ONE_MINUTE_SECONDS = 60
17 |
18 |
19 | class PlayerClient(object):
20 | Gst.init(None)
21 |
22 | def __init__(self, platform, clock_ip, clock_port, use_local_clock=False):
23 | if not issubclass(platform, PlayerPlatform):
24 | raise PlayerException("Invalid player platform {}, available platforms: {}".format(platform,
25 | get_player_platforms()))
26 | self._use_local_clock = use_local_clock
27 |
28 | if not self._use_local_clock:
29 | validate_ip_port(clock_ip, clock_port)
30 | clock_name = "clock0"
31 | try:
32 | self._clock = GstNet.NetClientClock.new(clock_name, clock_ip, clock_port, 0)
33 | except TypeError as e:
34 | raise PlayerException("GstNet.NetClientClock.new({}, {}, {}) failed ({}). Set environment variable "
35 | "GST_DEBUG=1 for more info".format(clock_name, clock_ip, clock_port, e))
36 | else:
37 | self._clock = Gst.SystemClock.obtain()
38 |
39 | self._pipeline = None
40 |
41 | def run_player_thread():
42 | GObject.MainLoop().run()
43 |
44 | self._gobject_thread = threading.Thread(target=run_player_thread)
45 | self._gobject_thread.start()
46 |
47 | self._platform = platform
48 |
49 | logger.info("Player constructed (platform=%s, clock_ip=%s, clock_port=%s, use_local_clock=%s)", platform, clock_ip, clock_port, use_local_clock)
50 |
51 | def _construct_pipeline(self, filename, videocrop_config, text_overlay, time_overlay):
52 | real_path = os.path.realpath(os.path.expanduser(filename))
53 |
54 | launch_cmd = ""
55 | if os.path.isfile(real_path):
56 | launch_cmd += "filesrc location={}".format(real_path)
57 |
58 | if self._platform == PlayerPlatformPC:
59 | launch_cmd += " ! decodebin"
60 | elif self._platform == PlayerPlatformRaspberryPi:
61 | launch_cmd += " ! qtdemux ! h264parse ! omxh264dec"
62 |
63 | launch_cmd += " ! videocrop bottom={} left={} right={} top={}".format(videocrop_config.bottom,
64 | videocrop_config.left,
65 | videocrop_config.right,
66 | videocrop_config.top)
67 | launch_cmd += " ! videoconvert"
68 | if text_overlay:
69 | launch_cmd += ' ! textoverlay text="{}" font-desc="Sans, 12"'.format(text_overlay)
70 |
71 | launch_cmd += ' ! timeoverlay font-desc="Sans, {}"'.format(12 if time_overlay else 1e-3)
72 | launch_cmd += " ! queue"
73 |
74 | logger.debug("Creating pipeline from launch command %s ..", launch_cmd)
75 | else:
76 | error_image_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "assets", "error.jpg")
77 | launch_cmd += "filesrc location={}".format(error_image_location)
78 | launch_cmd += " ! jpegparse ! jpegdec ! imagefreeze"
79 | if text_overlay:
80 | launch_cmd += ' ! textoverlay text="{}\n{}"'.format("%s not found" % filename, text_overlay)
81 | launch_cmd += " ! videoconvert"
82 |
83 | if self._platform == PlayerPlatformPC:
84 | launch_cmd += " ! ximagesink" # or ! fakesink sync=true # sync required for realtime playback
85 | else:
86 | launch_cmd += " ! mmalvideosink"
87 |
88 | logger.debug("gst-launch-1.0 -v %s", launch_cmd)
89 | self._pipeline = Gst.parse_launch(launch_cmd)
90 |
91 | def _destroy_pipeline(self):
92 | if self._pipeline is not None:
93 | self._pipeline.set_state(Gst.State.NULL)
94 | self._pipeline = None
95 |
96 | def close(self):
97 | self.stop()
98 | GObject.MainLoop().quit()
99 | logger.info("Waiting for the GObject Thread to join ..")
100 | self._gobject_thread.join()
101 |
102 | def play(self, filename, base_time_nsecs, videocrop_config, text_overlay, time_overlay):
103 | if not self._use_local_clock:
104 | # Make sure the client clock is in sync with the server
105 | sync_grace = time.time() + 5.0
106 | delta = (base_time_nsecs - self._clock.get_time()) / 1e9
107 | while delta > ONE_MINUTE_SECONDS or delta < 0:
108 | delta = (base_time_nsecs - self._clock.get_time()) / 1e9
109 | time.sleep(0.1)
110 | if time.time() > sync_grace:
111 | msg = 'Delta between clock and base_time negative or too large: {} > ONE_MINUTE_SECONDS ({})' \
112 | .format(delta, ONE_MINUTE_SECONDS)
113 | raise PlayerException(msg)
114 | else:
115 | base_time_nsecs = self._clock.get_time()
116 |
117 | if self._pipeline:
118 | self._destroy_pipeline()
119 | self._construct_pipeline(filename, videocrop_config, text_overlay, time_overlay)
120 |
121 | self._pipeline.set_start_time(Gst.CLOCK_TIME_NONE)
122 | self._pipeline.use_clock(self._clock)
123 | self._pipeline.set_base_time(base_time_nsecs)
124 |
125 | self._pipeline.set_state(Gst.State.PLAYING)
126 |
127 | # Make sure we have transitioned to the PLAYING state
128 | play_grace = time.time() + 5.0
129 | ret, state, pending = self._pipeline.get_state(timeout=Gst.SECOND)
130 | while state != Gst.State.PLAYING:
131 | ret, state, pending = self._pipeline.get_state(timeout=Gst.SECOND)
132 | time.sleep(0.1)
133 | if time.time() > play_grace:
134 | msg = 'Transition to playing state took too long!'
135 | raise PlayerException(msg)
136 |
137 | logger.info('play(base_time_nsecs=%d, clock_time_nsecs=%d)', base_time_nsecs, self._clock.get_time())
138 |
139 | def stop(self):
140 | self._destroy_pipeline()
141 |
142 | def is_playing(self):
143 | ret, state, pending = self._pipeline.get_state(timeout=Gst.SECOND)
144 | return state == Gst.State.PLAYING
145 |
146 | def get_duration(self):
147 | _, duration = self._pipeline.query_duration(Gst.Format.TIME)
148 | return round(duration / 1e9, 2)
149 |
150 | def get_position(self):
151 | _, position = self._pipeline.query_position(Gst.Format.TIME)
152 | return round(position / 1e9, 2)
153 |
--------------------------------------------------------------------------------
/web/src/components/ScreenGrid.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Sync media
18 |
19 |
20 |
21 |
22 | IP:
23 | Username:
24 | Media path:
25 |
26 |
Age: {{client.age}}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Add screen
38 |
39 |
40 |
41 |
53 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
210 |
255 |
--------------------------------------------------------------------------------
/web/dist/js/app.7e10ce6b.js:
--------------------------------------------------------------------------------
1 | (function(t){function e(e){for(var a,o,s=e[0],c=e[1],l=e[2],u=0,p=[];u9?e:"0"+e,i=n>9?n:"0"+n,o=a>9?a:"0"+a;return r+":"+i+":"+o});var l=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{attrs:{id:"app"}},[n("NavBar"),n("Player")],1)},d=[],u=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("b-navbar",{attrs:{toggleable:"md",type:"dark",variant:"info",id:"navbar"}},[n("b-container",[n("b-navbar-toggle",{attrs:{target:"nav_collapse"}}),n("b-navbar-brand",{attrs:{href:"#"}},[t._v("Video wall")]),n("b-collapse",{attrs:{"is-nav":"",id:"nav_collapse"}},[n("b-navbar-nav",{staticClass:"ml-auto"},[n("b-nav-item",{attrs:{to:"/player"}},[t._v("Player")])],1)],1)],1)],1)},p=[],f={name:"NavBar",components:{}},v=f,m=(n("34ba"),n("2877")),b=Object(m["a"])(v,u,p,!1,null,null,null);b.options.__file="NavBar.vue";var y=b.exports,h=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{attrs:{id:"videoWall"}},[n("ScreenGrid",{attrs:{clientConfig:t.serverState.client_config,clients:t.serverState.clients}}),n("center",[n("PlayerBar",{attrs:{playerState:t.serverState.player},on:{play:function(e){t.play(e)}}})],1)],1)},g=[],_=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{attrs:{id:"playerBar"}},[n("PlaylistModal",{attrs:{playerState:t.playerState},on:{play:function(e){t.$emit("play",e)}}}),n("b-button-group",[n("b-button",{attrs:{variant:"outline-secondary"}},[n("v-icon",{attrs:{name:"backward"}})],1),n("b-button",{attrs:{variant:"outline-secondary",disabled:!t.playerState.current_media_filename},on:{click:function(e){t.$emit("play",t.playerState.current_media_filename)}}},[n("v-icon",{attrs:{name:"sync-alt"}})],1),n("b-button",{attrs:{variant:"outline-secondary"}},[n("v-icon",{attrs:{name:"forward"}})],1)],1),n("div",{staticClass:"playerTime"},[n("b-form-input",{attrs:{type:"text",placeholder:t._f("hoursMinutesSeconds")(t.playerState.position),disabled:""}})],1),n("div",{attrs:{id:"playerProgress"}},[n("b-progress",{attrs:{max:t.playerState.duration,variant:"info"}},[n("b-progress-bar",{attrs:{value:t.playerState.position},domProps:{textContent:t._s(t.playerState.current_media_filename)}})],1)],1),n("div",{staticClass:"playerTime"},[n("b-form-input",{attrs:{type:"text",placeholder:t._f("hoursMinutesSeconds")(t.playerState.duration),disabled:""}})],1),n("b-button-group",[n("b-button",{directives:[{name:"b-modal",rawName:"v-b-modal.playlistModal",modifiers:{playlistModal:!0}}],attrs:{variant:"outline-secondary"}},[n("v-icon",{attrs:{name:"list-ul"}})],1)],1)],1)},S=[],x=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("b-modal",{attrs:{id:"playlistModal","hide-footer":"",title:"Playlist"}},[n("b-list-group",t._l(t.playerState.media_filenames,function(e){return n("b-list-group-item",{key:e,staticClass:"flex-column align-items-start",attrs:{variant:t.playerState.current_media_filename==e?"info":""}},[n("div",[n("div",{staticClass:"d-flex w-100 justify-content-between"},[n("h5",{staticClass:"mb-1",domProps:{textContent:t._s(e)}}),n("b-button-group",{staticClass:"playlistVideoButtons",attrs:{size:"sm"}},[n("b-button",{attrs:{variant:"outline-secondary"},on:{click:function(n){t.$emit("play",e)}}},[t.playerState.current_media_filename!=e?n("v-icon",{attrs:{name:"play"}}):n("v-icon",{attrs:{name:"sync-alt"}})],1),t.playerState.current_media_filename!=e?n("b-button",{attrs:{variant:"outline-secondary"},on:{click:function(n){t.deleteMedia(e)}}},[n("v-icon",{attrs:{name:"times"}})],1):t._e()],1)],1)])])})),t.err?n("b-alert",{attrs:{variant:"danger",show:"",dismissible:""},domProps:{textContent:t._s(t.err)}}):t._e(),n("vue2-dropzone",{ref:"dropzone",attrs:{id:"dropzone",options:t.dropzoneOptions},on:{"vdropzone-file-added":function(e){t.err=""},"vdropzone-complete":function(e){t.uploadComplete(t.$refs.dropzone)},"vdropzone-error":t.uploadError}})],1)},w=[],C=(n("b012"),n("1516")),k=n.n(C),P=n("92c3"),M=n.n(P),O=(n("1e3f"),{components:{draggable:k.a,vue2Dropzone:M.a},props:{playerState:{type:Object,required:!0}},data:function(){return{dropzoneOptions:{url:"/upload",thumbnailWidth:200,maxFilesize:500},err:""}},methods:{uploadComplete:function(t){t.removeAllFiles(),this.syncMedia()},uploadError:function(t){this.err=JSON.parse(t.xhr.response).reason},deleteMedia:function(t){this.$socket.sendObj({command:"delete",arguments:{filename:t}}),this.syncMedia()},syncMedia:function(){this.$socket.sendObj({command:"sync_media",arguments:{}})}}}),j=O,$=(n("e486"),Object(m["a"])(j,x,w,!1,null,null,null));$.options.__file="PlaylistModal.vue";var z=$.exports,E={name:"PlayerBar",components:{PlaylistModal:z},props:{playerState:{type:Object,required:!0}}},G=E,I=(n("9f4d"),Object(m["a"])(G,_,S,!1,null,null,null));I.options.__file="PlayerBar.vue";var A=I.exports,B=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("b-card",{staticClass:"mx-auto",style:{width:t.screenGrid.w+"px"},attrs:{id:"screenGrid"}},[n("div",{attrs:{slot:"header"},slot:"header"},[n("b-button-toolbar",{staticClass:"float-sm-right",attrs:{size:"sm"}},[n("b-button",{directives:[{name:"b-modal",rawName:"v-b-modal.clientsModal",modifiers:{clientsModal:!0}}],staticStyle:{"margin-right":"5px"},attrs:{variant:"outline-secondary"}},[n("v-icon",{attrs:{name:"users"}})],1),t.editState.active?n("b-button-group",{staticClass:"mx-1"},[n("b-button",{attrs:{variant:"outline-secondary"},on:{click:t.cancelEdit}},[n("v-icon",{attrs:{name:"times"}})],1),n("b-button",{attrs:{variant:"outline-secondary"},on:{click:t.applyEdit}},[n("v-icon",{attrs:{name:"check"}})],1)],1):n("b-button",{attrs:{variant:"outline-secondary"},on:{click:t.edit}},[n("v-icon",{attrs:{name:"edit"}})],1)],1)],1),n("div",{attrs:{id:"screenGridLayout"},on:{click:function(e){t.screenGridLayoutClicked(e)}}},[n("b-modal",{attrs:{id:"clientsModal","hide-footer":"",title:"Clients"}},[n("b-list-group",[n("b-button",{on:{click:t.syncMedia}},[t._v("Sync media")]),t._l(t.clients,function(e){return n("b-list-group-item",{key:e.ip,staticClass:"flex-column align-items-start",attrs:{variant:t.clientActive(e.ip)?"success":"danger"}},[n("div",[n("div",{staticClass:"d-flex justify-content-between align-items-center"},[n("table",{staticClass:"clientTable"},[n("tr",[n("td",[t._v("IP:")]),n("td",{domProps:{textContent:t._s(e.ip)}})]),n("tr",[n("td",[t._v("Username:")]),n("td",{domProps:{textContent:t._s(e.username)}})]),n("tr",[n("td",[t._v("Media path:")]),n("td",{domProps:{textContent:t._s(e.media_path)}})])]),n("b-badge",{attrs:{variant:"info"}},[t._v("Age: "+t._s(e.age))])],1)])])})],2)],1),n("b-modal",{ref:"addModal",attrs:{"hide-footer":"",title:"Add screen for client"}},[n("div",{staticClass:"d-block text-center"},[n("b-form",{on:{submit:function(e){e.preventDefault(),t.addScreen()}}},[n("b-form-select",{attrs:{options:t.clientIpOptions,required:""},model:{value:t.editState.addScreenIp,callback:function(e){t.$set(t.editState,"addScreenIp",e)},expression:"editState.addScreenIp"}}),n("b-button",{attrs:{type:"submit",variant:"primary"}},[t._v("Add screen")])],1)],1)]),n("grid-layout",{style:{height:t.screenGrid.h+"px",cursor:t.editState.active?"cell":"default"},attrs:{layout:t.screens,colNum:1280,rowHeight:1,maxRows:720,isDraggable:t.editState.active,isResizable:t.editState.active,isMirrored:!1,margin:[0,0],verticalCompact:!1,autoSize:!0,useCssTransforms:!0}},t._l(t.screens,function(e){return n("grid-item",{key:e.i,staticClass:"screen",style:{cursor:t.editState.active?"grabbing":"default"},attrs:{x:e.x,y:e.y,w:e.w,h:e.h,i:e.i}},[n("b-card",{attrs:{footer:e.x+","+e.y+" ["+e.w+"x"+e.h+"]"}},[n("div",{attrs:{slot:"header"},slot:"header"},[n("span",{domProps:{textContent:t._s("Screen "+e.i)}}),n("b-button-toolbar",{staticClass:"float-sm-right"},[n("b-button-group",{staticClass:"mx-1",attrs:{size:"sm"}},[n("b-button",{attrs:{variant:t.clientActive(e.i)?"success":"danger"}},[n("v-icon",{attrs:{name:"plug"}})],1),t.editState.active?n("b-button",{attrs:{variant:"danger"},on:{click:function(n){t.removeScreen(e.i)}}},[n("v-icon",{attrs:{name:"trash-alt"}})],1):t._e()],1)],1)],1)])],1)}))],1)])},N=[],T=(n("ac6a"),n("7be8")),q=n.n(T),J={name:"ScreenGrid",components:{VueGridLayout:q.a},props:{clientConfig:{type:Object,required:!0},clients:{type:Array,required:!0}},data:function(){return{editState:{active:!1,addScreenIp:"",clickedPosition:{x:0,y:0}},screenGrid:{w:1280,h:720},screens:[]}},watch:{clientConfig:function(t){if(!this.editState.active)for(var e in this.screens=[],t)this.screens.push({i:e,x:t[e].videocrop_config.left,y:t[e].videocrop_config.top,w:1280-t[e].videocrop_config.left-t[e].videocrop_config.right,h:720-t[e].videocrop_config.top-t[e].videocrop_config.bottom})}},computed:{clientIpOptions:function(){var t=this,e=[];return this.clients.forEach(function(n){n.ip in t.clientConfig||e.push(n.ip)}),e}},methods:{edit:function(){this.editState.active=!0},cancelEdit:function(){this.editState.active=!1},applyEdit:function(){var t={};this.screens.forEach(function(e){t[e.i]={videocrop_config:{bottom:720-e.y-e.h,left:e.x,right:1280-e.x-e.w,top:e.y}}}),this.$socket.sendObj({command:"set_client_config",arguments:{config:t}}),this.editState.active=!1},addScreen:function(){this.screens.push({x:this.editState.clickedPosition.x,y:this.editState.clickedPosition.y,w:200,h:200,i:this.editState.addScreenIp}),this.$refs.addModal.hide()},removeScreen:function(t){this.screens=this.screens.filter(function(e){return e.i!==t})},screenGridLayoutClicked:function(t){this.editState.active&&"vue-grid-layout"==t.target.getAttribute("class")&&(this.$refs.addModal.show(),this.editState.clickedPosition.x=t.offsetX,this.editState.clickedPosition.y=t.offsetY)},clientActive:function(t){var e=!1;return this.clients.forEach(function(n){n.ip==t&&n.age<5&&(e=!0)}),e},syncMedia:function(){this.$socket.sendObj({command:"sync_media",arguments:{}})}}},L=J,R=(n("0e07"),Object(m["a"])(L,B,N,!1,null,null,null));R.options.__file="ScreenGrid.vue";var D=R.exports,V={name:"Player",components:{PlayerBar:A,ScreenGrid:D},created:function(){var t=this;this.$options.sockets.onmessage=function(e){return t.messageReceived(e)},this.$options.sockets.onopen=function(e){return t.newConnection(e)}},data:function(){return{serverState:{client_config:{},clients:[],player:{}}}},methods:{newConnection:function(t){console.log(t)},messageReceived:function(t){this.serverState=JSON.parse(t.data)},play:function(t){this.$socket.sendObj({command:"play",arguments:{filename:t}})}}},F=V,W=(n("6131"),Object(m["a"])(F,h,g,!1,null,null,null));W.options.__file="Player.vue";var H=W.exports,U={name:"app",components:{NavBar:y,Player:H}},X=U,Y=(n("034f"),Object(m["a"])(X,l,d,!1,null,null,null));Y.options.__file="App.vue";var K=Y.exports;a["default"].use(r["a"]),a["default"].use(i["a"]),a["default"].component("v-icon",o["a"]),a["default"].use(c.a,"ws://"+location.host+"/ws",{format:"json",reconnection:!0}),a["default"].config.productionTip=!1;var Q=[{path:"/",component:H}],Z=new r["a"]({routes:Q});new a["default"]({router:Z,render:function(t){return t(K)}}).$mount("#app")},6131:function(t,e,n){"use strict";var a=n("5004"),r=n.n(a);r.a},"82b7":function(t,e,n){},"9f4d":function(t,e,n){"use strict";var a=n("1ab3"),r=n.n(a);r.a},c21b:function(t,e,n){},cfd9:function(t,e,n){},e486:function(t,e,n){"use strict";var a=n("3c61"),r=n.n(a);r.a}});
2 | //# sourceMappingURL=app.7e10ce6b.js.map
--------------------------------------------------------------------------------
/web/dist/js/app.7e10ce6b.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/App.vue?4199","webpack:///./src/components/ScreenGrid.vue?1708","webpack:///./src/components/NavBar.vue?7be5","webpack:///./src/filters.js","webpack:///./src/App.vue?5161","webpack:///./src/components/NavBar.vue?7529","webpack:///src/components/NavBar.vue","webpack:///./src/components/NavBar.vue?e8e4","webpack:///./src/components/NavBar.vue","webpack:///./src/components/Player.vue?e5e0","webpack:///./src/components/PlayerBar.vue?835a","webpack:///./src/components/PlaylistModal.vue?00ee","webpack:///src/components/PlaylistModal.vue","webpack:///./src/components/PlaylistModal.vue?098c","webpack:///./src/components/PlaylistModal.vue","webpack:///src/components/PlayerBar.vue","webpack:///./src/components/PlayerBar.vue?d403","webpack:///./src/components/PlayerBar.vue","webpack:///./src/components/ScreenGrid.vue?c9b5","webpack:///src/components/ScreenGrid.vue","webpack:///./src/components/ScreenGrid.vue?3e2e","webpack:///./src/components/ScreenGrid.vue","webpack:///src/components/Player.vue","webpack:///./src/components/Player.vue?c370","webpack:///./src/components/Player.vue","webpack:///src/App.vue","webpack:///./src/App.vue?1160","webpack:///./src/App.vue","webpack:///./src/main.js","webpack:///./src/components/Player.vue?aae0","webpack:///./src/components/PlayerBar.vue?9584","webpack:///./src/components/PlaylistModal.vue?ed8b"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","app","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_ScreenGrid_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_ScreenGrid_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NavBar_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_NavBar_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default","Vue","filter","hours","parseInt","Math","floor","minutes","seconds","dHours","dMins","dSecs","Appvue_type_template_id_b51b35c4_render","_vm","this","_h","$createElement","_c","_self","attrs","id","staticRenderFns","NavBarvue_type_template_id_1a3d3978_render","toggleable","type","variant","target","href","_v","is-nav","staticClass","to","NavBarvue_type_template_id_1a3d3978_staticRenderFns","NavBarvue_type_script_lang_js_","components","components_NavBarvue_type_script_lang_js_","component","componentNormalizer","options","__file","NavBar","Playervue_type_template_id_3c78357e_render","clientConfig","serverState","client_config","clients","playerState","player","on","play","$event","Playervue_type_template_id_3c78357e_staticRenderFns","PlayerBarvue_type_template_id_db8a2516_render","$emit","disabled","current_media_filename","click","placeholder","_f","position","max","duration","domProps","textContent","_s","directives","rawName","modifiers","playlistModal","PlayerBarvue_type_template_id_db8a2516_staticRenderFns","PlaylistModalvue_type_template_id_3cd2be0c_render","hide-footer","title","_l","media_filename","size","deleteMedia","_e","show","dismissible","err","ref","dropzoneOptions","vdropzone-file-added","vdropzone-complete","uploadComplete","$refs","dropzone","vdropzone-error","uploadError","PlaylistModalvue_type_template_id_3cd2be0c_staticRenderFns","PlaylistModalvue_type_script_lang_js_","draggable","vuedraggable_default","a","vue2Dropzone","vue2Dropzone_default","props","required","url","thumbnailWidth","maxFilesize","methods","removeAllFiles","syncMedia","JSON","parse","xhr","response","reason","filename","$socket","sendObj","command","arguments","components_PlaylistModalvue_type_script_lang_js_","PlaylistModal_component","PlaylistModal","PlayerBarvue_type_script_lang_js_","components_PlayerBarvue_type_script_lang_js_","PlayerBar_component","PlayerBar","ScreenGridvue_type_template_id_115dd8f9_render","style","width","screenGrid","slot","clientsModal","staticStyle","margin-right","editState","cancelEdit","applyEdit","edit","screenGridLayoutClicked","client","ip","clientActive","username","media_path","age","submit","preventDefault","addScreen","clientIpOptions","model","callback","$$v","$set","expression","height","cursor","active","layout","screens","colNum","rowHeight","maxRows","isDraggable","isResizable","isMirrored","margin","verticalCompact","autoSize","useCssTransforms","screen","x","y","w","h","footer","removeScreen","ScreenGridvue_type_template_id_115dd8f9_staticRenderFns","ScreenGridvue_type_script_lang_js_","VueGridLayout","vue_grid_layout_common_default","Array","addScreenIp","clickedPosition","watch","_clientConfig","videocrop_config","left","top","right","bottom","computed","_this","forEach","config","addModal","hide","e","getAttribute","offsetX","offsetY","components_ScreenGridvue_type_script_lang_js_","ScreenGrid_component","ScreenGrid","Playervue_type_script_lang_js_","created","$options","sockets","onmessage","messageReceived","onopen","newConnection","console","log","components_Playervue_type_script_lang_js_","Player_component","Player","Appvue_type_script_lang_js_","src_Appvue_type_script_lang_js_","App_component","App","use","VueRouter","BootstrapVue","Icon","VueNativeSock","location","host","format","reconnection","productionTip","routes","path","router","render","$mount","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_PlayerBar_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_PlayerBar_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_PlaylistModal_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0__","_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_node_modules_css_loader_index_js_ref_6_oneOf_1_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_lib_index_js_ref_6_oneOf_1_2_node_modules_cache_loader_dist_cjs_js_ref_0_0_node_modules_vue_loader_lib_index_js_vue_loader_options_PlaylistModal_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_0___default"],"mappings":"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","file":"js/app.7e10ce6b.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t\"app\": 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// add entry module to deferred list\n \tdeferredModules.push([0,\"chunk-vendors\"]);\n \t// run deferred modules when ready\n \treturn checkDeferredModules();\n","import mod from \"-!../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&\"","import mod from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ScreenGrid.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ScreenGrid.vue?vue&type=style&index=0&lang=css&\"","import mod from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./NavBar.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./NavBar.vue?vue&type=style&index=0&lang=css&\"","import Vue from \"vue\"\n\nVue.filter(\"hoursMinutesSeconds\", function(value) {\n let hours = parseInt(Math.floor(value / 3600)); \n let minutes = parseInt(Math.floor((value - (hours * 3600)) / 60)); \n let seconds= parseInt((value - ((hours * 3600) + (minutes * 60))) % 60); \n\n let dHours = (hours > 9 ? hours : '0' + hours);\n let dMins = (minutes > 9 ? minutes : '0' + minutes);\n let dSecs = (seconds > 9 ? seconds : '0' + seconds);\n\n return dHours + \":\" + dMins + \":\" + dSecs;\n})","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"app\"}},[_c('NavBar'),_c('Player')],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-navbar',{attrs:{\"toggleable\":\"md\",\"type\":\"dark\",\"variant\":\"info\",\"id\":\"navbar\"}},[_c('b-container',[_c('b-navbar-toggle',{attrs:{\"target\":\"nav_collapse\"}}),_c('b-navbar-brand',{attrs:{\"href\":\"#\"}},[_vm._v(\"Video wall\")]),_c('b-collapse',{attrs:{\"is-nav\":\"\",\"id\":\"nav_collapse\"}},[_c('b-navbar-nav',{staticClass:\"ml-auto\"},[_c('b-nav-item',{attrs:{\"to\":\"/player\"}},[_vm._v(\"Player\")])],1)],1)],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n \n \n \n Video wall \n\n \n \n Player \n \n \n \n \n \n\n\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./NavBar.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./NavBar.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./NavBar.vue?vue&type=template&id=1a3d3978&\"\nimport script from \"./NavBar.vue?vue&type=script&lang=js&\"\nexport * from \"./NavBar.vue?vue&type=script&lang=js&\"\nimport style0 from \"./NavBar.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"NavBar.vue\"\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"videoWall\"}},[_c('ScreenGrid',{attrs:{\"clientConfig\":_vm.serverState.client_config,\"clients\":_vm.serverState.clients}}),_c('center',[_c('PlayerBar',{attrs:{\"playerState\":_vm.serverState.player},on:{\"play\":function($event){_vm.play($event)}}})],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"playerBar\"}},[_c('PlaylistModal',{attrs:{\"playerState\":_vm.playerState},on:{\"play\":function($event){_vm.$emit('play', $event)}}}),_c('b-button-group',[_c('b-button',{attrs:{\"variant\":\"outline-secondary\"}},[_c('v-icon',{attrs:{\"name\":\"backward\"}})],1),_c('b-button',{attrs:{\"variant\":\"outline-secondary\",\"disabled\":!_vm.playerState.current_media_filename},on:{\"click\":function($event){_vm.$emit('play', _vm.playerState.current_media_filename)}}},[_c('v-icon',{attrs:{\"name\":\"sync-alt\"}})],1),_c('b-button',{attrs:{\"variant\":\"outline-secondary\"}},[_c('v-icon',{attrs:{\"name\":\"forward\"}})],1)],1),_c('div',{staticClass:\"playerTime\"},[_c('b-form-input',{attrs:{\"type\":\"text\",\"placeholder\":_vm._f(\"hoursMinutesSeconds\")(_vm.playerState.position),\"disabled\":\"\"}})],1),_c('div',{attrs:{\"id\":\"playerProgress\"}},[_c('b-progress',{attrs:{\"max\":_vm.playerState.duration,\"variant\":\"info\"}},[_c('b-progress-bar',{attrs:{\"value\":_vm.playerState.position},domProps:{\"textContent\":_vm._s(_vm.playerState.current_media_filename)}})],1)],1),_c('div',{staticClass:\"playerTime\"},[_c('b-form-input',{attrs:{\"type\":\"text\",\"placeholder\":_vm._f(\"hoursMinutesSeconds\")(_vm.playerState.duration),\"disabled\":\"\"}})],1),_c('b-button-group',[_c('b-button',{directives:[{name:\"b-modal\",rawName:\"v-b-modal.playlistModal\",modifiers:{\"playlistModal\":true}}],attrs:{\"variant\":\"outline-secondary\"}},[_c('v-icon',{attrs:{\"name\":\"list-ul\"}})],1)],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-modal',{attrs:{\"id\":\"playlistModal\",\"hide-footer\":\"\",\"title\":\"Playlist\"}},[_c('b-list-group',_vm._l((_vm.playerState.media_filenames),function(media_filename){return _c('b-list-group-item',{key:media_filename,staticClass:\"flex-column align-items-start\",attrs:{\"variant\":_vm.playerState.current_media_filename == media_filename ? 'info' : ''}},[_c('div',[_c('div',{staticClass:\"d-flex w-100 justify-content-between\"},[_c('h5',{staticClass:\"mb-1\",domProps:{\"textContent\":_vm._s(media_filename)}}),_c('b-button-group',{staticClass:\"playlistVideoButtons\",attrs:{\"size\":\"sm\"}},[_c('b-button',{attrs:{\"variant\":\"outline-secondary\"},on:{\"click\":function($event){_vm.$emit('play', media_filename)}}},[(_vm.playerState.current_media_filename != media_filename)?_c('v-icon',{attrs:{\"name\":\"play\"}}):_c('v-icon',{attrs:{\"name\":\"sync-alt\"}})],1),(_vm.playerState.current_media_filename != media_filename)?_c('b-button',{attrs:{\"variant\":\"outline-secondary\"},on:{\"click\":function($event){_vm.deleteMedia(media_filename)}}},[_c('v-icon',{attrs:{\"name\":\"times\"}})],1):_vm._e()],1)],1)])])})),(_vm.err)?_c('b-alert',{attrs:{\"variant\":\"danger\",\"show\":\"\",\"dismissible\":\"\"},domProps:{\"textContent\":_vm._s(_vm.err)}}):_vm._e(),_c('vue2-dropzone',{ref:\"dropzone\",attrs:{\"id\":\"dropzone\",\"options\":_vm.dropzoneOptions},on:{\"vdropzone-file-added\":function($event){_vm.err = ''},\"vdropzone-complete\":function($event){_vm.uploadComplete(_vm.$refs.dropzone)},\"vdropzone-error\":_vm.uploadError}})],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n \n \n \n \n
\n
\n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n \n \n \n\n\n\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlaylistModal.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlaylistModal.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./PlaylistModal.vue?vue&type=template&id=3cd2be0c&\"\nimport script from \"./PlaylistModal.vue?vue&type=script&lang=js&\"\nexport * from \"./PlaylistModal.vue?vue&type=script&lang=js&\"\nimport style0 from \"./PlaylistModal.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"PlaylistModal.vue\"\nexport default component.exports","\n \n
\n
\n \n \n \n \n \n \n
\n \n
\n
\n \n \n \n
\n
\n \n
\n
\n \n \n
\n \n\n\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlayerBar.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlayerBar.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./PlayerBar.vue?vue&type=template&id=db8a2516&\"\nimport script from \"./PlayerBar.vue?vue&type=script&lang=js&\"\nexport * from \"./PlayerBar.vue?vue&type=script&lang=js&\"\nimport style0 from \"./PlayerBar.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"PlayerBar.vue\"\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-card',{staticClass:\"mx-auto\",style:({width: ((_vm.screenGrid.w) + \"px\")}),attrs:{\"id\":\"screenGrid\"}},[_c('div',{attrs:{\"slot\":\"header\"},slot:\"header\"},[_c('b-button-toolbar',{staticClass:\"float-sm-right\",attrs:{\"size\":\"sm\"}},[_c('b-button',{directives:[{name:\"b-modal\",rawName:\"v-b-modal.clientsModal\",modifiers:{\"clientsModal\":true}}],staticStyle:{\"margin-right\":\"5px\"},attrs:{\"variant\":\"outline-secondary\"}},[_c('v-icon',{attrs:{\"name\":\"users\"}})],1),(_vm.editState.active)?_c('b-button-group',{staticClass:\"mx-1\"},[_c('b-button',{attrs:{\"variant\":\"outline-secondary\"},on:{\"click\":_vm.cancelEdit}},[_c('v-icon',{attrs:{\"name\":\"times\"}})],1),_c('b-button',{attrs:{\"variant\":\"outline-secondary\"},on:{\"click\":_vm.applyEdit}},[_c('v-icon',{attrs:{\"name\":\"check\"}})],1)],1):_c('b-button',{attrs:{\"variant\":\"outline-secondary\"},on:{\"click\":_vm.edit}},[_c('v-icon',{attrs:{\"name\":\"edit\"}})],1)],1)],1),_c('div',{attrs:{\"id\":\"screenGridLayout\"},on:{\"click\":function($event){_vm.screenGridLayoutClicked($event)}}},[_c('b-modal',{attrs:{\"id\":\"clientsModal\",\"hide-footer\":\"\",\"title\":\"Clients\"}},[_c('b-list-group',[_c('b-button',{on:{\"click\":_vm.syncMedia}},[_vm._v(\"Sync media\")]),_vm._l((_vm.clients),function(client){return _c('b-list-group-item',{key:client.ip,staticClass:\"flex-column align-items-start\",attrs:{\"variant\":_vm.clientActive(client.ip) ? 'success' : 'danger'}},[_c('div',[_c('div',{staticClass:\"d-flex justify-content-between align-items-center\"},[_c('table',{staticClass:\"clientTable\"},[_c('tr',[_c('td',[_vm._v(\"IP:\")]),_c('td',{domProps:{\"textContent\":_vm._s(client.ip)}})]),_c('tr',[_c('td',[_vm._v(\"Username:\")]),_c('td',{domProps:{\"textContent\":_vm._s(client.username)}})]),_c('tr',[_c('td',[_vm._v(\"Media path:\")]),_c('td',{domProps:{\"textContent\":_vm._s(client.media_path)}})])]),_c('b-badge',{attrs:{\"variant\":\"info\"}},[_vm._v(\"Age: \"+_vm._s(client.age))])],1)])])})],2)],1),_c('b-modal',{ref:\"addModal\",attrs:{\"hide-footer\":\"\",\"title\":\"Add screen for client\"}},[_c('div',{staticClass:\"d-block text-center\"},[_c('b-form',{on:{\"submit\":function($event){$event.preventDefault();_vm.addScreen()}}},[_c('b-form-select',{attrs:{\"options\":_vm.clientIpOptions,\"required\":\"\"},model:{value:(_vm.editState.addScreenIp),callback:function ($$v) {_vm.$set(_vm.editState, \"addScreenIp\", $$v)},expression:\"editState.addScreenIp\"}}),_c('b-button',{attrs:{\"type\":\"submit\",\"variant\":\"primary\"}},[_vm._v(\"Add screen\")])],1)],1)]),_c('grid-layout',{style:({height: ((_vm.screenGrid.h) + \"px\"), cursor: _vm.editState.active ? 'cell' : 'default'}),attrs:{\"layout\":_vm.screens,\"colNum\":1280,\"rowHeight\":1,\"maxRows\":720,\"isDraggable\":_vm.editState.active,\"isResizable\":_vm.editState.active,\"isMirrored\":false,\"margin\":[0, 0],\"verticalCompact\":false,\"autoSize\":true,\"useCssTransforms\":true}},_vm._l((_vm.screens),function(screen){return _c('grid-item',{key:screen.i,staticClass:\"screen\",style:({cursor: _vm.editState.active ? 'grabbing' : 'default'}),attrs:{\"x\":screen.x,\"y\":screen.y,\"w\":screen.w,\"h\":screen.h,\"i\":screen.i}},[_c('b-card',{attrs:{\"footer\":((screen.x) + \",\" + (screen.y) + \" [\" + (screen.w) + \"x\" + (screen.h) + \"]\")}},[_c('div',{attrs:{\"slot\":\"header\"},slot:\"header\"},[_c('span',{domProps:{\"textContent\":_vm._s((\"Screen \" + (screen.i)))}}),_c('b-button-toolbar',{staticClass:\"float-sm-right\"},[_c('b-button-group',{staticClass:\"mx-1\",attrs:{\"size\":\"sm\"}},[_c('b-button',{attrs:{\"variant\":_vm.clientActive(screen.i) ? 'success': 'danger'}},[_c('v-icon',{attrs:{\"name\":\"plug\"}})],1),(_vm.editState.active)?_c('b-button',{attrs:{\"variant\":\"danger\"},on:{\"click\":function($event){_vm.removeScreen(screen.i)}}},[_c('v-icon',{attrs:{\"name\":\"trash-alt\"}})],1):_vm._e()],1)],1)],1)])],1)}))],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n \n \n \n \n \n \n \n \n \n \n
\n \n
\n \n Sync media \n \n \n
\n
\n IP: \n Username: \n Media path: \n
\n
Age: {{client.age}} \n
\n
\n \n \n \n
\n \n \n \n \n Add screen \n \n
\n \n
\n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n\n\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ScreenGrid.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./ScreenGrid.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./ScreenGrid.vue?vue&type=template&id=115dd8f9&\"\nimport script from \"./ScreenGrid.vue?vue&type=script&lang=js&\"\nexport * from \"./ScreenGrid.vue?vue&type=script&lang=js&\"\nimport style0 from \"./ScreenGrid.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"ScreenGrid.vue\"\nexport default component.exports","\n \n \n\n\n\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Player.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??ref--12-0!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Player.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./Player.vue?vue&type=template&id=3c78357e&\"\nimport script from \"./Player.vue?vue&type=script&lang=js&\"\nexport * from \"./Player.vue?vue&type=script&lang=js&\"\nimport style0 from \"./Player.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"Player.vue\"\nexport default component.exports","\n \n \n\n\n\n\n","import mod from \"-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../node_modules/cache-loader/dist/cjs.js??ref--12-0!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/cache-loader/dist/cjs.js??ref--0-0!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=b51b35c4&\"\nimport script from \"./App.vue?vue&type=script&lang=js&\"\nexport * from \"./App.vue?vue&type=script&lang=js&\"\nimport style0 from \"./App.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\ncomponent.options.__file = \"App.vue\"\nexport default component.exports","import Vue from 'vue'\n\nimport VueRouter from 'vue-router'\nVue.use(VueRouter)\n\nimport BootstrapVue from 'bootstrap-vue'\nVue.use(BootstrapVue);\nimport 'bootstrap/dist/css/bootstrap.css'\nimport 'bootstrap-vue/dist/bootstrap-vue.css'\n\nimport Icon from 'vue-awesome/components/Icon'\nimport 'vue-awesome/icons'\nVue.component('v-icon', Icon)\n\nimport VueNativeSock from 'vue-native-websocket'\nVue.use(VueNativeSock, \"ws://\" + location.host + \"/ws\", {\n format: 'json',\n reconnection: true\n})\n\nimport \"./filters\"\n\nimport App from './App.vue'\nimport Player from './components/Player.vue'\n\nVue.config.productionTip = false\n\nconst routes = [\n { path: '/', component: Player }\n]\n\nconst router = new VueRouter({\n routes\n})\n\nnew Vue({\n router,\n render: h => h(App)\n}).$mount('#app')\n","import mod from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Player.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Player.vue?vue&type=style&index=0&lang=css&\"","import mod from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlayerBar.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlayerBar.vue?vue&type=style&index=0&lang=css&\"","import mod from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlaylistModal.vue?vue&type=style&index=0&lang=css&\"; export default mod; export * from \"-!../../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../../node_modules/css-loader/index.js??ref--6-oneOf-1-1!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/postcss-loader/lib/index.js??ref--6-oneOf-1-2!../../node_modules/cache-loader/dist/cjs.js??ref--0-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./PlaylistModal.vue?vue&type=style&index=0&lang=css&\""],"sourceRoot":""}
--------------------------------------------------------------------------------