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
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"
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"
42 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
44 #include <QAudioDevice>
46 #include <QMediaDevices>
48 #include <QAudioDeviceInfo>
52 #include <QVBoxLayout>
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"
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.
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
111 RtpPlayerTreeWidgetItem(QTreeWidget
*tree
) :
112 QTreeWidgetItem(tree
)
116 bool operator< (const QTreeWidgetItem
&other
) const
118 // Handle numeric sorting
119 switch (treeWidget()->sortColumn()) {
123 case sample_rate_col_
:
124 return text(treeWidget()->sortColumn()).toInt() < other
.text(treeWidget()->sortColumn()).toInt();
126 return text(treeWidget()->sortColumn()).toInt() < other
.text(treeWidget()->sortColumn()).toInt();
131 v1
= data(first_pkt_col_
, Qt::UserRole
).toInt();
132 v2
= other
.data(first_pkt_col_
, Qt::UserRole
).toInt();
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)));
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)
171 , marker_stream_requested_out_rate_(0)
173 , listener_removed_(true)
174 , block_redraw_(false)
176 , read_capture_enabled_(capture_running
)
177 , silence_skipped_time_(0.0)
178 #endif // QT_MULTIMEDIA_LIB
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(¬ify_timer_
, &QTimer::timeout
, this, &RtpPlayerDialog::outputNotify
);
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);
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);
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);
319 stereo_available_
= isStereoAvailable();
322 ui
->outputDeviceComboBox
->blockSignals(false);
324 ui
->audioPlot
->setMouseTracking(true);
325 ui
->audioPlot
->setEnabled(true);
326 ui
->audioPlot
->setInteractions(
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
;
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
);
391 player_button
->setEnabled(false);
392 player_button
->setText(tr("No Audio"));
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
*>();
409 cleanupMarkerStream();
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
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
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
);
464 report_failure("RTP Player - tap registration failed: %s", error_string
->str
);
465 g_string_free(error_string
, TRUE
);
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;
483 void RtpPlayerDialog::rescanPackets(bool rescale_axes
)
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();
494 QAudioDeviceInfo cur_out_device
= getCurrentDeviceInfo();
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
;
512 case RtpAudioStream::Uninterrupted
:
513 timing_mode
= RtpAudioStream::Uninterrupted
;
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
);
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_
);
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());
578 audio_stream
->setMaxSampleValue(total_max_sample_value
);
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());
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)")
596 .arg(QString::number(audio_stream
->stopRelTime() - audio_stream
->startRelTime(), 'f', prefs
.gui_decimal_places1
));
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) {
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();
619 seq_graph
->setName(tr("Out of Sequence"));
620 legend_out_of_sequence
= true;
624 if (audio_stream
->jitterDropped() > 0) {
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();
635 seq_graph
->setName(tr("Jitter Drops"));
636 legend_jitter_dropped
= true;
640 if (audio_stream
->wrongTimestamps() > 0) {
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();
651 seq_graph
->setName(tr("Wrong Timestamps"));
652 legend_wrong_timestamps
= true;
656 if (audio_stream
->insertedSilences() > 0) {
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();
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
*>();
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
));
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
);
715 void RtpPlayerDialog::addSingleRtpStream(rtpstream_id_t
*id
)
719 AudioRouting audio_routing
= AudioRouting(AUDIO_UNMUTED
, channel_mono
);
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
)) {
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_
) {
759 audio_routing
.setChannel(channel_stereo_right
);
761 audio_routing
.setChannel(channel_stereo_left
);
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
);
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();
797 void RtpPlayerDialog::unlockUI()
799 if (--lock_ui_
== 0) {
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()) {
810 // Delete all existing rows
812 highlightItem(last_ti_
, false);
816 for (int row
= ui
->streamTreeWidget
->topLevelItemCount() - 1; row
>= 0; row
--) {
817 QTreeWidgetItem
*ti
= ui
->streamTreeWidget
->topLevelItem(row
);
821 // Add all new streams
822 for (int i
=0; i
< stream_ids
.size(); i
++) {
823 addSingleRtpStream(stream_ids
[i
]);
828 #ifdef QT_MULTIMEDIA_LIB
829 QTimer::singleShot(0, this, SLOT(retapPackets()));
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()) {
842 int tli_count
= ui
->streamTreeWidget
->topLevelItemCount();
845 for (int i
=0; i
< stream_ids
.size(); i
++) {
846 addSingleRtpStream(stream_ids
[i
]);
849 if (tli_count
== 0) {
854 #ifdef QT_MULTIMEDIA_LIB
855 QTimer::singleShot(0, this, SLOT(retapPackets()));
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()) {
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
])) {
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;
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()) {
915 case Qt::Key_Underscore
: // Shifted minus on U.S. keyboards
916 case Qt::Key_O
: // GTK+
918 on_actionZoomOut_triggered();
921 case Qt::Key_Equal
: // Unshifted plus on U.S. keyboards
922 case Qt::Key_I
: // GTK+
923 if (keyEvent
.modifiers() == Qt::ControlModifier
) {
925 on_actionSelectInvert_triggered();
929 on_actionZoomIn_triggered();
939 panXAxis(-1 * pan_secs
);
942 case Qt::Key_ParenRight
: // Shifted 0 on U.S. keyboards
943 on_actionReset_triggered();
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();
959 on_actionGoToPacket_triggered();
963 if (keyEvent
.modifiers() == Qt::ControlModifier
) {
965 on_actionSelectAll_triggered();
967 } else if (keyEvent
.modifiers() == (Qt::ShiftModifier
| Qt::ControlModifier
)) {
969 on_actionSelectNone_triggered();
974 if (keyEvent
.modifiers() == Qt::ShiftModifier
) {
975 on_actionAudioRoutingUnmute_triggered();
977 } else if (keyEvent
.modifiers() == Qt::ControlModifier
) {
978 on_actionAudioRoutingMuteInvert_triggered();
981 on_actionAudioRoutingMute_triggered();
985 on_actionRemoveStream_triggered();
988 if (keyEvent
.modifiers() == Qt::ControlModifier
) {
990 on_actionRemoveStream_triggered();
997 case Qt::Key_PageDown
:
1000 // Route keys to QTreeWidget
1001 ui
->streamTreeWidget
->setFocus();
1004 if (keyEvent
.modifiers() == Qt::NoModifier
) {
1005 on_actionPlay_triggered();
1010 on_actionStop_triggered();
1013 if (keyEvent
.modifiers() == Qt::ShiftModifier
) {
1015 on_actionDeselectInaudible_triggered();
1018 on_actionSelectInaudible_triggered();
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();
1043 enable_play
= false;
1044 ui
->skipSilenceButton
->setEnabled(false);
1045 ui
->minSilenceSpinBox
->setEnabled(false);
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;
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
);
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);
1093 ui
->audioPlot
->replot();
1096 void RtpPlayerDialog::handleItemHighlight(QTreeWidgetItem
*ti
, bool scroll
)
1099 if (ti
!= last_ti_
) {
1101 highlightItem(last_ti_
, false);
1103 highlightItem(ti
, true);
1106 ui
->streamTreeWidget
->scrollToItem(ti
, QAbstractItemView::EnsureVisible
);
1107 ui
->audioPlot
->replot();
1112 highlightItem(last_ti_
, false);
1113 ui
->audioPlot
->replot();
1119 void RtpPlayerDialog::highlightItem(QTreeWidgetItem
*ti
, bool highlight
)
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
*>();
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.
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
*)
1172 void RtpPlayerDialog::graphDoubleClicked(QMouseEvent
*event
)
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());
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
);
1204 return findItem(plottable
);
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
)) {
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();
1232 hint
+= tr("%1 streams").arg(row_count
);
1234 if (row_count
> 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())) {
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())
1260 if (!playback_error_
.isEmpty()) {
1261 hint
+= " <font color=\"red\">";
1262 hint
+= playback_error_
;
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());
1287 void RtpPlayerDialog::updateGraphs()
1289 QCustomPlot
*ap
= ui
->audioPlot
;
1291 // Create new plots, just existing ones
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());
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();
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_
;
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
;
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
);
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
);
1379 h_factor
= pow(h_factor
, -1);
1382 ap
->xAxis
->scaleRange(h_factor
, ap
->xAxis
->range().center());
1386 // XXX I tried using seconds but pixels make more sense at varying zoom
1388 void RtpPlayerDialog::panXAxis(int x_pixels
)
1390 QCustomPlot
*ap
= ui
->audioPlot
;
1393 h_pan
= ap
->xAxis
->range().size() * x_pixels
/ ap
->xAxis
->axisRect()->width();
1395 ap
->xAxis
->moveRange(h_pan
);
1400 void RtpPlayerDialog::on_playButton_clicked()
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_
;
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();
1426 QAudioDeviceInfo cur_out_device
= getCurrentDeviceInfo();
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();
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;
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();
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();
1482 notify_timer_
.stop();
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
;
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_
);
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
);
1523 QAudioOutput
*RtpPlayerDialog::getSilenceAudioOutput()
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_
);
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
);
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_
;
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
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_
;
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_
;
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();
1622 marker_stream_
->resume();
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);
1639 void RtpPlayerDialog::on_actionReset_triggered()
1644 void RtpPlayerDialog::on_actionZoomIn_triggered()
1649 void RtpPlayerDialog::on_actionZoomOut_triggered()
1654 void RtpPlayerDialog::on_actionMoveLeft10_triggered()
1659 void RtpPlayerDialog::on_actionMoveRight10_triggered()
1664 void RtpPlayerDialog::on_actionMoveLeft1_triggered()
1669 void RtpPlayerDialog::on_actionMoveRight1_triggered()
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
)
1685 int packet_num
= ti
->data(first_pkt_col_
, Qt::UserRole
).toInt(&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
*>();
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);
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();
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
*>();
1745 AudioRouting audio_routing
= audio_stream
->getAudioRouting();
1746 audio_routing
= audio_routing
.getNextChannel(stereo_available_
);
1747 changeAudioRoutingOnItem(item
, audio_routing
);
1752 void RtpPlayerDialog::removeRow(QTreeWidgetItem
*ti
)
1754 if (last_ti_
&& (last_ti_
== ti
)) {
1755 highlightItem(last_ti_
, false);
1758 RtpAudioStream
*audio_stream
= ti
->data(stream_data_col_
, Qt::UserRole
).value
<RtpAudioStream
*>();
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
*>();
1767 ti
->setData(graph_audio_data_col_
, Qt::UserRole
, QVariant());
1768 audio_graph
->remove(ui
->audioPlot
);
1772 graph
= ti
->data(graph_sequence_data_col_
, Qt::UserRole
).value
<QCPGraph
*>();
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
*>();
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
*>();
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
*>();
1792 ti
->setData(graph_silence_data_col_
, Qt::UserRole
, QVariant());
1793 ui
->audioPlot
->removeGraph(graph
);
1799 void RtpPlayerDialog::on_actionRemoveStream_triggered()
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
1820 // If called with channel_any, just muted flag should be changed
1821 void RtpPlayerDialog::changeAudioRoutingOnItem(QTreeWidgetItem
*ti
, AudioRouting new_audio_routing
)
1826 RtpAudioStream
*audio_stream
= ti
->data(stream_data_col_
, Qt::UserRole
).value
<RtpAudioStream
*>();
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
*>();
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
)
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();
1865 // Invert mute/unmute on item
1866 void RtpPlayerDialog::invertAudioMutingOnItem(QTreeWidgetItem
*ti
)
1871 RtpAudioStream
*audio_stream
= ti
->data(stream_data_col_
, Qt::UserRole
).value
<RtpAudioStream
*>();
1875 AudioRouting audio_routing
= audio_stream
->getAudioRouting();
1877 if (audio_routing
.isMuted()) {
1878 changeAudioRoutingOnItem(ti
, AudioRouting(AUDIO_UNMUTED
, channel_any
));
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()
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();
1931 const QString
RtpPlayerDialog::getFormatedTime(double f_time
)
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");
1939 time_str
= QString::number(f_time
, 'f', 6);
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
);
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);
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
));
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
));
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
&)
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
*>();
2048 changeAudioRoutingOnItem(ti
, audio_stream
->getAudioRouting().convert(stereo_available_
));
2051 marker_stream_requested_out_rate_
= 0;
2052 cleanupMarkerStream();
2053 fillAudioRateMenu();
2058 void RtpPlayerDialog::on_outputAudioRate_currentTextChanged(const QString
& rate_string
)
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
*>();
2070 audio_stream
->setRequestedPlayRate(selected_rate
);
2072 marker_stream_requested_out_rate_
= selected_rate
;
2073 cleanupMarkerStream();
2078 void RtpPlayerDialog::on_jitterSpinBox_valueChanged(double)
2083 void RtpPlayerDialog::on_timingComboBox_currentIndexChanged(int)
2088 void RtpPlayerDialog::on_todCheckBox_toggled(bool)
2090 QCPAxis
*x_axis
= ui
->audioPlot
->xAxis
;
2093 // Create plot with new tod settings
2096 // Move view to same place as was shown before the change
2097 if (ui
->todCheckBox
->isChecked()) {
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_
;
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()
2122 if (ui
->todCheckBox
->isChecked()) {
2123 start_pos
= start_marker_time_
+ first_stream_abs_start_time_
;
2125 start_pos
= start_marker_time_
;
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);
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_
;
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
);
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
;
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) {
2187 QAudioDeviceInfo cur_out_device
= getCurrentDeviceInfo();
2188 foreach(int count
, cur_out_device
.supportedChannelCounts()) {
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();
2212 void RtpPlayerDialog::on_actionSelectAll_triggered()
2214 ui
->streamTreeWidget
->selectAll();
2218 void RtpPlayerDialog::on_actionSelectInvert_triggered()
2224 void RtpPlayerDialog::on_actionSelectNone_triggered()
2226 ui
->streamTreeWidget
->clearSelection();
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
)
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
2257 /* the magic word 0x2e736e64 == .snd */
2258 phton32(pd
, 0x2e736e64);
2259 nchars
= save_file
->write((const char *)pd
, 4);
2264 /* header offset == 24 bytes */
2266 nchars
= save_file
->write((const char *)pd
, 4);
2271 /* total length; it is permitted to set this to 0xffffffff */
2272 phton32(pd
, 0xffffffff);
2273 nchars
= save_file
->write((const char *)pd
, 4);
2278 /* encoding format == 16-bit linear PCM */
2280 nchars
= save_file
->write((const char *)pd
, 4);
2285 /* sample rate [Hz] */
2286 phton32(pd
, audio_rate
);
2287 nchars
= save_file
->write((const char *)pd
, 4);
2293 phton32(pd
, channels
);
2294 nchars
= save_file
->write((const char *)pd
, 4);
2299 return save_file
->pos();
2302 qint64
RtpPlayerDialog::saveAudioHeaderWAV(QFile
*save_file
, quint32 channels
, unsigned audio_rate
, qint64 samples
)
2306 int32_t subchunk2Size
;
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);
2321 /* RIFF header, ChunkSize */
2322 data32
= 36 + subchunk2Size
;
2323 nchars
= save_file
->write((const char *)&data32
, 4);
2328 /* RIFF header, Format 0x57415645 == WAVE */
2329 phton32(pd
, 0x57415645);
2330 nchars
= save_file
->write((const char *)pd
, 4);
2335 /* WAVE fmt header, Subchunk1ID 0x666d7420 == 'fmt ' */
2336 phton32(pd
, 0x666d7420);
2337 nchars
= save_file
->write((const char *)pd
, 4);
2342 /* WAVE fmt header, Subchunk1Size */
2344 nchars
= save_file
->write((const char *)&data32
, 4);
2349 /* WAVE fmt header, AudioFormat 1 == PCM */
2351 nchars
= save_file
->write((const char *)&data16
, 2);
2356 /* WAVE fmt header, NumChannels */
2358 nchars
= save_file
->write((const char *)&data16
, 2);
2363 /* WAVE fmt header, SampleRate */
2364 data32
= audio_rate
;
2365 nchars
= save_file
->write((const char *)&data32
, 4);
2370 /* WAVE fmt header, ByteRate */
2371 data32
= audio_rate
* channels
* sizeof(SAMPLE
);
2372 nchars
= save_file
->write((const char *)&data32
, 4);
2377 /* WAVE fmt header, BlockAlign */
2378 data16
= channels
* (int16_t)sizeof(SAMPLE
);
2379 nchars
= save_file
->write((const char *)&data16
, 2);
2384 /* WAVE fmt header, BitsPerSample */
2385 data16
= (int16_t)sizeof(SAMPLE
) * 8;
2386 nchars
= save_file
->write((const char *)&data16
, 2);
2391 /* WAVE data header, Subchunk2ID 0x64617461 == 'data' */
2392 phton32(pd
, 0x64617461);
2393 nchars
= save_file
->write((const char *)pd
, 4);
2398 /* WAVE data header, Subchunk2Size */
2399 data32
= subchunk2Size
;
2400 nchars
= save_file
->write((const char *)&data32
, 4);
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
)
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
))) {
2426 bool RtpPlayerDialog::writeAudioStreamsSamples(QFile
*out_file
, QVector
<RtpAudioStream
*> streams
, bool swap_bytes
)
2431 // Did we read something in last cycle?
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
)) {
2440 // same as phton16(), but more clear in compare
2442 pd
[0] = (uint8_t)(sample
>> 8);
2443 pd
[1] = (uint8_t)(sample
>> 0);
2446 pd
[1] = (uint8_t)(sample
>> 8);
2447 pd
[0] = (uint8_t)(sample
>> 0);
2451 // for 0x0000 doesn't matter on order
2452 phton16(pd
, 0x0000);
2454 if (sizeof(sample
) != out_file
->write((char *)&pd
, sizeof(sample
))) {
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
);
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
;
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
);
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
;
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
*>();
2519 ids
<< audio_stream
->getID();
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
2537 !audio_stream
->getAudioRouting().isMuted() &&
2538 (audio_stream
->sampleRate()>0)
2540 streams
<< audio_stream
;
2548 void RtpPlayerDialog::saveAudio(save_mode_t save_mode
)
2550 qint64 minSilenceSamples
;
2552 qint64 lead_silence_samples
;
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"));
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."));
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
;
2593 startSample
= (start_marker_time_
- first_stream_rel_start_time_
) * save_audio_rate
;
2595 lead_silence_samples
= 0;
2597 case save_mode_sync_stream
:
2598 // Skip start of first stream, no lead silence
2599 startSample
= minSilenceSamples
;
2600 lead_silence_samples
= 0;
2602 case save_mode_sync_file
:
2604 // Full first stream, lead silence
2606 lead_silence_samples
= first_stream_rel_start_time_
* save_audio_rate
;
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
);
2618 audio_stream
->seekSample(startSample
);
2622 if (streams
.count() < 1) {
2623 QMessageBox::warning(this, tr("Warning"), tr("No streams are suitable for save"));
2628 file
.open(QIODevice::WriteOnly
);
2630 if (!file
.isOpen() || (file
.error() != QFile::NoError
)) {
2631 QMessageBox::warning(this, tr("Warning"), tr("Save failed!"));
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"));
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!"));
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"));
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!"));
2662 case save_audio_none
:
2670 void RtpPlayerDialog::savePayload()
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
*>();
2682 if (items
.count() != 1 || !audio_stream
) {
2683 QMessageBox::warning(this, tr("Warning"), tr("Payload save works with just one audio stream."));
2687 save_payload_t format
= selectFilePayloadFormatAndName(&path
);
2688 if (format
== save_payload_none
) return;
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!"));
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()
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();
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()));
2787 // _U_ is used for case w have no LIBPCAP
2788 void RtpPlayerDialog::captureEvent(CaptureEvent e _U_
)
2791 bool new_read_capture_enabled
= false;
2794 if ((e
.captureContext() & CaptureEvent::Capture
) &&
2795 (e
.eventType() == CaptureEvent::Prepared
)
2797 new_read_capture_enabled
= true;
2799 } else if ((e
.captureContext() & CaptureEvent::Capture
) &&
2800 (e
.eventType() == CaptureEvent::Finished
)
2802 new_read_capture_enabled
= false;
2808 if (read_capture_enabled_
&& !new_read_capture_enabled
) {
2809 // Capturing ended, automatically refresh data
2812 read_capture_enabled_
= new_read_capture_enabled
;
2815 QTimer::singleShot(0, this, SLOT(retapPackets()));
2821 #endif // QT_MULTIMEDIA_LIB