Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qffmpegmediadataholder.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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
4#include "playbackengine/qffmpegmediadataholder_p.h"
5
9#include "qiodevice.h"
10#include "qdatetime.h"
12
13#include <QtMultimedia/qplaybackoptions.h>
14#include <QtMultimedia/private/qmediametadata_p.h>
15
16#include <math.h>
17#include <optional>
18
19#include <QtCore/private/qminimalflatset_p.h>
20
21QT_BEGIN_NAMESPACE
22
23Q_STATIC_LOGGING_CATEGORY(qLcMediaDataHolder, "qt.multimedia.ffmpeg.mediadataholder")
24
25namespace QFFmpeg {
26
27static std::optional<TrackDuration> streamDuration(const AVStream &stream)
28{
29 if (stream.duration > 0)
30 return toTrackDuration(AVStreamDuration(stream.duration), &stream);
31
32 // In some cases ffmpeg reports negative duration that is definitely invalid.
33 // However, the correct duration may be read from the metadata.
34
35 if (stream.duration < 0 && stream.duration != AV_NOPTS_VALUE) {
36 qCWarning(qLcMediaDataHolder) << "AVStream duration" << stream.duration
37 << "is invalid. Taking it from the metadata";
38 }
39
40 if (const auto duration = av_dict_get(stream.metadata, "DURATION", nullptr, 0)) {
41 const auto time = QTime::fromString(QString::fromUtf8(duration->value));
42 return TrackDuration(qint64(1000) * time.msecsSinceStartOfDay());
43 }
44
45 return {};
46}
47
48static QTransform displayMatrixToTransform(const int32_t *displayMatrix)
49{
50 // displayMatrix is stored as
51 //
52 // . -- X axis
53 // |
54 // | | a b u |
55 // Y | c d v |
56 // axis | x y w |
57 //
58 // where a, b, c, d, x, y are 16.16 fixed-point values,
59 // and u, v, w are 30.2 point values.
60 // Only a, b, c, d impacts on mirroring and rotation,
61 // so it's enough to propagate them to QTransform.
62 //
63 // If we were interested in getting proper XY scales,
64 // we would divide a,b,c,d by 2^16. The whole scale doesn't
65 // impact mirroring and rotation, so we don't do so.
66
67 auto toRotateMirrorValue = [displayMatrix](int index) {
68 // toRotateScaleValue would be:
69 // return displayMatrix[index] / qreal(1 << 16);
70 return displayMatrix[index];
71 };
72
73 return QTransform(toRotateMirrorValue(0), toRotateMirrorValue(1),
74 toRotateMirrorValue(3), toRotateMirrorValue(4),
75 0, 0);
76}
77
78static VideoTransformation streamTransformation(const AVStream *stream)
79{
80 Q_ASSERT(stream);
81
82 using SideDataSize = decltype(AVPacketSideData::size);
83 constexpr SideDataSize displayMatrixSize = sizeof(int32_t) * 9;
84 const AVPacketSideData *sideData = streamSideData(stream, AV_PKT_DATA_DISPLAYMATRIX);
85 if (!sideData || sideData->size < displayMatrixSize)
86 return {};
87
88 const auto displayMatrix = reinterpret_cast<const int32_t *>(sideData->data);
89 const QTransform transform = displayMatrixToTransform(displayMatrix);
90 const VideoTransformationOpt result = qVideoTransformationFromMatrix(transform);
91 if (!result) {
92 qCWarning(qLcMediaDataHolder)
93 << "Video stream contains malformed display matrix" << transform;
94 return {};
95 }
96 return *result;
97}
98
99static bool colorTransferSupportsHdr(const AVStream *stream)
100{
101 if (!stream)
102 return false;
103
104 const AVCodecParameters *codecPar = stream->codecpar;
105 if (!codecPar)
106 return false;
107
108 const QVideoFrameFormat::ColorTransfer colorTransfer = fromAvColorTransfer(codecPar->color_trc);
109
110 // Assume that content is using HDR if the color transfer supports high
111 // dynamic range. The video may still not utilize the extended range,
112 // but we can't determine the actual range without decoding frames.
113 return colorTransfer == QVideoFrameFormat::ColorTransfer_ST2084
114 || colorTransfer == QVideoFrameFormat::ColorTransfer_STD_B67;
115}
116
118{
119 // TODO: Add QMediaMetaData::Mirrored and take from it and QMediaMetaData::Orientation:
120 // int orientation = m_metaData.value(QMediaMetaData::Orientation).toInt();
121 // return static_cast<QtVideo::Rotation>(orientation);
122
123 const int streamIndex = m_currentAVStreamIndex[QPlatformMediaPlayer::VideoStream];
124 if (streamIndex < 0)
125 return {};
126
127 return streamTransformation(m_context->streams[streamIndex]);
128}
129
131{
132 return m_context.get();
133}
134
135int MediaDataHolder::currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
136{
137 return m_currentAVStreamIndex[trackType];
138}
139
169
170QPlatformMediaPlayer::TrackType MediaDataHolder::trackTypeFromMediaType(int mediaType)
171{
172 switch (mediaType) {
173 case AVMEDIA_TYPE_AUDIO:
174 return QPlatformMediaPlayer::AudioStream;
175 case AVMEDIA_TYPE_VIDEO:
176 return QPlatformMediaPlayer::VideoStream;
177 case AVMEDIA_TYPE_SUBTITLE:
178 return QPlatformMediaPlayer::SubtitleStream;
179 default:
180 return QPlatformMediaPlayer::NTrackTypes;
181 }
182}
183
184namespace {
185q23::expected<AVFormatContextUPtr, MediaDataHolder::ContextError>
186loadMedia(const QUrl &mediaUrl, QIODevice *stream, const QPlaybackOptions &playbackOptions,
187 const std::shared_ptr<ICancelToken> &cancelToken)
188{
189 using std::chrono::duration_cast;
190 using std::chrono::microseconds;
191 using std::chrono::milliseconds;
192
193 const QByteArray url = mediaUrl.toString(QUrl::PreferLocalFile).toUtf8();
194
195 AVFormatContextUPtr context{ avformat_alloc_context() };
196
197 if (stream) {
198 if (!stream->isOpen()) {
199 if (!stream->open(QIODevice::ReadOnly))
200 return q23::unexpected{
201 MediaDataHolder::ContextError{
202 QMediaPlayer::ResourceError,
203 QLatin1String("Could not open source device."),
204 },
205 };
206 }
207
208 auto seek = &seekQIODevice;
209
210 if (!stream->isSequential()) {
211 stream->seek(0);
212 } else {
213 context->ctx_flags |= AVFMTCTX_UNSEEKABLE;
214 seek = nullptr;
215 }
216
217 constexpr int bufferSize = 32768;
218 unsigned char *buffer = (unsigned char *)av_malloc(bufferSize);
219 context->pb = avio_alloc_context(buffer, bufferSize, false, stream, &readQIODevice, nullptr,
220 seek);
221 }
222
223 AVDictionaryHolder dict;
224 using RtmpProtocols =
225 QMinimalVarLengthFlatSet<std::basic_string_view<char16_t>, 6, std::less<>>;
226
227 static const RtmpProtocols rtmpProtocols{
228 u"rtmp", u"rtmpe", u"rtmps", u"rtmpt", u"rtmpse", u"rtmpte",
229 };
230
231 // for rtmp streams, the `timout` parameter implies acting as a server:
232 // https://ffmpeg.org/ffmpeg-protocols.html#rtmp
233 // This is not the semantics of QPlaybackOptions::networkTimeout, and will cause failures when
234 // opening streams
235 const bool setNetworkTimeout = !rtmpProtocols.contains(mediaUrl.scheme());
236
237 if (setNetworkTimeout) {
238 const milliseconds timeout = playbackOptions.networkTimeout();
239 av_dict_set_int(dict, "timeout", duration_cast<microseconds>(timeout).count(), 0);
240 qCDebug(qLcMediaDataHolder) << "Using custom network timeout:" << timeout;
241 }
242
243 {
244 const int probeSize = playbackOptions.probeSize();
245 if (probeSize != -1) {
246 constexpr int minProbeSizeFFmpeg = 32;
247 if (probeSize >= minProbeSizeFFmpeg) {
248 av_dict_set_int(dict, "probesize", probeSize, 0);
249 qCDebug(qLcMediaDataHolder) << "Using custom probesize" << probeSize;
250 } else
251 qCWarning(qLcMediaDataHolder) << "Invalid probe size, using default";
252 }
253 }
254
255 const QByteArray protocolWhitelist = qgetenv("QT_FFMPEG_PROTOCOL_WHITELIST");
256 if (!protocolWhitelist.isNull())
257 av_dict_set(dict, "protocol_whitelist", protocolWhitelist.data(), 0);
258
259 if (playbackOptions.playbackIntent() == QPlaybackOptions::PlaybackIntent::LowLatencyStreaming) {
260 av_dict_set(dict, "fflags", "nobuffer", 0);
261 av_dict_set_int(dict, "flush_packets", 1, 0);
262 qCDebug(qLcMediaDataHolder) << "Enabled low latency streaming";
263 }
264
265 // QTBUG-145590: for hls streams, we want to disable http persistent connections to allow FFmpeg
266 // (before FFmpeg 8?) to mix raw and encrypted streams
267 // compare https://trac.ffmpeg.org/ticket/10599
268 if (avformat_version() < AV_VERSION_INT(62, 12, 100))
269 av_dict_set_int(dict, "http_persistent", 0, 0);
270
271 context->interrupt_callback.opaque = cancelToken.get();
272 context->interrupt_callback.callback = [](void *opaque) {
273 const auto *cancelToken = static_cast<const ICancelToken *>(opaque);
274 if (cancelToken && cancelToken->isCancelled())
275 return 1;
276 return 0;
277 };
278
279 int ret = 0;
280 {
281 AVFormatContext *contextRaw = context.release();
282 ret = avformat_open_input(&contextRaw, url.constData(), nullptr, dict);
283 context.reset(contextRaw);
284 }
285
286 if (ret < 0) {
287 auto code = QMediaPlayer::ResourceError;
288 if (ret == AVERROR(EACCES))
289 code = QMediaPlayer::AccessDeniedError;
290 else if (ret == AVERROR(EINVAL) || ret == AVERROR_INVALIDDATA)
291 code = QMediaPlayer::FormatError;
292
293 qCWarning(qLcMediaDataHolder)
294 << "Could not open media. FFmpeg error description:" << AVError(ret);
295
296 return q23::unexpected{
297 MediaDataHolder::ContextError{ code, QMediaPlayer::tr("Could not open file") },
298 };
299 }
300
301 ret = avformat_find_stream_info(context.get(), nullptr);
302 if (ret < 0) {
303 return q23::unexpected{
304 MediaDataHolder::ContextError{
305 QMediaPlayer::FormatError,
306 QMediaPlayer::tr("Could not find stream information for media file") },
307 };
308 }
309
310 if (qLcMediaDataHolder().isInfoEnabled())
311 av_dump_format(context.get(), 0, url.constData(), 0);
312
313
314 return context;
315}
316
317} // namespace
318
319MediaDataHolder::Maybe MediaDataHolder::create(const QUrl &url, QIODevice *stream,
320 const QPlaybackOptions &options,
321 const std::shared_ptr<ICancelToken> &cancelToken)
322{
323 q23::expected context = loadMedia(url, stream, options, cancelToken);
324 if (context) {
325 // MediaDataHolder is wrapped in a shared pointer to interop with signal/slot mechanism
326 return std::make_shared<MediaDataHolder>(
327 MediaDataHolder{ std::move(context.value()), cancelToken });
328 }
329 return q23::unexpected{ context.error() };
330}
331
332MediaDataHolder::MediaDataHolder(AVFormatContextUPtr context,
333 const std::shared_ptr<ICancelToken> &cancelToken)
334 : m_cancelToken{ cancelToken }
335{
336 Q_ASSERT(context);
337
338 m_context = std::move(context);
339 m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);
340
341 for (unsigned int i = 0; i < m_context->nb_streams; ++i) {
342
343 const auto *stream = m_context->streams[i];
344 const auto trackType = trackTypeFromMediaType(stream->codecpar->codec_type);
345
346 if (trackType == QPlatformMediaPlayer::NTrackTypes)
347 continue;
348
349 if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC)
350 continue; // Ignore attached picture streams because we treat them as metadata
351
352 if (stream->time_base.num <= 0 || stream->time_base.den <= 0) {
353 // An invalid stream timebase is not expected to be given by FFmpeg
354 qCWarning(qLcMediaDataHolder) << "A stream for the track type" << trackType
355 << "has an invalid timebase:" << stream->time_base;
356 continue;
357 }
358
359 auto metaData = QFFmpegMetaData::fromAVMetaData(stream->metadata);
360 const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
361
362 if (trackType != QPlatformMediaPlayer::SubtitleStream) {
363 insertMediaData(metaData, trackType, stream);
364
365 if (isDefault && m_requestedStreams[trackType] < 0)
366 m_requestedStreams[trackType] = m_streamMap[trackType].size();
367 }
368
369 if (auto duration = streamDuration(*stream)) {
370 m_duration = qMax(m_duration, *duration);
371 metaData.insert(QMediaMetaData::Duration, toUserDuration(*duration).get());
372 }
373
374 m_streamMap[trackType].append({ (int)i, isDefault, metaData });
375 }
376
377 // With some media files, streams may be lacking duration info. Let's
378 // get it from ffmpeg's duration estimation instead.
379 if (m_duration == TrackDuration(0) && m_context->duration > 0ll) {
380 m_duration = toTrackDuration(AVContextDuration(m_context->duration));
381 }
382
383 for (auto trackType :
384 { QPlatformMediaPlayer::VideoStream, QPlatformMediaPlayer::AudioStream }) {
385 auto &requestedStream = m_requestedStreams[trackType];
386 auto &streamMap = m_streamMap[trackType];
387
388 if (requestedStream < 0 && !streamMap.empty())
389 requestedStream = 0;
390
391 if (requestedStream >= 0)
392 m_currentAVStreamIndex[trackType] = streamMap[requestedStream].avStreamIndex;
393 }
394
395 updateMetaData();
396}
397
398namespace {
399
400/*!
401 \internal
402
403 Attempt to find an attached picture from the context's streams.
404 This will find ID3v2 pictures on audio files, and also pictures
405 attached to videos.
406 */
407QImage getAttachedPicture(const AVFormatContext *context)
408{
409 if (!context)
410 return {};
411
412 for (unsigned int i = 0; i < context->nb_streams; ++i) {
413 const AVStream* stream = context->streams[i];
414 if (!stream || !(stream->disposition & AV_DISPOSITION_ATTACHED_PIC))
415 continue;
416
417 const AVPacket *compressedImage = &stream->attached_pic;
418 if (!compressedImage || !compressedImage->data || compressedImage->size <= 0)
419 continue;
420
421 // Feed raw compressed data to QImage::fromData, which will decompress it
422 // if it is a recognized format.
423 QImage image = QImage::fromData({ compressedImage->data, compressedImage->size });
424 if (!image.isNull())
425 return image;
426 }
427
428 return {};
429}
430
431} // namespace
432
433void MediaDataHolder::updateMetaData()
434{
435 m_metaData = {};
436
437 if (!m_context)
438 return;
439
440 m_metaData = QFFmpegMetaData::fromAVMetaData(m_context->metadata);
441 m_metaData.insert(QMediaMetaData::FileFormat,
442 QVariant::fromValue(QFFmpegMediaFormatInfo::fileFormatForAVInputFormat(
443 *m_context->iformat)));
444 m_metaData.insert(QMediaMetaData::Duration, toUserDuration(m_duration).get());
445
446 if (!m_cachedThumbnail.has_value())
447 m_cachedThumbnail = getAttachedPicture(m_context.get());
448
449 QtMultimediaPrivate::setCoverArtImage(m_metaData, *m_cachedThumbnail);
450
451 for (auto trackType :
452 { QPlatformMediaPlayer::AudioStream, QPlatformMediaPlayer::VideoStream }) {
453 const auto streamIndex = m_currentAVStreamIndex[trackType];
454 if (streamIndex >= 0)
455 insertMediaData(m_metaData, trackType, m_context->streams[streamIndex]);
456 }
457}
458
460{
461 if (!m_context)
462 return false;
463
465 streamNumber = -1;
467 return false;
470
472 qCDebug(qLcMediaDataHolder) << ">>>>> change track" << type << "from" << oldIndex << "to"
473 << avStreamIndex;
474
475 // TODO: maybe add additional verifications
477
479
480 return true;
481}
482
487
489 QPlatformMediaPlayer::TrackType trackType) const
490{
491 Q_ASSERT(trackType < QPlatformMediaPlayer::NTrackTypes);
492
493 return m_streamMap[trackType];
494}
495
496} // namespace QFFmpeg
497
498QT_END_NAMESPACE
int currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
VideoTransformation transformation() const
const QList< StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
Definition qlist.h:81
static VideoTransformation streamTransformation(const AVStream *stream)
static bool colorTransferSupportsHdr(const AVStream *stream)
static void insertMediaData(QMediaMetaData &metaData, QPlatformMediaPlayer::TrackType trackType, const AVStream *stream)
static std::optional< TrackDuration > streamDuration(const AVStream &stream)
std::conditional_t< QT_FFMPEG_AVIO_WRITE_CONST, const uint8_t *, uint8_t * > AvioWriteBufferType
static QTransform displayMatrixToTransform(const int32_t *displayMatrix)
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
virtual bool isCancelled() const =0