Skip to content
Open
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
93 changes: 61 additions & 32 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 @@ -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");
Expand All @@ -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<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 +466,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
22 changes: 21 additions & 1 deletion 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 @@ -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);
Expand Down Expand Up @@ -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<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
2 changes: 2 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
Loading