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