summaryrefslogtreecommitdiffstats
path: root/ui/qt/rtp_audio_stream.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'ui/qt/rtp_audio_stream.cpp')
-rw-r--r--ui/qt/rtp_audio_stream.cpp953
1 files changed, 953 insertions, 0 deletions
diff --git a/ui/qt/rtp_audio_stream.cpp b/ui/qt/rtp_audio_stream.cpp
new file mode 100644
index 00000000..5ceb809f
--- /dev/null
+++ b/ui/qt/rtp_audio_stream.cpp
@@ -0,0 +1,953 @@
+/* rtp_audio_frame.cpp
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * Copyright 1998 Gerald Combs
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "rtp_audio_stream.h"
+
+#ifdef QT_MULTIMEDIA_LIB
+
+#include <speex/speex_resampler.h>
+
+#include <epan/rtp_pt.h>
+#include <epan/to_str.h>
+
+#include <epan/dissectors/packet-rtp.h>
+
+#include <ui/rtp_media.h>
+#include <ui/rtp_stream.h>
+#include <ui/tap-rtp-common.h>
+
+#include <wsutil/nstime.h>
+
+#include <ui/qt/utils/rtp_audio_routing_filter.h>
+#include <ui/qt/utils/rtp_audio_file.h>
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+#include <QAudioDevice>
+#include <QAudioSink>
+#endif
+#include <QAudioFormat>
+#include <QAudioOutput>
+#include <QVariant>
+#include <QTimer>
+
+// To do:
+// - Only allow one rtpstream_info_t per RtpAudioStream?
+
+static const spx_int16_t visual_sample_rate_ = 1000;
+
+RtpAudioStream::RtpAudioStream(QObject *parent, rtpstream_id_t *id, bool stereo_required) :
+ QObject(parent)
+ , first_packet_(true)
+ , decoders_hash_(rtp_decoder_hash_table_new())
+ , global_start_rel_time_(0.0)
+ , start_abs_offset_(0.0)
+ , start_rel_time_(0.0)
+ , stop_rel_time_(0.0)
+ , stereo_required_(stereo_required)
+ , first_sample_rate_(0)
+ , audio_out_rate_(0)
+ , audio_requested_out_rate_(0)
+ , max_sample_val_(1)
+ , max_sample_val_used_(1)
+ , color_(0)
+ , jitter_buffer_size_(50)
+ , timing_mode_(RtpAudioStream::JitterBuffer)
+ , start_play_time_(0)
+ , audio_output_(NULL)
+{
+ rtpstream_id_copy(id, &id_);
+ memset(&rtpstream_, 0, sizeof(rtpstream_));
+ rtpstream_id_copy(&id_, &rtpstream_.id);
+
+ // Rates will be set later, we just init visual resampler
+ visual_resampler_ = speex_resampler_init(1, visual_sample_rate_,
+ visual_sample_rate_, SPEEX_RESAMPLER_QUALITY_MIN, NULL);
+
+ try {
+ // RtpAudioFile is ready for writing Frames
+ audio_file_ = new RtpAudioFile(prefs.gui_rtp_player_use_disk1, prefs.gui_rtp_player_use_disk2);
+ } catch (...) {
+ speex_resampler_destroy(visual_resampler_);
+ rtpstream_info_free_data(&rtpstream_);
+ rtpstream_id_free(&id_);
+ throw -1;
+ }
+
+ // RTP_STREAM_DEBUG("Writing to %s", tempname.toUtf8().constData());
+}
+
+RtpAudioStream::~RtpAudioStream()
+{
+ for (int i = 0; i < rtp_packets_.size(); i++) {
+ rtp_packet_t *rtp_packet = rtp_packets_[i];
+ g_free(rtp_packet->info);
+ g_free(rtp_packet->payload_data);
+ g_free(rtp_packet);
+ }
+ g_hash_table_destroy(decoders_hash_);
+ speex_resampler_destroy(visual_resampler_);
+ rtpstream_info_free_data(&rtpstream_);
+ rtpstream_id_free(&id_);
+ if (audio_file_) delete audio_file_;
+ // temp_file_ is released by audio_output_
+ if (audio_output_) delete audio_output_;
+}
+
+bool RtpAudioStream::isMatch(const rtpstream_id_t *id) const
+{
+ if (id
+ && rtpstream_id_equal(&id_, id, RTPSTREAM_ID_EQUAL_SSRC))
+ return true;
+ return false;
+}
+
+bool RtpAudioStream::isMatch(const _packet_info *pinfo, const _rtp_info *rtp_info) const
+{
+ if (pinfo && rtp_info
+ && rtpstream_id_equal_pinfo_rtp_info(&id_, pinfo, rtp_info))
+ return true;
+ return false;
+}
+
+void RtpAudioStream::addRtpPacket(const struct _packet_info *pinfo, const struct _rtp_info *rtp_info)
+{
+ if (!rtp_info) return;
+
+ if (first_packet_) {
+ rtpstream_info_analyse_init(&rtpstream_, pinfo, rtp_info);
+ first_packet_ = false;
+ }
+ rtpstream_info_analyse_process(&rtpstream_, pinfo, rtp_info);
+
+ rtp_packet_t *rtp_packet = g_new0(rtp_packet_t, 1);
+ rtp_packet->info = (struct _rtp_info *) g_memdup2(rtp_info, sizeof(struct _rtp_info));
+ if (rtp_info->info_all_data_present && (rtp_info->info_payload_len != 0)) {
+ rtp_packet->payload_data = (guint8 *) g_memdup2(&(rtp_info->info_data[rtp_info->info_payload_offset]),
+ rtp_info->info_payload_len);
+ }
+
+ if (rtp_packets_.size() < 1) { // First packet
+ start_abs_offset_ = nstime_to_sec(&pinfo->abs_ts) - start_rel_time_;
+ start_rel_time_ = stop_rel_time_ = nstime_to_sec(&pinfo->rel_ts);
+ }
+ rtp_packet->frame_num = pinfo->num;
+ rtp_packet->arrive_offset = nstime_to_sec(&pinfo->rel_ts) - start_rel_time_;
+
+ rtp_packets_ << rtp_packet;
+}
+
+void RtpAudioStream::clearPackets()
+{
+ for (int i = 0; i < rtp_packets_.size(); i++) {
+ rtp_packet_t *rtp_packet = rtp_packets_[i];
+ g_free(rtp_packet->info);
+ g_free(rtp_packet->payload_data);
+ g_free(rtp_packet);
+ }
+ rtp_packets_.clear();
+ rtpstream_info_free_data(&rtpstream_);
+ memset(&rtpstream_, 0, sizeof(rtpstream_));
+ rtpstream_id_copy(&id_, &rtpstream_.id);
+ first_packet_ = true;
+}
+
+void RtpAudioStream::reset(double global_start_time)
+{
+ global_start_rel_time_ = global_start_time;
+ stop_rel_time_ = start_rel_time_;
+ audio_out_rate_ = 0;
+ max_sample_val_ = 1;
+ packet_timestamps_.clear();
+ visual_samples_.clear();
+ out_of_seq_timestamps_.clear();
+ jitter_drop_timestamps_.clear();
+}
+
+AudioRouting RtpAudioStream::getAudioRouting()
+{
+ return audio_routing_;
+}
+
+void RtpAudioStream::setAudioRouting(AudioRouting audio_routing)
+{
+ audio_routing_ = audio_routing;
+}
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+void RtpAudioStream::decode(QAudioDevice out_device)
+#else
+void RtpAudioStream::decode(QAudioDeviceInfo out_device)
+#endif
+{
+ if (rtp_packets_.size() < 1) return;
+
+ audio_file_->setFrameWriteStage();
+ decodeAudio(out_device);
+
+ // Skip silence at begin of the stream
+ audio_file_->setFrameReadStage(prepend_samples_);
+
+ speex_resampler_reset_mem(visual_resampler_);
+ decodeVisual();
+ audio_file_->setDataReadStage();
+}
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+quint32 RtpAudioStream::calculateAudioOutRate(QAudioDevice out_device, unsigned int sample_rate, unsigned int requested_out_rate)
+#else
+quint32 RtpAudioStream::calculateAudioOutRate(QAudioDeviceInfo out_device, unsigned int sample_rate, unsigned int requested_out_rate)
+#endif
+{
+ quint32 out_rate;
+
+ // Use the first non-zero rate we find. Ajust it to match
+ // our audio hardware.
+ QAudioFormat format;
+ format.setSampleRate(sample_rate);
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ // Must match rtp_media.h.
+ format.setSampleFormat(QAudioFormat::Int16);
+#else
+ format.setSampleSize(SAMPLE_BYTES * 8); // bits
+ format.setSampleType(QAudioFormat::SignedInt);
+#endif
+ if (stereo_required_) {
+ format.setChannelCount(2);
+ } else {
+ format.setChannelCount(1);
+ }
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ format.setCodec("audio/pcm");
+#endif
+
+ if (!out_device.isNull() &&
+ !out_device.isFormatSupported(format) &&
+ (requested_out_rate == 0)
+ ) {
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ out_rate = out_device.preferredFormat().sampleRate();
+#else
+ out_rate = out_device.nearestFormat(format).sampleRate();
+#endif
+ } else {
+ if ((requested_out_rate != 0) &&
+ (requested_out_rate != sample_rate)
+ ) {
+ out_rate = requested_out_rate;
+ } else {
+ out_rate = sample_rate;
+ }
+ }
+
+ RTP_STREAM_DEBUG("Audio sample rate is %u", out_rate);
+
+ return out_rate;
+}
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+void RtpAudioStream::decodeAudio(QAudioDevice out_device)
+#else
+void RtpAudioStream::decodeAudio(QAudioDeviceInfo out_device)
+#endif
+{
+ // XXX This is more messy than it should be.
+
+ gint32 resample_buff_bytes = 0x1000;
+ SAMPLE *resample_buff = (SAMPLE *) g_malloc(resample_buff_bytes);
+ char *write_buff = NULL;
+ qint64 write_bytes = 0;
+ unsigned int channels = 0;
+ unsigned int sample_rate = 0;
+ guint32 last_sequence = 0;
+ guint32 last_sequence_w = 0; // Last sequence number we wrote data
+
+ double rtp_time_prev = 0.0;
+ double arrive_time_prev = 0.0;
+ double pack_period = 0.0;
+ double start_time = 0.0;
+ double start_rtp_time = 0.0;
+ guint64 start_timestamp = 0;
+
+ size_t decoded_bytes_prev = 0;
+ unsigned int audio_resampler_input_rate = 0;
+ struct SpeexResamplerState_ *audio_resampler = NULL;
+
+ for (int cur_packet = 0; cur_packet < rtp_packets_.size(); cur_packet++) {
+ SAMPLE *decode_buff = NULL;
+ // TODO: Update a progress bar here.
+ rtp_packet_t *rtp_packet = rtp_packets_[cur_packet];
+
+ stop_rel_time_ = start_rel_time_ + rtp_packet->arrive_offset;
+
+ QString payload_name;
+ if (rtp_packet->info->info_payload_type_str) {
+ payload_name = rtp_packet->info->info_payload_type_str;
+ } else {
+ payload_name = try_val_to_str_ext(rtp_packet->info->info_payload_type, &rtp_payload_type_short_vals_ext);
+ }
+ if (!payload_name.isEmpty()) {
+ payload_names_ << payload_name;
+ }
+
+ if (cur_packet < 1) { // First packet
+ start_timestamp = rtp_packet->info->info_extended_timestamp;
+ start_rtp_time = 0;
+ rtp_time_prev = 0;
+ last_sequence = rtp_packet->info->info_extended_seq_num - 1;
+ }
+
+ size_t decoded_bytes = decode_rtp_packet(rtp_packet, &decode_buff, decoders_hash_, &channels, &sample_rate);
+ // XXX: We don't actually *do* anything with channels, and just treat
+ // everything as if it were mono
+
+ unsigned rtp_clock_rate = sample_rate;
+ if (rtp_packet->info->info_payload_type == PT_G722) {
+ // G.722 sample rate is 16kHz, but RTP clock rate is 8kHz
+ // for historic reasons.
+ rtp_clock_rate = 8000;
+ }
+
+ // Length 2 for PT_PCM mean silence packet probably, ignore
+ if (decoded_bytes == 0 || sample_rate == 0 ||
+ ((rtp_packet->info->info_payload_type == PT_PCMU ||
+ rtp_packet->info->info_payload_type == PT_PCMA
+ ) && (decoded_bytes == 2)
+ )
+ ) {
+ // We didn't decode anything. Clean up and prep for
+ // the next packet.
+ last_sequence = rtp_packet->info->info_extended_seq_num;
+ g_free(decode_buff);
+ continue;
+ }
+
+ if (audio_out_rate_ == 0) {
+ first_sample_rate_ = sample_rate;
+
+ // We calculate audio_out_rate just for first sample_rate.
+ // All later are just resampled to it.
+ // Side effect: it creates and initiates resampler if needed
+ audio_out_rate_ = calculateAudioOutRate(out_device, sample_rate, audio_requested_out_rate_);
+
+ // Calculate count of prepend samples for the stream
+ // The earliest stream starts at 0.
+ // Note: Order of operations and separation to two formulas is
+ // important.
+ // When joined, calculated incorrectly - probably caused by
+ // conversions between int/double
+ prepend_samples_ = (start_rel_time_ - global_start_rel_time_) * sample_rate;
+ prepend_samples_ = prepend_samples_ * audio_out_rate_ / sample_rate;
+
+ // Prepend silence to match our sibling streams.
+ if ((prepend_samples_ > 0) && (audio_out_rate_ != 0)) {
+ audio_file_->frameWriteSilence(rtp_packet->frame_num, prepend_samples_);
+ }
+ }
+
+ if (rtp_packet->info->info_extended_seq_num != last_sequence+1) {
+ out_of_seq_timestamps_.append(stop_rel_time_);
+ }
+ last_sequence = rtp_packet->info->info_extended_seq_num;
+
+ double rtp_time = (double)(rtp_packet->info->info_extended_timestamp-start_timestamp)/rtp_clock_rate - start_rtp_time;
+ double arrive_time;
+ if (timing_mode_ == RtpTimestamp) {
+ arrive_time = rtp_time;
+ } else {
+ arrive_time = rtp_packet->arrive_offset - start_time;
+ }
+
+ double diff = qAbs(arrive_time - rtp_time);
+ if (diff*1000 > jitter_buffer_size_ && timing_mode_ != Uninterrupted) {
+ // rtp_player.c:628
+
+ jitter_drop_timestamps_.append(stop_rel_time_);
+ RTP_STREAM_DEBUG("Packet drop by jitter buffer exceeded %f > %d", diff*1000, jitter_buffer_size_);
+
+ /* if there was a silence period (more than two packetization
+ * period) resync the source */
+ if ((rtp_time - rtp_time_prev) > pack_period*2) {
+ qint64 silence_samples;
+ RTP_STREAM_DEBUG("Resync...");
+
+ silence_samples = (qint64)((arrive_time - arrive_time_prev)*sample_rate - decoded_bytes_prev / SAMPLE_BYTES);
+ silence_samples = silence_samples * audio_out_rate_ / sample_rate;
+ silence_timestamps_.append(stop_rel_time_);
+ // Timestamp shift can make silence calculation negative
+ if ((silence_samples > 0) && (audio_out_rate_ != 0)) {
+ audio_file_->frameWriteSilence(rtp_packet->frame_num, silence_samples);
+ }
+
+ decoded_bytes_prev = 0;
+ start_timestamp = rtp_packet->info->info_extended_timestamp;
+ start_rtp_time = 0;
+ start_time = rtp_packet->arrive_offset;
+ rtp_time_prev = 0;
+ }
+
+ } else {
+ // rtp_player.c:664
+ /* Add silence if it is necessary */
+ qint64 silence_samples;
+
+ if (timing_mode_ == Uninterrupted) {
+ silence_samples = 0;
+ } else {
+ silence_samples = (int)((rtp_time - rtp_time_prev)*sample_rate - decoded_bytes_prev / SAMPLE_BYTES);
+ silence_samples = silence_samples * audio_out_rate_ / sample_rate;
+ }
+
+ if (silence_samples != 0) {
+ wrong_timestamp_timestamps_.append(stop_rel_time_);
+ }
+
+ if (silence_samples > 0) {
+ silence_timestamps_.append(stop_rel_time_);
+ if ((silence_samples > 0) && (audio_out_rate_ != 0)) {
+ audio_file_->frameWriteSilence(rtp_packet->frame_num, silence_samples);
+ }
+ }
+
+ // XXX rtp_player.c:696 adds audio here.
+ rtp_time_prev = rtp_time;
+ pack_period = (double) decoded_bytes / SAMPLE_BYTES / sample_rate;
+ decoded_bytes_prev = decoded_bytes;
+ arrive_time_prev = arrive_time;
+ }
+
+ // Prepare samples to write
+ write_buff = (char *) decode_buff;
+ write_bytes = decoded_bytes;
+
+ if (audio_out_rate_ != sample_rate) {
+ // Resample the audio to match output rate.
+ // Buffer is in SAMPLEs
+ spx_uint32_t in_len = (spx_uint32_t) (write_bytes / SAMPLE_BYTES);
+ // Output is audio_out_rate_/sample_rate bigger than input
+ spx_uint32_t out_len = (spx_uint32_t) ((guint64)in_len * audio_out_rate_ / sample_rate);
+ resample_buff = resizeBufferIfNeeded(resample_buff, &resample_buff_bytes, out_len * SAMPLE_BYTES);
+
+ if (audio_resampler &&
+ sample_rate != audio_resampler_input_rate
+ ) {
+ // Clear old resampler because input rate changed
+ speex_resampler_destroy(audio_resampler);
+ audio_resampler_input_rate = 0;
+ audio_resampler = NULL;
+ }
+ if (!audio_resampler) {
+ audio_resampler_input_rate = sample_rate;
+ audio_resampler = speex_resampler_init(1, sample_rate, audio_out_rate_, 10, NULL);
+ RTP_STREAM_DEBUG("Started resampling from %u to (out) %u Hz.", sample_rate, audio_out_rate_);
+ }
+ speex_resampler_process_int(audio_resampler, 0, decode_buff, &in_len, resample_buff, &out_len);
+
+ write_buff = (char *) resample_buff;
+ write_bytes = out_len * SAMPLE_BYTES;
+ }
+
+ // We should write only newer data to avoid duplicates in replay
+ if (last_sequence_w < last_sequence) {
+ // Write the decoded, possibly-resampled audio to our temp file.
+ audio_file_->frameWriteSamples(rtp_packet->frame_num, write_buff, write_bytes);
+ last_sequence_w = last_sequence;
+ }
+
+ g_free(decode_buff);
+ }
+ g_free(resample_buff);
+
+ if (audio_resampler) speex_resampler_destroy(audio_resampler);
+}
+
+// We preallocate buffer, 320 samples is enough for most scenarios
+#define VISUAL_BUFF_LEN (320)
+#define VISUAL_BUFF_BYTES (SAMPLE_BYTES * VISUAL_BUFF_LEN)
+void RtpAudioStream::decodeVisual()
+{
+ spx_uint32_t read_len = 0;
+ gint32 read_buff_bytes = VISUAL_BUFF_BYTES;
+ SAMPLE *read_buff = (SAMPLE *) g_malloc(read_buff_bytes);
+ gint32 resample_buff_bytes = VISUAL_BUFF_BYTES;
+ SAMPLE *resample_buff = (SAMPLE *) g_malloc(resample_buff_bytes);
+ unsigned int sample_no = 0;
+ spx_uint32_t out_len;
+ guint32 frame_num;
+ rtp_frame_type type;
+
+ speex_resampler_set_rate(visual_resampler_, audio_out_rate_, visual_sample_rate_);
+
+ // Loop over every frame record
+ // readFrameSamples() maintains size of buffer for us
+ while (audio_file_->readFrameSamples(&read_buff_bytes, &read_buff, &read_len, &frame_num, &type)) {
+ out_len = (spx_uint32_t)(((guint64)read_len * visual_sample_rate_ ) / audio_out_rate_);
+
+ if (type == RTP_FRAME_AUDIO) {
+ // We resample only audio samples
+ resample_buff = resizeBufferIfNeeded(resample_buff, &resample_buff_bytes, out_len * SAMPLE_BYTES);
+
+ // Resample
+ speex_resampler_process_int(visual_resampler_, 0, read_buff, &read_len, resample_buff, &out_len);
+
+ // Create timestamp and visual sample
+ for (unsigned i = 0; i < out_len; i++) {
+ double time = start_rel_time_ + (double) sample_no / visual_sample_rate_;
+ packet_timestamps_[time] = frame_num;
+ if (qAbs(resample_buff[i]) > max_sample_val_) max_sample_val_ = qAbs(resample_buff[i]);
+ visual_samples_.append(resample_buff[i]);
+ sample_no++;
+ }
+ } else {
+ // Insert end of line mark
+ double time = start_rel_time_ + (double) sample_no / visual_sample_rate_;
+ packet_timestamps_[time] = frame_num;
+ visual_samples_.append(SAMPLE_NaN);
+ sample_no += out_len;
+ }
+ }
+
+ max_sample_val_used_ = max_sample_val_;
+ g_free(resample_buff);
+ g_free(read_buff);
+}
+
+const QStringList RtpAudioStream::payloadNames() const
+{
+ QStringList payload_names = payload_names_.values();
+ payload_names.sort();
+ return payload_names;
+}
+
+const QVector<double> RtpAudioStream::visualTimestamps(bool relative)
+{
+ QVector<double> ts_keys = packet_timestamps_.keys().toVector();
+ if (relative) return ts_keys;
+
+ QVector<double> adj_timestamps;
+ for (int i = 0; i < ts_keys.size(); i++) {
+ adj_timestamps.append(ts_keys[i] + start_abs_offset_ - start_rel_time_);
+ }
+ return adj_timestamps;
+}
+
+// Scale the height of the waveform to global scale (max_sample_val_used_)
+// and adjust its Y offset so that they overlap slightly (stack_offset_).
+static const double stack_offset_ = G_MAXINT16 / 3;
+const QVector<double> RtpAudioStream::visualSamples(int y_offset)
+{
+ QVector<double> adj_samples;
+ double scaled_offset = y_offset * stack_offset_;
+ for (int i = 0; i < visual_samples_.size(); i++) {
+ if (SAMPLE_NaN != visual_samples_[i]) {
+ adj_samples.append(((double)visual_samples_[i] * G_MAXINT16 / max_sample_val_used_) + scaled_offset);
+ } else {
+ // Convert to break in graph line
+ adj_samples.append(qQNaN());
+ }
+ }
+ return adj_samples;
+}
+
+const QVector<double> RtpAudioStream::outOfSequenceTimestamps(bool relative)
+{
+ if (relative) return out_of_seq_timestamps_;
+
+ QVector<double> adj_timestamps;
+ for (int i = 0; i < out_of_seq_timestamps_.size(); i++) {
+ adj_timestamps.append(out_of_seq_timestamps_[i] + start_abs_offset_ - start_rel_time_);
+ }
+ return adj_timestamps;
+}
+
+const QVector<double> RtpAudioStream::outOfSequenceSamples(int y_offset)
+{
+ QVector<double> adj_samples;
+ double scaled_offset = y_offset * stack_offset_; // XXX Should be different for seq, jitter, wrong & silence
+ for (int i = 0; i < out_of_seq_timestamps_.size(); i++) {
+ adj_samples.append(scaled_offset);
+ }
+ return adj_samples;
+}
+
+const QVector<double> RtpAudioStream::jitterDroppedTimestamps(bool relative)
+{
+ if (relative) return jitter_drop_timestamps_;
+
+ QVector<double> adj_timestamps;
+ for (int i = 0; i < jitter_drop_timestamps_.size(); i++) {
+ adj_timestamps.append(jitter_drop_timestamps_[i] + start_abs_offset_ - start_rel_time_);
+ }
+ return adj_timestamps;
+}
+
+const QVector<double> RtpAudioStream::jitterDroppedSamples(int y_offset)
+{
+ QVector<double> adj_samples;
+ double scaled_offset = y_offset * stack_offset_; // XXX Should be different for seq, jitter, wrong & silence
+ for (int i = 0; i < jitter_drop_timestamps_.size(); i++) {
+ adj_samples.append(scaled_offset);
+ }
+ return adj_samples;
+}
+
+const QVector<double> RtpAudioStream::wrongTimestampTimestamps(bool relative)
+{
+ if (relative) return wrong_timestamp_timestamps_;
+
+ QVector<double> adj_timestamps;
+ for (int i = 0; i < wrong_timestamp_timestamps_.size(); i++) {
+ adj_timestamps.append(wrong_timestamp_timestamps_[i] + start_abs_offset_ - start_rel_time_);
+ }
+ return adj_timestamps;
+}
+
+const QVector<double> RtpAudioStream::wrongTimestampSamples(int y_offset)
+{
+ QVector<double> adj_samples;
+ double scaled_offset = y_offset * stack_offset_; // XXX Should be different for seq, jitter, wrong & silence
+ for (int i = 0; i < wrong_timestamp_timestamps_.size(); i++) {
+ adj_samples.append(scaled_offset);
+ }
+ return adj_samples;
+}
+
+const QVector<double> RtpAudioStream::insertedSilenceTimestamps(bool relative)
+{
+ if (relative) return silence_timestamps_;
+
+ QVector<double> adj_timestamps;
+ for (int i = 0; i < silence_timestamps_.size(); i++) {
+ adj_timestamps.append(silence_timestamps_[i] + start_abs_offset_ - start_rel_time_);
+ }
+ return adj_timestamps;
+}
+
+const QVector<double> RtpAudioStream::insertedSilenceSamples(int y_offset)
+{
+ QVector<double> adj_samples;
+ double scaled_offset = y_offset * stack_offset_; // XXX Should be different for seq, jitter, wrong & silence
+ for (int i = 0; i < silence_timestamps_.size(); i++) {
+ adj_samples.append(scaled_offset);
+ }
+ return adj_samples;
+}
+
+quint32 RtpAudioStream::nearestPacket(double timestamp, bool is_relative)
+{
+ if (packet_timestamps_.size() < 1) return 0;
+
+ if (!is_relative) timestamp -= start_abs_offset_;
+ QMap<double, quint32>::iterator it = packet_timestamps_.lowerBound(timestamp);
+ if (it == packet_timestamps_.end()) return 0;
+ return it.value();
+}
+
+QAudio::State RtpAudioStream::outputState() const
+{
+ if (!audio_output_) return QAudio::IdleState;
+ return audio_output_->state();
+}
+
+const QString RtpAudioStream::formatDescription(const QAudioFormat &format)
+{
+ QString fmt_descr = QString("%1 Hz, ").arg(format.sampleRate());
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ switch (format.sampleFormat()) {
+ case QAudioFormat::UInt8:
+ fmt_descr += "UInt8";
+ break;
+ case QAudioFormat::Int16:
+ fmt_descr += "Int16";
+ break;
+ case QAudioFormat::Int32:
+ fmt_descr += "Int32";
+ break;
+ case QAudioFormat::Float:
+ fmt_descr += "Float";
+ break;
+ default:
+ fmt_descr += "Unknown";
+ break;
+ }
+#else
+ switch (format.sampleType()) {
+ case QAudioFormat::SignedInt:
+ fmt_descr += "Int";
+ fmt_descr += QString::number(format.sampleSize());
+ fmt_descr += format.byteOrder() == QAudioFormat::BigEndian ? "BE" : "LE";
+ break;
+ case QAudioFormat::UnSignedInt:
+ fmt_descr += "UInt";
+ fmt_descr += QString::number(format.sampleSize());
+ fmt_descr += format.byteOrder() == QAudioFormat::BigEndian ? "BE" : "LE";
+ break;
+ case QAudioFormat::Float:
+ fmt_descr += "Float";
+ break;
+ default:
+ fmt_descr += "Unknown";
+ break;
+ }
+#endif
+
+ return fmt_descr;
+}
+
+QString RtpAudioStream::getIDAsQString()
+{
+ gchar *src_addr_str = address_to_display(NULL, &id_.src_addr);
+ gchar *dst_addr_str = address_to_display(NULL, &id_.dst_addr);
+ QString str = QString("%1:%2 - %3:%4 %5")
+ .arg(src_addr_str)
+ .arg(id_.src_port)
+ .arg(dst_addr_str)
+ .arg(id_.dst_port)
+ .arg(QString("0x%1").arg(id_.ssrc, 0, 16));
+ wmem_free(NULL, src_addr_str);
+ wmem_free(NULL, dst_addr_str);
+
+ return str;
+}
+
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+bool RtpAudioStream::prepareForPlay(QAudioDevice out_device)
+#else
+bool RtpAudioStream::prepareForPlay(QAudioDeviceInfo out_device)
+#endif
+{
+ qint64 start_pos;
+ qint64 size;
+
+ if (audio_routing_.isMuted())
+ return false;
+
+ if (audio_output_)
+ return false;
+
+ if (audio_out_rate_ == 0) {
+ /* It is observed, but is not an error
+ QString error = tr("RTP stream (%1) is empty or codec is unsupported.")
+ .arg(getIDAsQString());
+
+ emit playbackError(error);
+ */
+ return false;
+ }
+
+ QAudioFormat format;
+ format.setSampleRate(audio_out_rate_);
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ // Must match rtp_media.h.
+ format.setSampleFormat(QAudioFormat::Int16);
+#else
+ format.setSampleSize(SAMPLE_BYTES * 8); // bits
+ format.setSampleType(QAudioFormat::SignedInt);
+#endif
+ if (stereo_required_) {
+ format.setChannelCount(2);
+ } else {
+ format.setChannelCount(1);
+ }
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ format.setCodec("audio/pcm");
+#endif
+
+ // RTP_STREAM_DEBUG("playing %s %d samples @ %u Hz",
+ // sample_file_->fileName().toUtf8().constData(),
+ // (int) sample_file_->size(), audio_out_rate_);
+
+ if (!out_device.isFormatSupported(format)) {
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ QString playback_error = tr("%1 does not support PCM at %2. Preferred format is %3")
+ .arg(out_device.description(), formatDescription(format), formatDescription(out_device.preferredFormat()));
+#else
+ QString playback_error = tr("%1 does not support PCM at %2. Preferred format is %3")
+ .arg(out_device.deviceName())
+ .arg(formatDescription(format))
+ .arg(formatDescription(out_device.nearestFormat(format)));
+#endif
+ emit playbackError(playback_error);
+ }
+
+ start_pos = (qint64)(start_play_time_ * SAMPLE_BYTES * audio_out_rate_);
+ // Round to SAMPLE_BYTES boundary
+ start_pos = (start_pos / SAMPLE_BYTES) * SAMPLE_BYTES;
+ size = audio_file_->sampleFileSize();
+ if (stereo_required_) {
+ // There is 2x more samples for stereo
+ start_pos *= 2;
+ size *= 2;
+ }
+ if (start_pos < size) {
+ audio_file_->setDataReadStage();
+ temp_file_ = new AudioRoutingFilter(audio_file_, stereo_required_, audio_routing_);
+ temp_file_->seek(start_pos);
+ if (audio_output_) delete audio_output_;
+#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
+ audio_output_ = new QAudioSink(out_device, format, this);
+ connect(audio_output_, &QAudioSink::stateChanged, this, &RtpAudioStream::outputStateChanged);
+#else
+ audio_output_ = new QAudioOutput(out_device, format, this);
+ connect(audio_output_, &QAudioOutput::stateChanged, this, &RtpAudioStream::outputStateChanged);
+#endif
+ return true;
+ } else {
+ // Report stopped audio if start position is later than stream ends
+ outputStateChanged(QAudio::StoppedState);
+ return false;
+ }
+
+ return false;
+}
+
+void RtpAudioStream::startPlaying()
+{
+ // On Win32/Qt 6.x start() returns, but state() is QAudio::StoppedState even
+ // everything is OK
+ audio_output_->start(temp_file_);
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ // Bug is related to Qt 4.x and probably for 5.x, but not for 6.x
+ // QTBUG-6548 StoppedState is not always emitted on error, force a cleanup
+ // in case playback fails immediately.
+ if (audio_output_ && audio_output_->state() == QAudio::StoppedState) {
+ outputStateChanged(QAudio::StoppedState);
+ }
+#endif
+}
+
+void RtpAudioStream::pausePlaying()
+{
+ if (audio_routing_.isMuted())
+ return;
+
+ if (audio_output_) {
+ if (QAudio::ActiveState == audio_output_->state()) {
+ audio_output_->suspend();
+ } else if (QAudio::SuspendedState == audio_output_->state()) {
+ audio_output_->resume();
+ }
+ }
+}
+
+void RtpAudioStream::stopPlaying()
+{
+ if (audio_routing_.isMuted())
+ return;
+
+ if (audio_output_) {
+ if (audio_output_->state() == QAudio::StoppedState) {
+ // Looks like "delayed" QTBUG-6548
+ // It may happen that stream is stopped, but no signal emited
+ // Probably triggered by some issue in sound system which is not
+ // handled by Qt correctly
+ outputStateChanged(QAudio::StoppedState);
+ } else {
+ audio_output_->stop();
+ }
+ }
+}
+
+void RtpAudioStream::seekPlaying(qint64 samples _U_)
+{
+ if (audio_routing_.isMuted())
+ return;
+
+ if (audio_output_) {
+ audio_output_->suspend();
+ audio_file_->seekSample(samples);
+ audio_output_->resume();
+ }
+}
+
+void RtpAudioStream::outputStateChanged(QAudio::State new_state)
+{
+ if (!audio_output_) return;
+
+ // On some platforms including macOS and Windows, the stateChanged signal
+ // is emitted while a QMutexLocker is active. As a result we shouldn't
+ // delete audio_output_ here.
+ switch (new_state) {
+ case QAudio::StoppedState:
+ {
+ // RTP_STREAM_DEBUG("stopped %f", audio_output_->processedUSecs() / 100000.0);
+ // Detach from parent (RtpAudioStream) to prevent deleteLater
+ // from being run during destruction of this class.
+ QAudio::Error error = audio_output_->error();
+
+ audio_output_->setParent(0);
+ audio_output_->disconnect();
+ audio_output_->deleteLater();
+ audio_output_ = NULL;
+ emit finishedPlaying(this, error);
+ break;
+ }
+ case QAudio::IdleState:
+ // Workaround for Qt behaving on some platforms with some soundcards:
+ // When ->stop() is called from outputStateChanged(),
+ // internalQMutexLock is locked and application hangs.
+ // We can stop the stream later.
+ QTimer::singleShot(0, this, SLOT(delayedStopStream()));
+
+ break;
+ default:
+ break;
+ }
+}
+
+void RtpAudioStream::delayedStopStream()
+{
+ audio_output_->stop();
+}
+
+SAMPLE *RtpAudioStream::resizeBufferIfNeeded(SAMPLE *buff, gint32 *buff_bytes, qint64 requested_size)
+{
+ if (requested_size > *buff_bytes) {
+ while ((requested_size > *buff_bytes))
+ *buff_bytes *= 2;
+ buff = (SAMPLE *) g_realloc(buff, *buff_bytes);
+ }
+
+ return buff;
+}
+
+void RtpAudioStream::seekSample(qint64 samples)
+{
+ audio_file_->seekSample(samples);
+}
+
+qint64 RtpAudioStream::readSample(SAMPLE *sample)
+{
+ return audio_file_->readSample(sample);
+}
+
+bool RtpAudioStream::savePayload(QIODevice *file)
+{
+ for (int cur_packet = 0; cur_packet < rtp_packets_.size(); cur_packet++) {
+ // TODO: Update a progress bar here.
+ rtp_packet_t *rtp_packet = rtp_packets_[cur_packet];
+
+ if ((rtp_packet->info->info_payload_type != PT_CN) &&
+ (rtp_packet->info->info_payload_type != PT_CN_OLD)) {
+ // All other payloads
+ int64_t nchars;
+
+ if (rtp_packet->payload_data && (rtp_packet->info->info_payload_len > 0)) {
+ nchars = file->write((char *)rtp_packet->payload_data, rtp_packet->info->info_payload_len);
+ if (nchars != rtp_packet->info->info_payload_len) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+
+#endif // QT_MULTIMEDIA_LIB