diff --git a/src/core/Source.cpp b/src/core/Source.cpp index 53805359..421196bd 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(); @@ -250,12 +252,14 @@ void Video::build() int Video::getWidth() const { - return this->_impl->getWidth(); + int w = this->_impl->getWidth(); + return w > 0 ? w : DEFAULT_WIDTH; } int Video::getHeight() const { - return this->_impl->getHeight(); + int h = this->_impl->getHeight(); + return h > 0 ? h : DEFAULT_HEIGHT; } void Video::update() { @@ -354,17 +358,46 @@ 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 firstFrameMaxAttempts = FIRST_FRAME_TIMEOUT / THUMBNAIL_POLL_INTERVAL; + const int thumbnailMaxAttempts = 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]() { + emit frameSizeKnown(getId(), getWidth(), getHeight()); + _emitPropertyChanged("icon"); + }, firstFrameMaxAttempts); } - - 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, thumbnailMaxAttempts]() { + // The first frame just arrived, so getWidth()/getHeight() now report + // the real resolution instead of the DEFAULT_WIDTH/HEIGHT placeholder. + emit frameSizeKnown(getId(), getWidth(), getHeight()); + + // 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"); + }, thumbnailMaxAttempts); + } + else + { + _impl->resetMovie(); + _emitPropertyChanged("icon"); + } + }, firstFrameMaxAttempts); } _emitPropertyChanged("uri"); @@ -386,37 +419,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..e5e44c3b 100644 --- a/src/core/Source.h +++ b/src/core/Source.h @@ -24,6 +24,7 @@ #include +#include #include #include #include @@ -329,9 +330,26 @@ class Video : public Texture Q_PROPERTY(double rate READ getRate WRITE setRate) public: - // Thumbnail generation timeout (in ms). + // Thumbnail generation timeout (in ms): how long to wait for the *seeked* + // frame once _generateThumbnail() has asked to seek to it. Fine to give up + // quickly here — worst case is a generic icon instead of a real thumbnail. static const int ICON_TIMEOUT = 1000; + // How long to wait for the *first* decoded frame (gates both the thumbnail + // attempt and frameSizeKnown, i.e. shape auto-fit). Much more generous than + // ICON_TIMEOUT: giving up here permanently strands any shape created before + // the deadline at the DEFAULT_WIDTH/HEIGHT placeholder, which is a real + // mis-crop, not just a missing thumbnail. + static const int FIRST_FRAME_TIMEOUT = 15000; + + // How often to re-check for a decoded frame while polling (in ms). + static const int THUMBNAIL_POLL_INTERVAL = 50; + + /// Frame size assumed before the first frame arrives (drives a new mapping's + /// initial input shape; auto-fitted once the real resolution is known). + static const int DEFAULT_WIDTH = 640; + static const int DEFAULT_HEIGHT = 480; + public: Q_INVOKABLE Video(int id=NULL_UID); Video(const QString uri_, VideoType type, double rate, uid id=NULL_UID); @@ -385,6 +403,12 @@ class Video : public Texture virtual QIcon getIcon() const { return _icon; } +signals: + /// Emitted once, the first time a real frame's resolution becomes known, so + /// the UI can fit input shapes created (at the default size) before any + /// frame had arrived. + void frameSizeKnown(int sourceId, int width, int height); + protected: /// Starts playback. @@ -393,9 +417,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..968d20aa 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) @@ -820,6 +822,61 @@ void MainWindow::autoFitSyphonInputShapes(int sourceId, int width, int height) #endif } +void MainWindow::autoFitVideoInputShapes(int sourceId, int width, int height) +{ + if (width <= 0 || height <= 0) + return; + + Source::ptr source = mappingManager->getSourceById(sourceId); + if (source.isNull() || source->getSourceType() != SourceType::Video) + return; + + const qreal defW = Video::DEFAULT_WIDTH; + const qreal defH = Video::DEFAULT_HEIGHT; + const qreal sx = width / defW; + const qreal sy = height / defH; + const qreal eps = 1.0; + + bool changed = false; + QMap layers = mappingManager->getSourceLayers(source); + for (QMap::const_iterator it = layers.constBegin(); + it != layers.constEnd(); ++it) + { + Layer::ptr layer = it.value(); + if (layer.isNull() || !layer->hasInputShape()) + continue; + + MShape::ptr input = layer->getInputShape(); + QVector verts = input->getVertices(); + if (verts.isEmpty()) + continue; + + // Only rescale shapes still at the untouched default size, so we never + // clobber a shape the user has already adjusted. + qreal minX = verts[0].x(), maxX = verts[0].x(); + qreal minY = verts[0].y(), maxY = verts[0].y(); + for (const QPointF& v : verts) + { + minX = qMin(minX, v.x()); maxX = qMax(maxX, v.x()); + minY = qMin(minY, v.y()); maxY = qMax(maxY, v.y()); + } + if (qAbs((maxX - minX) - defW) > eps || qAbs((maxY - minY) - defH) > eps) + continue; + + for (QPointF& v : verts) + v = QPointF(v.x() * sx, v.y() * sy); + input->setVertices(verts); + input->build(); + changed = true; + } + + if (changed) + { + updateMappers(); + updateCanvases(); + } +} + void MainWindow::addMesh() { // A source must be selected to add a mapping. @@ -3166,6 +3223,15 @@ void MainWindow::addSourceItem(uid sourceId, const QIcon& icon, const QString& n Qt::QueuedConnection); #endif + // Fit input shapes once a Video/Camera source's real resolution becomes + // known (it may still be the DEFAULT_WIDTH/HEIGHT placeholder if a shape + // gets added before the first frame has arrived). + if (sourceType == SourceType::Video) + connect(qSharedPointerCast