Compare commits
2 commits
25f1ac3b73
...
37a3f85037
Author | SHA1 | Date | |
---|---|---|---|
37a3f85037 | |||
a0d37e3566 |
12 changed files with 2357 additions and 0 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
8
dev.mjs
Normal file
8
dev.mjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { ServiceCore } from 'service-core'
|
||||
import * as index from './index.mjs'
|
||||
|
||||
var core = new ServiceCore('ffmpeg_service', import.meta.url, 4040, '')
|
||||
|
||||
core.init(index).then(function() {
|
||||
return core.run()
|
||||
})
|
6
index.mjs
Normal file
6
index.mjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
export function start(http, port, ctx) {
|
||||
return import('./server/server.mjs')
|
||||
.then(function(server) {
|
||||
return server.run(http, port, ctx)
|
||||
})
|
||||
}
|
34
package.json
Normal file
34
package.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "ffmpeg-service",
|
||||
"version": "1.0.0",
|
||||
"description": "FFmpeg streaming service",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:server": "node dev.mjs | bunyan",
|
||||
"dev:server:watch": "npm-watch dev:server",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"watch": {
|
||||
"dev:server": {
|
||||
"patterns": [
|
||||
"server/*"
|
||||
],
|
||||
"extensions": "mjs",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.nfp.is/TheThing/ffmpeg-service.git"
|
||||
},
|
||||
"author": "Jonatan Nilsson",
|
||||
"license": "WTFPL",
|
||||
"devDependencies": {
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"flaska": "^1.2.3",
|
||||
"ws": "^8.6.0"
|
||||
}
|
||||
}
|
BIN
public/assets/Inter.var.woff2
Normal file
BIN
public/assets/Inter.var.woff2
Normal file
Binary file not shown.
167
public/assets/app.css
Normal file
167
public/assets/app.css
Normal file
|
@ -0,0 +1,167 @@
|
|||
:root {
|
||||
--bg: #282828;
|
||||
--foreground: white;
|
||||
--header-bg: linear-gradient(#303030, #181818);
|
||||
--header-fg: #555;
|
||||
--header-title-fg: white;
|
||||
--nav-above-border-color: #000000;
|
||||
--nav-bg: linear-gradient(#262626, #181818);
|
||||
--nav-fg: #666;
|
||||
--nav-active-fg: #eb6e00;
|
||||
--nav-top-border-color: #303030;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
font-style: oblique 0deg 10deg;
|
||||
src: url("Inter.var.woff2?v=3.19") format("woff2");
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
font-feature-settings: "zero", "calt", "ccmp", "kern";
|
||||
background: var(--bg);
|
||||
color: var(--foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filler {
|
||||
flex: 2 1 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid black;
|
||||
background: var(--header-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--header-fg);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
padding: 1.5rem;
|
||||
font-weight: 400;
|
||||
align-self: stretch;
|
||||
color: white;
|
||||
}
|
||||
|
||||
header .led {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
header span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 2 1 auto;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: var(--nav-bg);
|
||||
border-top: 1px solid var(--nav-top-border-color);
|
||||
position: relative;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
nav a,
|
||||
nav a:visited {
|
||||
width: 100px;
|
||||
padding: 0.5rem 0.5rem 0.5rem;
|
||||
color: var(--nav-fg);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
height: 5rem;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
nav a svg {
|
||||
height: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
fill: var(--nav-fg);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
color: var(--nav-active-fg);
|
||||
background: #181818;
|
||||
border: 1px solid #141414;
|
||||
box-shadow: rgba(0, 0, 0, .35) 0px 5px 16px -6px inset;
|
||||
}
|
||||
nav a.active svg {
|
||||
fill: var(--nav-active-fg);
|
||||
}
|
||||
|
||||
nav::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--nav-above-border-color);
|
||||
}
|
||||
|
||||
/* Taken from https://github.com/aus/led.css */
|
||||
.led {
|
||||
margin-top: 1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: #fff5 3px 5px 7px -8px inset, #000 0 -1px 6px 1px;
|
||||
}
|
||||
.led-red {
|
||||
background-color: #F00;
|
||||
box-shadow: #000 0 -1px 6px 1px, inset #600 0 -1px 8px, #F00 0 3px 11px;
|
||||
}
|
||||
.led-orange {
|
||||
background-color: #FF7000;
|
||||
box-shadow: #000 0 -1px 6px 1px, inset #630 0 -1px 8px, #FF7000 0 3px 11px;
|
||||
}
|
||||
|
||||
.led-yellow {
|
||||
background-color: #FF0;
|
||||
box-shadow: #000 0 -1px 6px 1px, inset #660 0 -1px 8px, #FF0 0 3px 11px;
|
||||
}
|
||||
|
||||
.led-green {
|
||||
background-color: #80FF00;
|
||||
box-shadow: #000 0 -1px 6px 1px, inset #460 0 -1px 8px, #80FF00 0 3px 11px;
|
||||
}
|
||||
|
||||
.led-blue {
|
||||
background-color: #06F;
|
||||
box-shadow: #000 0 -1px 6px 1px, inset #006 0 -1px 8px, #06F 0 3px 11px;
|
||||
}
|
149
public/assets/app.js
Normal file
149
public/assets/app.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
class WSClient
|
||||
{
|
||||
constructor(url = 'ws://localhost:4040') {
|
||||
this.url = url
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.reconnectingAt = null
|
||||
this.retryDuration = 1000
|
||||
this.open()
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.connected || this.connecting) return
|
||||
this.connecting = true
|
||||
this.socket = new WebSocket(this.url)
|
||||
|
||||
this.socket.addEventListener('open', this.onopen.bind(this))
|
||||
this.socket.addEventListener('message', this.onmessage.bind(this))
|
||||
this.socket.addEventListener('error', this.onerror.bind(this))
|
||||
this.socket.addEventListener('close', this.onclose.bind(this))
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
onopen() {
|
||||
this.retryDuration = 1000
|
||||
this.connecting = false
|
||||
this.reconnectingAt = null
|
||||
this.connected = true
|
||||
console.log('Sending hello server')
|
||||
this.send({ msg: 'Hello Server!' })
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
if (!this.connected) return
|
||||
this.socket.send(JSON.stringify(payload))
|
||||
}
|
||||
|
||||
onerror(err) {
|
||||
console.error(err)
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
onclose() {
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.reconnectingAt) return
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
|
||||
this.reconnectingAt = new Date(new Date().getTime() + this.retryDuration)
|
||||
setTimeout(() => {
|
||||
this.reconnectingAt = null
|
||||
this.retryDuration *= 1.5
|
||||
this.open()
|
||||
}, this.retryDuration)
|
||||
m.redraw()
|
||||
}
|
||||
|
||||
onmessage(event) {
|
||||
try {
|
||||
let data = JSON.parse(event.data)
|
||||
console.log('got message', data)
|
||||
} catch (err) { console.error(err) }
|
||||
}
|
||||
}
|
||||
|
||||
const client = new WSClient()
|
||||
|
||||
class Header {
|
||||
view(vnode) {
|
||||
return [
|
||||
m('h1', 'Fíladelfíu streymi'),
|
||||
m('div.status.green', { hidden: true }, 'No errors'),
|
||||
m('div.filler'),
|
||||
m('span', 'Live:'),
|
||||
m('div.led'),
|
||||
client.connected
|
||||
? null
|
||||
: m('div.overlay', [
|
||||
m('h2', client.connecting ? 'Connecting to server...' : 'Connection lost'),
|
||||
m('')
|
||||
]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Nav {
|
||||
view(vnode) {
|
||||
let path = m.route.get()
|
||||
return [
|
||||
m(m.route.Link, {
|
||||
class: path === '/' ? 'active' : '',
|
||||
href: '/',
|
||||
}, [
|
||||
m('svg', {
|
||||
viewBox: '0 0 512 512'
|
||||
}, m('path', {
|
||||
d: 'M32 400C32 426.5 53.49 448 80 448H496C504.8 448 512 455.2 512 464C512 472.8 504.8 480 496 480H80C35.82 480 0 444.2 0 400V48C0 39.16 7.164 32 16 32C24.84 32 32 39.16 32 48V400zM331.3 299.3C325.1 305.6 314.9 305.6 308.7 299.3L223.1 214.6L123.3 315.3C117.1 321.6 106.9 321.6 100.7 315.3C94.44 309.1 94.44 298.9 100.7 292.7L212.7 180.7C218.9 174.4 229.1 174.4 235.3 180.7L320 265.4L452.7 132.7C458.9 126.4 469.1 126.4 475.3 132.7C481.6 138.9 481.6 149.1 475.3 155.3L331.3 299.3z'
|
||||
})),
|
||||
m('span', 'Status')
|
||||
]),
|
||||
m(m.route.Link, {
|
||||
class: path === '/settings' ? 'active' : '',
|
||||
href: '/settings',
|
||||
}, [
|
||||
m('svg', {
|
||||
viewBox: '0 0 512 512'
|
||||
}, m('path', {
|
||||
d: 'M0 416C0 407.2 7.164 400 16 400H81.6C89.01 363.5 121.3 336 160 336C198.7 336 230.1 363.5 238.4 400H496C504.8 400 512 407.2 512 416C512 424.8 504.8 432 496 432H238.4C230.1 468.5 198.7 496 160 496C121.3 496 89.01 468.5 81.6 432H16C7.164 432 0 424.8 0 416V416zM208 416C208 389.5 186.5 368 160 368C133.5 368 112 389.5 112 416C112 442.5 133.5 464 160 464C186.5 464 208 442.5 208 416zM352 176C390.7 176 422.1 203.5 430.4 240H496C504.8 240 512 247.2 512 256C512 264.8 504.8 272 496 272H430.4C422.1 308.5 390.7 336 352 336C313.3 336 281 308.5 273.6 272H16C7.164 272 0 264.8 0 256C0 247.2 7.164 240 16 240H273.6C281 203.5 313.3 176 352 176zM400 256C400 229.5 378.5 208 352 208C325.5 208 304 229.5 304 256C304 282.5 325.5 304 352 304C378.5 304 400 282.5 400 256zM496 80C504.8 80 512 87.16 512 96C512 104.8 504.8 112 496 112H270.4C262.1 148.5 230.7 176 192 176C153.3 176 121 148.5 113.6 112H16C7.164 112 0 104.8 0 96C0 87.16 7.164 80 16 80H113.6C121 43.48 153.3 16 192 16C230.7 16 262.1 43.48 270.4 80H496zM144 96C144 122.5 165.5 144 192 144C218.5 144 240 122.5 240 96C240 69.49 218.5 48 192 48C165.5 48 144 69.49 144 96z'
|
||||
})),
|
||||
m('span', 'Settings')
|
||||
]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Status {
|
||||
view(vnode) {
|
||||
return [
|
||||
m('span', 'Hello world')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Settings {
|
||||
view(vnode) {
|
||||
return [
|
||||
m('span', 'Settings')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const rootHeader = document.getElementById('header')
|
||||
const rootNav = document.getElementById('nav')
|
||||
const rootMain = document.getElementById('main')
|
||||
|
||||
const allRoutes = {
|
||||
'/': Status,
|
||||
'/settings': Settings,
|
||||
}
|
||||
|
||||
m.mount(rootHeader, Header)
|
||||
m.mount(rootNav, Nav)
|
||||
m.route(rootMain, '/', allRoutes)
|
BIN
public/assets/favicon.ico
Normal file
BIN
public/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
1842
public/assets/mithril.js
Normal file
1842
public/assets/mithril.js
Normal file
File diff suppressed because it is too large
Load diff
21
public/index.html
Normal file
21
public/index.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Streymi control</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
<link rel="Stylesheet" href="/assets/app.css" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header id="header"></header>
|
||||
<main id="main"></main>
|
||||
<nav id="nav"></nav>
|
||||
</div>
|
||||
<script type="text/javascript" src="/assets/mithril.js"></script>
|
||||
<script type="text/javascript" src="/assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
34
server/serve.mjs
Normal file
34
server/serve.mjs
Normal file
|
@ -0,0 +1,34 @@
|
|||
import path from 'path'
|
||||
import { FileResponse } from 'flaska'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
export default class ServeHandler {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
fs: opts.fs || fs,
|
||||
root: opts.root,
|
||||
})
|
||||
}
|
||||
|
||||
/** GET: /::file */
|
||||
serve(ctx) {
|
||||
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
|
||||
|
||||
if (!file.startsWith(this.root)) {
|
||||
ctx.status = 404
|
||||
ctx.body = 'HTTP 404 Error'
|
||||
return
|
||||
}
|
||||
|
||||
return this.fs.stat(file).catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
file = path.resolve(path.join(this.root, 'index.html'))
|
||||
return this.fs.stat(file)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
.then(function(stat) {
|
||||
ctx.body = new FileResponse(file, stat)
|
||||
})
|
||||
}
|
||||
}
|
95
server/server.mjs
Normal file
95
server/server.mjs
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { Flaska, QueryHandler } from 'flaska'
|
||||
import { WebSocket, WebSocketServer } from 'ws'
|
||||
|
||||
import ServeHandler from './serve.mjs'
|
||||
|
||||
export function run(http, port, core) {
|
||||
let localUtil = new core.sc.Util(import.meta.url)
|
||||
|
||||
// Create our server
|
||||
const flaska = new Flaska({
|
||||
log: core.log,
|
||||
}, http)
|
||||
|
||||
flaska.before(function(ctx) {
|
||||
ctx.state.started = new Date().getTime()
|
||||
})
|
||||
|
||||
//
|
||||
flaska.after(function(ctx) {
|
||||
let ended = new Date().getTime()
|
||||
var requestTime = ended - ctx.state.started
|
||||
|
||||
let status = ''
|
||||
let level = 'info'
|
||||
if (ctx.status >= 400) {
|
||||
status = ctx.status + ' '
|
||||
level = 'warn'
|
||||
}
|
||||
if (ctx.status >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
|
||||
ctx.log[level]({
|
||||
duration: requestTime,
|
||||
status: ctx.status,
|
||||
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
||||
})
|
||||
|
||||
const serve = new ServeHandler({
|
||||
root: localUtil.getPathFromRoot('../public'),
|
||||
})
|
||||
flaska.get('/::file', serve.serve.bind(serve))
|
||||
|
||||
return flaska.listenAsync(port).then(function() {
|
||||
/*const wss = new WebSocketServer({ server: flaska.server })
|
||||
|
||||
wss.on('connection', function(ws) {
|
||||
console.log('new connection')
|
||||
ws.isAlive = true
|
||||
ws.on('pong', function() {
|
||||
ws.isAlive = true
|
||||
})
|
||||
|
||||
ws.on('message', function message(data, isBinary) {
|
||||
if (isBinary) {
|
||||
return console.log('got binary, not supported')
|
||||
}
|
||||
let payload
|
||||
try {
|
||||
payload = JSON.parse(data.toString())
|
||||
} catch (err) {
|
||||
core.log.error(err)
|
||||
return
|
||||
}
|
||||
console.log('got', payload)
|
||||
wss.clients.forEach(function each(client) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(payload));
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const interval = setInterval(function() {
|
||||
if (wss.clients.length > 0) {
|
||||
core.log.info('Connected clients: ' + wss.clients.length)
|
||||
}
|
||||
wss.clients.forEach(function(ws) {
|
||||
if (ws.isAlive === false) {
|
||||
return ws.terminate()
|
||||
}
|
||||
|
||||
ws.isAlive = false
|
||||
ws.ping()
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
wss.on('close', function() {
|
||||
clearInterval(interval)
|
||||
})
|
||||
*/
|
||||
|
||||
core.log.info('Server is listening on port ' + port)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue