diff --git a/src/core/Source.cpp b/src/core/Source.cpp index 53805359..5b3fcf95 100644 --- a/src/core/Source.cpp +++ b/src/core/Source.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace mmp { @@ -206,6 +207,7 @@ void Image::_doPlay() /* Implementation of the Video class */ Video::Video(int id) : Texture(id), _uri(""), + _videoType(VIDEO_URI), _impl(nullptr) { _impl = new VideoPlayerImpl(); @@ -354,17 +356,40 @@ bool Video::setUri(const QString &uri) // Set uri. _uri = uri; - // Try to get thumbnail. - // Wait for the first samples to be available to make sure we are ready. - if (!_impl->waitForNextBits(ICON_TIMEOUT)) + // Show a generic icon right away. A real thumbnail (video files only) is + // filled in below, asynchronously, once frames actually start arriving: + // we cannot wait for it here without blocking the GUI thread, which is + // also the thread Qt Multimedia needs free to deliver those frames. + _setFallbackIcon(); + + const int maxAttempts = ICON_TIMEOUT / THUMBNAIL_POLL_INTERVAL; + + if (_videoType == VIDEO_WEBCAM) { - qDebug() << "No bits coming" << Qt::endl; - return false; + // No thumbnail to generate for a camera: just confirm the feed comes up. + _pollForBits([this]() { + _emitPropertyChanged("icon"); + }, maxAttempts); } - - if (_videoType != VIDEO_WEBCAM) { // Generated thumbnail if source type is not camera - if (!_generateThumbnail()) - qDebug() << "Could not generate thumbnail for " << uri << ": using generic icon." << Qt::endl; + else + { + _pollForBits([this, maxAttempts]() { + // Try seeking to the middle of the movie for a representative frame. + if (_impl->seekTo(0.5)) + { + _pollForBits([this]() { + if (!_generateThumbnail()) + qDebug() << "Could not generate thumbnail for " << _uri << ": using generic icon." << Qt::endl; + _impl->resetMovie(); + _emitPropertyChanged("icon"); + }, maxAttempts); + } + else + { + _impl->resetMovie(); + _emitPropertyChanged("icon"); + } + }, maxAttempts); } _emitPropertyChanged("uri"); @@ -386,37 +411,44 @@ void Video::_doPause() _impl->setPlayState(false); } -bool Video::_generateThumbnail() +void Video::_setFallbackIcon() { static QFileIconProvider provider; - // Default (in case seeking and loading don't work). _icon = provider.icon(QFileInfo(_uri)); - if (_icon.isNull()) { - if (_uri.startsWith(QString("/dev/video"))) { - _icon = QIcon(":/add-camera"); - } - else { - _icon = QIcon(":/add-video"); - } - } + if (_icon.isNull()) + _icon = (_videoType == VIDEO_WEBCAM) ? QIcon(":/add-camera") : QIcon(":/add-video"); +} - // Try seeking to the middle of the movie. - if (!_impl->seekTo(0.5)) +void Video::_pollForBits(std::function onReady, int attemptsLeft) +{ + if (_impl->hasBits() && _impl->bitsHaveChanged()) { - _impl->resetMovie(); - return false; + onReady(); + return; } - // Try to get a sample from the current position. - // NOTE: There is no guarantee the sample has yet been acquired. - const uchar* bits; - if (!_impl->waitForNextBits(ICON_TIMEOUT, &bits)) + if (attemptsLeft <= 0) { - qDebug() << "Second waiting wrong..." << Qt::endl; - return false; + qDebug() << "No bits coming for " << _uri << Qt::endl; + return; } + // Re-check shortly, giving the Qt event loop a chance to actually run and + // deliver the frame we're waiting for. + QTimer::singleShot(THUMBNAIL_POLL_INTERVAL, this, [this, onReady, attemptsLeft]() { + _pollForBits(onReady, attemptsLeft - 1); + }); +} + +bool Video::_generateThumbnail() +{ + // Assumes the caller (_pollForBits()'s onReady callback) has already + // confirmed a fresh frame is available. + const uchar* bits = _impl->getBits(); + if (!bits) + return false; + // Copy bits into thumbnail QImage. QImage thumbnail(getWidth(), getHeight(), QImage::Format_ARGB32); for (int y=0; yresetMovie(); - return true; } diff --git a/src/core/Source.h b/src/core/Source.h index 5f40202f..9f75f0a2 100644 --- a/src/core/Source.h +++ b/src/core/Source.h @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -332,6 +333,9 @@ class Video : public Texture // Thumbnail generation timeout (in ms). static const int ICON_TIMEOUT = 1000; + // How often to re-check for a decoded frame while polling (in ms). + static const int THUMBNAIL_POLL_INTERVAL = 50; + public: Q_INVOKABLE Video(int id=NULL_UID); Video(const QString uri_, VideoType type, double rate, uid id=NULL_UID); @@ -393,9 +397,25 @@ class Video : public Texture /// Pauses playback. virtual void _doPause(); - // Try to generate a thumbnail from currently loaded movie. + // Sets a generic fallback icon (file icon, or a generic video/camera icon). + void _setFallbackIcon(); + + // Builds a thumbnail icon from the frame currently held by _impl. Assumes + // the caller has already confirmed a fresh frame is available (see + // _pollForBits()). bool _generateThumbnail(); + // Polls (via the Qt event loop, never blocking) until _impl has a fresh + // frame, then calls onReady. Gives up silently after attemptsLeft tries. + // + // Qt Multimedia delivers frames asynchronously through the event loop of + // the thread that created the QMediaPlayer/QVideoSink (here, the GUI + // thread), so this cannot be replaced by a synchronous/blocking wait: + // blocking the GUI thread also blocks the delivery of the very frame being + // waited for, and starves the media backend's own event processing while + // doing so. See the removed VideoImpl::waitForNextBits(). + void _pollForBits(std::function onReady, int attemptsLeft); + QString _uri; QIcon _icon; VideoType _videoType; diff --git a/src/core/VideoImpl.cpp b/src/core/VideoImpl.cpp index 8cca8322..6da9b55a 100644 --- a/src/core/VideoImpl.cpp +++ b/src/core/VideoImpl.cpp @@ -19,7 +19,6 @@ * along with this program. If not, see . */ #include "VideoImpl.h" -#include #include #include @@ -152,18 +151,4 @@ void VideoImpl::update() void VideoImpl::lockMutex() { _mutex.lock(); } void VideoImpl::unlockMutex() { _mutex.unlock(); } -bool VideoImpl::waitForNextBits(int timeout, const uchar** bits) -{ - QElapsedTimer timer; - timer.start(); - while (timer.elapsed() < timeout) { - if (hasBits() && bitsHaveChanged()) { - if (bits) - *bits = getBits(); - return true; - } - } - return false; -} - } diff --git a/src/core/VideoImpl.h b/src/core/VideoImpl.h index 410a6970..01c66675 100644 --- a/src/core/VideoImpl.h +++ b/src/core/VideoImpl.h @@ -108,9 +108,6 @@ class VideoImpl /// Unlocks mutex. void unlockMutex(); - /// Blocks until new bits are available (up to timeout ms). Returns false on timeout. - bool waitForNextBits(int timeout, const uchar** bits = nullptr); - protected: virtual void freeResources(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c1414c62..98ee59e0 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -370,6 +370,8 @@ void MainWindow::sourcePropertyChanged(uid id, QString propertyName, QVariant va QListWidgetItem* sourceItem = getItemFromId(*sourceList, id); if (propertyName == "name") sourceItem->setText(source->getName()); + else if (propertyName == "icon") + sourceItem->setIcon(source->getIcon()); } void MainWindow::closeEvent(QCloseEvent *event)