(function() { 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
` var Canvus = BlarfEl.querySelector("canvas") var CanvCtx = Canvus.getContext("2d") var CanvImageData var LatencyMS = 1000 var AudCtx var AudScript, AudWorklet var AudHz var AudMuted = true var AudSampleIndex = 0 function create_audio(hz, channels) { if(AudCtx) { AudCtx.close() AudScript = null AudWorklet = null } AudHz = hz var DebugSine = 0 AudCtx = new AudioContext({sampleRate: hz}) 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) { var outL = e.outputBuffer.getChannelData(0) var outR = channels > 1 ? e.outputBuffer.getChannelData(1) : null var leftToWrite = outL.length var offset = 0 while(AudioQueue.length && leftToWrite) { var amount = Math.min(leftToWrite, AudioQueue[0].left.length) if(!AudMuted) { outL.set(AudioQueue[0].left.subarray(0, amount), offset) if(outR) outR.set(AudioQueue[0].right.subarray(0, amount), offset) } AudioQueue[0].left = AudioQueue[0].left.subarray(amount) if(outR) AudioQueue[0].right = AudioQueue[0].right.subarray(amount) if(AudioQueue[0].left.length == 0) { AudioQueue.shift() } leftToWrite -= amount offset += amount } } AudScript.connect(AudCtx.destination) } } var LastControlsInterrupt function interruptcontrols() { LastControlsInterrupt = document.timeline.currentTime } interruptcontrols() function togglemute() { if(!AudCtx) { return; } AudCtx.resume() AudMuted = !AudMuted if(AudWorklet) { AudWorklet.port.postMessage(AudMuted) } document.querySelectorAll(".MKVSpeaker *").forEach(function(el) { el.style.display = el.style.display == "none" ? "" : "none" }) interruptcontrols() } document.querySelector(".MKVSpeaker").onclick = togglemute document.onkeypress = function(e) { if(document.activeElement.tagName != "TEXTAREA" && e.key.toUpperCase() == "M") { togglemute() } } BlarfEl.onmousemove = function() { interruptcontrols() } var RenderStartTime, VideoStartTime function crop_audio_queue(durationToRemove) { while(AudioQueue.length && durationToRemove) { var amount = Math.min(durationToRemove, AudioQueue[0].left.length) AudioQueue[0].left = AudioQueue[0].left.subarray(amount) AudioQueue[0].right = AudioQueue[0].right.subarray(amount) if(AudioQueue[0].left.length == 0) { AudioQueue.shift() } 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]) { Statistics[e.data.id] = {sum: 0, count: 0} } Statistics[e.data.id].sum += e.data.taken Statistics[e.data.id].count++ var stats = document.querySelector(".MKVStats") if(stats) { /* var text = "" for(var k in Statistics) { text = text + k + ":" + (Math.floor(100 * Statistics[k].sum / Statistics[k].count) / 100) + "," } stats.innerText = text*/ 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" } } var VideoBufferingOffset = 0 function toHex(buffer) { return Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)).join(''); } function pad(str, n, z) { z = z || '0' str = str + '' while(str.length < n) { str = z + str } return str } class EBMLParser { Accum = new Uint8Array([]) I = 0 IdStack = [] SizeStack = [] get_varint() { if(this.Accum.length == 0) return null; var bytes = Math.clz32(this.Accum[this.I]) - 23 if(this.Accum.length - this.I < bytes) return null; var ret = this.Accum.subarray(this.I, this.I + bytes).slice(0) this.I += bytes return ret } poosh(toAdd) { var a = this.Accum this.Accum = new Uint8Array(a.length + toAdd.length) this.Accum.set(a) this.Accum.set(toAdd, a.length) } parse() { do { var IOld = this.I var elID = this.get_varint() if(elID === null) { this.I = IOld break } elID = EBMLParser.vi_to_i(elID) var elSize = this.get_varint() if(elSize === null) { this.I = IOld break } EBMLParser.parse_varint(elSize) elSize = EBMLParser.vi_to_i(elSize) if(elID == 0x18538067 || elID == 0x114D9B74 || elID == 0x1549A966 || elID == 0x1F43B675 || elID == 0x1654AE6B || elID == 0xE0 || elID == 0xE1 || elID == 0xAE) { // tree this.IdStack.push(elID) this.SizeStack.push(elSize + (this.I - IOld)) if(this.onenter) { this.onenter(elID) } } else { // binary if(this.Accum.length - this.I >= elSize) { if(this.ondata) { this.ondata(elID, this.Accum.subarray(this.I, this.I + elSize)) } this.I += elSize } else { this.I = IOld break } } for(var i = 0; i < this.IdStack.length; i++) { this.SizeStack[i] -= this.I - IOld } while(this.SizeStack.length && this.SizeStack[this.SizeStack.length - 1] <= 0) { if(this.SizeStack[this.SizeStack.length] - 1 < 0) console.log("Shit") if(this.onexit) { this.onexit(this.IdStack[this.IdStack.length - 1]) } this.SizeStack.pop() this.IdStack.pop() } } while(true); this.Accum = this.Accum.subarray(this.I) this.I = 0 } static parse_varint(vi) { vi[0] = vi[0] & ((1 << (31 - Math.clz32(vi[0]))) - 1) } static vi_to_i(vi) { var ret = 0 for(var i = 0; i < vi.length; i++) { ret = ret * 256 + vi[i] } return ret } } class MatroskaState { tracks = [] onenter(elID) { if(elID == 0xAE) { // Track Entry this.tracks.push({}) } else if(elID == 0xE0) { // Track Entry -> Track Video this.tracks[this.tracks.length - 1].type = "video" } else if(elID == 0xE1) { // Track Entry -> Track Audio this.tracks[this.tracks.length - 1].type = "audio" } } ondata(elID, data) { if(elID == 0xD7) { // Track Entry -> Track Number this.tracks[this.tracks.length - 1].id = EBMLParser.vi_to_i(data) } else if(elID == 0xB0) { // Track Entry -> Track Video -> Width this.tracks[this.tracks.length - 1].width = EBMLParser.vi_to_i(data) } else if(elID == 0xBA) { // Track Entry -> Track Video -> Height this.tracks[this.tracks.length - 1].height = EBMLParser.vi_to_i(data) } else if(elID == 0x9F) { // Track Entry -> Track Audio -> Channels this.tracks[this.tracks.length - 1].channels = EBMLParser.vi_to_i(data) } else if(elID == 0xB5) { // Track Entry -> Track Audio -> Sampling Frequency var dv = new DataView(data.slice(0).buffer) this.tracks[this.tracks.length - 1].samplerate = data.length == 4 ? dv.getFloat32(0, false) : dv.getFloat64(0, false) } else if(elID == 0x86) { // Track Entry -> Codec Type this.tracks[this.tracks.length - 1].codec = new TextDecoder().decode(data); } else if(elID == 0x63A2) { // Track Entry -> Codec Private this.tracks[this.tracks.length - 1].priv = data.slice(0) } else if(elID == 0xE7) { // Cluster -> Timestamp this.currentClusterTime = EBMLParser.vi_to_i(data) if(!RenderStartTime) { RenderStartTime = performance.now() } } else if(elID == 0xA3) { // Cluster -> SimpleBlock var trackID = data[0] & 127 var track = this.tracks.find(function(t) {return t.id == trackID}) var timestamp = data[1] * 256 + data[2] var flags = data[3] var kf = !!(flags & 128) 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: playerTimestamp - VideoStartTime, packet: packet, kf: kf}) } } } onexit(elID) { if(elID == 0xAE) { // Track Entry var track = this.tracks[this.tracks.length - 1] var codec = track.codec var id = track.id var priv = track.priv var channels = track.samples // undefined if not audio TheWorker.postMessage({cmd: "create", codec: codec, id: id, priv: priv, channels: channels}) 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) } } } } var matr = new MatroskaState() var ebml = new EBMLParser() ebml.onenter = matr.onenter.bind(matr) ebml.ondata = matr.ondata.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() { var ws = new WebSocket(BlarfEl.getAttribute("data-target")) ws.binaryType = "arraybuffer" 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.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) { setTimeout(reconnect_ws, 5000) } } reconnect_ws() function render(timestamp) { 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" })()