Kerberos: add kerberos_inject_longterm_key() helper function
[wireshark-sm.git] / ui / qt / rtp_player_dialog.cpp
blobca8a6bfc06557e0ec020e778c4d70401a0da8673
1 /* rtp_player_dialog.cpp
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
10 #include "config.h"
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"
18 #include "file.h"
19 #include "frame_tvbuff.h"
21 #include "rtp_analysis_dialog.h"
23 #ifdef QT_MULTIMEDIA_LIB
25 #include <epan/dissectors/packet-rtp.h>
26 #include <epan/to_str.h>
28 #include <wsutil/report_message.h>
29 #include <wsutil/utf8_entities.h>
30 #include <wsutil/pint.h>
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 "main_application.h"
39 #include "ui/qt/widgets/wireshark_file_dialog.h"
41 #include <QAudio>
42 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
43 #include <algorithm>
44 #include <QAudioDevice>
45 #include <QAudioSink>
46 #include <QMediaDevices>
47 #else
48 #include <QAudioDeviceInfo>
49 #endif
50 #include <QFrame>
51 #include <QMenu>
52 #include <QVBoxLayout>
53 #include <QTimer>
55 #include <QAudioFormat>
56 #include <QAudioOutput>
57 #include <ui/qt/utils/rtp_audio_silence_generator.h>
59 #endif // QT_MULTIMEDIA_LIB
61 #include <QPushButton>
62 #include <QToolButton>
64 #include <ui/qt/utils/stock_icon.h>
65 #include "main_application.h"
67 // To do:
68 // - Threaded decoding?
70 // Current and former RTP player bugs. Many have attachments that can be usef for testing.
71 // Bug 3368 - The timestamp line in a RTP or RTCP packet display's "Not Representable"
72 // Bug 3952 - VoIP Call RTP Player: audio played is corrupted when RFC2833 packets are present
73 // Bug 4960 - RTP Player: Audio and visual feedback get rapidly out of sync
74 // Bug 5527 - Adding arbitrary value to x-axis RTP player
75 // Bug 7935 - Wrong Timestamps in RTP Player-Decode
76 // Bug 8007 - UI gets confused on playing decoded audio in rtp_player
77 // Bug 9007 - Switching SSRC values in RTP stream
78 // Bug 10613 - RTP audio player crashes
79 // Bug 11125 - RTP Player does not show progress in selected stream in Window 7
80 // Bug 11409 - Wireshark crashes when using RTP player
81 // Bug 12166 - RTP audio player crashes
83 // In some places we match by conv/call number, in others we match by first frame.
85 enum {
86 channel_col_,
87 src_addr_col_,
88 src_port_col_,
89 dst_addr_col_,
90 dst_port_col_,
91 ssrc_col_,
92 first_pkt_col_,
93 num_pkts_col_,
94 time_span_col_,
95 sample_rate_col_,
96 play_rate_col_,
97 payload_col_,
99 stream_data_col_ = src_addr_col_, // RtpAudioStream
100 graph_audio_data_col_ = src_port_col_, // QCPGraph (wave)
101 graph_sequence_data_col_ = dst_addr_col_, // QCPGraph (sequence)
102 graph_jitter_data_col_ = dst_port_col_, // QCPGraph (jitter)
103 graph_timestamp_data_col_ = ssrc_col_, // QCPGraph (timestamp)
104 // first_pkt_col_ is skipped, it is used for real data
105 graph_silence_data_col_ = num_pkts_col_, // QCPGraph (silence)
108 class RtpPlayerTreeWidgetItem : public QTreeWidgetItem
110 public:
111 RtpPlayerTreeWidgetItem(QTreeWidget *tree) :
112 QTreeWidgetItem(tree)
116 bool operator< (const QTreeWidgetItem &other) const
118 // Handle numeric sorting
119 switch (treeWidget()->sortColumn()) {
120 case src_port_col_:
121 case dst_port_col_:
122 case num_pkts_col_:
123 case sample_rate_col_:
124 return text(treeWidget()->sortColumn()).toInt() < other.text(treeWidget()->sortColumn()).toInt();
125 case play_rate_col_:
126 return text(treeWidget()->sortColumn()).toInt() < other.text(treeWidget()->sortColumn()).toInt();
127 case first_pkt_col_:
128 int v1;
129 int v2;
131 v1 = data(first_pkt_col_, Qt::UserRole).toInt();
132 v2 = other.data(first_pkt_col_, Qt::UserRole).toInt();
134 return v1 < v2;
135 default:
136 // Fall back to string comparison
137 return QTreeWidgetItem::operator <(other);
142 RtpPlayerDialog *RtpPlayerDialog::pinstance_{nullptr};
143 std::mutex RtpPlayerDialog::init_mutex_;
144 std::mutex RtpPlayerDialog::run_mutex_;
146 RtpPlayerDialog *RtpPlayerDialog::openRtpPlayerDialog(QWidget &parent, CaptureFile &cf, QObject *packet_list, bool capture_running)
148 std::lock_guard<std::mutex> lock(init_mutex_);
149 if (pinstance_ == nullptr)
151 pinstance_ = new RtpPlayerDialog(parent, cf, capture_running);
152 connect(pinstance_, SIGNAL(goToPacket(int)),
153 packet_list, SLOT(goToPacket(int)));
155 return pinstance_;
158 RtpPlayerDialog::RtpPlayerDialog(QWidget &parent, CaptureFile &cf, bool capture_running _U_) :
159 WiresharkDialog(parent, cf)
160 #ifdef QT_MULTIMEDIA_LIB
161 , ui(new Ui::RtpPlayerDialog)
162 , first_stream_rel_start_time_(0.0)
163 , first_stream_abs_start_time_(0.0)
164 , first_stream_rel_stop_time_(0.0)
165 , streams_length_(0.0)
166 , start_marker_time_(0.0)
167 , number_ticker_(new QCPAxisTicker)
168 , datetime_ticker_(new QCPAxisTickerDateTime)
169 , stereo_available_(false)
170 , marker_stream_(0)
171 , marker_stream_requested_out_rate_(0)
172 , last_ti_(0)
173 , listener_removed_(true)
174 , block_redraw_(false)
175 , lock_ui_(0)
176 , read_capture_enabled_(capture_running)
177 , silence_skipped_time_(0.0)
178 #endif // QT_MULTIMEDIA_LIB
180 ui->setupUi(this);
181 loadGeometry(parent.width(), parent.height());
182 setWindowTitle(mainApp->windowTitleString(tr("RTP Player")));
183 ui->streamTreeWidget->installEventFilter(this);
184 ui->audioPlot->installEventFilter(this);
185 installEventFilter(this);
187 #ifdef QT_MULTIMEDIA_LIB
188 ui->splitter->setStretchFactor(0, 3);
189 ui->splitter->setStretchFactor(1, 1);
191 ui->streamTreeWidget->sortByColumn(first_pkt_col_, Qt::AscendingOrder);
193 graph_ctx_menu_ = new QMenu(this);
195 graph_ctx_menu_->addAction(ui->actionZoomIn);
196 graph_ctx_menu_->addAction(ui->actionZoomOut);
197 graph_ctx_menu_->addAction(ui->actionReset);
198 graph_ctx_menu_->addSeparator();
199 graph_ctx_menu_->addAction(ui->actionMoveRight10);
200 graph_ctx_menu_->addAction(ui->actionMoveLeft10);
201 graph_ctx_menu_->addAction(ui->actionMoveRight1);
202 graph_ctx_menu_->addAction(ui->actionMoveLeft1);
203 graph_ctx_menu_->addSeparator();
204 graph_ctx_menu_->addAction(ui->actionGoToPacket);
205 graph_ctx_menu_->addAction(ui->actionGoToSetupPacketPlot);
206 set_action_shortcuts_visible_in_context_menu(graph_ctx_menu_->actions());
208 ui->audioPlot->setContextMenuPolicy(Qt::CustomContextMenu);
209 connect(ui->audioPlot, &QCustomPlot::customContextMenuRequested, this, &RtpPlayerDialog::showGraphContextMenu);
211 ui->streamTreeWidget->setMouseTracking(true);
212 mouse_update_timer_ = new QTimer(this);
213 mouse_update_timer_->setSingleShot(true);
214 mouse_update_timer_->setInterval(10);
215 connect(mouse_update_timer_, &QTimer::timeout, this, &RtpPlayerDialog::mouseMoveUpdate);
217 connect(ui->streamTreeWidget, &QTreeWidget::itemEntered, this, &RtpPlayerDialog::itemEntered);
219 connect(ui->audioPlot, &QCustomPlot::mouseMove, this, &RtpPlayerDialog::mouseMovePlot);
220 connect(ui->audioPlot, &QCustomPlot::mousePress, this, &RtpPlayerDialog::graphClicked);
221 connect(ui->audioPlot, &QCustomPlot::mouseDoubleClick, this, &RtpPlayerDialog::graphDoubleClicked);
222 connect(ui->audioPlot, &QCustomPlot::plottableClick, this, &RtpPlayerDialog::plotClicked);
224 cur_play_pos_ = new QCPItemStraightLine(ui->audioPlot);
225 cur_play_pos_->setVisible(false);
227 start_marker_pos_ = new QCPItemStraightLine(ui->audioPlot);
228 start_marker_pos_->setPen(QPen(Qt::green,4));
229 setStartPlayMarker(0);
230 drawStartPlayMarker();
231 start_marker_pos_->setVisible(true);
233 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
234 notify_timer_.setInterval(100); // ~15 fps
235 connect(&notify_timer_, &QTimer::timeout, this, &RtpPlayerDialog::outputNotify);
236 #endif
238 datetime_ticker_->setDateTimeFormat("yyyy-MM-dd\nhh:mm:ss.zzz");
240 ui->audioPlot->xAxis->setNumberFormat("gb");
241 ui->audioPlot->xAxis->setNumberPrecision(3);
242 ui->audioPlot->xAxis->setTicker(datetime_ticker_);
243 ui->audioPlot->yAxis->setVisible(false);
245 ui->playButton->setIcon(StockIcon("media-playback-start"));
246 ui->playButton->setEnabled(false);
247 ui->pauseButton->setIcon(StockIcon("media-playback-pause"));
248 ui->pauseButton->setCheckable(true);
249 ui->pauseButton->setVisible(false);
250 ui->stopButton->setIcon(StockIcon("media-playback-stop"));
251 ui->stopButton->setEnabled(false);
252 ui->skipSilenceButton->setIcon(StockIcon("media-seek-forward"));
253 ui->skipSilenceButton->setCheckable(true);
254 ui->skipSilenceButton->setEnabled(false);
256 read_btn_ = ui->buttonBox->addButton(ui->actionReadCapture->text(), QDialogButtonBox::ActionRole);
257 read_btn_->setToolTip(ui->actionReadCapture->toolTip());
258 read_btn_->setEnabled(false);
259 connect(read_btn_, &QPushButton::pressed, this, &RtpPlayerDialog::on_actionReadCapture_triggered);
261 inaudible_btn_ = new QToolButton();
262 ui->buttonBox->addButton(inaudible_btn_, QDialogButtonBox::ActionRole);
263 inaudible_btn_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
264 inaudible_btn_->setPopupMode(QToolButton::MenuButtonPopup);
266 connect(ui->actionInaudibleButton, &QAction::triggered, this, &RtpPlayerDialog::on_actionSelectInaudible_triggered);
267 inaudible_btn_->setDefaultAction(ui->actionInaudibleButton);
268 // Overrides text striping of shortcut undercode in QAction
269 inaudible_btn_->setText(ui->actionInaudibleButton->text());
270 inaudible_btn_->setEnabled(false);
271 inaudible_btn_->setMenu(ui->menuInaudible);
273 analyze_btn_ = RtpAnalysisDialog::addAnalyzeButton(ui->buttonBox, this);
275 prepare_btn_ = ui->buttonBox->addButton(ui->actionPrepareFilter->text(), QDialogButtonBox::ActionRole);
276 prepare_btn_->setToolTip(ui->actionPrepareFilter->toolTip());
277 connect(prepare_btn_, &QPushButton::pressed, this, &RtpPlayerDialog::on_actionPrepareFilter_triggered);
279 export_btn_ = ui->buttonBox->addButton(ui->actionExportButton->text(), QDialogButtonBox::ActionRole);
280 export_btn_->setToolTip(ui->actionExportButton->toolTip());
281 export_btn_->setEnabled(false);
282 export_btn_->setMenu(ui->menuExport);
284 // Ordered, unique device names starting with the system default
285 QMap<QString, bool> out_device_map; // true == default device
286 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
287 out_device_map.insert(QMediaDevices::defaultAudioOutput().description(), true);
288 foreach (QAudioDevice out_device, QMediaDevices::audioOutputs()) {
289 if (!out_device_map.contains(out_device.description())) {
290 out_device_map.insert(out_device.description(), false);
293 #else
294 out_device_map.insert(QAudioDeviceInfo::defaultOutputDevice().deviceName(), true);
295 foreach (QAudioDeviceInfo out_device, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
296 if (!out_device_map.contains(out_device.deviceName())) {
297 out_device_map.insert(out_device.deviceName(), false);
300 #endif
302 ui->outputDeviceComboBox->blockSignals(true);
303 foreach (QString out_name, out_device_map.keys()) {
304 ui->outputDeviceComboBox->addItem(out_name);
305 if (out_device_map.value(out_name)) {
306 ui->outputDeviceComboBox->setCurrentIndex(ui->outputDeviceComboBox->count() - 1);
309 if (ui->outputDeviceComboBox->count() < 1) {
310 ui->outputDeviceComboBox->setEnabled(false);
311 ui->playButton->setEnabled(false);
312 ui->pauseButton->setEnabled(false);
313 ui->stopButton->setEnabled(false);
314 ui->skipSilenceButton->setEnabled(false);
315 ui->minSilenceSpinBox->setEnabled(false);
316 ui->outputDeviceComboBox->addItem(tr("No devices available"));
317 ui->outputAudioRate->setEnabled(false);
318 } else {
319 stereo_available_ = isStereoAvailable();
320 fillAudioRateMenu();
322 ui->outputDeviceComboBox->blockSignals(false);
324 ui->audioPlot->setMouseTracking(true);
325 ui->audioPlot->setEnabled(true);
326 ui->audioPlot->setInteractions(
327 QCP::iRangeDrag |
328 QCP::iRangeZoom
331 graph_ctx_menu_->addSeparator();
332 list_ctx_menu_ = new QMenu(this);
333 list_ctx_menu_->addAction(ui->actionPlay);
334 graph_ctx_menu_->addAction(ui->actionPlay);
335 list_ctx_menu_->addAction(ui->actionStop);
336 graph_ctx_menu_->addAction(ui->actionStop);
337 list_ctx_menu_->addMenu(ui->menuSelect);
338 graph_ctx_menu_->addMenu(ui->menuSelect);
339 list_ctx_menu_->addMenu(ui->menuAudioRouting);
340 graph_ctx_menu_->addMenu(ui->menuAudioRouting);
341 list_ctx_menu_->addAction(ui->actionRemoveStream);
342 graph_ctx_menu_->addAction(ui->actionRemoveStream);
343 list_ctx_menu_->addAction(ui->actionGoToSetupPacketTree);
344 set_action_shortcuts_visible_in_context_menu(list_ctx_menu_->actions());
346 connect(&cap_file_, &CaptureFile::captureEvent, this, &RtpPlayerDialog::captureEvent);
347 connect(this, SIGNAL(updateFilter(QString, bool)),
348 &parent, SLOT(filterPackets(QString, bool)));
349 connect(this, SIGNAL(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)),
350 &parent, SLOT(rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *>)));
351 connect(this, SIGNAL(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)),
352 &parent, SLOT(rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *>)));
353 connect(this, SIGNAL(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)),
354 &parent, SLOT(rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *>)));
355 #endif // QT_MULTIMEDIA_LIB
358 // _U_ is used when no QT_MULTIMEDIA_LIB is available
359 QToolButton *RtpPlayerDialog::addPlayerButton(QDialogButtonBox *button_box, QDialog *dialog _U_)
361 if (!button_box) return NULL;
363 QAction *ca;
364 QToolButton *player_button = new QToolButton();
365 button_box->addButton(player_button, QDialogButtonBox::ActionRole);
366 player_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
367 player_button->setPopupMode(QToolButton::MenuButtonPopup);
369 ca = new QAction(tr("&Play Streams"), player_button);
370 ca->setToolTip(tr("Open RTP player dialog"));
371 ca->setIcon(StockIcon("media-playback-start"));
372 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerReplace()));
373 player_button->setDefaultAction(ca);
374 // Overrides text striping of shortcut undercode in QAction
375 player_button->setText(ca->text());
377 #if defined(QT_MULTIMEDIA_LIB)
378 QMenu *button_menu = new QMenu(player_button);
379 button_menu->setToolTipsVisible(true);
380 ca = button_menu->addAction(tr("&Set playlist"));
381 ca->setToolTip(tr("Replace existing playlist in RTP Player with new one"));
382 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerReplace()));
383 ca = button_menu->addAction(tr("&Add to playlist"));
384 ca->setToolTip(tr("Add new set to existing playlist in RTP Player"));
385 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerAdd()));
386 ca = button_menu->addAction(tr("&Remove from playlist"));
387 ca->setToolTip(tr("Remove selected streams from playlist in RTP Player"));
388 connect(ca, SIGNAL(triggered()), dialog, SLOT(rtpPlayerRemove()));
389 player_button->setMenu(button_menu);
390 #else
391 player_button->setEnabled(false);
392 player_button->setText(tr("No Audio"));
393 #endif
395 return player_button;
398 #ifdef QT_MULTIMEDIA_LIB
399 RtpPlayerDialog::~RtpPlayerDialog()
401 std::lock_guard<std::mutex> lock(init_mutex_);
402 if (pinstance_ != nullptr) {
403 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
404 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
405 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
406 if (audio_stream)
407 delete audio_stream;
409 cleanupMarkerStream();
410 delete ui;
411 pinstance_ = nullptr;
415 void RtpPlayerDialog::accept()
417 if (!listener_removed_) {
418 remove_tap_listener(this);
419 listener_removed_ = true;
422 int row_count = ui->streamTreeWidget->topLevelItemCount();
423 // Stop all streams before the dialogs are closed.
424 for (int row = 0; row < row_count; row++) {
425 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
426 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
427 audio_stream->stopPlaying();
429 WiresharkDialog::accept();
432 void RtpPlayerDialog::reject()
434 RtpPlayerDialog::accept();
437 void RtpPlayerDialog::retapPackets()
439 if (!listener_removed_) {
440 // Retap is running, nothing better we can do
441 return;
443 lockUI();
444 ui->hintLabel->setText("<i><small>" + tr("Decoding streams...") + "</i></small>");
445 mainApp->processEvents();
447 // Clear packets from existing streams before retap
448 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
449 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
450 RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
452 row_stream->clearPackets();
455 // destroyCheck is protection against destroying dialog during recap.
456 // It stores dialog pointer in data() and if dialog destroyed, it
457 // returns null
458 QPointer<RtpPlayerDialog> destroyCheck=this;
459 GString *error_string;
461 listener_removed_ = false;
462 error_string = register_tap_listener("rtp", this, NULL, 0, NULL, tapPacket, NULL, NULL);
463 if (error_string) {
464 report_failure("RTP Player - tap registration failed: %s", error_string->str);
465 g_string_free(error_string, TRUE);
466 unlockUI();
467 return;
469 cap_file_.retapPackets();
471 // Check if dialog exists still
472 if (destroyCheck.data()) {
473 if (!listener_removed_) {
474 remove_tap_listener(this);
475 listener_removed_ = true;
477 fillTappedColumns();
478 rescanPackets(true);
480 unlockUI();
483 void RtpPlayerDialog::rescanPackets(bool rescale_axes)
485 lockUI();
486 // Show information for a user - it can last long time...
487 playback_error_.clear();
488 ui->hintLabel->setText("<i><small>" + tr("Decoding streams...") + "</i></small>");
489 mainApp->processEvents();
491 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
492 QAudioDevice cur_out_device = getCurrentDeviceInfo();
493 #else
494 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
495 #endif
496 int row_count = ui->streamTreeWidget->topLevelItemCount();
498 // Reset stream values
499 for (int row = 0; row < row_count; row++) {
500 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
501 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
502 audio_stream->setStereoRequired(stereo_available_);
503 audio_stream->reset(first_stream_rel_start_time_);
505 audio_stream->setJitterBufferSize((int) ui->jitterSpinBox->value());
507 RtpAudioStream::TimingMode timing_mode = RtpAudioStream::JitterBuffer;
508 switch (ui->timingComboBox->currentIndex()) {
509 case RtpAudioStream::RtpTimestamp:
510 timing_mode = RtpAudioStream::RtpTimestamp;
511 break;
512 case RtpAudioStream::Uninterrupted:
513 timing_mode = RtpAudioStream::Uninterrupted;
514 break;
515 default:
516 break;
518 audio_stream->setTimingMode(timing_mode);
520 //if (!cur_out_device.isNull()) {
521 audio_stream->decode(cur_out_device);
525 for (int col = 0; col < ui->streamTreeWidget->columnCount() - 1; col++) {
526 ui->streamTreeWidget->resizeColumnToContents(col);
529 createPlot(rescale_axes);
531 updateWidgets();
532 unlockUI();
535 void RtpPlayerDialog::createPlot(bool rescale_axes)
537 bool legend_out_of_sequence = false;
538 bool legend_jitter_dropped = false;
539 bool legend_wrong_timestamps = false;
540 bool legend_inserted_silences = false;
541 bool relative_timestamps = !ui->todCheckBox->isChecked();
542 int row_count = ui->streamTreeWidget->topLevelItemCount();
543 int16_t total_max_sample_value = 1;
545 ui->audioPlot->clearGraphs();
547 if (relative_timestamps) {
548 ui->audioPlot->xAxis->setTicker(number_ticker_);
549 } else {
550 ui->audioPlot->xAxis->setTicker(datetime_ticker_);
553 // Calculate common Y scale for graphs
554 for (int row = 0; row < row_count; row++) {
555 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
556 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
557 int16_t max_sample_value = audio_stream->getMaxSampleValue();
559 if (max_sample_value > total_max_sample_value) {
560 total_max_sample_value = max_sample_value;
564 // Clear existing graphs
565 for (int row = 0; row < row_count; row++) {
566 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
567 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
568 int y_offset = row_count - row - 1;
569 AudioRouting audio_routing = audio_stream->getAudioRouting();
571 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant());
572 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant());
573 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant());
574 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant());
575 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant());
577 // Set common scale
578 audio_stream->setMaxSampleValue(total_max_sample_value);
580 // Waveform
581 RtpAudioGraph *audio_graph = new RtpAudioGraph(ui->audioPlot, audio_stream->color());
582 audio_graph->setMuted(audio_routing.isMuted());
583 audio_graph->setData(audio_stream->visualTimestamps(relative_timestamps), audio_stream->visualSamples(y_offset));
584 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant::fromValue<RtpAudioGraph *>(audio_graph));
585 //RTP_STREAM_DEBUG("Plotting %s, %d samples", ti->text(src_addr_col_).toUtf8().constData(), audio_graph->wave->data()->size());
587 QString span_str;
588 if (ui->todCheckBox->isChecked()) {
589 QDateTime date_time1 = QDateTime::fromMSecsSinceEpoch((audio_stream->startRelTime() + first_stream_abs_start_time_ - audio_stream->startRelTime()) * 1000.0);
590 QDateTime date_time2 = QDateTime::fromMSecsSinceEpoch((audio_stream->stopRelTime() + first_stream_abs_start_time_ - audio_stream->startRelTime()) * 1000.0);
591 QString time_str1 = date_time1.toString("yyyy-MM-dd hh:mm:ss.zzz");
592 QString time_str2 = date_time2.toString("yyyy-MM-dd hh:mm:ss.zzz");
593 span_str = QStringLiteral("%1 - %2 (%3)")
594 .arg(time_str1)
595 .arg(time_str2)
596 .arg(QString::number(audio_stream->stopRelTime() - audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1));
597 } else {
598 span_str = QStringLiteral("%1 - %2 (%3)")
599 .arg(QString::number(audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1))
600 .arg(QString::number(audio_stream->stopRelTime(), 'f', prefs.gui_decimal_places1))
601 .arg(QString::number(audio_stream->stopRelTime() - audio_stream->startRelTime(), 'f', prefs.gui_decimal_places1));
603 ti->setText(time_span_col_, span_str);
604 ti->setText(sample_rate_col_, QString::number(audio_stream->sampleRate()));
605 ti->setText(play_rate_col_, QString::number(audio_stream->playRate()));
606 ti->setText(payload_col_, audio_stream->payloadNames().join(", "));
608 if (audio_stream->outOfSequence() > 0) {
609 // Sequence numbers
610 QCPGraph *seq_graph = ui->audioPlot->addGraph();
611 seq_graph->setLineStyle(QCPGraph::lsNone);
612 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssSquare, tango_aluminium_6, Qt::white, mainApp->font().pointSize())); // Arbitrary
613 seq_graph->setSelectable(QCP::stNone);
614 seq_graph->setData(audio_stream->outOfSequenceTimestamps(relative_timestamps), audio_stream->outOfSequenceSamples(y_offset));
615 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
616 if (legend_out_of_sequence) {
617 seq_graph->removeFromLegend();
618 } else {
619 seq_graph->setName(tr("Out of Sequence"));
620 legend_out_of_sequence = true;
624 if (audio_stream->jitterDropped() > 0) {
625 // Jitter drops
626 QCPGraph *seq_graph = ui->audioPlot->addGraph();
627 seq_graph->setLineStyle(QCPGraph::lsNone);
628 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, tango_scarlet_red_5, Qt::white, mainApp->font().pointSize())); // Arbitrary
629 seq_graph->setSelectable(QCP::stNone);
630 seq_graph->setData(audio_stream->jitterDroppedTimestamps(relative_timestamps), audio_stream->jitterDroppedSamples(y_offset));
631 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
632 if (legend_jitter_dropped) {
633 seq_graph->removeFromLegend();
634 } else {
635 seq_graph->setName(tr("Jitter Drops"));
636 legend_jitter_dropped = true;
640 if (audio_stream->wrongTimestamps() > 0) {
641 // Wrong timestamps
642 QCPGraph *seq_graph = ui->audioPlot->addGraph();
643 seq_graph->setLineStyle(QCPGraph::lsNone);
644 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDiamond, tango_sky_blue_5, Qt::white, mainApp->font().pointSize())); // Arbitrary
645 seq_graph->setSelectable(QCP::stNone);
646 seq_graph->setData(audio_stream->wrongTimestampTimestamps(relative_timestamps), audio_stream->wrongTimestampSamples(y_offset));
647 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
648 if (legend_wrong_timestamps) {
649 seq_graph->removeFromLegend();
650 } else {
651 seq_graph->setName(tr("Wrong Timestamps"));
652 legend_wrong_timestamps = true;
656 if (audio_stream->insertedSilences() > 0) {
657 // Inserted silence
658 QCPGraph *seq_graph = ui->audioPlot->addGraph();
659 seq_graph->setLineStyle(QCPGraph::lsNone);
660 seq_graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, tango_butter_5, Qt::white, mainApp->font().pointSize())); // Arbitrary
661 seq_graph->setSelectable(QCP::stNone);
662 seq_graph->setData(audio_stream->insertedSilenceTimestamps(relative_timestamps), audio_stream->insertedSilenceSamples(y_offset));
663 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant::fromValue<QCPGraph *>(seq_graph));
664 if (legend_inserted_silences) {
665 seq_graph->removeFromLegend();
666 } else {
667 seq_graph->setName(tr("Inserted Silence"));
668 legend_inserted_silences = true;
672 ui->audioPlot->legend->setVisible(legend_out_of_sequence || legend_jitter_dropped || legend_wrong_timestamps || legend_inserted_silences);
674 ui->audioPlot->replot();
675 if (rescale_axes) resetXAxis();
678 void RtpPlayerDialog::fillTappedColumns()
680 // true just for first stream
681 bool is_first = true;
683 // Get all rows, immutable list. Later changes in rows might reorder them
684 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->findItems(
685 QStringLiteral("*"), Qt::MatchWrap | Qt::MatchWildcard | Qt::MatchRecursive);
687 // Update rows by calculated values, it might reorder them in view...
688 foreach(QTreeWidgetItem *ti, items) {
689 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
690 if (audio_stream) {
691 rtpstream_info_t *rtpstream = audio_stream->getStreamInfo();
693 // 0xFFFFFFFF mean no setup frame
694 // first_packet_num == setup_frame_number happens, when
695 // rtp_udp is active or Decode as was used
696 if ((rtpstream->setup_frame_number == 0xFFFFFFFF) ||
697 (rtpstream->rtp_stats.first_packet_num == rtpstream->setup_frame_number)
699 int packet = rtpstream->rtp_stats.first_packet_num;
700 ti->setText(first_pkt_col_, QStringLiteral("RTP %1").arg(packet));
701 ti->setData(first_pkt_col_, Qt::UserRole, QVariant(packet));
702 } else {
703 int packet = rtpstream->setup_frame_number;
704 ti->setText(first_pkt_col_, QStringLiteral("SETUP %1").arg(rtpstream->setup_frame_number));
705 ti->setData(first_pkt_col_, Qt::UserRole, QVariant(packet));
707 ti->setText(num_pkts_col_, QString::number(rtpstream->packet_count));
708 updateStartStopTime(rtpstream, is_first);
709 is_first = false;
712 setMarkers();
715 void RtpPlayerDialog::addSingleRtpStream(rtpstream_id_t *id)
717 bool found = false;
719 AudioRouting audio_routing = AudioRouting(AUDIO_UNMUTED, channel_mono);
721 if (!id) return;
723 // Find the RTP streams associated with this conversation.
724 // gtk/rtp_player.c:mark_rtp_stream_to_play does this differently.
726 QList<RtpAudioStream *> streams = stream_hash_.values(rtpstream_id_to_hash(id));
727 for (int i = 0; i < streams.size(); i++) {
728 RtpAudioStream *row_stream = streams.at(i);
729 if (row_stream->isMatch(id)) {
730 found = true;
731 break;
736 if (found) {
737 return;
740 try {
741 int tli_count = ui->streamTreeWidget->topLevelItemCount();
743 RtpAudioStream *audio_stream = new RtpAudioStream(this, id, stereo_available_);
744 audio_stream->setColor(ColorUtils::graphColor(tli_count));
746 QTreeWidgetItem *ti = new RtpPlayerTreeWidgetItem(ui->streamTreeWidget);
747 stream_hash_.insert(rtpstream_id_to_hash(id), audio_stream);
748 ti->setText(src_addr_col_, address_to_qstring(&(id->src_addr)));
749 ti->setText(src_port_col_, QString::number(id->src_port));
750 ti->setText(dst_addr_col_, address_to_qstring(&(id->dst_addr)));
751 ti->setText(dst_port_col_, QString::number(id->dst_port));
752 ti->setText(ssrc_col_, int_to_qstring(id->ssrc, 8, 16));
754 // Calculated items are updated after every retapPackets()
756 ti->setData(stream_data_col_, Qt::UserRole, QVariant::fromValue(audio_stream));
757 if (stereo_available_) {
758 if (tli_count%2) {
759 audio_routing.setChannel(channel_stereo_right);
760 } else {
761 audio_routing.setChannel(channel_stereo_left);
763 } else {
764 audio_routing.setChannel(channel_mono);
766 ti->setToolTip(channel_col_, tr("Double click on cell to change audio routing"));
767 formatAudioRouting(ti, audio_routing);
768 audio_stream->setAudioRouting(audio_routing);
770 for (int col = 0; col < ui->streamTreeWidget->columnCount(); col++) {
771 QBrush fgBrush = ti->foreground(col);
772 fgBrush.setColor(audio_stream->color());
773 fgBrush.setStyle(Qt::SolidPattern);
774 ti->setForeground(col, fgBrush);
777 connect(audio_stream, &RtpAudioStream::finishedPlaying, this, &RtpPlayerDialog::playFinished);
778 connect(audio_stream, &RtpAudioStream::playbackError, this, &RtpPlayerDialog::setPlaybackError);
779 } catch (...) {
780 qWarning() << "Stream ignored, try to add fewer streams to playlist";
783 RTP_STREAM_DEBUG("adding stream %d to layout",
784 ui->streamTreeWidget->topLevelItemCount());
787 void RtpPlayerDialog::lockUI()
789 if (0 == lock_ui_++) {
790 if (playing_streams_.count() > 0) {
791 on_stopButton_clicked();
793 setEnabled(false);
797 void RtpPlayerDialog::unlockUI()
799 if (--lock_ui_ == 0) {
800 setEnabled(true);
804 void RtpPlayerDialog::replaceRtpStreams(QVector<rtpstream_id_t *> stream_ids)
806 std::unique_lock<std::mutex> lock(run_mutex_, std::try_to_lock);
807 if (lock.owns_lock()) {
808 lockUI();
810 // Delete all existing rows
811 if (last_ti_) {
812 highlightItem(last_ti_, false);
813 last_ti_ = NULL;
816 for (int row = ui->streamTreeWidget->topLevelItemCount() - 1; row >= 0; row--) {
817 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
818 removeRow(ti);
821 // Add all new streams
822 for (int i=0; i < stream_ids.size(); i++) {
823 addSingleRtpStream(stream_ids[i]);
825 setMarkers();
827 unlockUI();
828 #ifdef QT_MULTIMEDIA_LIB
829 QTimer::singleShot(0, this, SLOT(retapPackets()));
830 #endif
831 } else {
832 ws_warning("replaceRtpStreams was called while other thread locked it. Current call is ignored, try it later.");
836 void RtpPlayerDialog::addRtpStreams(QVector<rtpstream_id_t *> stream_ids)
838 std::unique_lock<std::mutex> lock(run_mutex_, std::try_to_lock);
839 if (lock.owns_lock()) {
840 lockUI();
842 int tli_count = ui->streamTreeWidget->topLevelItemCount();
844 // Add new streams
845 for (int i=0; i < stream_ids.size(); i++) {
846 addSingleRtpStream(stream_ids[i]);
849 if (tli_count == 0) {
850 setMarkers();
853 unlockUI();
854 #ifdef QT_MULTIMEDIA_LIB
855 QTimer::singleShot(0, this, SLOT(retapPackets()));
856 #endif
857 } else {
858 ws_warning("addRtpStreams was called while other thread locked it. Current call is ignored, try it later.");
862 void RtpPlayerDialog::removeRtpStreams(QVector<rtpstream_id_t *> stream_ids)
864 std::unique_lock<std::mutex> lock(run_mutex_, std::try_to_lock);
865 if (lock.owns_lock()) {
866 lockUI();
867 int tli_count = ui->streamTreeWidget->topLevelItemCount();
869 for (int i=0; i < stream_ids.size(); i++) {
870 for (int row = 0; row < tli_count; row++) {
871 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
872 RtpAudioStream *row_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
873 if (row_stream->isMatch(stream_ids[i])) {
874 removeRow(ti);
875 tli_count--;
876 break;
880 updateGraphs();
882 updateWidgets();
883 unlockUI();
884 } else {
885 ws_warning("removeRtpStreams was called while other thread locked it. Current call is ignored, try it later.");
889 void RtpPlayerDialog::setMarkers()
891 setStartPlayMarker(0);
892 drawStartPlayMarker();
895 void RtpPlayerDialog::showEvent(QShowEvent *)
897 // We could use loadSplitterState(ui->splitter) instead of always
898 // resetting the plot size to 75%
899 QList<int> split_sizes = ui->splitter->sizes();
900 int tot_size = split_sizes[0] + split_sizes[1];
901 int plot_size = tot_size * 3 / 4;
902 split_sizes.clear();
903 split_sizes << plot_size << tot_size - plot_size;
904 ui->splitter->setSizes(split_sizes);
907 bool RtpPlayerDialog::eventFilter(QObject *, QEvent *event)
909 if (event->type() == QEvent::KeyPress) {
910 QKeyEvent &keyEvent = static_cast<QKeyEvent&>(*event);
911 int pan_secs = keyEvent.modifiers() & Qt::ShiftModifier ? 1 : 10;
913 switch(keyEvent.key()) {
914 case Qt::Key_Minus:
915 case Qt::Key_Underscore: // Shifted minus on U.S. keyboards
916 case Qt::Key_O: // GTK+
917 case Qt::Key_R:
918 on_actionZoomOut_triggered();
919 return true;
920 case Qt::Key_Plus:
921 case Qt::Key_Equal: // Unshifted plus on U.S. keyboards
922 case Qt::Key_I: // GTK+
923 if (keyEvent.modifiers() == Qt::ControlModifier) {
924 // Ctrl+I
925 on_actionSelectInvert_triggered();
926 return true;
927 } else {
928 // I
929 on_actionZoomIn_triggered();
930 return true;
932 break;
933 case Qt::Key_Right:
934 case Qt::Key_L:
935 panXAxis(pan_secs);
936 return true;
937 case Qt::Key_Left:
938 case Qt::Key_H:
939 panXAxis(-1 * pan_secs);
940 return true;
941 case Qt::Key_0:
942 case Qt::Key_ParenRight: // Shifted 0 on U.S. keyboards
943 on_actionReset_triggered();
944 return true;
945 case Qt::Key_G:
946 if (keyEvent.modifiers() == Qt::ShiftModifier) {
947 // Goto SETUP frame, use correct call based on caller
948 QPoint pos1 = ui->audioPlot->mapFromGlobal(QCursor::pos());
949 QPoint pos2 = ui->streamTreeWidget->mapFromGlobal(QCursor::pos());
950 if (ui->audioPlot->rect().contains(pos1)) {
951 // audio plot, by mouse coords
952 on_actionGoToSetupPacketPlot_triggered();
953 } else if (ui->streamTreeWidget->rect().contains(pos2)) {
954 // packet tree, by cursor
955 on_actionGoToSetupPacketTree_triggered();
957 return true;
958 } else {
959 on_actionGoToPacket_triggered();
960 return true;
962 case Qt::Key_A:
963 if (keyEvent.modifiers() == Qt::ControlModifier) {
964 // Ctrl+A
965 on_actionSelectAll_triggered();
966 return true;
967 } else if (keyEvent.modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
968 // Ctrl+Shift+A
969 on_actionSelectNone_triggered();
970 return true;
972 break;
973 case Qt::Key_M:
974 if (keyEvent.modifiers() == Qt::ShiftModifier) {
975 on_actionAudioRoutingUnmute_triggered();
976 return true;
977 } else if (keyEvent.modifiers() == Qt::ControlModifier) {
978 on_actionAudioRoutingMuteInvert_triggered();
979 return true;
980 } else {
981 on_actionAudioRoutingMute_triggered();
982 return true;
984 case Qt::Key_Delete:
985 on_actionRemoveStream_triggered();
986 return true;
987 case Qt::Key_X:
988 if (keyEvent.modifiers() == Qt::ControlModifier) {
989 // Ctrl+X
990 on_actionRemoveStream_triggered();
991 return true;
993 break;
994 case Qt::Key_Down:
995 case Qt::Key_Up:
996 case Qt::Key_PageUp:
997 case Qt::Key_PageDown:
998 case Qt::Key_Home:
999 case Qt::Key_End:
1000 // Route keys to QTreeWidget
1001 ui->streamTreeWidget->setFocus();
1002 break;
1003 case Qt::Key_P:
1004 if (keyEvent.modifiers() == Qt::NoModifier) {
1005 on_actionPlay_triggered();
1006 return true;
1008 break;
1009 case Qt::Key_S:
1010 on_actionStop_triggered();
1011 return true;
1012 case Qt::Key_N:
1013 if (keyEvent.modifiers() == Qt::ShiftModifier) {
1014 // Shift+N
1015 on_actionDeselectInaudible_triggered();
1016 return true;
1017 } else {
1018 on_actionSelectInaudible_triggered();
1019 return true;
1021 break;
1025 return false;
1028 void RtpPlayerDialog::contextMenuEvent(QContextMenuEvent *event)
1030 list_ctx_menu_->popup(event->globalPos());
1033 void RtpPlayerDialog::updateWidgets()
1035 bool enable_play = true;
1036 bool enable_pause = false;
1037 bool enable_stop = false;
1038 bool enable_timing = true;
1039 int count = ui->streamTreeWidget->topLevelItemCount();
1040 qsizetype selected = ui->streamTreeWidget->selectedItems().count();
1042 if (count < 1) {
1043 enable_play = false;
1044 ui->skipSilenceButton->setEnabled(false);
1045 ui->minSilenceSpinBox->setEnabled(false);
1046 } else {
1047 ui->skipSilenceButton->setEnabled(true);
1048 ui->minSilenceSpinBox->setEnabled(true);
1051 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1052 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1054 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1055 if (audio_stream->outputState() != QAudio::IdleState) {
1056 enable_play = false;
1057 enable_pause = true;
1058 enable_stop = true;
1059 enable_timing = false;
1063 ui->actionAudioRoutingP->setVisible(!stereo_available_);
1064 ui->actionAudioRoutingL->setVisible(stereo_available_);
1065 ui->actionAudioRoutingLR->setVisible(stereo_available_);
1066 ui->actionAudioRoutingR->setVisible(stereo_available_);
1068 ui->playButton->setEnabled(enable_play);
1069 if (enable_play) {
1070 ui->playButton->setVisible(true);
1071 ui->pauseButton->setVisible(false);
1072 } else if (enable_pause) {
1073 ui->playButton->setVisible(false);
1074 ui->pauseButton->setVisible(true);
1076 ui->outputDeviceComboBox->setEnabled(enable_play);
1077 ui->outputAudioRate->setEnabled(enable_play);
1078 ui->pauseButton->setEnabled(enable_pause);
1079 ui->stopButton->setEnabled(enable_stop);
1080 ui->actionStop->setEnabled(enable_stop);
1081 cur_play_pos_->setVisible(enable_stop);
1083 ui->jitterSpinBox->setEnabled(enable_timing);
1084 ui->timingComboBox->setEnabled(enable_timing);
1085 ui->todCheckBox->setEnabled(enable_timing);
1087 read_btn_->setEnabled(read_capture_enabled_);
1088 inaudible_btn_->setEnabled(count > 0);
1089 analyze_btn_->setEnabled(selected > 0);
1090 prepare_btn_->setEnabled(selected > 0);
1092 updateHintLabel();
1093 ui->audioPlot->replot();
1096 void RtpPlayerDialog::handleItemHighlight(QTreeWidgetItem *ti, bool scroll)
1098 if (ti) {
1099 if (ti != last_ti_) {
1100 if (last_ti_) {
1101 highlightItem(last_ti_, false);
1103 highlightItem(ti, true);
1105 if (scroll)
1106 ui->streamTreeWidget->scrollToItem(ti, QAbstractItemView::EnsureVisible);
1107 ui->audioPlot->replot();
1108 last_ti_ = ti;
1110 } else {
1111 if (last_ti_) {
1112 highlightItem(last_ti_, false);
1113 ui->audioPlot->replot();
1114 last_ti_ = NULL;
1119 void RtpPlayerDialog::highlightItem(QTreeWidgetItem *ti, bool highlight)
1121 QFont font;
1122 RtpAudioGraph *audio_graph;
1124 font.setBold(highlight);
1125 for(int i=0; i<ui->streamTreeWidget->columnCount(); i++) {
1126 ti->setFont(i, font);
1129 audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1130 if (audio_graph) {
1131 audio_graph->setHighlight(highlight);
1135 void RtpPlayerDialog::itemEntered(QTreeWidgetItem *item, int column _U_)
1137 handleItemHighlight(item, false);
1140 void RtpPlayerDialog::mouseMovePlot(QMouseEvent *event)
1142 // The calculations are expensive, so just store the position and
1143 // calculate no more than once per some interval. (On Linux the
1144 // QMouseEvents can be sent absurdly often, every 25 microseconds!)
1145 mouse_pos_ = event->pos();
1146 if (!mouse_update_timer_->isActive()) {
1147 mouse_update_timer_->start();
1151 void RtpPlayerDialog::mouseMoveUpdate()
1153 // findItemByCoords is expensive (because of calling pointDistance),
1154 // and updateHintLabel calls it as well via getHoveredPacket. Some
1155 // way to only perform the distance calculations once would be better.
1156 updateHintLabel();
1158 QTreeWidgetItem *ti = findItemByCoords(mouse_pos_);
1159 handleItemHighlight(ti, true);
1162 void RtpPlayerDialog::showGraphContextMenu(const QPoint &pos)
1164 graph_ctx_menu_->popup(ui->audioPlot->mapToGlobal(pos));
1167 void RtpPlayerDialog::graphClicked(QMouseEvent*)
1169 updateWidgets();
1172 void RtpPlayerDialog::graphDoubleClicked(QMouseEvent *event)
1174 updateWidgets();
1175 if (event->button() == Qt::LeftButton) {
1176 // Move start play line
1177 double ts = ui->audioPlot->xAxis->pixelToCoord(event->pos().x());
1179 setStartPlayMarker(ts);
1180 drawStartPlayMarker();
1182 ui->audioPlot->replot();
1186 void RtpPlayerDialog::plotClicked(QCPAbstractPlottable *plottable _U_, int dataIndex _U_, QMouseEvent *event)
1188 // Delivered plottable very often points to different element than a mouse
1189 // so we find right one by mouse coordinates
1190 QTreeWidgetItem *ti = findItemByCoords(event->pos());
1191 if (ti) {
1192 if (event->modifiers() == Qt::NoModifier) {
1193 ti->setSelected(true);
1194 } else if (event->modifiers() == Qt::ControlModifier) {
1195 ti->setSelected(!ti->isSelected());
1200 QTreeWidgetItem *RtpPlayerDialog::findItemByCoords(QPoint point)
1202 QCPAbstractPlottable *plottable=ui->audioPlot->plottableAt(point);
1203 if (plottable) {
1204 return findItem(plottable);
1207 return NULL;
1210 QTreeWidgetItem *RtpPlayerDialog::findItem(QCPAbstractPlottable *plottable)
1212 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1213 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1214 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1215 if (audio_graph && audio_graph->isMyPlottable(plottable)) {
1216 return ti;
1220 return NULL;
1223 void RtpPlayerDialog::updateHintLabel()
1225 int packet_num = getHoveredPacket();
1226 QString hint = "<small><i>";
1227 double start_pos = getStartPlayMarker();
1228 int row_count = ui->streamTreeWidget->topLevelItemCount();
1229 qsizetype selected = ui->streamTreeWidget->selectedItems().count();
1230 int not_muted = 0;
1232 hint += tr("%1 streams").arg(row_count);
1234 if (row_count > 0) {
1235 if (selected > 0) {
1236 hint += tr(", %1 selected").arg(selected);
1239 for (int row = 0; row < row_count; row++) {
1240 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1241 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1242 if (audio_stream && (!audio_stream->getAudioRouting().isMuted())) {
1243 not_muted++;
1247 hint += tr(", %1 not muted").arg(not_muted);
1250 if (packet_num == 0) {
1251 hint += tr(", start: %1. Double click on graph to set start of playback.")
1252 .arg(getFormatedTime(start_pos));
1253 } else if (packet_num > 0) {
1254 hint += tr(", start: %1, cursor: %2. Press \"G\" to go to packet %3. Double click on graph to set start of playback.")
1255 .arg(getFormatedTime(start_pos))
1256 .arg(getFormatedHoveredTime())
1257 .arg(packet_num);
1260 if (!playback_error_.isEmpty()) {
1261 hint += " <font color=\"red\">";
1262 hint += playback_error_;
1263 hint += " </font>";
1266 hint += "</i></small>";
1267 ui->hintLabel->setText(hint);
1270 void RtpPlayerDialog::resetXAxis()
1272 QCustomPlot *ap = ui->audioPlot;
1274 double pixel_pad = 10.0; // per side
1276 ap->rescaleAxes(true);
1278 double axis_pixels = ap->xAxis->axisRect()->width();
1279 ap->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->xAxis->range().center());
1281 axis_pixels = ap->yAxis->axisRect()->height();
1282 ap->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->yAxis->range().center());
1284 ap->replot();
1287 void RtpPlayerDialog::updateGraphs()
1289 QCustomPlot *ap = ui->audioPlot;
1291 // Create new plots, just existing ones
1292 createPlot(false);
1294 // Rescale Y axis
1295 double pixel_pad = 10.0; // per side
1296 double axis_pixels = ap->yAxis->axisRect()->height();
1297 ap->yAxis->rescale(true);
1298 ap->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, ap->yAxis->range().center());
1300 ap->replot();
1303 void RtpPlayerDialog::playFinished(RtpAudioStream *stream, QAudio::Error error)
1305 if ((error != QAudio::NoError) && (error != QAudio::UnderrunError)) {
1306 setPlaybackError(tr("Playback of stream %1 failed!")
1307 .arg(stream->getIDAsQString())
1310 playing_streams_.removeOne(stream);
1311 if (playing_streams_.isEmpty()) {
1312 if (marker_stream_) {
1313 marker_stream_->stop();
1315 updateWidgets();
1319 void RtpPlayerDialog::setPlayPosition(double secs)
1321 double cur_secs = cur_play_pos_->point1->key();
1323 if (ui->todCheckBox->isChecked()) {
1324 secs += first_stream_abs_start_time_;
1325 } else {
1326 secs += first_stream_rel_start_time_;
1328 if (secs > cur_secs) {
1329 cur_play_pos_->point1->setCoords(secs, 0.0);
1330 cur_play_pos_->point2->setCoords(secs, 1.0);
1331 ui->audioPlot->replot();
1335 void RtpPlayerDialog::setPlaybackError(const QString playback_error)
1337 playback_error_ = playback_error;
1338 updateHintLabel();
1341 tap_packet_status RtpPlayerDialog::tapPacket(void *tapinfo_ptr, packet_info *pinfo, epan_dissect_t *, const void *rtpinfo_ptr, tap_flags_t)
1343 RtpPlayerDialog *rtp_player_dialog = dynamic_cast<RtpPlayerDialog *>((RtpPlayerDialog*)tapinfo_ptr);
1344 if (!rtp_player_dialog) return TAP_PACKET_DONT_REDRAW;
1346 const struct _rtp_info *rtpinfo = (const struct _rtp_info *)rtpinfo_ptr;
1347 if (!rtpinfo) return TAP_PACKET_DONT_REDRAW;
1349 /* ignore RTP Version != 2 */
1350 if (rtpinfo->info_version != 2)
1351 return TAP_PACKET_DONT_REDRAW;
1353 rtp_player_dialog->addPacket(pinfo, rtpinfo);
1355 return TAP_PACKET_DONT_REDRAW;
1358 void RtpPlayerDialog::addPacket(packet_info *pinfo, const _rtp_info *rtpinfo)
1360 // Search stream in hash key, if there are multiple streams with same hash
1361 QList<RtpAudioStream *> streams = stream_hash_.values(pinfo_rtp_info_to_hash(pinfo, rtpinfo));
1362 for (int i = 0; i < streams.size(); i++) {
1363 RtpAudioStream *row_stream = streams.at(i);
1364 if (row_stream->isMatch(pinfo, rtpinfo)) {
1365 row_stream->addRtpPacket(pinfo, rtpinfo);
1366 break;
1370 // qDebug() << "=ap no match!" << address_to_qstring(&pinfo->src) << address_to_qstring(&pinfo->dst);
1373 void RtpPlayerDialog::zoomXAxis(bool in)
1375 QCustomPlot *ap = ui->audioPlot;
1376 double h_factor = ap->axisRect()->rangeZoomFactor(Qt::Horizontal);
1378 if (!in) {
1379 h_factor = pow(h_factor, -1);
1382 ap->xAxis->scaleRange(h_factor, ap->xAxis->range().center());
1383 ap->replot();
1386 // XXX I tried using seconds but pixels make more sense at varying zoom
1387 // levels.
1388 void RtpPlayerDialog::panXAxis(int x_pixels)
1390 QCustomPlot *ap = ui->audioPlot;
1391 double h_pan;
1393 h_pan = ap->xAxis->range().size() * x_pixels / ap->xAxis->axisRect()->width();
1394 if (x_pixels) {
1395 ap->xAxis->moveRange(h_pan);
1396 ap->replot();
1400 void RtpPlayerDialog::on_playButton_clicked()
1402 double start_time;
1403 QList<RtpAudioStream *> streams_to_start;
1405 ui->hintLabel->setText("<i><small>" + tr("Preparing to play...") + "</i></small>");
1406 mainApp->processEvents();
1407 ui->pauseButton->setChecked(false);
1409 // Protect start time against move of marker during the play
1410 start_marker_time_play_ = start_marker_time_;
1411 silence_skipped_time_ = 0.0;
1412 cur_play_pos_->point1->setCoords(start_marker_time_play_, 0.0);
1413 cur_play_pos_->point2->setCoords(start_marker_time_play_, 1.0);
1414 cur_play_pos_->setVisible(true);
1415 playback_error_.clear();
1417 if (ui->todCheckBox->isChecked()) {
1418 start_time = start_marker_time_play_;
1419 } else {
1420 start_time = start_marker_time_play_ - first_stream_rel_start_time_;
1423 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1424 QAudioDevice cur_out_device = getCurrentDeviceInfo();
1425 #else
1426 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
1427 #endif
1428 playing_streams_.clear();
1429 int row_count = ui->streamTreeWidget->topLevelItemCount();
1430 for (int row = 0; row < row_count; row++) {
1431 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1432 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1433 // All streams starts at first_stream_rel_start_time_
1434 audio_stream->setStartPlayTime(start_time);
1435 if (audio_stream->prepareForPlay(cur_out_device)) {
1436 playing_streams_ << audio_stream;
1440 // Prepare silent stream for progress marker
1441 if (!marker_stream_) {
1442 marker_stream_ = getSilenceAudioOutput();
1443 } else {
1444 marker_stream_->stop();
1447 // Start progress marker and then audio streams
1448 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1449 notify_timer_start_diff_ = -1;
1450 #endif
1451 marker_stream_->start(new AudioSilenceGenerator(marker_stream_));
1452 // It may happen that stream play is finished before all others are started
1453 // therefore we do not use playing_streams_ there, but separate temporarily
1454 // list. It avoids access element/remove element race condition.
1455 streams_to_start = playing_streams_;
1456 for( int i = 0; i<streams_to_start.count(); ++i ) {
1457 streams_to_start[i]->startPlaying();
1460 updateWidgets();
1463 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1464 QAudioDevice RtpPlayerDialog::getCurrentDeviceInfo()
1466 QAudioDevice cur_out_device = QMediaDevices::defaultAudioOutput();
1467 QString cur_out_name = currentOutputDeviceName();
1468 foreach (QAudioDevice out_device, QMediaDevices::audioOutputs()) {
1469 if (cur_out_name == out_device.description()) {
1470 cur_out_device = out_device;
1474 return cur_out_device;
1477 void RtpPlayerDialog::sinkStateChanged()
1479 if (marker_stream_->state() == QAudio::ActiveState) {
1480 notify_timer_.start();
1481 } else {
1482 notify_timer_.stop();
1485 #else
1486 QAudioDeviceInfo RtpPlayerDialog::getCurrentDeviceInfo()
1488 QAudioDeviceInfo cur_out_device = QAudioDeviceInfo::defaultOutputDevice();
1489 QString cur_out_name = currentOutputDeviceName();
1490 foreach (QAudioDeviceInfo out_device, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) {
1491 if (cur_out_name == out_device.deviceName()) {
1492 cur_out_device = out_device;
1496 return cur_out_device;
1498 #endif
1500 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1501 QAudioSink *RtpPlayerDialog::getSilenceAudioOutput()
1503 QAudioDevice cur_out_device = getCurrentDeviceInfo();
1505 QAudioFormat format;
1506 if (marker_stream_requested_out_rate_ > 0) {
1507 format.setSampleRate(marker_stream_requested_out_rate_);
1508 } else {
1509 format.setSampleRate(8000);
1511 // Must match rtp_media.h.
1512 format.setSampleFormat(QAudioFormat::Int16);
1513 format.setChannelCount(1);
1514 if (!cur_out_device.isFormatSupported(format)) {
1515 format = cur_out_device.preferredFormat();
1518 QAudioSink *sink = new QAudioSink(cur_out_device, format, this);
1519 connect(sink, &QAudioSink::stateChanged, this, &RtpPlayerDialog::sinkStateChanged);
1520 return sink;
1522 #else
1523 QAudioOutput *RtpPlayerDialog::getSilenceAudioOutput()
1525 QAudioOutput *o;
1526 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
1528 QAudioFormat format;
1529 if (marker_stream_requested_out_rate_ > 0) {
1530 format.setSampleRate(marker_stream_requested_out_rate_);
1531 } else {
1532 format.setSampleRate(8000);
1534 format.setSampleSize(SAMPLE_BYTES * 8); // bits
1535 format.setSampleType(QAudioFormat::SignedInt);
1536 format.setChannelCount(1);
1537 format.setCodec("audio/pcm");
1538 if (!cur_out_device.isFormatSupported(format)) {
1539 format = cur_out_device.nearestFormat(format);
1542 o = new QAudioOutput(cur_out_device, format, this);
1543 o->setNotifyInterval(100); // ~15 fps
1544 connect(o, &QAudioOutput::notify, this, &RtpPlayerDialog::outputNotify);
1546 return o;
1548 #endif
1550 void RtpPlayerDialog::outputNotify()
1552 double new_current_pos = 0.0;
1553 double current_pos = 0.0;
1554 qint64 usecs = marker_stream_->processedUSecs();
1556 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1557 // First notify can show end of buffer, not play point so we have
1558 // remember the shift
1559 if ( -1 == notify_timer_start_diff_ || 0 == notify_timer_start_diff_) {
1560 notify_timer_start_diff_ = usecs;
1562 usecs -= notify_timer_start_diff_;
1563 #endif
1564 double secs = usecs / 1000000.0;
1566 if (ui->skipSilenceButton->isChecked() && !playing_streams_.isEmpty()) {
1567 // We should check whether we can skip some silence
1568 // We must calculate in time domain as every stream can use different
1569 // play rate
1570 double min_silence = playing_streams_[0]->getEndOfSilenceTime();
1571 for( int i = 1; i<playing_streams_.count(); ++i ) {
1572 qint64 cur_silence = playing_streams_[i]->getEndOfSilenceTime();
1573 if (cur_silence < min_silence) {
1574 min_silence = cur_silence;
1578 if (min_silence > 0.0) {
1579 double silence_duration;
1581 // Calculate silence duration we can skip
1582 new_current_pos = first_stream_rel_start_time_ + min_silence;
1583 if (ui->todCheckBox->isChecked()) {
1584 current_pos = secs + start_marker_time_play_ + first_stream_rel_start_time_;
1585 } else {
1586 current_pos = secs + start_marker_time_play_;
1588 silence_duration = new_current_pos - current_pos;
1590 if (silence_duration >= ui->minSilenceSpinBox->value()) {
1591 // Skip silence gap and update cursor difference
1592 for( int i = 0; i<playing_streams_.count(); ++i ) {
1593 // Convert silence from time domain to samples
1594 qint64 skip_samples = playing_streams_[i]->convertTimeToSamples(min_silence);
1595 playing_streams_[i]->seekPlaying(skip_samples);
1597 silence_skipped_time_ = silence_duration;
1602 // Calculate new cursor position
1603 if (ui->todCheckBox->isChecked()) {
1604 secs += start_marker_time_play_;
1605 secs += silence_skipped_time_;
1606 } else {
1607 secs += start_marker_time_play_;
1608 secs -= first_stream_rel_start_time_;
1609 secs += silence_skipped_time_;
1611 setPlayPosition(secs);
1614 void RtpPlayerDialog::on_pauseButton_clicked()
1616 for( int i = 0; i<playing_streams_.count(); ++i ) {
1617 playing_streams_[i]->pausePlaying();
1619 if (ui->pauseButton->isChecked()) {
1620 marker_stream_->suspend();
1621 } else {
1622 marker_stream_->resume();
1624 updateWidgets();
1627 void RtpPlayerDialog::on_stopButton_clicked()
1629 // We need copy of list because items will be removed during stopPlaying()
1630 QList<RtpAudioStream *> ps=QList<RtpAudioStream *>(playing_streams_);
1631 for( int i = 0; i<ps.count(); ++i ) {
1632 ps[i]->stopPlaying();
1634 marker_stream_->stop();
1635 cur_play_pos_->setVisible(false);
1636 updateWidgets();
1639 void RtpPlayerDialog::on_actionReset_triggered()
1641 resetXAxis();
1644 void RtpPlayerDialog::on_actionZoomIn_triggered()
1646 zoomXAxis(true);
1649 void RtpPlayerDialog::on_actionZoomOut_triggered()
1651 zoomXAxis(false);
1654 void RtpPlayerDialog::on_actionMoveLeft10_triggered()
1656 panXAxis(-10);
1659 void RtpPlayerDialog::on_actionMoveRight10_triggered()
1661 panXAxis(10);
1664 void RtpPlayerDialog::on_actionMoveLeft1_triggered()
1666 panXAxis(-1);
1669 void RtpPlayerDialog::on_actionMoveRight1_triggered()
1671 panXAxis(1);
1674 void RtpPlayerDialog::on_actionGoToPacket_triggered()
1676 int packet_num = getHoveredPacket();
1677 if (packet_num > 0) emit goToPacket(packet_num);
1680 void RtpPlayerDialog::handleGoToSetupPacket(QTreeWidgetItem *ti)
1682 if (ti) {
1683 bool ok;
1685 int packet_num = ti->data(first_pkt_col_, Qt::UserRole).toInt(&ok);
1686 if (ok) {
1687 emit goToPacket(packet_num);
1692 void RtpPlayerDialog::on_actionGoToSetupPacketPlot_triggered()
1694 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1695 handleGoToSetupPacket(findItemByCoords(pos));
1698 void RtpPlayerDialog::on_actionGoToSetupPacketTree_triggered()
1700 handleGoToSetupPacket(last_ti_);
1703 // Make waveform graphs selectable and update the treewidget selection accordingly.
1704 void RtpPlayerDialog::on_streamTreeWidget_itemSelectionChanged()
1706 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
1707 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
1708 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1709 if (audio_graph) {
1710 audio_graph->setSelected(ti->isSelected());
1714 qsizetype selected = ui->streamTreeWidget->selectedItems().count();
1715 if (selected == 0) {
1716 analyze_btn_->setEnabled(false);
1717 prepare_btn_->setEnabled(false);
1718 export_btn_->setEnabled(false);
1719 } else if (selected == 1) {
1720 analyze_btn_->setEnabled(true);
1721 prepare_btn_->setEnabled(true);
1722 export_btn_->setEnabled(true);
1723 ui->actionSavePayload->setEnabled(true);
1724 } else {
1725 analyze_btn_->setEnabled(true);
1726 prepare_btn_->setEnabled(true);
1727 export_btn_->setEnabled(true);
1728 ui->actionSavePayload->setEnabled(false);
1731 if (!block_redraw_) {
1732 ui->audioPlot->replot();
1733 updateHintLabel();
1737 // Change channel audio routing if double clicked channel column
1738 void RtpPlayerDialog::on_streamTreeWidget_itemDoubleClicked(QTreeWidgetItem *item, const int column)
1740 if (column == channel_col_) {
1741 RtpAudioStream *audio_stream = item->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1742 if (!audio_stream)
1743 return;
1745 AudioRouting audio_routing = audio_stream->getAudioRouting();
1746 audio_routing = audio_routing.getNextChannel(stereo_available_);
1747 changeAudioRoutingOnItem(item, audio_routing);
1749 updateHintLabel();
1752 void RtpPlayerDialog::removeRow(QTreeWidgetItem *ti)
1754 if (last_ti_ && (last_ti_ == ti)) {
1755 highlightItem(last_ti_, false);
1756 last_ti_ = NULL;
1758 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1759 if (audio_stream) {
1760 stream_hash_.remove(audio_stream->getHash(), audio_stream);
1761 ti->setData(stream_data_col_, Qt::UserRole, QVariant());
1762 delete audio_stream;
1765 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1766 if (audio_graph) {
1767 ti->setData(graph_audio_data_col_, Qt::UserRole, QVariant());
1768 audio_graph->remove(ui->audioPlot);
1771 QCPGraph *graph;
1772 graph = ti->data(graph_sequence_data_col_, Qt::UserRole).value<QCPGraph*>();
1773 if (graph) {
1774 ti->setData(graph_sequence_data_col_, Qt::UserRole, QVariant());
1775 ui->audioPlot->removeGraph(graph);
1778 graph = ti->data(graph_jitter_data_col_, Qt::UserRole).value<QCPGraph*>();
1779 if (graph) {
1780 ti->setData(graph_jitter_data_col_, Qt::UserRole, QVariant());
1781 ui->audioPlot->removeGraph(graph);
1784 graph = ti->data(graph_timestamp_data_col_, Qt::UserRole).value<QCPGraph*>();
1785 if (graph) {
1786 ti->setData(graph_timestamp_data_col_, Qt::UserRole, QVariant());
1787 ui->audioPlot->removeGraph(graph);
1790 graph = ti->data(graph_silence_data_col_, Qt::UserRole).value<QCPGraph*>();
1791 if (graph) {
1792 ti->setData(graph_silence_data_col_, Qt::UserRole, QVariant());
1793 ui->audioPlot->removeGraph(graph);
1796 delete ti;
1799 void RtpPlayerDialog::on_actionRemoveStream_triggered()
1801 lockUI();
1802 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1804 block_redraw_ = true;
1805 for(int i = static_cast<int>(items.count()) - 1; i>=0; i-- ) {
1806 removeRow(items[i]);
1808 block_redraw_ = false;
1809 // TODO: Recalculate legend
1810 // - Graphs used for legend could be removed above and we must add new
1811 // - If no legend is required, it should be removed
1813 // Redraw existing waveforms and rescale Y axis
1814 updateGraphs();
1816 updateWidgets();
1817 unlockUI();
1820 // If called with channel_any, just muted flag should be changed
1821 void RtpPlayerDialog::changeAudioRoutingOnItem(QTreeWidgetItem *ti, AudioRouting new_audio_routing)
1823 if (!ti)
1824 return;
1826 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1827 if (!audio_stream)
1828 return;
1830 AudioRouting audio_routing = audio_stream->getAudioRouting();
1831 audio_routing.mergeAudioRouting(new_audio_routing);
1832 formatAudioRouting(ti, audio_routing);
1834 audio_stream->setAudioRouting(audio_routing);
1836 RtpAudioGraph *audio_graph = ti->data(graph_audio_data_col_, Qt::UserRole).value<RtpAudioGraph*>();
1837 if (audio_graph) {
1839 audio_graph->setSelected(ti->isSelected());
1840 audio_graph->setMuted(audio_routing.isMuted());
1841 if (!block_redraw_) {
1842 ui->audioPlot->replot();
1847 // Find current item and apply change on it
1848 void RtpPlayerDialog::changeAudioRouting(AudioRouting new_audio_routing)
1850 lockUI();
1851 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1853 block_redraw_ = true;
1854 for(int i = 0; i<items.count(); i++ ) {
1856 QTreeWidgetItem *ti = items[i];
1857 changeAudioRoutingOnItem(ti, new_audio_routing);
1859 block_redraw_ = false;
1860 ui->audioPlot->replot();
1861 updateHintLabel();
1862 unlockUI();
1865 // Invert mute/unmute on item
1866 void RtpPlayerDialog::invertAudioMutingOnItem(QTreeWidgetItem *ti)
1868 if (!ti)
1869 return;
1871 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1872 if (!audio_stream)
1873 return;
1875 AudioRouting audio_routing = audio_stream->getAudioRouting();
1876 // Invert muting
1877 if (audio_routing.isMuted()) {
1878 changeAudioRoutingOnItem(ti, AudioRouting(AUDIO_UNMUTED, channel_any));
1879 } else {
1880 changeAudioRoutingOnItem(ti, AudioRouting(AUDIO_MUTED, channel_any));
1884 void RtpPlayerDialog::on_actionAudioRoutingP_triggered()
1886 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_mono));
1889 void RtpPlayerDialog::on_actionAudioRoutingL_triggered()
1891 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_left));
1894 void RtpPlayerDialog::on_actionAudioRoutingLR_triggered()
1896 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_both));
1899 void RtpPlayerDialog::on_actionAudioRoutingR_triggered()
1901 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_stereo_right));
1904 void RtpPlayerDialog::on_actionAudioRoutingMute_triggered()
1906 changeAudioRouting(AudioRouting(AUDIO_MUTED, channel_any));
1909 void RtpPlayerDialog::on_actionAudioRoutingUnmute_triggered()
1911 changeAudioRouting(AudioRouting(AUDIO_UNMUTED, channel_any));
1914 void RtpPlayerDialog::on_actionAudioRoutingMuteInvert_triggered()
1916 lockUI();
1917 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
1919 block_redraw_ = true;
1920 for(int i = 0; i<items.count(); i++ ) {
1922 QTreeWidgetItem *ti = items[i];
1923 invertAudioMutingOnItem(ti);
1925 block_redraw_ = false;
1926 ui->audioPlot->replot();
1927 updateHintLabel();
1928 unlockUI();
1931 const QString RtpPlayerDialog::getFormatedTime(double f_time)
1933 QString time_str;
1935 if (ui->todCheckBox->isChecked()) {
1936 QDateTime date_time = QDateTime::fromMSecsSinceEpoch(f_time * 1000.0);
1937 time_str = date_time.toString("yyyy-MM-dd hh:mm:ss.zzz");
1938 } else {
1939 time_str = QString::number(f_time, 'f', 6);
1940 time_str += " s";
1943 return time_str;
1946 const QString RtpPlayerDialog::getFormatedHoveredTime()
1948 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1949 QTreeWidgetItem *ti = findItemByCoords(pos);
1950 if (!ti) return tr("Unknown");
1952 double ts = ui->audioPlot->xAxis->pixelToCoord(pos.x());
1954 return getFormatedTime(ts);
1957 int RtpPlayerDialog::getHoveredPacket()
1959 QPoint pos = ui->audioPlot->mapFromGlobal(QCursor::pos());
1960 QTreeWidgetItem *ti = findItemByCoords(pos);
1961 if (!ti) return 0;
1963 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
1965 double ts = ui->audioPlot->xAxis->pixelToCoord(pos.x());
1967 return audio_stream->nearestPacket(ts, !ui->todCheckBox->isChecked());
1970 // Used by RtpAudioStreams to initialize QAudioOutput. We could alternatively
1971 // pass the corresponding QAudioDeviceInfo directly.
1972 QString RtpPlayerDialog::currentOutputDeviceName()
1974 return ui->outputDeviceComboBox->currentText();
1977 void RtpPlayerDialog::fillAudioRateMenu()
1979 ui->outputAudioRate->blockSignals(true);
1980 ui->outputAudioRate->clear();
1981 ui->outputAudioRate->addItem(tr("Automatic"));
1982 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
1983 // XXX QAudioDevice doesn't provide supportedSampleRates(). Fake it with
1984 // what's available.
1985 QAudioDevice cur_out_device = getCurrentDeviceInfo();
1986 QSet<int>sample_rates;
1987 if (!cur_out_device.isNull()) {
1988 sample_rates.insert(cur_out_device.preferredFormat().sampleRate());
1989 // Add 8000 if supported
1990 if ((cur_out_device.minimumSampleRate() <= 8000) &&
1991 (8000 <= cur_out_device.maximumSampleRate())
1993 sample_rates.insert(8000);
1995 // Add 16000 if supported
1996 if ((cur_out_device.minimumSampleRate() <= 16000) &&
1997 (16000 <= cur_out_device.maximumSampleRate())
1999 sample_rates.insert(16000);
2001 // Add 44100 if supported
2002 if ((cur_out_device.minimumSampleRate() <= 44100) &&
2003 (44100 <= cur_out_device.maximumSampleRate())
2005 sample_rates.insert(44100);
2009 // Sort values
2010 QList<int> sorter = sample_rates.values();
2011 std::sort(sorter.begin(), sorter.end());
2013 // Insert rates to the list
2014 for (auto rate : sorter) {
2015 ui->outputAudioRate->addItem(QString::number(rate));
2017 #else
2018 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
2020 if (!cur_out_device.isNull()) {
2021 foreach (int rate, cur_out_device.supportedSampleRates()) {
2022 ui->outputAudioRate->addItem(QString::number(rate));
2025 #endif
2026 ui->outputAudioRate->blockSignals(false);
2029 void RtpPlayerDialog::cleanupMarkerStream()
2031 if (marker_stream_) {
2032 marker_stream_->stop();
2033 delete marker_stream_;
2034 marker_stream_ = NULL;
2038 void RtpPlayerDialog::on_outputDeviceComboBox_currentTextChanged(const QString &)
2040 lockUI();
2041 stereo_available_ = isStereoAvailable();
2042 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2043 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2044 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2045 if (!audio_stream)
2046 continue;
2048 changeAudioRoutingOnItem(ti, audio_stream->getAudioRouting().convert(stereo_available_));
2051 marker_stream_requested_out_rate_ = 0;
2052 cleanupMarkerStream();
2053 fillAudioRateMenu();
2054 rescanPackets();
2055 unlockUI();
2058 void RtpPlayerDialog::on_outputAudioRate_currentTextChanged(const QString & rate_string)
2060 lockUI();
2061 // Any unconvertable string is converted to 0 => used as Automatic rate
2062 unsigned selected_rate = rate_string.toInt();
2064 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2065 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2066 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2067 if (!audio_stream)
2068 continue;
2070 audio_stream->setRequestedPlayRate(selected_rate);
2072 marker_stream_requested_out_rate_ = selected_rate;
2073 cleanupMarkerStream();
2074 rescanPackets();
2075 unlockUI();
2078 void RtpPlayerDialog::on_jitterSpinBox_valueChanged(double)
2080 rescanPackets();
2083 void RtpPlayerDialog::on_timingComboBox_currentIndexChanged(int)
2085 rescanPackets();
2088 void RtpPlayerDialog::on_todCheckBox_toggled(bool)
2090 QCPAxis *x_axis = ui->audioPlot->xAxis;
2091 double move;
2093 // Create plot with new tod settings
2094 createPlot();
2096 // Move view to same place as was shown before the change
2097 if (ui->todCheckBox->isChecked()) {
2098 // rel -> abs
2099 // based on abs time of first sample
2100 setStartPlayMarker(first_stream_abs_start_time_ + start_marker_time_ - first_stream_rel_start_time_);
2101 move = first_stream_abs_start_time_ - first_stream_rel_start_time_;
2102 } else {
2103 // abs -> rel
2104 // based on 0s
2105 setStartPlayMarker(first_stream_rel_start_time_ + start_marker_time_);
2106 move = - first_stream_abs_start_time_ + first_stream_rel_start_time_;
2108 x_axis->moveRange(move);
2109 drawStartPlayMarker();
2110 ui->audioPlot->replot();
2113 void RtpPlayerDialog::on_buttonBox_helpRequested()
2115 mainApp->helpTopicAction(HELP_TELEPHONY_RTP_PLAYER_DIALOG);
2118 double RtpPlayerDialog::getStartPlayMarker()
2120 double start_pos;
2122 if (ui->todCheckBox->isChecked()) {
2123 start_pos = start_marker_time_ + first_stream_abs_start_time_;
2124 } else {
2125 start_pos = start_marker_time_;
2128 return start_pos;
2131 void RtpPlayerDialog::drawStartPlayMarker()
2133 double pos = getStartPlayMarker();
2135 start_marker_pos_->point1->setCoords(pos, 0.0);
2136 start_marker_pos_->point2->setCoords(pos, 1.0);
2138 updateHintLabel();
2141 void RtpPlayerDialog::setStartPlayMarker(double new_time)
2143 if (ui->todCheckBox->isChecked()) {
2144 new_time = qBound(first_stream_abs_start_time_, new_time, first_stream_abs_start_time_ + streams_length_);
2145 // start_play_time is relative, we must calculate it
2146 start_marker_time_ = new_time - first_stream_abs_start_time_;
2147 } else {
2148 new_time = qBound(first_stream_rel_start_time_, new_time, first_stream_rel_start_time_ + streams_length_);
2149 start_marker_time_ = new_time;
2153 void RtpPlayerDialog::updateStartStopTime(rtpstream_info_t *rtpstream, bool is_first)
2155 // Calculate start time of first last packet of last stream
2156 double stream_rel_start_time = nstime_to_sec(&rtpstream->start_rel_time);
2157 double stream_abs_start_time = nstime_to_sec(&rtpstream->start_abs_time);
2158 double stream_rel_stop_time = nstime_to_sec(&rtpstream->stop_rel_time);
2160 if (is_first) {
2161 // Take start/stop time for first stream
2162 first_stream_rel_start_time_ = stream_rel_start_time;
2163 first_stream_abs_start_time_ = stream_abs_start_time;
2164 first_stream_rel_stop_time_ = stream_rel_stop_time;
2165 } else {
2166 // Calculate min/max for start/stop time for other streams
2167 first_stream_rel_start_time_ = qMin(first_stream_rel_start_time_, stream_rel_start_time);
2168 first_stream_abs_start_time_ = qMin(first_stream_abs_start_time_, stream_abs_start_time);
2169 first_stream_rel_stop_time_ = qMax(first_stream_rel_stop_time_, stream_rel_stop_time);
2171 streams_length_ = first_stream_rel_stop_time_ - first_stream_rel_start_time_;
2174 void RtpPlayerDialog::formatAudioRouting(QTreeWidgetItem *ti, AudioRouting audio_routing)
2176 ti->setText(channel_col_, tr(audio_routing.formatAudioRoutingToString()));
2179 bool RtpPlayerDialog::isStereoAvailable()
2181 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
2182 QAudioDevice cur_out_device = getCurrentDeviceInfo();
2183 if (cur_out_device.maximumChannelCount() > 1) {
2184 return true;
2186 #else
2187 QAudioDeviceInfo cur_out_device = getCurrentDeviceInfo();
2188 foreach(int count, cur_out_device.supportedChannelCounts()) {
2189 if (count > 1) {
2190 return true;
2193 #endif
2195 return false;
2198 void RtpPlayerDialog::invertSelection()
2200 block_redraw_ = true;
2201 ui->streamTreeWidget->blockSignals(true);
2202 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2203 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2204 ti->setSelected(!ti->isSelected());
2206 ui->streamTreeWidget->blockSignals(false);
2207 block_redraw_ = false;
2208 ui->audioPlot->replot();
2209 updateHintLabel();
2212 void RtpPlayerDialog::on_actionSelectAll_triggered()
2214 ui->streamTreeWidget->selectAll();
2215 updateHintLabel();
2218 void RtpPlayerDialog::on_actionSelectInvert_triggered()
2220 invertSelection();
2221 updateHintLabel();
2224 void RtpPlayerDialog::on_actionSelectNone_triggered()
2226 ui->streamTreeWidget->clearSelection();
2227 updateHintLabel();
2230 void RtpPlayerDialog::on_actionPlay_triggered()
2232 if (ui->playButton->isEnabled()) {
2233 ui->playButton->animateClick();
2234 } else if (ui->pauseButton->isEnabled()) {
2235 ui->pauseButton->animateClick();
2239 void RtpPlayerDialog::on_actionStop_triggered()
2241 if (ui->stopButton->isEnabled()) {
2242 ui->stopButton->animateClick();
2246 qint64 RtpPlayerDialog::saveAudioHeaderAU(QFile *save_file, quint32 channels, unsigned audio_rate)
2248 uint8_t pd[4];
2249 int64_t nchars;
2251 /* https://pubs.opengroup.org/external/auformat.html */
2252 /* First we write the .au header. All values in the header are
2253 * 4-byte big-endian values, so we use pntoh32() to copy them
2254 * to a 4-byte buffer, in big-endian order, and then write out
2255 * the buffer. */
2257 /* the magic word 0x2e736e64 == .snd */
2258 phton32(pd, 0x2e736e64);
2259 nchars = save_file->write((const char *)pd, 4);
2260 if (nchars != 4) {
2261 return -1;
2264 /* header offset == 24 bytes */
2265 phton32(pd, 24);
2266 nchars = save_file->write((const char *)pd, 4);
2267 if (nchars != 4) {
2268 return -1;
2271 /* total length; it is permitted to set this to 0xffffffff */
2272 phton32(pd, 0xffffffff);
2273 nchars = save_file->write((const char *)pd, 4);
2274 if (nchars != 4) {
2275 return -1;
2278 /* encoding format == 16-bit linear PCM */
2279 phton32(pd, 3);
2280 nchars = save_file->write((const char *)pd, 4);
2281 if (nchars != 4) {
2282 return -1;
2285 /* sample rate [Hz] */
2286 phton32(pd, audio_rate);
2287 nchars = save_file->write((const char *)pd, 4);
2288 if (nchars != 4) {
2289 return -1;
2292 /* channels */
2293 phton32(pd, channels);
2294 nchars = save_file->write((const char *)pd, 4);
2295 if (nchars != 4) {
2296 return -1;
2299 return save_file->pos();
2302 qint64 RtpPlayerDialog::saveAudioHeaderWAV(QFile *save_file, quint32 channels, unsigned audio_rate, qint64 samples)
2304 uint8_t pd[4];
2305 int64_t nchars;
2306 int32_t subchunk2Size;
2307 int32_t data32;
2308 int16_t data16;
2310 subchunk2Size = sizeof(SAMPLE) * channels * (int32_t)samples;
2312 /* http://soundfile.sapp.org/doc/WaveFormat/ */
2314 /* RIFF header, ChunkID 0x52494646 == RIFF */
2315 phton32(pd, 0x52494646);
2316 nchars = save_file->write((const char *)pd, 4);
2317 if (nchars != 4) {
2318 return -1;
2321 /* RIFF header, ChunkSize */
2322 data32 = 36 + subchunk2Size;
2323 nchars = save_file->write((const char *)&data32, 4);
2324 if (nchars != 4) {
2325 return -1;
2328 /* RIFF header, Format 0x57415645 == WAVE */
2329 phton32(pd, 0x57415645);
2330 nchars = save_file->write((const char *)pd, 4);
2331 if (nchars != 4) {
2332 return -1;
2335 /* WAVE fmt header, Subchunk1ID 0x666d7420 == 'fmt ' */
2336 phton32(pd, 0x666d7420);
2337 nchars = save_file->write((const char *)pd, 4);
2338 if (nchars != 4) {
2339 return -1;
2342 /* WAVE fmt header, Subchunk1Size */
2343 data32 = 16;
2344 nchars = save_file->write((const char *)&data32, 4);
2345 if (nchars != 4) {
2346 return -1;
2349 /* WAVE fmt header, AudioFormat 1 == PCM */
2350 data16 = 1;
2351 nchars = save_file->write((const char *)&data16, 2);
2352 if (nchars != 2) {
2353 return -1;
2356 /* WAVE fmt header, NumChannels */
2357 data16 = channels;
2358 nchars = save_file->write((const char *)&data16, 2);
2359 if (nchars != 2) {
2360 return -1;
2363 /* WAVE fmt header, SampleRate */
2364 data32 = audio_rate;
2365 nchars = save_file->write((const char *)&data32, 4);
2366 if (nchars != 4) {
2367 return -1;
2370 /* WAVE fmt header, ByteRate */
2371 data32 = audio_rate * channels * sizeof(SAMPLE);
2372 nchars = save_file->write((const char *)&data32, 4);
2373 if (nchars != 4) {
2374 return -1;
2377 /* WAVE fmt header, BlockAlign */
2378 data16 = channels * (int16_t)sizeof(SAMPLE);
2379 nchars = save_file->write((const char *)&data16, 2);
2380 if (nchars != 2) {
2381 return -1;
2384 /* WAVE fmt header, BitsPerSample */
2385 data16 = (int16_t)sizeof(SAMPLE) * 8;
2386 nchars = save_file->write((const char *)&data16, 2);
2387 if (nchars != 2) {
2388 return -1;
2391 /* WAVE data header, Subchunk2ID 0x64617461 == 'data' */
2392 phton32(pd, 0x64617461);
2393 nchars = save_file->write((const char *)pd, 4);
2394 if (nchars != 4) {
2395 return -1;
2398 /* WAVE data header, Subchunk2Size */
2399 data32 = subchunk2Size;
2400 nchars = save_file->write((const char *)&data32, 4);
2401 if (nchars != 4) {
2402 return -1;
2405 /* Now we are ready for saving data */
2407 return save_file->pos();
2410 bool RtpPlayerDialog::writeAudioSilenceSamples(QFile *out_file, qint64 samples, int stream_count)
2412 uint8_t pd[2];
2414 phton16(pd, 0x0000);
2415 for(int s=0; s < stream_count; s++) {
2416 for(qint64 i=0; i < samples; i++) {
2417 if (sizeof(SAMPLE) != out_file->write((char *)&pd, sizeof(SAMPLE))) {
2418 return false;
2423 return true;
2426 bool RtpPlayerDialog::writeAudioStreamsSamples(QFile *out_file, QVector<RtpAudioStream *> streams, bool swap_bytes)
2428 SAMPLE sample;
2429 uint8_t pd[2];
2431 // Did we read something in last cycle?
2432 bool read = true;
2434 while (read) {
2435 read = false;
2436 // Loop over all streams, read one sample from each, write to output
2437 foreach(RtpAudioStream *audio_stream, streams) {
2438 if (sizeof(sample) == audio_stream->readSample(&sample)) {
2439 if (swap_bytes) {
2440 // same as phton16(), but more clear in compare
2441 // to else branch
2442 pd[0] = (uint8_t)(sample >> 8);
2443 pd[1] = (uint8_t)(sample >> 0);
2444 } else {
2445 // just copy
2446 pd[1] = (uint8_t)(sample >> 8);
2447 pd[0] = (uint8_t)(sample >> 0);
2449 read = true;
2450 } else {
2451 // for 0x0000 doesn't matter on order
2452 phton16(pd, 0x0000);
2454 if (sizeof(sample) != out_file->write((char *)&pd, sizeof(sample))) {
2455 return false;
2460 return true;
2463 save_audio_t RtpPlayerDialog::selectFileAudioFormatAndName(QString *file_path)
2465 QString ext_filter = "";
2466 QString ext_filter_wav = tr("WAV (*.wav)");
2467 QString ext_filter_au = tr("Sun Audio (*.au)");
2468 ext_filter.append(ext_filter_wav);
2469 ext_filter.append(";;");
2470 ext_filter.append(ext_filter_au);
2472 QString sel_filter;
2473 *file_path = WiresharkFileDialog::getSaveFileName(
2474 this, tr("Save audio"), mainApp->openDialogInitialDir().absoluteFilePath(""),
2475 ext_filter, &sel_filter);
2477 if (file_path->isEmpty()) return save_audio_none;
2479 save_audio_t save_format = save_audio_none;
2480 if (0 == QString::compare(sel_filter, ext_filter_au)) {
2481 save_format = save_audio_au;
2482 } else if (0 == QString::compare(sel_filter, ext_filter_wav)) {
2483 save_format = save_audio_wav;
2486 return save_format;
2489 save_payload_t RtpPlayerDialog::selectFilePayloadFormatAndName(QString *file_path)
2491 QString ext_filter = "";
2492 QString ext_filter_raw = tr("Raw (*.raw)");
2493 ext_filter.append(ext_filter_raw);
2495 QString sel_filter;
2496 *file_path = WiresharkFileDialog::getSaveFileName(
2497 this, tr("Save payload"), mainApp->openDialogInitialDir().absoluteFilePath(""),
2498 ext_filter, &sel_filter);
2500 if (file_path->isEmpty()) return save_payload_none;
2502 save_payload_t save_format = save_payload_none;
2503 if (0 == QString::compare(sel_filter, ext_filter_raw)) {
2504 save_format = save_payload_data;
2507 return save_format;
2510 QVector<rtpstream_id_t *>RtpPlayerDialog::getSelectedRtpStreamIDs()
2512 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
2513 QVector<rtpstream_id_t *> ids;
2515 if (items.count() > 0) {
2516 foreach(QTreeWidgetItem *ti, items) {
2517 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2518 if (audio_stream) {
2519 ids << audio_stream->getID();
2524 return ids;
2527 QVector<RtpAudioStream *>RtpPlayerDialog::getSelectedAudibleNonmutedAudioStreams()
2529 QList<QTreeWidgetItem *> items = ui->streamTreeWidget->selectedItems();
2530 QVector<RtpAudioStream *> streams;
2532 if (items.count() > 0) {
2533 foreach(QTreeWidgetItem *ti, items) {
2534 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2535 // Ignore muted streams and streams with no audio
2536 if (audio_stream &&
2537 !audio_stream->getAudioRouting().isMuted() &&
2538 (audio_stream->sampleRate()>0)
2540 streams << audio_stream;
2545 return streams;
2548 void RtpPlayerDialog::saveAudio(save_mode_t save_mode)
2550 qint64 minSilenceSamples;
2551 qint64 startSample;
2552 qint64 lead_silence_samples;
2553 qint64 maxSample;
2554 QString path;
2555 QVector<RtpAudioStream *>streams;
2557 streams = getSelectedAudibleNonmutedAudioStreams();
2558 if (streams.count() < 1) {
2559 QMessageBox::warning(this, tr("Warning"), tr("No stream selected or none of selected streams provide audio"));
2560 return;
2563 unsigned save_audio_rate = streams[0]->playRate();
2564 // Check whether all streams use same audio rate
2565 foreach(RtpAudioStream *audio_stream, streams) {
2566 if (save_audio_rate != audio_stream->playRate()) {
2567 QMessageBox::warning(this, tr("Error"), tr("All selected streams must use same play rate. Manual set of Output Audio Rate might help."));
2568 return;
2572 save_audio_t format = selectFileAudioFormatAndName(&path);
2573 if (format == save_audio_none) return;
2575 // Use start silence and length of first stream
2576 minSilenceSamples = streams[0]->getLeadSilenceSamples();
2577 maxSample = streams[0]->getTotalSamples();
2578 // Find shortest start silence and longest stream
2579 foreach(RtpAudioStream *audio_stream, streams) {
2580 if (minSilenceSamples > audio_stream->getLeadSilenceSamples()) {
2581 minSilenceSamples = audio_stream->getLeadSilenceSamples();
2583 if (maxSample < audio_stream->getTotalSamples()) {
2584 maxSample = audio_stream->getTotalSamples();
2588 switch (save_mode) {
2589 case save_mode_from_cursor:
2590 if (ui->todCheckBox->isChecked()) {
2591 startSample = start_marker_time_ * save_audio_rate;
2592 } else {
2593 startSample = (start_marker_time_ - first_stream_rel_start_time_) * save_audio_rate;
2595 lead_silence_samples = 0;
2596 break;
2597 case save_mode_sync_stream:
2598 // Skip start of first stream, no lead silence
2599 startSample = minSilenceSamples;
2600 lead_silence_samples = 0;
2601 break;
2602 case save_mode_sync_file:
2603 default:
2604 // Full first stream, lead silence
2605 startSample = 0;
2606 lead_silence_samples = first_stream_rel_start_time_ * save_audio_rate;
2607 break;
2610 QVector<RtpAudioStream *>temp = QVector<RtpAudioStream *>(streams);
2612 // Remove streams shorter than startSample and
2613 // seek to correct start for longer ones
2614 foreach(RtpAudioStream *audio_stream, temp) {
2615 if (startSample > audio_stream->getTotalSamples()) {
2616 streams.removeAll(audio_stream);
2617 } else {
2618 audio_stream->seekSample(startSample);
2622 if (streams.count() < 1) {
2623 QMessageBox::warning(this, tr("Warning"), tr("No streams are suitable for save"));
2624 return;
2627 QFile file(path);
2628 file.open(QIODevice::WriteOnly);
2630 if (!file.isOpen() || (file.error() != QFile::NoError)) {
2631 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2632 } else {
2633 switch (format) {
2634 case save_audio_au:
2635 if (-1 == saveAudioHeaderAU(&file, static_cast<quint32>(streams.count()), save_audio_rate)) {
2636 QMessageBox::warning(this, tr("Error"), tr("Can't write header of AU file"));
2637 return;
2639 if (lead_silence_samples > 0) {
2640 if (!writeAudioSilenceSamples(&file, lead_silence_samples, static_cast<int>(streams.count()))) {
2641 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2644 if (!writeAudioStreamsSamples(&file, streams, true)) {
2645 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2647 break;
2648 case save_audio_wav:
2649 if (-1 == saveAudioHeaderWAV(&file, static_cast<quint32>(streams.count()), save_audio_rate, (maxSample - startSample) + lead_silence_samples)) {
2650 QMessageBox::warning(this, tr("Error"), tr("Can't write header of WAV file"));
2651 return;
2653 if (lead_silence_samples > 0) {
2654 if (!writeAudioSilenceSamples(&file, lead_silence_samples, static_cast<int>(streams.count()))) {
2655 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2658 if (!writeAudioStreamsSamples(&file, streams, false)) {
2659 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2661 break;
2662 case save_audio_none:
2663 break;
2667 file.close();
2670 void RtpPlayerDialog::savePayload()
2672 QString path;
2673 QList<QTreeWidgetItem *> items;
2674 RtpAudioStream *audio_stream = NULL;
2676 items = ui->streamTreeWidget->selectedItems();
2677 foreach(QTreeWidgetItem *ti, items) {
2678 audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2679 if (audio_stream)
2680 break;
2682 if (items.count() != 1 || !audio_stream) {
2683 QMessageBox::warning(this, tr("Warning"), tr("Payload save works with just one audio stream."));
2684 return;
2687 save_payload_t format = selectFilePayloadFormatAndName(&path);
2688 if (format == save_payload_none) return;
2690 QFile file(path);
2691 file.open(QIODevice::WriteOnly);
2693 if (!file.isOpen() || (file.error() != QFile::NoError)) {
2694 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2695 } else if (!audio_stream->savePayload(&file)) {
2696 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
2699 file.close();
2702 void RtpPlayerDialog::on_actionSaveAudioFromCursor_triggered()
2704 saveAudio(save_mode_from_cursor);
2707 void RtpPlayerDialog::on_actionSaveAudioSyncStream_triggered()
2709 saveAudio(save_mode_sync_stream);
2712 void RtpPlayerDialog::on_actionSaveAudioSyncFile_triggered()
2714 saveAudio(save_mode_sync_file);
2717 void RtpPlayerDialog::on_actionSavePayload_triggered()
2719 savePayload();
2722 void RtpPlayerDialog::selectInaudible(bool select)
2724 block_redraw_ = true;
2725 ui->streamTreeWidget->blockSignals(true);
2726 for (int row = 0; row < ui->streamTreeWidget->topLevelItemCount(); row++) {
2727 QTreeWidgetItem *ti = ui->streamTreeWidget->topLevelItem(row);
2728 RtpAudioStream *audio_stream = ti->data(stream_data_col_, Qt::UserRole).value<RtpAudioStream*>();
2729 // Streams with no audio
2730 if (audio_stream && (audio_stream->sampleRate()==0)) {
2731 ti->setSelected(select);
2734 ui->streamTreeWidget->blockSignals(false);
2735 block_redraw_ = false;
2736 ui->audioPlot->replot();
2737 updateHintLabel();
2740 void RtpPlayerDialog::on_actionSelectInaudible_triggered()
2742 selectInaudible(true);
2745 void RtpPlayerDialog::on_actionDeselectInaudible_triggered()
2747 selectInaudible(false);
2750 void RtpPlayerDialog::on_actionPrepareFilter_triggered()
2752 QVector<rtpstream_id_t *> ids = getSelectedRtpStreamIDs();
2753 QString filter = make_filter_based_on_rtpstream_id(ids);
2754 if (filter.length() > 0) {
2755 emit updateFilter(filter);
2759 void RtpPlayerDialog::rtpAnalysisReplace()
2761 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2763 emit rtpAnalysisDialogReplaceRtpStreams(getSelectedRtpStreamIDs());
2766 void RtpPlayerDialog::rtpAnalysisAdd()
2768 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2770 emit rtpAnalysisDialogAddRtpStreams(getSelectedRtpStreamIDs());
2773 void RtpPlayerDialog::rtpAnalysisRemove()
2775 if (ui->streamTreeWidget->selectedItems().count() < 1) return;
2777 emit rtpAnalysisDialogRemoveRtpStreams(getSelectedRtpStreamIDs());
2780 void RtpPlayerDialog::on_actionReadCapture_triggered()
2782 #ifdef QT_MULTIMEDIA_LIB
2783 QTimer::singleShot(0, this, SLOT(retapPackets()));
2784 #endif
2787 // _U_ is used for case w have no LIBPCAP
2788 void RtpPlayerDialog::captureEvent(CaptureEvent e _U_)
2790 #ifdef HAVE_LIBPCAP
2791 bool new_read_capture_enabled = false;
2792 bool found = false;
2794 if ((e.captureContext() & CaptureEvent::Capture) &&
2795 (e.eventType() == CaptureEvent::Prepared)
2797 new_read_capture_enabled = true;
2798 found = true;
2799 } else if ((e.captureContext() & CaptureEvent::Capture) &&
2800 (e.eventType() == CaptureEvent::Finished)
2802 new_read_capture_enabled = false;
2803 found = true;
2806 if (found) {
2807 bool retap = false;
2808 if (read_capture_enabled_ && !new_read_capture_enabled) {
2809 // Capturing ended, automatically refresh data
2810 retap = true;
2812 read_capture_enabled_ = new_read_capture_enabled;
2813 updateWidgets();
2814 if (retap) {
2815 QTimer::singleShot(0, this, SLOT(retapPackets()));
2818 #endif
2821 #endif // QT_MULTIMEDIA_LIB