1 /* rtp_stream_dialog.cpp
2 *
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 #include "rtp_stream_dialog.h"
11 #include <ui_rtp_stream_dialog.h>
12
13 #include "file.h"
14
15 #include "epan/addr_resolv.h"
16 #include <epan/rtp_pt.h>
17
18 #include <wsutil/utf8_entities.h>
19
20 #include <ui/qt/utils/qt_ui_utils.h>
21 #include "rtp_analysis_dialog.h"
22 #include "wireshark_application.h"
23 #include "ui/qt/widgets/wireshark_file_dialog.h"
24
25 #include <QAction>
26 #include <QClipboard>
27 #include <QKeyEvent>
28 #include <QPushButton>
29 #include <QTextStream>
30 #include <QTreeWidgetItem>
31 #include <QTreeWidgetItemIterator>
32 #include <QDateTime>
33
34 #include <ui/qt/utils/color_utils.h>
35
36 /*
37 * @file RTP stream dialog
38 *
39 * Displays a list of RTP streams with the following information:
40 * - UDP 4-tuple
41 * - SSRC
42 * - Payload type
43 * - Stats: Packets, lost, max delta, max jitter, mean jitter
44 * - Problems
45 *
46 * Finds reverse streams
47 * "Save As" rtpdump
48 * Mark packets
49 * Go to the setup frame
50 * Prepare filter
51 * Copy As CSV and YAML
52 * Analyze
53 */
54
55 // To do:
56 // - Add more statistics to the hint text (e.g. lost packets).
57 // - Add more statistics to the main list (e.g. stream duration)
58
59 const int src_addr_col_ = 0;
60 const int src_port_col_ = 1;
61 const int dst_addr_col_ = 2;
62 const int dst_port_col_ = 3;
63 const int ssrc_col_ = 4;
64 const int start_time_col_ = 5;
65 const int duration_col_ = 6;
66 const int payload_col_ = 7;
67 const int packets_col_ = 8;
68 const int lost_col_ = 9;
69 const int min_delta_col_ = 10;
70 const int mean_delta_col_ = 11;
71 const int max_delta_col_ = 12;
72 const int min_jitter_col_ = 13;
73 const int mean_jitter_col_ = 14;
74 const int max_jitter_col_ = 15;
75 const int status_col_ = 16;
76 const int ssrc_fmt_col_ = 17;
77 const int lost_perc_col_ = 18;
78
79 enum { rtp_stream_type_ = 1000 };
80
81 bool operator==(rtpstream_id_t const& a, rtpstream_id_t const& b);
82
83 class RtpStreamTreeWidgetItem : public QTreeWidgetItem
84 {
85 public:
RtpStreamTreeWidgetItem(QTreeWidget * tree,rtpstream_info_t * stream_info)86 RtpStreamTreeWidgetItem(QTreeWidget *tree, rtpstream_info_t *stream_info) :
87 QTreeWidgetItem(tree, rtp_stream_type_),
88 stream_info_(stream_info),
89 tod_(0)
90 {
91 drawData();
92 }
93
streamInfo() const94 rtpstream_info_t *streamInfo() const { return stream_info_; }
95
drawData()96 void drawData() {
97 rtpstream_info_calc_t calc;
98
99 if (!stream_info_) {
100 return;
101 }
102 rtpstream_info_calculate(stream_info_, &calc);
103
104 setText(src_addr_col_, calc.src_addr_str);
105 setText(src_port_col_, QString::number(calc.src_port));
106 setText(dst_addr_col_, calc.dst_addr_str);
107 setText(dst_port_col_, QString::number(calc.dst_port));
108 setText(ssrc_col_, QString("0x%1").arg(calc.ssrc, 0, 16));
109 if (tod_) {
110 QDateTime abs_dt = QDateTime::fromMSecsSinceEpoch(nstime_to_msec(&stream_info_->start_fd->abs_ts));
111 setText(start_time_col_, QString("%1")
112 .arg(abs_dt.toString("yyyy-MM-dd hh:mm:ss.zzz")));
113 } else {
114 setText(start_time_col_, QString::number(calc.start_time_ms, 'f', 6));
115 }
116 setText(duration_col_, QString::number(calc.duration_ms, 'f', prefs.gui_decimal_places1));
117 setText(payload_col_, calc.all_payload_type_names);
118 setText(packets_col_, QString::number(calc.packet_count));
119 setText(lost_col_, QObject::tr("%1 (%L2%)").arg(calc.lost_num).arg(QString::number(calc.lost_perc, 'f', 1)));
120 setText(min_delta_col_, QString::number(calc.min_delta, 'f', prefs.gui_decimal_places3)); // This is RTP. Do we need nanoseconds?
121 setText(mean_delta_col_, QString::number(calc.mean_delta, 'f', prefs.gui_decimal_places3)); // This is RTP. Do we need nanoseconds?
122 setText(max_delta_col_, QString::number(calc.max_delta, 'f', prefs.gui_decimal_places3)); // This is RTP. Do we need nanoseconds?
123 setText(min_jitter_col_, QString::number(calc.min_jitter, 'f', prefs.gui_decimal_places3));
124 setText(mean_jitter_col_, QString::number(calc.mean_jitter, 'f', prefs.gui_decimal_places3));
125 setText(max_jitter_col_, QString::number(calc.max_jitter, 'f', prefs.gui_decimal_places3));
126
127 if (calc.problem) {
128 setText(status_col_, UTF8_BULLET);
129 setTextAlignment(status_col_, Qt::AlignCenter);
130 QColor bgColor(ColorUtils::warningBackground());
131 QColor textColor(QApplication::palette().text().color());
132 for (int i = 0; i < columnCount(); i++) {
133 QBrush bgBrush = background(i);
134 bgBrush.setColor(bgColor);
135 setBackground(i, bgBrush);
136 QBrush fgBrush = foreground(i);
137 fgBrush.setColor(textColor);
138 setForeground(i, fgBrush);
139 }
140 }
141
142 rtpstream_info_calc_free(&calc);
143 }
144 // Return a QString, int, double, or invalid QVariant representing the raw column data.
colData(int col) const145 QVariant colData(int col) const {
146 rtpstream_info_calc_t calc;
147 if (!stream_info_) {
148 return QVariant();
149 }
150
151 rtpstream_info_calculate(stream_info_, &calc);
152
153 switch(col) {
154 case src_addr_col_:
155 return text(col);
156 case src_port_col_:
157 return calc.src_port;
158 case dst_addr_col_:
159 return text(col);
160 case dst_port_col_:
161 return calc.dst_port;
162 case ssrc_col_:
163 return calc.ssrc;
164 case start_time_col_:
165 return calc.start_time_ms;
166 case duration_col_:
167 return calc.duration_ms;
168 case payload_col_:
169 return text(col);
170 case packets_col_:
171 return calc.packet_count;
172 case lost_col_:
173 return calc.lost_num;
174 case min_delta_col_:
175 return calc.min_delta;
176 case mean_delta_col_:
177 return calc.mean_delta;
178 case max_delta_col_:
179 return calc.max_delta;
180 case min_jitter_col_:
181 return calc.min_jitter;
182 case mean_jitter_col_:
183 return calc.mean_jitter;
184 case max_jitter_col_:
185 return calc.max_jitter;
186 case status_col_:
187 return calc.problem ? "Problem" : "";
188 case ssrc_fmt_col_:
189 return QString("0x%1").arg(calc.ssrc, 0, 16);
190 case lost_perc_col_:
191 return QString::number(calc.lost_perc, 'f', prefs.gui_decimal_places1);
192 default:
193 break;
194 }
195 return QVariant();
196 }
197
operator <(const QTreeWidgetItem & other) const198 bool operator< (const QTreeWidgetItem &other) const
199 {
200 rtpstream_info_calc_t calc1;
201 rtpstream_info_calc_t calc2;
202
203 if (other.type() != rtp_stream_type_) return QTreeWidgetItem::operator <(other);
204 const RtpStreamTreeWidgetItem &other_rstwi = dynamic_cast<const RtpStreamTreeWidgetItem&>(other);
205
206 switch (treeWidget()->sortColumn()) {
207 case src_addr_col_:
208 return cmp_address(&(stream_info_->id.src_addr), &(other_rstwi.stream_info_->id.src_addr)) < 0;
209 case src_port_col_:
210 return stream_info_->id.src_port < other_rstwi.stream_info_->id.src_port;
211 case dst_addr_col_:
212 return cmp_address(&(stream_info_->id.dst_addr), &(other_rstwi.stream_info_->id.dst_addr)) < 0;
213 case dst_port_col_:
214 return stream_info_->id.dst_port < other_rstwi.stream_info_->id.dst_port;
215 case ssrc_col_:
216 return stream_info_->id.ssrc < other_rstwi.stream_info_->id.ssrc;
217 case start_time_col_:
218 rtpstream_info_calculate(stream_info_, &calc1);
219 rtpstream_info_calculate(other_rstwi.stream_info_, &calc2);
220 return calc1.start_time_ms < calc2.start_time_ms;
221 case duration_col_:
222 rtpstream_info_calculate(stream_info_, &calc1);
223 rtpstream_info_calculate(other_rstwi.stream_info_, &calc2);
224 return calc1.duration_ms < calc2.duration_ms;
225 case payload_col_:
226 return g_strcmp0(stream_info_->all_payload_type_names, other_rstwi.stream_info_->all_payload_type_names);
227 case packets_col_:
228 return stream_info_->packet_count < other_rstwi.stream_info_->packet_count;
229 case lost_col_:
230 return lost_ < other_rstwi.lost_;
231 case min_delta_col_:
232 return stream_info_->rtp_stats.min_delta < other_rstwi.stream_info_->rtp_stats.min_delta;
233 case mean_delta_col_:
234 return stream_info_->rtp_stats.mean_delta < other_rstwi.stream_info_->rtp_stats.mean_delta;
235 case max_delta_col_:
236 return stream_info_->rtp_stats.max_delta < other_rstwi.stream_info_->rtp_stats.max_delta;
237 case min_jitter_col_:
238 return stream_info_->rtp_stats.min_jitter < other_rstwi.stream_info_->rtp_stats.min_jitter;
239 case mean_jitter_col_:
240 return stream_info_->rtp_stats.mean_jitter < other_rstwi.stream_info_->rtp_stats.mean_jitter;
241 case max_jitter_col_:
242 return stream_info_->rtp_stats.max_jitter < other_rstwi.stream_info_->rtp_stats.max_jitter;
243 default:
244 break;
245 }
246
247 // Fall back to string comparison
248 return QTreeWidgetItem::operator <(other);
249 }
250
setTOD(gboolean tod)251 void setTOD(gboolean tod)
252 {
253 tod_ = tod;
254 }
255
256 private:
257 rtpstream_info_t *stream_info_;
258 guint32 lost_;
259 gboolean tod_;
260 };
261
262
263 RtpStreamDialog *RtpStreamDialog::pinstance_{nullptr};
264 std::mutex RtpStreamDialog::mutex_;
265
openRtpStreamDialog(QWidget & parent,CaptureFile & cf,QObject * packet_list)266 RtpStreamDialog *RtpStreamDialog::openRtpStreamDialog(QWidget &parent, CaptureFile &cf, QObject *packet_list)
267 {
268 std::lock_guard<std::mutex> lock(mutex_);
269 if (pinstance_ == nullptr)
270 {
271 pinstance_ = new RtpStreamDialog(parent, cf);
272 connect(pinstance_, SIGNAL(packetsMarked()),
273 packet_list, SLOT(redrawVisiblePackets()));
274 connect(pinstance_, SIGNAL(goToPacket(int)),
275 packet_list, SLOT(goToPacket(int)));
276 }
277 return pinstance_;
278 }
279
RtpStreamDialog(QWidget & parent,CaptureFile & cf)280 RtpStreamDialog::RtpStreamDialog(QWidget &parent, CaptureFile &cf) :
281 WiresharkDialog(parent, cf),
282 ui(new Ui::RtpStreamDialog),
283 need_redraw_(false)
284 {
285 ui->setupUi(this);
286 loadGeometry(parent.width() * 4 / 5, parent.height() * 2 / 3);
287 setWindowSubtitle(tr("RTP Streams"));
288 ui->streamTreeWidget->installEventFilter(this);
289
290 ctx_menu_.addMenu(ui->menuSelect);
291 ctx_menu_.addMenu(ui->menuFindReverse);
292 ctx_menu_.addAction(ui->actionGoToSetup);
293 ctx_menu_.addAction(ui->actionMarkPackets);
294 ctx_menu_.addAction(ui->actionPrepareFilter);
295 ctx_menu_.addAction(ui->actionExportAsRtpDump);
296 ctx_menu_.addAction(ui->actionCopyAsCsv);
297 ctx_menu_.addAction(ui->actionCopyAsYaml);
298 ctx_menu_.addAction(ui->actionAnalyze);
299 set_action_shortcuts_visible_in_context_menu(ctx_menu_.actions());
300
301 ui->streamTreeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
302 ui->streamTreeWidget->header()->setSortIndicator(0, Qt::AscendingOrder);
303 connect(ui->streamTreeWidget, SIGNAL(customContextMenuRequested(QPoint)),
304 SLOT(showStreamMenu(QPoint)));
305
306 find_reverse_button_ = new QToolButton();
307 ui->buttonBox->addButton(find_reverse_button_, QDialogButtonBox::ActionRole);
308 find_reverse_button_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
309 find_reverse_button_->setPopupMode(QToolButton::MenuButtonPopup);
310
311 connect(ui->actionFindReverse, SIGNAL(triggered()), this, SLOT(on_actionFindReverseNormal_triggered()));
312 find_reverse_button_->setDefaultAction(ui->actionFindReverse);
313 // Overrides text striping of shortcut undercode in QAction
314 find_reverse_button_->setText(ui->actionFindReverseNormal->text());
315 find_reverse_button_->setMenu(ui->menuFindReverse);
316
317 analyze_button_ = RtpAnalysisDialog::addAnalyzeButton(ui->buttonBox, this);
318 prepare_button_ = ui->buttonBox->addButton(ui->actionPrepareFilter->text(), QDialogButtonBox::ActionRole);
319 prepare_button_->setToolTip(ui->actionPrepareFilter->toolTip());
320 connect(prepare_button_, SIGNAL(pressed()), this, SLOT(on_actionPrepareFilter_triggered()));
321 player_button_ = RtpPlayerDialog::addPlayerButton(ui->buttonBox, this);
322 copy_button_ = ui->buttonBox->addButton(ui->actionCopyButton->text(), QDialogButtonBox::ActionRole);
323 copy_button_->setToolTip(ui->actionCopyButton->toolTip());
324 export_button_ = ui->buttonBox->addButton(ui->actionExportAsRtpDump->text(), QDialogButtonBox::ActionRole);
325 export_button_->setToolTip(ui->actionExportAsRtpDump->toolTip());
326 connect(export_button_, SIGNAL(pressed()), this, SLOT(on_actionExportAsRtpDump_triggered()));
327
328 QMenu *copy_menu = new QMenu(copy_button_);
329 QAction *ca;
330 ca = copy_menu->addAction(tr("as CSV"));
331 ca->setToolTip(ui->actionCopyAsCsv->toolTip());
332 connect(ca, SIGNAL(triggered()), this, SLOT(on_actionCopyAsCsv_triggered()));
333 ca = copy_menu->addAction(tr("as YAML"));
334 ca->setToolTip(ui->actionCopyAsYaml->toolTip());
335 connect(ca, SIGNAL(triggered()), this, SLOT(on_actionCopyAsYaml_triggered()));
336 copy_button_->setMenu(copy_menu);
337 connect(&cap_file_, SIGNAL(captureEvent(CaptureEvent)),
338 this, SLOT(captureEvent(CaptureEvent)));
339
340 /* Register the tap listener */
341 memset(&tapinfo_, 0, sizeof(rtpstream_tapinfo_t));
342 tapinfo_.tap_reset = tapReset;
343 tapinfo_.tap_draw = tapDraw;
344 tapinfo_.tap_mark_packet = tapMarkPacket;
345 tapinfo_.tap_data = this;
346 tapinfo_.mode = TAP_ANALYSE;
347
348 register_tap_listener_rtpstream(&tapinfo_, NULL, show_tap_registration_error);
349 if (cap_file_.isValid() && cap_file_.capFile()->dfilter) {
350 // Activate display filter checking
351 tapinfo_.apply_display_filter = true;
352 ui->displayFilterCheckBox->setChecked(true);
353 }
354
355 connect(this, SIGNAL(updateFilter(QString, bool)),
356 &parent, SLOT(filterPackets(QString, bool)));
357 connect(&parent, SIGNAL(displayFilterSuccess(bool)),
358 this, SLOT(displayFilterSuccess(bool)));
359 connect(this, SIGNAL(rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)),
360 &parent, SLOT(rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)));
361 connect(this, SIGNAL(rtpPlayerDialogAddRtpStreams(QVector<rtpstream_id_t *>)),
362 &parent, SLOT(rtpPlayerDialogAddRtpStreams(QVector<rtpstream_id_t *>)));
363 connect(this, SIGNAL(rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)),
364 &parent, SLOT(rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)));
365 connect(this, SIGNAL(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)),
366 &parent, SLOT(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)));
367 connect(this, SIGNAL(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)),
368 &parent, SLOT(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)));
369 connect(this, SIGNAL(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)),
370 &parent, SLOT(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)));
371
372 /* Scan for RTP streams (redissect all packets) */
373 rtpstream_scan(&tapinfo_, cf.capFile(), NULL);
374
375 updateWidgets();
376 }
377
~RtpStreamDialog()378 RtpStreamDialog::~RtpStreamDialog()
379 {
380 std::lock_guard<std::mutex> lock(mutex_);
381 freeLastSelected();
382 delete ui;
383 remove_tap_listener_rtpstream(&tapinfo_);
384 pinstance_ = nullptr;
385 }
386
setRtpStreamSelection(rtpstream_id_t * id,bool state)387 void RtpStreamDialog::setRtpStreamSelection(rtpstream_id_t *id, bool state)
388 {
389 QTreeWidgetItemIterator iter(ui->streamTreeWidget);
390 while (*iter) {
391 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
392 rtpstream_info_t *stream_info = rsti->streamInfo();
393 if (stream_info) {
394 if (rtpstream_id_equal(id,&stream_info->id,RTPSTREAM_ID_EQUAL_SSRC)) {
395 (*iter)->setSelected(state);
396 }
397 }
398 ++iter;
399 }
400 }
401
selectRtpStream(QVector<rtpstream_id_t * > stream_ids)402 void RtpStreamDialog::selectRtpStream(QVector<rtpstream_id_t *> stream_ids)
403 {
404 std::lock_guard<std::mutex> lock(mutex_);
405 foreach(rtpstream_id_t *id, stream_ids) {
406 setRtpStreamSelection(id, true);
407 }
408 }
409
deselectRtpStream(QVector<rtpstream_id_t * > stream_ids)410 void RtpStreamDialog::deselectRtpStream(QVector<rtpstream_id_t *> stream_ids)
411 {
412 std::lock_guard<std::mutex> lock(mutex_);
413 foreach(rtpstream_id_t *id, stream_ids) {
414 setRtpStreamSelection(id, false);
415 }
416 }
417
eventFilter(QObject *,QEvent * event)418 bool RtpStreamDialog::eventFilter(QObject *, QEvent *event)
419 {
420 if (ui->streamTreeWidget->hasFocus() && event->type() == QEvent::KeyPress) {
421 QKeyEvent &keyEvent = static_cast<QKeyEvent&>(*event);
422 switch(keyEvent.key()) {
423 case Qt::Key_G:
424 on_actionGoToSetup_triggered();
425 return true;
426 case Qt::Key_M:
427 on_actionMarkPackets_triggered();
428 return true;
429 case Qt::Key_P:
430 on_actionPrepareFilter_triggered();
431 return true;
432 case Qt::Key_R:
433 if (keyEvent.modifiers() == Qt::ShiftModifier) {
434 on_actionFindReversePair_triggered();
435 } else if (keyEvent.modifiers() == Qt::ControlModifier) {
436 on_actionFindReverseSingle_triggered();
437 } else {
438 on_actionFindReverseNormal_triggered();
439 }
440 return true;
441 case Qt::Key_I:
442 if (keyEvent.modifiers() == Qt::ControlModifier) {
443 // Ctrl+I
444 on_actionSelectInvert_triggered();
445 return true;
446 }
447 break;
448 case Qt::Key_A:
449 if (keyEvent.modifiers() == Qt::ControlModifier) {
450 // Ctrl+A
451 on_actionSelectAll_triggered();
452 return true;
453 } else if (keyEvent.modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
454 // Ctrl+Shift+A
455 on_actionSelectNone_triggered();
456 return true;
457 } else if (keyEvent.modifiers() == Qt::NoModifier) {
458 on_actionAnalyze_triggered();
459 }
460 break;
461 default:
462 break;
463 }
464 }
465 return false;
466 }
467
captureEvent(CaptureEvent e)468 void RtpStreamDialog::captureEvent(CaptureEvent e)
469 {
470 if (e.captureContext() == CaptureEvent::Retap)
471 {
472 switch (e.eventType())
473 {
474 case CaptureEvent::Started:
475 ui->displayFilterCheckBox->setEnabled(false);
476 break;
477 case CaptureEvent::Finished:
478 ui->displayFilterCheckBox->setEnabled(true);
479 break;
480 default:
481 break;
482 }
483 }
484
485 }
486
tapReset(rtpstream_tapinfo_t * tapinfo)487 void RtpStreamDialog::tapReset(rtpstream_tapinfo_t *tapinfo)
488 {
489 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
490 if (rtp_stream_dialog) {
491 rtp_stream_dialog->freeLastSelected();
492 /* Copy currently selected rtpstream_ids */
493 QTreeWidgetItemIterator iter(rtp_stream_dialog->ui->streamTreeWidget);
494 while (*iter) {
495 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
496 rtpstream_info_t *stream_info = rsti->streamInfo();
497 if ((*iter)->isSelected()) {
498 rtpstream_id_t *i = (rtpstream_id_t *)g_malloc0(sizeof(rtpstream_id_t));
499 rtpstream_id_copy(&stream_info->id, i);
500 rtp_stream_dialog->last_selected_.append(*i);
501 }
502 ++iter;
503 }
504 /* invalidate items which refer to old strinfo_list items. */
505 rtp_stream_dialog->ui->streamTreeWidget->clear();
506 }
507 }
508
tapDraw(rtpstream_tapinfo_t * tapinfo)509 void RtpStreamDialog::tapDraw(rtpstream_tapinfo_t *tapinfo)
510 {
511 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
512 if (rtp_stream_dialog) {
513 rtp_stream_dialog->updateStreams();
514 }
515 }
516
tapMarkPacket(rtpstream_tapinfo_t * tapinfo,frame_data * fd)517 void RtpStreamDialog::tapMarkPacket(rtpstream_tapinfo_t *tapinfo, frame_data *fd)
518 {
519 if (!tapinfo) return;
520
521 RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
522 if (rtp_stream_dialog) {
523 cf_mark_frame(rtp_stream_dialog->cap_file_.capFile(), fd);
524 rtp_stream_dialog->need_redraw_ = true;
525 }
526 }
527
528 /* Operator == for rtpstream_id_t */
operator ==(rtpstream_id_t const & a,rtpstream_id_t const & b)529 bool operator==(rtpstream_id_t const& a, rtpstream_id_t const& b)
530 {
531 return rtpstream_id_equal(&a, &b, RTPSTREAM_ID_EQUAL_SSRC);
532 }
533
updateStreams()534 void RtpStreamDialog::updateStreams()
535 {
536 // string_list is reverse ordered, so we must add
537 // just first "to_insert_count" of streams
538 GList *cur_stream = g_list_first(tapinfo_.strinfo_list);
539 guint tap_len = g_list_length(tapinfo_.strinfo_list);
540 guint tree_len = static_cast<guint>(ui->streamTreeWidget->topLevelItemCount());
541 guint to_insert_count = tap_len - tree_len;
542
543 // Add any missing items
544 while (cur_stream && cur_stream->data && to_insert_count) {
545 rtpstream_info_t *stream_info = gxx_list_data(rtpstream_info_t*, cur_stream);
546 RtpStreamTreeWidgetItem *rsti = new RtpStreamTreeWidgetItem(ui->streamTreeWidget, stream_info);
547 cur_stream = gxx_list_next(cur_stream);
548 to_insert_count--;
549
550 // Check if item was selected last time. If so, select it
551 if (-1 != last_selected_.indexOf(stream_info->id)) {
552 rsti->setSelected(true);
553 }
554 }
555
556 // Recalculate values
557 QTreeWidgetItemIterator iter(ui->streamTreeWidget);
558 while (*iter) {
559 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
560 rsti->drawData();
561 ++iter;
562 }
563
564 // Resize columns
565 for (int i = 0; i < ui->streamTreeWidget->columnCount(); i++) {
566 ui->streamTreeWidget->resizeColumnToContents(i);
567 }
568
569 ui->streamTreeWidget->setSortingEnabled(true);
570
571 updateWidgets();
572
573 if (need_redraw_) {
574 emit packetsMarked();
575 need_redraw_ = false;
576 }
577 }
578
updateWidgets()579 void RtpStreamDialog::updateWidgets()
580 {
581 bool selected = ui->streamTreeWidget->selectedItems().count() > 0;
582
583 QString hint = "<small><i>";
584 hint += tr("%1 streams").arg(ui->streamTreeWidget->topLevelItemCount());
585
586 if (selected) {
587 int tot_packets = 0;
588 foreach(QTreeWidgetItem *ti, ui->streamTreeWidget->selectedItems()) {
589 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
590 if (rsti->streamInfo()) {
591 tot_packets += rsti->streamInfo()->packet_count;
592 }
593 }
594 hint += tr(", %1 selected, %2 total packets")
595 .arg(ui->streamTreeWidget->selectedItems().count())
596 .arg(tot_packets);
597 }
598
599 hint += ". Right-click for more options.";
600 hint += "</i></small>";
601 ui->hintLabel->setText(hint);
602
603 bool enable = selected && !file_closed_;
604 bool has_data = ui->streamTreeWidget->topLevelItemCount() > 0;
605
606 find_reverse_button_->setEnabled(has_data);
607 prepare_button_->setEnabled(enable);
608 export_button_->setEnabled(enable);
609 copy_button_->setEnabled(has_data);
610 analyze_button_->setEnabled(enable);
611
612 ui->actionFindReverseNormal->setEnabled(enable);
613 ui->actionFindReversePair->setEnabled(has_data);
614 ui->actionFindReverseSingle->setEnabled(has_data);
615 ui->actionGoToSetup->setEnabled(enable);
616 ui->actionMarkPackets->setEnabled(enable);
617 ui->actionPrepareFilter->setEnabled(enable);
618 ui->actionExportAsRtpDump->setEnabled(enable);
619 ui->actionCopyAsCsv->setEnabled(has_data);
620 ui->actionCopyAsYaml->setEnabled(has_data);
621 ui->actionAnalyze->setEnabled(enable);
622
623 #if defined(QT_MULTIMEDIA_LIB)
624 player_button_->setEnabled(enable);
625 #endif
626
627 WiresharkDialog::updateWidgets();
628 }
629
streamRowData(int row) const630 QList<QVariant> RtpStreamDialog::streamRowData(int row) const
631 {
632 QList<QVariant> row_data;
633
634 if (row >= ui->streamTreeWidget->topLevelItemCount()) {
635 return row_data;
636 }
637
638 for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
639 if (row < 0) {
640 row_data << ui->streamTreeWidget->headerItem()->text(col);
641 } else {
642 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(row));
643 if (rsti) {
644 row_data << rsti->colData(col);
645 }
646 }
647 }
648
649 // Add additional columns to export
650 if (row < 0) {
651 row_data << QString("SSRC formatted");
652 row_data << QString("Lost percentage");
653 } else {
654 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(row));
655 if (rsti) {
656 row_data << rsti->colData(ssrc_fmt_col_);
657 row_data << rsti->colData(lost_perc_col_);
658 }
659 }
660 return row_data;
661 }
662
freeLastSelected()663 void RtpStreamDialog::freeLastSelected()
664 {
665 /* Free old IDs */
666 for(int i=0; i<last_selected_.length(); i++) {
667 rtpstream_id_t id = last_selected_.at(i);
668 rtpstream_id_free(&id);
669 }
670 /* Clear list and reuse it */
671 last_selected_.clear();
672 }
673
captureFileClosing()674 void RtpStreamDialog::captureFileClosing()
675 {
676 remove_tap_listener_rtpstream(&tapinfo_);
677
678 WiresharkDialog::captureFileClosing();
679 }
680
captureFileClosed()681 void RtpStreamDialog::captureFileClosed()
682 {
683 ui->todCheckBox->setEnabled(false);
684 ui->displayFilterCheckBox->setEnabled(false);
685
686 WiresharkDialog::captureFileClosed();
687 }
688
showStreamMenu(QPoint pos)689 void RtpStreamDialog::showStreamMenu(QPoint pos)
690 {
691 ui->actionGoToSetup->setEnabled(!file_closed_);
692 ui->actionMarkPackets->setEnabled(!file_closed_);
693 ui->actionPrepareFilter->setEnabled(!file_closed_);
694 ui->actionExportAsRtpDump->setEnabled(!file_closed_);
695 ui->actionAnalyze->setEnabled(!file_closed_);
696 ctx_menu_.popup(ui->streamTreeWidget->viewport()->mapToGlobal(pos));
697 }
698
on_actionCopyAsCsv_triggered()699 void RtpStreamDialog::on_actionCopyAsCsv_triggered()
700 {
701 QString csv;
702 QTextStream stream(&csv, QIODevice::Text);
703 for (int row = -1; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
704 QStringList rdsl;
705 foreach (QVariant v, streamRowData(row)) {
706 if (!v.isValid()) {
707 rdsl << "\"\"";
708 } else if (v.type() == QVariant::String) {
709 rdsl << QString("\"%1\"").arg(v.toString());
710 } else {
711 rdsl << v.toString();
712 }
713 }
714 stream << rdsl.join(",") << '\n';
715 }
716 wsApp->clipboard()->setText(stream.readAll());
717 }
718
on_actionCopyAsYaml_triggered()719 void RtpStreamDialog::on_actionCopyAsYaml_triggered()
720 {
721 QString yaml;
722 QTextStream stream(&yaml, QIODevice::Text);
723 stream << "---" << '\n';
724 for (int row = -1; row < ui->streamTreeWidget->topLevelItemCount(); row ++) {
725 stream << "-" << '\n';
726 foreach (QVariant v, streamRowData(row)) {
727 stream << " - " << v.toString() << '\n';
728 }
729 }
730 wsApp->clipboard()->setText(stream.readAll());
731 }
732
on_actionExportAsRtpDump_triggered()733 void RtpStreamDialog::on_actionExportAsRtpDump_triggered()
734 {
735 if (file_closed_ || ui->streamTreeWidget->selectedItems().count() < 1) return;
736
737 // XXX If the user selected multiple frames is this the one we actually want?
738 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
739 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
740 rtpstream_info_t *stream_info = rsti->streamInfo();
741 if (stream_info) {
742 QString file_name;
743 QDir path(wsApp->lastOpenDir());
744 QString save_file = path.canonicalPath() + "/" + cap_file_.fileBaseName();
745 QString extension;
746 file_name = WiresharkFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save RTPDump As…")),
747 save_file, "RTPDump Format (*.rtpdump)", &extension);
748
749 if (file_name.length() > 0) {
750 gchar *dest_file = qstring_strdup(file_name);
751 gboolean save_ok = rtpstream_save(&tapinfo_, cap_file_.capFile(), stream_info, dest_file);
752 g_free(dest_file);
753 // else error dialog?
754 if (save_ok) {
755 wsApp->setLastOpenDirFromFilename(file_name);
756 }
757 }
758
759 }
760 }
761
762 // Search for reverse stream of every selected stream
on_actionFindReverseNormal_triggered()763 void RtpStreamDialog::on_actionFindReverseNormal_triggered()
764 {
765 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
766
767 ui->streamTreeWidget->blockSignals(true);
768
769 // Traverse all items and if stream is selected, search reverse from
770 // current position till last item (NxN/2)
771 for (int fwd_row = 0; fwd_row < ui->streamTreeWidget->topLevelItemCount(); fwd_row++) {
772 RtpStreamTreeWidgetItem *fwd_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(fwd_row));
773 rtpstream_info_t *fwd_stream = fwd_rsti->streamInfo();
774 if (fwd_stream && fwd_rsti->isSelected()) {
775 for (int rev_row = fwd_row + 1; rev_row < ui->streamTreeWidget->topLevelItemCount(); rev_row++) {
776 RtpStreamTreeWidgetItem *rev_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(rev_row));
777 rtpstream_info_t *rev_stream = rev_rsti->streamInfo();
778 if (rev_stream && rtpstream_info_is_reverse(fwd_stream, rev_stream)) {
779 rev_rsti->setSelected(true);
780 break;
781 }
782 }
783 }
784 }
785 ui->streamTreeWidget->blockSignals(false);
786 updateWidgets();
787 }
788
789 // Select all pairs of forward/reverse streams
on_actionFindReversePair_triggered()790 void RtpStreamDialog::on_actionFindReversePair_triggered()
791 {
792 ui->streamTreeWidget->blockSignals(true);
793 ui->streamTreeWidget->clearSelection();
794
795 // Traverse all items and search reverse from current position till last
796 // item (NxN/2)
797 for (int fwd_row = 0; fwd_row < ui->streamTreeWidget->topLevelItemCount(); fwd_row++) {
798 RtpStreamTreeWidgetItem *fwd_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(fwd_row));
799 rtpstream_info_t *fwd_stream = fwd_rsti->streamInfo();
800 if (fwd_stream) {
801 for (int rev_row = fwd_row + 1; rev_row < ui->streamTreeWidget->topLevelItemCount(); rev_row++) {
802 RtpStreamTreeWidgetItem *rev_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(rev_row));
803 rtpstream_info_t *rev_stream = rev_rsti->streamInfo();
804 if (rev_stream && rtpstream_info_is_reverse(fwd_stream, rev_stream)) {
805 fwd_rsti->setSelected(true);
806 rev_rsti->setSelected(true);
807 break;
808 }
809 }
810 }
811 }
812 ui->streamTreeWidget->blockSignals(false);
813 updateWidgets();
814 }
815
816 // Select all streams which don't have reverse stream
on_actionFindReverseSingle_triggered()817 void RtpStreamDialog::on_actionFindReverseSingle_triggered()
818 {
819 ui->streamTreeWidget->blockSignals(true);
820 ui->streamTreeWidget->selectAll();
821
822 // Traverse all items and search reverse from current position till last
823 // item (NxN/2)
824 for (int fwd_row = 0; fwd_row < ui->streamTreeWidget->topLevelItemCount(); fwd_row++) {
825 RtpStreamTreeWidgetItem *fwd_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(fwd_row));
826 rtpstream_info_t *fwd_stream = fwd_rsti->streamInfo();
827 if (fwd_stream) {
828 for (int rev_row = fwd_row + 1; rev_row < ui->streamTreeWidget->topLevelItemCount(); rev_row++) {
829 RtpStreamTreeWidgetItem *rev_rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(rev_row));
830 rtpstream_info_t *rev_stream = rev_rsti->streamInfo();
831 if (rev_stream && rtpstream_info_is_reverse(fwd_stream, rev_stream)) {
832 fwd_rsti->setSelected(false);
833 rev_rsti->setSelected(false);
834 break;
835 }
836 }
837 }
838 }
839 ui->streamTreeWidget->blockSignals(false);
840 updateWidgets();
841 }
842
on_actionGoToSetup_triggered()843 void RtpStreamDialog::on_actionGoToSetup_triggered()
844 {
845 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
846 // XXX If the user selected multiple frames is this the one we actually want?
847 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
848 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
849 rtpstream_info_t *stream_info = rsti->streamInfo();
850 if (stream_info) {
851 emit goToPacket(stream_info->setup_frame_number);
852 }
853 }
854
on_actionMarkPackets_triggered()855 void RtpStreamDialog::on_actionMarkPackets_triggered()
856 {
857 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
858 rtpstream_info_t *stream_a, *stream_b = NULL;
859
860 QTreeWidgetItem *ti = ui->streamTreeWidget->selectedItems()[0];
861 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
862 stream_a = rsti->streamInfo();
863 if (ui->streamTreeWidget->selectedItems().count() > 1) {
864 ti = ui->streamTreeWidget->selectedItems()[1];
865 rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
866 stream_b = rsti->streamInfo();
867 }
868
869 if (stream_a == NULL && stream_b == NULL) return;
870
871 // XXX Mark the setup frame as well?
872 need_redraw_ = false;
873 rtpstream_mark(&tapinfo_, cap_file_.capFile(), stream_a, stream_b);
874 updateWidgets();
875 }
876
on_actionPrepareFilter_triggered()877 void RtpStreamDialog::on_actionPrepareFilter_triggered()
878 {
879 QVector<rtpstream_id_t *> ids = getSelectedRtpIds();
880 QString filter = make_filter_based_on_rtpstream_id(ids);
881 if (filter.length() > 0) {
882 remove_tap_listener_rtpstream(&tapinfo_);
883 emit updateFilter(filter);
884 }
885 }
886
on_streamTreeWidget_itemSelectionChanged()887 void RtpStreamDialog::on_streamTreeWidget_itemSelectionChanged()
888 {
889 updateWidgets();
890 }
891
on_buttonBox_helpRequested()892 void RtpStreamDialog::on_buttonBox_helpRequested()
893 {
894 wsApp->helpTopicAction(HELP_TELEPHONY_RTP_STREAMS_DIALOG);
895 }
896
on_displayFilterCheckBox_toggled(bool checked _U_)897 void RtpStreamDialog::on_displayFilterCheckBox_toggled(bool checked _U_)
898 {
899 if (!cap_file_.isValid()) {
900 return;
901 }
902
903 tapinfo_.apply_display_filter = checked;
904
905 cap_file_.retapPackets();
906 }
907
on_todCheckBox_toggled(bool checked)908 void RtpStreamDialog::on_todCheckBox_toggled(bool checked)
909 {
910 QTreeWidgetItemIterator iter(ui->streamTreeWidget);
911 while (*iter) {
912 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
913 rsti->setTOD(checked);
914 rsti->drawData();
915 ++iter;
916 }
917 ui->streamTreeWidget->resizeColumnToContents(start_time_col_);
918 }
919
on_actionSelectAll_triggered()920 void RtpStreamDialog::on_actionSelectAll_triggered()
921 {
922 ui->streamTreeWidget->selectAll();
923 }
924
on_actionSelectInvert_triggered()925 void RtpStreamDialog::on_actionSelectInvert_triggered()
926 {
927 invertSelection();
928 }
929
on_actionSelectNone_triggered()930 void RtpStreamDialog::on_actionSelectNone_triggered()
931 {
932 ui->streamTreeWidget->clearSelection();
933 }
934
getSelectedRtpIds()935 QVector<rtpstream_id_t *>RtpStreamDialog::getSelectedRtpIds()
936 {
937 // Gather up our selected streams...
938 QVector<rtpstream_id_t *> stream_ids;
939 foreach(QTreeWidgetItem *ti, ui->streamTreeWidget->selectedItems()) {
940 RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ti);
941 rtpstream_info_t *selected_stream = rsti->streamInfo();
942 if (selected_stream) {
943 stream_ids << &(selected_stream->id);
944 }
945 }
946
947 return stream_ids;
948 }
949
rtpPlayerReplace()950 void RtpStreamDialog::rtpPlayerReplace()
951 {
952 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
953
954 emit rtpPlayerDialogReplaceRtpStreams(getSelectedRtpIds());
955 }
956
rtpPlayerAdd()957 void RtpStreamDialog::rtpPlayerAdd()
958 {
959 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
960
961 emit rtpPlayerDialogAddRtpStreams(getSelectedRtpIds());
962 }
963
rtpPlayerRemove()964 void RtpStreamDialog::rtpPlayerRemove()
965 {
966 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
967
968 emit rtpPlayerDialogRemoveRtpStreams(getSelectedRtpIds());
969 }
970
rtpAnalysisReplace()971 void RtpStreamDialog::rtpAnalysisReplace()
972 {
973 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
974
975 emit rtpAnalysisDialogReplaceRtpStreams(getSelectedRtpIds());
976 }
977
rtpAnalysisAdd()978 void RtpStreamDialog::rtpAnalysisAdd()
979 {
980 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
981
982 emit rtpAnalysisDialogAddRtpStreams(getSelectedRtpIds());
983 }
984
rtpAnalysisRemove()985 void RtpStreamDialog::rtpAnalysisRemove()
986 {
987 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
988
989 emit rtpAnalysisDialogRemoveRtpStreams(getSelectedRtpIds());
990 }
991
displayFilterSuccess(bool success)992 void RtpStreamDialog::displayFilterSuccess(bool success)
993 {
994 if (success && ui->displayFilterCheckBox->isChecked()) {
995 cap_file_.retapPackets();
996 }
997 }
998
invertSelection()999 void RtpStreamDialog::invertSelection()
1000 {
1001 ui->streamTreeWidget->blockSignals(true);
1002 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1003 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1004 ti->setSelected(!ti->isSelected());
1005 }
1006 ui->streamTreeWidget->blockSignals(false);
1007 updateWidgets();
1008 }
1009
on_actionAnalyze_triggered()1010 void RtpStreamDialog::on_actionAnalyze_triggered()
1011 {
1012 RtpStreamDialog::rtpAnalysisAdd();
1013 }
1014
1015