mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
Merge pull request #257 from glyptodon/audio-timing
GUAC-427: Limit audio latency with respect to graphical frames
This commit is contained in:
@@ -24,46 +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)
|
||||
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;
|
||||
|
||||
};
|
||||
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user