Compare commits

..

9 Commits

Author SHA1 Message Date
Mid
7fd717d75e Status updates 2025-08-31 20:11:05 +03:00
Mid
126f8d0ba6 State change bug fix 2025-08-31 20:10:51 +03:00
Mid
862c52f567 Fix memory leakage 2025-08-31 20:07:15 +03:00
Mid
e929b5af1e Fixes 2025-06-17 22:10:19 +03:00
Mid
079fd61390 Update README.md 2025-04-15 12:19:54 +03:00
Mid
2fffb905e3 Improve crackling in worklet codepath 2025-04-15 12:09:31 +03:00
Mid
0da0701d34 Suspend immediately 2025-04-15 12:08:11 +03:00
Mid
0408433fc3 WebSocket receive bug fix 2025-04-15 12:07:43 +03:00
Mid
1acf98ef54 Avoid SIGPIPE 2025-04-15 12:07:27 +03:00
5 changed files with 299 additions and 107 deletions

View File

@@ -5,7 +5,7 @@ This repository actually holds three programs, so be careful to not lost in the
* After `make` completes, `wsrA` is the relay program, which can be run as so: `./wsrA port=12345 key=MyFancyStreamingKeyIsOVERHERE`. * 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. * 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;`. * 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` and `support.wasm` are the frontend, which must be accessible to the browser. * `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. * 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. * 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`. * You may also disable chat by setting `ENABLE_CHAT` to `false`.

191
blarf.js
View File

@@ -2,22 +2,51 @@
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">🔈&#xFE0E;</span><span class="MKVSpeakerOn" style="display:none;">🔊&#xFE0E;</span></div> <div class="MKVSpeaker"><span class="MKVSpeakerOff">🔈&#xFE0E;</span><span class="MKVSpeakerOn" style="display:none;">🔊&#xFE0E;</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, AudWorklet 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) {
@@ -31,6 +60,7 @@
var DebugSine = 0 var DebugSine = 0
AudCtx = new AudioContext({sampleRate: hz}) AudCtx = new AudioContext({sampleRate: hz})
AudCtx.suspend()
if(AudCtx.audioWorklet) { if(AudCtx.audioWorklet) {
AudCtx.audioWorklet.addModule("rawpcmworklet.js").then(function() { AudCtx.audioWorklet.addModule("rawpcmworklet.js").then(function() {
@@ -45,22 +75,16 @@
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
/*for(var i = 0; i < outL.length; i++) {
outL[i] = Math.sin(440 * 2 * 3.14159 * (DebugSine / AudHz))
DebugSine++
}
AudioQueue = []
return*/
var leftToWrite = outL.length var leftToWrite = outL.length
var offset = 0 var offset = 0
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)
@@ -84,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" })
@@ -99,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()
} }
} }
@@ -110,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()
@@ -142,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]) {
@@ -158,14 +196,10 @@
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 toHex(buffer) { function toHex(buffer) {
@@ -334,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
@@ -354,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})
} }
} }
} }
@@ -379,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)
} }
@@ -393,17 +433,47 @@
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 = "&bull; " + obj.status.viewer_count
}
} else {
ebml.poosh(new Uint8Array(ev.data)) ebml.poosh(new Uint8Array(ev.data))
ebml.parse() ebml.parse()
// It would make more sense for this to be in `render` but we need the guarantee that this will run when the tab is out of focus while(document.hidden && VideoQueue.length > 1 && VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t <= LatencyMS) {
if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) { BufferPool.add(VideoQueue.shift().imgData)
AudWorklet.port.postMessage({msg: "data", audio: AudioQueue}) }
AudioQueue.length = 0
} }
} }
ws.onclose = function(ev) { ws.onclose = function(ev) {
@@ -413,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"
})() })()

View File

@@ -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 {
@@ -131,10 +141,9 @@
converse.initialize({ converse.initialize({
view_mode: 'embedded', view_mode: 'embedded',
websocket_url: CHAT_HOST_WS_URL, websocket_url: CHAT_HOST_WS_URL,
login: 'anonymous', authentication: 'anonymous',
jid: un + '@' + CHAT_HOST, jid: CHAT_HOST,
auto_login: true, auto_login: true,
password: 'lol',
auto_join_rooms: [CHAT_MUC], auto_join_rooms: [CHAT_MUC],
show_message_avatar: false, show_message_avatar: false,
show_controlbox_by_default: false, show_controlbox_by_default: false,

71
main2.c
View File

@@ -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);

View File

@@ -1,27 +1,51 @@
// To explain succinctly, the people who designed AudioWorklet and // To explain succinctly, the people who designed AudioWorklet and
// deprecated ScriptProcessorNode are retarded and we need a worklet // deprecated ScriptProcessorNode are retarded and we need a worklet
// that does basically nothing to get glitchless audio. // 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 { class RawPCMWorklet extends AudioWorkletProcessor {
constructor() { constructor() {
super() super()
this.left = new Float32Array() this.ringL = new Float32Array(144000)
this.right = new Float32Array() 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) => { this.port.onmessage = (event) => {
var newaudioframes = event.data.audio if(event.data === true || event.data === false) {
this.mute = event.data
return
}
for(var i = 0; i < newaudioframes.length; i++) { var newaudioframes = event.data
var newleft = new Float32Array(this.left.length + newaudioframes[i].left.length) var writeIndex = newaudioframes.t
newleft.set(this.left, 0)
newleft.set(newaudioframes[i].left, this.left.length)
this.left = newleft
var newright = new Float32Array(this.right.length + newaudioframes[i].right.length) var newlen = newaudioframes.left.length
newright.set(this.right, 0)
newright.set(newaudioframes[i].right, this.right.length) if(newaudioframes.left.length > this.ringL.length) {
this.right = newright 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)
} }
} }
} }
@@ -32,15 +56,40 @@ class RawPCMWorklet extends AudioWorkletProcessor {
var left = output[0] var left = output[0]
var right = output[1] var right = output[1]
var available = Math.min(left.length, this.left.length) /*if(this.ringWrite < 16384) {
return true
}*/
left.set(this.left.slice(0, available)) //var available = Math.min(left.length, Math.max(0, this.ringWrite - this.ringRead))
right.set(this.right.slice(0, available)) var available = left.length
this.left = this.left.slice(available) if(this.mute === false) {
this.right = this.right.slice(available) 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))
return true; 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
} }
} }