diff options
| author | Anton <anton@picapica.im> | 2026-08-12 22:52:20 +0400 |
|---|---|---|
| committer | Anton <anton@picapica.im> | 2026-08-12 22:52:20 +0400 |
| commit | 9c5a67f89a6f90080e171fb6c7bb0964bd9e69f3 (patch) | |
| tree | fdfac4241af1508608eb40ef60d2c3daf86a897f /pica-client/audiovideocallcontroller.cpp | |
| parent | 386f189f03fc199285716d78a059b790bd1b9a6b (diff) | |
| download | pica-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.cpp | 121 |
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(µ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)); +} + |
