├── .gitignore
├── LICENSE.md
├── README.md
├── bible
├── .gitignore
├── README.md
├── assets
│ ├── bundle.css
│ ├── bundle.css.map
│ ├── bundle.js
│ ├── bundle.js.map
│ └── global.css
├── config.dist.js
├── controller.html
├── css
│ └── bootstrap-darkly.min.css
├── overlay.html
├── package.json
├── pnpm-lock.yaml
├── rollup.config.js
└── src
│ ├── App.svelte
│ ├── Keypad.svelte
│ ├── WebsocketConfig.svelte
│ ├── main.js
│ └── websocket-client.js
├── clock
└── clock.html
├── css
├── bootstrap-darkly.min.css
└── bootstrap.min.css
├── js
├── bootstrap.min.js
└── controller.js
├── lower-third-simple
├── .gitignore
├── README.md
├── assets
│ ├── bundle.css
│ ├── bundle.css.map
│ ├── bundle.js
│ ├── bundle.js.map
│ └── global.css
├── config.dist.js
├── controller-old.html
├── controller.html
├── css
│ └── bootstrap-darkly.min.css
├── overlay.html
├── package.json
├── rollup.config.js
└── src
│ ├── App.svelte
│ ├── WebsocketConfig.svelte
│ ├── main.js
│ └── websocket-client.js
├── lower-third
├── controller.html
└── overlay.html
├── package.json
├── pnpm-lock.yaml
├── requirements.txt
├── scoreboard
├── controller.html
├── css
│ └── overlay.css
└── overlay.html
├── server.js
└── server.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .DS_Store?
3 | ._*
4 | .Spotlight-V100
5 | .Trashes
6 | ehthumbs.db
7 | Thumbs.db
8 | node_modules
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Filip Hanes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Websocket Overlay
2 | My other project `web-overlays` implements websocket, mqtt and gun protocols for syncing data.
3 |
4 | Simple and powerfull remote controlled html pages useful for overlays in OBS Studio, Casper CD, XSplit or simply fullscreen browser.
5 |
6 | Check out overlays project using [gundb](https://gun.eco/): https://github.com/filiphanes/gun-overlays
7 |
8 | ## Features
9 | - server holds overlay state, not only relay commands
10 | - on refresh or reconnect, state is updated from server for overlay and controller so you don't loose texts etc.
11 | - multiple overlay-controller groups on 1 server instance (via different websocket paths)
12 | - automatic reconnection every 5s
13 |
14 | # Install
15 | You can run server with Python or Nodejs.
16 |
17 | Python 3.6+ is needed. You can download it from https://www.python.org/downloads/
18 |
19 | pip3 install websockets
20 |
21 | NodeJS is needed. You can download it from https://nodejs.org/en/
22 |
23 | npm install ws
24 |
25 | # Run
26 | ## 1. Run websocket server
27 | Python server:
28 |
29 | python3 server.py 127.0.0.1 8089
30 |
31 | Node server:
32 |
33 | node server.js 127.0.0.1 8089
34 |
35 | ## 2. Open controller
36 | Open in browser `lower-third-simple/controller.html`.
37 |
38 | ## 3. Open overlay
39 | Open in browser `lower-third-simple/overlay.html`
40 |
41 | # Setup in playout software
42 | Setting you might need to change is websocket URI in `overlay.html` and `controller.html` in directory `your-overlay/`.
43 | FSet it to the same IP address and port as your server is running on.
44 |
45 | WEBSOCKET_URI = "ws://127.0.0.1:8089/"
46 |
47 | ## OBS Studio
48 | 1. Click the plus button under Sources
49 | 2. Select BrowserSource
50 | 3. Name the source and click "OK"
51 | 4. Check the "Local file" box
52 | 5. Click the "Browse" button on the right and select the client.html you want to use
53 | 6. Set the Resolution to 1920x1080 (Width: 1920; Height: 1080) or the overlay resolution
54 | 7. Set FPS to you stream FPS (examples: 25, 30, 50, 60)
55 |
56 | ## Caspar CG
57 | https://github.com/CasparCG/help/wiki/Media:-HTML-Templates
58 |
59 | ## ProPresenter
60 | https://learn.renewedvision.com/propresenter6/the-features-of-propresenter/web-view
61 |
62 | ## XSplit
63 | https://help.xsplit.com/en/articles/5142996-webpage
64 |
65 |
66 | # New overlays
67 | You can create your own overlay and associated controller without implementing server.
68 |
69 | ## Server API
70 | Server API is made simple and universal. Server only keeps state of overlay and broadcasts state updates from controller to all connected clients.
71 |
72 | - Controller and overlay connects to the same websocket `ws://host:port/path`. Path can be any string you choose.
73 | - Server keeps 1 state object for each path, so you can have multiple overlays with different state on one server with different path. State object is not persisted between server restarts. State is kept only in memory.
74 | - Each websocket message is json. Server keeps state of overlay in js object (dict in python).
75 | - When server recieves json it updates all key/value pairs, but not removes existing.
76 | - When new client (overlay or controller) connects to websocket, servers sends him full state in json message.
77 | - Controller is meant to send state updates (websocket messages) to server which are broadcasted to all connected overlays and controllers (you can control multiple opened overlays with multiple opened controllers).
78 | - When controller recieves message he updates his internal state.
79 | - Overlay usually only recieves updates from server, but in some situation may want to send updates from overlay (i.e. set value state after finishing animation).
80 |
81 | Usually you want to send commands to overlay like show or hide something. With state updating aproach you don't send `{"command":"show-counter"}` and `{"command":"hide-counter"}`, but rather `{"show-counter":true}` and `{"show-counter":false}`.
82 |
83 | This approach is more robust against errors on network. Controller and overlay can connect and disconnect any time and he recieves full state and don't need to replay commands from history.
84 |
85 | # Thanks
86 | This project was inspired by
87 | - https://github.com/lebaston100/htmlOverlayFramework
88 | - https://github.com/hberntsen/websocket-png-overlayer
89 | - https://github.com/Scrxtchy/ts3-overlay-ws
90 | - https://github.com/slugalisk/win-loss-overlay
91 |
92 | # TODO
93 | - more overlays
94 | - remember controller config in browser session storage
95 | - public websocket server with public overlays
96 |
--------------------------------------------------------------------------------
/bible/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.js
3 |
--------------------------------------------------------------------------------
/bible/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filiphanes/websocket-overlays/14a4688f6c1b26d65e5c9a87b66004eb53723f6a/bible/README.md
--------------------------------------------------------------------------------
/bible/assets/bundle.css:
--------------------------------------------------------------------------------
1 | .control-button.svelte-18fg1qh{width:6rem}.books-filter.svelte-18fg1qh,.address-filter.svelte-18fg1qh{display:block;width:49%;margin:0 1% 0 0;padding:0;height:10rem;overflow:scroll;float:left}.book-item.svelte-18fg1qh{width:100%;margin:0 0 .25rem 0;padding:.25rem .5rem;text-align:left}.address-item.svelte-18fg1qh{width:100%;margin:0 0 .25rem 0}.address-set.svelte-18fg1qh{width:auto;padding:.25rem .5rem;margin:0;text-align:left}.address-remove.svelte-18fg1qh{margin:0;max-width:2rem;padding:.25rem .5rem}.vers.svelte-18fg1qh,.address.svelte-18fg1qh{color:white}body{color:white}
2 | .keypad.svelte-1bz6mfx{display:grid;grid-template-columns:repeat(3, 2.8rem);grid-template-rows:repeat(5, 2.8rem);grid-gap:0.2rem;margin:0 0 0.5rem 0}button.svelte-1bz6mfx{margin:0;line-height:2rem;width:2.8rem}
3 |
4 | /*# sourceMappingURL=bundle.css.map */
--------------------------------------------------------------------------------
/bible/assets/bundle.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "file": "bundle.css",
4 | "sources": [
5 | "../src/App.svelte",
6 | "../src/Keypad.svelte"
7 | ],
8 | "sourcesContent": [
9 | "\n\n\n\n
\n \n {bookFilter=''}} style=\"max-width: 2rem;\">× \n
\n\n {#each filteredBooks as book}\n {book.name} \n {/each}\n
\n\n {#each filteredLastAddresses as address, i}\n
\n {addressAsString(address)} \n × \n
\n {/each}\n
\n\n\n Kapitola: {selected.chapter}\n \n
\n\n Verš: {selected.verse}\n \n
\n\n\n {#if shown}Skryť{:else}Zobraziť{/if}\n \nZmeniť \n{line1}
\n{@html line2} \n\n",
10 | "\n\n\n\n\n 1 \n 2 \n 3 \n\n 4 \n 5 \n 6 \n\n 7 \n 8 \n 9 \n\n ← \n 0 \n C \n\n -1 \n +1 \n
\n"
11 | ],
12 | "names": [],
13 | "mappings": "AAgIE,eAAe,eAAC,CAAC,AACf,KAAK,CAAE,IAAI,AAEb,CAAC,AACD,4BAAa,CACb,eAAe,eAAC,CAAC,AACf,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAChB,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,KAAK,CACb,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,IAAI,AACb,CAAC,AACD,UAAU,eAAC,CAAC,AACV,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CACpB,OAAO,CAAE,MAAM,CAAC,KAAK,CACrB,UAAU,CAAE,IAAI,AAClB,CAAC,AACD,aAAa,eAAC,CAAC,AACb,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,AACtB,CAAC,AACD,YAAY,eAAC,CAAC,AACZ,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,MAAM,CAAC,KAAK,CACrB,MAAM,CAAE,CAAC,CACT,UAAU,CAAE,IAAI,AAClB,CAAC,AACD,eAAe,eAAC,CAAC,AACf,MAAM,CAAE,CAAC,CACT,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,MAAM,CAAC,KAAK,AACvB,CAAC,AACD,oBAAK,CACL,QAAQ,eAAC,CAAC,AACR,KAAK,CAAE,KAAK,AACd,CAAC,AACO,IAAI,AAAE,CAAC,AACb,KAAK,CAAE,KAAK,AACd,CAAC;ACzJD,OAAO,eAAC,CAAC,AACP,OAAO,CAAE,IAAI,CACb,qBAAqB,CAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CACxC,kBAAkB,CAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CACrC,QAAQ,CAAE,MAAM,CAChB,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,AACtB,CAAC,AAED,MAAM,eAAC,CAAC,AACN,MAAM,CAAE,CAAC,CACT,WAAW,CAAE,IAAI,CACjB,KAAK,CAAE,MAAM,AACf,CAAC"
14 | }
--------------------------------------------------------------------------------
/bible/assets/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | color: #333;
9 | margin: 0;
10 | padding: 8px;
11 | box-sizing: border-box;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
13 | }
14 |
15 | a {
16 | color: rgb(0,100,200);
17 | text-decoration: none;
18 | }
19 |
20 | a:hover {
21 | text-decoration: underline;
22 | }
23 |
24 | a:visited {
25 | color: rgb(0,80,160);
26 | }
27 |
28 | label {
29 | display: block;
30 | }
31 |
32 | input, button, select, textarea {
33 | font-family: inherit;
34 | font-size: inherit;
35 | padding: 0.4em;
36 | margin: 0 0 0.5em 0;
37 | box-sizing: border-box;
38 | border: 1px solid #ccc;
39 | border-radius: 2px;
40 | }
41 |
42 | input:disabled {
43 | color: #ccc;
44 | }
45 |
46 | input[type="range"] {
47 | height: 0;
48 | }
49 |
50 | button {
51 | background-color: #f4f4f4;
52 | outline: none;
53 | }
54 |
55 | button:active {
56 | background-color: #ddd;
57 | }
58 |
59 | button:focus {
60 | border-color: #666;
61 | }
--------------------------------------------------------------------------------
/bible/config.dist.js:
--------------------------------------------------------------------------------
1 | WEBSOCKET_URI = "ws://127.0.0.1:8089/bible"
2 |
--------------------------------------------------------------------------------
/bible/controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Bible verses
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/bible/overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lower Third
6 |
7 |
49 |
50 |
51 |
52 |
Main header line
53 |
54 |
55 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/bible/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "devDependencies": {
5 | "@rollup/plugin-json": "^4.0.0",
6 | "npm-run-all": "^4.1.5",
7 | "rollup": "^1.27.5",
8 | "rollup-plugin-commonjs": "^9.3.4",
9 | "rollup-plugin-livereload": "^1.0.4",
10 | "rollup-plugin-node-resolve": "^4.2.4",
11 | "rollup-plugin-svelte": "^5.1.1",
12 | "rollup-plugin-terser": "^4.0.4",
13 | "sirv-cli": "^0.4.5",
14 | "svelte": "^3.15.0"
15 | },
16 | "scripts": {
17 | "build": "rollup -c",
18 | "autobuild": "rollup -c -w",
19 | "dev": "run-p start:dev autobuild",
20 | "start": "sirv public",
21 | "start:dev": "sirv public --dev"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/bible/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from 'rollup-plugin-node-resolve';
3 | import commonjs from 'rollup-plugin-commonjs';
4 | import json from '@rollup/plugin-json';
5 | import livereload from 'rollup-plugin-livereload';
6 | import { terser } from 'rollup-plugin-terser';
7 |
8 | const production = !process.env.ROLLUP_WATCH;
9 |
10 | export default {
11 | input: 'src/main.js',
12 | output: {
13 | sourcemap: true,
14 | format: 'iife',
15 | name: 'app',
16 | file: 'assets/bundle.js'
17 | },
18 | plugins: [
19 | json(),
20 | svelte({
21 | // enable run-time checks when not in production
22 | dev: !production,
23 | // we'll extract any component CSS out into
24 | // a separate file better for performance
25 | css: css => {
26 | css.write('assets/bundle.css');
27 | }
28 | }),
29 |
30 | // If you have external dependencies installed from
31 | // npm, you'll most likely need these plugins. In
32 | // some cases you'll need additional configuration
33 | // consult the documentation for details:
34 | // https://github.com/rollup/rollup-plugin-commonjs
35 | resolve({ browser: true }),
36 | commonjs(),
37 |
38 | // Watch the `public` directory and refresh the
39 | // browser on changes when not in production
40 | !production && livereload('assets'),
41 |
42 | // If we're building for production (npm run build
43 | // instead of npm run dev), minify
44 | production && terser()
45 | ],
46 | watch: {
47 | clearScreen: false
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/bible/src/App.svelte:
--------------------------------------------------------------------------------
1 |
127 |
128 |
172 |
173 |
174 |
175 | {bookFilter=''}} style="max-width: 2rem;">×
176 |
177 |
178 | {#each filteredBooks as book}
179 | {book.name}
180 | {/each}
181 |
182 |
183 | {#each filteredLastAddresses as address, i}
184 |
185 | {addressAsString(address)}
186 | ×
187 |
188 | {/each}
189 |
190 |
191 |
192 | Kapitola: {selected.chapter}
193 |
194 |
195 |
196 | Verš: {selected.verse}
197 |
198 |
199 |
200 |
201 | {#if shown}Skryť{:else}Zobraziť{/if}
202 |
203 | Zmeniť
204 | {line1}
205 | {@html line2}
206 |
207 |
--------------------------------------------------------------------------------
/bible/src/Keypad.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
31 |
32 |
33 | 1
34 | 2
35 | 3
36 |
37 | 4
38 | 5
39 | 6
40 |
41 | 7
42 | 8
43 | 9
44 |
45 | ←
46 | 0
47 | C
48 |
49 | -1
50 | +1
51 |
52 |
--------------------------------------------------------------------------------
/bible/src/WebsocketConfig.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
Connection settings
7 |
17 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/bible/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | var app = new App({
4 | target: document.body
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/bible/src/websocket-client.js:
--------------------------------------------------------------------------------
1 | //Modify this to point to the ip address where your server is running on
2 | WEBSOCKET_URI = WEBSOCKET_URI || "ws://127.0.0.1:8089/";
3 |
4 | let websocket;
5 | let socketIsOpen = false;
6 | let intervalID = 0;
7 | let closedByUser = false;
8 |
9 | export function sendCommand(obj) {
10 | console.log(JSON.stringify(obj));
11 | if (socketIsOpen) {
12 | websocket.send(JSON.stringify(obj));
13 | } else {
14 | console.error('Not connected\n');
15 | }
16 | }
17 |
18 |
19 | export function doConnect(onMessage) {
20 | websocket = new WebSocket(WEBSOCKET_URI);
21 | websocket.onopen = function(evt) {
22 | socketIsOpen = true;
23 | console.info("Connection opened");
24 | clearInterval(intervalID);
25 | intervalID = 0;
26 | };
27 | websocket.onclose = function(evt) {
28 | socketIsOpen = true;
29 | if (!intervalID && !closedByUser) {
30 | intervalID = setInterval(doConnect, 5000);
31 | } else if (closedByUser) {
32 | closedByUser = false;
33 | }
34 | console.info("Connection closed");
35 | };
36 | websocket.onmessage = function(evt) {
37 | var jsonOBJ = JSON.parse(evt.data);
38 | console.log(evt.data);
39 | onMessage(jsonOBJ);
40 | };
41 | websocket.onerror = function(evt) {
42 | console.error('Connection failed, is the Server running?');
43 | socketIsOpen = false;
44 | if (!intervalID) {
45 | intervalID = setInterval(doConnect, 5000);
46 | }
47 | };
48 | }
49 |
50 | export function doDisconnect() {
51 | socketIsOpen = false;
52 | closedByUser = true;
53 | websocket.close();
54 | }
--------------------------------------------------------------------------------
/clock/clock.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v4.3.1 (https://getbootstrap.com/)
3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active",Wn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',qn=".dropdown-toggle",Mn="> .dropdown-menu .active",Kn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&g(this._element).hasClass(Pn)||g(this._element).hasClass(Ln))){var t,i,e=g(this._element).closest(xn)[0],o=_.getSelectorFromElement(this._element);if(e){var r="UL"===e.nodeName||"OL"===e.nodeName?Un:Fn;i=(i=g.makeArray(g(e).find(r)))[i.length-1]}var s=g.Event(On.HIDE,{relatedTarget:this._element}),a=g.Event(On.SHOW,{relatedTarget:i});if(i&&g(i).trigger(s),g(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){o&&(t=document.querySelector(o)),this._activate(this._element,e);var l=function(){var t=g.Event(On.HIDDEN,{relatedTarget:n._element}),e=g.Event(On.SHOWN,{relatedTarget:i});g(i).trigger(t),g(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){g.removeData(this._element,wn),this._element=null},t._activate=function(t,e,n){var i=this,o=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?g(e).children(Fn):g(e).find(Un))[0],r=n&&o&&g(o).hasClass(jn),s=function(){return i._transitionComplete(t,o,n)};if(o&&r){var a=_.getTransitionDurationFromElement(o);g(o).removeClass(Hn).one(_.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){g(e).removeClass(Pn);var i=g(e.parentNode).find(Mn)[0];i&&g(i).removeClass(Pn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(g(t).addClass(Pn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),_.reflow(t),t.classList.contains(jn)&&t.classList.add(Hn),t.parentNode&&g(t.parentNode).hasClass(kn)){var o=g(t).closest(Rn)[0];if(o){var r=[].slice.call(o.querySelectorAll(qn));g(r).addClass(Pn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=g(this),e=t.data(wn);if(e||(e=new i(this),t.data(wn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}}]),i}();g(document).on(On.CLICK_DATA_API,Wn,function(t){t.preventDefault(),Kn._jQueryInterface.call(g(this),"show")}),g.fn.tab=Kn._jQueryInterface,g.fn.tab.Constructor=Kn,g.fn.tab.noConflict=function(){return g.fn.tab=Nn,Kn._jQueryInterface};var Qn="toast",Bn="bs.toast",Vn="."+Bn,Yn=g.fn[Qn],zn={CLICK_DISMISS:"click.dismiss"+Vn,HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn},Xn="fade",$n="hide",Gn="show",Jn="showing",Zn={animation:"boolean",autohide:"boolean",delay:"number"},ti={animation:!0,autohide:!0,delay:500},ei='[data-dismiss="toast"]',ni=function(){function i(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var t=i.prototype;return t.show=function(){var t=this;g(this._element).trigger(zn.SHOW),this._config.animation&&this._element.classList.add(Xn);var e=function(){t._element.classList.remove(Jn),t._element.classList.add(Gn),g(t._element).trigger(zn.SHOWN),t._config.autohide&&t.hide()};if(this._element.classList.remove($n),this._element.classList.add(Jn),this._config.animation){var n=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},t.hide=function(t){var e=this;this._element.classList.contains(Gn)&&(g(this._element).trigger(zn.HIDE),t?this._close():this._timeout=setTimeout(function(){e._close()},this._config.delay))},t.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains(Gn)&&this._element.classList.remove(Gn),g(this._element).off(zn.CLICK_DISMISS),g.removeData(this._element,Bn),this._element=null,this._config=null},t._getConfig=function(t){return t=l({},ti,g(this._element).data(),"object"==typeof t&&t?t:{}),_.typeCheckConfig(Qn,t,this.constructor.DefaultType),t},t._setListeners=function(){var t=this;g(this._element).on(zn.CLICK_DISMISS,ei,function(){return t.hide(!0)})},t._close=function(){var t=this,e=function(){t._element.classList.add($n),g(t._element).trigger(zn.HIDDEN)};if(this._element.classList.remove(Gn),this._config.animation){var n=_.getTransitionDurationFromElement(this._element);g(this._element).one(_.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},i._jQueryInterface=function(n){return this.each(function(){var t=g(this),e=t.data(Bn);if(e||(e=new i(this,"object"==typeof n&&n),t.data(Bn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n](this)}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"DefaultType",get:function(){return Zn}},{key:"Default",get:function(){return ti}}]),i}();g.fn[Qn]=ni._jQueryInterface,g.fn[Qn].Constructor=ni,g.fn[Qn].noConflict=function(){return g.fn[Qn]=Yn,ni._jQueryInterface},function(){if("undefined"==typeof g)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=g.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||4<=t[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(),t.Util=_,t.Alert=p,t.Button=P,t.Carousel=lt,t.Collapse=bt,t.Dropdown=Jt,t.Modal=ve,t.Popover=sn,t.Scrollspy=Dn,t.Tab=Kn,t.Toast=ni,t.Tooltip=Be,Object.defineProperty(t,"__esModule",{value:!0})});
7 | //# sourceMappingURL=bootstrap.min.js.map
--------------------------------------------------------------------------------
/js/controller.js:
--------------------------------------------------------------------------------
1 | //Modify this to point to the ip address where your server is running on
2 | WEBSOCKET_URI = WEBSOCKET_URI || "ws://127.0.0.1:8089/";
3 |
4 | var socketIsOpen = 0;
5 | var intervalID = 0;
6 | var closedByUser = 0;
7 |
8 | window.addEventListener("load", function () {
9 | document.getElementById("WEBSOCKET_URI").value = document.getElementById("WEBSOCKET_URI").value || WEBSOCKET_URI;
10 | document.getElementById("inputtext").value = '{"ping":"test"}'
11 | document.getElementById("btt_disconnect").disabled = true;
12 | doConnect();
13 | }, false);
14 |
15 | function getInput(id) {
16 | return document.getElementById(id).value;
17 | }
18 |
19 | function sendCommand(obj) {
20 | if (socketIsOpen) {
21 | websocket.send(JSON.stringify(obj));
22 | } else {
23 | console.log('Fail: Not connected\n');
24 | }
25 | }
26 |
27 |
28 | function doConnect() {
29 | WEBSOCKET_URI = document.getElementById("WEBSOCKET_URI").value;
30 | websocket = new WebSocket(WEBSOCKET_URI);
31 | websocket.onopen = function(evt) {
32 | socketIsOpen = 1;
33 | writeToScreen("\nInfo: Connection opened");
34 | document.getElementById("btt_connect").disabled = true;
35 | document.getElementById("btt_disconnect").disabled = false;
36 | clearInterval(intervalID);
37 | intervalID = 0;
38 | };
39 | websocket.onclose = function(evt) {
40 | socketIsOpen = 0;
41 | if (!intervalID && !closedByUser) {
42 | intervalID = setInterval(doConnect, 5000);
43 | } else if (closedByUser) {
44 | closedByUser = 0;
45 | }
46 | writeToScreen("\nInfo: Connection closed");
47 | document.getElementById("btt_connect").disabled = false;
48 | document.getElementById("btt_disconnect").disabled = true;
49 | };
50 | websocket.onmessage = function(evt) {
51 | var jsonOBJ = JSON.parse(evt.data);
52 | writeToScreen('\n' + evt.data);
53 | onMessage(jsonOBJ);
54 | };
55 | websocket.onerror = function(evt) {
56 | writeToScreen('\nConnection failed, is the Server running?');
57 | socketIsOpen = 0;
58 | document.getElementById("btt_connect").disabled = false;
59 | document.getElementById("btt_disconnect").disabled = true;
60 | if (!intervalID) {
61 | intervalID = setInterval(doConnect, 5000);
62 | }
63 | };
64 | }
65 |
66 | function writeToScreen(message) {
67 | document.getElementById("outputtext").value += message;
68 | document.getElementById("outputtext").scrollTop = document.getElementById("outputtext").scrollHeight;
69 | }
70 |
71 | function doDisconnect() {
72 | socketIsOpen = 0;
73 | closedByUser = 1;
74 | websocket.close();
75 | }
--------------------------------------------------------------------------------
/lower-third-simple/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.js
3 |
--------------------------------------------------------------------------------
/lower-third-simple/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filiphanes/websocket-overlays/14a4688f6c1b26d65e5c9a87b66004eb53723f6a/lower-third-simple/README.md
--------------------------------------------------------------------------------
/lower-third-simple/assets/bundle.css:
--------------------------------------------------------------------------------
1 | .control-button.svelte-12wxbio{max-width:6rem}
2 |
3 | /*# sourceMappingURL=bundle.css.map */
--------------------------------------------------------------------------------
/lower-third-simple/assets/bundle.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "file": "bundle.css",
4 | "sources": [
5 | "../src/App.svelte"
6 | ],
7 | "sourcesContent": [
8 | "\n\n\n\n\t{#each lines as line, i}\n\t
\n\t\t changeLine(i)}>\n\t\ttoggleLine(i)}>\n\t\t\t{#if shown === i}Hide{:else}Show{/if}\n\t\t \n\t
\n\t{/each}\n
\n"
9 | ],
10 | "names": [],
11 | "mappings": "AACA,eAAe,eAAC,CAAC,AAChB,SAAS,CAAE,IAAI,AAChB,CAAC"
12 | }
--------------------------------------------------------------------------------
/lower-third-simple/assets/bundle.js:
--------------------------------------------------------------------------------
1 | var app=function(){"use strict";function n(){}function t(n){return n()}function e(){return Object.create(null)}function o(n){n.forEach(t)}function r(n){return"function"==typeof n}function c(n,t){return n!=n?t==t:n!==t||n&&"object"==typeof n||"function"==typeof n}function i(n,t){n.appendChild(t)}function l(n,t,e){n.insertBefore(t,e||null)}function u(n){n.parentNode.removeChild(n)}function s(n){return document.createElement(n)}function a(n){return document.createTextNode(n)}function f(){return a(" ")}function d(n,t,e,o){return n.addEventListener(t,e,o),()=>n.removeEventListener(t,e,o)}function h(n,t,e){null==e?n.removeAttribute(t):n.setAttribute(t,e)}function p(n,t){(null!=t||n.value)&&(n.value=t)}let g;function $(n){g=n}function m(){if(!g)throw new Error("Function called outside component initialization");return g}const v=[],y=[],_=[],b=[],w=Promise.resolve();let x=!1;function E(n){_.push(n)}function O(){const n=new Set;do{for(;v.length;){const n=v.shift();$(n),S(n.$$)}for(;y.length;)y.pop()();for(let t=0;t<_.length;t+=1){const e=_[t];n.has(e)||(e(),n.add(e))}_.length=0}while(v.length);for(;b.length;)b.pop()();x=!1}function S(n){n.fragment&&(n.update(n.dirty),o(n.before_update),n.fragment.p(n.dirty,n.ctx),n.dirty=null,n.after_update.forEach(E))}const C=new Set;function k(n,t){n.$$.dirty||(v.push(n),x||(x=!0,w.then(O)),n.$$.dirty=e()),n.$$.dirty[t]=!0}function N(c,i,l,u,s,a){const f=g;$(c);const d=i.props||{},h=c.$$={fragment:null,ctx:null,props:a,update:n,not_equal:s,bound:e(),on_mount:[],on_destroy:[],before_update:[],after_update:[],context:new Map(f?f.$$.context:[]),callbacks:e(),dirty:null};let p=!1;var m,v,y;h.ctx=l?l(c,d,(n,t,e=t)=>(h.ctx&&s(h.ctx[n],h.ctx[n]=e)&&(h.bound[n]&&h.bound[n](e),p&&k(c,n)),t)):d,h.update(),p=!0,o(h.before_update),h.fragment=u(h.ctx),i.target&&(i.hydrate?h.fragment.l((y=i.target,Array.from(y.childNodes))):h.fragment.c(),i.intro&&((m=c.$$.fragment)&&m.i&&(C.delete(m),m.i(v))),function(n,e,c){const{fragment:i,on_mount:l,on_destroy:u,after_update:s}=n.$$;i.m(e,c),E(()=>{const e=l.map(t).filter(r);u?u.push(...e):o(e),n.$$.on_mount=[]}),s.forEach(E)}(c,i.target,i.anchor),O()),$(f)}class I{$destroy(){var t,e;e=1,(t=this).$$.fragment&&(o(t.$$.on_destroy),t.$$.fragment.d(e),t.$$.on_destroy=t.$$.fragment=null,t.$$.ctx={}),this.$destroy=n}$on(n,t){const e=this.$$.callbacks[n]||(this.$$.callbacks[n]=[]);return e.push(t),()=>{const n=e.indexOf(t);-1!==n&&e.splice(n,1)}}$set(){}}let T;WEBSOCKET_URI=WEBSOCKET_URI||"ws://127.0.0.1:8089/";let B=!1,L=0,W=!1;function j(n){console.log(JSON.stringify(n)),B?T.send(JSON.stringify(n)):console.error("Not connected\n")}function A(n){(T=new WebSocket(WEBSOCKET_URI)).onopen=function(n){B=!0,console.info("Connection opened"),clearInterval(L),L=0},T.onclose=function(n){B=!0,L||W?W&&(W=!1):L=setInterval(A,5e3),console.info("Connection closed")},T.onmessage=function(t){var e=JSON.parse(t.data);console.log(t.data),n(e)},T.onerror=function(n){console.error("Connection failed, is the Server running?"),B=!1,L||(L=setInterval(A,5e3))}}function J(n,t,e){const o=Object.create(n);return o.line=t[e],o.each_value=t,o.i=e,o}function K(n){var t;return{c(){t=a("Show")},m(n,e){l(n,t,e)},d(n){n&&u(t)}}}function P(n){var t;return{c(){t=a("Hide")},m(n,e){l(n,t,e)},d(n){n&&u(t)}}}function R(n){var t,e,r,c,a,g,$;function m(){n.input_input_handler.call(e,n)}function v(...t){return n.change_handler(n,...t)}function y(n,t){return t.shown===t.i?P:K}var _=y(0,n),b=_(n);function w(...t){return n.click_handler(n,...t)}return{c(){t=s("div"),e=s("input"),r=f(),c=s("button"),b.c(),g=f(),h(e,"class","form-control"),h(e,"type","text"),h(e,"placeholder","Text"),h(c,"class",a="form-control btn "+(n.shown===n.i?"btn-danger":"btn-primary")+" control-button svelte-12wxbio"),h(t,"class","input-group"),$=[d(e,"input",m),d(e,"change",v),d(c,"click",w)]},m(o,u){l(o,t,u),i(t,e),p(e,n.line),i(t,r),i(t,c),b.m(c,null),i(t,g)},p(t,o){n=o,t.lines&&e.value!==n.line&&p(e,n.line),_!==(_=y(0,n))&&(b.d(1),(b=_(n))&&(b.c(),b.m(c,null))),t.shown&&a!==(a="form-control btn "+(n.shown===n.i?"btn-danger":"btn-primary")+" control-button svelte-12wxbio")&&h(c,"class",a)},d(n){n&&u(t),b.d(),o($)}}}function U(t){var e;let o=t.lines,r=[];for(let n=0;n=0})}function l(){e("lines",o=[...o.filter(n=>n),""])}function u(n){console.log(n),n.hasOwnProperty("lines")&&(e("lines",o=n.lines),l()),n.hasOwnProperty("shown")&&e("shown",r=n.shown)}function s(n){let t={lines:o};r===n&&(t.line1=o[n]),j(t),l()}c=(async()=>{A(u)}),m().$$.on_mount.push(c),function(n){m().$$.on_destroy.push(n)}(async()=>{B=!1,W=!0,T.close()}),l();return{lines:o,shown:r,toggleLine:i,changeLine:s,input_input_handler:function({line:n,each_value:t,i:r}){t[r]=this.value,e("lines",o)},change_handler:({i:n},t)=>s(n),click_handler:({i:n},t)=>i(n)}}return new class extends I{constructor(n){super(),N(this,n,q,U,c,[])}}({target:document.body})}();
2 | //# sourceMappingURL=bundle.js.map
3 |
--------------------------------------------------------------------------------
/lower-third-simple/assets/bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"bundle.js","sources":["../node_modules/.registry.npmjs.org/svelte/3.12.1/node_modules/svelte/internal/index.mjs","../src/websocket-client.js","../src/App.svelte","../src/main.js"],"sourcesContent":["function noop() { }\nconst identity = x => x;\nfunction assign(tar, src) {\n // @ts-ignore\n for (const k in src)\n tar[k] = src[k];\n return tar;\n}\nfunction is_promise(value) {\n return value && typeof value === 'object' && typeof value.then === 'function';\n}\nfunction add_location(element, file, line, column, char) {\n element.__svelte_meta = {\n loc: { file, line, column, char }\n };\n}\nfunction run(fn) {\n return fn();\n}\nfunction blank_object() {\n return Object.create(null);\n}\nfunction run_all(fns) {\n fns.forEach(run);\n}\nfunction is_function(thing) {\n return typeof thing === 'function';\n}\nfunction safe_not_equal(a, b) {\n return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}\nfunction not_equal(a, b) {\n return a != a ? b == b : a !== b;\n}\nfunction validate_store(store, name) {\n if (!store || typeof store.subscribe !== 'function') {\n throw new Error(`'${name}' is not a store with a 'subscribe' method`);\n }\n}\nfunction subscribe(store, callback) {\n const unsub = store.subscribe(callback);\n return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;\n}\nfunction get_store_value(store) {\n let value;\n subscribe(store, _ => value = _)();\n return value;\n}\nfunction component_subscribe(component, store, callback) {\n component.$$.on_destroy.push(subscribe(store, callback));\n}\nfunction create_slot(definition, ctx, fn) {\n if (definition) {\n const slot_ctx = get_slot_context(definition, ctx, fn);\n return definition[0](slot_ctx);\n }\n}\nfunction get_slot_context(definition, ctx, fn) {\n return definition[1]\n ? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))\n : ctx.$$scope.ctx;\n}\nfunction get_slot_changes(definition, ctx, changed, fn) {\n return definition[1]\n ? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {})))\n : ctx.$$scope.changed || {};\n}\nfunction exclude_internal_props(props) {\n const result = {};\n for (const k in props)\n if (k[0] !== '$')\n result[k] = props[k];\n return result;\n}\nfunction once(fn) {\n let ran = false;\n return function (...args) {\n if (ran)\n return;\n ran = true;\n fn.call(this, ...args);\n };\n}\nfunction null_to_empty(value) {\n return value == null ? '' : value;\n}\nfunction set_store_value(store, ret, value = ret) {\n store.set(value);\n return ret;\n}\n\nconst is_client = typeof window !== 'undefined';\nlet now = is_client\n ? () => window.performance.now()\n : () => Date.now();\nlet raf = is_client ? cb => requestAnimationFrame(cb) : noop;\n// used internally for testing\nfunction set_now(fn) {\n now = fn;\n}\nfunction set_raf(fn) {\n raf = fn;\n}\n\nconst tasks = new Set();\nlet running = false;\nfunction run_tasks() {\n tasks.forEach(task => {\n if (!task[0](now())) {\n tasks.delete(task);\n task[1]();\n }\n });\n running = tasks.size > 0;\n if (running)\n raf(run_tasks);\n}\nfunction clear_loops() {\n // for testing...\n tasks.forEach(task => tasks.delete(task));\n running = false;\n}\nfunction loop(fn) {\n let task;\n if (!running) {\n running = true;\n raf(run_tasks);\n }\n return {\n promise: new Promise(fulfil => {\n tasks.add(task = [fn, fulfil]);\n }),\n abort() {\n tasks.delete(task);\n }\n };\n}\n\nfunction append(target, node) {\n target.appendChild(node);\n}\nfunction insert(target, node, anchor) {\n target.insertBefore(node, anchor || null);\n}\nfunction detach(node) {\n node.parentNode.removeChild(node);\n}\nfunction destroy_each(iterations, detaching) {\n for (let i = 0; i < iterations.length; i += 1) {\n if (iterations[i])\n iterations[i].d(detaching);\n }\n}\nfunction element(name) {\n return document.createElement(name);\n}\nfunction element_is(name, is) {\n return document.createElement(name, { is });\n}\nfunction object_without_properties(obj, exclude) {\n // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion\n const target = {};\n for (const k in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, k)\n // @ts-ignore\n && exclude.indexOf(k) === -1) {\n // @ts-ignore\n target[k] = obj[k];\n }\n }\n return target;\n}\nfunction svg_element(name) {\n return document.createElementNS('http://www.w3.org/2000/svg', name);\n}\nfunction text(data) {\n return document.createTextNode(data);\n}\nfunction space() {\n return text(' ');\n}\nfunction empty() {\n return text('');\n}\nfunction listen(node, event, handler, options) {\n node.addEventListener(event, handler, options);\n return () => node.removeEventListener(event, handler, options);\n}\nfunction prevent_default(fn) {\n return function (event) {\n event.preventDefault();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_propagation(fn) {\n return function (event) {\n event.stopPropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction self(fn) {\n return function (event) {\n // @ts-ignore\n if (event.target === this)\n fn.call(this, event);\n };\n}\nfunction attr(node, attribute, value) {\n if (value == null)\n node.removeAttribute(attribute);\n else\n node.setAttribute(attribute, value);\n}\nfunction set_attributes(node, attributes) {\n for (const key in attributes) {\n if (key === 'style') {\n node.style.cssText = attributes[key];\n }\n else if (key in node) {\n node[key] = attributes[key];\n }\n else {\n attr(node, key, attributes[key]);\n }\n }\n}\nfunction set_svg_attributes(node, attributes) {\n for (const key in attributes) {\n attr(node, key, attributes[key]);\n }\n}\nfunction set_custom_element_data(node, prop, value) {\n if (prop in node) {\n node[prop] = value;\n }\n else {\n attr(node, prop, value);\n }\n}\nfunction xlink_attr(node, attribute, value) {\n node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}\nfunction get_binding_group_value(group) {\n const value = [];\n for (let i = 0; i < group.length; i += 1) {\n if (group[i].checked)\n value.push(group[i].__value);\n }\n return value;\n}\nfunction to_number(value) {\n return value === '' ? undefined : +value;\n}\nfunction time_ranges_to_array(ranges) {\n const array = [];\n for (let i = 0; i < ranges.length; i += 1) {\n array.push({ start: ranges.start(i), end: ranges.end(i) });\n }\n return array;\n}\nfunction children(element) {\n return Array.from(element.childNodes);\n}\nfunction claim_element(nodes, name, attributes, svg) {\n for (let i = 0; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeName === name) {\n for (let j = 0; j < node.attributes.length; j += 1) {\n const attribute = node.attributes[j];\n if (!attributes[attribute.name])\n node.removeAttribute(attribute.name);\n }\n return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes\n }\n }\n return svg ? svg_element(name) : element(name);\n}\nfunction claim_text(nodes, data) {\n for (let i = 0; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeType === 3) {\n node.data = '' + data;\n return nodes.splice(i, 1)[0];\n }\n }\n return text(data);\n}\nfunction claim_space(nodes) {\n return claim_text(nodes, ' ');\n}\nfunction set_data(text, data) {\n data = '' + data;\n if (text.data !== data)\n text.data = data;\n}\nfunction set_input_value(input, value) {\n if (value != null || input.value) {\n input.value = value;\n }\n}\nfunction set_input_type(input, type) {\n try {\n input.type = type;\n }\n catch (e) {\n // do nothing\n }\n}\nfunction set_style(node, key, value, important) {\n node.style.setProperty(key, value, important ? 'important' : '');\n}\nfunction select_option(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n if (option.__value === value) {\n option.selected = true;\n return;\n }\n }\n}\nfunction select_options(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n option.selected = ~value.indexOf(option.__value);\n }\n}\nfunction select_value(select) {\n const selected_option = select.querySelector(':checked') || select.options[0];\n return selected_option && selected_option.__value;\n}\nfunction select_multiple_value(select) {\n return [].map.call(select.querySelectorAll(':checked'), option => option.__value);\n}\nfunction add_resize_listener(element, fn) {\n if (getComputedStyle(element).position === 'static') {\n element.style.position = 'relative';\n }\n const object = document.createElement('object');\n object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');\n object.type = 'text/html';\n object.tabIndex = -1;\n let win;\n object.onload = () => {\n win = object.contentDocument.defaultView;\n win.addEventListener('resize', fn);\n };\n if (/Trident/.test(navigator.userAgent)) {\n element.appendChild(object);\n object.data = 'about:blank';\n }\n else {\n object.data = 'about:blank';\n element.appendChild(object);\n }\n return {\n cancel: () => {\n win && win.removeEventListener && win.removeEventListener('resize', fn);\n element.removeChild(object);\n }\n };\n}\nfunction toggle_class(element, name, toggle) {\n element.classList[toggle ? 'add' : 'remove'](name);\n}\nfunction custom_event(type, detail) {\n const e = document.createEvent('CustomEvent');\n e.initCustomEvent(type, false, false, detail);\n return e;\n}\nclass HtmlTag {\n constructor(html, anchor = null) {\n this.e = element('div');\n this.a = anchor;\n this.u(html);\n }\n m(target, anchor = null) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert(target, this.n[i], anchor);\n }\n this.t = target;\n }\n u(html) {\n this.e.innerHTML = html;\n this.n = Array.from(this.e.childNodes);\n }\n p(html) {\n this.d();\n this.u(html);\n this.m(this.t, this.a);\n }\n d() {\n this.n.forEach(detach);\n }\n}\n\nlet stylesheet;\nlet active = 0;\nlet current_rules = {};\n// https://github.com/darkskyapp/string-hash/blob/master/index.js\nfunction hash(str) {\n let hash = 5381;\n let i = str.length;\n while (i--)\n hash = ((hash << 5) - hash) ^ str.charCodeAt(i);\n return hash >>> 0;\n}\nfunction create_rule(node, a, b, duration, delay, ease, fn, uid = 0) {\n const step = 16.666 / duration;\n let keyframes = '{\\n';\n for (let p = 0; p <= 1; p += step) {\n const t = a + (b - a) * ease(p);\n keyframes += p * 100 + `%{${fn(t, 1 - t)}}\\n`;\n }\n const rule = keyframes + `100% {${fn(b, 1 - b)}}\\n}`;\n const name = `__svelte_${hash(rule)}_${uid}`;\n if (!current_rules[name]) {\n if (!stylesheet) {\n const style = element('style');\n document.head.appendChild(style);\n stylesheet = style.sheet;\n }\n current_rules[name] = true;\n stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);\n }\n const animation = node.style.animation || '';\n node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`;\n active += 1;\n return name;\n}\nfunction delete_rule(node, name) {\n node.style.animation = (node.style.animation || '')\n .split(', ')\n .filter(name\n ? anim => anim.indexOf(name) < 0 // remove specific animation\n : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations\n )\n .join(', ');\n if (name && !--active)\n clear_rules();\n}\nfunction clear_rules() {\n raf(() => {\n if (active)\n return;\n let i = stylesheet.cssRules.length;\n while (i--)\n stylesheet.deleteRule(i);\n current_rules = {};\n });\n}\n\nfunction create_animation(node, from, fn, params) {\n if (!from)\n return noop;\n const to = node.getBoundingClientRect();\n if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom)\n return noop;\n const { delay = 0, duration = 300, easing = identity, \n // @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation?\n start: start_time = now() + delay, \n // @ts-ignore todo:\n end = start_time + duration, tick = noop, css } = fn(node, { from, to }, params);\n let running = true;\n let started = false;\n let name;\n function start() {\n if (css) {\n name = create_rule(node, 0, 1, duration, delay, easing, css);\n }\n if (!delay) {\n started = true;\n }\n }\n function stop() {\n if (css)\n delete_rule(node, name);\n running = false;\n }\n loop(now => {\n if (!started && now >= start_time) {\n started = true;\n }\n if (started && now >= end) {\n tick(1, 0);\n stop();\n }\n if (!running) {\n return false;\n }\n if (started) {\n const p = now - start_time;\n const t = 0 + 1 * easing(p / duration);\n tick(t, 1 - t);\n }\n return true;\n });\n start();\n tick(0, 1);\n return stop;\n}\nfunction fix_position(node) {\n const style = getComputedStyle(node);\n if (style.position !== 'absolute' && style.position !== 'fixed') {\n const { width, height } = style;\n const a = node.getBoundingClientRect();\n node.style.position = 'absolute';\n node.style.width = width;\n node.style.height = height;\n add_transform(node, a);\n }\n}\nfunction add_transform(node, a) {\n const b = node.getBoundingClientRect();\n if (a.left !== b.left || a.top !== b.top) {\n const style = getComputedStyle(node);\n const transform = style.transform === 'none' ? '' : style.transform;\n node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;\n }\n}\n\nlet current_component;\nfunction set_current_component(component) {\n current_component = component;\n}\nfunction get_current_component() {\n if (!current_component)\n throw new Error(`Function called outside component initialization`);\n return current_component;\n}\nfunction beforeUpdate(fn) {\n get_current_component().$$.before_update.push(fn);\n}\nfunction onMount(fn) {\n get_current_component().$$.on_mount.push(fn);\n}\nfunction afterUpdate(fn) {\n get_current_component().$$.after_update.push(fn);\n}\nfunction onDestroy(fn) {\n get_current_component().$$.on_destroy.push(fn);\n}\nfunction createEventDispatcher() {\n const component = current_component;\n return (type, detail) => {\n const callbacks = component.$$.callbacks[type];\n if (callbacks) {\n // TODO are there situations where events could be dispatched\n // in a server (non-DOM) environment?\n const event = custom_event(type, detail);\n callbacks.slice().forEach(fn => {\n fn.call(component, event);\n });\n }\n };\n}\nfunction setContext(key, context) {\n get_current_component().$$.context.set(key, context);\n}\nfunction getContext(key) {\n return get_current_component().$$.context.get(key);\n}\n// TODO figure out if we still want to support\n// shorthand events, or if we want to implement\n// a real bubbling mechanism\nfunction bubble(component, event) {\n const callbacks = component.$$.callbacks[event.type];\n if (callbacks) {\n callbacks.slice().forEach(fn => fn(event));\n }\n}\n\nconst dirty_components = [];\nconst intros = { enabled: false };\nconst binding_callbacks = [];\nconst render_callbacks = [];\nconst flush_callbacks = [];\nconst resolved_promise = Promise.resolve();\nlet update_scheduled = false;\nfunction schedule_update() {\n if (!update_scheduled) {\n update_scheduled = true;\n resolved_promise.then(flush);\n }\n}\nfunction tick() {\n schedule_update();\n return resolved_promise;\n}\nfunction add_render_callback(fn) {\n render_callbacks.push(fn);\n}\nfunction add_flush_callback(fn) {\n flush_callbacks.push(fn);\n}\nfunction flush() {\n const seen_callbacks = new Set();\n do {\n // first, call beforeUpdate functions\n // and update components\n while (dirty_components.length) {\n const component = dirty_components.shift();\n set_current_component(component);\n update(component.$$);\n }\n while (binding_callbacks.length)\n binding_callbacks.pop()();\n // then, once components are updated, call\n // afterUpdate functions. This may cause\n // subsequent updates...\n for (let i = 0; i < render_callbacks.length; i += 1) {\n const callback = render_callbacks[i];\n if (!seen_callbacks.has(callback)) {\n callback();\n // ...so guard against infinite loops\n seen_callbacks.add(callback);\n }\n }\n render_callbacks.length = 0;\n } while (dirty_components.length);\n while (flush_callbacks.length) {\n flush_callbacks.pop()();\n }\n update_scheduled = false;\n}\nfunction update($$) {\n if ($$.fragment) {\n $$.update($$.dirty);\n run_all($$.before_update);\n $$.fragment.p($$.dirty, $$.ctx);\n $$.dirty = null;\n $$.after_update.forEach(add_render_callback);\n }\n}\n\nlet promise;\nfunction wait() {\n if (!promise) {\n promise = Promise.resolve();\n promise.then(() => {\n promise = null;\n });\n }\n return promise;\n}\nfunction dispatch(node, direction, kind) {\n node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));\n}\nconst outroing = new Set();\nlet outros;\nfunction group_outros() {\n outros = {\n r: 0,\n c: [],\n p: outros // parent group\n };\n}\nfunction check_outros() {\n if (!outros.r) {\n run_all(outros.c);\n }\n outros = outros.p;\n}\nfunction transition_in(block, local) {\n if (block && block.i) {\n outroing.delete(block);\n block.i(local);\n }\n}\nfunction transition_out(block, local, detach, callback) {\n if (block && block.o) {\n if (outroing.has(block))\n return;\n outroing.add(block);\n outros.c.push(() => {\n outroing.delete(block);\n if (callback) {\n if (detach)\n block.d(1);\n callback();\n }\n });\n block.o(local);\n }\n}\nconst null_transition = { duration: 0 };\nfunction create_in_transition(node, fn, params) {\n let config = fn(node, params);\n let running = false;\n let animation_name;\n let task;\n let uid = 0;\n function cleanup() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);\n tick(0, 1);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n if (task)\n task.abort();\n running = true;\n add_render_callback(() => dispatch(node, true, 'start'));\n task = loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(1, 0);\n dispatch(node, true, 'end');\n cleanup();\n return running = false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(t, 1 - t);\n }\n }\n return running;\n });\n }\n let started = false;\n return {\n start() {\n if (started)\n return;\n delete_rule(node);\n if (is_function(config)) {\n config = config();\n wait().then(go);\n }\n else {\n go();\n }\n },\n invalidate() {\n started = false;\n },\n end() {\n if (running) {\n cleanup();\n running = false;\n }\n }\n };\n}\nfunction create_out_transition(node, fn, params) {\n let config = fn(node, params);\n let running = true;\n let animation_name;\n const group = outros;\n group.r += 1;\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 1, 0, duration, delay, easing, css);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n add_render_callback(() => dispatch(node, false, 'start'));\n loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(0, 1);\n dispatch(node, false, 'end');\n if (!--group.r) {\n // this will result in `end()` being called,\n // so we don't need to clean up here\n run_all(group.c);\n }\n return false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(1 - t, t);\n }\n }\n return running;\n });\n }\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go();\n });\n }\n else {\n go();\n }\n return {\n end(reset) {\n if (reset && config.tick) {\n config.tick(1, 0);\n }\n if (running) {\n if (animation_name)\n delete_rule(node, animation_name);\n running = false;\n }\n }\n };\n}\nfunction create_bidirectional_transition(node, fn, params, intro) {\n let config = fn(node, params);\n let t = intro ? 0 : 1;\n let running_program = null;\n let pending_program = null;\n let animation_name = null;\n function clear_animation() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function init(program, duration) {\n const d = program.b - t;\n duration *= Math.abs(d);\n return {\n a: t,\n b: program.b,\n d,\n duration,\n start: program.start,\n end: program.start + duration,\n group: program.group\n };\n }\n function go(b) {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n const program = {\n start: now() + delay,\n b\n };\n if (!b) {\n // @ts-ignore todo: improve typings\n program.group = outros;\n outros.r += 1;\n }\n if (running_program) {\n pending_program = program;\n }\n else {\n // if this is an intro, and there's a delay, we need to do\n // an initial tick and/or apply CSS animation immediately\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, b, duration, delay, easing, css);\n }\n if (b)\n tick(0, 1);\n running_program = init(program, duration);\n add_render_callback(() => dispatch(node, b, 'start'));\n loop(now => {\n if (pending_program && now > pending_program.start) {\n running_program = init(pending_program, duration);\n pending_program = null;\n dispatch(node, running_program.b, 'start');\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css);\n }\n }\n if (running_program) {\n if (now >= running_program.end) {\n tick(t = running_program.b, 1 - t);\n dispatch(node, running_program.b, 'end');\n if (!pending_program) {\n // we're done\n if (running_program.b) {\n // intro — we can tidy up immediately\n clear_animation();\n }\n else {\n // outro — needs to be coordinated\n if (!--running_program.group.r)\n run_all(running_program.group.c);\n }\n }\n running_program = null;\n }\n else if (now >= running_program.start) {\n const p = now - running_program.start;\n t = running_program.a + running_program.d * easing(p / running_program.duration);\n tick(t, 1 - t);\n }\n }\n return !!(running_program || pending_program);\n });\n }\n }\n return {\n run(b) {\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go(b);\n });\n }\n else {\n go(b);\n }\n },\n end() {\n clear_animation();\n running_program = pending_program = null;\n }\n };\n}\n\nfunction handle_promise(promise, info) {\n const token = info.token = {};\n function update(type, index, key, value) {\n if (info.token !== token)\n return;\n info.resolved = key && { [key]: value };\n const child_ctx = assign(assign({}, info.ctx), info.resolved);\n const block = type && (info.current = type)(child_ctx);\n if (info.block) {\n if (info.blocks) {\n info.blocks.forEach((block, i) => {\n if (i !== index && block) {\n group_outros();\n transition_out(block, 1, 1, () => {\n info.blocks[i] = null;\n });\n check_outros();\n }\n });\n }\n else {\n info.block.d(1);\n }\n block.c();\n transition_in(block, 1);\n block.m(info.mount(), info.anchor);\n flush();\n }\n info.block = block;\n if (info.blocks)\n info.blocks[index] = block;\n }\n if (is_promise(promise)) {\n const current_component = get_current_component();\n promise.then(value => {\n set_current_component(current_component);\n update(info.then, 1, info.value, value);\n set_current_component(null);\n }, error => {\n set_current_component(current_component);\n update(info.catch, 2, info.error, error);\n set_current_component(null);\n });\n // if we previously had a then/catch block, destroy it\n if (info.current !== info.pending) {\n update(info.pending, 0);\n return true;\n }\n }\n else {\n if (info.current !== info.then) {\n update(info.then, 1, info.value, promise);\n return true;\n }\n info.resolved = { [info.value]: promise };\n }\n}\n\nconst globals = (typeof window !== 'undefined' ? window : global);\n\nfunction destroy_block(block, lookup) {\n block.d(1);\n lookup.delete(block.key);\n}\nfunction outro_and_destroy_block(block, lookup) {\n transition_out(block, 1, 1, () => {\n lookup.delete(block.key);\n });\n}\nfunction fix_and_destroy_block(block, lookup) {\n block.f();\n destroy_block(block, lookup);\n}\nfunction fix_and_outro_and_destroy_block(block, lookup) {\n block.f();\n outro_and_destroy_block(block, lookup);\n}\nfunction update_keyed_each(old_blocks, changed, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) {\n let o = old_blocks.length;\n let n = list.length;\n let i = o;\n const old_indexes = {};\n while (i--)\n old_indexes[old_blocks[i].key] = i;\n const new_blocks = [];\n const new_lookup = new Map();\n const deltas = new Map();\n i = n;\n while (i--) {\n const child_ctx = get_context(ctx, list, i);\n const key = get_key(child_ctx);\n let block = lookup.get(key);\n if (!block) {\n block = create_each_block(key, child_ctx);\n block.c();\n }\n else if (dynamic) {\n block.p(changed, child_ctx);\n }\n new_lookup.set(key, new_blocks[i] = block);\n if (key in old_indexes)\n deltas.set(key, Math.abs(i - old_indexes[key]));\n }\n const will_move = new Set();\n const did_move = new Set();\n function insert(block) {\n transition_in(block, 1);\n block.m(node, next);\n lookup.set(block.key, block);\n next = block.first;\n n--;\n }\n while (o && n) {\n const new_block = new_blocks[n - 1];\n const old_block = old_blocks[o - 1];\n const new_key = new_block.key;\n const old_key = old_block.key;\n if (new_block === old_block) {\n // do nothing\n next = new_block.first;\n o--;\n n--;\n }\n else if (!new_lookup.has(old_key)) {\n // remove old block\n destroy(old_block, lookup);\n o--;\n }\n else if (!lookup.has(new_key) || will_move.has(new_key)) {\n insert(new_block);\n }\n else if (did_move.has(old_key)) {\n o--;\n }\n else if (deltas.get(new_key) > deltas.get(old_key)) {\n did_move.add(new_key);\n insert(new_block);\n }\n else {\n will_move.add(old_key);\n o--;\n }\n }\n while (o--) {\n const old_block = old_blocks[o];\n if (!new_lookup.has(old_block.key))\n destroy(old_block, lookup);\n }\n while (n)\n insert(new_blocks[n - 1]);\n return new_blocks;\n}\nfunction measure(blocks) {\n const rects = {};\n let i = blocks.length;\n while (i--)\n rects[blocks[i].key] = blocks[i].node.getBoundingClientRect();\n return rects;\n}\n\nfunction get_spread_update(levels, updates) {\n const update = {};\n const to_null_out = {};\n const accounted_for = { $$scope: 1 };\n let i = levels.length;\n while (i--) {\n const o = levels[i];\n const n = updates[i];\n if (n) {\n for (const key in o) {\n if (!(key in n))\n to_null_out[key] = 1;\n }\n for (const key in n) {\n if (!accounted_for[key]) {\n update[key] = n[key];\n accounted_for[key] = 1;\n }\n }\n levels[i] = n;\n }\n else {\n for (const key in o) {\n accounted_for[key] = 1;\n }\n }\n }\n for (const key in to_null_out) {\n if (!(key in update))\n update[key] = undefined;\n }\n return update;\n}\nfunction get_spread_object(spread_props) {\n return typeof spread_props === 'object' && spread_props !== null ? spread_props : {};\n}\n\nconst invalid_attribute_name_character = /[\\s'\">/=\\u{FDD0}-\\u{FDEF}\\u{FFFE}\\u{FFFF}\\u{1FFFE}\\u{1FFFF}\\u{2FFFE}\\u{2FFFF}\\u{3FFFE}\\u{3FFFF}\\u{4FFFE}\\u{4FFFF}\\u{5FFFE}\\u{5FFFF}\\u{6FFFE}\\u{6FFFF}\\u{7FFFE}\\u{7FFFF}\\u{8FFFE}\\u{8FFFF}\\u{9FFFE}\\u{9FFFF}\\u{AFFFE}\\u{AFFFF}\\u{BFFFE}\\u{BFFFF}\\u{CFFFE}\\u{CFFFF}\\u{DFFFE}\\u{DFFFF}\\u{EFFFE}\\u{EFFFF}\\u{FFFFE}\\u{FFFFF}\\u{10FFFE}\\u{10FFFF}]/u;\n// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n// https://infra.spec.whatwg.org/#noncharacter\nfunction spread(args) {\n const attributes = Object.assign({}, ...args);\n let str = '';\n Object.keys(attributes).forEach(name => {\n if (invalid_attribute_name_character.test(name))\n return;\n const value = attributes[name];\n if (value === undefined)\n return;\n if (value === true)\n str += \" \" + name;\n const escaped = String(value)\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n str += \" \" + name + \"=\" + JSON.stringify(escaped);\n });\n return str;\n}\nconst escaped = {\n '\"': '"',\n \"'\": ''',\n '&': '&',\n '<': '<',\n '>': '>'\n};\nfunction escape(html) {\n return String(html).replace(/[\"'&<>]/g, match => escaped[match]);\n}\nfunction each(items, fn) {\n let str = '';\n for (let i = 0; i < items.length; i += 1) {\n str += fn(items[i], i);\n }\n return str;\n}\nconst missing_component = {\n $$render: () => ''\n};\nfunction validate_component(component, name) {\n if (!component || !component.$$render) {\n if (name === 'svelte:component')\n name += ' this={...}';\n throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules`);\n }\n return component;\n}\nfunction debug(file, line, column, values) {\n console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`); // eslint-disable-line no-console\n console.log(values); // eslint-disable-line no-console\n return '';\n}\nlet on_destroy;\nfunction create_ssr_component(fn) {\n function $$render(result, props, bindings, slots) {\n const parent_component = current_component;\n const $$ = {\n on_destroy,\n context: new Map(parent_component ? parent_component.$$.context : []),\n // these will be immediately discarded\n on_mount: [],\n before_update: [],\n after_update: [],\n callbacks: blank_object()\n };\n set_current_component({ $$ });\n const html = fn(result, props, bindings, slots);\n set_current_component(parent_component);\n return html;\n }\n return {\n render: (props = {}, options = {}) => {\n on_destroy = [];\n const result = { head: '', css: new Set() };\n const html = $$render(result, props, {}, options);\n run_all(on_destroy);\n return {\n html,\n css: {\n code: Array.from(result.css).map(css => css.code).join('\\n'),\n map: null // TODO\n },\n head: result.head\n };\n },\n $$render\n };\n}\nfunction add_attribute(name, value, boolean) {\n if (value == null || (boolean && !value))\n return '';\n return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `\"${value}\"`}`}`;\n}\nfunction add_classes(classes) {\n return classes ? ` class=\"${classes}\"` : ``;\n}\n\nfunction bind(component, name, callback) {\n if (component.$$.props.indexOf(name) === -1)\n return;\n component.$$.bound[name] = callback;\n callback(component.$$.ctx[name]);\n}\nfunction mount_component(component, target, anchor) {\n const { fragment, on_mount, on_destroy, after_update } = component.$$;\n fragment.m(target, anchor);\n // onMount happens before the initial afterUpdate\n add_render_callback(() => {\n const new_on_destroy = on_mount.map(run).filter(is_function);\n if (on_destroy) {\n on_destroy.push(...new_on_destroy);\n }\n else {\n // Edge case - component was destroyed immediately,\n // most likely as a result of a binding initialising\n run_all(new_on_destroy);\n }\n component.$$.on_mount = [];\n });\n after_update.forEach(add_render_callback);\n}\nfunction destroy_component(component, detaching) {\n if (component.$$.fragment) {\n run_all(component.$$.on_destroy);\n component.$$.fragment.d(detaching);\n // TODO null out other refs, including component.$$ (but need to\n // preserve final state?)\n component.$$.on_destroy = component.$$.fragment = null;\n component.$$.ctx = {};\n }\n}\nfunction make_dirty(component, key) {\n if (!component.$$.dirty) {\n dirty_components.push(component);\n schedule_update();\n component.$$.dirty = blank_object();\n }\n component.$$.dirty[key] = true;\n}\nfunction init(component, options, instance, create_fragment, not_equal, prop_names) {\n const parent_component = current_component;\n set_current_component(component);\n const props = options.props || {};\n const $$ = component.$$ = {\n fragment: null,\n ctx: null,\n // state\n props: prop_names,\n update: noop,\n not_equal,\n bound: blank_object(),\n // lifecycle\n on_mount: [],\n on_destroy: [],\n before_update: [],\n after_update: [],\n context: new Map(parent_component ? parent_component.$$.context : []),\n // everything else\n callbacks: blank_object(),\n dirty: null\n };\n let ready = false;\n $$.ctx = instance\n ? instance(component, props, (key, ret, value = ret) => {\n if ($$.ctx && not_equal($$.ctx[key], $$.ctx[key] = value)) {\n if ($$.bound[key])\n $$.bound[key](value);\n if (ready)\n make_dirty(component, key);\n }\n return ret;\n })\n : props;\n $$.update();\n ready = true;\n run_all($$.before_update);\n $$.fragment = create_fragment($$.ctx);\n if (options.target) {\n if (options.hydrate) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment.l(children(options.target));\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment.c();\n }\n if (options.intro)\n transition_in(component.$$.fragment);\n mount_component(component, options.target, options.anchor);\n flush();\n }\n set_current_component(parent_component);\n}\nlet SvelteElement;\nif (typeof HTMLElement !== 'undefined') {\n SvelteElement = class extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n }\n connectedCallback() {\n // @ts-ignore todo: improve typings\n for (const key in this.$$.slotted) {\n // @ts-ignore todo: improve typings\n this.appendChild(this.$$.slotted[key]);\n }\n }\n attributeChangedCallback(attr, _oldValue, newValue) {\n this[attr] = newValue;\n }\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n // TODO should this delegate to addEventListener?\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set() {\n // overridden by instance, if it has props\n }\n };\n}\nclass SvelteComponent {\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set() {\n // overridden by instance, if it has props\n }\n}\n\nfunction dispatch_dev(type, detail) {\n document.dispatchEvent(custom_event(type, detail));\n}\nfunction append_dev(target, node) {\n dispatch_dev(\"SvelteDOMInsert\", { target, node });\n append(target, node);\n}\nfunction insert_dev(target, node, anchor) {\n dispatch_dev(\"SvelteDOMInsert\", { target, node, anchor });\n insert(target, node, anchor);\n}\nfunction detach_dev(node) {\n dispatch_dev(\"SvelteDOMRemove\", { node });\n detach(node);\n}\nfunction detach_between_dev(before, after) {\n while (before.nextSibling && before.nextSibling !== after) {\n detach_dev(before.nextSibling);\n }\n}\nfunction detach_before_dev(after) {\n while (after.previousSibling) {\n detach_dev(after.previousSibling);\n }\n}\nfunction detach_after_dev(before) {\n while (before.nextSibling) {\n detach_dev(before.nextSibling);\n }\n}\nfunction listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) {\n const modifiers = options === true ? [\"capture\"] : options ? Array.from(Object.keys(options)) : [];\n if (has_prevent_default)\n modifiers.push('preventDefault');\n if (has_stop_propagation)\n modifiers.push('stopPropagation');\n dispatch_dev(\"SvelteDOMAddEventListener\", { node, event, handler, modifiers });\n const dispose = listen(node, event, handler, options);\n return () => {\n dispatch_dev(\"SvelteDOMRemoveEventListener\", { node, event, handler, modifiers });\n dispose();\n };\n}\nfunction attr_dev(node, attribute, value) {\n attr(node, attribute, value);\n if (value == null)\n dispatch_dev(\"SvelteDOMRemoveAttribute\", { node, attribute });\n else\n dispatch_dev(\"SvelteDOMSetAttribute\", { node, attribute, value });\n}\nfunction prop_dev(node, property, value) {\n node[property] = value;\n dispatch_dev(\"SvelteDOMSetProperty\", { node, property, value });\n}\nfunction dataset_dev(node, property, value) {\n node.dataset[property] = value;\n dispatch_dev(\"SvelteDOMSetDataset\", { node, property, value });\n}\nfunction set_data_dev(text, data) {\n data = '' + data;\n if (text.data === data)\n return;\n dispatch_dev(\"SvelteDOMSetData\", { node: text, data });\n text.data = data;\n}\nclass SvelteComponentDev extends SvelteComponent {\n constructor(options) {\n if (!options || (!options.target && !options.$$inline)) {\n throw new Error(`'target' is a required option`);\n }\n super();\n }\n $destroy() {\n super.$destroy();\n this.$destroy = () => {\n console.warn(`Component was already destroyed`); // eslint-disable-line no-console\n };\n }\n}\n\nexport { HtmlTag, SvelteComponent, SvelteComponentDev, SvelteElement, add_attribute, add_classes, add_flush_callback, add_location, add_render_callback, add_resize_listener, add_transform, afterUpdate, append, append_dev, assign, attr, attr_dev, beforeUpdate, bind, binding_callbacks, blank_object, bubble, check_outros, children, claim_element, claim_space, claim_text, clear_loops, component_subscribe, createEventDispatcher, create_animation, create_bidirectional_transition, create_in_transition, create_out_transition, create_slot, create_ssr_component, current_component, custom_event, dataset_dev, debug, destroy_block, destroy_component, destroy_each, detach, detach_after_dev, detach_before_dev, detach_between_dev, detach_dev, dirty_components, dispatch_dev, each, element, element_is, empty, escape, escaped, exclude_internal_props, fix_and_destroy_block, fix_and_outro_and_destroy_block, fix_position, flush, getContext, get_binding_group_value, get_current_component, get_slot_changes, get_slot_context, get_spread_object, get_spread_update, get_store_value, globals, group_outros, handle_promise, identity, init, insert, insert_dev, intros, invalid_attribute_name_character, is_client, is_function, is_promise, listen, listen_dev, loop, measure, missing_component, mount_component, noop, not_equal, now, null_to_empty, object_without_properties, onDestroy, onMount, once, outro_and_destroy_block, prevent_default, prop_dev, raf, run, run_all, safe_not_equal, schedule_update, select_multiple_value, select_option, select_options, select_value, self, setContext, set_attributes, set_current_component, set_custom_element_data, set_data, set_data_dev, set_input_type, set_input_value, set_now, set_raf, set_store_value, set_style, set_svg_attributes, space, spread, stop_propagation, subscribe, svg_element, text, tick, time_ranges_to_array, to_number, toggle_class, transition_in, transition_out, update_keyed_each, validate_component, validate_store, xlink_attr };\n","//Modify this to point to the ip address where your server is running on\nWEBSOCKET_URI = WEBSOCKET_URI || \"ws://127.0.0.1:8089/\";\n\nlet websocket;\nlet socketIsOpen = false;\nlet intervalID = 0;\nlet closedByUser = false;\n\nexport function sendCommand(obj) {\n console.log(JSON.stringify(obj));\n\tif (socketIsOpen) {\n\t\twebsocket.send(JSON.stringify(obj));\n\t} else {\n\t\tconsole.error('Not connected\\n');\n\t}\n}\n\n\nexport function doConnect(onMessage) {\n\twebsocket = new WebSocket(WEBSOCKET_URI);\n\twebsocket.onopen = function(evt) {\n\t\tsocketIsOpen = true;\n\t\tconsole.info(\"Connection opened\");\n\t\tclearInterval(intervalID);\n\t\tintervalID = 0;\n\t};\n\twebsocket.onclose = function(evt) {\n\t\tsocketIsOpen = true;\n\t\tif (!intervalID && !closedByUser) {\n\t\t\tintervalID = setInterval(doConnect, 5000);\n\t\t} else if (closedByUser) {\n\t\t\tclosedByUser = false;\n\t\t}\n\t\tconsole.info(\"Connection closed\");\n\t};\n\twebsocket.onmessage = function(evt) {\n\t\tvar jsonOBJ = JSON.parse(evt.data);\n\t\tconsole.log(evt.data);\n\t\tonMessage(jsonOBJ);\n\t};\n\twebsocket.onerror = function(evt) {\n\t\tconsole.error('Connection failed, is the Server running?');\n\t\tsocketIsOpen = false;\n\t\tif (!intervalID) {\n\t\t\tintervalID = setInterval(doConnect, 5000);\n\t\t}\n\t};\n}\n\nexport function doDisconnect() {\n\tsocketIsOpen = false;\n\tclosedByUser = true;\n\twebsocket.close();\n}","\n\n\n\n\t{#each lines as line, i}\n\t
\n\t\t changeLine(i)}>\n\t\ttoggleLine(i)}>\n\t\t\t{#if shown === i}Hide{:else}Show{/if}\n\t\t \n\t
\n\t{/each}\n
\n","import App from './App.svelte';\n\nvar app = new App({\n\ttarget: document.body\n});\n\nexport default app;"],"names":["noop","run","fn","blank_object","Object","create","run_all","fns","forEach","is_function","thing","safe_not_equal","a","b","append","target","node","appendChild","insert","anchor","insertBefore","detach","parentNode","removeChild","element","name","document","createElement","text","data","createTextNode","space","listen","event","handler","options","addEventListener","removeEventListener","attr","attribute","value","removeAttribute","setAttribute","set_input_value","input","current_component","set_current_component","component","get_current_component","Error","dirty_components","binding_callbacks","render_callbacks","flush_callbacks","resolved_promise","Promise","resolve","update_scheduled","add_render_callback","push","flush","seen_callbacks","Set","length","shift","update","$$","pop","i","callback","has","add","fragment","dirty","before_update","p","ctx","after_update","outroing","make_dirty","key","then","init","instance","create_fragment","not_equal","prop_names","parent_component","props","bound","on_mount","on_destroy","context","Map","callbacks","ready","block","local","ret","hydrate","l","Array","from","childNodes","c","intro","delete","m","new_on_destroy","map","filter","mount_component","SvelteComponent","[object Object]","detaching","this","d","$destroy","type","index","indexOf","splice","websocket","WEBSOCKET_URI","socketIsOpen","intervalID","closedByUser","sendCommand","obj","console","log","JSON","stringify","send","error","doConnect","onMessage","WebSocket","onopen","evt","info","clearInterval","onclose","setInterval","onmessage","jsonOBJ","parse","onerror","shown","change_handler","click_handler","line","lines","iterations","toggleLine","line1","show","cleanLines","hasOwnProperty","changeLine","command","async","onDestroy","close","body"],"mappings":"gCAAA,SAASA,KAgBT,SAASC,EAAIC,GACT,OAAOA,IAEX,SAASC,IACL,OAAOC,OAAOC,OAAO,MAEzB,SAASC,EAAQC,GACbA,EAAIC,QAAQP,GAEhB,SAASQ,EAAYC,GACjB,MAAwB,mBAAVA,EAElB,SAASC,EAAeC,EAAGC,GACvB,OAAOD,GAAKA,EAAIC,GAAKA,EAAID,IAAMC,GAAOD,GAAkB,iBAANA,GAAgC,mBAANA,EA6GhF,SAASE,EAAOC,EAAQC,GACpBD,EAAOE,YAAYD,GAEvB,SAASE,EAAOH,EAAQC,EAAMG,GAC1BJ,EAAOK,aAAaJ,EAAMG,GAAU,MAExC,SAASE,EAAOL,GACZA,EAAKM,WAAWC,YAAYP,GAQhC,SAASQ,EAAQC,GACb,OAAOC,SAASC,cAAcF,GAqBlC,SAASG,EAAKC,GACV,OAAOH,SAASI,eAAeD,GAEnC,SAASE,IACL,OAAOH,EAAK,KAKhB,SAASI,EAAOhB,EAAMiB,EAAOC,EAASC,GAElC,OADAnB,EAAKoB,iBAAiBH,EAAOC,EAASC,GAC/B,IAAMnB,EAAKqB,oBAAoBJ,EAAOC,EAASC,GAuB1D,SAASG,EAAKtB,EAAMuB,EAAWC,GACd,MAATA,EACAxB,EAAKyB,gBAAgBF,GAErBvB,EAAK0B,aAAaH,EAAWC,GAoFrC,SAASG,EAAgBC,EAAOJ,IACf,MAATA,GAAiBI,EAAMJ,SACvBI,EAAMJ,MAAQA,GA+NtB,IAAIK,EACJ,SAASC,EAAsBC,GAC3BF,EAAoBE,EAExB,SAASC,IACL,IAAKH,EACD,MAAM,IAAII,MAAM,oDACpB,OAAOJ,EA4CX,MAAMK,EAAmB,GAEnBC,EAAoB,GACpBC,EAAmB,GACnBC,EAAkB,GAClBC,EAAmBC,QAAQC,UACjC,IAAIC,GAAmB,EAWvB,SAASC,EAAoBxD,GACzBkD,EAAiBO,KAAKzD,GAK1B,SAAS0D,IACL,MAAMC,EAAiB,IAAIC,IAC3B,EAAG,CAGC,KAAOZ,EAAiBa,QAAQ,CAC5B,MAAMhB,EAAYG,EAAiBc,QACnClB,EAAsBC,GACtBkB,EAAOlB,EAAUmB,IAErB,KAAOf,EAAkBY,QACrBZ,EAAkBgB,KAAlBhB,GAIJ,IAAK,IAAIiB,EAAI,EAAGA,EAAIhB,EAAiBW,OAAQK,GAAK,EAAG,CACjD,MAAMC,EAAWjB,EAAiBgB,GAC7BP,EAAeS,IAAID,KACpBA,IAEAR,EAAeU,IAAIF,IAG3BjB,EAAiBW,OAAS,QACrBb,EAAiBa,QAC1B,KAAOV,EAAgBU,QACnBV,EAAgBc,KAAhBd,GAEJI,GAAmB,EAEvB,SAASQ,EAAOC,GACRA,EAAGM,WACHN,EAAGD,OAAOC,EAAGO,OACbnE,EAAQ4D,EAAGQ,eACXR,EAAGM,SAASG,EAAET,EAAGO,MAAOP,EAAGU,KAC3BV,EAAGO,MAAQ,KACXP,EAAGW,aAAarE,QAAQkD,IAiBhC,MAAMoB,EAAW,IAAIhB,IAglBrB,SAASiB,EAAWhC,EAAWiC,GACtBjC,EAAUmB,GAAGO,QACdvB,EAAiBS,KAAKZ,GAtpBrBU,IACDA,GAAmB,EACnBH,EAAiB2B,KAAKrB,IAspBtBb,EAAUmB,GAAGO,MAAQtE,KAEzB4C,EAAUmB,GAAGO,MAAMO,IAAO,EAE9B,SAASE,EAAKnC,EAAWZ,EAASgD,EAAUC,EAAiBC,EAAWC,GACpE,MAAMC,EAAmB1C,EACzBC,EAAsBC,GACtB,MAAMyC,EAAQrD,EAAQqD,OAAS,GACzBtB,EAAKnB,EAAUmB,GAAK,CACtBM,SAAU,KACVI,IAAK,KAELY,MAAOF,EACPrB,OAAQjE,EACRqF,UAAAA,EACAI,MAAOtF,IAEPuF,SAAU,GACVC,WAAY,GACZjB,cAAe,GACfG,aAAc,GACde,QAAS,IAAIC,IAAIN,EAAmBA,EAAiBrB,GAAG0B,QAAU,IAElEE,UAAW3F,IACXsE,MAAO,MAEX,IAAIsB,GAAQ,EA/lBhB,IAAuBC,EAAOC,EAlZZzE,EAk/Bd0C,EAAGU,IAAMO,EACHA,EAASpC,EAAWyC,EAAO,CAACR,EAAKkB,EAAK1D,EAAQ0D,KACxChC,EAAGU,KAAOS,EAAUnB,EAAGU,IAAII,GAAMd,EAAGU,IAAII,GAAOxC,KAC3C0B,EAAGuB,MAAMT,IACTd,EAAGuB,MAAMT,GAAKxC,GACduD,GACAhB,EAAWhC,EAAWiC,IAEvBkB,IAETV,EACNtB,EAAGD,SACH8B,GAAQ,EACRzF,EAAQ4D,EAAGQ,eACXR,EAAGM,SAAWY,EAAgBlB,EAAGU,KAC7BzC,EAAQpB,SACJoB,EAAQgE,QAERjC,EAAGM,SAAS4B,GApgCN5E,EAogCiBW,EAAQpB,OAngChCsF,MAAMC,KAAK9E,EAAQ+E,cAugClBrC,EAAGM,SAASgC,IAEZrE,EAAQsE,SAxnBGT,EAynBGjD,EAAUmB,GAAGM,WAxnBtBwB,EAAM5B,IACfU,EAAS4B,OAAOV,GAChBA,EAAM5B,EAAE6B,KAkiBhB,SAAyBlD,EAAWhC,EAAQI,GACxC,MAAMqD,SAAEA,EAAQkB,SAAEA,EAAQC,WAAEA,EAAUd,aAAEA,GAAiB9B,EAAUmB,GACnEM,EAASmC,EAAE5F,EAAQI,GAEnBuC,EAAoB,KAChB,MAAMkD,EAAiBlB,EAASmB,IAAI5G,GAAK6G,OAAOrG,GAC5CkF,EACAA,EAAWhC,QAAQiD,GAKnBtG,EAAQsG,GAEZ7D,EAAUmB,GAAGwB,SAAW,KAE5Bb,EAAarE,QAAQkD,GAqEjBqD,CAAgBhE,EAAWZ,EAAQpB,OAAQoB,EAAQhB,QACnDyC,KAEJd,EAAsByC,GAsC1B,MAAMyB,EACFC,WA7GJ,IAA2BlE,EAAWmE,EAAAA,EA8GN,GA9GLnE,EA8GDoE,MA7GRjD,GAAGM,WACblE,EAAQyC,EAAUmB,GAAGyB,YACrB5C,EAAUmB,GAAGM,SAAS4C,EAAEF,GAGxBnE,EAAUmB,GAAGyB,WAAa5C,EAAUmB,GAAGM,SAAW,KAClDzB,EAAUmB,GAAGU,IAAM,IAwGnBuC,KAAKE,SAAWrH,EAEpBiH,IAAIK,EAAMjD,GACN,MAAMyB,EAAaqB,KAAKjD,GAAG4B,UAAUwB,KAAUH,KAAKjD,GAAG4B,UAAUwB,GAAQ,IAEzE,OADAxB,EAAUnC,KAAKU,GACR,KACH,MAAMkD,EAAQzB,EAAU0B,QAAQnD,IACjB,IAAXkD,GACAzB,EAAU2B,OAAOF,EAAO,IAGpCN,SCt0CJ,IAAIS,EAFJC,cAAgBA,eAAiB,uBAGjC,IAAIC,GAAe,EACfC,EAAa,EACbC,GAAe,EAEZ,SAASC,EAAYC,GACxBC,QAAQC,IAAIC,KAAKC,UAAUJ,IAC1BJ,EACHF,EAAUW,KAAKF,KAAKC,UAAUJ,IAE9BC,QAAQK,MAAM,mBAKT,SAASC,EAAUC,IACzBd,EAAY,IAAIe,UAAUd,gBAChBe,OAAS,SAASC,GAC3Bf,GAAe,EACfK,QAAQW,KAAK,qBACbC,cAAchB,GACdA,EAAa,GAEdH,EAAUoB,QAAU,SAASH,GAC5Bf,GAAe,EACVC,GAAeC,EAETA,IACVA,GAAe,GAFfD,EAAakB,YAAYR,EAAW,KAIrCN,QAAQW,KAAK,sBAEdlB,EAAUsB,UAAY,SAASL,GAC9B,IAAIM,EAAUd,KAAKe,MAAMP,EAAI9G,MAC7BoG,QAAQC,IAAIS,EAAI9G,MAChB2G,EAAUS,IAEXvB,EAAUyB,QAAU,SAASR,GAC5BV,QAAQK,MAAM,6CACdV,GAAe,EACVC,IACJA,EAAakB,YAAYR,EAAW,2YCgC/Ba,UAAUhF,2PAFiBgF,UAAUhF,EAAE,aAAa,2GAD7CiF,eAEDC,kCAHGC,wEAAAA,YAAAA,oGAEkBH,UAAUhF,EAAE,aAAa,wHALpDoF,2BAALzF,mEAAAA,sEAAAA,sEAAKyF,cAALzF,4FAAAA,wBAAAA,SAAAA,8BF8EH,SAAsB0F,EAAYvC,GAC9B,IAAK,IAAI9C,EAAI,EAAGA,EAAIqF,EAAW1F,OAAQK,GAAK,EACpCqF,EAAWrF,IACXqF,EAAWrF,GAAGgD,EAAEF,6BE7I5B,IAAIsC,EAAQ,CAAC,EAAE,EAAE,EAAE,GACfJ,GAAS,EF4gBb,IAAiBlJ,EElgBjB,SAASwJ,EAAWtF,aAElBgF,EADGA,IAAUhF,GACJ,EAEDA,GAET2D,EAAY,CACXqB,MAASA,EACTO,MAASH,EAAMpF,GACfwF,KAAQR,GAAS,IAGnB,SAASS,cAERL,EAAQ,IAAIA,EAAM1C,OAAOV,GAAGA,GAAI,KAGjC,SAASoC,EAAU3G,GAClBoG,QAAQC,IAAIrG,GACLA,EAAKiI,eAAe,qBAC1BN,EAAQ3H,EAAK2H,OACbK,KAEMhI,EAAKiI,eAAe,oBACpBV,EAAQvH,EAAKuH,OAUrB,SAASW,EAAW3F,GACnB,IAAI4F,EAAU,CACbR,MAASA,GAENJ,IAAUhF,IACb4F,EAAe,MAAIR,EAAMpF,IAE1B2D,EAAYiC,GACZH,IFwdgB3J,EE1gBT+J,WACP1B,EAAUC,KF0gBPxF,IAAwBkB,GAAGwB,SAAS/B,KAAKzD,GAK7C,SAAmBA,GACf8C,IAAwBkB,GAAGyB,WAAWhC,KAAKzD,GE7gB/CgK,CAAUD,UDkCTrC,GAAe,EACfE,GAAe,EACfJ,EAAUyC,UCaXN,gNC/DU,gEAAQ,CACjB9I,OAAQW,SAAS0I"}
--------------------------------------------------------------------------------
/lower-third-simple/assets/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | color: #333;
9 | margin: 0;
10 | padding: 8px;
11 | box-sizing: border-box;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
13 | }
14 |
15 | a {
16 | color: rgb(0,100,200);
17 | text-decoration: none;
18 | }
19 |
20 | a:hover {
21 | text-decoration: underline;
22 | }
23 |
24 | a:visited {
25 | color: rgb(0,80,160);
26 | }
27 |
28 | label {
29 | display: block;
30 | }
31 |
32 | input, button, select, textarea {
33 | font-family: inherit;
34 | font-size: inherit;
35 | padding: 0.4em;
36 | margin: 0 0 0.5em 0;
37 | box-sizing: border-box;
38 | border: 1px solid #ccc;
39 | border-radius: 2px;
40 | }
41 |
42 | input:disabled {
43 | color: #ccc;
44 | }
45 |
46 | input[type="range"] {
47 | height: 0;
48 | }
49 |
50 | button {
51 | background-color: #f4f4f4;
52 | outline: none;
53 | }
54 |
55 | button:active {
56 | background-color: #ddd;
57 | }
58 |
59 | button:focus {
60 | border-color: #666;
61 | }
--------------------------------------------------------------------------------
/lower-third-simple/config.dist.js:
--------------------------------------------------------------------------------
1 | WEBSOCKET_URI = "ws://127.0.0.1:8089/bible"
2 |
--------------------------------------------------------------------------------
/lower-third-simple/controller-old.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lower Third Controller
6 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Lower Third control
23 |
24 |
25 | Show
26 | Hide
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Connection settings
35 |
45 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
98 |
99 |
--------------------------------------------------------------------------------
/lower-third-simple/controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lower third simple
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/lower-third-simple/css/bootstrap-darkly.min.css:
--------------------------------------------------------------------------------
1 | ../../css/bootstrap-darkly.min.css
--------------------------------------------------------------------------------
/lower-third-simple/overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lower Third
6 |
9 |
45 |
46 |
47 |
48 |
49 | Main header line
50 |
51 |
52 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/lower-third-simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "devDependencies": {
5 | "npm-run-all": "^4.1.5",
6 | "rollup": "^1.27.4",
7 | "rollup-plugin-commonjs": "^9.3.4",
8 | "rollup-plugin-livereload": "^1.0.4",
9 | "rollup-plugin-node-resolve": "^4.2.4",
10 | "rollup-plugin-svelte": "^5.1.1",
11 | "rollup-plugin-terser": "^4.0.4",
12 | "sirv-cli": "^0.4.5",
13 | "svelte": "^3.15.0"
14 | },
15 | "scripts": {
16 | "build": "rollup -c",
17 | "autobuild": "rollup -c -w",
18 | "dev": "run-p start:dev autobuild",
19 | "start": "sirv public",
20 | "start:dev": "sirv public --dev"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lower-third-simple/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import resolve from 'rollup-plugin-node-resolve';
3 | import commonjs from 'rollup-plugin-commonjs';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 |
7 | const production = !process.env.ROLLUP_WATCH;
8 |
9 | export default {
10 | input: 'src/main.js',
11 | output: {
12 | sourcemap: true,
13 | format: 'iife',
14 | name: 'app',
15 | file: 'assets/bundle.js'
16 | },
17 | plugins: [
18 | svelte({
19 | // enable run-time checks when not in production
20 | dev: !production,
21 | // we'll extract any component CSS out into
22 | // a separate file better for performance
23 | css: css => {
24 | css.write('assets/bundle.css');
25 | }
26 | }),
27 |
28 | // If you have external dependencies installed from
29 | // npm, you'll most likely need these plugins. In
30 | // some cases you'll need additional configuration
31 | // consult the documentation for details:
32 | // https://github.com/rollup/rollup-plugin-commonjs
33 | resolve({ browser: true }),
34 | commonjs(),
35 |
36 | // Watch the `public` directory and refresh the
37 | // browser on changes when not in production
38 | !production && livereload('assets'),
39 |
40 | // If we're building for production (npm run build
41 | // instead of npm run dev), minify
42 | production && terser()
43 | ],
44 | watch: {
45 | clearScreen: false
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/lower-third-simple/src/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
68 |
69 |
70 | {#each lines as line, i}
71 |
72 | changeLine(i)}>
75 | toggleLine(i)}>
77 | {#if shown === i}Hide{:else}Show{/if}
78 |
79 |
80 | {/each}
81 |
82 |
--------------------------------------------------------------------------------
/lower-third-simple/src/WebsocketConfig.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
Connection settings
7 |
17 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/lower-third-simple/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | var app = new App({
4 | target: document.body
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/lower-third-simple/src/websocket-client.js:
--------------------------------------------------------------------------------
1 | //Modify this to point to the ip address where your server is running on
2 | WEBSOCKET_URI = WEBSOCKET_URI || "ws://127.0.0.1:8089/";
3 |
4 | let websocket;
5 | let socketIsOpen = false;
6 | let intervalID = 0;
7 | let closedByUser = false;
8 |
9 | export function sendCommand(obj) {
10 | console.log(JSON.stringify(obj));
11 | if (socketIsOpen) {
12 | websocket.send(JSON.stringify(obj));
13 | } else {
14 | console.error('Not connected\n');
15 | }
16 | }
17 |
18 |
19 | export function doConnect(onMessage) {
20 | websocket = new WebSocket(WEBSOCKET_URI);
21 | websocket.onopen = function(evt) {
22 | socketIsOpen = true;
23 | console.info("Connection opened");
24 | clearInterval(intervalID);
25 | intervalID = 0;
26 | };
27 | websocket.onclose = function(evt) {
28 | socketIsOpen = true;
29 | if (!intervalID && !closedByUser) {
30 | intervalID = setInterval(doConnect, 5000);
31 | } else if (closedByUser) {
32 | closedByUser = false;
33 | }
34 | console.info("Connection closed");
35 | };
36 | websocket.onmessage = function(evt) {
37 | var jsonOBJ = JSON.parse(evt.data);
38 | console.log(evt.data);
39 | onMessage(jsonOBJ);
40 | };
41 | websocket.onerror = function(evt) {
42 | console.error('Connection failed, is the Server running?');
43 | socketIsOpen = false;
44 | if (!intervalID) {
45 | intervalID = setInterval(doConnect, 5000);
46 | }
47 | };
48 | }
49 |
50 | export function doDisconnect() {
51 | socketIsOpen = false;
52 | closedByUser = true;
53 | websocket.close();
54 | }
--------------------------------------------------------------------------------
/lower-third/controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lower Third Controller
6 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Lower Third control
20 |
21 |
22 |
23 |
24 |
25 | Show
26 | Hide
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Connection settings
35 |
45 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
95 |
96 |
--------------------------------------------------------------------------------
/lower-third/overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lower Third
6 |
9 |
61 |
62 |
63 |
64 |
65 | Main header line
66 | Sub header line
67 |
68 |
69 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "websocket-overlay",
3 | "version": "1.0.0",
4 | "description": "HTML5 overlays controlled via websocket server from controller.html",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/filiphanes/websocket-overlays.git"
13 | },
14 | "keywords": [
15 | "html5",
16 | "overlay",
17 | "websocket",
18 | "obs-studio",
19 | "splitx",
20 | "casparcg",
21 | "broadcast"
22 | ],
23 | "author": "Filip Hanes ",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/filiphanes/websocket-overlays/issues"
27 | },
28 | "homepage": "https://github.com/filiphanes/websocket-overlays#readme",
29 | "dependencies": {
30 | "ws": "^7.0.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | gun: 0.2020.301
3 | ws: 7.2.3
4 | lockfileVersion: 5.1
5 | packages:
6 | /@peculiar/asn1-schema/1.0.5:
7 | dependencies:
8 | asn1js: 2.0.26
9 | tslib: 1.11.1
10 | dev: false
11 | engines:
12 | node: '>=8.0.0'
13 | optional: true
14 | resolution:
15 | integrity: sha512-rzzorGYnQNmVHleLvC8gJSbbdNYtg+EB9s075dHvwpxs10teXHYnRmTWhCVuWjbSVSofwdm7IYPtMTWTbcNUWA==
16 | /@peculiar/json-schema/1.1.10:
17 | dependencies:
18 | tslib: 1.11.1
19 | dev: false
20 | engines:
21 | node: '>=8.0.0'
22 | optional: true
23 | resolution:
24 | integrity: sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA==
25 | /@peculiar/webcrypto/1.0.26:
26 | dependencies:
27 | '@peculiar/asn1-schema': 1.0.5
28 | '@peculiar/json-schema': 1.1.10
29 | asn1js: 2.0.26
30 | pvtsutils: 1.0.10
31 | tslib: 1.11.1
32 | webcrypto-core: 1.0.18
33 | dev: false
34 | engines:
35 | node: '>=10.12.0'
36 | optional: true
37 | resolution:
38 | integrity: sha512-7Ws4t+eUiLPSL9q8OCcbevd9S8qjqBDAXJrbnjzbuWu3nI8NgBvc7qZ97DZO8y8qxEoh5cGqK2p+p+UhxLw9Kg==
39 | /@unimodules/core/5.1.0:
40 | dependencies:
41 | compare-versions: 3.6.0
42 | dev: false
43 | optional: true
44 | resolution:
45 | integrity: sha512-gaamGkJ4PVwusWEfsZyPo4uhrVWPDE0BmHc/lTYfkZCv2oIAswC7gG/ULRdtZpYdwnYqFIZng+WQxwuVrJUNDw==
46 | /@unimodules/react-native-adapter/5.1.1:
47 | dependencies:
48 | invariant: 2.2.4
49 | lodash: 4.17.15
50 | prop-types: 15.7.2
51 | dev: false
52 | optional: true
53 | peerDependencies:
54 | react-native: '*'
55 | resolution:
56 | integrity: sha512-PlP6QQ2Z3ckORhS07tWcIweK+CkkxyzitJ1j1FD+N+G7G/CB99/vSfCEQ7BFVAPRO5vPrcS2QcwSDgvz06wKVA==
57 | /addressparser/0.3.2:
58 | dev: false
59 | optional: true
60 | resolution:
61 | integrity: sha1-WYc/Nej89sc2HBAjkmHXbhU0i7I=
62 | /asmcrypto.js/0.22.0:
63 | dev: false
64 | optional: true
65 | resolution:
66 | integrity: sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA==
67 | /asn1js/2.0.26:
68 | dependencies:
69 | pvutils: 1.0.17
70 | dev: false
71 | engines:
72 | node: '>=6.0.0'
73 | optional: true
74 | resolution:
75 | integrity: sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==
76 | /b64-lite/1.4.0:
77 | dependencies:
78 | base-64: 0.1.0
79 | dev: false
80 | optional: true
81 | resolution:
82 | integrity: sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w==
83 | /b64u-lite/1.1.0:
84 | dependencies:
85 | b64-lite: 1.4.0
86 | dev: false
87 | optional: true
88 | resolution:
89 | integrity: sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A==
90 | /base-64/0.1.0:
91 | dev: false
92 | optional: true
93 | resolution:
94 | integrity: sha1-eAqZyE59YAJgNhURxId2E78k9rs=
95 | /base64-js/1.3.1:
96 | dev: false
97 | resolution:
98 | integrity: sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
99 | /buffer/5.5.0:
100 | dependencies:
101 | base64-js: 1.3.1
102 | ieee754: 1.1.13
103 | dev: false
104 | resolution:
105 | integrity: sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==
106 | /bufferutil/4.0.1:
107 | dependencies:
108 | node-gyp-build: 3.7.0
109 | dev: false
110 | requiresBuild: true
111 | resolution:
112 | integrity: sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
113 | /compare-versions/3.6.0:
114 | dev: false
115 | optional: true
116 | resolution:
117 | integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
118 | /emailjs-base64/1.1.4:
119 | dev: false
120 | optional: true
121 | resolution:
122 | integrity: sha512-4h0xp1jgVTnIQBHxSJWXWanNnmuc5o+k4aHEpcLXSToN8asjB5qbXAexs7+PEsUKcEyBteNYsSvXUndYT2CGGA==
123 | /emailjs-mime-codec/2.0.9:
124 | dependencies:
125 | emailjs-base64: 1.1.4
126 | ramda: 0.26.1
127 | text-encoding: 0.7.0
128 | dev: false
129 | optional: true
130 | resolution:
131 | integrity: sha512-7qJo4pFGcKlWh/kCeNjmcgj34YoJWY0ekZXEHYtluWg4MVBnXqGM4CRMtZQkfYwitOhUgaKN5EQktJddi/YIDQ==
132 | /emailjs/2.2.0:
133 | dependencies:
134 | addressparser: 0.3.2
135 | emailjs-mime-codec: 2.0.9
136 | dev: false
137 | optional: true
138 | resolution:
139 | integrity: sha1-ulsj5KSwpFEPZS6HOxVOlAe2ygM=
140 | /expo-random/8.1.0:
141 | dependencies:
142 | base64-js: 1.3.1
143 | dev: false
144 | optional: true
145 | resolution:
146 | integrity: sha512-9n2gg83Hpg3ErkKu+a3FFOGmaPIxaHn6RuzjW24xFckdfmnrAKtbs1aU1aAcmoL1kXPvDeufRSEV/3lW93u6ug==
147 | /gun/0.2020.301:
148 | dependencies:
149 | buffer: 5.5.0
150 | ws: 7.2.3_5290a7aab7631971258e1bd11475725e
151 | dev: false
152 | engines:
153 | node: '>=0.8.4'
154 | optionalDependencies:
155 | bufferutil: 4.0.1
156 | emailjs: 2.2.0
157 | isomorphic-webcrypto: 2.3.6
158 | text-encoding: 0.7.0
159 | utf-8-validate: 5.0.2
160 | resolution:
161 | integrity: sha512-Jb5VNKgBt2my+XHB/K65gTOMbBICgglc4kVaMmQ/lD7ZyxLvwUSXissfcLHAwBxUWPU3P4GqTi+fQmsK068FQw==
162 | /ieee754/1.1.13:
163 | dev: false
164 | resolution:
165 | integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
166 | /invariant/2.2.4:
167 | dependencies:
168 | loose-envify: 1.4.0
169 | dev: false
170 | optional: true
171 | resolution:
172 | integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
173 | /isomorphic-webcrypto/2.3.6:
174 | dependencies:
175 | '@peculiar/webcrypto': 1.0.26
176 | asmcrypto.js: 0.22.0
177 | b64-lite: 1.4.0
178 | b64u-lite: 1.1.0
179 | msrcrypto: 1.5.8
180 | str2buf: 1.3.0
181 | webcrypto-shim: 0.1.5
182 | dev: false
183 | optional: true
184 | optionalDependencies:
185 | '@unimodules/core': 5.1.0
186 | '@unimodules/react-native-adapter': 5.1.1
187 | expo-random: 8.1.0
188 | react-native-securerandom: 0.1.1
189 | resolution:
190 | integrity: sha512-d1prB3b0UMWOao5DK3+O2Dr5ZJCakzB5Q+2kCWNkNuM9ln7VB8TSw2SwUjbnErzg7cgsYja+VPQaeBtXEojpew==
191 | /js-tokens/4.0.0:
192 | dev: false
193 | optional: true
194 | resolution:
195 | integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
196 | /lodash/4.17.15:
197 | dev: false
198 | optional: true
199 | resolution:
200 | integrity: sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
201 | /loose-envify/1.4.0:
202 | dependencies:
203 | js-tokens: 4.0.0
204 | dev: false
205 | hasBin: true
206 | optional: true
207 | resolution:
208 | integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
209 | /msrcrypto/1.5.8:
210 | dev: false
211 | optional: true
212 | resolution:
213 | integrity: sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q==
214 | /node-gyp-build/3.7.0:
215 | dev: false
216 | hasBin: true
217 | resolution:
218 | integrity: sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
219 | /object-assign/4.1.1:
220 | dev: false
221 | engines:
222 | node: '>=0.10.0'
223 | optional: true
224 | resolution:
225 | integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
226 | /prop-types/15.7.2:
227 | dependencies:
228 | loose-envify: 1.4.0
229 | object-assign: 4.1.1
230 | react-is: 16.13.1
231 | dev: false
232 | optional: true
233 | resolution:
234 | integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
235 | /pvtsutils/1.0.10:
236 | dependencies:
237 | tslib: 1.11.1
238 | dev: false
239 | optional: true
240 | resolution:
241 | integrity: sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw==
242 | /pvutils/1.0.17:
243 | dev: false
244 | engines:
245 | node: '>=6.0.0'
246 | optional: true
247 | resolution:
248 | integrity: sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==
249 | /ramda/0.26.1:
250 | dev: false
251 | optional: true
252 | resolution:
253 | integrity: sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
254 | /react-is/16.13.1:
255 | dev: false
256 | optional: true
257 | resolution:
258 | integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
259 | /react-native-securerandom/0.1.1:
260 | dependencies:
261 | base64-js: 1.3.1
262 | dev: false
263 | optional: true
264 | peerDependencies:
265 | react-native: '*'
266 | resolution:
267 | integrity: sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA=
268 | /str2buf/1.3.0:
269 | dev: false
270 | optional: true
271 | resolution:
272 | integrity: sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA==
273 | /text-encoding/0.7.0:
274 | deprecated: no longer maintained
275 | dev: false
276 | optional: true
277 | resolution:
278 | integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==
279 | /tslib/1.11.1:
280 | dev: false
281 | optional: true
282 | resolution:
283 | integrity: sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
284 | /utf-8-validate/5.0.2:
285 | dependencies:
286 | node-gyp-build: 3.7.0
287 | dev: false
288 | requiresBuild: true
289 | resolution:
290 | integrity: sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
291 | /webcrypto-core/1.0.18:
292 | dependencies:
293 | pvtsutils: 1.0.10
294 | tslib: 1.11.1
295 | dev: false
296 | optional: true
297 | resolution:
298 | integrity: sha512-wHRMXYxtDUWsTXNyRdaYlbcbq1OJF9pQov5THqvn5OBvixpCjnjU2spvEscxqRY8bLlpHk2S7RtROIMyNoEyFg==
299 | /webcrypto-shim/0.1.5:
300 | dev: false
301 | optional: true
302 | resolution:
303 | integrity: sha512-mE+E00gulvbLjHaAwl0kph60oOLQRsKyivEFgV9DMM/3Y05F1vZvGq12hAcNzHRnYxyEOABBT/XMtwGSg5xA7A==
304 | /ws/7.2.3:
305 | dev: false
306 | engines:
307 | node: '>=8.3.0'
308 | peerDependencies:
309 | bufferutil: ^4.0.1
310 | utf-8-validate: ^5.0.2
311 | peerDependenciesMeta:
312 | bufferutil:
313 | optional: true
314 | utf-8-validate:
315 | optional: true
316 | resolution:
317 | integrity: sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
318 | /ws/7.2.3_5290a7aab7631971258e1bd11475725e:
319 | dependencies:
320 | bufferutil: 4.0.1
321 | utf-8-validate: 5.0.2
322 | dev: false
323 | engines:
324 | node: '>=8.3.0'
325 | peerDependencies:
326 | bufferutil: ^4.0.1
327 | utf-8-validate: ^5.0.2
328 | peerDependenciesMeta:
329 | bufferutil:
330 | optional: true
331 | utf-8-validate:
332 | optional: true
333 | resolution:
334 | integrity: sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
335 | specifiers:
336 | gun: ^0.2020.301
337 | ws: ^7.0.1
338 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | websockets
--------------------------------------------------------------------------------
/scoreboard/controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Scoreboard Controller
6 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Scoreboard control
26 |
36 |
45 |
46 | Show
47 | Hide
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Connection settings
56 |
66 |
77 |
83 |
84 |
85 |
86 |
87 |
The scoreboard animation in this example was made by glowdragon.de
88 |
89 |
90 |
135 |
136 |
--------------------------------------------------------------------------------
/scoreboard/css/overlay.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Montserrat');
2 |
3 | html, body {
4 | height: 1080px;
5 | width: 1920px;
6 | margin: 0;
7 | overflow: hidden;
8 | font-family: 'Montserrat';
9 | background-color: transparent;
10 | }
11 |
12 | .clear {
13 | clear: both;
14 | }
15 |
16 | #scoreboard {
17 | -webkit-animation-timing-function: ease-in-out;
18 | -webkit-animation-duration: 2s;
19 | width: 128px;
20 | height: 50px;
21 | margin: 1080px auto 0;
22 | border-radius: 25px 25px 25px 25px;
23 | box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.75);
24 | }
25 |
26 | @-webkit-keyframes animation-scoreboard{
27 | 0% { width: 128px; margin-top: 1080px; }
28 | 33% { width: 128px; margin-top: 800px; }
29 | 66% { width: 640px; margin-top: 800px; }
30 | 100% { width: 640px; margin-top: 800px; }
31 | }
32 |
33 | .team {
34 | -webkit-animation-timing-function: ease-in-out;
35 | animation-timing-function: ease-in-out;
36 | -webkit-animation-duration: 2s;
37 | animation-duration: 2s;
38 | float: left;
39 | display: block;
40 | width: 0px;
41 | height: 44px;
42 | padding-top: 6px;
43 | border: 0 solid black;
44 | text-align: center;
45 | font-size: 22pt;
46 | color: white;
47 | }
48 |
49 | #team1div {
50 | background-color: #3093c7;
51 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3093c7), to(#1c5a85));
52 | background-image: -webkit-linear-gradient(top, #3093c7, #1c5a85);
53 | background-image: -moz-linear-gradient(top, #3093c7, #1c5a85);
54 | background-image: -ms-linear-gradient(top, #3093c7, #1c5a85);
55 | background-image: -o-linear-gradient(top, #3093c7, #1c5a85);
56 | background-image: linear-gradient(to bottom, #3093c7, #1c5a85);
57 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#3093c7, endColorstr=#1c5a85);
58 | border-radius: 25px 0px 0px 25px;
59 | }
60 |
61 | #team2div {
62 | background-color: #a90329;
63 | background-image: -webkit-gradient(linear, left top, left bottom, from(#a90329), to(#6d0019));
64 | background-image: -webkit-linear-gradient(top, #a90329, #6d0019);
65 | background-image: -moz-linear-gradient(top, #a90329, #6d0019);
66 | background-image: -ms-linear-gradient(top, #a90329, #6d0019);
67 | background-image: -o-linear-gradient(top, #a90329, #6d0019);
68 | background-image: linear-gradient(to bottom, #a90329, #6d0019);
69 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#a90329, endColorstr=#6d0019);
70 | border-radius: 0px 25px 25px 0px;
71 | }
72 |
73 | @-webkit-keyframes animation-team {
74 | 0% { width: 0px; }
75 | 33% { width: 0px; }
76 | 66% { width: 256px; }
77 | 100% { width: 256px; }
78 | }
79 |
80 | .team-name {
81 | -webkit-animation-timing-function: ease-in-out;
82 | -webkit-animation-duration: 2s;
83 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 1);
84 | width: 0px;
85 | }
86 |
87 | @-webkit-keyframes animation-team-name {
88 | 0% { opacity: 0; }
89 | 66% { opacity: 0; }
90 | 100% { opacity: 1; }
91 | }
92 |
93 | #scorePlaceholder {
94 | float: left;
95 | width: 128px;
96 | }
97 |
98 | #score {
99 | -webkit-animation-timing-function: ease-in-out;
100 | animation-timing-function: ease-in-out;
101 | -webkit-animation-duration: 2s;
102 | animation-duration: 2s;
103 | position: absolute;
104 | top: 1080px;
105 | left: 893px;
106 | width: 134px;
107 | height: 47px;
108 | padding-top: 9px;
109 | text-align: center;
110 | background-color: white;
111 | font-size: 22pt;
112 | color: black;
113 | border-radius: 3px;
114 | border: 0 solid black;
115 | box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.75);
116 | }
117 |
118 | @-webkit-keyframes animation-score {
119 | 0% { top: 1080px; }
120 | 33% { top: 797px; }
121 | 100% { top: 797px; }
122 | }
123 |
124 | .show #score {
125 | top: 797px;
126 | }
127 | .show #scoreboard {
128 | margin-top: 800px;
129 | width: 640px;
130 | }
131 | .show .team {
132 | width: 256px;
133 | }
134 | .show .team-name {
135 | width: 256px;
136 | }
137 |
138 | .animated #score {
139 | visibility: visible;
140 | -webkit-animation-name: "animation-score";
141 | animation-name: "animation-score";
142 | animation-direction: normal;
143 | }
144 | .animated #scoreboard {
145 | visibility: visible;
146 | -webkit-animation-name: "animation-scoreboard";
147 | animation-name: "animation-scoreboard";
148 | animation-direction: normal;
149 | }
150 | .animated .team {
151 | -webkit-animation-name: "animation-team";
152 | animation-name: "animation-team";
153 | animation-direction: normal;
154 | }
155 | .animated .team-name {
156 | -webkit-animation-name: "animation-team-name";
157 | animation-name: "animation-team-name";
158 | animation-direction: normal;
159 | }
160 |
161 | .show.animated #score,
162 | .show.animated #scoreboard,
163 | .show.animated .team,
164 | .show.animated .team-name {
165 | animation-direction: reverse;
166 | }
--------------------------------------------------------------------------------
/scoreboard/overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Scoreboard
6 |
9 |
10 |
70 |
71 |
72 |
73 | 0 - 0
74 |
75 |
76 |
77 | Team 1
78 |
79 |
.
80 |
81 | Team 2
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws');
2 | const url = require('url');
3 |
4 | const server = new WebSocket.Server({
5 | // host: process.argv[1] || '0.0.0.0',
6 | port: process.argv[2] || 8089,
7 | perMessageDeflate: {
8 | zlibDeflateOptions: {
9 | // See zlib defaults.
10 | chunkSize: 1024,
11 | memLevel: 7,
12 | level: 3
13 | },
14 | zlibInflateOptions: {
15 | chunkSize: 10 * 1024
16 | },
17 | // Other options settable:
18 | clientNoContextTakeover: true, // Defaults to negotiated value.
19 | serverNoContextTakeover: true, // Defaults to negotiated value.
20 | serverMaxWindowBits: 10, // Defaults to negotiated value.
21 | // Below options specified as default values.
22 | concurrencyLimit: 10, // Limits zlib concurrency for perf.
23 | threshold: 1024 // Size (in bytes) below which messages
24 | // should not be compressed.
25 | }
26 | });
27 |
28 | let STATE = {};
29 |
30 | server.on('connection', function connection(ws, req) {
31 | const ip = req.connection.remoteAddress;
32 | const port = req.connection.remotePort;
33 | const pathname = url.parse(req.url).pathname;
34 | console.log(ip, port, pathname, 'connected');
35 | if (!STATE[pathname]) {
36 | STATE[pathname] = {};
37 | }
38 | ws.send(JSON.stringify(STATE[pathname]));
39 |
40 | ws.on('message', function incoming(message) {
41 | console.log(ip, port, message)
42 | const data = JSON.parse(message);
43 | Object.assign(STATE[pathname], data);
44 | // Broadcast to everyone else.
45 | server.clients.forEach(function each(client) {
46 | if (client !== ws && client.readyState === WebSocket.OPEN) {
47 | client.send(message);
48 | }
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | "Websocket server that synchronizes state across clients"
4 |
5 | import asyncio
6 | import json
7 | import sys
8 | import websockets
9 | from collections import defaultdict
10 |
11 | CLIENTS = defaultdict(set)
12 | STATE = defaultdict(dict)
13 |
14 | async def distributor(websocket, path):
15 | CLIENTS[path].add(websocket)
16 | await websocket.send(json.dumps(STATE[path]))
17 | print('%s:%s connected' % websocket.remote_address, path)
18 | try:
19 | async for message in websocket:
20 | try:
21 | STATE[path].update(json.loads(message))
22 | except Exception:
23 | print('Message is not valid JSON', message)
24 | if len(CLIENTS[path]) > 1: # asyncio.wait doesn't accept an empty list
25 | await asyncio.wait([client.send(message) for client in CLIENTS[path] if client != websocket])
26 | print('%s:%s' % websocket.remote_address, message)
27 | finally:
28 | CLIENTS[path].remove(websocket)
29 | if not CLIENTS[path]:
30 | del CLIENTS[path]
31 | print('%s:%s disconnected' % websocket.remote_address)
32 |
33 | def help():
34 | print('''Websocket sync server
35 | Relays each message to all clients.
36 | Usage:', sys.argv[0], '[host [port]]''')
37 |
38 | if __name__ == '__main__':
39 | if '-h' in sys.argv or '--help' in sys.argv:
40 | help()
41 | else:
42 | host = 'localhost'
43 | port = 8000
44 |
45 | try:
46 | host = sys.argv[1]
47 | port = int(sys.argv[2])
48 | except (IndexError, ValueError):
49 | pass
50 |
51 | try:
52 | asyncio.get_event_loop().run_until_complete(
53 | websockets.serve(distributor, host, port))
54 | asyncio.get_event_loop().run_forever()
55 | except Exception:
56 | help()
57 | raise
58 |
--------------------------------------------------------------------------------