scrap 3las

This commit is contained in:
2026-04-02 23:29:18 +02:00
parent 9e7ef5d96c
commit 20f5090be0
12 changed files with 112 additions and 1558 deletions
+1 -11
View File
@@ -23,17 +23,7 @@
<meta property="og:image" content="https://fmdx.org/img/webserver_icon.png">
<meta property="og:description" content="Server description: <%= tunerDescMeta %>.">
<!-- 3LAS Scripts for Audio streaming -->
<script src="js/3las/util/3las.helpers.js"></script>
<script src="js/3las/util/3las.logging.js"></script>
<script src="js/3las/fallback/3las.liveaudioplayer.js"></script>
<script src="js/3las/fallback/3las.formatreader.js"></script>
<script src="js/3las/fallback/formats/3las.formatreader.mpeg.js"></script>
<script src="js/3las/fallback/formats/3las.formatreader.wav.js"></script>
<script src="js/3las/util/3las.websocketclient.js"></script>
<script src="js/3las/fallback/3las.fallback.js"></script>
<script src="js/3las/3las.js"></script>
<script src="js/3las/main.js"></script>
<script src="js/audio.js"></script>
<script type="text/javascript">
window.addEventListener('load', Init, false);
document.ontouchmove = function(e) {
-181
View File
@@ -1,181 +0,0 @@
var audioStreamRestartInterval;
var elapsedTimeConnectionWatchdog;
var _3LAS_Settings = /** @class */ (function () {
function _3LAS_Settings() {
this.SocketHost = location.hostname ? location.hostname : "127.0.0.1";
this.SocketPort = location.port;
this.SocketPath = "/";
this.Fallback = new Fallback_Settings();
}
return _3LAS_Settings;
}());
var _3LAS = /** @class */ (function () {
function _3LAS(logger, settings) {
this.Logger = logger;
if (!this.Logger) {
this.Logger = new Logging(null, null);
}
this.Settings = settings;
try {
this.Fallback = new Fallback(this.Logger, this.Settings.Fallback);
this.Fallback.ActivityCallback = this.OnActivity.bind(this);
}
catch (_b) {
this.Fallback = null;
}
if (this.Fallback == null) {
this.Logger.Log('3LAS: Browser does not support either media handling methods.');
throw new Error();
}
if (isAndroid) {
this.WakeLock = new WakeLock(this.Logger);
}
}
Object.defineProperty(_3LAS.prototype, "Volume", {
get: function () {
return this.Fallback.Volume;
},
set: function (value) {
this.Fallback.Volume = value;
},
enumerable: false,
configurable: true
});
_3LAS.prototype.CanChangeVolume = function () {
return true;
};
_3LAS.prototype.Start = function () {
this.ConnectivityFlag = false;
this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug
// Restart audio stream if radio data connection was reestablished
// to prevent stuttering audio in some cases
if (audioStreamRestartInterval) {
clearInterval(audioStreamRestartInterval);
}
audioStreamRestartInterval = setInterval(() => {
if (requiresAudioStreamRestart) {
requiresAudioStreamRestart = false;
if (Stream) {
this.Stop();
this.Start();
console.log("Audio stream restarted after radio data loss.");
}
}
}, 3000);
// Stream connection watchdog monitors mp3 frames
console.log("Stream connection watchdog active.");
let intervalReconnectWatchdog = setInterval(() => {
if (Stream) {
let endTimeConnectionWatchdog = performance.now();
elapsedTimeConnectionWatchdog = endTimeConnectionWatchdog - window.startTimeConnectionWatchdog;
//console.log(`Stream frame elapsed time: ${parseInt(elapsedTimeConnectionWatchdog)} ms`);
if (elapsedTimeConnectionWatchdog > 5000 && shouldReconnect) {
clearInterval(intervalReconnectWatchdog);
setTimeout(() => {
clearInterval(intervalReconnectWatchdog);
console.log("Unstable internet connection detected, reconnecting... (" + parseInt(elapsedTimeConnectionWatchdog) + " ms)");
this.Stop();
this.Start();
}, 2000);
}
} else {
clearInterval(intervalReconnectWatchdog);
this.Stop();
console.log("Stream connection watchdog inactive.");
}
}, 3000);
// This is stupid, but required for Android.... thanks Google :(
if (this.WakeLock)
this.WakeLock.Begin();
try {
if (window.location.protocol === 'https:') {
this.WebSocket = new WebSocketClient(this.Logger, 'wss://' + this.Settings.SocketHost + ':' + this.Settings.SocketPort.toString() + window.location.pathname + 'audio' , this.OnSocketError.bind(this), this.OnSocketConnect.bind(this), this.OnSocketDataReady.bind(this), this.OnSocketDisconnect.bind(this));
}
else {
this.WebSocket = new WebSocketClient(this.Logger, 'ws://' + this.Settings.SocketHost + ':' + this.Settings.SocketPort.toString() + window.location.pathname + 'audio' , this.OnSocketError.bind(this), this.OnSocketConnect.bind(this), this.OnSocketDataReady.bind(this), this.OnSocketDisconnect.bind(this));
}
this.Logger.Log("Init of WebSocketClient succeeded");
this.Logger.Log("Trying to connect to server.");
}
catch (e) {
this.Logger.Log("Init of WebSocketClient failed: " + e);
throw new Error();
}
};
_3LAS.prototype.Stop = function () {
try {
// Close WebSocket connection
if (this.WebSocket) {
this.WebSocket.Close();
this.WebSocket.OnClose();
this.WebSocket = null;
this.Logger.Log("WebSocket connection closed.");
}
// Stop WakeLock if it exists and is an Android device
if (isAndroid && this.WakeLock) {
this.WakeLock.End();
this.Logger.Log("WakeLock stopped.");
}
// Reset Fallback if it exists
if (this.Fallback) {
this.Fallback.OnSocketDisconnect();
this.Fallback.Stop();
this.Fallback.Reset();
this.Logger.Log("Fallback reset.");
}
// Reset connectivity flag
if (this.ConnectivityFlag) {
this.ConnectivityFlag = null;
if (this.ConnectivityCallback) {
this.ConnectivityCallback(null);
}
}
this.Logger.Log("3LAS stopped successfully.");
} catch (e) {
this.Logger.Log("Error while stopping 3LAS: " + e);
}
};
_3LAS.prototype.OnActivity = function () {
if (this.ActivityCallback)
this.ActivityCallback();
if (!this.ConnectivityFlag) {
this.ConnectivityFlag = true;
if (this.ConnectivityCallback)
this.ConnectivityCallback(true);
}
};
// Callback function from socket connection
_3LAS.prototype.OnSocketError = function (message) {
this.Logger.Log("Network error: " + message);
this.Fallback.OnSocketError(message);
};
_3LAS.prototype.OnSocketConnect = function () {
this.Logger.Log("Established connection with server.");
this.Fallback.OnSocketConnect();
this.Fallback.Init(this.WebSocket);
};
_3LAS.prototype.OnSocketDisconnect = function () {
this.Logger.Log("Lost connection to server.");
this.Fallback.OnSocketDisconnect();
this.Fallback.Reset();
if (this.ConnectivityFlag) {
this.ConnectivityFlag = false;
if (this.ConnectivityCallback)
this.ConnectivityCallback(false);
}
};
_3LAS.prototype.OnSocketDataReady = function (data) {
this.Fallback.OnSocketDataReady(data);
};
return _3LAS;
}());
-167
View File
@@ -1,167 +0,0 @@
/*
Socket fallback is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var Fallback_Settings = /** @class */ (function () {
function Fallback_Settings() {
this.Formats = [
{ "Mime": "audio/mpeg", "Name": "mp3" },
{ "Mime": "audio/wave", "Name": "wav" }
];
this.MaxVolume = 1.0;
this.AutoCorrectSpeed = false;
this.InitialBufferLength = 1.0 / 2.5;
}
return Fallback_Settings;
}());
var Fallback = /** @class */ (function () {
function Fallback(logger, settings) {
this.Logger = logger;
if (!this.Logger) {
this.Logger = new Logging(null, null);
}
// Create audio context
if (typeof AudioContext !== "undefined")
this.Audio = new AudioContext();
else if (typeof webkitAudioContext !== "undefined")
this.Audio = new webkitAudioContext();
else if (typeof mozAudioContext !== "undefined")
this.Audio = new mozAudioContext();
else {
this.Logger.Log('3LAS: Browser does not support "AudioContext".');
throw new Error();
}
this.Settings = settings;
this.Logger.Log("Detected: " +
(OSName == "MacOSX" ? "Mac OSX" : (OSName == "Unknown" ? "Unknown OS" : OSName)) + ", " +
(BrowserName == "IE" ? "Internet Explorer" : (BrowserName == "NativeChrome" ? "Chrome legacy" : (BrowserName == "Unknown" ? "Unknown Browser" : BrowserName))));
this.SelectedFormatMime = "";
this.SelectedFormatName = "";
for (var i = 0; i < this.Settings.Formats.length; i++) {
if (!AudioFormatReader.CanDecodeTypes([this.Settings.Formats[i].Mime]))
continue;
this.SelectedFormatMime = this.Settings.Formats[i].Mime;
this.SelectedFormatName = this.Settings.Formats[i].Name;
break;
}
if (this.SelectedFormatMime == "" || this.SelectedFormatName == "") {
this.Logger.Log("None of the available MIME types are supported.");
throw new Error();
}
this.Logger.Log("Using websocket fallback with MIME: " + this.SelectedFormatMime);
try {
this.Player = new LiveAudioPlayer(this.Audio, this.Logger, this.Settings.MaxVolume, this.Settings.InitialBufferLength, this.Settings.AutoCorrectSpeed);
this.Logger.Log("Init of LiveAudioPlayer succeeded");
}
catch (e) {
this.Logger.Log("Init of LiveAudioPlayer failed: " + e);
throw new Error();
}
try {
this.FormatReader = AudioFormatReader.Create(this.SelectedFormatMime, this.Audio, this.Logger, this.OnReaderError.bind(this), this.Player.CheckBeforeDecode, this.OnReaderDataReady.bind(this), AudioFormatReader.DefaultSettings());
this.Logger.Log("Init of AudioFormatReader succeeded");
}
catch (e) {
this.Logger.Log("Init of AudioFormatReader failed: " + e);
throw new Error();
}
this.PacketModCounter = 0;
this.LastCheckTime = 0;
this.FocusChecker = 0;
}
Fallback.prototype.Init = function (webSocket) {
this.MobileUnmute();
this.WebSocket = webSocket;
this.WebSocket?.Send(JSON.stringify({
"type": "fallback",
"data": this.SelectedFormatName
}));
this.StartFocusChecker();
};
Fallback.prototype.MobileUnmute = function () {
var amplification = this.Audio.createGain();
// Set volume to max
amplification.gain.value = 1.0;
// Connect gain node to context
amplification.connect(this.Audio.destination);
// Create one second buffer with silence
var audioBuffer = this.Audio.createBuffer(2, this.Audio.sampleRate, this.Audio.sampleRate);
// Create new audio source for the buffer
var sourceNode = this.Audio.createBufferSource();
// Make sure the node deletes itself after playback
sourceNode.onended = function (_ev) {
sourceNode.disconnect();
amplification.disconnect();
};
// Pass audio data to source
sourceNode.buffer = audioBuffer;
// Connect the source to the gain node
sourceNode.connect(amplification);
// Play source
sourceNode.start();
};
Object.defineProperty(Fallback.prototype, "Volume", {
get: function () {
return this.Player.Volume / this.Settings.MaxVolume;
},
set: function (value) {
this.Player.Volume = value * this.Settings.MaxVolume;
},
enumerable: false,
configurable: true
});
// Callback functions from format reader
Fallback.prototype.OnReaderError = function () {
this.Logger.Log("Reader error: Decoding failed.");
};
Fallback.prototype.OnReaderDataReady = function () {
while (this.FormatReader.SamplesAvailable()) {
this.Player.PushBuffer(this.FormatReader.PopSamples());
}
};
// Callback function from socket connection
Fallback.prototype.OnSocketError = function (message) {
};
Fallback.prototype.OnSocketConnect = function () {
};
Fallback.prototype.OnSocketDisconnect = function () {
};
Fallback.prototype.OnSocketDataReady = function (data) {
this.PacketModCounter++;
if (this.PacketModCounter > 100) {
if (this.ActivityCallback)
this.ActivityCallback();
this.PacketModCounter = 0;
}
this.FormatReader.PushData(new Uint8Array(data));
};
Fallback.prototype.StartFocusChecker = function () {
if (!this.FocusChecker) {
this.LastCheckTime = Date.now();
this.FocusChecker = window.setInterval(this.CheckFocus.bind(this), 2000);
}
};
Fallback.prototype.StopFocusChecker = function () {
if (this.FocusChecker) {
window.clearInterval(this.FocusChecker);
this.FocusChecker = 0;
}
};
Fallback.prototype.CheckFocus = function () {
var checkTime = Date.now();
// Check if focus was lost
if (checkTime - this.LastCheckTime > 10000) {
// If so, drop all samples in the buffer
this.Logger.Log("Focus lost, purging format reader.");
this.FormatReader.PurgeData();
}
this.LastCheckTime = checkTime;
};
Fallback.prototype.Reset = function () {
this.StopFocusChecker();
this.FormatReader.Reset();
this.Player.Reset();
this.WebSocket = null;
};
return Fallback;
}());
-193
View File
@@ -1,193 +0,0 @@
/*
Audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var AudioFormatReader = /** @class */ (function () {
function AudioFormatReader(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) {
if (!audio)
throw new Error('AudioFormatReader: audio must be specified');
// Check callback argument
if (typeof errorCallback !== 'function')
throw new Error('AudioFormatReader: errorCallback must be specified');
if (typeof beforeDecodeCheck !== 'function')
throw new Error('AudioFormatReader: beforeDecodeCheck must be specified');
if (typeof dataReadyCallback !== 'function')
throw new Error('AudioFormatReader: dataReadyCallback must be specified');
this.Audio = audio;
this.Logger = logger;
this.ErrorCallback = errorCallback;
this.BeforeDecodeCheck = beforeDecodeCheck;
this.DataReadyCallback = dataReadyCallback;
this.Id = 0;
this.LastPushedId = -1;
this.Samples = new Array();
this.BufferStore = {};
this.DataBuffer = new Uint8Array(0);
}
// Pushes frame data into the buffer
AudioFormatReader.prototype.PushData = function (data) {
// Append data to framedata buffer
this.DataBuffer = this.ConcatUint8Array(this.DataBuffer, data);
// Try to extract frames
this.ExtractAll();
};
// Check if samples are available
AudioFormatReader.prototype.SamplesAvailable = function () {
return (this.Samples.length > 0);
};
// Get a single bunch of sampels from the reader
AudioFormatReader.prototype.PopSamples = function () {
if (this.Samples.length > 0) {
// Get first bunch of samples, remove said bunch from the array and hand it back to callee
return this.Samples.shift();
}
else
return null;
};
// Deletes all encoded and decoded data from the reader (does not effect headers, etc.)
AudioFormatReader.prototype.PurgeData = function () {
this.Id = 0;
this.LastPushedId = -1;
this.Samples = new Array();
this.BufferStore = {};
this.DataBuffer = new Uint8Array(0);
};
// Used to force frame extraction externaly
AudioFormatReader.prototype.Poke = function () {
this.ExtractAll();
};
// Deletes all data from the reader (does effect headers, etc.)
AudioFormatReader.prototype.Reset = function () {
this.PurgeData();
};
// Extracts and converts the raw data
AudioFormatReader.prototype.ExtractAll = function () {
};
// Checks if a decode makes sense
AudioFormatReader.prototype.OnBeforeDecode = function (id, duration) {
return true;
//TODO Fix this
/*
if(this.BeforeDecodeCheck(duration)) {
return true;
}
else {
this.OnDataReady(id, this.Audio.createBuffer(1, Math.ceil(duration * this.Audio.sampleRate), this.Audio.sampleRate));
return false;
}
*/
};
// Stores the converted bnuches of samples in right order
AudioFormatReader.prototype.OnDataReady = function (id, audioBuffer) {
if (this.LastPushedId + 1 == id) {
// Push samples into array
this.Samples.push(audioBuffer);
this.LastPushedId++;
while (this.BufferStore[this.LastPushedId + 1]) {
// Push samples we decoded earlier in correct order
this.Samples.push(this.BufferStore[this.LastPushedId + 1]);
delete this.BufferStore[this.LastPushedId + 1];
this.LastPushedId++;
}
// Callback to tell that data is ready
this.DataReadyCallback();
}
else {
// Is out of order, will be pushed later
this.BufferStore[id] = audioBuffer;
}
};
// Used to concatenate two Uint8Array (b comes BEHIND a)
AudioFormatReader.prototype.ConcatUint8Array = function (a, b) {
var tmp = new Uint8Array(a.length + b.length);
tmp.set(a, 0);
tmp.set(b, a.length);
return tmp;
};
AudioFormatReader.CanDecodeTypes = function (mimeTypes) {
var audioTag = new Audio();
var result = false;
for (var i = 0; i < mimeTypes.length; i++) {
var mimeType = mimeTypes[i];
var answer = audioTag.canPlayType(mimeType);
if (answer != "probably" && answer != "maybe")
continue;
result = true;
break;
}
audioTag = null;
return result;
};
AudioFormatReader.DefaultSettings = function () {
var settings = {};
// WAV
settings["wav"] = {};
// Duration of wave samples to decode together
settings["wav"]["BatchDuration"] = 1 / 10; // 0.1 seconds
/*
if (isAndroid && isNativeChrome)
settings["wav"]["BatchDuration"] = 96 / 375;
else if (isAndroid && isFirefox)
settings["wav"]["BatchDuration"] = 96 / 375;
else
settings["wav"]["BatchDuration"] = 16 / 375;
*/
// Duration of addtional samples to decode to account for edge effects
settings["wav"]["ExtraEdgeDuration"] = 1 / 300; // 0.00333... seconds
/*
if (isAndroid && isNativeChrome)
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
else if (isAndroid && isFirefox)
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
else
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
*/
// MPEG
settings["mpeg"] = {};
// Adds a minimal ID3v2 tag before decoding frames.
settings["mpeg"]["AddID3Tag"] = false;
// Minimum number of frames to decode together
// Theoretical minimum is 2.
// Recommended value is 3 or higher.
if (isAndroid)
settings["mpeg"]["MinDecodeFrames"] = 3;
else
settings["mpeg"]["MinDecodeFrames"] = 3;
return settings;
};
AudioFormatReader.Create = function (mime, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings) {
if (settings === void 0) { settings = null; }
if (typeof mime !== "string")
throw new Error('CreateAudioFormatReader: Invalid MIME-Type, must be string');
if (!settings)
settings = this.DefaultSettings();
var fullMime = mime;
if (mime.indexOf("audio/pcm") == 0)
mime = "audio/pcm";
// Load format handler according to MIME-Type
switch (mime.replace(/\s/g, "")) {
// MPEG Audio (mp3)
case "audio/mpeg":
case "audio/MPA":
case "audio/mpa-robust":
if (!AudioFormatReader.CanDecodeTypes(new Array("audio/mpeg", "audio/MPA", "audio/mpa-robust")))
throw new Error('CreateAudioFormatReader: Browser can not decode specified MIME-Type (' + mime + ')');
return new AudioFormatReader_MPEG(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings["mpeg"]["AddID3Tag"], settings["mpeg"]["MinDecodeFrames"]);
break;
// Waveform Audio File Format
case "audio/vnd.wave":
case "audio/wav":
case "audio/wave":
case "audio/x-wav":
if (!AudioFormatReader.CanDecodeTypes(new Array("audio/wav", "audio/wave")))
throw new Error('CreateAudioFormatReader: Browser can not decode specified MIME-Type (' + mime + ')');
return new AudioFormatReader_WAV(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings["wav"]["BatchDuration"], settings["wav"]["ExtraEdgeDuration"]);
break;
// Unknown codec
default:
throw new Error('CreateAudioFormatReader: Specified MIME-Type (' + mime + ') not supported');
break;
}
};
return AudioFormatReader;
}());
@@ -1,146 +0,0 @@
/*
Live audio player is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var LiveAudioPlayer = /** @class */ (function () {
function LiveAudioPlayer(audio, logger, maxVolume, startOffset, variableSpeed) {
if (maxVolume === void 0) { maxVolume = 1.0; }
if (startOffset === void 0) { startOffset = 0.33; }
if (variableSpeed === void 0) { variableSpeed = false; }
this.Audio = audio;
this.Logger = logger;
this.MaxVolume = maxVolume;
this.StartOffset = startOffset;
this.VariableSpeed = variableSpeed;
this.OffsetMin = this.StartOffset - LiveAudioPlayer.OffsetVariance;
this.OffsetMax = this.StartOffset + LiveAudioPlayer.OffsetVariance;
// Set speed to default
this.PlaybackSpeed = 1.0;
// Reset variable for scheduling times
this.NextScheduleTime = 0.0;
// Create gain node for volume control
this.Amplification = this.Audio.createGain();
// Set volume to max
this.Amplification.gain.value = 1.0;
// Connect gain node to context
this.Amplification.connect(this.Audio.destination);
}
Object.defineProperty(LiveAudioPlayer.prototype, "Volume", {
get: function () {
// Get volume from gain node
return this.Amplification.gain.value;
},
set: function (value) {
// Clamp value to [1e-20 ; MaxVolume]
if (value > 1.0)
value = this.MaxVolume;
else if (value <= 0.0)
value = 1e-20;
// Cancel any scheduled ramps
this.Amplification.gain.cancelScheduledValues(this.Audio.currentTime);
// Change volume following a ramp (more userfriendly)
this.Amplification.gain.exponentialRampToValueAtTime(value, this.Audio.currentTime + 0.5);
},
enumerable: false,
configurable: true
});
// Recieves an audiobuffer and schedules it for seamless playback
LiveAudioPlayer.prototype.PushBuffer = function (buffer) {
// Check if this is the first buffer we received
if (this.NextScheduleTime == 0.0) {
// Start playing [StartOffset] s from now
this.NextScheduleTime = this.Audio.currentTime + this.StartOffset;
}
var duration;
if (this.VariableSpeed)
duration = buffer.duration / this.PlaybackSpeed; // Use regular duration
else
duration = buffer.duration; // Use duration adjusted for playback speed
// Before creating a buffer and scheduling playback, check if playing this buffer makes sense at all
// If a buffer should have been started so far in the past that it would have finished playing by now, we are better of skipping it.
// But we still need to move the time forward to keep future timings right.
if (this.NextScheduleTime + duration > this.Audio.currentTime) {
var skipDurationTime = void 0;
// If the playback start time is in the past but the playback end time is in the future, we need to partially play the buffer.
if (this.Audio.currentTime >= this.NextScheduleTime) {
// Calculate the time we need to skip
skipDurationTime = this.Audio.currentTime - this.NextScheduleTime + 0.05;
}
else {
// No skipping needed
skipDurationTime = 0.0;
}
// Check if we'd skip the whole buffer anyway
if (skipDurationTime < duration) {
// Create new audio source for the buffer
var sourceNode_1 = this.Audio.createBufferSource();
// Make sure the node deletes itself after playback
sourceNode_1.onended = function (_ev) {
sourceNode_1.disconnect();
};
// Prevent looping (the standard says that it should be off by default)
sourceNode_1.loop = false;
// Pass audio data to source
sourceNode_1.buffer = buffer;
//Connect the source to the gain node
sourceNode_1.connect(this.Amplification);
if (this.VariableSpeed) {
var scheduleOffset = this.NextScheduleTime - this.Audio.currentTime;
// Check if we are to far or too close to target schedule time
if (this.NextScheduleTime - this.Audio.currentTime > this.OffsetMax) {
if (this.PlaybackSpeed < 1.0 + LiveAudioPlayer.SpeedCorrectionFactor) {
// We are too slow, speed up playback (somewhat noticeable)
this.Logger.Log("Buffer size too large, speeding up playback.");
this.PlaybackSpeed = 1.0 + LiveAudioPlayer.SpeedCorrectionFactor;
duration = buffer.duration / this.PlaybackSpeed;
}
}
else if (this.NextScheduleTime - this.Audio.currentTime < this.OffsetMin) {
if (this.PlaybackSpeed > 1.0 - LiveAudioPlayer.SpeedCorrectionFactor) {
// We are too fast, slow down playback (somewhat noticeable)
this.Logger.Log("Buffer size too small, slowing down playback.");
this.PlaybackSpeed = 1.0 - LiveAudioPlayer.SpeedCorrectionFactor;
duration = buffer.duration / this.PlaybackSpeed;
}
}
else {
// Check if we are in time
if ((this.PlaybackSpeed > 1.0 && (this.NextScheduleTime - this.Audio.currentTime < this.StartOffset)) ||
(this.PlaybackSpeed < 1.0 && (this.NextScheduleTime - this.Audio.currentTime > this.StartOffset))) {
// We are within our min/max offset, set playpacks to default
this.Logger.Log("Buffer size within limits, using normal playback speed.");
this.PlaybackSpeed = 1.0;
duration = buffer.duration;
}
}
// Set playback speed
sourceNode_1.playbackRate.value = this.PlaybackSpeed;
}
// Schedule playback
sourceNode_1.start(this.NextScheduleTime + skipDurationTime, skipDurationTime);
}
else {
this.Logger.Log("Skipped buffer because it became too old.");
}
}
else {
this.Logger.Log("Skipped buffer because it was too old.");
}
// Move time forward
this.NextScheduleTime += duration;
};
LiveAudioPlayer.prototype.Reset = function () {
this.NextScheduleTime = 0.0;
};
LiveAudioPlayer.prototype.CheckBeforeDecode = function (playbackLength) {
if (this.NextScheduleTime == 0)
return true;
return this.NextScheduleTime + playbackLength > this.Audio.currentTime;
};
// Crystal oscillator have a variance of about +/- 20ppm
// So worst case would be a difference of 40ppm between two oscillators.
LiveAudioPlayer.SpeedCorrectionFactor = 40 / 1.0e6;
// Hystersis value for speed up/down trigger
LiveAudioPlayer.OffsetVariance = 0.2;
return LiveAudioPlayer;
}());
@@ -1,319 +0,0 @@
/*
MPEG audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
window.startTimeConnectionWatchdog = performance.now();
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var MPEGFrameInfo = /** @class */ (function () {
function MPEGFrameInfo(data, sampleCount, sampleRate) {
this.Data = data;
this.SampleCount = sampleCount;
this.SampleRate = sampleRate;
}
return MPEGFrameInfo;
}());
var AudioFormatReader_MPEG = /** @class */ (function (_super) {
__extends(AudioFormatReader_MPEG, _super);
function AudioFormatReader_MPEG(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, addId3Tag, minDecodeFrames) {
var _this = _super.call(this, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) || this;
_this._OnDecodeSuccess = _this.OnDecodeSuccess.bind(_this);
_this._OnDecodeError = _this.OnDecodeError.bind(_this);
_this.AddId3Tag = addId3Tag;
_this.MinDecodeFrames = minDecodeFrames;
_this.Frames = new Array();
_this.FrameStartIdx = -1;
_this.FrameEndIdx = -1;
_this.FrameSamples = 0;
_this.FrameSampleRate = 0;
_this.TimeBudget = 0;
return _this;
}
// Deletes all frames from the databuffer and framearray and all samples from the samplearray
AudioFormatReader_MPEG.prototype.PurgeData = function () {
_super.prototype.PurgeData.call(this);
this.Frames = new Array();
this.FrameStartIdx = -1;
this.FrameEndIdx = -1;
this.FrameSamples = 0;
this.FrameSampleRate = 0;
this.TimeBudget = 0;
};
// Extracts all currently possible frames
AudioFormatReader_MPEG.prototype.ExtractAll = function () {
// Look for frames
this.FindFrame();
// Repeat as long as we can extract frames
while (this.CanExtractFrame()) {
// Extract frame and push into array
this.Frames.push(this.ExtractFrame());
// Look for frames
this.FindFrame();
}
// Check if we have enough frames to decode
if (this.Frames.length >= this.MinDecodeFrames) {
// Note:
// =====
// mp3 frames have an overlap of [granule size] so we can't use the first or last [granule size] samples.
// [granule size] is equal to half of a [frame size] in samples (using the mp3's sample rate).
// Sum up the playback time of each decoded frame and data buffer lengths
// Note: Since mp3-Frames overlap by half of their sample-length we expect the
// first and last frame to be only half as long. Some decoders will still output
// the full frame length by adding zeros.
var bufferLength = 0;
var expectedTotalPlayTime_1 = 0;
var firstGranulePlayTime_1 = 0;
var lastGranulePlayTime_1 = 0;
firstGranulePlayTime_1 = this.Frames[0].SampleCount / this.Frames[0].SampleRate / 2.0; // Only half of data is usable due to overlap
expectedTotalPlayTime_1 += firstGranulePlayTime_1;
bufferLength += this.Frames[0].Data.length;
for (var i = 1; i < this.Frames.length - 1; i++) {
expectedTotalPlayTime_1 += this.Frames[i].SampleCount / this.Frames[i].SampleRate;
bufferLength += this.Frames[i].Data.length;
}
lastGranulePlayTime_1 = this.Frames[this.Frames.length - 1].SampleCount / this.Frames[this.Frames.length - 1].SampleRate / 2.0; // Only half of data is usable due to overlap
expectedTotalPlayTime_1 += lastGranulePlayTime_1;
bufferLength += this.Frames[this.Frames.length - 1].Data.length;
// If needed, add some space for the ID3v2 tag
if (this.AddId3Tag) {
bufferLength += AudioFormatReader_MPEG.Id3v2Tag.length;
}
// Create a buffer long enough to hold everything
var decodeBuffer = new Uint8Array(bufferLength);
var offset = 0;
// If needed, add ID3v2 tag to beginning of buffer
if (this.AddId3Tag) {
decodeBuffer.set(AudioFormatReader_MPEG.Id3v2Tag, offset);
offset += AudioFormatReader_MPEG.Id3v2Tag.length;
}
// Add the frames to the window
for (var i = 0; i < this.Frames.length; i++) {
decodeBuffer.set(this.Frames[i].Data, offset);
offset += this.Frames[i].Data.length;
}
// Remove the used frames from the array
this.Frames.splice(0, this.Frames.length - 1);
// Increment Id
var id_1 = this.Id++;
// Check if decoded frames might be too far back in the past
if (!this.OnBeforeDecode(id_1, expectedTotalPlayTime_1))
return;
// Push window to the decoder
this.Audio.decodeAudioData(decodeBuffer.buffer, (function (decodedData) {
var _id = id_1;
var _expectedTotalPlayTime = expectedTotalPlayTime_1;
var _firstGranulePlayTime = firstGranulePlayTime_1;
var _lastGranulePlayTime = lastGranulePlayTime_1;
this._OnDecodeSuccess(decodedData, _id, _expectedTotalPlayTime, _firstGranulePlayTime, _lastGranulePlayTime);
}).bind(this), this._OnDecodeError.bind(this));
}
};
// Finds frame boundries within the data buffer
AudioFormatReader_MPEG.prototype.FindFrame = function () {
// Find frame start
if (this.FrameStartIdx < 0) {
var i = 0;
// Make sure we don't exceed array bounds
while ((i + 1) < this.DataBuffer.length) {
// Look for MPEG sync word
if (this.DataBuffer[i] == 0xFF && (this.DataBuffer[i + 1] & 0xE0) == 0xE0) {
// Sync found, set frame start
this.FrameStartIdx = i;
break;
}
i++;
}
}
// Find frame end
if (this.FrameStartIdx >= 0 && this.FrameEndIdx < 0) {
// Check if we have enough data to process the header
if ((this.FrameStartIdx + 2) < this.DataBuffer.length) {
// Get header data
// Version index
var ver = (this.DataBuffer[this.FrameStartIdx + 1] & 0x18) >>> 3;
// Layer index
var lyr = (this.DataBuffer[this.FrameStartIdx + 1] & 0x06) >>> 1;
// Padding? 0/1
var pad = (this.DataBuffer[this.FrameStartIdx + 2] & 0x02) >>> 1;
// Bitrate index
var brx = (this.DataBuffer[this.FrameStartIdx + 2] & 0xF0) >>> 4;
// SampRate index
var srx = (this.DataBuffer[this.FrameStartIdx + 2] & 0x0C) >>> 2;
// Resolve flags to real values
var bitrate = AudioFormatReader_MPEG.MPEG_bitrates[ver][lyr][brx] * 1000;
var samprate = AudioFormatReader_MPEG.MPEG_srates[ver][srx];
var samples = AudioFormatReader_MPEG.MPEG_frame_samples[ver][lyr];
var slot_size = AudioFormatReader_MPEG.MPEG_slot_size[lyr];
// In-between calculations
var bps = samples / 8.0;
var fsize = ((bps * bitrate) / samprate) + ((pad == 1) ? slot_size : 0);
// Truncate to integer
var frameSize = Math.floor(fsize);
// Store number of samples and samplerate for frame
this.FrameSamples = samples;
this.FrameSampleRate = samprate;
// Set end frame boundry
this.FrameEndIdx = this.FrameStartIdx + frameSize;
}
}
};
// Checks if there is a frame ready to be extracted
AudioFormatReader_MPEG.prototype.CanExtractFrame = function () {
if (this.FrameStartIdx < 0 || this.FrameEndIdx < 0)
return false;
else if (this.FrameEndIdx <= this.DataBuffer.length)
return true;
else
return false;
};
// Extract a single frame from the buffer
AudioFormatReader_MPEG.prototype.ExtractFrame = function () {
// Extract frame data from buffer
var frameArray = this.DataBuffer.buffer.slice(this.FrameStartIdx, this.FrameEndIdx);
// Remove frame from buffer
if ((this.FrameEndIdx + 1) < this.DataBuffer.length)
this.DataBuffer = new Uint8Array(this.DataBuffer.buffer.slice(this.FrameEndIdx));
else
this.DataBuffer = new Uint8Array(0);
// Reset Start/End indices
this.FrameStartIdx = 0;
this.FrameEndIdx = -1;
return new MPEGFrameInfo(new Uint8Array(frameArray), this.FrameSamples, this.FrameSampleRate);
};
// Is called if the decoding of the window succeeded
AudioFormatReader_MPEG.prototype.OnDecodeSuccess = function (decodedData, id, expectedTotalPlayTime, firstGranulePlayTime, lastGranulePlayTime) {
window.startTimeConnectionWatchdog = performance.now();
var extractSampleCount;
var extractSampleOffset;
var delta = 0.001;
// Check if we got the expected number of samples
if (expectedTotalPlayTime > decodedData.duration) {
// We got less samples than expect, we suspect that they were truncated equally at start and end.
// This can happen in case of sample rate conversions.
extractSampleCount = decodedData.length;
extractSampleOffset = 0;
this.TimeBudget += (expectedTotalPlayTime - decodedData.duration);
}
else if (expectedTotalPlayTime < decodedData.duration) {
// We got more samples than expect, we suspect that zeros were added equally at start and end.
// This can happen in case of sample rate conversions or edge frame handling.
extractSampleCount = Math.ceil(expectedTotalPlayTime * decodedData.sampleRate);
var budgetSamples = this.TimeBudget * decodedData.sampleRate;
if (budgetSamples > 1.0) {
if (budgetSamples > decodedData.length - extractSampleCount) {
budgetSamples = decodedData.length - extractSampleCount;
}
extractSampleCount += budgetSamples;
this.TimeBudget -= (budgetSamples / decodedData.sampleRate);
}
var diff = decodedData.duration - expectedTotalPlayTime;
if ((diff - firstGranulePlayTime - lastGranulePlayTime) >= -delta) {
// Both first and last granule are present. Cut out middle section.
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
}
if (Math.abs(firstGranulePlayTime - lastGranulePlayTime) <= delta) {
// First and last granule are equal. We need to make an educated guess which one is present.
if (isIOS || isIPadOS || isMacOSX) {
// I don't know why, but Apple does things differently.
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
}
else {
// Assume last granule is present.
extractSampleOffset = decodedData.length - extractSampleCount;
}
}
else if (Math.abs(diff - firstGranulePlayTime) <= delta) {
// Last granule is present.
extractSampleOffset = decodedData.length - extractSampleCount;
}
else if (Math.abs(diff - lastGranulePlayTime) <= delta) {
// First granule is present.
extractSampleOffset = 0;
}
else {
// The difference is not equal to neither the first, the last nor the sum of both granule. So we just use the middle.
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
}
}
else {
// We got the expected number of samples, no adaption needed
extractSampleCount = decodedData.length;
extractSampleOffset = 0;
}
// Create a buffer that can hold the frame to extract
var audioBuffer = this.Audio.createBuffer(decodedData.numberOfChannels, extractSampleCount, decodedData.sampleRate);
// Fill buffer with the last part of the decoded frame leave out last granule
for (var i = 0; i < decodedData.numberOfChannels; i++)
audioBuffer.getChannelData(i).set(decodedData.getChannelData(i).subarray(extractSampleOffset, extractSampleOffset + extractSampleCount));
this.OnDataReady(id, audioBuffer);
};
// Is called in case the decoding of the window fails
AudioFormatReader_MPEG.prototype.OnDecodeError = function (_error) {
this.ErrorCallback();
};
// MPEG versions - use [version]
AudioFormatReader_MPEG.MPEG_versions = new Array(25, 0, 2, 1);
// Layers - use [layer]
AudioFormatReader_MPEG.MPEG_layers = new Array(0, 3, 2, 1);
// Bitrates - use [version][layer][bitrate]
AudioFormatReader_MPEG.MPEG_bitrates = new Array(new Array(// Version 2.5
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1
), new Array(// Reserved
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // Invalid
), new Array(// Version 2
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1
), new Array(// Version 1
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
new Array(0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0), // Layer 3
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0), // Layer 2
new Array(0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0) // Layer 1
));
// Sample rates - use [version][srate]
AudioFormatReader_MPEG.MPEG_srates = new Array(new Array(11025, 12000, 8000, 0), // MPEG 2.5
new Array(0, 0, 0, 0), // Reserved
new Array(22050, 24000, 16000, 0), // MPEG 2
new Array(44100, 48000, 32000, 0) // MPEG 1
);
// Samples per frame - use [version][layer]
AudioFormatReader_MPEG.MPEG_frame_samples = new Array(
// Rsvd 3 2 1 < Layer v Version
new Array(0, 576, 1152, 384), // 2.5
new Array(0, 0, 0, 0), // Reserved
new Array(0, 576, 1152, 384), // 2
new Array(0, 1152, 1152, 384) // 1
);
AudioFormatReader_MPEG.Id3v2Tag = new Uint8Array(new Array(0x49, 0x44, 0x33, // File identifier: "ID3"
0x03, 0x00, // Version 2.3
0x00, // Flags: no unsynchronisation, no extended header, no experimental indicator
0x00, 0x00, 0x00, 0x0D, // Size of the (tag-)frames, extended header and padding
0x54, 0x49, 0x54, 0x32, // Title frame: "TIT2"
0x00, 0x00, 0x00, 0x02, // Size of the frame data
0x00, 0x00, // Frame Flags
0x00, 0x20, 0x00 // Frame data (space character) and padding
));
// Slot size (MPEG unit of measurement) - use [layer]
AudioFormatReader_MPEG.MPEG_slot_size = new Array(0, 1, 1, 4); // Rsvd, 3, 2, 1
return AudioFormatReader_MPEG;
}(AudioFormatReader));
@@ -1,222 +0,0 @@
/*
WAV audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var AudioFormatReader_WAV = /** @class */ (function (_super) {
__extends(AudioFormatReader_WAV, _super);
function AudioFormatReader_WAV(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, batchDuration, extraEdgeDuration) {
var _this = _super.call(this, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) || this;
_this._OnDecodeSuccess = _this.OnDecodeSuccess.bind(_this);
_this._OnDecodeError = _this.OnDecodeError.bind(_this);
_this.BatchDuration = batchDuration;
_this.ExtraEdgeDuration = extraEdgeDuration;
_this.GotHeader = false;
_this.RiffHeader = null;
_this.WaveSampleRate = 0;
_this.WaveBitsPerSample = 0;
_this.WaveBytesPerSample = 0;
_this.WaveBlockAlign = 0;
_this.WaveChannels = 0;
_this.BatchSamples = 0;
_this.BatchBytes = 0;
_this.ExtraEdgeSamples = 0;
_this.TotalBatchSampleSize = 0;
_this.TotalBatchByteSize = 0;
_this.SampleBudget = 0;
return _this;
}
// Deletes all samples from the databuffer and the samplearray
AudioFormatReader_WAV.prototype.PurgeData = function () {
_super.prototype.PurgeData.call(this);
this.SampleBudget = 0;
};
// Deletes all data from the reader (deos effect headers, etc.)
AudioFormatReader_WAV.prototype.Reset = function () {
_super.prototype.Reset.call(this);
this.GotHeader = false;
this.RiffHeader = null;
this.WaveSampleRate = 0;
this.WaveBitsPerSample = 0;
this.WaveBytesPerSample = 0;
this.WaveBlockAlign = 0;
this.WaveChannels = 0;
this.BatchSamples = 0;
this.BatchBytes = 0;
this.ExtraEdgeSamples = 0;
this.TotalBatchSampleSize = 0;
this.TotalBatchByteSize = 0;
this.SampleBudget = 0;
};
AudioFormatReader_WAV.prototype.ExtractAll = function () {
if (!this.GotHeader)
this.FindAndExtractHeader();
else {
var _loop_1 = function () {
// Extract samples
var tmpSamples = this_1.ExtractIntSamples();
// Increment Id
var id = this_1.Id++;
if (!this_1.OnBeforeDecode(id, this_1.BatchDuration))
return "continue";
// Note:
// =====
// When audio data is resampled we get edge-effects at beginnging and end.
// We should be able to compensate for that by keeping the last sample of the
// previous batch and adding it to the beginning of the current one, but then
// cutting it out AFTER the resampling (since the same effects apply to it)
// The effects at the end can be compensated by cutting the resampled samples shorter
// This is not trivial for non-natural ratios (e.g. 16kHz -> 44.1kHz). Because we would have
// to cut out a non-natural number of samples at beginning and end.
// TODO: All of the above...
// Create a buffer long enough to hold everything
var samplesBuffer = new Uint8Array(this_1.RiffHeader.length + tmpSamples.length);
var offset = 0;
// Add header
samplesBuffer.set(this_1.RiffHeader, offset);
offset += this_1.RiffHeader.length;
// Add samples
samplesBuffer.set(tmpSamples, offset);
// Push pages to the decoder
this_1.Audio.decodeAudioData(samplesBuffer.buffer, (function (decodedData) {
var _id = id;
this._OnDecodeSuccess(decodedData, _id);
}).bind(this_1), this_1._OnDecodeError);
};
var this_1 = this;
while (this.CanExtractSamples()) {
_loop_1();
}
}
};
// Finds riff header within the data buffer and extracts it
AudioFormatReader_WAV.prototype.FindAndExtractHeader = function () {
var curpos = 0;
// Make sure a whole header can fit
if (!((curpos + 4) < this.DataBuffer.length))
return;
// Check chunkID, should be "RIFF"
if (!(this.DataBuffer[curpos] == 0x52 && this.DataBuffer[curpos + 1] == 0x49 && this.DataBuffer[curpos + 2] == 0x46 && this.DataBuffer[curpos + 3] == 0x46))
return;
curpos += 8;
if (!((curpos + 4) < this.DataBuffer.length))
return;
// Check riffType, should be "WAVE"
if (!(this.DataBuffer[curpos] == 0x57 && this.DataBuffer[curpos + 1] == 0x41 && this.DataBuffer[curpos + 2] == 0x56 && this.DataBuffer[curpos + 3] == 0x45))
return;
curpos += 4;
if (!((curpos + 4) < this.DataBuffer.length))
return;
// Check for format subchunk, should be "fmt "
if (!(this.DataBuffer[curpos] == 0x66 && this.DataBuffer[curpos + 1] == 0x6d && this.DataBuffer[curpos + 2] == 0x74 && this.DataBuffer[curpos + 3] == 0x20))
return;
curpos += 4;
if (!((curpos + 4) < this.DataBuffer.length))
return;
var subChunkSize = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8 | this.DataBuffer[curpos + 2] << 16 | this.DataBuffer[curpos + 3] << 24;
if (!((curpos + 4 + subChunkSize) < this.DataBuffer.length))
return;
curpos += 6;
this.WaveChannels = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
curpos += 2;
this.WaveSampleRate = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8 | this.DataBuffer[curpos + 2] << 16 | this.DataBuffer[curpos + 3] << 24;
curpos += 8;
this.WaveBlockAlign = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
curpos += 2;
this.WaveBitsPerSample = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
this.WaveBytesPerSample = this.WaveBitsPerSample / 8;
curpos += subChunkSize - 14;
while (true) {
if ((curpos + 8) < this.DataBuffer.length) {
subChunkSize = this.DataBuffer[curpos + 4] | this.DataBuffer[curpos + 5] << 8 | this.DataBuffer[curpos + 6] << 16 | this.DataBuffer[curpos + 7] << 24;
// Check for data subchunk, should be "data"
if (this.DataBuffer[curpos] == 0x64 && this.DataBuffer[curpos + 1] == 0x61 && this.DataBuffer[curpos + 2] == 0x74 && this.DataBuffer[curpos + 3] == 0x61) // Data chunk found
break;
else
curpos += 8 + subChunkSize;
}
else
return;
}
curpos += 8;
this.RiffHeader = new Uint8Array(this.DataBuffer.buffer.slice(0, curpos));
this.BatchSamples = Math.ceil(this.BatchDuration * this.WaveSampleRate);
this.ExtraEdgeSamples = Math.ceil(this.ExtraEdgeDuration * this.WaveSampleRate);
this.BatchBytes = this.BatchSamples * this.WaveBlockAlign;
this.TotalBatchSampleSize = (this.BatchSamples + this.ExtraEdgeSamples);
this.TotalBatchByteSize = this.TotalBatchSampleSize * this.WaveBlockAlign;
var chunkSize = this.RiffHeader.length + this.TotalBatchByteSize - 8;
// Fix header chunksizes
this.RiffHeader[4] = chunkSize & 0xFF;
this.RiffHeader[5] = (chunkSize & 0xFF00) >>> 8;
this.RiffHeader[6] = (chunkSize & 0xFF0000) >>> 16;
this.RiffHeader[7] = (chunkSize & 0xFF000000) >>> 24;
this.RiffHeader[this.RiffHeader.length - 4] = (this.TotalBatchByteSize & 0xFF);
this.RiffHeader[this.RiffHeader.length - 3] = (this.TotalBatchByteSize & 0xFF00) >>> 8;
this.RiffHeader[this.RiffHeader.length - 2] = (this.TotalBatchByteSize & 0xFF0000) >>> 16;
this.RiffHeader[this.RiffHeader.length - 1] = (this.TotalBatchByteSize & 0xFF000000) >>> 24;
this.GotHeader = true;
};
// Checks if there is a samples ready to be extracted
AudioFormatReader_WAV.prototype.CanExtractSamples = function () {
if (this.DataBuffer.length >= this.TotalBatchByteSize)
return true;
else
return false;
};
// Extract a single batch of samples from the buffer
AudioFormatReader_WAV.prototype.ExtractIntSamples = function () {
// Extract sample data from buffer
var intSampleArray = new Uint8Array(this.DataBuffer.buffer.slice(0, this.TotalBatchByteSize));
// Remove samples from buffer
this.DataBuffer = new Uint8Array(this.DataBuffer.buffer.slice(this.BatchBytes));
return intSampleArray;
};
// Is called if the decoding of the samples succeeded
AudioFormatReader_WAV.prototype.OnDecodeSuccess = function (decodedData, id) {
// Calculate the length of the parts
var pickSize = this.BatchDuration * decodedData.sampleRate;
this.SampleBudget += (pickSize - Math.ceil(pickSize));
pickSize = Math.ceil(pickSize);
var pickOffset = (decodedData.length - pickSize) / 2.0;
if (pickOffset < 0)
pickOffset = 0; // This should never happen!
else
pickOffset = Math.floor(pickOffset);
if (this.SampleBudget < -1.0) {
var correction = -1.0 * Math.floor(Math.abs(this.SampleBudget));
this.SampleBudget -= correction;
pickSize += correction;
}
else if (this.SampleBudget > 1.0) {
var correction = Math.floor(this.SampleBudget);
this.SampleBudget -= correction;
pickSize += correction;
}
// Create a buffer that can hold a single part
var audioBuffer = this.Audio.createBuffer(decodedData.numberOfChannels, pickSize, decodedData.sampleRate);
// Fill buffer with the last part of the decoded frame
for (var i = 0; i < decodedData.numberOfChannels; i++)
audioBuffer.getChannelData(i).set(decodedData.getChannelData(i).slice(pickOffset, -pickOffset));
this.OnDataReady(id, audioBuffer);
};
// Is called in case the decoding of the window fails
AudioFormatReader_WAV.prototype.OnDecodeError = function (_error) {
this.ErrorCallback();
};
return AudioFormatReader_WAV;
}(AudioFormatReader));
-70
View File
@@ -1,70 +0,0 @@
let Stream;
let shouldReconnect = true;
let newVolumeGlobal = 1;
function Init(_ev) {
$(".playbutton").off('click').on('click', OnPlayButtonClick); // Ensure only one event handler is attached
$("#volumeSlider").off("input").on("input", updateVolume); // Ensure only one event handler is attached
}
function createStream() {
try {
const settings = new _3LAS_Settings();
Stream = new _3LAS(null, settings);
Stream.Volume = $('#volumeSlider').val();
Stream.ConnectivityCallback = OnConnectivityCallback;
} catch (error) {
console.error("Initialization Error: ", error);
}
}
function destroyStream() {
if (Stream) {
Stream.Stop();
Stream = null;
}
}
function OnConnectivityCallback(isConnected) {
console.log("Connectivity changed:", isConnected);
if (Stream) Stream.Volume = $('#volumeSlider').val();
else console.warn("Stream is not initialized.");
}
function OnPlayButtonClick(_ev) {
const $playbutton = $('.playbutton');
const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if (Stream) {
console.log("Stopping stream...");
shouldReconnect = false;
destroyStream();
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "none";
} else {
console.log("Starting stream...");
shouldReconnect = true;
createStream();
Stream.Start();
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
}
$playbutton.addClass('bg-gray').prop('disabled', true);
setTimeout(() => {
$playbutton.removeClass('bg-gray').prop('disabled', false);
}, 3000);
}
function updateVolume() {
if (Stream) {
const newVolume = $(this).val();
newVolumeGlobal = newVolume;
console.log("Volume updated to:", newVolume);
Stream.Volume = newVolume;
} else console.warn("Stream is not initialized.");
}
$(document).ready(Init);
-140
View File
@@ -1,140 +0,0 @@
/*
Helpers is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var isAndroid;
var isIOS;
var isIPadOS;
var isWindows;
var isLinux;
var isBSD;
var isMacOSX;
var isInternetExplorer;
var isEdge;
;
var isSafari;
;
var isOpera;
;
var isChrome;
;
var isFirefox;
;
var webkitVer;
var isNativeChrome;
;
var BrowserName;
var OSName;
{
var ua = navigator.userAgent.toLowerCase();
isAndroid = (ua.match('android') ? true : false);
isIOS = (ua.match(/(iphone|ipod)/g) ? true : false);
isIPadOS = ((ua.match('ipad') || (navigator.platform == 'MacIntel' && navigator.maxTouchPoints > 1)) ? true : false);
isWindows = (ua.match('windows') ? true : false);
isLinux = (ua.match('android') ? false : (ua.match('linux') ? true : false));
isBSD = (ua.match('bsd') ? true : false);
isMacOSX = !isIOS && !isIPadOS && (ua.match('mac os x') ? true : false);
isInternetExplorer = (ua.match('msie') ? true : false);
isEdge = (ua.match('edg') ? true : false);
isSafari = (ua.match(/(chromium|chrome|crios)/g) ? false : (ua.match('safari') ? true : false));
isOpera = (ua.match('opera') ? true : false);
isChrome = !isSafari && (ua.match(/(chromium|chrome|crios)/g) ? true : false);
isFirefox = (ua.match('like gecko') ? false : (ua.match(/(gecko|fennec|firefox)/g) ? true : false));
webkitVer = parseInt((/WebKit\/([0-9]+)/.exec(navigator.appVersion) || ["", "0"])[1], 10) || void 0; // also match AppleWebKit
isNativeChrome = isAndroid && webkitVer <= 537 && navigator.vendor.toLowerCase().indexOf('google') == 0;
BrowserName = "Unknown";
if (isInternetExplorer)
BrowserName = "IE";
else if (isEdge)
BrowserName = "Edge";
else if (isSafari)
BrowserName = "Safari";
else if (isOpera)
BrowserName = "Opera";
else if (isChrome)
BrowserName = "Chrome";
else if (isFirefox)
BrowserName = "Firefox";
else if (isNativeChrome)
BrowserName = "NativeChrome";
else
BrowserName = "Unknown";
OSName = "Unknown";
if (isAndroid)
OSName = "Android";
else if (isIOS)
OSName = "iOS";
else if (isIPadOS)
OSName = "iPadOS";
else if (isWindows)
OSName = "Windows";
else if (isLinux)
OSName = "Linux";
else if (isBSD)
OSName = "BSD";
else if (isMacOSX)
OSName = "MacOSX";
else
OSName = "Unknown";
}
;
var WakeLock = /** @class */ (function () {
function WakeLock(logger) {
this.Logger = logger;
this.Logger.Log("Preparing WakeLock");
if (typeof navigator.wakeLock == "undefined") {
this.Logger.Log("Using video loop method.");
var video = document.createElement('video');
video.setAttribute('loop', '');
video.setAttribute('style', 'position: fixed; opacity: 0.1; pointer-events: none;');
WakeLock.AddSourceToVideo(video, 'webm', 'data:video/webm;base64,' + WakeLock.VideoWebm);
WakeLock.AddSourceToVideo(video, 'mp4', 'data:video/mp4;base64,' + WakeLock.VideoMp4);
document.body.appendChild(video);
this.LockElement = video;
}
else {
this.Logger.Log("Using WakeLock API.");
this.LockElement = null;
}
}
WakeLock.prototype.Begin = function () {
var _this = this;
if (this.LockElement == null) {
try {
navigator.wakeLock.request("screen").then(function (obj) {
_this.Logger.Log("WakeLock request successful. Lock acquired.");
_this.LockElement = obj; // Not an audio/video element
console.log("WakeLock request successful.");
}, function () {
_this.Logger.Log("WakeLock request failed.");
console.log("WakeLock request failed.");
});
}
catch (err) {
this.Logger.Log("WakeLock request failed.");
console.log("WakeLock request failed.");
}
}
else {
this.Logger.Log("WakeLock video loop started.");
// Ensure it's an audio/video element before calling play()
if (_this.LockElement instanceof HTMLMediaElement) {
_this.LockElement.play().catch(err => {
console.error("LockElement failed:", err);
});
} else {
console.warn("LockElement not a media element or already assigned.");
}
}
};
WakeLock.AddSourceToVideo = function (element, type, dataURI) {
var source = document.createElement('source');
source.src = dataURI;
source.type = 'video/' + type;
element.appendChild(source);
};
WakeLock.VideoWebm = 'GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=';
WakeLock.VideoMp4 = 'AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAG21kYXQAAAGzABAHAAABthADAowdbb9/AAAC6W1vb3YAAABsbXZoZAAAAAB8JbCAfCWwgAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIVdHJhawAAAFx0a2hkAAAAD3wlsIB8JbCAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAIAAAACAAAAAABsW1kaWEAAAAgbWRoZAAAAAB8JbCAfCWwgAAAA+gAAAAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAVxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAEcc3RibAAAALhzdHNkAAAAAAAAAAEAAACobXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAIAAgASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAFJlc2RzAAAAAANEAAEABDwgEQAAAAADDUAAAAAABS0AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiB9FAEQBFGMAAAGyTGF2YzUyLjg3LjQGAQIAAAAYc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAAEwAAAAEAAAAUc3RjbwAAAAAAAAABAAAALAAAAGB1ZHRhAAAAWG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAK2lsc3QAAAAjqXRvbwAAABtkYXRhAAAAAQAAAABMYXZmNTIuNzguMw==';
return WakeLock;
}());
-26
View File
@@ -1,26 +0,0 @@
/*
Logging is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var Logging = /** @class */ (function () {
function Logging(parentElement, childElementType) {
this.ParentElement = parentElement;
this.ChildElementType = childElementType;
}
Logging.prototype.Log = function (message) {
var dateTime = new Date();
var lineText = "[" + (dateTime.getHours() > 9 ? dateTime.getHours() : "0" + dateTime.getHours()) + ":" +
(dateTime.getMinutes() > 9 ? dateTime.getMinutes() : "0" + dateTime.getMinutes()) + ":" +
(dateTime.getSeconds() > 9 ? dateTime.getSeconds() : "0" + dateTime.getSeconds()) +
"] " + message;
if (this.ParentElement && this.ChildElementType) {
var line = document.createElement(this.ChildElementType);
line.innerText = lineText;
this.ParentElement.appendChild(line);
}
else {
//console.log(lineText);
}
};
return Logging;
}());
-83
View File
@@ -1,83 +0,0 @@
/*
WebSocket client is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
var WebSocketClient = /** @class */ (function () {
function WebSocketClient(logger, uri, errorCallback, connectCallback, dataReadyCallback, disconnectCallback) {
this.Logger = logger;
this.Uri = uri;
// Check callback argument
if (typeof errorCallback !== 'function')
throw new Error('WebSocketClient: ErrorCallback must be specified');
if (typeof connectCallback !== 'function')
throw new Error('WebSocketClient: ConnectCallback must be specified');
if (typeof dataReadyCallback !== 'function')
throw new Error('WebSocketClient: DataReadyCallback must be specified');
if (typeof disconnectCallback !== 'function')
throw new Error('WebSocketClient: DisconnectCallback must be specified');
this.ErrorCallback = errorCallback;
this.ConnectCallback = connectCallback;
this.DataReadyCallback = dataReadyCallback;
this.DisconnectCallback = disconnectCallback;
// Client is not yet connected
this.IsConnected = false;
// Create socket, connect to URI
if (typeof WebSocket !== "undefined")
this.Socket = new WebSocket(this.Uri);
else if (typeof webkitWebSocket !== "undefined")
this.Socket = new webkitWebSocket(this.Uri);
else if (typeof mozWebSocket !== "undefined")
this.Socket = new mozWebSocket(this.Uri);
else
throw new Error('WebSocketClient: Browser does not support "WebSocket".');
this.Socket.binaryType = 'arraybuffer';
this.Socket.addEventListener("open", this.OnOpen.bind(this));
this.Socket.addEventListener("error", this.OnError.bind(this));
this.Socket.addEventListener("close", this.OnClose.bind(this));
this.Socket.addEventListener("message", this.OnMessage.bind(this));
}
Object.defineProperty(WebSocketClient.prototype, "Connected", {
get: function () {
return this.IsConnected;
},
enumerable: false,
configurable: true
});
WebSocketClient.prototype.Send = function (message) {
if (!this.IsConnected)
return;
this.Socket.send(message);
};
// Handle errors
WebSocketClient.prototype.OnError = function (_ev) {
if (this.IsConnected == true)
this.ErrorCallback("Socket fault.");
else
this.ErrorCallback("Could not connect to server.");
};
// Change connetion status once connected
WebSocketClient.prototype.OnOpen = function (_ev) {
if (this.Socket.readyState == 1) {
this.IsConnected = true;
this.ConnectCallback();
}
};
// Change connetion status on disconnect
WebSocketClient.prototype.OnClose = function (_ev) {
if (this.IsConnected == true && (this.Socket.readyState == 2 || this.Socket.readyState == 3)) {
this.IsConnected = false;
this.DisconnectCallback();
}
};
WebSocketClient.prototype.Close = function () {
if (this.Socket) {
this.Socket.close();
}
};
// Handle incomping data
WebSocketClient.prototype.OnMessage = function (ev) {
// Trigger callback
this.DataReadyCallback(ev.data);
};
return WebSocketClient;
}());
+111
View File
@@ -0,0 +1,111 @@
class WebSocketAudioPlayer {
constructor(url) {
this.url = url;
this.ws = null;
this.audio = null;
this.mediaSource = null;
this.sourceBuffer = null;
this.queue = [];
this.started = false;
}
start() {
this.audio = new Audio();
this.mediaSource = new MediaSource();
this.audio.src = URL.createObjectURL(this.mediaSource);
this.mediaSource.addEventListener('sourceopen', () => {
this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg');
this.sourceBuffer.addEventListener('updateend', () => this._appendNext());
this.ws = new WebSocket(this.url);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => this.ws.send(JSON.stringify({ type: 'fallback', data: 'mp3' }));
this.ws.onmessage = (event) => {
this.queue.push(event.data);
this._appendNext();
if (!this.started && this.queue.length === 0) { this.audio.play(); this.started = true; }
};
this.ws.onclose = () => {
if (this.mediaSource.readyState === 'open') this.mediaSource.endOfStream();
};
});
this.audio.addEventListener('canplay', () => {
if (!this.started) { this.audio.play(); this.started = true; }
});
}
stop() {
this.ws?.close();
this.audio?.pause();
this.queue = [];
this.started = false;
}
setVolume(v) {
if (this.audio) this.audio.volume = Math.max(0, Math.min(1, v));
}
_appendNext() {
if (this.sourceBuffer.updating || this.queue.length === 0) return;
this.sourceBuffer.appendBuffer(this.queue.shift());
}
}
let Stream = null;
let shouldReconnect = true;
function Init(_ev) {
$(".playbutton").off('click').on('click', OnPlayButtonClick);
$("#volumeSlider").off("input").on("input", updateVolume);
}
function createStream() {
try {
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const audioUrl = `${wsProtocol}//${location.hostname}/audio`;
Stream = new WebSocketAudioPlayer(audioUrl);
Stream.setVolume($('#volumeSlider').val());
} catch (error) {
console.error("Initialization Error: ", error);
}
}
function destroyStream() {
if (Stream) {
Stream.stop();
Stream = null;
}
}
function OnPlayButtonClick(_ev) {
const $playbutton = $('.playbutton');
const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if (Stream) {
console.log("Stopping stream...");
shouldReconnect = false;
destroyStream();
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "none";
} else {
console.log("Starting stream...");
shouldReconnect = true;
createStream();
Stream.start();
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
}
$playbutton.addClass('bg-gray').prop('disabled', true);
setTimeout(() => $playbutton.removeClass('bg-gray').prop('disabled', false), 3000);
}
function updateVolume() {
const newVolume = $(this).val();
if (Stream) Stream.setVolume(newVolume);
else console.warn("Stream is not initialized.");
}
$(document).ready(Init);