diff --git a/blarf.js b/blarf.js
index 23e6e1d..2b87a8f 100644
--- a/blarf.js
+++ b/blarf.js
@@ -2,24 +2,51 @@
var VideoQueue = []
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")
BlarfEl.innerHTML = `
-
🔈︎🔊︎
-
00:00:00
-
+
+
🔈︎🔊︎
+
00:00:00
+
+
+
+
+
`
var Canvus = BlarfEl.querySelector("canvas")
var CanvCtx = Canvus.getContext("2d")
+ var CanvImageData
+
+ var LatencyMS = 1000
var AudCtx
var AudScript, AudWorklet
var AudHz
- var AudDejitter
var AudMuted = true
+ var AudSampleIndex = 0
function create_audio(hz, channels) {
if(AudCtx) {
@@ -30,9 +57,6 @@
AudHz = hz
- // Fill up buffer for 1 second before playing
- AudDejitter = AudHz
-
var DebugSine = 0
AudCtx = new AudioContext({sampleRate: hz})
@@ -104,7 +128,7 @@
document.querySelector(".MKVSpeaker").onclick = togglemute
document.onkeypress = function(e) {
- if(e.key.toUpperCase() == "M") {
+ if(document.activeElement.tagName != "TEXTAREA" && e.key.toUpperCase() == "M") {
togglemute()
}
}
@@ -130,20 +154,31 @@
}
}
+ 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"})
- VideoQueue.push({t: e.data.t, imgData: imgData})
+// 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({left: e.data.left, right: e.data.right || e.data.left})
+ AudioQueue.push({t: e.data.t, left: e.data.left, right: e.data.right || e.data.left})
- // Prevent the audio queue filling up and causing ever-increasing AV desync
- if(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)
- crop_audio_queue(durationToRemove)
+ if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) {
+ AudWorklet.port.postMessage(merge_audio_queue())
+ AudioQueue.length = 0
}
}
@@ -333,10 +368,7 @@
this.currentClusterTime = EBMLParser.vi_to_i(data)
if(!RenderStartTime) {
- RenderStartTime = document.timeline.currentTime + 600
- }
- if(!VideoStartTime) {
- VideoStartTime = this.currentClusterTime
+ RenderStartTime = performance.now()
}
} else if(elID == 0xA3) {
// Cluster -> SimpleBlock
@@ -353,10 +385,16 @@
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)
+ var playerTimestamp = this.currentClusterTime + timestamp
+
if(track) {
+ if(!VideoStartTime) {
+ VideoStartTime = playerTimestamp
+ }
+
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})
}
}
}
@@ -378,6 +416,9 @@
if(track.type == "video") {
Canvus.width = track.width
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 {
create_audio(track.samplerate, track.channels)
}
@@ -410,24 +451,28 @@
s += AudioQueue[i].left.length
}
- return {msg: "data", left: L, right: R}
+ var ret = {msg: "data", t: AudSampleIndex, left: L, right: R}
+
+ AudSampleIndex += L.length
+
+ return ret
}
function reconnect_ws() {
var ws = new WebSocket(BlarfEl.getAttribute("data-target"))
ws.binaryType = "arraybuffer"
ws.onmessage = function(ev) {
- ebml.poosh(new Uint8Array(ev.data))
- 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
- if(AudCtx.state == "running" && AudWorklet && AudioQueue.length) {
- AudWorklet.port.postMessage(merge_audio_queue())
- AudioQueue.length = 0
- }
- if(VideoQueue.length) {
- while(document.timeline.currentTime - VideoQueue[0].t > 5000) {
- VideoQueue.shift()
+ 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.parse()
+
+ while(document.hidden && VideoQueue.length > 1 && VideoQueue[VideoQueue.length - 1].t - VideoQueue[0].t <= LatencyMS) {
+ BufferPool.add(VideoQueue.shift().imgData)
}
}
}
@@ -438,19 +483,37 @@
reconnect_ws()
function render(timestamp) {
- document.querySelector(".MKVControls").style.opacity = Math.max(0, Math.min(1, 5 - (timestamp - LastControlsInterrupt) / 1000))
-
- var nextImg = null
- while(RenderStartTime && VideoQueue.length && VideoQueue[0].t + VideoBufferingOffset <= (timestamp - RenderStartTime)) {
- nextImg = VideoQueue[0].imgData
- VideoQueue.shift()
- }
-
- if(nextImg) {
- CanvCtx.putImageData(nextImg, 0, 0)
+ try {
+ document.querySelector(".MKVControls").style.opacity = Math.max(0, Math.min(1, 5 - (timestamp - LastControlsInterrupt) / 1000))
+
+ var nextImg = null
+ while(RenderStartTime && VideoQueue.length && VideoQueue[0].t <= (timestamp - RenderStartTime - LatencyMS)) {
+ if(nextImg) BufferPool.add(nextImg.imgData)
+ nextImg = VideoQueue[0]
+ 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)
+
+ document.querySelector(".MKVControls").style.display = "none"
})()
diff --git a/index.html b/index.html
index 0ed3813..d0833e9 100644
--- a/index.html
+++ b/index.html
@@ -42,6 +42,9 @@
font-size: 0.4cm;
background: rgb(0, 0, 0);
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 > * {
vertical-align: middle;
@@ -53,6 +56,9 @@
cursor: pointer;
font-size: 0.75cm;
}
+ div#BLARF .MKVStatus {
+ margin-right: 0.5em;
+ }
div#BLARF > canvas {
background: url(intermission.jpg) black;
background-position: 0 30%;
@@ -71,6 +77,10 @@
display: block;
line-height: initial;
}
+ span.chat-msg__heading {
+ width: inherit !important;
+ margin-bottom: 0;
+ }
@media(max-aspect-ratio: 1) {
div.everything {
@@ -122,6 +132,11 @@