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 "config.h"
11
12 #include <ui/rtp_media.h>
13 #include <ui/tap-rtp-common.h>
14 #include "rtp_player_dialog.h"
15 #include <ui_rtp_player_dialog.h>
16 #include "epan/epan_dissect.h"
17
18 #include "file.h"
19 #include "frame_tvbuff.h"
20
21 #include "rtp_analysis_dialog.h"
22
23 #ifdef QT_MULTIMEDIA_LIB
24
25 #include <epan/dissectors/packet-rtp.h>
26 #include <epan/to_str.h>
27
28 #include <wsutil/report_message.h>
29 #include <wsutil/utf8_entities.h>
30 #include <wsutil/pint.h>
31
32 #include <ui/qt/utils/color_utils.h>
33 #include <ui/qt/widgets/qcustomplot.h>
34 #include <ui/qt/utils/qt_ui_utils.h>
35 #include "rtp_audio_stream.h"
36 #include <ui/qt/utils/tango_colors.h>
37 #include <widgets/rtp_audio_graph.h>
38 #include "wireshark_application.h"
39 #include "ui/qt/widgets/wireshark_file_dialog.h"
40
41 #include <QAudio>
42 #include <QAudioDeviceInfo>
43 #include <QFrame>
44 #include <QMenu>
45 #include <QVBoxLayout>
46 #include <QTimer>
47
48 #include <QAudioFormat>
49 #include <QAudioOutput>
50 #include <ui/qt/utils/rtp_audio_silence_generator.h>
51
52 #endif // QT_MULTIMEDIA_LIB
53
54 #include <QPushButton>
55 #include <QToolButton>
56
57 #include <ui/qt/utils/stock_icon.h>
58 #include "wireshark_application.h"
59
60 // To do:
61 // - Threaded decoding?
62
63 // Current and former RTP player bugs. Many have attachments that can be usef for testing.
64 // Bug 3368 - The timestamp line in a RTP or RTCP packet display's "Not Representable"
65 // Bug 3952 - VoIP Call RTP Player: audio played is corrupted when RFC2833 packets are present
66 // Bug 4960 - RTP Player: Audio and visual feedback get rapidly out of sync
67 // Bug 5527 - Adding arbitrary value to x-axis RTP player
68 // Bug 7935 - Wrong Timestamps in RTP Player-Decode
69 // Bug 8007 - UI gets confused on playing decoded audio in rtp_player
70 // Bug 9007 - Switching SSRC values in RTP stream
71 // Bug 10613 - RTP audio player crashes
72 // Bug 11125 - RTP Player does not show progress in selected stream in Window 7
73 // Bug 11409 - Wireshark crashes when using RTP player
74 // Bug 12166 - RTP audio player crashes
75
76 // In some places we match by conv/call number, in others we match by first frame.
77
78 enum {
79 channel_col_,
80 src_addr_col_,
81 src_port_col_,
82 dst_addr_col_,
83 dst_port_col_,
84 ssrc_col_,
85 first_pkt_col_,
86 num_pkts_col_,
87 time_span_col_,
88 sample_rate_col_,
89 play_rate_col_,
90 payload_col_,
91
92 stream_data_col_ = src_addr_col_, // RtpAudioStream
93 graph_audio_data_col_ = src_port_col_, // QCPGraph (wave)
94 graph_sequence_data_col_ = dst_addr_col_, // QCPGraph (sequence)
95 graph_jitter_data_col_ = dst_port_col_, // QCPGraph (jitter)
96 graph_timestamp_data_col_ = ssrc_col_, // QCPGraph (timestamp)
97 // first_pkt_col_ is skipped, it is used for real data
98 graph_silence_data_col_ = num_pkts_col_, // QCPGraph (silence)
99 };
100
101 class RtpPlayerTreeWidgetItem : public QTreeWidgetItem
102 {
103 public:
RtpPlayerTreeWidgetItem(QTreeWidget * tree)104 RtpPlayerTreeWidgetItem(QTreeWidget *tree) :
105 QTreeWidgetItem(tree)
106 {
107 }
108
operator <(const QTreeWidgetItem & other) const109 bool operator< (const QTreeWidgetItem &other) const
110 {
111 // Handle numeric sorting
112 switch (treeWidget()->sortColumn()) {
113 case src_port_col_:
114 case dst_port_col_:
115 case num_pkts_col_:
116 case sample_rate_col_:
117 return text(treeWidget()->sortColumn()).toInt() < other.text(treeWidget()->sortColumn()).toInt();
118 case play_rate_col_:
119 return text(treeWidget()->sortColumn()).toInt() < other.text(treeWidget()->sortColumn()).toInt();
120 case first_pkt_col_:
121 int v1;
122 int v2;
123
124 v1 = data(first_pkt_col_, Qt::UserRole).toInt();
125 v2 = other.data(first_pkt_col_, Qt::UserRole).toInt();
126
127 return v1 < v2;
128 default:
129 // Fall back to string comparison
130 return QTreeWidgetItem::operator <(other);
131 break;
132 }
133 }
134 };
135
136
137 RtpPlayerDialog *RtpPlayerDialog::pinstance_{nullptr};
138 std::mutex RtpPlayerDialog::mutex_;
139
openRtpPlayerDialog(QWidget & parent,CaptureFile & cf,QObject * packet_list,bool capture_running)140 RtpPlayerDialog *RtpPlayerDialog::openRtpPlayerDialog(QWidget &parent, CaptureFile &cf, QObject *packet_list, bool capture_running)
141 {
142 std::lock_guard<std::mutex> lock(mutex_);
143 if (pinstance_ == nullptr)
144 {
145 pinstance_ = new RtpPlayerDialog(parent, cf, capture_running);
146 connect(pinstance_, SIGNAL(goToPacket(int)),
147 packet_list, SLOT(goToPacket(int)));
148 }
149 return pinstance_;
150 }
151
RtpPlayerDialog(QWidget & parent,CaptureFile & cf,bool capture_running)152 RtpPlayerDialog::RtpPlayerDialog(QWidget &parent, CaptureFile &cf, bool capture_running) :
153 WiresharkDialog(parent, cf)
154 #ifdef QT_MULTIMEDIA_LIB
155 , ui(new Ui::RtpPlayerDialog)
156 , first_stream_rel_start_time_(0.0)
157 , first_stream_abs_start_time_(0.0)
158 , first_stream_rel_stop_time_(0.0)
159 , streams_length_(0.0)
160 , start_marker_time_(0.0)
161 #endif // QT_MULTIMEDIA_LIB
162 , number_ticker_(new QCPAxisTicker)
163 , datetime_ticker_(new QCPAxisTickerDateTime)
164 , stereo_available_(false)
165 , marker_stream_(0)
166 , marker_stream_requested_out_rate_(0)
167 , last_ti_(0)
168 , listener_removed_(true)
169 , block_redraw_(false)
170 , lock_ui_(0)
171 , read_capture_enabled_(capture_running)
172 , silence_skipped_time_(0.0)
173 {
174 ui->setupUi(this);
175 loadGeometry(parent.width(), parent.height());
176 setWindowTitle(wsApp->windowTitleString(tr("RTP Player")));
177 ui->streamTreeWidget->installEventFilter(this);
178 ui->audioPlot->installEventFilter(this);
179 installEventFilter(this);
180
181 #ifdef QT_MULTIMEDIA_LIB
182 ui->splitter->setStretchFactor(0, 3);
183 ui->splitter->setStretchFactor(1, 1);
184
185 ui->streamTreeWidget->sortByColumn(first_pkt_col_, Qt::AscendingOrder);
186
187 graph_ctx_menu_ = new QMenu(this);
188
189 graph_ctx_menu_->addAction(ui->actionZoomIn);
190 graph_ctx_menu_->addAction(ui->actionZoomOut);
191 graph_ctx_menu_->addAction(ui->actionReset);
192 graph_ctx_menu_->addSeparator();
193 graph_ctx_menu_->addAction(ui->actionMoveRight10);
194 graph_ctx_menu_->addAction(ui->actionMoveLeft10);
195 graph_ctx_menu_->addAction(ui->actionMoveRight1);
196 graph_ctx_menu_->addAction(ui->actionMoveLeft1);
197 graph_ctx_menu_->addSeparator();
198 graph_ctx_menu_->addAction(ui->actionGoToPacket);
199 graph_ctx_menu_->addAction(ui->actionGoToSetupPacketPlot);
200 set_action_shortcuts_visible_in_context_menu(graph_ctx_menu_->actions());
201
202 ui->streamTreeWidget->setMouseTracking(true);
203 connect(ui->streamTreeWidget, SIGNAL(itemEntered(QTreeWidgetItem *, int)),
204 this, SLOT(itemEntered(QTreeWidgetItem *, int)));
205
206 connect(ui->audioPlot, SIGNAL(mouseMove(QMouseEvent*)),
207 this, SLOT(mouseMovePlot(QMouseEvent*)));
208 connect(ui->audioPlot, SIGNAL(mouseMove(QMouseEvent*)),
209 this, SLOT(mouseMovePlot(QMouseEvent*)));
210 connect(ui->audioPlot, SIGNAL(mousePress(QMouseEvent*)),
211 this, SLOT(graphClicked(QMouseEvent*)));
212 connect(ui->audioPlot, SIGNAL(mouseDoubleClick(QMouseEvent*)),
213 this, SLOT(graphDoubleClicked(QMouseEvent*)));
214 connect(ui->audioPlot, SIGNAL(plottableClick(QCPAbstractPlottable*,int,QMouseEvent*)),
215 this, SLOT(plotClicked(QCPAbstractPlottable*,int,QMouseEvent*)));
216
217 cur_play_pos_ = new QCPItemStraightLine(ui->audioPlot);
218 cur_play_pos_->setVisible(false);
219
220 start_marker_pos_ = new QCPItemStraightLine(ui->audioPlot);
221 start_marker_pos_->setPen(QPen(Qt::green,4));
222 setStartPlayMarker(0);
223 drawStartPlayMarker();
224 start_marker_pos_->setVisible(true);
225
226 datetime_ticker_->setDateTimeFormat("yyyy-MM-dd\nhh:mm:ss.zzz");
227
228 ui->audioPlot->xAxis->setNumberFormat("gb");
229 ui->audioPlot->xAxis->setNumberPrecision(3);
230 ui->audioPlot->xAxis->setTicker(datetime_ticker_);
231 ui->audioPlot->yAxis->setVisible(false);
232
233 ui->playButton->setIcon(StockIcon("media-playback-start"));
234 ui->playButton->setEnabled(false);
235 ui->pauseButton->setIcon(StockIcon("media-playback-pause"));
236 ui->pauseButton->setCheckable(true);
237 ui->pauseButton->setVisible(false);
238 ui->stopButton->setIcon(StockIcon("media-playback-stop"));
239 ui->stopButton->setEnabled(false);
240 ui->skipSilenceButton->setIcon(StockIcon("media-seek-forward"));
241 ui->skipSilenceButton->setCheckable(true);
242 ui->skipSilenceButton->setEnabled(false);
243
244 read_btn_ = ui->buttonBox->addButton(ui->actionReadCapture->text(), QDialogButtonBox::ActionRole);
245 read_btn_->setToolTip(ui->actionReadCapture->toolTip());
246 read_btn_->setEnabled(false);
247 connect(read_btn_, SIGNAL(pressed()), this, SLOT(on_actionReadCapture_triggered()));
248
249 inaudible_btn_ = new QToolButton();
250 ui->buttonBox->addButton(inaudible_btn_, QDialogButtonBox::ActionRole);
251 inaudible_btn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
252 inaudible_btn_->setPopupMode(QToolButton::MenuButtonPopup);
253
254 connect(ui->actionInaudibleButton, SIGNAL(triggered()), this, SLOT(on_actionSelectInaudible_triggered()));
255 inaudible_btn_->setDefaultAction(ui->actionInaudibleButton);
256 // Overrides text striping of shortcut undercode in QAction
257 inaudible_btn_->setText(ui->actionInaudibleButton->text());
258 inaudible_btn_->setEnabled(false);
259 inaudible_btn_->setMenu(ui->menuInaudible);
260
261 analyze_btn_ = RtpAnalysisDialog::addAnalyzeButton(ui->buttonBox, this);
262
263 prepare_btn_ = ui->buttonBox->addButton(ui->actionPrepareFilter->text(), QDialogButtonBox::ActionRole);
264 prepare_btn_->setToolTip(ui->actionPrepareFilter->toolTip());
265 connect(prepare_btn_, SIGNAL(pressed()), this, SLOT(on_actionPrepareFilter_triggered()));
266
267 export_btn_ = ui->buttonBox->addButton(ui->actionExportButton->text(), QDialogButtonBox::ActionRole);
268 export_btn_->setToolTip(ui->actionExportButton->toolTip());
269 export_btn_->setEnabled(false);
270 export_btn_->setMenu(ui->menuExport);
271
272 // Ordered, unique device names starting with the system default
273 QMap<QString, bool> out_device_map; // true == default device
274 out_device_map.insert(QAudioDeviceInfo::defaultOutputDevice().deviceName(), true);
275 foreach (QAudioDeviceInfo out_device, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
276 if (!out_device_map.contains(out_device.deviceName())) {
277 out_device_map.insert(out_device.deviceName(), false);
278 }
279 }
280
281 ui->outputDeviceComboBox->blockSignals(true);
282 foreach (QString out_name, out_device_map.keys()) {
283 ui->outputDeviceComboBox->addItem(out_name);
284 if (out_device_map.value(out_name)) {
285 ui->outputDeviceComboBox->setCurrentIndex(ui->outputDeviceComboBox->count() - 1);
286 }
287 }
288 if (ui->outputDeviceComboBox->count() < 1) {
289 ui->outputDeviceComboBox->setEnabled(false);
290 ui->playButton->setEnabled(false);
291 ui->pauseButton->setEnabled(false);
292 ui->stopButton->setEnabled(false);
293 ui->skipSilenceButton->setEnabled(false);
294 ui->minSilenceSpinBox->setEnabled(false);
295 ui->outputDeviceComboBox->addItem(tr("No devices available"));
296 ui->outputAudioRate->setEnabled(false);
297 } else {
298 stereo_available_ = isStereoAvailable();
299 fillAudioRateMenu();
300 }
301 ui->outputDeviceComboBox->blockSignals(false);
302
303 ui->audioPlot->setMouseTracking(true);
304 ui->audioPlot->setEnabled(true);
305 ui->audioPlot->setInteractions(
306 QCP::iRangeDrag |
307 QCP::iRangeZoom
308 );
309
310 graph_ctx_menu_->addSeparator();
311 list_ctx_menu_ = new QMenu(this);
312 list_ctx_menu_->addAction(ui->actionPlay);
313 graph_ctx_menu_->addAction(ui->actionPlay);
314 list_ctx_menu_->addAction(ui->actionStop);
315 graph_ctx_menu_->addAction(ui->actionStop);
316 list_ctx_menu_->addMenu(ui->menuSelect);
317 graph_ctx_menu_->addMenu(ui->menuSelect);
318 list_ctx_menu_->addMenu(ui->menuAudioRouting);
319 graph_ctx_menu_->addMenu(ui->menuAudioRouting);
320 list_ctx_menu_->addAction(ui->actionRemoveStream);
321 graph_ctx_menu_->addAction(ui->actionRemoveStream);
322 list_ctx_menu_->addAction(ui->actionGoToSetupPacketTree);
323 set_action_shortcuts_visible_in_context_menu(list_ctx_menu_->actions());
324
325 connect(&cap_file_, SIGNAL(captureEvent(CaptureEvent)),
326 this, SLOT(captureEvent(CaptureEvent)));
327 connect(this, SIGNAL(updateFilter(QString, bool)),
328 &parent, SLOT(filterPackets(QString, bool)));
329 connect(this, SIGNAL(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)),
330 &parent, SLOT(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)));
331 connect(this, SIGNAL(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)),
332 &parent, SLOT(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)));
333 connect(this, SIGNAL(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)),
334 &parent, SLOT(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)));
335 #endif // QT_MULTIMEDIA_LIB
336 }
337
338 // _U_ is used when no QT_MULTIMEDIA_LIB is available
addPlayerButton(QDialogButtonBox * button_box,QDialog * dialog _U_)339 QToolButton *RtpPlayerDialog::addPlayerButton(QDialogButtonBox *button_box, QDialog *dialog _U_)
340 {
341 if (!button_box) return NULL;
342
343 QAction *ca;
344 QToolButton *player_button = new QToolButton();
345 button_box->addButton(player_button, QDialogButtonBox::ActionRole);
346 player_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
347 player_button->setPopupMode(QToolButton::MenuButtonPopup);
348
349 ca = new QAction(tr("&Play Streams"));
350 ca->setToolTip(tr("Open RTP player dialog"));
351 ca->setIcon(StockIcon("media-playback-start"));
352 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerReplace()));
353 player_button->setDefaultAction(ca);
354 // Overrides text striping of shortcut undercode in QAction
355 player_button->setText(ca->text());
356
357 #if defined(QT_MULTIMEDIA_LIB)
358 QMenu *button_menu = new QMenu(player_button);
359 button_menu->setToolTipsVisible(true);
360 ca = button_menu->addAction(tr("&Set playlist"));
361 ca->setToolTip(tr("Replace existing playlist in RTP Player with new one"));
362 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerReplace()));
363 ca = button_menu->addAction(tr("&Add to playlist"));
364 ca->setToolTip(tr("Add new set to existing playlist in RTP Player"));
365 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerAdd()));
366 ca = button_menu->addAction(tr("&Remove from playlist"));
367 ca->setToolTip(tr("Remove selected streams from playlist in RTP Player"));
368 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerRemove()));
369 player_button->setMenu(button_menu);
370 #else
371 player_button->setEnabled(false);
372 player_button->setText(tr("No Audio"));
373 #endif
374
375 return player_button;
376 }
377
378 #ifdef QT_MULTIMEDIA_LIB
~RtpPlayerDialog()379 RtpPlayerDialog::~RtpPlayerDialog()
380 {
381 std::lock_guard<std::mutex> lock(mutex_);
382 cleanupMarkerStream();
383 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
384 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
385 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
386 if (audio_stream)
387 delete audio_stream;
388 }
389 delete ui;
390 pinstance_ = nullptr;
391 }
392
accept()393 void RtpPlayerDialog::accept()
394 {
395 if (!listener_removed_) {
396 remove_tap_listener(this);
397 listener_removed_ = true;
398 }
399
400 int row_count = ui->streamTreeWidget->topLevelItemCount();
401 // Stop all streams before the dialogs are closed.
402 for (int row = 0; row < row_count; row++) {
403 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
404 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
405 audio_stream->stopPlaying();
406 }
407 WiresharkDialog::accept();
408 }
409
reject()410 void RtpPlayerDialog::reject()
411 {
412 RtpPlayerDialog::accept();
413 }
414
retapPackets()415 void RtpPlayerDialog::retapPackets()
416 {
417 if (!listener_removed_) {
418 // Retap is running, nothing better we can do
419 return;
420 }
421 lockUI();
422 ui->hintLabel->setText("<i><small>" + tr("Decoding streams...") + "</i></small>");
423 wsApp->processEvents();
424
425 // Clear packets from existing streams before retap
426 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
427 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
428 RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
429
430 row_stream->clearPackets();
431 }
432
433 // destroyCheck is protection againts destroying dialog during recap.
434 // It stores dialog pointer in data() and if dialog destroyed, it
435 // returns null
436 QPointer<RtpPlayerDialog> destroyCheck=this;
437 GString *error_string;
438
439 listener_removed_ = false;
440 error_string = register_tap_listener("rtp", this, NULL, 0, NULL, tapPacket, NULL, NULL);
441 if (error_string) {
442 report_failure("RTP Player - tap registration failed: %s", error_string->str);
443 g_string_free(error_string, TRUE);
444 unlockUI();
445 return;
446 }
447 cap_file_.retapPackets();
448
449 // Check if dialog exists still
450 if (destroyCheck.data()) {
451 if (!listener_removed_) {
452 remove_tap_listener(this);
453 listener_removed_ = true;
454 }
455 fillTappedColumns();
456 rescanPackets(true);
457 }
458 unlockUI();
459 }
460
rescanPackets(bool rescale_axes)461 void RtpPlayerDialog::rescanPackets(bool rescale_axes)
462 {
463 lockUI();
464 // Show information for a user - it can last long time...
465 playback_error_.clear();
466 ui->hintLabel->setText("<i><small>" + tr("Decoding streams...") + "</i></small>");
467 wsApp->processEvents();
468
469 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
470 int row_count = ui->streamTreeWidget->topLevelItemCount();
471
472 // Reset stream values
473 for (int row = 0; row < row_count; row++) {
474 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
475 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
476 audio_stream->setStereoRequired(stereo_available_);
477 audio_stream->reset(first_stream_rel_start_time_);
478
479 audio_stream->setJitterBufferSize((int) ui->jitterSpinBox->value());
480
481 RtpAudioStream::TimingMode timing_mode = RtpAudioStream::JitterBuffer;
482 switch (ui->timingComboBox->currentIndex()) {
483 case RtpAudioStream::RtpTimestamp:
484 timing_mode = RtpAudioStream::RtpTimestamp;
485 break;
486 case RtpAudioStream::Uninterrupted:
487 timing_mode = RtpAudioStream::Uninterrupted;
488 break;
489 default:
490 break;
491 }
492 audio_stream->setTimingMode(timing_mode);
493
494 audio_stream->decode(cur_out_device);
495 }
496
497 for (int col = 0; col < ui->streamTreeWidget->columnCount() - 1; col++) {
498 ui->streamTreeWidget->resizeColumnToContents(col);
499 }
500
501 createPlot(rescale_axes);
502
503 updateWidgets();
504 unlockUI();
505 }
506
createPlot(bool rescale_axes)507 void RtpPlayerDialog::createPlot(bool rescale_axes)
508 {
509 bool legend_out_of_sequence = false;
510 bool legend_jitter_dropped = false;
511 bool legend_wrong_timestamps = false;
512 bool legend_inserted_silences = false;
513 bool relative_timestamps = !ui->todCheckBox->isChecked();
514 int row_count = ui->streamTreeWidget->topLevelItemCount();
515 gint16 total_max_sample_value = 1;
516
517 ui->audioPlot->clearGraphs();
518
519 if (relative_timestamps) {
520 ui->audioPlot->xAxis->setTicker(number_ticker_);
521 } else {
522 ui->audioPlot->xAxis->setTicker(datetime_ticker_);
523 }
524
525 // Calculate common Y scale for graphs
526 for (int row = 0; row < row_count; row++) {
527 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
528 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
529 gint16 max_sample_value = audio_stream->getMaxSampleValue();
530
531 if (max_sample_value > total_max_sample_value) {
532 total_max_sample_value = max_sample_value;
533 }
534 }
535
536 // Clear existing graphs
537 for (int row = 0; row < row_count; row++) {
538 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
539 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
540 int y_offset = row_count - row - 1;
541 AudioRouting audio_routing = audio_stream->getAudioRouting();
542
543 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant());
544 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant());
545 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant());
546 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant());
547 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant());
548
549 // Set common scale
550 audio_stream->setMaxSampleValue(total_max_sample_value);
551
552 // Waveform
553 RtpAudioGraph *audio_graph = new RtpAudioGraph(ui->audioPlot, audio_stream->color());
554 audio_graph->setMuted(audio_routing.isMuted());
555 audio_graph->setData(audio_stream->visualTimestamps(relative_timestamps), audio_stream->visualSamples(y_offset));
556 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant::fromValue<RtpAudioGraph *>(audio_graph));
557 //RTP_STREAM_DEBUG("Plotting %s, %d samples", ti->text(src_addr_col_).toUtf8().constData(), audio_graph->wave->data()->size());
558
559 QString span_str;
560 if (ui->todCheckBox->isChecked()) {
561 QDateTime date_time1 = QDateTime::fromMSecsSinceEpoch((audio_stream->startRelTime() + first_stream_abs_start_time_ - audio_stream->startRelTime()) * 1000.0);
562 QDateTime date_time2 = QDateTime::fromMSecsSinceEpoch((audio_stream->stopRelTime() + first_stream_abs_start_time_ - audio_stream->startRelTime()) * 1000.0);
563 QString time_str1 = date_time1.toString("yyyy-MM-dd hh:mm:ss.zzz");
564 QString time_str2 = date_time2.toString("yyyy-MM-dd hh:mm:ss.zzz");
565 span_str = QString("%1 - %2 (%3)")
566 .arg(time_str1)
567 .arg(time_str2)
568 .arg(QString::number(audio_stream->stopRelTime() - audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1));
569 } else {
570 span_str = QString("%1 - %2 (%3)")
571 .arg(QString::number(audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1))
572 .arg(QString::number(audio_stream->stopRelTime(), 'f', prefs.gui_decimal_places1))
573 .arg(QString::number(audio_stream->stopRelTime() - audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1));
574 }
575 ti->setText(time_span_col_, span_str);
576 ti->setText(sample_rate_col_, QString::number(audio_stream->sampleRate()));
577 ti->setText(play_rate_col_, QString::number(audio_stream->playRate()));
578 ti->setText(payload_col_, audio_stream->payloadNames().join(", "));
579
580 if (audio_stream->outOfSequence() > 0) {
581 // Sequence numbers
582 QCPGraph *seq_graph = ui->audioPlot->addGraph();
583 seq_graph->setLineStyle(QCPGraph::lsNone);
584 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssSquare, tango_aluminium_6, Qt::white, wsApp->font().pointSize())); // Arbitrary
585 seq_graph->setSelectable(QCP::stNone);
586 seq_graph->setData(audio_stream->outOfSequenceTimestamps(relative_timestamps), audio_stream->outOfSequenceSamples(y_offset));
587 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
588 if (legend_out_of_sequence) {
589 seq_graph->removeFromLegend();
590 } else {
591 seq_graph->setName(tr("Out of Sequence"));
592 legend_out_of_sequence = true;
593 }
594 }
595
596 if (audio_stream->jitterDropped() > 0) {
597 // Jitter drops
598 QCPGraph *seq_graph = ui->audioPlot->addGraph();
599 seq_graph->setLineStyle(QCPGraph::lsNone);
600 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, tango_scarlet_red_5, Qt::white, wsApp->font().pointSize())); // Arbitrary
601 seq_graph->setSelectable(QCP::stNone);
602 seq_graph->setData(audio_stream->jitterDroppedTimestamps(relative_timestamps), audio_stream->jitterDroppedSamples(y_offset));
603 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
604 if (legend_jitter_dropped) {
605 seq_graph->removeFromLegend();
606 } else {
607 seq_graph->setName(tr("Jitter Drops"));
608 legend_jitter_dropped = true;
609 }
610 }
611
612 if (audio_stream->wrongTimestamps() > 0) {
613 // Wrong timestamps
614 QCPGraph *seq_graph = ui->audioPlot->addGraph();
615 seq_graph->setLineStyle(QCPGraph::lsNone);
616 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDiamond, tango_sky_blue_5, Qt::white, wsApp->font().pointSize())); // Arbitrary
617 seq_graph->setSelectable(QCP::stNone);
618 seq_graph->setData(audio_stream->wrongTimestampTimestamps(relative_timestamps), audio_stream->wrongTimestampSamples(y_offset));
619 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
620 if (legend_wrong_timestamps) {
621 seq_graph->removeFromLegend();
622 } else {
623 seq_graph->setName(tr("Wrong Timestamps"));
624 legend_wrong_timestamps = true;
625 }
626 }
627
628 if (audio_stream->insertedSilences() > 0) {
629 // Inserted silence
630 QCPGraph *seq_graph = ui->audioPlot->addGraph();
631 seq_graph->setLineStyle(QCPGraph::lsNone);
632 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, tango_butter_5, Qt::white, wsApp->font().pointSize())); // Arbitrary
633 seq_graph->setSelectable(QCP::stNone);
634 seq_graph->setData(audio_stream->insertedSilenceTimestamps(relative_timestamps), audio_stream->insertedSilenceSamples(y_offset));
635 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
636 if (legend_inserted_silences) {
637 seq_graph->removeFromLegend();
638 } else {
639 seq_graph->setName(tr("Inserted Silence"));
640 legend_inserted_silences = true;
641 }
642 }
643 }
644 ui->audioPlot->legend->setVisible(legend_out_of_sequence || legend_jitter_dropped || legend_wrong_timestamps || legend_inserted_silences);
645
646 ui->audioPlot->replot();
647 if (rescale_axes) resetXAxis();
648 }
649
fillTappedColumns()650 void RtpPlayerDialog::fillTappedColumns()
651 {
652 // true just for first stream
653 bool is_first = true;
654
655 // Get all rows, immutable list. Later changes in rows migth reorder them
656 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->findItems(
657 QString("*"), Qt::MatchWrap | Qt::MatchWildcard | Qt::MatchRecursive);
658
659 // Update rows by calculated values, it might reorder them in view...
660 foreach(QTreeWidgetItem *ti, items) {
661 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
662 if (audio_stream) {
663 rtpstream_info_t *rtpstream = audio_stream->getStreamInfo();
664
665 // 0xFFFFFFFF mean no setup frame
666 // first_packet_num == setup_frame_number happens, when
667 // rtp_udp is active or Decode as was used
668 if ((rtpstream->setup_frame_number == 0xFFFFFFFF) ||
669 (rtpstream->rtp_stats.first_packet_num == rtpstream->setup_frame_number)
670 ) {
671 int packet = rtpstream->rtp_stats.first_packet_num;
672 ti->setText(first_pkt_col_, QString("RTP %1").arg(packet));
673 ti->setData(first_pkt_col_, Qt::UserRole, QVariant(packet));
674 } else {
675 int packet = rtpstream->setup_frame_number;
676 ti->setText(first_pkt_col_, QString("SETUP %1").arg(rtpstream->setup_frame_number));
677 ti->setData(first_pkt_col_, Qt::UserRole, QVariant(packet));
678 }
679 ti->setText(num_pkts_col_, QString::number(rtpstream->packet_count));
680 updateStartStopTime(rtpstream, is_first);
681 is_first = false;
682 }
683 }
684 setMarkers();
685 }
686
addSingleRtpStream(rtpstream_id_t * id)687 void RtpPlayerDialog::addSingleRtpStream(rtpstream_id_t *id)
688 {
689 bool found = false;
690
691 AudioRouting audio_routing = AudioRouting(AUDIO_UNMUTED, channel_mono);
692
693 if (!id) return;
694
695 // Find the RTP streams associated with this conversation.
696 // gtk/rtp_player.c:mark_rtp_stream_to_play does this differently.
697
698 QList<RtpAudioStream *> streams = stream_hash_.values(rtpstream_id_to_hash(id));
699 for (int i = 0; i < streams.size(); i++) {
700 RtpAudioStream *row_stream = streams.at(i);
701 if (row_stream->isMatch(id)) {
702 found = true;
703 break;
704 }
705 }
706
707
708 if (found) {
709 return;
710 }
711
712 try {
713 int tli_count = ui->streamTreeWidget->topLevelItemCount();
714
715 RtpAudioStream *audio_stream = new RtpAudioStream(this, id, stereo_available_);
716 audio_stream->setColor(ColorUtils::graphColor(tli_count));
717
718 QTreeWidgetItem *ti = new RtpPlayerTreeWidgetItem(ui->streamTreeWidget);
719 stream_hash_.insert(rtpstream_id_to_hash(id), audio_stream);
720 ti->setText(src_addr_col_, address_to_qstring(&(id->src_addr)));
721 ti->setText(src_port_col_, QString::number(id->src_port));
722 ti->setText(dst_addr_col_, address_to_qstring(&(id->dst_addr)));
723 ti->setText(dst_port_col_, QString::number(id->dst_port));
724 ti->setText(ssrc_col_, int_to_qstring(id->ssrc, 8, 16));
725
726 // Calculated items are updated after every retapPackets()
727
728 ti->setData(stream_data_col_, Qt::UserRole, QVariant::fromValue(audio_stream));
729 if (stereo_available_) {
730 if (tli_count%2) {
731 audio_routing.setChannel(channel_stereo_right);
732 } else {
733 audio_routing.setChannel(channel_stereo_left);
734 }
735 } else {
736 audio_routing.setChannel(channel_mono);
737 }
738 ti->setToolTip(channel_col_, QString(tr("Double click on cell to change audio routing")));
739 formatAudioRouting(ti, audio_routing);
740 audio_stream->setAudioRouting(audio_routing);
741
742 for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
743 QBrush fgBrush = ti->foreground(col);
744 fgBrush.setColor(audio_stream->color());
745 ti->setForeground(col, fgBrush);
746 }
747
748 connect(audio_stream, SIGNAL(finishedPlaying(RtpAudioStream *, QAudio::Error)), this, SLOT(playFinished(RtpAudioStream *, QAudio::Error)));
749 connect(audio_stream, SIGNAL(playbackError(QString)), this, SLOT(setPlaybackError(QString)));
750 } catch (...) {
751 qWarning() << "Stream ignored, try to add fewer streams to playlist";
752 }
753
754 RTP_STREAM_DEBUG("adding stream %d to layout",
755 ui->streamTreeWidget->topLevelItemCount());
756 }
757
lockUI()758 void RtpPlayerDialog::lockUI()
759 {
760 if (0 == lock_ui_++) {
761 if (playing_streams_.count() > 0) {
762 on_stopButton_clicked();
763 }
764 setEnabled(false);
765 }
766 }
767
unlockUI()768 void RtpPlayerDialog::unlockUI()
769 {
770 if (--lock_ui_ == 0) {
771 setEnabled(true);
772 }
773 }
774
replaceRtpStreams(QVector<rtpstream_id_t * > stream_ids)775 void RtpPlayerDialog::replaceRtpStreams(QVector<rtpstream_id_t *> stream_ids)
776 {
777 std::lock_guard<std::mutex> lock(mutex_);
778 lockUI();
779
780 // Delete all existing rows
781 if (last_ti_) {
782 highlightItem(last_ti_, false);
783 last_ti_ = NULL;
784 }
785
786 for (int row = ui->streamTreeWidget->topLevelItemCount() - 1; row >= 0; row--) {
787 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
788 removeRow(ti);
789 }
790
791 // Add all new streams
792 for (int i=0; i < stream_ids.size(); i++) {
793 addSingleRtpStream(stream_ids[i]);
794 }
795 setMarkers();
796
797 unlockUI();
798 #ifdef QT_MULTIMEDIA_LIB
799 QTimer::singleShot(0, this, SLOT(retapPackets()));
800 #endif
801 }
802
addRtpStreams(QVector<rtpstream_id_t * > stream_ids)803 void RtpPlayerDialog::addRtpStreams(QVector<rtpstream_id_t *> stream_ids)
804 {
805 std::lock_guard<std::mutex> lock(mutex_);
806 lockUI();
807
808 int tli_count = ui->streamTreeWidget->topLevelItemCount();
809
810 // Add new streams
811 for (int i=0; i < stream_ids.size(); i++) {
812 addSingleRtpStream(stream_ids[i]);
813 }
814
815 if (tli_count == 0) {
816 setMarkers();
817 }
818
819 unlockUI();
820 #ifdef QT_MULTIMEDIA_LIB
821 QTimer::singleShot(0, this, SLOT(retapPackets()));
822 #endif
823 }
824
removeRtpStreams(QVector<rtpstream_id_t * > stream_ids)825 void RtpPlayerDialog::removeRtpStreams(QVector<rtpstream_id_t *> stream_ids)
826 {
827 std::lock_guard<std::mutex> lock(mutex_);
828 lockUI();
829 int tli_count = ui->streamTreeWidget->topLevelItemCount();
830
831 for (int i=0; i < stream_ids.size(); i++) {
832 for (int row = 0; row < tli_count; row++) {
833 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
834 RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
835 if (row_stream->isMatch(stream_ids[i])) {
836 removeRow(ti);
837 tli_count--;
838 break;
839 }
840 }
841 }
842 updateGraphs();
843
844 updateWidgets();
845 unlockUI();
846 }
847
setMarkers()848 void RtpPlayerDialog::setMarkers()
849 {
850 setStartPlayMarker(0);
851 drawStartPlayMarker();
852 }
853
showEvent(QShowEvent *)854 void RtpPlayerDialog::showEvent(QShowEvent *)
855 {
856 QList<int> split_sizes = ui->splitter->sizes();
857 int tot_size = split_sizes[0] + split_sizes[1];
858 int plot_size = tot_size * 3 / 4;
859 split_sizes.clear();
860 split_sizes << plot_size << tot_size - plot_size;
861 ui->splitter->setSizes(split_sizes);
862 }
863
eventFilter(QObject *,QEvent * event)864 bool RtpPlayerDialog::eventFilter(QObject *, QEvent *event)
865 {
866 if (event->type() == QEvent::KeyPress) {
867 QKeyEvent &keyEvent = static_cast<QKeyEvent&>(*event);
868 int pan_secs = keyEvent.modifiers() & Qt::ShiftModifier ? 1 : 10;
869
870 switch(keyEvent.key()) {
871 case Qt::Key_Minus:
872 case Qt::Key_Underscore: // Shifted minus on U.S. keyboards
873 case Qt::Key_O: // GTK+
874 case Qt::Key_R:
875 on_actionZoomOut_triggered();
876 return true;
877 case Qt::Key_Plus:
878 case Qt::Key_Equal: // Unshifted plus on U.S. keyboards
879 case Qt::Key_I: // GTK+
880 if (keyEvent.modifiers() == Qt::ControlModifier) {
881 // Ctrl+I
882 on_actionSelectInvert_triggered();
883 return true;
884 } else {
885 // I
886 on_actionZoomIn_triggered();
887 return true;
888 }
889 break;
890 case Qt::Key_Right:
891 case Qt::Key_L:
892 panXAxis(pan_secs);
893 return true;
894 case Qt::Key_Left:
895 case Qt::Key_H:
896 panXAxis(-1 * pan_secs);
897 return true;
898 case Qt::Key_0:
899 case Qt::Key_ParenRight: // Shifted 0 on U.S. keyboards
900 on_actionReset_triggered();
901 return true;
902 case Qt::Key_G:
903 if (keyEvent.modifiers() == Qt::ShiftModifier) {
904 // Goto SETUP frame, use correct call based on caller
905 QPoint pos1 = ui->audioPlot->mapFromGlobal(QCursor::pos());
906 QPoint pos2 = ui->streamTreeWidget->mapFromGlobal(QCursor::pos());
907 if (ui->audioPlot->rect().contains(pos1)) {
908 // audio plot, by mouse coords
909 on_actionGoToSetupPacketPlot_triggered();
910 } else if (ui->streamTreeWidget->rect().contains(pos2)) {
911 // packet tree, by cursor
912 on_actionGoToSetupPacketTree_triggered();
913 }
914 return true;
915 } else {
916 on_actionGoToPacket_triggered();
917 return true;
918 }
919 case Qt::Key_A:
920 if (keyEvent.modifiers() == Qt::ControlModifier) {
921 // Ctrl+A
922 on_actionSelectAll_triggered();
923 return true;
924 } else if (keyEvent.modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
925 // Ctrl+Shift+A
926 on_actionSelectNone_triggered();
927 return true;
928 }
929 break;
930 case Qt::Key_M:
931 if (keyEvent.modifiers() == Qt::ShiftModifier) {
932 on_actionAudioRoutingUnmute_triggered();
933 return true;
934 } else if (keyEvent.modifiers() == Qt::ControlModifier) {
935 on_actionAudioRoutingMuteInvert_triggered();
936 return true;
937 } else {
938 on_actionAudioRoutingMute_triggered();
939 return true;
940 }
941 case Qt::Key_Delete:
942 on_actionRemoveStream_triggered();
943 return true;
944 case Qt::Key_X:
945 if (keyEvent.modifiers() == Qt::ControlModifier) {
946 // Ctrl+X
947 on_actionRemoveStream_triggered();
948 return true;
949 }
950 break;
951 case Qt::Key_Down:
952 case Qt::Key_Up:
953 case Qt::Key_PageUp:
954 case Qt::Key_PageDown:
955 case Qt::Key_Home:
956 case Qt::Key_End:
957 // Route keys to QTreeWidget
958 ui->streamTreeWidget->setFocus();
959 break;
960 case Qt::Key_P:
961 if (keyEvent.modifiers() == Qt::NoModifier) {
962 on_actionPlay_triggered();
963 return true;
964 }
965 break;
966 case Qt::Key_S:
967 on_actionStop_triggered();
968 return true;
969 case Qt::Key_N:
970 if (keyEvent.modifiers() == Qt::ShiftModifier) {
971 // Shift+N
972 on_actionDeselectInaudible_triggered();
973 return true;
974 } else {
975 on_actionSelectInaudible_triggered();
976 return true;
977 }
978 break;
979 }
980 }
981
982 return false;
983 }
984
contextMenuEvent(QContextMenuEvent * event)985 void RtpPlayerDialog::contextMenuEvent(QContextMenuEvent *event)
986 {
987 list_ctx_menu_->exec(event->globalPos());
988 }
989
updateWidgets()990 void RtpPlayerDialog::updateWidgets()
991 {
992 bool enable_play = true;
993 bool enable_pause = false;
994 bool enable_stop = false;
995 bool enable_timing = true;
996 int count = ui->streamTreeWidget->topLevelItemCount();
997 int selected = ui->streamTreeWidget->selectedItems().count();
998
999 if (count < 1) {
1000 enable_play = false;
1001 ui->skipSilenceButton->setEnabled(false);
1002 ui->minSilenceSpinBox->setEnabled(false);
1003 } else {
1004 ui->skipSilenceButton->setEnabled(true);
1005 ui->minSilenceSpinBox->setEnabled(true);
1006 }
1007
1008 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1009 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1010
1011 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1012 if (audio_stream->outputState() != QAudio::IdleState) {
1013 enable_play = false;
1014 enable_pause = true;
1015 enable_stop = true;
1016 enable_timing = false;
1017 }
1018 }
1019
1020 ui->actionAudioRoutingP->setVisible(!stereo_available_);
1021 ui->actionAudioRoutingL->setVisible(stereo_available_);
1022 ui->actionAudioRoutingLR->setVisible(stereo_available_);
1023 ui->actionAudioRoutingR->setVisible(stereo_available_);
1024
1025 ui->playButton->setEnabled(enable_play);
1026 if (enable_play) {
1027 ui->playButton->setVisible(true);
1028 ui->pauseButton->setVisible(false);
1029 } else if (enable_pause) {
1030 ui->playButton->setVisible(false);
1031 ui->pauseButton->setVisible(true);
1032 }
1033 ui->outputDeviceComboBox->setEnabled(enable_play);
1034 ui->outputAudioRate->setEnabled(enable_play);
1035 ui->pauseButton->setEnabled(enable_pause);
1036 ui->stopButton->setEnabled(enable_stop);
1037 ui->actionStop->setEnabled(enable_stop);
1038 cur_play_pos_->setVisible(enable_stop);
1039
1040 ui->jitterSpinBox->setEnabled(enable_timing);
1041 ui->timingComboBox->setEnabled(enable_timing);
1042 ui->todCheckBox->setEnabled(enable_timing);
1043
1044 read_btn_->setEnabled(read_capture_enabled_);
1045 inaudible_btn_->setEnabled(count > 0);
1046 analyze_btn_->setEnabled(selected > 0);
1047 prepare_btn_->setEnabled(selected > 0);
1048
1049 updateHintLabel();
1050 ui->audioPlot->replot();
1051 }
1052
handleItemHighlight(QTreeWidgetItem * ti,bool scroll)1053 void RtpPlayerDialog::handleItemHighlight(QTreeWidgetItem *ti, bool scroll)
1054 {
1055 if (ti) {
1056 if (ti != last_ti_) {
1057 if (last_ti_) {
1058 highlightItem(last_ti_, false);
1059 }
1060 highlightItem(ti, true);
1061
1062 if (scroll)
1063 ui->streamTreeWidget->scrollToItem(ti, QAbstractItemView::EnsureVisible);
1064 ui->audioPlot->replot();
1065 last_ti_ = ti;
1066 }
1067 } else {
1068 if (last_ti_) {
1069 highlightItem(last_ti_, false);
1070 ui->audioPlot->replot();
1071 last_ti_ = NULL;
1072 }
1073 }
1074 }
1075
highlightItem(QTreeWidgetItem * ti,bool highlight)1076 void RtpPlayerDialog::highlightItem(QTreeWidgetItem *ti, bool highlight)
1077 {
1078 QFont font;
1079 RtpAudioGraph *audio_graph;
1080
1081 font.setBold(highlight);
1082 for(int i=0; i<ui->streamTreeWidget->columnCount(); i++) {
1083 ti->setFont(i, font);
1084 }
1085
1086 audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1087 if (audio_graph) {
1088 audio_graph->setHighlight(highlight);
1089 }
1090 }
1091
itemEntered(QTreeWidgetItem * item,int column _U_)1092 void RtpPlayerDialog::itemEntered(QTreeWidgetItem *item, int column _U_)
1093 {
1094 handleItemHighlight(item, false);
1095 }
1096
mouseMovePlot(QMouseEvent * event)1097 void RtpPlayerDialog::mouseMovePlot(QMouseEvent *event)
1098 {
1099 updateHintLabel();
1100
1101 QTreeWidgetItem *ti = findItemByCoords(event->pos());
1102 handleItemHighlight(ti, true);
1103 }
1104
graphClicked(QMouseEvent * event)1105 void RtpPlayerDialog::graphClicked(QMouseEvent *event)
1106 {
1107 updateWidgets();
1108 if (event->button() == Qt::RightButton) {
1109 graph_ctx_menu_->exec(event->globalPos());
1110 }
1111 }
1112
graphDoubleClicked(QMouseEvent * event)1113 void RtpPlayerDialog::graphDoubleClicked(QMouseEvent *event)
1114 {
1115 updateWidgets();
1116 if (event->button() == Qt::LeftButton) {
1117 // Move start play line
1118 double ts = ui->audioPlot->xAxis->pixelToCoord(event->pos().x());
1119
1120 setStartPlayMarker(ts);
1121 drawStartPlayMarker();
1122
1123 ui->audioPlot->replot();
1124 }
1125 }
1126
plotClicked(QCPAbstractPlottable * plottable _U_,int dataIndex _U_,QMouseEvent * event)1127 void RtpPlayerDialog::plotClicked(QCPAbstractPlottable *plottable _U_, int dataIndex _U_, QMouseEvent *event)
1128 {
1129 // Delivered plottable very often points to different element than a mouse
1130 // so we find right one by mouse coordinates
1131 QTreeWidgetItem *ti = findItemByCoords(event->pos());
1132 if (ti) {
1133 if (event->modifiers() == Qt::NoModifier) {
1134 ti->setSelected(true);
1135 } else if (event->modifiers() == Qt::ControlModifier) {
1136 ti->setSelected(!ti->isSelected());
1137 }
1138 }
1139 }
1140
findItemByCoords(QPoint point)1141 QTreeWidgetItem *RtpPlayerDialog::findItemByCoords(QPoint point)
1142 {
1143 QCPAbstractPlottable *plottable=ui->audioPlot->plottableAt(point);
1144 if (plottable) {
1145 return findItem(plottable);
1146 }
1147
1148 return NULL;
1149 }
1150
findItem(QCPAbstractPlottable * plottable)1151 QTreeWidgetItem *RtpPlayerDialog::findItem(QCPAbstractPlottable *plottable)
1152 {
1153 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1154 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1155 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1156 if (audio_graph && audio_graph->isMyPlottable(plottable)) {
1157 return ti;
1158 }
1159 }
1160
1161 return NULL;
1162 }
1163
updateHintLabel()1164 void RtpPlayerDialog::updateHintLabel()
1165 {
1166 int packet_num = getHoveredPacket();
1167 QString hint = "<small><i>";
1168 double start_pos = getStartPlayMarker();
1169 int row_count = ui->streamTreeWidget->topLevelItemCount();
1170 int selected = ui->streamTreeWidget->selectedItems().count();
1171 int not_muted = 0;
1172
1173 hint += tr("%1 streams").arg(row_count);
1174
1175 if (row_count > 0) {
1176 if (selected > 0) {
1177 hint += tr(", %1 selected").arg(selected);
1178 }
1179
1180 for (int row = 0; row < row_count; row++) {
1181 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1182 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1183 if (audio_stream && (!audio_stream->getAudioRouting().isMuted())) {
1184 not_muted++;
1185 }
1186 }
1187
1188 hint += tr(", %1 not muted").arg(not_muted);
1189 }
1190
1191 if (packet_num == 0) {
1192 hint += tr(", start: %1. Double click on graph to set start of playback.")
1193 .arg(getFormatedTime(start_pos));
1194 } else if (packet_num > 0) {
1195 hint += tr(", start: %1, cursor: %2. Press \"G\" to go to packet %3. Double click on graph to set start of playback.")
1196 .arg(getFormatedTime(start_pos))
1197 .arg(getFormatedHoveredTime())
1198 .arg(packet_num);
1199 }
1200
1201 if (!playback_error_.isEmpty()) {
1202 hint += " <font color=\"red\">";
1203 hint += playback_error_;
1204 hint += " </font>";
1205 }
1206
1207 hint += "</i></small>";
1208 ui->hintLabel->setText(hint);
1209 }
1210
resetXAxis()1211 void RtpPlayerDialog::resetXAxis()
1212 {
1213 QCustomPlot *ap = ui->audioPlot;
1214
1215 double pixel_pad = 10.0; // per side
1216
1217 ap->rescaleAxes(true);
1218
1219 double axis_pixels = ap->xAxis->axisRect()->width();
1220 ap->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->xAxis->range().center());
1221
1222 axis_pixels = ap->yAxis->axisRect()->height();
1223 ap->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->yAxis->range().center());
1224
1225 ap->replot();
1226 }
1227
updateGraphs()1228 void RtpPlayerDialog::updateGraphs()
1229 {
1230 QCustomPlot *ap = ui->audioPlot;
1231
1232 // Create new plots, just existing ones
1233 createPlot(false);
1234
1235 // Rescale Y axis
1236 double pixel_pad = 10.0; // per side
1237 double axis_pixels = ap->yAxis->axisRect()->height();
1238 ap->yAxis->rescale(true);
1239 ap->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->yAxis->range().center());
1240
1241 ap->replot();
1242 }
1243
playFinished(RtpAudioStream * stream,QAudio::Error error)1244 void RtpPlayerDialog::playFinished(RtpAudioStream *stream, QAudio::Error error)
1245 {
1246 if ((error != QAudio::NoError) && (error != QAudio::UnderrunError)) {
1247 setPlaybackError(tr("Playback of stream %1 failed!")
1248 .arg(stream->getIDAsQString())
1249 );
1250 }
1251 playing_streams_.removeOne(stream);
1252 if (playing_streams_.isEmpty()) {
1253 marker_stream_->stop();
1254 updateWidgets();
1255 }
1256 }
1257
setPlayPosition(double secs)1258 void RtpPlayerDialog::setPlayPosition(double secs)
1259 {
1260 double cur_secs = cur_play_pos_->point1->key();
1261
1262 if (ui->todCheckBox->isChecked()) {
1263 secs += first_stream_abs_start_time_;
1264 } else {
1265 secs += first_stream_rel_start_time_;
1266 }
1267 if (secs > cur_secs) {
1268 cur_play_pos_->point1->setCoords(secs, 0.0);
1269 cur_play_pos_->point2->setCoords(secs, 1.0);
1270 ui->audioPlot->replot();
1271 }
1272 }
1273
setPlaybackError(const QString playback_error)1274 void RtpPlayerDialog::setPlaybackError(const QString playback_error)
1275 {
1276 playback_error_ = playback_error;
1277 updateHintLabel();
1278 }
1279
tapPacket(void * tapinfo_ptr,packet_info * pinfo,epan_dissect_t *,const void * rtpinfo_ptr)1280 tap_packet_status RtpPlayerDialog::tapPacket(void *tapinfo_ptr, packet_info *pinfo, epan_dissect_t *, const void *rtpinfo_ptr)
1281 {
1282 RtpPlayerDialog *rtp_player_dialog = dynamic_cast<RtpPlayerDialog *>((RtpPlayerDialog*)tapinfo_ptr);
1283 if (!rtp_player_dialog) return TAP_PACKET_DONT_REDRAW;
1284
1285 const struct _rtp_info *rtpinfo = (const struct _rtp_info *)rtpinfo_ptr;
1286 if (!rtpinfo) return TAP_PACKET_DONT_REDRAW;
1287
1288 /* ignore RTP Version != 2 */
1289 if (rtpinfo->info_version != 2)
1290 return TAP_PACKET_DONT_REDRAW;
1291
1292 rtp_player_dialog->addPacket(pinfo, rtpinfo);
1293
1294 return TAP_PACKET_DONT_REDRAW;
1295 }
1296
addPacket(packet_info * pinfo,const _rtp_info * rtpinfo)1297 void RtpPlayerDialog::addPacket(packet_info *pinfo, const _rtp_info *rtpinfo)
1298 {
1299 // Search stream in hash key, if there are multiple streams with same hash
1300 QList<RtpAudioStream *> streams = stream_hash_.values(pinfo_rtp_info_to_hash(pinfo, rtpinfo));
1301 for (int i = 0; i < streams.size(); i++) {
1302 RtpAudioStream *row_stream = streams.at(i);
1303 if (row_stream->isMatch(pinfo, rtpinfo)) {
1304 row_stream->addRtpPacket(pinfo, rtpinfo);
1305 break;
1306 }
1307 }
1308
1309 // qDebug() << "=ap no match!" << address_to_qstring(&pinfo->src) << address_to_qstring(&pinfo->dst);
1310 }
1311
zoomXAxis(bool in)1312 void RtpPlayerDialog::zoomXAxis(bool in)
1313 {
1314 QCustomPlot *ap = ui->audioPlot;
1315 double h_factor = ap->axisRect()->rangeZoomFactor(Qt::Horizontal);
1316
1317 if (!in) {
1318 h_factor = pow(h_factor, -1);
1319 }
1320
1321 ap->xAxis->scaleRange(h_factor, ap->xAxis->range().center());
1322 ap->replot();
1323 }
1324
1325 // XXX I tried using seconds but pixels make more sense at varying zoom
1326 // levels.
panXAxis(int x_pixels)1327 void RtpPlayerDialog::panXAxis(int x_pixels)
1328 {
1329 QCustomPlot *ap = ui->audioPlot;
1330 double h_pan;
1331
1332 h_pan = ap->xAxis->range().size() * x_pixels / ap->xAxis->axisRect()->width();
1333 if (x_pixels) {
1334 ap->xAxis->moveRange(h_pan);
1335 ap->replot();
1336 }
1337 }
1338
on_playButton_clicked()1339 void RtpPlayerDialog::on_playButton_clicked()
1340 {
1341 double start_time;
1342
1343 ui->hintLabel->setText("<i><small>" + tr("Preparing to play...") + "</i></small>");
1344 wsApp->processEvents();
1345 ui->pauseButton->setChecked(false);
1346
1347 // Protect start time against move of marker during the play
1348 start_marker_time_play_ = start_marker_time_;
1349 silence_skipped_time_ = 0.0;
1350 cur_play_pos_->point1->setCoords(start_marker_time_play_, 0.0);
1351 cur_play_pos_->point2->setCoords(start_marker_time_play_, 1.0);
1352 cur_play_pos_->setVisible(true);
1353 playback_error_.clear();
1354
1355 if (ui->todCheckBox->isChecked()) {
1356 start_time = start_marker_time_play_;
1357 } else {
1358 start_time = start_marker_time_play_ - first_stream_rel_start_time_;
1359 }
1360
1361 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
1362 playing_streams_.clear();
1363 int row_count = ui->streamTreeWidget->topLevelItemCount();
1364 for (int row = 0; row < row_count; row++) {
1365 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1366 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1367 // All streams starts at first_stream_rel_start_time_
1368 audio_stream->setStartPlayTime(start_time);
1369 if (audio_stream->prepareForPlay(cur_out_device)) {
1370 playing_streams_ << audio_stream;
1371 }
1372 }
1373
1374 // Prepare silent stream for progress marker
1375 if (!marker_stream_) {
1376 marker_stream_ = getSilenceAudioOutput();
1377 } else {
1378 marker_stream_->stop();
1379 }
1380
1381 // Start progress marker and then audio streams
1382 marker_stream_->start(new AudioSilenceGenerator());
1383 for( int i = 0; i<playing_streams_.count(); ++i ) {
1384 playing_streams_[i]->startPlaying();
1385 }
1386
1387 updateWidgets();
1388 }
1389
getCurrentDeviceInfo()1390 QAudioDeviceInfo RtpPlayerDialog::getCurrentDeviceInfo()
1391 {
1392 QAudioDeviceInfo cur_out_device = QAudioDeviceInfo::defaultOutputDevice();
1393 QString cur_out_name = currentOutputDeviceName();
1394 foreach (QAudioDeviceInfo out_device, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
1395 if (cur_out_name == out_device.deviceName()) {
1396 cur_out_device = out_device;
1397 }
1398 }
1399
1400 return cur_out_device;
1401 }
1402
getSilenceAudioOutput()1403 QAudioOutput *RtpPlayerDialog::getSilenceAudioOutput()
1404 {
1405 QAudioOutput *o;
1406 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
1407
1408 QAudioFormat format;
1409 if (marker_stream_requested_out_rate_ > 0) {
1410 format.setSampleRate(marker_stream_requested_out_rate_);
1411 } else {
1412 format.setSampleRate(8000);
1413 }
1414 format.setSampleSize(SAMPLE_BYTES * 8); // bits
1415 format.setSampleType(QAudioFormat::SignedInt);
1416 format.setChannelCount(1);
1417 format.setCodec("audio/pcm");
1418 if (!cur_out_device.isFormatSupported(format)) {
1419 format = cur_out_device.nearestFormat(format);
1420 }
1421
1422 o = new QAudioOutput(cur_out_device, format, this);
1423 o->setNotifyInterval(100); // ~15 fps
1424 connect(o, SIGNAL(notify()), this, SLOT(outputNotify()));
1425
1426 return o;
1427 }
1428
outputNotify()1429 void RtpPlayerDialog::outputNotify()
1430 {
1431 double new_current_pos = 0.0;
1432 double current_pos = 0.0;
1433
1434 double secs = marker_stream_->processedUSecs() / 1000000.0;
1435
1436 if (ui->skipSilenceButton->isChecked()) {
1437 // We should check whether we can skip some silence
1438 // We must calculate in time domain as every stream can use different
1439 // play rate
1440 double min_silence = playing_streams_[0]->getEndOfSilenceTime();
1441 for( int i = 1; i<playing_streams_.count(); ++i ) {
1442 qint64 cur_silence = playing_streams_[i]->getEndOfSilenceTime();
1443 if (cur_silence < min_silence) {
1444 min_silence = cur_silence;
1445 }
1446 }
1447
1448 if (min_silence > 0.0) {
1449 double silence_duration;
1450
1451 // Calculate silence duration we can skip
1452 new_current_pos = first_stream_rel_start_time_ + min_silence;
1453 if (ui->todCheckBox->isChecked()) {
1454 current_pos = secs + start_marker_time_play_ + first_stream_rel_start_time_;
1455 } else {
1456 current_pos = secs + start_marker_time_play_;
1457 }
1458 silence_duration = new_current_pos - current_pos;
1459
1460 if (silence_duration >= ui->minSilenceSpinBox->value()) {
1461 // Skip silence gap and update cursor difference
1462 for( int i = 0; i<playing_streams_.count(); ++i ) {
1463 // Convert silence from time domain to samples
1464 qint64 skip_samples = playing_streams_[i]->convertTimeToSamples(min_silence);
1465 playing_streams_[i]->seekPlaying(skip_samples);
1466 }
1467 silence_skipped_time_ = silence_duration;
1468 }
1469 }
1470 }
1471
1472 // Calculate new cursor position
1473 if (ui->todCheckBox->isChecked()) {
1474 secs += start_marker_time_play_;
1475 secs += silence_skipped_time_;
1476 } else {
1477 secs += start_marker_time_play_;
1478 secs -= first_stream_rel_start_time_;
1479 secs += silence_skipped_time_;
1480 }
1481 setPlayPosition(secs);
1482 }
1483
on_pauseButton_clicked()1484 void RtpPlayerDialog::on_pauseButton_clicked()
1485 {
1486 for( int i = 0; i<playing_streams_.count(); ++i ) {
1487 playing_streams_[i]->pausePlaying();
1488 }
1489 if (ui->pauseButton->isChecked()) {
1490 marker_stream_->suspend();
1491 } else {
1492 marker_stream_->resume();
1493 }
1494 updateWidgets();
1495 }
1496
on_stopButton_clicked()1497 void RtpPlayerDialog::on_stopButton_clicked()
1498 {
1499 // We need copy of list because items will be removed during stopPlaying()
1500 QList<RtpAudioStream *> ps=QList<RtpAudioStream *>(playing_streams_);
1501 for( int i = 0; i<ps.count(); ++i ) {
1502 ps[i]->stopPlaying();
1503 }
1504 marker_stream_->stop();
1505 cur_play_pos_->setVisible(false);
1506 updateWidgets();
1507 }
1508
on_actionReset_triggered()1509 void RtpPlayerDialog::on_actionReset_triggered()
1510 {
1511 resetXAxis();
1512 }
1513
on_actionZoomIn_triggered()1514 void RtpPlayerDialog::on_actionZoomIn_triggered()
1515 {
1516 zoomXAxis(true);
1517 }
1518
on_actionZoomOut_triggered()1519 void RtpPlayerDialog::on_actionZoomOut_triggered()
1520 {
1521 zoomXAxis(false);
1522 }
1523
on_actionMoveLeft10_triggered()1524 void RtpPlayerDialog::on_actionMoveLeft10_triggered()
1525 {
1526 panXAxis(-10);
1527 }
1528
on_actionMoveRight10_triggered()1529 void RtpPlayerDialog::on_actionMoveRight10_triggered()
1530 {
1531 panXAxis(10);
1532 }
1533
on_actionMoveLeft1_triggered()1534 void RtpPlayerDialog::on_actionMoveLeft1_triggered()
1535 {
1536 panXAxis(-1);
1537 }
1538
on_actionMoveRight1_triggered()1539 void RtpPlayerDialog::on_actionMoveRight1_triggered()
1540 {
1541 panXAxis(1);
1542 }
1543
on_actionGoToPacket_triggered()1544 void RtpPlayerDialog::on_actionGoToPacket_triggered()
1545 {
1546 int packet_num = getHoveredPacket();
1547 if (packet_num > 0) emit goToPacket(packet_num);
1548 }
1549
handleGoToSetupPacket(QTreeWidgetItem * ti)1550 void RtpPlayerDialog::handleGoToSetupPacket(QTreeWidgetItem *ti)
1551 {
1552 if (ti) {
1553 bool ok;
1554
1555 int packet_num = ti->data(first_pkt_col_, Qt::UserRole).toInt(&ok);
1556 if (ok) {
1557 emit goToPacket(packet_num);
1558 }
1559 }
1560 }
1561
on_actionGoToSetupPacketPlot_triggered()1562 void RtpPlayerDialog::on_actionGoToSetupPacketPlot_triggered()
1563 {
1564 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1565 handleGoToSetupPacket(findItemByCoords(pos));
1566 }
1567
on_actionGoToSetupPacketTree_triggered()1568 void RtpPlayerDialog::on_actionGoToSetupPacketTree_triggered()
1569 {
1570 handleGoToSetupPacket(last_ti_);
1571 }
1572
1573 // Make waveform graphs selectable and update the treewidget selection accordingly.
on_streamTreeWidget_itemSelectionChanged()1574 void RtpPlayerDialog::on_streamTreeWidget_itemSelectionChanged()
1575 {
1576 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1577 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1578 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1579 if (audio_graph) {
1580 audio_graph->setSelected(ti->isSelected());
1581 }
1582 }
1583
1584 int selected = ui->streamTreeWidget->selectedItems().count();
1585 if (selected == 0) {
1586 analyze_btn_->setEnabled(false);
1587 prepare_btn_->setEnabled(false);
1588 export_btn_->setEnabled(false);
1589 } else if (selected == 1) {
1590 analyze_btn_->setEnabled(true);
1591 prepare_btn_->setEnabled(true);
1592 export_btn_->setEnabled(true);
1593 ui->actionSavePayload->setEnabled(true);
1594 } else {
1595 analyze_btn_->setEnabled(true);
1596 prepare_btn_->setEnabled(true);
1597 export_btn_->setEnabled(true);
1598 ui->actionSavePayload->setEnabled(false);
1599 }
1600
1601 if (!block_redraw_) {
1602 ui->audioPlot->replot();
1603 updateHintLabel();
1604 }
1605 }
1606
1607 // Change channel audio routing if double clicked channel column
on_streamTreeWidget_itemDoubleClicked(QTreeWidgetItem * item,const int column)1608 void RtpPlayerDialog::on_streamTreeWidget_itemDoubleClicked(QTreeWidgetItem *item, const int column)
1609 {
1610 if (column == channel_col_) {
1611 RtpAudioStream *audio_stream = item->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1612 if (!audio_stream)
1613 return;
1614
1615 AudioRouting audio_routing = audio_stream->getAudioRouting();
1616 audio_routing = audio_routing.getNextChannel(stereo_available_);
1617 changeAudioRoutingOnItem(item, audio_routing);
1618 }
1619 updateHintLabel();
1620 }
1621
removeRow(QTreeWidgetItem * ti)1622 void RtpPlayerDialog::removeRow(QTreeWidgetItem *ti)
1623 {
1624 if (last_ti_ && (last_ti_ == ti)) {
1625 highlightItem(last_ti_, false);
1626 last_ti_ = NULL;
1627 }
1628 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1629 if (audio_stream) {
1630 stream_hash_.remove(audio_stream->getHash(), audio_stream);
1631 ti->setData(stream_data_col_, Qt::UserRole, QVariant());
1632 delete audio_stream;
1633 }
1634
1635 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1636 if (audio_graph) {
1637 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant());
1638 audio_graph->remove(ui->audioPlot);
1639 }
1640
1641 QCPGraph *graph;
1642 graph = ti->data(graph_sequence_data_col_, Qt::UserRole).value<QCPGraph*>();
1643 if (graph) {
1644 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant());
1645 ui->audioPlot->removeGraph(graph);
1646 }
1647
1648 graph = ti->data(graph_jitter_data_col_, Qt::UserRole).value<QCPGraph*>();
1649 if (graph) {
1650 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant());
1651 ui->audioPlot->removeGraph(graph);
1652 }
1653
1654 graph = ti->data(graph_timestamp_data_col_, Qt::UserRole).value<QCPGraph*>();
1655 if (graph) {
1656 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant());
1657 ui->audioPlot->removeGraph(graph);
1658 }
1659
1660 graph = ti->data(graph_silence_data_col_, Qt::UserRole).value<QCPGraph*>();
1661 if (graph) {
1662 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant());
1663 ui->audioPlot->removeGraph(graph);
1664 }
1665
1666 delete ti;
1667 }
1668
on_actionRemoveStream_triggered()1669 void RtpPlayerDialog::on_actionRemoveStream_triggered()
1670 {
1671 lockUI();
1672 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1673
1674 block_redraw_ = true;
1675 for(int i = items.count() - 1; i>=0; i-- ) {
1676 removeRow(items[i]);
1677 }
1678 block_redraw_ = false;
1679 // TODO: Recalculate legend
1680 // - Graphs used for legend could be removed above and we must add new
1681 // - If no legend is required, it should be removed
1682
1683 // Redraw existing waveforms and rescale Y axis
1684 updateGraphs();
1685
1686 updateWidgets();
1687 unlockUI();
1688 }
1689
1690 // If called with channel_any, just muted flag should be changed
changeAudioRoutingOnItem(QTreeWidgetItem * ti,AudioRouting new_audio_routing)1691 void RtpPlayerDialog::changeAudioRoutingOnItem(QTreeWidgetItem *ti, AudioRouting new_audio_routing)
1692 {
1693 if (!ti)
1694 return;
1695
1696 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1697 if (!audio_stream)
1698 return;
1699
1700 AudioRouting audio_routing = audio_stream->getAudioRouting();
1701 audio_routing.mergeAudioRouting(new_audio_routing);
1702 formatAudioRouting(ti, audio_routing);
1703
1704 audio_stream->setAudioRouting(audio_routing);
1705
1706 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1707 if (audio_graph) {
1708
1709 audio_graph->setSelected(ti->isSelected());
1710 audio_graph->setMuted(audio_routing.isMuted());
1711 if (!block_redraw_) {
1712 ui->audioPlot->replot();
1713 }
1714 }
1715 }
1716
1717 // Find current item and apply change on it
changeAudioRouting(AudioRouting new_audio_routing)1718 void RtpPlayerDialog::changeAudioRouting(AudioRouting new_audio_routing)
1719 {
1720 lockUI();
1721 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1722
1723 block_redraw_ = true;
1724 for(int i = 0; i<items.count(); i++ ) {
1725
1726 QTreeWidgetItem *ti = items[i];
1727 changeAudioRoutingOnItem(ti, new_audio_routing);
1728 }
1729 block_redraw_ = false;
1730 ui->audioPlot->replot();
1731 updateHintLabel();
1732 unlockUI();
1733 }
1734
1735 // Invert mute/unmute on item
invertAudioMutingOnItem(QTreeWidgetItem * ti)1736 void RtpPlayerDialog::invertAudioMutingOnItem(QTreeWidgetItem *ti)
1737 {
1738 if (!ti)
1739 return;
1740
1741 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1742 if (!audio_stream)
1743 return;
1744
1745 AudioRouting audio_routing = audio_stream->getAudioRouting();
1746 // Invert muting
1747 if (audio_routing.isMuted()) {
1748 changeAudioRoutingOnItem(ti, AudioRouting(AUDIO_UNMUTED, channel_any));
1749 } else {
1750 changeAudioRoutingOnItem(ti, AudioRouting(AUDIO_MUTED, channel_any));
1751 }
1752 }
1753
on_actionAudioRoutingP_triggered()1754 void RtpPlayerDialog::on_actionAudioRoutingP_triggered()
1755 {
1756 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_mono));
1757 }
1758
on_actionAudioRoutingL_triggered()1759 void RtpPlayerDialog::on_actionAudioRoutingL_triggered()
1760 {
1761 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_left));
1762 }
1763
on_actionAudioRoutingLR_triggered()1764 void RtpPlayerDialog::on_actionAudioRoutingLR_triggered()
1765 {
1766 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_both));
1767 }
1768
on_actionAudioRoutingR_triggered()1769 void RtpPlayerDialog::on_actionAudioRoutingR_triggered()
1770 {
1771 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_right));
1772 }
1773
on_actionAudioRoutingMute_triggered()1774 void RtpPlayerDialog::on_actionAudioRoutingMute_triggered()
1775 {
1776 changeAudioRouting(AudioRouting(AUDIO_MUTED, channel_any));
1777 }
1778
on_actionAudioRoutingUnmute_triggered()1779 void RtpPlayerDialog::on_actionAudioRoutingUnmute_triggered()
1780 {
1781 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_any));
1782 }
1783
on_actionAudioRoutingMuteInvert_triggered()1784 void RtpPlayerDialog::on_actionAudioRoutingMuteInvert_triggered()
1785 {
1786 lockUI();
1787 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1788
1789 block_redraw_ = true;
1790 for(int i = 0; i<items.count(); i++ ) {
1791
1792 QTreeWidgetItem *ti = items[i];
1793 invertAudioMutingOnItem(ti);
1794 }
1795 block_redraw_ = false;
1796 ui->audioPlot->replot();
1797 updateHintLabel();
1798 unlockUI();
1799 }
1800
getFormatedTime(double f_time)1801 const QString RtpPlayerDialog::getFormatedTime(double f_time)
1802 {
1803 QString time_str;
1804
1805 if (ui->todCheckBox->isChecked()) {
1806 QDateTime date_time = QDateTime::fromMSecsSinceEpoch(f_time * 1000.0);
1807 time_str = date_time.toString("yyyy-MM-dd hh:mm:ss.zzz");
1808 } else {
1809 time_str = QString::number(f_time, 'f', 6);
1810 time_str += " s";
1811 }
1812
1813 return time_str;
1814 }
1815
getFormatedHoveredTime()1816 const QString RtpPlayerDialog::getFormatedHoveredTime()
1817 {
1818 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1819 QTreeWidgetItem *ti = findItemByCoords(pos);
1820 if (!ti) return tr("Unknown");
1821
1822 double ts = ui->audioPlot->xAxis->pixelToCoord(pos.x());
1823
1824 return getFormatedTime(ts);
1825 }
1826
getHoveredPacket()1827 int RtpPlayerDialog::getHoveredPacket()
1828 {
1829 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1830 QTreeWidgetItem *ti = findItemByCoords(pos);
1831 if (!ti) return 0;
1832
1833 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1834
1835 double ts = ui->audioPlot->xAxis->pixelToCoord(pos.x());
1836
1837 return audio_stream->nearestPacket(ts, !ui->todCheckBox->isChecked());
1838 }
1839
1840 // Used by RtpAudioStreams to initialize QAudioOutput. We could alternatively
1841 // pass the corresponding QAudioDeviceInfo directly.
currentOutputDeviceName()1842 QString RtpPlayerDialog::currentOutputDeviceName()
1843 {
1844 return ui->outputDeviceComboBox->currentText();
1845 }
1846
fillAudioRateMenu()1847 void RtpPlayerDialog::fillAudioRateMenu()
1848 {
1849 ui->outputAudioRate->blockSignals(true);
1850 ui->outputAudioRate->clear();
1851 ui->outputAudioRate->addItem(tr("Automatic"));
1852 foreach (int rate, getCurrentDeviceInfo().supportedSampleRates()) {
1853 ui->outputAudioRate->addItem(QString::number(rate));
1854 }
1855 ui->outputAudioRate->blockSignals(false);
1856 }
1857
cleanupMarkerStream()1858 void RtpPlayerDialog::cleanupMarkerStream()
1859 {
1860 if (marker_stream_) {
1861 marker_stream_->stop();
1862 delete marker_stream_;
1863 marker_stream_ = NULL;
1864 }
1865 }
1866
on_outputDeviceComboBox_currentIndexChanged(const QString &)1867 void RtpPlayerDialog::on_outputDeviceComboBox_currentIndexChanged(const QString &)
1868 {
1869 lockUI();
1870 stereo_available_ = isStereoAvailable();
1871 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1872 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1873 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1874 if (!audio_stream)
1875 continue;
1876
1877 changeAudioRoutingOnItem(ti, audio_stream->getAudioRouting().convert(stereo_available_));
1878 }
1879
1880 marker_stream_requested_out_rate_ = 0;
1881 cleanupMarkerStream();
1882 fillAudioRateMenu();
1883 rescanPackets();
1884 unlockUI();
1885 }
1886
on_outputAudioRate_currentIndexChanged(const QString & rate_string)1887 void RtpPlayerDialog::on_outputAudioRate_currentIndexChanged(const QString & rate_string)
1888 {
1889 lockUI();
1890 // Any unconvertable string is converted to 0 => used as Automatic rate
1891 unsigned selected_rate = rate_string.toInt();
1892
1893 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1894 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1895 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1896 if (!audio_stream)
1897 continue;
1898
1899 audio_stream->setRequestedPlayRate(selected_rate);
1900 }
1901 marker_stream_requested_out_rate_ = selected_rate;
1902 cleanupMarkerStream();
1903 rescanPackets();
1904 unlockUI();
1905 }
1906
on_jitterSpinBox_valueChanged(double)1907 void RtpPlayerDialog::on_jitterSpinBox_valueChanged(double)
1908 {
1909 rescanPackets();
1910 }
1911
on_timingComboBox_currentIndexChanged(int)1912 void RtpPlayerDialog::on_timingComboBox_currentIndexChanged(int)
1913 {
1914 rescanPackets();
1915 }
1916
on_todCheckBox_toggled(bool)1917 void RtpPlayerDialog::on_todCheckBox_toggled(bool)
1918 {
1919 QCPAxis *x_axis = ui->audioPlot->xAxis;
1920 double move;
1921
1922 // Create plot with new tod settings
1923 createPlot();
1924
1925 // Move view to same place as was shown before the change
1926 if (ui->todCheckBox->isChecked()) {
1927 // rel -> abs
1928 // based on abs time of first sample
1929 setStartPlayMarker(first_stream_abs_start_time_ + start_marker_time_ - first_stream_rel_start_time_);
1930 move = first_stream_abs_start_time_ - first_stream_rel_start_time_;
1931 } else {
1932 // abs -> rel
1933 // based on 0s
1934 setStartPlayMarker(first_stream_rel_start_time_ + start_marker_time_);
1935 move = - first_stream_abs_start_time_ + first_stream_rel_start_time_;
1936 }
1937 x_axis->moveRange(move);
1938 drawStartPlayMarker();
1939 ui->audioPlot->replot();
1940 }
1941
on_buttonBox_helpRequested()1942 void RtpPlayerDialog::on_buttonBox_helpRequested()
1943 {
1944 wsApp->helpTopicAction(HELP_TELEPHONY_RTP_PLAYER_DIALOG);
1945 }
1946
getStartPlayMarker()1947 double RtpPlayerDialog::getStartPlayMarker()
1948 {
1949 double start_pos;
1950
1951 if (ui->todCheckBox->isChecked()) {
1952 start_pos = start_marker_time_ + first_stream_abs_start_time_;
1953 } else {
1954 start_pos = start_marker_time_;
1955 }
1956
1957 return start_pos;
1958 }
1959
drawStartPlayMarker()1960 void RtpPlayerDialog::drawStartPlayMarker()
1961 {
1962 double pos = getStartPlayMarker();
1963
1964 start_marker_pos_->point1->setCoords(pos, 0.0);
1965 start_marker_pos_->point2->setCoords(pos, 1.0);
1966
1967 updateHintLabel();
1968 }
1969
setStartPlayMarker(double new_time)1970 void RtpPlayerDialog::setStartPlayMarker(double new_time)
1971 {
1972 if (ui->todCheckBox->isChecked()) {
1973 new_time = qBound(first_stream_abs_start_time_, new_time, first_stream_abs_start_time_ + streams_length_);
1974 // start_play_time is relative, we must calculate it
1975 start_marker_time_ = new_time - first_stream_abs_start_time_;
1976 } else {
1977 new_time = qBound(first_stream_rel_start_time_, new_time, first_stream_rel_start_time_ + streams_length_);
1978 start_marker_time_ = new_time;
1979 }
1980 }
1981
updateStartStopTime(rtpstream_info_t * rtpstream,bool is_first)1982 void RtpPlayerDialog::updateStartStopTime(rtpstream_info_t *rtpstream, bool is_first)
1983 {
1984 // Calculate start time of first last packet of last stream
1985 double stream_rel_start_time = nstime_to_sec(&rtpstream->start_rel_time);
1986 double stream_abs_start_time = nstime_to_sec(&rtpstream->start_abs_time);
1987 double stream_rel_stop_time = nstime_to_sec(&rtpstream->stop_rel_time);
1988
1989 if (is_first) {
1990 // Take start/stop time for first stream
1991 first_stream_rel_start_time_ = stream_rel_start_time;
1992 first_stream_abs_start_time_ = stream_abs_start_time;
1993 first_stream_rel_stop_time_ = stream_rel_stop_time;
1994 } else {
1995 // Calculate min/max for start/stop time for other streams
1996 first_stream_rel_start_time_ = qMin(first_stream_rel_start_time_, stream_rel_start_time);
1997 first_stream_abs_start_time_ = qMin(first_stream_abs_start_time_, stream_abs_start_time);
1998 first_stream_rel_stop_time_ = qMax(first_stream_rel_stop_time_, stream_rel_stop_time);
1999 }
2000 streams_length_ = first_stream_rel_stop_time_ - first_stream_rel_start_time_;
2001 }
2002
formatAudioRouting(QTreeWidgetItem * ti,AudioRouting audio_routing)2003 void RtpPlayerDialog::formatAudioRouting(QTreeWidgetItem *ti, AudioRouting audio_routing)
2004 {
2005 ti->setText(channel_col_, tr(audio_routing.formatAudioRoutingToString()));
2006 }
2007
isStereoAvailable()2008 bool RtpPlayerDialog::isStereoAvailable()
2009 {
2010 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
2011 foreach(int count, cur_out_device.supportedChannelCounts()) {
2012 if (count>1) {
2013 return true;
2014 }
2015 }
2016
2017 return false;
2018 }
2019
invertSelection()2020 void RtpPlayerDialog::invertSelection()
2021 {
2022 block_redraw_ = true;
2023 ui->streamTreeWidget->blockSignals(true);
2024 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2025 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2026 ti->setSelected(!ti->isSelected());
2027 }
2028 ui->streamTreeWidget->blockSignals(false);
2029 block_redraw_ = false;
2030 ui->audioPlot->replot();
2031 updateHintLabel();
2032 }
2033
on_actionSelectAll_triggered()2034 void RtpPlayerDialog::on_actionSelectAll_triggered()
2035 {
2036 ui->streamTreeWidget->selectAll();
2037 updateHintLabel();
2038 }
2039
on_actionSelectInvert_triggered()2040 void RtpPlayerDialog::on_actionSelectInvert_triggered()
2041 {
2042 invertSelection();
2043 updateHintLabel();
2044 }
2045
on_actionSelectNone_triggered()2046 void RtpPlayerDialog::on_actionSelectNone_triggered()
2047 {
2048 ui->streamTreeWidget->clearSelection();
2049 updateHintLabel();
2050 }
2051
on_actionPlay_triggered()2052 void RtpPlayerDialog::on_actionPlay_triggered()
2053 {
2054 if (ui->playButton->isEnabled()) {
2055 ui->playButton->animateClick();
2056 } else if (ui->pauseButton->isEnabled()) {
2057 ui->pauseButton->animateClick();
2058 }
2059 }
2060
on_actionStop_triggered()2061 void RtpPlayerDialog::on_actionStop_triggered()
2062 {
2063 if (ui->stopButton->isEnabled()) {
2064 ui->stopButton->animateClick();
2065 }
2066 }
2067
saveAudioHeaderAU(QFile * save_file,int channels,unsigned audio_rate)2068 qint64 RtpPlayerDialog::saveAudioHeaderAU(QFile *save_file, int channels, unsigned audio_rate)
2069 {
2070 uint8_t pd[4];
2071 int64_t nchars;
2072
2073 /* https://pubs.opengroup.org/external/auformat.html */
2074 /* First we write the .au header. All values in the header are
2075 * 4-byte big-endian values, so we use pntoh32() to copy them
2076 * to a 4-byte buffer, in big-endian order, and then write out
2077 * the buffer. */
2078
2079 /* the magic word 0x2e736e64 == .snd */
2080 phton32(pd, 0x2e736e64);
2081 nchars = save_file->write((const char *)pd, 4);
2082 if (nchars != 4) {
2083 return -1;
2084 }
2085
2086 /* header offset == 24 bytes */
2087 phton32(pd, 24);
2088 nchars = save_file->write((const char *)pd, 4);
2089 if (nchars != 4) {
2090 return -1;
2091 }
2092
2093 /* total length; it is permitted to set this to 0xffffffff */
2094 phton32(pd, 0xffffffff);
2095 nchars = save_file->write((const char *)pd, 4);
2096 if (nchars != 4) {
2097 return -1;
2098 }
2099
2100 /* encoding format == 16-bit linear PCM */
2101 phton32(pd, 3);
2102 nchars = save_file->write((const char *)pd, 4);
2103 if (nchars != 4) {
2104 return -1;
2105 }
2106
2107 /* sample rate [Hz] */
2108 phton32(pd, audio_rate);
2109 nchars = save_file->write((const char *)pd, 4);
2110 if (nchars != 4) {
2111 return -1;
2112 }
2113
2114 /* channels */
2115 phton32(pd, channels);
2116 nchars = save_file->write((const char *)pd, 4);
2117 if (nchars != 4) {
2118 return -1;
2119 }
2120
2121 return save_file->pos();
2122 }
2123
saveAudioHeaderWAV(QFile * save_file,int channels,unsigned audio_rate,qint64 samples)2124 qint64 RtpPlayerDialog::saveAudioHeaderWAV(QFile *save_file, int channels, unsigned audio_rate, qint64 samples)
2125 {
2126 uint8_t pd[4];
2127 int64_t nchars;
2128 gint32 subchunk2Size;
2129 gint32 data32;
2130 gint16 data16;
2131
2132 subchunk2Size = sizeof(SAMPLE) * channels * (gint32)samples;
2133
2134 /* http://soundfile.sapp.org/doc/WaveFormat/ */
2135
2136 /* RIFF header, ChunkID 0x52494646 == RIFF */
2137 phton32(pd, 0x52494646);
2138 nchars = save_file->write((const char *)pd, 4);
2139 if (nchars != 4) {
2140 return -1;
2141 }
2142
2143 /* RIFF header, ChunkSize */
2144 data32 = 36 + subchunk2Size;
2145 nchars = save_file->write((const char *)&data32, 4);
2146 if (nchars != 4) {
2147 return -1;
2148 }
2149
2150 /* RIFF header, Format 0x57415645 == WAVE */
2151 phton32(pd, 0x57415645);
2152 nchars = save_file->write((const char *)pd, 4);
2153 if (nchars != 4) {
2154 return -1;
2155 }
2156
2157 /* WAVE fmt header, Subchunk1ID 0x666d7420 == 'fmt ' */
2158 phton32(pd, 0x666d7420);
2159 nchars = save_file->write((const char *)pd, 4);
2160 if (nchars != 4) {
2161 return -1;
2162 }
2163
2164 /* WAVE fmt header, Subchunk1Size */
2165 data32 = 16;
2166 nchars = save_file->write((const char *)&data32, 4);
2167 if (nchars != 4) {
2168 return -1;
2169 }
2170
2171 /* WAVE fmt header, AudioFormat 1 == PCM */
2172 data16 = 1;
2173 nchars = save_file->write((const char *)&data16, 2);
2174 if (nchars != 2) {
2175 return -1;
2176 }
2177
2178 /* WAVE fmt header, NumChannels */
2179 data16 = channels;
2180 nchars = save_file->write((const char *)&data16, 2);
2181 if (nchars != 2) {
2182 return -1;
2183 }
2184
2185 /* WAVE fmt header, SampleRate */
2186 data32 = audio_rate;
2187 nchars = save_file->write((const char *)&data32, 4);
2188 if (nchars != 4) {
2189 return -1;
2190 }
2191
2192 /* WAVE fmt header, ByteRate */
2193 data32 = audio_rate * channels * sizeof(SAMPLE);
2194 nchars = save_file->write((const char *)&data32, 4);
2195 if (nchars != 4) {
2196 return -1;
2197 }
2198
2199 /* WAVE fmt header, BlockAlign */
2200 data16 = channels * (gint16)sizeof(SAMPLE);
2201 nchars = save_file->write((const char *)&data16, 2);
2202 if (nchars != 2) {
2203 return -1;
2204 }
2205
2206 /* WAVE fmt header, BitsPerSample */
2207 data16 = (gint16)sizeof(SAMPLE) * 8;
2208 nchars = save_file->write((const char *)&data16, 2);
2209 if (nchars != 2) {
2210 return -1;
2211 }
2212
2213 /* WAVE data header, Subchunk2ID 0x64617461 == 'data' */
2214 phton32(pd, 0x64617461);
2215 nchars = save_file->write((const char *)pd, 4);
2216 if (nchars != 4) {
2217 return -1;
2218 }
2219
2220 /* WAVE data header, Subchunk2Size */
2221 data32 = subchunk2Size;
2222 nchars = save_file->write((const char *)&data32, 4);
2223 if (nchars != 4) {
2224 return -1;
2225 }
2226
2227 /* Now we are ready for saving data */
2228
2229 return save_file->pos();
2230 }
2231
writeAudioSilenceSamples(QFile * out_file,qint64 samples,int stream_count)2232 bool RtpPlayerDialog::writeAudioSilenceSamples(QFile *out_file, qint64 samples, int stream_count)
2233 {
2234 uint8_t pd[2];
2235
2236 phton16(pd, 0x0000);
2237 for(int s=0; s < stream_count; s++) {
2238 for(qint64 i=0; i < samples; i++) {
2239 if (sizeof(SAMPLE) != out_file->write((char *)&pd, sizeof(SAMPLE))) {
2240 return false;
2241 }
2242 }
2243 }
2244
2245 return true;
2246 }
2247
writeAudioStreamsSamples(QFile * out_file,QVector<RtpAudioStream * > streams,bool swap_bytes)2248 bool RtpPlayerDialog::writeAudioStreamsSamples(QFile *out_file, QVector<RtpAudioStream *> streams, bool swap_bytes)
2249 {
2250 SAMPLE sample;
2251 uint8_t pd[2];
2252
2253 // Did we read something in last cycle?
2254 bool read = true;
2255
2256 while (read) {
2257 read = false;
2258 // Loop over all streams, read one sample from each, write to output
2259 foreach(RtpAudioStream *audio_stream, streams) {
2260 if (sizeof(sample) == audio_stream->readSample(&sample)) {
2261 if (swap_bytes) {
2262 // same as phton16(), but more clear in compare
2263 // to else branch
2264 pd[0] = (guint8)(sample >> 8);
2265 pd[1] = (guint8)(sample >> 0);
2266 } else {
2267 // just copy
2268 pd[1] = (guint8)(sample >> 8);
2269 pd[0] = (guint8)(sample >> 0);
2270 }
2271 read = true;
2272 } else {
2273 // for 0x0000 doesn't matter on order
2274 phton16(pd, 0x0000);
2275 }
2276 if (sizeof(sample) != out_file->write((char *)&pd, sizeof(sample))) {
2277 return false;
2278 }
2279 }
2280 }
2281
2282 return true;
2283 }
2284
selectFileAudioFormatAndName(QString * file_path)2285 save_audio_t RtpPlayerDialog::selectFileAudioFormatAndName(QString *file_path)
2286 {
2287 QString ext_filter = "";
2288 QString ext_filter_wav = tr("WAV (*.wav)");
2289 QString ext_filter_au = tr("Sun Audio (*.au)");
2290 ext_filter.append(ext_filter_wav);
2291 ext_filter.append(";;");
2292 ext_filter.append(ext_filter_au);
2293
2294 QString sel_filter;
2295 *file_path = WiresharkFileDialog::getSaveFileName(
2296 this, tr("Save audio"), wsApp->lastOpenDir().absoluteFilePath(""),
2297 ext_filter, &sel_filter);
2298
2299 if (file_path->isEmpty()) return save_audio_none;
2300
2301 save_audio_t save_format = save_audio_none;
2302 if (0 == QString::compare(sel_filter, ext_filter_au)) {
2303 save_format = save_audio_au;
2304 } else if (0 == QString::compare(sel_filter, ext_filter_wav)) {
2305 save_format = save_audio_wav;
2306 }
2307
2308 return save_format;
2309 }
2310
selectFilePayloadFormatAndName(QString * file_path)2311 save_payload_t RtpPlayerDialog::selectFilePayloadFormatAndName(QString *file_path)
2312 {
2313 QString ext_filter = "";
2314 QString ext_filter_raw = tr("Raw (*.raw)");
2315 ext_filter.append(ext_filter_raw);
2316
2317 QString sel_filter;
2318 *file_path = WiresharkFileDialog::getSaveFileName(
2319 this, tr("Save payload"), wsApp->lastOpenDir().absoluteFilePath(""),
2320 ext_filter, &sel_filter);
2321
2322 if (file_path->isEmpty()) return save_payload_none;
2323
2324 save_payload_t save_format = save_payload_none;
2325 if (0 == QString::compare(sel_filter, ext_filter_raw)) {
2326 save_format = save_payload_data;
2327 }
2328
2329 return save_format;
2330 }
2331
getSelectedRtpStreamIDs()2332 QVector<rtpstream_id_t *>RtpPlayerDialog::getSelectedRtpStreamIDs()
2333 {
2334 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
2335 QVector<rtpstream_id_t *> ids;
2336
2337 if (items.count() > 0) {
2338 foreach(QTreeWidgetItem *ti, items) {
2339 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2340 if (audio_stream) {
2341 ids << audio_stream->getID();
2342 }
2343 }
2344 }
2345
2346 return ids;
2347 }
2348
getSelectedAudibleNonmutedAudioStreams()2349 QVector<RtpAudioStream *>RtpPlayerDialog::getSelectedAudibleNonmutedAudioStreams()
2350 {
2351 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
2352 QVector<RtpAudioStream *> streams;
2353
2354 if (items.count() > 0) {
2355 foreach(QTreeWidgetItem *ti, items) {
2356 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2357 // Ignore muted streams and streams with no audio
2358 if (audio_stream &&
2359 !audio_stream->getAudioRouting().isMuted() &&
2360 (audio_stream->sampleRate()>0)
2361 ) {
2362 streams << audio_stream;
2363 }
2364 }
2365 }
2366
2367 return streams;
2368 }
2369
saveAudio(save_mode_t save_mode)2370 void RtpPlayerDialog::saveAudio(save_mode_t save_mode)
2371 {
2372 qint64 minSilenceSamples;
2373 qint64 startSample;
2374 qint64 lead_silence_samples;
2375 qint64 maxSample;
2376 QString path;
2377 QVector<RtpAudioStream *>streams;
2378
2379 streams = getSelectedAudibleNonmutedAudioStreams();
2380 if (streams.count() < 1) {
2381 QMessageBox::warning(this, tr("Warning"), tr("No stream selected or none of selected streams provide audio"));
2382 return;
2383 }
2384
2385 unsigned save_audio_rate = streams[0]->playRate();
2386 // Check whether all streams use same audio rate
2387 foreach(RtpAudioStream *audio_stream, streams) {
2388 if (save_audio_rate != audio_stream->playRate()) {
2389 QMessageBox::warning(this, tr("Error"), tr("All selected streams must use same play rate. Manual set of Output Audio Rate might help."));
2390 return;
2391 }
2392 }
2393
2394 save_audio_t format = selectFileAudioFormatAndName(&path);
2395 if (format == save_audio_none) return;
2396
2397 // Use start silence and length of first stream
2398 minSilenceSamples = streams[0]->getLeadSilenceSamples();
2399 maxSample = streams[0]->getTotalSamples();
2400 // Find shortest start silence and longest stream
2401 foreach(RtpAudioStream *audio_stream, streams) {
2402 if (minSilenceSamples > audio_stream->getLeadSilenceSamples()) {
2403 minSilenceSamples = audio_stream->getLeadSilenceSamples();
2404 }
2405 if (maxSample < audio_stream->getTotalSamples()) {
2406 maxSample = audio_stream->getTotalSamples();
2407 }
2408 }
2409
2410 switch (save_mode) {
2411 case save_mode_from_cursor:
2412 if (ui->todCheckBox->isChecked()) {
2413 startSample = start_marker_time_ * save_audio_rate;
2414 } else {
2415 startSample = (start_marker_time_ - first_stream_rel_start_time_) * save_audio_rate;
2416 }
2417 lead_silence_samples = 0;
2418 break;
2419 case save_mode_sync_stream:
2420 // Skip start of first stream, no lead silence
2421 startSample = minSilenceSamples;
2422 lead_silence_samples = 0;
2423 break;
2424 case save_mode_sync_file:
2425 default:
2426 // Full first stream, lead silence
2427 startSample = 0;
2428 lead_silence_samples = first_stream_rel_start_time_ * save_audio_rate;
2429 break;
2430 }
2431
2432 QVector<RtpAudioStream *>temp = QVector<RtpAudioStream *>(streams);
2433
2434 // Remove streams shorter than startSample and
2435 // seek to correct start for longer ones
2436 foreach(RtpAudioStream *audio_stream, temp) {
2437 if (startSample > audio_stream->getTotalSamples()) {
2438 streams.removeAll(audio_stream);
2439 } else {
2440 audio_stream->seekSample(startSample);
2441 }
2442 }
2443
2444 if (streams.count() < 1) {
2445 QMessageBox::warning(this, tr("Warning"), tr("No streams are suitable for save"));
2446 return;
2447 }
2448
2449 QFile file(path);
2450 file.open(QIODevice::WriteOnly);
2451
2452 if (!file.isOpen() || (file.error() != QFile::NoError)) {
2453 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2454 } else {
2455 switch (format) {
2456 case save_audio_au:
2457 if (-1 == saveAudioHeaderAU(&file, streams.count(), save_audio_rate)) {
2458 QMessageBox::warning(this, tr("Error"), tr("Can't write header of AU file"));
2459 return;
2460 }
2461 if (lead_silence_samples > 0) {
2462 if (!writeAudioSilenceSamples(&file, lead_silence_samples, streams.count())) {
2463 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2464 }
2465 }
2466 if (!writeAudioStreamsSamples(&file, streams, true)) {
2467 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2468 }
2469 break;
2470 case save_audio_wav:
2471 if (-1 == saveAudioHeaderWAV(&file, streams.count(), save_audio_rate, (maxSample - startSample) + lead_silence_samples)) {
2472 QMessageBox::warning(this, tr("Error"), tr("Can't write header of WAV file"));
2473 return;
2474 }
2475 if (lead_silence_samples > 0) {
2476 if (!writeAudioSilenceSamples(&file, lead_silence_samples, streams.count())) {
2477 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2478 }
2479 }
2480 if (!writeAudioStreamsSamples(&file, streams, false)) {
2481 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2482 }
2483 break;
2484 case save_audio_none:
2485 break;
2486 }
2487 }
2488
2489 file.close();
2490 }
2491
savePayload()2492 void RtpPlayerDialog::savePayload()
2493 {
2494 QString path;
2495 QList<QTreeWidgetItem *> items;
2496 RtpAudioStream *audio_stream = NULL;
2497
2498 items = ui->streamTreeWidget->selectedItems();
2499 foreach(QTreeWidgetItem *ti, items) {
2500 audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2501 if (audio_stream)
2502 break;
2503 }
2504 if (items.count() != 1 || !audio_stream) {
2505 QMessageBox::warning(this, tr("Warning"), tr("Payload save works with just one audio stream."));
2506 return;
2507 }
2508
2509 save_payload_t format = selectFilePayloadFormatAndName(&path);
2510 if (format == save_payload_none) return;
2511
2512 QFile file(path);
2513 file.open(QIODevice::WriteOnly);
2514
2515 if (!file.isOpen() || (file.error() != QFile::NoError)) {
2516 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2517 } else if (!audio_stream->savePayload(&file)) {
2518 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2519 }
2520
2521 file.close();
2522 }
2523
on_actionSaveAudioFromCursor_triggered()2524 void RtpPlayerDialog::on_actionSaveAudioFromCursor_triggered()
2525 {
2526 saveAudio(save_mode_from_cursor);
2527 }
2528
on_actionSaveAudioSyncStream_triggered()2529 void RtpPlayerDialog::on_actionSaveAudioSyncStream_triggered()
2530 {
2531 saveAudio(save_mode_sync_stream);
2532 }
2533
on_actionSaveAudioSyncFile_triggered()2534 void RtpPlayerDialog::on_actionSaveAudioSyncFile_triggered()
2535 {
2536 saveAudio(save_mode_sync_file);
2537 }
2538
on_actionSavePayload_triggered()2539 void RtpPlayerDialog::on_actionSavePayload_triggered()
2540 {
2541 savePayload();
2542 }
2543
selectInaudible(bool select)2544 void RtpPlayerDialog::selectInaudible(bool select)
2545 {
2546 block_redraw_ = true;
2547 ui->streamTreeWidget->blockSignals(true);
2548 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2549 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2550 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2551 // Streams with no audio
2552 if (audio_stream && (audio_stream->sampleRate()==0)) {
2553 ti->setSelected(select);
2554 }
2555 }
2556 ui->streamTreeWidget->blockSignals(false);
2557 block_redraw_ = false;
2558 ui->audioPlot->replot();
2559 updateHintLabel();
2560 }
2561
on_actionSelectInaudible_triggered()2562 void RtpPlayerDialog::on_actionSelectInaudible_triggered()
2563 {
2564 selectInaudible(true);
2565 }
2566
on_actionDeselectInaudible_triggered()2567 void RtpPlayerDialog::on_actionDeselectInaudible_triggered()
2568 {
2569 selectInaudible(false);
2570 }
2571
on_actionPrepareFilter_triggered()2572 void RtpPlayerDialog::on_actionPrepareFilter_triggered()
2573 {
2574 QVector<rtpstream_id_t *> ids = getSelectedRtpStreamIDs();
2575 QString filter = make_filter_based_on_rtpstream_id(ids);
2576 if (filter.length() > 0) {
2577 emit updateFilter(filter);
2578 }
2579 }
2580
rtpAnalysisReplace()2581 void RtpPlayerDialog::rtpAnalysisReplace()
2582 {
2583 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2584
2585 emit rtpAnalysisDialogReplaceRtpStreams(getSelectedRtpStreamIDs());
2586 }
2587
rtpAnalysisAdd()2588 void RtpPlayerDialog::rtpAnalysisAdd()
2589 {
2590 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2591
2592 emit rtpAnalysisDialogAddRtpStreams(getSelectedRtpStreamIDs());
2593 }
2594
rtpAnalysisRemove()2595 void RtpPlayerDialog::rtpAnalysisRemove()
2596 {
2597 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2598
2599 emit rtpAnalysisDialogRemoveRtpStreams(getSelectedRtpStreamIDs());
2600 }
2601
on_actionReadCapture_triggered()2602 void RtpPlayerDialog::on_actionReadCapture_triggered()
2603 {
2604 #ifdef QT_MULTIMEDIA_LIB
2605 QTimer::singleShot(0, this, SLOT(retapPackets()));
2606 #endif
2607 }
2608
2609 // _U_ is used for case w have no LIBPCAP
captureEvent(CaptureEvent e _U_)2610 void RtpPlayerDialog::captureEvent(CaptureEvent e _U_)
2611 {
2612 #ifdef HAVE_LIBPCAP
2613 bool new_read_capture_enabled = false;
2614 bool found = false;
2615
2616 if ((e.captureContext() & CaptureEvent::Capture) &&
2617 (e.eventType() == CaptureEvent::Prepared)
2618 ) {
2619 new_read_capture_enabled = true;
2620 found = true;
2621 } else if ((e.captureContext() & CaptureEvent::Capture) &&
2622 (e.eventType() == CaptureEvent::Finished)
2623 ) {
2624 new_read_capture_enabled = false;
2625 found = true;
2626 }
2627
2628 if (found) {
2629 bool retap = false;
2630 if (read_capture_enabled_ && !new_read_capture_enabled) {
2631 // Capturing ended, automatically refresh data
2632 retap = true;
2633 }
2634 read_capture_enabled_ = new_read_capture_enabled;
2635 updateWidgets();
2636 if (retap) {
2637 QTimer::singleShot(0, this, SLOT(retapPackets()));
2638 }
2639 }
2640 #endif
2641 }
2642
2643 #endif // QT_MULTIMEDIA_LIB
2644