Compare commits

...
3 Commits
12 changed files with 1440 additions and 36 deletions
+9
View File
@@ -8,6 +8,15 @@
<link href="css/libs/jquery-ui.min.css" type="text/css" rel="stylesheet">
<script src="js/libs/jquery.min.js"></script>
<script src="js/libs/jquery-ui.min.js"></script>
<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/audio.js"></script>
</head>
<body>
+9 -1
View File
@@ -21,8 +21,16 @@
<meta property="og:image" content="https://fmdx.org/img/webserver_icon.png">
<meta property="og:description" content="Server description: <%= tunerDescMeta %>.">
<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/audio.js"></script>
<script src="js/3las_helper.js"></script>
<script type="text/javascript">
document.ontouchmove = function(e) {
e.preventDefault();
+181
View File
@@ -0,0 +1,181 @@
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 > 2000 && 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
@@ -0,0 +1,167 @@
/*
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 / 3.0;
}
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
@@ -0,0 +1,193 @@
/*
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;
}());
@@ -0,0 +1,146 @@
/*
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;
}());
@@ -0,0 +1,319 @@
/*
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));
@@ -0,0 +1,222 @@
/*
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));
@@ -2,9 +2,6 @@
Helpers is part of 3LAS (Low Latency Live Audio Streaming)
https://github.com/JoJoBond/3LAS
*/
// Terrible
var isAndroid;
var isIOS;
var isIPadOS;
@@ -14,12 +11,18 @@ var isBSD;
var isMacOSX;
var isInternetExplorer;
var isEdge;
;
var isSafari;
;
var isOpera;
;
var isChrome;
;
var isFirefox;
;
var webkitVer;
var isNativeChrome;
;
var BrowserName;
var OSName;
{
@@ -40,23 +43,39 @@ var OSName;
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";
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";
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 () {
@@ -72,7 +91,8 @@ var WakeLock = /** @class */ (function () {
WakeLock.AddSourceToVideo(video, 'mp4', 'data:video/mp4;base64,' + WakeLock.VideoMp4);
document.body.appendChild(video);
this.LockElement = video;
} else {
}
else {
this.Logger.Log("Using WakeLock API.");
this.LockElement = null;
}
@@ -89,7 +109,8 @@ var WakeLock = /** @class */ (function () {
_this.Logger.Log("WakeLock request failed.");
console.log("WakeLock request failed.");
});
} catch (err) {
}
catch (err) {
this.Logger.Log("WakeLock request failed.");
console.log("WakeLock request failed.");
}
@@ -102,7 +123,9 @@ var WakeLock = /** @class */ (function () {
_this.LockElement.play().catch(err => {
console.error("LockElement failed:", err);
});
} else console.warn("LockElement not a media element or already assigned.");
} else {
console.warn("LockElement not a media element or already assigned.");
}
}
};
WakeLock.AddSourceToVideo = function (element, type, dataURI) {
@@ -114,4 +137,4 @@ var WakeLock = /** @class */ (function () {
WakeLock.VideoWebm = 'GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=';
WakeLock.VideoMp4 = 'AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAG21kYXQAAAGzABAHAAABthADAowdbb9/AAAC6W1vb3YAAABsbXZoZAAAAAB8JbCAfCWwgAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIVdHJhawAAAFx0a2hkAAAAD3wlsIB8JbCAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAIAAAACAAAAAABsW1kaWEAAAAgbWRoZAAAAAB8JbCAfCWwgAAAA+gAAAAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAVxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAEcc3RibAAAALhzdHNkAAAAAAAAAAEAAACobXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAIAAgASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAFJlc2RzAAAAAANEAAEABDwgEQAAAAADDUAAAAAABS0AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiB9FAEQBFGMAAAGyTGF2YzUyLjg3LjQGAQIAAAAYc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAAEwAAAAEAAAAUc3RjbwAAAAAAAAABAAAALAAAAGB1ZHRhAAAAWG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAK2lsc3QAAAAjqXRvbwAAABtkYXRhAAAAAQAAAABMYXZmNTIuNzguMw==';
return WakeLock;
}());
}());
+26
View File
@@ -0,0 +1,26 @@
/*
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
@@ -0,0 +1,83 @@
/*
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;
}());
+39 -12
View File
@@ -9,6 +9,10 @@ class WebSocketAudioPlayer {
this.started = false;
}
supported() {
return MediaSource.isTypeSupported("audio/mpeg")
}
_syncToLive() {
if (!this.audio || !this.sourceBuffer) return;
const buffered = this.sourceBuffer.buffered;
@@ -17,10 +21,10 @@ class WebSocketAudioPlayer {
const liveEdge = buffered.end(buffered.length - 1);
const lag = liveEdge - this.audio.currentTime;
if (lag > .3) this.audio.currentTime = liveEdge - 0.1; // snap to near live edge
if (lag > .9) this.audio.currentTime = liveEdge - 0.15; // snap to near live edge
}
start() {
Start() {
this.audio = new Audio();
this.mediaSource = new MediaSource();
this.audio.src = URL.createObjectURL(this.mediaSource);
@@ -48,7 +52,7 @@ class WebSocketAudioPlayer {
});
}
stop() {
Stop() { // capital to match 3las
this.ws?.close();
this.audio?.pause();
this.queue = [];
@@ -68,6 +72,7 @@ class WebSocketAudioPlayer {
let Stream = null;
let shouldReconnect = true;
let firefox = false;
function Init(_ev) {
$(".playbutton").off('click').on('click', OnPlayButtonClick);
@@ -75,18 +80,31 @@ function Init(_ev) {
}
function createStream() {
try {
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
Stream = new WebSocketAudioPlayer(`${wsProtocol}//${location.hostname}/audio`);
Stream.setVolume($('#volumeSlider').val());
} catch (error) {
console.error("Initialization Error: ", error);
try {
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${wsProtocol}//${location.hostname}/audio`;
if (MediaSource.isTypeSupported("audio/mpeg")) {
firefox = false;
Stream = new WebSocketAudioPlayer(url);
Stream.setVolume($('#volumeSlider').val());
} else {
firefox = true;
alert("Your browser does not support MP3 over MSE, meaning we have to use 3LAS which is terrible and gives terrible audio quality. If you want better sound, switch to anything chromium based")
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.Stop();
Stream = null;
}
}
@@ -105,20 +123,29 @@ function OnPlayButtonClick(_ev) {
console.log("Starting stream...");
shouldReconnect = true;
createStream();
Stream.start();
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), 2000);
if (Stream) Stream.setVolume($("#volumeSlider").val());
if (Stream && firefox) Stream.setVolume($("#volumeSlider").val());
}
function updateVolume() {
if(firefox) {
const newVolume = $(this).val();
if (Stream) Stream.setVolume(newVolume);
else console.warn("Stream is not initialized.");
} else {
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);