From d872802ba2c6491779a5a12aa996b1655d620760 Mon Sep 17 00:00:00 2001 From: Richard Fuchs Date: Wed, 11 Mar 2020 11:51:48 -0400 Subject: [PATCH] TT#76711 T.38 <> audio gateway Change-Id: I4552e07eee632fa730745410b08c3bf70ab67ab2 --- README.md | 45 +- daemon/Makefile | 2 +- daemon/call.c | 78 +++- daemon/call_interfaces.c | 29 ++ daemon/codec.c | 255 ++++++++++- daemon/media_player.c | 18 +- daemon/media_socket.c | 23 +- daemon/redis.c | 2 +- daemon/sdp.c | 198 ++++++++- daemon/t38.c | 726 ++++++++++++++++++++++++++++++++ include/call.h | 5 + include/call_interfaces.h | 3 + include/codec.h | 11 +- include/media_socket.h | 5 + include/t38.h | 97 +++++ lib/codeclib.c | 6 + lib/codeclib.h | 20 +- perl/NGCP/Rtpengine/AutoTest.pm | 23 +- t/.gitignore | 5 + t/Makefile | 16 +- t/auto-daemon-tests-t38.pl | 686 ++++++++++++++++++++++++++++++ t/auto-daemon-tests.pl | 462 ++++++++++++++++++++ t/pcm_rtp_test.pl | 59 +++ t/spandsp_fax_pcm_test.pl | 32 ++ t/spandsp_fax_t38_test.pl | 38 ++ t/spandsp_recv_fax_pcm.c | 151 +++++++ t/spandsp_recv_fax_t38.c | 118 ++++++ t/spandsp_send_fax_pcm.c | 156 +++++++ t/spandsp_send_fax_t38.c | 125 ++++++ t/t38_udptl_test.pl | 63 +++ t/test.tif | Bin 0 -> 513386 bytes t/transcode-test.c | 4 +- utils/rtpengine-ng-client | 3 +- 33 files changed, 3404 insertions(+), 60 deletions(-) create mode 100644 daemon/t38.c create mode 100644 include/t38.h create mode 100755 t/auto-daemon-tests-t38.pl create mode 100755 t/pcm_rtp_test.pl create mode 100755 t/spandsp_fax_pcm_test.pl create mode 100755 t/spandsp_fax_t38_test.pl create mode 100644 t/spandsp_recv_fax_pcm.c create mode 100644 t/spandsp_recv_fax_t38.c create mode 100644 t/spandsp_send_fax_pcm.c create mode 100644 t/spandsp_send_fax_t38.c create mode 100755 t/t38_udptl_test.pl create mode 100644 t/test.tif diff --git a/README.md b/README.md index e21f94fad..c32600465 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ the following additional features are available: - Transcoding between RFC 2833/4733 DTMF event packets and in-band DTMF tones (and vice versa) - Injection of DTMF events or PCM DTMF tones into running audio streams - Playback of pre-recorded streams/announcements +- Transcoding between T.38 and PCM (G.711 or other audio codecs) *Rtpengine* does not (yet) support: @@ -411,9 +412,6 @@ the necessary conversions. If repacketization (using the `ptime` option) is requested, the transcoding feature will also be engaged for the call, even if no additional codecs were requested. -Non-audio pseudo-codecs (such as T.38) are not currently supported, with the exception of RFC -2833/4733 DTMF event packets (`telephone-event`) as described below. - G.729 support ------------- @@ -464,6 +462,16 @@ Enabling DTMF transcoding (in one of the two ways described above) implicitly en `always transcode` for the call and forces all of the audio to pass through the transcoding engine. Therefore, for performance reasons, this should only be done when really necessary. +T.38 +---- +*Rtpengine* can translate between fax endpoints that speak T.38 over UDPTL and fax endpoints that speak +T.30 over regular audio channels. Any audio codec can theoretically be used for T.30 transmissions, but +codecs that are too compressed will make the fax transmission fail. The most commonly used audio codecs +for fax are the G.711 codecs (`PCMU` and `PCMA`), which are the default codecs *rtpengine* will use in +this case if no other codecs are specified. + +For further information, see the section on the `T.38` dictionary key below. + Call recording ============== @@ -1058,6 +1066,37 @@ Optionally included keys are: who has sent the offer. It will be inserted in the `answer` SDP. This option is also ignored in `answer` messages. +* `T.38` + + Contains a list of strings. Each string is a flag that controls the behaviour regarding + T.38 transcoding. These flags are ignored if the message is not an `offer`. + Recognised flags are: + + - `decode` + + If the received SDP contains a media section with an `image` type, `UDPTL` + transport, and `t38` format string, this flag instructs *rtpengine* to convert + this media section into an `audio` type using RTP as transport protocol. + Other transport protocols (such as SRTP) can be selected using `transport protocol` + as described above. + + The default audio codecs to be offered are `PCMU` and `PCMA`. Other audio codecs + can be specified using the `transcode=` flag described above, in which case the + default codecs will not be offered automatically. + + - `force` + + If the received SDP contains an audio media section using RTP transport, this flag + instructs *rtpengine* to convert it to an `image` type media section using the UDPTL + protocol. The first supported audio codec that was offered will be used to transport + T.30. Default options for T.38 are used for the generated SDP. + + - `stop` + + Stops a currently active T.38 gateway that was previously engaged using the `decode` + or `force` flags. This is useful to handle a rejected T.38 offer and revert the + session back to media passthrough. + * `supports` Contains a list of strings. Each string indicates support for an additional feature diff --git a/daemon/Makefile b/daemon/Makefile index 7d0b11254..d5c83938e 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -127,7 +127,7 @@ SRCS= main.c kernel.c poller.c aux.c control_tcp.c call.c control_udp.c redis.c bencode.c cookie_cache.c udp_listener.c control_ng.strhash.c sdp.strhash.c stun.c rtcp.c \ crypto.c rtp.c call_interfaces.strhash.c dtls.c log.c cli.c graphite.c ice.c \ media_socket.c homer.c recording.c statistics.c cdr.c ssrc.c iptables.c tcp_listener.c \ - codec.c load.c dtmf.c timerthread.c media_player.c jitter_buffer.c + codec.c load.c dtmf.c timerthread.c media_player.c jitter_buffer.c t38.c LIBSRCS= loglib.c auxlib.c rtplib.c str.c socket.c streambuf.c ssllib.c dtmflib.c ifeq ($(with_transcoding),yes) LIBSRCS+= codeclib.c resample.c diff --git a/daemon/call.c b/daemon/call.c index e5becbbc9..7729d4840 100644 --- a/daemon/call.c +++ b/daemon/call.c @@ -46,6 +46,7 @@ #include "codec.h" #include "media_player.h" #include "jitter_buffer.h" +#include "t38.h" /* also serves as array index for callstream->peers[] */ @@ -1816,6 +1817,19 @@ static void __update_media_id(struct call_media *media, struct call_media *other } } +static void __t38_reset(struct call_media *media, struct call_media *other_media) { + ilog(LOG_DEBUG, "Stopping T.38 gateway and resetting %s/" STR_FORMAT " to %s/" STR_FORMAT, + media->protocol->name, + STR_FMT(&media->format_str), + other_media->protocol->name, + STR_FMT(&other_media->format_str)); + + media->protocol = other_media->protocol; + media->type_id = other_media->type_id; + call_str_cpy(media->call, &media->type, &other_media->type); + call_str_cpy(media->call, &media->format_str, &other_media->format_str); +} + static void __update_media_protocol(struct call_media *media, struct call_media *other_media, struct stream_params *sp, struct sdp_ng_flags *flags) { @@ -1845,6 +1859,42 @@ static void __update_media_protocol(struct call_media *media, struct call_media if (!flags) return; + // T.38 decoder? + if (other_media->type_id == MT_IMAGE && proto_is(other_media->protocol, PROTO_UDPTL) + && flags->t38_decode) + { + media->protocol = flags->transport_protocol; + if (!media->protocol) + media->protocol = &transport_protocols[PROTO_RTP_AVP]; + media->type_id = MT_AUDIO; + call_str_cpy_c(media->call, &media->type, "audio"); + return; + } + + // T.38 encoder? + if (other_media->type_id == MT_AUDIO && proto_is_rtp(other_media->protocol) + && flags->t38_force) + { + media->protocol = &transport_protocols[PROTO_UDPTL]; + media->type_id = MT_IMAGE; + call_str_cpy_c(media->call, &media->type, "image"); + call_str_cpy_c(media->call, &media->format_str, "t38"); + return; + } + + // previous T.38 gateway but now stopping? + if (flags->t38_stop) { + if (other_media->type_id == MT_AUDIO && proto_is_rtp(other_media->protocol) + && media->type_id == MT_IMAGE + && proto_is(media->protocol, PROTO_UDPTL)) + __t38_reset(media, other_media); + else if (media->type_id == MT_AUDIO && proto_is_rtp(media->protocol) + && other_media->type_id == MT_IMAGE + && proto_is(other_media->protocol, PROTO_UDPTL)) + __t38_reset(media, other_media); + // drop through for protocol override + } + /* allow override of outgoing protocol even if we know it already */ /* but only if this is an RTP-based protocol */ if (flags->transport_protocol @@ -1962,10 +2012,13 @@ int monologue_offer_answer(struct call_monologue *other_ml, GQueue *streams, } if (str_cmp_str(&other_media->format_str, &sp->format_str)) call_str_cpy(call, &other_media->format_str, &sp->format_str); - if (str_cmp_str(&media->format_str, &sp->format_str)) - call_str_cpy(call, &media->format_str, &sp->format_str); + if (str_cmp_str(&media->format_str, &sp->format_str)) { + // update opposite side format string only if protocols match + if (media->protocol == other_media->protocol) + call_str_cpy(call, &media->format_str, &sp->format_str); + } codec_rtp_payload_types(media, other_media, &sp->rtp_payload_types, flags); - codec_handlers_update(media, other_media, flags); + codec_handlers_update(media, other_media, flags, sp); /* send and recv are from our POV */ bf_copy_same(&media->media_flags, &sp->sp_flags, @@ -2064,6 +2117,7 @@ init: ice_update(media->ice_agent, NULL); /* this is in case rtcp-mux has changed */ recording_setup_media(media); + t38_gateway_start(media->t38_gateway); } return 0; @@ -2324,6 +2378,8 @@ no_stats_output: for (l = c->medias.head; l; l = l->next) { md = l->data; ice_shutdown(&md->ice_agent); + t38_gateway_stop(md->t38_gateway); + t38_gateway_put(&md->t38_gateway); } for (l = c->monologues.head; l; l = l->next) { @@ -2411,6 +2467,8 @@ static void __call_free(void *p) { g_queue_clear_full(&md->codecs_prefs_recv, (GDestroyNotify) payload_type_free); g_queue_clear_full(&md->codecs_prefs_send, (GDestroyNotify) payload_type_free); codec_handlers_free(md); + codec_handler_free(&md->t38_handler); + t38_gateway_put(&md->t38_gateway); g_queue_clear_full(&md->sdp_attributes, free); g_slice_free1(sizeof(*md), md); } @@ -2804,6 +2862,16 @@ struct call_monologue *call_get_mono_dialogue(struct call *call, const str *from } + +static void monologue_stop(struct call_monologue *ml) { + media_player_stop(ml->player); + for (GList *l = ml->medias.head; l; l = l->next) { + struct call_media *m = l->data; + t38_gateway_stop(m->t38_gateway); + } +} + + int call_delete_branch(const str *callid, const str *branch, const str *fromtag, const str *totag, bencode_item_t *output, int delete_delay) { @@ -2866,7 +2934,7 @@ do_delete: if (output) ng_call_stats(c, fromtag, totag, output, NULL); - media_player_stop(ml->player); + monologue_stop(ml); if (delete_delay > 0) { ilog(LOG_INFO, "Scheduling deletion of call branch '" STR_FORMAT_M "' " @@ -2887,7 +2955,7 @@ do_delete: del_all: for (i = c->monologues.head; i; i = i->next) { ml = i->data; - media_player_stop(ml->player); + monologue_stop(ml); } if (delete_delay > 0) { diff --git a/daemon/call_interfaces.c b/daemon/call_interfaces.c index 92ef5d3aa..e226d3f54 100644 --- a/daemon/call_interfaces.c +++ b/daemon/call_interfaces.c @@ -548,6 +548,26 @@ INLINE void ng_sdes_option(struct sdp_ng_flags *out, str *s, void *dummy) { } +#ifdef WITH_TRANSCODING +INLINE void ng_t38_option(struct sdp_ng_flags *out, str *s, void *dummy) { + switch (__csh_lookup(s)) { + case CSH_LOOKUP("decode"): + out->t38_decode = 1; + break; + case CSH_LOOKUP("force"): + out->t38_force = 1; + break; + case CSH_LOOKUP("stop"): + out->t38_stop = 1; + break; + default: + ilog(LOG_WARN, "Unknown 'T.38' flag encountered: '" STR_FORMAT "'", + STR_FMT(s)); + } +} +#endif + + static void call_ng_flags_list(struct sdp_ng_flags *out, bencode_item_t *input, const char *key, void (*callback)(struct sdp_ng_flags *, str *, void *), void *parm) { @@ -728,6 +748,10 @@ static void call_ng_flags_flags(struct sdp_ng_flags *out, str *s, void *dummy) { if (call_ng_flags_prefix(out, s, "codec-set-", call_ng_flags_str_ht_split, &out->codec_set)) return; + if (call_ng_flags_prefix(out, s, "T38-", ng_t38_option, NULL)) + return; + if (call_ng_flags_prefix(out, s, "T.38-", ng_t38_option, NULL)) + return; #endif } @@ -809,6 +833,10 @@ static void call_ng_process_flags(struct sdp_ng_flags *out, bencode_item_t *inpu call_ng_flags_list(out, input, "rtcp-mux", call_ng_flags_rtcp_mux, NULL); call_ng_flags_list(out, input, "SDES", ng_sdes_option, NULL); +#ifdef WITH_TRANSCODING + call_ng_flags_list(out, input, "T38", ng_t38_option, NULL); + call_ng_flags_list(out, input, "T.38", ng_t38_option, NULL); +#endif bencode_get_alt(input, "transport-protocol", "transport protocol", &out->transport_protocol_str); out->transport_protocol = transport_protocol(&out->transport_protocol_str); @@ -1234,6 +1262,7 @@ static void ng_stats_media(bencode_item_t *list, const struct call_media *m, BF_M("unidirectional", UNIDIRECTIONAL); BF_M("loop check", LOOP_CHECK); BF_M("transcoding", TRANSCODE); + BF_M("generator/sink", GENERATOR); stats: for (l = m->streams.head; l; l = l->next) { diff --git a/daemon/codec.c b/daemon/codec.c index bf0ca16fe..31436c8ab 100644 --- a/daemon/codec.c +++ b/daemon/codec.c @@ -16,6 +16,8 @@ #include "call_interfaces.h" #include "dtmf.h" #include "dtmflib.h" +#include "t38.h" +#include "media_player.h" @@ -25,8 +27,6 @@ static codec_handler_func handler_func_passthrough; static struct rtp_payload_type *__rtp_payload_type_copy(const struct rtp_payload_type *pt); static void __rtp_payload_type_dup(struct call *call, struct rtp_payload_type *pt); static void __rtp_payload_type_add_name(GHashTable *, struct rtp_payload_type *pt); -static int packet_encoded_rtp(encoder_t *enc, void *u1, void *u2); -static int packet_decoded_fifo(decoder_t *decoder, AVFrame *frame, void *u1, void *u2); static struct codec_handler codec_handler_stub = { @@ -107,6 +107,7 @@ static codec_handler_func handler_func_transcode; static codec_handler_func handler_func_playback; static codec_handler_func handler_func_inject_dtmf; static codec_handler_func handler_func_dtmf; +static codec_handler_func handler_func_t38; static struct ssrc_entry *__ssrc_handler_transcode_new(void *p); static struct ssrc_entry *__ssrc_handler_new(void *p); @@ -114,6 +115,10 @@ static void __free_ssrc_handler(void *); static void __transcode_packet_free(struct transcode_packet *); +static int packet_encoded_rtp(encoder_t *enc, void *u1, void *u2); +static int packet_decoded_fifo(decoder_t *decoder, AVFrame *frame, void *u1, void *u2); +static int packet_decoded_direct(decoder_t *decoder, AVFrame *frame, void *u1, void *u2); + static struct codec_handler codec_handler_stub_ssrc = { .source_pt.payload_type = -1, @@ -150,7 +155,8 @@ void codec_handler_free(struct codec_handler **handler) { static struct codec_handler *__handler_new(const struct rtp_payload_type *pt) { struct codec_handler *handler = g_slice_alloc0(sizeof(*handler)); - handler->source_pt = *pt; + if (pt) + handler->source_pt = *pt; handler->output_handler = handler; // default handler->dtmf_payload_type = -1; handler->packet_encoded = packet_encoded_rtp; @@ -236,14 +242,17 @@ reset: check_output:; // check if we have multiple decoders transcoding to the same output PT - struct codec_handler *output_handler = g_hash_table_lookup(output_transcoders, - GINT_TO_POINTER(dest->payload_type)); + struct codec_handler *output_handler = NULL; + if (output_transcoders) + output_handler = g_hash_table_lookup(output_transcoders, + GINT_TO_POINTER(dest->payload_type)); if (output_handler) { ilog(LOG_DEBUG, "Using existing encoder context"); handler->output_handler = output_handler; } else { - g_hash_table_insert(output_transcoders, GINT_TO_POINTER(dest->payload_type), handler); + if (output_transcoders) + g_hash_table_insert(output_transcoders, GINT_TO_POINTER(dest->payload_type), handler); handler->output_handler = handler; // make sure we don't have a stale pointer } } @@ -528,6 +537,7 @@ static void __check_dtmf_injector(const struct sdp_ng_flags *flags, struct call_ + static struct codec_handler *__get_pt_handler(struct call_media *receiver, struct rtp_payload_type *pt) { ensure_codec_def(pt, receiver); struct codec_handler *handler; @@ -560,13 +570,150 @@ static struct codec_handler *__get_pt_handler(struct call_media *receiver, struc return handler; } + + + +static void __check_t38_decoder(struct call_media *t38_media) { + if (t38_media->t38_handler) + return; + ilog(LOG_DEBUG, "Creating T.38 packet handler"); + t38_media->t38_handler = __handler_new(NULL); + t38_media->t38_handler->func = handler_func_t38; +} + +static int packet_encoded_t38(encoder_t *enc, void *u1, void *u2) { + struct media_packet *mp = u2; + + if (!mp->media) + return 0; + + return t38_gateway_input_samples(mp->media->t38_gateway, + (int16_t *) enc->avpkt.data, enc->avpkt.size / 2); +} + +static void __generator_stop(struct call_media *media) { + if (media->t38_gateway) { + t38_gateway_stop(media->t38_gateway); + t38_gateway_put(&media->t38_gateway); + } +} + +static void __check_t38_gateway(struct call_media *pcm_media, struct call_media *t38_media, + const struct stream_params *sp) +{ + const struct t38_options *t_opts; + struct t38_options t_opts_stor = {0,}; + + if (sp) + t_opts = &sp->t38_options; + else { + // create our own options + t_opts_stor.max_ec_entries = 3; + t_opts = &t_opts_stor; + } + + MEDIA_SET(pcm_media, TRANSCODE); + MEDIA_SET(pcm_media, GENERATOR); + MEDIA_SET(t38_media, TRANSCODE); + MEDIA_SET(t38_media, GENERATOR); + + if (t38_gateway_pair(t38_media, pcm_media, t_opts)) + return; + + // need a packet handler on the T.38 side + __check_t38_decoder(t38_media); + + + // for each codec type supported by the pcm_media, we create a codec handler that + // links to the T.38 encoder + for (GList *l = pcm_media->codecs_prefs_recv.head; l; l = l->next) { + struct rtp_payload_type *pt = l->data; + struct codec_handler *handler = __get_pt_handler(pcm_media, pt); + if (!pt->codec_def) { + // should not happen + ilog(LOG_WARN, "Unsupported codec " STR_FORMAT " for T.38 transcoding", + STR_FMT(&pt->encoding_with_params)); + continue; + } + + ilog(LOG_DEBUG, "Creating T.38 encoder for " STR_FORMAT, STR_FMT(&pt->encoding_with_params)); + + __make_transcoder(handler, &pcm_media->t38_gateway->pcm_pt, NULL, -1, 0); + + handler->packet_decoded = packet_decoded_direct; + handler->packet_encoded = packet_encoded_t38; + } +} + +// call must be locked in W +static int codec_handler_udptl_update(struct call_media *receiver, struct call_media *sink) { + // anything to do? + if (proto_is(sink->protocol, PROTO_UDPTL)) + return 0; + + if (sink->type_id == MT_AUDIO && proto_is_rtp(sink->protocol) && receiver->type_id == MT_IMAGE) { + if (!str_cmp(&receiver->format_str, "t38")) { + __check_t38_gateway(sink, receiver, NULL); + return 1; + } + } + ilog(LOG_WARN, "Unsupported non-RTP protocol: " STR_FORMAT "/" STR_FORMAT + " -> " STR_FORMAT "/" STR_FORMAT, + STR_FMT(&receiver->type), STR_FMT(&receiver->format_str), + STR_FMT(&sink->type), STR_FMT(&sink->format_str)); + return 0; +} + +// call must be locked in W +// for transcoding RTP types to non-RTP +static int codec_handler_non_rtp_update(struct call_media *receiver, struct call_media *sink, + const struct sdp_ng_flags *flags, const struct stream_params *sp) +{ + if (proto_is(sink->protocol, PROTO_UDPTL) && !str_cmp(&sink->format_str, "t38")) { + __check_t38_gateway(receiver, sink, sp); + return 1; + } + ilog(LOG_WARN, "Unsupported non-RTP protocol: " STR_FORMAT "/" STR_FORMAT + " -> " STR_FORMAT "/" STR_FORMAT, + STR_FMT(&receiver->type), STR_FMT(&receiver->format_str), + STR_FMT(&sink->type), STR_FMT(&sink->format_str)); + return 0; +} + + + // call must be locked in W void codec_handlers_update(struct call_media *receiver, struct call_media *sink, - const struct sdp_ng_flags *flags) + const struct sdp_ng_flags *flags, const struct stream_params *sp) { + MEDIA_CLEAR(receiver, GENERATOR); + MEDIA_CLEAR(sink, GENERATOR); + + // non-RTP protocol? + if (proto_is(receiver->protocol, PROTO_UDPTL)) { + if (codec_handler_udptl_update(receiver, sink)) + return; + } + // everything else is unsupported: pass through + if (proto_is_not_rtp(receiver->protocol)) { + __generator_stop(receiver); + __generator_stop(sink); + return; + } + if (!receiver->codec_handlers) receiver->codec_handlers = g_hash_table_new(g_direct_hash, g_direct_equal); + // should we transcode to a non-RTP protocol? + if (proto_is_not_rtp(sink->protocol)) { + if (codec_handler_non_rtp_update(receiver, sink, flags, sp)) + return; + } + + // we're doing some kind of media passthrough - shut down local generators + __generator_stop(receiver); + __generator_stop(sink); + MEDIA_CLEAR(receiver, TRANSCODE); receiver->rtcp_handler = NULL; GSList *passthrough_handlers = NULL; @@ -800,6 +947,11 @@ static struct codec_handler *codec_handler_get_rtp(struct call_media *m, int pay return h; } +static struct codec_handler *codec_handler_get_udptl(struct call_media *m) { + if (m->t38_handler) + return m->t38_handler; + return NULL; +} #endif @@ -814,6 +966,8 @@ struct codec_handler *codec_handler_get(struct call_media *m, int payload_type) if (m->protocol->rtp) ret = codec_handler_get_rtp(m, payload_type); + else if (m->protocol->index == PROTO_UDPTL) + ret = codec_handler_get_udptl(m); out: if (ret) @@ -1179,6 +1333,13 @@ static int handler_func_dtmf(struct codec_handler *h, struct media_packet *mp) { return __handler_func_sequencer(mp, packet); } + +static int handler_func_t38(struct codec_handler *h, struct media_packet *mp) { + if (!mp->media) + return 0; + + return t38_gateway_input_udptl(mp->media->t38_gateway, &mp->raw); +} #endif @@ -1621,6 +1782,9 @@ discard: static int packet_decoded_fifo(decoder_t *decoder, AVFrame *frame, void *u1, void *u2) { return packet_decoded_common(decoder, frame, u1, u2, encoder_input_fifo); } +static int packet_decoded_direct(decoder_t *decoder, AVFrame *frame, void *u1, void *u2) { + return packet_decoded_common(decoder, frame, u1, u2, encoder_input_data); +} static int packet_decode(struct codec_ssrc_handler *ch, struct transcode_packet *packet, struct media_packet *mp) { @@ -1805,6 +1969,10 @@ void __rtp_payload_type_add_recv(struct call_media *media, { if (!pt) return; + if (proto_is_not_rtp(media->protocol)) { + payload_type_free(pt); + return; + } // update ptime in case it was overridden if (media->ptime > 0) pt->ptime = media->ptime; @@ -1818,6 +1986,10 @@ void __rtp_payload_type_add_send(struct call_media *other_media, { if (!pt) return; + if (proto_is_not_rtp(other_media->protocol)) { + payload_type_free(pt); + return; + } // update ptime in case it was overridden if (other_media->ptime > 0) pt->ptime = other_media->ptime; @@ -1829,6 +2001,8 @@ void __rtp_payload_type_add_send(struct call_media *other_media, void __rtp_payload_type_add_send_dup(struct call_media *other_media, struct rtp_payload_type *pt) { + if (proto_is_not_rtp(other_media->protocol)) + return; pt = __rtp_payload_type_copy(pt); __rtp_payload_type_add_send(other_media, pt); } @@ -1900,10 +2074,19 @@ void codec_rtp_payload_types(struct call_media *media, struct call_media *other_ int strip_all = 0, mask_all = 0; // start fresh - // receiving part for 'media' - g_queue_clear_full(&media->codecs_prefs_recv, (GDestroyNotify) payload_type_free); - g_hash_table_remove_all(media->codecs_recv); - g_hash_table_remove_all(media->codec_names_recv); + if (!proto_is_rtp(other_media->protocol) && proto_is_rtp(media->protocol) && flags->opmode == OP_OFFER) { + // leave them alone if incoming proto is not RTP but outgoing is, + // as this is needed for T.38 decoding during a re-invite. + // this special case is only needed in an offer as in the answer + // we can go by media->codecs_prefs_send. + ; + } + else { + // receiving part for 'media' + g_queue_clear_full(&media->codecs_prefs_recv, (GDestroyNotify) payload_type_free); + g_hash_table_remove_all(media->codecs_recv); + g_hash_table_remove_all(media->codec_names_recv); + } // and sending part for 'other_media' g_queue_clear_full(&other_media->codecs_prefs_send, (GDestroyNotify) payload_type_free); g_hash_table_remove_all(other_media->codecs_send); @@ -1990,6 +2173,56 @@ void codec_rtp_payload_types(struct call_media *media, struct call_media *other_ STR_FMT(&pt->encoding_with_params), pt->payload_type); __rtp_payload_type_add_recv(media, pt); } + + if (media->type_id == MT_AUDIO && other_media->type_id == MT_IMAGE) { + if (media->codecs_prefs_recv.length == 0) { + // find some codecs to put into our outgoing SDP body + + if (media->codecs_prefs_send.length && media->t38_gateway + && flags->opmode == OP_ANSWER) + { + // audio -> T.38 transcoder, answer: + // we answer with the codec that we're sending audio with, taken from + // our PCM player + if (media->t38_gateway && media->t38_gateway->pcm_player + && media->t38_gateway->pcm_player->handler) + __rtp_payload_type_add_recv(media, + __rtp_payload_type_copy(&media->t38_gateway->pcm_player->handler->dest_pt)); + } + else if (flags->opmode == OP_OFFER) { + // T.38 -> audio transcoder, initial offer, and no codecs have been given. + // Default to PCMA and PCMU + // XXX can we improve the codec lookup/synthesis? + static const str PCMU_str = STR_CONST_INIT("PCMU"); + static const str PCMA_str = STR_CONST_INIT("PCMA"); + pt = codec_add_payload_type(&PCMU_str, media); + assert(pt != NULL); + __rtp_payload_type_add_recv(media, pt); + pt = codec_add_payload_type(&PCMA_str, media); + assert(pt != NULL); + __rtp_payload_type_add_recv(media, pt); + + ilog(LOG_DEBUG, "Using default codecs PCMU and PCMA for T.38 gateway"); + } + } + else if (flags->opmode == OP_OFFER) { + // re-invite - we remember some codecs from before, or perhaps they + // were added manually through the transcoding options. make sure + // they're all supported by us + + for (GList *l = media->codecs_prefs_recv.head; l;) { + pt = l->data; + ensure_codec_def(pt, media); + if (pt->codec_def) { + l = l->next; + continue; + } + ilog(LOG_DEBUG, "Eliminating unsupported codec " STR_FORMAT, + STR_FMT(&pt->encoding_with_params)); + l = __delete_receiver_codec(media, l); + } + } + } #endif g_hash_table_destroy(removed); diff --git a/daemon/media_player.c b/daemon/media_player.c index 0c4e7b346..91ca9c910 100644 --- a/daemon/media_player.c +++ b/daemon/media_player.c @@ -25,7 +25,10 @@ #ifdef WITH_TRANSCODING static struct timerthread media_player_thread; static MYSQL __thread *mysql_conn; + +static void media_player_read_packet(struct media_player *mp); #endif + static struct timerthread send_timer_thread; @@ -33,7 +36,6 @@ static struct timerthread send_timer_thread; static void send_timer_send_nolock(struct send_timer *st, struct codec_packet *cp); static void send_timer_send_lock(struct send_timer *st, struct codec_packet *cp); -static void media_player_read_packet(struct media_player *mp); @@ -51,6 +53,7 @@ static void media_player_shutdown(struct media_player *mp) { ilog(LOG_DEBUG, "shutting down media_player"); timerthread_obj_deschedule(&mp->tt_obj); + mp->next_run.tv_sec = 0; avformat_close_input(&mp->fmtctx); if (mp->sink) { @@ -222,6 +225,7 @@ int media_player_setup(struct media_player *mp, const struct rtp_payload_type *s struct rtp_payload_type *dst_pt; for (GList *l = mp->media->codecs_prefs_send.head; l; l = l->next) { dst_pt = l->data; + ensure_codec_def(dst_pt, mp->media); if (dst_pt->codec_def && !dst_pt->codec_def->supplemental) goto found; } @@ -241,7 +245,17 @@ found: mp->sync_ts += ts_diff_us * dst_pt->clock_rate / 1000000 / dst_pt->codec_def->clockrate_mult; } - mp->handler = codec_handler_make_playback(src_pt, dst_pt, mp->sync_ts); + // if we already have a handler, see if anything needs changing + if (mp->handler) { + if (rtp_payload_type_cmp(&mp->handler->dest_pt, dst_pt) + || rtp_payload_type_cmp(&mp->handler->source_pt, src_pt)) + { + ilog(LOG_DEBUG, "Resetting codec handler for media player"); + codec_handler_free(&mp->handler); + } + } + if (!mp->handler) + mp->handler = codec_handler_make_playback(src_pt, dst_pt, mp->sync_ts); if (!mp->handler) return -1; diff --git a/daemon/media_socket.c b/daemon/media_socket.c index e6ca24c88..64d94e894 100644 --- a/daemon/media_socket.c +++ b/daemon/media_socket.c @@ -1030,6 +1030,7 @@ void kernelize(struct packet_stream *stream) { struct packet_stream *sink = NULL; const char *nk_warn_msg; int non_forwarding = 0; + struct call_media *media = stream->media; if (PS_ISSET(stream, KERNELIZED)) return; @@ -1046,9 +1047,11 @@ void kernelize(struct packet_stream *stream) { else goto no_kernel; } + if (MEDIA_ISSET(media, GENERATOR)) + goto no_kernel; if (!stream->selected_sfd) goto no_kernel; - if (stream->media->monologue->block_media || call->block_media) + if (media->monologue->block_media || call->block_media) goto no_kernel; if (!stream->endpoint.address.family) goto no_kernel; @@ -1090,16 +1093,16 @@ void kernelize(struct packet_stream *stream) { __re_address_translate_ep(&reti.local, &stream->selected_sfd->socket.local); reti.tos = call->tos; - reti.rtcp_mux = MEDIA_ISSET(stream->media, RTCP_MUX); - reti.dtls = MEDIA_ISSET(stream->media, DTLS); - reti.stun = stream->media->ice_agent ? 1 : 0; + reti.rtcp_mux = MEDIA_ISSET(media, RTCP_MUX); + reti.dtls = MEDIA_ISSET(media, DTLS); + reti.stun = media->ice_agent ? 1 : 0; reti.non_forwarding = non_forwarding; __re_address_translate_ep(&reti.dst_addr, &sink->endpoint); __re_address_translate_ep(&reti.src_addr, &sink->selected_sfd->socket.local); if (stream->ssrc_in) { reti.ssrc = htonl(stream->ssrc_in->parent->h.ssrc); - if (MEDIA_ISSET(stream->media, TRANSCODE)) { + if (MEDIA_ISSET(media, TRANSCODE)) { reti.ssrc_out = htonl(stream->ssrc_in->ssrc_map_out); reti.transcoding = 1; } @@ -1119,7 +1122,7 @@ void kernelize(struct packet_stream *stream) { ZERO(stream->kernel_stats); - if (proto_is_rtp(stream->media->protocol)) { + if (proto_is_rtp(media->protocol)) { GList *values, *l; struct rtp_stats *rs; @@ -1133,13 +1136,17 @@ void kernelize(struct packet_stream *stream) { } rs = l->data; // only add payload types that are passthrough - struct codec_handler *ch = codec_handler_get(stream->media, rs->payload_type); + struct codec_handler *ch = codec_handler_get(media, rs->payload_type); if (!ch->kernelize) continue; reti.payload_types[reti.num_payload_types++] = rs->payload_type; } g_list_free(values); } + else { + if (MEDIA_ISSET(media, TRANSCODE)) + goto no_kernel; + } recording_stream_kernel_info(stream, &reti); @@ -1813,7 +1820,7 @@ static int stream_packet(struct packet_handler_ctx *phc) { if (G_UNLIKELY(!phc->sink || !phc->sink->selected_sfd || !phc->out_srtp || !phc->out_srtp->selected_sfd || !phc->in_srtp->selected_sfd)) { - ilog(LOG_WARNING, "RTP packet from %s%s%s discarded", FMT_M(endpoint_print_buf(&phc->mp.fsin))); + ilog(LOG_WARNING, "Media packet from %s%s%s discarded", FMT_M(endpoint_print_buf(&phc->mp.fsin))); atomic64_inc(&phc->mp.stream->stats.errors); atomic64_inc(&rtpe_statsps.errors); goto out; diff --git a/daemon/redis.c b/daemon/redis.c index 269e65c36..0b8b303cc 100644 --- a/daemon/redis.c +++ b/daemon/redis.c @@ -1452,7 +1452,7 @@ static int json_link_medias(struct call *c, struct redis_list *medias, for (GList *l = other_ml->medias.head; l; l = l->next) { struct call_media *other_m = l->data; if (other_m->index == med->index) { - codec_handlers_update(med, other_m, NULL); + codec_handlers_update(med, other_m, NULL, NULL); break; } } diff --git a/daemon/sdp.c b/daemon/sdp.c index ce7bb355b..1ccbbc8d3 100644 --- a/daemon/sdp.c +++ b/daemon/sdp.c @@ -169,6 +169,31 @@ struct attribute_fmtp { unsigned int payload_type; }; +struct attribute_t38faxratemanagement { + enum { + RM_UNKNOWN = 0, + RM_LOCALTCF, + RM_TRANSFERREDTCF, + } rm; +}; + +struct attribute_t38faxudpec { + enum { + EC_UNKNOWN = 0, + EC_NONE, + EC_REDUNDANCY, + EC_FEC, + } ec; +}; + +struct attribute_t38faxudpecdepth { + str minred_str; + str maxred_str; + + int minred; + int maxred; +}; + struct sdp_attribute { /* example: a=rtpmap:8 PCMA/8000 */ str full_line, /* including a= and \r\n */ line_value, /* without a= and without \r\n */ @@ -204,6 +229,16 @@ struct sdp_attribute { /* example: a=rtpmap:8 PCMA/8000 */ ATTR_RTPENGINE, ATTR_PTIME, ATTR_RTCP_FB, + ATTR_T38FAXVERSION, + ATTR_T38FAXUDPEC, + ATTR_T38FAXUDPECDEPTH, + ATTR_T38FAXUDPFECMAXSPAN, + ATTR_T38FAXMAXDATAGRAM, + ATTR_T38FAXMAXIFP, + ATTR_T38FAXFILLBITREMOVAL, + ATTR_T38FAXTRANSCODINGMMR, + ATTR_T38FAXTRANSCODINGJBIG, + ATTR_T38FAXRATEMANAGEMENT, ATTR_END_OF_CANDIDATES, } attr; @@ -217,6 +252,10 @@ struct sdp_attribute { /* example: a=rtpmap:8 PCMA/8000 */ struct attribute_setup setup; struct attribute_rtpmap rtpmap; struct attribute_fmtp fmtp; + struct attribute_t38faxudpec t38faxudpec; + int i; + struct attribute_t38faxudpecdepth t38faxudpecdepth; + struct attribute_t38faxratemanagement t38faxratemanagement; } u; }; @@ -769,6 +808,71 @@ static int parse_attribute_fmtp(struct sdp_attribute *output) { return 0; } +static int parse_attribute_int(struct sdp_attribute *output, int attr_id, int defval) { + output->attr = attr_id; + output->u.i = str_to_i(&output->value, defval); + return 0; +} + +// XXX combine this with parse_attribute_setup ? +static int parse_attribute_t38faxudpec(struct sdp_attribute *output) { + output->attr = ATTR_T38FAXUDPEC; + + switch (__csh_lookup(&output->value)) { + case CSH_LOOKUP("t38UDPNoEC"): + output->u.t38faxudpec.ec = EC_NONE; + break; + case CSH_LOOKUP("t38UDPRedundancy"): + output->u.t38faxudpec.ec = EC_REDUNDANCY; + break; + case CSH_LOOKUP("t38UDPFEC"): + output->u.t38faxudpec.ec = EC_FEC; + break; + default: + output->u.t38faxudpec.ec = EC_UNKNOWN; + break; + } + + return 0; +} + +// XXX combine this with parse_attribute_setup ? +static int parse_attribute_t38faxratemanagement(struct sdp_attribute *output) { + output->attr = ATTR_T38FAXRATEMANAGEMENT; + + switch (__csh_lookup(&output->value)) { + case CSH_LOOKUP("localTFC"): + output->u.t38faxratemanagement.rm = RM_LOCALTCF; + break; + case CSH_LOOKUP("transferredTCF"): + output->u.t38faxratemanagement.rm = RM_TRANSFERREDTCF; + break; + default: + output->u.t38faxratemanagement.rm = RM_UNKNOWN; + break; + } + + return 0; +} + +static int parse_attribute_t38faxudpecdepth(struct sdp_attribute *output) { + PARSE_DECL; + struct attribute_t38faxudpecdepth *a; + + output->attr = ATTR_T38FAXUDPECDEPTH; + a = &output->u.t38faxudpecdepth; + + PARSE_INIT; + EXTRACT_TOKEN(u.t38faxudpecdepth.minred_str); + a->maxred_str = *value_str; + + a->minred = str_to_i(&a->minred_str, 0); + a->maxred = str_to_i(&a->maxred_str, -1); + + return 0; +} + + static int parse_attribute(struct sdp_attribute *a) { int ret; @@ -873,6 +977,36 @@ static int parse_attribute(struct sdp_attribute *a) { case CSH_LOOKUP("rtcp-fb"): a->attr = ATTR_RTCP_FB; break; + case CSH_LOOKUP("T38FaxVersion"): + ret = parse_attribute_int(a, ATTR_T38FAXVERSION, -1); + break; + case CSH_LOOKUP("T38FaxUdpEC"): + ret = parse_attribute_t38faxudpec(a); + break; + case CSH_LOOKUP("T38FaxUdpECDepth"): + ret = parse_attribute_t38faxudpecdepth(a); + break; + case CSH_LOOKUP("T38FaxUdpFECMaxSpan"): + ret = parse_attribute_int(a, ATTR_T38FAXUDPFECMAXSPAN, 0); + break; + case CSH_LOOKUP("T38FaxMaxDatagram"): + ret = parse_attribute_int(a, ATTR_T38FAXMAXDATAGRAM, -1); + break; + case CSH_LOOKUP("T38FaxMaxIFP"): + ret = parse_attribute_int(a, ATTR_T38FAXMAXIFP, -1); + break; + case CSH_LOOKUP("T38FaxFillBitRemoval"): + a->attr = ATTR_T38FAXFILLBITREMOVAL; + break; + case CSH_LOOKUP("T38FaxTranscodingMMR"): + a->attr = ATTR_T38FAXTRANSCODINGMMR; + break; + case CSH_LOOKUP("T38FaxTranscodingJBIG"): + a->attr = ATTR_T38FAXTRANSCODINGJBIG; + break; + case CSH_LOOKUP("T38FaxRateManagement"): + ret = parse_attribute_t38faxratemanagement(a); + break; } return ret; @@ -1228,6 +1362,63 @@ no_cand: sp->ice_pwd = attr->value; } +static void __sdp_t38(struct stream_params *sp, struct sdp_media *media) { + struct sdp_attribute *attr; + struct t38_options *to = &sp->t38_options; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXVERSION); + if (attr) + to->version = attr->u.i; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXUDPEC); + if (attr) { + if (attr->u.t38faxudpec.ec == EC_REDUNDANCY) + to->max_ec_entries = to->min_ec_entries = 3; // defaults + else if (attr->u.t38faxudpec.ec == EC_FEC) { + // defaults + to->max_ec_entries = to->min_ec_entries = 3; + to->fec_span = 3; + } + // else default to 0 + } + else // no EC specified, defaults: + to->max_ec_entries = to->min_ec_entries = 3; // defaults + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXUDPECDEPTH); + if (attr) { + to->min_ec_entries = attr->u.t38faxudpecdepth.minred; + to->max_ec_entries = attr->u.t38faxudpecdepth.maxred; + } + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXUDPFECMAXSPAN); + if (attr) + to->fec_span = attr->u.i; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXMAXDATAGRAM); + if (attr) + to->max_datagram = attr->u.i; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXMAXIFP); + if (attr) + to->max_ifp = attr->u.i; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXFILLBITREMOVAL); + if (attr) + to->fill_bit_removal = 1; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXTRANSCODINGMMR); + if (attr) + to->transcoding_mmr = 1; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXTRANSCODINGJBIG); + if (attr) + to->transcoding_jbig = 1; + + attr = attr_get_by_id(&media->attributes, ATTR_T38FAXRATEMANAGEMENT); + if (attr) + to->local_tcf = (attr->u.t38faxratemanagement.rm == RM_LOCALTCF) ? 1 : 0; +} + /* XXX split this function up */ int sdp_streams(const GQueue *sessions, GQueue *streams, struct sdp_ng_flags *flags) { @@ -1340,6 +1531,7 @@ int sdp_streams(const GQueue *sessions, GQueue *streams, struct sdp_ng_flags *fl SP_SET(sp, RTCP_FB); __sdp_ice(sp, media); + __sdp_t38(sp, media); /* determine RTCP endpoint */ @@ -1720,6 +1912,10 @@ static int process_media_attributes(struct sdp_chopper *chop, struct sdp_media * for (l = attrs->list.head; l; l = l->next) { attr = l->data; + // strip all attributes if we're sink and generator - make our own clean SDP + if (MEDIA_ISSET(media, GENERATOR)) + goto strip; + switch (attr->attr) { case ATTR_ICE: case ATTR_ICE_UFRAG: @@ -2145,7 +2341,7 @@ int sdp_replace(struct sdp_chopper *chop, GQueue *sessions, struct call_monologu chopper_append_c(chop, "\r\n"); } - if (call_media->protocol && call_media->protocol->rtp) + if (proto_is_rtp(call_media->protocol)) insert_codec_parameters(chop, call_media); insert_sdp_attributes(chop, call_media); diff --git a/daemon/t38.c b/daemon/t38.c new file mode 100644 index 000000000..689775310 --- /dev/null +++ b/daemon/t38.c @@ -0,0 +1,726 @@ +#include "t38.h" + + + +#ifdef WITH_TRANSCODING + + +#include +#include +#include "codec.h" +#include "call.h" +#include "log.h" +#include "str.h" +#include "media_player.h" + + + +struct udptl_packet { + seq_packet_t p; + str *s; +}; + + + +static void __add_udptl_len(GString *s, const void *buf, unsigned int len) { + if (len < 0x80) { + g_string_append_c(s, len); + if (len) + g_string_append_len(s, buf, len); + return; + } + + if (len < 0x4000) { + uint16_t enc_len = htons(0x8000 | len); + g_string_append_len(s, (void *) &enc_len, 2); + g_string_append_len(s, buf, len); + return; + } + + // fragmented - we don't support more than 65535 bytes + unsigned int mult = len >> 14; + g_string_append_c(s, 0xc0 | mult); + mult <<= 14; + // one portion + g_string_append_len(s, buf, mult); + // remainder - may be zero length + __add_udptl_len(s, buf + mult, len - mult); +} + +static void __add_udptl_raw(GString *s, const char *buf, size_t len) { + assert(len < 0x10000); + + if (len == 0) { + // add a single zero byte, length 1 + __add_udptl_len(s, "\x00", 1); + return; + } + + __add_udptl_len(s, buf, len); +} + +static void __add_udptl(GString *s, const str *buf) { + __add_udptl_raw(s, buf->s, buf->len); +} + + +static void g_string_null_extend(GString *s, size_t len) { + if (s->len >= len) + return; + + size_t oldb = s->len; + size_t newb = len - s->len; + g_string_set_size(s, len); + memset(s->str + oldb, 0, newb); +} + +// call is locked in R or W +static int t38_gateway_handler(t38_core_state_t *stat, void *user_data, const uint8_t *b, int len, int count) { + struct t38_gateway *tg = user_data; + + // cap the max length of packet we can handle + if (len < 0 || len >= 0x10000) { + ilog(LOG_ERR, "Received %i bytes from T.38 encoder - discarding", len); + return -1; + } + + ilog(LOG_DEBUG, "Received %i bytes from T.38 encoder", len); + + // build udptl packet: use a conservative guess for required buffer + GString *s = g_string_sized_new(512); + + // add seqnum + uint16_t seq = htons(tg->seqnum); + g_string_append_len(s, (void *) &seq, 2); + + // add primary IFP packet + str buf = STR_CONST_INIT_LEN((char *) b, len); + __add_udptl(s, &buf); + + // add error correction packets + if (tg->options.fec_span > 1) { + // forward error correction + g_string_append_c(s, 0x80); + + // figure out how many packets we have and which span to use + unsigned int packets = tg->options.fec_span * tg->options.max_ec_entries; + if (packets > tg->udptl_ec_out.length) + packets = tg->udptl_ec_out.length; + unsigned int span = packets / tg->options.max_ec_entries; + if (!span) + span = 1; + packets = span * tg->options.max_ec_entries; // our own packets we use + unsigned int entries = packets / span; // FEC entries in the output + if (entries > tg->udptl_ec_out.length) + entries = tg->udptl_ec_out.length; + packets = entries * span; + + assert(span < 0x80); + assert(entries < 0x80); + + g_string_append_c(s, 0x01); + g_string_append_c(s, span); + + // create needed number of FEC packet entries + GQueue fec = G_QUEUE_INIT; + for (int i = 0; i < entries; i++) + g_queue_push_tail(&fec, g_string_new("")); + + // take each input packet, going backwards in time, and XOR it into + // the respective output FEC packet + GList *inp = tg->udptl_ec_out.head; + for (int i = 0; i < packets; i++) { + assert(inp != NULL); + str *ip = inp->data; + // just keep shifting the list around + GString *outp = g_queue_pop_head(&fec); + + // extend string as needed + g_string_null_extend(outp, ip->len); + + for (size_t j = 0; j < ip->len; j++) + outp->str[j] ^= ip->s[j]; + + g_queue_push_tail(&fec, outp); + inp = inp->next; + } + + // output list is now complete, but in reverse. append it to output buffer + GString *ec = g_string_sized_new(512); + entries = 0; + int going = 1; + while (fec.length) { + GString *outp = g_queue_pop_tail(&fec); + if (going) { + if (s->len + ec->len + outp->len > tg->options.max_datagram) + going = 0; + else { + __add_udptl_raw(ec, outp->str, outp->len); + entries++; + } + } + g_string_free(outp, TRUE); + } + + g_string_append_c(s, entries); + g_string_append_len(s, ec->str, ec->len); + g_string_free(ec, TRUE); + } + else { + // redundancy error correction + g_string_append_c(s, 0x00); + + GString *ec = g_string_sized_new(512); + int entries = 0; + + for (GList *l = tg->udptl_ec_out.head; l; l = l->next) { + str *ec_s = l->data; + // stop when we exceed max datagram length + if (s->len + ec->len + ec_s->len > tg->options.max_datagram) + break; + // add redundancy packet + __add_udptl(ec, ec_s); + entries++; + } + + // number of entries - must be <0x80 as verified in settings + g_string_append_c(s, entries); + g_string_append_len(s, ec->str, ec->len); + g_string_free(ec, TRUE); + } + + // done building our packet - add primary to our error correction buffer + tg->seqnum++; + unsigned int q_entries = tg->options.max_ec_entries * tg->options.fec_span; + if (q_entries) { + while (tg->udptl_ec_out.length >= q_entries) { + str *ec_s = g_queue_pop_tail(&tg->udptl_ec_out); + free(ec_s); + } + g_queue_push_head(&tg->udptl_ec_out, str_dup(&buf)); + } + + // send our packet if we can + struct packet_stream *ps = NULL; + if (tg->t38_media && tg->t38_media->streams.head) + ps = tg->t38_media->streams.head->data; + if (ps) + mutex_lock(&ps->out_lock); + struct stream_fd *sfd = NULL; + if (ps) + sfd = ps->selected_sfd; + if (sfd) { + for (int i = 0; i < count; i++) { + ilog(LOG_DEBUG, "Sending %u UDPTL bytes", (unsigned int) s->len); + socket_sendto(&sfd->socket, s->str, s->len, &ps->endpoint); + } + } + else + ilog(LOG_WARN | LOG_FLAG_LIMIT, "Unable to send T.38 UDPTL packet due to lack of " + "socket or stream"); + if (ps) + mutex_unlock(&ps->out_lock); + + g_string_free(s, TRUE); + + return 0; +} + +void __t38_gateway_free(void *p) { + struct t38_gateway *tg = p; + ilog(LOG_DEBUG, "Destroying T.38 gateway"); + if (tg->gw) + t38_gateway_free(tg->gw); + if (tg->pcm_player) { + media_player_stop(tg->pcm_player); + media_player_put(&tg->pcm_player); + } + if (tg->udptl_fec) + g_hash_table_destroy(tg->udptl_fec); + g_queue_clear_full(&tg->udptl_ec_out, free); + packet_sequencer_destroy(&tg->sequencer); +} + +// call is locked in R and mp is locked +static void t38_pcm_player(struct media_player *mp) { + if (!mp || !mp->media) + return; + + struct t38_gateway *tg = mp->media->t38_gateway; + if (!tg) + return; + + ilog(LOG_DEBUG, "Generating T.38 PCM samples"); + + mutex_lock(&tg->lock); + + int16_t smp[80]; + int num = t38_gateway_tx(tg->gw, smp, 80); + if (num <= 0) { + // use a fixed interval of 10 ms + timeval_add_usec(&mp->next_run, 10000); + timerthread_obj_schedule_abs(&mp->tt_obj, &mp->next_run); + mutex_unlock(&tg->lock); + return; + } + + ilog(LOG_DEBUG, "Generated %i T.38 PCM samples", num); + + // this reschedules our player as well + media_player_add_packet(tg->pcm_player, (char *) smp, num * 2, num * 1000000 / 8000, tg->pts); + + tg->pts += num; + + mutex_unlock(&tg->lock); +} + + +static void __udptl_packet_free(struct udptl_packet *p) { + if (p->s) + free(p->s); + g_slice_free1(sizeof(*p), p); +} + + +static void __t38_options_normalise(struct t38_options *opts) { + if (opts->version < 0) + opts->version = 0; + if (opts->fec_span < 1) + opts->fec_span = 1; + if (opts->min_ec_entries < 0) + opts->min_ec_entries = 0; + if (opts->min_ec_entries >= 0x80) + opts->min_ec_entries = 0x7f; + if (opts->max_ec_entries < 0) + opts->max_ec_entries = 0; + if (opts->max_ec_entries >= 0x80) + opts->max_ec_entries = 0x7f; + if (opts->max_ifp <= 0 || opts->max_ifp >= 0x4000) + opts->max_ifp = 0x3fff; + if (opts->max_datagram <= 0 || opts->max_datagram >= 0x4000) + opts->max_datagram = 0x3fff; +} + +// call is locked in W +int t38_gateway_pair(struct call_media *t38_media, struct call_media *pcm_media, + const struct t38_options *options) +{ + const char *err = NULL; + + if (!t38_media || !pcm_media || !options) + return -1; + + struct t38_options opts = *options; + __t38_options_normalise(&opts); + + // do we have one yet? + if (t38_media->t38_gateway + && t38_media->t38_gateway == pcm_media->t38_gateway) + { + // XXX check options here? + return 0; + } + + // release old structs, if any + t38_gateway_put(&t38_media->t38_gateway); + t38_gateway_put(&pcm_media->t38_gateway); + + ilog(LOG_DEBUG, "Creating new T.38 gateway"); + + // create and init new + struct t38_gateway *tg = obj_alloc0("t38_gateway", sizeof(*tg), __t38_gateway_free); + + tg->t38_media = t38_media; + tg->pcm_media = pcm_media; + mutex_init(&tg->lock); + tg->udptl_fec = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, + (GDestroyNotify) __udptl_packet_free); + tg->options = opts; + + tg->pcm_pt.payload_type = -1; + str_init(&tg->pcm_pt.encoding, "PCM-S16LE"); + tg->pcm_pt.encoding_with_params = tg->pcm_pt.encoding; + tg->pcm_pt.clock_rate = 8000; + tg->pcm_pt.channels = 1; + + err = "Failed to init PCM codec"; + ensure_codec_def(&tg->pcm_pt, pcm_media); + if (!tg->pcm_pt.codec_def) + goto err; + + err = "Failed to create spandsp T.38 gateway"; + if (!(tg->gw = t38_gateway_init(NULL, t38_gateway_handler, tg))) + goto err; + + err = "Failed to create media player"; + if (!(tg->pcm_player = media_player_new(pcm_media->monologue))) + goto err; + // even though we call media_player_set_media() here, we need to call it again in + // t38_gateway_start because our sink might not have any streams added here yet, + // leaving the media_player setup incomplete + media_player_set_media(tg->pcm_player, pcm_media); + tg->pcm_player->run_func = t38_pcm_player; + + // set options + t38_core_state_t *t38 = t38_gateway_get_t38_core_state(tg->gw); + t38_gateway_set_ecm_capability(tg->gw, TRUE); + t38_gateway_set_transmit_on_idle(tg->gw, TRUE); + t38_gateway_set_supported_modems(tg->gw, T30_SUPPORT_V17 | T30_SUPPORT_V27TER | T30_SUPPORT_V29 + | T30_SUPPORT_V34HDX | T30_SUPPORT_IAF); + t38_set_t38_version(t38, opts.version); + t38_set_data_rate_management_method(t38, + opts.local_tcf ? 1 : 2); + t38_set_fill_bit_removal(t38, opts.fill_bit_removal); + t38_set_mmr_transcoding(t38, opts.transcoding_mmr); + t38_set_jbig_transcoding(t38, opts.transcoding_jbig); + t38_set_max_datagram_size(t38, opts.max_ifp); + + packet_sequencer_init(&tg->sequencer, (GDestroyNotify) __udptl_packet_free); + tg->sequencer.seq = 0; + + // done - add references to media structs + t38_media->t38_gateway = tg; + pcm_media->t38_gateway = obj_get(tg); + + // add SDP options for T38 + g_queue_clear_full(&t38_media->sdp_attributes, free); + + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxVersion:%i", tg->options.version)); + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38MaxBitRate:14400")); + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxRateManagement:%s", + tg->options.local_tcf ? "localTFC" : "transferredTCF")); + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxMaxBuffer:1800")); + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxMaxDatagram:512")); + + if (tg->options.max_ec_entries == 0) + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxUdpEC:t38UDPNoEC")); + else if (tg->options.fec_span > 1) + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxUdpEC:t38UDPFEC")); + else + g_queue_push_tail(&t38_media->sdp_attributes, str_sprintf("T38FaxUdpEC:t38UDPRedundancy")); + // XXX more options possible here + + return 0; + +err: + if (err) + ilog(LOG_ERR, "Failed to create T.38 gateway: %s", err); + t38_gateway_put(&tg); + return -1; +} + + +// call is locked in W +void t38_gateway_start(struct t38_gateway *tg) { + if (!tg) + return; + + // set up our player first + media_player_set_media(tg->pcm_player, tg->pcm_media); + if (media_player_setup(tg->pcm_player, &tg->pcm_pt)) + return; + + // now start our player if we can or should + // already running? + if (tg->pcm_player->next_run.tv_sec) + return; + + // only start our player only if we can send both ways + if (!tg->pcm_media->codecs_prefs_send.length) + return; + if (!tg->pcm_media->streams.length) + return; + if (!tg->t38_media->streams.length) + return; + + struct packet_stream *ps; + ps = tg->pcm_media->streams.head->data; + if (!PS_ISSET(ps, FILLED)) + return; + ps = tg->t38_media->streams.head->data; + if (!PS_ISSET(ps, FILLED)) + return; + + ilog(LOG_DEBUG, "Starting T.38 PCM player"); + + // start off PCM player + tg->pcm_player->next_run = rtpe_now; + timerthread_obj_schedule_abs(&tg->pcm_player->tt_obj, &tg->pcm_player->next_run); +} + + +// call is locked in R +int t38_gateway_input_samples(struct t38_gateway *tg, int16_t amp[], int len) { + if (!tg) + return 0; + if (len <= 0) + return 0; + + ilog(LOG_DEBUG, "Adding %i samples to T.38 encoder", len); + + mutex_lock(&tg->lock); + + int left = t38_gateway_rx(tg->gw, amp, len); + if (left) + ilog(LOG_WARN | LOG_FLAG_LIMIT, "%i PCM samples were not processed by the T.38 gateway", + left); + + mutex_unlock(&tg->lock); + + return 0; +} + + +static ssize_t __get_udptl_len(str *s) { + ssize_t ret; + + if (s->len < 1) + return -1; + + if (!(s->s[0] & 0x80)) { + ret = s->s[0]; + str_shift(s, 1); + return ret; + } + + if (s->len < 2) + return -1; + + if (!(s->s[0] & 0x40)) { + ret = ntohs(*((uint16_t *) s->s)) & 0x3fff; + str_shift(s, 2); + return ret; + } + + ilog(LOG_INFO | LOG_FLAG_LIMIT, "Decoding UDPTL fragments is not supported"); + return -1; +} + +static int __get_udptl(str *piece, str *s) { + ssize_t len = __get_udptl_len(s); + if (len < 0) + return -1; + + return str_shift_ret(s, len, piece); +} + + +static struct udptl_packet *__make_udptl_packet(const str *piece, uint16_t seq) { + struct udptl_packet *up = g_slice_alloc0(sizeof(*up)); + up->p.seq = seq; + up->s = str_dup(piece); + return up; +} + +static void __fec_save(struct t38_gateway *tg, const str *piece, uint16_t seq) { + struct udptl_packet *up = __make_udptl_packet(piece, seq); + g_hash_table_insert(tg->udptl_fec, GUINT_TO_POINTER(seq), up); +} + +int t38_gateway_input_udptl(struct t38_gateway *tg, const str *buf) { + const char *err = NULL; + + if (!tg) + return 0; + if (!buf || !buf->len) + return 0; + + if (buf->len < 4) { + ilog(LOG_INFO | LOG_FLAG_LIMIT, "Ignoring short UDPTL packet (%i bytes)", buf->len); + return 0; + } + + ilog(LOG_DEBUG, "Processing %i UDPTL bytes", buf->len); + + str s = *buf; + str piece; + + // get seq num + uint16_t seq; + if (str_shift_ret(&s, 2, &piece)) + goto err; + seq = ntohs(*((uint16_t *) piece.s)); + + err = "Invalid primary UDPTL packet"; + if (__get_udptl(&piece, &s)) + goto err; + + ilog(LOG_DEBUG, "Received primary IFP packet, len %i, seq %i", piece.len, seq); + str primary = piece; + struct udptl_packet *up = __make_udptl_packet(&primary, seq); + + err = "Error correction mode byte missing"; + if (str_shift_ret(&s, 1, &piece)) + goto err; + char fec = piece.s[0]; + + mutex_lock(&tg->lock); + + // XXX possible short path here without going through the sequencer + int ret = packet_sequencer_insert(&tg->sequencer, &up->p); + if (ret < 0) { + // main seq is dupe - everything else must be dupe too + __udptl_packet_free(up); + goto out; + } + + up = NULL; + + if (!(fec & 0x80)) { + // packet redundancy + if (packet_sequencer_next_ok(&tg->sequencer)) + goto seq_ok; + + // process EC packets as well as something's wrong + ssize_t num_packets = __get_udptl_len(&s); + err = "Invalid number of EC packets"; + if (num_packets < 0 || num_packets > 100) + goto err; + for (int i = 0; i < num_packets; i++) { + if (__get_udptl(&piece, &s)) { + ilog(LOG_WARN | LOG_FLAG_LIMIT, + "Invalid UDPTL error correction packet at index %i", + i); + break; + } + // ignore zero-length packets + if (!piece.len) + continue; + ilog(LOG_DEBUG, "Received secondary IFP packet, len %i, seq %i", piece.len, + seq - 1 - i); + up = __make_udptl_packet(&piece, seq - 1 - i); + packet_sequencer_insert(&tg->sequencer, &up->p); + up = NULL; + + // can we stop here? + if (packet_sequencer_next_ok(&tg->sequencer)) + break; + } + } + else { + // FEC + // start by saving the new packet + __fec_save(tg, &primary, seq); + + if (packet_sequencer_next_ok(&tg->sequencer)) + goto seq_ok; + + // process all FEC packets + err = "Invalid number of FEC packets"; + if (str_shift_ret(&s, 2, &piece)) + goto err; + if (piece.s[0] != 0x01) + goto err; + unsigned int span = piece.s[1]; + if (span <= 0 || span >= 0x80) + goto err; + ssize_t entries = __get_udptl_len(&s); + if (entries < 0 || entries > 100) + goto err; + + // first seq we can possibly recover + uint16_t seq_start = seq - span * entries; + + while (entries) { + // get our entry + if (__get_udptl(&piece, &s)) { + ilog(LOG_WARN | LOG_FLAG_LIMIT, + "Invalid UDPTL error correction packet at index %i", + seq_start); + break; + } + // check each of the entries covered by `span` + for (int i = 0; i < span; i++) { + uint16_t seq_fec = seq_start + i * span; + // skip if we already know this packet + if (g_hash_table_lookup(tg->udptl_fec, GUINT_TO_POINTER(seq_fec))) + continue; + + // can we recover it? we need all other packets from the series + GString *rec_s = g_string_new(""); + int complete = 1; + + for (int j = 0; j < span; j++) { + uint16_t seq_rec = seq_start + i * span; + if (seq_rec == seq_fec) + continue; + struct udptl_packet *recp = + g_hash_table_lookup(tg->udptl_fec, GUINT_TO_POINTER(seq_rec)); + if (!recp) { + ilog(LOG_WARN | LOG_FLAG_LIMIT, "Unable to recover UDPTL FEC " + "packet with seq %i due to missing seq %i", + seq_fec, seq_rec); + complete = 0; + break; + } + + // XOR in packet + for (size_t j = 0; j < recp->s->len; j++) + rec_s->str[j] ^= recp->s->s[j]; + } + + if (complete) { + ilog(LOG_WARN | LOG_FLAG_LIMIT, "Recovered UDPTL " + "packet with seq %i from FEC", + seq_fec); + + str rec_str = STR_CONST_INIT_LEN(rec_s->str, rec_s->len); + __fec_save(tg, &rec_str, seq_fec); + up = __make_udptl_packet(&rec_str, seq_fec); + packet_sequencer_insert(&tg->sequencer, &up->p); + up = NULL; + } + + g_string_free(rec_s, TRUE); + + // no point in continuing further: one packet was missing, which means + // that no other packet in this span can be recovered + break; + } + + // proceed to next entry + entries--; + seq_start++; + } + } + +seq_ok:; + + t38_core_state_t *t38 = t38_gateway_get_t38_core_state(tg->gw); + + // process any packets that we can + while (1) { + up = packet_sequencer_next_packet(&tg->sequencer); + if (!up) + break; + + ilog(LOG_DEBUG, "Processing %i IFP bytes, seq %i", up->s->len, up->p.seq); + + t38_core_rx_ifp_packet(t38, (uint8_t *) up->s->s, up->s->len, up->p.seq); + + __udptl_packet_free(up); + } + +out: + mutex_unlock(&tg->lock); + return 0; + +err: + if (err) + ilog(LOG_ERR | LOG_FLAG_LIMIT, "Failed to process UDPTL/T.38/IFP packet: %s", err); + return -1; +} + + +void t38_gateway_stop(struct t38_gateway *tg) { + if (!tg) + return; + if (tg->pcm_player) + media_player_stop(tg->pcm_player); + if (tg->t38_media) + g_queue_clear_full(&tg->t38_media->sdp_attributes, free); +} + + + +#endif diff --git a/include/call.h b/include/call.h index da33d994c..cabe9d030 100644 --- a/include/call.h +++ b/include/call.h @@ -20,6 +20,7 @@ #include "recording.h" #include "statistics.h" #include "codeclib.h" +#include "t38.h" #define UNDEFINED ((unsigned int) -1) @@ -151,6 +152,7 @@ enum call_type { #define MEDIA_FLAG_TRANSCODE 0x00800000 #define MEDIA_FLAG_PTIME_OVERRIDE 0x01000000 #define MEDIA_FLAG_RTCP_FB SHARED_FLAG_RTCP_FB +#define MEDIA_FLAG_GENERATOR 0x02000000 /* access macros */ #define SP_ISSET(p, f) bf_isset(&(p)->sp_flags, SP_FLAG_ ## f) @@ -223,6 +225,7 @@ struct stream_params { str ice_pwd; int ptime; str media_id; + struct t38_options t38_options; }; struct endpoint_map { @@ -330,6 +333,8 @@ struct call_media { struct codec_handler *codec_handler_cache; struct rtcp_handler *rtcp_handler; struct codec_handler *dtmf_injector; + struct t38_gateway *t38_gateway; + struct codec_handler *t38_handler; int ptime; // either from SDP or overridden diff --git a/include/call_interfaces.h b/include/call_interfaces.h index f225e7450..89d2d2413 100644 --- a/include/call_interfaces.h +++ b/include/call_interfaces.h @@ -76,6 +76,9 @@ struct sdp_ng_flags { always_transcode:1, asymmetric_codecs:1, inject_dtmf:1, + t38_decode:1, + t38_force:1, + t38_stop:1, supports_load_limit:1, dtls_off:1, sdes_off:1, diff --git a/include/codec.h b/include/codec.h index 702a8956c..6f205eb07 100644 --- a/include/codec.h +++ b/include/codec.h @@ -18,6 +18,7 @@ struct ssrc_hash; struct sdp_ng_flags; struct codec_ssrc_handler; struct rtp_header; +struct stream_params; typedef int codec_handler_func(struct codec_handler *, struct media_packet *); @@ -35,8 +36,10 @@ struct codec_handler { struct ssrc_hash *ssrc_hash; struct codec_handler *output_handler; // == self, or other PT handler +#ifdef WITH_TRANSCODING int (*packet_encoded)(encoder_t *enc, void *u1, void *u2); int (*packet_decoded)(decoder_t *, AVFrame *, void *, void *); +#endif // for media playback struct codec_ssrc_handler *ssrc_handler; @@ -54,7 +57,6 @@ struct codec_handler *codec_handler_get(struct call_media *, int payload_type); void codec_handlers_free(struct call_media *); struct codec_handler *codec_handler_make_playback(const struct rtp_payload_type *src_pt, const struct rtp_payload_type *dst_pt, unsigned long ts); -void codec_handler_free(struct codec_handler **handler); void ensure_codec_def(struct rtp_payload_type *pt, struct call_media *media); void codec_add_raw_packet(struct media_packet *mp); @@ -76,7 +78,9 @@ void __rtp_payload_type_add_send(struct call_media *other_media, struct rtp_payl #ifdef WITH_TRANSCODING -void codec_handlers_update(struct call_media *receiver, struct call_media *sink, const struct sdp_ng_flags *); +void codec_handler_free(struct codec_handler **handler); +void codec_handlers_update(struct call_media *receiver, struct call_media *sink, const struct sdp_ng_flags *, + const struct stream_params *); void codec_add_dtmf_event(struct codec_ssrc_handler *ch, int code, int level, uint64_t ts); uint64_t codec_last_dtmf_event(struct codec_ssrc_handler *ch); uint64_t codec_encoder_pts(struct codec_ssrc_handler *ch); @@ -86,7 +90,8 @@ uint64_t codec_decoder_unskip_pts(struct codec_ssrc_handler *ch); #else INLINE void codec_handlers_update(struct call_media *receiver, struct call_media *sink, - const struct sdp_ng_flags *flags) { } + const struct sdp_ng_flags *flags, const struct stream_params *sp) { } +INLINE void codec_handler_free(struct codec_handler **handler) { } #endif diff --git a/include/media_socket.h b/include/media_socket.h index 5ab3e8827..d3f9df3eb 100644 --- a/include/media_socket.h +++ b/include/media_socket.h @@ -202,6 +202,11 @@ INLINE int proto_is_not_rtp(const struct transport_protocol *protocol) { return 0; return protocol->rtp ? 0 : 1; } +INLINE int proto_is(const struct transport_protocol *protocol, enum transport_protocol_index idx) { + if (!protocol) + return 0; + return (protocol->index == idx) ? 1 : 0; +} #endif diff --git a/include/t38.h b/include/t38.h new file mode 100644 index 000000000..857eb8378 --- /dev/null +++ b/include/t38.h @@ -0,0 +1,97 @@ +#ifndef _T38_H_ +#define _T38_H_ + + +struct t38_gateway; + +struct t38_options { + int version; + int fec_span; // 1 means no FEC + int min_ec_entries; // currently ignored + int max_ec_entries; + int max_ifp; + int max_datagram; + + int local_tcf:1; + int fill_bit_removal:1; + int transcoding_mmr:1; + int transcoding_jbig:1; +}; + + + +#ifdef WITH_TRANSCODING + + + +#include +#include +#include +#include +#include +#include + +#include "rtplib.h" +#include "aux.h" +#include "obj.h" +#include "codeclib.h" + + + +struct call_media; +struct media_packet; +struct media_player; + + +struct t38_gateway { + struct obj obj; // use refcount as this struct is shared between two medias + mutex_t lock; + struct call_media *t38_media; + struct call_media *pcm_media; + struct rtp_payload_type pcm_pt; // PCM input for spandsp + t38_gateway_state_t *gw; + + struct t38_options options; + + // udptl outgoing stuff + uint16_t seqnum; + GQueue udptl_ec_out; // seq, seq-1, seq-2, ... + // udptl incoming stuff + packet_sequencer_t sequencer; + GHashTable *udptl_fec; + + // player for PCM data + struct media_player *pcm_player; + unsigned long long pts; +}; + + + +int t38_gateway_pair(struct call_media *t38_media, struct call_media *pcm_media, const struct t38_options *); +void t38_gateway_start(struct t38_gateway *); +int t38_gateway_input_samples(struct t38_gateway *, int16_t amp[], int len); +int t38_gateway_input_udptl(struct t38_gateway *, const str *); +void t38_gateway_stop(struct t38_gateway *); + + +INLINE void t38_gateway_put(struct t38_gateway **tp) { + if (!tp || !*tp) + return; + obj_put(*tp); + *tp = NULL; +} + + +#else + +#include "compat.h" + +// stubs +INLINE void t38_gateway_start(struct t38_gateway *tg) { } +INLINE void t38_gateway_stop(struct t38_gateway *tg) { } +INLINE void t38_gateway_put(struct t38_gateway **tp) { } + + +#endif + +#endif diff --git a/lib/codeclib.c b/lib/codeclib.c index 72b9741ff..df5934343 100644 --- a/lib/codeclib.c +++ b/lib/codeclib.c @@ -958,6 +958,12 @@ void *packet_sequencer_force_next_packet(packet_sequencer_t *ps) { return __packet_sequencer_next_packet(ps, 0); } +int packet_sequencer_next_ok(packet_sequencer_t *ps) { + if (g_tree_lookup(ps->packets, GINT_TO_POINTER(ps->seq))) + return 1; + return 0; +} + int packet_sequencer_insert(packet_sequencer_t *ps, seq_packet_t *p) { int ret = 0; diff --git a/lib/codeclib.h b/lib/codeclib.h index 55bce19b8..87dc7fd1f 100644 --- a/lib/codeclib.h +++ b/lib/codeclib.h @@ -7,6 +7,14 @@ struct packet_sequencer_s; typedef struct codec_def_s codec_def_t; typedef struct packet_sequencer_s packet_sequencer_t; +enum media_type { + MT_UNKNOWN = 0, + MT_AUDIO, + MT_VIDEO, + MT_IMAGE, + MT_OTHER, +}; + #ifndef WITHOUT_CODECLIB @@ -47,14 +55,6 @@ typedef void set_dec_options_f(decoder_t *, const str *); -enum media_type { - MT_UNKNOWN = 0, - MT_AUDIO, - MT_VIDEO, - MT_IMAGE, - MT_OTHER, -}; - struct codec_type_s { void (*def_init)(codec_def_t *); @@ -226,6 +226,7 @@ void __packet_sequencer_init(packet_sequencer_t *ps, GDestroyNotify); INLINE void packet_sequencer_init(packet_sequencer_t *ps, GDestroyNotify); void packet_sequencer_destroy(packet_sequencer_t *ps); void *packet_sequencer_next_packet(packet_sequencer_t *ps); +int packet_sequencer_next_ok(packet_sequencer_t *ps); void *packet_sequencer_force_next_packet(packet_sequencer_t *ps); int packet_sequencer_insert(packet_sequencer_t *ps, seq_packet_t *); @@ -263,9 +264,6 @@ INLINE char *av_error(int no) { #else // stubs -enum media_type { - MT_INVALID = -1, -}; struct codec_def_s { int dtmf; }; diff --git a/perl/NGCP/Rtpengine/AutoTest.pm b/perl/NGCP/Rtpengine/AutoTest.pm index cdaec2349..17742c9b6 100644 --- a/perl/NGCP/Rtpengine/AutoTest.pm +++ b/perl/NGCP/Rtpengine/AutoTest.pm @@ -20,7 +20,7 @@ BEGIN { require Exporter; @ISA = qw(Exporter); our @EXPORT = qw(autotest_start new_call offer answer ft tt snd srtp_snd rtp rcv srtp_rcv - srtp_dec escape rtpm reverse_tags new_tt crlf sdp_split rtpe_req offer_answer); + srtp_dec escape rtpm rtpmre reverse_tags new_tt crlf sdp_split rtpe_req offer_answer); }; @@ -166,6 +166,9 @@ sub rcv { if ($cb) { $p = $cb->($hdr_mark, $pt, $seq, $ts, $ssrc, $payload, $p, $cb_arg); } + if ($p !~ $match) { + print(unpack('H*', $p) . "\n"); + } like $p, $match, 'received packet matches'; my @matches = $p =~ $match; for my $m (@matches) { @@ -196,22 +199,24 @@ sub srtp_dec { sub escape { return "\Q$_[0]\E"; } -sub rtpm { - my ($pt, $seq, $ts, $ssrc, $payload, $alt_payload) = @_; - print("rtp matcher $pt $seq $ts $ssrc " . unpack('H*', $payload) . "\n"); +sub rtpmre { + my ($pt, $seq, $ts, $ssrc, $xre) = @_; + #print("rtp matcher $pt $seq $ts $ssrc $xre\n"); my $re = ''; $re .= escape(pack('C', 0x80)); $re .= escape(pack('C', $pt)); $re .= $seq >= 0 ? escape(pack('n', $seq)) : '(..)'; $re .= $ts >= 0 ? escape(pack('N', $ts)) : '(....)'; $re .= $ssrc >= 0 ? escape(pack('N', $ssrc)) : '(....)'; + $re .= $xre; + return qr/^$re$/s; +} +sub rtpm { + my ($pt, $seq, $ts, $ssrc, $payload, $alt_payload) = @_; if (!$alt_payload) { - $re .= escape($payload); + return rtpmre($pt, $seq, $ts, $ssrc, escape($payload)); } - else { - $re .= '(' . escape($payload) . '|' . escape($alt_payload) . ')'; - } - return qr/^$re$/s; + return rtpmre($pt, $seq, $ts, $ssrc, '(' . escape($payload) . '|' . escape($alt_payload) . ')'); } sub ft { return $ft; } diff --git a/t/.gitignore b/t/.gitignore index d0321297f..74e4d8496 100644 --- a/t/.gitignore +++ b/t/.gitignore @@ -54,3 +54,8 @@ test-dtmf-detect dtmf_rx_fillin.h *-test.c jitter_buffer.c +t38.c +spandsp_recv_fax_pcm +spandsp_recv_fax_t38 +spandsp_send_fax_pcm +spandsp_send_fax_t38 diff --git a/t/Makefile b/t/Makefile index 518830fc1..027426676 100644 --- a/t/Makefile +++ b/t/Makefile @@ -63,6 +63,8 @@ HASHSRCS= ifeq ($(with_transcoding),yes) SRCS+= transcode-test.c test-dtmf-detect.c payload-tracker-test.c +SRCS+= spandsp_recv_fax_pcm.c spandsp_recv_fax_t38.c spandsp_send_fax_pcm.c \ + spandsp_send_fax_t38.c ifeq ($(with_amr_tests),yes) SRCS+= amr-decode-test.c amr-encode-test.c endif @@ -70,7 +72,7 @@ LIBSRCS+= codeclib.c resample.c socket.c streambuf.c dtmflib.c DAEMONSRCS+= codec.c call.c ice.c kernel.c media_socket.c stun.c bencode.c poller.c \ dtls.c recording.c statistics.c rtcp.c redis.c iptables.c graphite.c \ cookie_cache.c udp_listener.c homer.c load.c cdr.c dtmf.c timerthread.c \ - media_player.c jitter_buffer.c + media_player.c jitter_buffer.c t38.c HASHSRCS+= call_interfaces.c control_ng.c sdp.c endif @@ -117,6 +119,16 @@ daemon-tests: tests-preload.so bitstr-test: bitstr-test.o +spandsp_send_fax_pcm: spandsp_send_fax_pcm.o + +spandsp_recv_fax_pcm: spandsp_recv_fax_pcm.o + +spandsp_send_fax_t38: spandsp_send_fax_t38.o + +spandsp_recv_fax_t38: spandsp_recv_fax_t38.o + +spandsp_raw_fax_tests: spandsp_send_fax_pcm spandsp_recv_fax_pcm spandsp_send_fax_t38 spandsp_recv_fax_t38 + amr-decode-test: amr-decode-test.o $(COMMONOBJS) codeclib.o resample.o dtmflib.o amr-encode-test: amr-encode-test.o $(COMMONOBJS) codeclib.o resample.o dtmflib.o @@ -130,7 +142,7 @@ transcode-test: transcode-test.o $(COMMONOBJS) codeclib.o resample.o codec.o ssr rtcp.o redis.o iptables.o graphite.o call_interfaces.strhash.o sdp.strhash.o rtp.o crypto.o \ control_ng.strhash.o \ streambuf.o cookie_cache.o udp_listener.o homer.o load.o cdr.o dtmf.o timerthread.o \ - media_player.o jitter_buffer.o dtmflib.o + media_player.o jitter_buffer.o dtmflib.o t38.o payload-tracker-test: payload-tracker-test.o $(COMMONOBJS) ssrc.o aux.o auxlib.o rtp.o crypto.o codeclib.o \ resample.o dtmflib.o diff --git a/t/auto-daemon-tests-t38.pl b/t/auto-daemon-tests-t38.pl new file mode 100755 index 000000000..5fe59d34f --- /dev/null +++ b/t/auto-daemon-tests-t38.pl @@ -0,0 +1,686 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use NGCP::Rtpengine::Test; +use NGCP::Rtpclient::SRTP; +use NGCP::Rtpengine::AutoTest; +use Test::More; +use IPC::Open3; + + +autotest_start(qw(--config-file=none -t -1 -i 203.0.113.1 -i 2001:db8:4321::1 + -n 2223 -c 12345 -f -L 7 -E -u 2222 --jitter-buffer=10)) + or die; + + +my ($sock_a, $sock_b, $port_a, $port_b, $ssrc, $resp, $srtp_ctx_a, $srtp_ctx_b, @ret1, @ret2); + + + +sub fec { + my ($seq_out, $num_ec, $span, $packets) = @_; + + my $ec_ents = 0; + my $ec_list = ''; + + for my $ec_pack (0 .. ($num_ec-1)) { + my $ec_seq = $seq_out - $num_ec * $span + $ec_pack; + last if $ec_seq < 0 || !exists($packets->[$ec_seq]); + my $xor = ''; + for my $fec_iter ((0 .. ($span-1))) { + my $fec_seq = $ec_seq + $fec_iter * $num_ec; + my $ecpkt = $packets->[$fec_seq]; + ok (defined $ecpkt, "FEC packet $fec_seq exists"); + ok (length($ecpkt) < 0x80, 'FEC packet short enough'); + $xor ^= $ecpkt; + } + $ec_list .= pack('Ca*', length($xor), $xor); + $ec_ents++; + } + + return ($ec_ents, $ec_list); +} + +sub t38_gw_test { + my ($testname, $pcm_cmd, $t38_cmd, %opts) = @_; + + my ($pcm_pid, $pcm_src, $pcm_sink); + ok($pcm_pid = open3($pcm_sink, $pcm_src, '>&STDERR', $pcm_cmd), + "$testname - spandsp_send_fax_pcm"); + + unlink('out.tif'); + ok (! -e 'out.tif', 'output file does not exists'); + + my ($t38_pid, $t38_src, $t38_sink); + ok($t38_pid = open3($t38_sink, $t38_src, '>&STDERR', $t38_cmd), + "$testname - spandsp_recv_fax_t38"); + + my ($buf, $rin); + my $seq = -1; + my $t38_pkt = ''; + my $udptl_seq = 0; + my @udptl_ec_in; + my @udptl_ec_out; + my $done = 0; + my $sqo = 1000; + my $tso = 3000; + my $ts = -1; + + my $rev = $opts{reverse} // 0; + my $pcm_sock = $rev ? $sock_a : $sock_b; + my $pcm_port = $rev ? $port_b : $port_a; + my $t38_sock = $rev ? $sock_b : $sock_a; + my $t38_port = $rev ? $port_a : $port_b; + + my $num_ec = $opts{num_ec} // 3; + my $span = $opts{span} // 1; + my $fec = $span > 1; + + # speed is controlled by the PCM generator + while (!$done && sysread($pcm_src, $buf = '', 160) == 160) { + # send generated PCM to rtpengine + snd($pcm_sock, $pcm_port, rtp(8, $sqo += 1, $tso += 160, 0x1234, $buf)); + # it will also have generated a block of PCM + if ($seq == -1) { + ($seq, $ts, $ssrc, $buf) = rcv($pcm_sock, $pcm_port, rtpmre(8 | 0x80, -1, -1, -1, '(' . ("." x 160) . ')')); + } + else { + ($buf) = rcv($pcm_sock, $pcm_port, rtpmre(8, $seq += 1, $ts += 160, $ssrc, '(' . ("." x 160) . ')')); + } + # write it back to our PCM endpoint + is length($buf), 160, 'buf length ok'; + ok (syswrite($pcm_sink, $buf), 'PCM writeback'); + + # read from our local T.38 producer? + $rin = ''; + vec($rin, fileno($t38_src), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0) == 1) { + my $ret = sysread($t38_src, $buf = '', 1); + ok (defined($ret), 'T.38 read ok'); + + if ($ret == 0) { + # EOF + $done = 1; + ok (waitpid($t38_pid, 0), 'T.38 spandsp finished'); + undef($t38_pid); + last; + } + + $t38_pkt .= $buf; + # complete packet? + my ($seq_out, $len, $pkt) = unpack('SSa*', $t38_pkt); + next unless defined($pkt); # nope + next if length($pkt) < $len; # nope + + # extract... + substr($t38_pkt, 0, $len + 4) = ''; + substr($pkt, $len) = ''; + + ok ($len > 0 && $len < 0x80, "local packet $seq_out short enough"); + + # save for EC + $udptl_ec_out[$seq_out] = $pkt; + + # redundancy: + my $ec_method = 0x00; + my $ec_span = ''; + my $ec_ents = 0; + my $ec_list = ''; + if (!$fec) { + for my $ec_seq (reverse(($seq_out - $num_ec) .. ($seq_out - 1))) { + last if $ec_seq < 0 || !exists($udptl_ec_out[$ec_seq]); + my $ecpkt = $udptl_ec_out[$ec_seq]; + ok (length($ecpkt) < 0x80, 'EC packet short enough'); + $ec_list .= pack('Ca*', length($ecpkt), $ecpkt); + $ec_ents++; + } + } + else { + $ec_method = 0x80; + $ec_span = pack('CC', 1, $span); + ($ec_ents, $ec_list) = fec($seq_out, $num_ec, $span, \@udptl_ec_out); + } + + # pack into UDPTL with redundancy + my $udptl = pack('nCa*Ca*Ca*', $seq_out, length($pkt), $pkt, $ec_method, + $ec_span, $ec_ents, $ec_list); + + # send + snd($t38_sock, $t38_port, $udptl); + } + + # read from our UDPTL source? + $rin = ''; + vec($rin, fileno($t38_sock), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0) == 1) { + my ($enc_seq, $len, $pkt) = rcv($t38_sock, $t38_port, qr/^(..)(.)(.*)$/s); + + # allow for duplicates, as they're generated in some cases + ok ($enc_seq == $udptl_seq || $enc_seq == $udptl_seq + 1, "UDPTL seq $enc_seq"); + $udptl_seq = $enc_seq; + + $len = ord($len); + ok ($len > 0 && $len < 0x80, 'remote packet short enough'); + + # extract... + my $ifp = substr($pkt, 0, $len, ''); + ok (length($ifp) == $len, 'length matches'); + + $udptl_ec_in[$udptl_seq] = $ifp; + + my $red = substr($pkt, 0, 1, ''); + ok ($red eq ($fec ? "\x80" : "\x00"), 'redundacy method'); + + if (!$fec) { + my $nec = substr($pkt, 0, 1, ''); + ok ($nec eq chr($udptl_seq > 3 ? 3 : $udptl_seq), "num EC packets " . ord($nec)); + $nec = ord($nec); + + # check EC packets + for my $ec_seq (reverse(($udptl_seq - $nec) .. ($udptl_seq - 1))) { + my $len = substr($pkt, 0, 1, ''); + $len = ord($len); + ok ($len > 0 && $len < 0x80, 'EC packet short enough'); + my $ec = substr($pkt, 0, $len, ''); + if ($ec_seq == 0 && !exists($udptl_ec_in[$ec_seq])) { + # this happens on T.38=force before the answer + # was seen. seq 0 is generated but not sent as + # we don't have an endpoint yet. + # XXX can this be fixed? queue packet? + ; + } + else { + ok ($ec eq $udptl_ec_in[$ec_seq], 'EC packet matches'); + } + } + } + else { + ok (substr($pkt, 0, 1, '') eq "\x01", 'FEC span header'); + my $nspan = substr($pkt, 0, 1, ''); + $nspan = ord($nspan); + ok ($nspan >= 1, 'FEC span min'); + my $expspan = $span; + my $expent = $num_ec; + while ($udptl_seq < $expspan * $expent) { + if ($expspan > 1) { + $expspan--; + next; + } + $expent--; + } + ok ($expspan == $nspan, "FEC span $expspan == $nspan"); + my $nec = ord(substr($pkt, 0, 1, '')); + ok ($expent == $nec, "FEC num entries $expent == $nec"); + # extract all entries and compare with self-generated list + my ($fec_entries, $fec_blob) = fec($udptl_seq, $nec, $nspan, \@udptl_ec_in); + my $recv_blob = ''; + for (1 .. $nec) { + my $len = substr($pkt, 0, 1, ''); + $len = ord($len); + ok ($len > 0 && $len < 0x80, 'FEC packet short enough'); + my $ec = substr($pkt, 0, $len, ''); + $recv_blob .= pack('Ca*', $len, $ec); + } + ok ($fec_entries == $nec, "num actual FEC entries $fec_entries == $nec"); + ok ($recv_blob eq $fec_blob, 'FEC blob matches'); + } + + # everything passed, write to T.38 end + ok (syswrite($t38_sink, pack('SSa*', $udptl_seq, length($ifp), $ifp)), 'T.38 writeback'); + } + } + + # delete to stop PCM player + rtpe_req('delete', "$testname delete", { 'from-tag' => ft() }); + + undef($t38_src); + undef($t38_sink); + undef($pcm_src); + undef($pcm_sink); + + if ($t38_pid) { + ok (waitpid($t38_pid, 0), 'T.38 spandsp finished'); + undef($t38_pid); + } + if ($pcm_pid) { + ok (waitpid($pcm_pid, 0), 'PCM spandsp finished'); + undef($pcm_pid); + } + + ok (-f 'out.tif', 'output file exists'); + ok (-s 'out.tif' > 10000, 'output file large enough'); + unlink('out.tif'); + +} + + + + +($sock_a, $sock_b) = new_call([qw(198.51.100.1 4020)], [qw(198.51.100.3 4022)]); + +($port_a) = offer('T.38 after re-invite', { ICE => 'remove', + }, < 'remove' }, < [ 'force' ], ICE => 'remove', + }, < 'remove' }, < 1); + + + + +done_testing(); +exit; + + + +($sock_a, $sock_b) = new_call([qw(198.51.100.1 4016)], [qw(198.51.100.3 4018)]); + +($port_a) = offer('plain T.38, reverse invite', { 'T.38' => [ 'force' ], ICE => 'remove', + }, < 'remove' }, < 1); + + + + +($sock_a, $sock_b) = new_call([qw(198.51.100.1 4000)], [qw(198.51.100.3 4002)]); + +($port_a) = offer('plain T.38, forward invite', { 'T.38' => [ 'decode' ], ICE => 'remove', + 'codec' => { 'transcode' => ['PCMA'] } }, < 'remove' }, < [ 'decode' ], ICE => 'remove', + 'codec' => { 'transcode' => ['PCMA'] } }, < 'remove' }, < [ 'decode' ], ICE => 'remove', + 'codec' => { 'transcode' => ['PCMA'] } }, < 'remove' }, < 3); + + + + + +($sock_a, $sock_b) = new_call([qw(198.51.100.1 4012)], [qw(198.51.100.3 4014)]); + +($port_a) = offer('FEC span 5', { 'T.38' => [ 'decode' ], ICE => 'remove', + 'codec' => { 'transcode' => ['PCMA'] } }, < 'remove' }, < 5); + + + + +# XXX packet loss tests +# XXX tests of different SDP options + + + + +done_testing(); diff --git a/t/auto-daemon-tests.pl b/t/auto-daemon-tests.pl index ab71d7176..abdce172e 100755 --- a/t/auto-daemon-tests.pl +++ b/t/auto-daemon-tests.pl @@ -18,6 +18,468 @@ my ($sock_a, $sock_b, $port_a, $port_b, $ssrc, $resp, $srtp_ctx_a, $srtp_ctx_b, +# T.38 signalling scenarios + +new_call(); + +offer('forward T.38 invite without codecs given', { 'T.38' => [ 'decode' ], ICE => 'remove', + }, < ft() }); + + + + + + +new_call(); + +offer('T.38 forward re-invite', { ICE => 'remove', + }, < 'remove', + }, < 'remove', 'T.38' => [ 'force' ], + }, < 'remove', + }, < ft() }); + + + + +new_call(); + +offer('T.38 reverse re-invite', { ICE => 'remove', + }, < 'remove', + }, < 'remove', 'T.38' => [ 'decode' ], + }, < 'remove' }, < ft() }); + + + + + + +new_call(); + +offer('T.38 forward re-invite w/ unsupported codec', { ICE => 'remove', + }, < 'remove', + }, < 'remove', 'T.38' => [ 'force' ], + }, < 'remove', + }, < ft() }); + + + + +new_call(); + +offer('T.38 reverse re-invite w/ unsupported codec', { ICE => 'remove', + }, < 'remove', + }, < 'remove', 'T.38' => [ 'decode' ], + }, < 'remove' }, < ft() }); + + + + + + # github issue 850 new_call; diff --git a/t/pcm_rtp_test.pl b/t/pcm_rtp_test.pl new file mode 100755 index 000000000..f03b7ca13 --- /dev/null +++ b/t/pcm_rtp_test.pl @@ -0,0 +1,59 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open3; +use IO::Socket; +use IO::Socket::IP; + +my $laddr = shift or die; +my $lport = shift or die; +my $raddr = shift or die; +my $rport = shift or die; + +my $sock = IO::Socket::IP->new(Type => &SOCK_DGRAM, Proto => 'udp', + LocalHost => $laddr, LocalPort => $lport, + PeerHost => $raddr, PeerPort => $rport, + ) + or die; + +my ($src, $sink); +my $pid = open3($sink, $src, '>&STDERR', @ARGV) or die; + +my ($playsrc, $playsink); +open($playsrc, '|-', qw(play -q -c 1 -e a-law -r 8000 -t raw -)) or die; +open($playsink, '|-', qw(play -q -c 1 -e a-law -r 8000 -t raw -)) or die; + +my $lseq = rand(); +my $lssrc = rand(); +my $lts = rand(); +my $lpt = 8; # PCMA +my $lmark = 0x80; +my $rseq = -1; +my $rts = -1; + +while (1) { + my $buf; + + last unless sysread($src, $buf = '', 160); + syswrite($playsrc, $buf); + + my $rtp = pack('CCnNN a*', 0x80, $lpt | $lmark, $lseq, $lts, $lssrc, $buf); + last unless $sock->syswrite($rtp) or last; + $lseq++; + $lts += 160; + $lmark = 0x00; + + last unless $sock->sysread($buf = '', 0xffff); + + my ($ver, $rpt, $seq, $ts, $rssrc, $payload) = unpack('CCnNN a*', $buf); + die unless length($payload) == 160; + die unless ($rpt & 0x7f) == $lpt; + die unless ($rseq == -1 || (($rseq + 1) & 0xffff) == $seq); + die unless ($rts == -1 || (($rts + 160) & 0xffffffff) == $ts); + syswrite($playsink, $payload); + $rseq = $seq; + $rts = $ts; + + last unless syswrite($sink, $payload); +} diff --git a/t/spandsp_fax_pcm_test.pl b/t/spandsp_fax_pcm_test.pl new file mode 100755 index 000000000..690dc7ea4 --- /dev/null +++ b/t/spandsp_fax_pcm_test.pl @@ -0,0 +1,32 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; +use POSIX ":sys_wait_h"; + +my ($send_src, $send_sink); +my $send_pid = open2($send_src, $send_sink, './spandsp_send_fax_pcm test.tif') or die; + +unlink('out.tif'); + +my ($recv_src, $recv_sink); +my $recv_pid = open2($recv_src, $recv_sink, './spandsp_recv_fax_pcm out.tif') or die; + +while ($send_pid && $recv_pid) { + + my $buf; + + if (sysread($send_src, $buf = '', 160)) { + syswrite($recv_sink, $buf) or die; + } + + if (sysread($recv_src, $buf = '', 160)) { + syswrite($send_sink, $buf) or die; + } + + undef($send_pid) if waitpid($send_pid, WNOHANG); + undef($recv_pid) if waitpid($recv_pid, WNOHANG); +} + +sleep(5); diff --git a/t/spandsp_fax_t38_test.pl b/t/spandsp_fax_t38_test.pl new file mode 100755 index 000000000..6e586945d --- /dev/null +++ b/t/spandsp_fax_t38_test.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; +use POSIX ":sys_wait_h"; + +my ($send_src, $send_sink); +my $send_pid = open2($send_src, $send_sink, './spandsp_send_fax_t38 test.tif') or die; + +unlink('out.tif'); + +my ($recv_src, $recv_sink); +my $recv_pid = open2($recv_src, $recv_sink, './spandsp_recv_fax_t38 out.tif') or die; + +while ($send_pid && $recv_pid) { + + my ($buf, $rin); + + $rin = ''; + vec($rin, fileno($send_src), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0.02) == 1) { + sysread($send_src, $buf = '', 1); + syswrite($recv_sink, $buf) or last; + } + + $rin = ''; + vec($rin, fileno($recv_src), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0.02) == 1) { + sysread($recv_src, $buf = '', 1); + syswrite($send_sink, $buf) or last; + } + + undef($send_pid) if waitpid($send_pid, WNOHANG); + undef($recv_pid) if waitpid($recv_pid, WNOHANG); +} + +sleep(5); diff --git a/t/spandsp_recv_fax_pcm.c b/t/spandsp_recv_fax_pcm.c new file mode 100644 index 000000000..dc09c5c00 --- /dev/null +++ b/t/spandsp_recv_fax_pcm.c @@ -0,0 +1,151 @@ +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SAMPLES_PER_CHUNK 160 +#define MICROSECONDS_PER_CHUNK 20000 + + +// from ITU G.191 +void alaw_compress (size_t lseg, int16_t *linbuf, uint8_t *logbuf) { + short ix, iexp; + long n; + + for (n = 0; n < lseg; n++) { + ix = linbuf[n] < 0 /* 0 <= ix < 2048 */ + ? (~linbuf[n]) >> 4 /* 1's complement for negative values */ + : (linbuf[n]) >> 4; + + /* Do more, if exponent > 0 */ + if (ix > 15) { /* exponent=0 for ix <= 15 */ + iexp = 1; /* first step: */ + while (ix > 16 + 15) { /* find mantissa and exponent */ + ix >>= 1; + iexp++; + } + ix -= 16; /* second step: remove leading '1' */ + + ix += iexp << 4; /* now compute encoded value */ + } + if (linbuf[n] >= 0) + ix |= (0x0080); /* add sign bit */ + + logbuf[n] = ix ^ (0x0055); /* toggle even bits */ + } +} +void alaw_expand (size_t lseg, uint8_t *logbuf, int16_t *linbuf) { + short ix, mant, iexp; + long n; + + for (n = 0; n < lseg; n++) { + ix = logbuf[n] ^ (0x0055); /* re-toggle toggled bits */ + + ix &= (0x007F); /* remove sign bit */ + iexp = ix >> 4; /* extract exponent */ + mant = ix & (0x000F); /* now get mantissa */ + if (iexp > 0) + mant = mant + 16; /* add leading '1', if exponent > 0 */ + + mant = (mant << 4) + (0x0008); /* now mantissa left justified and */ + /* 1/2 quantization step added */ + if (iexp > 1) /* now left shift according exponent */ + mant = mant << (iexp - 1); + + linbuf[n] = logbuf[n] > 127 /* invert, if negative sample */ + ? mant : -mant; + } +} + + + +int done = 0; + +static void phase_e_handler(t30_state_t *s, void *user_data, int result) { + fprintf(stderr, "recv: phase E result %i\n", result); + assert(result == T30_ERR_OK); + done = 1; +} + + +int main(int argc, char **argv) { + assert(argc == 2); + const char *output_file_name = argv[1]; + + fax_state_t *fax = fax_init(NULL, FALSE); + assert(fax != NULL); + + int use_transmit_on_idle = 1; + int use_tep = 0; + int supported_modems = T30_SUPPORT_V27TER | T30_SUPPORT_V29 | T30_SUPPORT_V17; + int use_ecm = 0; + + // taken from t38_gateway_tests.c + t30_state_t *t30 = fax_get_t30_state(fax); + fax_set_transmit_on_idle(fax, use_transmit_on_idle); + fax_set_tep_mode(fax, use_tep); + t30_set_supported_modems(t30, supported_modems); + t30_set_tx_ident(t30, "11111111"); + t30_set_tx_nsf(t30, (const uint8_t *) "\x50\x00\x00\x00Spandsp\x00", 12); + t30_set_rx_file(t30, output_file_name, -1); + t30_set_phase_e_handler(t30, phase_e_handler, NULL); + t30_set_ecm_capability(t30, use_ecm); + if (use_ecm) + t30_set_supported_compressions(t30, T30_SUPPORT_T4_1D_COMPRESSION | T30_SUPPORT_T4_2D_COMPRESSION | T30_SUPPORT_T6_COMPRESSION); + t30_set_minimum_scan_line_time(t30, 40); + + struct timeval now, next; + + setbuf(stdout, NULL); + gettimeofday(&now, NULL); + + while (!done) { + next = now; + next.tv_usec += MICROSECONDS_PER_CHUNK; + while (next.tv_usec >= 1000000) { + next.tv_usec -= 1000000; + next.tv_sec++; + } + + int16_t samples[SAMPLES_PER_CHUNK]; + + int ret = fax_tx(fax, samples, SAMPLES_PER_CHUNK); + assert(ret == SAMPLES_PER_CHUNK); + + uint8_t alaw[SAMPLES_PER_CHUNK]; + alaw_compress(SAMPLES_PER_CHUNK, samples, alaw); + + ret = fwrite(alaw, SAMPLES_PER_CHUNK, 1, stdout); + assert(ret == 1); + + ret = fread(alaw, SAMPLES_PER_CHUNK, 1, stdin); + assert(ret == 1); + + alaw_expand(SAMPLES_PER_CHUNK, alaw, samples); + + ret = fax_rx(fax, samples, SAMPLES_PER_CHUNK); + assert(ret == 0); + + while (1) { + gettimeofday(&now, NULL); + long long diff = ((long long) next.tv_sec - now.tv_sec) * 1000000 + + ((long long) next.tv_usec - now.tv_usec); + if (diff <= 0) + break; + usleep(diff); + } + } + + return 0; +} diff --git a/t/spandsp_recv_fax_t38.c b/t/spandsp_recv_fax_t38.c new file mode 100644 index 000000000..709117f1e --- /dev/null +++ b/t/spandsp_recv_fax_t38.c @@ -0,0 +1,118 @@ +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SAMPLES_PER_CHUNK 160 + + +static int packet_handler(t38_core_state_t *s, void *user_data, const uint8_t *buf, int len, int count) { + static uint16_t seq = 0; + + fprintf(stderr, "recv: writing %i bytes %i times\n", len, count); + + for (int i = 0; i < count; i++) { + uint16_t hdr[2] = {seq, len}; + int ret = write(1, hdr, sizeof(hdr)); + assert(ret == sizeof(hdr)); + ret = write(1, buf, len); + assert(ret == len); + } + + seq++; + + return 0; +} + +static void phase_e_handler(t30_state_t *s, void *user_data, int result) { + fprintf(stderr, "phase E result %i\n", result); + assert(result == T30_ERR_OK); +} + +static size_t nb_read(int fd, void *b, size_t len) { + size_t left = len; + while (left) { + ssize_t ret = read(fd, b + len - left, left); + if (ret > 0) { + left -= ret; + continue; + } + if (ret == 0) + return 0; + if (errno == EAGAIN && left != len) { + usleep(10000); + continue; + } + return -1; + } + return len; +} + +int main(int argc, char **argv) { + assert(argc == 2); + const char *output_file_name = argv[1]; + + t38_terminal_state_t *fax = t38_terminal_init(NULL, FALSE, packet_handler, NULL); + assert(fax != NULL); + + int use_tep = 0; + int supported_modems = T30_SUPPORT_V27TER | T30_SUPPORT_V29 | T30_SUPPORT_V17; + int use_ecm = 0; + int t38_version = 0; + int options = 0; + + + // taken from t38_terminal_tests.c + t30_state_t *t30 = t38_terminal_get_t30_state(fax); + t38_core_state_t *t38 = t38_terminal_get_t38_core_state(fax); + t38_set_t38_version(t38, t38_version); + t38_terminal_set_config(fax, options); + t38_terminal_set_tep_mode(fax, use_tep); + + t30_set_supported_modems(t30, supported_modems); + t30_set_tx_ident(t30, "22222222"); + t30_set_tx_nsf(t30, (const uint8_t *) "\x50\x00\x00\x00Spandsp\x00", 12); + t30_set_rx_file(t30, output_file_name, -1); + t30_set_ecm_capability(t30, use_ecm); + t30_set_phase_e_handler(t30, phase_e_handler, NULL); + + fcntl(0, F_SETFL, O_NONBLOCK); + + while (1) { + int done = t38_terminal_send_timeout(fax, SAMPLES_PER_CHUNK); + if (done) + break; + + uint16_t hdr[2]; + int ret = nb_read(0, hdr, sizeof(hdr)); + if (ret < 0 && errno == EAGAIN) { + usleep(20000); + continue; + } + assert(ret == sizeof(hdr)); + uint8_t buf[512]; + assert(hdr[1] <= sizeof(buf)); + do + ret = nb_read(0, buf, hdr[1]); + while (ret < 0 && errno == EAGAIN); + assert(ret == hdr[1]); + fprintf(stderr, "recv: processing %u bytes, seq %u\n", hdr[1], hdr[0]); + t38_core_rx_ifp_packet(t38, buf, hdr[1], hdr[0]); + } + + return 0; +} diff --git a/t/spandsp_send_fax_pcm.c b/t/spandsp_send_fax_pcm.c new file mode 100644 index 000000000..434916597 --- /dev/null +++ b/t/spandsp_send_fax_pcm.c @@ -0,0 +1,156 @@ +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SAMPLES_PER_CHUNK 160 +#define MICROSECONDS_PER_CHUNK 20000 + + +// from ITU G.191 +void alaw_compress (size_t lseg, int16_t *linbuf, uint8_t *logbuf) { + short ix, iexp; + long n; + + for (n = 0; n < lseg; n++) { + ix = linbuf[n] < 0 /* 0 <= ix < 2048 */ + ? (~linbuf[n]) >> 4 /* 1's complement for negative values */ + : (linbuf[n]) >> 4; + + /* Do more, if exponent > 0 */ + if (ix > 15) { /* exponent=0 for ix <= 15 */ + iexp = 1; /* first step: */ + while (ix > 16 + 15) { /* find mantissa and exponent */ + ix >>= 1; + iexp++; + } + ix -= 16; /* second step: remove leading '1' */ + + ix += iexp << 4; /* now compute encoded value */ + } + if (linbuf[n] >= 0) + ix |= (0x0080); /* add sign bit */ + + logbuf[n] = ix ^ (0x0055); /* toggle even bits */ + } +} +void alaw_expand (size_t lseg, uint8_t *logbuf, int16_t *linbuf) { + short ix, mant, iexp; + long n; + + for (n = 0; n < lseg; n++) { + ix = logbuf[n] ^ (0x0055); /* re-toggle toggled bits */ + + ix &= (0x007F); /* remove sign bit */ + iexp = ix >> 4; /* extract exponent */ + mant = ix & (0x000F); /* now get mantissa */ + if (iexp > 0) + mant = mant + 16; /* add leading '1', if exponent > 0 */ + + mant = (mant << 4) + (0x0008); /* now mantissa left justified and */ + /* 1/2 quantization step added */ + if (iexp > 1) /* now left shift according exponent */ + mant = mant << (iexp - 1); + + linbuf[n] = logbuf[n] > 127 /* invert, if negative sample */ + ? mant : -mant; + } +} + + + +int done = 0; + +static void phase_e_handler(t30_state_t *s, void *user_data, int result) { + fprintf(stderr, "send: phase E result %i\n", result); + assert(result == T30_ERR_OK); + done = 1; +} + + +int main(int argc, char **argv) { + assert(argc == 2); + const char *input_file_name = argv[1]; + + fax_state_t *fax = fax_init(NULL, TRUE); + assert(fax != NULL); + + int use_transmit_on_idle = 1; + int use_tep = 0; + int supported_modems = T30_SUPPORT_V27TER | T30_SUPPORT_V29 | T30_SUPPORT_V17; + int use_ecm = 0; + + // taken from t38_gateway_tests.c + t30_state_t *t30 = fax_get_t30_state(fax); + fax_set_transmit_on_idle(fax, use_transmit_on_idle); + fax_set_tep_mode(fax, use_tep); + t30_set_supported_modems(t30, supported_modems); + t30_set_tx_ident(t30, "11111111"); + t30_set_tx_nsf(t30, (const uint8_t *) "\x50\x00\x00\x00Spandsp\x00", 12); + t30_set_tx_file(t30, input_file_name, -1, -1); + t30_set_phase_e_handler(t30, phase_e_handler, NULL); + t30_set_ecm_capability(t30, use_ecm); + if (use_ecm) + t30_set_supported_compressions(t30, T30_SUPPORT_T4_1D_COMPRESSION | T30_SUPPORT_T4_2D_COMPRESSION | T30_SUPPORT_T6_COMPRESSION); + t30_set_minimum_scan_line_time(t30, 40); + + struct timeval now, next; + + setbuf(stdout, NULL); + gettimeofday(&now, NULL); + + while (!done) { + next = now; + next.tv_usec += MICROSECONDS_PER_CHUNK; + while (next.tv_usec >= 1000000) { + next.tv_usec -= 1000000; + next.tv_sec++; + } + + int16_t samples[SAMPLES_PER_CHUNK]; + + int ret = fax_tx(fax, samples, SAMPLES_PER_CHUNK); + assert(ret == SAMPLES_PER_CHUNK); + + uint8_t alaw[SAMPLES_PER_CHUNK]; + alaw_compress(SAMPLES_PER_CHUNK, samples, alaw); + + ret = fwrite(alaw, SAMPLES_PER_CHUNK, 1, stdout); + if (ret < 1) + break; + + ret = fread(alaw, SAMPLES_PER_CHUNK, 1, stdin); + if (ret == 0) + break; + assert(ret == 1); + + alaw_expand(SAMPLES_PER_CHUNK, alaw, samples); + + ret = fax_rx(fax, samples, SAMPLES_PER_CHUNK); + assert(ret == 0); + + while (1) { + gettimeofday(&now, NULL); + long long diff = ((long long) next.tv_sec - now.tv_sec) * 1000000 + + ((long long) next.tv_usec - now.tv_usec); + if (diff <= 0) + break; + usleep(diff); + } + } + +// assert(done == 1); + + return 0; +} diff --git a/t/spandsp_send_fax_t38.c b/t/spandsp_send_fax_t38.c new file mode 100644 index 000000000..dad85bdbf --- /dev/null +++ b/t/spandsp_send_fax_t38.c @@ -0,0 +1,125 @@ +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SAMPLES_PER_CHUNK 160 + + +static int packet_handler(t38_core_state_t *s, void *user_data, const uint8_t *buf, int len, int count) { + static uint16_t seq = 0; + + fprintf(stderr, "send: writing %i bytes %i times, seq %u\n", len, count, seq); + + for (int i = 0; i < count; i++) { + uint16_t hdr[2] = {seq, len}; + int ret = write(1, hdr, sizeof(hdr)); + assert(ret == sizeof(hdr)); + ret = write(1, buf, len); + assert(ret == len); + } + + seq++; + + return 0; +} + +int g_done = 0; + +static void phase_e_handler(t30_state_t *s, void *user_data, int result) { + fprintf(stderr, "phase E result %i\n", result); + assert(result == T30_ERR_OK); + g_done = 1; +} + +static size_t nb_read(int fd, void *b, size_t len) { + size_t left = len; + while (left) { + ssize_t ret = read(fd, b + len - left, left); + if (ret > 0) { + left -= ret; + continue; + } + if (ret == 0) + return 0; + if (errno == EAGAIN && left != len) { + usleep(10000); + continue; + } + return -1; + } + return len; +} + +int main(int argc, char **argv) { + assert(argc == 2); + const char *input_file_name = argv[1]; + + t38_terminal_state_t *fax = t38_terminal_init(NULL, TRUE, packet_handler, NULL); + assert(fax != NULL); + + int use_tep = 0; + int supported_modems = T30_SUPPORT_V27TER | T30_SUPPORT_V29 | T30_SUPPORT_V17; + int use_ecm = 0; + int t38_version = 0; + int options = 0; + + + // taken from t38_terminal_tests.c + t30_state_t *t30 = t38_terminal_get_t30_state(fax); + t38_core_state_t *t38 = t38_terminal_get_t38_core_state(fax); + t38_set_t38_version(t38, t38_version); + t38_terminal_set_config(fax, options); + t38_terminal_set_tep_mode(fax, use_tep); + + t30_set_supported_modems(t30, supported_modems); + t30_set_tx_ident(t30, "11111111"); + t30_set_tx_nsf(t30, (const uint8_t *) "\x50\x00\x00\x00Spandsp\x00", 12); + t30_set_tx_file(t30, input_file_name, -1, -1); + t30_set_ecm_capability(t30, use_ecm); + t30_set_phase_e_handler(t30, phase_e_handler, NULL); + + fcntl(0, F_SETFL, O_NONBLOCK); + + while (1) { + int done = t38_terminal_send_timeout(fax, SAMPLES_PER_CHUNK); + if (done) + break; + + uint16_t hdr[2]; + int ret = nb_read(0, hdr, sizeof(hdr)); + if (ret < 0 && errno == EAGAIN) { + usleep(20000); + continue; + } + if (ret == 0) + break; + assert(ret == sizeof(hdr)); + uint8_t buf[512]; + assert(hdr[1] <= sizeof(buf)); + do + ret = nb_read(0, buf, hdr[1]); + while (ret < 0 && errno == EAGAIN); + assert(ret == hdr[1]); + fprintf(stderr, "send: processing %u bytes, seq %u\n", hdr[1], hdr[0]); + t38_core_rx_ifp_packet(t38, buf, hdr[1], hdr[0]); + } + + //assert(g_done == 1); + + return 0; +} diff --git a/t/t38_udptl_test.pl b/t/t38_udptl_test.pl new file mode 100755 index 000000000..690fb6b99 --- /dev/null +++ b/t/t38_udptl_test.pl @@ -0,0 +1,63 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open3; +use IO::Socket; +use IO::Socket::IP; + +my $laddr = shift or die; +my $lport = shift or die; +my $raddr = shift or die; +my $rport = shift or die; + +my $sock = IO::Socket::IP->new(Type => &SOCK_DGRAM, Proto => 'udp', + LocalHost => $laddr, LocalPort => $lport, + PeerHost => $raddr, PeerPort => $rport, + ) + or die; + +my $devnull; +die unless open($devnull, '>', '/dev/null'); + +my ($src, $sink); +my $pid = open3($sink, $src, ">&".fileno($devnull), @ARGV) or die; + +my $lseq = 0; +my $rseq = 0; +my $srcbuf = ''; + +local $| = 1; + +while (1) { + my $rin = ''; + vec($rin, fileno($src), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0.01) == 1) { + my $ret = sysread($src, my $buf, 1); + last unless $ret; + $srcbuf .= $buf; + my ($seq_out, $len, $pkt) = unpack('SSa*', $srcbuf); + next unless defined($pkt); + next if length($pkt) < $len; + + substr($srcbuf, 0, $len + 4) = ''; + substr($pkt, $len) = ''; + + my $udptl = pack('nCa*Ca*Ca*', $seq_out, length($pkt), $pkt, 0x00, + '', 0, ''); + + print('!'); + last unless $sock->syswrite($udptl); + } + + $rin = ''; + vec($rin, fileno($sock), 1) = 1; + while (select(my $rout = $rin, undef, undef, 0.01) == 1) { + my $ret = $sock->sysread(my $buf, 0xffff); + my ($seq, $len, $pkt) = unpack('nCa*', $buf); + my $t38 = substr($pkt, 0, $len); + + print('.'); + last unless syswrite($sink, pack('SSa*', $seq, length($t38), $t38)); + } +} diff --git a/t/test.tif b/t/test.tif new file mode 100644 index 0000000000000000000000000000000000000000..b97d712af3c83893c0ca65e8554cbc8a94fa2ca5 GIT binary patch literal 513386 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 literal 0 HcmV?d00001 diff --git a/t/transcode-test.c b/t/transcode-test.c index 6bd3c34d0..d5c2e66d5 100644 --- a/t/transcode-test.c +++ b/t/transcode-test.c @@ -108,7 +108,7 @@ static void __sdp_pt_fmt(int num, str codec, int clockrate, str full_codec, str static void offer(void) { printf("offer\n"); codec_rtp_payload_types(media_B, media_A, &rtp_types, &flags); - codec_handlers_update(media_B, media_A, &flags); + codec_handlers_update(media_B, media_A, &flags, NULL); g_queue_clear(&rtp_types); memset(&flags, 0, sizeof(flags)); } @@ -116,7 +116,7 @@ static void offer(void) { static void answer(void) { printf("answer\n"); codec_rtp_payload_types(media_A, media_B, &rtp_types, &flags); - codec_handlers_update(media_A, media_B, &flags); + codec_handlers_update(media_A, media_B, &flags, NULL); } #define expect(side, dir, codecs) \ diff --git a/utils/rtpengine-ng-client b/utils/rtpengine-ng-client index a9b1380a3..24dd10bb5 100755 --- a/utils/rtpengine-ng-client +++ b/utils/rtpengine-ng-client @@ -71,6 +71,7 @@ GetOptions( 'blob=s' => \$options{'blob'}, 'blob-file=s' => \$options{'blob-file'}, 'db-id=i' => \$options{'db-id'}, + 'T38=s@' => \$options{'T.38'}, ) or die; my $cmd = shift(@ARGV) or die; @@ -89,7 +90,7 @@ for my $x (split(/,/, 'trust address,symmetric,asymmetric,unidirectional,force,s for my $x (split(/,/, 'origin,session connection')) { defined($options{'replace-' . $x}) and push(@{$packet{replace}}, $x); } -for my $x (split(/,/, 'rtcp-mux,SDES,supports')) { +for my $x (split(/,/, 'rtcp-mux,SDES,supports,T.38')) { $packet{$x} = $options{$x} if defined($options{$x}) && ref($options{$x}) eq 'ARRAY'; }