summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorGravatar Berthold Stoeger <bstoeger@mail.tuwien.ac.at>2018-07-10 15:04:35 +0200
committerGravatar Dirk Hohndel <dirk@hohndel.org>2018-07-28 15:31:25 -0700
commitfce42d4858d33e10b7a1c48d75838f1901b6b123 (patch)
tree3be7516c1e306e4fb8cce9cd1f3e357e3a5575df /core
parent51066e5478d76824c5da53f37184e0e0d1f3e4af (diff)
downloadsubsurface-fce42d4858d33e10b7a1c48d75838f1901b6b123.tar.gz
Dive media: Extract thumbnails from videos with ffmpeg
Extract thumbnails using ffmpeg. Behavior is controlled by three new preferences fields: - extract_video_thumbnails (bool): if true, thumbnails are calculated. - extract_video_thumbnail_position (int 0..100): position in video where thumbnail is fetched. - ffmpeg_executable (string): path of ffmpeg executable. If ffmpeg refuses to start, extract_video_thumbnails is set to false to avoid unnecessary churn. Video thumbnails are marked by an overlay. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
Diffstat (limited to 'core')
-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
9 files changed, 360 insertions, 19 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