Upgrading to SnapStream 0.2.0 (and adding tsx loading)

This commit is contained in:
James Barnsley
2021-03-23 21:04:08 +13:00
parent 9a4cb4d86d
commit 7dd6e80315
13 changed files with 1688 additions and 2274 deletions

View File

@ -1,7 +1,8 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-class-properties"

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1616484761";
var build = "1616485783";
var version = "3.57.1";
// Construct the script tag

1196
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,7 @@
"@babel/preset-env": "7.10.1",
"@babel/preset-react": "7.10.1",
"@babel/preset-stage-2": "7.8.3",
"@babel/preset-typescript": "^7.6.0",
"@babel/register": "7.10.1",
"babel-eslint": "^10",
"babel-jest": "^26.6.3",

View File

@ -23,9 +23,10 @@ export default class ErrorBoundary extends React.Component {
render = () => {
const { hasError, info: { componentStack } = {} } = this.state;
const { children } = this.props;
const { children, silent } = this.props;
if (hasError) {
if (silent) return null;
return (
<ErrorMessage type="error-boundary">
{componentStack && <pre className="error-message__trace">{componentStack}</pre>}

View File

@ -1,872 +0,0 @@
/* eslint-disable max-classes-per-file */
// Modified for Iris
// Original source https://github.com/badaix/snapcast
const Flac = require('libflacjs/dist/libflac.js');
function setCookie(key, value, exdays = -1) {
let d = new Date();
if (exdays < 0)
exdays = 10 * 365;
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/";
}
function getCookie(key, defaultValue = "") {
let name = key + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let c of ca) {
c = c.trimLeft();
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
setCookie(key, defaultValue);
return defaultValue;
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
class Tv {
constructor(sec, usec) {
this.sec = 0;
this.usec = 0;
this.sec = sec;
this.usec = usec;
}
setMilliseconds(ms) {
this.sec = Math.floor(ms / 1000);
this.usec = Math.floor(ms * 1000) % 1000000;
}
getMilliseconds() {
return this.sec * 1000 + this.usec / 1000;
}
}
class BaseMessage {
constructor(buffer) {
this.type = 0;
this.id = 0;
this.refersTo = 0;
this.received = new Tv(0, 0);
this.sent = new Tv(0, 0);
this.size = 0;
}
deserialize(buffer) {
let view = new DataView(buffer);
this.type = view.getUint16(0, true);
this.id = view.getUint16(2, true);
this.refersTo = view.getUint16(4, true);
this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true));
this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true));
this.size = view.getUint32(22, true);
}
serialize() {
this.size = 26 + this.getSize();
let buffer = new ArrayBuffer(this.size);
let view = new DataView(buffer);
view.setUint16(0, this.type, true);
view.setUint16(2, this.id, true);
view.setUint16(4, this.refersTo, true);
view.setInt32(6, this.sent.sec, true);
view.setInt32(10, this.sent.usec, true);
view.setInt32(14, this.received.sec, true);
view.setInt32(18, this.received.usec, true);
view.setUint32(22, this.size, true);
return buffer;
}
getSize() {
return 0;
}
}
class CodecMessage extends BaseMessage {
constructor(buffer) {
super(buffer);
this.codec = "";
this.payload = new ArrayBuffer(0);
if (buffer) {
this.deserialize(buffer);
}
this.type = 1;
}
deserialize(buffer) {
super.deserialize(buffer);
let view = new DataView(buffer);
let codecSize = view.getInt32(26, true);
let decoder = new TextDecoder("utf-8");
this.codec = decoder.decode(buffer.slice(30, 30 + codecSize));
let payloadSize = view.getInt32(30 + codecSize, true);
//console.log("payload size: " + payloadSize);
this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize);
//console.log("payload: " + this.payload);
}
}
class TimeMessage extends BaseMessage {
constructor(buffer) {
super(buffer);
this.latency = new Tv(0, 0);
if (buffer) {
this.deserialize(buffer);
}
this.type = 4;
}
deserialize(buffer) {
super.deserialize(buffer);
let view = new DataView(buffer);
this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true));
}
serialize() {
let buffer = super.serialize();
let view = new DataView(buffer);
view.setInt32(26, this.latency.sec, true);
view.setInt32(30, this.latency.sec, true);
return buffer;
}
getSize() {
return 8;
}
}
class JsonMessage extends BaseMessage {
constructor(buffer) {
super(buffer);
if (buffer) {
this.deserialize(buffer);
}
}
deserialize(buffer) {
super.deserialize(buffer);
let view = new DataView(buffer);
let size = view.getInt32(26, true);
let decoder = new TextDecoder();
this.json = JSON.parse(decoder.decode(buffer.slice(30)));
}
serialize() {
let buffer = super.serialize();
let view = new DataView(buffer);
let jsonStr = JSON.stringify(this.json);
view.setUint32(26, jsonStr.length, true);
let encoder = new TextEncoder();
let encoded = encoder.encode(jsonStr);
for (let i = 0; i < encoded.length; ++i)
view.setUint8(30 + i, encoded[i]);
return buffer;
}
getSize() {
let encoder = new TextEncoder();
let encoded = encoder.encode(JSON.stringify(this.json));
return encoded.length + 4;
// return JSON.stringify(this.json).length;
}
}
class HelloMessage extends JsonMessage {
constructor(buffer) {
super(buffer);
this.mac = "";
this.hostname = "";
this.version = "0.1.0";
this.clientName = "Snapweb";
this.os = "";
this.arch = "web";
this.instance = 1;
this.uniqueId = "";
this.snapStreamProtocolVersion = 2;
if (buffer) {
this.deserialize(buffer);
}
this.type = 5;
}
deserialize(buffer) {
super.deserialize(buffer);
this.mac = this.json["MAC"];
this.hostname = this.json["HostName"];
this.version = this.json["Version"];
this.clientName = this.json["ClientName"];
this.os = this.json["OS"];
this.arch = this.json["Arch"];
this.instance = this.json["Instance"];
this.uniqueId = this.json["ID"];
this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"];
}
serialize() {
this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion };
return super.serialize();
}
}
class ServerSettingsMessage extends JsonMessage {
constructor(buffer) {
super(buffer);
this.bufferMs = 0;
this.latency = 0;
this.volumePercent = 0;
this.muted = false;
if (buffer) {
this.deserialize(buffer);
}
this.type = 3;
}
deserialize(buffer) {
super.deserialize(buffer);
this.bufferMs = this.json["bufferMs"];
this.latency = this.json["latency"];
this.volumePercent = this.json["volume"];
this.muted = this.json["muted"];
}
serialize() {
this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted };
return super.serialize();
}
}
class PcmChunkMessage extends BaseMessage {
constructor(buffer, sampleFormat) {
super(buffer);
this.timestamp = new Tv(0, 0);
// payloadSize: number = 0;
this.payload = new ArrayBuffer(0);
this.idx = 0;
this.deserialize(buffer);
this.sampleFormat = sampleFormat;
this.type = 2;
}
deserialize(buffer) {
super.deserialize(buffer);
let view = new DataView(buffer);
this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true));
// this.payloadSize = view.getUint32(34, true);
this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize);
//console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength);
}
readFrames(frames) {
let frameCnt = frames;
let frameSize = this.sampleFormat.frameSize();
if (this.idx + frames > this.payloadSize() / frameSize)
frameCnt = (this.payloadSize() / frameSize) - this.idx;
let begin = this.idx * frameSize;
this.idx += frameCnt;
let end = begin + frameCnt * frameSize;
//console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength);
return this.payload.slice(begin, end);
}
getFrameCount() {
return (this.payloadSize() / this.sampleFormat.frameSize());
}
isEndOfChunk() {
return this.idx >= this.getFrameCount();
}
startMs() {
return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate);
}
duration() {
return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate);
}
payloadSize() {
return this.payload.byteLength;
}
clearPayload() {
this.payload = new ArrayBuffer(0);
}
addPayload(buffer) {
let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength);
let view = new DataView(payload);
let viewOld = new DataView(this.payload);
let viewNew = new DataView(buffer);
for (let i = 0; i < viewOld.byteLength; ++i) {
view.setInt8(i, viewOld.getInt8(i));
}
for (let i = 0; i < viewNew.byteLength; ++i) {
view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i));
}
this.payload = payload;
}
}
class AudioStream {
constructor(timeProvider, sampleFormat, bufferMs) {
this.timeProvider = timeProvider;
this.sampleFormat = sampleFormat;
this.bufferMs = bufferMs;
this.chunks = new Array();
// setRealSampleRate(sampleRate: number) {
// if (sampleRate == this.sampleFormat.rate) {
// this.correctAfterXFrames = 0;
// }
// else {
// this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
// console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
// }
// }
this.chunk = undefined;
this.volume = 1;
this.muted = false;
this.lastLog = 0;
}
setVolume(percent, muted) {
let base = 10;
this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1);
//console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted);
this.muted = muted;
}
addChunk(chunk) {
this.chunks.push(chunk);
// let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
// let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds();
// console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2));
while (this.chunks.length > 0) {
let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
// todo: consider buffer ms
if (age > 5000 + this.bufferMs) {
this.chunks.shift();
//console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length);
}
else
break;
}
}
getNextBuffer(buffer, playTimeMs) {
if (!this.chunk) {
this.chunk = this.chunks.shift();
}
// let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs;
let frames = buffer.length;
// console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2));
let left = new Float32Array(frames);
let right = new Float32Array(frames);
let read = 0;
let pos = 0;
// let volume = this.muted ? 0 : this.volume;
let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs);
if (this.chunk) {
let age = serverPlayTimeMs - this.chunk.startMs(); // - 500;
let reqChunkDuration = frames / this.sampleFormat.msRate();
let secs = Math.floor(Date.now() / 1000);
if (this.lastLog != secs) {
this.lastLog = secs;
//console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration);
}
if (age < -reqChunkDuration) {
//console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2));
//console.log("Chunk too young, returning silence");
}
else {
if (Math.abs(age) > 5) {
// We are 5ms apart, do a hard sync, i.e. don't play faster/slower,
// but seek to the desired position instead
while (this.chunk && age > this.chunk.duration()) {
//console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")");
this.chunk = this.chunks.shift();
if (!this.chunk)
break;
age = serverPlayTimeMs - this.chunk.startMs();
}
if (this.chunk) {
if (age > 0) {
//console.log("Fast forwarding " + age.toFixed(2) + "ms");
this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate()));
}
else if (age < 0) {
//console.log("Playing silence " + -age.toFixed(2) + "ms");
let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate());
left.fill(0, 0, silentFrames);
right.fill(0, 0, silentFrames);
read = silentFrames;
pos = silentFrames;
}
age = 0;
}
}
// else if (age > 0.1) {
// let rate = age * 0.0005;
// rate = 1.0 - Math.min(rate, 0.0005);
// console.debug("Age > 0, rate: " + rate);
// // we are late (age > 0), this means we are not playing fast enough
// // => the real sample rate seems to be lower, we have to drop some frames
// this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
// }
// else if (age < -0.1) {
// let rate = -age * 0.0005;
// rate = 1.0 + Math.min(rate, 0.0005);
// console.debug("Age < 0, rate: " + rate);
// // we are early (age > 0), this means we are playing too fast
// // => the real sample rate seems to be higher, we have to insert some frames
// this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
// }
// else {
// this.setRealSampleRate(this.sampleFormat.rate);
// }
let addFrames = 0;
let everyN = 0;
if (age > 0.1) {
addFrames = Math.ceil(age); // / 5);
}
else if (age < -0.1) {
addFrames = Math.floor(age); // / 5);
}
// addFrames = -2;
let readFrames = frames + addFrames - read;
if (addFrames != 0)
everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1));
// addFrames = 0;
// console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN);
while ((read < readFrames) && this.chunk) {
let pcmChunk = this.chunk;
let pcmBuffer = pcmChunk.readFrames(readFrames - read);
let payload = new Int16Array(pcmBuffer);
// console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length);
// read += (pcmBuffer.byteLength / this.sampleFormat.frameSize());
for (let i = 0; i < payload.length; i += 2) {
read++;
left[pos] = (payload[i] / 32768); // * volume;
right[pos] = (payload[i + 1] / 32768); // * volume;
if ((everyN != 0) && (read % everyN == 0)) {
if (addFrames > 0) {
pos--;
}
else {
left[pos + 1] = left[pos];
right[pos + 1] = right[pos];
pos++;
//console.log("Add: " + pos);
}
}
pos++;
}
if (pcmChunk.isEndOfChunk()) {
this.chunk = this.chunks.shift();
}
}
if (addFrames != 0)
//console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN);
if (read == readFrames)
read = frames;
}
}
if (read < frames) {
//console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length);
left.fill(0, pos);
right.fill(0, pos);
}
buffer.copyToChannel(left, 0, 0);
buffer.copyToChannel(right, 1, 0);
}
}
class TimeProvider {
constructor(ctx = undefined) {
this.diffBuffer = new Array();
this.diff = 0;
if (ctx) {
this.setAudioContext(ctx);
}
let userAgent = navigator.userAgent.toLowerCase();
this.isFirefoxMobile = (userAgent.indexOf('firefox') > -1) && (userAgent.indexOf('android') > -1);
}
setAudioContext(ctx) {
this.ctx = ctx;
this.reset();
}
reset() {
this.diffBuffer.length = 0;
this.diff = 0;
}
setDiff(c2s, s2c) {
if (this.now() == 0) {
this.reset();
}
else {
if (this.diffBuffer.push((c2s - s2c) / 2) > 100)
this.diffBuffer.shift();
let sorted = [...this.diffBuffer];
sorted.sort();
this.diff = sorted[Math.floor(sorted.length / 2)];
}
// console.debug("c2s: " + c2s.toFixed(2) + ", s2c: " + s2c.toFixed(2) + ", diff: " + this.diff.toFixed(2) + ", now: " + this.now().toFixed(2) + ", server.now: " + this.serverNow().toFixed(2) + ", win.now: " + window.performance.now().toFixed(2));
//console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now());
}
now() {
if (!this.ctx) {
return window.performance.now();
}
else {
if (this.isFirefoxMobile) {
return this.ctx.currentTime * 1000;
}
else {
return (this.ctx.getOutputTimestamp().contextTime || this.ctx.currentTime) * 1000;
}
}
}
nowSec() {
return this.now() / 1000;
}
serverNow() {
return this.serverTime(this.now());
}
serverTime(localTimeMs) {
return localTimeMs + this.diff;
}
}
class SampleFormat {
constructor() {
this.rate = 48000;
this.channels = 2;
this.bits = 16;
}
msRate() {
return this.rate / 1000;
}
toString() {
return this.rate + ":" + this.bits + ":" + this.channels;
}
sampleSize() {
if (this.bits == 24) {
return 4;
}
return this.bits / 8;
}
frameSize() {
return this.channels * this.sampleSize();
}
durationMs(bytes) {
return (bytes / this.frameSize()) * this.msRate();
}
}
class Decoder {
setHeader(buffer) {
return new SampleFormat();
}
decode(chunk) {
return null;
}
}
class OpusDecoder extends Decoder {
constructor() {
super();
}
setHeader(buffer) {
let view = new DataView(buffer);
let ID_OPUS = 0x4F505553;
if (buffer.byteLength < 12) {
console.error("Opus header too small: " + buffer.byteLength);
return null;
}
else if (view.getUint32(0, true) != ID_OPUS) {
console.error("Opus header too small: " + buffer.byteLength);
return null;
}
let format = new SampleFormat();
format.rate = view.getUint32(4, true);
format.bits = view.getUint16(8, true);
format.channels = view.getUint16(10, true);
//console.log("Opus samplerate: " + format.toString());
return format;
}
decode(chunk) {
return null;
}
}
class FlacDecoder extends Decoder {
constructor() {
super();
this.header = null;
this.cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
this.decoder = Flac.create_libflac_decoder(true);
if (this.decoder) {
let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false);
//console.log("Flac init: " + init_status);
Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true });
}
this.sampleFormat = new SampleFormat();
this.flacChunk = new ArrayBuffer(0);
// this.pcmChunk = new PcmChunkMessage();
// Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals});
// flac_ok &= init_status == 0;
//console.log("flac init : " + flac_ok);//DEBUG
}
decode(chunk) {
//console.log("Flac decode: " + chunk.payload.byteLength);
this.flacChunk = chunk.payload.slice(0);
this.pcmChunk = chunk;
this.pcmChunk.clearPayload();
this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true };
//console.log("Flac len: " + this.flacChunk.byteLength);
while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) {
let state = Flac.FLAC__stream_decoder_get_state(this.decoder);
//console.log("State: " + state);
}
//console.log("Pcm payload: " + this.pcmChunk!.payloadSize());
if (this.cacheInfo.cachedBlocks > 0) {
let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate();
//console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms");
this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs);
}
return this.pcmChunk;
}
read_callback_fn(bufferSize) {
//console.log(' decode read callback, buffer bytes max=', bufferSize);
if (this.header) {
//console.log(" header: " + this.header.byteLength);
let data = new Uint8Array(this.header);
this.header = null;
return { buffer: data, readDataLength: data.byteLength, error: false };
}
else if (this.flacChunk) {
//console.log(" flacChunk: " + this.flacChunk.byteLength);
// a fresh read => next call to write will not be from cached data
this.cacheInfo.isCachedChunk = false;
let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength)));
this.flacChunk = this.flacChunk.slice(data.byteLength);
return { buffer: data, readDataLength: data.byteLength, error: false };
}
return { buffer: new Uint8Array(0), readDataLength: 0, error: false };
}
write_callback_fn(data, frameInfo) {
//console.log(" write frame metadata: " + frameInfo + ", len: " + data.length);
if (this.cacheInfo.isCachedChunk) {
// there was no call to read, so it's some cached data
this.cacheInfo.cachedBlocks += frameInfo.blocksize;
}
let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize);
let view = new DataView(payload);
for (let channel = 0; channel < frameInfo.channels; ++channel) {
let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength);
//console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize);
for (let i = 0; i < frameInfo.blocksize; ++i) {
view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true);
}
}
this.pcmChunk.addPayload(payload);
//console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize());
}
/** @memberOf decode */
metadata_callback_fn(data) {
//console.info('meta data: ', data);
// let view = new DataView(data);
this.sampleFormat.rate = data.sampleRate;
this.sampleFormat.channels = data.channels;
this.sampleFormat.bits = data.bitsPerSample;
//console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString());
}
/** @memberOf decode */
error_callback_fn(err, errMsg) {
console.error('decode error callback', err, errMsg);
}
setHeader(buffer) {
this.header = buffer.slice(0);
Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder);
return this.sampleFormat;
}
}
class PlayBuffer {
constructor(buffer, playTime, source, destination) {
this.num = 0;
this.buffer = buffer;
this.playTime = playTime;
this.source = source;
this.source.buffer = this.buffer;
this.source.connect(destination);
this.onended = (playBuffer) => { };
}
start() {
this.source.onended = (ev) => {
this.onended(this);
};
this.source.start(this.playTime);
}
}
class PcmDecoder extends Decoder {
setHeader(buffer) {
let sampleFormat = new SampleFormat();
let view = new DataView(buffer);
sampleFormat.channels = view.getUint16(22, true);
sampleFormat.rate = view.getUint32(24, true);
sampleFormat.bits = view.getUint16(34, true);
return sampleFormat;
}
decode(chunk) {
return chunk;
}
}
class SnapStream {
constructor(protocol, host, port, name) {
this.name = name;
this.playTime = 0;
this.msgId = 0;
this.bufferDurationMs = 80; // 0;
this.bufferFrameCount = 3844; // 9600; // 2400;//8192;
this.syncHandle = -1;
// ageBuffer: Array<number>;
this.audioBuffers = new Array();
this.freeBuffers = new Array();
// median: number = 0;
this.audioBufferCount = 3;
this.bufferMs = 1000;
this.bufferNum = 0;
this.streamsocket = new WebSocket(protocol + '://' + host + ':' + port + '/stream');
this.streamsocket.binaryType = "arraybuffer";
this.streamsocket.onmessage = (msg) => {
let view = new DataView(msg.data);
let type = view.getUint16(0, true);
if (type == 1) {
let codec = new CodecMessage(msg.data);
//console.log("Codec: " + codec.codec);
if (codec.codec == "flac") {
this.decoder = new FlacDecoder();
}
else if (codec.codec == "pcm") {
this.decoder = new PcmDecoder();
}
else if (codec.codec == "opus") {
this.decoder = new OpusDecoder();
alert("Codec not supported: " + codec.codec);
}
else {
alert("Codec not supported: " + codec.codec);
}
if (this.decoder) {
this.sampleFormat = this.decoder.setHeader(codec.payload);
console.log("Sampleformat: " + this.sampleFormat.toString());
if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) {
alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString());
}
else {
if (this.bufferDurationMs != 0) {
this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate());
}
this.stopAudio();
this.ctx = new AudioContext({ latencyHint: "playback", sampleRate: this.sampleFormat.rate });
this.timeProvider.setAudioContext(this.ctx);
this.gainNode = this.ctx.createGain();
this.gainNode.connect(this.ctx.destination);
this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
// this.timeProvider = new TimeProvider(this.ctx);
this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs);
//console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency);
this.play();
}
}
}
else if (type == 2) {
let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat);
if (this.decoder) {
let decoded = this.decoder.decode(pcmChunk);
if (decoded) {
this.stream.addChunk(decoded);
}
}
}
else if (type == 3) {
this.serverSettings = new ServerSettingsMessage(msg.data);
if (this.gainNode) {
this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
}
this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency;
//console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted);
}
else if (type == 4) {
if (this.timeProvider) {
let time = new TimeMessage(msg.data);
this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds());
}
//console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff);
}
else {
console.info("Message not handled, type: " + type);
}
};
this.streamsocket.onopen = (e) => {
console.log('SnapStream socket opened', e);
let hello = new HelloMessage();
hello.mac = "00:00:00:00:00:00";
hello.arch = "web";
hello.os = navigator.platform;
hello.hostname = this.name;
hello.uniqueId = getCookie("uniqueId", uuidv4());
this.sendMessage(hello);
this.syncTime();
this.syncHandle = window.setInterval(() => this.syncTime(), 1000);
};
this.streamsocket.onerror = (e) => {
console.error('SnapStream socket error', e);
};
this.streamsocket.onclose = (e) => {
console.log('SnapStream socket closed', e);
this.stop();
};
// this.ageBuffer = new Array<number>();
this.timeProvider = new TimeProvider();
}
sendMessage(msg) {
msg.sent = new Tv(0, 0);
msg.sent.setMilliseconds(this.timeProvider.now());
msg.id = ++this.msgId;
if (this.streamsocket.readyState != this.streamsocket.OPEN) {
this.stop();
}
else {
this.streamsocket.send(msg.serialize());
}
}
syncTime() {
let t = new TimeMessage();
t.latency.setMilliseconds(this.timeProvider.now());
this.sendMessage(t);
//console.log("prepareSource median: " + Math.round(this.median * 10) / 10);
}
stopAudio() {
if (this.ctx && this.ctx.state !== 'closed') {
this.ctx.close();
}
while (this.audioBuffers.length > 0) {
let buffer = this.audioBuffers.pop();
buffer.onended = (playBuffer) => { };
buffer.source.stop();
}
while (this.freeBuffers.length > 0) {
this.freeBuffers.pop();
}
}
stop() {
window.clearInterval(this.syncHandle);
this.stopAudio();
if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) {
this.streamsocket.close();
}
}
play() {
this.playTime = this.timeProvider.nowSec() + 0.1;
for (let i = 1; i <= this.audioBufferCount; ++i) {
this.playNext();
}
}
playNext() {
let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate);
let playTimeMs = (this.playTime + this.ctx.baseLatency) * 1000 - this.bufferMs;
this.stream.getNextBuffer(buffer, playTimeMs);
let source = this.ctx.createBufferSource();
let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode);
this.audioBuffers.push(playBuffer);
playBuffer.num = ++this.bufferNum;
playBuffer.onended = (buffer) => {
let diff = this.timeProvider.nowSec() - buffer.playTime;
this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer);
// console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length);
this.playNext();
};
playBuffer.start();
this.playTime += this.bufferFrameCount / this.sampleFormat.rate;
}
}
export {
SnapStream,
}
export default {
SnapStream,
};

1044
src/js/components/SnapStream.tsx Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as coreActions from '../services/core/actions';
import { SnapStream } from './SnapStream';
import { SnapStream } from './SnapStream.tsx';
class Stream extends React.Component {
constructor(props) {
@ -19,14 +18,13 @@ class Stream extends React.Component {
host,
port,
ssl,
username,
} = this.props;
if (this.snapstream) {
this.snapstream.play();
} else {
const protocol = (ssl ? 'wss' : 'ws');
this.snapstream = new SnapStream(protocol, host, port, 'Iris');
const baseUrl = `${ssl ? 'wss' : 'ws'}://${host}:${port}`;
this.snapstream = new SnapStream(baseUrl);
}
}

View File

@ -1,5 +1,4 @@
const isDev = process.env.NODE_ENV !== "production";
const isDev = process.env.NODE_ENV !== 'production';
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@ -24,7 +23,7 @@ const config = {
]
},
{
test: /\.js$/,
test: /.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [
{