-
Notifications
You must be signed in to change notification settings - Fork 31
/
webaudio.js
121 lines (99 loc) · 4.89 KB
/
webaudio.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
export default class WebAudio {
constructor(url) {
this.url = url
this.connected = false;
//constants for audio behavoir
this.maximumAudioLag =0.25; //amount of seconds we can potentially be behind the server audio stream
this.syncLagInterval = 500; //check every x milliseconds if we are behind the server audio stream
this.updateBufferEvery = 20; //add recieved data to the player buffer every x milliseconds
this.reduceBufferInterval = 20; //trim the output audio stream buffer every x milliseconds so we don't overflow
this.maximumSecondsOfBuffering = .2; //maximum amount of data to store in the play buffer
this.connectionCheckInterval = 100; //check the connection every x milliseconds
//register all our background timers. these need to be created only once - and will run independent of the object's streams/properties
setInterval(() => this.updateQueue(), this.updateBufferEvery);
setInterval(() => this.syncInterval(), this.syncLagInterval);
setInterval(() => this.reduceBuffer(), this.reduceBufferInterval);
setInterval(() => this.tryLastPacket(), this.connectionCheckInterval);
}
//registers all the event handlers for when this stream is closed - or when data arrives.
registerHandlers() {
this.mediaSource.addEventListener('sourceended', e => this.socketDisconnected(e))
this.mediaSource.addEventListener('sourceclose', e => this.socketDisconnected(e))
this.mediaSource.addEventListener('error', e => this.socketDisconnected(e))
this.buffer.addEventListener('error', e => this.socketDisconnected(e))
this.buffer.addEventListener('abort', e => this.socketDisconnected(e))
}
//starts the web audio stream. only call this method on button click.
start() {
if (!!this.connected) return;
if (!!this.audio) this.audio.remove();
this.queue = null;
this.mediaSource = new MediaSource()
this.mediaSource.addEventListener('sourceopen', e => this.onSourceOpen())
//first we need a media source - and an audio object that contains it.
this.audio = document.createElement('audio');
this.audio.src = window.URL.createObjectURL(this.mediaSource);
//start our stream - we can only do this on user input
this.audio.play();
}
wsConnect() {
if (!!this.socket) this.socket.close();
this.socket = new WebSocket(this.url, ['binary', 'base64'])
this.socket.binaryType = 'arraybuffer'
this.socket.addEventListener('message', e => this.websocketDataArrived(e), false);
}
//this is called when the media source contains data
onSourceOpen(e) {
this.buffer = this.mediaSource.addSourceBuffer('audio/webm; codecs="opus"')
this.registerHandlers();
this.wsConnect();
}
//whenever data arrives in our websocket this is called.
websocketDataArrived(e) {
this.lastPacket = Date.now();
this.connected = true;
this.queue = this.queue == null ? e.data : this.concat(this.queue, e.data);
}
//whenever a disconnect happens this is called.
socketDisconnected(e) {
console.log(e);
this.connected = false;
}
tryLastPacket() {
if (this.lastPacket == null) return;
if ((Date.now() - this.lastPacket) > 1000) {
this.socketDisconnected('timeout');
}
}
//this updates the buffer with the data from our queue
updateQueue() {
if (!(!!this.queue && !!this.buffer && !this.buffer.updating)) {
return;
}
this.buffer.appendBuffer(this.queue);
this.queue = null;
}
//reduces the stream buffer to the minimal size that we need for streaming
reduceBuffer() {
if (!(this.buffer && !this.buffer.updating && !!this.audio && !!this.audio.currentTime && this.audio.currentTime > 1)) {
return;
}
this.buffer.remove(0, this.audio.currentTime - 1);
}
//synchronizes the current time of the stream with the server
syncInterval() {
if (!(this.audio && this.audio.currentTime && this.audio.currentTime > 1 && this.buffer && this.buffer.buffered && this.buffer.buffered.length > 1)) {
return;
}
var currentTime = this.audio.currentTime;
var targetTime = this.buffer.buffered.end(this.buffer.buffered.length - 1);
if (targetTime > (currentTime + this.maximumAudioLag)) this.audio.fastSeek(targetTime);
}
//joins two data arrays - helper function
concat(buffer1, buffer2) {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
}