1 /* rtp_stream_dialog.cpp
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
7 * SPDX-License-Identifier: GPL-2.0-or-later*/
9 #include "rtp_stream_dialog.h"
10 #include <ui_rtp_stream_dialog.h>
14 #include "epan/addr_resolv.h"
15 #include <epan/rtp_pt.h>
17 #include <wsutil/utf8_entities.h>
19 #include <ui/qt/utils/qt_ui_utils.h>
20 #include "rtp_analysis_dialog.h"
21 #include "wireshark_application.h"
25 #include <QFileDialog>
27 #include <QPushButton>
28 #include <QTextStream>
29 #include <QTreeWidgetItem>
30 #include <QTreeWidgetItemIterator>
32 #include <ui/qt/utils/tango_colors.h>
35 * @file RTP stream dialog
37 * Displays a list of RTP streams with the following information:
41 * - Stats: Packets, lost, max delta, max jitter, mean jitter
44 * Finds reverse streams
47 * Go to the setup frame
49 * Copy As CSV and YAML
54 // - Add more statistics to the hint text (e.g. lost packets).
55 // - Add more statistics to the main list (e.g. stream duration)
57 const int src_addr_col_ = 0;
58 const int src_port_col_ = 1;
59 const int dst_addr_col_ = 2;
60 const int dst_port_col_ = 3;
61 const int ssrc_col_ = 4;
62 const int payload_col_ = 5;
63 const int packets_col_ = 6;
64 const int lost_col_ = 7;
65 const int max_delta_col_ = 8;
66 const int max_jitter_col_ = 9;
67 const int mean_jitter_col_ = 10;
68 const int status_col_ = 11;
70 enum { rtp_stream_type_ = 1000 };
72 class RtpStreamTreeWidgetItem : public QTreeWidgetItem
75 RtpStreamTreeWidgetItem(QTreeWidget *tree, rtp_stream_info_t *stream_info) :
76 QTreeWidgetItem(tree, rtp_stream_type_),
77 stream_info_(stream_info)
82 rtp_stream_info_t *streamInfo() const { return stream_info_; }
88 setText(src_addr_col_, address_to_display_qstring(&stream_info_->src_addr));
89 setText(src_port_col_, QString::number(stream_info_->src_port));
90 setText(dst_addr_col_, address_to_display_qstring(&stream_info_->dest_addr));
91 setText(dst_port_col_, QString::number(stream_info_->dest_port));
92 setText(ssrc_col_, QString("0x%1").arg(stream_info_->ssrc, 0, 16));
94 if (stream_info_->payload_type_name != NULL) {
95 setText(payload_col_, stream_info_->payload_type_name);
97 setText(payload_col_, val_ext_to_qstring(stream_info_->payload_type,
98 &rtp_payload_type_short_vals_ext,
102 setText(packets_col_, QString::number(stream_info_->packet_count));
106 expected = (stream_info_->rtp_stats.stop_seq_nr + stream_info_->rtp_stats.cycles*65536)
107 - stream_info_->rtp_stats.start_seq_nr + 1;
108 lost_ = expected - stream_info_->rtp_stats.total_nr;
110 pct_loss = (double)(lost_*100.0)/(double)expected;
115 setText(lost_col_, QObject::tr("%1 (%L2%)").arg(lost_).arg(QString::number(pct_loss, 'f', 1)));
116 setText(max_delta_col_, QString::number(stream_info_->rtp_stats.max_delta, 'f', 3)); // This is RTP. Do we need nanoseconds?
117 setText(max_jitter_col_, QString::number(stream_info_->rtp_stats.max_jitter, 'f', 3));
118 setText(mean_jitter_col_, QString::number(stream_info_->rtp_stats.mean_jitter, 'f', 3));
120 if (stream_info_->problem) {
121 setText(status_col_, UTF8_BULLET);
122 setTextAlignment(status_col_, Qt::AlignCenter);
123 for (int i = 0; i < columnCount(); i++) {
124 setBackgroundColor(i, ws_css_warn_background);
125 setTextColor(i, ws_css_warn_text);
129 // Return a QString, int, double, or invalid QVariant representing the raw column data.
130 QVariant colData(int col) const {
138 case payload_col_: // XXX Return numeric value?
141 return stream_info_->src_port;
143 return stream_info_->dest_port;
145 return stream_info_->ssrc;
147 return stream_info_->packet_count;
151 return stream_info_->rtp_stats.max_delta;
152 case max_jitter_col_:
153 return stream_info_->rtp_stats.max_jitter;
154 case mean_jitter_col_:
155 return stream_info_->rtp_stats.mean_jitter;
157 return stream_info_->problem ? "Problem" : "";
164 bool operator< (const QTreeWidgetItem &other) const
166 if (other.type() != rtp_stream_type_) return QTreeWidgetItem::operator <(other);
167 const RtpStreamTreeWidgetItem &other_rstwi = dynamic_cast<const RtpStreamTreeWidgetItem&>(other);
169 switch (treeWidget()->sortColumn()) {
171 return cmp_address(&(stream_info_->src_addr), &(other_rstwi.stream_info_->src_addr)) < 0;
173 return stream_info_->src_port < other_rstwi.stream_info_->src_port;
175 return cmp_address(&(stream_info_->dest_addr), &(other_rstwi.stream_info_->dest_addr)) < 0;
177 return stream_info_->dest_port < other_rstwi.stream_info_->dest_port;
179 return stream_info_->ssrc < other_rstwi.stream_info_->ssrc;
181 return stream_info_->payload_type < other_rstwi.stream_info_->payload_type; // XXX Compare payload_type_name instead?
183 return stream_info_->packet_count < other_rstwi.stream_info_->packet_count;
185 return lost_ < other_rstwi.lost_;
187 return stream_info_->rtp_stats.max_delta < other_rstwi.stream_info_->rtp_stats.max_delta;
188 case max_jitter_col_:
189 return stream_info_->rtp_stats.max_jitter < other_rstwi.stream_info_->rtp_stats.max_jitter;
190 case mean_jitter_col_:
191 return stream_info_->rtp_stats.mean_jitter < other_rstwi.stream_info_->rtp_stats.mean_jitter;
196 // Fall back to string comparison
197 return QTreeWidgetItem::operator <(other);
201 rtp_stream_info_t *stream_info_;
205 RtpStreamDialog::RtpStreamDialog(QWidget &parent, CaptureFile &cf) :
206 WiresharkDialog(parent, cf),
207 ui(new Ui::RtpStreamDialog),
211 loadGeometry(parent.width() * 4 / 5, parent.height() * 2 / 3);
212 setWindowSubtitle(tr("RTP Streams"));
213 ui->streamTreeWidget->installEventFilter(this);
215 ctx_menu_.addAction(ui->actionSelectNone);
216 ctx_menu_.addAction(ui->actionFindReverse);
217 ctx_menu_.addAction(ui->actionGoToSetup);
218 ctx_menu_.addAction(ui->actionMarkPackets);
219 ctx_menu_.addAction(ui->actionPrepareFilter);
220 ctx_menu_.addAction(ui->actionExportAsRtpDump);
221 ctx_menu_.addAction(ui->actionCopyAsCsv);
222 ctx_menu_.addAction(ui->actionCopyAsYaml);
223 ctx_menu_.addAction(ui->actionAnalyze);
224 ui->streamTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
225 ui->streamTreeWidget->header()->setSortIndicator(0, Qt::AscendingOrder);
226 connect(ui->streamTreeWidget, SIGNAL(customContextMenuRequested(QPoint)),
227 SLOT(showStreamMenu(QPoint)));
229 // Some GTK+ buttons have been left out intentionally in order to
230 // reduce clutter. Do you have a strong and informed opinion about
231 // this? Perhaps you should volunteer to maintain this code!
232 find_reverse_button_ = ui->buttonBox->addButton(ui->actionFindReverse->text(), QDialogButtonBox::ApplyRole);
233 find_reverse_button_->setToolTip(ui->actionFindReverse->toolTip());
234 prepare_button_ = ui->buttonBox->addButton(ui->actionPrepareFilter->text(), QDialogButtonBox::ApplyRole);
235 prepare_button_->setToolTip(ui->actionPrepareFilter->toolTip());
236 export_button_ = ui->buttonBox->addButton(tr("Export" UTF8_HORIZONTAL_ELLIPSIS), QDialogButtonBox::ApplyRole);
237 export_button_->setToolTip(ui->actionExportAsRtpDump->toolTip());
238 copy_button_ = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ApplyRole);
239 analyze_button_ = ui->buttonBox->addButton(ui->actionAnalyze->text(), QDialogButtonBox::ApplyRole);
240 analyze_button_->setToolTip(ui->actionAnalyze->toolTip());
242 QMenu *copy_menu = new QMenu(copy_button_);
244 ca = copy_menu->addAction(tr("as CSV"));
245 ca->setToolTip(ui->actionCopyAsCsv->toolTip());
246 connect(ca, SIGNAL(triggered()), this, SLOT(on_actionCopyAsCsv_triggered()));
247 ca = copy_menu->addAction(tr("as YAML"));
248 ca->setToolTip(ui->actionCopyAsYaml->toolTip());
249 connect(ca, SIGNAL(triggered()), this, SLOT(on_actionCopyAsYaml_triggered()));
250 copy_button_->setMenu(copy_menu);
252 /* Register the tap listener */
253 memset(&tapinfo_, 0, sizeof(rtpstream_tapinfo_t));
254 tapinfo_.tap_reset = tapReset;
255 tapinfo_.tap_draw = tapDraw;
256 tapinfo_.tap_mark_packet = tapMarkPacket;
257 tapinfo_.tap_data = this;
258 tapinfo_.mode = TAP_ANALYSE;
260 register_tap_listener_rtp_stream(&tapinfo_, NULL);
261 /* Scan for RTP streams (redissect all packets) */
262 rtpstream_scan(&tapinfo_, cf.capFile(), NULL);
267 RtpStreamDialog::~RtpStreamDialog()
270 remove_tap_listener_rtp_stream(&tapinfo_);
273 bool RtpStreamDialog::eventFilter(QObject *, QEvent *event)
275 if (ui->streamTreeWidget->hasFocus() && event->type() == QEvent::KeyPress) {
276 QKeyEvent &keyEvent = static_cast<QKeyEvent&>(*event);
277 switch(keyEvent.key()) {
279 on_actionGoToSetup_triggered();
282 on_actionMarkPackets_triggered();
285 on_actionPrepareFilter_triggered();
288 on_actionFindReverse_triggered();
291 // XXX "Shift+Ctrl+A" is a fairly standard shortcut for "select none".
292 // However, the main window uses this for displaying the profile dialog.
293 // if (keyEvent.modifiers() == (Qt::ControlModifier | Qt::ShiftModifier))
294 // on_actionSelectNone_triggered();
304 void RtpStreamDialog::tapReset(rtpstream_tapinfo_t *tapinfo)
306 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
307 if (rtp_stream_dialog) {
308 /* invalidate items which refer to old strinfo_list items. */
309 rtp_stream_dialog->ui->streamTreeWidget->clear();
313 void RtpStreamDialog::tapDraw(rtpstream_tapinfo_t *tapinfo)
315 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
316 if (rtp_stream_dialog) {
317 rtp_stream_dialog->updateStreams();
321 void RtpStreamDialog::tapMarkPacket(rtpstream_tapinfo_t *tapinfo, frame_data *fd)
323 if (!tapinfo) return;
325 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
326 if (rtp_stream_dialog) {
327 cf_mark_frame(rtp_stream_dialog->cap_file_.capFile(), fd);
328 rtp_stream_dialog->need_redraw_ = true;
332 void RtpStreamDialog::updateStreams()
334 GList *cur_stream = g_list_nth(tapinfo_.strinfo_list, ui->streamTreeWidget->topLevelItemCount());
336 // Add any missing items
337 while (cur_stream && cur_stream->data) {
338 rtp_stream_info_t *stream_info = (rtp_stream_info_t*) cur_stream->data;
339 new RtpStreamTreeWidgetItem(ui->streamTreeWidget, stream_info);
340 cur_stream = g_list_next(cur_stream);
343 // Recalculate values
344 QTreeWidgetItemIterator iter(ui->streamTreeWidget);
346 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
352 for (int i = 0; i < ui->streamTreeWidget->columnCount(); i++) {
353 ui->streamTreeWidget->resizeColumnToContents(i);
356 ui->streamTreeWidget->setSortingEnabled(true);
361 emit packetsMarked();
362 need_redraw_ = false;
366 void RtpStreamDialog::updateWidgets()
368 bool selected = ui->streamTreeWidget->selectedItems().count() > 0;
370 QString hint = "<small><i>";
371 hint += tr("%1 streams").arg(ui->streamTreeWidget->topLevelItemCount());
375 foreach(QTreeWidgetItem *ti, ui->streamTreeWidget->selectedItems()) {
376 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
377 if (rsti->streamInfo()) {
378 tot_packets += rsti->streamInfo()->packet_count;
381 hint += tr(", %1 selected, %2 total packets")
382 .arg(ui->streamTreeWidget->selectedItems().count())
386 hint += ". Right-click for more options.";
387 hint += "</i></small>";
388 ui->hintLabel->setText(hint);
390 bool enable = selected && !file_closed_;
391 bool has_data = ui->streamTreeWidget->topLevelItemCount() > 0;
393 find_reverse_button_->setEnabled(enable);
394 prepare_button_->setEnabled(enable);
395 export_button_->setEnabled(enable);
396 copy_button_->setEnabled(has_data);
397 analyze_button_->setEnabled(selected);
399 ui->actionFindReverse->setEnabled(enable);
400 ui->actionGoToSetup->setEnabled(enable);
401 ui->actionMarkPackets->setEnabled(enable);
402 ui->actionPrepareFilter->setEnabled(enable);
403 ui->actionExportAsRtpDump->setEnabled(enable);
404 ui->actionCopyAsCsv->setEnabled(has_data);
405 ui->actionCopyAsYaml->setEnabled(has_data);
406 ui->actionAnalyze->setEnabled(selected);
408 WiresharkDialog::updateWidgets();
411 QList<QVariant> RtpStreamDialog::streamRowData(int row) const
413 QList<QVariant> row_data;
415 if (row >= ui->streamTreeWidget->topLevelItemCount()) {
419 for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
421 row_data << ui->streamTreeWidget->headerItem()->text(col);
423 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(row));
425 row_data << rsti->colData(col);
432 void RtpStreamDialog::captureFileClosing()
434 remove_tap_listener_rtp_stream(&tapinfo_);
435 WiresharkDialog::captureFileClosing();
438 void RtpStreamDialog::showStreamMenu(QPoint pos)
440 ctx_menu_.popup(ui->streamTreeWidget->viewport()->mapToGlobal(pos));
443 void RtpStreamDialog::on_actionAnalyze_triggered()
445 rtp_stream_info_t *stream_a, *stream_b = NULL;
447 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
448 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
449 stream_a = rsti->streamInfo();
450 if (ui->streamTreeWidget->selectedItems().count() > 1) {
451 ti = ui->streamTreeWidget->selectedItems()[1];
452 rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
453 stream_b = rsti->streamInfo();
456 if (stream_a == NULL && stream_b == NULL) return;
458 RtpAnalysisDialog *rtp_analysis_dialog = new RtpAnalysisDialog(*this, cap_file_, stream_a, stream_b);
459 connect(rtp_analysis_dialog, SIGNAL(goToPacket(int)), this, SIGNAL(goToPacket(int)));
460 rtp_analysis_dialog->show();
463 void RtpStreamDialog::on_actionCopyAsCsv_triggered()
466 QTextStream stream(&csv, QIODevice::Text);
467 for (int row = -1; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
469 foreach (QVariant v, streamRowData(row)) {
472 } else if (v.type() == QVariant::String) {
473 rdsl << QString("\"%1\"").arg(v.toString());
475 rdsl << v.toString();
478 stream << rdsl.join(",") << endl;
480 wsApp->clipboard()->setText(stream.readAll());
483 void RtpStreamDialog::on_actionCopyAsYaml_triggered()
486 QTextStream stream(&yaml, QIODevice::Text);
487 stream << "---" << endl;
488 for (int row = -1; row < ui->streamTreeWidget->topLevelItemCount(); row ++) {
489 stream << "-" << endl;
490 foreach (QVariant v, streamRowData(row)) {
491 stream << " - " << v.toString() << endl;
494 wsApp->clipboard()->setText(stream.readAll());
497 void RtpStreamDialog::on_actionExportAsRtpDump_triggered()
499 if (file_closed_ || ui->streamTreeWidget->selectedItems().count() < 1) return;
501 // XXX If the user selected multiple frames is this the one we actually want?
502 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
503 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
504 rtp_stream_info_t *stream_info = rsti->streamInfo();
507 QDir path(wsApp->lastOpenDir());
508 QString save_file = path.canonicalPath() + "/" + cap_file_.fileTitle();
510 file_name = QFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save RTPDump As" UTF8_HORIZONTAL_ELLIPSIS)),
511 save_file, "RTPDump Format (*.rtpdump)", &extension);
513 if (file_name.length() > 0) {
514 gchar *dest_file = qstring_strdup(file_name);
515 gboolean save_ok = rtpstream_save(&tapinfo_, cap_file_.capFile(), stream_info, dest_file);
517 // else error dialog?
519 path = QDir(file_name);
520 wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData());
527 void RtpStreamDialog::on_actionFindReverse_triggered()
529 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
531 // Gather up our selected streams...
532 QList<rtp_stream_info_t *> selected_streams;
533 foreach(QTreeWidgetItem *ti, ui->streamTreeWidget->selectedItems()) {
534 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
535 rtp_stream_info_t *stream_info = rsti->streamInfo();
537 selected_streams << stream_info;
541 // ...and compare them to our unselected streams.
542 QTreeWidgetItemIterator iter(ui->streamTreeWidget, QTreeWidgetItemIterator::Unselected);
544 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
545 rtp_stream_info_t *stream_info = rsti->streamInfo();
547 foreach (rtp_stream_info_t *fwd_stream, selected_streams) {
548 if (rtp_stream_info_is_reverse(fwd_stream, stream_info)) {
549 (*iter)->setSelected(true);
557 void RtpStreamDialog::on_actionGoToSetup_triggered()
559 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
560 // XXX If the user selected multiple frames is this the one we actually want?
561 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
562 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
563 rtp_stream_info_t *stream_info = rsti->streamInfo();
565 emit goToPacket(stream_info->setup_frame_number);
569 void RtpStreamDialog::on_actionMarkPackets_triggered()
571 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
572 rtp_stream_info_t *stream_a, *stream_b = NULL;
574 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
575 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
576 stream_a = rsti->streamInfo();
577 if (ui->streamTreeWidget->selectedItems().count() > 1) {
578 ti = ui->streamTreeWidget->selectedItems()[1];
579 rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
580 stream_b = rsti->streamInfo();
583 if (stream_a == NULL && stream_b == NULL) return;
585 // XXX Mark the setup frame as well?
586 need_redraw_ = false;
587 rtpstream_mark(&tapinfo_, cap_file_.capFile(), stream_a, stream_b);
591 void RtpStreamDialog::on_actionPrepareFilter_triggered()
593 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
595 // Gather up our selected streams...
596 QStringList stream_filters;
597 foreach(QTreeWidgetItem *ti, ui->streamTreeWidget->selectedItems()) {
598 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
599 rtp_stream_info_t *stream_info = rsti->streamInfo();
601 QString ip_proto = stream_info->src_addr.type == AT_IPv6 ? "ipv6" : "ip";
602 stream_filters << QString("(%1.src==%2 && udp.srcport==%3 && %1.dst==%4 && udp.dstport==%5 && rtp.ssrc==0x%6)")
604 .arg(address_to_qstring(&stream_info->src_addr)) // %2
605 .arg(stream_info->src_port) // %3
606 .arg(address_to_qstring(&stream_info->dest_addr)) // %4
607 .arg(stream_info->dest_port) // %5
608 .arg(stream_info->ssrc, 0, 16);
611 if (stream_filters.length() > 0) {
612 QString filter = stream_filters.join(" || ");
613 remove_tap_listener_rtp_stream(&tapinfo_);
614 emit updateFilter(filter);
618 void RtpStreamDialog::on_actionSelectNone_triggered()
620 ui->streamTreeWidget->clearSelection();
623 void RtpStreamDialog::on_streamTreeWidget_itemSelectionChanged()
628 void RtpStreamDialog::on_buttonBox_clicked(QAbstractButton *button)
630 if (button == find_reverse_button_) {
631 on_actionFindReverse_triggered();
632 } else if (button == prepare_button_) {
633 on_actionPrepareFilter_triggered();
634 } else if (button == export_button_) {
635 on_actionExportAsRtpDump_triggered();
636 } else if (button == analyze_button_) {
637 on_actionAnalyze_triggered();
641 void RtpStreamDialog::on_buttonBox_helpRequested()
643 wsApp->helpTopicAction(HELP_RTP_ANALYSIS_DIALOG);
652 * indent-tabs-mode: nil
655 * ex: set shiftwidth=4 tabstop=8 expandtab:
656 * :indentSize=4:tabSize=8:noTabs=true: