Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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
5
8#include "qffmpegioutils_p.h"
9#include "qiodevice.h"
10#include "qdatetime.h"
11#include "qloggingcategory.h"
12
13#include <math.h>
14#include <optional>
15
16extern "C" {
17#include "libavutil/display.h"
18}
19
21
22static Q_LOGGING_CATEGORY(qLcMediaDataHolder, "qt.multimedia.ffmpeg.mediadataholder")
23
24namespace QFFmpeg {
25
26static std::optional<qint64> streamDuration(const AVStream &stream)
27{
28 const auto &factor = stream.time_base;
29
30 if (stream.duration > 0 && factor.num > 0 && factor.den > 0) {
31 return qint64(1000000) * stream.duration * factor.num / factor.den;
32 }
33
34 // In some cases ffmpeg reports negative duration that is definitely invalid.
35 // However, the correct duration may be read from the metadata.
36
37 if (stream.duration < 0) {
38 qCWarning(qLcMediaDataHolder) << "AVStream duration" << stream.duration
39 << "is invalid. Taking it from the metadata";
40 }
41
42 if (const auto duration = av_dict_get(stream.metadata, "DURATION", nullptr, 0)) {
43 const auto time = QTime::fromString(QString::fromUtf8(duration->value));
44 return qint64(1000) * time.msecsSinceStartOfDay();
45 }
46
47 return {};
48}
49
50static int streamOrientation(const AVStream *stream)
51{
53
54 using SideDataSize = decltype(AVPacketSideData::size);
55 constexpr SideDataSize displayMatrixSize = sizeof(int32_t) * 9;
56 const auto *sideData = streamSideData(stream, AV_PKT_DATA_DISPLAYMATRIX);
57 if (!sideData || sideData->size < displayMatrixSize)
58 return 0;
59
60 auto displayMatrix = reinterpret_cast<const int32_t *>(sideData->data);
61 auto rotation = static_cast<int>(std::round(av_display_rotation_get(displayMatrix)));
62 // Convert counterclockwise rotation angle to clockwise, restricted to 0, 90, 180 and 270
63 if (rotation % 90 != 0)
64 return 0;
65 return rotation < 0 ? -rotation % 360 : -rotation % 360 + 360;
66}
67
68
69static bool colorTransferSupportsHdr(const AVStream *stream)
70{
71 if (!stream)
72 return false;
73
74 const AVCodecParameters *codecPar = stream->codecpar;
75 if (!codecPar)
76 return false;
77
78 const QVideoFrameFormat::ColorTransfer colorTransfer = fromAvColorTransfer(codecPar->color_trc);
79
80 // Assume that content is using HDR if the color transfer supports high
81 // dynamic range. The video may still not utilize the extended range,
82 // but we can't determine the actual range without decoding frames.
83 return colorTransfer == QVideoFrameFormat::ColorTransfer_ST2084
85}
86
87QtVideo::Rotation MediaDataHolder::rotation() const
88{
89 int orientation = m_metaData.value(QMediaMetaData::Orientation).toInt();
90 return static_cast<QtVideo::Rotation>(orientation);
91}
92
93AVFormatContext *MediaDataHolder::avContext()
94{
95 return m_context.get();
96}
97
98int MediaDataHolder::currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
99{
100 return m_currentAVStreamIndex[trackType];
101}
102
103static void insertMediaData(QMediaMetaData &metaData, QPlatformMediaPlayer::TrackType trackType,
104 const AVStream *stream)
105{
107 const auto *codecPar = stream->codecpar;
108
109 switch (trackType) {
111 metaData.insert(QMediaMetaData::VideoBitRate, (int)codecPar->bit_rate);
114 codecPar->codec_id)));
115 metaData.insert(QMediaMetaData::Resolution, QSize(codecPar->width, codecPar->height));
117 qreal(stream->avg_frame_rate.num) / qreal(stream->avg_frame_rate.den));
118 metaData.insert(QMediaMetaData::Orientation, QVariant::fromValue(streamOrientation(stream)));
119 metaData.insert(QMediaMetaData::HasHdrContent, colorTransferSupportsHdr(stream));
120 break;
122 metaData.insert(QMediaMetaData::AudioBitRate, (int)codecPar->bit_rate);
125 codecPar->codec_id)));
126 break;
127 default:
128 break;
129 }
130};
131
132QPlatformMediaPlayer::TrackType MediaDataHolder::trackTypeFromMediaType(int mediaType)
133{
134 switch (mediaType) {
135 case AVMEDIA_TYPE_AUDIO:
137 case AVMEDIA_TYPE_VIDEO:
139 case AVMEDIA_TYPE_SUBTITLE:
141 default:
143 }
144}
145
146namespace {
147QMaybe<AVFormatContextUPtr, MediaDataHolder::ContextError>
148loadMedia(const QUrl &mediaUrl, QIODevice *stream, const std::shared_ptr<ICancelToken> &cancelToken)
149{
151
152 AVFormatContextUPtr context{ avformat_alloc_context() };
153
154 if (stream) {
155 if (!stream->isOpen()) {
156 if (!stream->open(QIODevice::ReadOnly))
157 return MediaDataHolder::ContextError{
158 QMediaPlayer::ResourceError, QLatin1String("Could not open source device.")
159 };
160 }
161 if (!stream->isSequential())
162 stream->seek(0);
163
164 constexpr int bufferSize = 32768;
165 unsigned char *buffer = (unsigned char *)av_malloc(bufferSize);
166 context->pb = avio_alloc_context(buffer, bufferSize, false, stream, &readQIODevice, nullptr,
167 &seekQIODevice);
168 }
169
170 AVDictionaryHolder dict;
171 constexpr auto NetworkTimeoutUs = "5000000";
172 av_dict_set(dict, "timeout", NetworkTimeoutUs, 0);
173
174 const QByteArray protocolWhitelist = qgetenv("QT_FFMPEG_PROTOCOL_WHITELIST");
175 if (!protocolWhitelist.isNull())
176 av_dict_set(dict, "protocol_whitelist", protocolWhitelist.data(), 0);
177
178 context->interrupt_callback.opaque = cancelToken.get();
179 context->interrupt_callback.callback = [](void *opaque) {
180 const auto *cancelToken = static_cast<const ICancelToken *>(opaque);
181 if (cancelToken && cancelToken->isCancelled())
182 return 1;
183 return 0;
184 };
185
186 int ret = 0;
187 {
188 AVFormatContext *contextRaw = context.release();
189 ret = avformat_open_input(&contextRaw, url.constData(), nullptr, dict);
190 context.reset(contextRaw);
191 }
192
193 if (ret < 0) {
194 auto code = QMediaPlayer::ResourceError;
195 if (ret == AVERROR(EACCES))
197 else if (ret == AVERROR(EINVAL))
199
200 return MediaDataHolder::ContextError{ code, QMediaPlayer::tr("Could not open file") };
201 }
202
203 ret = avformat_find_stream_info(context.get(), nullptr);
204 if (ret < 0) {
205 return MediaDataHolder::ContextError{
207 QMediaPlayer::tr("Could not find stream information for media file")
208 };
209 }
210
211#ifndef QT_NO_DEBUG
212 av_dump_format(context.get(), 0, url.constData(), 0);
213#endif
214 return context;
215}
216
217} // namespace
218
219MediaDataHolder::Maybe MediaDataHolder::create(const QUrl &url, QIODevice *stream,
220 const std::shared_ptr<ICancelToken> &cancelToken)
221{
222 QMaybe context = loadMedia(url, stream, cancelToken);
223 if (context) {
224 // MediaDataHolder is wrapped in a shared pointer to interop with signal/slot mechanism
225 return QSharedPointer<MediaDataHolder>{ new MediaDataHolder{ std::move(context.value()), cancelToken } };
226 }
227 return context.error();
228}
229
230MediaDataHolder::MediaDataHolder(AVFormatContextUPtr context,
231 const std::shared_ptr<ICancelToken> &cancelToken)
232 : m_cancelToken{ cancelToken }
233{
235
236 m_context = std::move(context);
237 m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);
238
239 for (unsigned int i = 0; i < m_context->nb_streams; ++i) {
240
241 const auto *stream = m_context->streams[i];
242 const auto trackType = trackTypeFromMediaType(stream->codecpar->codec_type);
243
244 if (trackType == QPlatformMediaPlayer::NTrackTypes)
245 continue;
246
247 if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC)
248 continue; // Ignore attached picture streams because we treat them as metadata
249
250 auto metaData = QFFmpegMetaData::fromAVMetaData(stream->metadata);
251 const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
252
253 if (trackType != QPlatformMediaPlayer::SubtitleStream) {
254 insertMediaData(metaData, trackType, stream);
255
256 if (isDefault && m_requestedStreams[trackType] < 0)
257 m_requestedStreams[trackType] = m_streamMap[trackType].size();
258 }
259
260 if (auto duration = streamDuration(*stream)) {
261 m_duration = qMax(m_duration, *duration);
262 metaData.insert(QMediaMetaData::Duration, *duration / qint64(1000));
263 }
264
265 m_streamMap[trackType].append({ (int)i, isDefault, metaData });
266 }
267
268 // With some media files, streams may be lacking duration info. Let's
269 // get it from ffmpeg's duration estimation instead.
270 if (m_duration == 0 && m_context->duration > 0ll) {
271 m_duration = m_context->duration;
272 }
273
274 for (auto trackType :
276 auto &requestedStream = m_requestedStreams[trackType];
277 auto &streamMap = m_streamMap[trackType];
278
279 if (requestedStream < 0 && !streamMap.empty())
280 requestedStream = 0;
281
282 if (requestedStream >= 0)
283 m_currentAVStreamIndex[trackType] = streamMap[requestedStream].avStreamIndex;
284 }
285
286 updateMetaData();
287}
288
289namespace {
290
298QImage getAttachedPicture(const AVFormatContext *context)
299{
300 if (!context)
301 return {};
302
303 for (unsigned int i = 0; i < context->nb_streams; ++i) {
304 const AVStream* stream = context->streams[i];
305 if (!stream || !(stream->disposition & AV_DISPOSITION_ATTACHED_PIC))
306 continue;
307
308 const AVPacket *compressedImage = &stream->attached_pic;
309 if (!compressedImage || !compressedImage->data || compressedImage->size <= 0)
310 continue;
311
312 // Feed raw compressed data to QImage::fromData, which will decompress it
313 // if it is a recognized format.
314 QImage image = QImage::fromData({ compressedImage->data, compressedImage->size });
315 if (!image.isNull())
316 return image;
317 }
318
319 return {};
320}
321
322}
323
324void MediaDataHolder::updateMetaData()
325{
326 m_metaData = {};
327
328 if (!m_context)
329 return;
330
331 m_metaData = QFFmpegMetaData::fromAVMetaData(m_context->metadata);
332 m_metaData.insert(QMediaMetaData::FileFormat,
334 m_context->iformat)));
335 m_metaData.insert(QMediaMetaData::Duration, m_duration / qint64(1000));
336
337 if (!m_cachedThumbnail.has_value())
338 m_cachedThumbnail = getAttachedPicture(m_context.get());
339
340 if (!m_cachedThumbnail->isNull())
341 m_metaData.insert(QMediaMetaData::ThumbnailImage, m_cachedThumbnail.value());
342
343 for (auto trackType :
345 const auto streamIndex = m_currentAVStreamIndex[trackType];
346 if (streamIndex >= 0)
347 insertMediaData(m_metaData, trackType, m_context->streams[streamIndex]);
348 }
349}
350
351bool MediaDataHolder::setActiveTrack(QPlatformMediaPlayer::TrackType type, int streamNumber)
352{
353 if (!m_context)
354 return false;
355
356 if (streamNumber < 0 || streamNumber >= m_streamMap[type].size())
357 streamNumber = -1;
358 if (m_requestedStreams[type] == streamNumber)
359 return false;
360 m_requestedStreams[type] = streamNumber;
361 const int avStreamIndex = m_streamMap[type].value(streamNumber).avStreamIndex;
362
363 const int oldIndex = m_currentAVStreamIndex[type];
364 qCDebug(qLcMediaDataHolder) << ">>>>> change track" << type << "from" << oldIndex << "to"
365 << avStreamIndex;
366
367 // TODO: maybe add additional verifications
368 m_currentAVStreamIndex[type] = avStreamIndex;
369
370 updateMetaData();
371
372 return true;
373}
374
375int MediaDataHolder::activeTrack(QPlatformMediaPlayer::TrackType type) const
376{
377 return type < QPlatformMediaPlayer::NTrackTypes ? m_requestedStreams[type] : -1;
378}
379
380const QList<MediaDataHolder::StreamInfo> &MediaDataHolder::streamInfo(
381 QPlatformMediaPlayer::TrackType trackType) const
382{
384
385 return m_streamMap[trackType];
386}
387
388} // namespace QFFmpeg
389
\inmodule QtCore
Definition qbytearray.h:57
static QMediaFormat::VideoCodec videoCodecForAVCodecId(AVCodecID id)
static QMediaFormat::FileFormat fileFormatForAVInputFormat(const AVInputFormat *format)
static QMediaFormat::AudioCodec audioCodecForAVCodecId(AVCodecID id)
static QMediaMetaData fromAVMetaData(const AVDictionary *tags)
\inmodule QtCore \reentrant
Definition qiodevice.h:34
\inmodule QtGui
Definition qimage.h:37
static QImage fromData(QByteArrayView data, const char *format=nullptr)
Definition qimage.cpp:3841
\inmodule QtMultimedia
Q_INVOKABLE void insert(Key k, const QVariant &value)
\qmlmethod void QtMultimedia::mediaMetaData::insert(Key k, variant value) Inserts a value into a Key:...
\inmodule QtCore
Definition qsize.h:25
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:6018
QByteArray toUtf8() const &
Definition qstring.h:634
constexpr int msecsSinceStartOfDay() const
Returns the number of msecs since the start of the day, i.e.
Definition qdatetime.h:244
\inmodule QtCore
Definition qurl.h:94
@ PreferLocalFile
Definition qurl.h:114
QString toString(FormattingOptions options=FormattingOptions(PrettyDecoded)) const
Returns a string representation of the URL.
Definition qurl.cpp:2831
static auto fromValue(T &&value) noexcept(std::is_nothrow_copy_constructible_v< T > &&Private::CanUseInternalSpace< T >) -> std::enable_if_t< std::conjunction_v< std::is_copy_constructible< T >, std::is_destructible< T > >, QVariant >
Definition qvariant.h:536
ColorTransfer
\value ColorTransfer_Unknown The color transfer function is unknown.
Combined button and popup list for selecting options.
Definition image.cpp:4
static void * context
EGLStreamKHR stream
#define Q_LOGGING_CATEGORY(name,...)
#define qCWarning(category,...)
#define qCDebug(category,...)
return ret
constexpr const T & qMax(const T &a, const T &b)
Definition qminmax.h:42
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLenum GLuint buffer
GLenum type
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
QLatin1StringView QLatin1String
Definition qstringfwd.h:31
Q_CORE_EXPORT QByteArray qgetenv(const char *varName)
long long qint64
Definition qtypes.h:60
double qreal
Definition qtypes.h:187
QUrl url("example.com")
[constructor-url-reference]