Qt 6.x
The Qt SDK
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
qffmpegvideoframeencoder.cpp
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
9#include "private/qplatformmediarecorder_p.h"
10#include "private/qmultimediautils_p.h"
11#include <qloggingcategory.h>
12
14
15static Q_LOGGING_CATEGORY(qLcVideoFrameEncoder, "qt.multimedia.ffmpeg.videoencoder")
16
17namespace QFFmpeg {
18
19VideoFrameEncoder::Data::~Data()
20{
21 if (converter)
22 sws_freeContext(converter);
23}
24
25VideoFrameEncoder::VideoFrameEncoder(const QMediaEncoderSettings &encoderSettings,
26 const QSize &sourceSize, float frameRate,
27 AVPixelFormat sourceFormat, AVPixelFormat sourceSWFormat)
28 : d(new Data)
29{
30 d->settings = encoderSettings;
31 d->frameRate = frameRate;
32 d->sourceSize = sourceSize;
33
34 if (!d->settings.videoResolution().isValid())
35 d->settings.setVideoResolution(d->sourceSize);
36
37 d->sourceFormat = sourceFormat;
38 d->sourceSWFormat = sourceSWFormat;
39
40 Q_ASSERT(isSwPixelFormat(sourceSWFormat));
41
42 const auto qVideoCodec = encoderSettings.videoCodec();
43 const auto codecID = QFFmpegMediaFormatInfo::codecIdForVideoCodec(qVideoCodec);
44
45 std::tie(d->codec, d->accel) = findHwEncoder(codecID, sourceSize);
46
47 if (!d->codec)
48 d->codec = findSwEncoder(codecID, sourceFormat, sourceSWFormat);
49
50 if (!d->codec) {
51 qWarning() << "Could not find encoder for codecId" << codecID;
52 d = {};
53 return;
54 }
55
56 qCDebug(qLcVideoFrameEncoder) << "found encoder" << d->codec->name << "for id" << d->codec->id;
57
58 d->targetFormat = findTargetFormat(sourceFormat, sourceSWFormat, d->codec, d->accel.get());
59
60 if (d->targetFormat == AV_PIX_FMT_NONE) {
61 qWarning() << "Could not find target format for codecId" << codecID;
62 d = {};
63 return;
64 }
65
66 const bool needToScale = d->sourceSize != d->settings.videoResolution();
67 const bool zeroCopy = d->sourceFormat == d->targetFormat && !needToScale;
68
69 if (zeroCopy) {
70 qCDebug(qLcVideoFrameEncoder) << "zero copy encoding, format" << d->targetFormat;
71 // no need to initialize any converters
72 return;
73 }
74
75 if (isHwPixelFormat(d->sourceFormat))
76 d->downloadFromHW = true;
77 else
78 d->sourceSWFormat = d->sourceFormat;
79
80 if (isHwPixelFormat(d->targetFormat)) {
81 Q_ASSERT(d->accel);
82 // if source and target formats don't agree, but the target is a HW format, we need to upload
83 d->uploadToHW = true;
84 d->targetSWFormat = findTargetSWFormat(d->sourceSWFormat, *d->accel);
85
86 if (d->targetSWFormat == AV_PIX_FMT_NONE) {
87 qWarning() << "Cannot find software target format. sourceSWFormat:" << d->sourceSWFormat
88 << "targetFormat:" << d->targetFormat;
89 d = {};
90 return;
91 }
92
93 qCDebug(qLcVideoFrameEncoder)
94 << "using sw format" << d->targetSWFormat << "as transfer format.";
95
96 // need to create a frames context to convert the input data
97 d->accel->createFramesContext(d->targetSWFormat, d->settings.videoResolution());
98 } else {
99 d->targetSWFormat = d->targetFormat;
100 }
101
102 if (d->sourceSWFormat != d->targetSWFormat || needToScale) {
103 const auto targetSize = d->settings.videoResolution();
104 qCDebug(qLcVideoFrameEncoder)
105 << "camera and encoder use different formats:" << d->sourceSWFormat
106 << d->targetSWFormat << "or sizes:" << d->sourceSize << targetSize;
107
108 d->converter =
109 sws_getContext(d->sourceSize.width(), d->sourceSize.height(), d->sourceSWFormat,
110 targetSize.width(), targetSize.height(), d->targetSWFormat,
111 SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
112 }
113
114 qCDebug(qLcVideoFrameEncoder) << "VideoFrameEncoder conversions initialized:"
115 << "sourceFormat:" << d->sourceFormat
116 << (isHwPixelFormat(d->sourceFormat) ? "(hw)" : "(sw)")
117 << "targetFormat:" << d->targetFormat
118 << (isHwPixelFormat(d->targetFormat) ? "(hw)" : "(sw)")
119 << "sourceSWFormat:" << d->sourceSWFormat
120 << "targetSWFormat:" << d->targetSWFormat;
121}
122
123VideoFrameEncoder::~VideoFrameEncoder()
124{
125}
126
127void QFFmpeg::VideoFrameEncoder::initWithFormatContext(AVFormatContext *formatContext)
128{
129 if (!d)
130 return;
131
132 d->stream = avformat_new_stream(formatContext, nullptr);
133 d->stream->id = formatContext->nb_streams - 1;
134 //qCDebug(qLcVideoFrameEncoder) << "Video stream: index" << d->stream->id;
135 d->stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
136 d->stream->codecpar->codec_id = d->codec->id;
137
138 // Apples HEVC decoders don't like the hev1 tag ffmpeg uses by default, use hvc1 as the more commonly accepted tag
139 if (d->codec->id == AV_CODEC_ID_HEVC)
140 d->stream->codecpar->codec_tag = MKTAG('h','v','c','1');
141
142 // ### Fix hardcoded values
143 d->stream->codecpar->format = d->targetFormat;
144 d->stream->codecpar->width = d->settings.videoResolution().width();
145 d->stream->codecpar->height = d->settings.videoResolution().height();
146 d->stream->codecpar->sample_aspect_ratio = AVRational{1, 1};
147 float requestedRate = d->frameRate;
148 constexpr int TimeScaleFactor = 1000; // Allows not to follow fixed rate
149 d->stream->time_base = AVRational{ 1, static_cast<int>(requestedRate * TimeScaleFactor) };
150
151 float delta = 1e10;
152 if (d->codec->supported_framerates) {
153 // codec only supports fixed frame rates
154 auto *best = d->codec->supported_framerates;
155 qCDebug(qLcVideoFrameEncoder) << "Finding fixed rate:";
156 for (auto *f = d->codec->supported_framerates; f->num != 0; f++) {
157 auto maybeRate = toFloat(*f);
158 if (!maybeRate)
159 continue;
160 float d = qAbs(*maybeRate - requestedRate);
161 qCDebug(qLcVideoFrameEncoder) << " " << f->num << f->den << d;
162 if (d < delta) {
163 best = f;
164 delta = d;
165 }
166 }
167 qCDebug(qLcVideoFrameEncoder) << "Fixed frame rate required. Requested:" << requestedRate << "Using:" << best->num << "/" << best->den;
168 d->stream->time_base = *best;
169 requestedRate = toFloat(*best).value_or(0.f);
170 }
171
172 Q_ASSERT(d->codec);
173 d->codecContext.reset(avcodec_alloc_context3(d->codec));
174 if (!d->codecContext) {
175 qWarning() << "Could not allocate codec context";
176 d = {};
177 return;
178 }
179
180 avcodec_parameters_to_context(d->codecContext.get(), d->stream->codecpar);
181 d->codecContext->time_base = d->stream->time_base;
182 qCDebug(qLcVideoFrameEncoder) << "requesting time base" << d->codecContext->time_base.num << d->codecContext->time_base.den;
183 auto [num, den] = qRealToFraction(requestedRate);
184 d->codecContext->framerate = { num, den };
185 if (d->accel) {
186 auto deviceContext = d->accel->hwDeviceContextAsBuffer();
187 if (deviceContext)
188 d->codecContext->hw_device_ctx = av_buffer_ref(deviceContext);
189 auto framesContext = d->accel->hwFramesContextAsBuffer();
190 if (framesContext)
191 d->codecContext->hw_frames_ctx = av_buffer_ref(framesContext);
192 }
193}
194
196{
197 if (!d) {
198 qWarning() << "Cannot open null VideoFrameEncoder";
199 return false;
200 }
201 AVDictionaryHolder opts;
202 applyVideoEncoderOptions(d->settings, d->codec->name, d->codecContext.get(), opts);
203 int res = avcodec_open2(d->codecContext.get(), d->codec, opts);
204 if (res < 0) {
205 d->codecContext.reset();
206 qWarning() << "Couldn't open codec for writing" << err2str(res);
207 return false;
208 }
209 qCDebug(qLcVideoFrameEncoder) << "video codec opened" << res << "time base" << d->codecContext->time_base.num << d->codecContext->time_base.den;
210 d->stream->time_base = d->stream->time_base;
211 return true;
212}
213
215{
216 Q_ASSERT(d);
217 qint64 div = 1'000'000 * d->stream->time_base.num;
218 return div != 0 ? (us * d->stream->time_base.den + div / 2) / div : 0;
219}
220
221const AVRational &VideoFrameEncoder::getTimeBase() const
222{
223 return d->stream->time_base;
224}
225
227{
228 if (!d->codecContext) {
229 qWarning() << "codec context is not initialized!";
230 return AVERROR(EINVAL);
231 }
232
233 if (!frame)
234 return avcodec_send_frame(d->codecContext.get(), frame.get());
235
236 int64_t pts = 0;
237 AVRational timeBase = {};
238 getAVFrameTime(*frame, pts, timeBase);
239
240 if (d->downloadFromHW) {
241 auto f = makeAVFrame();
242
243 int err = av_hwframe_transfer_data(f.get(), frame.get(), 0);
244 if (err < 0) {
245 qCDebug(qLcVideoFrameEncoder) << "Error transferring frame data to surface." << err2str(err);
246 return err;
247 }
248
249 frame = std::move(f);
250 }
251
252 if (d->converter) {
253 auto f = makeAVFrame();
254
255 f->format = d->targetSWFormat;
256 f->width = d->settings.videoResolution().width();
257 f->height = d->settings.videoResolution().height();
258
259 av_frame_get_buffer(f.get(), 0);
260 const auto scaledHeight = sws_scale(d->converter, frame->data, frame->linesize, 0,
261 frame->height, f->data, f->linesize);
262
263 if (scaledHeight != f->height)
264 qCWarning(qLcVideoFrameEncoder) << "Scaled height" << scaledHeight << "!=" << f->height;
265
266 frame = std::move(f);
267 }
268
269 if (d->uploadToHW) {
270 auto *hwFramesContext = d->accel->hwFramesContextAsBuffer();
272 auto f = makeAVFrame();
273
274 if (!f)
275 return AVERROR(ENOMEM);
276 int err = av_hwframe_get_buffer(hwFramesContext, f.get(), 0);
277 if (err < 0) {
278 qCDebug(qLcVideoFrameEncoder) << "Error getting HW buffer" << err2str(err);
279 return err;
280 } else {
281 qCDebug(qLcVideoFrameEncoder) << "got HW buffer";
282 }
283 if (!f->hw_frames_ctx) {
284 qCDebug(qLcVideoFrameEncoder) << "no hw frames context";
285 return AVERROR(ENOMEM);
286 }
287 err = av_hwframe_transfer_data(f.get(), frame.get(), 0);
288 if (err < 0) {
289 qCDebug(qLcVideoFrameEncoder) << "Error transferring frame data to surface." << err2str(err);
290 return err;
291 }
292 frame = std::move(f);
293 }
294
295 qCDebug(qLcVideoFrameEncoder) << "sending frame" << pts << "*" << timeBase.num << "/"
296 << timeBase.den;
297
298 setAVFrameTime(*frame, pts, timeBase);
299 return avcodec_send_frame(d->codecContext.get(), frame.get());
300}
301
303{
304 if (!d || !d->codecContext)
305 return nullptr;
306 AVPacket *packet = av_packet_alloc();
307 int ret = avcodec_receive_packet(d->codecContext.get(), packet);
308 if (ret < 0) {
309 av_packet_free(&packet);
310 if (ret != AVERROR(EOF) && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF)
311 qCDebug(qLcVideoFrameEncoder) << "Error receiving packet" << ret << err2str(ret);
312 return nullptr;
313 }
314 auto ts = timeStampMs(packet->pts, d->stream->time_base);
315
316 qCDebug(qLcVideoFrameEncoder) << "got a packet" << packet->pts << packet->dts << (ts ? *ts : 0);
317
318 if (packet->dts != AV_NOPTS_VALUE && packet->pts < packet->dts) {
319 // the case seems to be an ffmpeg bug
320 packet->dts = AV_NOPTS_VALUE;
321 }
322
323 packet->stream_index = d->stream->id;
324 return packet;
325}
326
327} // namespace QFFmpeg
328
void reset(T *ptr=nullptr) noexcept
T * get() const noexcept
static AVCodecID codecIdForVideoCodec(QMediaFormat::VideoCodec codec)
void initWithFormatContext(AVFormatContext *formatContext)
const AVRational & getTimeBase() const
qint64 getPts(qint64 ms) const
int sendFrame(AVFrameUPtr frame)
QMediaFormat::VideoCodec videoCodec() const
\inmodule QtCore
Definition qsize.h:25
int height
the height of the widget excluding any window frame
Definition qwidget.h:115
AVFrameUPtr makeAVFrame()
Definition qffmpeg_p.h:119
void getAVFrameTime(const AVFrame &frame, int64_t &pts, AVRational &timeBase)
Definition qffmpeg_p.h:73
void applyVideoEncoderOptions(const QMediaEncoderSettings &settings, const QByteArray &codecName, AVCodecContext *codec, AVDictionary **opts)
QString err2str(int errnum)
Definition qffmpeg_p.h:56
void setAVFrameTime(AVFrame &frame, int64_t pts, const AVRational &timeBase)
Definition qffmpeg_p.h:63
std::optional< qint64 > timeStampMs(qint64 ts, AVRational base)
Definition qffmpeg_p.h:41
std::optional< float > toFloat(AVRational r)
Definition qffmpeg_p.h:51
std::unique_ptr< AVFrame, AVDeleter< decltype(&av_frame_free), &av_frame_free > > AVFrameUPtr
Definition qffmpeg_p.h:117
Combined button and popup list for selecting options.
AVBufferRef * hwFramesContext
#define qWarning
Definition qlogging.h:162
#define Q_LOGGING_CATEGORY(name,...)
#define qCWarning(category,...)
#define qCDebug(category,...)
return ret
QT_BEGIN_NAMESPACE Fraction qRealToFraction(qreal value)
constexpr T qAbs(const T &t)
Definition qnumeric.h:328
GLfloat GLfloat f
GLuint res
GLuint num
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
long long qint64
Definition qtypes.h:55
QFrame frame
[0]