#pragma once #include #include #include #include #include #include #include #include #include #include #include namespace ZShell::models { namespace { inline const QStringList kVideoExtensions = { "mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob", }; inline bool hasVideoExtension(const QString& path) { return kVideoExtensions.contains(QFileInfo(path).suffix().toLower()); } inline const QStringList kHardwareCodecs = { "av1", "hevc", "h264", "vp9", "vp8", "vc1", "mpeg2", "mpeg2video", "mpeg4", "mjpeg", }; inline bool probeHardwareCodec(const QString& path) { QProcess process; process.start( "ffprobe", { "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "default=nw=1:nk=1", path, }); process.waitForFinished(5000); if (process.exitCode() != 0) { return false; } const QString codec = QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower(); return kHardwareCodecs.contains(codec); } inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) { if (!source.isValid() || source.isEmpty() || !fit.isValid() || fit.isEmpty()) { return source; } const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding); if (cover.width() <= source.width() && cover.height() <= source.height()) { return cover; } return source; } inline QImage cleanImage(const QImage& src) { QImage out(src.size(), QImage::Format_RGB888); if (out.isNull()) { return out; } const auto rowBytes = static_cast(std::min(src.bytesPerLine(), out.bytesPerLine())); for (int y = 0; y < src.height(); ++y) { std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes); } return out; } inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) { QImageReader reader(path); reader.setAutoTransform(true); const QSize source = reader.size(); const QSize target = coverSizeNoUpscale(source, fit); if (target != source && !target.isEmpty()) { reader.setScaledSize(target); } QImage image = reader.read(); if (image.isNull()) { return {}; } image = cleanImage(image.convertToFormat(QImage::Format_RGB888)); QByteArray bytes; QBuffer buffer(&bytes); if (!buffer.open(QIODevice::WriteOnly) || !image.save(&buffer, "JPEG", 90)) { return {}; } return bytes; } inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) { auto runProcess = [](const QString& program, const QStringList& args) { QProcess process; process.start(program, args); if (!process.waitForFinished(15000)) { process.kill(); process.waitForFinished(1000); } return qMakePair(process.exitCode(), process.readAllStandardOutput()); }; double duration = 0.0; const auto probe = runProcess( "ffprobe", { "-v", "error", "-show_entries", "format=duration", "-of", "default=nw=1:nk=1", src, }); if (probe.first == 0) { duration = QString::fromUtf8(probe.second).trimmed().toDouble(); } QSize source; const auto resProbe = runProcess( "ffprobe", { "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=s=x:p=0", src, }); if (resProbe.first == 0) { const auto parts = QString::fromUtf8(resProbe.second).trimmed().split('x'); if (parts.size() == 2) { source = QSize(parts[0].toInt(), parts[1].toInt()); } } const int samples = duration > 0 ? std::min(10, std::max(4, int(duration))) : 8; QSize target = coverSizeNoUpscale(source, fit); if (target.isEmpty()) { target = fit; } target.setWidth(target.width() & ~1); target.setHeight(target.height() & ~1); const QString scale = QStringLiteral( "scale=%1:%2:force_original_aspect_ratio=decrease," "scale=trunc(iw/2)*2:trunc(ih/2)*2") .arg(target.width()) .arg(target.height()); QTemporaryDir tmpDir; if (!tmpDir.isValid()) { return {}; } 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 {}; } QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex))); if (!frame.open(QIODevice::ReadOnly)) { return {}; } return frame.readAll(); } inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) { QImage image = QImage::fromData(source, "JPEG"); if (image.isNull()) { return {}; } if (image.width() > maxSide || image.height() > maxSide) { image = image.scaled( maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation); } image = cleanImage(image.convertToFormat(QImage::Format_RGB888)); QByteArray bytes; QBuffer buffer(&bytes); if (!buffer.open(QIODevice::WriteOnly) || !image.save(&buffer, "JPEG", 85)) { return {}; } return bytes; } } // namespace } // namespace ZShell::models