93148319dfdf3da187a8f7c3e633357cba3a7d17
[gd/wireshark/.git] / ui / qt / rtp_player_dialog.cpp
1 /* rtp_player_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_player_dialog.h"
11 #include <ui_rtp_player_dialog.h>
12
13 #ifdef QT_MULTIMEDIA_LIB
14
15 #include <epan/dissectors/packet-rtp.h>
16
17 #include <wsutil/report_message.h>
18 #include <wsutil/utf8_entities.h>
19
20 #include <ui/qt/utils/color_utils.h>
21 #include <ui/qt/widgets/qcustomplot.h>
22 #include <ui/qt/utils/qt_ui_utils.h>
23 #include "rtp_audio_stream.h"
24 #include <ui/qt/utils/tango_colors.h>
25
26 #include <QAudio>
27 #include <QAudioDeviceInfo>
28 #include <QFrame>
29 #include <QMenu>
30 #include <QVBoxLayout>
31
32 #endif // QT_MULTIMEDIA_LIB
33
34 #include <QPushButton>
35
36 #include <ui/qt/utils/stock_icon.h>
37 #include "wireshark_application.h"
38
39 // To do:
40 // - Fully implement shorcuts (drag, go to packet, etc.)
41 // - Figure out selection and highlighting.
42 // - Make streams checkable.
43 // - Add silence, drop & jitter indicators to the graph.
44 // - How to handle multiple channels?
45 // - Threaded decoding?
46 // - Play MP3s. As per Zawinski's Law we already read emails.
47 // - RTP audio streams are currently keyed on src addr + src port + dst addr
48 //   + dst port + ssrc. This means that we can have multiple rtp_stream_info
49 //   structs per RtpAudioStream. Should we make them 1:1 instead?
50
51 // Current and former RTP player bugs. Many have attachments that can be usef for testing.
52 // Bug 3368 - The timestamp line in a RTP or RTCP packet display's "Not Representable"
53 // Bug 3952 - VoIP Call RTP Player: audio played is corrupted when RFC2833 packets are present
54 // Bug 4960 - RTP Player: Audio and visual feedback get rapidly out of sync
55 // Bug 5527 - Adding arbitrary value to x-axis RTP player
56 // Bug 7935 - Wrong Timestamps in RTP Player-Decode
57 // Bug 8007 - UI gets confused on playing decoded audio in rtp_player
58 // Bug 9007 - Switching SSRC values in RTP stream
59 // Bug 10613 - RTP audio player crashes
60 // Bug 11125 - RTP Player does not show progress in selected stream in Window 7
61 // Bug 11409 - Wireshark crashes when using RTP player
62 // Bug 12166 - RTP audio player crashes
63
64 // XXX It looks like we duplicate some functionality here and in the RTP
65 // analysis code, which has its own routines for writing audio data to a
66 // file.
67
68 // In some places we match by conv/call number, in others we match by first frame.
69
70 enum {
71     src_addr_col_,
72     src_port_col_,
73     dst_addr_col_,
74     dst_port_col_,
75     ssrc_col_,
76     first_pkt_col_,
77     num_pkts_col_,
78     time_span_col_,
79     sample_rate_col_,
80     payload_col_,
81
82     stream_data_col_ = src_addr_col_, // RtpAudioStream
83     graph_data_col_ = src_port_col_ // QCPGraph
84 };
85
86 #ifdef QT_MULTIMEDIA_LIB
87 static const double wf_graph_normal_width_ = 0.5;
88 static const double wf_graph_selected_width_ = 2.0;
89 #endif
90
91 RtpPlayerDialog::RtpPlayerDialog(QWidget &parent, CaptureFile &cf) :
92     WiresharkDialog(parent, cf)
93 #ifdef QT_MULTIMEDIA_LIB
94     , ui(new Ui::RtpPlayerDialog)
95     , start_rel_time_(0.0)
96 #endif // QT_MULTIMEDIA_LIB
97 {
98     ui->setupUi(this);
99     setWindowTitle(wsApp->windowTitleString(tr("RTP Player")));
100     loadGeometry(parent.width(), parent.height());
101
102 #ifdef QT_MULTIMEDIA_LIB
103     ui->splitter->setStretchFactor(0, 3);
104     ui->splitter->setStretchFactor(1, 1);
105
106     ctx_menu_ = new QMenu(this);
107
108     ctx_menu_->addAction(ui->actionZoomIn);
109     ctx_menu_->addAction(ui->actionZoomOut);
110     ctx_menu_->addAction(ui->actionReset);
111     ctx_menu_->addSeparator();
112     ctx_menu_->addAction(ui->actionMoveRight10);
113     ctx_menu_->addAction(ui->actionMoveLeft10);
114     ctx_menu_->addAction(ui->actionMoveRight1);
115     ctx_menu_->addAction(ui->actionMoveLeft1);
116     ctx_menu_->addSeparator();
117     ctx_menu_->addAction(ui->actionGoToPacket);
118     ctx_menu_->addSeparator();
119     ctx_menu_->addAction(ui->actionDragZoom);
120     ctx_menu_->addAction(ui->actionToggleTimeOrigin);
121 //    ctx_menu_->addAction(ui->actionCrosshairs);
122
123     connect(ui->audioPlot, SIGNAL(mouseMove(QMouseEvent*)),
124             this, SLOT(updateHintLabel()));
125     connect(ui->audioPlot, SIGNAL(mousePress(QMouseEvent*)),
126             this, SLOT(graphClicked(QMouseEvent*)));
127
128     cur_play_pos_ = new QCPItemStraightLine(ui->audioPlot);
129     ui->audioPlot->addItem(cur_play_pos_);
130     cur_play_pos_->setVisible(false);
131
132     ui->audioPlot->xAxis->setNumberFormat("gb");
133     ui->audioPlot->xAxis->setNumberPrecision(3);
134     ui->audioPlot->xAxis->setDateTimeFormat("yyyy-MM-dd\nhh:mm:ss.zzz");
135     ui->audioPlot->yAxis->setVisible(false);
136
137     ui->playButton->setIcon(StockIcon("media-playback-start"));
138     ui->stopButton->setIcon(StockIcon("media-playback-stop"));
139
140     // Ordered, unique device names starting with the system default
141     QMap<QString, bool> out_device_map; // true == default device
142     out_device_map.insert(QAudioDeviceInfo::defaultOutputDevice().deviceName(), true);
143     foreach (QAudioDeviceInfo out_device, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
144         if (!out_device_map.contains(out_device.deviceName())) {
145             out_device_map.insert(out_device.deviceName(), false);
146         }
147     }
148
149     foreach (QString out_name, out_device_map.keys()) {
150         ui->outputDeviceComboBox->addItem(out_name);
151         if (out_device_map.value(out_name)) {
152             ui->outputDeviceComboBox->setCurrentIndex(ui->outputDeviceComboBox->count() - 1);
153         }
154     }
155     if (ui->outputDeviceComboBox->count() < 1) {
156         ui->outputDeviceComboBox->setEnabled(false);
157         ui->playButton->setEnabled(false);
158         ui->stopButton->setEnabled(false);
159         ui->outputDeviceComboBox->addItem(tr("No devices available"));
160     }
161
162     ui->audioPlot->setMouseTracking(true);
163     ui->audioPlot->setEnabled(true);
164     ui->audioPlot->setInteractions(
165                 QCP::iRangeDrag |
166                 QCP::iRangeZoom
167                 );
168     ui->audioPlot->setFocus();
169
170     QTimer::singleShot(0, this, SLOT(retapPackets()));
171 #endif // QT_MULTIMEDIA_LIB
172 }
173
174 QPushButton *RtpPlayerDialog::addPlayerButton(QDialogButtonBox *button_box)
175 {
176     if (!button_box) return NULL;
177
178     QPushButton *player_button;
179     player_button = button_box->addButton(tr("Play Streams"), QDialogButtonBox::ApplyRole);
180     player_button->setIcon(StockIcon("media-playback-start"));
181     return player_button;
182 }
183
184 #ifdef QT_MULTIMEDIA_LIB
185 RtpPlayerDialog::~RtpPlayerDialog()
186 {
187     delete ui;
188 }
189
190 void RtpPlayerDialog::accept()
191 {
192     int row_count = ui->streamTreeWidget->topLevelItemCount();
193     // Stop all streams before the dialogs are closed.
194     for (int row = 0; row < row_count; row++) {
195         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
196         RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
197         audio_stream->stopPlaying();
198     }
199     WiresharkDialog::accept();
200 }
201
202 void RtpPlayerDialog::reject()
203 {
204     RtpPlayerDialog::accept();
205 }
206
207 void RtpPlayerDialog::retapPackets()
208 {
209     GString *error_string;
210
211     error_string = register_tap_listener("rtp", this, NULL, 0, NULL, tapPacket, NULL, NULL);
212     if (error_string) {
213         report_failure("RTP Player - tap registration failed: %s", error_string->str);
214         g_string_free(error_string, TRUE);
215         return;
216     }
217     cap_file_.retapPackets();
218     remove_tap_listener(this);
219
220     rescanPackets(true);
221 }
222
223 void RtpPlayerDialog::rescanPackets(bool rescale_axes)
224 {
225     int row_count = ui->streamTreeWidget->topLevelItemCount();
226     // Clear existing graphs and reset stream values
227     for (int row = 0; row < row_count; row++) {
228         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
229         RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
230         audio_stream->reset(start_rel_time_);
231
232         ti->setData(graph_data_col_, Qt::UserRole, QVariant());
233     }
234     ui->audioPlot->clearGraphs();
235
236     bool show_legend = false;
237     bool relative_timestamps = !ui->todCheckBox->isChecked();
238
239     ui->audioPlot->xAxis->setTickLabelType(relative_timestamps ? QCPAxis::ltNumber : QCPAxis::ltDateTime);
240
241     for (int row = 0; row < row_count; row++) {
242         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
243         RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
244         int y_offset = row_count - row - 1;
245
246         audio_stream->setJitterBufferSize((int) ui->jitterSpinBox->value());
247
248         RtpAudioStream::TimingMode timing_mode = RtpAudioStream::JitterBuffer;
249         switch (ui->timingComboBox->currentIndex()) {
250         case RtpAudioStream::RtpTimestamp:
251             timing_mode = RtpAudioStream::RtpTimestamp;
252             break;
253         case RtpAudioStream::Uninterrupted:
254             timing_mode = RtpAudioStream::Uninterrupted;
255             break;
256         default:
257             break;
258         }
259         audio_stream->setTimingMode(timing_mode);
260
261         audio_stream->decode();
262
263         // Waveform
264         QCPGraph *audio_graph = ui->audioPlot->addGraph();
265         QPen wf_pen(audio_stream->color());
266         wf_pen.setWidthF(wf_graph_normal_width_);
267         audio_graph->setPen(wf_pen);
268         wf_pen.setWidthF(wf_graph_selected_width_);
269         audio_graph->setSelectedPen(wf_pen);
270         audio_graph->setSelectable(false);
271         audio_graph->setData(audio_stream->visualTimestamps(relative_timestamps), audio_stream->visualSamples(y_offset));
272         audio_graph->removeFromLegend();
273         ti->setData(graph_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(audio_graph));
274         RTP_STREAM_DEBUG("Plotting %s, %d samples", ti->text(src_addr_col_).toUtf8().constData(), audio_graph->data()->keys().length());
275
276         QString span_str = QString("%1 - %2 (%3)")
277                 .arg(QString::number(audio_stream->startRelTime(), 'g', 3))
278                 .arg(QString::number(audio_stream->stopRelTime(), 'g', 3))
279                 .arg(QString::number(audio_stream->stopRelTime() - audio_stream->startRelTime(), 'g', 3));
280         ti->setText(time_span_col_, span_str);
281         ti->setText(sample_rate_col_, QString::number(audio_stream->sampleRate()));
282         ti->setText(payload_col_, audio_stream->payloadNames().join(", "));
283
284         if (audio_stream->outOfSequence() > 0) {
285             // Sequence numbers
286             QCPGraph *seq_graph = ui->audioPlot->addGraph();
287             seq_graph->setLineStyle(QCPGraph::lsNone);
288             seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssSquare, tango_aluminium_6, Qt::white, 4)); // Arbitrary
289             seq_graph->setSelectable(false);
290             seq_graph->setData(audio_stream->outOfSequenceTimestamps(relative_timestamps), audio_stream->outOfSequenceSamples(y_offset));
291             if (row < 1) {
292                 seq_graph->setName(tr("Out of Sequence"));
293                 show_legend = true;
294             } else {
295                 seq_graph->removeFromLegend();
296             }
297         }
298
299         if (audio_stream->jitterDropped() > 0) {
300             // Jitter drops
301             QCPGraph *seq_graph = ui->audioPlot->addGraph();
302             seq_graph->setLineStyle(QCPGraph::lsNone);
303             seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, tango_scarlet_red_5, Qt::white, 4)); // Arbitrary
304             seq_graph->setSelectable(false);
305             seq_graph->setData(audio_stream->jitterDroppedTimestamps(relative_timestamps), audio_stream->jitterDroppedSamples(y_offset));
306             if (row < 1) {
307                 seq_graph->setName(tr("Jitter Drops"));
308                 show_legend = true;
309             } else {
310                 seq_graph->removeFromLegend();
311             }
312         }
313
314         if (audio_stream->wrongTimestamps() > 0) {
315             // Wrong timestamps
316             QCPGraph *seq_graph = ui->audioPlot->addGraph();
317             seq_graph->setLineStyle(QCPGraph::lsNone);
318             seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDiamond, tango_sky_blue_5, Qt::white, 4)); // Arbitrary
319             seq_graph->setSelectable(false);
320             seq_graph->setData(audio_stream->wrongTimestampTimestamps(relative_timestamps), audio_stream->wrongTimestampSamples(y_offset));
321             if (row < 1) {
322                 seq_graph->setName(tr("Wrong Timestamps"));
323                 show_legend = true;
324             } else {
325                 seq_graph->removeFromLegend();
326             }
327         }
328
329         if (audio_stream->insertedSilences() > 0) {
330             // Inserted silence
331             QCPGraph *seq_graph = ui->audioPlot->addGraph();
332             seq_graph->setLineStyle(QCPGraph::lsNone);
333             seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, tango_butter_5, Qt::white, 4)); // Arbitrary
334             seq_graph->setSelectable(false);
335             seq_graph->setData(audio_stream->insertedSilenceTimestamps(relative_timestamps), audio_stream->insertedSilenceSamples(y_offset));
336             if (row < 1) {
337                 seq_graph->setName(tr("Inserted Silence"));
338                 show_legend = true;
339             } else {
340                 seq_graph->removeFromLegend();
341             }
342         }
343     }
344     ui->audioPlot->legend->setVisible(show_legend);
345
346     for (int col = 0; col < ui->streamTreeWidget->columnCount() - 1; col++) {
347         ui->streamTreeWidget->resizeColumnToContents(col);
348     }
349
350     ui->audioPlot->replot();
351     if (rescale_axes) resetXAxis();
352
353     updateWidgets();
354 }
355
356 void RtpPlayerDialog::addRtpStream(rtpstream_info_t *rtpstream)
357 {
358     if (!rtpstream) return;
359
360     // Find the RTP streams associated with this conversation.
361     // gtk/rtp_player.c:mark_rtp_stream_to_play does this differently.
362
363     RtpAudioStream *audio_stream = NULL;
364     int tli_count = ui->streamTreeWidget->topLevelItemCount();
365     for (int row = 0; row < tli_count; row++) {
366         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
367         RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
368         if (row_stream->isMatch(rtpstream)) {
369             audio_stream = row_stream;
370             break;
371         }
372     }
373
374     if (!audio_stream) {
375         audio_stream = new RtpAudioStream(this, rtpstream);
376         audio_stream->setColor(ColorUtils::graphColor(tli_count));
377
378         QTreeWidgetItem *ti = new QTreeWidgetItem(ui->streamTreeWidget);
379         ti->setText(src_addr_col_, address_to_qstring(&rtpstream->id.src_addr));
380         ti->setText(src_port_col_, QString::number(rtpstream->id.src_port));
381         ti->setText(dst_addr_col_, address_to_qstring(&rtpstream->id.dst_addr));
382         ti->setText(dst_port_col_, QString::number(rtpstream->id.dst_port));
383         ti->setText(ssrc_col_, int_to_qstring(rtpstream->id.ssrc, 8, 16));
384         ti->setText(first_pkt_col_, QString::number(rtpstream->setup_frame_number));
385         ti->setText(num_pkts_col_, QString::number(rtpstream->packet_count));
386
387         ti->setData(stream_data_col_, Qt::UserRole, QVariant::fromValue(audio_stream));
388
389         for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
390             ti->setTextColor(col, audio_stream->color());
391         }
392
393         connect(ui->playButton, SIGNAL(clicked(bool)), audio_stream, SLOT(startPlaying()));
394         connect(ui->stopButton, SIGNAL(clicked(bool)), audio_stream, SLOT(stopPlaying()));
395
396         connect(audio_stream, SIGNAL(startedPlaying()), this, SLOT(updateWidgets()));
397         connect(audio_stream, SIGNAL(finishedPlaying()), this, SLOT(updateWidgets()));
398         connect(audio_stream, SIGNAL(playbackError(QString)), this, SLOT(setPlaybackError(QString)));
399         connect(audio_stream, SIGNAL(processedSecs(double)), this, SLOT(setPlayPosition(double)));
400     }
401     // TODO: does not do anything
402     // audio_stream->addRtpStream(rtpstream);
403     double start_rel_time = nstime_to_sec(&rtpstream->start_rel_time);
404     if (tli_count < 2) {
405         start_rel_time_ = start_rel_time;
406     } else {
407         start_rel_time_ = qMin(start_rel_time_, start_rel_time);
408     }
409     RTP_STREAM_DEBUG("adding stream %d to layout, %u packets, start %u",
410                      ui->streamTreeWidget->topLevelItemCount(),
411                      rtpstream->packet_count,
412                      rtpstream->start_fd ? rtpstream->start_fd->num : 0);
413 }
414
415 void RtpPlayerDialog::showEvent(QShowEvent *)
416 {
417     QList<int> split_sizes = ui->splitter->sizes();
418     int tot_size = split_sizes[0] + split_sizes[1];
419     int plot_size = tot_size * 3 / 4;
420     split_sizes.clear();
421     split_sizes << plot_size << tot_size - plot_size;
422     ui->splitter->setSizes(split_sizes);
423 }
424
425 void RtpPlayerDialog::keyPressEvent(QKeyEvent *event)
426 {
427     int pan_secs = event->modifiers() & Qt::ShiftModifier ? 1 : 10;
428
429     switch(event->key()) {
430     case Qt::Key_Minus:
431     case Qt::Key_Underscore:    // Shifted minus on U.S. keyboards
432     case Qt::Key_O:             // GTK+
433     case Qt::Key_R:
434         on_actionZoomOut_triggered();
435         break;
436     case Qt::Key_Plus:
437     case Qt::Key_Equal:         // Unshifted plus on U.S. keyboards
438     case Qt::Key_I:             // GTK+
439         on_actionZoomIn_triggered();
440         break;
441
442     case Qt::Key_Right:
443     case Qt::Key_L:
444         panXAxis(pan_secs);
445         break;
446     case Qt::Key_Left:
447     case Qt::Key_H:
448         panXAxis(-1 * pan_secs);
449         break;
450
451     case Qt::Key_Space:
452 //        toggleTracerStyle();
453         break;
454
455     case Qt::Key_0:
456     case Qt::Key_ParenRight:    // Shifted 0 on U.S. keyboards
457     case Qt::Key_Home:
458         on_actionReset_triggered();
459         break;
460
461     case Qt::Key_G:
462         on_actionGoToPacket_triggered();
463         break;
464     case Qt::Key_T:
465 //        on_actionToggleTimeOrigin_triggered();
466         break;
467     case Qt::Key_Z:
468 //        on_actionDragZoom_triggered();
469         break;
470     }
471
472     QDialog::keyPressEvent(event);
473 }
474
475 void RtpPlayerDialog::updateWidgets()
476 {
477     bool enable_play = true;
478     bool enable_stop = false;
479     bool enable_timing = true;
480
481     for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
482         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
483
484         RtpAudioStream *audio_stream = ti->data(src_addr_col_, Qt::UserRole).value<RtpAudioStream*>();
485         if (audio_stream->outputState() != QAudio::IdleState) {
486             enable_play = false;
487             enable_stop = true;
488             enable_timing = false;
489         }
490     }
491
492     ui->playButton->setEnabled(enable_play);
493     ui->outputDeviceComboBox->setEnabled(enable_play);
494     ui->stopButton->setEnabled(enable_stop);
495     cur_play_pos_->setVisible(enable_stop);
496
497     ui->jitterSpinBox->setEnabled(enable_timing);
498     ui->timingComboBox->setEnabled(enable_timing);
499     ui->todCheckBox->setEnabled(enable_timing);
500
501     updateHintLabel();
502     ui->audioPlot->replot();
503 }
504
505 void RtpPlayerDialog::graphClicked(QMouseEvent *event)
506 {
507     updateWidgets();
508     if (event->button() == Qt::RightButton) {
509         ctx_menu_->exec(event->globalPos());
510     }
511     ui->audioPlot->setFocus();
512 }
513
514 void RtpPlayerDialog::updateHintLabel()
515 {
516     int packet_num = getHoveredPacket();
517     QString hint = "<small><i>";
518
519     if (packet_num > 0) {
520         hint += tr("%1. Press \"G\" to go to packet %2")
521                 .arg(getHoveredTime())
522                 .arg(packet_num);
523     } else if (!playback_error_.isEmpty()) {
524         hint += playback_error_;
525     }
526
527     hint += "</i></small>";
528     ui->hintLabel->setText(hint);
529 }
530
531 void RtpPlayerDialog::resetXAxis()
532 {
533     QCustomPlot *ap = ui->audioPlot;
534     QCPRange x_range = ap->xAxis->range();
535
536     double pixel_pad = 10.0; // per side
537
538     ap->rescaleAxes(true);
539
540     double axis_pixels = ap->xAxis->axisRect()->width();
541     ap->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, x_range.center());
542
543     axis_pixels = ap->yAxis->axisRect()->height();
544     ap->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->yAxis->range().center());
545
546     ap->replot();
547 }
548
549 void RtpPlayerDialog::setPlayPosition(double secs)
550 {
551     secs+= start_rel_time_;
552     double cur_secs = cur_play_pos_->point1->key();
553     if (secs > cur_secs) {
554         cur_play_pos_->point1->setCoords(secs, 0.0);
555         cur_play_pos_->point2->setCoords(secs, 1.0);
556         ui->audioPlot->replot();
557     }
558 }
559
560 gboolean RtpPlayerDialog::tapPacket(void *tapinfo_ptr, packet_info *pinfo, epan_dissect_t *, const void *rtpinfo_ptr)
561 {
562     RtpPlayerDialog *rtp_player_dialog = dynamic_cast<RtpPlayerDialog *>((RtpPlayerDialog*)tapinfo_ptr);
563     if (!rtp_player_dialog) return FALSE;
564
565     const struct _rtp_info *rtpinfo = (const struct _rtp_info *)rtpinfo_ptr;
566     if (!rtpinfo) return FALSE;
567
568     /* we ignore packets that are not displayed */
569     if (pinfo->fd->flags.passed_dfilter == 0)
570         return FALSE;
571     /* also ignore RTP Version != 2 */
572     else if (rtpinfo->info_version != 2)
573         return FALSE;
574
575     rtp_player_dialog->addPacket(pinfo, rtpinfo);
576
577     return FALSE;
578 }
579
580 void RtpPlayerDialog::addPacket(packet_info *pinfo, const _rtp_info *rtpinfo)
581 {
582     for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
583         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
584         RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
585
586         if (row_stream->isMatch(pinfo, rtpinfo)) {
587             row_stream->addRtpPacket(pinfo, rtpinfo);
588             return;
589         }
590     }
591 //    qDebug() << "=ap no match!" << address_to_qstring(&pinfo->src) << address_to_qstring(&pinfo->dst);
592 }
593
594 void RtpPlayerDialog::zoomXAxis(bool in)
595 {
596     QCustomPlot *ap = ui->audioPlot;
597     double h_factor = ap->axisRect()->rangeZoomFactor(Qt::Horizontal);
598
599     if (!in) {
600         h_factor = pow(h_factor, -1);
601     }
602
603     ap->xAxis->scaleRange(h_factor, ap->xAxis->range().center());
604     ap->replot();
605 }
606
607 // XXX I tried using seconds but pixels make more sense at varying zoom
608 // levels.
609 void RtpPlayerDialog::panXAxis(int x_pixels)
610 {
611     QCustomPlot *ap = ui->audioPlot;
612     double h_pan;
613
614     h_pan = ap->xAxis->range().size() * x_pixels / ap->xAxis->axisRect()->width();
615     if (x_pixels) {
616         ap->xAxis->moveRange(h_pan);
617         ap->replot();
618     }
619 }
620
621 void RtpPlayerDialog::on_playButton_clicked()
622 {
623     double left = start_rel_time_;
624     cur_play_pos_->point1->setCoords(left, 0.0);
625     cur_play_pos_->point2->setCoords(left, 1.0);
626     cur_play_pos_->setVisible(true);
627     playback_error_.clear();
628     ui->audioPlot->replot();
629 }
630
631 void RtpPlayerDialog::on_stopButton_clicked()
632 {
633     cur_play_pos_->setVisible(false);
634 }
635
636 void RtpPlayerDialog::on_actionReset_triggered()
637 {
638     resetXAxis();
639 }
640
641 void RtpPlayerDialog::on_actionZoomIn_triggered()
642 {
643     zoomXAxis(true);
644 }
645
646 void RtpPlayerDialog::on_actionZoomOut_triggered()
647 {
648     zoomXAxis(false);
649 }
650
651 void RtpPlayerDialog::on_actionMoveLeft10_triggered()
652 {
653     panXAxis(-10);
654 }
655
656 void RtpPlayerDialog::on_actionMoveRight10_triggered()
657 {
658     panXAxis(10);
659 }
660
661 void RtpPlayerDialog::on_actionMoveLeft1_triggered()
662 {
663     panXAxis(-1);
664 }
665
666 void RtpPlayerDialog::on_actionMoveRight1_triggered()
667 {
668     panXAxis(1);
669 }
670
671 void RtpPlayerDialog::on_actionGoToPacket_triggered()
672 {
673     int packet_num = getHoveredPacket();
674     if (packet_num > 0) emit goToPacket(packet_num);
675 }
676
677 // XXX Make waveform graphs selectable and update the treewidget selection accordingly.
678 void RtpPlayerDialog::on_streamTreeWidget_itemSelectionChanged()
679 {
680     for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
681         QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
682         QCPGraph *audio_graph = ti->data(graph_data_col_, Qt::UserRole).value<QCPGraph*>();
683         if (audio_graph) {
684             audio_graph->setSelected(ti->isSelected());
685         }
686     }
687     ui->audioPlot->replot();
688 }
689
690 double RtpPlayerDialog::getLowestTimestamp()
691 {
692     double lowest = QCPRange::maxRange;
693
694     for (int i = 0; i < ui->audioPlot->graphCount(); i++) {
695         QCPGraph *graph = ui->audioPlot->graph(i);
696         if (!graph->visible()) continue;
697         QCPDataMap *dm = graph->data();
698         if (dm->keys().length() < 1) continue;
699         lowest = qMin(lowest, dm->keys().first());
700     }
701     return lowest;
702 }
703
704 const QString RtpPlayerDialog::getHoveredTime()
705 {
706     QTreeWidgetItem *ti = ui->streamTreeWidget->currentItem();
707     if (!ti) return tr("Unknown");
708
709     QString time_str;
710     double ts = ui->audioPlot->xAxis->pixelToCoord(ui->audioPlot->mapFromGlobal(QCursor::pos()).x());
711
712     if (ui->todCheckBox->isChecked()) {
713         QDateTime date_time = QDateTime::fromMSecsSinceEpoch(ts * 1000.0);
714         time_str = date_time.toString("yyyy-MM-dd hh:mm:ss.zzz");
715     } else {
716         time_str = QString::number(ts, 'f', 3);
717         time_str += " s";
718     }
719     return time_str;
720 }
721
722 int RtpPlayerDialog::getHoveredPacket()
723 {
724     QTreeWidgetItem *ti = ui->streamTreeWidget->currentItem();
725     if (!ti) return 0;
726
727     RtpAudioStream *audio_stream = ti->data(src_addr_col_, Qt::UserRole).value<RtpAudioStream*>();
728
729     double ts = ui->audioPlot->xAxis->pixelToCoord(ui->audioPlot->mapFromGlobal(QCursor::pos()).x());
730
731     return audio_stream->nearestPacket(ts, !ui->todCheckBox->isChecked());
732 }
733
734 // Used by RtpAudioStreams to initialize QAudioOutput. We could alternatively
735 // pass the corresponding QAudioDeviceInfo directly.
736 QString RtpPlayerDialog::currentOutputDeviceName()
737 {
738     return ui->outputDeviceComboBox->currentText();
739 }
740
741 void RtpPlayerDialog::on_outputDeviceComboBox_currentIndexChanged(const QString &)
742 {
743     rescanPackets();
744 }
745
746 void RtpPlayerDialog::on_jitterSpinBox_valueChanged(double)
747 {
748     rescanPackets();
749 }
750
751 void RtpPlayerDialog::on_timingComboBox_currentIndexChanged(int)
752 {
753     rescanPackets();
754 }
755
756 void RtpPlayerDialog::on_todCheckBox_toggled(bool)
757 {
758     QCPAxis *x_axis = ui->audioPlot->xAxis;
759     double old_lowest = getLowestTimestamp();
760
761     rescanPackets();
762     x_axis->moveRange(getLowestTimestamp() - old_lowest);
763     ui->audioPlot->replot();
764 }
765
766 void RtpPlayerDialog::on_buttonBox_helpRequested()
767 {
768     wsApp->helpTopicAction(HELP_TELEPHONY_RTP_PLAYER_DIALOG);
769 }
770
771 #if 0
772 // This also serves as a title in RtpAudioFrame.
773 static const QString stream_key_tmpl_ = "%1:%2 " UTF8_RIGHTWARDS_ARROW " %3:%4 0x%5";
774 const QString RtpPlayerDialog::streamKey(const rtpstream_info_t *rtpstream)
775 {
776     const QString stream_key = QString(stream_key_tmpl_)
777             .arg(address_to_display_qstring(&rtpstream->src_addr))
778             .arg(rtpstream->src_port)
779             .arg(address_to_display_qstring(&rtpstream->dst_addr))
780             .arg(rtpstream->dst_port)
781             .arg(rtpstream->ssrc, 0, 16);
782     return stream_key;
783 }
784
785 const QString RtpPlayerDialog::streamKey(const packet_info *pinfo, const struct _rtp_info *rtpinfo)
786 {
787     const QString stream_key = QString(stream_key_tmpl_)
788             .arg(address_to_display_qstring(&pinfo->src))
789             .arg(pinfo->srcport)
790             .arg(address_to_display_qstring(&pinfo->dst))
791             .arg(pinfo->destport)
792             .arg(rtpinfo->info_sync_src, 0, 16);
793     return stream_key;
794 }
795 #endif
796
797 #endif // QT_MULTIMEDIA_LIB
798
799 /*
800  * Editor modelines
801  *
802  * Local Variables:
803  * c-basic-offset: 4
804  * tab-width: 8
805  * indent-tabs-mode: nil
806  * End:
807  *
808  * ex: set shiftwidth=4 tabstop=8 expandtab:
809  * :indentSize=4:tabSize=8:noTabs=true:
810  */