/* (C) 2011 by Harald Welte * (C) 2011 by On-Waves e.h.f * All Rights Reserved * * SPDX-License-Identifier: GPL-2.0+ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ /*! \file osmo_ortp.c * \brief Integration of libortp into osmocom framework (select, logging) */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" static PayloadType *payload_type_efr; static PayloadType *payload_type_hr; static RtpProfile *osmo_pt_profile; static void *tall_rtp_ctx; /* malloc integration */ static void *osmo_ortp_malloc(size_t sz) { return talloc_size(tall_rtp_ctx, sz); } static void *osmo_ortp_realloc(void *ptr, size_t sz) { return talloc_realloc_size(tall_rtp_ctx, ptr, sz); } static void osmo_ortp_free(void *ptr) { talloc_free(ptr); } static OrtpMemoryFunctions osmo_ortp_memfn = { .malloc_fun = osmo_ortp_malloc, .realloc_fun = osmo_ortp_realloc, .free_fun = osmo_ortp_free }; /* logging */ struct level_map { OrtpLogLevel ortp; int osmo_level; }; static const struct level_map level_map[] = { { ORTP_DEBUG, LOGL_DEBUG }, { ORTP_MESSAGE, LOGL_INFO }, { ORTP_WARNING, LOGL_NOTICE }, { ORTP_ERROR, LOGL_ERROR }, { ORTP_FATAL, LOGL_FATAL }, }; static int ortp_to_osmo_lvl(OrtpLogLevel lev) { int i; for (i = 0; i < ARRAY_SIZE(level_map); i++) { if (level_map[i].ortp == lev) return level_map[i].osmo_level; } /* default */ return LOGL_ERROR; } static void my_ortp_logfn( #if HAVE_ORTP_LOG_DOMAIN const char *domain, #endif OrtpLogLevel lev, const char *fmt, va_list args) { /* Some strings coming from ortp are not endline terminated and mangle * the output. Make sure all strings are endl terminated before * printing. */ int needs_endl; const char *domain_str; char *str; size_t fmt_len = strlen(fmt); #if HAVE_ORTP_LOG_DOMAIN /* domain can be NULL, found experimentally */ domain_str = domain ? : ""; #else domain_str = ""; #endif size_t domain_len = strlen(domain_str); if (fmt_len == 0) return; needs_endl = fmt[fmt_len - 1] != '\n' ? 1 : 0; str = osmo_ortp_malloc(domain_len + 2 /*": "*/ + fmt_len + needs_endl + 1); sprintf(str, "%s%s%s%s", domain_str, domain_len ? ": " : "", fmt, needs_endl ? "\n" : ""); osmo_vlogp(DLMIB, ortp_to_osmo_lvl(lev), __FILE__, 0, 0, str, args); osmo_ortp_free(str); } /* ORTP signal callbacks */ static void ortp_sig_cb_ssrc(RtpSession *rs, void *data) { int port = rtp_session_get_local_port(rs); uint32_t ssrc = rtp_session_get_recv_ssrc(rs); LOGP(DLMIB, LOGL_INFO, "osmo-ortp(%d): ssrc_changed to 0x%08x, resetting\n", port, ssrc); rtp_session_reset(rs); } static void ortp_sig_cb_pt(RtpSession *rs, void *data) { int port = rtp_session_get_local_port(rs); int pt = rtp_session_get_recv_payload_type(rs); LOGP(DLMIB, LOGL_NOTICE, "osmo-ortp(%d): payload_type_changed to 0x%02x\n", port, pt); } static void ortp_sig_cb_net(RtpSession *rs, void *data) { int port = rtp_session_get_local_port(rs); LOGP(DLMIB, LOGL_ERROR, "osmo-ortp(%d): network_error %s\n", port, (char *)data); } static void ortp_sig_cb_ts(RtpSession *rs, void *data) { int port = rtp_session_get_local_port(rs); uint32_t ts = rtp_session_get_current_recv_ts(rs); LOGP(DLMIB, LOGL_NOTICE, "osmo-ortp(%d): timestamp_jump, new TS %d, resyncing\n", port, ts); rtp_session_resync(rs); } static inline bool recv_with_cb(struct osmo_rtp_socket *rs) { uint8_t *payload; mblk_t *mblk = rtp_session_recvm_with_ts(rs->sess, rs->rx_user_ts); if (!mblk) return false; int plen = rtp_get_payload(mblk, &payload); /* hand into receiver */ if (rs->rx_cb && plen > 0) rs->rx_cb(rs, payload, plen, rtp_get_seqnumber(mblk), rtp_get_timestamp(mblk), rtp_get_markbit(mblk)); freemsg(mblk); if (plen > 0) return true; return false; } /*! \brief poll the socket for incoming data * \param[in] rs the socket to be polled * \returns number of packets received + handed to the rx_cb */ int osmo_rtp_socket_poll(struct osmo_rtp_socket *rs) { if (rs->flags & OSMO_RTP_F_DISABLED) return 0; if (recv_with_cb(rs)) return 1; /* this happens every time we miss an incoming RTP frame, which is quite common * when a voice channel is first activated, or also in case of packet loss. * See also https://osmocom.org/issues/4464 */ LOGP(DLMIB, LOGL_DEBUG, "osmo_rtp_socket_poll(%u): No message received\n", rs->rx_user_ts); return 0; } /* Osmo FD callbacks */ static int osmo_rtp_fd_cb(struct osmo_fd *fd, unsigned int what) { struct osmo_rtp_socket *rs = fd->data; if (what & OSMO_FD_READ) { /* in polling mode, we don't want to be called here */ if (rs->flags & OSMO_RTP_F_POLL) { osmo_fd_read_disable(fd); return 0; } if (!recv_with_cb(rs)) LOGP(DLMIB, LOGL_INFO, "recvm_with_ts(%u): ERROR!\n", rs->rx_user_ts); rs->rx_user_ts += 160; } /* writing is not queued at the moment, so OSMO_FD_WRITE * shouldn't occur */ return 0; } /* Internal API coming from rtpsession_priv.h, used in osmo_rtcp_fd_cb */ #pragma message ("Using internal ortp API: rtp_session_rtcp_rec") int rtp_session_rtcp_recv(RtpSession * session); static int osmo_rtcp_fd_cb(struct osmo_fd *fd, unsigned int what) { struct osmo_rtp_socket *rs = fd->data; /* We probably don't need this at all, as * rtp_session_recvm_with_ts() will alway also poll the RTCP * file descriptor for new data */ return rtp_session_rtcp_recv(rs->sess); } static int osmo_rtp_socket_fdreg(struct osmo_rtp_socket *rs) { int rc; osmo_fd_setup(&rs->rtp_bfd, rtp_session_get_rtp_socket(rs->sess), OSMO_FD_READ, osmo_rtp_fd_cb, rs, 0); osmo_fd_setup(&rs->rtcp_bfd, rtp_session_get_rtcp_socket(rs->sess), OSMO_FD_READ, osmo_rtcp_fd_cb, rs, 0); rc = osmo_fd_register(&rs->rtp_bfd); if (rc < 0) return rc; rc = osmo_fd_register(&rs->rtcp_bfd); if (rc < 0) { osmo_fd_unregister(&rs->rtp_bfd); return rc; } return 0; } static void create_payload_types() { PayloadType *pt; /* EFR */ pt = payload_type_new(); pt->type = PAYLOAD_AUDIO_PACKETIZED; pt->clock_rate = 8000; pt->mime_type = "EFR"; pt->normal_bitrate = 12200; pt->channels = 1; payload_type_efr = pt; /* HR */ pt = payload_type_new(); pt->type = PAYLOAD_AUDIO_PACKETIZED; pt->clock_rate = 8000; pt->mime_type = "HR"; pt->normal_bitrate = 6750; /* FIXME */ pt->channels = 1; payload_type_hr = pt; /* create a new RTP profile as clone of AV profile */ osmo_pt_profile = rtp_profile_clone(&av_profile); /* add the GSM specific payload types. They are all dynamically * assigned, but in the Osmocom GSM system we have allocated * them as follows: */ rtp_profile_set_payload(osmo_pt_profile, RTP_PT_GSM_EFR, payload_type_efr); rtp_profile_set_payload(osmo_pt_profile, RTP_PT_GSM_HALF, payload_type_hr); rtp_profile_set_payload(osmo_pt_profile, RTP_PT_AMR, &payload_type_amr); } /* public functions */ /*! \brief initialize Osmocom RTP code * \param[in] ctx default talloc context for library-internal allocations */ void osmo_rtp_init(void *ctx) { tall_rtp_ctx = ctx; ortp_set_memory_functions(&osmo_ortp_memfn); ortp_init(); ortp_set_log_level_mask( #if HAVE_ORTP_LOG_DOMAIN ORTP_LOG_DOMAIN, #endif 0xffff); ortp_set_log_handler(my_ortp_logfn); create_payload_types(); } /*! \brief Set Osmocom RTP socket parameters * \param[in] rs OsmoRTP socket * \param[in] param defined which parameter to set OSMO_RTP_P_JITBUF - enables regular jitter buffering OSMO_RTP_P_JIT_ADAP - enables adaptive jitter buffering * \param[in] val Size of jitter buffer (in ms), 0 means disable buffering * \returns negative value on error, 0 or 1 otherwise (depending on whether given jitter buffering is enabled) */ int osmo_rtp_socket_set_param(struct osmo_rtp_socket *rs, enum osmo_rtp_param param, int val) { switch (param) { case OSMO_RTP_P_JIT_ADAP: rtp_session_enable_adaptive_jitter_compensation(rs->sess, (bool)val); /* fall-through on-purpose - we have to set val anyway */ case OSMO_RTP_P_JITBUF: rtp_session_enable_jitter_buffer(rs->sess, (val) ? TRUE : FALSE); if (val) rtp_session_set_jitter_compensation(rs->sess, val); break; default: return -EINVAL; } if (param == OSMO_RTP_P_JIT_ADAP) return rtp_session_adaptive_jitter_compensation_enabled(rs->sess); return rtp_session_jitter_buffer_enabled(rs->sess); } /*! \brief Create a new RTP socket * \param[in] talloc_cxt talloc context for this allocation. NULL for * dafault context * \param[in] flags Flags like OSMO_RTP_F_POLL * \returns pointer to library-allocated \a struct osmo_rtp_socket */ struct osmo_rtp_socket *osmo_rtp_socket_create(void *talloc_ctx, unsigned int flags) { struct osmo_rtp_socket *rs; if (!talloc_ctx) talloc_ctx = tall_rtp_ctx; rs = talloc_zero(talloc_ctx, struct osmo_rtp_socket); if (!rs) return NULL; rs->flags = OSMO_RTP_F_DISABLED | flags; rs->sess = rtp_session_new(RTP_SESSION_SENDRECV); if (!rs->sess) { talloc_free(rs); return NULL; } rtp_session_set_data(rs->sess, rs); rtp_session_set_profile(rs->sess, osmo_pt_profile); rtp_session_set_jitter_compensation(rs->sess, 100); /* ortp >= 0.24.0 doesn't differentiate between SO_REUSEADDR and * SO_REUSEPORT, and has both enabled by default. The latter means that * we can end up with non-unique port bindings as we will not fail to * bind the same port twice */ rtp_session_set_reuseaddr(rs->sess, false); rtp_session_signal_connect(rs->sess, "ssrc_changed", (RtpCallback) ortp_sig_cb_ssrc, RTP_SIGNAL_PTR_CAST(rs)); rtp_session_signal_connect(rs->sess, "payload_type_changed", (RtpCallback) ortp_sig_cb_pt, RTP_SIGNAL_PTR_CAST(rs)); rtp_session_signal_connect(rs->sess, "network_error", (RtpCallback) ortp_sig_cb_net, RTP_SIGNAL_PTR_CAST(rs)); rtp_session_signal_connect(rs->sess, "timestamp_jump", (RtpCallback) ortp_sig_cb_ts, RTP_SIGNAL_PTR_CAST(rs)); /* initialize according to the RFC */ rtp_session_set_seq_number(rs->sess, random()); rs->tx_timestamp = random(); /* Make sure ssrc changes are detected immediately */ rtp_session_set_ssrc_changed_threshold(rs->sess, 0); return rs; } /*! \brief bind a RTP socket to a local port * \param[in] rs OsmoRTP socket * \param[in] ip hostname/ip as string * \param[in] port UDP port number, -1 for random selection * \returns 0 on success, <0 on error */ int osmo_rtp_socket_bind(struct osmo_rtp_socket *rs, const char *ip, int port) { int rc, rtcp = (-1 != port) ? port + 1 : -1; rc = rtp_session_set_local_addr(rs->sess, ip, port, rtcp); if (rc < 0) return rc; rs->rtp_bfd.fd = rtp_session_get_rtp_socket(rs->sess); rs->rtcp_bfd.fd = rtp_session_get_rtcp_socket(rs->sess); return 0; } /*! \brief connect a OsmoRTP socket to a remote port * \param[in] rs OsmoRTP socket * \param[in] ip String representation of remote hostname or IP address * \param[in] port UDP port number to connect to * * If the OsmoRTP socket is not in POLL mode, this function will also * cause the RTP and RTCP file descriptors to be registred with the * libosmocore select() loop integration. * * \returns 0 on success, <0 in case of error */ int osmo_rtp_socket_connect(struct osmo_rtp_socket *rs, const char *ip, uint16_t port) { int rc; if (!port) { LOGP(DLMIB, LOGL_INFO, "osmo_rtp_socket_connect() refused to " "set remote %s:%u\n", ip, port); return 0; } /* We don't want the connected mode enabled during * rtp_session_set_remote_addr(), because that will already setup a * connection and updating the remote address will no longer have an * effect. Contrary to what one may expect, this must be 0 at first, * and we're setting to 1 further down to establish a connection once * the first RTP packet is received (OS#1661). */ rtp_session_set_connected_mode(rs->sess, 0); rc = rtp_session_set_remote_addr(rs->sess, ip, port); if (rc < 0) return rc; /* enable the use of connect() so later getsockname() will * actually return the IP address that was chosen for the local * sid of the connection */ rtp_session_set_connected_mode(rs->sess, 1); rs->flags &= ~OSMO_RTP_F_DISABLED; if (rs->flags & OSMO_RTP_F_POLL) return rc; else return osmo_rtp_socket_fdreg(rs); } /*! \brief Automatically associates a RTP socket with the first incoming UDP packet * \param[in] rs OsmoRTP socket * * The bound RTP socket will wait for incoming RTP packets and as soon as it * sees one, will 'connect' to it, so all replies will go to that sources and * incoming messages from other sources will be discarded. This obviously only * works once. * * \returns 0 on success, <0 in case of error. */ int osmo_rtp_socket_autoconnect(struct osmo_rtp_socket *rs) { rtp_session_set_symmetric_rtp(rs->sess, 1); rtp_session_set_connected_mode(rs->sess, 1); rs->flags &= ~OSMO_RTP_F_DISABLED; if (rs->flags & OSMO_RTP_F_POLL) return 0; else return osmo_rtp_socket_fdreg(rs); } /*! \brief Increment timestamp on a RTP socket without sending any packet * \param[in] rs OsmoRTP socket * \param[in] duration duration in number of RTP clock ticks * * Useful to keep the RTP internal clock up to date if an RTP frame should be * send at a given time but no audio content is available. When next packet is * sent, the receiver will see a different increase on the sequence number and * the timestamp, and it should then take it as a synchronization point. For * that same reason, it is advisable to enable the marker bit on the next RTP * packet to be sent after calling this function. * * \returns 0 on success, <0 in case of error. */ int osmo_rtp_skipped_frame(struct osmo_rtp_socket *rs, unsigned int duration) { if (rs->flags & OSMO_RTP_F_DISABLED) return 0; rs->tx_timestamp += duration; return 0; } /*! \brief Send one RTP frame via a RTP socket * \param[in] rs OsmoRTP socket * \param[in] payload pointer to buffer with RTP payload data * \param[in] payload_len length of \a payload in bytes * \param[in] duration duration in number of RTP clock ticks * \returns 0 on success, <0 in case of error. */ int osmo_rtp_send_frame(struct osmo_rtp_socket *rs, const uint8_t *payload, unsigned int payload_len, unsigned int duration) { return osmo_rtp_send_frame_ext(rs, payload, payload_len, duration, false); } /*! \brief Send one RTP frame via a RTP socket * \param[in] rs OsmoRTP socket * \param[in] payload pointer to buffer with RTP payload data * \param[in] payload_len length of \a payload in bytes * \param[in] duration duration in number of RTP clock ticks * \param[in] marker the status of Marker bit in RTP header * \returns 0 on success, <0 in case of error. */ int osmo_rtp_send_frame_ext(struct osmo_rtp_socket *rs, const uint8_t *payload, unsigned int payload_len, unsigned int duration, bool marker) { mblk_t *mblk; int rc; if (rs->flags & OSMO_RTP_F_DISABLED) return 0; mblk = rtp_session_create_packet(rs->sess, RTP_FIXED_HEADER_SIZE, payload, payload_len); if (!mblk) return -ENOMEM; rtp_set_markbit(mblk, marker); rc = rtp_session_sendm_with_ts(rs->sess, mblk, rs->tx_timestamp); rs->tx_timestamp += duration; if (rc < 0) { /* no need to free() the mblk, as rtp_session_rtp_send() * unconditionally free()s the mblk even in case of * error */ return rc; } return rc; } /*! \brief Set the payload type of a RTP socket * \param[in] rs OsmoRTP socket * \param[in] payload_type RTP payload type * \returns 0 on success, < 0 otherwise */ int osmo_rtp_socket_set_pt(struct osmo_rtp_socket *rs, int payload_type) { int rc; rc = rtp_session_set_payload_type(rs->sess, payload_type); //rtp_session_set_rtcp_report_interval(rs->sess, 5*1000); return rc; } /*! \brief Set the DSCP (Differentiated Services Code Point) for outgoing RTP packets * \param[in] rs OsmoRTP socket * \param[in] dscp DSCP value * \returns 0 on success, < 0 otherwise */ int osmo_rtp_socket_set_dscp(struct osmo_rtp_socket *rs, int dscp) { return rtp_session_set_dscp(rs->sess, dscp); } /*! \brief completely close the RTP socket and release all resources * \param[in] rs OsmoRTP socket to be released * \returns 0 on success */ int osmo_rtp_socket_free(struct osmo_rtp_socket *rs) { if (rs->rtp_bfd.list.next && rs->rtp_bfd.list.next != LLIST_POISON1) osmo_fd_unregister(&rs->rtp_bfd); if (rs->rtcp_bfd.list.next && rs->rtcp_bfd.list.next != LLIST_POISON1) osmo_fd_unregister(&rs->rtcp_bfd); if (rs->sess) { rtp_session_release_sockets(rs->sess); rtp_session_destroy(rs->sess); rs->sess = NULL; } talloc_free(rs); return 0; } /*! \brief obtain the locally bound IPv4 address and UDP port * \param[in] rs OsmoRTP socket * \param[out] ip Pointer to caller-allocated uint32_t for IPv4 address * \oaram[out] port Pointer to caller-allocated int for UDP port number * \returns 0 on success, <0 on error, -EIO in case of IPv6 socket */ int osmo_rtp_get_bound_ip_port(struct osmo_rtp_socket *rs, uint32_t *ip, int *port) { int rc; struct sockaddr_storage ss; struct sockaddr_in *sin = (struct sockaddr_in *) &ss; socklen_t alen = sizeof(ss); rc = getsockname(rs->rtp_bfd.fd, (struct sockaddr *)&ss, &alen); if (rc < 0) return rc; if (ss.ss_family != AF_INET) return -EIO; *ip = ntohl(sin->sin_addr.s_addr); *port = rtp_session_get_local_port(rs->sess); return 0; } /*! \brief obtain the locally bound address and port * \param[in] rs OsmoRTP socket * \param[out] addr caller-allocated char ** to which the string pointer for * the address is stored * \param[out] port caller-allocated int * to which the port number is * stored * \returns 0 on success, <0 in case of error */ int osmo_rtp_get_bound_addr(struct osmo_rtp_socket *rs, const char **addr, int *port) { int rc; struct sockaddr_storage ss; socklen_t alen = sizeof(ss); static char hostbuf[256]; memset(hostbuf, 0, sizeof(hostbuf)); rc = getsockname(rs->rtp_bfd.fd, (struct sockaddr *)&ss, &alen); if (rc < 0) return rc; rc = getnameinfo((struct sockaddr *)&ss, alen, hostbuf, sizeof(hostbuf), NULL, 0, NI_NUMERICHOST); if (rc < 0) return rc; *port = rtp_session_get_local_port(rs->sess); *addr = hostbuf; return 0; } void osmo_rtp_socket_log_stats(struct osmo_rtp_socket *rs, int subsys, int level, const char *pfx) { const rtp_stats_t *stats; stats = rtp_session_get_stats(rs->sess); if (!stats) return; LOGP(subsys, level, "%sRTP Tx(%"PRIu64" pkts, %"PRIu64" bytes) " "Rx(%"PRIu64" pkts, %"PRIu64" bytes, %"PRIu64" late, " "%"PRIu64" loss, %"PRIu64" qmax)\n", pfx, stats->packet_sent, stats->sent, stats->packet_recv, stats->hw_recv, stats->outoftime, stats->cum_packet_loss, stats->discarded); } void osmo_rtp_socket_stats(struct osmo_rtp_socket *rs, uint32_t *sent_packets, uint32_t *sent_octets, uint32_t *recv_packets, uint32_t *recv_octets, uint32_t *recv_lost, uint32_t *last_jitter) { const rtp_stats_t *stats; *sent_packets = *sent_octets = *recv_packets = *recv_octets = 0; *recv_lost = *last_jitter = 0; stats = rtp_session_get_stats(rs->sess); if (stats) { /* truncate from 64bit to 32bit here */ *sent_packets = stats->packet_sent; *sent_octets = stats->sent; *recv_packets = stats->packet_recv; *recv_octets = stats->recv; *recv_lost = stats->cum_packet_loss; } const jitter_stats_t *jitter; jitter = rtp_session_get_jitter_stats(rs->sess); if (jitter) *last_jitter = jitter->jitter; } void osmo_rtp_set_source_desc(struct osmo_rtp_socket *rs, const char *cname, const char *name, const char *email, const char *phone, const char *loc, const char *tool, const char *note) { rtp_session_set_source_description(rs->sess, cname, name, email, phone, loc, tool, note); }