48228305f13037f58a3618b9f3ee088af5b6254a
[gd/wireshark/.git] / ui / qt / rtp_stream_dialog.cpp
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 #include "rtp_stream_dialog.h"
10 #include <ui_rtp_stream_dialog.h>
11
12 #include "file.h"
13
14 #include "epan/addr_resolv.h"
15 #include <epan/rtp_pt.h>
16
17 #include <wsutil/utf8_entities.h>
18
19 #include <ui/qt/utils/qt_ui_utils.h>
20 #include "rtp_analysis_dialog.h"
21 #include "wireshark_application.h"
22
23 #include <QAction>
24 #include <QClipboard>
25 #include <QFileDialog>
26 #include <QKeyEvent>
27 #include <QPushButton>
28 #include <QTextStream>
29 #include <QTreeWidgetItem>
30 #include <QTreeWidgetItemIterator>
31
32 #include <ui/qt/utils/tango_colors.h>
33
34 /*
35  * @file RTP stream dialog
36  *
37  * Displays a list of RTP streams with the following information:
38  * - UDP 4-tuple
39  * - SSRC
40  * - Payload type
41  * - Stats: Packets, lost, max delta, max jitter, mean jitter
42  * - Problems
43  *
44  * Finds reverse streams
45  * "Save As" rtpdump
46  * Mark packets
47  * Go to the setup frame
48  * Prepare filter
49  * Copy As CSV and YAML
50  * Analyze
51  */
52
53 // To do:
54 // - Add more statistics to the hint text (e.g. lost packets).
55 // - Add more statistics to the main list (e.g. stream duration)
56
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;
69
70 enum { rtp_stream_type_ = 1000 };
71
72 class RtpStreamTreeWidgetItem : public QTreeWidgetItem
73 {
74 public:
75     RtpStreamTreeWidgetItem(QTreeWidget *tree, rtp_stream_info_t *stream_info) :
76         QTreeWidgetItem(tree, rtp_stream_type_),
77         stream_info_(stream_info)
78     {
79         drawData();
80     }
81
82     rtp_stream_info_t *streamInfo() const { return stream_info_; }
83
84     void drawData() {
85         if (!stream_info_) {
86             return;
87         }
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));
93
94         if (stream_info_->payload_type_name != NULL) {
95             setText(payload_col_, stream_info_->payload_type_name);
96         } else {
97             setText(payload_col_, val_ext_to_qstring(stream_info_->payload_type,
98                                                  &rtp_payload_type_short_vals_ext,
99                                                  "Unknown (%u)"));
100         }
101
102         setText(packets_col_, QString::number(stream_info_->packet_count));
103
104         guint32 expected;
105         double pct_loss;
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;
109         if (expected) {
110             pct_loss = (double)(lost_*100.0)/(double)expected;
111         } else {
112             pct_loss = 0;
113         }
114
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));
119
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);
126             }
127         }
128     }
129     // Return a QString, int, double, or invalid QVariant representing the raw column data.
130     QVariant colData(int col) const {
131         if (!stream_info_) {
132             return QVariant();
133         }
134
135         switch(col) {
136         case src_addr_col_:
137         case dst_addr_col_:
138         case payload_col_: // XXX Return numeric value?
139             return text(col);
140         case src_port_col_:
141             return stream_info_->src_port;
142         case dst_port_col_:
143             return stream_info_->dest_port;
144         case ssrc_col_:
145             return stream_info_->ssrc;
146         case packets_col_:
147             return stream_info_->packet_count;
148         case lost_col_:
149             return lost_;
150         case max_delta_col_:
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;
156         case status_col_:
157             return stream_info_->problem ? "Problem" : "";
158         default:
159             break;
160         }
161         return QVariant();
162     }
163
164     bool operator< (const QTreeWidgetItem &other) const
165     {
166         if (other.type() != rtp_stream_type_) return QTreeWidgetItem::operator <(other);
167         const RtpStreamTreeWidgetItem &other_rstwi = dynamic_cast<const RtpStreamTreeWidgetItem&>(other);
168
169         switch (treeWidget()->sortColumn()) {
170         case src_addr_col_:
171             return cmp_address(&(stream_info_->src_addr), &(other_rstwi.stream_info_->src_addr)) < 0;
172         case src_port_col_:
173             return stream_info_->src_port < other_rstwi.stream_info_->src_port;
174         case dst_addr_col_:
175             return cmp_address(&(stream_info_->dest_addr), &(other_rstwi.stream_info_->dest_addr)) < 0;
176         case dst_port_col_:
177             return stream_info_->dest_port < other_rstwi.stream_info_->dest_port;
178         case ssrc_col_:
179             return stream_info_->ssrc < other_rstwi.stream_info_->ssrc;
180         case payload_col_:
181             return stream_info_->payload_type < other_rstwi.stream_info_->payload_type; // XXX Compare payload_type_name instead?
182         case packets_col_:
183             return stream_info_->packet_count < other_rstwi.stream_info_->packet_count;
184         case lost_col_:
185             return lost_ < other_rstwi.lost_;
186         case max_delta_col_:
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;
192         default:
193             break;
194         }
195
196         // Fall back to string comparison
197         return QTreeWidgetItem::operator <(other);
198     }
199
200 private:
201     rtp_stream_info_t *stream_info_;
202     guint32 lost_;
203 };
204
205 RtpStreamDialog::RtpStreamDialog(QWidget &parent, CaptureFile &cf) :
206     WiresharkDialog(parent, cf),
207     ui(new Ui::RtpStreamDialog),
208     need_redraw_(false)
209 {
210     ui->setupUi(this);
211     loadGeometry(parent.width() * 4 / 5, parent.height() * 2 / 3);
212     setWindowSubtitle(tr("RTP Streams"));
213     ui->streamTreeWidget->installEventFilter(this);
214
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)));
228
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());
241
242     QMenu *copy_menu = new QMenu(copy_button_);
243     QAction *ca;
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);
251
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;
259
260     register_tap_listener_rtp_stream(&tapinfo_, NULL);
261     /* Scan for RTP streams (redissect all packets) */
262     rtpstream_scan(&tapinfo_, cf.capFile(), NULL);
263
264     updateWidgets();
265 }
266
267 RtpStreamDialog::~RtpStreamDialog()
268 {
269     delete ui;
270     remove_tap_listener_rtp_stream(&tapinfo_);
271 }
272
273 bool RtpStreamDialog::eventFilter(QObject *, QEvent *event)
274 {
275     if (ui->streamTreeWidget->hasFocus() && event->type() == QEvent::KeyPress) {
276         QKeyEvent &keyEvent = static_cast<QKeyEvent&>(*event);
277         switch(keyEvent.key()) {
278         case Qt::Key_G:
279             on_actionGoToSetup_triggered();
280             return true;
281         case Qt::Key_M:
282             on_actionMarkPackets_triggered();
283             return true;
284         case Qt::Key_P:
285             on_actionPrepareFilter_triggered();
286             return true;
287         case Qt::Key_R:
288             on_actionFindReverse_triggered();
289             return true;
290         case Qt::Key_A:
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();
295 //            return true;
296             break;
297         default:
298             break;
299         }
300     }
301     return false;
302 }
303
304 void RtpStreamDialog::tapReset(rtpstream_tapinfo_t *tapinfo)
305 {
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();
310     }
311 }
312
313 void RtpStreamDialog::tapDraw(rtpstream_tapinfo_t *tapinfo)
314 {
315     RtpStreamDialog *rtp_stream_dialog = dynamic_cast<RtpStreamDialog *>((RtpStreamDialog *)tapinfo->tap_data);
316     if (rtp_stream_dialog) {
317         rtp_stream_dialog->updateStreams();
318     }
319 }
320
321 void RtpStreamDialog::tapMarkPacket(rtpstream_tapinfo_t *tapinfo, frame_data *fd)
322 {
323     if (!tapinfo) return;
324
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;
329     }
330 }
331
332 void RtpStreamDialog::updateStreams()
333 {
334     GList *cur_stream = g_list_nth(tapinfo_.strinfo_list, ui->streamTreeWidget->topLevelItemCount());
335
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);
341     }
342
343     // Recalculate values
344     QTreeWidgetItemIterator iter(ui->streamTreeWidget);
345     while (*iter) {
346         RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
347         rsti->drawData();
348         ++iter;
349     }
350
351     // Resize columns
352     for (int i = 0; i < ui->streamTreeWidget->columnCount(); i++) {
353         ui->streamTreeWidget->resizeColumnToContents(i);
354     }
355
356     ui->streamTreeWidget->setSortingEnabled(true);
357
358     updateWidgets();
359
360     if (need_redraw_) {
361         emit packetsMarked();
362         need_redraw_ = false;
363     }
364 }
365
366 void RtpStreamDialog::updateWidgets()
367 {
368     bool selected = ui->streamTreeWidget->selectedItems().count() > 0;
369
370     QString hint = "<small><i>";
371     hint += tr("%1 streams").arg(ui->streamTreeWidget->topLevelItemCount());
372
373     if (selected) {
374         int tot_packets = 0;
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;
379             }
380         }
381         hint += tr(", %1 selected, %2 total packets")
382                 .arg(ui->streamTreeWidget->selectedItems().count())
383                 .arg(tot_packets);
384     }
385
386     hint += ". Right-click for more options.";
387     hint += "</i></small>";
388     ui->hintLabel->setText(hint);
389
390     bool enable = selected && !file_closed_;
391     bool has_data = ui->streamTreeWidget->topLevelItemCount() > 0;
392
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);
398
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);
407
408     WiresharkDialog::updateWidgets();
409 }
410
411 QList<QVariant> RtpStreamDialog::streamRowData(int row) const
412 {
413     QList<QVariant> row_data;
414
415     if (row >= ui->streamTreeWidget->topLevelItemCount()) {
416         return row_data;
417     }
418
419     for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
420         if (row < 0) {
421             row_data << ui->streamTreeWidget->headerItem()->text(col);
422         } else {
423             RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(ui->streamTreeWidget->topLevelItem(row));
424             if (rsti) {
425                 row_data << rsti->colData(col);
426             }
427         }
428     }
429     return row_data;
430 }
431
432 void RtpStreamDialog::captureFileClosing()
433 {
434     remove_tap_listener_rtp_stream(&tapinfo_);
435     WiresharkDialog::captureFileClosing();
436 }
437
438 void RtpStreamDialog::showStreamMenu(QPoint pos)
439 {
440     ctx_menu_.popup(ui->streamTreeWidget->viewport()->mapToGlobal(pos));
441 }
442
443 void RtpStreamDialog::on_actionAnalyze_triggered()
444 {
445     rtp_stream_info_t *stream_a, *stream_b = NULL;
446
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();
454     }
455
456     if (stream_a == NULL && stream_b == NULL) return;
457
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();
461 }
462
463 void RtpStreamDialog::on_actionCopyAsCsv_triggered()
464 {
465     QString csv;
466     QTextStream stream(&csv, QIODevice::Text);
467     for (int row = -1; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
468         QStringList rdsl;
469         foreach (QVariant v, streamRowData(row)) {
470             if (!v.isValid()) {
471                 rdsl << "\"\"";
472             } else if (v.type() == QVariant::String) {
473                 rdsl << QString("\"%1\"").arg(v.toString());
474             } else {
475                 rdsl << v.toString();
476             }
477         }
478         stream << rdsl.join(",") << endl;
479     }
480     wsApp->clipboard()->setText(stream.readAll());
481 }
482
483 void RtpStreamDialog::on_actionCopyAsYaml_triggered()
484 {
485     QString yaml;
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;
492         }
493     }
494     wsApp->clipboard()->setText(stream.readAll());
495 }
496
497 void RtpStreamDialog::on_actionExportAsRtpDump_triggered()
498 {
499     if (file_closed_ || ui->streamTreeWidget->selectedItems().count() < 1) return;
500
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();
505     if (stream_info) {
506         QString file_name;
507         QDir path(wsApp->lastOpenDir());
508         QString save_file = path.canonicalPath() + "/" + cap_file_.fileTitle();
509         QString extension;
510         file_name = QFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save RTPDump As" UTF8_HORIZONTAL_ELLIPSIS)),
511                                                  save_file, "RTPDump Format (*.rtpdump)", &extension);
512
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);
516             g_free(dest_file);
517             // else error dialog?
518             if (save_ok) {
519                 path = QDir(file_name);
520                 wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData());
521             }
522         }
523
524     }
525 }
526
527 void RtpStreamDialog::on_actionFindReverse_triggered()
528 {
529     if (ui->streamTreeWidget->selectedItems().count() < 1) return;
530
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();
536         if (stream_info) {
537             selected_streams << stream_info;
538         }
539     }
540
541     // ...and compare them to our unselected streams.
542     QTreeWidgetItemIterator iter(ui->streamTreeWidget, QTreeWidgetItemIterator::Unselected);
543     while (*iter) {
544         RtpStreamTreeWidgetItem *rsti = static_cast<RtpStreamTreeWidgetItem*>(*iter);
545         rtp_stream_info_t *stream_info = rsti->streamInfo();
546         if (stream_info) {
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);
550                 }
551             }
552         }
553         ++iter;
554     }
555 }
556
557 void RtpStreamDialog::on_actionGoToSetup_triggered()
558 {
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();
564     if (stream_info) {
565         emit goToPacket(stream_info->setup_frame_number);
566     }
567 }
568
569 void RtpStreamDialog::on_actionMarkPackets_triggered()
570 {
571     if (ui->streamTreeWidget->selectedItems().count() < 1) return;
572     rtp_stream_info_t *stream_a, *stream_b = NULL;
573
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();
581     }
582
583     if (stream_a == NULL && stream_b == NULL) return;
584
585     // XXX Mark the setup frame as well?
586     need_redraw_ = false;
587     rtpstream_mark(&tapinfo_, cap_file_.capFile(), stream_a, stream_b);
588     updateWidgets();
589 }
590
591 void RtpStreamDialog::on_actionPrepareFilter_triggered()
592 {
593     if (ui->streamTreeWidget->selectedItems().count() < 1) return;
594
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();
600         if (stream_info) {
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)")
603                              .arg(ip_proto) // %1
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);
609         }
610     }
611     if (stream_filters.length() > 0) {
612         QString filter = stream_filters.join(" || ");
613         remove_tap_listener_rtp_stream(&tapinfo_);
614         emit updateFilter(filter);
615     }
616 }
617
618 void RtpStreamDialog::on_actionSelectNone_triggered()
619 {
620     ui->streamTreeWidget->clearSelection();
621 }
622
623 void RtpStreamDialog::on_streamTreeWidget_itemSelectionChanged()
624 {
625     updateWidgets();
626 }
627
628 void RtpStreamDialog::on_buttonBox_clicked(QAbstractButton *button)
629 {
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();
638     }
639 }
640
641 void RtpStreamDialog::on_buttonBox_helpRequested()
642 {
643     wsApp->helpTopicAction(HELP_RTP_ANALYSIS_DIALOG);
644 }
645
646 /*
647  * Editor modelines
648  *
649  * Local Variables:
650  * c-basic-offset: 4
651  * tab-width: 8
652  * indent-tabs-mode: nil
653  * End:
654  *
655  * ex: set shiftwidth=4 tabstop=8 expandtab:
656  * :indentSize=4:tabSize=8:noTabs=true:
657  */