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>
This commit is contained in:
Berthold Stoeger 2018-07-10 15:04:35 +02:00 committed by Dirk Hohndel
parent 51066e5478
commit fce42d4858
14 changed files with 727 additions and 20 deletions

View file

@ -97,6 +97,7 @@ set(SUBSURFACE_CORE_LIB_SRCS
uemis.c
uemis-downloader.c
version.c
videoframeextractor.cpp
windowtitleupdate.cpp
worldmap-save.c

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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)
{

View file

@ -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");
};

View file

@ -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);
}
/*

View file

@ -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);
}

View file

@ -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

View file

@ -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());

View file

@ -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;

View file

@ -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>
@ -209,6 +209,79 @@
</layout>
</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">

263
icons/video_overlay.svg Normal file
View file

@ -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>

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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>