summaryrefslogtreecommitdiffstats
path: root/pica-client/videodevice.cpp
diff options
context:
space:
mode:
authorAnton <anton@picapica.im>2026-09-26 19:00:40 +0400
committerAnton <anton@picapica.im>2026-09-26 19:00:40 +0400
commit76225a538e1e960eda9e31a34846f4bc62adef87 (patch)
treee1a62b1c2c55b5c6c1e319d3c6726da4f4f96f7f /pica-client/videodevice.cpp
parentbc708f0cc6c01999a8e111d1c66c899106e91bc7 (diff)
downloadpica-pica-76225a538e1e960eda9e31a34846f4bc62adef87.tar.gz
pica-pica-76225a538e1e960eda9e31a34846f4bc62adef87.zip
pica-client: configurable resolutions, codecs, bitrates for a call
Diffstat (limited to 'pica-client/videodevice.cpp')
-rw-r--r--pica-client/videodevice.cpp467
1 files changed, 421 insertions, 46 deletions
diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp
index 5b7d5fb..3fb468c 100644
--- a/pica-client/videodevice.cpp
+++ b/pica-client/videodevice.cpp
@@ -15,13 +15,18 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "videodevice.h"
+#include "callsettings.h"
#include "../PICA_proto.h"
#include "../PICA_media.h"
#include <QFile>
#include <QDebug>
#include <QMutexLocker>
+#include <QMap>
+#include <QPair>
#include <cstring>
+#include <algorithm>
+#include <functional>
#ifdef Q_OS_LINUX
#include <linux/videodev2.h>
@@ -54,11 +59,6 @@ extern "C" {
#endif
}
-// 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;
-
// Largest amount of encoded data one 0x77 message can carry over a c2c or
// directc2c connection: the protocol's per-message payload cap, minus the
// sequence number + timestamp header that every call media packet carries.
@@ -106,6 +106,131 @@ static const struct
};
#endif
+// Which FFmpeg encoder produces each of the codecs the "Video Codec" setting
+// offers, on the GPU and in software. Two names per codec because FFmpeg names
+// its external library encoders after the library rather than after the codec,
+// and the id as well so that a build without the library named here can still
+// fall back to whatever other encoder it has for the same codec.
+static const struct
+{
+ const char *codec;
+ AVCodecID id;
+ const char *vaapiEncoder;
+ const char *swEncoder;
+} kVideoEncoders[] =
+{
+ { "h264", AV_CODEC_ID_H264, "h264_vaapi", "libx264" },
+ { "hevc", AV_CODEC_ID_HEVC, "hevc_vaapi", "libx265" },
+ { "vp9", AV_CODEC_ID_VP9, "vp9_vaapi", "libvpx-vp9" }
+};
+
+static int videoEncoderIndex(const QString &codec)
+{
+ for (unsigned int i = 0; i < sizeof(kVideoEncoders) / sizeof(kVideoEncoders[0]); i++)
+ {
+ if (codec == QLatin1String(kVideoEncoders[i].codec))
+ return (int)i;
+ }
+
+ // An unrecognised setting - written by a build offering something this one
+ // does not, or by hand - falls back to the codec every peer can decode
+ // rather than leaving the call without video.
+ return 0;
+}
+
+// Sets the private options that make an encoder suitable for a live call:
+// low latency, no frame reordering, and parameter sets the receiver can pick
+// up mid-stream. Each of the three encoders spells all of that differently,
+// which is why this is keyed on the encoder rather than on the codec.
+static void applyLiveEncoderOptions(AVCodecContext *enc_ctx, const QString &encoderName, int sliceMaxSize)
+{
+ if (encoderName == QLatin1String("libx264"))
+ {
+ av_opt_set(enc_ctx->priv_data, "preset", "veryfast", 0);
+ av_opt_set(enc_ctx->priv_data, "tune", "zerolatency", 0);
+
+ // Keeps every slice within one datagram of a mediac2c connection, so
+ // that a lost fragment costs a slice of the picture rather than the
+ // whole frame. Harmless when the media goes over TCP instead - it
+ // only splits frames into more NAL units than strictly necessary.
+ av_opt_set(enc_ctx->priv_data, "x264opts",
+ QString("slice-max-size=%1").arg(sliceMaxSize).toUtf8().constData(), 0);
+ }
+ else if (encoderName == QLatin1String("libx265"))
+ {
+ av_opt_set(enc_ctx->priv_data, "preset", "ultrafast", 0);
+ av_opt_set(enc_ctx->priv_data, "tune", "zerolatency", 0);
+
+ // repeat-headers is the x265 equivalent of leaving
+ // AV_CODEC_FLAG_GLOBAL_HEADER off below: without it the VPS/SPS/PPS go
+ // out once, at the start, and a receiver that joins the stream later -
+ // which every receiver does, the call being already running by the
+ // time its decoder opens - never sees them.
+ //
+ // x265 has no slice-max-size; the nearest thing is a fixed number of
+ // slices per frame, which does not bound their size. Fragmentation
+ // handles oversized frames either way, a loss just costs more of the
+ // picture than it does with x264.
+ av_opt_set(enc_ctx->priv_data, "x265-params", "repeat-headers=1:bframes=0", 0);
+ }
+ else if (encoderName == QLatin1String("libvpx-vp9"))
+ {
+ // "realtime" with a high cpu-used is libvpx's zerolatency: it caps how
+ // long the encoder may spend per frame rather than letting it take as
+ // long as the quality target needs.
+ av_opt_set(enc_ctx->priv_data, "deadline", "realtime", 0);
+ av_opt_set_int(enc_ctx->priv_data, "cpu-used", 8, 0);
+ // No lookahead - it would hold frames back, which is latency in a
+ // live call - and keep the bitstream decodable across a loss.
+ av_opt_set_int(enc_ctx->priv_data, "lag-in-frames", 0, 0);
+ av_opt_set_int(enc_ctx->priv_data, "error-resilient", 1, 0);
+
+ // VP9 carries no parameter sets to repeat: everything a decoder needs
+ // is in the keyframe itself, so joining mid-stream needs nothing
+ // special here.
+ }
+}
+
+#ifdef Q_OS_WIN
+// Defined in videodshow.cpp, alongside the enumeration functions - see the
+// comment on pica_enumerate_dshow_video() for why the answers come from
+// DirectShow rather than from FFmpeg's demuxer.
+QList<VideoCaptureFormat> pica_dshow_capture_formats(const QString &device);
+#endif
+
+#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
+// Frame rates worth offering out of a camera that reports a continuous range
+// rather than a list of what it supports. Such a camera will take anything in
+// between, so this is only a question of what to put in a dropdown.
+static const int kStandardFrameRates[] = { 60, 50, 30, 25, 24, 20, 15, 10, 5 };
+
+// Sizes offered for the same reason, out of a camera that reports a range of
+// sizes rather than a list. Ones outside the range, or that the reported step
+// does not land on, are dropped by the caller.
+static const struct
+{
+ int width;
+ int height;
+} kStandardSizes[] =
+{
+ { 1920, 1080 }, { 1280, 720 }, { 1024, 768 }, { 800, 600 },
+ { 848, 480 }, { 640, 480 }, { 640, 360 }, { 424, 240 },
+ { 352, 288 }, { 320, 240 }, { 176, 144 }, { 160, 120 }
+};
+
+// Largest picture first, and highest frame rate first within each.
+static bool captureFormatLessThan(const VideoCaptureFormat &a, const VideoCaptureFormat &b)
+{
+ const qint64 areaA = (qint64)a.width * a.height;
+ const qint64 areaB = (qint64)b.width * b.height;
+
+ if (areaA != areaB)
+ return areaA > areaB;
+
+ return a.width > b.width;
+}
+#endif
+
#ifdef HAVE_VAAPI
// Picks the GPU pixel format out of what the decoder offers. Returning the
// first entry instead - which is what happens when VAAPI is not among them -
@@ -166,10 +291,211 @@ QStringList VideoDevice::CompressedFormats(const QString &device)
return result;
}
+#ifdef Q_OS_LINUX
+// Every picture size the camera offers in one pixel format.
+//
+// A camera answers this in one of two ways: a list of the sizes it has, or a
+// range it will scale anything within. There is no list to show for the
+// second, so the standard sizes that fall inside the range stand in for one.
+static QList<QPair<int, int> > v4l2FrameSizes(int fd, unsigned int pixelformat)
+{
+ QList<QPair<int, int> > sizes;
+ struct v4l2_frmsizeenum fs;
+
+ memset(&fs, 0, sizeof fs);
+ fs.pixel_format = pixelformat;
+
+ for (fs.index = 0; ::ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fs) == 0; fs.index++)
+ {
+ if (fs.type == V4L2_FRMSIZE_TYPE_DISCRETE)
+ {
+ sizes << qMakePair((int)fs.discrete.width, (int)fs.discrete.height);
+ continue;
+ }
+
+ for (unsigned int i = 0; i < sizeof(kStandardSizes) / sizeof(kStandardSizes[0]); i++)
+ {
+ const unsigned int w = kStandardSizes[i].width;
+ const unsigned int h = kStandardSizes[i].height;
+
+ if (w < fs.stepwise.min_width || w > fs.stepwise.max_width ||
+ h < fs.stepwise.min_height || h > fs.stepwise.max_height)
+ continue;
+
+ // A continuous camera reports a step of one, so this only ever
+ // excludes anything on a genuinely stepwise one.
+ if (fs.stepwise.step_width > 1 && (w - fs.stepwise.min_width) % fs.stepwise.step_width)
+ continue;
+
+ if (fs.stepwise.step_height > 1 && (h - fs.stepwise.min_height) % fs.stepwise.step_height)
+ continue;
+
+ sizes << qMakePair((int)w, (int)h);
+ }
+
+ // A non-discrete answer is a single entry describing the whole range;
+ // there is no index 1 to ask for.
+ break;
+ }
+
+ return sizes;
+}
+
+// v4l2 reports frame intervals - seconds per frame - and we want the rate, so
+// every one of these is a reciprocal.
+static double v4l2IntervalToFps(const struct v4l2_fract &f)
+{
+ if (f.numerator == 0 || f.denominator == 0)
+ return 0.0;
+
+ return (double)f.denominator / (double)f.numerator;
+}
+
+// The frame rates the camera offers for one pixel format at one size, highest
+// first. Same two shapes of answer as the sizes above, handled the same way.
+static QList<int> v4l2FrameRates(int fd, unsigned int pixelformat, int width, int height)
+{
+ QList<int> rates;
+ struct v4l2_frmivalenum fi;
+
+ memset(&fi, 0, sizeof fi);
+ fi.pixel_format = pixelformat;
+ fi.width = width;
+ fi.height = height;
+
+ for (fi.index = 0; ::ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fi) == 0; fi.index++)
+ {
+ if (fi.type == V4L2_FRMIVAL_TYPE_DISCRETE)
+ {
+ int fps = qRound(v4l2IntervalToFps(fi.discrete));
+
+ if (fps > 0 && !rates.contains(fps))
+ rates << fps;
+
+ continue;
+ }
+
+ // The longest interval is the lowest rate, hence the crossed over
+ // names here.
+ const double minFps = v4l2IntervalToFps(fi.stepwise.max);
+ const double maxFps = v4l2IntervalToFps(fi.stepwise.min);
+
+ for (unsigned int i = 0; i < sizeof(kStandardFrameRates) / sizeof(kStandardFrameRates[0]); i++)
+ {
+ const int fps = kStandardFrameRates[i];
+
+ if (fps >= minFps && fps <= maxFps && !rates.contains(fps))
+ rates << fps;
+ }
+
+ break;
+ }
+
+ std::sort(rates.begin(), rates.end(), std::greater<int>());
+
+ return rates;
+}
+#endif
+
+QList<VideoCaptureFormat> VideoDevice::CaptureFormats(const QString &device)
+{
+ QList<VideoCaptureFormat> result;
+
+#ifdef Q_OS_LINUX
+ int fd = ::open(device.toLatin1().constData(), O_RDWR | O_NONBLOCK);
+ if (fd < 0)
+ return result;
+
+ // Keyed on the size, because a camera reports its sizes once per pixel
+ // format and the same size usually comes back several times over. What
+ // differs between those is the format and the frame rates available in
+ // it, so both are merged into the one entry for the size.
+ QMap<QPair<int, int>, VideoCaptureFormat> bySize;
+
+ 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++)
+ {
+ // What this pixel format is called in a 0x75 message, empty for an
+ // uncompressed one and for a compressed one we would not forward.
+ QString codec;
+
+ for (unsigned int i = 0; i < sizeof(kCompressedFormatPreference) / sizeof(kCompressedFormatPreference[0]); i++)
+ {
+ if (kCompressedFormatPreference[i].v4l2_pixelformat == fmt.pixelformat)
+ {
+ codec = QLatin1String(kCompressedFormatPreference[i].ffmpeg_codec);
+ break;
+ }
+ }
+
+ QList<QPair<int, int> > sizes = v4l2FrameSizes(fd, fmt.pixelformat);
+
+ for (int s = 0; s < sizes.size(); s++)
+ {
+ VideoCaptureFormat &entry = bySize[sizes.at(s)];
+
+ entry.width = sizes.at(s).first;
+ entry.height = sizes.at(s).second;
+
+ if (!codec.isEmpty() && !entry.compressedFormats.contains(codec))
+ entry.compressedFormats << codec;
+
+ QList<int> rates = v4l2FrameRates(fd, fmt.pixelformat, entry.width, entry.height);
+
+ for (int r = 0; r < rates.size(); r++)
+ if (!entry.frameRates.contains(rates.at(r)))
+ entry.frameRates << rates.at(r);
+ }
+ }
+
+ ::close(fd);
+
+ for (QMap<QPair<int, int>, VideoCaptureFormat>::iterator it = bySize.begin(); it != bySize.end(); ++it)
+ {
+ VideoCaptureFormat entry = it.value();
+
+ // The formats were collected in whatever order the camera listed them
+ // in; report them in preference order, the same one
+ // CompressedFormats() and VideoDevice::Capture() work in.
+ QStringList ordered;
+
+ for (unsigned int i = 0; i < sizeof(kCompressedFormatPreference) / sizeof(kCompressedFormatPreference[0]); i++)
+ {
+ QString codec = QLatin1String(kCompressedFormatPreference[i].ffmpeg_codec);
+
+ if (entry.compressedFormats.contains(codec) && !ordered.contains(codec))
+ ordered << codec;
+ }
+
+ entry.compressedFormats = ordered;
+
+ std::sort(entry.frameRates.begin(), entry.frameRates.end(), std::greater<int>());
+
+ result << entry;
+ }
+
+ std::sort(result.begin(), result.end(), captureFormatLessThan);
+#elif defined(Q_OS_WIN)
+ result = pica_dshow_capture_formats(device);
+#else
+ Q_UNUSED(device);
+#endif
+
+ return result;
+}
+
VideoFrameAssembler::VideoFrameAssembler()
- : m_timestamp(0), m_inProgress(false), m_lastSeq(0),
- m_lastWasFrameEnd(false), m_haveLastSeq(false)
+ : m_timestamp(0), m_lastSeq(0)
{
+ // Through reset() rather than an initialiser list of its own: a fresh
+ // assembler and one that has been reset are the same thing, and stating
+ // that start-of-stream state twice is how the two came to disagree - the
+ // constructor's copy is what the first call after launch actually uses.
+ reset();
}
void VideoFrameAssembler::reset()
@@ -177,9 +503,24 @@ void VideoFrameAssembler::reset()
m_buffer.clear();
m_inProgress = false;
m_haveLastSeq = false;
- // The first fragment received is as likely to be the middle of a frame as
- // its beginning, so assembly only starts after the first frame boundary.
- m_lastWasFrameEnd = false;
+
+ // The next fragment is taken to begin a frame, rather than to be the
+ // middle of one whose start was lost.
+ //
+ // Every caller resets a stream that is about to start, never one being
+ // joined in flight - a call resets before the first packet can arrive, and
+ // the switch to a mediac2c connection deliberately does not reset, since
+ // media keeps arriving over either transport for the whole call. So the
+ // first fragment after this really is the first one the sender sent.
+ //
+ // Assuming the opposite costs the stream's first frame, and that frame is
+ // the one that matters: with a camera's own compressed stream being
+ // forwarded untouched, it carries the parameter sets and the only keyframe
+ // the camera will produce for the next ten seconds, so losing it means ten
+ // seconds of black. If this assumption is ever wrong the cost is one
+ // truncated frame that the decoder rejects, and the next frame boundary
+ // resynchronizes as usual.
+ m_lastWasFrameEnd = true;
}
QByteArray VideoFrameAssembler::addFragment(quint16 seq_num, quint32 timestamp, const QByteArray &data)
@@ -242,9 +583,11 @@ QByteArray VideoFrameAssembler::addFragment(quint16 seq_num, quint32 timestamp,
}
VideoDevice::VideoDevice(QObject *parent)
- : QObject(parent), m_width(640), m_height(480), m_preferCompressed(false),
+ : QObject(parent), m_width(kDefaultCaptureWidth), m_height(kDefaultCaptureHeight),
+ m_frameRate(kDefaultCaptureFrameRate), m_bitrate(kDefaultVideoBitrateKbps * 1000),
+ m_preferCompressed(false),
m_useVaapi(false), m_useVaapiRender(false), m_abort(0),
- m_maxFragmentSize(kMaxFragmentSize)
+ m_maxFragmentSize(kMaxFragmentSize), m_decoderStarted(0)
{
}
@@ -261,12 +604,16 @@ VideoDevice::~VideoDevice()
Close();
}
-void VideoDevice::configureCapture(QString deviceName, int width, int height,
- bool preferCompressed, bool useVaapi)
+void VideoDevice::configureCapture(QString deviceName, int width, int height, int frameRate,
+ bool preferCompressed, QString codec, int bitrate,
+ bool useVaapi)
{
m_deviceName = deviceName;
m_width = width;
m_height = height;
+ m_frameRate = frameRate > 0 ? frameRate : kDefaultCaptureFrameRate;
+ m_codec = codec;
+ m_bitrate = bitrate > 0 ? bitrate : kDefaultVideoBitrateKbps * 1000;
m_preferCompressed = preferCompressed;
m_useVaapi = useVaapi;
}
@@ -284,6 +631,9 @@ void VideoDevice::configurePlayback(QString codec, int width, int height,
// A renderer that failed during an earlier call says nothing about this
// one - it may well be a different window.
m_hwRenderDisabled.storeRelaxed(0);
+ // A fresh decode session, so the startup queue depth applies again until
+ // this one's loop is consuming - see enqueueFrame().
+ m_decoderStarted.storeRelaxed(0);
}
void VideoDevice::disableHardwareRendering()
@@ -295,7 +645,10 @@ void VideoDevice::enqueueFrame(QByteArray encodedFrame)
{
QMutexLocker locker(&m_queueMutex);
- while (m_frameQueue.size() >= kMaxQueueDepth)
+ // Deeper while the decoder is still starting up - see kStartupQueueDepth.
+ const int depth = m_decoderStarted.loadRelaxed() ? kMaxQueueDepth : kStartupQueueDepth;
+
+ while (m_frameQueue.size() >= depth)
m_frameQueue.dequeue();
m_frameQueue.enqueue(encodedFrame);
@@ -414,8 +767,18 @@ void VideoDevice::Capture()
QStringList attempts;
if (m_preferCompressed)
+ {
attempts = CompressedFormats(m_deviceName);
+ // The configured codec first when the camera can produce it itself:
+ // forwarding that one means the peer receives exactly what was asked
+ // for, rather than whichever compressed format the camera happened to
+ // rank highest. The rest keep their preference order behind it, since
+ // forwarding any of them still beats decoding and re-encoding.
+ if (attempts.removeAll(m_codec) > 0)
+ attempts.prepend(m_codec);
+ }
+
attempts << QString();
// Listing a format and being able to produce it at a given size and frame
@@ -452,7 +815,7 @@ void VideoDevice::Capture()
ret = openCamera(ifmt, deviceUtf8,
kConstraints[c].size ? m_width : 0,
kConstraints[c].size ? m_height : 0,
- kConstraints[c].rate ? kFrameRate : 0,
+ kConstraints[c].rate ? m_frameRate : 0,
attempts.at(a), &ifmt_ctx);
if (ret >= 0)
@@ -467,7 +830,7 @@ void VideoDevice::Capture()
if (!kConstraints[c].size)
asked = QStringLiteral("any size or rate");
else if (kConstraints[c].rate)
- asked = QString("%1x%2 @%3").arg(m_width).arg(m_height).arg(kFrameRate);
+ asked = QString("%1x%2 @%3").arg(m_width).arg(m_height).arg(m_frameRate);
else
asked = QString("%1x%2 at any rate").arg(m_width).arg(m_height);
@@ -604,7 +967,7 @@ void VideoDevice::runPassthroughLoop(AVFormatContext *ifmt_ctx, AVStream *in_st,
// one that was asked for - and when the open had to fall back to letting the
// camera choose, was not asked for at all. Encoding at a rate the frames do
// not arrive at gives a stream whose timestamps disagree with reality.
-static AVRational cameraFrameRate(AVStream *in_st)
+static AVRational cameraFrameRate(AVStream *in_st, int fallbackRate)
{
AVRational fr = in_st->avg_frame_rate;
@@ -612,14 +975,19 @@ static AVRational cameraFrameRate(AVStream *in_st)
fr = in_st->r_frame_rate;
if (fr.num <= 0 || fr.den <= 0)
- fr = AVRational{ kFrameRate, 1 };
+ fr = AVRational{ fallbackRate, 1 };
return fr;
}
void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
{
- const AVRational frameRate = cameraFrameRate(in_st);
+ const AVRational frameRate = cameraFrameRate(in_st, m_frameRate);
+
+ // Which encoder produces the configured codec. Looked up once: the GPU
+ // attempt and the software fallback below are two encoders for the same
+ // codec, so the peer is told the same thing either way.
+ const int encIdx = videoEncoderIndex(m_codec);
// Two seconds between keyframes. A receiver that joins late or loses a
// packet waits this long for a picture, so it is a latency figure as much
@@ -657,7 +1025,7 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
#ifdef HAVE_VAAPI
if (m_useVaapi && VaapiContext::isAvailable())
{
- enc = avcodec_find_encoder_by_name("h264_vaapi");
+ enc = avcodec_find_encoder_by_name(kVideoEncoders[encIdx].vaapiEncoder);
AVBufferRef *hw_device_ref = enc ? VaapiContext::deviceRef() : nullptr;
if (hw_device_ref)
@@ -684,18 +1052,18 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
enc_ctx->pix_fmt = AV_PIX_FMT_VAAPI;
enc_ctx->time_base = av_inv_q(frameRate);
enc_ctx->framerate = frameRate;
- enc_ctx->bit_rate = kBitrate;
+ enc_ctx->bit_rate = m_bitrate;
enc_ctx->gop_size = gopSize;
enc_ctx->max_b_frames = 0;
enc_ctx->hw_frames_ctx = av_buffer_ref(hw_frames_ref);
- // No preset/tune here: those are x264's own options and
- // mean nothing to a VAAPI encoder.
+ // No preset/tune here: those belong to the software
+ // encoders and mean nothing to a VAAPI one.
if ((ret = avcodec_open2(enc_ctx, enc, nullptr)) >= 0)
hardware = true;
else
- qWarning() << QString("VAAPI H.264 encoder would not open (%1), encoding in software")
- .arg(ff_errstr(ret));
+ qWarning() << QString("The VAAPI %1 encoder would not open (%2), encoding in software")
+ .arg(QLatin1String(kVideoEncoders[encIdx].codec), ff_errstr(ret));
}
else
{
@@ -711,7 +1079,8 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
else
{
- qWarning() << "FFmpeg has no h264_vaapi encoder, encoding in software";
+ qWarning() << QString("FFmpeg has no %1 encoder, encoding in software")
+ .arg(QLatin1String(kVideoEncoders[encIdx].vaapiEncoder));
}
if (!hardware)
@@ -723,27 +1092,35 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
#endif
+ // Named rather than taken from enc->name because the VAAPI branch above
+ // may have left a different encoder in enc; filled in below for the
+ // software path, which is the only one the private options apply to.
+ QString swEncoderName;
+
if (!hardware)
{
- enc = avcodec_find_encoder_by_name("libx264");
+ enc = avcodec_find_encoder_by_name(kVideoEncoders[encIdx].swEncoder);
if (!enc)
- enc = avcodec_find_encoder(AV_CODEC_ID_H264);
+ enc = avcodec_find_encoder(kVideoEncoders[encIdx].id);
enc_ctx = enc ? avcodec_alloc_context3(enc) : nullptr;
if (!enc || !enc_ctx)
{
- QString msg = "H.264 encoder not available";
+ QString msg = QString("No %1 encoder available").arg(QLatin1String(kVideoEncoders[encIdx].codec));
qWarning() << msg;
emit errorOccurred(msg);
+ if (enc_ctx) avcodec_free_context(&enc_ctx);
avcodec_free_context(&dec_ctx);
return;
}
+ swEncoderName = QLatin1String(enc->name);
+
enc_ctx->width = m_width;
enc_ctx->height = m_height;
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->time_base = av_inv_q(frameRate);
enc_ctx->framerate = frameRate;
- enc_ctx->bit_rate = kBitrate;
+ enc_ctx->bit_rate = m_bitrate;
enc_ctx->gop_size = gopSize;
// B-frames reorder output, which would cost latency in a live call.
enc_ctx->max_b_frames = 0;
@@ -751,19 +1128,11 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
// 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);
-
- // Keeps every slice within one datagram of a mediac2c connection, so
- // that a lost fragment costs a slice of the picture rather than the
- // whole frame. Harmless when the media goes over TCP instead - it
- // only splits frames into more NAL units than strictly necessary.
- av_opt_set(enc_ctx->priv_data, "x264opts",
- QString("slice-max-size=%1").arg(kSliceMaxSize).toUtf8().constData(), 0);
+ applyLiveEncoderOptions(enc_ctx, swEncoderName, kSliceMaxSize);
if ((ret = avcodec_open2(enc_ctx, enc, nullptr)) < 0)
{
- QString msg = QString("Could not open H.264 encoder: %1").arg(ff_errstr(ret));
+ QString msg = QString("Could not open the %1 encoder: %2").arg(swEncoderName, ff_errstr(ret));
qWarning() << msg;
emit errorOccurred(msg);
avcodec_free_context(&enc_ctx);
@@ -772,8 +1141,9 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
}
- emit accelerationInUse(hardware ? QStringLiteral("GPU encoding (VAAPI)")
- : QStringLiteral("CPU encoding (libx264)"));
+ emit accelerationInUse(hardware
+ ? QString("GPU encoding (VAAPI %1)").arg(QLatin1String(kVideoEncoders[encIdx].codec))
+ : QString("CPU encoding (%1)").arg(swEncoderName));
// 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
@@ -910,7 +1280,7 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
{
if (!loggedEncodeError)
{
- qWarning() << "H.264 encode failed";
+ qWarning() << QString("%1 encode failed").arg(QLatin1String(avcodec_get_name(enc_ctx->codec_id)));
loggedEncodeError = true;
}
continue;
@@ -1011,6 +1381,12 @@ void VideoDevice::Play()
continue;
frameData = m_frameQueue.dequeue();
+
+ // The codec is open and the loop is consuming, so the backlog held
+ // for startup is no longer wanted: from here on the newest frame
+ // wins. Set under the queue mutex so enqueueFrame() cannot read it
+ // while this frame is being taken.
+ m_decoderStarted.storeRelaxed(1);
}
AVPacket *in_pkt = av_packet_alloc();
@@ -1027,7 +1403,7 @@ void VideoDevice::Play()
{
if (!loggedDecodeError)
{
- qWarning() << QString("H.264 decode failed: %1").arg(ff_errstr(ret));
+ qWarning() << QString("%1 decode failed: %2").arg(m_codec, ff_errstr(ret));
loggedDecodeError = true;
}
continue;
@@ -1211,7 +1587,6 @@ 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;
}