GUAC-427: Limit audio latency to reasonable bounds relative to sync instructions.

This commit is contained in:
Michael Jumper
2015-09-04 18:57:20 -07:00
parent 193f2c676d
commit 8a9c7ce35f
2 changed files with 97 additions and 35 deletions

View File

@@ -24,48 +24,86 @@ var Guacamole = Guacamole || {};
/**
* Abstract audio channel which queues and plays arbitrary audio data.
*
* @constructor
*/
Guacamole.AudioChannel = function() {
Guacamole.AudioChannel = function AudioChannel() {
/**
* Reference to this AudioChannel.
*
* @private
* @type Guacamole.AudioChannel
*/
var channel = this;
/**
* When the next packet should play.
* The earliest possible time that the next packet could play without
* overlapping an already-playing packet, in milliseconds.
*
* @private
* @type Number
*/
var next_packet_time = 0;
var nextPacketTime = Guacamole.AudioChannel.getTimestamp();
/**
* The last time that sync() was called, in milliseconds. If sync() has
* never been called, this will be the time the Guacamole.AudioChannel
* was created.
*
* @type Number
*/
var lastSync = nextPacketTime;
/**
* Notifies this Guacamole.AudioChannel that all audio up to the current
* point in time has been given via play(), and that any difference in time
* between queued audio packets and the current time can be considered
* latency.
*/
this.sync = function sync() {
// Calculate elapsed time since last sync
var now = Guacamole.AudioChannel.getTimestamp();
var elapsed = now - lastSync;
// Reschedule future playback time such that playback latency is
// bounded within the duration of the last audio frame
nextPacketTime = Math.min(nextPacketTime, now + elapsed);
// Record sync time
lastSync = now;
};
/**
* Queues up the given data for playing by this channel once all previously
* queued data has been played. If no data has been queued, the data will
* play immediately.
*
* @param {String} mimetype The mimetype of the data provided.
* @param {Number} duration The duration of the data provided, in
* milliseconds.
* @param {Blob} data The blob data to play.
* @param {String} mimetype
* The mimetype of the audio data provided.
*
* @param {Number} duration
* The duration of the data provided, in milliseconds.
*
* @param {Blob} data
* The blob of audio data to play.
*/
this.play = function(mimetype, duration, data) {
this.play = function play(mimetype, duration, data) {
var packet =
new Guacamole.AudioChannel.Packet(mimetype, data);
var packet = new Guacamole.AudioChannel.Packet(mimetype, data);
var now = Guacamole.AudioChannel.getTimestamp();
// Determine exactly when packet CAN play
var packetTime = Guacamole.AudioChannel.getTimestamp();
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// If underflow is detected, reschedule new packets relative to now.
if (next_packet_time < now) {
duration += next_packet_time - now;
next_packet_time = now;
}
// Schedule packet
packet.play(nextPacketTime);
// Schedule next packet
packet.play(next_packet_time);
next_packet_time += duration;
// Update timeline
nextPacketTime += duration;
};

View File

@@ -77,12 +77,17 @@ Guacamole.Client = function(tunnel) {
*/
var layers = {};
/**
* All audio channels currentl in use by the client. Initially, this will
* be empty, but channels may be allocated by the server upon request.
*
* @type Object.<Number, Guacamole.AudioChannel>
*/
var audioChannels = {};
// No initial parsers
var parsers = [];
// No initial audio channels
var audio_channels = [];
// No initial streams
var streams = [];
@@ -494,6 +499,27 @@ Guacamole.Client = function(tunnel) {
*/
this.onsync = null;
/**
* Returns the audio channel having the given index, creating a new channel
* if necessary.
*
* @param {Number} index
* The index of the audio channel to retrieve.
*
* @returns {Guacamole.AudioChannel}
* The audio channel having the given index.
*/
var getAudioChannel = function getAudioChannel(index) {
// Get audio channel, creating it first if necessary
var audio_channel = audioChannels[index];
if (!audio_channel)
audio_channel = audioChannels[index] = new Guacamole.AudioChannel();
return audio_channel;
};
/**
* Returns the layer with the given index, creating it if necessary.
* Positive indices refer to visible layers, an index of zero refers to
@@ -540,18 +566,6 @@ Guacamole.Client = function(tunnel) {
}
function getAudioChannel(index) {
var audio_channel = audio_channels[index];
// If audio channel not yet created, create it
if (audio_channel == null)
audio_channel = audio_channels[index] = new Guacamole.AudioChannel();
return audio_channel;
}
/**
* Handlers for all defined layer properties.
* @private
@@ -1097,11 +1111,21 @@ Guacamole.Client = function(tunnel) {
var timestamp = parseInt(parameters[0]);
// Flush display, send sync when done
display.flush(function __send_sync_response() {
display.flush(function displaySyncComplete() {
// Synchronize all audio channels
for (var index in audioChannels) {
var audioChannel = audioChannels[index];
if (audioChannel)
audioChannel.sync();
}
// Send sync response to server
if (timestamp !== currentTimestamp) {
tunnel.sendMessage("sync", timestamp);
currentTimestamp = timestamp;
}
});
// If received first update, no longer waiting.