diff --git a/src/protocols/spice/channels/audio.c b/src/protocols/spice/channels/audio.c index ecabac10..e18ad2b7 100644 --- a/src/protocols/spice/channels/audio.c +++ b/src/protocols/spice/channels/audio.c @@ -25,6 +25,9 @@ #include #include +#include +#include + void guac_spice_client_audio_playback_data_handler( SpicePlaybackChannel* channel, gpointer data, gint size, guac_client* client) { @@ -76,10 +79,220 @@ void guac_spice_client_audio_playback_stop_handler( } +/** + * Parses the given raw audio mimetype, producing the corresponding rate, + * number of channels, and bytes per sample. + * + * @param mimetype + * The raw audio mimetype to parse. + * + * @param rate + * A pointer to an int where the sample rate for the PCM format described + * by the given mimetype should be stored. + * + * @param channels + * A pointer to an int where the number of channels used by the PCM format + * described by the given mimetype should be stored. + * + * @param bps + * A pointer to an int where the number of bytes used the PCM format for + * each sample (independent of number of channels) described by the given + * mimetype should be stored. + * + * @return + * Zero if the given mimetype is a raw audio mimetype and has been parsed + * successfully, non-zero otherwise. + */ +static int guac_spice_audio_parse_mimetype(const char* mimetype, int* rate, + int* channels, int* bps) { + + int parsed_rate = -1; + int parsed_channels = 1; + int parsed_bps; + + /* PCM audio with one byte per sample */ + if (strncmp(mimetype, "audio/L8;", 9) == 0) { + mimetype += 8; /* Advance to semicolon ONLY */ + parsed_bps = 1; + } + + /* PCM audio with two bytes per sample */ + else if (strncmp(mimetype, "audio/L16;", 10) == 0) { + mimetype += 9; /* Advance to semicolon ONLY */ + parsed_bps = 2; + } + + /* Unsupported mimetype */ + else + return 1; + + /* Parse each parameter name/value pair within the mimetype */ + do { + + /* Advance to first character of parameter (current is either a + * semicolon or a comma) */ + mimetype++; + + /* Parse number of channels */ + if (strncmp(mimetype, "channels=", 9) == 0) { + + mimetype += 9; + parsed_channels = strtol(mimetype, (char**) &mimetype, 10); + + /* Fail if value invalid / out of range */ + if (errno == EINVAL || errno == ERANGE) + return 1; + + } + + /* Parse number of rate */ + else if (strncmp(mimetype, "rate=", 5) == 0) { + + mimetype += 5; + parsed_rate = strtol(mimetype, (char**) &mimetype, 10); + + /* Fail if value invalid / out of range */ + if (errno == EINVAL || errno == ERANGE) + return 1; + + } + + /* Advance to next parameter */ + mimetype = strchr(mimetype, ','); + + } while (mimetype != NULL); + + /* Mimetype is invalid if rate was not specified */ + if (parsed_rate == -1) + return 1; + + /* Parse success */ + *rate = parsed_rate; + *channels = parsed_channels; + *bps = parsed_bps; + + return 0; + +} + +static int guac_spice_audio_blob_handler(guac_user* user, guac_stream* stream, + void* data, int length) { + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + + /* Write blob to audio stream */ + spice_record_channel_send_data(spice_client->record_channel, data, length, (unsigned long) time(NULL)); + + return 0; + +} + +static int guac_spice_audio_end_handler(guac_user* user, guac_stream* stream) { + + /* Ignore - the RECORD_CHANNEL channel will simply not receive anything */ + return 0; + +} + + +int guac_spice_client_audio_record_handler(guac_user* user, guac_stream* stream, + char* mimetype) { + + guac_user_log(user, GUAC_LOG_DEBUG, "Calling audio input handler."); + + guac_client* client = user->client; + guac_spice_client* spice_client = (guac_spice_client*) client->data; + spice_client->audio_input = stream; + + int rate; + int channels; + int bps; + + /* Parse mimetype, abort on parse error */ + if (guac_spice_audio_parse_mimetype(mimetype, &rate, &channels, &bps)) { + guac_user_log(user, GUAC_LOG_WARNING, "Denying user audio stream with " + "unsupported mimetype: \"%s\"", mimetype); + guac_protocol_send_ack(user->socket, stream, "Unsupported audio " + "mimetype", GUAC_PROTOCOL_STATUS_CLIENT_BAD_TYPE); + return 0; + } + + /* Init stream data */ + stream->blob_handler = guac_spice_audio_blob_handler; + stream->end_handler = guac_spice_audio_end_handler; + + return 0; + + +} + +/** + * Sends an "ack" instruction over the socket associated with the Guacamole + * stream over which audio data is being received. The "ack" instruction will + * only be sent if the Guacamole audio stream has been established (through + * receipt of an "audio" instruction), is still open (has not received an "end" + * instruction nor been associated with an "ack" having an error code), and is + * associated with an active Spice RECORD_CHANNEL channel. + * + * @param user + * The guac_user associated with the audio input stream. + * + * @param stream + * The guac_stream associated with the audio input for the client. + * + * @param message + * An arbitrary human-readable message to send along with the "ack". + * + * @param status + * The Guacamole protocol status code to send with the "ack". This should + * be GUAC_PROTOCOL_STATUS_SUCCESS if the audio stream has been set up + * successfully or GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED if the audio stream + * has been closed (but may usable again if reopened). + */ +static void guac_spice_audio_stream_ack(guac_user* user, guac_stream* stream, + const char* message, guac_protocol_status status) { + + /* Do not send ack unless both sides of the audio stream are ready */ + if (user == NULL || stream == NULL) + return; + + /* Send ack instruction */ + guac_protocol_send_ack(user->socket, stream, message, status); + guac_socket_flush(user->socket); + +} + +static void* spice_client_record_start_callback(guac_user* owner, void* data) { + + guac_spice_client* spice_client = (guac_spice_client*) data; + + guac_spice_audio_stream_ack(owner, spice_client->audio_input, "OK", + GUAC_PROTOCOL_STATUS_SUCCESS); + + return NULL; + +} + +static void* spice_client_record_stop_callback(guac_user* owner, void* data) { + + guac_spice_client* spice_client = (guac_spice_client*) data; + + /* The stream is now closed */ + guac_spice_audio_stream_ack(owner, spice_client->audio_input, "CLOSED", + GUAC_PROTOCOL_STATUS_RESOURCE_CLOSED); + + return NULL; + +} + void guac_spice_client_audio_record_start_handler(SpiceRecordChannel* channel, gint format, gint channels, gint rate, guac_client* client) { - + guac_client_log(client, GUAC_LOG_DEBUG, "Calling audio record start handler."); + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_for_owner(client, spice_client_record_start_callback, spice_client); } @@ -87,5 +300,8 @@ void guac_spice_client_audio_record_stop_handler(SpiceRecordChannel* channel, guac_client* client) { guac_client_log(client, GUAC_LOG_DEBUG, "Calling audio record stop handler."); - + + guac_spice_client* spice_client = (guac_spice_client*) client->data; + guac_client_for_owner(client, spice_client_record_stop_callback, spice_client); + } \ No newline at end of file diff --git a/src/protocols/spice/channels/audio.h b/src/protocols/spice/channels/audio.h index 1097bd9f..789131a9 100644 --- a/src/protocols/spice/channels/audio.h +++ b/src/protocols/spice/channels/audio.h @@ -95,6 +95,11 @@ void guac_spice_client_audio_playback_start_handler( void guac_spice_client_audio_playback_stop_handler( SpicePlaybackChannel* channel, guac_client* client); +/** + * Handler for inbound audio data (audio input). + */ +guac_user_audio_handler guac_spice_client_audio_record_handler; + /** * The callback function invoked by the client when the SPICE server requests * that the client begin recording audio data to send to the server. diff --git a/src/protocols/spice/client.c b/src/protocols/spice/client.c index 02903f65..17f95be9 100644 --- a/src/protocols/spice/client.c +++ b/src/protocols/spice/client.c @@ -327,7 +327,7 @@ void guac_spice_client_channel_handler(SpiceSession *spice_session, } /* Check for audio recording channel and set up the channel. */ - if (SPICE_IS_RECORD_CHANNEL(channel) && settings->audio_enabled) { + if (SPICE_IS_RECORD_CHANNEL(channel) && settings->audio_input_enabled) { guac_client_log(client, GUAC_LOG_DEBUG, "Setting up audio record channel."); spice_client->record_channel = SPICE_RECORD_CHANNEL(channel); g_signal_connect(channel, SPICE_SIGNAL_RECORD_START, diff --git a/src/protocols/spice/settings.c b/src/protocols/spice/settings.c index b128bb29..07744457 100644 --- a/src/protocols/spice/settings.c +++ b/src/protocols/spice/settings.c @@ -55,6 +55,7 @@ const char* GUAC_SPICE_CLIENT_ARGS[] = { "clipboard-encoding", "enable-audio", + "enable-audio-input", "file-transfer", "file-directory", "file-transfer-ro", @@ -198,6 +199,11 @@ enum SPICE_ARGS_IDX { */ IDX_ENABLE_AUDIO, + /** + * "true" if audio input should be enabled, "false" or blank otherwise. + */ + IDX_ENABLE_AUDIO_INPUT, + /** * "true" if file transfer should be enabled, "false" or blank otherwise. */ @@ -474,6 +480,11 @@ guac_spice_settings* guac_spice_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, IDX_ENABLE_AUDIO, false); + /* Audio input enable/disable */ + settings->audio_input_enabled = + guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, + IDX_ENABLE_AUDIO_INPUT, false); + /* File transfer enable/disable */ settings->file_transfer = guac_user_parse_args_boolean(user, GUAC_SPICE_CLIENT_ARGS, argv, diff --git a/src/protocols/spice/settings.h b/src/protocols/spice/settings.h index 2e467155..a413fdc8 100644 --- a/src/protocols/spice/settings.h +++ b/src/protocols/spice/settings.h @@ -111,6 +111,11 @@ typedef struct guac_spice_settings { * Whether audio is enabled. */ bool audio_enabled; + + /** + * Whether audio input is enabled. + */ + bool audio_input_enabled; /** * If file transfer capability should be enabled. diff --git a/src/protocols/spice/spice.h b/src/protocols/spice/spice.h index ca40349b..b1729ba9 100644 --- a/src/protocols/spice/spice.h +++ b/src/protocols/spice/spice.h @@ -163,10 +163,15 @@ typedef struct guac_spice_client { pthread_mutex_t message_lock; /** - * Audio output, if any. + * Audio output stream, if any. */ guac_audio_stream* audio_playback; + /** + * Audio input stream, if any. + */ + guac_stream* audio_input; + } guac_spice_client; /** diff --git a/src/protocols/spice/user.c b/src/protocols/spice/user.c index d54044bd..8ae41be9 100644 --- a/src/protocols/spice/user.c +++ b/src/protocols/spice/user.c @@ -19,6 +19,7 @@ #include "config.h" +#include "channels/audio.h" #include "channels/clipboard.h" #include "input.h" #include "common/display.h" @@ -66,6 +67,10 @@ int guac_spice_user_join_handler(guac_user* user, int argc, char** argv) { return 1; } + /* Handle inbound audio streams if audio input is enabled */ + if (settings->audio_input_enabled) + user->audio_handler = guac_spice_client_audio_record_handler; + } /* If not owner, synchronize with current state */