mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 05:31:22 +00:00
GUAC-427: Limit audio latency to reasonable bounds relative to sync instructions.
This commit is contained in:
@@ -24,48 +24,86 @@ var Guacamole = Guacamole || {};
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract audio channel which queues and plays arbitrary audio data.
|
* Abstract audio channel which queues and plays arbitrary audio data.
|
||||||
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
Guacamole.AudioChannel = function() {
|
Guacamole.AudioChannel = function AudioChannel() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to this AudioChannel.
|
* Reference to this AudioChannel.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
|
* @type Guacamole.AudioChannel
|
||||||
*/
|
*/
|
||||||
var channel = this;
|
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
|
* @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
|
* 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
|
* queued data has been played. If no data has been queued, the data will
|
||||||
* play immediately.
|
* play immediately.
|
||||||
*
|
*
|
||||||
* @param {String} mimetype The mimetype of the data provided.
|
* @param {String} mimetype
|
||||||
* @param {Number} duration The duration of the data provided, in
|
* The mimetype of the audio data provided.
|
||||||
* milliseconds.
|
*
|
||||||
* @param {Blob} data The blob data to play.
|
* @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 =
|
var packet = new Guacamole.AudioChannel.Packet(mimetype, data);
|
||||||
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.
|
// Schedule packet
|
||||||
if (next_packet_time < now) {
|
packet.play(nextPacketTime);
|
||||||
duration += next_packet_time - now;
|
|
||||||
next_packet_time = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule next packet
|
// Update timeline
|
||||||
packet.play(next_packet_time);
|
nextPacketTime += duration;
|
||||||
next_packet_time += duration;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -77,12 +77,17 @@ Guacamole.Client = function(tunnel) {
|
|||||||
*/
|
*/
|
||||||
var layers = {};
|
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
|
// No initial parsers
|
||||||
var parsers = [];
|
var parsers = [];
|
||||||
|
|
||||||
// No initial audio channels
|
|
||||||
var audio_channels = [];
|
|
||||||
|
|
||||||
// No initial streams
|
// No initial streams
|
||||||
var streams = [];
|
var streams = [];
|
||||||
|
|
||||||
@@ -494,6 +499,27 @@ Guacamole.Client = function(tunnel) {
|
|||||||
*/
|
*/
|
||||||
this.onsync = null;
|
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.
|
* Returns the layer with the given index, creating it if necessary.
|
||||||
* Positive indices refer to visible layers, an index of zero refers to
|
* 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.
|
* Handlers for all defined layer properties.
|
||||||
* @private
|
* @private
|
||||||
@@ -1097,11 +1111,21 @@ Guacamole.Client = function(tunnel) {
|
|||||||
var timestamp = parseInt(parameters[0]);
|
var timestamp = parseInt(parameters[0]);
|
||||||
|
|
||||||
// Flush display, send sync when done
|
// 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) {
|
if (timestamp !== currentTimestamp) {
|
||||||
tunnel.sendMessage("sync", timestamp);
|
tunnel.sendMessage("sync", timestamp);
|
||||||
currentTimestamp = timestamp;
|
currentTimestamp = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If received first update, no longer waiting.
|
// If received first update, no longer waiting.
|
||||||
|
Reference in New Issue
Block a user