diff options
author | Berthold Stoeger <bstoeger@mail.tuwien.ac.at> | 2018-07-10 15:04:35 +0200 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2018-07-28 15:31:25 -0700 |
commit | fce42d4858d33e10b7a1c48d75838f1901b6b123 (patch) | |
tree | 3be7516c1e306e4fb8cce9cd1f3e357e3a5575df /core | |
parent | 51066e5478d76824c5da53f37184e0e0d1f3e4af (diff) | |
download | subsurface-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.txt | 1 | ||||
-rw-r--r-- | core/imagedownloader.cpp | 123 | ||||
-rw-r--r-- | core/imagedownloader.h | 11 | ||||
-rw-r--r-- | core/pref.h | 3 | ||||
-rw-r--r-- | core/subsurface-qt/SettingsObjectWrapper.cpp | 52 | ||||
-rw-r--r-- | core/subsurface-qt/SettingsObjectWrapper.h | 28 | ||||
-rw-r--r-- | core/subsurfacestartup.c | 4 | ||||
-rw-r--r-- | core/videoframeextractor.cpp | 120 | ||||
-rw-r--r-- | core/videoframeextractor.h | 37 |
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 |