summaryrefslogtreecommitdiffstats
path: root/pica-client/videodevice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client/videodevice.cpp')
-rw-r--r--pica-client/videodevice.cpp505
1 files changed, 402 insertions, 103 deletions
diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp
index f657706..7e4976a 100644
--- a/pica-client/videodevice.cpp
+++ b/pica-client/videodevice.cpp
@@ -400,6 +400,78 @@ static QList<int> v4l2FrameRates(int fd, unsigned int pixelformat, int width, in
}
#endif
+void pica_merge_capture_rates(VideoCaptureFormat &entry, const QString &codec,
+ const QList<int> &rates)
+{
+ int existing = -1;
+
+ for (int p = 0; p < entry.pixelFormats.size(); p++)
+ {
+ if (entry.pixelFormats.at(p).codec == codec)
+ {
+ existing = p;
+ break;
+ }
+ }
+
+ // Two of a platform's pixel formats can carry the same codec name - v4l2
+ // reports MJPEG and plain JPEG separately and both come out as "mjpeg" -
+ // so a format already seen is added to rather than started again.
+ if (existing < 0)
+ {
+ VideoPixelFormat pf;
+
+ pf.codec = codec;
+ entry.pixelFormats << pf;
+ existing = entry.pixelFormats.size() - 1;
+ }
+
+ VideoPixelFormat &pf = entry.pixelFormats[existing];
+
+ for (int r = 0; r < rates.size(); r++)
+ {
+ if (!pf.frameRates.contains(rates.at(r)))
+ pf.frameRates << rates.at(r);
+
+ if (!entry.frameRates.contains(rates.at(r)))
+ entry.frameRates << rates.at(r);
+ }
+}
+
+void pica_finish_capture_format(VideoCaptureFormat &entry)
+{
+ std::sort(entry.frameRates.begin(), entry.frameRates.end(), std::greater<int>());
+
+ // Uncompressed first, then the compressed formats in whatever order the
+ // platform put compressedFormats in, rather than the order the camera
+ // happened to list them.
+ QList<VideoPixelFormat> ordered;
+
+ for (int p = 0; p < entry.pixelFormats.size(); p++)
+ {
+ if (entry.pixelFormats.at(p).codec.isEmpty())
+ ordered << entry.pixelFormats.at(p);
+ }
+
+ for (int c = 0; c < entry.compressedFormats.size(); c++)
+ {
+ for (int p = 0; p < entry.pixelFormats.size(); p++)
+ {
+ if (entry.pixelFormats.at(p).codec == entry.compressedFormats.at(c))
+ {
+ ordered << entry.pixelFormats.at(p);
+ break;
+ }
+ }
+ }
+
+ for (int p = 0; p < ordered.size(); p++)
+ std::sort(ordered[p].frameRates.begin(), ordered[p].frameRates.end(),
+ std::greater<int>());
+
+ entry.pixelFormats = ordered;
+}
+
QList<VideoCaptureFormat> VideoDevice::CaptureFormats(const QString &device)
{
QList<VideoCaptureFormat> result;
@@ -410,9 +482,10 @@ QList<VideoCaptureFormat> VideoDevice::CaptureFormats(const QString &device)
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.
+ // format and the same size usually comes back several times over. Each
+ // format it comes back in becomes an entry in pixelFormats, keeping the
+ // rates that format can manage with the format itself - see
+ // VideoPixelFormat for why that association matters.
QMap<QPair<int, int>, VideoCaptureFormat> bySize;
struct v4l2_fmtdesc fmt;
@@ -447,11 +520,11 @@ QList<VideoCaptureFormat> VideoDevice::CaptureFormats(const QString &device)
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);
+ // Kept against the format it belongs to - MJPEG at 1280x720 and
+ // uncompressed at 1280x720 are not the same offer - and merged
+ // into the size's own list as well.
+ pica_merge_capture_rates(entry, codec,
+ v4l2FrameRates(fd, fmt.pixelformat, entry.width, entry.height));
}
}
@@ -476,7 +549,7 @@ QList<VideoCaptureFormat> VideoDevice::CaptureFormats(const QString &device)
entry.compressedFormats = ordered;
- std::sort(entry.frameRates.begin(), entry.frameRates.end(), std::greater<int>());
+ pica_finish_capture_format(entry);
result << entry;
}
@@ -714,15 +787,211 @@ static QByteArray cameraOpenUrl(const QString &device)
#endif
}
+// Compressed formats in the order to prefer them as something to decode and
+// re-encode, which is not the order to prefer them for forwarding untouched -
+// that one is in kCompressedFormatPreference, and ranks by bytes on the wire.
+//
+// Here MJPEG comes first instead of last. Every frame is a standalone JPEG, so
+// there are no reference frames to track and no reordering delay, it is the
+// cheapest of the three to decode, and it is the one format essentially every
+// UVC camera offers. Decoding a camera's H.264 only to re-encode it costs more
+// and adds the decoder's delay for nothing, since the result is re-encoded to
+// the configured codec and bitrate either way.
+static const char * const kTranscodeSourcePreference[] = { "mjpeg", "h264", "hevc" };
+
+// One way of opening the camera: which pixel format to ask the driver for, what
+// to do with the packets that come back, and how much of the request to insist
+// on. Built by buildCaptureAttempts() and tried in order.
+struct CaptureAttempt
+{
+ // v4l2 input_format / dshow codec id to ask for. Empty leaves the choice
+ // to the camera, which means its default - uncompressed, on every camera
+ // seen so far.
+ QString inputFormat;
+
+ // Forward the camera's packets as they arrive instead of decoding and
+ // re-encoding them. Only ever set for a compressed inputFormat, and only
+ // when the "prefer compressed formats" setting asked for it: a compressed
+ // format is also worth asking for purely to get the frame rate, which is
+ // what the same format with this false means.
+ bool passthrough;
+
+ bool constrainSize;
+ bool constrainRate;
+
+ // Why this attempt is in the list, for the log when it fails.
+ QString reason;
+};
+
+// Whether a camera has said this format can manage this frame rate.
+//
+// An empty rate list means the camera would not say, which is not the same as
+// saying no - those formats are treated as capable and left to prove it at open
+// time, which is how this worked before any of it was enumerated.
+static bool formatHasRate(const VideoPixelFormat &format, int frameRate)
+{
+ return format.frameRates.isEmpty() || frameRate <= 0 ||
+ format.frameRates.contains(frameRate);
+}
+
+// The orders to try opening the camera in, best first.
+//
+// The point of consulting the enumerated formats here is that a camera asked
+// for a frame rate a format cannot carry does not fail - it opens and then
+// delivers fewer frames, silently. USB 2.0 does not have the bandwidth for
+// uncompressed video at 30fps much above VGA, so asking for the camera's
+// default format at 1280x720@30 gets 10fps and no indication of it. Asking for
+// MJPEG at the same size gets 30, and decoding it costs far less than the
+// missing two thirds of the frames.
+//
+// Hence the order: forward a compressed stream if that was asked for, then
+// uncompressed if it can carry the rate - no decode, no second generation of
+// lossy compression - and then compressed as a source to decode and re-encode,
+// which is what lifts the frame rate above what the bus will carry raw.
+//
+// Everything above only considers formats the camera has said can do the job.
+// The tail then repeats the whole list with the rate and then the size dropped,
+// because listing a format and delivering it at a given size and rate are
+// separate claims and cameras routinely honour the first without the others.
+// Whatever eventually opens is what gets announced to the peer, so relaxing
+// these changes what is sent, not what is claimed.
+static QList<CaptureAttempt> buildCaptureAttempts(const QString &device, int width, int height,
+ int frameRate, bool preferCompressed,
+ const QString &configuredCodec)
+{
+ QList<CaptureAttempt> attempts;
+ QStringList passthroughOrder;
+
+ if (preferCompressed)
+ {
+ passthroughOrder = VideoDevice::CompressedFormats(device);
+
+ // 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 (passthroughOrder.removeAll(configuredCodec) > 0)
+ passthroughOrder.prepend(configuredCodec);
+ }
+
+ // What the camera says it can do at this size. Absent - an unsupported
+ // platform, or a camera that would not be enumerated - leaves every
+ // candidate below looking capable, which is the behaviour from before any
+ // of this was known.
+ VideoCaptureFormat atSize;
+
+ atSize.width = 0;
+ atSize.height = 0;
+
+ const QList<VideoCaptureFormat> formats = VideoDevice::CaptureFormats(device);
+
+ for (int i = 0; i < formats.size(); i++)
+ {
+ if (formats.at(i).width == width && formats.at(i).height == height)
+ {
+ atSize = formats.at(i);
+ break;
+ }
+ }
+
+ // Indexed by codec name, empty string being the uncompressed one. A format
+ // the camera did not list at this size is absent, and absent is not
+ // capable - unless nothing was enumerated at all, below.
+ const bool enumerated = !atSize.pixelFormats.isEmpty();
+
+ for (int i = 0; i < passthroughOrder.size(); i++)
+ {
+ const QString codec = passthroughOrder.at(i);
+ bool capable = !enumerated;
+
+ for (int p = 0; p < atSize.pixelFormats.size(); p++)
+ {
+ if (atSize.pixelFormats.at(p).codec == codec)
+ {
+ capable = formatHasRate(atSize.pixelFormats.at(p), frameRate);
+ break;
+ }
+ }
+
+ if (capable)
+ attempts << CaptureAttempt{ codec, true, true, true,
+ QStringLiteral("forwarded unchanged") };
+ }
+
+ bool uncompressedCapable = !enumerated;
+
+ for (int p = 0; p < atSize.pixelFormats.size(); p++)
+ {
+ if (atSize.pixelFormats.at(p).codec.isEmpty())
+ {
+ uncompressedCapable = formatHasRate(atSize.pixelFormats.at(p), frameRate);
+ break;
+ }
+ }
+
+ if (uncompressedCapable)
+ attempts << CaptureAttempt{ QString(), false, true, true,
+ QStringLiteral("uncompressed, re-encoded here") };
+
+ for (unsigned int i = 0; i < sizeof(kTranscodeSourcePreference) / sizeof(kTranscodeSourcePreference[0]); i++)
+ {
+ const QString codec = QLatin1String(kTranscodeSourcePreference[i]);
+
+ for (int p = 0; p < atSize.pixelFormats.size(); p++)
+ {
+ if (atSize.pixelFormats.at(p).codec == codec &&
+ formatHasRate(atSize.pixelFormats.at(p), frameRate))
+ {
+ attempts << CaptureAttempt{ codec, false, true, true,
+ QStringLiteral("compressed for the frame rate, re-encoded here") };
+ break;
+ }
+ }
+ }
+
+ // The loosened tail. Same candidates, in the same order, with the rate
+ // given up and then the size as well - and without consulting what the
+ // camera claimed, since by this point its claims have been wrong.
+ QStringList tail = passthroughOrder;
+ QList<bool> tailPassthrough;
+
+ for (int i = 0; i < passthroughOrder.size(); i++)
+ tailPassthrough << true;
+
+ tail << QString();
+ tailPassthrough << false;
+
+ for (unsigned int i = 0; i < sizeof(kTranscodeSourcePreference) / sizeof(kTranscodeSourcePreference[0]); i++)
+ {
+ const QString codec = QLatin1String(kTranscodeSourcePreference[i]);
+
+ if (!atSize.compressedFormats.contains(codec))
+ continue;
+
+ tail << codec;
+ tailPassthrough << false;
+ }
+
+ for (int i = 0; i < tail.size(); i++)
+ attempts << CaptureAttempt{ tail.at(i), tailPassthrough.at(i), true, false,
+ QStringLiteral("any rate the camera will give") };
+
+ attempts << CaptureAttempt{ QString(), false, false, false,
+ QStringLiteral("anything the camera will give") };
+
+ return attempts;
+}
+
// One attempt at opening the camera.
//
-// passthroughCodec empty asks for whatever the camera gives; a width of zero
+// inputFormat empty asks for whatever the camera gives; a width of zero
// drops the size and frame rate constraints as well. *ifmt_ctx is left null on
// failure - avformat_open_input() frees and clears it - so the caller can just
// try again.
static int openCamera(const AVInputFormat *ifmt, const QByteArray &url,
int width, int height, int framerate,
- const QString &passthroughCodec, AVFormatContext **ifmt_ctx)
+ const QString &inputFormat, AVFormatContext **ifmt_ctx)
{
AVDictionary *opts = nullptr;
@@ -732,7 +1001,7 @@ static int openCamera(const AVInputFormat *ifmt, const QByteArray &url,
if (framerate > 0)
av_dict_set(&opts, "framerate", QString::number(framerate).toUtf8().constData(), 0);
- if (!passthroughCodec.isEmpty())
+ if (!inputFormat.isEmpty())
{
#ifdef Q_OS_WIN
// dshow has no input_format option. It picks the compressed stream
@@ -741,7 +1010,7 @@ static int openCamera(const AVInputFormat *ifmt, const QByteArray &url,
// has to be set on a context allocated here, since
// avformat_open_input() would otherwise make one only after the point
// where the demuxer reads it.
- const AVCodec *dec = avcodec_find_decoder_by_name(passthroughCodec.toUtf8().constData());
+ const AVCodec *dec = avcodec_find_decoder_by_name(inputFormat.toUtf8().constData());
if (!dec)
{
@@ -759,7 +1028,7 @@ static int openCamera(const AVInputFormat *ifmt, const QByteArray &url,
(*ifmt_ctx)->video_codec_id = dec->id;
#else
- av_dict_set(&opts, "input_format", passthroughCodec.toUtf8().constData(), 0);
+ av_dict_set(&opts, "input_format", inputFormat.toUtf8().constData(), 0);
#endif
}
@@ -790,88 +1059,51 @@ 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
- // these are ones the camera has already said it can deliver, best first.
- // The empty string on the end is the fallback of taking whatever comes and
- // re-encoding it.
- 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
- // rate are three separate claims, and cameras routinely honour the first
- // without the others - an integrated webcam offering MJPEG but not at
- // 640x480 15fps is what prompted this. So each candidate is tried with the
- // constraints loosened a step at a time, giving up the frame rate before
- // the size and both before giving up the format, since the format is worth
- // the most: it decides whether anything has to be re-encoded at all.
- //
- // Whatever comes back is what gets announced to the peer - see
- // captureStarted() and AudioVideoCallController::video_capture_started() -
- // so relaxing these changes what is sent, not what is claimed.
- static const struct
- {
- bool size;
- bool rate;
- } kConstraints[] =
- {
- { true, true },
- { true, false },
- { false, false },
- };
+ // Which formats to try, in which order, and what to do with each - worked
+ // out from what the camera said it can deliver at this size and rate. See
+ // buildCaptureAttempts().
+ const QList<CaptureAttempt> attempts =
+ buildCaptureAttempts(m_deviceName, m_width, m_height, m_frameRate,
+ m_preferCompressed, m_codec);
QString passthroughCodec;
AVFormatContext *ifmt_ctx = nullptr;
int ret = AVERROR(EINVAL);
- bool opened = false;
- for (int a = 0; a < attempts.size() && !opened; a++)
+ for (int a = 0; a < attempts.size(); a++)
{
- for (unsigned int c = 0; c < sizeof(kConstraints) / sizeof(kConstraints[0]); c++)
- {
- ret = openCamera(ifmt, deviceUtf8,
- kConstraints[c].size ? m_width : 0,
- kConstraints[c].size ? m_height : 0,
- kConstraints[c].rate ? m_frameRate : 0,
- attempts.at(a), &ifmt_ctx);
+ const CaptureAttempt &attempt = attempts.at(a);
- if (ret >= 0)
- {
- passthroughCodec = attempts.at(a);
- opened = true;
- break;
- }
+ ret = openCamera(ifmt, deviceUtf8,
+ attempt.constrainSize ? m_width : 0,
+ attempt.constrainSize ? m_height : 0,
+ attempt.constrainRate ? m_frameRate : 0,
+ attempt.inputFormat, &ifmt_ctx);
- QString asked;
+ if (ret >= 0)
+ {
+ // Only a forwarding attempt makes this a passthrough; the same
+ // format asked for to get the frame rate goes to the transcode
+ // loop instead.
+ passthroughCodec = attempt.passthrough ? attempt.inputFormat : QString();
+ break;
+ }
- 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(m_frameRate);
- else
- asked = QString("%1x%2 at any rate").arg(m_width).arg(m_height);
+ QString asked;
- qWarning() << QString("Camera '%1': no %2 at %3 (%4)")
- .arg(m_deviceName,
- attempts.at(a).isEmpty() ? QStringLiteral("stream") : attempts.at(a),
- asked,
- ff_errstr(ret));
- }
+ if (!attempt.constrainSize)
+ asked = QStringLiteral("any size or rate");
+ else if (attempt.constrainRate)
+ 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);
+
+ qWarning() << QString("Camera '%1': no %2 at %3 - %4 (%5)")
+ .arg(m_deviceName,
+ attempt.inputFormat.isEmpty() ? QStringLiteral("stream") : attempt.inputFormat,
+ asked,
+ attempt.reason,
+ ff_errstr(ret));
}
if (ret < 0)
@@ -957,6 +1189,25 @@ void VideoDevice::emitFragments(const unsigned char *data, int size)
}
}
+// The frame rate the camera actually settled on, which is not necessarily the
+// 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, and it
+// is also what captureStarted() reports, so that the rate a camera quietly
+// substituted is visible rather than having to be measured.
+static AVRational cameraFrameRate(AVStream *in_st, int fallbackRate)
+{
+ AVRational fr = in_st->avg_frame_rate;
+
+ if (fr.num <= 0 || fr.den <= 0)
+ fr = in_st->r_frame_rate;
+
+ if (fr.num <= 0 || fr.den <= 0)
+ fr = AVRational{ fallbackRate, 1 };
+
+ return fr;
+}
+
void VideoDevice::runPassthroughLoop(AVFormatContext *ifmt_ctx, AVStream *in_st, const QString &codec)
{
// The camera decides the picture size here, since nothing rescales it on
@@ -964,7 +1215,8 @@ void VideoDevice::runPassthroughLoop(AVFormatContext *ifmt_ctx, AVStream *in_st,
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);
+ emit captureStarted(codec, width, height,
+ av_q2d(cameraFrameRate(in_st, m_frameRate)));
// Nothing encodes here at all - the camera's own packets go out as they
// arrive - so any hardware encoding setting simply has nothing to act on.
@@ -995,21 +1247,38 @@ void VideoDevice::runPassthroughLoop(AVFormatContext *ifmt_ctx, AVStream *in_st,
av_packet_free(&pkt);
}
-// The frame rate the camera actually settled on, which is not necessarily the
-// 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, int fallbackRate)
+// The pixel format to hand libswscale for a decoded camera frame, and whether
+// that frame's values use the full 0-255 range rather than the 16-235 one.
+//
+// MJPEG decodes to one of the yuvj* formats, which mean "the yuv* of the same
+// name, with full range values". libswscale still understands them and infers
+// the range, but warns that they are deprecated - and since capturing MJPEG
+// and re-encoding it is now the ordinary path for anything above VGA on USB
+// 2.0, that warning would otherwise be printed for most calls. Saying the same
+// thing the way libswscale asks for it keeps the range explicit instead of
+// inferred, and the log readable.
+static AVPixelFormat swscaleSourceFormat(AVPixelFormat format, bool *fullRange)
{
- AVRational fr = in_st->avg_frame_rate;
+ *fullRange = true;
- if (fr.num <= 0 || fr.den <= 0)
- fr = in_st->r_frame_rate;
-
- if (fr.num <= 0 || fr.den <= 0)
- fr = AVRational{ fallbackRate, 1 };
+ switch (format)
+ {
+ case AV_PIX_FMT_YUVJ420P:
+ return AV_PIX_FMT_YUV420P;
+ case AV_PIX_FMT_YUVJ422P:
+ return AV_PIX_FMT_YUV422P;
+ case AV_PIX_FMT_YUVJ444P:
+ return AV_PIX_FMT_YUV444P;
+ case AV_PIX_FMT_YUVJ440P:
+ return AV_PIX_FMT_YUV440P;
+ case AV_PIX_FMT_YUVJ411P:
+ return AV_PIX_FMT_YUV411P;
+ default:
+ break;
+ }
- return fr;
+ *fullRange = false;
+ return format;
}
void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
@@ -1173,9 +1442,19 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
}
+ // Which format the camera is actually delivering, reported alongside the
+ // encoder because the two together explain the frame rate: an uncompressed
+ // camera stream is what caps the rate on a USB 2.0 bus, and seeing
+ // "mjpeg" here is what says that cap has been stepped around. See
+ // buildCaptureAttempts().
+ const QString source = in_st->codecpar->codec_id == AV_CODEC_ID_RAWVIDEO
+ ? QStringLiteral("uncompressed")
+ : QString::fromLatin1(avcodec_get_name(in_st->codecpar->codec_id));
+
emit accelerationInUse(hardware
- ? QString("GPU encoding (VAAPI %1)").arg(QLatin1String(kVideoEncoders[encIdx].codec))
- : QString("CPU encoding (%1)").arg(swEncoderName));
+ ? QString("GPU encoding (VAAPI %1) from %2")
+ .arg(QLatin1String(kVideoEncoders[encIdx].codec), source)
+ : QString("CPU encoding (%1) from %2").arg(swEncoderName, source));
// 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
@@ -1183,8 +1462,12 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
// 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.
+ //
+ // The rate is the camera's, not the encoder's: the encoder is configured
+ // from it (see frameRate above) and encodes every frame it is given, so
+ // what the camera delivers is what goes out.
emit captureStarted(QLatin1String(avcodec_get_name(enc_ctx->codec_id)),
- enc_ctx->width, enc_ctx->height);
+ enc_ctx->width, enc_ctx->height, av_q2d(frameRate));
// What the camera's frames are scaled into. With a hardware encoder this
// is only a staging buffer in system memory - NV12, which is what VAAPI
@@ -1247,7 +1530,11 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
// what was requested.
if (!sws)
{
- sws = sws_getContext(cam_frame->width, cam_frame->height, (AVPixelFormat)cam_frame->format,
+ bool srcFullRange = false;
+ const AVPixelFormat srcFormat =
+ swscaleSourceFormat((AVPixelFormat)cam_frame->format, &srcFullRange);
+
+ sws = sws_getContext(cam_frame->width, cam_frame->height, srcFormat,
enc_ctx->width, enc_ctx->height, sw_pix_fmt,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!sws)
@@ -1260,6 +1547,18 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
av_frame_unref(cam_frame);
continue;
}
+
+ // The range the format name no longer carries. The encoder
+ // wants the studio range, so this is a real conversion and not
+ // just a label: without it a JPEG frame's blacks and whites
+ // would be clipped rather than compressed into 16-235.
+ if (srcFullRange)
+ {
+ const int *coefs = sws_getCoefficients(SWS_CS_DEFAULT);
+
+ sws_setColorspaceDetails(sws, coefs, 1, coefs, 0,
+ 0, 1 << 16, 1 << 16);
+ }
}
if (av_frame_make_writable(enc_frame) < 0)