This commit is contained in:
Martin Donnelly 2022-07-07 11:38:15 +01:00
commit 42a9be107e
41 changed files with 14611 additions and 0 deletions

62
.eslintrc.json Normal file
View File

@ -0,0 +1,62 @@
{
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module"
},
"env": {
"es6": true,
"browser": true
},
"plugins": [
"svelte3"
],
"overrides": [
{
"files": [
"**/*.svelte"
],
"processor": "svelte3/svelte3"
}
],
"rules": {
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "stroustrup", {}],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": [1, "last"],
"consistent-this": [1, "_this"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
"indent": ["error", 2, { "SwitchCase": 1 }],
"lines-around-comment": ["error", { "beforeBlockComment": true, "allowArrayStart": true }],
"max-len": [1, 240, 2], // 2 spaces per tab, max 80 chars per line
"new-cap": 1,
"newline-before-return": "error",
"no-array-constructor": 1,
"no-inner-declarations": [1, "both"],
"no-mixed-spaces-and-tabs": 1,
"no-multi-spaces": 2,
"no-new-object": 1,
"no-shadow-restricted-names": 1,
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", { "blocks": "never", "switches": "always" }],
"prefer-const": "error",
"prefer-template": "error",
"one-var": 0,
"quote-props": ["error", "always"],
"quotes": [1, "single"],
"radix": 1,
"semi": [1, "always"],
"space-before-blocks": [1, "always"],
"space-infix-ops": 1,
"vars-on-top": 1,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
"spaced-comment": ["error", "always", { "markers": ["/"] }]
}
}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
.DS_Store

20
Docker/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# FROM node:current-slim
FROM martind2000/node-python3:16
ARG VERSION
ENV VERSION ${VERSION:-development}
WORKDIR /app
COPY ./Docker/start.sh ./package*.json ./server.js /app/
COPY ./public /app/public
RUN npm install
# RUN ls -lh .
EXPOSE 8130
RUN chmod +x /app/start.sh
ENTRYPOINT ["/app/start.sh"]

View File

@ -0,0 +1,12 @@
[
{
"name": "Slack",
"script": "app/predict.js",
"env": {
"NODE_ENV": "production"
},
"autorestart": false,
"instances": 1,
"cron_restart": "10 15 * * 2,5"
}
]

4
Docker/start.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
set -ex
node server.js

35
Makefile Normal file
View File

@ -0,0 +1,35 @@
PROJECT = slack
VERSION = $(shell git rev-parse --short HEAD)
ECR_REPO = mail.caliban.io:5000
#APP_IMAGE = 482681734622.dkr.ecr.eu-west-1.amazonaws.com/$(PROJECT):$(VERSION)
APP_IMAGE = $(ECR_REPO)/$(PROJECT):$(VERSION)
NO_CACHE = true
#build docker image
build:
npm run build
# docker build ./Docker/. -t $(APP_IMAGE) --build-arg VERSION=$(VERSION) --no-cache=$(NO_CACHE) --compress=true
docker-compose build
.PHONY: build
#push docker image to registry
push: build
docker push $(APP_IMAGE)
.PHONY: push
#push docker image to registry
run: build
docker run $(APP_IMAGE)
.PHONY: run
ver:
@echo '$(VERSION)'
#echo $ERSION
.PHONY: ver
tar:
# docker build . -t $(APP_IMAGE) --build-arg VERSION=$(VERSION) --no-cache=$(NO_CACHE)
tar -C ./ -czvf ./archive.tar.gz 'package.json' 'ncas/' 'helpers/' -X *.js
.PHONY: build

106
README.md Normal file
View File

@ -0,0 +1,106 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# Updates
## 2020-04-17
Added [svelte-preprocess](https://www.npmjs.com/package/svelte-preprocess) preprocessor with support for: PostCSS, SCSS, Less, Stylus, Coffeescript, TypeScript and Pug.
Added [rollup-plugin-node-builtins](https://www.npmjs.com/package/rollup-plugin-node-builtins) Allows the node builtins to be required/imported. Doing so gives the proper shims to support modules that were designed for Browserify, some modules require rollup-plugin-node-globals.
Added [rollup-plugin-node-globals](https://www.npmjs.com/package/rollup-plugin-node-globals) Plugin to insert node globals including so code that works with browserify should work even if it uses process or buffers.
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Deploying to the web
### With [now](https://zeit.co/now)
Install `now` if you haven't already:
```bash
npm install -g now
```
Then, from within your project folder:
```bash
cd public
now deploy --name my-project
```
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

4
copy.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
# rm -rf /home/martin/dev/silvrgit/dist/*
cp -r /home/martin/dev/Svelte/svelte-silvrtree/public/* /home/martin/dev/Server/silvrgit/dist

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
version: '3.5'
services:
multiview:
container_name: multiview
build:
context: .
dockerfile: ./Docker/Dockerfile
image: multiview_docker
restart: always
ports:
- "8080:8130"

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

9475
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "docker-multiview",
"version": "1.0.0",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^12.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"autoprefixer": "^9.8.0",
"eslint": "^7.1.0",
"eslint-plugin-svelte3": "^2.7.3",
"rollup": "^2.11.2",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^6.1.0",
"svelte": "^3.0.0",
"svelte-preprocess": "^3.7.1"
},
"dependencies": {
"compression": "^1.7.4",
"node-sass": "^6.0.1",
"polka": "^0.5.2",
"rollup-plugin-replace": "^2.2.0",
"sirv": "^2.0.2",
"sirv-cli": "^2.0.2",
"video.js": "^7.19.2",
"videojs-youtube": "^2.6.1"
}
}

3500
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

9
public/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/img/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

2
public/build/bundle.css Normal file
View File

@ -0,0 +1,2 @@
/*# sourceMappingURL=bundle.css.map */

View File

@ -0,0 +1,8 @@
{
"version": 3,
"file": "bundle.css",
"sources": [],
"sourcesContent": [],
"names": [],
"mappings": ""
}

20
public/build/bundle.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

79
public/global.css Normal file
View File

@ -0,0 +1,79 @@
html, body {
background-color: #000000;
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
font-family: 'Muli', sans-serif;
color: #cccccc;
}
#container {
width: 100%;
height: 100%;
padding: 1px 5px;
}
.quarter {
width: calc(50% - 5px);
float: left;
}
.stream {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border: 3px solid #000000;
}
.wrapper {
padding-bottom: 56.55%;
width: 100%;
position: relative;
overflow: hidden;
}
.active {
border: 3px solid #ff3333;
}
.title {
background-color: #000000;
opacity: 0.3;
border-bottom-right-radius: 4px;
padding: 0px 8px 5px 5px;
color: #ffffff;
position: absolute;
top: 0;
left: 0;
z-index: 1000;
}
.overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1500;
}
.live .overlay {
cursor: pointer;
}
.offline {
background-color: #111111;
}
.offline p {
position: absolute;
text-align: center;
top: 25%;
left: 25%;
width: 50%;
height: 50%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1706.667" height="1706.667" viewBox="0 0 1280.000000 1280.000000"><path d="M0 182.8v182.9l182.8-.1 182.7-.1.1-182.8.1-182.7H0v182.8zM457 182.8v182.9h366V0H457v182.8zM914.4 182.7l.1 182.8 182.8.1 182.7.1V0H914.3l.1 182.7zM0 640v183l182.8-.2 182.7-.3.1-182.7.1-182.8H0v183zM457 640v183h366V457H457v183zM914.7 457.7c-.2.5-.4 82.8-.4 183V823H1280V457h-182.4c-100.4 0-182.7.3-182.9.7zM0 1097.2V1280H365.7l-.1-182.8-.1-182.7-182.7-.1L0 914.3v182.9zM457 1097.2V1280h366l-.2-182.8-.3-182.7-182.7-.1-182.8-.1v182.9zM914.3 1097.2V1280H1280V914.3H914.3v182.9z"/></svg>

After

Width:  |  Height:  |  Size: 616 B

36
public/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Multiview</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/img/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/img/favicon.ico">
<meta name="apple-mobile-web-app-title" content="Multiview">
<meta name="application-name" content="Multiview">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="/img/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<link href="//vjs.zencdn.net/7.10.2/video-js.min.css" rel="stylesheet">
<script src="//vjs.zencdn.net/7.10.2/video.min.js"></script>
<script src="//www.youtube.com/iframe_api"></script>
<script src= "//player.twitch.tv/js/embed/v1.js"></script>
<link href="//fonts.googleapis.com/css?family=Muli" rel="stylesheet">
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>

544
public/js/videojs.youtube.min.js vendored Normal file
View File

@ -0,0 +1,544 @@
(function (root, factory) {
if (typeof exports === "object" && typeof module !== "undefined") {
var videojs = require("video.js");
module.exports = factory(videojs.default || videojs)
} else if (typeof define === "function" && define.amd) {
define(["videojs"], function (videojs) {
return root.Youtube = factory(videojs)
})
} else {
root.Youtube = factory(root.videojs)
}
})(this, function (videojs) {
"use strict";
var _isOnMobile = videojs.browser.IS_IOS || videojs.browser.IS_NATIVE_ANDROID;
var Tech = videojs.getTech("Tech");
var Youtube = videojs.extend(Tech, {
constructor: function (options, ready) {
Tech.call(this, options, ready);
this.setPoster(options.poster);
this.setSrc(this.options_.source, true);
this.setTimeout(function () {
if (this.el_) {
this.el_.parentNode.className += " vjs-youtube";
if (_isOnMobile) {
this.el_.parentNode.className += " vjs-youtube-mobile"
}
if (Youtube.isApiReady) {
this.initYTPlayer()
} else {
Youtube.apiReadyQueue.push(this)
}
}
}.bind(this))
}, dispose: function () {
if (this.ytPlayer) {
if (this.ytPlayer.stopVideo) {
this.ytPlayer.stopVideo()
}
if (this.ytPlayer.destroy) {
this.ytPlayer.destroy()
}
} else {
var index = Youtube.apiReadyQueue.indexOf(this);
if (index !== -1) {
Youtube.apiReadyQueue.splice(index, 1)
}
}
this.ytPlayer = null;
this.el_.parentNode.className = this.el_.parentNode.className.replace(" vjs-youtube", "").replace(" vjs-youtube-mobile", "");
this.el_.parentNode.removeChild(this.el_);
Tech.prototype.dispose.call(this)
}, createEl: function () {
var div = document.createElement("div");
div.setAttribute("id", this.options_.techId);
div.setAttribute("style", "width:100%;height:100%;top:0;left:0;position:absolute");
div.setAttribute("class", "vjs-tech");
var divWrapper = document.createElement("div");
divWrapper.appendChild(div);
if (!_isOnMobile && !this.options_.ytControls) {
var divBlocker = document.createElement("div");
divBlocker.setAttribute("class", "vjs-iframe-blocker");
divBlocker.setAttribute("style", "position:absolute;top:0;left:0;width:100%;height:100%");
divBlocker.onclick = function () {
this.pause()
}.bind(this);
divWrapper.appendChild(divBlocker)
}
return divWrapper
}, initYTPlayer: function () {
var playerVars = {controls: 0, modestbranding: 1, rel: 0, showinfo: 0, loop: this.options_.loop ? 1 : 0};
if (typeof this.options_.autohide !== "undefined") {
playerVars.autohide = this.options_.autohide
}
if (typeof this.options_["cc_load_policy"] !== "undefined") {
playerVars["cc_load_policy"] = this.options_["cc_load_policy"]
}
if (typeof this.options_.ytControls !== "undefined") {
playerVars.controls = this.options_.ytControls
}
if (typeof this.options_.disablekb !== "undefined") {
playerVars.disablekb = this.options_.disablekb
}
if (typeof this.options_.color !== "undefined") {
playerVars.color = this.options_.color
}
if (!playerVars.controls) {
playerVars.fs = 0
} else if (typeof this.options_.fs !== "undefined") {
playerVars.fs = this.options_.fs
}
if (this.options_.source.src.indexOf("end=") !== -1) {
var srcEndTime = this.options_.source.src.match(/end=([0-9]*)/);
this.options_.end = parseInt(srcEndTime[1])
}
if (typeof this.options_.end !== "undefined") {
playerVars.end = this.options_.end
}
if (typeof this.options_.hl !== "undefined") {
playerVars.hl = this.options_.hl
} else if (typeof this.options_.language !== "undefined") {
playerVars.hl = this.options_.language.substr(0, 2)
}
if (typeof this.options_["iv_load_policy"] !== "undefined") {
playerVars["iv_load_policy"] = this.options_["iv_load_policy"]
}
if (typeof this.options_.list !== "undefined") {
playerVars.list = this.options_.list
} else if (this.url && typeof this.url.listId !== "undefined") {
playerVars.list = this.url.listId
}
if (typeof this.options_.listType !== "undefined") {
playerVars.listType = this.options_.listType
}
if (typeof this.options_.modestbranding !== "undefined") {
playerVars.modestbranding = this.options_.modestbranding
}
if (typeof this.options_.playlist !== "undefined") {
playerVars.playlist = this.options_.playlist
}
if (typeof this.options_.playsinline !== "undefined") {
playerVars.playsinline = this.options_.playsinline
}
if (typeof this.options_.rel !== "undefined") {
playerVars.rel = this.options_.rel
}
if (typeof this.options_.showinfo !== "undefined") {
playerVars.showinfo = this.options_.showinfo
}
if (this.options_.source.src.indexOf("start=") !== -1) {
var srcStartTime = this.options_.source.src.match(/start=([0-9]*)/);
this.options_.start = parseInt(srcStartTime[1])
}
if (typeof this.options_.start !== "undefined") {
playerVars.start = this.options_.start
}
if (typeof this.options_.theme !== "undefined") {
playerVars.theme = this.options_.theme
}
if (typeof this.options_.customVars !== "undefined") {
var customVars = this.options_.customVars;
Object.keys(customVars).forEach(function (key) {
playerVars[key] = customVars[key]
})
}
this.activeVideoId = this.url ? this.url.videoId : null;
this.activeList = playerVars.list;
var playerConfig = {
videoId: this.activeVideoId,
playerVars: playerVars,
events: {
onReady: this.onPlayerReady.bind(this),
onPlaybackQualityChange: this.onPlayerPlaybackQualityChange.bind(this),
onPlaybackRateChange: this.onPlayerPlaybackRateChange.bind(this),
onStateChange: this.onPlayerStateChange.bind(this),
onVolumeChange: this.onPlayerVolumeChange.bind(this),
onError: this.onPlayerError.bind(this)
}
};
if (typeof this.options_.enablePrivacyEnhancedMode !== "undefined" && this.options_.enablePrivacyEnhancedMode) {
playerConfig.host = "https://www.youtube-nocookie.com"
}
this.ytPlayer = new YT.Player(this.options_.techId, playerConfig)
}, onPlayerReady: function () {
if (this.options_.muted) {
this.ytPlayer.mute()
}
var playbackRates = this.ytPlayer.getAvailablePlaybackRates();
if (playbackRates.length > 1) {
this.featuresPlaybackRate = true
}
this.playerReady_ = true;
this.triggerReady();
if (this.playOnReady) {
this.play()
} else if (this.cueOnReady) {
this.cueVideoById_(this.url.videoId);
this.activeVideoId = this.url.videoId
}
}, onPlayerPlaybackQualityChange: function () {
}, onPlayerPlaybackRateChange: function () {
this.trigger("ratechange")
}, onPlayerStateChange: function (e) {
var state = e.data;
if (state === this.lastState || this.errorNumber) {
return
}
this.lastState = state;
switch (state) {
case-1:
this.trigger("loadstart");
this.trigger("loadedmetadata");
this.trigger("durationchange");
this.trigger("ratechange");
break;
case YT.PlayerState.ENDED:
this.trigger("ended");
break;
case YT.PlayerState.PLAYING:
this.trigger("timeupdate");
this.trigger("durationchange");
this.trigger("playing");
this.trigger("play");
if (this.isSeeking) {
this.onSeeked()
}
break;
case YT.PlayerState.PAUSED:
this.trigger("canplay");
if (this.isSeeking) {
this.onSeeked()
} else {
this.trigger("pause")
}
break;
case YT.PlayerState.BUFFERING:
this.player_.trigger("timeupdate");
this.player_.trigger("waiting");
break
}
}, onPlayerVolumeChange: function () {
this.trigger("volumechange")
}, onPlayerError: function (e) {
this.errorNumber = e.data;
this.trigger("pause");
this.trigger("error")
}, error: function () {
var code = 1e3 + this.errorNumber;
switch (this.errorNumber) {
case 5:
return {code: code, message: "Error while trying to play the video"};
case 2:
case 100:
return {code: code, message: "Unable to find the video"};
case 101:
case 150:
return {code: code, message: "Playback on other Websites has been disabled by the video owner."}
}
return {code: code, message: "YouTube unknown error (" + this.errorNumber + ")"}
}, loadVideoById_: function (id) {
var options = {videoId: id};
if (this.options_.start) {
options.startSeconds = this.options_.start
}
if (this.options_.end) {
options.endEnd = this.options_.end
}
this.ytPlayer.loadVideoById(options)
}, cueVideoById_: function (id) {
var options = {videoId: id};
if (this.options_.start) {
options.startSeconds = this.options_.start
}
if (this.options_.end) {
options.endEnd = this.options_.end
}
this.ytPlayer.cueVideoById(options)
}, src: function (src) {
if (src) {
this.setSrc({src: src})
}
return this.source
}, poster: function () {
if (_isOnMobile) {
return null
}
return this.poster_
}, setPoster: function (poster) {
this.poster_ = poster
}, setSrc: function (source) {
if (!source || !source.src) {
return
}
delete this.errorNumber;
this.source = source;
this.url = Youtube.parseUrl(source.src);
if (!this.options_.poster) {
if (this.url.videoId) {
this.poster_ = "https://img.youtube.com/vi/" + this.url.videoId + "/0.jpg";
this.trigger("posterchange");
this.checkHighResPoster()
}
}
if (this.options_.autoplay && !_isOnMobile) {
if (this.isReady_) {
this.play()
} else {
this.playOnReady = true
}
} else if (this.activeVideoId !== this.url.videoId) {
if (this.isReady_) {
this.cueVideoById_(this.url.videoId);
this.activeVideoId = this.url.videoId
} else {
this.cueOnReady = true
}
}
}, autoplay: function () {
return this.options_.autoplay
}, setAutoplay: function (val) {
this.options_.autoplay = val
}, loop: function () {
return this.options_.loop
}, setLoop: function (val) {
this.options_.loop = val
}, play: function () {
if (!this.url || !this.url.videoId) {
return
}
this.wasPausedBeforeSeek = false;
if (this.isReady_) {
if (this.url.listId) {
if (this.activeList === this.url.listId) {
this.ytPlayer.playVideo()
} else {
this.ytPlayer.loadPlaylist(this.url.listId);
this.activeList = this.url.listId
}
}
if (this.activeVideoId === this.url.videoId) {
this.ytPlayer.playVideo()
} else {
this.loadVideoById_(this.url.videoId);
this.activeVideoId = this.url.videoId
}
} else {
this.trigger("waiting");
this.playOnReady = true
}
}, pause: function () {
if (this.ytPlayer) {
this.ytPlayer.pauseVideo()
}
}, paused: function () {
return this.ytPlayer ? this.lastState !== YT.PlayerState.PLAYING && this.lastState !== YT.PlayerState.BUFFERING : true
}, currentTime: function () {
return this.ytPlayer ? this.ytPlayer.getCurrentTime() : 0
}, setCurrentTime: function (seconds) {
if (this.lastState === YT.PlayerState.PAUSED) {
this.timeBeforeSeek = this.currentTime()
}
if (!this.isSeeking) {
this.wasPausedBeforeSeek = this.paused()
}
this.ytPlayer.seekTo(seconds, true);
this.trigger("timeupdate");
this.trigger("seeking");
this.isSeeking = true;
if (this.lastState === YT.PlayerState.PAUSED && this.timeBeforeSeek !== seconds) {
clearInterval(this.checkSeekedInPauseInterval);
this.checkSeekedInPauseInterval = setInterval(function () {
if (this.lastState !== YT.PlayerState.PAUSED || !this.isSeeking) {
clearInterval(this.checkSeekedInPauseInterval)
} else if (this.currentTime() !== this.timeBeforeSeek) {
this.trigger("timeupdate");
this.onSeeked()
}
}.bind(this), 250)
}
}, seeking: function () {
return this.isSeeking
}, seekable: function () {
if (!this.ytPlayer) {
return videojs.createTimeRange()
}
return videojs.createTimeRange(0, this.ytPlayer.getDuration())
}, onSeeked: function () {
clearInterval(this.checkSeekedInPauseInterval);
this.isSeeking = false;
if (this.wasPausedBeforeSeek) {
this.pause()
}
this.trigger("seeked")
}, playbackRate: function () {
return this.ytPlayer ? this.ytPlayer.getPlaybackRate() : 1
}, setPlaybackRate: function (suggestedRate) {
if (!this.ytPlayer) {
return
}
this.ytPlayer.setPlaybackRate(suggestedRate)
}, duration: function () {
return this.ytPlayer ? this.ytPlayer.getDuration() : 0
}, currentSrc: function () {
return this.source && this.source.src
}, ended: function () {
return this.ytPlayer ? this.lastState === YT.PlayerState.ENDED : false
}, volume: function () {
return this.ytPlayer ? this.ytPlayer.getVolume() / 100 : 1
}, setVolume: function (percentAsDecimal) {
if (!this.ytPlayer) {
return
}
this.ytPlayer.setVolume(percentAsDecimal * 100)
}, muted: function () {
return this.ytPlayer ? this.ytPlayer.isMuted() : false
}, setMuted: function (mute) {
if (!this.ytPlayer) {
return
} else {
this.muted(true)
}
if (mute) {
this.ytPlayer.mute()
} else {
this.ytPlayer.unMute()
}
this.setTimeout(function () {
this.trigger("volumechange")
}, 50)
}, buffered: function () {
if (!this.ytPlayer || !this.ytPlayer.getVideoLoadedFraction) {
return videojs.createTimeRange()
}
var bufferedEnd = this.ytPlayer.getVideoLoadedFraction() * this.ytPlayer.getDuration();
return videojs.createTimeRange(0, bufferedEnd)
}, preload: function () {
}, load: function () {
}, reset: function () {
}, networkState: function () {
if (!this.ytPlayer) {
return 0
}
switch (this.ytPlayer.getPlayerState()) {
case-1:
return 0;
case 3:
return 2;
default:
return 1
}
}, readyState: function () {
if (!this.ytPlayer) {
return 0
}
switch (this.ytPlayer.getPlayerState()) {
case-1:
return 0;
case 5:
return 1;
case 3:
return 2;
default:
return 4
}
}, supportsFullScreen: function () {
return document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled
}, checkHighResPoster: function () {
var uri = "https://img.youtube.com/vi/" + this.url.videoId + "/maxresdefault.jpg";
try {
var image = new Image;
image.onload = function () {
if ("naturalHeight" in image) {
if (image.naturalHeight <= 90 || image.naturalWidth <= 120) {
return
}
} else if (image.height <= 90 || image.width <= 120) {
return
}
this.poster_ = uri;
this.trigger("posterchange")
}.bind(this);
image.onerror = function () {
};
image.src = uri
} catch (e) {
}
}
});
Youtube.isSupported = function () {
return true
};
Youtube.canPlaySource = function (e) {
return Youtube.canPlayType(e.type)
};
Youtube.canPlayType = function (e) {
return e === "video/youtube"
};
Youtube.parseUrl = function (url) {
var result = {videoId: null};
var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = url.match(regex);
if (match && match[2].length === 11) {
result.videoId = match[2]
}
var regPlaylist = /[?&]list=([^#\&\?]+)/;
match = url.match(regPlaylist);
if (match && match[1]) {
result.listId = match[1]
}
return result
};
function apiLoaded() {
YT.ready(function () {
Youtube.isApiReady = true;
for (var i = 0; i < Youtube.apiReadyQueue.length; ++i) {
Youtube.apiReadyQueue[i].initYTPlayer()
}
})
}
function loadScript(src, callback) {
var loaded = false;
var tag = document.createElement("script");
var firstScriptTag = document.getElementsByTagName("script")[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
tag.onload = function () {
if (!loaded) {
loaded = true;
callback()
}
};
tag.onreadystatechange = function () {
if (!loaded && (this.readyState === "complete" || this.readyState === "loaded")) {
loaded = true;
callback()
}
};
tag.src = src
}
function injectCss() {
var css = ".vjs-youtube .vjs-iframe-blocker { display: none; }" + ".vjs-youtube.vjs-user-inactive .vjs-iframe-blocker { display: block; }" + ".vjs-youtube .vjs-poster { background-size: cover; }" + ".vjs-youtube-mobile .vjs-big-play-button { display: none; }";
var head = document.head || document.getElementsByTagName("head")[0];
var style = document.createElement("style");
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
head.appendChild(style)
}
Youtube.apiReadyQueue = [];
if (typeof document !== "undefined") {
loadScript("https://www.youtube.com/iframe_api", apiLoaded);
injectCss()
}
if (typeof videojs.registerTech !== "undefined") {
videojs.registerTech("Youtube", Youtube)
} else {
videojs.registerComponent("Youtube", Youtube)
}
});

74
public/list.json Normal file
View File

@ -0,0 +1,74 @@
[
{
'type': 'live',
'title': 'Sky News',
'id' : 'skynews',
'src': 'http://skydvn-sn-mobile-prod.skydvn.com/skynews/1404/latest.m3u8#{now}'
},
{
'type': 'live',
'title': '',
'id' : '',
'src': ''
},
{
'type': 'youtube',
'title': '',
'id' : '',
'src': ''
},
{
'type': 'live',
'title': '',
'id' : '',
'src': ''
},
{
'type': '',
'title': '',
'id' : '',
'src': ''
},
{
'type': '',
'title': '',
'id' : '',
'src': ''
},
{
'type': '',
'title': '',
'id' : '',
'src': ''
},
{
'type': '',
'title': '',
'id' : '',
'src': ''
},
{
'type': '',
'title': '',
'id' : '',
'src': ''
}
]
/*
<Live title="Sky News" id="skynews" src="http://skydvn-sn-mobile-prod.skydvn.com/skynews/1404/latest.m3u8#{now}"/>
<Live title="BBC News" id="bbcnews24" src="http://vs-hls-push-uk.live.cf.md.bbci.co.uk/manifest/x=3/i=urn:bbc:pips:service:bbc_news_channel_hd/mobile_wifi_main_sd_abr_v2_http.m3u8#{now}"/>
<Youtube title="EuroNews" id="euronews" src="https://www.youtube.com/embed/sPgqEHsONK8?enablejsapi=1&autoplay=1&mute=1&controls=0&fs=0&modestbranding=1&cc_load_policy=1"/>
<Live title="BBC Parliament" id="bbcparliament" src="http://vs-hls-pushb-uk-live.akamaized.net/manifest/x=3/i=urn:bbc:pips:service:bbc_parliament/mobile_wifi_main_sd_abr_v2_akamai_hls_live_http.m3u8#{now}"/>
<Live title="Bloomberg" id="bloomberg" src="https://liveprodeuwest.akamaized.net/eu1/Channel-EUTVqvs-AWS-ireland-1/Source-EUTVqvs-1000-1_live.m3u8#{now}"/>
<Live title="BBC Scotland" id="bbcscotland" src="https://vs-hls-pushb-uk-live.akamaized.net/content/x=3/v=pv14/b=5070016/t=3840/i=urn:bbc:pips:service:bbc_scotland_hd/main.m3u8#{now}"/>
<Twitch title="twitch.tv/rukpolitics" id="rukpolitics" channel="rukpolitics"/>
<Twitch title="twitch.tv/ukcommons" id="ukcommons" channel="ukcommons"/>
<Twitch title="twitch.tv/democracylive" id="democracylive" channel="democracylive"/>
*/

106
public/service-worker.js Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2016 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const CACHE_VERSION = 8;
const dataCacheName = `multiview-v${CACHE_VERSION}`;
const cacheName = `multiview-final-${CACHE_VERSION}`;
const filesToCache = [
'/',
'/index.html',
'/service-worker.js',
'/site.webmanifest',
'/favicon.png',
'/browserconfig.xml',
'/build/bundle.css',
'/build/bundle.js',
'/img/android-chrome-192x192.png',
'/img/android-chrome-512x512.png',
'/img/favicon.ico',
'/img/favicon-16x16.png',
'/img/favicon-32x32.png'
];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName && key !== dataCacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
/*
* Fixes a corner case in which the app wasn't returning the latest data.
* You can reproduce the corner case by commenting out the line below and
* then doing the following steps: 1) load app for first time so that the
* initial New York City data is shown 2) press the refresh button on the
* app 3) go offline 4) reload the app. You expect to see the newer NYC
* data, but you actually see the initial data. This happens because the
* service worker is not yet activated. The code below essentially lets
* you activate the service worker faster.
*/
return self.clients.claim();
});
self.addEventListener('fetch', function(e) {
console.warn('[Service Worker] Fetch', e.request.url);
const dataUrl = '/getnexttraintimes?';
if (e.request.url.indexOf(dataUrl) > -1) {
console.log('!');
/*
* When the request URL contains dataUrl, the app is asking for fresh
* weather data. In this case, the service worker always goes to the
* network and then caches the response. This is called the "Cache then
* network" strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
*/
e.respondWith(
caches.open(dataCacheName).then(function(cache) {
return fetch(e.request).then(function(response) {
cache.put(e.request.url, response.clone());
return response;
});
})
);
}
else
/*
* The app is asking for app shell files. In this scenario the app uses the
* "Cache, falling back to the network" offline strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
*/
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});

21
public/site.webmanifest Normal file
View File

@ -0,0 +1,21 @@
{
"name": "Multiview",
"short_name": "Multiview",
"icons": [
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"start_url": ".",
"imgdisplay": "standalone",
"display": "standalone"
}

92
rollup.config.js Normal file
View File

@ -0,0 +1,92 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import replace from 'rollup-plugin-replace';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import builtins from 'rollup-plugin-node-builtins';
// import globals from 'rollup-plugin-node-globals';
const production = !process.env.ROLLUP_WATCH;
const preprocess = sveltePreprocess({
'scss': {
'includePaths': ['src']
},
'postcss': {
'plugins': [require('autoprefixer')]
}
});
export default {
'input': 'src/main.js',
'output': {
'sourcemap': (!production),
'format': 'iife',
'name': 'app',
'file': 'public/build/bundle.js'
},
'plugins': [
/* globals(),*/
builtins(),
svelte({
// enable run-time checks when not in production
'dev': !production,
preprocess,
'hydratable':true,
// we'll extract any component CSS out into
// a separate file - better for performance
'css': css => {
css.write('public/build/bundle.css');
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
'browser': true,
'dedupe': ['svelte']
}),
commonjs(),
replace({
'exclude': 'node_modules/**',
'ENV': JSON.stringify(production ? 'production' : 'development')
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
'watch': {
'clearScreen': false
}
};
function serve() {
let started = false;
return {
writeBundle() {
if (!started) {
started = true;
require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
'stdio': ['ignore', 'inherit', 'inherit'],
'shell': true
});
}
}
};
}

16
server.js Normal file
View File

@ -0,0 +1,16 @@
const sirv = require('sirv');
const polka = require('polka');
const compress = require('compression')();
// Init `sirv` handler
const assets = sirv('public', {
'maxAge': 31536000, // 1Y
'immutable': true
});
polka()
.use(compress, assets)
.listen(8130, err => {
if (err) throw err;
console.log('> Running on localhost:8130');
});

37
src/App.svelte Normal file
View File

@ -0,0 +1,37 @@
<script>
import {Playing} from './store/state';
import Youtube from "./components/Youtube.svelte";
import Live from "./components/Live.svelte";
import Twitch from "./components/Twitch.svelte";
let now = 0;
$: {
now = new Date().getTime.toString(36);
}
</script>
<style>
</style>
<div>
Playing:{$Playing}
</div>
<div id="container">
<Live title="Sky News" id="skynews" src="https://linear021-gb-hls1-prd-ak.cdn.skycdp.com/Content/HLS_001_sd/Live/channel(skynews)/index_mob.m3u8"/>
<!-- <Live title="Sky News" id="skynews" src="http://skydvn-sn-mobile-prod.skydvn.com/skynews/1404/latest.m3u8#{now}"/> -->
<Live title="BBC News" id="bbcnews24" src="https://vs-hls-push-uk-live.akamaized.net/x=3/i=urn:bbc:pips:service:bbc_news_channel_hd/mobile_wifi_main_sd_abr_v2.m3u8"/>
<Youtube title="EuroNews" id="euronews" src="https://www.youtube.com/embed/sPgqEHsONK8?enablejsapi=1&autoplay=1&mute=1&controls=0&fs=0&modestbranding=1&cc_load_policy=1"/>
<Live title="BBC Parliament" id="bbcparliament" src="https://vs-hls-pushb-uk-live.akamaized.net/x=3/i=urn:bbc:pips:service:bbc_parliament/mobile_wifi_main_sd_abr_v2_akamai_hls_live_http.m3u8"/>
<Live title="Bloomberg" id="bloomberg" src="https://bloomberg-bloombergtv-1-gb.samsung.wurl.com/manifest/playlist.m3u8"/>
<Live title="BBC Scotland" id="bbcscotland" src="https://vs-hls-pushb-uk-live.akamaized.net/x=3/i=urn:bbc:pips:service:bbc_scotland_hd/mobile_wifi_main_sd_abr_v2_akamai_hls_live_http.m3u8"/>
<!-- <Twitch title="twitch.tv/rukpolitics" id="rukpolitics" channel="rukpolitics"/>-->
<!-- <Twitch title="twitch.tv/rifftrax" id="rifftrax" channel="rifftrax"/> -->
<!-- <Twitch title="twitch.tv/democracylive" id="democracylive" channel="democracylive"/>-->
</div>

View File

@ -0,0 +1,73 @@
<script>
import {onMount} from 'svelte';
import {Playing, actions} from '../store/state';
import videojs from 'video.js';
export let id;
export let src;
export let title;
let fullId = '';
let active = '';
const dataSetup = {"youtube": {"ytControls": 0}};
let player;
$: fullId = `${id}-live`;
function mute() {
console.log(`${fullId} - mute`);
player.muted(true);
}
function unMute() {
console.log(`${fullId} - unmute`);
player.muted(false)
}
function handleClick() {
actions.setPlaying(fullId);
}
onMount(() => {
try {
player = videojs(fullId);
} catch (e) {
console.log(e);
}
Playing.subscribe((v) => {
active = (fullId !== '' && v === fullId) ? 'active' : '';
if (player) {
mute();
if (active) {
unMute();
}
}
})
console.log(`mounted ${fullId} player`);
})
</script>
<style>
</style>
<div class="quarter" on:click={handleClick}>
<div class="wrapper">
<div class="stream live {active}">
<div class="overlay"></div>
<div class="title">{title}</div>
<video id="{fullId}" class="video-js vjs-16-9" autoplay muted preload="auto" data-setup='{JSON.stringify(dataSetup)}'>
<source src="{src}" type="application/x-mpegURL">
</video>
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
<script>
import {onMount} from 'svelte';
import {Playing, actions} from '../store/state';
export let id;
export let channel;
export let title;
let fullId = '';
let active = '';
let player;
$: fullId = `${id}-twitch`;
function mute() {
console.log(`${fullId} - mute`);
player.setMuted(true);
}
function unMute() {
console.log(`${fullId} - unmute`);
player.setMuted(false);
}
function handleClick() {
console.log(`click ${fullId}`);
actions.setPlaying(fullId);
}
onMount(() => {
try {
player = new Twitch.Player(fullId, {
'channel': channel,
'muted': true,
'width': '100%',
'height': '100%',
'parent': ['multiview.silvrtree.local'],
});
} catch (e) {
console.log(e);
}
Playing.subscribe((v) => {
active = (fullId !== '' && v === fullId) ? 'active' : '';
if (player) {
mute();
if (active) {
unMute();
}
}
})
console.log(`mounted ${fullId} player`);
})
</script>
<style>
</style>
<div class="quarter" on:click={handleClick}>
<div class="wrapper">
<div class="stream live twitch {active}" data-video-id="4" id="{fullId}">
<div class="overlay"></div>
<div class="title">{title}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,90 @@
<script>
import {onMount} from 'svelte';
import {Playing, actions} from '../store/state';
export let id;
export let src;
export let title;
let fullId;
let active = '';
let player;
$: fullId = `${id}-youtube`;
function mute() {
console.log(`${fullId} - mute`);
player.mute();
}
function unMute() {
console.log(`${fullId} - unmute`);
player.unMute();
}
Playing.subscribe((v) => {
if (typeof (fullId) !== 'undefined') {
// console.log(`${fullId} playing`, v);
active = (fullId !== '' && v === fullId) ? 'active' : '';
if (player) {
mute();
if (active) {
unMute();
}
}
}
})
function handleClick() {
actions.setPlaying(fullId);
}
async function createPlayer() {
console.log(`${fullId} createPlayer`);
try {
player = new YT.Player(fullId, {
'events': {
'onReady': function (event) {
console.log('READY!!');
event.target.mute();
// mute();
}
}
});
// console.log(`${fullId} Player`, player);
} catch (e) {
console.log(e);
}
}
onMount(async () => {
setTimeout(async () => {
await createPlayer()
}, 1500);
})
</script>
<style>
</style>
<div class="quarter" on:click={handleClick}>
<div class="wrapper">
<div class="stream live youtube {active}" data-youtube-id={fullId}>
<div class="overlay"></div>
<div class="title">{title}</div>
<!-- <video id={fullId} class="video-js vjs-default-skin" controls>
<source src="{src}" type="video/youtube">
</video>-->
<iframe allow="autoplay" title="{title}" id={fullId} type="text/html" frameborder="0" width="100%" height="100%"
src="{src}"></iframe>
</div>
</div>
</div>

34
src/main.js Normal file
View File

@ -0,0 +1,34 @@
import App from './App.svelte';
const app = new App({
'target': document.body,
'props': {
}
});
/*
if ('serviceWorker' in navigator) {
//
navigator.serviceWorker.ready.then(function(reg) {
console.warn('Ready??', reg);
// main();
});
window.addEventListener('load', function() {
navigator.serviceWorker
.register('./service-worker.js')
.then((r) => {
console.warn('Service Worker Registered', r.scope);
})
.catch((error) => {
// registration failed
console.error(`Registration failed with ${ error}`);
});
});
//
}
*/
export default app;

24
src/store/state.js Normal file
View File

@ -0,0 +1,24 @@
/**
* Created by WebStorm.
* User: martin
* Date: 27/05/2020
* Time: 10:04
*/
import { writable } from 'svelte/store';
const Playing = writable('');
const actions = {
setPlaying(id) {
console.log('>> setPlaying', id);
Playing.update((v) => {
return (v === id) ? '' : id;
});
}
};
export { Playing, actions };