diff options
| -rw-r--r-- | Makefile.am | 1 | ||||
| -rw-r--r-- | PICA_client.c | 46 | ||||
| -rw-r--r-- | PICA_client.h | 8 | ||||
| -rw-r--r-- | PICA_proto.h | 7 | ||||
| -rw-r--r-- | doc/proto-doc-latest.txt | 30 | ||||
| -rw-r--r-- | pica-client/audiodevice.cpp | 539 | ||||
| -rw-r--r-- | pica-client/audiodevice.h | 76 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.cpp | 121 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.h | 20 | ||||
| -rw-r--r-- | pica-client/skynet.cpp | 70 | ||||
| -rw-r--r-- | pica-client/skynet.h | 20 |
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(µphone_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 |
