diff options
Diffstat (limited to 'protocols/Tox/toxcore/toxav')
-rw-r--r-- | protocols/Tox/toxcore/toxav/Makefile.inc | 36 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/codec.c | 357 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/codec.h | 116 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/msi.c | 1947 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/msi.h | 267 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/rtp.c | 600 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/rtp.h | 196 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/toxav.c | 1148 | ||||
-rw-r--r-- | protocols/Tox/toxcore/toxav/toxav.h | 389 |
9 files changed, 5056 insertions, 0 deletions
diff --git a/protocols/Tox/toxcore/toxav/Makefile.inc b/protocols/Tox/toxcore/toxav/Makefile.inc new file mode 100644 index 0000000000..de8ef8ff38 --- /dev/null +++ b/protocols/Tox/toxcore/toxav/Makefile.inc @@ -0,0 +1,36 @@ +if BUILD_AV + +lib_LTLIBRARIES += libtoxav.la +libtoxav_la_include_HEADERS = ../toxav/toxav.h +libtoxav_la_includedir = $(includedir)/tox + +libtoxav_la_SOURCES = ../toxav/rtp.h \ + ../toxav/rtp.c \ + ../toxav/msi.h \ + ../toxav/msi.c \ + ../toxav/codec.h \ + ../toxav/codec.c \ + ../toxav/toxav.h \ + ../toxav/toxav.c + + +libtoxav_la_CFLAGS = -I../toxcore \ + -I../toxav \ + $(LIBSODIUM_CFLAGS) \ + $(NACL_CFLAGS) \ + $(AV_CFLAGS) \ + $(PTHREAD_CFLAGS) + +libtoxav_la_LDFLAGS = $(TOXAV_LT_LDFLAGS) \ + $(LIBSODIUM_LDFLAGS) \ + $(NACL_LDFLAGS) \ + $(EXTRA_LT_LDFLAGS) \ + $(WINSOCK2_LIBS) + +libtoxav_la_LIBADD = libtoxcore.la \ + $(LIBSODIUM_LIBS) \ + $(NACL_LIBS) \ + $(PTHREAD_LIBS) \ + $(AV_LIBS) + +endif
\ No newline at end of file diff --git a/protocols/Tox/toxcore/toxav/codec.c b/protocols/Tox/toxcore/toxav/codec.c new file mode 100644 index 0000000000..10dc4f53ff --- /dev/null +++ b/protocols/Tox/toxcore/toxav/codec.c @@ -0,0 +1,357 @@ +/** codec.c + * + * Audio and video codec intitialization, encoding/decoding and playback + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include "../toxcore/logger.h" + +#include <stdio.h> +#include <stdlib.h> +#include <math.h> +#include <assert.h> + +#include "rtp.h" +#include "codec.h" + +JitterBuffer *create_queue(unsigned int capacity) +{ + unsigned int size = 1; + + while (size <= capacity) { + size *= 2; + } + + JitterBuffer *q; + + if ( !(q = calloc(sizeof(JitterBuffer), 1)) ) return NULL; + + if (!(q->queue = calloc(sizeof(RTPMessage *), size))) { + free(q); + return NULL; + } + + q->size = size; + q->capacity = capacity; + return q; +} + +static void clear_queue(JitterBuffer *q) +{ + for (; q->bottom != q->top; ++q->bottom) { + if (q->queue[q->bottom % q->size]) { + rtp_free_msg(NULL, q->queue[q->bottom % q->size]); + q->queue[q->bottom % q->size] = NULL; + } + } +} + +void terminate_queue(JitterBuffer *q) +{ + if (!q) return; + + clear_queue(q); + free(q->queue); + free(q); +} + +void queue(JitterBuffer *q, RTPMessage *pk) +{ + uint16_t sequnum = pk->header->sequnum; + + unsigned int num = sequnum % q->size; + + if ((uint32_t)(sequnum - q->bottom) > q->size) { + clear_queue(q); + q->bottom = sequnum; + q->queue[num] = pk; + q->top = sequnum + 1; + return; + } + + if (q->queue[num]) + return; + + q->queue[num] = pk; + + if ((sequnum - q->bottom) >= (q->top - q->bottom)) + q->top = sequnum + 1; +} + +/* success is 0 when there is nothing to dequeue, 1 when there's a good packet, 2 when there's a lost packet */ +RTPMessage *dequeue(JitterBuffer *q, int *success) +{ + if (q->top == q->bottom) { + *success = 0; + return NULL; + } + + unsigned int num = q->bottom % q->size; + + if (q->queue[num]) { + RTPMessage *ret = q->queue[num]; + q->queue[num] = NULL; + ++q->bottom; + *success = 1; + return ret; + } + + if ((uint32_t)(q->top - q->bottom) > q->capacity) { + ++q->bottom; + *success = 2; + return NULL; + } + + *success = 0; + return NULL; +} + + +int init_video_decoder(CodecState *cs) +{ + int rc = vpx_codec_dec_init_ver(&cs->v_decoder, VIDEO_CODEC_DECODER_INTERFACE, NULL, 0, VPX_DECODER_ABI_VERSION); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Init video_decoder failed: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + return 0; +} + +int init_audio_decoder(CodecState *cs, uint32_t audio_channels) +{ + int rc; + cs->audio_decoder = opus_decoder_create(cs->audio_sample_rate, audio_channels, &rc ); + + if ( rc != OPUS_OK ) { + LOGGER_ERROR("Error while starting audio decoder: %s", opus_strerror(rc)); + return -1; + } + + cs->audio_decoder_channels = audio_channels; + return 0; +} + +int reconfigure_video_encoder_resolution(CodecState *cs, uint16_t width, uint16_t height) +{ + vpx_codec_enc_cfg_t cfg = *cs->v_encoder.config.enc; + + if (cfg.g_w == width && cfg.g_h == height) + return 0; + + if (width * height > cs->max_width * cs->max_height) + return -1; + + LOGGER_DEBUG("New video resolution: %u %u", width, height); + cfg.g_w = width; + cfg.g_h = height; + int rc = vpx_codec_enc_config_set(&cs->v_encoder, &cfg); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Failed to set encoder control setting: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + return 0; +} + +int reconfigure_video_encoder_bitrate(CodecState *cs, uint32_t video_bitrate) +{ + vpx_codec_enc_cfg_t cfg = *cs->v_encoder.config.enc; + + if (cfg.rc_target_bitrate == video_bitrate) + return 0; + + LOGGER_DEBUG("New video bitrate: %u", video_bitrate); + cfg.rc_target_bitrate = video_bitrate; + int rc = vpx_codec_enc_config_set(&cs->v_encoder, &cfg); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Failed to set encoder control setting: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + return 0; +} + +int init_video_encoder(CodecState *cs, uint16_t max_width, uint16_t max_height, uint32_t video_bitrate) +{ + vpx_codec_enc_cfg_t cfg; + int rc = vpx_codec_enc_config_default(VIDEO_CODEC_ENCODER_INTERFACE, &cfg, 0); + + if (rc != VPX_CODEC_OK) { + LOGGER_ERROR("Failed to get config: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + cfg.rc_target_bitrate = video_bitrate; + cfg.g_w = max_width; + cfg.g_h = max_height; + cfg.g_pass = VPX_RC_ONE_PASS; + cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT | VPX_ERROR_RESILIENT_PARTITIONS; + cfg.g_lag_in_frames = 0; + cfg.kf_min_dist = 0; + cfg.kf_max_dist = 300; + cfg.kf_mode = VPX_KF_AUTO; + + cs->max_width = max_width; + cs->max_height = max_height; + cs->bitrate = video_bitrate; + + rc = vpx_codec_enc_init_ver(&cs->v_encoder, VIDEO_CODEC_ENCODER_INTERFACE, &cfg, 0, VPX_ENCODER_ABI_VERSION); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Failed to initialize encoder: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + rc = vpx_codec_control(&cs->v_encoder, VP8E_SET_CPUUSED, 7); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Failed to set encoder control setting: %s", vpx_codec_err_to_string(rc)); + return -1; + } + + return 0; +} + +int init_audio_encoder(CodecState *cs, uint32_t audio_channels) +{ + int rc = OPUS_OK; + cs->audio_encoder = opus_encoder_create(cs->audio_sample_rate, audio_channels, OPUS_APPLICATION_AUDIO, &rc); + + if ( rc != OPUS_OK ) { + LOGGER_ERROR("Error while starting audio encoder: %s", opus_strerror(rc)); + return -1; + } + + rc = opus_encoder_ctl(cs->audio_encoder, OPUS_SET_BITRATE(cs->audio_bitrate)); + + if ( rc != OPUS_OK ) { + LOGGER_ERROR("Error while setting encoder ctl: %s", opus_strerror(rc)); + return -1; + } + + rc = opus_encoder_ctl(cs->audio_encoder, OPUS_SET_COMPLEXITY(10)); + + if ( rc != OPUS_OK ) { + LOGGER_ERROR("Error while setting encoder ctl: %s", opus_strerror(rc)); + return -1; + } + + cs->audio_encoder_channels = audio_channels; + return 0; +} + + +CodecState *codec_init_session ( uint32_t audio_bitrate, + uint16_t audio_frame_duration, + uint32_t audio_sample_rate, + uint32_t encoder_audio_channels, + uint32_t decoder_audio_channels, + uint32_t audio_VAD_tolerance_ms, + uint16_t max_video_width, + uint16_t max_video_height, + uint32_t video_bitrate ) +{ + CodecState *retu = calloc(sizeof(CodecState), 1); + + if (!retu) return NULL; + + retu->audio_bitrate = audio_bitrate; + retu->audio_sample_rate = audio_sample_rate; + + /* Encoders */ + if (!max_video_width || !max_video_height) { /* Disable video */ + /*video_width = 320; + video_height = 240; */ + } else { + retu->capabilities |= ( 0 == init_video_encoder(retu, max_video_width, max_video_height, + video_bitrate) ) ? v_encoding : 0; + retu->capabilities |= ( 0 == init_video_decoder(retu) ) ? v_decoding : 0; + } + + retu->capabilities |= ( 0 == init_audio_encoder(retu, encoder_audio_channels) ) ? a_encoding : 0; + retu->capabilities |= ( 0 == init_audio_decoder(retu, decoder_audio_channels) ) ? a_decoding : 0; + + if ( retu->capabilities == 0 ) { /* everything failed */ + free (retu); + return NULL; + } + + + retu->EVAD_tolerance = audio_VAD_tolerance_ms > audio_frame_duration ? + audio_VAD_tolerance_ms / audio_frame_duration : audio_frame_duration; + + return retu; +} + +void codec_terminate_session ( CodecState *cs ) +{ + if (!cs) return; + + if ( cs->audio_encoder ) + opus_encoder_destroy(cs->audio_encoder); + + if ( cs->audio_decoder ) + opus_decoder_destroy(cs->audio_decoder); + + if ( cs->capabilities & v_decoding ) + vpx_codec_destroy(&cs->v_decoder); + + if ( cs->capabilities & v_encoding ) + vpx_codec_destroy(&cs->v_encoder); + + LOGGER_DEBUG("Terminated codec state: %p", cs); + free(cs); +} + +static float calculate_sum_sq (int16_t *n, uint16_t k) +{ + float result = 0; + uint16_t i = 0; + + for ( ; i < k; i ++) result += (float) (n[i] * n[i]); + + return result; +} + +int energy_VAD(CodecState *cs, int16_t *PCM, uint16_t frame_size, float energy) +{ + float frame_energy = sqrt(calculate_sum_sq(PCM, frame_size)) / frame_size; + + if ( frame_energy > energy) { + cs->EVAD_tolerance_cr = cs->EVAD_tolerance; /* Reset counter */ + return 1; + } + + if ( cs->EVAD_tolerance_cr ) { + cs->EVAD_tolerance_cr --; + return 1; + } + + return 0; +} diff --git a/protocols/Tox/toxcore/toxav/codec.h b/protocols/Tox/toxcore/toxav/codec.h new file mode 100644 index 0000000000..db4fbea0dc --- /dev/null +++ b/protocols/Tox/toxcore/toxav/codec.h @@ -0,0 +1,116 @@ +/** codec.h + * + * Audio and video codec intitialization, encoding/decoding and playback + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef _CODEC_H_ +#define _CODEC_H_ + +#include <stdio.h> +#include <math.h> +#include <pthread.h> + +#include <vpx/vpx_decoder.h> +#include <vpx/vpx_encoder.h> +#include <vpx/vp8dx.h> +#include <vpx/vp8cx.h> +#define VIDEO_CODEC_DECODER_INTERFACE (vpx_codec_vp8_dx()) +#define VIDEO_CODEC_ENCODER_INTERFACE (vpx_codec_vp8_cx()) + +/* Audio encoding/decoding */ +#include <opus.h> + +typedef enum _Capabilities { + none, + a_encoding = 1 << 0, + a_decoding = 1 << 1, + v_encoding = 1 << 2, + v_decoding = 1 << 3 +} Capabilities; + +extern const uint16_t min_jbuf_size; + +typedef struct _CodecState { + + /* video encoding */ + vpx_codec_ctx_t v_encoder; + uint32_t frame_counter; + + /* video decoding */ + vpx_codec_ctx_t v_decoder; + int bitrate; + int max_width; + int max_height; + + /* audio encoding */ + OpusEncoder *audio_encoder; + int audio_bitrate; + int audio_sample_rate; + int audio_encoder_channels; + + /* audio decoding */ + OpusDecoder *audio_decoder; + int audio_decoder_channels; + + uint64_t capabilities; /* supports*/ + + /* Voice activity detection */ + uint32_t EVAD_tolerance; /* In frames */ + uint32_t EVAD_tolerance_cr; +} CodecState; + + +typedef struct _JitterBuffer { + RTPMessage **queue; + uint32_t size; + uint32_t capacity; + uint16_t bottom; + uint16_t top; +} JitterBuffer; + +JitterBuffer *create_queue(unsigned int capacity); +void terminate_queue(JitterBuffer *q); +void queue(JitterBuffer *q, RTPMessage *pk); +RTPMessage *dequeue(JitterBuffer *q, int *success); + + +CodecState *codec_init_session ( uint32_t audio_bitrate, + uint16_t audio_frame_duration, + uint32_t audio_sample_rate, + uint32_t encoder_audio_channels, + uint32_t decoder_audio_channels, + uint32_t audio_VAD_tolerance_ms, + uint16_t max_video_width, + uint16_t max_video_height, + uint32_t video_bitrate ); + +void codec_terminate_session(CodecState *cs); + +/* Reconfigure video encoder + return 0 on success. + return -1 on failure. */ +int reconfigure_video_encoder_resolution(CodecState *cs, uint16_t width, uint16_t height); +int reconfigure_video_encoder_bitrate(CodecState *cs, uint32_t video_bitrate); + +/* Calculate energy and return 1 if has voice, 0 if not */ +int energy_VAD(CodecState *cs, int16_t *PCM, uint16_t frame_size, float energy); + +#endif /* _CODEC_H_ */ diff --git a/protocols/Tox/toxcore/toxav/msi.c b/protocols/Tox/toxcore/toxav/msi.c new file mode 100644 index 0000000000..91742c3579 --- /dev/null +++ b/protocols/Tox/toxcore/toxav/msi.c @@ -0,0 +1,1947 @@ +/** msi.c + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include "../toxcore/logger.h" +#include "../toxcore/util.h" + +#include "msi.h" + +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <stdbool.h> + +#define MSI_MAXMSG_SIZE 256 + +/* Define default timeout for a request. + * There is no behavior specified by the msi on what will + * client do on timeout, but to call timeout callback. + */ +#define m_deftout 10000 /* in milliseconds */ + +/** + * Protocol: + * + * |id [1 byte]| |size [1 byte]| |data [$size bytes]| |...{repeat}| |0 {end byte}| + */ + +typedef uint8_t MSIRawCSettingsType[23]; + +typedef enum { + IDRequest = 1, + IDResponse, + IDReason, + IDCallId, + IDCSettings, + +} MSIHeaderID; + +typedef enum { + TypeRequest, + TypeResponse, + +} MSIMessageType; + +typedef enum { + invite, + start, + cancel, + reject, + end, + +} MSIRequest; + +typedef enum { + ringing, + starting, + ending, + error + +} MSIResponse; + + +#define GENERIC_HEADER(header, val_type) \ +typedef struct _MSIHeader##header { \ +val_type value; \ +_Bool exists; \ +} MSIHeader##header; + + +GENERIC_HEADER ( Request, MSIRequest ) +GENERIC_HEADER ( Response, MSIResponse ) +GENERIC_HEADER ( CallId, MSICallIDType ) +GENERIC_HEADER ( Reason, MSIReasonStrType ) +GENERIC_HEADER ( CSettings, MSIRawCSettingsType ) + + +/** + * @brief This is the message structure. It contains all of the headers and + * destination/source of the message stored in friend_id. + * + */ +typedef struct _MSIMessage { + + MSIHeaderRequest request; + MSIHeaderResponse response; + MSIHeaderReason reason; + MSIHeaderCallId callid; + MSIHeaderCSettings csettings; + + int friend_id; + +} MSIMessage; + + +inline__ void invoke_callback(MSISession *session, int32_t call_index, MSICallbackID id) +{ + if ( session->callbacks[id].function ) { + LOGGER_DEBUG("Invoking callback function: %d", id); + session->callbacks[id].function ( session->agent_handler, call_index, session->callbacks[id].data ); + } +} + +/** + * @brief Parse raw 'data' received from socket into MSIMessage struct. + * Every message has to have end value of 'end_byte' or _undefined_ behavior + * occures. The best practice is to check the end of the message at the handle_packet. + * + * @param msg Container. + * @param data The data. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +static int parse_raw_data ( MSIMessage *msg, const uint8_t *data, uint16_t length ) +{ + +#define FAIL_CONSTRAINT(constraint, wanted) if ((constraint -= wanted) < 1) { LOGGER_ERROR("Read over length!"); return -1; } +#define FAIL_SIZE(byte, valid) if ( byte != valid ) { LOGGER_ERROR("Invalid data size!"); return -1; } +#define FAIL_LIMITS(byte, high) if ( byte > high ) { LOGGER_ERROR("Failed limit!"); return -1; } + + if ( msg == NULL ) { + LOGGER_ERROR("Could not parse message: no storage!"); + return -1; + } + + if ( data[length - 1] ) { /* End byte must have value 0 */ + LOGGER_ERROR("Invalid end byte"); + return -1; + } + + const uint8_t *it = data; + int size_constraint = length; + + while ( *it ) {/* until end byte is hit */ + switch (*it) { + case IDRequest: + FAIL_CONSTRAINT(size_constraint, 3); + FAIL_SIZE(it[1], 1); +// FAIL_LIMITS(it[2], invite, end); + FAIL_LIMITS(it[2], end); + msg->request.value = it[2]; + it += 3; + msg->request.exists = 1; + break; + + case IDResponse: + FAIL_CONSTRAINT(size_constraint, 3); + FAIL_SIZE(it[1], 1); +// FAIL_LIMITS(it[2], ringing, error); + FAIL_LIMITS(it[2], error); + msg->response.value = it[2]; + it += 3; + msg->response.exists = 1; + break; + + case IDCallId: + FAIL_CONSTRAINT(size_constraint, sizeof(MSICallIDType) + 2); + FAIL_SIZE(it[1], sizeof(MSICallIDType)); + memcpy(msg->callid.value, it + 2, sizeof(MSICallIDType)); + it += sizeof(MSICallIDType) + 2; + msg->callid.exists = 1; + break; + + case IDReason: + FAIL_CONSTRAINT(size_constraint, sizeof(MSIReasonStrType) + 2); + FAIL_SIZE(it[1], sizeof(MSIReasonStrType)); + memcpy(msg->reason.value, it + 2, sizeof(MSIReasonStrType)); + it += sizeof(MSIReasonStrType) + 2; + msg->reason.exists = 1; + break; + + case IDCSettings: + FAIL_CONSTRAINT(size_constraint, sizeof(MSIRawCSettingsType) + 2); + FAIL_SIZE(it[1], sizeof(MSIRawCSettingsType)); + memcpy(msg->csettings.value, it + 2, sizeof(MSIRawCSettingsType)); + it += sizeof(MSIRawCSettingsType) + 2; + msg->csettings.exists = 1; + break; + + default: + LOGGER_ERROR("Invalid id byte"); + return -1; + break; + } + } + + return 0; +} + +/** + * @brief Create the message. + * + * @param type Request or response. + * @param type_id Type of request/response. + * @return MSIMessage* Created message. + * @retval NULL Error occurred. + */ +MSIMessage *msi_new_message ( MSIMessageType type, const uint8_t type_value ) +{ + MSIMessage *retu = calloc ( sizeof ( MSIMessage ), 1 ); + + if ( retu == NULL ) { + LOGGER_WARNING("Allocation failed! Program might misbehave!"); + return NULL; + } + + if ( type == TypeRequest ) { + retu->request.exists = 1; + retu->request.value = type_value; + + } else { + retu->response.exists = 1; + retu->response.value = type_value; + } + + return retu; +} + + +/** + * @brief Parse data from handle_packet. + * + * @param data The data. + * @return MSIMessage* Parsed message. + * @retval NULL Error occurred. + */ +MSIMessage *parse_recv ( const uint8_t *data, uint16_t length ) +{ + if ( data == NULL ) { + LOGGER_WARNING("Tried to parse empty message!"); + return NULL; + } + + MSIMessage *retu = calloc ( sizeof ( MSIMessage ), 1 ); + + if ( retu == NULL ) { + LOGGER_WARNING("Allocation failed! Program might misbehave!"); + return NULL; + } + + if ( parse_raw_data ( retu, data, length ) == -1 ) { + + free ( retu ); + return NULL; + } + + return retu; +} + + +/** + * @brief Speaks for it self. + * + * @param dest Container. + * @param header_field Field. + * @param header_value Field value. + * @param value_len Length of field value. + * @param length Pointer to container length. + * @return uint8_t* Iterated container. + */ +uint8_t *format_output ( uint8_t *dest, MSIHeaderID id, const void *value, uint8_t value_len, uint16_t *length ) +{ + if ( dest == NULL ) { + LOGGER_ERROR("No destination space!"); + return NULL; + } + + if (value == NULL || value_len == 0) { + LOGGER_ERROR("Empty header value"); + return NULL; + } + + *dest = id; + dest ++; + *dest = value_len; + dest ++; + + memcpy(dest, value, value_len); + + *length += (2 + value_len); + + return dest + value_len; /* Set to next position ready to be written */ +} + + +/** + * @brief Parse MSIMessage to send. + * + * @param msg The message. + * @param dest Destination. + * @return uint16_t Its final size. + */ +uint16_t parse_send ( MSIMessage *msg, uint8_t *dest ) +{ + if (msg == NULL) { + LOGGER_ERROR("No message!"); + return 0; + } + + if (dest == NULL ) { + LOGGER_ERROR("No destination!"); + return 0; + } + + uint8_t *it = dest; + uint16_t size = 0; + + if (msg->request.exists) { + uint8_t cast = msg->request.value; + it = format_output(it, IDRequest, &cast, 1, &size); + } + + if (msg->response.exists) { + uint8_t cast = msg->response.value; + it = format_output(it, IDResponse, &cast, 1, &size); + } + + if (msg->callid.exists) { + it = format_output(it, IDCallId, &msg->callid.value, sizeof(msg->callid.value), &size); + } + + if (msg->reason.exists) { + it = format_output(it, IDReason, &msg->reason.value, sizeof(msg->reason.value), &size); + } + + if (msg->csettings.exists) { + it = format_output(it, IDCSettings, &msg->csettings.value, sizeof(msg->csettings.value), &size); + } + + *it = 0; + size ++; + + return size; +} + +void msi_msg_set_reason ( MSIMessage *msg, const MSIReasonStrType value ) +{ + if ( !msg ) return; + + msg->reason.exists = 1; + memcpy(msg->reason.value, value, sizeof(MSIReasonStrType)); +} + +void msi_msg_set_callid ( MSIMessage *msg, const MSICallIDType value ) +{ + if ( !msg ) return; + + msg->callid.exists = 1; + memcpy(msg->callid.value, value, sizeof(MSICallIDType)); +} + +void msi_msg_set_csettings ( MSIMessage *msg, const MSICSettings *value ) +{ + if ( !msg ) return; + + msg->csettings.exists = 1; + + msg->csettings.value[0] = value->call_type; + uint8_t *iter = msg->csettings.value + 1; + + /* Video bitrate */ + uint32_t lval = htonl(value->video_bitrate); + memcpy(iter, &lval, 4); + iter += 4; + + /* Video max width */ + uint16_t sval = htons(value->max_video_width); + memcpy(iter, &sval, 2); + iter += 2; + + /* Video max height */ + sval = htons(value->max_video_height); + memcpy(iter, &sval, 2); + iter += 2; + + /* Audio bitrate */ + lval = htonl(value->audio_bitrate); + memcpy(iter, &lval, 4); + iter += 4; + + /* Audio frame duration */ + sval = htons(value->audio_frame_duration); + memcpy(iter, &sval, 2); + iter += 2; + + /* Audio sample rate */ + lval = htonl(value->audio_sample_rate); + memcpy(iter, &lval, 4); + iter += 4; + + /* Audio channels */ + lval = htonl(value->audio_channels); + memcpy(iter, &lval, 4); +} + +void msi_msg_get_csettings ( MSIMessage *msg, MSICSettings *dest ) +{ + if ( !msg || !dest || !msg->csettings.exists ) return; + + dest->call_type = msg->csettings.value[0]; + uint8_t *iter = msg->csettings.value + 1; + + memcpy(&dest->video_bitrate, iter, 4); + iter += 4; + dest->video_bitrate = ntohl(dest->video_bitrate); + + memcpy(&dest->max_video_width, iter, 2); + iter += 2; + dest->max_video_width = ntohs(dest->max_video_width); + + memcpy(&dest->max_video_height, iter, 2); + iter += 2; + dest->max_video_height = ntohs(dest->max_video_height); + + memcpy(&dest->audio_bitrate, iter, 4); + iter += 4; + dest->audio_bitrate = ntohl(dest->audio_bitrate); + + memcpy(&dest->audio_frame_duration, iter, 2); + iter += 2; + dest->audio_frame_duration = ntohs(dest->audio_frame_duration); + + memcpy(&dest->audio_sample_rate, iter, 4); + iter += 4; + dest->audio_sample_rate = ntohl(dest->audio_sample_rate); + + memcpy(&dest->audio_channels, iter, 4); + dest->audio_channels = ntohl(dest->audio_channels); +} + +typedef struct _Timer { + void *(*func)(void *); + void *func_arg1; + int func_arg2; + uint64_t timeout; + int idx; + +} Timer; + +typedef struct _TimerHandler { + Timer **timers; + pthread_mutex_t mutex; + + uint32_t max_capacity; + uint32_t size; + uint64_t resolution; + + _Bool running; + +} TimerHandler; + +struct timer_function_args { + void *arg1; + int arg2; +}; + +/** + * @brief Allocate timer in array + * + * @param timers_container Handler + * @param func Function to be executed + * @param arg Its args + * @param timeout Timeout in ms + * @return int + */ +static int timer_alloc ( TimerHandler *timers_container, void *(func)(void *), void *arg1, int arg2, uint32_t timeout) +{ + static int timer_id; + pthread_mutex_lock(&timers_container->mutex); + + uint32_t i = 0; + + for (; i < timers_container->max_capacity && timers_container->timers[i]; i ++); + + if (i == timers_container->max_capacity) { + LOGGER_WARNING("Maximum capacity reached!"); + pthread_mutex_unlock(&timers_container->mutex); + return -1; + } + + Timer *timer = timers_container->timers[i] = calloc(sizeof(Timer), 1); + + if (timer == NULL) { + LOGGER_ERROR("Failed to allocate timer!"); + pthread_mutex_unlock(&timers_container->mutex); + return -1; + } + + timers_container->size ++; + + timer->func = func; + timer->func_arg1 = arg1; + timer->func_arg2 = arg2; + timer->timeout = timeout + current_time_monotonic(); /* In ms */ + ++timer_id; + timer->idx = timer_id; + + /* reorder */ + if (i) { + int64_t j = i - 1; + + for (; j >= 0 && timeout < timers_container->timers[j]->timeout; j--) { + Timer *tmp = timers_container->timers[j]; + timers_container->timers[j] = timer; + timers_container->timers[j + 1] = tmp; + } + } + + pthread_mutex_unlock(&timers_container->mutex); + + LOGGER_DEBUG("Allocated timer index: %ull timeout: %ull, current size: %ull", i, timeout, timers_container->size); + return timer->idx; +} + +/** + * @brief Remove timer from array + * + * @param timers_container handler + * @param idx timer id + * @param lock_mutex (does the mutex need to be locked) + * @return int + */ +static int timer_release ( TimerHandler *timers_container, int idx , int lock_mutex) +{ + if (lock_mutex) + pthread_mutex_lock(&timers_container->mutex); + + Timer **timed_events = timers_container->timers; + + size_t i; + int rc = -1; + + for (i = 0; i < timers_container->max_capacity; ++i) { + if (timed_events[i] && timed_events[i]->idx == idx) { + rc = i; + break; + } + } + + if (rc == -1) { + LOGGER_WARNING("No event with id: %d", idx); + + if (lock_mutex) pthread_mutex_unlock(&timers_container->mutex); + + return -1; + } + + free(timed_events[rc]); + + timed_events[rc] = NULL; + + i = rc + 1; + + for (; i < timers_container->max_capacity && timed_events[i]; i ++) { + timed_events[i - 1] = timed_events[i]; + timed_events[i] = NULL; + } + + timers_container->size--; + + LOGGER_DEBUG("Popped id: %d, current size: %ull ", idx, timers_container->size); + + if (lock_mutex) pthread_mutex_unlock(&timers_container->mutex); + + return 0; +} + +/** + * @brief Main poll for timer execution + * + * @param arg ... + * @return void* + */ +static void *timer_poll( void *arg ) +{ + TimerHandler *handler = arg; + + while ( handler->running ) { + + pthread_mutex_lock(&handler->mutex); + + if ( handler->running ) { + + uint64_t time = current_time_monotonic(); + + while ( handler->timers[0] && handler->timers[0]->timeout < time ) { + pthread_t tid; + + struct timer_function_args *args = malloc(sizeof(struct timer_function_args)); + args->arg1 = handler->timers[0]->func_arg1; + args->arg2 = handler->timers[0]->func_arg2; + + if ( 0 != pthread_create(&tid, NULL, handler->timers[0]->func, args) || + 0 != pthread_detach(tid) ) { + LOGGER_ERROR("Failed to execute timer at: %d!", handler->timers[0]->timeout); + free(args); + } else { + LOGGER_DEBUG("Exectued timer assigned at: %d", handler->timers[0]->timeout); + } + + timer_release(handler, handler->timers[0]->idx, 0); + } + + } + + pthread_mutex_unlock(&handler->mutex); + + usleep(handler->resolution); + } + + pthread_exit(NULL); +} + +/** + * @brief Start timer poll and return handler + * + * @param max_capacity capacity + * @param resolution ... + * @return TimerHandler* + */ +static TimerHandler *timer_init_session (int max_capacity, int resolution) +{ + TimerHandler *handler = calloc(1, sizeof(TimerHandler)); + + if (handler == NULL) { + LOGGER_ERROR("Failed to allocate memory, program might misbehave!"); + return NULL; + } + + handler->timers = calloc(max_capacity, sizeof(Timer *)); + + if (handler->timers == NULL) { + LOGGER_ERROR("Failed to allocate %d timed events!", max_capacity); + free(handler); + return NULL; + } + + handler->max_capacity = max_capacity; + handler->running = 1; + handler->resolution = resolution; + + pthread_mutex_init(&handler->mutex, NULL); + + + pthread_t _tid; + + if ( 0 != pthread_create(&_tid, NULL, timer_poll, handler) || 0 != pthread_detach(_tid) ) { + LOGGER_ERROR("Failed to start timer poll thread!"); + free(handler->timers); + free(handler); + return NULL; + } + + return handler; +} + +/** + * @brief Terminate timer session + * + * @param handler The timer handler + * @return void + */ +static void timer_terminate_session(TimerHandler *handler) +{ + pthread_mutex_lock(&handler->mutex); + + handler->running = 0; + + pthread_mutex_unlock(&handler->mutex); + + size_t i = 0; + + for (; i < handler->max_capacity; i ++) + free(handler->timers[i]); + + free(handler->timers); + + pthread_mutex_destroy( &handler->mutex ); +} + +/** + * @brief Generate _random_ alphanumerical string. + * + * @param str Destination. + * @param size Size of string. + * @return void + */ +static void t_randomstr ( uint8_t *str, uint32_t size ) +{ + if (str == NULL) { + LOGGER_DEBUG("Empty destination!"); + return; + } + + static const uint8_t _bytes[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + + uint32_t _it = 0; + + for ( ; _it < size; _it++ ) { + str[_it] = _bytes[ random_int() % 61 ]; + } +} + + +typedef enum { + error_none, + error_deadcall, /* has call id but it's from old call */ + error_id_mismatch, /* non-existing call */ + + error_no_callid, /* not having call id */ + error_no_call, /* no call in session */ + error_no_crypto_key, /* no crypto key */ + + error_busy + +} MSICallError; /* Error codes */ + + +/** + * @brief Stringify error code. + * + * @param error_code The code. + * @return const uint8_t* The string. + */ +static inline__ const uint8_t *stringify_error ( MSICallError error_code ) +{ + static const uint8_t *strings[] = { + ( uint8_t *) "", + ( uint8_t *) "Using dead call", + ( uint8_t *) "Call id not set to any call", + ( uint8_t *) "Call id not available", + ( uint8_t *) "No active call in session", + ( uint8_t *) "No Crypto-key set", + ( uint8_t *) "Callee busy" + }; + + return strings[error_code]; +} + +/** + * @brief Speaks for it self. + * + * @param session Control session. + * @param msg The message. + * @param to Where to. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +static int send_message ( MSISession *session, MSICall *call, MSIMessage *msg, uint32_t to ) +{ + msi_msg_set_callid ( msg, call->id ); + + uint8_t msg_string_final [MSI_MAXMSG_SIZE]; + uint16_t length = parse_send ( msg, msg_string_final ); + + if (!length) { + LOGGER_WARNING("Parsing message failed; nothing sent!"); + return -1; + } + + if ( m_msi_packet(session->messenger_handle, to, msg_string_final, length) ) { + LOGGER_DEBUG("Sent message"); + return 0; + } + + return -1; +} + +inline__ int send_reponse ( MSISession *session, MSICall *call, MSIResponse response, uint32_t to ) +{ + MSIMessage *msg = msi_new_message ( TypeResponse, response ); + int ret = send_message ( session, call, msg, to ); + free ( msg ); + return ret; +} + +/** + * @brief Determine 'bigger' call id + * + * @param first duh + * @param second duh + * @return int + * @retval 0 it's first + * @retval 1 it's second + */ +static int call_id_bigger( const uint8_t *first, const uint8_t *second) +{ + return (memcmp(first, second, sizeof(MSICallIDType)) < 0); +} + + +/** + * @brief Speaks for it self. + * + * @param session Control session. + * @param msg The message. + * @param peer_id The peer. + * @return -1, 0 + */ +static int flush_peer_csettings ( MSICall *call, MSIMessage *msg, int peer_id ) +{ + if ( msg->csettings.exists ) { + msi_msg_get_csettings(msg, &call->csettings_peer[peer_id]); + + LOGGER_DEBUG("Peer: %d \n" + "Type: %u \n" + "Video bitrate: %u \n" + "Video height: %u \n" + "Video width: %u \n" + "Audio bitrate: %u \n" + "Audio framedur: %u \n" + "Audio sample rate: %u \n" + "Audio channels: %u \n", peer_id, + call->csettings_peer[peer_id].call_type, + call->csettings_peer[peer_id].video_bitrate, + call->csettings_peer[peer_id].max_video_height, + call->csettings_peer[peer_id].max_video_width, + call->csettings_peer[peer_id].audio_bitrate, + call->csettings_peer[peer_id].audio_frame_duration, + call->csettings_peer[peer_id].audio_sample_rate, + call->csettings_peer[peer_id].audio_channels ); + + return 0; + } + + LOGGER_WARNING("No csettings header!"); + return -1; +} + +static int terminate_call ( MSISession *session, MSICall *call ); + +static void handle_remote_connection_change(Messenger *messenger, int friend_num, uint8_t status, void *session_p) +{ + (void)messenger; + MSISession *session = session_p; + + switch ( status ) { + case 0: { /* Went offline */ + int32_t j = 0; + + for ( ; j < session->max_calls; j ++ ) { + + if ( !session->calls[j] ) continue; + + uint16_t i = 0; + + for ( ; i < session->calls[j]->peer_count; i ++ ) + if ( session->calls[j]->peers[i] == (uint32_t)friend_num ) { + invoke_callback(session, j, MSI_OnPeerTimeout); + terminate_call(session, session->calls[j]); + LOGGER_DEBUG("Remote: %d timed out!", friend_num); + return; /* TODO: On group calls change behaviour */ + } + } + } + break; + + default: + break; + } +} + +static MSICall *find_call ( MSISession *session, uint8_t *call_id ) +{ + if ( call_id == NULL ) return NULL; + + int32_t i = 0; + + for (; i < session->max_calls; i ++ ) + if ( session->calls[i] && memcmp(session->calls[i]->id, call_id, sizeof(session->calls[i]->id)) == 0 ) { + return session->calls[i]; + } + + return NULL; +} + +/** + * @brief Sends error response to peer. + * + * @param session The session. + * @param errid The id. + * @param to Where to? + * @return int + * @retval -1/0 It's usually always success. + */ +static int send_error ( MSISession *session, MSICall *call, MSICallError errid, uint32_t to ) +{ + if (!call) { + LOGGER_WARNING("Cannot handle error on 'null' call"); + return -1; + } + + LOGGER_DEBUG("Sending error: %d on call: %s", errid, call->id); + + MSIMessage *msg_error = msi_new_message ( TypeResponse, error ); + + msi_msg_set_reason ( msg_error, stringify_error(errid) ); + send_message ( session, call, msg_error, to ); + free ( msg_error ); + + return 0; +} + + + +/** + * @brief Add peer to peer list. + * + * @param call What call. + * @param peer_id Its id. + * @return void + */ +static void add_peer( MSICall *call, int peer_id ) +{ + uint32_t *peers = !call->peers ? peers = calloc(sizeof(uint32_t), 1) : + realloc( call->peers, sizeof(uint32_t) * call->peer_count); + + if (!peers) { + LOGGER_WARNING("Allocation failed! Program might misbehave!"); + return; + } + + call->peer_count ++; + call->peers = peers; + call->peers[call->peer_count - 1] = peer_id; + + LOGGER_DEBUG("Added peer: %d", peer_id); +} + + +/** + * @brief Speaks for it self. + * + * @param session Control session. + * @param peers Amount of peers. (Currently it only supports 1) + * @param ringing_timeout Ringing timeout. + * @return MSICall* The created call. + */ +static MSICall *init_call ( MSISession *session, int peers, int ringing_timeout ) +{ + + if (peers == 0) { + LOGGER_ERROR("No peers!"); + return NULL; + } + + int32_t call_idx = 0; + + for (; call_idx < session->max_calls; call_idx ++) { + if ( !session->calls[call_idx] ) { + + if (!(session->calls[call_idx] = calloc ( sizeof ( MSICall ), 1 ))) { + LOGGER_WARNING("Allocation failed! Program might misbehave!"); + return NULL; + } + + break; + } + } + + if ( call_idx == session->max_calls ) { + LOGGER_WARNING("Reached maximum amount of calls!"); + return NULL; + } + + + MSICall *call = session->calls[call_idx]; + + call->call_idx = call_idx; + + if ( !(call->csettings_peer = calloc ( sizeof ( MSICSettings ), peers )) ) { + LOGGER_WARNING("Allocation failed! Program might misbehave!"); + free(call); + return NULL; + } + + call->session = session; + + call->request_timer_id = 0; + call->ringing_timer_id = 0; + + call->ringing_tout_ms = ringing_timeout; + + pthread_mutex_init ( &call->mutex, NULL ); + + LOGGER_DEBUG("Started new call with index: %u", call_idx); + return call; +} + + +/** + * @brief Terminate the call. + * + * @param session Control session. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +static int terminate_call ( MSISession *session, MSICall *call ) +{ + if ( !call ) { + LOGGER_WARNING("Tried to terminate non-existing call!"); + return -1; + } + + LOGGER_DEBUG("Terminated call id: %d", call->call_idx); + /* Check event loop and cancel timed events if there are any + * NOTE: This has to be done before possibly + * locking the mutex the second time + */ + timer_release ( session->timer_handler, call->request_timer_id, 1); + timer_release ( session->timer_handler, call->ringing_timer_id, 1); + + /* Get a handle */ + pthread_mutex_lock ( &call->mutex ); + + session->calls[call->call_idx] = NULL; + + free ( call->csettings_peer ); + free ( call->peers); + + /* Release handle */ + pthread_mutex_unlock ( &call->mutex ); + + pthread_mutex_destroy ( &call->mutex ); + + free ( call ); + + return 0; +} + + +/** + * @brief Function called at request timeout. If not called in thread it might cause trouble + * + * @param arg Control session + * @return void* + */ +static void *handle_timeout ( void *arg ) +{ + /* TODO: Cancel might not arrive there; set up + * timers on these cancels and terminate call on + * their timeout + */ + struct timer_function_args *args = arg; + int call_index = args->arg2; + MSISession *session = args->arg1; + MSICall *call = session->calls[call_index]; + + if (call) { + LOGGER_DEBUG("[Call: %d] Request timed out!", call->call_idx); + + invoke_callback(session, call_index, MSI_OnRequestTimeout); + } + + if ( call && call->session ) { + + /* TODO: Cancel all? */ + /* uint16_t _it = 0; + * for ( ; _it < _session->call->peer_count; _it++ ) */ + msi_cancel ( call->session, call->call_idx, call->peers [0], "Request timed out" ); + /*terminate_call(call->session, call);*/ + } + + free(arg); + pthread_exit(NULL); +} + + +/********** Request handlers **********/ +static int handle_recv_invite ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + LOGGER_DEBUG("Session: %p Handling 'invite' on call: %d", session, call ? call->call_idx : -1); + + pthread_mutex_lock(&session->mutex); + + if (!msg->csettings.exists) {/**/ + LOGGER_WARNING("Peer sent invalid codec settings!"); + send_error ( session, call, error_no_callid, msg->friend_id ); + pthread_mutex_unlock(&session->mutex); + return 0; + } + + if ( call ) { + if ( call->peers[0] == (uint32_t)msg->friend_id ) { + if (call->state == call_inviting) { + /* The glare case. A calls B when at the same time + * B calls A. Who has advantage is set bey calculating + * 'bigger' Call id and then that call id is being used in + * future. User with 'bigger' Call id has the advantage + * as in he will wait the response from the other. + */ + LOGGER_DEBUG("Glare case; Peer: %d", call->peers[0]); + + if ( call_id_bigger (call->id, msg->callid.value) == 1 ) { /* Peer has advantage */ + + /* Terminate call; peer will timeout(call) if call initialization fails */ + terminate_call(session, call); + + call = init_call ( session, 1, 0 ); + + if ( !call ) { + pthread_mutex_unlock(&session->mutex); + LOGGER_ERROR("Starting call"); + return 0; + } + + } else { + pthread_mutex_unlock(&session->mutex); + return 0; /* Wait for ringing from peer */ + } + } else if (call->state == call_active) { + /* Request for media change; call callback and send starting response */ + if (flush_peer_csettings(call, msg, 0) != 0) { /**/ + LOGGER_WARNING("Peer sent invalid csetting!"); + send_error ( session, call, error_no_callid, msg->friend_id ); + pthread_mutex_unlock(&session->mutex); + return 0; + } + + LOGGER_DEBUG("Set new call type: %s", call->csettings_peer[0].call_type == type_audio ? "audio" : "video"); + send_reponse(session, call, starting, msg->friend_id); + pthread_mutex_unlock(&session->mutex); + invoke_callback(session, call->call_idx, MSI_OnMediaChange); + return 1; + } + } else { + send_error ( session, call, error_busy, msg->friend_id ); /* TODO: Ugh*/ + terminate_call(session, call); + pthread_mutex_unlock(&session->mutex); + return 0; + } + } else { + call = init_call ( session, 1, 0 ); + + if ( !call ) { + pthread_mutex_unlock(&session->mutex); + LOGGER_ERROR("Starting call"); + return 0; + } + } + + if ( !msg->callid.exists ) { + send_error ( session, call, error_no_callid, msg->friend_id ); + terminate_call(session, call); + pthread_mutex_unlock(&session->mutex); + return 0; + } + + memcpy ( call->id, msg->callid.value, sizeof(msg->callid.value) ); + call->state = call_starting; + + add_peer( call, msg->friend_id); + + flush_peer_csettings ( call, msg, 0 ); + + send_reponse(session, call, ringing, msg->friend_id); + + pthread_mutex_unlock(&session->mutex); + + invoke_callback(session, call->call_idx, MSI_OnInvite); + + return 1; +} + +static int handle_recv_start ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + (void)msg; + + LOGGER_DEBUG("Session: %p Handling 'start' on call: %d, friend id: %d", session, call->call_idx, msg->friend_id ); + + pthread_mutex_lock(&session->mutex); + + call->state = call_active; + + pthread_mutex_unlock(&session->mutex); + + invoke_callback(session, call->call_idx, MSI_OnStart); + return 1; +} + +static int handle_recv_reject ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + LOGGER_DEBUG("Session: %p Handling 'reject' on call: %u", session, call->call_idx); + + invoke_callback(session, call->call_idx, MSI_OnReject); + + pthread_mutex_lock(&session->mutex); + + send_reponse(session, call, ending, msg->friend_id); + terminate_call(session, call); + + pthread_mutex_unlock(&session->mutex); + + return 1; +} + +static int handle_recv_cancel ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + (void)msg; + + LOGGER_DEBUG("Session: %p Handling 'cancel' on call: %u", session, call->call_idx); + + invoke_callback(session, call->call_idx, MSI_OnCancel); + + pthread_mutex_lock(&session->mutex); + + terminate_call ( session, call ); + + pthread_mutex_unlock(&session->mutex); + + return 1; +} + +static int handle_recv_end ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + LOGGER_DEBUG("Session: %p Handling 'end' on call: %d", session, call->call_idx); + + invoke_callback(session, call->call_idx, MSI_OnEnd); + pthread_mutex_lock(&session->mutex); + + send_reponse(session, call, ending, msg->friend_id); + terminate_call ( session, call ); + + pthread_mutex_unlock(&session->mutex); + + + return 1; +} + +/********** Response handlers **********/ +static int handle_recv_ringing ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + (void)msg; + + pthread_mutex_lock(&session->mutex); + + if ( call->ringing_timer_id ) { + LOGGER_WARNING("Call already ringing"); + pthread_mutex_unlock(&session->mutex); + return 0; + } + + LOGGER_DEBUG("Session: %p Handling 'ringing' on call: %d", session, call->call_idx ); + + call->ringing_timer_id = timer_alloc ( session->timer_handler, handle_timeout, session, call->call_idx, + call->ringing_tout_ms ); + + pthread_mutex_unlock(&session->mutex); + + invoke_callback(session, call->call_idx, MSI_OnRinging); + return 1; +} +static int handle_recv_starting ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'starting' on non-existing call"); + return 0; + } + + pthread_mutex_lock(&session->mutex); + + if ( call->state == call_active ) { /* Change media */ + + LOGGER_DEBUG("Session: %p Changing media on call: %d", session, call->call_idx ); + pthread_mutex_unlock(&session->mutex); + + invoke_callback(session, call->call_idx, MSI_OnMediaChange); + + } else if ( call->state == call_inviting ) { + LOGGER_DEBUG("Session: %p Handling 'starting' on call: %d", session, call->call_idx ); + + call->state = call_active; + + MSIMessage *msg_start = msi_new_message ( TypeRequest, start ); + send_message ( session, call, msg_start, msg->friend_id ); + free ( msg_start ); + + + flush_peer_csettings ( call, msg, 0 ); + + /* This is here in case of glare */ + timer_release ( session->timer_handler, call->ringing_timer_id, 1 ); + + pthread_mutex_unlock(&session->mutex); + + invoke_callback(session, call->call_idx, MSI_OnStarting); + } else { + LOGGER_ERROR("Invalid call state"); + terminate_call(session, call ); + pthread_mutex_unlock(&session->mutex); + return 0; + } + + return 1; +} +static int handle_recv_ending ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + if ( !call ) { + LOGGER_WARNING("Session: %p Handling 'start' on no call"); + return 0; + } + + (void)msg; + + LOGGER_DEBUG("Session: %p Handling 'ending' on call: %d", session, call->call_idx ); + + invoke_callback(session, call->call_idx, MSI_OnEnding); + + /* Terminate call */ + pthread_mutex_lock(&session->mutex); + terminate_call ( session, call ); + pthread_mutex_unlock(&session->mutex); + + return 1; +} +static int handle_recv_error ( MSISession *session, MSICall *call, MSIMessage *msg ) +{ + + if ( !call ) { + LOGGER_WARNING("Handling 'error' on non-existing call!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + LOGGER_DEBUG("Session: %p Handling 'error' on call: %d", session, call->call_idx ); + + invoke_callback(session, call->call_idx, MSI_OnEnding); + + pthread_mutex_lock(&session->mutex); + + /* Handle error accordingly */ + if ( msg->reason.exists ) { + /* TODO */ + } + + terminate_call ( session, call ); + + pthread_mutex_unlock(&session->mutex); + + return 1; +} + + +/** + * @brief BASIC call flow: + * + * ALICE BOB + * | invite --> | + * | | + * | <-- ringing | + * | | + * | <-- starting | + * | | + * | start --> | + * | | + * | <-- MEDIA TRANS --> | + * | | + * | end --> | + * | | + * | <-- ending | + * + * Alice calls Bob by sending invite packet. + * Bob recvs the packet and sends an ringing packet; + * which notifies Alice that her invite is acknowledged. + * Ringing screen shown on both sides. + * Bob accepts the invite for a call by sending starting packet. + * Alice recvs the starting packet and sends the started packet to + * inform Bob that she recved the starting packet. + * Now the media transmission is established ( i.e. RTP transmission ). + * Alice hangs up and sends end packet. + * Bob recves the end packet and sends ending packet + * as the acknowledgement that the call is ending. + * + * + */ +static void msi_handle_packet ( Messenger *messenger, int source, const uint8_t *data, uint16_t length, void *object ) +{ + LOGGER_DEBUG("Got msi message"); + /* Unused */ + (void)messenger; + + MSISession *session = object; + MSIMessage *msg; + + if ( !length ) { + LOGGER_WARNING("Lenght param negative"); + return; + } + + msg = parse_recv ( data, length ); + + if ( !msg ) { + LOGGER_WARNING("Error parsing message"); + return; + } else { + LOGGER_DEBUG("Successfully parsed message"); + } + + msg->friend_id = source; + + + /* Find what call */ + MSICall *call = msg->callid.exists ? find_call(session, msg->callid.value ) : NULL; + + /* Now handle message */ + + if ( msg->request.exists ) { /* Handle request */ + + switch (msg->request.value) { + case invite: + handle_recv_invite ( session, call, msg ); + break; + + case start: + handle_recv_start ( session, call, msg ); + break; + + case cancel: + handle_recv_cancel ( session, call, msg ); + break; + + case reject: + handle_recv_reject ( session, call, msg ); + break; + + case end: + handle_recv_end ( session, call, msg ); + break; + } + + } else if ( msg->response.exists ) { /* Handle response */ + + /* Got response so cancel timer */ + if ( call ) timer_release ( session->timer_handler, call->request_timer_id, 1 ); + + switch (msg->response.value) { + case ringing: + handle_recv_ringing ( session, call, msg ); + break; + + case starting: + handle_recv_starting ( session, call, msg ); + break; + + case ending: + handle_recv_ending ( session, call, msg ); + break; + + case error: + handle_recv_error ( session, call, msg ); + break; + } + + } else { + LOGGER_WARNING("Invalid message: no resp nor requ headers"); + } + + free ( msg ); +} + + +/** + * @brief Callback setter. + * + * @param callback The callback. + * @param id The id. + * @return void + */ +void msi_register_callback ( MSISession *session, MSICallbackType callback, MSICallbackID id, void *userdata ) +{ + session->callbacks[id].function = callback; + session->callbacks[id].data = userdata; +} + + +/** + * @brief Start the control session. + * + * @param messenger Tox* object. + * @param max_calls Amount of calls possible + * @return MSISession* The created session. + * @retval NULL Error occurred. + */ +MSISession *msi_init_session ( Messenger *messenger, int32_t max_calls ) +{ + if (messenger == NULL) { + LOGGER_ERROR("Could not init session on empty messenger!"); + return NULL; + } + + TimerHandler *handler = timer_init_session(max_calls * 10, 10000); + + if ( !max_calls || !handler ) { + LOGGER_WARNING("Invalid max call treshold or timer handler initialization failed!"); + return NULL; + } + + MSISession *retu = calloc ( sizeof ( MSISession ), 1 ); + + if (retu == NULL) { + LOGGER_ERROR("Allocation failed! Program might misbehave!"); + return NULL; + } + + retu->messenger_handle = messenger; + retu->agent_handler = NULL; + retu->timer_handler = handler; + + if (!(retu->calls = calloc( sizeof (MSICall *), max_calls ))) { + LOGGER_ERROR("Allocation failed! Program might misbehave!"); + free(retu); + return NULL; + } + + retu->max_calls = max_calls; + + retu->frequ = 10000; /* default value? */ + retu->call_timeout = 30000; /* default value? */ + + + m_callback_msi_packet(messenger, msi_handle_packet, retu ); + + /* This is called when remote terminates session */ + m_callback_connectionstatus_internal_av(messenger, handle_remote_connection_change, retu); + + pthread_mutex_init(&retu->mutex, NULL); + + LOGGER_DEBUG("New msi session: %p max calls: %u", retu, max_calls); + return retu; +} + + +/** + * @brief Terminate control session. + * + * @param session The session + * @return int + */ +int msi_terminate_session ( MSISession *session ) +{ + if (session == NULL) { + LOGGER_ERROR("Tried to terminate non-existing session"); + return -1; + } + + pthread_mutex_lock(&session->mutex); + m_callback_msi_packet((struct Messenger *) session->messenger_handle, NULL, NULL); + pthread_mutex_unlock(&session->mutex); + + int _status = 0; + + /* If have calls, cancel them */ + int32_t idx = 0; + + for (; idx < session->max_calls; idx ++) if ( session->calls[idx] ) { + /* Cancel all? */ + uint16_t _it = 0; + /*for ( ; _it < session->calls[idx]->peer_count; _it++ ) + * FIXME: will not work on multiple peers, must cancel call for all peers + */ + msi_cancel ( session, idx, session->calls[idx]->peers [_it], "MSI session terminated!" ); + } + + timer_terminate_session(session->timer_handler); + + pthread_mutex_destroy(&session->mutex); + + LOGGER_DEBUG("Terminated session: %p", session); + free ( session ); + return _status; +} + + +/** + * @brief Send invite request to friend_id. + * + * @param session Control session. + * @param call_type Type of the call. Audio or Video(both audio and video) + * @param rngsec Ringing timeout. + * @param friend_id The friend. + * @return int + */ +int msi_invite ( MSISession *session, int32_t *call_index, MSICSettings csettings, uint32_t rngsec, uint32_t friend_id ) +{ + pthread_mutex_lock(&session->mutex); + + LOGGER_DEBUG("Session: %p Inviting friend: %u", session, friend_id); + + + int i = 0; + + for (; i < session->max_calls; i ++) + if (session->calls[i] && session->calls[i]->peers[0] == friend_id) { + LOGGER_ERROR("Already in a call with friend %d", friend_id); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + + MSICall *call = init_call ( session, 1, rngsec ); /* Just one peer for now */ + + if ( !call ) { + pthread_mutex_unlock(&session->mutex); + LOGGER_ERROR("Cannot handle more calls"); + return -1; + } + + *call_index = call->call_idx; + + t_randomstr ( call->id, sizeof(call->id) ); + + add_peer ( call, friend_id ); + + call->csettings_local = csettings; + + MSIMessage *msg_invite = msi_new_message ( TypeRequest, invite ); + + msi_msg_set_csettings(msg_invite, &csettings); + send_message ( session, call, msg_invite, friend_id ); + free( msg_invite ); + + call->state = call_inviting; + + call->request_timer_id = timer_alloc ( session->timer_handler, handle_timeout, session, call->call_idx, m_deftout ); + + LOGGER_DEBUG("Invite sent"); + + pthread_mutex_unlock(&session->mutex); + + return 0; +} + + +/** + * @brief Hangup active call. + * + * @param session Control session. + * @param call_id To which call is this action handled. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +int msi_hangup ( MSISession *session, int32_t call_index ) +{ + pthread_mutex_lock(&session->mutex); + LOGGER_DEBUG("Session: %p Hanging up call: %u", session, call_index); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + LOGGER_ERROR("Invalid call index!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + if ( session->calls[call_index]->state != call_active ) { + LOGGER_ERROR("No call with such index or call is not active!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSIMessage *msg_end = msi_new_message ( TypeRequest, end ); + + /* hangup for each peer */ + int it = 0; + + for ( ; it < session->calls[call_index]->peer_count; it ++ ) + send_message ( session, session->calls[call_index], msg_end, session->calls[call_index]->peers[it] ); + + session->calls[call_index]->state = call_hanged_up; + + free ( msg_end ); + + session->calls[call_index]->request_timer_id = + timer_alloc ( session->timer_handler, handle_timeout, session, call_index, m_deftout ); + + pthread_mutex_unlock(&session->mutex); + return 0; +} + + +/** + * @brief Answer active call request. + * + * @param session Control session. + * @param call_id To which call is this action handled. + * @param call_type Answer with Audio or Video(both). + * @return int + */ +int msi_answer ( MSISession *session, int32_t call_index, MSICSettings csettings ) +{ + pthread_mutex_lock(&session->mutex); + LOGGER_DEBUG("Session: %p Answering call: %u", session, call_index); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + LOGGER_ERROR("Invalid call index!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSIMessage *msg_starting = msi_new_message ( TypeResponse, starting ); + + session->calls[call_index]->csettings_local = csettings; + + msi_msg_set_csettings(msg_starting, &csettings); + + send_message ( session, session->calls[call_index], msg_starting, session->calls[call_index]->peers[0] ); + free ( msg_starting ); + + session->calls[call_index]->state = call_active; + + pthread_mutex_unlock(&session->mutex); + return 0; +} + + +/** + * @brief Cancel request. + * + * @param session Control session. + * @param call_id To which call is this action handled. + * @param reason Set optional reason header. Pass NULL if none. + * @return int + */ +int msi_cancel ( MSISession *session, int32_t call_index, uint32_t peer, const char *reason ) +{ + pthread_mutex_lock(&session->mutex); + LOGGER_DEBUG("Session: %p Canceling call: %u; reason: %s", session, call_index, reason ? reason : "Unknown"); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + LOGGER_ERROR("Invalid call index!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSIMessage *msg_cancel = msi_new_message ( TypeRequest, cancel ); + + /* FIXME */ +#if 0 + + if ( reason && strlen(reason) < sizeof(MSIReasonStrType) ) { + MSIReasonStrType reason_cast; + memset(reason_cast, '\0', sizeof(MSIReasonStrType)); + memcpy(reason_cast, reason, strlen(reason)); + msi_msg_set_reason(msg_cancel, reason_cast); + } + +#else + (void)reason; + +#endif + + send_message ( session, session->calls[call_index], msg_cancel, peer ); + free ( msg_cancel ); + + terminate_call ( session, session->calls[call_index] ); + pthread_mutex_unlock(&session->mutex); + + return 0; +} + + +/** + * @brief Reject request. + * + * @param session Control session. + * @param call_id To which call is this action handled. + * @return int + */ +int msi_reject ( MSISession *session, int32_t call_index, const char *reason ) +{ + pthread_mutex_lock(&session->mutex); + LOGGER_DEBUG("Session: %p Rejecting call: %u; reason: %s", session, call_index, reason ? reason : "Unknown"); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + LOGGER_ERROR("Invalid call index!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSIMessage *msg_reject = msi_new_message ( TypeRequest, reject ); + + /* FIXME */ +#if 0 + + if ( reason && strlen(reason) < sizeof(MSIReasonStrType) ) { + MSIReasonStrType reason_cast; + memset(reason_cast, '\0', sizeof(MSIReasonStrType)); + memcpy(reason_cast, reason, strlen(reason)); + msi_msg_set_reason(msg_reject, reason_cast); + } + +#else + (void)reason; + +#endif + + send_message ( session, session->calls[call_index], msg_reject, + session->calls[call_index]->peers[session->calls[call_index]->peer_count - 1] ); + free ( msg_reject ); + + session->calls[call_index]->state = call_hanged_up; + session->calls[call_index]->request_timer_id = + timer_alloc ( session->timer_handler, handle_timeout, session, call_index, m_deftout ); + + pthread_mutex_unlock(&session->mutex); + return 0; +} + + +/** + * @brief Send invite request to friend_id. + * + * @param session Control session. + * @param call_index Call index. + * @param call_type Type of the call. Audio or Video(both audio and video) + * @param rngsec Ringing timeout. + * @param friend_id The friend. + * @return int + */ +int msi_change_csettings(MSISession *session, int32_t call_index, MSICSettings csettings) +{ + pthread_mutex_lock(&session->mutex); + + LOGGER_DEBUG("Changing media on call: %d", call_index); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + LOGGER_ERROR("Invalid call index!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSICall *call = session->calls[call_index]; + + if ( call->state != call_active ) { + LOGGER_ERROR("Call is not active!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + MSICSettings *local = &call->csettings_local; + + if ( + local->call_type == csettings.call_type && + local->video_bitrate == csettings.video_bitrate && + local->max_video_width == csettings.max_video_width && + local->max_video_height == csettings.max_video_height && + local->audio_bitrate == csettings.audio_bitrate && + local->audio_frame_duration == csettings.audio_frame_duration && + local->audio_sample_rate == csettings.audio_sample_rate && + local->audio_channels == csettings.audio_channels ) { + LOGGER_ERROR("Call is already set accordingly!"); + pthread_mutex_unlock(&session->mutex); + return -1; + } + + *local = csettings; + + MSIMessage *msg_invite = msi_new_message ( TypeRequest, invite ); + + msi_msg_set_csettings ( msg_invite, local ); + send_message ( session, call, msg_invite, call->peers[0] ); + free ( msg_invite ); + + LOGGER_DEBUG("Request for media change sent"); + + pthread_mutex_unlock(&session->mutex); + + return 0; +} + + +/** + * @brief Terminate the current call. + * + * @param session Control session. + * @param call_id To which call is this action handled. + * @return int + */ +int msi_stopcall ( MSISession *session, int32_t call_index ) +{ + pthread_mutex_lock(&session->mutex); + LOGGER_DEBUG("Session: %p Stopping call index: %u", session, call_index); + + if ( call_index < 0 || call_index >= session->max_calls || !session->calls[call_index] ) { + pthread_mutex_unlock(&session->mutex); + return -1; + } + + /* just terminate it */ + + terminate_call ( session, session->calls[call_index] ); + + pthread_mutex_unlock(&session->mutex); + return 0; +} diff --git a/protocols/Tox/toxcore/toxav/msi.h b/protocols/Tox/toxcore/toxav/msi.h new file mode 100644 index 0000000000..64fa08819a --- /dev/null +++ b/protocols/Tox/toxcore/toxav/msi.h @@ -0,0 +1,267 @@ +/** msi.h + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __TOXMSI +#define __TOXMSI + +#include <inttypes.h> +#include <pthread.h> + +#include "../toxcore/Messenger.h" + +typedef uint8_t MSICallIDType[12]; +typedef uint8_t MSIReasonStrType[255]; +typedef void ( *MSICallbackType ) ( void *agent, int32_t call_idx, void *arg ); + + +/** + * @brief Call type identifier. Also used as rtp callback prefix. + */ +typedef enum { + type_audio = 192, + type_video +} MSICallType; + + +/** + * @brief Call state identifiers. + */ +typedef enum { + call_inviting, /* when sending call invite */ + call_starting, /* when getting call invite */ + call_active, + call_hold, + call_hanged_up + +} MSICallState; + + +/** + * @brief Encoding settings. + */ +typedef struct _MSICodecSettings { + MSICallType call_type; + + uint32_t video_bitrate; /* In kbits/s */ + uint16_t max_video_width; /* In px */ + uint16_t max_video_height; /* In px */ + + uint32_t audio_bitrate; /* In bits/s */ + uint16_t audio_frame_duration; /* In ms */ + uint32_t audio_sample_rate; /* In Hz */ + uint32_t audio_channels; +} MSICSettings; + + +/** + * @brief Callbacks ids that handle the states + */ +typedef enum { + /* Requests */ + MSI_OnInvite, + MSI_OnStart, + MSI_OnCancel, + MSI_OnReject, + MSI_OnEnd, + + /* Responses */ + MSI_OnRinging, + MSI_OnStarting, + MSI_OnEnding, + + /* Protocol */ + MSI_OnRequestTimeout, + MSI_OnPeerTimeout, + MSI_OnMediaChange +} MSICallbackID; + +/** + * @brief Callbacks container + */ +typedef struct _MSICallbackCont { + MSICallbackType function; + void *data; +} MSICallbackCont; + +/** + * @brief The call struct. + * + */ +typedef struct _MSICall { /* Call info structure */ + struct _MSISession *session; /* Session pointer */ + + MSICallState state; + + MSICSettings csettings_local; /* Local call settings */ + MSICSettings *csettings_peer; /* Peers call settings */ + + MSICallIDType id; /* Random value identifying the call */ + + int ringing_tout_ms; /* Ringing timeout in ms */ + + int request_timer_id; /* Timer id for outgoing request/action */ + int ringing_timer_id; /* Timer id for ringing timeout */ + + + pthread_mutex_t mutex; /* */ + uint32_t *peers; + uint16_t peer_count; + + int32_t call_idx; /* Index of this call in MSISession */ +} MSICall; + + +/** + * @brief Control session struct + * + */ +typedef struct _MSISession { + + /* Call handlers */ + MSICall **calls; + int32_t max_calls; + + void *agent_handler; + Messenger *messenger_handle; + + uint32_t frequ; + uint32_t call_timeout; /* Time of the timeout for some action to end; 0 if infinite */ + + pthread_mutex_t mutex; + + void *timer_handler; + MSICallbackCont callbacks[11]; /* Callbacks used by this session */ +} MSISession; + +/** + * @brief Callback setter. + * + * @param session The container. + * @param callback The callback. + * @param id The id. + * @return void + */ +void msi_register_callback(MSISession *session, MSICallbackType callback, MSICallbackID id, void *userdata); + + +/** + * @brief Start the control session. + * + * @param messenger Tox* object. + * @param max_calls Amount of calls possible + * @return MSISession* The created session. + * @retval NULL Error occurred. + */ +MSISession *msi_init_session ( Messenger *messenger, int32_t max_calls ); + + +/** + * @brief Terminate control session. + * + * @param session The session + * @return int + */ +int msi_terminate_session ( MSISession *session ); + + +/** + * @brief Send invite request to friend_id. + * + * @param session Control session. + * @param call_index Set to new call index. + * @param call_type Type of the call. Audio or Video(both audio and video) + * @param rngsec Ringing timeout. + * @param friend_id The friend. + * @return int + */ +int msi_invite ( MSISession *session, int32_t *call_index, MSICSettings csettings, uint32_t rngsec, + uint32_t friend_id ); + + +/** + * @brief Hangup active call. + * + * @param session Control session. + * @param call_index To which call is this action handled. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +int msi_hangup ( MSISession *session, int32_t call_index ); + + +/** + * @brief Answer active call request. + * + * @param session Control session. + * @param call_index To which call is this action handled. + * @param call_type Answer with Audio or Video(both). + * @return int + */ +int msi_answer ( MSISession *session, int32_t call_index, MSICSettings csettings ); + + +/** + * @brief Cancel request. + * + * @param session Control session. + * @param call_index To which call is this action handled. + * @param peer To which peer. + * @param reason Set optional reason header. Pass NULL if none. + * @return int + */ +int msi_cancel ( MSISession *session, int32_t call_index, uint32_t peer, const char *reason ); + + +/** + * @brief Reject request. + * + * @param session Control session. + * @param call_index To which call is this action handled. + * @param reason Set optional reason header. Pass NULL if none. + * @return int + */ +int msi_reject ( MSISession *session, int32_t call_index, const char *reason ); + + +/** + * @brief Send invite request to friend_id. + * + * @param session Control session. + * @param call_index Call index. + * @param call_type Type of the call. Audio or Video(both audio and video) + * @param rngsec Ringing timeout. + * @param friend_id The friend. + * @return int + */ +int msi_change_csettings ( MSISession *session, int32_t call_index, MSICSettings csettings ); + + +/** + * @brief Terminate the current call. + * + * @param session Control session. + * @param call_index To which call is this action handled. + * @return int + */ +int msi_stopcall ( MSISession *session, int32_t call_index ); + +#endif /* __TOXMSI */ diff --git a/protocols/Tox/toxcore/toxav/rtp.c b/protocols/Tox/toxcore/toxav/rtp.c new file mode 100644 index 0000000000..de6c9c418c --- /dev/null +++ b/protocols/Tox/toxcore/toxav/rtp.c @@ -0,0 +1,600 @@ +/** rtp.c + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include "../toxcore/logger.h" +#include "../toxcore/util.h" + +#include "rtp.h" +#include <stdlib.h> +void toxav_handle_packet(RTPSession *_session, RTPMessage *_msg); + +#define size_32 4 + +#define ADD_FLAG_VERSION(_h, _v) do { ( _h->flags ) &= 0x3F; ( _h->flags ) |= ( ( ( _v ) << 6 ) & 0xC0 ); } while(0) +#define ADD_FLAG_PADDING(_h, _v) do { if ( _v > 0 ) _v = 1; ( _h->flags ) &= 0xDF; ( _h->flags ) |= ( ( ( _v ) << 5 ) & 0x20 ); } while(0) +#define ADD_FLAG_EXTENSION(_h, _v) do { if ( _v > 0 ) _v = 1; ( _h->flags ) &= 0xEF;( _h->flags ) |= ( ( ( _v ) << 4 ) & 0x10 ); } while(0) +#define ADD_FLAG_CSRCC(_h, _v) do { ( _h->flags ) &= 0xF0; ( _h->flags ) |= ( ( _v ) & 0x0F ); } while(0) +#define ADD_SETTING_MARKER(_h, _v) do { if ( _v > 1 ) _v = 1; ( _h->marker_payloadt ) &= 0x7F; ( _h->marker_payloadt ) |= ( ( ( _v ) << 7 ) /*& 0x80 */ ); } while(0) +#define ADD_SETTING_PAYLOAD(_h, _v) do { if ( _v > 127 ) _v = 127; ( _h->marker_payloadt ) &= 0x80; ( _h->marker_payloadt ) |= ( ( _v ) /* & 0x7F */ ); } while(0) + +#define GET_FLAG_VERSION(_h) (( _h->flags & 0xd0 ) >> 6) +#define GET_FLAG_PADDING(_h) (( _h->flags & 0x20 ) >> 5) +#define GET_FLAG_EXTENSION(_h) (( _h->flags & 0x10 ) >> 4) +#define GET_FLAG_CSRCC(_h) ( _h->flags & 0x0f ) +#define GET_SETTING_MARKER(_h) (( _h->marker_payloadt ) >> 7) +#define GET_SETTING_PAYLOAD(_h) ((_h->marker_payloadt) & 0x7f) + +/** + * @brief Checks if message came in late. + * + * @param session Control session. + * @param msg The message. + * @return int + * @retval -1 The message came in order. + * @retval 0 The message came late. + */ +inline__ int check_late_message (RTPSession *session, RTPMessage *msg) +{ + /* + * Check Sequence number. If this new msg has lesser number then the session->rsequnum + * it shows that the message came in late. Also check timestamp to be 100% certain. + * + */ + return ( msg->header->sequnum < session->rsequnum && msg->header->timestamp < session->timestamp ) ? 0 : -1; +} + + +/** + * @brief Extracts header from payload. + * + * @param payload The payload. + * @param length The size of payload. + * @return RTPHeader* Extracted header. + * @retval NULL Error occurred while extracting header. + */ +RTPHeader *extract_header ( const uint8_t *payload, int length ) +{ + if ( !payload || !length ) { + LOGGER_WARNING("No payload to extract!"); + return NULL; + } + + RTPHeader *_retu = calloc(1, sizeof (RTPHeader)); + + if ( !_retu ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + return NULL; + } + + bytes_to_U16(&_retu->sequnum, payload); + + const uint8_t *_it = payload + 2; + + _retu->flags = *_it; + ++_it; + + /* This indicates if the first 2 bits are valid. + * Now it may happen that this is out of order but + * it cuts down chances of parsing some invalid value + */ + + if ( GET_FLAG_VERSION(_retu) != RTP_VERSION ) { + /* Deallocate */ + LOGGER_WARNING("Invalid version!"); + free(_retu); + return NULL; + } + + /* + * Added a check for the size of the header little sooner so + * I don't need to parse the other stuff if it's bad + */ + uint8_t _cc = GET_FLAG_CSRCC ( _retu ); + int _length = 12 /* Minimum header len */ + ( _cc * 4 ); + + if ( length < _length ) { + /* Deallocate */ + LOGGER_WARNING("Length invalid!"); + free(_retu); + return NULL; + } + + memset(_retu->csrc, 0, 16 * sizeof (uint32_t)); + + _retu->marker_payloadt = *_it; + ++_it; + _retu->length = _length; + + + bytes_to_U32(&_retu->timestamp, _it); + _it += 4; + bytes_to_U32(&_retu->ssrc, _it); + + uint8_t _x; + + for ( _x = 0; _x < _cc; _x++ ) { + _it += 4; + bytes_to_U32(&(_retu->csrc[_x]), _it); + } + + return _retu; +} + +/** + * @brief Extracts external header from payload. Must be called AFTER extract_header()! + * + * @param payload The ITERATED payload. + * @param length The size of payload. + * @return RTPExtHeader* Extracted extension header. + * @retval NULL Error occurred while extracting extension header. + */ +RTPExtHeader *extract_ext_header ( const uint8_t *payload, uint16_t length ) +{ + const uint8_t *_it = payload; + + RTPExtHeader *_retu = calloc(1, sizeof (RTPExtHeader)); + + if ( !_retu ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + return NULL; + } + + uint16_t _ext_length; + bytes_to_U16(&_ext_length, _it); + _it += 2; + + + if ( length < ( _ext_length * sizeof(uint32_t) ) ) { + LOGGER_WARNING("Length invalid!"); + free(_retu); + return NULL; + } + + _retu->length = _ext_length; + bytes_to_U16(&_retu->type, _it); + _it += 2; + + if ( !(_retu->table = calloc(_ext_length, sizeof (uint32_t))) ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + free(_retu); + return NULL; + } + + uint16_t _x; + + for ( _x = 0; _x < _ext_length; _x++ ) { + _it += 4; + bytes_to_U32(&(_retu->table[_x]), _it); + } + + return _retu; +} + +/** + * @brief Adds header to payload. Make sure _payload_ has enough space. + * + * @param header The header. + * @param payload The payload. + * @return uint8_t* Iterated position. + */ +uint8_t *add_header ( RTPHeader *header, uint8_t *payload ) +{ + uint8_t _cc = GET_FLAG_CSRCC ( header ); + + uint8_t *_it = payload; + + + /* Add sequence number first */ + U16_to_bytes(_it, header->sequnum); + _it += 2; + + *_it = header->flags; + ++_it; + *_it = header->marker_payloadt; + ++_it; + + + U32_to_bytes( _it, header->timestamp); + _it += 4; + U32_to_bytes( _it, header->ssrc); + + uint8_t _x; + + for ( _x = 0; _x < _cc; _x++ ) { + _it += 4; + U32_to_bytes( _it, header->csrc[_x]); + } + + return _it + 4; +} + +/** + * @brief Adds extension header to payload. Make sure _payload_ has enough space. + * + * @param header The header. + * @param payload The payload. + * @return uint8_t* Iterated position. + */ +uint8_t *add_ext_header ( RTPExtHeader *header, uint8_t *payload ) +{ + uint8_t *_it = payload; + + U16_to_bytes(_it, header->length); + _it += 2; + U16_to_bytes(_it, header->type); + _it -= 2; /* Return to 0 position */ + + if ( header->table ) { + uint16_t _x; + + for ( _x = 0; _x < header->length; _x++ ) { + _it += 4; + U32_to_bytes(_it, header->table[_x]); + } + } + + return _it + 4; +} + +/** + * @brief Builds header from control session values. + * + * @param session Control session. + * @return RTPHeader* Created header. + */ +RTPHeader *build_header ( RTPSession *session ) +{ + RTPHeader *_retu = calloc ( 1, sizeof (RTPHeader) ); + + if ( !_retu ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + return NULL; + } + + ADD_FLAG_VERSION ( _retu, session->version ); + ADD_FLAG_PADDING ( _retu, session->padding ); + ADD_FLAG_EXTENSION ( _retu, session->extension ); + ADD_FLAG_CSRCC ( _retu, session->cc ); + ADD_SETTING_MARKER ( _retu, session->marker ); + ADD_SETTING_PAYLOAD ( _retu, session->payload_type ); + + _retu->sequnum = session->sequnum; + _retu->timestamp = current_time_monotonic(); /* milliseconds */ + _retu->ssrc = session->ssrc; + + int i; + + for ( i = 0; i < session->cc; i++ ) + _retu->csrc[i] = session->csrc[i]; + + _retu->length = 12 /* Minimum header len */ + ( session->cc * size_32 ); + + return _retu; +} + + +/** + * @brief Parses data into RTPMessage struct. Stores headers separately from the payload data + * and so the length variable is set accordingly. _sequnum_ argument is + * passed by the handle_packet() since it's parsed already. + * + * @param session Control session. + * @param sequnum Sequence number that's parsed from payload in handle_packet() + * @param data Payload data. + * @param length Payload size. + * @return RTPMessage* + * @retval NULL Error occurred. + */ +RTPMessage *msg_parse ( const uint8_t *data, int length ) +{ + RTPMessage *_retu = calloc(1, sizeof (RTPMessage)); + + _retu->header = extract_header ( data, length ); /* It allocates memory and all */ + + if ( !_retu->header ) { + LOGGER_WARNING("Header failed to extract!"); + free(_retu); + return NULL; + } + + uint16_t _from_pos = _retu->header->length; + _retu->length = length - _from_pos; + + + + if ( GET_FLAG_EXTENSION ( _retu->header ) ) { + _retu->ext_header = extract_ext_header ( data + _from_pos, length ); + + if ( _retu->ext_header ) { + _retu->length -= ( 4 /* Minimum ext header len */ + _retu->ext_header->length * size_32 ); + _from_pos += ( 4 /* Minimum ext header len */ + _retu->ext_header->length * size_32 ); + } else { /* Error */ + LOGGER_WARNING("Ext Header failed to extract!"); + rtp_free_msg(NULL, _retu); + return NULL; + } + } else { + _retu->ext_header = NULL; + } + + if ( length - _from_pos <= MAX_RTP_SIZE ) + memcpy ( _retu->data, data + _from_pos, length - _from_pos ); + else { + LOGGER_WARNING("Invalid length!"); + rtp_free_msg(NULL, _retu); + return NULL; + } + + _retu->next = NULL; + + return _retu; +} + +/** + * @brief Callback for networking core. + * + * @param object RTPSession object. + * @param ip_port Where the message comes from. + * @param data Message data. + * @param length Message length. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +int rtp_handle_packet ( void *object, const uint8_t *data, uint32_t length ) +{ + RTPSession *_session = object; + RTPMessage *_msg; + + if ( !_session || length < 13 ) { /* 12 is the minimum length for rtp + desc. byte */ + LOGGER_WARNING("No session or invalid length of received buffer!"); + return -1; + } + + _msg = msg_parse ( data + 1, length - 1 ); + + if ( !_msg ) { + LOGGER_WARNING("Could not parse message!"); + return -1; + } + + /* Check if message came in late */ + if ( check_late_message(_session, _msg) < 0 ) { /* Not late */ + _session->rsequnum = _msg->header->sequnum; + _session->timestamp = _msg->header->timestamp; + } + + toxav_handle_packet(_session, _msg); + + return 0; +} + + + +/** + * @brief Stores headers and payload data in one container ( data ) + * and the length is set accordingly. Returned message is used for sending _only_. + * + * @param session The control session. + * @param data Payload data to send ( This is what you pass ). + * @param length Size of the payload data. + * @return RTPMessage* Created message. + * @retval NULL Error occurred. + */ +RTPMessage *rtp_new_message ( RTPSession *session, const uint8_t *data, uint32_t length ) +{ + if ( !session ) { + LOGGER_WARNING("No session!"); + return NULL; + } + + uint8_t *_from_pos; + RTPMessage *_retu = calloc(1, sizeof (RTPMessage)); + + if ( !_retu ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + return NULL; + } + + /* Sets header values and copies the extension header in _retu */ + _retu->header = build_header ( session ); /* It allocates memory and all */ + _retu->ext_header = session->ext_header; + + + uint32_t _total_length = length + _retu->header->length + 1; + + _retu->data[0] = session->prefix; + + if ( _retu->ext_header ) { + _total_length += ( 4 /* Minimum ext header len */ + _retu->ext_header->length * size_32 ); + + _from_pos = add_header ( _retu->header, _retu->data + 1 ); + _from_pos = add_ext_header ( _retu->ext_header, _from_pos + 1 ); + } else { + _from_pos = add_header ( _retu->header, _retu->data + 1 ); + } + + /* + * Parses the extension header into the message + * Of course if any + */ + + /* Appends _data on to _retu->_data */ + memcpy ( _from_pos, data, length ); + + _retu->length = _total_length; + + _retu->next = NULL; + + return _retu; +} + + +/** + * @brief Sends data to _RTPSession::dest + * + * @param session The session. + * @param messenger Tox* object. + * @param data The payload. + * @param length Size of the payload. + * @return int + * @retval -1 On error. + * @retval 0 On success. + */ +int rtp_send_msg ( RTPSession *session, Messenger *messenger, const uint8_t *data, uint16_t length ) +{ + RTPMessage *msg = rtp_new_message (session, data, length); + + if ( !msg ) { + LOGGER_WARNING("No session!"); + return -1; + } + + if ( -1 == send_custom_lossy_packet(messenger, session->dest, msg->data, msg->length) ) { + LOGGER_WARNING("Failed to send full packet! std error: %s", strerror(errno)); + rtp_free_msg ( session, msg ); + return -1; + } + + + /* Set sequ number */ + session->sequnum = session->sequnum >= MAX_SEQU_NUM ? 0 : session->sequnum + 1; + rtp_free_msg ( session, msg ); + + return 0; +} + + +/** + * @brief Speaks for it self. + * + * @param session The control session msg belongs to. You set it as NULL when freeing recved messages. + * Otherwise set it to session the message was created from. + * @param msg The message. + * @return void + */ +void rtp_free_msg ( RTPSession *session, RTPMessage *msg ) +{ + if ( !session ) { + if ( msg->ext_header ) { + free ( msg->ext_header->table ); + free ( msg->ext_header ); + } + } else { + if ( msg->ext_header && session->ext_header != msg->ext_header ) { + free ( msg->ext_header->table ); + free ( msg->ext_header ); + } + } + + free ( msg->header ); + free ( msg ); +} + +/** + * @brief Must be called before calling any other rtp function. It's used + * to initialize RTP control session. + * + * @param payload_type Type of payload used to send. You can use values in toxmsi.h::MSICallType + * @param messenger Tox* object. + * @param friend_num Friend id. + * @return RTPSession* Created control session. + * @retval NULL Error occurred. + */ +RTPSession *rtp_init_session ( int payload_type, Messenger *messenger, int friend_num ) +{ + RTPSession *_retu = calloc(1, sizeof(RTPSession)); + + if ( !_retu ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + return NULL; + } + + if ( -1 == custom_lossy_packet_registerhandler(messenger, friend_num, payload_type, rtp_handle_packet, _retu)) { + LOGGER_ERROR("Error setting custom register handler for rtp session"); + free(_retu); + return NULL; + } + + LOGGER_DEBUG("Registered packet handler: pt: %d; fid: %d", payload_type, friend_num); + + _retu->version = RTP_VERSION; /* It's always 2 */ + _retu->padding = 0; /* If some additional data is needed about the packet */ + _retu->extension = 0; /* If extension to header is needed */ + _retu->cc = 1; /* Amount of contributors */ + _retu->csrc = NULL; /* Container */ + _retu->ssrc = random_int(); + _retu->marker = 0; + _retu->payload_type = payload_type % 128; + + _retu->dest = friend_num; + + _retu->rsequnum = _retu->sequnum = 0; + + _retu->ext_header = NULL; /* When needed allocate */ + + + if ( !(_retu->csrc = calloc(1, sizeof (uint32_t))) ) { + LOGGER_WARNING("Alloc failed! Program might misbehave!"); + free(_retu); + return NULL; + } + + _retu->csrc[0] = _retu->ssrc; /* Set my ssrc to the list receive */ + + /* Also set payload type as prefix */ + _retu->prefix = payload_type; + + /* + * + */ + return _retu; +} + + +/** + * @brief Terminate the session. + * + * @param session The session. + * @param messenger The messenger who owns the session + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +void rtp_terminate_session ( RTPSession *session, Messenger *messenger ) +{ + if ( !session ) return; + + custom_lossy_packet_registerhandler(messenger, session->dest, session->prefix, NULL, NULL); + + free ( session->ext_header ); + free ( session->csrc ); + + LOGGER_DEBUG("Terminated RTP session: %p", session); + + /* And finally free session */ + free ( session ); + +} diff --git a/protocols/Tox/toxcore/toxav/rtp.h b/protocols/Tox/toxcore/toxav/rtp.h new file mode 100644 index 0000000000..d57c5ef720 --- /dev/null +++ b/protocols/Tox/toxcore/toxav/rtp.h @@ -0,0 +1,196 @@ +/** rtp.h + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __TOXRTP +#define __TOXRTP + +#define RTP_VERSION 2 +#include <inttypes.h> +#include <pthread.h> + +#include "../toxcore/util.h" +#include "../toxcore/network.h" +#include "../toxcore/net_crypto.h" +#include "../toxcore/Messenger.h" + +#define MAX_SEQU_NUM 65535 +#define MAX_RTP_SIZE 65535 + +/** + * @brief Standard rtp header + * + */ + +typedef struct _RTPHeader { + uint8_t flags; /* Version(2),Padding(1), Ext(1), Cc(4) */ + uint8_t marker_payloadt; /* Marker(1), PlayLoad Type(7) */ + uint16_t sequnum; /* Sequence Number */ + uint32_t timestamp; /* Timestamp */ + uint32_t ssrc; /* SSRC */ + uint32_t csrc[16]; /* CSRC's table */ + uint32_t length; /* Length of the header in payload string. */ + +} RTPHeader; + + +/** + * @brief Standard rtp extension header. + * + */ +typedef struct _RTPExtHeader { + uint16_t type; /* Extension profile */ + uint16_t length; /* Number of extensions */ + uint32_t *table; /* Extension's table */ + +} RTPExtHeader; + + +/** + * @brief Standard rtp message. + * + */ +typedef struct _RTPMessage { + RTPHeader *header; + RTPExtHeader *ext_header; + + uint8_t data[MAX_RTP_SIZE]; + uint32_t length; + + struct _RTPMessage *next; +} RTPMessage; + + +/** + * @brief Our main session descriptor. + * It measures the session variables and controls + * the entire session. There are functions for manipulating + * the session so tend to use those instead of directly modifying + * session parameters. + * + */ +typedef struct _RTPSession { + uint8_t version; + uint8_t padding; + uint8_t extension; + uint8_t cc; + uint8_t marker; + uint8_t payload_type; + uint16_t sequnum; /* Set when sending */ + uint16_t rsequnum; /* Check when recving msg */ + uint32_t timestamp; + uint32_t ssrc; + uint32_t *csrc; + + /* If some additional data must be sent via message + * apply it here. Only by allocating this member you will be + * automatically placing it within a message. + */ + RTPExtHeader *ext_header; + + /* Msg prefix for core to know when recving */ + uint8_t prefix; + + int dest; + int32_t call_index; + struct _ToxAv *av; + +} RTPSession; + + +/** + * @brief Release all messages held by session. + * + * @param session The session. + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +int rtp_release_session_recv ( RTPSession *session ); + + +/** + * @brief Call this to change queue limit + * + * @param session The session + * @param limit new limit + * @return void + */ +void rtp_queue_adjust_limit ( RTPSession *session, uint64_t limit ); + +/** + * @brief Get's oldest message in the list. + * + * @param session Where the list is. + * @return RTPMessage* The message. You need to call rtp_msg_free() to free it. + * @retval NULL No messages in the list, or no list. + */ +RTPMessage *rtp_recv_msg ( RTPSession *session ); + + +/** + * @brief Sends msg to _RTPSession::dest + * + * @param session The session. + * @param msg The message + * @param messenger Tox* object. + * @return int + * @retval -1 On error. + * @retval 0 On success. + */ +int rtp_send_msg ( RTPSession *session, Messenger *messenger, const uint8_t *data, uint16_t length ); + + +/** + * @brief Speaks for it self. + * + * @param session The control session msg belongs to. It can be NULL. + * @param msg The message. + * @return void + */ +void rtp_free_msg ( RTPSession *session, RTPMessage *msg ); + +/** + * @brief Must be called before calling any other rtp function. It's used + * to initialize RTP control session. + * + * @param payload_type Type of payload used to send. You can use values in toxmsi.h::MSICallType + * @param messenger Tox* object. + * @param friend_num Friend id. + * @return RTPSession* Created control session. + * @retval NULL Error occurred. + */ +RTPSession *rtp_init_session ( int payload_type, Messenger *messenger, int friend_num ); + + +/** + * @brief Terminate the session. + * + * @param session The session. + * @param messenger The messenger who owns the session + * @return int + * @retval -1 Error occurred. + * @retval 0 Success. + */ +void rtp_terminate_session ( RTPSession *session, Messenger *messenger ); + + + +#endif /* __TOXRTP */ diff --git a/protocols/Tox/toxcore/toxav/toxav.c b/protocols/Tox/toxcore/toxav/toxav.c new file mode 100644 index 0000000000..cd0ec70ed7 --- /dev/null +++ b/protocols/Tox/toxcore/toxav/toxav.c @@ -0,0 +1,1148 @@ +/** toxav.c + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + + +#define _GNU_SOURCE /* implicit declaration warning */ + +#include "rtp.h" +#include "codec.h" +#include "msi.h" +#include "toxav.h" + +#include "../toxcore/logger.h" + +#include <assert.h> +#include <stdlib.h> +#include <string.h> + +/* Assume 24 fps*/ +#define MAX_ENCODE_TIME_US ((1000 / 24) * 1000) +#define MAX_DECODE_TIME_US 0 + +#define MAX_VIDEOFRAME_SIZE 0x40000 /* 256KiB */ +#define VIDEOFRAME_PIECE_SIZE 0x500 /* 1.25 KiB*/ +#define VIDEOFRAME_HEADER_SIZE 0x2 + + +#define inline__ inline __attribute__((always_inline)) + +/* call index invalid: true if invalid */ +#define cii(c_idx, session) (c_idx < 0 || c_idx >= session->max_calls) + + +const ToxAvCSettings av_DefaultSettings = { + TypeAudio, + + 500, + 1280, + 720, + + 64000, + 20, + 48000, + 1 +}; + +const uint32_t av_jbufdc = 3; +const uint32_t av_VADd = 40; + + +static const uint8_t audio_index = 0, video_index = 1; + +typedef struct { + uint32_t size; + uint8_t data[0]; +} DECODE_PACKET; + +#define VIDEO_DECODE_QUEUE_SIZE 2 +#define AUDIO_DECODE_QUEUE_SIZE 16 + +typedef struct _CallSpecific { + RTPSession *crtps[2]; /** Audio is first and video is second */ + CodecState *cs;/** Each call have its own encoders and decoders. + * You can, but don't have to, reuse encoders for + * multiple calls. If you choose to reuse encoders, + * make sure to also reuse encoded payload for every call. + * Decoders have to be unique for each call. FIXME: Now add refcounted encoders and + * reuse them really. + */ + JitterBuffer *j_buf; /** Jitter buffer for audio */ + + uint32_t frame_limit; /* largest address written to in frame_buf for current input frame*/ + uint8_t frame_id, frame_outid; /* id of input and output video frame */ + void *frame_buf; /* buffer for split video payloads */ + + _Bool call_active; + pthread_mutex_t mutex; + + /* used in the "decode on another thread" system */ + volatile _Bool exit, decoding; + uint8_t video_decode_read, video_decode_write, audio_decode_read, audio_decode_write; + pthread_mutex_t decode_cond_mutex; + pthread_cond_t decode_cond; + DECODE_PACKET *volatile video_decode_queue[VIDEO_DECODE_QUEUE_SIZE]; + DECODE_PACKET *volatile audio_decode_queue[AUDIO_DECODE_QUEUE_SIZE]; +} CallSpecific; + +struct _ToxAv { + Messenger *messenger; + MSISession *msi_session; /** Main msi session */ + CallSpecific *calls; /** Per-call params */ + + void (*audio_callback)(ToxAv *, int32_t, int16_t *, int, void *); + void (*video_callback)(ToxAv *, int32_t, vpx_image_t *, void *); + + void *audio_callback_userdata; + void *video_callback_userdata; + + uint32_t max_calls; +}; + +static void *toxav_decoding(void *arg); + +static MSICSettings msicsettings_cast (const ToxAvCSettings *from) +{ + MSICSettings csettings; + csettings.call_type = from->call_type; + + csettings.video_bitrate = from->video_bitrate; + csettings.max_video_width = from->max_video_width; + csettings.max_video_height = from->max_video_height; + + csettings.audio_bitrate = from->audio_bitrate; + csettings.audio_frame_duration = from->audio_frame_duration; + csettings.audio_sample_rate = from->audio_sample_rate; + csettings.audio_channels = from->audio_channels; + + return csettings; +} + +static ToxAvCSettings toxavcsettings_cast (const MSICSettings *from) +{ + ToxAvCSettings csettings; + csettings.call_type = from->call_type; + + csettings.video_bitrate = from->video_bitrate; + csettings.max_video_width = from->max_video_width; + csettings.max_video_height = from->max_video_height; + + csettings.audio_bitrate = from->audio_bitrate; + csettings.audio_frame_duration = from->audio_frame_duration; + csettings.audio_sample_rate = from->audio_sample_rate; + csettings.audio_channels = from->audio_channels; + + return csettings; +} + +/** + * @brief Start new A/V session. There can only be one session at the time. If you register more + * it will result in undefined behaviour. + * + * @param messenger The messenger handle. + * @param userdata The agent handling A/V session (i.e. phone). + * @param video_width Width of video frame. + * @param video_height Height of video frame. + * @return ToxAv* + * @retval NULL On error. + */ +ToxAv *toxav_new( Tox *messenger, int32_t max_calls) +{ + ToxAv *av = calloc ( sizeof(ToxAv), 1); + + if (av == NULL) { + LOGGER_WARNING("Allocation failed!"); + return NULL; + } + + av->messenger = (Messenger *)messenger; + av->msi_session = msi_init_session(av->messenger, max_calls); + av->msi_session->agent_handler = av; + av->calls = calloc(sizeof(CallSpecific), max_calls); + av->max_calls = max_calls; + + return av; +} + +/** + * @brief Remove A/V session. + * + * @param av Handler. + * @return void + */ +void toxav_kill ( ToxAv *av ) +{ + uint32_t i; + + for (i = 0; i < av->max_calls; i ++) { + if ( av->calls[i].crtps[audio_index] ) + rtp_terminate_session(av->calls[i].crtps[audio_index], av->msi_session->messenger_handle); + + + if ( av->calls[i].crtps[video_index] ) + rtp_terminate_session(av->calls[i].crtps[video_index], av->msi_session->messenger_handle); + + + + if ( av->calls[i].j_buf ) terminate_queue(av->calls[i].j_buf); + + if ( av->calls[i].cs ) codec_terminate_session(av->calls[i].cs); + } + + msi_terminate_session(av->msi_session); + + free(av->calls); + free(av); +} + +/** + * @brief Register callback for call state. + * + * @param av Handler. + * @param callback The callback + * @param id One of the ToxAvCallbackID values + * @return void + */ +void toxav_register_callstate_callback ( ToxAv *av, ToxAVCallback callback, ToxAvCallbackID id, void *userdata ) +{ + msi_register_callback(av->msi_session, (MSICallbackType)callback, (MSICallbackID) id, userdata); +} + +/** + * @brief Register callback for recieving audio data + * + * @param callback The callback + * @return void + */ +void toxav_register_audio_recv_callback (ToxAv *av, void (*callback)(ToxAv *, int32_t, int16_t *, int, void *), + void *user_data) +{ + av->audio_callback = callback; + av->audio_callback_userdata = user_data; +} + +/** + * @brief Register callback for recieving video data + * + * @param callback The callback + * @return void + */ +void toxav_register_video_recv_callback (ToxAv *av, void (*callback)(ToxAv *, int32_t, vpx_image_t *, void *), + void *user_data) +{ + av->video_callback = callback; + av->video_callback_userdata = user_data; +} + +/** + * @brief Call user. Use its friend_id. + * + * @param av Handler. + * @param user The user. + * @param call_type Call type. + * @param ringing_seconds Ringing timeout. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_call (ToxAv *av, int32_t *call_index, int user, const ToxAvCSettings *csettings, int ringing_seconds ) +{ + return msi_invite(av->msi_session, call_index, msicsettings_cast(csettings), ringing_seconds * 1000, user); +} + +/** + * @brief Hangup active call. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_hangup ( ToxAv *av, int32_t call_index ) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + if ( av->msi_session->calls[call_index]->state != call_active ) { + return ErrorInvalidState; + } + + return msi_hangup(av->msi_session, call_index); +} + +/** + * @brief Answer incomming call. + * + * @param av Handler. + * @param call_type Answer with... + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_answer ( ToxAv *av, int32_t call_index, const ToxAvCSettings *csettings ) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + if ( av->msi_session->calls[call_index]->state != call_starting ) { + return ErrorInvalidState; + } + + return msi_answer(av->msi_session, call_index, msicsettings_cast(csettings)); +} + +/** + * @brief Reject incomming call. + * + * @param av Handler. + * @param reason Optional reason. Set NULL if none. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_reject ( ToxAv *av, int32_t call_index, const char *reason ) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + if ( av->msi_session->calls[call_index]->state != call_starting ) { + return ErrorInvalidState; + } + + return msi_reject(av->msi_session, call_index, reason); +} + +/** + * @brief Cancel outgoing request. + * + * @param av Handler. + * @param reason Optional reason. + * @param peer_id peer friend_id + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_cancel ( ToxAv *av, int32_t call_index, int peer_id, const char *reason ) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + if ( av->msi_session->calls[call_index]->state != call_inviting ) { + return ErrorInvalidState; + } + + return msi_cancel(av->msi_session, call_index, peer_id, reason); +} + +/** + * @brief Notify peer that we are changing call type + * + * @param av Handler. + * @return int + * @param call_type Change to... + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_change_settings(ToxAv *av, int32_t call_index, const ToxAvCSettings *csettings) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + return msi_change_csettings(av->msi_session, call_index, msicsettings_cast(csettings)); +} + +/** + * @brief Terminate transmission. Note that transmission will be terminated without informing remote peer. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_stop_call ( ToxAv *av, int32_t call_index ) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) { + return ErrorNoCall; + } + + return msi_stopcall(av->msi_session, call_index); +} + +/** + * @brief Must be call before any RTP transmission occurs. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_prepare_transmission ( ToxAv *av, int32_t call_index, uint32_t jbuf_capacity, uint32_t VAD_treshold, + int support_video ) +{ + if ( !av->msi_session || cii(call_index, av->msi_session) || + !av->msi_session->calls[call_index] || !av->msi_session->calls[call_index]->csettings_peer || + av->calls[call_index].call_active) { + LOGGER_ERROR("Error while starting RTP session: invalid call!\n"); + return ErrorInternal; + } + + CallSpecific *call = &av->calls[call_index]; + + call->crtps[audio_index] = + rtp_init_session(type_audio, av->messenger, av->msi_session->calls[call_index]->peers[0]); + + + if ( !call->crtps[audio_index] ) { + LOGGER_ERROR("Error while starting audio RTP session!\n"); + return ErrorInternal; + } + + call->crtps[audio_index]->call_index = call_index; + call->crtps[audio_index]->av = av; + + if ( support_video ) { + call->crtps[video_index] = + rtp_init_session(type_video, av->messenger, av->msi_session->calls[call_index]->peers[0]); + + if ( !call->crtps[video_index] ) { + LOGGER_ERROR("Error while starting video RTP session!\n"); + goto error; + } + + call->crtps[video_index]->call_index = call_index; + call->crtps[video_index]->av = av; + + call->frame_limit = 0; + call->frame_id = 0; + call->frame_outid = 0; + + call->frame_buf = calloc(MAX_VIDEOFRAME_SIZE, 1); + + if (!call->frame_buf) { + LOGGER_WARNING("Frame buffer allocation failed!"); + goto error; + } + + } + + if ( !(call->j_buf = create_queue(jbuf_capacity)) ) { + LOGGER_WARNING("Jitter buffer creaton failed!"); + goto error; + } + + ToxAvCSettings csettings_peer = toxavcsettings_cast(&av->msi_session->calls[call_index]->csettings_peer[0]); + ToxAvCSettings csettings_local = toxavcsettings_cast(&av->msi_session->calls[call_index]->csettings_local); + LOGGER_DEBUG( + "Type: %u \n" + "Video bitrate: %u \n" + "Video height: %u \n" + "Video width: %u \n" + "Audio bitrate: %u \n" + "Audio framedur: %u \n" + "Audio sample rate: %u \n" + "Audio channels: %u \n", + csettings_peer.call_type, + csettings_peer.video_bitrate, + csettings_peer.max_video_height, + csettings_peer.max_video_width, + csettings_peer.audio_bitrate, + csettings_peer.audio_frame_duration, + csettings_peer.audio_sample_rate, + csettings_peer.audio_channels ); + + if ( (call->cs = codec_init_session(csettings_local.audio_bitrate, + csettings_local.audio_frame_duration, + csettings_local.audio_sample_rate, + csettings_local.audio_channels, + csettings_peer.audio_channels, + VAD_treshold, + csettings_local.max_video_width, + csettings_local.max_video_height, + csettings_local.video_bitrate) )) { + + if ( pthread_mutex_init(&call->mutex, NULL) != 0 ) goto error; + + //todo: add error checks + pthread_mutex_init(&call->decode_cond_mutex, NULL); + pthread_cond_init(&call->decode_cond, NULL); + + void **arg = malloc(2 * sizeof(void *)); + arg[0] = av; + arg[1] = call; + + pthread_t temp; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 1 << 18); + pthread_create(&temp, &attr, toxav_decoding, arg); + pthread_attr_destroy(&attr); + + + LOGGER_WARNING("Got here"); + call->call_active = 1; + + return ErrorNone; + } + +error: + rtp_terminate_session(call->crtps[audio_index], av->messenger); + rtp_terminate_session(call->crtps[video_index], av->messenger); + free(call->frame_buf); + terminate_queue(call->j_buf); + codec_terminate_session(call->cs); + + return ErrorInternal; +} + +/** + * @brief Call this at the end of the transmission. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_kill_transmission ( ToxAv *av, int32_t call_index ) +{ + if (cii(call_index, av->msi_session)) { + LOGGER_WARNING("Invalid call index: %d", call_index); + return ErrorNoCall; + } + + CallSpecific *call = &av->calls[call_index]; + + pthread_mutex_lock(&call->mutex); + + if (!call->call_active) { + pthread_mutex_unlock(&call->mutex); + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + + call->call_active = 0; + + rtp_terminate_session(call->crtps[audio_index], av->messenger); + call->crtps[audio_index] = NULL; + rtp_terminate_session(call->crtps[video_index], av->messenger); + call->crtps[video_index] = NULL; + terminate_queue(call->j_buf); + call->j_buf = NULL; + + int i; + DECODE_PACKET *p; + + call->exit = 1; + pthread_mutex_lock(&call->decode_cond_mutex); + pthread_cond_signal(&call->decode_cond); + pthread_cond_wait(&call->decode_cond, &call->decode_cond_mutex); + pthread_mutex_unlock(&call->decode_cond_mutex); + pthread_mutex_destroy(&call->decode_cond_mutex); + pthread_cond_destroy(&call->decode_cond); + + for (i = 0; i != VIDEO_DECODE_QUEUE_SIZE; i++) { + p = call->video_decode_queue[i]; + call->video_decode_queue[i] = NULL; + + if (p) { + free(p); + } + } + + for (i = 0; i != AUDIO_DECODE_QUEUE_SIZE; i++) { + p = call->audio_decode_queue[i]; + call->audio_decode_queue[i] = NULL; + + if (p) { + free(p); + } + } + + codec_terminate_session(call->cs); + call->cs = NULL; + + pthread_mutex_unlock(&call->mutex); + pthread_mutex_destroy(&call->mutex); + + memset(call, 0, sizeof(CallSpecific)); + return ErrorNone; +} + + +/** + * @brief Send RTP payload. + * + * @param av Handler. + * @param type Type of payload. + * @param payload The payload. + * @param length Size of it. + * @return int + * @retval 0 Success. + * @retval -1 Failure. + */ +static int toxav_send_rtp_payload(ToxAv *av, int32_t call_index, ToxAvCallType type, const uint8_t *payload, + unsigned int length) +{ + CallSpecific *call = &av->calls[call_index]; + + if (call->crtps[type - TypeAudio]) { + + if (type == TypeAudio) { + return rtp_send_msg(call->crtps[type - TypeAudio], av->messenger, payload, length); + } else { + if (length == 0 || length > MAX_VIDEOFRAME_SIZE) { + LOGGER_ERROR("Invalid video frame size: %u\n", length); + return ErrorInternal; + } + + /* number of pieces - 1*/ + uint8_t numparts = (length - 1) / VIDEOFRAME_PIECE_SIZE; + + uint8_t load[2 + VIDEOFRAME_PIECE_SIZE]; + load[0] = call->frame_outid++; + load[1] = 0; + + int i; + + for (i = 0; i < numparts; i++) { + memcpy(load + VIDEOFRAME_HEADER_SIZE, payload, VIDEOFRAME_PIECE_SIZE); + payload += VIDEOFRAME_PIECE_SIZE; + + if (rtp_send_msg(call->crtps[type - TypeAudio], av->messenger, + load, VIDEOFRAME_HEADER_SIZE + VIDEOFRAME_PIECE_SIZE) != 0) { + + return ErrorInternal; + } + + load[1]++; + } + + /* remainder = length % VIDEOFRAME_PIECE_SIZE, VIDEOFRAME_PIECE_SIZE if = 0 */ + length = ((length - 1) % VIDEOFRAME_PIECE_SIZE) + 1; + memcpy(load + VIDEOFRAME_HEADER_SIZE, payload, length); + + return rtp_send_msg(call->crtps[type - TypeAudio], av->messenger, load, VIDEOFRAME_HEADER_SIZE + length); + } + } else { + return ErrorNoRtpSession; + } +} + +/** + * @brief Encode and send video packet. + * + * @param av Handler. + * @param input The packet. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_send_video ( ToxAv *av, int32_t call_index, const uint8_t *frame, unsigned int frame_size) +{ + + if (cii(call_index, av->msi_session)) { + LOGGER_WARNING("Invalid call index: %d", call_index); + return ErrorNoCall; + } + + CallSpecific *call = &av->calls[call_index]; + pthread_mutex_lock(&call->mutex); + + + if (!call->call_active) { + pthread_mutex_unlock(&call->mutex); + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + int rc = toxav_send_rtp_payload(av, call_index, TypeVideo, frame, frame_size); + pthread_mutex_unlock(&call->mutex); + + return rc; +} + +/** + * @brief Encode video frame + * + * @param av Handler + * @param dest Where to + * @param dest_max Max size + * @param input What to encode + * @return int + * @retval ToxAvError On error. + * @retval >0 On success + */ +int toxav_prepare_video_frame(ToxAv *av, int32_t call_index, uint8_t *dest, int dest_max, vpx_image_t *input) +{ + if (cii(call_index, av->msi_session)) { + LOGGER_WARNING("Invalid call index: %d", call_index); + return ErrorNoCall; + } + + + CallSpecific *call = &av->calls[call_index]; + pthread_mutex_lock(&call->mutex); + + if (!call->call_active) { + pthread_mutex_unlock(&call->mutex); + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + if (reconfigure_video_encoder_resolution(call->cs, input->d_w, input->d_h) != 0) { + pthread_mutex_unlock(&call->mutex); + return ErrorInternal; + } + + int rc = vpx_codec_encode(&call->cs->v_encoder, input, call->cs->frame_counter, 1, 0, MAX_ENCODE_TIME_US); + + if ( rc != VPX_CODEC_OK) { + LOGGER_ERROR("Could not encode video frame: %s\n", vpx_codec_err_to_string(rc)); + pthread_mutex_unlock(&call->mutex); + return ErrorInternal; + } + + ++call->cs->frame_counter; + + vpx_codec_iter_t iter = NULL; + const vpx_codec_cx_pkt_t *pkt; + int copied = 0; + + while ( (pkt = vpx_codec_get_cx_data(&call->cs->v_encoder, &iter)) ) { + if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) { + if ( copied + pkt->data.frame.sz > dest_max ) { + pthread_mutex_unlock(&call->mutex); + return ErrorPacketTooLarge; + } + + memcpy(dest + copied, pkt->data.frame.buf, pkt->data.frame.sz); + copied += pkt->data.frame.sz; + } + } + + pthread_mutex_unlock(&call->mutex); + return copied; +} + +/** + * @brief Send audio frame. + * + * @param av Handler. + * @param data The audio data encoded with toxav_prepare_audio_frame(). + * @param size Its size in number of bytes. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_send_audio ( ToxAv *av, int32_t call_index, const uint8_t *data, unsigned int size) +{ + if (size > MAX_CRYPTO_DATA_SIZE) + return ErrorInternal; + + if (cii(call_index, av->msi_session) || !av->calls[call_index].call_active) { + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + CallSpecific *call = &av->calls[call_index]; + pthread_mutex_lock(&call->mutex); + + + if (!call->call_active) { + pthread_mutex_unlock(&call->mutex); + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + int rc = toxav_send_rtp_payload(av, call_index, TypeAudio, data, size); + pthread_mutex_unlock(&call->mutex); + + return rc; +} + +/** + * @brief Encode audio frame + * + * @param av Handler + * @param dest dest + * @param dest_max Max dest size + * @param frame The frame + * @param frame_size The frame size + * @return int + * @retval ToxAvError On error. + * @retval >0 On success + */ +int toxav_prepare_audio_frame ( ToxAv *av, int32_t call_index, uint8_t *dest, int dest_max, const int16_t *frame, + int frame_size) +{ + if (cii(call_index, av->msi_session) || !av->calls[call_index].call_active) { + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + CallSpecific *call = &av->calls[call_index]; + pthread_mutex_lock(&call->mutex); + + + if (!call->call_active) { + pthread_mutex_unlock(&call->mutex); + LOGGER_WARNING("Action on inactive call: %d", call_index); + return ErrorNoCall; + } + + int32_t rc = opus_encode(call->cs->audio_encoder, frame, frame_size, dest, dest_max); + pthread_mutex_unlock(&call->mutex); + + if (rc < 0) { + LOGGER_ERROR("Failed to encode payload: %s\n", opus_strerror(rc)); + return ErrorInternal; + } + + return rc; +} + +/** + * @brief Get peer transmission type. It can either be audio or video. + * + * @param av Handler. + * @param peer The peer + * @return int + * @retval ToxAvCallType On success. + * @retval ToxAvError On error. + */ +int toxav_get_peer_csettings ( ToxAv *av, int32_t call_index, int peer, ToxAvCSettings *dest ) +{ + if ( peer < 0 || cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] + || av->msi_session->calls[call_index]->peer_count <= peer ) + return ErrorInternal; + + *dest = toxavcsettings_cast(&av->msi_session->calls[call_index]->csettings_peer[peer]); + return ErrorNone; +} + +/** + * @brief Get id of peer participating in conversation + * + * @param av Handler + * @param peer peer index + * @return int + * @retval ToxAvError No peer id + */ +int toxav_get_peer_id ( ToxAv *av, int32_t call_index, int peer ) +{ + if ( peer < 0 || cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] + || av->msi_session->calls[call_index]->peer_count <= peer ) + return ErrorInternal; + + return av->msi_session->calls[call_index]->peers[peer]; +} + +/** + * @brief Get id of peer participating in conversation + * + * @param av Handler + * @param peer peer index + * @return int + * @retval ToxAvError No peer id + */ +ToxAvCallState toxav_get_call_state(ToxAv *av, int32_t call_index) +{ + if ( cii(call_index, av->msi_session) || !av->msi_session->calls[call_index] ) + return av_CallNonExistant; + + return av->msi_session->calls[call_index]->state; + +} + +/** + * @brief Is certain capability supported + * + * @param av Handler + * @return int + * @retval 1 Yes. + * @retval 0 No. + */ +inline__ int toxav_capability_supported ( ToxAv *av, int32_t call_index, ToxAvCapabilities capability ) +{ + return av->calls[call_index].cs ? av->calls[call_index].cs->capabilities & (Capabilities) capability : 0; + /* 0 is error here */ +} + +inline__ Tox *toxav_get_tox(ToxAv *av) +{ + return (Tox *)av->messenger; +} + +int toxav_has_activity(ToxAv *av, int32_t call_index, int16_t *PCM, uint16_t frame_size, float ref_energy) +{ + if ( !av->calls[call_index].cs ) return ErrorInvalidCodecState; + + return energy_VAD(av->calls[call_index].cs, PCM, frame_size, ref_energy); +} + + +static void decode_video(ToxAv *av, CallSpecific *call, DECODE_PACKET *p) +{ + int32_t call_index = call - av->calls; + + int rc = vpx_codec_decode(&call->cs->v_decoder, p->data, p->size, NULL, MAX_DECODE_TIME_US); + + if (rc != VPX_CODEC_OK) { + LOGGER_ERROR("Error decoding video: %s\n", vpx_codec_err_to_string(rc)); + } + + vpx_codec_iter_t iter = NULL; + vpx_image_t *img; + img = vpx_codec_get_frame(&call->cs->v_decoder, &iter); + + if (img && av->video_callback) { + av->video_callback(av, call_index, img, av->video_callback_userdata); + } else { + LOGGER_WARNING("Video packet dropped due to missing callback or no image!"); + } + + free(p); +} + +static void decode_audio(ToxAv *av, CallSpecific *call, DECODE_PACKET *p) +{ + int32_t call_index = call - av->calls; + + // ToxAvCSettings csettings; + // toxav_get_peer_csettings(av, call_index, 0, &csettings); + + int frame_size = 10000; /* FIXME: not static? */ + int16_t dest[frame_size]; + + int dec_size = opus_decode(call->cs->audio_decoder, p->data, p->size, dest, frame_size, (p->size == 0)); + free(p); + + if (dec_size < 0) { + LOGGER_WARNING("Decoding error: %s", opus_strerror(dec_size)); + return; + } + + if ( av->audio_callback ) + av->audio_callback(av, call_index, dest, dec_size, av->audio_callback_userdata); + else + LOGGER_WARNING("Audio packet dropped due to missing callback!"); +} + +static void *toxav_decoding(void *arg) +{ + void **pp = arg; + ToxAv *av = pp[0]; + CallSpecific *call = pp[1]; + free(pp); + + while (1) { + DECODE_PACKET *p; + _Bool video = 0; + + pthread_mutex_lock(&call->decode_cond_mutex); + + if (call->exit) { + break; + } + + uint8_t r; + + /* first check for available packets, otherwise wait for condition*/ + r = call->audio_decode_read; + p = call->audio_decode_queue[r]; + + if (!p) { + r = call->video_decode_read; + p = call->video_decode_queue[r]; + + if (!p) { + pthread_cond_wait(&call->decode_cond, &call->decode_cond_mutex); + r = call->audio_decode_read; + p = call->audio_decode_queue[r]; + + if (!p) { + r = call->video_decode_read; + p = call->video_decode_queue[r]; + video = 1; + } + } else { + video = 1; + } + } + + if (video) { + if (p) { + call->video_decode_queue[r] = NULL; + call->video_decode_read = (r + 1) % VIDEO_DECODE_QUEUE_SIZE; + } + } else { + call->audio_decode_queue[r] = NULL; + call->audio_decode_read = (r + 1) % AUDIO_DECODE_QUEUE_SIZE; + } + + pthread_mutex_unlock(&call->decode_cond_mutex); + + if (p) { + if (video) { + decode_video(av, call, p); + } else { + decode_audio(av, call, p); + } + } + } + + call->exit = 0; + pthread_cond_signal(&call->decode_cond); + pthread_mutex_unlock(&call->decode_cond_mutex); + + return NULL; +} + +void toxav_handle_packet(RTPSession *_session, RTPMessage *_msg) +{ + ToxAv *av = _session->av; + int32_t call_index = _session->call_index; + CallSpecific *call = &av->calls[call_index]; + + if (!call->call_active) return; + + if (_session->payload_type == type_audio % 128) { + queue(call->j_buf, _msg); + + int success = 0; + + while ((_msg = dequeue(call->j_buf, &success)) || success == 2) { + DECODE_PACKET *p; + + if (success == 2) { + p = malloc(sizeof(DECODE_PACKET)); + + if (p) { + p->size = 0; + } + } else { + p = malloc(sizeof(DECODE_PACKET) + _msg->length); + + if (p) { + p->size = _msg->length; + memcpy(p->data, _msg->data, _msg->length); + } + + rtp_free_msg(NULL, _msg); + } + + if (p) { + /* do the decoding on another thread */ + pthread_mutex_lock(&call->decode_cond_mutex); + uint8_t w = call->audio_decode_write; + + if (call->audio_decode_queue[w] == NULL) { + call->audio_decode_queue[w] = p; + call->audio_decode_write = (w + 1) % AUDIO_DECODE_QUEUE_SIZE; + pthread_cond_signal(&call->decode_cond); + } else { + LOGGER_DEBUG("Dropped audio frame\n"); + free(p); + } + + pthread_mutex_unlock(&call->decode_cond_mutex); + } else { + //malloc failed + } + } + + } else { + uint8_t *packet = _msg->data; + int recved_size = _msg->length; + + if (recved_size < VIDEOFRAME_HEADER_SIZE) { + goto end; + } + + uint8_t i = packet[0] - call->frame_id; + + if (i == 0) { + /* piece of current frame */ + } else if (i > 0 && i < 128) { + /* recieved a piece of a frame ahead, flush current frame and start reading this new frame */ + DECODE_PACKET *p = malloc(sizeof(DECODE_PACKET) + call->frame_limit); + + if (p) { + p->size = call->frame_limit; + memcpy(p->data, call->frame_buf, call->frame_limit); + + /* do the decoding on another thread */ + pthread_mutex_lock(&call->decode_cond_mutex); + uint8_t w = call->video_decode_write; + + if (call->video_decode_queue[w] == NULL) { + call->video_decode_queue[w] = p; + call->video_decode_write = (w + 1) % VIDEO_DECODE_QUEUE_SIZE; + pthread_cond_signal(&call->decode_cond); + } else { + LOGGER_DEBUG("Dropped video frame\n"); + free(p); + } + + pthread_mutex_unlock(&call->decode_cond_mutex); + } else { + //malloc failed + } + + call->frame_id = packet[0]; + memset(call->frame_buf, 0, call->frame_limit); + call->frame_limit = 0; + } else { + /* old packet, dont read */ + LOGGER_DEBUG("Old packet: %u\n", i); + goto end; + } + + if (packet[1] > (MAX_VIDEOFRAME_SIZE - VIDEOFRAME_PIECE_SIZE + 1) / + VIDEOFRAME_PIECE_SIZE) { //TODO, fix this check? not sure + /* packet out of buffer range */ + goto end; + } + + LOGGER_DEBUG("Video Packet: %u %u\n", packet[0], packet[1]); + memcpy(call->frame_buf + packet[1] * VIDEOFRAME_PIECE_SIZE, packet + VIDEOFRAME_HEADER_SIZE, + recved_size - VIDEOFRAME_HEADER_SIZE); + uint32_t limit = packet[1] * VIDEOFRAME_PIECE_SIZE + recved_size - VIDEOFRAME_HEADER_SIZE; + + if (limit > call->frame_limit) { + call->frame_limit = limit; + LOGGER_DEBUG("Limit: %u\n", call->frame_limit); + } + +end: + ; + rtp_free_msg(NULL, _msg); + } +} diff --git a/protocols/Tox/toxcore/toxav/toxav.h b/protocols/Tox/toxcore/toxav/toxav.h new file mode 100644 index 0000000000..e31c7aad1f --- /dev/null +++ b/protocols/Tox/toxcore/toxav/toxav.h @@ -0,0 +1,389 @@ +/** toxav.h + * + * Copyright (C) 2013 Tox project All Rights Reserved. + * + * This file is part of Tox. + * + * Tox is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Tox is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Tox. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +#ifndef __TOXAV +#define __TOXAV +#include <inttypes.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* vpx_image_t */ +#include <vpx/vpx_image.h> + +typedef void ( *ToxAVCallback ) ( void *agent, int32_t call_idx, void *arg ); +typedef struct _ToxAv ToxAv; + +#ifndef __TOX_DEFINED__ +#define __TOX_DEFINED__ +typedef struct Tox Tox; +#endif + +#define RTP_PAYLOAD_SIZE 65535 + + +/** + * @brief Callbacks ids that handle the call states. + */ +typedef enum { + /* Requests */ + av_OnInvite, + av_OnStart, + av_OnCancel, + av_OnReject, + av_OnEnd, + + /* Responses */ + av_OnRinging, + av_OnStarting, + av_OnEnding, + + /* Protocol */ + av_OnRequestTimeout, + av_OnPeerTimeout, + av_OnMediaChange +} ToxAvCallbackID; + + +/** + * @brief Call type identifier. + */ +typedef enum { + TypeAudio = 192, + TypeVideo +} ToxAvCallType; + + +typedef enum { + av_CallNonExistant = -1, + av_CallInviting, /* when sending call invite */ + av_CallStarting, /* when getting call invite */ + av_CallActive, + av_CallHold, + av_CallHanged_up +} ToxAvCallState; + +/** + * @brief Error indicators. + */ +typedef enum { + ErrorNone = 0, + ErrorInternal = -1, /* Internal error */ + ErrorAlreadyInCall = -2, /* Already has an active call */ + ErrorNoCall = -3, /* Trying to perform call action while not in a call */ + ErrorInvalidState = -4, /* Trying to perform call action while in invalid state*/ + ErrorNoRtpSession = -5, /* Trying to perform rtp action on invalid session */ + ErrorAudioPacketLost = -6, /* Indicating packet loss */ + ErrorStartingAudioRtp = -7, /* Error in toxav_prepare_transmission() */ + ErrorStartingVideoRtp = -8 , /* Error in toxav_prepare_transmission() */ + ErrorTerminatingAudioRtp = -9, /* Returned in toxav_kill_transmission() */ + ErrorTerminatingVideoRtp = -10, /* Returned in toxav_kill_transmission() */ + ErrorPacketTooLarge = -11, /* Buffer exceeds size while encoding */ + ErrorInvalidCodecState = -12, /* Codec state not initialized */ + +} ToxAvError; + + +/** + * @brief Locally supported capabilities. + */ +typedef enum { + AudioEncoding = 1 << 0, + AudioDecoding = 1 << 1, + VideoEncoding = 1 << 2, + VideoDecoding = 1 << 3 +} ToxAvCapabilities; + + +/** + * @brief Encoding settings. + */ +typedef struct _ToxAvCodecSettings { + ToxAvCallType call_type; + + uint32_t video_bitrate; /* In kbits/s */ + uint16_t max_video_width; /* In px */ + uint16_t max_video_height; /* In px */ + + uint32_t audio_bitrate; /* In bits/s */ + uint16_t audio_frame_duration; /* In ms */ + uint32_t audio_sample_rate; /* In Hz */ + uint32_t audio_channels; +} ToxAvCSettings; + +extern const ToxAvCSettings av_DefaultSettings; +extern const uint32_t av_jbufdc; /* Jitter buffer default capacity */ +extern const uint32_t av_VADd; /* VAD default treshold */ + +/** + * @brief Start new A/V session. There can only be one session at the time. If you register more + * it will result in undefined behaviour. + * + * @param messenger The messenger handle. + * @param userdata The agent handling A/V session (i.e. phone). + * @param video_width Width of video frame. + * @param video_height Height of video frame. + * @return ToxAv* + * @retval NULL On error. + */ +ToxAv *toxav_new(Tox *messenger, int32_t max_calls); + +/** + * @brief Remove A/V session. + * + * @param av Handler. + * @return void + */ +void toxav_kill(ToxAv *av); + +/** + * @brief Register callback for call state. + * + * @param av Handler. + * @param callback The callback + * @param id One of the ToxAvCallbackID values + * @return void + */ +void toxav_register_callstate_callback (ToxAv *av, ToxAVCallback callback, ToxAvCallbackID id, void *userdata); + +/** + * @brief Register callback for recieving audio data + * + * @param av Handler. + * @param callback The callback + * @return void + */ +void toxav_register_audio_recv_callback (ToxAv *av, void (*callback)(ToxAv *, int32_t, int16_t *, int, void *), + void *user_data); + +/** + * @brief Register callback for recieving video data + * + * @param av Handler. + * @param callback The callback + * @return void + */ +void toxav_register_video_recv_callback (ToxAv *av, void (*callback)(ToxAv *, int32_t, vpx_image_t *, void *), + void *user_data); + +/** + * @brief Call user. Use its friend_id. + * + * @param av Handler. + * @param user The user. + * @param call_type Call type. + * @param ringing_seconds Ringing timeout. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_call(ToxAv *av, int32_t *call_index, int user, const ToxAvCSettings *csettings, int ringing_seconds); + +/** + * @brief Hangup active call. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_hangup(ToxAv *av, int32_t call_index); + +/** + * @brief Answer incomming call. + * + * @param av Handler. + * @param call_type Answer with... + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_answer(ToxAv *av, int32_t call_index, const ToxAvCSettings *csettings ); + +/** + * @brief Reject incomming call. + * + * @param av Handler. + * @param reason Optional reason. Set NULL if none. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_reject(ToxAv *av, int32_t call_index, const char *reason); + +/** + * @brief Cancel outgoing request. + * + * @param av Handler. + * @param reason Optional reason. + * @param peer_id peer friend_id + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_cancel(ToxAv *av, int32_t call_index, int peer_id, const char *reason); + +/** + * @brief Notify peer that we are changing call settings + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_change_settings(ToxAv *av, int32_t call_index, const ToxAvCSettings *csettings); + +/** + * @brief Terminate transmission. Note that transmission will be terminated without informing remote peer. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_stop_call(ToxAv *av, int32_t call_index); + +/** + * @brief Must be call before any RTP transmission occurs. + * + * @param av Handler. + * @param support_video Is video supported ? 1 : 0 + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_prepare_transmission(ToxAv *av, int32_t call_index, uint32_t jbuf_size, uint32_t VAD_treshold, + int support_video); + +/** + * @brief Call this at the end of the transmission. + * + * @param av Handler. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_kill_transmission(ToxAv *av, int32_t call_index); + +/** + * @brief Encode and send video packet. + * + * @param av Handler. + * @param frame The encoded frame. + * @param frame_size The size of the encoded frame. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_send_video ( ToxAv *av, int32_t call_index, const uint8_t *frame, unsigned int frame_size); + +/** + * @brief Send audio frame. + * + * @param av Handler. + * @param data The audio data encoded with toxav_prepare_audio_frame(). + * @param size Its size in number of bytes. + * @return int + * @retval 0 Success. + * @retval ToxAvError On error. + */ +int toxav_send_audio ( ToxAv *av, int32_t call_index, const uint8_t *frame, unsigned int size); + +/** + * @brief Encode video frame + * + * @param av Handler + * @param dest Where to + * @param dest_max Max size + * @param input What to encode + * @return int + * @retval ToxAvError On error. + * @retval >0 On success + */ +int toxav_prepare_video_frame ( ToxAv *av, int32_t call_index, uint8_t *dest, int dest_max, vpx_image_t *input ); + +/** + * @brief Encode audio frame + * + * @param av Handler + * @param dest dest + * @param dest_max Max dest size + * @param frame The frame + * @param frame_size The frame size + * @return int + * @retval ToxAvError On error. + * @retval >0 On success + */ +int toxav_prepare_audio_frame ( ToxAv *av, int32_t call_index, uint8_t *dest, int dest_max, const int16_t *frame, + int frame_size); + +/** + * @brief Get peer transmission type. It can either be audio or video. + * + * @param av Handler. + * @param peer The peer + * @return int + * @retval ToxAvCallType On success. + * @retval ToxAvError On error. + */ +int toxav_get_peer_csettings ( ToxAv *av, int32_t call_index, int peer, ToxAvCSettings *dest ); + +/** + * @brief Get id of peer participating in conversation + * + * @param av Handler + * @param peer peer index + * @return int + * @retval ToxAvError No peer id + */ +int toxav_get_peer_id ( ToxAv *av, int32_t call_index, int peer ); + +/** + * @brief Get current call state + * + * @param av Handler + * @param call_index What call + * @return int + * @retval ToxAvCallState State id + */ +ToxAvCallState toxav_get_call_state ( ToxAv *av, int32_t call_index ); +/** + * @brief Is certain capability supported + * + * @param av Handler + * @return int + * @retval 1 Yes. + * @retval 0 No. + */ +int toxav_capability_supported ( ToxAv *av, int32_t call_index, ToxAvCapabilities capability ); + + +Tox *toxav_get_tox(ToxAv *av); + +int toxav_has_activity ( ToxAv *av, int32_t call_index, int16_t *PCM, uint16_t frame_size, float ref_energy ); + +#ifdef __cplusplus +} +#endif + +#endif /* __TOXAV */ |