diff --git a/src/core/Source.cpp b/src/core/Source.cpp index 5b3fcf95..421196bd 100644 --- a/src/core/Source.cpp +++ b/src/core/Source.cpp @@ -252,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() { @@ -362,18 +364,24 @@ bool Video::setUri(const QString &uri) // also the thread Qt Multimedia needs free to deliver those frames. _setFallbackIcon(); - const int maxAttempts = ICON_TIMEOUT / THUMBNAIL_POLL_INTERVAL; + const int firstFrameMaxAttempts = FIRST_FRAME_TIMEOUT / THUMBNAIL_POLL_INTERVAL; + const int thumbnailMaxAttempts = ICON_TIMEOUT / THUMBNAIL_POLL_INTERVAL; if (_videoType == VIDEO_WEBCAM) { // No thumbnail to generate for a camera: just confirm the feed comes up. _pollForBits([this]() { + emit frameSizeKnown(getId(), getWidth(), getHeight()); _emitPropertyChanged("icon"); - }, maxAttempts); + }, firstFrameMaxAttempts); } else { - _pollForBits([this, maxAttempts]() { + _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)) { @@ -382,14 +390,14 @@ bool Video::setUri(const QString &uri) qDebug() << "Could not generate thumbnail for " << _uri << ": using generic icon." << Qt::endl; _impl->resetMovie(); _emitPropertyChanged("icon"); - }, maxAttempts); + }, thumbnailMaxAttempts); } else { _impl->resetMovie(); _emitPropertyChanged("icon"); } - }, maxAttempts); + }, firstFrameMaxAttempts); } _emitPropertyChanged("uri"); diff --git a/src/core/Source.h b/src/core/Source.h index 9f75f0a2..e5e44c3b 100644 --- a/src/core/Source.h +++ b/src/core/Source.h @@ -330,12 +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); @@ -389,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. diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 98ee59e0..968d20aa 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -822,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. @@ -3168,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