#include "ssrc.h" #include #include #include "helpers.h" #include "call.h" #include "rtplib.h" #include "codeclib.h" #include "bufferpool.h" typedef void mos_calc_fn(struct ssrc_stats_block *ssb); static mos_calc_fn mos_calc_legacy; #ifdef WITH_TRANSCODING static mos_calc_fn mos_calc_nb; static mos_calc_fn mos_calc_fb; static mos_calc_fn *mos_calcs[__MOS_TYPES] = { [MOS_NB] = mos_calc_nb, [MOS_FB] = mos_calc_fb, [MOS_LEGACY] = mos_calc_legacy, }; #endif static void __free_ssrc_entry_call(struct ssrc_entry_call *e); static void init_ssrc_entry(struct ssrc_entry *ent, uint32_t ssrc) { ent->ssrc = ssrc; mutex_init(&ent->lock); ent->link.data = ent; } static struct ssrc_entry *create_ssrc_entry_call(void *uptr) { struct ssrc_entry_call *ent; ent = obj_alloc0(struct ssrc_entry_call, __free_ssrc_entry_call); payload_tracker_init(&ent->tracker); while (!ent->ssrc_map_out) ent->ssrc_map_out = ssl_random(); ent->seq_out = ssl_random(); atomic64_set_na(&ent->last_sample, rtpe_now); ent->stats = bufferpool_alloc0(shm_bufferpool, sizeof(*ent->stats)); //ent->seq_out = ssl_random(); //ent->ts_out = ssl_random(); ent->lost_bits = -1; return &ent->h; } static void add_ssrc_entry(uint32_t ssrc, struct ssrc_entry *ent, struct ssrc_hash *ht) { init_ssrc_entry(ent, ssrc); g_queue_push_head_link(&ht->nq, &ent->link); obj_hold(ent); // queue entry } static void free_sender_report(struct ssrc_sender_report_item *i) { g_free(i); } static void free_rr_time(struct ssrc_rr_time_item *i) { g_free(i); } static void free_stats_block(struct ssrc_stats_block *ssb) { g_free(ssb); } static void __free_ssrc_entry_call(struct ssrc_entry_call *e) { g_queue_clear_full(&e->sender_reports, (GDestroyNotify) free_sender_report); g_queue_clear_full(&e->rr_time_reports, (GDestroyNotify) free_rr_time); g_queue_clear_full(&e->stats_blocks, (GDestroyNotify) free_stats_block); if (e->sequencers) g_hash_table_destroy(e->sequencers); bufferpool_unref(e->stats); } static void ssrc_entry_put(void *ep) { struct ssrc_entry_call *e = ep; obj_put(&e->h); } // returned as mos * 10 (i.e. 10 - 50 for 1.0 to 5.0) static int64_t mos_from_rx(int64_t Rx) { // Rx in e5 int64_t intmos; if (Rx < 0) intmos = 10; // e1 else if (Rx > 10000000) // e5 intmos = 45; // e1 else { Rx /= 100; // e5 -> e3 intmos = 100; // e2 intmos += 35 * Rx / 10000; // e2 int64_t RxRx = (Rx - 60000) * (100000 - Rx); // e6 RxRx /= 1000; // e6 -> e3 RxRx = Rx * RxRx; // e6 RxRx /= 1000; // e6 -> e3 RxRx *= 7; // e9 RxRx /= 10000000; // e9 -> e2 intmos += RxRx; // e2 intmos /= 10; // e2 -> e1 if (intmos < 10) intmos = 10; } return intmos; } #ifdef WITH_TRANSCODING static void mos_calc_nb(struct ssrc_stats_block *ssb) { uint64_t rtt = ssb->rtt; if (rtpe_config.mos == MOS_CQ && !rtt) return; // can not compute the MOS-CQ unless we have a valid RTT else if (rtpe_config.mos == MOS_LQ) rtt = 0; // ignore RTT // G.107 simplified, original formula in milliseconds (e0) rtt /= 2; rtt += ssb->jitter * 1000; // ms -> us, e0 -> e3 uint64_t Id = (24 * rtt) / 1000; // e3 if (rtt > 177300) Id += ((rtt - 177300) * 11) / 100; // e3 uint64_t r_factor = 0; if (ssb->packetloss <= 93) r_factor = 9320 - ssb->packetloss * 100; // e2 int64_t Rx = 18 * r_factor * r_factor; // e6 Rx /= 10; // e6 -> e5 Rx -= 279 * r_factor * 100; // e5 Rx += 112662000; // e5 Rx -= Id * 100; // e5 ssb->mos = mos_from_rx(Rx); } static void mos_calc_fb(struct ssrc_stats_block *ssb) { double rtt; if (rtpe_config.mos == MOS_CQ && !ssb->rtt) return; // can not compute the MOS-CQ unless we have a valid RTT else if (rtpe_config.mos == MOS_LQ) rtt = 0; // ignore RTT else rtt = ((double) ssb->rtt) / 1000. / 2.; // G.107.2 rtt += ssb->jitter; double Ppl = ssb->packetloss; double Iee = 10.2 + (132. - 10.2) * (Ppl / (Ppl + 4.3)); double Id; if (rtt <= 100) Id = 0; else { // x = (Math.log(Ta) - Math.log(100)) / Math.log(2) // = Math.log2(Ta / 100) // = Math.log2(Ta) - Math.log2(100) double x = log2(rtt) - log2(100); Id = 1.48 * 25 * (pow(1 + pow(x, 6), 1./6.) - 3 * pow(1 + pow(x / 3, 6), 1./6.) + 2); } static const double Ro = 148; static const double Is = 0; static const double A = 0; double Rx = Ro - Is - Id - Iee + A; ssb->mos = mos_from_rx(Rx / 1.48 * 100000); } #endif // returned as mos * 10 (i.e. 10 - 50 for 1.0 to 5.0) static void mos_calc_legacy(struct ssrc_stats_block *ssb) { uint64_t rtt = ssb->rtt; if (rtpe_config.mos == MOS_CQ && !rtt) return; // can not compute the MOS-CQ unless we have a valid RTT else if (rtpe_config.mos == MOS_LQ) rtt = 0; // ignore RTT // as per https://www.pingman.com/kb/article/how-is-mos-calculated-in-pingplotter-pro-50.html uint64_t eff_rtt = ssb->rtt / 1000 + ssb->jitter * 2 + 10; int64_t r; // e6 if (eff_rtt < 160) r = 93200000 - eff_rtt * 100000 / 4; else r = 93200000 - (eff_rtt * 100000 - 12000000); r = r - (ssb->packetloss * 2500000); ssb->mos = mos_from_rx(r / 10); // e5 } static void *find_ssrc(uint32_t ssrc, struct ssrc_hash *ht, unsigned int *iters) { LOCK(&ht->lock); for (GList *l = ht->nq.head; l; l = l->next) { struct ssrc_entry *ret = l->data; if (ret->ssrc == ssrc) { obj_hold(ret); // move to front g_queue_unlink(&ht->nq, &ret->link); g_queue_push_head_link(&ht->nq, &ret->link); return ret; } } if (iters) *iters = ht->iters; return NULL; } // returns a new reference void *get_ssrc_full(uint32_t ssrc, struct ssrc_hash *ht, bool *created) { struct ssrc_entry *ent; if (!ht->create_func) return NULL; while (true) { unsigned int iters; ent = find_ssrc(ssrc, ht, &iters); if (G_LIKELY(ent)) { if (created) *created = false; return ent; } // use precreated entry if possible ent = atomic_get_na(&ht->precreat); while (1) { if (!ent) break; // create one ourselves if (atomic_compare_exchange(&ht->precreat, &ent, NULL)) break; // something got in the way - retry } if (G_UNLIKELY(!ent)) ent = ht->create_func(ht->uptr); if (G_UNLIKELY(!ent)) return NULL; LOCK(&ht->lock); while (G_UNLIKELY(ht->nq.length > MAX_SSRC_ENTRIES)) { GList *link = g_queue_pop_tail_link(&ht->nq); struct ssrc_entry *old_ent = link->data; ilog(LOG_DEBUG, "SSRC hash table exceeded size limit (trying to add %s%x%s) - " "deleting SSRC %s%x%s", FMT_M(ssrc), FMT_M(old_ent->ssrc)); obj_put(old_ent); // for the queue entry } if (ht->iters != iters) { // preempted, something else created an entry // return created entry if slot is still empty struct ssrc_entry *null_entry = NULL; if (!atomic_compare_exchange(&ht->precreat, &null_entry, ent)) obj_put(ent); continue; } add_ssrc_entry(ssrc, ent, ht); if (created) *created = true; return ent; } } void ssrc_hash_destroy(struct ssrc_hash *ht) { for (GList *l = ht->nq.head; l;) { GList *next = l->next; ssrc_entry_put(l->data); l = next; } if (ht->precreat) obj_put((struct ssrc_entry *) ht->precreat); ht->precreat = NULL; g_queue_init(&ht->nq); ht->create_func = NULL; mutex_destroy(&ht->lock); } void ssrc_hash_foreach(struct ssrc_hash *sh, void (*f)(void *, void *), void *ptr) { if (!sh) return; LOCK(&sh->lock); for (GList *k = sh->nq.head; k; k = k->next) f(k->data, ptr); if (sh->precreat) f(sh->precreat, ptr); } void ssrc_hash_full_fast_init(struct ssrc_hash *sh, ssrc_create_func_t cfunc, void *uptr) { mutex_init(&sh->lock); sh->create_func = cfunc; sh->uptr = uptr; } void ssrc_hash_full_init(struct ssrc_hash *sh, ssrc_create_func_t cfunc, void *uptr) { ssrc_hash_destroy(sh); ssrc_hash_full_fast_init(sh, cfunc, uptr); sh->precreat = cfunc(uptr); // because object creation might be slow } void ssrc_hash_call_init(struct ssrc_hash *sh) { ssrc_hash_full_init(sh, create_ssrc_entry_call, NULL); } static void *__do_time_report_item(struct call_media *m, size_t struct_size, size_t reports_queue_offset, int64_t tv, uint32_t ssrc, uint32_t ntp_msw, uint32_t ntp_lsw, GDestroyNotify free_func, struct ssrc_entry **e_p) { struct ssrc_entry *e; struct ssrc_time_item *sti; sti = g_malloc0(struct_size); sti->received = tv; sti->ntp_middle_bits = ntp_msw << 16 | ntp_lsw >> 16; sti->ntp_ts_lsw = ntp_lsw; sti->ntp_ts_msw = ntp_msw; e = get_ssrc(ssrc, &m->ssrc_hash_in); if (G_UNLIKELY(!e)) { free_func(sti); return NULL; } mutex_lock(&e->lock); GQueue *q = (((void *) e) + reports_queue_offset); g_queue_push_tail(q, sti); while (q->length > 10) free_func(g_queue_pop_head(q)); *e_p = e; return sti; } // call must be locked in R static struct ssrc_entry_call *hunt_ssrc(struct call_media *media, uint32_t ssrc) { if (!media) return NULL; for (__auto_type sub = media->media_subscriptions.head; sub; sub = sub->next) { struct media_subscription * ms = sub->data; struct ssrc_entry_call *e = find_ssrc(ssrc, &ms->media->ssrc_hash_in, NULL); if (e) return e; } return NULL; } #define calc_rtt(m, ...) \ __calc_rtt(m, (struct crtt_args) {__VA_ARGS__}) static int64_t __calc_rtt(struct call_media *m, struct crtt_args a) { if (a.pt_p) *a.pt_p = -1; if (!a.ntp_middle_bits || !a.delay) return 0; struct ssrc_entry_call *e = find_ssrc(a.ssrc, a.ht, NULL); if (G_UNLIKELY(!e)) return 0; if (a.pt_p) *a.pt_p = e->tracker.most[0] == 255 ? -1 : e->tracker.most[0]; // grab the opposite side SSRC for the time reports uint32_t map_ssrc = e->ssrc_map_out; if (!map_ssrc) map_ssrc = e->h.ssrc; obj_put(&e->h); e = hunt_ssrc(m, map_ssrc); if (G_UNLIKELY(!e)) return 0; struct ssrc_time_item *sti; GQueue *q = (((void *) e) + a.reports_queue_offset); mutex_lock(&e->h.lock); // go through the list backwards until we find the SR referenced for (GList *l = q->tail; l; l = l->prev) { sti = l->data; if (sti->ntp_middle_bits != a.ntp_middle_bits) continue; goto found; } // not found mutex_unlock(&e->h.lock); obj_put(&e->h); return 0; found:; // `e` remains locked for access to `sti` int64_t rtt = a.tv - sti->received; mutex_unlock(&e->h.lock); rtt -= (int64_t) a.delay * 1000000LL / 65536LL; ilog(LOG_DEBUG, "Calculated round-trip time for %s%x%s is %" PRId64 " us", FMT_M(a.ssrc), rtt); if (rtt <= 0 || rtt > 10000000) { ilog(LOG_DEBUG, "Invalid RTT - discarding"); obj_put(&e->h); return 0; } e->last_rtt = rtt; obj_put(&e->h); return rtt; } void ssrc_sender_report(struct call_media *m, const struct ssrc_sender_report *sr, int64_t tv) { struct ssrc_entry *e; struct ssrc_sender_report_item *seri = __do_time_report_item(m, sizeof(*seri), G_STRUCT_OFFSET(struct ssrc_entry_call, sender_reports), tv, sr->ssrc, sr->ntp_msw, sr->ntp_lsw, (GDestroyNotify) free_sender_report, &e); if (!seri) return; seri->report = *sr; ilog(LOG_DEBUG, "SR from %s%x%s: RTP TS %u PC %u OC %u NTP TS %u/%u=%f", FMT_M(sr->ssrc), sr->timestamp, sr->packet_count, sr->octet_count, sr->ntp_msw, sr->ntp_lsw, ntp_ts_to_double(seri->time_item.ntp_ts_msw, seri->time_item.ntp_ts_lsw)); mutex_unlock(&e->lock); obj_put(e); } void ssrc_receiver_report(struct call_media *m, stream_fd *sfd, const struct ssrc_receiver_report *rr, int64_t tv) { ilog(LOG_DEBUG, "RR from %s%x%s about %s%x%s: FL %u TL %u HSR %u J %u LSR %u DLSR %u", FMT_M(rr->from), FMT_M(rr->ssrc), rr->fraction_lost, rr->packets_lost, rr->high_seq_received, rr->jitter, rr->lsr, rr->dlsr); int pt; int64_t rtt = calc_rtt(m, .ht = &m->ssrc_hash_out, .tv = tv, .pt_p = &pt, .ssrc = rr->ssrc, .ntp_middle_bits = rr->lsr, .delay = rr->dlsr, .reports_queue_offset = G_STRUCT_OFFSET(struct ssrc_entry_call, sender_reports)); struct ssrc_entry_call *other_e = get_ssrc(rr->from, &m->ssrc_hash_in); if (G_UNLIKELY(!other_e)) goto out_nl; // determine the clock rate for jitter values if (pt < 0) { ilog(LOG_DEBUG, "No payload type known for RTCP RR, discarding"); goto out_nl_put; } const rtp_payload_type *rpt = get_rtp_payload_type(pt, &m->codecs); if (!rpt) { ilog(LOG_INFO, "Invalid RTP payload type %i, discarding RTCP RR", pt); goto out_nl_put; } unsigned int jitter = rpt->clock_rate ? (rr->jitter * 1000 / rpt->clock_rate) : rr->jitter; ilog(LOG_DEBUG, "Calculated jitter for %s%x%s is %u ms", FMT_M(rr->ssrc), jitter); ilog(LOG_DEBUG, "Adding opposide side RTT of %u us", other_e->last_rtt); int64_t rtt_end2end = other_e->last_rtt ? (rtt + other_e->last_rtt) : 0; if (other_e->last_rtt_xr > 0) { // use the RTT from RTCP-XR (in ms) rtt_end2end = (int64_t) other_e->last_rtt_xr * 1000LL; } struct ssrc_stats_block *ssb = g_new(__typeof(*ssb), 1); *ssb = (struct ssrc_stats_block) { .jitter = jitter, .rtt = rtt_end2end, .rtt_leg = rtt, .reported = tv, .packetloss = (unsigned int) rr->fraction_lost * 100 / 256, }; RTPE_SAMPLE_SFD(jitter, jitter, sfd); RTPE_SAMPLE_SFD(rtt_e2e, rtt_end2end, sfd); RTPE_SAMPLE_SFD(rtt_dsct, rtt, sfd); RTPE_SAMPLE_SFD(packetloss, ssb->packetloss, sfd); mos_calc_fn *mos_calc; #ifdef WITH_TRANSCODING mos_calc = mos_calc_nb; if (rpt->codec_def) mos_calc = mos_calcs[rpt->codec_def->mos_type]; #else mos_calc = mos_calc_legacy; #endif other_e->packets_lost = rr->packets_lost; mos_calc(ssb); if (ssb->mos) { ilog(LOG_DEBUG, "Calculated MOS from RR for %s%x%s is %.1f", FMT_M(rr->from), (double) ssb->mos / 10.0); RTPE_SAMPLE_SFD(mos, ssb->mos, sfd); } // got a new stats block, add it to reporting ssrc mutex_lock(&other_e->h.lock); // discard stats block if last has been received less than a second ago if (G_LIKELY(other_e->stats_blocks.length > 0)) { struct ssrc_stats_block *last_ssb = g_queue_peek_tail(&other_e->stats_blocks); if (G_UNLIKELY(tv - last_ssb->reported < 1000000LL)) { free_stats_block(ssb); goto out_ul_oe; } } g_queue_push_tail(&other_e->stats_blocks, ssb); if (ssb->mos && ((G_UNLIKELY(!other_e->lowest_mos) || ssb->mos < other_e->lowest_mos->mos))) other_e->lowest_mos = ssb; if (G_UNLIKELY(!other_e->highest_mos) || ssb->mos > other_e->highest_mos->mos) other_e->highest_mos = ssb; // running tally if (!ssb->mos) { // when we do not have the RTT for both legs, we have no MOS other_e->no_mos_count++; } else { other_e->average_mos.jitter += ssb->jitter; other_e->average_mos.mos += ssb->mos; other_e->average_mos.rtt += ssb->rtt; other_e->average_mos.rtt_leg += ssb->rtt_leg; other_e->average_mos.packetloss += ssb->packetloss; } goto out_ul_oe; out_ul_oe: mutex_unlock(&other_e->h.lock); goto out_nl_put; out_nl_put: obj_put(&other_e->h); goto out_nl; out_nl: ; } void ssrc_receiver_rr_time(struct call_media *m, const struct ssrc_xr_rr_time *rr, int64_t tv) { struct ssrc_entry *e; struct ssrc_rr_time_item *srti = __do_time_report_item(m, sizeof(*srti), G_STRUCT_OFFSET(struct ssrc_entry_call, rr_time_reports), tv, rr->ssrc, rr->ntp_msw, rr->ntp_lsw, (GDestroyNotify) free_rr_time, &e); if (!srti) return; ilog(LOG_DEBUG, "XR RR TIME from %s%x%s: NTP TS %u/%u=%f", FMT_M(rr->ssrc), rr->ntp_msw, rr->ntp_lsw, ntp_ts_to_double(srti->time_item.ntp_ts_msw, srti->time_item.ntp_ts_lsw)); mutex_unlock(&e->lock); obj_put(e); } void ssrc_receiver_dlrr(struct call_media *m, const struct ssrc_xr_dlrr *dlrr, int64_t tv) { ilog(LOG_DEBUG, "XR DLRR from %s%x%s about %s%x%s: LRR %u DLRR %u", FMT_M(dlrr->from), FMT_M(dlrr->ssrc), dlrr->lrr, dlrr->dlrr); calc_rtt(m, .ht = &m->ssrc_hash_out, .tv = tv, .pt_p = NULL, .ssrc = dlrr->ssrc, .ntp_middle_bits = dlrr->lrr, .delay = dlrr->dlrr, .reports_queue_offset = G_STRUCT_OFFSET(struct ssrc_entry_call, rr_time_reports)); } void ssrc_voip_metrics(struct call_media *m, const struct ssrc_xr_voip_metrics *vm, int64_t tv) { ilog(LOG_DEBUG, "XR VM from %s%x%s about %s%x%s: LR %u DR %u BD %u GD %u BDu %u GDu %u RTD %u " "ESD %u SL %u NL %u RERL %u GMin %u R %u eR %u MOSL %u MOSC %u RX %u " "JBn %u JBm %u JBam %u", FMT_M(vm->from), FMT_M(vm->ssrc), vm->loss_rate, vm->discard_rate, vm->burst_den, vm->gap_den, vm->burst_dur, vm->gap_dur, vm->rnd_trip_delay, vm->end_sys_delay, vm->signal_lvl, vm->noise_lvl, vm->rerl, vm->gmin, vm->r_factor, vm->ext_r_factor, vm->mos_lq, vm->mos_cq, vm->rx_config, vm->jb_nom, vm->jb_max, vm->jb_abs_max); struct ssrc_entry_call *e = get_ssrc(vm->ssrc, &m->ssrc_hash_in); if (!e) return; e->last_rtt_xr = vm->rnd_trip_delay; obj_put(&e->h); } static void __pt_sort(struct payload_tracker *t, int pt) { // bubble up? while (t->idx[pt] > 0) { int this_idx = t->idx[pt]; int prev_idx = this_idx - 1; int prev_pt = t->most[prev_idx]; if (G_LIKELY(t->count[prev_pt] >= t->count[pt])) break; // bubble up! ilog(LOG_DEBUG, "bubble up pt %i from idx %u to %u", pt, this_idx, prev_idx); // swap entries in "most" list int prev = t->most[prev_idx]; t->most[prev_idx] = t->most[this_idx]; t->most[this_idx] = prev; // adjust indexes t->idx[pt]--; t->idx[prev_pt]++; } // bubble down? while (t->idx[pt] < t->most_len - 1) { int this_idx = t->idx[pt]; int next_idx = this_idx + 1; int next_pt = t->most[next_idx]; if (G_LIKELY(t->count[next_pt] <= t->count[pt])) break; // bubble down! ilog(LOG_DEBUG, "bubble down pt %i from idx %u to %u", pt, this_idx, next_idx); // swap entries in "most" list int next = t->most[next_idx]; t->most[next_idx] = t->most[this_idx]; t->most[this_idx] = next; // adjust indexes t->idx[pt]++; t->idx[next_pt]--; } } void payload_tracker_init(struct payload_tracker *t) { mutex_init(&t->lock); memset(&t->last, -1, sizeof(t->last)); memset(&t->count, 0, sizeof(t->count)); memset(&t->idx, -1, sizeof(t->idx)); memset(&t->most, -1, sizeof(t->most)); memset(&t->last_pts, 255, sizeof(t->last_pts)); t->last_idx = 0; t->most_len = 0; t->last_pt_idx = -1; } //#define PT_DBG(x...) ilog(LOG_DEBUG, x) #define PT_DBG(x...) ((void)0) void payload_tracker_add(struct payload_tracker *t, int pt) { if (G_UNLIKELY(pt < 0) || G_UNLIKELY(pt >= 128)) return; mutex_lock(&t->lock); int pt_idx = (t->last_pt_idx + 1) % G_N_ELEMENTS(t->last_pts); t->last_pts[pt_idx] = pt; t->last_pt_idx = pt_idx; PT_DBG("new pt: %i", pt); PT_DBG("last idx: %u", t->last_idx); int old_pt = t->last[t->last_idx]; PT_DBG("old pt: %u", old_pt); if (G_LIKELY(old_pt != 255)) { // overwriting old entry. is it the same as the new one? if (G_LIKELY(old_pt == pt)) { PT_DBG("old pt == new pt"); // no change goto out; } PT_DBG("decreasing old pt count from %u", t->count[old_pt]); // different: decrease old counter t->count[old_pt]--; } // fill in new entry t->last[t->last_idx] = pt; // increase new counter PT_DBG("increasing new pt count from %u", t->count[pt]); t->count[pt]++; // is this a new entry? if (G_UNLIKELY(t->idx[pt] == 255)) { // put to the end of the "most" list PT_DBG("inserting new entry at pos %u", t->most_len); t->idx[pt] = t->most_len; t->most[t->most_len] = pt; t->most_len++; } // now bubble sort both new and old entries __pt_sort(t, pt); if (G_LIKELY(old_pt != 255)) __pt_sort(t, old_pt); out: if (++t->last_idx >= G_N_ELEMENTS(t->last)) t->last_idx = 0; mutex_unlock(&t->lock); } // call master lock held in R void ssrc_collect_metrics(struct call_media *media) { for (GList *l = media->ssrc_hash_in.nq.head; l; l = l->next) { struct ssrc_entry_call *s = l->data; if (!s) break; // end of list // exclude zero values - technically possible but unlikely and probably just unset if (!s->jitter) continue; if (s->tracker.most_len > 0 && s->tracker.most[0] != 255) { const rtp_payload_type *rpt = get_rtp_payload_type(s->tracker.most[0], &media->codecs); if (rpt && rpt->clock_rate) s->jitter = s->jitter * 1000 / rpt->clock_rate; } if (media->streams.head) RTPE_SAMPLE_SFD(jitter_measured, s->jitter, media->streams.head->data->selected_sfd); } }