Compare commits

...

2 commits

Author SHA1 Message Date
37a3f85037 More development 2022-05-11 17:51:00 +00:00
a0d37e3566 Dev 2022-05-11 11:09:31 +00:00
12 changed files with 2357 additions and 0 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

8
dev.mjs Normal file
View 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
View 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
View 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"
}
}

Binary file not shown.

167
public/assets/app.css Normal file
View 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
View 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

Binary file not shown.

After

Width: 64px  |  Height: 64px  |  Size: 22 KiB

1842
public/assets/mithril.js Normal file

File diff suppressed because it is too large Load diff

21
public/index.html Normal file
View 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
View 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
View 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)
})
}