Compare commits
13 Commits
c50ed2b233
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd717d75e | ||
|
|
126f8d0ba6 | ||
|
|
862c52f567 | ||
|
|
e929b5af1e | ||
|
|
079fd61390 | ||
|
|
2fffb905e3 | ||
|
|
0da0701d34 | ||
|
|
0408433fc3 | ||
|
|
1acf98ef54 | ||
|
|
880fd39ea9 | ||
|
|
9c26df784d | ||
|
|
d1d7a4a940 | ||
|
|
16757236c6 |
11
README.md
Normal file
11
README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Matroska over WebSocket Streaming
|
||||||
|
|
||||||
|
This repository actually holds three programs, so be careful to not lost in the saurce.
|
||||||
|
|
||||||
|
* After `make` completes, `wsrA` is the relay program, which can be run as so: `./wsrA port=12345 key=MyFancyStreamingKeyIsOVERHERE`.
|
||||||
|
* Using the above example, one should stream to the HTTP path `/push/MyFancyStreamingKeyIsOVERHERE`. All other paths are expected to be used by WebSocket clients.
|
||||||
|
* If using a reverse proxy (highly recommended), make sure to disable request buffering and the maximum request size. For nginx, you want `proxy_request_buffering off; client_max_body_size 0;`.
|
||||||
|
* `index.html`, `blarf.js`, `blarfwork.js`, `support.js`, `support.wasm` and `rawpcmworklet.js` are the frontend, which must be accessible to the browser.
|
||||||
|
* You may insert a file named `intermission.jpg` that is shown when the stream is offline.
|
||||||
|
* Of course you'll have to change the feed and chat endpoints in the `index.html`, if you don't want my stream.
|
||||||
|
* You may also disable chat by setting `ENABLE_CHAT` to `false`.
|
||||||
164
base64.c
Normal file
164
base64.c
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/* This is a public domain base64 implementation written by WEI Zhicheng. */
|
||||||
|
|
||||||
|
#include "base64.h"
|
||||||
|
|
||||||
|
#define BASE64_PAD '='
|
||||||
|
#define BASE64DE_FIRST '+'
|
||||||
|
#define BASE64DE_LAST 'z'
|
||||||
|
|
||||||
|
/* BASE 64 encode table */
|
||||||
|
static const char base64en[] = {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||||
|
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||||
|
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||||
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||||
|
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||||
|
'w', 'x', 'y', 'z', '0', '1', '2', '3',
|
||||||
|
'4', '5', '6', '7', '8', '9', '+', '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ASCII order for BASE 64 decode, 255 in unused character */
|
||||||
|
static const unsigned char base64de[] = {
|
||||||
|
/* nul, soh, stx, etx, eot, enq, ack, bel, */
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* bs, ht, nl, vt, np, cr, so, si, */
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* dle, dc1, dc2, dc3, dc4, nak, syn, etb, */
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* can, em, sub, esc, fs, gs, rs, us, */
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* sp, '!', '"', '#', '$', '%', '&', ''', */
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* '(', ')', '*', '+', ',', '-', '.', '/', */
|
||||||
|
255, 255, 255, 62, 255, 255, 255, 63,
|
||||||
|
|
||||||
|
/* '0', '1', '2', '3', '4', '5', '6', '7', */
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59,
|
||||||
|
|
||||||
|
/* '8', '9', ':', ';', '<', '=', '>', '?', */
|
||||||
|
60, 61, 255, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', */
|
||||||
|
255, 0, 1, 2, 3, 4, 5, 6,
|
||||||
|
|
||||||
|
/* 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', */
|
||||||
|
7, 8, 9, 10, 11, 12, 13, 14,
|
||||||
|
|
||||||
|
/* 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', */
|
||||||
|
15, 16, 17, 18, 19, 20, 21, 22,
|
||||||
|
|
||||||
|
/* 'X', 'Y', 'Z', '[', '\', ']', '^', '_', */
|
||||||
|
23, 24, 25, 255, 255, 255, 255, 255,
|
||||||
|
|
||||||
|
/* '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', */
|
||||||
|
255, 26, 27, 28, 29, 30, 31, 32,
|
||||||
|
|
||||||
|
/* 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', */
|
||||||
|
33, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
|
||||||
|
/* 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', */
|
||||||
|
41, 42, 43, 44, 45, 46, 47, 48,
|
||||||
|
|
||||||
|
/* 'x', 'y', 'z', '{', '|', '}', '~', del, */
|
||||||
|
49, 50, 51, 255, 255, 255, 255, 255
|
||||||
|
};
|
||||||
|
|
||||||
|
unsigned int
|
||||||
|
base64_encode(const unsigned char *in, unsigned int inlen, char *out)
|
||||||
|
{
|
||||||
|
int s;
|
||||||
|
unsigned int i;
|
||||||
|
unsigned int j;
|
||||||
|
unsigned char c;
|
||||||
|
unsigned char l;
|
||||||
|
|
||||||
|
s = 0;
|
||||||
|
l = 0;
|
||||||
|
for (i = j = 0; i < inlen; i++) {
|
||||||
|
c = in[i];
|
||||||
|
|
||||||
|
switch (s) {
|
||||||
|
case 0:
|
||||||
|
s = 1;
|
||||||
|
out[j++] = base64en[(c >> 2) & 0x3F];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
s = 2;
|
||||||
|
out[j++] = base64en[((l & 0x3) << 4) | ((c >> 4) & 0xF)];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
s = 0;
|
||||||
|
out[j++] = base64en[((l & 0xF) << 2) | ((c >> 6) & 0x3)];
|
||||||
|
out[j++] = base64en[c & 0x3F];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
l = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (s) {
|
||||||
|
case 1:
|
||||||
|
out[j++] = base64en[(l & 0x3) << 4];
|
||||||
|
out[j++] = BASE64_PAD;
|
||||||
|
out[j++] = BASE64_PAD;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
out[j++] = base64en[(l & 0xF) << 2];
|
||||||
|
out[j++] = BASE64_PAD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[j] = 0;
|
||||||
|
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int
|
||||||
|
base64_decode(const char *in, unsigned int inlen, unsigned char *out)
|
||||||
|
{
|
||||||
|
unsigned int i;
|
||||||
|
unsigned int j;
|
||||||
|
unsigned char c;
|
||||||
|
|
||||||
|
if (inlen & 0x3) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = j = 0; i < inlen; i++) {
|
||||||
|
if (in[i] == BASE64_PAD) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (in[i] < BASE64DE_FIRST || in[i] > BASE64DE_LAST) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = base64de[(unsigned char)in[i]];
|
||||||
|
if (c == 255) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (i & 0x3) {
|
||||||
|
case 0:
|
||||||
|
out[j] = (c << 2) & 0xFF;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
out[j++] |= (c >> 4) & 0x3;
|
||||||
|
out[j] = (c & 0xF) << 4;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
out[j++] |= (c >> 2) & 0xF;
|
||||||
|
out[j] = (c & 0x3) << 6;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
out[j++] |= c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return j;
|
||||||
|
}
|
||||||
20
base64.h
Normal file
20
base64.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef BASE64_H
|
||||||
|
#define BASE64_H
|
||||||
|
|
||||||
|
#define BASE64_ENCODE_OUT_SIZE(s) ((unsigned int)((((s) + 2) / 3) * 4 + 1))
|
||||||
|
#define BASE64_DECODE_OUT_SIZE(s) ((unsigned int)(((s) / 4) * 3))
|
||||||
|
|
||||||
|
/*
|
||||||
|
* out is null-terminated encode string.
|
||||||
|
* return values is out length, exclusive terminating `\0'
|
||||||
|
*/
|
||||||
|
unsigned int
|
||||||
|
base64_encode(const unsigned char *in, unsigned int inlen, char *out);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return values is out length
|
||||||
|
*/
|
||||||
|
unsigned int
|
||||||
|
base64_decode(const char *in, unsigned int inlen, unsigned char *out);
|
||||||
|
|
||||||
|
#endif /* BASE64_H */
|
||||||
208
blarf.js
208
blarf.js
@@ -2,32 +2,75 @@
|
|||||||
var VideoQueue = []
|
var VideoQueue = []
|
||||||
var AudioQueue = []
|
var AudioQueue = []
|
||||||
|
|
||||||
|
class DynamicTypedArray {
|
||||||
|
constructor(type) {
|
||||||
|
this.type = type
|
||||||
|
this.backend = new type(1024)
|
||||||
|
this.length = 0
|
||||||
|
}
|
||||||
|
add(b) {
|
||||||
|
if(this.length + b.length > this.backend.length) {
|
||||||
|
var newlen = this.backend.length
|
||||||
|
while(this.length + b.length > newlen) { newlen = newlen * 2 }
|
||||||
|
var be2 = new this.type(newlen)
|
||||||
|
be2.set(this.backend, 0)
|
||||||
|
this.backend = be2
|
||||||
|
}
|
||||||
|
this.backend.set(b, this.length)
|
||||||
|
this.length += b.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var BlarfEl = document.getElementById("BLARF")
|
var BlarfEl = document.getElementById("BLARF")
|
||||||
BlarfEl.innerHTML = `
|
BlarfEl.innerHTML = `
|
||||||
<canvas width="1280" height="720"></canvas>
|
<canvas width="1280" height="720"></canvas>
|
||||||
<div class="MKVControls">
|
<div class="MKVControls">
|
||||||
|
<div>
|
||||||
<div class="MKVSpeaker"><span class="MKVSpeakerOff">🔈︎</span><span class="MKVSpeakerOn" style="display:none;">🔊︎</span></div>
|
<div class="MKVSpeaker"><span class="MKVSpeakerOff">🔈︎</span><span class="MKVSpeakerOn" style="display:none;">🔊︎</span></div>
|
||||||
<span class="MKVCurrentTime">00:00:00</span>
|
<span class="MKVCurrentTime">00:00:00</span>
|
||||||
<span class="MKVStats"></span>
|
<span class="MKVStats"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="MKVStatus"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
var Canvus = BlarfEl.querySelector("canvas")
|
var Canvus = BlarfEl.querySelector("canvas")
|
||||||
var CanvCtx = Canvus.getContext("2d")
|
var CanvCtx = Canvus.getContext("2d")
|
||||||
|
var CanvImageData
|
||||||
|
|
||||||
|
var LatencyMS = 1000
|
||||||
|
|
||||||
var AudCtx
|
var AudCtx
|
||||||
var AudScript
|
var AudScript, AudWorklet
|
||||||
var AudHz
|
var AudHz
|
||||||
|
var AudMuted = true
|
||||||
|
var AudSampleIndex = 0
|
||||||
|
|
||||||
function create_audio(hz, channels) {
|
function create_audio(hz, channels) {
|
||||||
if(AudCtx) {
|
if(AudCtx) {
|
||||||
AudCtx.close()
|
AudCtx.close()
|
||||||
|
AudScript = null
|
||||||
|
AudWorklet = null
|
||||||
}
|
}
|
||||||
|
|
||||||
AudHz = hz
|
AudHz = hz
|
||||||
|
|
||||||
|
var DebugSine = 0
|
||||||
|
|
||||||
AudCtx = new AudioContext({sampleRate: hz})
|
AudCtx = new AudioContext({sampleRate: hz})
|
||||||
AudScript = AudCtx.createScriptProcessor(1024, channels, channels)
|
AudCtx.suspend()
|
||||||
|
|
||||||
|
if(AudCtx.audioWorklet) {
|
||||||
|
AudCtx.audioWorklet.addModule("rawpcmworklet.js").then(function() {
|
||||||
|
AudWorklet = new AudioWorkletNode(AudCtx, "rawpcmworklet", {
|
||||||
|
outputChannelCount: [2]
|
||||||
|
})
|
||||||
|
AudWorklet.connect(AudCtx.destination)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
AudScript = AudCtx.createScriptProcessor(4096, channels, channels)
|
||||||
AudScript.onaudioprocess = function(e) {
|
AudScript.onaudioprocess = function(e) {
|
||||||
var outL = e.outputBuffer.getChannelData(0)
|
var outL = e.outputBuffer.getChannelData(0)
|
||||||
var outR = channels > 1 ? e.outputBuffer.getChannelData(1) : null
|
var outR = channels > 1 ? e.outputBuffer.getChannelData(1) : null
|
||||||
@@ -38,8 +81,10 @@
|
|||||||
while(AudioQueue.length && leftToWrite) {
|
while(AudioQueue.length && leftToWrite) {
|
||||||
var amount = Math.min(leftToWrite, AudioQueue[0].left.length)
|
var amount = Math.min(leftToWrite, AudioQueue[0].left.length)
|
||||||
|
|
||||||
|
if(!AudMuted) {
|
||||||
outL.set(AudioQueue[0].left.subarray(0, amount), offset)
|
outL.set(AudioQueue[0].left.subarray(0, amount), offset)
|
||||||
if(outR) outR.set(AudioQueue[0].right.subarray(0, amount), offset)
|
if(outR) outR.set(AudioQueue[0].right.subarray(0, amount), offset)
|
||||||
|
}
|
||||||
|
|
||||||
AudioQueue[0].left = AudioQueue[0].left.subarray(amount)
|
AudioQueue[0].left = AudioQueue[0].left.subarray(amount)
|
||||||
if(outR) AudioQueue[0].right = AudioQueue[0].right.subarray(amount)
|
if(outR) AudioQueue[0].right = AudioQueue[0].right.subarray(amount)
|
||||||
@@ -51,13 +96,10 @@
|
|||||||
leftToWrite -= amount
|
leftToWrite -= amount
|
||||||
offset += amount
|
offset += amount
|
||||||
}
|
}
|
||||||
|
|
||||||
if(RenderStartTime && leftToWrite) {
|
|
||||||
buffering(1000)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AudScript.connect(AudCtx.destination)
|
AudScript.connect(AudCtx.destination)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var LastControlsInterrupt
|
var LastControlsInterrupt
|
||||||
function interruptcontrols() {
|
function interruptcontrols() {
|
||||||
@@ -66,11 +108,16 @@
|
|||||||
interruptcontrols()
|
interruptcontrols()
|
||||||
|
|
||||||
function togglemute() {
|
function togglemute() {
|
||||||
if(AudCtx)
|
if(!AudCtx) {
|
||||||
if(document.querySelector(".MKVSpeakerOn").style.display == "none") {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AudCtx.resume()
|
AudCtx.resume()
|
||||||
} else {
|
|
||||||
AudCtx.suspend()
|
AudMuted = !AudMuted
|
||||||
|
|
||||||
|
if(AudWorklet) {
|
||||||
|
AudWorklet.port.postMessage(AudMuted)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll(".MKVSpeaker *").forEach(function(el) { el.style.display = el.style.display == "none" ? "" : "none" })
|
document.querySelectorAll(".MKVSpeaker *").forEach(function(el) { el.style.display = el.style.display == "none" ? "" : "none" })
|
||||||
@@ -81,7 +128,7 @@
|
|||||||
document.querySelector(".MKVSpeaker").onclick = togglemute
|
document.querySelector(".MKVSpeaker").onclick = togglemute
|
||||||
|
|
||||||
document.onkeypress = function(e) {
|
document.onkeypress = function(e) {
|
||||||
if(e.key.toUpperCase() == "M") {
|
if(document.activeElement.tagName != "TEXTAREA" && e.key.toUpperCase() == "M") {
|
||||||
togglemute()
|
togglemute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,30 +139,12 @@
|
|||||||
|
|
||||||
var RenderStartTime, VideoStartTime
|
var RenderStartTime, VideoStartTime
|
||||||
|
|
||||||
var Statistics = {}
|
function crop_audio_queue(durationToRemove) {
|
||||||
var TheWorker = new Worker("blarfwork.js")
|
|
||||||
TheWorker.onmessage = function(e) {
|
|
||||||
if(e.data.width) {
|
|
||||||
var imgData = new ImageData(new Uint8ClampedArray(e.data.data.buffer), e.data.width, e.data.height, {colorSpace: "srgb"})
|
|
||||||
VideoQueue.push({t: e.data.t, imgData: imgData})
|
|
||||||
} else if(e.data.samples) {
|
|
||||||
AudioQueue.push({left: e.data.left, right: e.data.right || e.data.left})
|
|
||||||
|
|
||||||
// Audio may be loaded but it might not play because of autoplay permissions
|
|
||||||
// In this case the audio queue will fill up and cause ever-increasing AV desync
|
|
||||||
|
|
||||||
// To prevent this, manually crop the audio to the duration in the video queue
|
|
||||||
|
|
||||||
if(AudCtx && AudCtx.state != "running") {
|
|
||||||
var durationInAudioQueue = AudioQueue.length ? AudioQueue.reduce((acc, el) => acc + el.left.length, 0) : 0
|
|
||||||
|
|
||||||
var durationToRemove = Math.max(durationInAudioQueue - (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : 0) * AudHz / 1000, 0)
|
|
||||||
|
|
||||||
while(AudioQueue.length && durationToRemove) {
|
while(AudioQueue.length && durationToRemove) {
|
||||||
var amount = Math.min(durationToRemove, AudioQueue[0].left.length)
|
var amount = Math.min(durationToRemove, AudioQueue[0].left.length)
|
||||||
|
|
||||||
AudioQueue[0].left = AudioQueue[0].left.subarray(amount)
|
AudioQueue[0].left = AudioQueue[0].left.subarray(amount)
|
||||||
AudioQueue[0].right = AudioQueue[0].left.subarray(amount)
|
AudioQueue[0].right = AudioQueue[0].right.subarray(amount)
|
||||||
|
|
||||||
if(AudioQueue[0].left.length == 0) {
|
if(AudioQueue[0].left.length == 0) {
|
||||||
AudioQueue.shift()
|
AudioQueue.shift()
|
||||||
@@ -124,6 +153,33 @@
|
|||||||
durationToRemove -= amount
|
durationToRemove -= amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var BufferPool = new Set()
|
||||||
|
|
||||||
|
var Statistics = {}
|
||||||
|
var TheWorker = new Worker("blarfwork.js")
|
||||||
|
TheWorker.onmessage = function(e) {
|
||||||
|
if(e.data.width) {
|
||||||
|
// var imgData = new ImageData(new Uint8ClampedArray(e.data.data.buffer), e.data.width, e.data.height, {colorSpace: "srgb"})
|
||||||
|
var b
|
||||||
|
if(BufferPool.size == 0) {
|
||||||
|
b = new Uint8ClampedArray(e.data.data.buffer)
|
||||||
|
} else {
|
||||||
|
for(const v of BufferPool) {
|
||||||
|
b = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
BufferPool.delete(b)
|
||||||
|
b.set(e.data.data)
|
||||||
|
}
|
||||||
|
VideoQueue.push({t: e.data.t, imgData: b, w: e.data.width, h: e.data.height})
|
||||||
|
} else if(e.data.samples) {
|
||||||
|
AudioQueue.push({t: e.data.t, left: e.data.left, right: e.data.right || e.data.left})
|
||||||
|
|
||||||
|
if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) {
|
||||||
|
AudWorklet.port.postMessage(merge_audio_queue())
|
||||||
|
AudioQueue.length = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Statistics[e.data.id]) {
|
if(!Statistics[e.data.id]) {
|
||||||
@@ -140,20 +196,11 @@
|
|||||||
text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + ","
|
text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + ","
|
||||||
}
|
}
|
||||||
stats.innerText = text*/
|
stats.innerText = text*/
|
||||||
stats.innerHTML = (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : "0") + "v" + (AudioQueue.reduce(function(acc, obj) {return acc + obj.left.length * 1000 / AudHz}, 0)|0) + "a"
|
stats.innerText = (VideoQueue.length ? (VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t) : "0") + "v" + (AudioQueue.reduce(function(acc, obj) {return acc + obj.left.length * 1000 / AudHz}, 0)|0) + "a"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvus.onclick = function() {
|
|
||||||
if(AudCtx) AudCtx.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
var VideoBufferingOffset = 0
|
var VideoBufferingOffset = 0
|
||||||
function buffering(millis) {
|
|
||||||
//var silence = new Float32Array(millis * 48);
|
|
||||||
//AudioQueue.push({left: silence, right: silence})
|
|
||||||
//VideoBufferingOffset += millis
|
|
||||||
}
|
|
||||||
|
|
||||||
function toHex(buffer) {
|
function toHex(buffer) {
|
||||||
return Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)).join('');
|
return Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)).join('');
|
||||||
@@ -321,10 +368,7 @@
|
|||||||
this.currentClusterTime = EBMLParser.vi_to_i(data)
|
this.currentClusterTime = EBMLParser.vi_to_i(data)
|
||||||
|
|
||||||
if(!RenderStartTime) {
|
if(!RenderStartTime) {
|
||||||
RenderStartTime = document.timeline.currentTime + 1000
|
RenderStartTime = performance.now()
|
||||||
}
|
|
||||||
if(!VideoStartTime) {
|
|
||||||
VideoStartTime = this.currentClusterTime
|
|
||||||
}
|
}
|
||||||
} else if(elID == 0xA3) {
|
} else if(elID == 0xA3) {
|
||||||
// Cluster -> SimpleBlock
|
// Cluster -> SimpleBlock
|
||||||
@@ -341,10 +385,16 @@
|
|||||||
var TotalTime = (this.currentClusterTime + timestamp) / 1000
|
var TotalTime = (this.currentClusterTime + timestamp) / 1000
|
||||||
document.querySelector(".MKVCurrentTime").innerText = pad(Math.floor(TotalTime / 3600), 2) + ":" + pad(Math.floor(TotalTime / 60 % 60), 2) + ":" + pad(Math.floor(TotalTime % 60), 2)
|
document.querySelector(".MKVCurrentTime").innerText = pad(Math.floor(TotalTime / 3600), 2) + ":" + pad(Math.floor(TotalTime / 60 % 60), 2) + ":" + pad(Math.floor(TotalTime % 60), 2)
|
||||||
|
|
||||||
|
var playerTimestamp = this.currentClusterTime + timestamp
|
||||||
|
|
||||||
if(track) {
|
if(track) {
|
||||||
|
if(!VideoStartTime) {
|
||||||
|
VideoStartTime = playerTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
var packet = data.subarray(4)
|
var packet = data.subarray(4)
|
||||||
|
|
||||||
TheWorker.postMessage({cmd: "decode", id: trackID, t: timestamp + this.currentClusterTime - VideoStartTime, packet: packet, kf: kf})
|
TheWorker.postMessage({cmd: "decode", id: trackID, t: playerTimestamp - VideoStartTime, packet: packet, kf: kf})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,6 +416,9 @@
|
|||||||
if(track.type == "video") {
|
if(track.type == "video") {
|
||||||
Canvus.width = track.width
|
Canvus.width = track.width
|
||||||
Canvus.height = track.height
|
Canvus.height = track.height
|
||||||
|
CanvImageData = new ImageData(new Uint8ClampedArray(Canvus.width * Canvus.height * 4), Canvus.width, Canvus.height, {"colorSpace": "srgb"})
|
||||||
|
RenderStartTime = null
|
||||||
|
VideoStartTime = null
|
||||||
} else {
|
} else {
|
||||||
create_audio(track.samplerate, track.channels)
|
create_audio(track.samplerate, track.channels)
|
||||||
}
|
}
|
||||||
@@ -380,12 +433,48 @@
|
|||||||
ebml.ondata = matr.ondata.bind(matr)
|
ebml.ondata = matr.ondata.bind(matr)
|
||||||
ebml.onexit = matr.onexit.bind(matr)
|
ebml.onexit = matr.onexit.bind(matr)
|
||||||
|
|
||||||
|
function merge_audio_queue() {
|
||||||
|
var s = 0
|
||||||
|
|
||||||
|
for(var i = 0; i < AudioQueue.length; i++) {
|
||||||
|
s += AudioQueue[i].left.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var L = new Float32Array(s)
|
||||||
|
var R = new Float32Array(s)
|
||||||
|
|
||||||
|
s = 0
|
||||||
|
|
||||||
|
for(var i = 0; i < AudioQueue.length; i++) {
|
||||||
|
L.set(AudioQueue[i].left, s)
|
||||||
|
R.set(AudioQueue[i].right, s)
|
||||||
|
s += AudioQueue[i].left.length
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = {msg: "data", t: AudSampleIndex, left: L, right: R}
|
||||||
|
|
||||||
|
AudSampleIndex += L.length
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
function reconnect_ws() {
|
function reconnect_ws() {
|
||||||
var ws = new WebSocket(BlarfEl.getAttribute("data-target"))
|
var ws = new WebSocket(BlarfEl.getAttribute("data-target"))
|
||||||
ws.binaryType = "arraybuffer"
|
ws.binaryType = "arraybuffer"
|
||||||
ws.onmessage = function(ev) {
|
ws.onmessage = function(ev) {
|
||||||
|
if(typeof ev.data === "string") {
|
||||||
|
var obj = JSON.parse(ev.data)
|
||||||
|
if(obj.status) {
|
||||||
|
BlarfEl.querySelector(".MKVStatus").innerHTML = "• " + obj.status.viewer_count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ebml.poosh(new Uint8Array(ev.data))
|
ebml.poosh(new Uint8Array(ev.data))
|
||||||
ebml.parse()
|
ebml.parse()
|
||||||
|
|
||||||
|
while(document.hidden && VideoQueue.length > 1 && VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t <= LatencyMS) {
|
||||||
|
BufferPool.add(VideoQueue.shift().imgData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ws.onclose = function(ev) {
|
ws.onclose = function(ev) {
|
||||||
setTimeout(reconnect_ws, 5000)
|
setTimeout(reconnect_ws, 5000)
|
||||||
@@ -394,14 +483,37 @@
|
|||||||
reconnect_ws()
|
reconnect_ws()
|
||||||
|
|
||||||
function render(timestamp) {
|
function render(timestamp) {
|
||||||
|
try {
|
||||||
document.querySelector(".MKVControls").style.opacity = Math.max(0, Math.min(1, 5 - (timestamp - LastControlsInterrupt) / 1000))
|
document.querySelector(".MKVControls").style.opacity = Math.max(0, Math.min(1, 5 - (timestamp - LastControlsInterrupt) / 1000))
|
||||||
|
|
||||||
while(RenderStartTime && VideoQueue.length && VideoQueue[0].t + VideoBufferingOffset <= (timestamp - RenderStartTime)) {
|
var nextImg = null
|
||||||
CanvCtx.putImageData(VideoQueue[0].imgData, 0, 0)
|
while(RenderStartTime && VideoQueue.length && VideoQueue[0].t <= (timestamp - RenderStartTime - LatencyMS)) {
|
||||||
|
if(nextImg) BufferPool.add(nextImg.imgData)
|
||||||
|
nextImg = VideoQueue[0]
|
||||||
VideoQueue.shift()
|
VideoQueue.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(nextImg) {
|
||||||
|
document.querySelector(".MKVControls").style.display = null
|
||||||
|
|
||||||
|
// Prevent the audio queue filling up and causing ever-increasing AV desync
|
||||||
|
if(AudCtx && AudCtx.state != "running" && AudioQueue && AudioQueue.length) {
|
||||||
|
if(AudioQueue[0].t < nextImg.t) {
|
||||||
|
crop_audio_queue(Math.round((nextImg.t - AudioQueue[0].t) / 1000 * AudHz))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvImageData.data.set(nextImg.imgData)
|
||||||
|
CanvCtx.putImageData(CanvImageData, 0, 0)
|
||||||
|
BufferPool.add(nextImg.imgData)
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(render)
|
requestAnimationFrame(render)
|
||||||
}
|
}
|
||||||
requestAnimationFrame(render)
|
requestAnimationFrame(render)
|
||||||
|
|
||||||
|
document.querySelector(".MKVControls").style.display = "none"
|
||||||
})()
|
})()
|
||||||
|
|||||||
42
index.html
42
index.html
@@ -42,6 +42,9 @@
|
|||||||
font-size: 0.4cm;
|
font-size: 0.4cm;
|
||||||
background: rgb(0, 0, 0);
|
background: rgb(0, 0, 0);
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
div#BLARF .MKVControls > * {
|
div#BLARF .MKVControls > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -53,6 +56,9 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.75cm;
|
font-size: 0.75cm;
|
||||||
}
|
}
|
||||||
|
div#BLARF .MKVStatus {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
div#BLARF > canvas {
|
div#BLARF > canvas {
|
||||||
background: url(intermission.jpg) black;
|
background: url(intermission.jpg) black;
|
||||||
background-position: 0 30%;
|
background-position: 0 30%;
|
||||||
@@ -71,6 +77,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
}
|
}
|
||||||
|
span.chat-msg__heading {
|
||||||
|
width: inherit !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media(max-aspect-ratio: 1) {
|
@media(max-aspect-ratio: 1) {
|
||||||
div.everything {
|
div.everything {
|
||||||
@@ -94,14 +104,22 @@
|
|||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="https://cdn.conversejs.org/10.1.4/dist/converse.min.css">
|
<link rel="stylesheet" type="text/css" media="screen" href="https://cdn.conversejs.org/10.1.4/dist/converse.min.css">
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Title</title>
|
<title>MWSS Stream</title>
|
||||||
|
<script>
|
||||||
|
var STREAM_SOURCE_WS = "wss://iki.mid.net.ua/streamout/"
|
||||||
|
|
||||||
|
var ENABLE_CHAT = true
|
||||||
|
var CHAT_HOST_WS_URL = "wss://mid.net.ua/xmpp"
|
||||||
|
var CHAT_HOST = "anon.mid.net.ua"
|
||||||
|
var CHAT_MUC = "stream@muc.anon.mid.net.ua"
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="everything">
|
<div class="everything">
|
||||||
<header></header>
|
<header></header>
|
||||||
<div class="stream">
|
<div class="stream">
|
||||||
<div class="feed">
|
<div class="feed">
|
||||||
<div id="BLARF" data-target="wss://iki.mid.net.ua/streamout/"></div>
|
<div id="BLARF" data-target=""></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat">
|
<div class="chat">
|
||||||
<converse-root style="position: relative;"></converse-root>
|
<converse-root style="position: relative;"></converse-root>
|
||||||
@@ -111,22 +129,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.conversejs.org/10.1.4/dist/converse.min.js" charset="utf-8"></script>
|
<script src="https://cdn.conversejs.org/10.1.4/dist/converse.min.js" charset="utf-8"></script>
|
||||||
<script src="blarf.js"></script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
document.querySelector("#BLARF").setAttribute("data-target", STREAM_SOURCE_WS)
|
||||||
|
|
||||||
function randomHex(size) {
|
function randomHex(size) {
|
||||||
return [...self.crypto.getRandomValues(new Uint8Array(size))].map(b=>b.toString(16).padStart(2, "0")).join("");
|
return [...self.crypto.getRandomValues(new Uint8Array(size))].map(b=>b.toString(16).padStart(2, "0")).join("")
|
||||||
}
|
}
|
||||||
const un = 'lol' + randomHex(16)
|
const un = 'lol' + randomHex(16)
|
||||||
|
|
||||||
|
if(ENABLE_CHAT) {
|
||||||
converse.initialize({
|
converse.initialize({
|
||||||
view_mode: 'embedded',
|
view_mode: 'embedded',
|
||||||
websocket_url: 'wss://mid.net.ua/xmpp',
|
websocket_url: CHAT_HOST_WS_URL,
|
||||||
login: 'anonymous',
|
authentication: 'anonymous',
|
||||||
jid: un + '@anon.mid.net.ua',
|
jid: CHAT_HOST,
|
||||||
auto_login: true,
|
auto_login: true,
|
||||||
password: 'lol',
|
auto_join_rooms: [CHAT_MUC],
|
||||||
auto_join_rooms: ['stream@muc.anon.mid.net.ua'],
|
|
||||||
show_message_avatar: false,
|
show_message_avatar: false,
|
||||||
show_controlbox_by_default: false,
|
show_controlbox_by_default: false,
|
||||||
roster_groups: false,
|
roster_groups: false,
|
||||||
@@ -137,6 +155,10 @@
|
|||||||
auto_reconnect: true,
|
auto_reconnect: true,
|
||||||
hide_muc_participants: true
|
hide_muc_participants: true
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
document.querySelector("div.everything .chat").style.display = "none"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="blarf.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
71
main2.c
71
main2.c
@@ -10,6 +10,7 @@
|
|||||||
#include<sys/types.h>
|
#include<sys/types.h>
|
||||||
#include<fcntl.h>
|
#include<fcntl.h>
|
||||||
#include<unistd.h>
|
#include<unistd.h>
|
||||||
|
#include<time.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include<stdbool.h>
|
#include<stdbool.h>
|
||||||
@@ -101,15 +102,18 @@ static void consume(Client *cli, size_t n) {
|
|||||||
cli->len -= n;
|
cli->len -= n;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void transmit(Client *cli, const char *buf, size_t sz) {
|
static int transmit(Client *cli, const char *buf, size_t sz) {
|
||||||
while(sz) {
|
while(sz) {
|
||||||
ssize_t s = send(cli->fd, buf, sz, 0);
|
ssize_t s = send(cli->fd, buf, sz, MSG_NOSIGNAL);
|
||||||
|
|
||||||
if(s >= 0) {
|
if(s >= 0) {
|
||||||
buf += s;
|
buf += s;
|
||||||
sz -= s;
|
sz -= s;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void transmit_all(const char *buf, size_t sz) {
|
static void transmit_all(const char *buf, size_t sz) {
|
||||||
@@ -120,13 +124,14 @@ static void transmit_all(const char *buf, size_t sz) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define WS_TXT 1
|
||||||
#define WS_BIN 2
|
#define WS_BIN 2
|
||||||
#define WS_CLOSE 8
|
#define WS_CLOSE 8
|
||||||
#define WS_FIN 128
|
#define WS_FIN 128
|
||||||
#define WS_HEADER_MAX 10
|
#define WS_HEADER_MAX 10
|
||||||
static int ws_header(size_t sz, uint8_t hdr[static WS_HEADER_MAX]) {
|
static int ws_header(size_t sz, bool binary, uint8_t hdr[static WS_HEADER_MAX]) {
|
||||||
int i;
|
int i;
|
||||||
hdr[0] = WS_BIN | WS_FIN;
|
hdr[0] = (binary ? WS_BIN : WS_TXT) | WS_FIN;
|
||||||
if(sz < 126) {
|
if(sz < 126) {
|
||||||
hdr[1] = sz;
|
hdr[1] = sz;
|
||||||
i = 2;
|
i = 2;
|
||||||
@@ -151,27 +156,53 @@ static int ws_header(size_t sz, uint8_t hdr[static WS_HEADER_MAX]) {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ws_send(Client *cli, const uint8_t *buf, size_t sz) {
|
static void ws_send(Client *cli, bool binary, const uint8_t *buf, size_t sz) {
|
||||||
if(sz == 0) return;
|
if(sz == 0) return;
|
||||||
|
|
||||||
uint8_t wshdr[WS_HEADER_MAX];
|
uint8_t wshdr[WS_HEADER_MAX];
|
||||||
int wshdrsz = ws_header(sz, wshdr);
|
int wshdrsz = ws_header(sz, binary, wshdr);
|
||||||
|
|
||||||
transmit(cli, wshdr, wshdrsz);
|
transmit(cli, wshdr, wshdrsz);
|
||||||
transmit(cli, buf, sz);
|
transmit(cli, buf, sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ws_broadcast(const uint8_t *buf, size_t sz) {
|
static void ws_broadcast(bool binary, const uint8_t *buf, size_t sz) {
|
||||||
if(sz == 0) return;
|
if(sz == 0) return;
|
||||||
|
|
||||||
uint8_t wshdr[WS_HEADER_MAX];
|
uint8_t wshdr[WS_HEADER_MAX];
|
||||||
int wshdrsz = ws_header(sz, wshdr);
|
int wshdrsz = ws_header(sz, binary, wshdr);
|
||||||
|
|
||||||
transmit_all(wshdr, wshdrsz);
|
transmit_all(wshdr, wshdrsz);
|
||||||
transmit_all(buf, sz);
|
transmit_all(buf, sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool should_send_status_update() {
|
||||||
|
static uint64_t lastSec = 0;
|
||||||
|
|
||||||
|
struct timespec tv;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &tv);
|
||||||
|
|
||||||
|
if(tv.tv_sec - lastSec < 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSec = tv.tv_sec;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_status_update() {
|
||||||
|
char buf[512] = {};
|
||||||
|
snprintf(buf, sizeof(buf) - 1, "{\"status\": {\"viewer_count\": %lu}}", clientsSz);
|
||||||
|
|
||||||
|
ws_broadcast(false, buf, strlen(buf));
|
||||||
|
}
|
||||||
|
|
||||||
static void stream_step(const uint8_t *newbuf, size_t newsz) {
|
static void stream_step(const uint8_t *newbuf, size_t newsz) {
|
||||||
|
if(should_send_status_update()) {
|
||||||
|
send_status_update();
|
||||||
|
}
|
||||||
|
|
||||||
if(Stream.state == LOADING_HEADER) {
|
if(Stream.state == LOADING_HEADER) {
|
||||||
Stream.mkvHeader = realloc(Stream.mkvHeader, Stream.mkvHeaderSz + newsz);
|
Stream.mkvHeader = realloc(Stream.mkvHeader, Stream.mkvHeaderSz + newsz);
|
||||||
memcpy(Stream.mkvHeader + Stream.mkvHeaderSz, newbuf, newsz);
|
memcpy(Stream.mkvHeader + Stream.mkvHeaderSz, newbuf, newsz);
|
||||||
@@ -179,16 +210,20 @@ static void stream_step(const uint8_t *newbuf, size_t newsz) {
|
|||||||
|
|
||||||
uint8_t *clusterEl = memmem(Stream.mkvHeader, Stream.mkvHeaderSz, "\x1F\x43\xB6\x75", 4);
|
uint8_t *clusterEl = memmem(Stream.mkvHeader, Stream.mkvHeaderSz, "\x1F\x43\xB6\x75", 4);
|
||||||
if(clusterEl) {
|
if(clusterEl) {
|
||||||
ws_broadcast(Stream.mkvHeader, clusterEl - Stream.mkvHeader);
|
ws_broadcast(true, Stream.mkvHeader, clusterEl - Stream.mkvHeader);
|
||||||
ws_broadcast(clusterEl, Stream.mkvHeader + Stream.mkvHeaderSz - clusterEl);
|
ws_broadcast(true, clusterEl, Stream.mkvHeader + Stream.mkvHeaderSz - clusterEl);
|
||||||
|
|
||||||
Stream.mkvHeaderSz = clusterEl - Stream.mkvHeader;
|
Stream.mkvHeaderSz = clusterEl - Stream.mkvHeader;
|
||||||
Stream.state = STREAMING;
|
Stream.state = STREAMING;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
static const uint8_t rootEl[4] = "\x1A\x45\xDF\xA3";
|
||||||
|
|
||||||
int i;
|
int i;
|
||||||
for(i = 0; i < newsz; i++) {
|
for(i = 0; i < newsz; i++) {
|
||||||
if(newbuf[i] == "\x1A\x45\xDF\xA3"[Stream.stateChangeIdx]) {
|
if(newbuf[i] == rootEl[0]) {
|
||||||
|
Stream.stateChangeIdx = 1;
|
||||||
|
} else if(newbuf[i] == rootEl[Stream.stateChangeIdx]) {
|
||||||
Stream.stateChangeIdx++;
|
Stream.stateChangeIdx++;
|
||||||
|
|
||||||
if(Stream.stateChangeIdx == 4) {
|
if(Stream.stateChangeIdx == 4) {
|
||||||
@@ -203,15 +238,17 @@ static void stream_step(const uint8_t *newbuf, size_t newsz) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(Stream.state == LOADING_HEADER) {
|
if(Stream.state == LOADING_HEADER) {
|
||||||
|
puts("New header");
|
||||||
|
|
||||||
if(i > 4) {
|
if(i > 4) {
|
||||||
ws_broadcast(newbuf, i - 4);
|
ws_broadcast(true, newbuf, i - 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream.mkvHeader = realloc(Stream.mkvHeader, Stream.mkvHeaderSz = 4 + (newsz - i));
|
Stream.mkvHeader = realloc(Stream.mkvHeader, Stream.mkvHeaderSz = 4 + (newsz - i));
|
||||||
memcpy(Stream.mkvHeader, "\x1A\x45\xDF\xA3", 4);
|
memcpy(Stream.mkvHeader, "\x1A\x45\xDF\xA3", 4);
|
||||||
memcpy(Stream.mkvHeader + 4, newbuf + i, newsz - i);
|
memcpy(Stream.mkvHeader + 4, newbuf + i, newsz - i);
|
||||||
} else {
|
} else {
|
||||||
ws_broadcast(newbuf, newsz);
|
ws_broadcast(true, newbuf, newsz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +332,7 @@ static int handle(Client *cli) {
|
|||||||
|
|
||||||
if(Stream.state == STREAMING && Stream.mkvHeader) {
|
if(Stream.state == STREAMING && Stream.mkvHeader) {
|
||||||
printf("Sending header\n");
|
printf("Sending header\n");
|
||||||
ws_send(cli, Stream.mkvHeader, Stream.mkvHeaderSz);
|
ws_send(cli, true, Stream.mkvHeader, Stream.mkvHeaderSz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +343,9 @@ static int handle(Client *cli) {
|
|||||||
size_t rsize = cli->len;
|
size_t rsize = cli->len;
|
||||||
int pret = phr_decode_chunked(&cli->chudec, cli->buf, &rsize);
|
int pret = phr_decode_chunked(&cli->chudec, cli->buf, &rsize);
|
||||||
|
|
||||||
if(pret == -1) return 0;
|
if(pret == -1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
stream_step(cli->buf, rsize);
|
stream_step(cli->buf, rsize);
|
||||||
|
|
||||||
@@ -367,6 +406,8 @@ static int handle(Client *cli) {
|
|||||||
cli->ws.incoming = realloc(cli->ws.incoming, cli->ws.incomingSz + payloadSz);
|
cli->ws.incoming = realloc(cli->ws.incoming, cli->ws.incomingSz + payloadSz);
|
||||||
memcpy(cli->ws.incoming + cli->ws.incomingSz, cli->buf + i + 4, payloadSz);
|
memcpy(cli->ws.incoming + cli->ws.incomingSz, cli->buf + i + 4, payloadSz);
|
||||||
|
|
||||||
|
consume(cli, i + 4 + payloadSz);
|
||||||
|
|
||||||
if(fin) {
|
if(fin) {
|
||||||
receive_ws(cli);
|
receive_ws(cli);
|
||||||
|
|
||||||
|
|||||||
96
rawpcmworklet.js
Normal file
96
rawpcmworklet.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// To explain succinctly, the people who designed AudioWorklet and
|
||||||
|
// deprecated ScriptProcessorNode are retarded and we need a worklet
|
||||||
|
// that does basically nothing
|
||||||
|
|
||||||
|
// Must be careful to create as little garbage as possible otherwise
|
||||||
|
// even this will produce cracks/pops/clicks/blips.
|
||||||
|
// It was like this on Firefox; Chromium managed.
|
||||||
|
|
||||||
|
class RawPCMWorklet extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.ringL = new Float32Array(144000)
|
||||||
|
this.ringR = new Float32Array(144000)
|
||||||
|
this.ringRead = 0
|
||||||
|
this.mute = true
|
||||||
|
|
||||||
|
for(var z = 0; z < 65536; z++) {
|
||||||
|
this.ringL[z] = Math.sin(z / 128 * 2 * Math.PI) * 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
if(event.data === true || event.data === false) {
|
||||||
|
this.mute = event.data
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newaudioframes = event.data
|
||||||
|
var writeIndex = newaudioframes.t
|
||||||
|
|
||||||
|
var newlen = newaudioframes.left.length
|
||||||
|
|
||||||
|
if(newaudioframes.left.length > this.ringL.length) {
|
||||||
|
newaudioframes.left = newaudioframes.left.slice(newaudioframes.left.length - this.ringL.length)
|
||||||
|
newaudioframes.right = newaudioframes.right.slice(newaudioframes.right.length - this.ringL.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(writeIndex % this.ringL.length + newaudioframes.left.length <= this.ringL.length) {
|
||||||
|
this.ringL.set(newaudioframes.left, writeIndex % this.ringL.length)
|
||||||
|
this.ringR.set(newaudioframes.right, writeIndex % this.ringL.length)
|
||||||
|
} else {
|
||||||
|
var boundary = this.ringL.length - writeIndex % this.ringL.length
|
||||||
|
|
||||||
|
this.ringL.set(newaudioframes.left.slice(0, boundary), writeIndex % this.ringL.length)
|
||||||
|
this.ringL.set(newaudioframes.left.slice(boundary), 0)
|
||||||
|
|
||||||
|
this.ringR.set(newaudioframes.right.slice(0, boundary), writeIndex % this.ringL.length)
|
||||||
|
this.ringR.set(newaudioframes.right.slice(boundary), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
const output = outputs[0]
|
||||||
|
|
||||||
|
var left = output[0]
|
||||||
|
var right = output[1]
|
||||||
|
|
||||||
|
/*if(this.ringWrite < 16384) {
|
||||||
|
return true
|
||||||
|
}*/
|
||||||
|
|
||||||
|
//var available = Math.min(left.length, Math.max(0, this.ringWrite - this.ringRead))
|
||||||
|
var available = left.length
|
||||||
|
|
||||||
|
if(this.mute === false) {
|
||||||
|
if(this.ringRead % this.ringL.length + available <= this.ringL.length) {
|
||||||
|
left.set(this.ringL.slice(this.ringRead % this.ringL.length, this.ringRead % this.ringL.length + available))
|
||||||
|
right.set(this.ringR.slice(this.ringRead % this.ringL.length, this.ringRead % this.ringL.length + available))
|
||||||
|
} else {
|
||||||
|
left.set(this.ringL.slice(this.ringRead % this.ringL.length))
|
||||||
|
right.set(this.ringR.slice(this.ringRead % this.ringL.length))
|
||||||
|
|
||||||
|
var boundary = this.ringL.length - this.ringRead % this.ringL.length
|
||||||
|
|
||||||
|
left.set(this.ringL.slice(0, available - boundary), boundary)
|
||||||
|
right.set(this.ringR.slice(0, available - boundary), boundary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ringRead += left.length
|
||||||
|
|
||||||
|
//console.log(this.ringRead / 44100)
|
||||||
|
|
||||||
|
/*for(var s = 0; s < available; s++) {
|
||||||
|
var sw = Math.sin((this.debug + s) / 48000 * 440 * 2 * 3.1415926) * 0.3
|
||||||
|
left[s] = sw
|
||||||
|
right[s] = sw
|
||||||
|
}
|
||||||
|
this.debug += available*/
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('rawpcmworklet', RawPCMWorklet);
|
||||||
Reference in New Issue
Block a user