Skip to content

Instantly share code, notes, and snippets.

@UCIS
Created June 17, 2020 18:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save UCIS/845eda1755d38eddfc3f0f99268c27da to your computer and use it in GitHub Desktop.
Save UCIS/845eda1755d38eddfc3f0f99268c27da to your computer and use it in GitHub Desktop.
Opus/WebM player in JavaScript
function OpusWebmPacker() {
var channels = 2;
var sample_rate = 48000;
var position = 0;
var packets = [];
var buffer = new Uint8Array(4 + 1275);
var buffer_offset = 0;
function Concat() {
var i, l = 0, a;
for (i = 0; i < arguments.length; i++) l += arguments[i].byteLength;
a = new Uint8Array (l);
for (i = 0, l = 0; i < arguments.length; l += arguments[i].byteLength, i++) a.set(arguments[i], l);
return a;
}
function EncodeID(id) {
if ((id & 0xFF000000) != 0) return new Uint8Array([(id >>> 24) & 0xFF, (id >>> 16) & 0xFF, (id >>> 8) & 0xFF, id & 0xFF]);
if ((id & 0xFF0000) != 0) return new Uint8Array([(id >>> 16) & 0xFF, (id >>> 8) & 0xFF, id & 0xFF]);
if ((id & 0xFF00) != 0) return new Uint8Array([(id >>> 8) & 0xFF, id & 0xFF]);
if ((id & 0xFF) != 0) return new Uint8Array([id & 0xFF]);
throw 'InvalidOperationException';
}
function EncodeLength(value) {
if (value <= 0x7F) return new Uint8Array([(0x80 | (value & 0x7F))]);
if (value <= 0x3FFF) return new Uint8Array([(0x40 | ((value >> 8) & 0x3F)), value & 0xFF]);
return new Uint8Array([0x08, (value >>> 24) & 0xFF, (value >>> 16) & 0xFF, (value >>> 8) & 0xFF, value & 0xFF]);
}
function EncodeUInt(value) {
if (value <= 0xFF) return new Uint8Array([value & 0xFF]);
if (value <= 0xFFFF) return new Uint8Array([(value >>> 8) & 0xFF, value & 0xFF]);
if (value <= 0xFFFFFF) return new Uint8Array([(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]);
return new Uint8Array([(value >>> 24) & 0xFF, (value >>> 16) & 0xFF, (value >>> 8) & 0xFF, value & 0xFF]);
var b = new DataView(new ArrayBuffer(4));
b.setUint32(0, value, false);
return b;
}
function MakeElement(id, data) {
data = new Uint8Array(data);
return Concat(EncodeID(id), EncodeLength(data.byteLength), data);
}
function MakeInfiniteElement(id, data) {
return Concat(EncodeID(id), EncodeLength(0xFFFFFFFF), data);
}
function MakeMaster(id) {
return MakeElement(id, Concat.apply(null, Array.prototype.slice.call(arguments, 1)));
}
function MakeInfiniteMaster(id, parts) {
return MakeInfiniteElement(id, Concat.apply(null, Array.prototype.slice.call(arguments, 1)));
}
function MakeUInt(id, value) {
return MakeElement(id, EncodeUInt(value));
}
function MakeString(id, value) {
return MakeUnicode(id, value);
}
function MakeUnicode(id, value) {
return MakeElement(id, (new TextEncoder()).encode(value));
}
function MakeFloat(id, value) {
var b = new DataView(new ArrayBuffer(4));
b.setFloat32(0, value, false);
return MakeElement(id, new Uint8Array(b.buffer));
}
function BuildHeader(channels, originalSampleRate) {
return Concat(
MakeMaster(0x1A45DFA3, //EBML
MakeUInt(0x4286, 1), //EBMLVersion
MakeUInt(0x42F7, 1), //EBMLReadVersion
MakeUInt(0x42F2, 4), //EBMLMaxIDLength
MakeUInt(0x42F3, 8), //EBMLMaxSizeLength
MakeString(0x4282, "webm"), //DocType
MakeUInt(0x4287, 4), //DocTypeVersion
MakeUInt(0x4285, 2) //DocTypeReadVersion
),
MakeInfiniteMaster(0x18538067, //Segment
MakeMaster(0x1549A966, //Info
MakeUInt(0x2AD7B1, 1000000), //TimestampScale = 1000000000/1000000=1ms
MakeUnicode(0x4D80, "URadioServer"), //MuxingApp
MakeUnicode(0x5741, "URadioServer"), //WritingApp
),
MakeMaster(0x1654AE6B, //Tracks
MakeMaster(0xAE, //TrackEntry
MakeUInt(0xD7, 1), //TrackNumber
MakeUInt(0x73C5, 1), //TrackUID
MakeUInt(0x9C, 0), //FlagLacing
MakeString(0x22B59C, "und"), //Language
MakeString(0x86, "A_OPUS"), //CodecID
MakeUInt(0x56AA, 6500000), //CodecDelay
MakeUInt(0x56BB, 80000000), //SeekPreRoll
MakeUInt(0x83, 2), //TrackType
MakeMaster(0xE1, //Audio
MakeFloat(0xB5, 48000), //SamplingFrequency
MakeUInt(0x9F, channels) //Channels
),
MakeElement(0x63A2, //CodecPrivate
new Uint8Array([
'O'.charCodeAt(0), 'p'.charCodeAt(0), 'u'.charCodeAt(0), 's'.charCodeAt(0), 'H'.charCodeAt(0), 'e'.charCodeAt(0), 'a'.charCodeAt(0), 'd'.charCodeAt(0),
1, channels & 0xFF, 0x38, 0x01,
(originalSampleRate >>> 0) & 0xFF, (originalSampleRate >>> 8) & 0xFF, (originalSampleRate >>> 16) & 0xFF, (originalSampleRate >>> 24) & 0xFF,
0, 0, 0
])
)
)
)
)
);
}
function Feed(data) {
data = new Uint8Array(data);
var copy_length, packet_length, num_samples;
while (data.length > 0) {
copy_length = Math.min(data.length, buffer.length - buffer_offset);
buffer.set(data.subarray(0, copy_length), buffer_offset);
buffer_offset += copy_length;
data = data.subarray(copy_length);
if (buffer_offset >= 4) {
packet_length = buffer[0] | (buffer[1] << 8);
num_samples = buffer[2] | (buffer[3] << 8);
if (packet_length > 1275 || (num_samples != 120 && num_samples != 240 && num_samples != 960 && num_samples != 1920 && num_samples != 2880)) {
buffer.set(buffer.subarray(1));
buffer_offset--;
continue;
}
packet_length += 4;
if (buffer_offset >= packet_length) {
packets.push(MakeMaster(0x1F43B675, //Cluster
MakeUInt(0xE7, position), //Timestamp
MakeElement(0xA3, //SimpleBlock
Concat(
EncodeLength(1), //Track Number
new Uint8Array([0, 0]), //Relative timestamp
new Uint8Array([0x80]), //Flags
buffer.subarray(4, packet_length)
)
)
));
position += num_samples * 1000 / 48000;
if (packet_length < buffer_offset) buffer.set(buffer.subarray(packet_length));
buffer_offset -= packet_length;
}
}
}
}
function GetBufferLength() {
return packets.length * 1275 + buffer_offset; //estimated buffer length, just to prevent overflow
}
function GetFrame() {
var frame = packets.shift();
return frame ? { data: frame } : null;
}
packets.push(BuildHeader(2, 48000));
return { Feed: Feed, GetBufferLength: GetBufferLength, GetFrame: GetFrame };
}
function AudioPlayer_MSE(receiver) {
var player = null, sourceBuffer = null, queue = [], socket = null, synchronizer = null;
var panner = null;
var audioContext = new (window.AudioContext || window.webkitAudioContext || Object)();
var websocket = window.WebSocket || window.MozWebSocket;
function sourceOpen() {
window.URL.revokeObjectURL(player.src);
if (streamType == 'opus') sourceBuffer = this.addSourceBuffer('audio/webm; codecs="opus"');
else sourceBuffer = this.addSourceBuffer('audio/mpeg');
sourceBuffer.addEventListener('updateend', sourceFeedBuffer);
sourceFeedBuffer();
}
function sourceFeedBuffer() {
if (!sourceBuffer || sourceBuffer.updating) return;
var frame;
if (synchronizer && (frame = synchronizer.GetFrame())) sourceBuffer.appendBuffer(frame.data);
else if (queue.length) sourceBuffer.appendBuffer(queue.shift());
}
function start(url) {
if (!player) {
player = document.createElement("audio");
player.controls = true;
document.getElementById('mpcontainer').style.display = '';
document.getElementById('mpcontainer').style.visibility = '';
document.getElementById('mpcontainer').appendChild(player);
if (audioContext.createMediaElementSource && audioContext.createStereoPanner) {
var playerSource = audioContext.createMediaElementSource(player);
panner = audioContext.createStereoPanner();
playerSource.connect(panner);
panner.connect(audioContext.destination);
}
}
if (socket) socket.close();
queue = [];
synchronizer = null;
var source = new MediaSource();
source.addEventListener('sourceopen', sourceOpen, false);
player.src = window.URL.createObjectURL(source);
if (streamType == 'opus') synchronizer = new OpusWebmPacker();
else if (window.navigator.userAgent.indexOf("Edge") > -1) synchronizer = new MP3FrameSynchronizer();
else synchronizer = null;
socket = new websocket(url, 'binary');
socket.binaryType = 'arraybuffer';
socket.onclose = function(e) { console.log([ 'wsaudio closed', e ]); };
socket.onopen = function() { console.log('wsaudio open'); };
socket.onerror = function(e) { console.log([ 'wsaudio error', e ]); };
socket.onmessage = function(e) {
if (synchronizer) {
if (synchronizer.GetBufferLength() < 102400) synchronizer.Feed(e.data);
} else {
if (queue.length < 25) queue.push(e.data);
}
sourceFeedBuffer();
};
player.play();
};
var obj = {};
obj.play = function(ws_url, type) {
if (source == null) obj.stop();
else {
if (type == 'opus') streamType = 'opus'; else streamType = 'audio/mpeg';
start(ws_url);
}
};
obj.stop = function() {
if (socket) socket.close();
socket = null;
queue = [];
if (player) player.pause();
if (player) player.parentNode.removeChild(player);
player = null;
sourceBuffer = null;
};
obj.setVolume = function(vol) {
if (player) player.volume = Math.min(1, vol / 100);
};
if (audioContext.createMediaElementSource && audioContext.createStereoPanner) {
obj.setBalance = function(value) {
if (panner) panner.pan.value = Math.min(1, Math.max(-1, value / 100));
};
}
return obj;
}
@guest271314
Copy link

A publicly disclosed working version would be helpful, see https://bugs.chromium.org/p/chromium/issues/detail?id=1161429

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment