Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 71 additions & 34 deletions src/core/Source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <QFileInfo>
#include <QImageReader>
#include <QSettings>
#include <QTimer>

namespace mmp {

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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");
Expand All @@ -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<void()> 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; y<getHeight(); y++)
Expand All @@ -434,9 +474,6 @@ bool Video::_generateThumbnail()
_icon = QIcon(QPixmap::fromImage(thumbnail).scaled(MM::MAPPING_LIST_ICON_SIZE, MM::MAPPING_LIST_ICON_SIZE,
Qt::IgnoreAspectRatio));

// Reset movie.
_impl->resetMovie();

return true;
}

Expand Down
44 changes: 42 additions & 2 deletions src/core/Source.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#include <QtGlobal>

#include <functional>
#include <string>
#include <QColor>
#include <QElapsedTimer>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand All @@ -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<void()> onReady, int attemptsLeft);

QString _uri;
QIcon _icon;
VideoType _videoType;
Expand Down
15 changes: 0 additions & 15 deletions src/core/VideoImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "VideoImpl.h"
#include <QElapsedTimer>
#include <QSettings>
#include <QDebug>

Expand Down Expand Up @@ -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;
}

}
3 changes: 0 additions & 3 deletions src/core/VideoImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
66 changes: 66 additions & 0 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<uid, Layer::ptr> layers = mappingManager->getSourceLayers(source);
for (QMap<uid, Layer::ptr>::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<QPointF> 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.
Expand Down Expand Up @@ -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<Video>(source).data(),
SIGNAL(frameSizeKnown(int, int, int)),
this, SLOT(autoFitVideoInputShapes(int, int, int)),
Qt::QueuedConnection);

// Add source item to sourceList widget.
QListWidgetItem* item = new QListWidgetItem(icon, name);
setItemId(*item, sourceId); // TODO: could possibly be replaced by a Source pointer
Expand Down
3 changes: 3 additions & 0 deletions src/gui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ private slots:
// Fits a Syphon source's input shapes to its real resolution once known.
void autoFitSyphonInputShapes(int sourceId, int width, int height);

// Fits a Video/Camera source's input shapes to its real resolution once known.
void autoFitVideoInputShapes(int sourceId, int width, int height);

void layerPropertyChanged(uid id, QString propertyName, QVariant value);
void sourcePropertyChanged(uid id, QString propertyName, QVariant value);

Expand Down