summaryrefslogtreecommitdiffstats
path: root/core/videoframeextractor.cpp
blob: c4e16a81b9a83d8ea032c2567f57b2fcddec15b1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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);
}