summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pica-client/audiovideocallcontroller.cpp23
-rw-r--r--pica-client/audiovideocallcontroller.h1
-rw-r--r--pica-client/dialogs/settingsdialog.cpp41
-rw-r--r--pica-client/dialogs/settingsdialog.h2
-rw-r--r--pica-client/mediadevice.h5
-rw-r--r--pica-client/videodevice.cpp214
-rw-r--r--pica-client/videodevice.h30
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