diff options
| author | Anton <anton@picapica.im> | 2026-08-13 01:28:32 +0400 |
|---|---|---|
| committer | Anton <anton@picapica.im> | 2026-08-13 01:28:32 +0400 |
| commit | d10deee6b3143fffd0d4d55b87bf6167c34dd459 (patch) | |
| tree | d10fc58c4d8382b37046062830010d305094ee69 /pica-client | |
| parent | 07697978227d9d938a2ce2b72010d3b9cebea041 (diff) | |
| download | pica-pica-d10deee6b3143fffd0d4d55b87bf6167c34dd459.tar.gz pica-pica-d10deee6b3143fffd0d4d55b87bf6167c34dd459.zip | |
pica-client: video call
Diffstat (limited to 'pica-client')
| -rw-r--r-- | pica-client/audiovideocallcontroller.cpp | 142 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.h | 27 | ||||
| -rw-r--r-- | pica-client/callwindow.cpp | 18 | ||||
| -rw-r--r-- | pica-client/callwindow.h | 5 | ||||
| -rw-r--r-- | pica-client/dialogs/settingsdialog.cpp | 128 | ||||
| -rw-r--r-- | pica-client/dialogs/settingsdialog.h | 25 | ||||
| -rw-r--r-- | pica-client/skynet.cpp | 44 | ||||
| -rw-r--r-- | pica-client/skynet.h | 12 | ||||
| -rw-r--r-- | pica-client/videodevice.cpp | 485 | ||||
| -rw-r--r-- | pica-client/videodevice.h | 127 |
10 files changed, 1004 insertions, 9 deletions
diff --git a/pica-client/audiovideocallcontroller.cpp b/pica-client/audiovideocallcontroller.cpp index 964b15c..0df0a79 100644 --- a/pica-client/audiovideocallcontroller.cpp +++ b/pica-client/audiovideocallcontroller.cpp @@ -22,8 +22,16 @@ #include <QDebug> #include <QMetaObject> +// Capture/encode resolution announced to the peer in the 0x75 message. Fixed +// for now - a resolution setting is a planned follow-up, see VideoDevice for +// the rest of the encoding parameters. +static const quint16 kVideoWidth = 640; +static const quint16 kVideoHeight = 480; + + AudioVideoCallController::AudioVideoCallController(QObject *parent) - : QObject(parent), callwindow(0), ringdevice(0), is_active(false), m_audioSeq(0) + : QObject(parent), callwindow(0), ringdevice(0), is_active(false), m_audioSeq(0), + m_videoSeq(0), m_videoFrameTimestamp(0), m_videoFrameStarted(false) { connect(skynet, SIGNAL(IncomingCall(QByteArray)), this, SLOT(call_from(QByteArray))); connect(skynet, SIGNAL(CallFailed(QByteArray,QString)), this, SLOT(call_failed(QByteArray,QString))); @@ -34,6 +42,9 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent) 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))); + connect(skynet, SIGNAL(IncomingVideoParams(QByteArray,QString,quint16,quint16)), this, SLOT(incoming_video_params(QByteArray,QString,quint16,quint16))); + connect(skynet, SIGNAL(IncomingVideoPacket(QByteArray,quint16,quint32,QByteArray)), this, SLOT(incoming_video_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 @@ -58,12 +69,24 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent) output = new AudioDevice(); output->moveToThread(&output_thread); output_thread.start(); + + // Camera capture+encode and remote video decode - same arrangement as + // the audio devices above. + cam = new VideoDevice(); + cam->moveToThread(&cam_thread); + connect(cam, SIGNAL(packetReady(QByteArray,bool)), this, SLOT(send_video_packet(QByteArray,bool))); + cam_thread.start(); + + remotevideo = new VideoDevice(); + remotevideo->moveToThread(&remotevideo_thread); + remotevideo_thread.start(); } AudioVideoCallController::~AudioVideoCallController() { stopTones(); stopAudioPipeline(); + stopVideoPipeline(); toneplayer_thread.quit(); toneplayer_thread.wait(); @@ -80,6 +103,14 @@ AudioVideoCallController::~AudioVideoCallController() output_thread.quit(); output_thread.wait(); delete output; + + cam_thread.quit(); + cam_thread.wait(); + delete cam; + + remotevideo_thread.quit(); + remotevideo_thread.wait(); + delete remotevideo; } void AudioVideoCallController::playEarpieceTone(void (TonePlayer::*tone)()) @@ -150,6 +181,53 @@ void AudioVideoCallController::stopAudioPipeline() output->Close(); } +void AudioVideoCallController::startVideoPipeline() +{ + Settings st(config_dbname); + QString camDev = st.loadValue("video.capture_device", QString()).toString(); + + // 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 + // visited) pick the first camera found. + if (camDev.isEmpty()) + { + VideoDevice enumerator; + QList<MediaDeviceInfo> cams = enumerator.Enumerate(CAPTURE); + + if (!cams.isEmpty()) + camDev = cams.first().device; + } + + // No camera at all - the call simply carries no outgoing video. Incoming + // video is unaffected, it only depends on what the peer sends. + if (camDev.isEmpty()) + return; + + m_videoSeq = 0; + m_videoFrameTimestamp = 0; + m_videoFrameStarted = false; + + skynet->SendVideoParams(m_peer_id, QStringLiteral("h264"), kVideoWidth, kVideoHeight); + + QMetaObject::invokeMethod(cam, "configureCapture", Qt::QueuedConnection, + Q_ARG(QString, camDev), Q_ARG(QString, QStringLiteral("h264")), + Q_ARG(int, kVideoWidth), Q_ARG(int, kVideoHeight)); + QMetaObject::invokeMethod(cam, "Capture", Qt::QueuedConnection); + + // remotevideo is configured lazily, once the peer's own 0x75 (see + // incoming_video_params()) tells us what it is sending. +} + +void AudioVideoCallController::stopVideoPipeline() +{ + // Direct calls, for the same reason stopAudioPipeline() uses them. + cam->Close(); + remotevideo->Close(); + + m_videoAssembler.reset(); +} + void AudioVideoCallController::call_failed(QByteArray peer_id, QString reason) { qDebug() << "Call failed: " << reason << "\n"; @@ -175,6 +253,7 @@ void AudioVideoCallController::call_accepted(QByteArray peer_id) is_active = true; callwindow->call_started(); startAudioPipeline(); + startVideoPipeline(); } void AudioVideoCallController::call_rejected(QByteArray peer_id) @@ -192,6 +271,7 @@ void AudioVideoCallController::call_hungup(QByteArray peer_id) return; stopAudioPipeline(); + stopVideoPipeline(); /* queued from the SkyNet thread as well - the window may already be closed */ if (callwindow) @@ -221,6 +301,9 @@ void AudioVideoCallController::start_call(QByteArray peer_id) connect(callwindow, SIGNAL(start_call_pressed()), this, SLOT(initiate_call())); connect(callwindow, SIGNAL(hang_call_pressed()), this, SLOT(end_call())); connect(callwindow, SIGNAL(callwindow_closed(CallWindow*)), this, SLOT(callwindow_closed(CallWindow*))); + /* decoded frames arrive from remotevideo's thread, so this is a queued + connection; it dies with the window, which is deleted on close */ + connect(remotevideo, SIGNAL(frameReady(QImage)), callwindow, SLOT(showRemoteFrame(QImage))); callwindow->show(); } @@ -238,6 +321,8 @@ void AudioVideoCallController::call_from(QByteArray peer_id) connect(callwindow, SIGNAL(accept_call_pressed()), this, SLOT(accept_call())); connect(callwindow, SIGNAL(hang_call_pressed()), this, SLOT(end_call())); connect(callwindow, SIGNAL(callwindow_closed(CallWindow*)), this, SLOT(callwindow_closed(CallWindow*))); + /* see the same connection in start_call() */ + connect(remotevideo, SIGNAL(frameReady(QImage)), callwindow, SLOT(showRemoteFrame(QImage))); callwindow->show(); /* play ring tone */ @@ -258,12 +343,14 @@ void AudioVideoCallController::accept_call() is_active = true; callwindow->call_started(); startAudioPipeline(); + startVideoPipeline(); } void AudioVideoCallController::end_call() { stopTones(); stopAudioPipeline(); + stopVideoPipeline(); if (is_active) skynet->HangupCall(m_peer_id); @@ -320,3 +407,56 @@ void AudioVideoCallController::incoming_audio_packet(QByteArray peer_id, quint16 output->enqueuePacket(data); } +void AudioVideoCallController::send_video_packet(QByteArray data, bool is_last_fragment) +{ + if (!is_active) + return; + + // Every fragment of one encoded frame carries the timestamp taken when + // that frame's first fragment was sent, so the receiving side can tell + // the fragments of one frame from those of the next. + if (!m_videoFrameStarted) + { + m_videoFrameTimestamp = (quint32)m_callClock.elapsed(); + m_videoFrameStarted = true; + } + + quint16 seq = m_videoSeq & VideoFrameAssembler::SeqNumMask; + m_videoSeq = (m_videoSeq + 1) & VideoFrameAssembler::SeqNumMask; + + if (is_last_fragment) + { + seq |= VideoFrameAssembler::LastFragmentFlag; + // The next fragment emitted belongs to a new frame. + m_videoFrameStarted = false; + } + + skynet->SendVideoPacket(m_peer_id, seq, m_videoFrameTimestamp, data); +} + +void AudioVideoCallController::incoming_video_params(QByteArray peer_id, QString codec, quint16 width, quint16 height) +{ + if (!is_active || peer_id != m_peer_id) + return; + + QMetaObject::invokeMethod(remotevideo, "configurePlayback", Qt::QueuedConnection, + Q_ARG(QString, codec), Q_ARG(int, (int)width), Q_ARG(int, (int)height)); + 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) + return; + + QByteArray frame = m_videoAssembler.addFragment(seq_num, timestamp, data); + + // Still waiting for the rest of this frame's fragments. + if (frame.isEmpty()) + return; + + // Direct call, not invokeMethod()/QueuedConnection - same reason as in + // incoming_audio_packet(). + remotevideo->enqueueFrame(frame); +} + diff --git a/pica-client/audiovideocallcontroller.h b/pica-client/audiovideocallcontroller.h index 8df6af1..05a56ed 100644 --- a/pica-client/audiovideocallcontroller.h +++ b/pica-client/audiovideocallcontroller.h @@ -47,7 +47,10 @@ private: AudioDevice *ringdevice; AudioDevice *microphone; AudioDevice *output; + // Local camera capture+encode. VideoDevice *cam; + // Decoding of the video stream received from the peer. + VideoDevice *remotevideo; bool is_active; QByteArray m_peer_id; @@ -65,11 +68,28 @@ private: QThread microphone_thread; QThread output_thread; + // Camera capture+encode and remote-video decode, same arrangement as + // the audio devices above (see VideoDevice). + QThread cam_thread; + QThread remotevideo_thread; + // Sequence number / capture-clock timestamp stamped onto outgoing // 0x76 audio packets (see PICA_PROTO_CALL_PACKET_HDRSIZE). quint16 m_audioSeq; QElapsedTimer m_callClock; + // Outgoing 0x77 video packets. The counter occupies the low 15 bits of + // the wire sequence number, the top bit being the last fragment marker. + // All fragments of one encoded frame share m_videoFrameTimestamp, which + // is re-read from m_callClock only when a new frame starts - tracked by + // m_videoFrameStarted, set false again after emitting a last fragment. + quint16 m_videoSeq; + quint32 m_videoFrameTimestamp; + bool m_videoFrameStarted; + + // Reassembly of incoming 0x77 fragments into whole encoded frames. + VideoFrameAssembler m_videoAssembler; + void playEarpieceTone(void (TonePlayer::*tone)()); void playRingTone(); void stopTones(); @@ -77,6 +97,9 @@ private: void startAudioPipeline(); void stopAudioPipeline(); + void startVideoPipeline(); + void stopVideoPipeline(); + private slots: // button handlers void initiate_call(); @@ -88,6 +111,10 @@ private slots: 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); + void send_video_packet(QByteArray data, bool is_last_fragment); + void incoming_video_params(QByteArray peer_id, QString codec, quint16 width, quint16 height); + void incoming_video_packet(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data); + }; #endif // AUDIOVIDEOCALLCONTROLLER_H diff --git a/pica-client/callwindow.cpp b/pica-client/callwindow.cpp index 21bbcb0..5eb21cc 100644 --- a/pica-client/callwindow.cpp +++ b/pica-client/callwindow.cpp @@ -34,10 +34,15 @@ CallWindow::CallWindow(QByteArray peer_id, bool incoming) lbTimer = new QLabel(this); lbTimer->setAlignment(Qt::AlignCenter); lbTimer->hide(); + lbVideo = new QLabel(this); + lbVideo->setAlignment(Qt::AlignCenter); + lbVideo->setMinimumSize(320, 240); + lbVideo->hide(); callTimer = new QTimer(this); callElapsedSeconds = 0; lv->addWidget(lbTimer); + lv->addWidget(lbVideo); lh->addWidget(pbAccept, Qt::AlignLeft); lh->addWidget(pbCall, Qt::AlignLeft); @@ -77,6 +82,19 @@ void CallWindow::call_ended() pbHang->setDisabled(true); } +void CallWindow::showRemoteFrame(QImage frame) +{ + if (frame.isNull()) + return; + + if (lbVideo->isHidden()) + lbVideo->show(); + + lbVideo->setPixmap(QPixmap::fromImage(frame).scaled(lbVideo->size(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); +} + void CallWindow::update_call_timer() { int hours, minutes, seconds; diff --git a/pica-client/callwindow.h b/pica-client/callwindow.h index 2cc0eeb..9b9869e 100644 --- a/pica-client/callwindow.h +++ b/pica-client/callwindow.h @@ -22,6 +22,7 @@ #include <QPushButton> #include <QLabel> #include <QTimer> +#include <QImage> class CallWindow : public QWidget { @@ -40,6 +41,9 @@ private: QPushButton *pbCall; QPushButton *pbHang; QLabel *lbTimer; + // Displays the remote side's video. Stays hidden until the first frame + // arrives, so an audio-only call keeps the compact window it had before. + QLabel *lbVideo; QTimer *callTimer; int callElapsedSeconds; @@ -48,6 +52,7 @@ private: public slots: void call_started(); void call_ended(); + void showRemoteFrame(QImage frame); private slots: void call(); void accept(); diff --git a/pica-client/dialogs/settingsdialog.cpp b/pica-client/dialogs/settingsdialog.cpp index 86faba6..536d141 100644 --- a/pica-client/dialogs/settingsdialog.cpp +++ b/pica-client/dialogs/settingsdialog.cpp @@ -155,12 +155,40 @@ SettingsDialog::SettingsDialog(QWidget *parent) : fillVideoDevices(); videoDevRefresh = new QPushButton(tr("Refresh 🔄"), this); connect(videoDevRefresh, SIGNAL(clicked()), this, SLOT(fillVideoDevices())); + + btVideoTest = new QPushButton(tr("Test 📷"), this); + connect(btVideoTest, SIGNAL(clicked()), this, SLOT(toggleVideoTest())); + + videoPreview = new QLabel(this); + videoPreview->setAlignment(Qt::AlignCenter); + videoPreview->setMinimumSize(320, 240); + videoPreview->setFrameShape(QFrame::StyledPanel); + videodevLayout->addWidget(videoDev); videodevLayout->addWidget(videoDevRefresh); + videodevLayout->addWidget(btVideoTest); + videodevLayout->addWidget(videoPreview); videodevLayout->addStretch(1); videodevtab->setLayout(videodevLayout); + // Capture+encode and decode ends of the test pipeline, each blocking its + // own thread while running, exactly as they do during a call. They stay + // idle until the Test button is pressed. + videoTestRunning = false; + + testCam = new VideoDevice(); + testCam->moveToThread(&testCamThread); + connect(testCam, SIGNAL(packetReady(QByteArray,bool)), this, SLOT(videoTestFragment(QByteArray,bool))); + connect(testCam, SIGNAL(errorOccurred(QString)), this, SLOT(videoTestError(QString))); + testCamThread.start(); + + testDecoder = new VideoDevice(); + testDecoder->moveToThread(&testDecoderThread); + connect(testDecoder, SIGNAL(frameReady(QImage)), this, SLOT(videoTestFrame(QImage))); + connect(testDecoder, SIGNAL(errorOccurred(QString)), this, SLOT(videoTestError(QString))); + testDecoderThread.start(); + tabW->addTab(directc2ctab, tr("Direct Connections")); tabW->addTab(multilogintab, tr("Multiple logins")); tabW->addTab(audiodevtab, tr("Audio Devices")); @@ -189,6 +217,106 @@ SettingsDialog::SettingsDialog(QWidget *parent) : setWindowTitle(tr("Pica Pica Messenger Settings")); } +SettingsDialog::~SettingsDialog() +{ + // Stops the loops first - the threads cannot finish while Capture()/Play() + // are still running inside them. + testCam->Close(); + testDecoder->Close(); + + testCamThread.quit(); + testCamThread.wait(); + delete testCam; + + testDecoderThread.quit(); + testDecoderThread.wait(); + delete testDecoder; +} + +void SettingsDialog::toggleVideoTest() +{ + if (videoTestRunning) + { + stopVideoTest(); + return; + } + + QString dev = videoDev->itemData(videoDev->currentIndex()).toString(); + + if (dev.isEmpty()) + { + videoPreview->setText(tr("No camera selected")); + return; + } + + testAssembler.reset(); + videoTestRunning = true; + btVideoTest->setText(tr("Stop ⏹")); + videoPreview->setText(tr("Starting...")); + + QMetaObject::invokeMethod(testCam, "configureCapture", Qt::QueuedConnection, + Q_ARG(QString, dev), Q_ARG(QString, QStringLiteral("h264")), + Q_ARG(int, 640), Q_ARG(int, 480)); + QMetaObject::invokeMethod(testCam, "Capture", Qt::QueuedConnection); + + QMetaObject::invokeMethod(testDecoder, "configurePlayback", Qt::QueuedConnection, + Q_ARG(QString, QStringLiteral("h264")), + Q_ARG(int, 640), Q_ARG(int, 480)); + QMetaObject::invokeMethod(testDecoder, "Play", Qt::QueuedConnection); +} + +void SettingsDialog::stopVideoTest() +{ + // Direct calls: both devices are blocked inside their capture/decode + // loops, so a queued call would never reach them (see the comments in + // AudioVideoCallController::stopAudioPipeline()). + testCam->Close(); + testDecoder->Close(); + + testAssembler.reset(); + videoTestRunning = false; + btVideoTest->setText(tr("Test 📷")); + videoPreview->clear(); +} + +void SettingsDialog::videoTestFragment(QByteArray data, bool is_last_fragment) +{ + if (!videoTestRunning) + return; + + // The encoder emits fragments exactly as they would go onto the wire, so + // feed them through the same assembler the receiving end of a call uses. + // The sequence number is rebuilt here the way send_video_packet() builds + // it, the timestamp being constant since only frame boundaries matter. + quint16 seq = is_last_fragment ? VideoFrameAssembler::LastFragmentFlag : 0; + QByteArray frame = testAssembler.addFragment(seq, 0, data); + + if (frame.isEmpty()) + return; + + // Direct call, for the same reason as in the call controller. + testDecoder->enqueueFrame(frame); +} + +void SettingsDialog::videoTestFrame(QImage frame) +{ + if (!videoTestRunning || frame.isNull()) + return; + + videoPreview->setPixmap(QPixmap::fromImage(frame).scaled(videoPreview->size(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); +} + +void SettingsDialog::videoTestError(QString message) +{ + if (!videoTestRunning) + return; + + stopVideoTest(); + videoPreview->setText(message); +} + void SettingsDialog::fillDevicesComboBox(QComboBox *cb, MediaDevice *dev, enum MediaDeviceStreamDirection dir) { cb->clear(); diff --git a/pica-client/dialogs/settingsdialog.h b/pica-client/dialogs/settingsdialog.h index cf767b2..06a2f13 100644 --- a/pica-client/dialogs/settingsdialog.h +++ b/pica-client/dialogs/settingsdialog.h @@ -24,13 +24,18 @@ #include <QRadioButton> #include <QComboBox> #include <QCheckBox> +#include <QLabel> +#include <QThread> +#include <QImage> #include "../mediadevice.h" +#include "../videodevice.h" class SettingsDialog : public QDialog { Q_OBJECT public: explicit SettingsDialog(QWidget *parent = 0); + ~SettingsDialog(); signals: @@ -61,6 +66,21 @@ private: QPushButton *btAudioTest; + // Local video pipeline test: frames from the selected camera are + // encoded, split into protocol sized fragments, reassembled and decoded + // again, then shown in videoPreview - the same path a call takes, minus + // the network, so the whole chain can be checked from this dialog. + QPushButton *btVideoTest; + QLabel *videoPreview; + VideoDevice *testCam; + VideoDevice *testDecoder; + QThread testCamThread; + QThread testDecoderThread; + VideoFrameAssembler testAssembler; + bool videoTestRunning; + + void stopVideoTest(); + QPushButton *btOk; QPushButton *btCancel; @@ -79,6 +99,11 @@ private slots: void fillAudioPlaybackDevices(); void fillAudioRingDevices(); + void toggleVideoTest(); + void videoTestFragment(QByteArray data, bool is_last_fragment); + void videoTestFrame(QImage frame); + void videoTestError(QString message); + }; #endif // SETTINGSDIALOG_H diff --git a/pica-client/skynet.cpp b/pica-client/skynet.cpp index 1cd42bf..0ca37cc 100644 --- a/pica-client/skynet.cpp +++ b/pica-client/skynet.cpp @@ -896,6 +896,38 @@ void SkyNet::do_SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestam } } +void SkyNet::SendVideoParams(QByteArray to, QString codec, quint16 width, quint16 height) +{ + QMetaObject::invokeMethod(this, "do_SendVideoParams", Qt::AutoConnection, + Q_ARG(QByteArray, to), Q_ARG(QString, codec), Q_ARG(quint16, width), Q_ARG(quint16, height)); +} + +void SkyNet::do_SendVideoParams(QByteArray to, QString codec, quint16 width, quint16 height) +{ + struct PICA_c2c *iptr; + + if ((iptr = find_active_chan(to))) + { + PICA_set_call_video_params(iptr, codec.toLatin1().constData(), width, height); + } +} + +void SkyNet::SendVideoPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data) +{ + QMetaObject::invokeMethod(this, "do_SendVideoPacket", Qt::AutoConnection, + Q_ARG(QByteArray, to), Q_ARG(quint16, seq_num), Q_ARG(quint32, timestamp), Q_ARG(QByteArray, data)); +} + +void SkyNet::do_SendVideoPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data) +{ + struct PICA_c2c *iptr; + + if ((iptr = find_active_chan(to))) + { + PICA_send_video_packet(iptr, seq_num, timestamp, data.constData(), data.size()); + } +} + void SkyNet::StartCall(QByteArray to) { QMetaObject::invokeMethod(this, "do_StartCall", Qt::AutoConnection, @@ -1187,6 +1219,16 @@ void SkyNet::emit_IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint emit IncomingAudioPacket(peer_id, seq_num, timestamp, data); } +void SkyNet::emit_IncomingVideoParams(QByteArray peer_id, QString codec, quint16 width, quint16 height) +{ + emit IncomingVideoParams(peer_id, codec, width, height); +} + +void SkyNet::emit_IncomingVideoPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data) +{ + emit IncomingVideoPacket(peer_id, seq_num, timestamp, data); +} + //callbacks void SkyNet::newmsg_cb(const unsigned char *peer_id, const char *msgbuf, unsigned int nb, int type) @@ -1428,6 +1470,7 @@ void SkyNet::call_audio_params_cb(const unsigned char *peer_id, const char *code void SkyNet::call_video_params_cb(const unsigned char *peer_id, const char *codec, uint16_t width, uint16_t height) { + skynet->emit_IncomingVideoParams(QByteArray((const char*)peer_id, PICA_ID_SIZE), QString::fromLatin1(codec), width, height); } 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) @@ -1437,4 +1480,5 @@ void SkyNet::call_audio_packet_cb(const unsigned char *peer_id, uint16_t seq_num 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) { + skynet->emit_IncomingVideoPacket(QByteArray((const char*)peer_id, PICA_ID_SIZE), seq_num, timestamp, QByteArray(pkt_data, size)); } diff --git a/pica-client/skynet.h b/pica-client/skynet.h index 80d9a2f..0baf496 100644 --- a/pica-client/skynet.h +++ b/pica-client/skynet.h @@ -52,6 +52,9 @@ public: void SendAudioParams(QByteArray to, QString codec, quint16 sample_rate); void SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data); + void SendVideoParams(QByteArray to, QString codec, quint16 width, quint16 height); + void SendVideoPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data); + signals: void MessageReceived(QByteArray from, QString msg); void UnableToDeliver(QByteArray to, QString msg); @@ -89,6 +92,9 @@ signals: void IncomingAudioParams(QByteArray peer_id, QString codec, quint16 sample_rate); void IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data); + + void IncomingVideoParams(QByteArray peer_id, QString codec, quint16 width, quint16 height); + void IncomingVideoPacket(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 @@ -165,6 +171,9 @@ private: void emit_IncomingAudioParams(QByteArray peer_id, QString codec, quint16 sample_rate); void emit_IncomingAudioPacket(QByteArray peer_id, quint16 seq_num, quint32 timestamp, QByteArray data); + void emit_IncomingVideoParams(QByteArray peer_id, QString codec, quint16 width, quint16 height); + void emit_IncomingVideoPacket(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); @@ -273,6 +282,9 @@ private slots: void do_SendAudioParams(QByteArray to, QString codec, quint16 sample_rate); void do_SendAudioPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data); + void do_SendVideoParams(QByteArray to, QString codec, quint16 width, quint16 height); + void do_SendVideoPacket(QByteArray to, quint16 seq_num, quint32 timestamp, QByteArray data); + }; #endif // SKYNET_H diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp index aabb245..4dd9d6d 100644 --- a/pica-client/videodevice.cpp +++ b/pica-client/videodevice.cpp @@ -1,5 +1,26 @@ +/* + (c) Copyright 2012 - 2026 Anton Sviridenko + https://picapica.im + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ #include "videodevice.h" +#include "../PICA_proto.h" + #include <QFile> +#include <QDebug> +#include <QMutexLocker> +#include <cstring> #ifdef Q_OS_LINUX #include <linux/videodev2.h> @@ -10,26 +31,471 @@ #include <fcntl.h> #endif -VideoDevice::VideoDevice() +// FFmpeg headers +extern "C" { +#include <libavformat/avformat.h> +#include <libavdevice/avdevice.h> +#include <libavcodec/avcodec.h> +#include <libavutil/opt.h> +#include <libavutil/imgutils.h> +#include <libavutil/frame.h> +#include <libavutil/mem.h> +#include <libavutil/error.h> +#include <libswscale/swscale.h> +} + +// Encoding defaults. Not exposed as settings yet - see the deferred list in +// the feature plan; a fixed, widely supported set of parameters for now. +static const int kFrameRate = 15; +static const int64_t kBitrate = 600000; +// Keyframe every ~2 seconds. There is no retransmission or keyframe request +// machinery yet, so this doubles as how quickly a receiver that missed data +// can resynchronize. +static const int kGopSize = kFrameRate * 2; + +// Largest amount of encoded data one 0x77 message can carry: the protocol's +// per-message payload cap, minus the sequence number + timestamp header that +// every call media packet carries. +static const int kMaxFragmentSize = PICA_PROTO_C2CMSG_MAXDATASIZE - PICA_PROTO_CALL_PACKET_HDRSIZE; + +static QString ff_errstr(int err) +{ + char buf[256] = {0}; + av_strerror(err, buf, sizeof(buf)); + return QString::fromLocal8Bit(buf); +} + +VideoFrameAssembler::VideoFrameAssembler() + : m_timestamp(0), m_inProgress(false) +{ +} + +void VideoFrameAssembler::reset() { + m_buffer.clear(); + m_inProgress = false; +} + +QByteArray VideoFrameAssembler::addFragment(quint16 seq_num, quint32 timestamp, const QByteArray &data) +{ + // Fragments of one encoded frame all carry the same timestamp; a + // different one while a frame is still being assembled means the rest of + // that frame is never coming, so drop what was collected and start over. + if (m_inProgress && timestamp != m_timestamp) + m_buffer.clear(); + + m_buffer.append(data); + m_timestamp = timestamp; + m_inProgress = true; + if (!(seq_num & LastFragmentFlag)) + return QByteArray(); + + QByteArray frame = m_buffer; + reset(); + + return frame; +} + +VideoDevice::VideoDevice(QObject *parent) + : QObject(parent), m_width(640), m_height(480), m_abort(0) +{ } VideoDevice::~VideoDevice() { + Close(); +} +void VideoDevice::configureCapture(QString deviceName, QString codec, int width, int height) +{ + m_deviceName = deviceName; + m_codec = codec; + m_width = width; + m_height = height; } -void VideoDevice::Capture() +void VideoDevice::configurePlayback(QString codec, int width, int height) { + m_codec = codec; + m_width = width; + m_height = height; } -void VideoDevice::Play() +void VideoDevice::enqueueFrame(QByteArray encodedFrame) { + QMutexLocker locker(&m_queueMutex); + + while (m_frameQueue.size() >= kMaxQueueDepth) + m_frameQueue.dequeue(); + + m_frameQueue.enqueue(encodedFrame); + m_queueCond.wakeAll(); } void VideoDevice::Close() { + m_abort.storeRelaxed(1); + + { + QMutexLocker locker(&m_queueMutex); + m_frameQueue.clear(); + } + m_queueCond.wakeAll(); +} + +void VideoDevice::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 = VideoDevice::PlatformDriverName().toUtf8(); + const AVInputFormat *ifmt = av_find_input_format(driverUtf8.constData()); + if (!ifmt) + { + QString msg = QString("Could not find '%1' video input driver").arg(VideoDevice::PlatformDriverName()); + qWarning() << msg; + emit errorOccurred(msg); + return; + } + + AVDictionary *opts = nullptr; + av_dict_set(&opts, "video_size", QString("%1x%2").arg(m_width).arg(m_height).toUtf8().constData(), 0); + av_dict_set(&opts, "framerate", QString::number(kFrameRate).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) + { + // Not fatal to the call - it simply proceeds without outgoing video. + QString msg = QString("Could not open camera '%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 camera 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]; + + // The camera hands us either raw frames or, on many webcams, an already + // compressed stream (MJPEG). Either way it goes through a decoder here to + // get raw pictures for the encoder. Forwarding an already compressed + // camera stream untouched is a planned optimization, not done yet. + const AVCodec *cam_dec = avcodec_find_decoder(in_st->codecpar->codec_id); + AVCodecContext *dec_ctx = cam_dec ? avcodec_alloc_context3(cam_dec) : nullptr; + if (!cam_dec || !dec_ctx || + avcodec_parameters_to_context(dec_ctx, in_st->codecpar) < 0 || + avcodec_open2(dec_ctx, cam_dec, nullptr) < 0) + { + QString msg = "Could not open camera 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("libx264"); + if (!enc) + enc = avcodec_find_encoder(AV_CODEC_ID_H264); + AVCodecContext *enc_ctx = enc ? avcodec_alloc_context3(enc) : nullptr; + if (!enc || !enc_ctx) + { + QString msg = "H.264 encoder not available"; + qWarning() << msg; + emit errorOccurred(msg); + avcodec_free_context(&dec_ctx); + avformat_close_input(&ifmt_ctx); + return; + } + + enc_ctx->width = m_width; + enc_ctx->height = m_height; + enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + enc_ctx->time_base = AVRational{1, kFrameRate}; + enc_ctx->framerate = AVRational{kFrameRate, 1}; + enc_ctx->bit_rate = kBitrate; + enc_ctx->gop_size = kGopSize; + // B-frames reorder output, which would cost latency in a live call. + enc_ctx->max_b_frames = 0; + + // Note the absence of AV_CODEC_FLAG_GLOBAL_HEADER: we want the parameter + // sets repeated in-band with every keyframe, since the receiving side has + // no out-of-band way to learn them and may start decoding mid-stream. + av_opt_set(enc_ctx->priv_data, "preset", "veryfast", 0); + av_opt_set(enc_ctx->priv_data, "tune", "zerolatency", 0); + + if ((ret = avcodec_open2(enc_ctx, enc, nullptr)) < 0) + { + QString msg = QString("Could not open H.264 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; + } + + AVFrame *enc_frame = av_frame_alloc(); + if (enc_frame) + { + enc_frame->format = enc_ctx->pix_fmt; + enc_frame->width = enc_ctx->width; + enc_frame->height = enc_ctx->height; + } + if (!enc_frame || av_frame_get_buffer(enc_frame, 0) < 0) + { + QString msg = "Could not allocate encoder frame"; + qWarning() << msg; + emit errorOccurred(msg); + if (enc_frame) av_frame_free(&enc_frame); + avcodec_free_context(&enc_ctx); + avcodec_free_context(&dec_ctx); + avformat_close_input(&ifmt_ctx); + return; + } + + SwsContext *sws = nullptr; + AVPacket *in_pkt = av_packet_alloc(); + AVFrame *cam_frame = av_frame_alloc(); + AVPacket *out_pkt = av_packet_alloc(); + int64_t pts = 0; + bool loggedReadError = false; + bool loggedScaleError = 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 (!loggedReadError) + { + qWarning() << QString("Camera read failed: %1").arg(ff_errstr(ret)); + loggedReadError = true; + } + break; + } + + ret = avcodec_send_packet(dec_ctx, in_pkt); + av_packet_unref(in_pkt); + if (ret < 0) + continue; + + while (avcodec_receive_frame(dec_ctx, cam_frame) == 0) + { + // Built lazily: the camera's actual frame format and size are + // only known once a frame has been decoded, and may differ from + // what was requested. + if (!sws) + { + sws = sws_getContext(cam_frame->width, cam_frame->height, (AVPixelFormat)cam_frame->format, + enc_ctx->width, enc_ctx->height, enc_ctx->pix_fmt, + SWS_BILINEAR, nullptr, nullptr, nullptr); + if (!sws) + { + if (!loggedScaleError) + { + qWarning() << "Could not set up capture scaler"; + loggedScaleError = true; + } + av_frame_unref(cam_frame); + continue; + } + } + + if (av_frame_make_writable(enc_frame) < 0) + { + av_frame_unref(cam_frame); + continue; + } + + sws_scale(sws, cam_frame->data, cam_frame->linesize, 0, cam_frame->height, + enc_frame->data, enc_frame->linesize); + av_frame_unref(cam_frame); + + enc_frame->pts = pts++; + + if (avcodec_send_frame(enc_ctx, enc_frame) != 0) + { + if (!loggedEncodeError) + { + qWarning() << "H.264 encode failed"; + loggedEncodeError = true; + } + continue; + } + + while (avcodec_receive_packet(enc_ctx, out_pkt) == 0) + { + // One encoded frame goes out as one or more 0x77 messages; + // the final one is flagged so the receiver knows the frame is + // complete (see the fragmentation notes in the protocol doc). + int offset = 0; + while (offset < out_pkt->size) + { + int chunk = qMin(kMaxFragmentSize, out_pkt->size - offset); + bool isLast = (offset + chunk >= out_pkt->size); + + emit packetReady(QByteArray((const char *)out_pkt->data + offset, chunk), isLast); + offset += chunk; + } + av_packet_unref(out_pkt); + } + } + } + + av_packet_free(&in_pkt); + av_packet_free(&out_pkt); + av_frame_free(&cam_frame); + av_frame_free(&enc_frame); + if (sws) sws_freeContext(sws); + avcodec_free_context(&enc_ctx); + avcodec_free_context(&dec_ctx); + avformat_close_input(&ifmt_ctx); +} + +void VideoDevice::Play() +{ + m_abort.storeRelaxed(0); + +#if LIBAVFORMAT_VERSION_MAJOR < 58 + avcodec_register_all(); +#endif + + // Only "h264" is understood by this slice - matches what + // AudioVideoCallController always negotiates via SendVideoParams(). + const AVCodec *dec = avcodec_find_decoder(AV_CODEC_ID_H264); + AVCodecContext *dec_ctx = dec ? avcodec_alloc_context3(dec) : nullptr; + if (m_codec != QLatin1String("h264") || !dec || !dec_ctx || avcodec_open2(dec_ctx, dec, nullptr) < 0) + { + QString msg = QString("Unsupported call video codec: %1").arg(m_codec); + qWarning() << msg; + emit errorOccurred(msg); + if (dec_ctx) avcodec_free_context(&dec_ctx); + return; + } + + SwsContext *sws = nullptr; + int sws_width = 0, sws_height = 0; + AVPixelFormat sws_fmt = AV_PIX_FMT_NONE; + AVFrame *dec_frame = av_frame_alloc(); + AVFrame *rgb_frame = av_frame_alloc(); + bool loggedDecodeError = false; + bool loggedScaleError = false; + + while (true) + { + QByteArray frameData; + { + QMutexLocker locker(&m_queueMutex); + while (m_frameQueue.isEmpty() && !m_abort.loadRelaxed()) + m_queueCond.wait(&m_queueMutex, 100); + + if (m_abort.loadRelaxed()) + break; + + if (m_frameQueue.isEmpty()) + continue; + + frameData = m_frameQueue.dequeue(); + } + + AVPacket *in_pkt = av_packet_alloc(); + if (av_new_packet(in_pkt, frameData.size()) < 0) + { + av_packet_free(&in_pkt); + continue; + } + memcpy(in_pkt->data, frameData.constData(), frameData.size()); + + int ret = avcodec_send_packet(dec_ctx, in_pkt); + av_packet_free(&in_pkt); + if (ret < 0) + { + if (!loggedDecodeError) + { + qWarning() << QString("H.264 decode failed: %1").arg(ff_errstr(ret)); + loggedDecodeError = true; + } + continue; + } + + while (avcodec_receive_frame(dec_ctx, dec_frame) == 0) + { + // Rebuilt whenever the incoming picture format or size changes - + // including the first frame, when they first become known. + if (!sws || dec_frame->width != sws_width || dec_frame->height != sws_height || + (AVPixelFormat)dec_frame->format != sws_fmt) + { + if (sws) sws_freeContext(sws); + sws_width = dec_frame->width; + sws_height = dec_frame->height; + sws_fmt = (AVPixelFormat)dec_frame->format; + + sws = sws_getContext(sws_width, sws_height, sws_fmt, + sws_width, sws_height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, nullptr, nullptr, nullptr); + + av_frame_unref(rgb_frame); + rgb_frame->format = AV_PIX_FMT_RGB24; + rgb_frame->width = sws_width; + rgb_frame->height = sws_height; + + if (!sws || av_frame_get_buffer(rgb_frame, 0) < 0) + { + if (!loggedScaleError) + { + qWarning() << "Could not set up playback scaler"; + loggedScaleError = true; + } + if (sws) { sws_freeContext(sws); sws = nullptr; } + av_frame_unref(dec_frame); + continue; + } + } + + if (av_frame_make_writable(rgb_frame) < 0) + { + av_frame_unref(dec_frame); + continue; + } + + sws_scale(sws, dec_frame->data, dec_frame->linesize, 0, dec_frame->height, + rgb_frame->data, rgb_frame->linesize); + + // Deep copy: the QImage outlives this iteration (it travels to + // the GUI thread through a queued signal), while rgb_frame's + // buffer gets reused by the next scale. + QImage img(rgb_frame->data[0], sws_width, sws_height, + rgb_frame->linesize[0], QImage::Format_RGB888); + emit frameReady(img.copy()); + + av_frame_unref(dec_frame); + } + } + + av_frame_free(&dec_frame); + av_frame_free(&rgb_frame); + if (sws) sws_freeContext(sws); + avcodec_free_context(&dec_ctx); } QList<MediaDeviceInfo> VideoDevice::Enumerate(enum MediaDeviceStreamDirection dir) @@ -79,3 +545,16 @@ QList<MediaDeviceInfo> VideoDevice::Enumerate(enum MediaDeviceStreamDirection di #endif return result; } + +QString VideoDevice::PlatformDriverName() +{ +#if defined(Q_OS_LINUX) + return QStringLiteral("v4l2"); +#elif defined(Q_OS_MACOS) || defined(Q_OS_MAC) + return QStringLiteral("avfoundation"); +#elif defined(Q_OS_WIN) + return QStringLiteral("dshow"); +#else + return QStringLiteral("v4l2"); +#endif +} diff --git a/pica-client/videodevice.h b/pica-client/videodevice.h index b7a344e..9ee4397 100644 --- a/pica-client/videodevice.h +++ b/pica-client/videodevice.h @@ -1,17 +1,134 @@ +/* + (c) Copyright 2012 - 2026 Anton Sviridenko + https://picapica.im + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ #ifndef VIDEODEVICE_H #define VIDEODEVICE_H #include "mediadevice.h" +#include <QObject> +#include <QString> +#include <QByteArray> +#include <QImage> +#include <QQueue> +#include <QMutex> +#include <QWaitCondition> +#include <QAtomicInt> -class VideoDevice : public MediaDevice +// Reassembles the fragments of one encoded video frame, as carried by 0x77 +// protocol messages - see the fragmentation description in +// doc/proto-doc-latest.txt. Used both on the receiving end of a call and by +// the settings dialog's local pipeline test. +class VideoFrameAssembler { public: - VideoDevice(); + // A 0x77 sequence number is a last fragment marker in its top bit plus a + // 15 bit packet counter. + static const quint16 LastFragmentFlag = 0x8000; + static const quint16 SeqNumMask = 0x7FFF; + + VideoFrameAssembler(); + + // Adds one received fragment. Returns the complete encoded frame once + // the fragment carrying the last fragment marker has been added, and an + // empty QByteArray while the frame is still incomplete. + QByteArray addFragment(quint16 seq_num, quint32 timestamp, const QByteArray &data); + + void reset(); + +private: + QByteArray m_buffer; + quint32 m_timestamp; + bool m_inProgress; +}; + +// Video counterpart of AudioDevice: a single instance is dedicated to one +// direction - either capture+encode (Capture(), reading from a local camera) +// or decode (Play(), fed from the network via enqueueFrame()). Each instance +// is meant to live on its own QThread, since both Capture() and Play() block +// the calling thread in a loop until Close() is called. +// +// Note the calling rules that come with that, the same ones AudioDevice +// follows: enqueueFrame() and Close() have to reach a loop that is already +// running, so they are internally thread-safe and must be called directly +// rather than through QMetaObject::invokeMethod() - a queued call would sit +// in the event queue behind the very loop it is meant to feed or interrupt. +// configureCapture()/configurePlayback() are the exception: they only run +// before the loop starts, while the thread is still idle. +class VideoDevice : public QObject, public MediaDevice +{ + Q_OBJECT +public: + VideoDevice(QObject *parent = nullptr); ~VideoDevice(); + + Q_INVOKABLE void configureCapture(QString deviceName, QString codec, int width, int height); + Q_INVOKABLE void configurePlayback(QString codec, int width, int height); + + // Push one complete encoded frame (already reassembled from 0x77 + // fragments by the caller) into the decode queue. Safe to call from any + // thread. Drops the oldest queued frame if the buffer is full - for live + // video, showing the newest frame matters more than showing every frame. + void enqueueFrame(QByteArray encodedFrame); + +public slots: + // Blocking capture+encode loop: opens the configured camera, encodes + // captured frames, and emits packetReady() for each encoded frame, split + // into fragments that fit a single 0x77 protocol message. Returns once + // Close() is called. + virtual void Capture() override; + + // Blocking decode loop: decodes frames pulled from the internal queue + // (see enqueueFrame()) and emits frameReady() for each decoded picture. + // 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 fragment of an encoded frame, ready to be sent over the network. + // isLastFragment marks the final fragment of a frame - it maps directly + // onto the 0x77 sequence number's last fragment marker bit. + void packetReady(QByteArray data, bool isLastFragment); + + // One decoded picture from the remote side, ready to be displayed. + void frameReady(QImage frame); + + void errorOccurred(QString message); + +public: virtual QList<MediaDeviceInfo> Enumerate(enum MediaDeviceStreamDirection dir); - void Capture(); - void Play(); - void Close(); + + // Name of the FFmpeg libavdevice demuxer for the host platform's native + // video capture API. Counterpart of AudioDevice::PlatformDriverName(). + static QString PlatformDriverName(); + +private: + QString m_deviceName; + QString m_codec; + int m_width; + int m_height; + + QAtomicInt m_abort; + + QMutex m_queueMutex; + QWaitCondition m_queueCond; + QQueue<QByteArray> m_frameQueue; + static const int kMaxQueueDepth = 2; }; #endif // VIDEODEVICE_H |
