|
|
@@ -14,11 +14,224 @@
|
|
|
#include <QImage>
|
|
|
#include <QImageReader>
|
|
|
#include <QIODevice>
|
|
|
+#include <QColor>
|
|
|
#include <QJsonObject>
|
|
|
#include <QJsonValue>
|
|
|
#include <QMap>
|
|
|
+#include <QAbstractVideoBuffer>
|
|
|
+#include <QAbstractVideoSurface>
|
|
|
+#include <QMediaContent>
|
|
|
+#include <QMediaPlayer>
|
|
|
#include <QPointer>
|
|
|
#include <QRegularExpression>
|
|
|
+#include <QRunnable>
|
|
|
+#include <QThreadPool>
|
|
|
+#include <QTimer>
|
|
|
+#include <QUrl>
|
|
|
+#include <QVideoFrame>
|
|
|
+#include <QVideoSurfaceFormat>
|
|
|
+
|
|
|
+#include <functional>
|
|
|
+#include <memory>
|
|
|
+
|
|
|
+namespace {
|
|
|
+inline quint8 clampByte(int v)
|
|
|
+{
|
|
|
+ return static_cast<quint8>(v < 0 ? 0 : (v > 255 ? 255 : v));
|
|
|
+}
|
|
|
+
|
|
|
+inline QRgb yuvToRgb(int y, int u, int v)
|
|
|
+{
|
|
|
+ const int c = y - 16;
|
|
|
+ const int d = u - 128;
|
|
|
+ const int e = v - 128;
|
|
|
+ return qRgb(clampByte((298 * c + 409 * e + 128) >> 8),
|
|
|
+ clampByte((298 * c - 100 * d - 208 * e + 128) >> 8),
|
|
|
+ clampByte((298 * c + 516 * d + 128) >> 8));
|
|
|
+}
|
|
|
+
|
|
|
+QImage nv12ToImage(const uchar *data, int w, int h, int yStride)
|
|
|
+{
|
|
|
+ if (!data || w <= 0 || h <= 0 || yStride < w)
|
|
|
+ return {};
|
|
|
+ QImage img(w, h, QImage::Format_RGB32);
|
|
|
+ const uchar *uv = data + yStride * h;
|
|
|
+ for (int row = 0; row < h; ++row)
|
|
|
+ {
|
|
|
+ auto *dst = reinterpret_cast<QRgb *>(img.scanLine(row));
|
|
|
+ const uchar *yp = data + row * yStride;
|
|
|
+ const uchar *uvp = uv + (row / 2) * yStride;
|
|
|
+ for (int x = 0; x < w; ++x)
|
|
|
+ {
|
|
|
+ const int uvx = x & ~1;
|
|
|
+ dst[x] = yuvToRgb(yp[x], uvp[uvx], uvp[uvx + 1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return img;
|
|
|
+}
|
|
|
+
|
|
|
+QImage yuyvToImage(const uchar *data, int w, int h, int stride)
|
|
|
+{
|
|
|
+ if (!data || w <= 0 || h <= 0 || stride < w * 2)
|
|
|
+ return {};
|
|
|
+ QImage img(w, h, QImage::Format_RGB32);
|
|
|
+ for (int row = 0; row < h; ++row)
|
|
|
+ {
|
|
|
+ auto *dst = reinterpret_cast<QRgb *>(img.scanLine(row));
|
|
|
+ const uchar *src = data + row * stride;
|
|
|
+ for (int x = 0; x + 1 < w; x += 2)
|
|
|
+ {
|
|
|
+ const int y0 = src[0];
|
|
|
+ const int u = src[1];
|
|
|
+ const int y1 = src[2];
|
|
|
+ const int v = src[3];
|
|
|
+ dst[x] = yuvToRgb(y0, u, v);
|
|
|
+ dst[x + 1] = yuvToRgb(y1, u, v);
|
|
|
+ src += 4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return img;
|
|
|
+}
|
|
|
+
|
|
|
+class PosterSurface : public QAbstractVideoSurface
|
|
|
+{
|
|
|
+public:
|
|
|
+ explicit PosterSurface(QObject *parent = nullptr)
|
|
|
+ : QAbstractVideoSurface(parent)
|
|
|
+ {
|
|
|
+ }
|
|
|
+
|
|
|
+ std::function<void(const QImage &)> onFrame;
|
|
|
+
|
|
|
+ QList<QVideoFrame::PixelFormat> supportedPixelFormats(
|
|
|
+ QAbstractVideoBuffer::HandleType type = QAbstractVideoBuffer::NoHandle) const override
|
|
|
+ {
|
|
|
+ Q_UNUSED(type);
|
|
|
+ return {QVideoFrame::Format_RGB32, QVideoFrame::Format_ARGB32,
|
|
|
+ QVideoFrame::Format_ARGB32_Premultiplied, QVideoFrame::Format_RGB24,
|
|
|
+ QVideoFrame::Format_Jpeg, QVideoFrame::Format_NV12, QVideoFrame::Format_YUYV};
|
|
|
+ }
|
|
|
+
|
|
|
+ bool present(const QVideoFrame &frame) override
|
|
|
+ {
|
|
|
+ if (m_done)
|
|
|
+ return true;
|
|
|
+ if (!frame.isValid())
|
|
|
+ return true;
|
|
|
+ QVideoFrame clone(frame);
|
|
|
+ if (!clone.map(QAbstractVideoBuffer::ReadOnly))
|
|
|
+ return false;
|
|
|
+ QImage img;
|
|
|
+ const QImage::Format fmt = QVideoFrame::imageFormatFromPixelFormat(clone.pixelFormat());
|
|
|
+ if (fmt != QImage::Format_Invalid)
|
|
|
+ {
|
|
|
+ img = QImage(clone.bits(), clone.width(), clone.height(), clone.bytesPerLine(), fmt)
|
|
|
+ .copy();
|
|
|
+ }
|
|
|
+ else if (clone.pixelFormat() == QVideoFrame::Format_Jpeg)
|
|
|
+ {
|
|
|
+ img.loadFromData(clone.bits(), clone.mappedBytes(), "JPEG");
|
|
|
+ }
|
|
|
+ else if (clone.pixelFormat() == QVideoFrame::Format_NV12)
|
|
|
+ {
|
|
|
+ img = nv12ToImage(clone.bits(), clone.width(), clone.height(), clone.bytesPerLine());
|
|
|
+ }
|
|
|
+ else if (clone.pixelFormat() == QVideoFrame::Format_YUYV)
|
|
|
+ {
|
|
|
+ img = yuyvToImage(clone.bits(), clone.width(), clone.height(), clone.bytesPerLine());
|
|
|
+ }
|
|
|
+ clone.unmap();
|
|
|
+ if (img.isNull())
|
|
|
+ return true;
|
|
|
+ if (img.format() != QImage::Format_RGB32 && img.format() != QImage::Format_ARGB32
|
|
|
+ && img.format() != QImage::Format_RGB888)
|
|
|
+ img = img.convertToFormat(QImage::Format_RGB32);
|
|
|
+ m_done = true;
|
|
|
+ if (onFrame)
|
|
|
+ onFrame(img);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ bool m_done = false;
|
|
|
+};
|
|
|
+}
|
|
|
+
|
|
|
+class ThumbJob : public QRunnable
|
|
|
+{
|
|
|
+public:
|
|
|
+ QPointer<FileCache> cache;
|
|
|
+ qint64 fileId = 0;
|
|
|
+ int maxEdge = 180;
|
|
|
+ QString path;
|
|
|
+ QByteArray bytes;
|
|
|
+ bool video = false;
|
|
|
+ QImage poster;
|
|
|
+ qreal dpr = 1.0;
|
|
|
+
|
|
|
+ void run() override
|
|
|
+ {
|
|
|
+ QImage img;
|
|
|
+ int srcW = 0;
|
|
|
+ int srcH = 0;
|
|
|
+ if (!video && !path.isEmpty())
|
|
|
+ {
|
|
|
+ QImageReader reader(path);
|
|
|
+ reader.setAutoTransform(true);
|
|
|
+ QSize sz = reader.size();
|
|
|
+ srcW = sz.width();
|
|
|
+ srcH = sz.height();
|
|
|
+ if (sz.isValid())
|
|
|
+ {
|
|
|
+ const int cap = qMax(32, maxEdge);
|
|
|
+ sz.scale(QSize(cap, cap), Qt::KeepAspectRatio);
|
|
|
+ reader.setScaledSize(sz);
|
|
|
+ }
|
|
|
+ img = reader.read();
|
|
|
+ }
|
|
|
+ if (img.isNull() && !video && !bytes.isEmpty())
|
|
|
+ img.loadFromData(bytes);
|
|
|
+ if (img.isNull() && !poster.isNull())
|
|
|
+ img = poster;
|
|
|
+ if (srcW <= 0 || srcH <= 0)
|
|
|
+ {
|
|
|
+ if (!poster.isNull())
|
|
|
+ {
|
|
|
+ srcW = poster.width();
|
|
|
+ srcH = poster.height();
|
|
|
+ }
|
|
|
+ else if (!img.isNull())
|
|
|
+ {
|
|
|
+ srcW = img.width();
|
|
|
+ srcH = img.height();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ QImage out;
|
|
|
+ if (!img.isNull())
|
|
|
+ {
|
|
|
+ const int cap = qMax(32, maxEdge);
|
|
|
+ QSize scaled = img.size();
|
|
|
+ scaled.scale(QSize(cap, cap), Qt::KeepAspectRatio);
|
|
|
+ out = img.scaled(qRound(scaled.width() * dpr), qRound(scaled.height() * dpr),
|
|
|
+ Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
|
+ out.setDevicePixelRatio(dpr);
|
|
|
+ }
|
|
|
+ FileCache *c = cache.data();
|
|
|
+ if (!c)
|
|
|
+ return;
|
|
|
+ QMetaObject::invokeMethod(c, "finishThumb", Qt::QueuedConnection,
|
|
|
+ Q_ARG(qint64, fileId), Q_ARG(int, maxEdge), Q_ARG(QImage, out));
|
|
|
+ if (srcW > 0 && srcH > 0)
|
|
|
+ {
|
|
|
+ const qint64 id = fileId;
|
|
|
+ const int w = srcW;
|
|
|
+ const int h = srcH;
|
|
|
+ QMetaObject::invokeMethod(c, [c, id, w, h]() {
|
|
|
+ c->rememberMediaSize(id, w, h);
|
|
|
+ }, Qt::QueuedConnection);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
FileCache &FileCache::instance()
|
|
|
{
|
|
|
@@ -42,6 +255,29 @@ bool FileCache::looksLikeImage(const QString &mime, const QString &name)
|
|
|
|| ext == QLatin1String("bmp") || ext == QLatin1String("webp");
|
|
|
}
|
|
|
|
|
|
+bool FileCache::looksLikeVideo(const QString &mime, const QString &name)
|
|
|
+{
|
|
|
+ const QString m = mime.toLower();
|
|
|
+ if (m.startsWith(QStringLiteral("video/")))
|
|
|
+ return true;
|
|
|
+ const QString ext = QFileInfo(name).suffix().toLower();
|
|
|
+ return ext == QLatin1String("mp4") || ext == QLatin1String("mov")
|
|
|
+ || ext == QLatin1String("m4v") || ext == QLatin1String("avi")
|
|
|
+ || ext == QLatin1String("mkv") || ext == QLatin1String("webm")
|
|
|
+ || ext == QLatin1String("wmv") || ext == QLatin1String("flv")
|
|
|
+ || ext == QLatin1String("mpeg") || ext == QLatin1String("mpg")
|
|
|
+ || ext == QLatin1String("3gp");
|
|
|
+}
|
|
|
+
|
|
|
+QString FileCache::convPreview(const QString &mime, const QString &name)
|
|
|
+{
|
|
|
+ if (looksLikeImage(mime, name))
|
|
|
+ return QStringLiteral("[图片]");
|
|
|
+ if (looksLikeVideo(mime, name))
|
|
|
+ return QStringLiteral("[视频]");
|
|
|
+ return name.isEmpty() ? QStringLiteral("[文件]") : QStringLiteral("[文件] %1").arg(name);
|
|
|
+}
|
|
|
+
|
|
|
QString FileCache::formatSize(qint64 bytes)
|
|
|
{
|
|
|
if (bytes < 0)
|
|
|
@@ -246,6 +482,8 @@ void FileCache::remember(qint64 fileId, const QByteArray &bytes, const QString &
|
|
|
m_localPath.insert(fileId, dest);
|
|
|
emit ready(fileId);
|
|
|
emit statusChanged(fileId);
|
|
|
+ if (looksLikeVideo(m_mime.value(fileId), m_name.value(fileId)))
|
|
|
+ requestVideoPoster(fileId);
|
|
|
}
|
|
|
|
|
|
bool FileCache::rememberPath(qint64 fileId, const QString &srcPath, const QString &mime, const QString &name)
|
|
|
@@ -273,6 +511,8 @@ bool FileCache::rememberPath(qint64 fileId, const QString &srcPath, const QStrin
|
|
|
m_localPath.insert(fileId, dest);
|
|
|
m_pendingUpload.insert(fileId);
|
|
|
emit statusChanged(fileId);
|
|
|
+ if (looksLikeVideo(m_mime.value(fileId), m_name.value(fileId, src.fileName())))
|
|
|
+ requestVideoPoster(fileId);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@@ -439,11 +679,96 @@ QString FileCache::name(qint64 fileId) const
|
|
|
return m_name.value(fileId);
|
|
|
}
|
|
|
|
|
|
+QPixmap FileCache::cachedThumbnail(qint64 fileId, int maxEdge) const
|
|
|
+{
|
|
|
+ return m_thumb.value(QStringLiteral("%1/%2").arg(fileId).arg(maxEdge));
|
|
|
+}
|
|
|
+
|
|
|
+QSize FileCache::mediaSize(qint64 fileId) const
|
|
|
+{
|
|
|
+ const_cast<FileCache *>(this)->loadMediaSizes();
|
|
|
+ return m_mediaSize.value(fileId);
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::rememberMediaSize(qint64 fileId, int w, int h)
|
|
|
+{
|
|
|
+ if (fileId <= 0 || w <= 0 || h <= 0)
|
|
|
+ return;
|
|
|
+ loadMediaSizes();
|
|
|
+ const QSize sz(w, h);
|
|
|
+ if (m_mediaSize.value(fileId) == sz)
|
|
|
+ return;
|
|
|
+ m_mediaSize.insert(fileId, sz);
|
|
|
+ persistMediaSizes();
|
|
|
+}
|
|
|
+
|
|
|
+QSize FileCache::probeImageSize(qint64 fileId)
|
|
|
+{
|
|
|
+ QSize s = mediaSize(fileId);
|
|
|
+ if (!s.isEmpty())
|
|
|
+ return s;
|
|
|
+ const QString path = m_localPath.value(fileId);
|
|
|
+ if (path.isEmpty() || !QFileInfo::exists(path))
|
|
|
+ return {};
|
|
|
+ if (!looksLikeImage(m_mime.value(fileId), m_name.value(fileId)))
|
|
|
+ return {};
|
|
|
+ QImageReader reader(path);
|
|
|
+ reader.setAutoTransform(true);
|
|
|
+ s = reader.size();
|
|
|
+ if (s.width() > 0 && s.height() > 0)
|
|
|
+ rememberMediaSize(fileId, s.width(), s.height());
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::loadMediaSizes()
|
|
|
+{
|
|
|
+ if (m_mediaSizeLoaded)
|
|
|
+ return;
|
|
|
+ m_mediaSizeLoaded = true;
|
|
|
+ const QString path = cacheDir() + QStringLiteral("/dims.txt");
|
|
|
+ QFile f(path);
|
|
|
+ if (!f.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
|
+ return;
|
|
|
+ while (!f.atEnd())
|
|
|
+ {
|
|
|
+ const QByteArray line = f.readLine().trimmed();
|
|
|
+ if (line.isEmpty())
|
|
|
+ continue;
|
|
|
+ const QList<QByteArray> parts = line.split(' ');
|
|
|
+ if (parts.size() < 3)
|
|
|
+ continue;
|
|
|
+ const qint64 id = parts.at(0).toLongLong();
|
|
|
+ const int w = parts.at(1).toInt();
|
|
|
+ const int h = parts.at(2).toInt();
|
|
|
+ if (id > 0 && w > 0 && h > 0)
|
|
|
+ m_mediaSize.insert(id, QSize(w, h));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::persistMediaSizes()
|
|
|
+{
|
|
|
+ const QString dir = cacheDir();
|
|
|
+ if (dir.isEmpty())
|
|
|
+ return;
|
|
|
+ QDir().mkpath(dir);
|
|
|
+ QFile f(dir + QStringLiteral("/dims.txt"));
|
|
|
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
|
|
+ return;
|
|
|
+ for (auto it = m_mediaSize.constBegin(); it != m_mediaSize.constEnd(); ++it)
|
|
|
+ {
|
|
|
+ const QSize s = it.value();
|
|
|
+ f.write(QByteArray::number(it.key()) + ' ' + QByteArray::number(s.width()) + ' '
|
|
|
+ + QByteArray::number(s.height()) + '\n');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
QPixmap FileCache::thumbnail(qint64 fileId, int maxEdge)
|
|
|
{
|
|
|
- const QString key = QStringLiteral("%1/%2").arg(fileId).arg(maxEdge);
|
|
|
- if (m_thumb.contains(key))
|
|
|
- return m_thumb.value(key);
|
|
|
+ const QPixmap cached = cachedThumbnail(fileId, maxEdge);
|
|
|
+ if (!cached.isNull())
|
|
|
+ return cached;
|
|
|
+ if (looksLikeVideo(m_mime.value(fileId), m_name.value(fileId)))
|
|
|
+ return {};
|
|
|
QImage img;
|
|
|
const QString path = findComplete(fileId);
|
|
|
if (!path.isEmpty())
|
|
|
@@ -465,24 +790,218 @@ QPixmap FileCache::thumbnail(qint64 fileId, int maxEdge)
|
|
|
return {};
|
|
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
|
const int cap = qMax(32, maxEdge);
|
|
|
- QSize box(cap, cap);
|
|
|
QSize scaled = img.size();
|
|
|
- scaled.scale(box, Qt::KeepAspectRatio);
|
|
|
+ scaled.scale(QSize(cap, cap), Qt::KeepAspectRatio);
|
|
|
const QImage out = img.scaled(qRound(scaled.width() * dpr), qRound(scaled.height() * dpr),
|
|
|
Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
|
QPixmap pm = QPixmap::fromImage(out);
|
|
|
pm.setDevicePixelRatio(dpr);
|
|
|
- m_thumb.insert(key, pm);
|
|
|
+ m_thumb.insert(QStringLiteral("%1/%2").arg(fileId).arg(maxEdge), pm);
|
|
|
return pm;
|
|
|
}
|
|
|
|
|
|
+void FileCache::requestThumbnail(qint64 fileId, int maxEdge)
|
|
|
+{
|
|
|
+ if (fileId <= 0)
|
|
|
+ return;
|
|
|
+ const QString key = QStringLiteral("%1/%2").arg(fileId).arg(maxEdge);
|
|
|
+ if (m_thumb.contains(key))
|
|
|
+ {
|
|
|
+ emit thumbReady(fileId, maxEdge);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (m_thumbQueued.contains(key))
|
|
|
+ return;
|
|
|
+ m_thumbQueued.insert(key);
|
|
|
+ m_thumbWait.enqueue(qMakePair(fileId, maxEdge));
|
|
|
+ pumpThumbs();
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::pumpThumbs()
|
|
|
+{
|
|
|
+ while (m_thumbActive < 2 && !m_thumbWait.isEmpty())
|
|
|
+ {
|
|
|
+ const auto job = m_thumbWait.dequeue();
|
|
|
+ ++m_thumbActive;
|
|
|
+ startThumbJob(job.first, job.second);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::startThumbJob(qint64 fileId, int maxEdge)
|
|
|
+{
|
|
|
+ auto *job = new ThumbJob;
|
|
|
+ job->setAutoDelete(true);
|
|
|
+ job->cache = this;
|
|
|
+ job->fileId = fileId;
|
|
|
+ job->maxEdge = maxEdge;
|
|
|
+ job->path = findComplete(fileId);
|
|
|
+ job->bytes = m_bytes.value(fileId);
|
|
|
+ job->video = looksLikeVideo(m_mime.value(fileId), m_name.value(fileId, QFileInfo(job->path).fileName()));
|
|
|
+ if (job->video && m_posterSrc.contains(fileId))
|
|
|
+ job->poster = m_posterSrc.value(fileId).toImage();
|
|
|
+ job->dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
|
+ QThreadPool::globalInstance()->start(job);
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::finishThumb(qint64 fileId, int maxEdge, QImage img)
|
|
|
+{
|
|
|
+ if (m_thumbActive > 0)
|
|
|
+ --m_thumbActive;
|
|
|
+ const QString key = QStringLiteral("%1/%2").arg(fileId).arg(maxEdge);
|
|
|
+ if (img.isNull())
|
|
|
+ {
|
|
|
+ m_thumbQueued.remove(key);
|
|
|
+ pumpThumbs();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const qreal dpr = img.devicePixelRatio() > 0 ? img.devicePixelRatio() : 1.0;
|
|
|
+ QPixmap pm = QPixmap::fromImage(img);
|
|
|
+ pm.setDevicePixelRatio(dpr);
|
|
|
+ m_thumb.insert(key, pm);
|
|
|
+ m_thumbQueued.remove(key);
|
|
|
+ emit thumbReady(fileId, maxEdge);
|
|
|
+ pumpThumbs();
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::requestVideoPoster(qint64 fileId)
|
|
|
+{
|
|
|
+ if (fileId <= 0 || m_posterSrc.contains(fileId))
|
|
|
+ return;
|
|
|
+ if (m_posterGrabbing.contains(fileId))
|
|
|
+ return;
|
|
|
+ for (qint64 id : m_posterWait)
|
|
|
+ {
|
|
|
+ if (id == fileId)
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const QString path = findComplete(fileId);
|
|
|
+ if (path.isEmpty() || !QFileInfo::exists(path))
|
|
|
+ return;
|
|
|
+ const QString name = m_name.value(fileId, QFileInfo(path).fileName());
|
|
|
+ if (!looksLikeVideo(m_mime.value(fileId), name))
|
|
|
+ return;
|
|
|
+ m_posterWait.enqueue(fileId);
|
|
|
+ pumpPosters();
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::pumpPosters()
|
|
|
+{
|
|
|
+ while (m_posterActive < 1 && !m_posterWait.isEmpty())
|
|
|
+ {
|
|
|
+ const qint64 id = m_posterWait.dequeue();
|
|
|
+ if (m_posterSrc.contains(id) || findComplete(id).isEmpty())
|
|
|
+ continue;
|
|
|
+ ++m_posterActive;
|
|
|
+ beginPosterGrab(id);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::beginPosterGrab(qint64 fileId)
|
|
|
+{
|
|
|
+ auto abortGrab = [this]() {
|
|
|
+ if (m_posterActive > 0)
|
|
|
+ --m_posterActive;
|
|
|
+ pumpPosters();
|
|
|
+ };
|
|
|
+ if (fileId <= 0 || m_posterSrc.contains(fileId) || m_posterGrabbing.contains(fileId))
|
|
|
+ {
|
|
|
+ abortGrab();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const QString path = findComplete(fileId);
|
|
|
+ if (path.isEmpty() || !QFileInfo::exists(path))
|
|
|
+ {
|
|
|
+ abortGrab();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const QString name = m_name.value(fileId, QFileInfo(path).fileName());
|
|
|
+ if (!looksLikeVideo(m_mime.value(fileId), name))
|
|
|
+ {
|
|
|
+ abortGrab();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ m_posterGrabbing.insert(fileId);
|
|
|
+ auto *player = new QMediaPlayer(this);
|
|
|
+ auto *surface = new PosterSurface(player);
|
|
|
+ auto *timeout = new QTimer(player);
|
|
|
+ timeout->setSingleShot(true);
|
|
|
+ timeout->setInterval(5000);
|
|
|
+
|
|
|
+ auto finished = std::make_shared<bool>(false);
|
|
|
+ QPointer<FileCache> self(this);
|
|
|
+ auto finish = [self, fileId, player, finished](const QImage &img) {
|
|
|
+ if (*finished)
|
|
|
+ return;
|
|
|
+ *finished = true;
|
|
|
+ if (!self)
|
|
|
+ {
|
|
|
+ player->deleteLater();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ self->m_posterGrabbing.remove(fileId);
|
|
|
+ if (self->m_posterActive > 0)
|
|
|
+ --self->m_posterActive;
|
|
|
+ player->stop();
|
|
|
+ player->setVideoOutput(static_cast<QAbstractVideoSurface *>(nullptr));
|
|
|
+ if (!img.isNull())
|
|
|
+ self->finishPoster(fileId, QPixmap::fromImage(img));
|
|
|
+ player->deleteLater();
|
|
|
+ self->pumpPosters();
|
|
|
+ };
|
|
|
+
|
|
|
+ surface->onFrame = [self, finish](const QImage &img) {
|
|
|
+ if (!self)
|
|
|
+ return;
|
|
|
+ const QImage copy = img;
|
|
|
+ QMetaObject::invokeMethod(self.data(), [finish, copy]() { finish(copy); },
|
|
|
+ Qt::QueuedConnection);
|
|
|
+ };
|
|
|
+ QObject::connect(timeout, &QTimer::timeout, player, [finish]() { finish(QImage()); });
|
|
|
+ QObject::connect(
|
|
|
+ player, static_cast<void (QMediaPlayer::*)(QMediaPlayer::Error)>(&QMediaPlayer::error),
|
|
|
+ player, [finish](QMediaPlayer::Error e) {
|
|
|
+ if (e != QMediaPlayer::NoError)
|
|
|
+ finish(QImage());
|
|
|
+ });
|
|
|
+ player->setMuted(true);
|
|
|
+ player->setVolume(0);
|
|
|
+ player->setVideoOutput(surface);
|
|
|
+ player->setMedia(QMediaContent(QUrl::fromLocalFile(path)));
|
|
|
+ timeout->start();
|
|
|
+ player->play();
|
|
|
+}
|
|
|
+
|
|
|
+void FileCache::finishPoster(qint64 fileId, const QPixmap &pm)
|
|
|
+{
|
|
|
+ if (fileId <= 0 || pm.isNull())
|
|
|
+ return;
|
|
|
+ m_posterSrc.insert(fileId, pm);
|
|
|
+ const qreal dpr = pm.devicePixelRatio() > 0 ? pm.devicePixelRatio() : 1.0;
|
|
|
+ if (mediaSize(fileId).isEmpty())
|
|
|
+ rememberMediaSize(fileId, qMax(1, qRound(pm.width() / dpr)),
|
|
|
+ qMax(1, qRound(pm.height() / dpr)));
|
|
|
+ const QString prefix = QString::number(fileId) + QLatin1Char('/');
|
|
|
+ for (auto it = m_thumb.begin(); it != m_thumb.end();)
|
|
|
+ {
|
|
|
+ if (it.key().startsWith(prefix))
|
|
|
+ it = m_thumb.erase(it);
|
|
|
+ else
|
|
|
+ ++it;
|
|
|
+ }
|
|
|
+ emit posterReady(fileId);
|
|
|
+ requestThumbnail(fileId, 180);
|
|
|
+}
|
|
|
+
|
|
|
void FileCache::ensure(qint64 fileId, const QString &url, const QString &mime, const QString &name)
|
|
|
{
|
|
|
if (fileId <= 0 || m_fetching.contains(fileId))
|
|
|
return;
|
|
|
noteMeta(fileId, url, mime, name, 0);
|
|
|
- if (loadDisk(fileId))
|
|
|
+ const QString local = findComplete(fileId);
|
|
|
+ if (!local.isEmpty())
|
|
|
{
|
|
|
+ m_localPath.insert(fileId, local);
|
|
|
emit ready(fileId);
|
|
|
emit statusChanged(fileId);
|
|
|
return;
|
|
|
@@ -727,4 +1246,6 @@ void FileCache::finishDownload(qint64 fileId, bool ok, const QString &error)
|
|
|
m_size.insert(fileId, QFileInfo(dest).size());
|
|
|
emit ready(fileId);
|
|
|
emit statusChanged(fileId);
|
|
|
+ if (looksLikeVideo(m_mime.value(fileId), m_name.value(fileId)))
|
|
|
+ requestVideoPoster(fileId);
|
|
|
}
|