-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathguacamole.js
13036 lines (10612 loc) · 398 KB
/
guacamole.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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;(function(){
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, returning
* strictly received packets as array buffers. Note that this object will
* overwrite any installed event handlers on the given Guacamole.InputStream.
*
* @constructor
* @param {Guacamole.InputStream} stream The stream that data will be read
* from.
*/
Guacamole.ArrayBufferReader = function(stream) {
/**
* Reference to this Guacamole.InputStream.
* @private
*/
var guac_reader = this;
// Receive blobs as array buffers
stream.onblob = function(data) {
// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
// Call handler, if present
if (guac_reader.ondata)
guac_reader.ondata(arrayBuffer);
};
// Simply call onend when end received
stream.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {ArrayBuffer} buffer The data packet received.
*/
this.ondata = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with arbitrary
* binary data, supplied as ArrayBuffers.
*
* @constructor
* @param {Guacamole.OutputStream} stream The stream that data will be written
* to.
*/
Guacamole.ArrayBufferWriter = function(stream) {
/**
* Reference to this Guacamole.StringWriter.
* @private
*/
var guac_writer = this;
// Simply call onack for acknowledgements
stream.onack = function(status) {
if (guac_writer.onack)
guac_writer.onack(status);
};
/**
* Encodes the given data as base64, sending it as a blob. The data must
* be small enough to fit into a single blob instruction.
*
* @private
* @param {Uint8Array} bytes The data to send.
*/
function __send_blob(bytes) {
var binary = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
binary += String.fromCharCode(bytes[i]);
// Send as base64
stream.sendBlob(window.btoa(binary));
}
/**
* The maximum length of any blob sent by this Guacamole.ArrayBufferWriter,
* in bytes. Data sent via
* [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds
* this length will be split into multiple blobs. As the Guacamole protocol
* limits the maximum size of any instruction or instruction element to
* 8192 bytes, and the contents of blobs will be base64-encoded, this value
* should only be increased with extreme caution.
*
* @type {Number}
* @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH}
*/
this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH;
/**
* Sends the given data.
*
* @param {ArrayBuffer|TypedArray} data The data to send.
*/
this.sendData = function(data) {
var bytes = new Uint8Array(data);
// If small enough to fit into single instruction, send as-is
if (bytes.length <= guac_writer.blobLength)
__send_blob(bytes);
// Otherwise, send as multiple instructions
else {
for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength)
__send_blob(bytes.subarray(offset, offset + guac_writer.blobLength));
}
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function() {
stream.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
* @event
* @param {Guacamole.Status} status The status of the operation.
*/
this.onack = null;
};
/**
* The default maximum blob length for new Guacamole.ArrayBufferWriter
* instances.
*
* @constant
* @type {Number}
*/
Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048;
var Guacamole = Guacamole || {};
/**
* Maintains a singleton instance of the Web Audio API AudioContext class,
* instantiating the AudioContext only in response to the first call to
* getAudioContext(), and only if no existing AudioContext instance has been
* provided via the singleton property. Subsequent calls to getAudioContext()
* will return the same instance.
*
* @namespace
*/
Guacamole.AudioContextFactory = {
/**
* A singleton instance of a Web Audio API AudioContext object, or null if
* no instance has yes been created. This property may be manually set if
* you wish to supply your own AudioContext instance, but care must be
* taken to do so as early as possible. Assignments to this property will
* not retroactively affect the value returned by previous calls to
* getAudioContext().
*
* @type {AudioContext}
*/
'singleton' : null,
/**
* Returns a singleton instance of a Web Audio API AudioContext object.
*
* @return {AudioContext}
* A singleton instance of a Web Audio API AudioContext object, or null
* if the Web Audio API is not supported.
*/
'getAudioContext' : function getAudioContext() {
// Fallback to Webkit-specific AudioContext implementation
var AudioContext = window.AudioContext || window.webkitAudioContext;
// Get new AudioContext instance if Web Audio API is supported
if (AudioContext) {
try {
// Create new instance if none yet exists
if (!Guacamole.AudioContextFactory.singleton)
Guacamole.AudioContextFactory.singleton = new AudioContext();
// Return singleton instance
return Guacamole.AudioContextFactory.singleton;
}
catch (e) {
// Do not use Web Audio API if not allowed by browser
}
}
// Web Audio API not supported
return null;
}
};
var Guacamole = Guacamole || {};
/**
* Abstract audio player which accepts, queues and plays back arbitrary audio
* data. It is up to implementations of this class to provide some means of
* handling a provided Guacamole.InputStream. Data received along the provided
* stream is to be played back immediately.
*
* @constructor
*/
Guacamole.AudioPlayer = function AudioPlayer() {
/**
* Notifies this Guacamole.AudioPlayer that all audio up to the current
* point in time has been given via the underlying stream, and that any
* difference in time between queued audio data and the current time can be
* considered latency.
*/
this.sync = function sync() {
// Default implementation - do nothing
};
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioPlayer, and thus will be properly handled
* by Guacamole.AudioPlayer.getInstance().
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioPlayer, false otherwise.
*/
Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioPlayer.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioPlayer, in rough order of priority. Beware that only the core
* mimetypes themselves will be listed. Any mimetype parameters, even required
* ones, will not be included in the list. For example, "audio/L8" is a
* supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by any built-in Guacamole.AudioPlayer,
* excluding any parameters.
*/
Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioPlayer.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioPlayer providing support for the given
* audio format. If support for the given audio format is not available, null
* is returned.
*
* @param {Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {String} mimetype
* The mimetype of the audio data in the provided stream.
*
* @return {Guacamole.AudioPlayer}
* A Guacamole.AudioPlayer instance supporting the given mimetype and
* reading from the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) {
// Use raw audio player if possible
if (Guacamole.RawAudioPlayer.isSupportedType(mimetype))
return new Guacamole.RawAudioPlayer(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioPlayer providing support for raw PCM format
* audio. This player relies only on the Web Audio API and does not require any
* browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioPlayer
* @param {Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {String} mimetype
* The mimetype of the audio data in the provided stream, which must be a
* "audio/L8" or "audio/L16" mimetype with necessary parameters, such as:
* "audio/L16;rate=44100,channels=2".
*/
Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) {
/**
* The format of audio this player will decode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
/**
* The earliest possible time that the next packet could play without
* overlapping an already-playing packet, in seconds. Note that while this
* value is in seconds, it is not an integer value and has microsecond
* resolution.
*
* @private
* @type {Number}
*/
var nextPacketTime = context.currentTime;
/**
* Guacamole.ArrayBufferReader wrapped around the audio input stream
* provided with this Guacamole.RawAudioPlayer was created.
*
* @private
* @type {Guacamole.ArrayBufferReader}
*/
var reader = new Guacamole.ArrayBufferReader(stream);
/**
* The minimum size of an audio packet split by splitAudioPacket(), in
* seconds. Audio packets smaller than this will not be split, nor will the
* split result of a larger packet ever be smaller in size than this
* minimum.
*
* @private
* @constant
* @type {Number}
*/
var MIN_SPLIT_SIZE = 0.02;
/**
* The maximum amount of latency to allow between the buffered data stream
* and the playback position, in seconds. Initially, this is set to
* roughly one third of a second.
*
* @private
* @type {Number}
*/
var maxLatency = 0.3;
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet
* received by this audio player. This depends only on the size of each
* sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {Number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The queue of all pending audio packets, as an array of sample arrays.
* Audio packets which are pending playback will be added to this queue for
* further manipulation prior to scheduling via the Web Audio API. Once an
* audio packet leaves this queue and is scheduled via the Web Audio API,
* no further modifications can be made to that packet.
*
* @private
* @type {SampleArray[]}
*/
var packetQueue = [];
/**
* Given an array of audio packets, returns a single audio packet
* containing the concatenation of those packets.
*
* @private
* @param {SampleArray[]} packets
* The array of audio packets to concatenate.
*
* @returns {SampleArray}
* A single audio packet containing the concatenation of all given
* audio packets. If no packets are provided, this will be undefined.
*/
var joinAudioPackets = function joinAudioPackets(packets) {
// Do not bother joining if one or fewer packets are in the queue
if (packets.length <= 1)
return packets[0];
// Determine total sample length of the entire queue
var totalLength = 0;
packets.forEach(function addPacketLengths(packet) {
totalLength += packet.length;
});
// Append each packet within queue
var offset = 0;
var joined = new SampleArray(totalLength);
packets.forEach(function appendPacket(packet) {
joined.set(packet, offset);
offset += packet.length;
});
return joined;
};
/**
* Given a single packet of audio data, splits off an arbitrary length of
* audio data from the beginning of that packet, returning the split result
* as an array of two packets. The split location is determined through an
* algorithm intended to minimize the liklihood of audible clicking between
* packets. If no such split location is possible, an array containing only
* the originally-provided audio packet is returned.
*
* @private
* @param {SampleArray} data
* The audio packet to split.
*
* @returns {SampleArray[]}
* An array of audio packets containing the result of splitting the
* provided audio packet. If splitting is possible, this array will
* contain two packets. If splitting is not possible, this array will
* contain only the originally-provided packet.
*/
var splitAudioPacket = function splitAudioPacket(data) {
var minValue = Number.MAX_VALUE;
var optimalSplitLength = data.length;
// Calculate number of whole samples in the provided audio packet AND
// in the minimum possible split packet
var samples = Math.floor(data.length / format.channels);
var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE);
// Calculate the beginning of the "end" of the audio packet
var start = Math.max(
format.channels * minSplitSamples,
format.channels * (samples - minSplitSamples)
);
// For all samples at the end of the given packet, find a point where
// the perceptible volume across all channels is lowest (and thus is
// the optimal point to split)
for (var offset = start; offset < data.length; offset += format.channels) {
// Calculate the sum of all values across all channels (the result
// will be proportional to the average volume of a sample)
var totalValue = 0;
for (var channel = 0; channel < format.channels; channel++) {
totalValue += Math.abs(data[offset + channel]);
}
// If this is the smallest average value thus far, set the split
// length such that the first packet ends with the current sample
if (totalValue <= minValue) {
optimalSplitLength = offset + format.channels;
minValue = totalValue;
}
}
// If packet is not split, return the supplied packet untouched
if (optimalSplitLength === data.length)
return [data];
// Otherwise, split the packet into two new packets according to the
// calculated optimal split length
return [
new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)),
new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample))
];
};
/**
* Pushes the given packet of audio data onto the playback queue. Unlike
* other private functions within Guacamole.RawAudioPlayer, the type of the
* ArrayBuffer packet of audio data here need not be specific to the type
* of audio (as with SampleArray). The ArrayBuffer type provided by a
* Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary
* conversions will be performed automatically internally.
*
* @private
* @param {ArrayBuffer} data
* A raw packet of audio data that should be pushed onto the audio
* playback queue.
*/
var pushAudioPacket = function pushAudioPacket(data) {
packetQueue.push(new SampleArray(data));
};
/**
* Shifts off and returns a packet of audio data from the beginning of the
* playback queue. The length of this audio packet is determined
* dynamically according to the click-reduction algorithm implemented by
* splitAudioPacket().
*
* @private
* @returns {SampleArray}
* A packet of audio data pulled from the beginning of the playback
* queue.
*/
var shiftAudioPacket = function shiftAudioPacket() {
// Flatten data in packet queue
var data = joinAudioPackets(packetQueue);
if (!data)
return null;
// Pull an appropriate amount of data from the front of the queue
packetQueue = splitAudioPacket(data);
data = packetQueue.shift();
return data;
};
/**
* Converts the given audio packet into an AudioBuffer, ready for playback
* by the Web Audio API. Unlike the raw audio packets received by this
* audio player, AudioBuffers require floating point samples and are split
* into isolated planes of channel-specific data.
*
* @private
* @param {SampleArray} data
* The raw audio packet that should be converted into a Web Audio API
* AudioBuffer.
*
* @returns {AudioBuffer}
* A new Web Audio API AudioBuffer containing the provided audio data,
* converted to the format used by the Web Audio API.
*/
var toAudioBuffer = function toAudioBuffer(data) {
// Calculate total number of samples
var samples = data.length / format.channels;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Get audio buffer for specified format
var audioBuffer = context.createBuffer(format.channels, samples, format.rate);
// Convert each channel
for (var channel = 0; channel < format.channels; channel++) {
var audioData = audioBuffer.getChannelData(channel);
// Fill audio buffer with data for channel
var offset = channel;
for (var i = 0; i < samples; i++) {
audioData[i] = data[offset] / maxSampleValue;
offset += format.channels;
}
}
return audioBuffer;
};
// Defer playback of received audio packets slightly
reader.ondata = function playReceivedAudio(data) {
// Push received samples onto queue
pushAudioPacket(new SampleArray(data));
// Shift off an arbitrary packet of audio data from the queue (this may
// be different in size from the packet just pushed)
var packet = shiftAudioPacket();
if (!packet)
return;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Set up buffer source
var source = context.createBufferSource();
source.connect(context.destination);
// Use noteOn() instead of start() if necessary
if (!source.start)
source.start = source.noteOn;
// Schedule packet
source.buffer = toAudioBuffer(packet);
source.start(nextPacketTime);
// Update timeline by duration of scheduled packet
nextPacketTime += packet.length / format.channels / format.rate;
};
/** @override */
this.sync = function sync() {
// Calculate elapsed time since last sync
var now = context.currentTime;
// Reschedule future playback time such that playback latency is
// bounded within a reasonable latency threshold
nextPacketTime = Math.min(nextPacketTime, now + maxLatency);
};
};
Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer();
/**
* Determines whether the given mimetype is supported by
* Guacamole.RawAudioPlayer.
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by Guacamole.RawAudioPlayer,
* false otherwise.
*/
Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return false;
return Guacamole.RawAudioFormat.parse(mimetype) !== null;
};
/**
* Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only
* the core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a raw audio mimetype that may be supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding
* any parameters. If the necessary JavaScript APIs for playing raw audio
* are absent, this list will be empty.
*/
Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return [];
// We support 8-bit and 16-bit raw PCM
return [
'audio/L8',
'audio/L16'
];
};
var Guacamole = Guacamole || {};
/**
* Abstract audio recorder which streams arbitrary audio data to an underlying
* Guacamole.OutputStream. It is up to implementations of this class to provide
* some means of handling this Guacamole.OutputStream. Data produced by the
* recorder is to be sent along the provided stream immediately.
*
* @constructor
*/
Guacamole.AudioRecorder = function AudioRecorder() {
/**
* Callback which is invoked when the audio recording process has stopped
* and the underlying Guacamole stream has been closed normally. Audio will
* only resume recording if a new Guacamole.AudioRecorder is started. This
* Guacamole.AudioRecorder instance MAY NOT be reused.
*
* @event
*/
this.onclose = null;
/**
* Callback which is invoked when the audio recording process cannot
* continue due to an error, if it has started at all. The underlying
* Guacamole stream is automatically closed. Future attempts to record
* audio should not be made, and this Guacamole.AudioRecorder instance
* MAY NOT be reused.
*
* @event
*/
this.onerror = null;
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioRecorder, and thus will be properly handled
* by Guacamole.AudioRecorder.getInstance().
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioRecorder, false otherwise.
*/
Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioRecorder.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, in rough order of priority. Beware that only the
* core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, excluding any parameters.
*/
Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioRecorder.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioRecorder providing support for the
* given audio format. If support for the given audio format is not available,
* null is returned.
*
* @param {Guacamole.OutputStream} stream
* The Guacamole.OutputStream to send audio data through.
*
* @param {String} mimetype
* The mimetype of the audio data to be sent along the provided stream.
*
* @return {Guacamole.AudioRecorder}
* A Guacamole.AudioRecorder instance supporting the given mimetype and
* writing to the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {
// Use raw audio recorder if possible
if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
return new Guacamole.RawAudioRecorder(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioRecorder providing support for raw PCM
* format audio. This recorder relies only on the Web Audio API and does not
* require any browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioRecorder
* @param {Guacamole.OutputStream} stream
* The Guacamole.OutputStream to write audio data to.
*
* @param {String} mimetype
* The mimetype of the audio data to send along the provided stream, which
* must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
* such as: "audio/L16;rate=44100,channels=2".
*/
Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) {
/**
* Reference to this RawAudioRecorder.
*
* @private
* @type {Guacamole.RawAudioRecorder}
*/
var recorder = this;
/**
* The size of audio buffer to request from the Web Audio API when
* recording or processing audio, in sample-frames. This must be a power of
* two between 256 and 16384 inclusive, as required by
* AudioContext.createScriptProcessor().
*
* @private
* @constant
* @type {Number}
*/
var BUFFER_SIZE = 2048;
/**
* The window size to use when applying Lanczos interpolation, commonly
* denoted by the variable "a".
* See: https://en.wikipedia.org/wiki/Lanczos_resampling
*
* @private
* @contant
* @type Number
*/
var LANCZOS_WINDOW_SIZE = 3;
/**
* The format of audio this recorder will encode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
// Some browsers do not implement navigator.mediaDevices - this
// shims in this functionality to ensure code compatibility.
if (!navigator.mediaDevices)
navigator.mediaDevices = {};
// Browsers that either do not implement navigator.mediaDevices
// at all or do not implement it completely need the getUserMedia
// method defined. This shims in this function by detecting
// one of the supported legacy methods.
if (!navigator.mediaDevices.getUserMedia)
navigator.mediaDevices.getUserMedia = (navigator.getUserMedia
|| navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia
|| navigator.msGetUserMedia).bind(navigator);
/**
* Guacamole.ArrayBufferWriter wrapped around the audio output stream
* provided when this Guacamole.RawAudioRecorder was created.
*
* @private
* @type {Guacamole.ArrayBufferWriter}
*/
var writer = new Guacamole.ArrayBufferWriter(stream);
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet sent
* by this audio recorder. This depends only on the size of each sample,
* and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {Number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The total number of audio samples read from the local audio input device
* over the life of this audio recorder.
*
* @private
* @type {Number}
*/
var readSamples = 0;
/**
* The total number of audio samples written to the underlying Guacamole
* connection over the life of this audio recorder.
*
* @private
* @type {Number}
*/
var writtenSamples = 0;
/**
* The audio stream provided by the browser, if allowed. If no stream has
* yet been received, this will be null.
*
* @type MediaStream
*/
var mediaStream = null;
/**
* The source node providing access to the local audio input device.
*
* @private
* @type {MediaStreamAudioSourceNode}
*/
var source = null;
/**
* The script processing node which receives audio input from the media
* stream source node as individual audio buffers.
*
* @private
* @type {ScriptProcessorNode}
*/
var processor = null;
/**
* The normalized sinc function. The normalized sinc function is defined as
* 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
*
* See: https://en.wikipedia.org/wiki/Sinc_function
*
* @private
* @param {Number} x
* The point at which the normalized sinc function should be computed.
*
* @returns {Number}
* The value of the normalized sinc function at x.
*/
var sinc = function sinc(x) {
// The value of sinc(0) is defined as 1
if (x === 0)
return 1;
// Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
var piX = Math.PI * x;
return Math.sin(piX) / piX;
};
/**
* Calculates the value of the Lanczos kernal at point x for a given window
* size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
*