summaryrefslogtreecommitdiffstats
path: root/pica-client/audiovideocallcontroller.cpp
diff options
context:
space:
mode:
authorAnton <anton@picapica.im>2026-08-12 22:52:20 +0400
committerAnton <anton@picapica.im>2026-08-12 22:52:20 +0400
commit9c5a67f89a6f90080e171fb6c7bb0964bd9e69f3 (patch)
treefdfac4241af1508608eb40ef60d2c3daf86a897f /pica-client/audiovideocallcontroller.cpp
parent386f189f03fc199285716d78a059b790bd1b9a6b (diff)
downloadpica-pica-9c5a67f89a6f90080e171fb6c7bb0964bd9e69f3.tar.gz
pica-pica-9c5a67f89a6f90080e171fb6c7bb0964bd9e69f3.zip
pica-client: voice calls
Diffstat (limited to 'pica-client/audiovideocallcontroller.cpp')
-rw-r--r--pica-client/audiovideocallcontroller.cpp121
1 files changed, 117 insertions, 4 deletions
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));
+}
+