summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile.am1
-rw-r--r--PICA_client.c46
-rw-r--r--PICA_client.h8
-rw-r--r--PICA_proto.h7
-rw-r--r--doc/proto-doc-latest.txt30
-rw-r--r--pica-client/audiodevice.cpp539
-rw-r--r--pica-client/audiodevice.h76
-rw-r--r--pica-client/audiovideocallcontroller.cpp121
-rw-r--r--pica-client/audiovideocallcontroller.h20
-rw-r--r--pica-client/skynet.cpp70
-rw-r--r--pica-client/skynet.h20
11 files changed, 888 insertions, 50 deletions
diff --git a/Makefile.am b/Makefile.am
index a603801..d322679 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -155,6 +155,7 @@ moc_dhparam.cpp \
moc_filetransfercontroller.cpp \
moc_nodesdialog.cpp \
moc_toneplayer.cpp \
+moc_audiodevice.cpp \
ui_mainwindow.h
moc_%.cpp: pica-client/%.h
diff --git a/PICA_client.c b/PICA_client.c
index 0737f94..bb5679a 100644
--- a/PICA_client.c
+++ b/PICA_client.c
@@ -1909,9 +1909,18 @@ static unsigned int procmsg_CALL_AUDIO_PACKET(unsigned char *buf, unsigned int n
return 0;
}
- uint16_t pkt_size = *(uint16_t*)(buf + 2);
+ uint16_t tail_size = *(uint16_t*)(buf + 2);
- callbacks.call_audio_packet_cb(chan->peer_id, pkt_size, buf + 4);
+ if (tail_size < PICA_PROTO_CALL_PACKET_HDRSIZE)
+ {
+ return 0;
+ }
+
+ uint16_t seq_num = *(uint16_t*)(buf + 4);
+ uint32_t timestamp = *(uint32_t*)(buf + 4 + PICA_PROTO_CALL_PACKET_SEQNUM_SIZE);
+ uint16_t pkt_size = tail_size - PICA_PROTO_CALL_PACKET_HDRSIZE;
+
+ callbacks.call_audio_packet_cb(chan->peer_id, seq_num, timestamp, pkt_size, buf + 4 + PICA_PROTO_CALL_PACKET_HDRSIZE);
return 1;
}
@@ -1938,9 +1947,18 @@ static unsigned int procmsg_CALL_VIDEO_PACKET(unsigned char *buf, unsigned int n
return 0;
}
- uint16_t pkt_size = *(uint16_t*)(buf + 2);
+ uint16_t tail_size = *(uint16_t*)(buf + 2);
+
+ if (tail_size < PICA_PROTO_CALL_PACKET_HDRSIZE)
+ {
+ return 0;
+ }
+
+ uint16_t seq_num = *(uint16_t*)(buf + 4);
+ uint32_t timestamp = *(uint32_t*)(buf + 4 + PICA_PROTO_CALL_PACKET_SEQNUM_SIZE);
+ uint16_t pkt_size = tail_size - PICA_PROTO_CALL_PACKET_HDRSIZE;
- callbacks.call_video_packet_cb(chan->peer_id, pkt_size, buf + 4);
+ callbacks.call_video_packet_cb(chan->peer_id, seq_num, timestamp, pkt_size, buf + 4 + PICA_PROTO_CALL_PACKET_HDRSIZE);
return 1;
}
@@ -3729,17 +3747,19 @@ int PICA_set_call_video_params(struct PICA_c2c *chn, const char *codec_name, uin
return PICA_OK;
}
-int PICA_send_audio_packet(struct PICA_c2c *chn, const char *buf, unsigned int len)
+int PICA_send_audio_packet(struct PICA_c2c *chn, uint16_t seq_num, uint32_t timestamp, const char *buf, unsigned int len)
{
struct PICA_proto_msg *mp;
if (chn->call_state != PICA_CALL_STATE_ACTIVE)
return PICA_ERRCALLNOTINPROGRESS;
- if ((mp = c2c_writebuf_push(chn, PICA_PROTO_CALL_AUDIO_PACKET, len + 4)))
+ if ((mp = c2c_writebuf_push(chn, PICA_PROTO_CALL_AUDIO_PACKET, len + 4 + PICA_PROTO_CALL_PACKET_HDRSIZE)))
{
- *((uint16_t*)mp->tail) = len;
- memcpy(mp->tail + 2, buf, len);
+ *((uint16_t*)mp->tail) = len + PICA_PROTO_CALL_PACKET_HDRSIZE;
+ *((uint16_t*)(mp->tail + 2)) = seq_num;
+ *((uint32_t*)(mp->tail + 2 + PICA_PROTO_CALL_PACKET_SEQNUM_SIZE)) = timestamp;
+ memcpy(mp->tail + 2 + PICA_PROTO_CALL_PACKET_HDRSIZE, buf, len);
}
else
@@ -3750,17 +3770,19 @@ int PICA_send_audio_packet(struct PICA_c2c *chn, const char *buf, unsigned int l
return PICA_OK;
}
-int PICA_send_video_packet(struct PICA_c2c *chn, const char *buf, unsigned int len)
+int PICA_send_video_packet(struct PICA_c2c *chn, uint16_t seq_num, uint32_t timestamp, const char *buf, unsigned int len)
{
struct PICA_proto_msg *mp;
if (chn->call_state != PICA_CALL_STATE_ACTIVE)
return PICA_ERRCALLNOTINPROGRESS;
- if ((mp = c2c_writebuf_push(chn, PICA_PROTO_CALL_VIDEO_PACKET, len + 4)))
+ if ((mp = c2c_writebuf_push(chn, PICA_PROTO_CALL_VIDEO_PACKET, len + 4 + PICA_PROTO_CALL_PACKET_HDRSIZE)))
{
- *((uint16_t*)mp->tail) = len;
- memcpy(mp->tail + 2, buf, len);
+ *((uint16_t*)mp->tail) = len + PICA_PROTO_CALL_PACKET_HDRSIZE;
+ *((uint16_t*)(mp->tail + 2)) = seq_num;
+ *((uint32_t*)(mp->tail + 2 + PICA_PROTO_CALL_PACKET_SEQNUM_SIZE)) = timestamp;
+ memcpy(mp->tail + 2 + PICA_PROTO_CALL_PACKET_HDRSIZE, buf, len);
}
else
diff --git a/PICA_client.h b/PICA_client.h
index 867f36b..90e16ef 100644
--- a/PICA_client.h
+++ b/PICA_client.h
@@ -424,9 +424,9 @@ struct PICA_client_callbacks
void (*call_video_params_cb)(const unsigned char *peer_id, const char *codec, uint16_t width, uint16_t height);
- void (*call_audio_packet_cb)(const unsigned char *peer_id, uint16_t size, const char *pkt_data);
+ void (*call_audio_packet_cb)(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data);
- void (*call_video_packet_cb)(const unsigned char *peer_id, uint16_t size, const char *pkt_data);
+ void (*call_video_packet_cb)(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data);
};
@@ -492,8 +492,8 @@ int PICA_reject_call(struct PICA_c2c *chn);
int PICA_hangup_call(struct PICA_c2c *chn);
int PICA_set_call_audio_params(struct PICA_c2c *chn, const char *codec_name, uint16_t sample_rate);
int PICA_set_call_video_params(struct PICA_c2c *chn, const char *codec_name, uint16_t width, uint16_t height);
-int PICA_send_audio_packet(struct PICA_c2c *chn, const char *buf, unsigned int len);
-int PICA_send_video_packet(struct PICA_c2c *chn, const char *buf, unsigned int len);
+int PICA_send_audio_packet(struct PICA_c2c *chn, uint16_t seq_num, uint32_t timestamp, const char *buf, unsigned int len);
+int PICA_send_video_packet(struct PICA_c2c *chn, uint16_t seq_num, uint32_t timestamp, const char *buf, unsigned int len);
void PICA_close_c2c(struct PICA_c2c *chn);
void PICA_close_c2n(struct PICA_c2n *cid);
diff --git a/PICA_proto.h b/PICA_proto.h
index b7a5e5d..78a5c7a 100644
--- a/PICA_proto.h
+++ b/PICA_proto.h
@@ -166,6 +166,13 @@
#define PICA_PROTO_CALL_CODECNAMESIZE 20
+// Sequence number (2 octets) + capture timestamp in milliseconds (4
+// octets), little-endian, prepended to the payload in the 0x76/0x77 packet
+// tail, ahead of the codec data.
+#define PICA_PROTO_CALL_PACKET_SEQNUM_SIZE 2
+#define PICA_PROTO_CALL_PACKET_TIMESTAMP_SIZE 4
+#define PICA_PROTO_CALL_PACKET_HDRSIZE (PICA_PROTO_CALL_PACKET_SEQNUM_SIZE + PICA_PROTO_CALL_PACKET_TIMESTAMP_SIZE)
+
struct PICA_proto_msg
{
unsigned char head[2];
diff --git a/doc/proto-doc-latest.txt b/doc/proto-doc-latest.txt
index 1a1cff3..750f348 100644
--- a/doc/proto-doc-latest.txt
+++ b/doc/proto-doc-latest.txt
@@ -360,11 +360,37 @@ Video stream parameters
====================================================
Voice (audio) packet
-[0x76 0x76][length 2 octets][data]
+[0x76 0x76][length 2 octets][sequence number, uint16_t, little-endian][timestamp, uint32_t, little-endian, milliseconds since the start of the call][data]
+
+length - covers everything following it: the 2-octet sequence number, the
+4-octet timestamp and the data, i.e. data length + 6.
+
+sequence number - incremented by one for every audio packet sent over this
+call, wraps around at 65536. Since c2c/directc2c connections are carried
+over TCP, packets already arrive in order and without loss; the sequence
+number's purpose is to let the receiving side detect encoder-side gaps (e.g.
+a dropped capture buffer) and, in the future, to support delivery over an
+unordered/lossy transport (e.g. UDP).
+
+timestamp - the capture timestamp of the packet's first sample, in
+milliseconds, relative to the moment the call became active (the 0x71
+message was processed). Used by the receiver for playout-buffer timing and
+audio/video synchronization.
====================================================
Video packet
-[0x77 0x77][length 2 octets][data]
+[0x77 0x77][length 2 octets][sequence number, uint16_t, little-endian][timestamp, uint32_t, little-endian, milliseconds since the start of the call][data]
+
+length - covers everything following it: the 2-octet sequence number, the
+4-octet timestamp and the data, i.e. data length + 6.
+
+sequence number - incremented by one for every video packet sent over this
+call, wraps around at 65536. Same purpose as in the 0x76 message: gap
+detection and future unordered/lossy transport support.
+
+timestamp - the capture timestamp of the encoded video frame, in
+milliseconds, relative to the moment the call became active. Used by the
+receiver for playout timing and audio/video synchronization.
====================================================
diff --git a/pica-client/audiodevice.cpp b/pica-client/audiodevice.cpp
index 17dd1b9..d2a2287 100644
--- a/pica-client/audiodevice.cpp
+++ b/pica-client/audiodevice.cpp
@@ -1,5 +1,5 @@
/*
- (c) Copyright 2012 - 2018 Anton Sviridenko
+ (c) Copyright 2012 - 2026 Anton Sviridenko
https://picapica.im
This program is free software: you can redistribute it and/or modify
@@ -16,30 +16,561 @@
*/
#include "audiodevice.h"
+#include <QDebug>
+#include <QMutexLocker>
+#include <cstring>
+
#ifdef Q_OS_LINUX
#include <alsa/asoundlib.h>
#endif
-AudioDevice::AudioDevice()
+// FFmpeg headers
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavdevice/avdevice.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/opt.h>
+#include <libavutil/channel_layout.h>
+#include <libavutil/samplefmt.h>
+#include <libavutil/audio_fifo.h>
+#include <libavutil/frame.h>
+#include <libavutil/mem.h>
+#include <libavutil/error.h>
+#include <libswresample/swresample.h>
+}
+
+// See toneplayer.cpp for the rationale - FFmpeg 5.1 (libavutil 57.24.100)
+// replaced the channels/channel_layout fields with the AVChannelLayout
+// ch_layout API; the legacy branch keeps us building on FFmpeg 4.x.
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 24, 100)
+# define AD_HAVE_CH_LAYOUT 1
+#else
+# define AD_HAVE_CH_LAYOUT 0
+#endif
+
+// Every call this codebase currently negotiates is mono - matches
+// TonePlayer's design and keeps the resampling/framing code below simple.
+static const int kChannels = 1;
+
+// Target bitrate for the Opus voice encoder. Not exposed as a setting yet;
+// a reasonable fixed default for a single speech channel.
+static const int64_t kOpusBitrate = 32000;
+
+static QString ff_errstr(int err)
+{
+ char buf[256] = {0};
+ av_strerror(err, buf, sizeof(buf));
+ return QString::fromLocal8Bit(buf);
+}
+
+static void applyMonoLayout(AVCodecContext *ctx)
+{
+#if AD_HAVE_CH_LAYOUT
+ av_channel_layout_default(&ctx->ch_layout, kChannels);
+#else
+ ctx->channels = kChannels;
+ ctx->channel_layout = av_get_default_channel_layout(kChannels);
+#endif
+}
+
+static void applyMonoLayout(AVFrame *frame)
+{
+#if AD_HAVE_CH_LAYOUT
+ av_channel_layout_default(&frame->ch_layout, kChannels);
+#else
+ frame->channels = kChannels;
+ frame->channel_layout = av_get_default_channel_layout(kChannels);
+#endif
+}
+
+static SwrContext *makeMonoResampler(AVSampleFormat inFmt, int inRate, AVSampleFormat outFmt, int outRate)
{
+ SwrContext *swr = nullptr;
+
+#if AD_HAVE_CH_LAYOUT
+ AVChannelLayout layout;
+ av_channel_layout_default(&layout, kChannels);
+ int ret = swr_alloc_set_opts2(&swr, &layout, outFmt, outRate, &layout, inFmt, inRate, 0, nullptr);
+ av_channel_layout_uninit(&layout);
+ if (ret < 0)
+ return nullptr;
+#else
+ swr = swr_alloc_set_opts(nullptr, AV_CH_LAYOUT_MONO, outFmt, outRate, AV_CH_LAYOUT_MONO, inFmt, inRate, 0, nullptr);
+ if (!swr)
+ return nullptr;
+#endif
+ if (swr_init(swr) < 0)
+ {
+ swr_free(&swr);
+ return nullptr;
+ }
+
+ return swr;
+}
+
+AudioDevice::AudioDevice(QObject *parent)
+ : QObject(parent), m_sampleRate(48000), m_abort(0)
+{
}
AudioDevice::~AudioDevice()
{
+ Close();
+}
+void AudioDevice::configureCapture(QString deviceName, QString codec, int sampleRate)
+{
+ m_deviceName = deviceName;
+ m_codec = codec;
+ m_sampleRate = sampleRate;
}
-void AudioDevice::Capture()
+void AudioDevice::configurePlayback(QString deviceName, QString codec, int sampleRate)
{
+ m_deviceName = deviceName;
+ m_codec = codec;
+ m_sampleRate = sampleRate;
}
-void AudioDevice::Play()
+void AudioDevice::enqueuePacket(QByteArray data)
{
+ QMutexLocker locker(&m_queueMutex);
+
+ while (m_playQueue.size() >= kMaxQueueDepth)
+ m_playQueue.dequeue();
+
+ m_playQueue.enqueue(data);
+ m_queueCond.wakeAll();
}
void AudioDevice::Close()
{
+ m_abort.storeRelaxed(1);
+ m_queueCond.wakeAll();
+}
+
+void AudioDevice::Capture()
+{
+ m_abort.storeRelaxed(0);
+
+ avdevice_register_all();
+#if LIBAVFORMAT_VERSION_MAJOR < 58
+ av_register_all();
+ avcodec_register_all();
+#endif
+
+ const QByteArray deviceUtf8 = m_deviceName.toUtf8();
+ const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8();
+ const AVInputFormat *ifmt = av_find_input_format(driverUtf8.constData());
+ if (!ifmt)
+ {
+ QString msg = QString("Could not find '%1' input driver").arg(AudioDevice::PlatformDriverName());
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ return;
+ }
+
+ AVDictionary *opts = nullptr;
+ av_dict_set(&opts, "sample_rate", QString::number(m_sampleRate).toUtf8().constData(), 0);
+ av_dict_set(&opts, "channels", QString::number(kChannels).toUtf8().constData(), 0);
+
+ AVFormatContext *ifmt_ctx = nullptr;
+ int ret = avformat_open_input(&ifmt_ctx, deviceUtf8.constData(), ifmt, &opts);
+ av_dict_free(&opts);
+ if (ret < 0)
+ {
+ QString msg = QString("Could not open capture device '%1': %2").arg(m_deviceName, ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ return;
+ }
+
+ if ((ret = avformat_find_stream_info(ifmt_ctx, nullptr)) < 0 || ifmt_ctx->nb_streams < 1)
+ {
+ QString msg = QString("Could not determine capture stream parameters: %1").arg(ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avformat_close_input(&ifmt_ctx);
+ return;
+ }
+
+ AVStream *in_st = ifmt_ctx->streams[0];
+ const AVCodec *pcm_dec = avcodec_find_decoder(in_st->codecpar->codec_id);
+ AVCodecContext *dec_ctx = pcm_dec ? avcodec_alloc_context3(pcm_dec) : nullptr;
+ if (!pcm_dec || !dec_ctx ||
+ avcodec_parameters_to_context(dec_ctx, in_st->codecpar) < 0 ||
+ avcodec_open2(dec_ctx, pcm_dec, nullptr) < 0)
+ {
+ QString msg = "Could not open capture format decoder";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ if (dec_ctx) avcodec_free_context(&dec_ctx);
+ avformat_close_input(&ifmt_ctx);
+ return;
+ }
+
+ const AVCodec *enc = avcodec_find_encoder_by_name("libopus");
+ if (!enc)
+ enc = avcodec_find_encoder(AV_CODEC_ID_OPUS);
+ AVCodecContext *enc_ctx = enc ? avcodec_alloc_context3(enc) : nullptr;
+ if (!enc || !enc_ctx)
+ {
+ QString msg = "Opus encoder not available";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&dec_ctx);
+ avformat_close_input(&ifmt_ctx);
+ return;
+ }
+
+ enc_ctx->sample_rate = m_sampleRate;
+ enc_ctx->sample_fmt = enc->sample_fmts ? enc->sample_fmts[0] : AV_SAMPLE_FMT_S16;
+ enc_ctx->bit_rate = kOpusBitrate;
+ enc_ctx->time_base = AVRational{1, m_sampleRate};
+ applyMonoLayout(enc_ctx);
+
+ if ((ret = avcodec_open2(enc_ctx, enc, nullptr)) < 0)
+ {
+ QString msg = QString("Could not open Opus encoder: %1").arg(ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&enc_ctx);
+ avcodec_free_context(&dec_ctx);
+ avformat_close_input(&ifmt_ctx);
+ return;
+ }
+
+ SwrContext *swr = makeMonoResampler(dec_ctx->sample_fmt, dec_ctx->sample_rate, enc_ctx->sample_fmt, enc_ctx->sample_rate);
+ if (!swr)
+ {
+ QString msg = "Could not set up capture resampler";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&enc_ctx);
+ avcodec_free_context(&dec_ctx);
+ avformat_close_input(&ifmt_ctx);
+ return;
+ }
+
+ AVAudioFifo *fifo = av_audio_fifo_alloc(enc_ctx->sample_fmt, kChannels, 1);
+ int frame_size = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 960;
+
+ AVPacket *in_pkt = av_packet_alloc();
+ AVFrame *dec_frame = av_frame_alloc();
+ AVPacket *out_pkt = av_packet_alloc();
+ int64_t pts = 0;
+ bool loggedDecodeError = false;
+ bool loggedEncodeError = false;
+
+ while (!m_abort.loadRelaxed())
+ {
+ ret = av_read_frame(ifmt_ctx, in_pkt);
+ if (ret < 0)
+ {
+ if (ret == AVERROR(EAGAIN))
+ continue;
+ if (!loggedDecodeError)
+ {
+ qWarning() << QString("Capture device read failed: %1").arg(ff_errstr(ret));
+ loggedDecodeError = true;
+ }
+ break;
+ }
+
+ ret = avcodec_send_packet(dec_ctx, in_pkt);
+ av_packet_unref(in_pkt);
+ if (ret < 0)
+ {
+ if (!loggedDecodeError)
+ {
+ qWarning() << QString("Capture format decode failed: %1").arg(ff_errstr(ret));
+ loggedDecodeError = true;
+ }
+ continue;
+ }
+
+ while (avcodec_receive_frame(dec_ctx, dec_frame) == 0)
+ {
+ uint8_t **converted = nullptr;
+ int out_samples = (int)av_rescale_rnd(swr_get_delay(swr, dec_frame->sample_rate) + dec_frame->nb_samples,
+ enc_ctx->sample_rate, dec_frame->sample_rate, AV_ROUND_UP);
+
+ if (out_samples > 0 &&
+ av_samples_alloc_array_and_samples(&converted, nullptr, kChannels, out_samples, enc_ctx->sample_fmt, 0) >= 0)
+ {
+ int converted_samples = swr_convert(swr, converted, out_samples,
+ (const uint8_t **)dec_frame->data, dec_frame->nb_samples);
+
+ if (converted_samples > 0 &&
+ av_audio_fifo_realloc(fifo, av_audio_fifo_size(fifo) + converted_samples) >= 0)
+ {
+ av_audio_fifo_write(fifo, (void **)converted, converted_samples);
+ }
+
+ av_freep(&converted[0]);
+ av_freep(&converted);
+ }
+ av_frame_unref(dec_frame);
+
+ while (av_audio_fifo_size(fifo) >= frame_size)
+ {
+ AVFrame *enc_frame = av_frame_alloc();
+ enc_frame->format = enc_ctx->sample_fmt;
+ enc_frame->sample_rate = enc_ctx->sample_rate;
+ enc_frame->nb_samples = frame_size;
+ applyMonoLayout(enc_frame);
+ av_frame_get_buffer(enc_frame, 0);
+
+ av_audio_fifo_read(fifo, (void **)enc_frame->data, frame_size);
+ enc_frame->pts = pts;
+ pts += frame_size;
+
+ if (avcodec_send_frame(enc_ctx, enc_frame) == 0)
+ {
+ while (avcodec_receive_packet(enc_ctx, out_pkt) == 0)
+ {
+ emit packetReady(QByteArray((const char *)out_pkt->data, out_pkt->size));
+ av_packet_unref(out_pkt);
+ }
+ }
+ else if (!loggedEncodeError)
+ {
+ qWarning() << "Opus encode failed";
+ loggedEncodeError = true;
+ }
+ av_frame_free(&enc_frame);
+ }
+ }
+ }
+
+ av_packet_free(&in_pkt);
+ av_packet_free(&out_pkt);
+ av_frame_free(&dec_frame);
+ av_audio_fifo_free(fifo);
+ swr_free(&swr);
+ avcodec_free_context(&enc_ctx);
+ avcodec_free_context(&dec_ctx);
+ avformat_close_input(&ifmt_ctx);
+}
+
+void AudioDevice::Play()
+{
+ m_abort.storeRelaxed(0);
+
+ avdevice_register_all();
+#if LIBAVFORMAT_VERSION_MAJOR < 58
+ av_register_all();
+ avcodec_register_all();
+#endif
+
+ AVFormatContext *oc = nullptr;
+ const QByteArray deviceUtf8 = m_deviceName.toUtf8();
+ const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8();
+
+ int ret = avformat_alloc_output_context2(&oc, nullptr, driverUtf8.constData(), deviceUtf8.constData());
+ if (ret < 0 || !oc)
+ {
+ QString msg = QString("Could not allocate '%1' output context: %2").arg(AudioDevice::PlatformDriverName(), ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ return;
+ }
+
+ const AVCodec *out_codec = avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);
+ AVCodecContext *out_ctx = out_codec ? avcodec_alloc_context3(out_codec) : nullptr;
+ if (!out_codec || !out_ctx)
+ {
+ QString msg = "PCM encoder not found";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avformat_free_context(oc);
+ return;
+ }
+
+ out_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
+ out_ctx->sample_rate = m_sampleRate;
+ out_ctx->time_base = AVRational{1, m_sampleRate};
+ applyMonoLayout(out_ctx);
+
+ if ((ret = avcodec_open2(out_ctx, out_codec, nullptr)) < 0)
+ {
+ QString msg = QString("Could not open playback codec: %1").arg(ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
+ return;
+ }
+
+ AVStream *st = avformat_new_stream(oc, nullptr);
+ if (!st || avcodec_parameters_from_context(st->codecpar, out_ctx) < 0)
+ {
+ QString msg = "Could not create playback stream";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
+ return;
+ }
+ st->time_base = out_ctx->time_base;
+
+ if (!(oc->oformat->flags & AVFMT_NOFILE))
+ {
+ if ((ret = avio_open(&oc->pb, deviceUtf8.constData(), AVIO_FLAG_WRITE)) < 0)
+ {
+ QString msg = QString("Could not open '%1' device '%2': %3").arg(AudioDevice::PlatformDriverName(), m_deviceName, ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
+ return;
+ }
+ }
+
+ if ((ret = avformat_write_header(oc, nullptr)) < 0)
+ {
+ QString msg = QString("Error writing playback header: %1").arg(ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
+ return;
+ }
+
+ // Only "opus" is understood by this slice - matches what
+ // AudioVideoCallController always negotiates via SendAudioParams().
+ const AVCodec *in_codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
+ AVCodecContext *dec_ctx = in_codec ? avcodec_alloc_context3(in_codec) : nullptr;
+ if (dec_ctx)
+ {
+ dec_ctx->sample_rate = m_sampleRate;
+ applyMonoLayout(dec_ctx);
+ }
+ if (m_codec != QLatin1String("opus") || !in_codec || !dec_ctx || avcodec_open2(dec_ctx, in_codec, nullptr) < 0)
+ {
+ QString msg = QString("Unsupported call audio codec: %1").arg(m_codec);
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ if (dec_ctx) avcodec_free_context(&dec_ctx);
+ if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
+ return;
+ }
+
+ SwrContext *swr = nullptr;
+ AVPacket *out_pkt = av_packet_alloc();
+ AVFrame *dec_frame = av_frame_alloc();
+ int64_t pts = 0;
+ bool loggedDecodeError = false;
+ bool loggedResamplerError = false;
+ bool loggedEncodeError = false;
+
+ while (true)
+ {
+ QByteArray pktData;
+ {
+ QMutexLocker locker(&m_queueMutex);
+ while (m_playQueue.isEmpty() && !m_abort.loadRelaxed())
+ m_queueCond.wait(&m_queueMutex, 100);
+
+ if (m_abort.loadRelaxed() && m_playQueue.isEmpty())
+ break;
+
+ if (m_playQueue.isEmpty())
+ continue;
+
+ pktData = m_playQueue.dequeue();
+ }
+
+ AVPacket *in_pkt = av_packet_alloc();
+ if (av_new_packet(in_pkt, pktData.size()) < 0)
+ {
+ av_packet_free(&in_pkt);
+ continue;
+ }
+ memcpy(in_pkt->data, pktData.constData(), pktData.size());
+
+ ret = avcodec_send_packet(dec_ctx, in_pkt);
+ av_packet_free(&in_pkt);
+ if (ret < 0)
+ {
+ if (!loggedDecodeError)
+ {
+ qWarning() << QString("Opus decode failed: %1").arg(ff_errstr(ret));
+ loggedDecodeError = true;
+ }
+ continue;
+ }
+
+ while (avcodec_receive_frame(dec_ctx, dec_frame) == 0)
+ {
+ if (!swr)
+ {
+ swr = makeMonoResampler((AVSampleFormat)dec_frame->format, dec_frame->sample_rate, out_ctx->sample_fmt, out_ctx->sample_rate);
+ if (!swr)
+ {
+ if (!loggedResamplerError)
+ {
+ qWarning() << "Could not set up playback resampler";
+ loggedResamplerError = true;
+ }
+ av_frame_unref(dec_frame);
+ continue;
+ }
+ }
+
+ int out_samples = (int)av_rescale_rnd(swr_get_delay(swr, dec_frame->sample_rate) + dec_frame->nb_samples,
+ out_ctx->sample_rate, dec_frame->sample_rate, AV_ROUND_UP);
+
+ AVFrame *out_frame = av_frame_alloc();
+ out_frame->format = out_ctx->sample_fmt;
+ out_frame->sample_rate = out_ctx->sample_rate;
+ out_frame->nb_samples = out_samples;
+ applyMonoLayout(out_frame);
+
+ if (out_samples > 0 && av_frame_get_buffer(out_frame, 0) == 0)
+ {
+ int converted_samples = swr_convert(swr, out_frame->data, out_samples,
+ (const uint8_t **)dec_frame->data, dec_frame->nb_samples);
+ if (converted_samples > 0)
+ {
+ out_frame->nb_samples = converted_samples;
+ out_frame->pts = pts;
+ pts += converted_samples;
+
+ if (avcodec_send_frame(out_ctx, out_frame) == 0)
+ {
+ while (avcodec_receive_packet(out_ctx, out_pkt) == 0)
+ {
+ out_pkt->stream_index = st->index;
+ av_interleaved_write_frame(oc, out_pkt);
+ av_packet_unref(out_pkt);
+ }
+ }
+ else if (!loggedEncodeError)
+ {
+ qWarning() << "Playback PCM encode failed";
+ loggedEncodeError = true;
+ }
+ }
+ }
+ av_frame_free(&out_frame);
+ av_frame_unref(dec_frame);
+ }
+ }
+
+ av_packet_free(&out_pkt);
+ av_frame_free(&dec_frame);
+ if (swr) swr_free(&swr);
+ avcodec_free_context(&dec_ctx);
+
+ av_write_trailer(oc);
+ if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb);
+ avcodec_free_context(&out_ctx);
+ avformat_free_context(oc);
}
QList<MediaDeviceInfo> AudioDevice::Enumerate(enum MediaDeviceStreamDirection dir)
diff --git a/pica-client/audiodevice.h b/pica-client/audiodevice.h
index 09edbdc..ef78f8e 100644
--- a/pica-client/audiodevice.h
+++ b/pica-client/audiodevice.h
@@ -1,5 +1,5 @@
/*
- (c) Copyright 2012 - 2018 Anton Sviridenko
+ (c) Copyright 2012 - 2026 Anton Sviridenko
https://picapica.im
This program is free software: you can redistribute it and/or modify
@@ -18,15 +18,64 @@
#define AUDIODEVICE_H
#include "mediadevice.h"
+#include <QObject>
+#include <QString>
+#include <QByteArray>
+#include <QQueue>
+#include <QMutex>
+#include <QWaitCondition>
+#include <QAtomicInt>
-class AudioDevice : public MediaDevice
+// A single AudioDevice instance is dedicated to one direction - either
+// capture+encode (via Capture(), fed from a local hardware input device) or
+// decode+playback (via Play(), fed from the network via enqueuePacket()).
+// Each instance is meant to live on its own QThread (see
+// AudioVideoCallController), since both Capture() and Play() block the
+// calling thread in a device I/O loop until Close() is called - the same
+// pattern TonePlayer already uses for tone playback.
+class AudioDevice : public QObject, public MediaDevice
{
+ Q_OBJECT
public:
- AudioDevice();
+ AudioDevice(QObject *parent = nullptr);
~AudioDevice();
- virtual void Capture();
- virtual void Play();
- virtual void Close();
+
+ // Configure before invoking Capture()/Play(). deviceName is a platform
+ // device identifier as accepted by the FFmpeg driver named by
+ // PlatformDriverName() (e.g. an ALSA PCM name), or "default".
+ Q_INVOKABLE void configureCapture(QString deviceName, QString codec, int sampleRate);
+ Q_INVOKABLE void configurePlayback(QString deviceName, QString codec, int sampleRate);
+
+ // Push a codec packet received from the network into the playback
+ // jitter buffer. Safe to call from any thread. Drops the oldest queued
+ // packet if the buffer is full, favoring low latency over completeness.
+ Q_INVOKABLE void enqueuePacket(QByteArray data);
+
+public slots:
+ // Blocking capture+encode loop: opens the configured local input
+ // device, encodes captured audio with the configured codec, and emits
+ // packetReady() for each encoded packet. Returns once Close() is
+ // called.
+ virtual void Capture() override;
+
+ // Blocking decode+playback loop: opens the configured local output
+ // device, decodes packets pulled from the internal queue (see
+ // enqueuePacket()) and writes the resulting audio to the device.
+ // Returns once Close() is called.
+ virtual void Play() override;
+
+ // Signals a running Capture()/Play() loop to stop and return. Safe to
+ // call from any thread, and safe to call even if nothing is running.
+ virtual void Close() override;
+
+signals:
+ // One encoded packet produced by Capture(), ready to be sent over the
+ // network.
+ void packetReady(QByteArray data);
+
+ void errorOccurred(QString message);
+
+public:
virtual QList<MediaDeviceInfo> Enumerate(enum MediaDeviceStreamDirection dir);
// Name of the FFmpeg libavdevice muxer/demuxer for the host platform's
@@ -34,6 +83,21 @@ public:
// "dsound" on Windows). Shared by every component that opens an audio
// device through FFmpeg (capture, playback, TonePlayer).
static QString PlatformDriverName();
+
+private:
+ QString m_deviceName;
+ QString m_codec;
+ int m_sampleRate;
+
+ QAtomicInt m_abort;
+
+ // Jitter buffer for the playback direction: Play() blocks pulling from
+ // here, enqueuePacket() (called from the controller on delivery of an
+ // incoming network packet) pushes into it.
+ QMutex m_queueMutex;
+ QWaitCondition m_queueCond;
+ QQueue<QByteArray> m_playQueue;
+ static const int kMaxQueueDepth = 4;
};
#endif // AUDIODEVICE_H
diff --git a/pica-client/audiovideocallcontroller.cpp b/pica-client/audiovideocallcontroller.cpp
index 2af7ced..1a4d8ae 100644
--- a/pica-client/audiovideocallcontroller.cpp
+++ b/pica-client/audiovideocallcontroller.cpp
@@ -23,14 +23,17 @@
#include <QMetaObject>
AudioVideoCallController::AudioVideoCallController(QObject *parent)
- : QObject(parent), callwindow(0), is_active(false)
+ : QObject(parent), callwindow(0), ringdevice(0), is_active(false), m_audioSeq(0)
{
connect(skynet, SIGNAL(IncomingCall(QByteArray)), this, SLOT(call_from(QByteArray)));
- connect(skynet, SIGNAL(CallFailed(QString)), this, SLOT(call_failed(QString)));
+ connect(skynet, SIGNAL(CallFailed(QByteArray,QString)), this, SLOT(call_failed(QByteArray,QString)));
connect(skynet, SIGNAL(CallAccepted(QByteArray)), this, SLOT(call_accepted(QByteArray)));
connect(skynet, SIGNAL(CallRejected(QByteArray)), this, SLOT(call_rejected(QByteArray)));
connect(skynet, SIGNAL(CallHungup(QByteArray)), this, SLOT(call_hungup(QByteArray)));
+ connect(skynet, SIGNAL(IncomingAudioParams(QByteArray,QString,quint16)), this, SLOT(incoming_audio_params(QByteArray,QString,quint16)));
+ connect(skynet, SIGNAL(IncomingAudioPacket(QByteArray,quint16,quint32,QByteArray)), this, SLOT(incoming_audio_packet(QByteArray,quint16,quint32,QByteArray)));
+
// Earpiece tones (ringback/busy/unreachable) and the incoming call bell
// are played by two TonePlayer instances, each living on its own
// QThread since TonePlayer::play() blocks the calling thread for the
@@ -42,11 +45,25 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent)
ringtoneplayer = new TonePlayer();
ringtoneplayer->moveToThread(&ringtoneplayer_thread);
ringtoneplayer_thread.start();
+
+ // Mic capture+encode and remote-audio decode+playback - same
+ // one-object-per-thread pattern as the tone players above, since
+ // AudioDevice::Capture()/Play() block their thread for the call's
+ // duration too.
+ microphone = new AudioDevice();
+ microphone->moveToThread(&microphone_thread);
+ connect(microphone, SIGNAL(packetReady(QByteArray)), this, SLOT(send_audio_packet(QByteArray)));
+ microphone_thread.start();
+
+ output = new AudioDevice();
+ output->moveToThread(&output_thread);
+ output_thread.start();
}
AudioVideoCallController::~AudioVideoCallController()
{
stopTones();
+ stopAudioPipeline();
toneplayer_thread.quit();
toneplayer_thread.wait();
@@ -55,6 +72,14 @@ AudioVideoCallController::~AudioVideoCallController()
ringtoneplayer_thread.quit();
ringtoneplayer_thread.wait();
delete ringtoneplayer;
+
+ microphone_thread.quit();
+ microphone_thread.wait();
+ delete microphone;
+
+ output_thread.quit();
+ output_thread.wait();
+ delete output;
}
void AudioVideoCallController::playEarpieceTone(void (TonePlayer::*tone)())
@@ -88,9 +113,41 @@ void AudioVideoCallController::stopTones()
ringtoneplayer->stop();
}
-void AudioVideoCallController::call_failed(QString reason)
+void AudioVideoCallController::startAudioPipeline()
+{
+ Settings st(config_dbname);
+ QString capDev = st.loadValue("audio.capture_device", "default").toString();
+
+ m_audioSeq = 0;
+ m_callClock.start();
+
+ // Declare our outgoing codec to the peer, then start capturing and
+ // encoding right away - the peer's decoder is only ready once it has
+ // processed this 0x74 message, but since the call is carried over TCP,
+ // packets sent immediately after are guaranteed to arrive after it.
+ skynet->SendAudioParams(m_peer_id, QStringLiteral("opus"), 48000);
+
+ QMetaObject::invokeMethod(microphone, "configureCapture", Qt::QueuedConnection,
+ Q_ARG(QString, capDev), Q_ARG(QString, QStringLiteral("opus")), Q_ARG(int, 48000));
+ QMetaObject::invokeMethod(microphone, "Capture", Qt::QueuedConnection);
+
+ // output is configured lazily, once the peer's own 0x74 (see
+ // incoming_audio_params()) tells us what codec/rate it is sending.
+}
+
+void AudioVideoCallController::stopAudioPipeline()
+{
+ QMetaObject::invokeMethod(microphone, "Close", Qt::QueuedConnection);
+ QMetaObject::invokeMethod(output, "Close", Qt::QueuedConnection);
+}
+
+void AudioVideoCallController::call_failed(QByteArray peer_id, QString reason)
{
qDebug() << "Call failed: " << reason << "\n";
+
+ if (peer_id != m_peer_id)
+ return;
+
is_active = false;
/* play unreachable */
playEarpieceTone(&TonePlayer::playUnreachable);
@@ -98,20 +155,39 @@ void AudioVideoCallController::call_failed(QString reason)
void AudioVideoCallController::call_accepted(QByteArray peer_id)
{
+ /* SkyNet lives in its own thread, so this signal is queued: by the time
+ it is delivered the local side may have already hung up and closed the
+ call window, or moved on to a different peer entirely. Either way the
+ call it refers to is gone - do not bring the audio pipeline up for it. */
+ if (peer_id != m_peer_id || !callwindow)
+ return;
+
stopTones();
is_active = true;
callwindow->call_started();
+ startAudioPipeline();
}
void AudioVideoCallController::call_rejected(QByteArray peer_id)
{
+ if (peer_id != m_peer_id)
+ return;
+
/* play busy tone */
playEarpieceTone(&TonePlayer::playBusy);
}
void AudioVideoCallController::call_hungup(QByteArray peer_id)
{
- callwindow->call_ended();
+ if (peer_id != m_peer_id)
+ return;
+
+ stopAudioPipeline();
+
+ /* queued from the SkyNet thread as well - the window may already be closed */
+ if (callwindow)
+ callwindow->call_ended();
+
/* play busy tone */
playEarpieceTone(&TonePlayer::playBusy);
is_active = false;
@@ -172,11 +248,13 @@ void AudioVideoCallController::accept_call()
skynet->AcceptCall(m_peer_id);
is_active = true;
callwindow->call_started();
+ startAudioPipeline();
}
void AudioVideoCallController::end_call()
{
stopTones();
+ stopAudioPipeline();
if (is_active)
skynet->HangupCall(m_peer_id);
@@ -193,3 +271,38 @@ void AudioVideoCallController::callwindow_closed(CallWindow *sender_window)
is_active = false;
}
+void AudioVideoCallController::send_audio_packet(QByteArray data)
+{
+ if (!is_active)
+ return;
+
+ quint16 seq = m_audioSeq++;
+ quint32 timestamp = (quint32)m_callClock.elapsed();
+
+ skynet->SendAudioPacket(m_peer_id, seq, timestamp, data);
+}
+
+void AudioVideoCallController::incoming_audio_params(QByteArray peer_id, QString codec, quint16 sample_rate)
+{
+ if (!is_active || peer_id != m_peer_id)
+ return;
+
+ Settings st(config_dbname);
+ QString playDev = st.loadValue("audio.playback_device", "default").toString();
+
+ QMetaObject::invokeMethod(output, "configurePlayback", Qt::QueuedConnection,
+ Q_ARG(QString, playDev), Q_ARG(QString, codec), Q_ARG(int, (int)sample_rate));
+ QMetaObject::invokeMethod(output, "Play", Qt::QueuedConnection);
+}
+
+void AudioVideoCallController::incoming_audio_packet(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data)
+{
+ Q_UNUSED(seq_num);
+ Q_UNUSED(timestamp);
+
+ if (!is_active || peer_id != m_peer_id)
+ return;
+
+ QMetaObject::invokeMethod(output, "enqueuePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
+}
+
diff --git a/pica-client/audiovideocallcontroller.h b/pica-client/audiovideocallcontroller.h
index 7a38f1b..8df6af1 100644
--- a/pica-client/audiovideocallcontroller.h
+++ b/pica-client/audiovideocallcontroller.h
@@ -19,6 +19,7 @@
#include <QObject>
#include <QThread>
+#include <QElapsedTimer>
#include "callwindow.h"
#include "audiodevice.h"
#include "videodevice.h"
@@ -36,7 +37,7 @@ public:
public slots:
void start_call(QByteArray peer_id);
void call_from(QByteArray peer_id);
- void call_failed(QString reason);
+ void call_failed(QByteArray peer_id, QString reason);
void call_accepted(QByteArray peer_id);
void call_rejected(QByteArray peer_id);
void call_hungup(QByteArray peer_id);
@@ -59,10 +60,23 @@ private:
TonePlayer *ringtoneplayer;
QThread ringtoneplayer_thread;
+ // Mic capture+encode and remote-audio decode+playback, each blocking
+ // its own thread for the duration of the call (see AudioDevice).
+ QThread microphone_thread;
+ QThread output_thread;
+
+ // Sequence number / capture-clock timestamp stamped onto outgoing
+ // 0x76 audio packets (see PICA_PROTO_CALL_PACKET_HDRSIZE).
+ quint16 m_audioSeq;
+ QElapsedTimer m_callClock;
+
void playEarpieceTone(void (TonePlayer::*tone)());
void playRingTone();
void stopTones();
+ void startAudioPipeline();
+ void stopAudioPipeline();
+
private slots:
// button handlers
void initiate_call();
@@ -70,6 +84,10 @@ private slots:
void end_call();
void callwindow_closed(CallWindow *sender_window);
+ void send_audio_packet(QByteArray data);
+ void incoming_audio_params(QByteArray peer_id, QString codec, quint16 sample_rate);
+ void incoming_audio_packet(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data);
+
};
#endif // AUDIOVIDEOCALLCONTROLLER_H
diff --git a/pica-client/skynet.cpp b/pica-client/skynet.cpp
index 85a55f6..1cd42bf 100644
--- a/pica-client/skynet.cpp
+++ b/pica-client/skynet.cpp
@@ -811,7 +811,7 @@ void SkyNet::do_AcceptCall(QByteArray from)
ret = PICA_pickup_call(iptr);
if (ret != PICA_OK)
- emit CallFailed(QString(tr("Failed to pickup the call: %1")).arg(ret));
+ emit CallFailed(from, QString(tr("Failed to pickup the call: %1")).arg(ret));
return;
}
@@ -834,7 +834,7 @@ void SkyNet::do_RejectCall(QByteArray from)
ret = PICA_reject_call(iptr);
if (ret != PICA_OK)
- emit CallFailed(QString(tr("Failed to reject the call: %1")).arg(ret));
+ emit CallFailed(from, QString(tr("Failed to reject the call: %1")).arg(ret));
return;
}
@@ -857,13 +857,45 @@ void SkyNet::do_HangupCall(QByteArray with)
ret = PICA_hangup_call(iptr);
if (ret != PICA_OK)
- emit CallFailed(QString(tr("Failed to hang up the call: %1")).arg(ret));
+ emit CallFailed(with, QString(tr("Failed to hang up the call: %1")).arg(ret));
return;
}
}
+void SkyNet::SendAudioParams(QByteArray to, QString codec, quint16 sample_rate)
+{
+ QMetaObject::invokeMethod(this, "do_SendAudioParams", Qt::AutoConnection,
+ Q_ARG(QByteArray, to), Q_ARG(QString, codec), Q_ARG(quint16, sample_rate));
+}
+
+void SkyNet::do_SendAudioParams(QByteArray to, QString codec, quint16 sample_rate)
+{
+ struct PICA_c2c *iptr;
+
+ if ((iptr = find_active_chan(to)))
+ {
+ PICA_set_call_audio_params(iptr, codec.toLatin1().constData(), sample_rate);
+ }
+}
+
+void SkyNet::SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data)
+{
+ QMetaObject::invokeMethod(this, "do_SendAudioPacket", Qt::AutoConnection,
+ Q_ARG(QByteArray, to), Q_ARG(quint16, seq_num), Q_ARG(quint32, timestamp), Q_ARG(QByteArray, data));
+}
+
+void SkyNet::do_SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data)
+{
+ struct PICA_c2c *iptr;
+
+ if ((iptr = find_active_chan(to)))
+ {
+ PICA_send_audio_packet(iptr, seq_num, timestamp, data.constData(), data.size());
+ }
+}
+
void SkyNet::StartCall(QByteArray to)
{
QMetaObject::invokeMethod(this, "do_StartCall", Qt::AutoConnection,
@@ -874,7 +906,7 @@ void SkyNet::do_StartCall(QByteArray to)
{
if (!self_aware)
{
- emit CallFailed(QString(tr("Pica Pica client is not online")));
+ emit CallFailed(to, QString(tr("Pica Pica client is not online")));
return;
}
@@ -886,7 +918,7 @@ void SkyNet::do_StartCall(QByteArray to)
ret = PICA_start_call(iptr);
if (ret != PICA_OK)
- emit CallFailed(QString(tr("Failed to initiate the call: %1")).arg(ret));
+ emit CallFailed(to, QString(tr("Failed to initiate the call: %1")).arg(ret));
return;
}
@@ -899,7 +931,7 @@ void SkyNet::do_StartCall(QByteArray to)
ret = PICA_new_c2c(active_nodelink, (const unsigned char*)to.constData(), NULL, &chan);
if (ret != PICA_OK)
- emit CallFailed(QString(tr("Failed to create the C2C connection for the call: %1")).arg(ret));
+ emit CallFailed(to, QString(tr("Failed to create the C2C connection for the call: %1")).arg(ret));
}
bool SkyNet::is_call_waiting_for(const QByteArray &peer_id)
@@ -922,7 +954,7 @@ void SkyNet::continue_start_call()
if (ret != PICA_OK)
{
- emit CallFailed(QString(tr("Failed to initiate the call: %1")).arg(ret));
+ emit CallFailed(call_waiting_to, QString(tr("Failed to initiate the call: %1")).arg(ret));
call_waiting_c2c_connect = false;
}
}
@@ -1120,9 +1152,9 @@ void SkyNet::emit_ConnectionStatusUpdated(QByteArray peer_id, QString status)
emit ConnectionStatusUpdated(peer_id, status);
}
-void SkyNet::emit_CallFailed(QString reason)
+void SkyNet::emit_CallFailed(QByteArray peer_id, QString reason)
{
- emit CallFailed(reason);
+ emit CallFailed(peer_id, reason);
}
void SkyNet::emit_IncomingCall(QByteArray from)
@@ -1145,6 +1177,16 @@ void SkyNet::emit_CallHungup(QByteArray by)
emit CallHungup(by);
}
+void SkyNet::emit_IncomingAudioParams(QByteArray peer_id, QString codec, quint16 sample_rate)
+{
+ emit IncomingAudioParams(peer_id, codec, sample_rate);
+}
+
+void SkyNet::emit_IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data)
+{
+ emit IncomingAudioPacket(peer_id, seq_num, timestamp, data);
+}
+
//callbacks
void SkyNet::newmsg_cb(const unsigned char *peer_id, const char *msgbuf, unsigned int nb, int type)
@@ -1182,7 +1224,7 @@ void SkyNet::c2c_failed(const unsigned char *peer_id)
if (skynet->is_call_waiting_for(peer))
{
- skynet->emit_CallFailed(QString(tr("Failed to establish the C2C connection to %1")).arg(QString(peer.toBase64())));
+ skynet->emit_CallFailed(peer, QString(tr("Failed to establish the C2C connection to %1")).arg(QString(peer.toBase64())));
skynet->call_waiting_c2c_connect = false;
}
}
@@ -1201,7 +1243,7 @@ void SkyNet::notfound_cb(const unsigned char *callee_id)
if (skynet->is_call_waiting_for(peer))
{
- skynet->emit_CallFailed(QString(tr("Requested peer was not found online in the Pica Pica network")));
+ skynet->emit_CallFailed(peer, QString(tr("Requested peer was not found online in the Pica Pica network")));
skynet->call_waiting_c2c_connect = false;
}
}
@@ -1381,16 +1423,18 @@ void SkyNet::call_hangup_cb(const unsigned char *peer_id)
void SkyNet::call_audio_params_cb(const unsigned char *peer_id, const char *codec, uint16_t sample_rate)
{
+ skynet->emit_IncomingAudioParams(QByteArray((const char*)peer_id, PICA_ID_SIZE), QString::fromLatin1(codec), sample_rate);
}
void SkyNet::call_video_params_cb(const unsigned char *peer_id, const char *codec, uint16_t width, uint16_t height)
{
}
-void SkyNet::call_audio_packet_cb(const unsigned char *peer_id, uint16_t size, const char *pkt_data)
+void SkyNet::call_audio_packet_cb(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data)
{
+ skynet->emit_IncomingAudioPacket(QByteArray((const char*)peer_id, PICA_ID_SIZE), seq_num, timestamp, QByteArray(pkt_data, size));
}
-void SkyNet::call_video_packet_cb(const unsigned char *peer_id, uint16_t size, const char *pkt_data)
+void SkyNet::call_video_packet_cb(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data)
{
}
diff --git a/pica-client/skynet.h b/pica-client/skynet.h
index 64f0874..80d9a2f 100644
--- a/pica-client/skynet.h
+++ b/pica-client/skynet.h
@@ -49,6 +49,9 @@ public:
void RejectCall(QByteArray from);
void HangupCall(QByteArray with);
+ void SendAudioParams(QByteArray to, QString codec, quint16 sample_rate);
+ void SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data);
+
signals:
void MessageReceived(QByteArray from, QString msg);
void UnableToDeliver(QByteArray to, QString msg);
@@ -78,11 +81,14 @@ signals:
void c2cClosed(QByteArray peer_id);
- void CallFailed(QString reason);
+ void CallFailed(QByteArray peer_id, QString reason);
void IncomingCall(QByteArray from);
void CallAccepted(QByteArray by);
void CallRejected(QByteArray by);
void CallHungup(QByteArray by);
+
+ void IncomingAudioParams(QByteArray peer_id, QString codec, quint16 sample_rate);
+ void IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data);
private:
// Constructed in init(), once this SkyNet instance is running on its
// own network thread and that thread's dedicated QSqlDatabase
@@ -150,12 +156,15 @@ private:
void emit_c2cClosed(QByteArray peer_id);
void emit_ConnectionStatusUpdated(QByteArray peer_id, QString status);
- void emit_CallFailed(QString reason);
+ void emit_CallFailed(QByteArray peer_id, QString reason);
void emit_IncomingCall(QByteArray from);
void emit_CallAccepted(QByteArray by);
void emit_CallRejected(QByteArray by);
void emit_CallHungup(QByteArray by);
+ void emit_IncomingAudioParams(QByteArray peer_id, QString codec, quint16 sample_rate);
+ void emit_IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data);
+
static void newmsg_cb(const unsigned char *peer_id, const char* msgbuf, unsigned int nb, int type);
static void msgok_cb(const unsigned char *peer_id);
@@ -210,9 +219,9 @@ private:
static void call_video_params_cb(const unsigned char *peer_id, const char *codec, uint16_t width, uint16_t height);
- static void call_audio_packet_cb(const unsigned char *peer_id, uint16_t size, const char *pkt_data);
+ static void call_audio_packet_cb(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data);
- static void call_video_packet_cb(const unsigned char *peer_id, uint16_t size, const char *pkt_data);
+ static void call_video_packet_cb(const unsigned char *peer_id, uint16_t seq_num, uint32_t timestamp, uint16_t size, const char *pkt_data);
private slots:
void nodelink_activated(PICA_c2n *c2n);
@@ -261,6 +270,9 @@ private slots:
void do_RejectCall(QByteArray from);
void do_HangupCall(QByteArray with);
+ void do_SendAudioParams(QByteArray to, QString codec, quint16 sample_rate);
+ void do_SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data);
+
};
#endif // SKYNET_H