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