aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/CMakeLists.txt1
-rw-r--r--core/imagedownloader.cpp123
-rw-r--r--core/imagedownloader.h11
-rw-r--r--core/pref.h3
-rw-r--r--core/subsurface-qt/SettingsObjectWrapper.cpp52
-rw-r--r--core/subsurface-qt/SettingsObjectWrapper.h28
-rw-r--r--core/subsurfacestartup.c4
-rw-r--r--core/videoframeextractor.cpp120
-rw-r--r--core/videoframeextractor.h37
-rw-r--r--desktop-widgets/preferences/preferences_defaults.cpp27
-rw-r--r--desktop-widgets/preferences/preferences_defaults.h2
-rw-r--r--desktop-widgets/preferences/preferences_defaults.ui75
-rw-r--r--icons/video_overlay.svg263
-rw-r--r--subsurface.qrc1
14 files changed, 727 insertions, 20 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index dbaa3bbe5..416c1ea5b 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -97,6 +97,7 @@ set(SUBSURFACE_CORE_LIB_SRCS
uemis.c
uemis-downloader.c
version.c
+ videoframeextractor.cpp
windowtitleupdate.cpp
worldmap-save.c
diff --git a/core/imagedownloader.cpp b/core/imagedownloader.cpp
index 7dc35695d..d78409a3c 100644
--- a/core/imagedownloader.cpp
+++ b/core/imagedownloader.cpp
@@ -4,6 +4,7 @@
#include "divelist.h"
#include "qthelper.h"
#include "imagedownloader.h"
+#include "videoframeextractor.h"
#include "qt-models/divepicturemodel.h"
#include "metadata.h"
#include <unistd.h>
@@ -96,7 +97,7 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS
if (type == MEDIATYPE_IO_ERROR)
return { failImage, MEDIATYPE_IO_ERROR, 0 };
else if (type == MEDIATYPE_VIDEO)
- return addVideoThumbnailToCache(originalFilename, md.duration);
+ return fetchVideoThumbnail(filename, originalFilename, md.duration);
// Try if Qt can parse this image. If it does, use this as a thumbnail.
QImage thumb(filename);
@@ -110,7 +111,7 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS
// Try to check for a video-file extension. Since we couldn't parse the video file,
// we pass 0 as the duration.
if (hasVideoFileExtension(filename))
- return addVideoThumbnailToCache(originalFilename, {0} );
+ return fetchVideoThumbnail(filename, originalFilename, {0} );
// Give up: we simply couldn't determine what this thing is.
// But since we managed to read this file, mark this file in the cache as unknown.
@@ -163,9 +164,22 @@ static QImage renderIcon(const char *id, int size)
return res;
}
+// As renderIcon, but render to a fixed width and scale height accordingly
+// and have a transparent background.
+static QImage renderIconWidth(const char *id, int size)
+{
+ QSvgRenderer svg{QString(id)};
+ QSize svgSize = svg.defaultSize();
+ QImage res(size, size * svgSize.height() / svgSize.width(), QImage::Format_ARGB32);
+ QPainter painter(&res);
+ svg.render(&painter);
+ return res;
+}
+
Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon
dummyImage(renderIcon(":camera-icon", maxThumbnailSize())),
videoImage(renderIcon(":video-icon", maxThumbnailSize())),
+ videoOverlayImage(renderIconWidth(":video-overlay", maxThumbnailSize())),
unknownImage(renderIcon(":unknown-icon", maxThumbnailSize()))
{
// Currently, we only process one image at a time. Stefan Fuchs reported problems when
@@ -173,6 +187,9 @@ Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailS
pool.setMaxThreadCount(1);
connect(ImageDownloader::instance(), &ImageDownloader::loaded, this, &Thumbnailer::imageDownloaded);
connect(ImageDownloader::instance(), &ImageDownloader::failed, this, &Thumbnailer::imageDownloadFailed);
+ connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::extracted, this, &Thumbnailer::frameExtracted);
+ connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::failed, this, &Thumbnailer::frameExtractionFailed);
+ connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::failed, this, &Thumbnailer::frameExtractionInvalid);
}
Thumbnailer *Thumbnailer::instance()
@@ -188,7 +205,17 @@ Thumbnailer::Thumbnail Thumbnailer::getPictureThumbnailFromStream(QDataStream &s
return { res, MEDIATYPE_PICTURE, 0 };
}
-Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &stream)
+void Thumbnailer::markVideoThumbnail(QImage &img)
+{
+ QSize size = img.size();
+ QImage marker = videoOverlayImage.scaledToWidth(size.width());
+ marker = marker.copy(0, (marker.size().height() - size.height()) / 2, size.width(), size.height());
+ QPainter painter(&img);
+ painter.drawImage(0, 0, marker);
+}
+
+Q_DECLARE_METATYPE(duration_t)
+Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &stream, const QString &filename)
{
quint32 duration, numPics;
stream >> duration >> numPics;
@@ -200,16 +227,27 @@ Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &str
if (stream.status() != QDataStream::Ok || duration > 36000 || numPics > 10000)
return { QImage(), MEDIATYPE_VIDEO, 0 };
+ // If the file didn't contain an image, but user turned on thumbnail extraction, schedule thumbnail
+ // for extraction. TODO: save failure to extract thumbnails to disk so that thumbnailing
+ // is not repeated ad-nauseum for broken images.
+ if (numPics == 0 && prefs.extract_video_thumbnails) {
+ QMetaObject::invokeMethod(VideoFrameExtractor::instance(), "extract", Qt::AutoConnection,
+ Q_ARG(QString, filename), Q_ARG(QString, filename), Q_ARG(duration_t, duration_t{(int32_t)duration}));
+ }
+
// Currently, we support only one picture
QImage res;
if (numPics > 0) {
quint32 offset;
- QImage res;
stream >> offset >> res;
}
- // No picture -> show dummy-icon
- return { res.isNull() ? videoImage : res, MEDIATYPE_VIDEO, (int32_t)duration };
+ if (res.isNull())
+ res = videoImage; // No picture -> show dummy-icon
+ else
+ markVideoThumbnail(res); // We got an image -> place our video marker on top of it
+
+ return { res, MEDIATYPE_VIDEO, (int32_t)duration };
}
// Fetch a thumbnail from cache.
@@ -248,13 +286,14 @@ Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture
switch (type) {
case MEDIATYPE_PICTURE: return getPictureThumbnailFromStream(stream);
- case MEDIATYPE_VIDEO: return getVideoThumbnailFromStream(stream);
+ case MEDIATYPE_VIDEO: return getVideoThumbnailFromStream(stream, picture_filename);
case MEDIATYPE_UNKNOWN: return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
default: return { QImage(), MEDIATYPE_UNKNOWN, 0 };
}
}
-Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration)
+Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration,
+ const QImage &image, duration_t position)
{
// The format of video thumbnails:
// uint32 MEDIATYPE_VIDEO
@@ -270,12 +309,36 @@ Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &pict
stream << (quint32)MEDIATYPE_VIDEO;
stream << (quint32)duration.seconds;
- stream << (quint32)0; // Currently, we don't support extraction of images
+
+ if (image.isNull()) {
+ // No image provided
+ stream << (quint32)0;
+ } else {
+ // Currently, we support at most one image
+ stream << (quint32)1;
+ stream << (quint32)position.seconds;
+ stream << image;
+ }
+
file.commit();
}
return { videoImage, MEDIATYPE_VIDEO, duration };
}
+Thumbnailer::Thumbnail Thumbnailer::fetchVideoThumbnail(const QString &filename, const QString &originalFilename, duration_t duration)
+{
+ if (prefs.extract_video_thumbnails) {
+ // Video-thumbnailing is enabled. Fetch thumbnail in background thread and in the meanwhile
+ // return a dummy image.
+ QMetaObject::invokeMethod(VideoFrameExtractor::instance(), "extract", Qt::AutoConnection,
+ Q_ARG(QString, originalFilename), Q_ARG(QString, filename), Q_ARG(duration_t, duration));
+ return { videoImage, MEDIATYPE_VIDEO, duration };
+ } else {
+ // Video-thumbnailing is disabled. Write a thumbnail without picture.
+ return addVideoThumbnailToCache(originalFilename, duration, QImage(), {0});
+ }
+}
+
Thumbnailer::Thumbnail Thumbnailer::addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail)
{
// The format of a picture-thumbnail is very simple:
@@ -304,6 +367,44 @@ Thumbnailer::Thumbnail Thumbnailer::addUnknownThumbnailToCache(const QString &pi
return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
}
+void Thumbnailer::frameExtracted(QString filename, QImage thumbnail, duration_t duration, duration_t offset)
+{
+ if (thumbnail.isNull()) {
+ frameExtractionFailed(filename, duration);
+ return;
+ } else {
+ int size = maxThumbnailSize();
+ thumbnail = thumbnail.scaled(size, size, Qt::KeepAspectRatio);
+ markVideoThumbnail(thumbnail);
+ addVideoThumbnailToCache(filename, duration, thumbnail, offset);
+ QMutexLocker l(&lock);
+ workingOn.remove(filename);
+ emit thumbnailChanged(filename, thumbnail, duration);
+ }
+}
+
+// If frame extraction failed, don't show an error image, because we don't want
+// to penalize users that haven't installed ffmpe. Simply remove this item from
+// the work-queue.
+void Thumbnailer::frameExtractionFailed(QString filename, duration_t duration)
+{
+ // Frame extraction failed, but this was due to ffmpeg not starting
+ // add to the thumbnail cache as a video image with unknown thumbnail.
+ addVideoThumbnailToCache(filename, duration, QImage(), { 0 });
+ QMutexLocker l(&lock);
+ workingOn.remove(filename);
+}
+
+void Thumbnailer::frameExtractionInvalid(QString filename, duration_t)
+{
+ // Frame extraction failed because ffmpeg could not parse the file.
+ // For now, let's mark this as an unknown file. The user may want
+ // to recalculate thumbnails with an updated ffmpeg binary..?
+ addUnknownThumbnailToCache(filename);
+ QMutexLocker l(&lock);
+ workingOn.remove(filename);
+}
+
void Thumbnailer::recalculate(QString filename)
{
Thumbnail thumbnail = getHashedImage(filename, true);
@@ -380,6 +481,10 @@ void Thumbnailer::calculateThumbnails(const QVector<QString> &filenames)
void Thumbnailer::clearWorkQueue()
{
+ // We also want to clear the working-queue of the video-frame-extractor so that
+ // we don't get thumbnails that we don't care about.
+ VideoFrameExtractor::instance()->clearWorkQueue();
+
QMutexLocker l(&lock);
for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
it->cancel();
diff --git a/core/imagedownloader.h b/core/imagedownloader.h
index cab945e5f..e0d70f59f 100644
--- a/core/imagedownloader.h
+++ b/core/imagedownloader.h
@@ -46,6 +46,9 @@ public:
public slots:
void imageDownloaded(QString filename);
void imageDownloadFailed(QString filename);
+ void frameExtracted(QString filename, QImage thumbnail, duration_t duration, duration_t offset);
+ void frameExtractionFailed(QString filename, duration_t duration);
+ void frameExtractionInvalid(QString filename, duration_t duration);
signals:
void thumbnailChanged(QString filename, QImage thumbnail, duration_t duration);
private:
@@ -56,22 +59,26 @@ private:
};
Thumbnailer();
+ Thumbnail fetchVideoThumbnail(const QString &filename, const QString &originalFilename, duration_t duration);
+ Thumbnail extractVideoThumbnail(const QString &picture_filename, duration_t duration);
Thumbnail addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail);
- Thumbnail addVideoThumbnailToCache(const QString &picture_filename, duration_t duration);
+ Thumbnail addVideoThumbnailToCache(const QString &picture_filename, duration_t duration, const QImage &thumbnail, duration_t position);
Thumbnail addUnknownThumbnailToCache(const QString &picture_filename);
void recalculate(QString filename);
void processItem(QString filename, bool tryDownload);
Thumbnail getThumbnailFromCache(const QString &picture_filename);
Thumbnail getPictureThumbnailFromStream(QDataStream &stream);
- Thumbnail getVideoThumbnailFromStream(QDataStream &stream);
+ Thumbnail getVideoThumbnailFromStream(QDataStream &stream, const QString &filename);
Thumbnail fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload);
Thumbnail getHashedImage(const QString &filename, bool tryDownload);
+ void markVideoThumbnail(QImage &img);
mutable QMutex lock;
QThreadPool pool;
QImage failImage; // Shown when image-fetching fails
QImage dummyImage; // Shown before thumbnail is fetched
QImage videoImage; // Place holder for videos
+ QImage videoOverlayImage; // Overlay for video thumbnails
QImage unknownImage; // Place holder for files where we couldn't determine the type
QMap<QString,QFuture<void>> workingOn;
diff --git a/core/pref.h b/core/pref.h
index bae7992fe..a037a2da5 100644
--- a/core/pref.h
+++ b/core/pref.h
@@ -103,6 +103,9 @@ struct preferences {
// ********** General **********
bool auto_recalculate_thumbnails;
+ bool extract_video_thumbnails;
+ int extract_video_thumbnails_position; // position in stream: 0=first 100=last second
+ const char *ffmpeg_executable; // path of ffmpeg binary
int defaultsetpoint; // default setpoint in mbar
const char *default_cylinder;
const char *default_filename;
diff --git a/core/subsurface-qt/SettingsObjectWrapper.cpp b/core/subsurface-qt/SettingsObjectWrapper.cpp
index 3085216c7..da2a86a68 100644
--- a/core/subsurface-qt/SettingsObjectWrapper.cpp
+++ b/core/subsurface-qt/SettingsObjectWrapper.cpp
@@ -1474,6 +1474,21 @@ bool GeneralSettingsObjectWrapper::autoRecalculateThumbnails() const
return prefs.auto_recalculate_thumbnails;
}
+bool GeneralSettingsObjectWrapper::extractVideoThumbnails() const
+{
+ return prefs.extract_video_thumbnails;
+}
+
+int GeneralSettingsObjectWrapper::extractVideoThumbnailsPosition() const
+{
+ return prefs.extract_video_thumbnails_position;
+}
+
+QString GeneralSettingsObjectWrapper::ffmpegExecutable() const
+{
+ return prefs.ffmpeg_executable;
+}
+
void GeneralSettingsObjectWrapper::setDefaultFilename(const QString& value)
{
if (value == prefs.default_filename)
@@ -1579,6 +1594,43 @@ void GeneralSettingsObjectWrapper::setAutoRecalculateThumbnails(bool value)
emit autoRecalculateThumbnailsChanged(value);
}
+void GeneralSettingsObjectWrapper::setExtractVideoThumbnails(bool value)
+{
+ if (value == prefs.extract_video_thumbnails)
+ return;
+
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("extract_video_thumbnails", value);
+ prefs.extract_video_thumbnails = value;
+ emit extractVideoThumbnailsChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setExtractVideoThumbnailsPosition(int value)
+{
+ if (value == prefs.extract_video_thumbnails_position)
+ return;
+
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("extract_video_thumbnails_position", value);
+ prefs.extract_video_thumbnails_position = value;
+ emit extractVideoThumbnailsPositionChanged(value);
+}
+
+void GeneralSettingsObjectWrapper::setFfmpegExecutable(const QString &value)
+{
+ if (value == prefs.ffmpeg_executable)
+ return;
+
+ QSettings s;
+ s.beginGroup(group);
+ s.setValue("ffmpeg_executable", value);
+ free((void *)prefs.ffmpeg_executable);
+ prefs.ffmpeg_executable = copy_qstring(value);
+ emit ffmpegExecutableChanged(value);
+}
+
LanguageSettingsObjectWrapper::LanguageSettingsObjectWrapper(QObject *parent) :
QObject(parent)
{
diff --git a/core/subsurface-qt/SettingsObjectWrapper.h b/core/subsurface-qt/SettingsObjectWrapper.h
index cd30cb1ac..a7957f9f1 100644
--- a/core/subsurface-qt/SettingsObjectWrapper.h
+++ b/core/subsurface-qt/SettingsObjectWrapper.h
@@ -435,14 +435,17 @@ private:
class GeneralSettingsObjectWrapper : public QObject {
Q_OBJECT
- Q_PROPERTY(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged)
- Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged)
- Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged)
- Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged)
- Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged)
- Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
- Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged)
- Q_PROPERTY(bool auto_recalculate_thumbnails READ autoRecalculateThumbnails WRITE setAutoRecalculateThumbnails NOTIFY autoRecalculateThumbnailsChanged)
+ Q_PROPERTY(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged)
+ Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged)
+ Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged)
+ Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged)
+ Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged)
+ Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
+ Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged)
+ Q_PROPERTY(bool auto_recalculate_thumbnails READ autoRecalculateThumbnails WRITE setAutoRecalculateThumbnails NOTIFY autoRecalculateThumbnailsChanged)
+ Q_PROPERTY(bool extract_video_thumbnails READ extractVideoThumbnails WRITE setExtractVideoThumbnails NOTIFY extractVideoThumbnailsChanged)
+ Q_PROPERTY(int extract_video_thumbnails_position READ extractVideoThumbnailsPosition WRITE setExtractVideoThumbnailsPosition NOTIFY extractVideoThumbnailsPositionChanged)
+ Q_PROPERTY(QString ffmpeg_executable READ ffmpegExecutable WRITE setFfmpegExecutable NOTIFY ffmpegExecutableChanged)
public:
GeneralSettingsObjectWrapper(QObject *parent);
@@ -454,6 +457,9 @@ public:
int o2Consumption() const;
int pscrRatio() const;
bool autoRecalculateThumbnails() const;
+ bool extractVideoThumbnails() const;
+ int extractVideoThumbnailsPosition() const;
+ QString ffmpegExecutable() const;
public slots:
void setDefaultFilename (const QString& value);
@@ -464,6 +470,9 @@ public slots:
void setO2Consumption (int value);
void setPscrRatio (int value);
void setAutoRecalculateThumbnails (bool value);
+ void setExtractVideoThumbnails (bool value);
+ void setExtractVideoThumbnailsPosition (int value);
+ void setFfmpegExecutable (const QString &value);
signals:
void defaultFilenameChanged(const QString& value);
@@ -474,6 +483,9 @@ signals:
void o2ConsumptionChanged(int value);
void pscrRatioChanged(int value);
void autoRecalculateThumbnailsChanged(int value);
+ void extractVideoThumbnailsChanged(bool value);
+ void extractVideoThumbnailsPositionChanged(int value);
+ void ffmpegExecutableChanged(const QString &value);
private:
const QString group = QStringLiteral("GeneralSettings");
};
diff --git a/core/subsurfacestartup.c b/core/subsurfacestartup.c
index d5c44d267..2bb905217 100644
--- a/core/subsurfacestartup.c
+++ b/core/subsurfacestartup.c
@@ -100,6 +100,8 @@ struct preferences default_prefs = {
.cloud_timeout = 5,
#endif
.auto_recalculate_thumbnails = true,
+ .extract_video_thumbnails = true,
+ .extract_video_thumbnails_position = 20, // The first fifth seems like a reasonable place
};
int run_survey;
@@ -287,6 +289,7 @@ void setup_system_prefs(void)
subsurface_OS_pref_setup();
default_prefs.divelist_font = strdup(system_divelist_default_font);
default_prefs.font_size = system_divelist_default_font_size;
+ default_prefs.ffmpeg_executable = strdup("ffmpeg");
#if !defined(SUBSURFACE_MOBILE)
default_prefs.default_filename = copy_string(system_default_filename());
@@ -331,6 +334,7 @@ void copy_prefs(struct preferences *src, struct preferences *dest)
dest->facebook.access_token = copy_string(src->facebook.access_token);
dest->facebook.user_id = copy_string(src->facebook.user_id);
dest->facebook.album_id = copy_string(src->facebook.album_id);
+ dest->ffmpeg_executable = copy_string(src->ffmpeg_executable);
}
/*
diff --git a/core/videoframeextractor.cpp b/core/videoframeextractor.cpp
new file mode 100644
index 000000000..c4e16a81b
--- /dev/null
+++ b/core/videoframeextractor.cpp
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "videoframeextractor.h"
+#include "imagedownloader.h"
+#include "core/pref.h"
+#include "core/dive.h" // for report_error()!
+
+#include <QtConcurrent>
+#include <QProcess>
+
+// Note: this is a global instead of a function-local variable on purpose.
+// We don't want this to be generated in a different thread context if
+// VideoFrameExtractor::instance() is called from a worker thread.
+static VideoFrameExtractor frameExtractor;
+VideoFrameExtractor *VideoFrameExtractor::instance()
+{
+ return &frameExtractor;
+}
+
+VideoFrameExtractor::VideoFrameExtractor()
+{
+ // Currently, we only process one video at a time.
+ // Eventually, we might want to increase this value.
+ pool.setMaxThreadCount(1);
+}
+
+void VideoFrameExtractor::extract(QString originalFilename, QString filename, duration_t duration)
+{
+ QMutexLocker l(&lock);
+ if (!workingOn.contains(originalFilename)) {
+ // We are not currently extracting this video - add it to the list.
+ workingOn.insert(originalFilename, QtConcurrent::run(&pool, [this, originalFilename, filename, duration]()
+ { processItem(originalFilename, filename, duration); }));
+ }
+}
+
+void VideoFrameExtractor::fail(const QString &originalFilename, duration_t duration, bool isInvalid)
+{
+ if (isInvalid)
+ emit invalid(originalFilename, duration);
+ else
+ emit failed(originalFilename, duration);
+ QMutexLocker l(&lock);
+ workingOn.remove(originalFilename);
+}
+
+void VideoFrameExtractor::clearWorkQueue()
+{
+ QMutexLocker l(&lock);
+ for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
+ it->cancel();
+ workingOn.clear();
+}
+
+// Trivial helper: bring value into given range
+template <typename T>
+T clamp(T v, T lo, T hi)
+{
+ return v < lo ? lo : v > hi ? hi : v;
+}
+
+void VideoFrameExtractor::processItem(QString originalFilename, QString filename, duration_t duration)
+{
+ // If video frame extraction is turned off (e.g. because we failed to start ffmpeg),
+ // abort immediately.
+ if (!prefs.extract_video_thumbnails) {
+ QMutexLocker l(&lock);
+ workingOn.remove(originalFilename);
+ return;
+ }
+
+ // Determine the time where we want to extract the image.
+ // If the duration is < 10 sec, just snap the first frame
+ duration_t position = { 0 };
+ if (duration.seconds > 10) {
+ // We round to second-precision. To be sure that we don't attempt reading past the
+ // video's end, round down by one second.
+ --duration.seconds;
+ position.seconds = clamp(duration.seconds * prefs.extract_video_thumbnails_position / 100,
+ 0, duration.seconds);
+ }
+ QString posString = QString("%1:%2:%3").arg(position.seconds / 3600, 2, 10, QChar('0'))
+ .arg((position.seconds % 3600) / 60, 2, 10, QChar('0'))
+ .arg(position.seconds % 60, 2, 10, QChar('0'));
+
+ QProcess ffmpeg;
+ ffmpeg.start(prefs.ffmpeg_executable, QStringList {
+ "-ss", posString, "-i", filename, "-vframes", "1", "-q:v", "2", "-f", "image2", "-"
+ });
+ if (!ffmpeg.waitForStarted()) {
+ // Since we couldn't sart ffmpeg, turn off thumbnailing
+ // TODO: call the proper preferences-functions
+ prefs.extract_video_thumbnails = false;
+ report_error(qPrintable(tr("ffmpeg failed to start - video thumbnail creation suspended")));
+ qDebug() << "Failed to start ffmpeg";
+ return fail(originalFilename, duration, false);
+ }
+ if (!ffmpeg.waitForFinished()) {
+ qDebug() << "Failed waiting for ffmpeg";
+ report_error(qPrintable(tr("failed waiting for ffmpeg - video thumbnail creation suspended")));
+ return fail(originalFilename, duration, false);
+ }
+
+ QByteArray data = ffmpeg.readAll();
+ QImage img;
+ img.loadFromData(data);
+ if (img.isNull()) {
+ qInfo() << "Failed reading ffmpeg output";
+ // For debugging:
+ //qInfo() << "stdout: " << QString::fromUtf8(data);
+ ffmpeg.setReadChannel(QProcess::StandardError);
+ // For debugging:
+ //QByteArray stderr_output = ffmpeg.readAll();
+ //qInfo() << "stderr: " << QString::fromUtf8(stderr_output);
+ return fail(originalFilename, duration, true);
+ }
+
+ emit extracted(originalFilename, img, duration, position);
+ QMutexLocker l(&lock);
+ workingOn.remove(originalFilename);
+}
diff --git a/core/videoframeextractor.h b/core/videoframeextractor.h
new file mode 100644
index 000000000..26650af8d
--- /dev/null
+++ b/core/videoframeextractor.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef VIDEOFRAMEEXTRACTOR_H
+#define VIDEOFRAMEEXTRACTOR_H
+
+#include "core/units.h"
+
+#include <QMutex>
+#include <QFuture>
+#include <QThreadPool>
+#include <QQueue>
+#include <QString>
+#include <QPair>
+
+class VideoFrameExtractor : public QObject {
+ Q_OBJECT
+public:
+ VideoFrameExtractor();
+ static VideoFrameExtractor *instance();
+signals:
+ void extracted(QString filename, QImage, duration_t duration, duration_t offset);
+ // There are two failure modes:
+ // failed() -> we failed to start ffmpeg. Write a thumbnail signalling "maybe try again".
+ // invalid() -> we started ffmpeg, but that couldn't extract an image. Signal "this file is broken".
+ void failed(QString filename, duration_t duration);
+ void invalid(QString filename, duration_t duration);
+public slots:
+ void extract(QString originalFilename, QString filename, duration_t duration);
+ void clearWorkQueue();
+private:
+ void processItem(QString originalFilename, QString filename, duration_t duration);
+ void fail(const QString &originalFilename, duration_t duration, bool isInvalid);
+ mutable QMutex lock;
+ QThreadPool pool;
+ QMap<QString, QFuture<void>> workingOn;
+};
+
+#endif
diff --git a/desktop-widgets/preferences/preferences_defaults.cpp b/desktop-widgets/preferences/preferences_defaults.cpp
index a7b5e4c1d..4d8aef610 100644
--- a/desktop-widgets/preferences/preferences_defaults.cpp
+++ b/desktop-widgets/preferences/preferences_defaults.cpp
@@ -43,6 +43,22 @@ void PreferencesDefaults::on_localDefaultFile_toggled(bool toggle)
ui->chooseFile->setEnabled(toggle);
}
+void PreferencesDefaults::on_ffmpegFile_clicked()
+{
+ QFileInfo fi(system_default_filename());
+ QString ffmpegFileName = QFileDialog::getOpenFileName(this, tr("Select ffmpeg executable"));
+
+ if (!ffmpegFileName.isEmpty())
+ ui->ffmpegExecutable->setText(ffmpegFileName);
+}
+
+void PreferencesDefaults::on_extractVideoThumbnails_toggled(bool toggled)
+{
+ ui->videoThumbnailPosition->setEnabled(toggled);
+ ui->ffmpegExecutable->setEnabled(toggled);
+ ui->ffmpegFile->setEnabled(toggled);
+}
+
void PreferencesDefaults::refreshSettings()
{
ui->font->setCurrentFont(QString(prefs.divelist_font));
@@ -73,6 +89,14 @@ void PreferencesDefaults::refreshSettings()
ui->defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
ui->btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
ui->chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
+
+ ui->videoThumbnailPosition->setEnabled(prefs.extract_video_thumbnails);
+ ui->ffmpegExecutable->setEnabled(prefs.extract_video_thumbnails);
+ ui->ffmpegFile->setEnabled(prefs.extract_video_thumbnails);
+
+ ui->extractVideoThumbnails->setChecked(prefs.extract_video_thumbnails);
+ ui->videoThumbnailPosition->setValue(prefs.extract_video_thumbnails_position);
+ ui->ffmpegExecutable->setText(prefs.ffmpeg_executable);
}
void PreferencesDefaults::syncSettings()
@@ -87,6 +111,9 @@ void PreferencesDefaults::syncSettings()
general->setDefaultFileBehavior(LOCAL_DEFAULT_FILE);
else if (ui->cloudDefaultFile->isChecked())
general->setDefaultFileBehavior(CLOUD_DEFAULT_FILE);
+ general->setExtractVideoThumbnails(ui->extractVideoThumbnails->isChecked());
+ general->setExtractVideoThumbnailsPosition(ui->videoThumbnailPosition->value());
+ general->setFfmpegExecutable(ui->ffmpegExecutable->text());
auto display = qPrefDisplay::instance();
display->set_divelist_font(ui->font->currentFont().toString());
diff --git a/desktop-widgets/preferences/preferences_defaults.h b/desktop-widgets/preferences/preferences_defaults.h
index 1674dd2ad..d7bb7db7f 100644
--- a/desktop-widgets/preferences/preferences_defaults.h
+++ b/desktop-widgets/preferences/preferences_defaults.h
@@ -20,6 +20,8 @@ public slots:
void on_chooseFile_clicked();
void on_btnUseDefaultFile_toggled(bool toggled);
void on_localDefaultFile_toggled(bool toggled);
+ void on_ffmpegFile_clicked();
+ void on_extractVideoThumbnails_toggled(bool toggled);
private:
Ui::PreferencesDefaults *ui;
diff --git a/desktop-widgets/preferences/preferences_defaults.ui b/desktop-widgets/preferences/preferences_defaults.ui
index 1a54d7b0a..731eed6ab 100644
--- a/desktop-widgets/preferences/preferences_defaults.ui
+++ b/desktop-widgets/preferences/preferences_defaults.ui
@@ -73,7 +73,7 @@
<item>
<widget class="QRadioButton" name="noDefaultFile">
<property name="text">
- <string>No default file</string>
+ <string>&amp;No default file</string>
</property>
</widget>
</item>
@@ -210,6 +210,79 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="groupBox_10">
+ <property name="title">
+ <string>Video thumbnails</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="horizontalSpacing">
+ <number>5</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>5</number>
+ </property>
+ <property name="margin">
+ <number>5</number>
+ </property>
+ <item row="1" column="0">
+ <widget class="QLabel" name="ffmpegExectuableLabel">
+ <property name="text">
+ <string>ffmpeg executable</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_3b">
+ <item>
+ <widget class="QLineEdit" name="ffmpegExecutable"/>
+ </item>
+ <item>
+ <widget class="QToolButton" name="ffmpegFile">
+ <property name="text">
+ <string>...</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="videoThumbnailPositionLabel">
+ <property name="text">
+ <string>Extract at position</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QSlider" name="videoThumbnailPosition">
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>20</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="extractVideoThumbnailsLabel">
+ <property name="text">
+ <string>Extract video thumbnails</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="extractVideoThumbnails">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<widget class="QGroupBox" name="groupBox_9">
<property name="title">
<string>Clear all settings</string>
diff --git a/icons/video_overlay.svg b/icons/video_overlay.svg
new file mode 100644
index 000000000..87ad6a6e3
--- /dev/null
+++ b/icons/video_overlay.svg
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="66"
+ width="22"
+ id="svg25756"
+ version="1.1"
+ viewBox="0 0 22 66"
+ sodipodi:docname="video_overlay.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1015"
+ id="namedview870"
+ showgrid="false"
+ inkscape:zoom="12.242424"
+ inkscape:cx="11"
+ inkscape:cy="33"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg25756" />
+ <metadata
+ id="metadata25760">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs3051">
+ <style
+ id="current-color-scheme"
+ type="text/css">
+ .ColorScheme-Text {
+ color:#4d4d4d;
+ }
+ </style>
+ </defs>
+ <path
+ id="path1012"
+ d="M 1.6913554e-16,4.7920842e-9 H 2.7632416 V 66 H 0 Z"
+ style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ id="path904"
+ d="m 9,31 v 4 l 4,-2 z"
+ style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:bevel;stroke-linecap:round" />
+ <path
+ id="path888"
+ d="M 0.69081079,39.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,36.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path916" />
+ <path
+ id="path920"
+ d="M 0.69081079,33.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,30.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path924" />
+ <path
+ id="path928"
+ d="M 0.69081079,27.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,24.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path932" />
+ <path
+ id="path936"
+ d="M 0.69081079,21.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,18.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path940" />
+ <path
+ id="path944"
+ d="M 0.69081079,15.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,12.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path948" />
+ <path
+ id="path952"
+ d="M 0.69081079,9.8833914 H 2.0724306 V 11.265012 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,6.8833914 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path956" />
+ <path
+ id="path960"
+ d="M 0.69081079,3.8833914 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,0.88339134 H 2.0724306 V 2.2650124 H 0.69081079 Z"
+ id="path964" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,42.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path968" />
+ <path
+ id="path976"
+ d="M 0.69081079,45.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,48.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path980" />
+ <path
+ id="path984"
+ d="M 0.69081079,51.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,54.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path988" />
+ <path
+ id="path992"
+ d="M 0.69081079,57.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,60.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path996" />
+ <path
+ id="path1000"
+ d="M 0.69081079,60.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ id="path1004"
+ d="M 0.69081079,63.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="M 0.69081079,63.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
+ id="path1008" />
+ <path
+ style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.237,4.7920842e-9 h 2.763242 V 66 H 19.237 Z"
+ id="path1018" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,39.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1020" />
+ <path
+ id="path1022"
+ d="m 19.927811,36.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,33.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1024" />
+ <path
+ id="path1026"
+ d="m 19.927811,30.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,27.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1028" />
+ <path
+ id="path1030"
+ d="m 19.927811,24.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,21.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1032" />
+ <path
+ id="path1034"
+ d="m 19.927811,18.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,15.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1036" />
+ <path
+ id="path1038"
+ d="m 19.927811,12.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,9.8833914 h 1.38162 v 1.3816206 h -1.38162 z"
+ id="path1040" />
+ <path
+ id="path1042"
+ d="m 19.927811,6.8833914 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,3.8833914 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1044" />
+ <path
+ id="path1046"
+ d="m 19.927811,0.88339134 h 1.38162 V 2.2650124 h -1.38162 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ id="path1048"
+ d="m 19.927811,42.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,45.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1050" />
+ <path
+ id="path1052"
+ d="m 19.927811,48.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,51.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1054" />
+ <path
+ id="path1056"
+ d="m 19.927811,54.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,57.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1058" />
+ <path
+ id="path1060"
+ d="m 19.927811,60.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,60.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1062" />
+ <path
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
+ d="m 19.927811,63.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ id="path1064" />
+ <path
+ id="path1066"
+ d="m 19.927811,63.883391 h 1.381621 v 1.381621 h -1.381621 z"
+ style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
+</svg>
diff --git a/subsurface.qrc b/subsurface.qrc
index 1281702ae..2caf317c6 100644
--- a/subsurface.qrc
+++ b/subsurface.qrc
@@ -93,6 +93,7 @@
<file alias="preferences-other-icon">icons/defaults.png</file>
<file alias="camera-icon">icons/camera.svg</file>
<file alias="video-icon">icons/video.svg</file>
+ <file alias="video-overlay">icons/video_overlay.svg</file>
<file alias="unknown-icon">icons/unknown.svg</file>
</qresource>
</RCC>