diff options
| author | Anton <anton@picapica.im> | 2026-08-13 22:58:28 +0400 |
|---|---|---|
| committer | Anton <anton@picapica.im> | 2026-08-13 22:58:28 +0400 |
| commit | 6ef469ba767574384c0373daa5363efb95102ed9 (patch) | |
| tree | 1a24e3812c1d5fd30399cd9751d60484ce40a905 /pica-client | |
| parent | 528e73385831dcd939c7e6b082fb274c27b80145 (diff) | |
| download | pica-pica-6ef469ba767574384c0373daa5363efb95102ed9.tar.gz pica-pica-6ef469ba767574384c0373daa5363efb95102ed9.zip | |
pica-client: add support for compressed video formats provided by camera: hevc, h264, mjpeg
Diffstat (limited to 'pica-client')
| -rw-r--r-- | pica-client/audiovideocallcontroller.cpp | 23 | ||||
| -rw-r--r-- | pica-client/audiovideocallcontroller.h | 1 | ||||
| -rw-r--r-- | pica-client/dialogs/settingsdialog.cpp | 41 | ||||
| -rw-r--r-- | pica-client/dialogs/settingsdialog.h | 2 | ||||
| -rw-r--r-- | pica-client/mediadevice.h | 5 | ||||
| -rw-r--r-- | pica-client/videodevice.cpp | 214 | ||||
| -rw-r--r-- | pica-client/videodevice.h | 30 |
7 files changed, 280 insertions, 36 deletions
diff --git a/pica-client/audiovideocallcontroller.cpp b/pica-client/audiovideocallcontroller.cpp index 0df0a79..54dfa26 100644 --- a/pica-client/audiovideocallcontroller.cpp +++ b/pica-client/audiovideocallcontroller.cpp @@ -74,6 +74,7 @@ AudioVideoCallController::AudioVideoCallController(QObject *parent) // the audio devices above. cam = new VideoDevice(); cam->moveToThread(&cam_thread); + connect(cam, SIGNAL(captureStarted(QString,int,int)), this, SLOT(video_capture_started(QString,int,int))); connect(cam, SIGNAL(packetReady(QByteArray,bool)), this, SLOT(send_video_packet(QByteArray,bool))); cam_thread.start(); @@ -208,11 +209,16 @@ void AudioVideoCallController::startVideoPipeline() m_videoFrameTimestamp = 0; m_videoFrameStarted = false; - skynet->SendVideoParams(m_peer_id, QStringLiteral("h264"), kVideoWidth, kVideoHeight); + bool preferCompressed = st.loadValue("video.prefer_compressed", 0).toBool(); + // The peer is told what we are sending only once the camera is open and + // the format is settled - with a compressed camera stream being forwarded + // as-is, the codec is the camera's choice, not ours (see + // video_capture_started()). QMetaObject::invokeMethod(cam, "configureCapture", Qt::QueuedConnection, - Q_ARG(QString, camDev), Q_ARG(QString, QStringLiteral("h264")), - Q_ARG(int, kVideoWidth), Q_ARG(int, kVideoHeight)); + Q_ARG(QString, camDev), + Q_ARG(int, kVideoWidth), Q_ARG(int, kVideoHeight), + Q_ARG(bool, preferCompressed)); QMetaObject::invokeMethod(cam, "Capture", Qt::QueuedConnection); // remotevideo is configured lazily, once the peer's own 0x75 (see @@ -407,6 +413,17 @@ void AudioVideoCallController::incoming_audio_packet(QByteArray peer_id, quint16 output->enqueuePacket(data); } +void AudioVideoCallController::video_capture_started(QString codec, int width, int height) +{ + if (!is_active) + return; + + // Announced only now, because what the camera turned out to deliver + // decides it: a compressed camera stream is forwarded in the camera's own + // codec and at the camera's own size, rather than in ours. + skynet->SendVideoParams(m_peer_id, codec, (quint16)width, (quint16)height); +} + void AudioVideoCallController::send_video_packet(QByteArray data, bool is_last_fragment) { if (!is_active) diff --git a/pica-client/audiovideocallcontroller.h b/pica-client/audiovideocallcontroller.h index 05a56ed..21a38f4 100644 --- a/pica-client/audiovideocallcontroller.h +++ b/pica-client/audiovideocallcontroller.h @@ -111,6 +111,7 @@ 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 video_capture_started(QString codec, int width, int height); 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); diff --git a/pica-client/dialogs/settingsdialog.cpp b/pica-client/dialogs/settingsdialog.cpp index 61f2ed9..7a7f868 100644 --- a/pica-client/dialogs/settingsdialog.cpp +++ b/pica-client/dialogs/settingsdialog.cpp @@ -47,6 +47,11 @@ static const int kAudioTestPacketMs = 20; // buffer and the sound card's own buffer to drain before closing the device. static const int kAudioTestDrainMs = 500; +// Size requested from the camera by the video pipeline test, matching what a +// call asks for. +static const int kVideoTestWidth = 640; +static const int kVideoTestHeight = 480; + SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { @@ -221,6 +226,8 @@ SettingsDialog::SettingsDialog(QWidget *parent) : videoDevRefresh = new QPushButton(tr("Refresh 🔄"), this); connect(videoDevRefresh, SIGNAL(clicked()), this, SLOT(fillVideoDevices())); + cbPreferCompressed = new QCheckBox(tr("Prefer compressed formats if provided by the camera"), this); + btVideoTest = new QPushButton(tr("Test 📷"), this); connect(btVideoTest, SIGNAL(clicked()), this, SLOT(toggleVideoTest())); @@ -231,6 +238,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) : videodevLayout->addWidget(videoDev); videodevLayout->addWidget(videoDevRefresh); + videodevLayout->addWidget(cbPreferCompressed); videodevLayout->addWidget(btVideoTest); videodevLayout->addWidget(videoPreview); videodevLayout->addStretch(1); @@ -244,6 +252,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) : testCam = new VideoDevice(); testCam->moveToThread(&testCamThread); + connect(testCam, SIGNAL(captureStarted(QString,int,int)), this, SLOT(videoTestCaptureStarted(QString,int,int))); connect(testCam, SIGNAL(packetReady(QByteArray,bool)), this, SLOT(videoTestFragment(QByteArray,bool))); connect(testCam, SIGNAL(errorOccurred(QString)), this, SLOT(videoTestError(QString))); testCamThread.start(); @@ -530,13 +539,28 @@ void SettingsDialog::toggleVideoTest() 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)); + Q_ARG(QString, dev), + Q_ARG(int, kVideoTestWidth), Q_ARG(int, kVideoTestHeight), + Q_ARG(bool, cbPreferCompressed->isChecked())); QMetaObject::invokeMethod(testCam, "Capture", Qt::QueuedConnection); + // The decoder is only started once the camera has settled on a format, + // which is what captureStarted() reports - the same order a call follows, + // where the peer's 0x75 message arrives before its first video packet. +} + +void SettingsDialog::videoTestCaptureStarted(QString codec, int width, int height) +{ + if (!videoTestRunning) + return; + + // Says which path the test is exercising: the camera's own compressed + // stream forwarded untouched, or frames decoded and re-encoded here. + videoPreview->setText(tr("Capturing %1 %2x%3...").arg(codec).arg(width).arg(height)); + QMetaObject::invokeMethod(testDecoder, "configurePlayback", Qt::QueuedConnection, - Q_ARG(QString, QStringLiteral("h264")), - Q_ARG(int, 640), Q_ARG(int, 480)); + Q_ARG(QString, codec), + Q_ARG(int, width), Q_ARG(int, height)); QMetaObject::invokeMethod(testDecoder, "Play", Qt::QueuedConnection); } @@ -601,6 +625,12 @@ void SettingsDialog::fillDevicesComboBox(QComboBox *cb, MediaDevice *dev, enum M QString item = QString(QLatin1String("%2\n(%1)")) .arg(md.at(i).device) .arg(md.at(i).humanReadable); + + // Cameras that can deliver a compressed stream themselves say so, + // which is what the "prefer compressed formats" setting acts on. + if (!md.at(i).compressedFormats.isEmpty()) + item += QString(QLatin1String(" [%1]")).arg(md.at(i).compressedFormats.join(QLatin1String(", "))); + cb->addItem(item, md.at(i).device); } } @@ -744,6 +774,8 @@ void SettingsDialog::loadSettings() if (videoDevItem >= 0) videoDev->setCurrentIndex(videoDevItem); + cbPreferCompressed->setChecked(st.loadValue("video.prefer_compressed", 0).toBool()); + QString audioCapDevVal = st.loadValue("audio.capture_device", "default").toString(); int audioCapDevItem = audioCaptureDev->findData(audioCapDevVal); if (audioCapDevItem >= 0) @@ -797,6 +829,7 @@ void SettingsDialog::storeSettings() st.storeValue("multiple_logins.state", QString::number(mlpstate)); st.storeValue("video.capture_device", videoDev->itemData(videoDev->currentIndex()).toString()); + st.storeValue("video.prefer_compressed", cbPreferCompressed->isChecked() ? "1" : "0"); st.storeValue("audio.capture_device", audioCaptureDev->itemData(audioCaptureDev->currentIndex()).toString()); st.storeValue("audio.playback_device", audioPlaybackDev->itemData(audioPlaybackDev->currentIndex()).toString()); st.storeValue("audio.ring_device", audioRingDev->itemData(audioRingDev->currentIndex()).toString()); diff --git a/pica-client/dialogs/settingsdialog.h b/pica-client/dialogs/settingsdialog.h index 881dd29..a6e6832 100644 --- a/pica-client/dialogs/settingsdialog.h +++ b/pica-client/dialogs/settingsdialog.h @@ -118,6 +118,7 @@ private: // the network, so the whole chain can be checked from this dialog. QPushButton *btVideoTest; QLabel *videoPreview; + QCheckBox *cbPreferCompressed; VideoDevice *testCam; VideoDevice *testDecoder; QThread testCamThread; @@ -157,6 +158,7 @@ private slots: void audioTestError(QString message); void toggleVideoTest(); + void videoTestCaptureStarted(QString codec, int width, int height); void videoTestFragment(QByteArray data, bool is_last_fragment); void videoTestFrame(QImage frame); void videoTestError(QString message); diff --git a/pica-client/mediadevice.h b/pica-client/mediadevice.h index 3099f41..542674b 100644 --- a/pica-client/mediadevice.h +++ b/pica-client/mediadevice.h @@ -2,6 +2,7 @@ #define MEDIADEVICE_H #include <QString> +#include <QStringList> #include <QList> enum MediaDeviceStreamDirection @@ -15,6 +16,10 @@ struct MediaDeviceInfo QString device; QString humanReadable; int index; + // Compressed formats this device can deliver by itself, as FFmpeg codec + // names, most preferred first. Filled in for cameras that offer such a + // stream (see VideoDevice::CompressedFormats), empty otherwise. + QStringList compressedFormats; }; class MediaDevice diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp index 4dd9d6d..adf9c6e 100644 --- a/pica-client/videodevice.cpp +++ b/pica-client/videodevice.cpp @@ -29,6 +29,12 @@ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> + +// Added to the kernel headers later than the formats around it, so define it +// here when building against an older set. +#ifndef V4L2_PIX_FMT_HEVC +#define V4L2_PIX_FMT_HEVC v4l2_fourcc('H', 'E', 'V', 'C') +#endif #endif // FFmpeg headers @@ -65,6 +71,64 @@ static QString ff_errstr(int err) return QString::fromLocal8Bit(buf); } +#ifdef Q_OS_LINUX +// Compressed formats worth forwarding untouched, best first: HEVC gives the +// most picture per byte, H.264 is the widely supported middle ground, and +// MJPEG - every frame a standalone JPEG - costs far more bandwidth but still +// beats decoding and re-encoding. The names are FFmpeg codec names, as used +// by the v4l2 demuxer's input_format option and by the 0x75 protocol message. +static const struct +{ + unsigned int v4l2_pixelformat; + const char *ffmpeg_codec; +} kCompressedFormatPreference[] = +{ + { V4L2_PIX_FMT_HEVC, "hevc" }, + { V4L2_PIX_FMT_H264, "h264" }, + { V4L2_PIX_FMT_MJPEG, "mjpeg" }, + // Some cameras report plain JPEG rather than MJPEG for the same stream. + { V4L2_PIX_FMT_JPEG, "mjpeg" } +}; +#endif + +QStringList VideoDevice::CompressedFormats(const QString &device) +{ + QStringList result; + +#ifdef Q_OS_LINUX + int fd = ::open(device.toLatin1().constData(), O_RDWR | O_NONBLOCK); + if (fd < 0) + return result; + + // Collect what the camera offers first, then report it in preference + // order rather than in the order the camera happens to list it. + QList<unsigned int> offered; + struct v4l2_fmtdesc fmt; + + memset(&fmt, 0, sizeof fmt); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + for (fmt.index = 0; ::ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0; fmt.index++) + { + if (fmt.flags & V4L2_FMT_FLAG_COMPRESSED) + offered << fmt.pixelformat; + } + ::close(fd); + + for (unsigned int i = 0; i < sizeof(kCompressedFormatPreference) / sizeof(kCompressedFormatPreference[0]); i++) + { + QString codec = QLatin1String(kCompressedFormatPreference[i].ffmpeg_codec); + + if (offered.contains(kCompressedFormatPreference[i].v4l2_pixelformat) && !result.contains(codec)) + result << codec; + } +#else + Q_UNUSED(device); +#endif + + return result; +} + VideoFrameAssembler::VideoFrameAssembler() : m_timestamp(0), m_inProgress(false) { @@ -98,7 +162,7 @@ QByteArray VideoFrameAssembler::addFragment(quint16 seq_num, quint32 timestamp, } VideoDevice::VideoDevice(QObject *parent) - : QObject(parent), m_width(640), m_height(480), m_abort(0) + : QObject(parent), m_width(640), m_height(480), m_preferCompressed(false), m_abort(0) { } @@ -107,12 +171,12 @@ VideoDevice::~VideoDevice() Close(); } -void VideoDevice::configureCapture(QString deviceName, QString codec, int width, int height) +void VideoDevice::configureCapture(QString deviceName, int width, int height, bool preferCompressed) { m_deviceName = deviceName; - m_codec = codec; m_width = width; m_height = height; + m_preferCompressed = preferCompressed; } void VideoDevice::configurePlayback(QString codec, int width, int height) @@ -165,13 +229,46 @@ void VideoDevice::Capture() return; } + // Ask the camera for a compressed stream when told to and it offers one: + // its packets can then go straight onto the network, with no decoding and + // re-encoding in between. The formats are queried rather than guessed, so + // this asks for one the camera has already said it can deliver. + QString passthroughCodec; + + if (m_preferCompressed) + { + QStringList formats = CompressedFormats(m_deviceName); + + if (!formats.isEmpty()) + passthroughCodec = formats.first(); + } + 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); + if (!passthroughCodec.isEmpty()) + av_dict_set(&opts, "input_format", passthroughCodec.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 && !passthroughCodec.isEmpty()) + { + // The camera listed the format but will not actually hand it over at + // this size or frame rate. Fall back to whatever it does give. + qWarning() << QString("Camera '%1' would not deliver %2 (%3), falling back to re-encoding") + .arg(m_deviceName, passthroughCodec, ff_errstr(ret)); + passthroughCodec.clear(); + + 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); + + 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. @@ -192,10 +289,72 @@ void VideoDevice::Capture() 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. + if (!passthroughCodec.isEmpty()) + runPassthroughLoop(ifmt_ctx, in_st, passthroughCodec); + else + runTranscodeLoop(ifmt_ctx, in_st); + + avformat_close_input(&ifmt_ctx); +} + +void VideoDevice::emitFragments(const unsigned char *data, int size) +{ + // 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 < size) + { + int chunk = qMin(kMaxFragmentSize, size - offset); + bool isLast = (offset + chunk >= size); + + emit packetReady(QByteArray((const char *)data + offset, chunk), isLast); + offset += chunk; + } +} + +void VideoDevice::runPassthroughLoop(AVFormatContext *ifmt_ctx, AVStream *in_st, const QString &codec) +{ + // The camera decides the picture size here, since nothing rescales it on + // the way out - announce what it actually produced. + int width = in_st->codecpar->width > 0 ? in_st->codecpar->width : m_width; + int height = in_st->codecpar->height > 0 ? in_st->codecpar->height : m_height; + + emit captureStarted(codec, width, height); + + AVPacket *pkt = av_packet_alloc(); + bool loggedReadError = false; + + while (!m_abort.loadRelaxed()) + { + int ret = av_read_frame(ifmt_ctx, pkt); + if (ret < 0) + { + if (ret == AVERROR(EAGAIN)) + continue; + if (!loggedReadError) + { + qWarning() << QString("Camera read failed: %1").arg(ff_errstr(ret)); + loggedReadError = true; + } + break; + } + + emitFragments(pkt->data, pkt->size); + av_packet_unref(pkt); + } + + av_packet_free(&pkt); +} + +void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st) +{ + int ret; + + // The camera hands us either raw frames or a compressed stream that is + // not being forwarded as-is; either way it goes through a decoder here to + // get raw pictures for the encoder. 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 || @@ -206,7 +365,6 @@ void VideoDevice::Capture() qWarning() << msg; emit errorOccurred(msg); if (dec_ctx) avcodec_free_context(&dec_ctx); - avformat_close_input(&ifmt_ctx); return; } @@ -220,7 +378,6 @@ void VideoDevice::Capture() qWarning() << msg; emit errorOccurred(msg); avcodec_free_context(&dec_ctx); - avformat_close_input(&ifmt_ctx); return; } @@ -247,10 +404,18 @@ void VideoDevice::Capture() emit errorOccurred(msg); avcodec_free_context(&enc_ctx); avcodec_free_context(&dec_ctx); - avformat_close_input(&ifmt_ctx); return; } + // Everything the camera sends is scaled to the encoder's size and re-encoded, + // so this is what the peer will receive regardless of what the camera + // produced. The codec name is taken from the encoder rather than written + // out literally, so that changing the encoder above cannot leave the peer + // being told something else - avcodec_get_name() yields exactly the + // FFmpeg codec names the 0x75 message is defined in terms of. + emit captureStarted(QLatin1String(avcodec_get_name(enc_ctx->codec_id)), + enc_ctx->width, enc_ctx->height); + AVFrame *enc_frame = av_frame_alloc(); if (enc_frame) { @@ -266,7 +431,6 @@ void VideoDevice::Capture() if (enc_frame) av_frame_free(&enc_frame); avcodec_free_context(&enc_ctx); avcodec_free_context(&dec_ctx); - avformat_close_input(&ifmt_ctx); return; } @@ -345,18 +509,7 @@ void VideoDevice::Capture() 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; - } + emitFragments(out_pkt->data, out_pkt->size); av_packet_unref(out_pkt); } } @@ -369,7 +522,6 @@ void VideoDevice::Capture() if (sws) sws_freeContext(sws); avcodec_free_context(&enc_ctx); avcodec_free_context(&dec_ctx); - avformat_close_input(&ifmt_ctx); } void VideoDevice::Play() @@ -380,11 +532,16 @@ void VideoDevice::Play() 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); + // The peer announces its codec by FFmpeg name in the 0x75 message, which + // is exactly what avcodec_find_decoder_by_name() takes - so whatever the + // other side ends up sending (its own camera's hevc/h264/mjpeg stream + // forwarded untouched, or h264 it encoded itself) is handled here without + // a codec list to keep in step. + const AVCodec *dec = avcodec_find_decoder_by_name(m_codec.toLatin1().constData()); + if (dec && dec->type != AVMEDIA_TYPE_VIDEO) + dec = nullptr; AVCodecContext *dec_ctx = dec ? avcodec_alloc_context3(dec) : nullptr; - if (m_codec != QLatin1String("h264") || !dec || !dec_ctx || avcodec_open2(dec_ctx, dec, nullptr) < 0) + if (!dec || !dec_ctx || avcodec_open2(dec_ctx, dec, nullptr) < 0) { QString msg = QString("Unsupported call video codec: %1").arg(m_codec); qWarning() << msg; @@ -538,6 +695,7 @@ QList<MediaDeviceInfo> VideoDevice::Enumerate(enum MediaDeviceStreamDirection di if (!(cap.device_caps & V4L2_CAP_VIDEO_CAPTURE)) continue; d.index = index++; + d.compressedFormats = CompressedFormats(d.device); result << d; } diff --git a/pica-client/videodevice.h b/pica-client/videodevice.h index 9ee4397..1b999c1 100644 --- a/pica-client/videodevice.h +++ b/pica-client/videodevice.h @@ -74,7 +74,12 @@ public: VideoDevice(QObject *parent = nullptr); ~VideoDevice(); - Q_INVOKABLE void configureCapture(QString deviceName, QString codec, int width, int height); + // preferCompressed asks for the camera's own compressed stream to be + // forwarded untouched when it offers one, saving the cost of decoding and + // re-encoding every frame. The codec that ends up being used is only + // known once the camera has been opened, and is reported by + // captureStarted() - it is not necessarily the one requested here. + Q_INVOKABLE void configureCapture(QString deviceName, int width, int height, bool preferCompressed); Q_INVOKABLE void configurePlayback(QString codec, int width, int height); // Push one complete encoded frame (already reassembled from 0x77 @@ -100,6 +105,12 @@ public slots: virtual void Close() override; signals: + // Emitted by Capture() once the camera is open and the format actually + // being sent is known - either the camera's own compressed format, when + // forwarding it untouched, or the one this class encodes to. Carries the + // values to announce to the peer, before any packet is emitted. + void captureStarted(QString codec, int width, int height); + // 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. @@ -117,11 +128,19 @@ public: // video capture API. Counterpart of AudioDevice::PlatformDriverName(). static QString PlatformDriverName(); + // Compressed formats the given camera can deliver by itself, as FFmpeg + // codec names, ordered by preference: the more efficient the codec, the + // earlier it comes. Empty if the camera offers none, or on platforms + // where the formats cannot be queried - in which case captured frames + // are decoded and re-encoded as usual. + static QStringList CompressedFormats(const QString &device); + private: QString m_deviceName; QString m_codec; int m_width; int m_height; + bool m_preferCompressed; QAtomicInt m_abort; @@ -129,6 +148,15 @@ private: QWaitCondition m_queueCond; QQueue<QByteArray> m_frameQueue; static const int kMaxQueueDepth = 2; + + // Sends one encoded frame out as one or more protocol sized fragments. + void emitFragments(const unsigned char *data, int size); + + // The two shapes Capture() can take: forwarding the camera's own + // compressed stream as it arrives, or decoding what the camera sends and + // re-encoding it. Both emit captureStarted() before their first packet. + void runPassthroughLoop(struct AVFormatContext *ifmt_ctx, struct AVStream *in_st, const QString &codec); + void runTranscodeLoop(struct AVFormatContext *ifmt_ctx, struct AVStream *in_st); }; #endif // VIDEODEVICE_H |
