feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
@@ -39,136 +40,43 @@ bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
QString videoThumbnailPath(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
QString key = QStringLiteral("%1_%2_%3")
|
||||
.arg(info.baseName())
|
||||
.arg(info.size())
|
||||
.arg(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||
key.replace(QRegularExpression("[^A-Za-z0-9._-]"), "_");
|
||||
|
||||
return QDir::homePath() + "/.cache/zshell/imagecache/video-previews/" +
|
||||
key + "_preview.jpg";
|
||||
}
|
||||
|
||||
bool generateVideoThumbnail(const QString& src, const QString& cachePath) {
|
||||
if (QFile::exists(cachePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString scale = QStringLiteral(
|
||||
"scale=1920:1080:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2");
|
||||
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
process.waitForFinished(15000);
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
double duration = 0.0;
|
||||
const auto probe = runProcess(
|
||||
bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
src,
|
||||
path,
|
||||
});
|
||||
if (probe.first == 0) {
|
||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
||||
}
|
||||
process.waitForFinished(5000);
|
||||
|
||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
||||
: 8;
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bestIndex = -1;
|
||||
double bestChroma = -1.0;
|
||||
double bestLuma = -1.0;
|
||||
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
|
||||
const QString seek = duration > 0
|
||||
? QString::number(frac * duration, 'f', 3)
|
||||
: QStringLiteral("0");
|
||||
|
||||
const QString framePath =
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
seek,
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QImage frame(framePath);
|
||||
if (frame.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = frame.convertToFormat(QImage::Format_RGB888);
|
||||
|
||||
double chroma = 0.0;
|
||||
double luma = 0.0;
|
||||
int count = 0;
|
||||
for (int y = 0; y < frame.height(); ++y) {
|
||||
const auto* line = frame.constScanLine(y);
|
||||
for (int x = 0; x < frame.width(); ++x) {
|
||||
const int r = line[x * 3];
|
||||
const int g = line[x * 3 + 1];
|
||||
const int b = line[x * 3 + 2];
|
||||
chroma += std::max({r, g, b}) - std::min({r, g, b});
|
||||
luma += r + g + b;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = std::max(1, count);
|
||||
chroma /= n;
|
||||
luma /= 3.0 * n;
|
||||
|
||||
if (chroma > bestChroma + 0.5 ||
|
||||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
|
||||
bestChroma = chroma;
|
||||
bestLuma = luma;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(cachePath).absolutePath());
|
||||
return QFile::copy(
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)),
|
||||
cachePath);
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
FileSystemEntry::FileSystemEntry(
|
||||
@@ -177,7 +85,6 @@ FileSystemEntry::FileSystemEntry(
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath)
|
||||
, m_thumbnailPath(path)
|
||||
, m_isImageInitialised(false)
|
||||
, m_mimeTypeInitialised(false) {}
|
||||
|
||||
@@ -227,16 +134,7 @@ bool FileSystemEntry::isVideo() const {
|
||||
}
|
||||
|
||||
QString FileSystemEntry::thumbnailPath() const {
|
||||
return m_thumbnailPath;
|
||||
}
|
||||
|
||||
void FileSystemEntry::setThumbnailPath(const QString& path) {
|
||||
if (m_thumbnailPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_thumbnailPath = path;
|
||||
emit thumbnailPathChanged();
|
||||
return m_path;
|
||||
}
|
||||
|
||||
QString FileSystemEntry::mimeType() const {
|
||||
@@ -404,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::requireHardwareCodec() const {
|
||||
return m_requireHardwareCodec;
|
||||
}
|
||||
|
||||
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_requireHardwareCodec = requireHardwareCodec;
|
||||
emit requireHardwareCodecChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const QString key =
|
||||
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
|
||||
const auto it = m_codecCache.constFind(key);
|
||||
if (it != m_codecCache.constEnd()) {
|
||||
return it.value();
|
||||
}
|
||||
}
|
||||
|
||||
const bool hardware = probeHardwareCodec(path);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
m_codecCache.insert(key, hardware);
|
||||
}
|
||||
|
||||
return hardware;
|
||||
}
|
||||
|
||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||
}
|
||||
@@ -434,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||
const auto paths = watcher->result();
|
||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||
!paths.isEmpty()) {
|
||||
// Ignore if dir or showHidden has changed
|
||||
m_watcher.addPaths(paths);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::update() {
|
||||
@@ -488,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
const auto showHidden = m_showHidden;
|
||||
const auto filter = m_filter;
|
||||
const auto nameFilters = m_nameFilters;
|
||||
const auto requireHardware = m_requireHardwareCodec;
|
||||
|
||||
QSet<QString> oldPaths;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
@@ -495,7 +432,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
}
|
||||
|
||||
const auto future = QtConcurrent::run(
|
||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||
: QDirIterator::NoIteratorFlags;
|
||||
|
||||
@@ -553,18 +490,15 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
|
||||
QString path = iter->next();
|
||||
|
||||
if (filter == Images) {
|
||||
if (hasVideoExtension(path)) {
|
||||
if (requireHardware && !isHardwareVideo(path)) {
|
||||
continue;
|
||||
}
|
||||
} else if (filter == Images || filter == Media) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
} else if (filter == Media) {
|
||||
if (!hasVideoExtension(path)) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPaths.insert(path);
|
||||
@@ -605,31 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::startThumbnailGeneration(FileSystemEntry* entry) {
|
||||
const QPointer<FileSystemEntry> guard(entry);
|
||||
const QString source = entry->path();
|
||||
const QString cachePath = videoThumbnailPath(source);
|
||||
|
||||
const auto future = QtConcurrent::run([source, cachePath]() {
|
||||
return generateVideoThumbnail(source, cachePath);
|
||||
});
|
||||
|
||||
const auto watcher = new QFutureWatcher<bool>(this);
|
||||
connect(
|
||||
watcher,
|
||||
&QFutureWatcher<bool>::finished,
|
||||
this,
|
||||
[guard, cachePath, watcher]() {
|
||||
if (guard && watcher->result()) {
|
||||
guard->setThumbnailPath(cachePath);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::applyChanges(
|
||||
@@ -642,7 +551,6 @@ void FileSystemModel::applyChanges(
|
||||
}
|
||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||
|
||||
// Batch remove old entries
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
for (int idx : std::as_const(removedIndices)) {
|
||||
@@ -670,14 +578,10 @@ void FileSystemModel::applyChanges(
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// Create new entries
|
||||
QList<FileSystemEntry*> newEntries;
|
||||
for (const auto& path : addedPaths) {
|
||||
auto* entry =
|
||||
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||
if (entry->isVideo()) {
|
||||
startThumbnailGeneration(entry);
|
||||
}
|
||||
newEntries << entry;
|
||||
}
|
||||
std::sort(
|
||||
@@ -687,7 +591,6 @@ void FileSystemModel::applyChanges(
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
|
||||
// Batch insert new entries
|
||||
int insertStart = -1;
|
||||
QList<FileSystemEntry*> batchItems;
|
||||
for (const auto& entry : std::as_const(newEntries)) {
|
||||
|
||||
Reference in New Issue
Block a user