summaryrefslogtreecommitdiffstats
path: root/pica-client/audiovideocallcontroller.cpp
diff options
context:
space:
mode:
authorAnton <anton@picapica.im>2026-11-14 00:27:08 +0400
committerAnton <anton@picapica.im>2026-11-14 00:27:08 +0400
commit6d8ece9aa9a90937434ef2a4b6917a319a325d4e (patch)
tree632f92290d841861c6b1234fc56954c0358ebbaa /pica-client/audiovideocallcontroller.cpp
parentc4ca50c617f2b158a4448b68dd7c7a289c1ff0d9 (diff)
downloadpica-pica-audio_video_calls.tar.gz
pica-pica-audio_video_calls.zip
pica-client: fix releasing call resourcesaudio_video_calls
Diffstat (limited to 'pica-client/audiovideocallcontroller.cpp')
-rw-r--r--pica-client/audiovideocallcontroller.cpp231
1 files changed, 183 insertions, 48 deletions
diff --git a/pica-client/audiovideocallcontroller.cpp b/pica-client/audiovideocallcontroller.cpp
index 8f7c8df..62cb0e0 100644
--- a/pica-client/audiovideocallcontroller.cpp
+++ b/pica-client/audiovideocallcontroller.cpp
@@ -19,13 +19,15 @@
#include "skynet.h"
#include "settings.h"
#include "callsettings.h"
+#include "accounts.h"
#include <QByteArray>
#include <QDebug>
#include <QMetaObject>
AudioVideoCallController::AudioVideoCallController(QObject *parent)
- : QObject(parent), callwindow(0), ringdevice(0), is_active(false),
+ : QObject(parent), callwindow(0), ringdevice(0), m_state(CallIdle),
+ m_audioPlaybackStarted(false), m_videoPlaybackStarted(false),
m_audioSeq(0),
m_videoSeq(0), m_videoFrameTimestamp(0), m_videoFrameStarted(false),
m_requestedFrameRate(0)
@@ -35,6 +37,10 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent)
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(c2cClosed(QByteArray)), this, SLOT(peer_disconnected(QByteArray)));
+
+ // setEchoCanceller() is invoked through the audio threads' event queues
+ qRegisterMetaType<EchoCancellerPtr>("EchoCancellerPtr");
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)));
@@ -172,6 +178,13 @@ void AudioVideoCallController::startAudioPipeline()
m_audioSeq = 0;
m_callClock.start();
+ m_audioPlaybackStarted = false;
+
+ // Before anything is queued for them - see MediaCallSession. output is
+ // opened here as well, although it is only started once the peer's 0x74
+ // arrives.
+ microphone->openSession(m_caller_id, m_callee_id);
+ output->openSession(m_caller_id, m_callee_id);
// One canceller for the call, shared by both directions: the playback
// side feeds it what it is about to play, the capture side subtracts the
@@ -195,10 +208,13 @@ void AudioVideoCallController::startAudioPipeline()
if (m_echoCanceller && !m_echoCanceller->isValid())
m_echoCanceller.clear();
- // Set before the blocking loops are started, so the queued Capture()/
- // Play() calls below are what publishes it to those threads.
- microphone->setEchoCanceller(m_echoCanceller);
- output->setEchoCanceller(m_echoCanceller);
+ // Queued like the calls that start the loops, and ahead of them: the
+ // loops of the previous call may still be on their way out, and they read
+ // the canceller they were given on their own threads.
+ QMetaObject::invokeMethod(microphone, "setEchoCanceller", Qt::QueuedConnection,
+ Q_ARG(EchoCancellerPtr, m_echoCanceller));
+ QMetaObject::invokeMethod(output, "setEchoCanceller", Qt::QueuedConnection,
+ Q_ARG(EchoCancellerPtr, m_echoCanceller));
// Declare our outgoing codec to the peer, then start capturing and
// encoding right away - the peer's decoder is only ready once it has
@@ -208,7 +224,8 @@ void AudioVideoCallController::startAudioPipeline()
QMetaObject::invokeMethod(microphone, "configureCapture", Qt::QueuedConnection,
Q_ARG(QString, capDev), Q_ARG(QString, cs.audioCodec),
- Q_ARG(int, sampleRate), Q_ARG(int, bitrate));
+ Q_ARG(int, sampleRate), Q_ARG(int, bitrate),
+ Q_ARG(QByteArray, m_caller_id), Q_ARG(QByteArray, m_callee_id));
QMetaObject::invokeMethod(microphone, "Capture", Qt::QueuedConnection);
// output is configured lazily, once the peer's own 0x74 (see
@@ -241,6 +258,12 @@ void AudioVideoCallController::startVideoPipeline()
QString camDev = st.loadValue("video.capture_device", QString()).toString();
const CallSettings cs = CallSettings::load();
+ // Before anything is queued for them - see MediaCallSession. And before
+ // the check for a camera below: remotevideo does not depend on having one.
+ m_videoPlaybackStarted = false;
+ cam->openSession(m_caller_id, m_callee_id);
+ remotevideo->openSession(m_caller_id, m_callee_id);
+
// Unlike the audio devices, which fall back to the platform's "default"
// device, there is no such name for cameras - so with nothing configured
// yet (the setting is only written once the settings dialog has been
@@ -286,7 +309,8 @@ void AudioVideoCallController::startVideoPipeline()
Q_ARG(int, cs.captureFrameRate),
Q_ARG(bool, preferCompressed),
Q_ARG(QString, cs.videoCodec), Q_ARG(int, cs.videoBitrateKbps * 1000),
- Q_ARG(int, cs.videoEncodingAcceleration));
+ Q_ARG(int, cs.videoEncodingAcceleration),
+ Q_ARG(QByteArray, m_caller_id), Q_ARG(QByteArray, m_callee_id));
QMetaObject::invokeMethod(cam, "Capture", Qt::QueuedConnection);
// remotevideo is configured lazily, once the peer's own 0x75 (see
@@ -302,14 +326,43 @@ void AudioVideoCallController::stopVideoPipeline()
m_videoAssembler.reset();
}
+void AudioVideoCallController::finishCall(bool notify_peer)
+{
+ const CallState state = m_state;
+
+ m_state = CallIdle;
+
+ stopTones();
+ stopAudioPipeline();
+ stopVideoPipeline();
+
+ if (!notify_peer)
+ return;
+
+ if (state == CallDialing || state == CallActive)
+ skynet->HangupCall(m_peer_id);
+ else if (state == CallRinging)
+ skynet->RejectCall(m_peer_id);
+}
+
void AudioVideoCallController::call_failed(QByteArray peer_id, QString reason)
{
qDebug() << "Call failed: " << reason << "\n";
- if (peer_id != m_peer_id)
+ /* Only starting a call and answering one can fail in a way that matters
+ here; anything else arrives when the call is already over. */
+ if (peer_id != m_peer_id || (m_state != CallDialing && m_state != CallActive))
return;
- is_active = false;
+ const bool was_active = (m_state == CallActive);
+
+ finishCall(false);
+
+ /* After a failed attempt to call the window is left as it is, so that
+ "Call" can be pressed again. */
+ if (was_active && callwindow)
+ callwindow->call_ended();
+
/* play unreachable */
playEarpieceTone(&TonePlayer::playUnreachable);
}
@@ -320,11 +373,11 @@ void AudioVideoCallController::call_accepted(QByteArray peer_id)
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)
+ if (peer_id != m_peer_id || m_state != CallDialing || !callwindow)
return;
stopTones();
- is_active = true;
+ m_state = CallActive;
callwindow->call_started();
startAudioPipeline();
startVideoPipeline();
@@ -332,28 +385,57 @@ void AudioVideoCallController::call_accepted(QByteArray peer_id)
void AudioVideoCallController::call_rejected(QByteArray peer_id)
{
- if (peer_id != m_peer_id)
+ if (peer_id != m_peer_id || m_state != CallDialing)
return;
+ finishCall(false);
+
/* play busy tone */
playEarpieceTone(&TonePlayer::playBusy);
}
void AudioVideoCallController::call_hungup(QByteArray peer_id)
{
- if (peer_id != m_peer_id)
+ /* queued from the SkyNet thread - the call may already be over here */
+ if (peer_id != m_peer_id || m_state == CallIdle)
return;
- stopAudioPipeline();
- stopVideoPipeline();
+ const bool was_ringing = (m_state == CallRinging);
+
+ finishCall(false);
+
+ if (was_ringing)
+ {
+ /* The caller gave up before the call was answered. Nothing is left
+ for the window to offer. */
+ if (callwindow)
+ callwindow->close();
+
+ return;
+ }
- /* 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;
+}
+
+void AudioVideoCallController::peer_disconnected(QByteArray peer_id)
+{
+ if (peer_id != m_peer_id || m_state == CallIdle)
+ return;
+
+ if (m_state == CallDialing)
+ {
+ finishCall(false);
+ playEarpieceTone(&TonePlayer::playUnreachable);
+ return;
+ }
+
+ /* No hangup message is going to come over a connection that is gone, and
+ for the call it amounts to the same thing. */
+ call_hungup(peer_id);
}
void AudioVideoCallController::start_call(QByteArray peer_id)
@@ -362,15 +444,20 @@ void AudioVideoCallController::start_call(QByteArray peer_id)
{
callwindow->show();
-
- /* call is already in progress */
- if (is_active)
+ /* one call at a time, and this one is not over yet */
+ if (m_state != CallIdle)
+ {
+ callwindow->raise();
return;
+ }
+
/* re-create callwindow for a new call */
callwindow->close();
}
m_peer_id = peer_id;
+ m_caller_id = Accounts::GetCurrentAccount().id;
+ m_callee_id = peer_id;
callwindow = new CallWindow(peer_id, false);
connect(callwindow, SIGNAL(start_call_pressed()), this, SLOT(initiate_call()));
connect(callwindow, SIGNAL(hang_call_pressed()), this, SLOT(end_call()));
@@ -388,14 +475,23 @@ void AudioVideoCallController::start_call(QByteArray peer_id)
void AudioVideoCallController::call_from(QByteArray peer_id)
{
- if (callwindow)
+ /* One call at a time: busy. The peer hears the busy tone and our own call
+ is not disturbed. */
+ if (m_state != CallIdle)
{
- /* HANDLE BUSY CASE!!! */
- /* call is already in progress */
+ skynet->RejectCall(peer_id);
return;
}
+ /* A window left open with no call in it - one where "Call" has not been
+ pressed, or whose call is over - gives way to the incoming call. */
+ if (callwindow)
+ callwindow->close();
+
+ m_state = CallRinging;
m_peer_id = peer_id;
+ m_caller_id = peer_id;
+ m_callee_id = Accounts::GetCurrentAccount().id;
callwindow = new CallWindow(peer_id, true);
connect(callwindow, SIGNAL(accept_call_pressed()), this, SLOT(accept_call()));
connect(callwindow, SIGNAL(hang_call_pressed()), this, SLOT(end_call()));
@@ -415,6 +511,11 @@ void AudioVideoCallController::call_from(QByteArray peer_id)
void AudioVideoCallController::initiate_call()
{
+ /* "Call" pressed again while the first press is still ringing */
+ if (m_state != CallIdle)
+ return;
+
+ m_state = CallDialing;
skynet->StartCall(m_peer_id);
/* play ringing tone */
playEarpieceTone(&TonePlayer::playCallInProgress);
@@ -422,9 +523,12 @@ void AudioVideoCallController::initiate_call()
void AudioVideoCallController::accept_call()
{
+ if (m_state != CallRinging)
+ return;
+
stopTones();
skynet->AcceptCall(m_peer_id);
- is_active = true;
+ m_state = CallActive;
callwindow->call_started();
startAudioPipeline();
startVideoPipeline();
@@ -432,28 +536,25 @@ void AudioVideoCallController::accept_call()
void AudioVideoCallController::end_call()
{
- stopTones();
- stopAudioPipeline();
- stopVideoPipeline();
-
- if (is_active)
- skynet->HangupCall(m_peer_id);
- else
- skynet->RejectCall(m_peer_id);
-
- is_active = false;
- callwindow->close();
+ /* the call ends with its window, see callwindow_closed() */
+ if (callwindow)
+ callwindow->close();
}
void AudioVideoCallController::callwindow_closed(CallWindow *sender_window)
{
+ if (sender_window != callwindow)
+ return;
+
+ /* However the window got closed - "Hang Up", its close button, or being
+ replaced for a new call - whatever call was in it goes with it. */
+ finishCall(true);
callwindow = 0;
- is_active = false;
}
void AudioVideoCallController::send_audio_packet(QByteArray data)
{
- if (!is_active)
+ if (m_state != CallActive)
return;
quint16 seq = m_audioSeq++;
@@ -480,7 +581,7 @@ void AudioVideoCallController::capture_clipping(double pinnedPercent, double pea
void AudioVideoCallController::incoming_audio_params(QByteArray peer_id, QString codec, quint16 sample_rate)
{
- if (!is_active || peer_id != m_peer_id)
+ if (m_state != CallActive || peer_id != m_peer_id)
return;
// The codec name is peer-supplied and would otherwise be handed straight
@@ -494,8 +595,26 @@ void AudioVideoCallController::incoming_audio_params(QByteArray peer_id, QString
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));
+ if (!m_audioPlaybackStarted)
+ {
+ m_audioPlaybackStarted = true;
+ QMetaObject::invokeMethod(output, "configurePlayback", Qt::QueuedConnection,
+ Q_ARG(QString, playDev), Q_ARG(QString, codec), Q_ARG(int, (int)sample_rate),
+ Q_ARG(QByteArray, m_caller_id), Q_ARG(QByteArray, m_callee_id));
+ }
+ else
+ {
+ // The peer changed what it is sending in the middle of the call.
+ // Direct call, not invokeMethod()/QueuedConnection - see the comment
+ // in stopAudioPipeline(): output's thread is blocked inside Play(),
+ // which starts over with the new parameters.
+ output->changePlayback(playDev, codec, (int)sample_rate);
+ }
+
+ // Queued in both cases. If Play() is running, this one only gets its turn
+ // when the call is over and returns at once; but if Play() has given up -
+ // on a codec that would not open, say - this is what starts it again for
+ // the new parameters.
QMetaObject::invokeMethod(output, "Play", Qt::QueuedConnection);
}
@@ -503,7 +622,7 @@ void AudioVideoCallController::incoming_audio_packet(QByteArray peer_id, quint16
{
Q_UNUSED(timestamp);
- if (!is_active || peer_id != m_peer_id)
+ if (m_state != CallActive || peer_id != m_peer_id)
return;
// Direct call, not invokeMethod()/QueuedConnection - see the comment in
@@ -517,7 +636,7 @@ void AudioVideoCallController::incoming_audio_packet(QByteArray peer_id, quint16
void AudioVideoCallController::video_capture_started(QString codec, int width, int height,
double frameRate)
{
- if (!is_active)
+ if (m_state != CallActive)
return;
// Logged rather than announced: the 0x74 message has no field for it, and
@@ -537,7 +656,7 @@ void AudioVideoCallController::video_capture_started(QString codec, int width, i
void AudioVideoCallController::send_video_packet(QByteArray data, bool is_last_fragment)
{
- if (!is_active)
+ if (m_state != CallActive)
return;
// Every fragment of one encoded frame carries the timestamp taken when
@@ -583,7 +702,7 @@ void AudioVideoCallController::media_transport_changed(QByteArray peer_id, bool
void AudioVideoCallController::incoming_video_params(QByteArray peer_id, QString codec, quint16 width, quint16 height)
{
- if (!is_active || peer_id != m_peer_id)
+ if (m_state != CallActive || peer_id != m_peer_id)
return;
// As in incoming_audio_params(): the peer-supplied codec name must be one
@@ -605,15 +724,31 @@ void AudioVideoCallController::incoming_video_params(QByteArray peer_id, QString
vaapiRendering = false;
#endif
- QMetaObject::invokeMethod(remotevideo, "configurePlayback", Qt::QueuedConnection,
- Q_ARG(QString, codec), Q_ARG(int, (int)width), Q_ARG(int, (int)height),
- Q_ARG(int, cs.videoDecodingAcceleration), Q_ARG(bool, vaapiRendering));
+ if (!m_videoPlaybackStarted)
+ {
+ m_videoPlaybackStarted = true;
+ QMetaObject::invokeMethod(remotevideo, "configurePlayback", Qt::QueuedConnection,
+ Q_ARG(QString, codec), Q_ARG(int, (int)width), Q_ARG(int, (int)height),
+ Q_ARG(int, cs.videoDecodingAcceleration), Q_ARG(bool, vaapiRendering),
+ Q_ARG(QByteArray, m_caller_id), Q_ARG(QByteArray, m_callee_id));
+ }
+ else
+ {
+ // The peer changed what it is sending in the middle of the call - as
+ // in incoming_audio_params(). A fragment of a frame in the old format
+ // that is still being put together is of no use to the new decoder.
+ m_videoAssembler.reset();
+ remotevideo->changePlayback(codec, (int)width, (int)height,
+ cs.videoDecodingAcceleration, vaapiRendering);
+ }
+
+ // Queued in both cases, see incoming_audio_params().
QMetaObject::invokeMethod(remotevideo, "Play", Qt::QueuedConnection);
}
void AudioVideoCallController::incoming_video_packet(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data)
{
- if (!is_active || peer_id != m_peer_id)
+ if (m_state != CallActive || peer_id != m_peer_id)
return;
QByteArray frame = m_videoAssembler.addFragment(seq_num, timestamp, data);