From b2219c66edebf877ef0955adfc32c136edea8be7 Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Thu, 11 May 2023 16:08:09 +0200 Subject: [PATCH] Crash fix when switching timelines containing video files. --- linphone-app/CMakeLists.txt | 2 + .../src/app/providers/ThumbnailProvider.cpp | 23 ++- .../src/app/providers/ThumbnailProvider.hpp | 29 ++- .../components/other/images/ImageModel.cpp | 193 +++++------------- .../components/other/images/ImageModel.hpp | 32 +-- .../other/images/VideoFrameGrabber.cpp | 141 +++++++++++++ .../other/images/VideoFrameGrabber.hpp | 66 ++++++ .../ui/modules/Linphone/File/FileView.qml | 2 +- 8 files changed, 304 insertions(+), 184 deletions(-) create mode 100644 linphone-app/src/components/other/images/VideoFrameGrabber.cpp create mode 100644 linphone-app/src/components/other/images/VideoFrameGrabber.hpp diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt index 63b6607db..e45a67115 100644 --- a/linphone-app/CMakeLists.txt +++ b/linphone-app/CMakeLists.txt @@ -261,6 +261,7 @@ set(SOURCES src/components/other/images/ImageModel.cpp src/components/other/images/ImageListModel.cpp src/components/other/images/ImageProxyModel.cpp + src/components/other/images/VideoFrameGrabber.cpp src/components/other/text-to-speech/TextToSpeech.cpp src/components/other/timeZone/TimeZoneModel.cpp src/components/other/timeZone/TimeZoneListModel.cpp @@ -402,6 +403,7 @@ set(HEADERS src/components/other/images/ImageModel.hpp src/components/other/images/ImageListModel.hpp src/components/other/images/ImageProxyModel.hpp + src/components/other/images/VideoFrameGrabber.hpp src/components/other/desktop-tools/DesktopTools.hpp src/components/other/text-to-speech/TextToSpeech.hpp src/components/other/timeZone/TimeZoneModel.hpp diff --git a/linphone-app/src/app/providers/ThumbnailProvider.cpp b/linphone-app/src/app/providers/ThumbnailProvider.cpp index dd0473f86..e6a3e7423 100644 --- a/linphone-app/src/app/providers/ThumbnailProvider.cpp +++ b/linphone-app/src/app/providers/ThumbnailProvider.cpp @@ -27,14 +27,21 @@ const QString ThumbnailProvider::ProviderId = "thumbnail"; -ThumbnailProvider::ThumbnailProvider () : QQuickImageProvider( - QQmlImageProviderBase::Image, - QQmlImageProviderBase::ForceAsynchronousImageLoading -) { +ThumbnailAsyncImageResponse::ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize) { + mPath = id; + connect(&mListener, &VideoFrameGrabberListener::imageGrabbed, this, &ThumbnailAsyncImageResponse::imageGrabbed); + ImageModel::retrieveImageAsync(id, &mListener); } -QImage ThumbnailProvider::requestImage (const QString &id, QSize *size, const QSize &) { - QImage image = ImageModel::createThumbnail(id); - *size = image.size(); - return image; +void ThumbnailAsyncImageResponse::imageGrabbed(QImage image) { + mImage = ImageModel::createThumbnail(mPath, image); + emit finished(); } + +QQuickTextureFactory *ThumbnailAsyncImageResponse::textureFactory() const { + return QQuickTextureFactory::textureFactoryForImage(mImage); +} +QQuickImageResponse *ThumbnailProvider::requestImageResponse(const QString &id, const QSize &requestedSize){ + ThumbnailAsyncImageResponse *response = new ThumbnailAsyncImageResponse(id, requestedSize); + return response; +} \ No newline at end of file diff --git a/linphone-app/src/app/providers/ThumbnailProvider.hpp b/linphone-app/src/app/providers/ThumbnailProvider.hpp index faa2fb59d..34ad4d313 100644 --- a/linphone-app/src/app/providers/ThumbnailProvider.hpp +++ b/linphone-app/src/app/providers/ThumbnailProvider.hpp @@ -21,17 +21,32 @@ #ifndef THUMBNAIL_PROVIDER_H_ #define THUMBNAIL_PROVIDER_H_ -#include +#include +#include "components/other/images/VideoFrameGrabber.hpp" + +// Thumbnails are created asynchronously with QQuickAsyncImageProvider and not QQuickImageProvider. +// This ensure to have async objects like QMediaPlayer and QAbstractVideoSurface while keeping them in the main thread (mandatory for VideoSurface). +// If not, there seems to have some deadlocks in Qt library when GUI objects are deleted while still playing media. // ============================================================================= - -class ThumbnailProvider : public QQuickImageProvider { +class ThumbnailAsyncImageResponse : public QQuickImageResponse { public: - ThumbnailProvider (); + ThumbnailAsyncImageResponse(const QString &id, const QSize &requestedSize); + + QQuickTextureFactory *textureFactory() const override; // Convert QImage into texture. If Image is null, then sourceSize will be egal to 0. So there will be no errors. + + void imageGrabbed(QImage image); + + QImage mImage; + QString mPath; + VideoFrameGrabberListener mListener; +}; - QImage requestImage (const QString &id, QSize *size, const QSize &requestedSize) override; - - static const QString ProviderId; +class ThumbnailProvider : public QQuickAsyncImageProvider { +public: + virtual QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override; + + static const QString ProviderId; }; #endif // THUMBNAIL_PROVIDER_H_ diff --git a/linphone-app/src/components/other/images/ImageModel.cpp b/linphone-app/src/components/other/images/ImageModel.cpp index 730aa3a23..3ecc417e4 100644 --- a/linphone-app/src/components/other/images/ImageModel.cpp +++ b/linphone-app/src/components/other/images/ImageModel.cpp @@ -30,76 +30,10 @@ #include "components/Components.hpp" #include "components/core/CoreManager.hpp" #include "utils/QExifImageHeader.hpp" +#include "VideoFrameGrabber.hpp" #include -VideoFrameGrabber::VideoFrameGrabber( QObject *parent) - : QAbstractVideoSurface(parent){ -} - -QList VideoFrameGrabber::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const { - Q_UNUSED(handleType); - return QList() - << QVideoFrame::Format_ARGB32 - << QVideoFrame::Format_ARGB32_Premultiplied - << QVideoFrame::Format_RGB32 - << QVideoFrame::Format_RGB24 - << QVideoFrame::Format_RGB565 - << QVideoFrame::Format_RGB555 - << QVideoFrame::Format_ARGB8565_Premultiplied - << QVideoFrame::Format_BGRA32 - << QVideoFrame::Format_BGRA32_Premultiplied - << QVideoFrame::Format_BGR32 - << QVideoFrame::Format_BGR24 - << QVideoFrame::Format_BGR565 - << QVideoFrame::Format_BGR555 - << QVideoFrame::Format_BGRA5658_Premultiplied - << QVideoFrame::Format_AYUV444 - << QVideoFrame::Format_AYUV444_Premultiplied - << QVideoFrame::Format_YUV444 - << QVideoFrame::Format_YUV420P - << QVideoFrame::Format_YV12 - << QVideoFrame::Format_UYVY - << QVideoFrame::Format_YUYV - << QVideoFrame::Format_NV12 - << QVideoFrame::Format_NV21 - << QVideoFrame::Format_IMC1 - << QVideoFrame::Format_IMC2 - << QVideoFrame::Format_IMC3 - << QVideoFrame::Format_IMC4 - << QVideoFrame::Format_Y8 - << QVideoFrame::Format_Y16 - << QVideoFrame::Format_Jpeg - << QVideoFrame::Format_CameraRaw - << QVideoFrame::Format_AdobeDng; -} - -bool VideoFrameGrabber::isFormatSupported(const QVideoSurfaceFormat &format) const { - const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat()); - const QSize size = format.frameSize(); - - return imageFormat != QImage::Format_Invalid - && !size.isEmpty() - && format.handleType() == QAbstractVideoBuffer::NoHandle; -} - -bool VideoFrameGrabber::start(const QVideoSurfaceFormat &format){ - return QAbstractVideoSurface::start(format); -} - -void VideoFrameGrabber::stop() { - QAbstractVideoSurface::stop(); -} - -bool VideoFrameGrabber::present(const QVideoFrame &frame){ - if (frame.isValid()) { - emit frameAvailable(frame.image()); - return true; - }else - return false; -} - - // ============================================================================= ImageModel::ImageModel (const QString& id, const QString& path, const QString& description, QObject * parent) : QObject(parent) { @@ -147,11 +81,50 @@ void ImageModel::setUrl(const QUrl& url){ setPath(url.toString(QUrl::RemoveScheme)); } -QImage ImageModel::createThumbnail(const QString& path){ +QImage ImageModel::createThumbnail(const QString& path, QImage originalImage){ + QImage thumbnail; + if (!originalImage.isNull()){ + int rotation = 0; + QExifImageHeader exifImageHeader; + if (exifImageHeader.loadFromJpeg(path)) + rotation = int(exifImageHeader.value(QExifImageHeader::ImageTag::Orientation).toShort()); + // Fill with color to replace transparency with white color instead of black (default). + QImage image(originalImage.size(), originalImage.format()); + image.fill(QColor(Qt::white).rgb()); + QPainter painter(&image); + painter.drawImage(0, 0, originalImage); + //-------------------- + double factor = image.width() / (double)image.height(); + Qt::AspectRatioMode aspectRatio = Qt::KeepAspectRatio; + if(factor < 0.2 || factor > 5) + aspectRatio = Qt::KeepAspectRatioByExpanding; + thumbnail = image.scaled( + Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight, + aspectRatio , Qt::SmoothTransformation + ); + if(aspectRatio == Qt::KeepAspectRatioByExpanding) + thumbnail = thumbnail.copy(0,0,Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight); + + if (rotation != 0) { + QTransform transform; + if (rotation == 3 || rotation == 4) + transform.rotate(180); + else if (rotation == 5 || rotation == 6) + transform.rotate(90); + else if (rotation == 7 || rotation == 8) + transform.rotate(-90); + thumbnail = thumbnail.transformed(transform); + if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7) + thumbnail = thumbnail.mirrored(true, false); + } + } + return thumbnail; +} + +void ImageModel::retrieveImageAsync(const QString& path, VideoFrameGrabberListener* requester){ QImage thumbnail; if(QFileInfo(path).isFile()){ QImage originalImage(path); - if( originalImage.isNull()){// Try to determine format from headers QImageReader reader(path); reader.setDecideFormatFromContent(true); @@ -159,83 +132,13 @@ QImage ImageModel::createThumbnail(const QString& path){ if(!format.isEmpty()) originalImage = QImage(path, format); else if(Utils::isVideo(path)){ - QObject context; - int mediaStep = 0; - QMediaPlayer player(&context); - VideoFrameGrabber grabber(&context); -// Media connections - QObject::connect(&player, QOverload::of(&QMediaPlayer::error), &context, [&context, &mediaStep, path](QMediaPlayer::Error error) mutable{ - mediaStep = -1; - }); - QObject::connect(&player, &QMediaPlayer::mediaStatusChanged, &context, [&context, &player, &mediaStep](QMediaPlayer::MediaStatus status) mutable{ - switch(status){ - case QMediaPlayer::LoadedMedia : if(mediaStep == 0){ - if( player.isVideoAvailable() ) - mediaStep = 1; - else - mediaStep = -1; - } - break; - case QMediaPlayer::UnknownMediaStatus: - case QMediaPlayer::InvalidMedia: - case QMediaPlayer::EndOfMedia: - mediaStep = -1; - break; - default:{} - } - }); - QObject::connect(&grabber, &VideoFrameGrabber::frameAvailable, &context, [&context,&originalImage, &player](QImage frame) mutable{ - originalImage = frame.copy(); - player.stop(); - }, Qt::DirectConnection); -// Processing - player.setVideoOutput(&grabber); - player.setMedia(QUrl::fromLocalFile(path)); - do{ - qApp->processEvents(); - if(mediaStep == 1){ - mediaStep = 2; - player.setPosition(player.duration() / 2); - player.play(); - } - }while(mediaStep >= 0 ); + VideoFrameGrabber *grabber = new VideoFrameGrabber(); + connect(grabber, &VideoFrameGrabber::grabFinished, requester, &VideoFrameGrabberListener::imageGrabbed); + grabber->requestFrame(path); } } - if (!originalImage.isNull()){ - int rotation = 0; - QExifImageHeader exifImageHeader; - if (exifImageHeader.loadFromJpeg(path)) - rotation = int(exifImageHeader.value(QExifImageHeader::ImageTag::Orientation).toShort()); -// Fill with color to replace transparency with white color instead of black (default). - QImage image(originalImage.size(), originalImage.format()); - image.fill(QColor(Qt::white).rgb()); - QPainter painter(&image); - painter.drawImage(0, 0, originalImage); -//-------------------- - double factor = image.width() / (double)image.height(); - Qt::AspectRatioMode aspectRatio = Qt::KeepAspectRatio; - if(factor < 0.2 || factor > 5) - aspectRatio = Qt::KeepAspectRatioByExpanding; - thumbnail = image.scaled( - Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight, - aspectRatio , Qt::SmoothTransformation - ); - if(aspectRatio == Qt::KeepAspectRatioByExpanding) - thumbnail = thumbnail.copy(0,0,Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight); - - if (rotation != 0) { - QTransform transform; - if (rotation == 3 || rotation == 4) - transform.rotate(180); - else if (rotation == 5 || rotation == 6) - transform.rotate(90); - else if (rotation == 7 || rotation == 8) - transform.rotate(-90); - thumbnail = thumbnail.transformed(transform); - if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7) - thumbnail = thumbnail.mirrored(true, false); - } + if(!originalImage.isNull()){ + emit requester->imageGrabbed(originalImage); } } - return thumbnail; -} \ No newline at end of file +} diff --git a/linphone-app/src/components/other/images/ImageModel.hpp b/linphone-app/src/components/other/images/ImageModel.hpp index 114f74cd3..dce74b17b 100644 --- a/linphone-app/src/components/other/images/ImageModel.hpp +++ b/linphone-app/src/components/other/images/ImageModel.hpp @@ -29,35 +29,20 @@ #include "utils/LinphoneEnums.hpp" #include +#include - -class VideoFrameGrabber : public QAbstractVideoSurface { - Q_OBJECT - public: - VideoFrameGrabber(QObject *parent = 0); - - QList supportedPixelFormats( - QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const override; - bool isFormatSupported(const QVideoSurfaceFormat &format) const override; - - bool start(const QVideoSurfaceFormat &format) override; - void stop() override; - bool present(const QVideoFrame &frame) override; - - signals: - void frameAvailable(QImage frame); -}; +class VideoFrameGrabberListener; class ImageModel : public QObject { - Q_OBJECT - + Q_OBJECT + public: - ImageModel (const QString& id, const QString& path, const QString& description, QObject * parent = nullptr); + ImageModel (const QString& id, const QString& path, const QString& description, QObject * parent = nullptr); Q_PROPERTY(QString path MEMBER mPath WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QString description MEMBER mDescription WRITE setDescription NOTIFY descriptionChanged) Q_PROPERTY(QString id MEMBER mId NOTIFY idChanged) - + QString getPath() const; QString getDescription() const; QString getId() const; @@ -66,13 +51,14 @@ public: void setDescription(const QString& description); Q_INVOKABLE void setUrl(const QUrl& url); - static QImage createThumbnail(const QString& path); + static QImage createThumbnail(const QString& path, QImage originalImage); // Build the thumbnail from an image. + static void retrieveImageAsync(const QString& path, VideoFrameGrabberListener* requester); // Get an image from the path. When it is ready, the signal imageGrabbed() is send to the listener. It can be direct if this is not a media file. signals: void pathChanged(); void descriptionChanged(); void idChanged(); - + private: QString mId; QString mPath; diff --git a/linphone-app/src/components/other/images/VideoFrameGrabber.cpp b/linphone-app/src/components/other/images/VideoFrameGrabber.cpp new file mode 100644 index 000000000..ec828c923 --- /dev/null +++ b/linphone-app/src/components/other/images/VideoFrameGrabber.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "VideoFrameGrabber.hpp" + +#include + +VideoFrameGrabberListener::VideoFrameGrabberListener(){ +} + +VideoFrameGrabber::VideoFrameGrabber( QObject *parent) + : QAbstractVideoSurface(parent){ + QObject::connect(&player, QOverload::of(&QMediaPlayer::error), this, [this](QMediaPlayer::Error error) mutable{ + end(); + }, Qt::DirectConnection); + QObject::connect(&player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) mutable{ + switch(status){ + case QMediaPlayer::LoadedMedia : if(!mLoadedMedia){ + mLoadedMedia = true; + if( player.isVideoAvailable() ){ + player.setPosition(player.duration() / 2); + player.play(); + }else{ + end(); + } + } + break; + case QMediaPlayer::UnknownMediaStatus: + case QMediaPlayer::InvalidMedia: + case QMediaPlayer::EndOfMedia: + case QMediaPlayer::NoMedia: + end(); + break; + default:{} + } + }, Qt::DirectConnection); + QObject::connect(this, &VideoFrameGrabber::frameAvailable, this, [this](QImage frame) mutable{ + mResult = frame.copy(); + player.setMedia(QUrl()); + }, Qt::DirectConnection); + + player.setVideoOutput(this); +} + + +void VideoFrameGrabber::requestFrame(const QString& path){ + mLoadedMedia = false; + mPath = path; + player.setMedia(QUrl::fromLocalFile(mPath)); +} + +void VideoFrameGrabber::end(){ + if(player.mediaStatus() != QMediaPlayer::NoMedia){ + player.setMedia(QUrl()); + }else if(!mResultSent){// Avoid sending multiple times before destroying the object + mResultSent = true; + emit grabFinished(mResult); + deleteLater(); + } +} + +QList VideoFrameGrabber::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const { + Q_UNUSED(handleType); + return QList() + << QVideoFrame::Format_ARGB32 + << QVideoFrame::Format_ARGB32_Premultiplied + << QVideoFrame::Format_RGB32 + << QVideoFrame::Format_RGB24 + << QVideoFrame::Format_RGB565 + << QVideoFrame::Format_RGB555 + << QVideoFrame::Format_ARGB8565_Premultiplied + << QVideoFrame::Format_BGRA32 + << QVideoFrame::Format_BGRA32_Premultiplied + << QVideoFrame::Format_BGR32 + << QVideoFrame::Format_BGR24 + << QVideoFrame::Format_BGR565 + << QVideoFrame::Format_BGR555 + << QVideoFrame::Format_BGRA5658_Premultiplied + << QVideoFrame::Format_AYUV444 + << QVideoFrame::Format_AYUV444_Premultiplied + << QVideoFrame::Format_YUV444 + << QVideoFrame::Format_YUV420P + << QVideoFrame::Format_YV12 + << QVideoFrame::Format_UYVY + << QVideoFrame::Format_YUYV + << QVideoFrame::Format_NV12 + << QVideoFrame::Format_NV21 + << QVideoFrame::Format_IMC1 + << QVideoFrame::Format_IMC2 + << QVideoFrame::Format_IMC3 + << QVideoFrame::Format_IMC4 + << QVideoFrame::Format_Y8 + << QVideoFrame::Format_Y16 + << QVideoFrame::Format_Jpeg + << QVideoFrame::Format_CameraRaw + << QVideoFrame::Format_AdobeDng; +} + +bool VideoFrameGrabber::isFormatSupported(const QVideoSurfaceFormat &format) const { + const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat()); + const QSize size = format.frameSize(); + + return imageFormat != QImage::Format_Invalid + && !size.isEmpty() + && format.handleType() == QAbstractVideoBuffer::NoHandle; +} + +bool VideoFrameGrabber::start(const QVideoSurfaceFormat &format){ + return QAbstractVideoSurface::start(format); +} + +void VideoFrameGrabber::stop() { + QAbstractVideoSurface::stop(); +} + +bool VideoFrameGrabber::present(const QVideoFrame &frame){ + if (frame.isValid()) { + emit frameAvailable(frame.image()); + return true; + }else + return false; +} + + diff --git a/linphone-app/src/components/other/images/VideoFrameGrabber.hpp b/linphone-app/src/components/other/images/VideoFrameGrabber.hpp new file mode 100644 index 000000000..e7e0fc8d8 --- /dev/null +++ b/linphone-app/src/components/other/images/VideoFrameGrabber.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef VIDEO_FRAME_GRABBER_H +#define VIDEO_FRAME_GRABBER_H + +#include +#include + +// Call VideoFrameGrabber::requestFrame() and wait for imageGrabbed() to get the image. +// You will need to link your listener with connect(grabber, &VideoFrameGrabber::grabFinished, listener, &VideoFrameGrabberListener::imageGrabbed); + +class VideoFrameGrabberListener: public QObject{ + Q_OBJECT +public: + VideoFrameGrabberListener(); +signals: + void imageGrabbed(QImage image); +}; + +class VideoFrameGrabber : public QAbstractVideoSurface { + Q_OBJECT +public: + VideoFrameGrabber(QObject *parent = 0); + + void requestFrame(const QString& path); // Function to call. + + void end(); + + QList supportedPixelFormats( + QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const override; + bool isFormatSupported(const QVideoSurfaceFormat &format) const override; + + bool start(const QVideoSurfaceFormat &format) override; + void stop() override; + bool present(const QVideoFrame &frame) override; + + QMediaPlayer player; + bool mLoadedMedia = false; + bool mResultSent = false; + QString mPath; + QImage mResult; + +signals: + void frameAvailable(QImage frame); + void grabFinished(QImage frame); +}; + +#endif diff --git a/linphone-app/ui/modules/Linphone/File/FileView.qml b/linphone-app/ui/modules/Linphone/File/FileView.qml index c16c5d742..a1a7d577c 100644 --- a/linphone-app/ui/modules/Linphone/File/FileView.qml +++ b/linphone-app/ui/modules/Linphone/File/FileView.qml @@ -255,7 +255,7 @@ Item { id: waitingProvider anchors.fill: parent - sourceComponent: thumbnailProvider.sourceComponent == thumbnailImage && thumbnailProvider.item.status != Image.Ready + sourceComponent: thumbnailProvider.sourceComponent == thumbnailImage && (thumbnailProvider.item.status != Image.Ready || thumbnailProvider.item.sourceSize.height == 0) ? extension : undefined states: State {