File indexing completed on 2025-01-05 03:53:05
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2021-07-24 0007 * Description : a MJPEG frame generator. 0008 * 0009 * SPDX-FileCopyrightText: 2021-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0010 * SPDX-FileCopyrightText: 2021 by Quoc Hưng Tran <quochungtran1999 at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "mjpegframetask.h" 0017 0018 // C++ includes 0019 0020 #include <cmath> 0021 0022 // Qt includes 0023 0024 #include <QString> 0025 #include <QBuffer> 0026 #include <QApplication> 0027 #include <QIcon> 0028 0029 // Local includes 0030 0031 #include "digikam_debug.h" 0032 #include "previewloadthread.h" 0033 #include "frameutils.h" 0034 #include "frameosd.h" 0035 #include "vidslidesettings.h" 0036 0037 namespace DigikamGenericMjpegStreamPlugin 0038 { 0039 0040 class Q_DECL_HIDDEN MjpegFrameTask::Private 0041 { 0042 public: 0043 0044 explicit Private(const MjpegStreamSettings& set) 0045 : settings (set), 0046 failedToLoad(false) 0047 { 0048 VidSlideSettings::VidType type = (VidSlideSettings::VidType)settings.outSize; 0049 0050 /** 0051 * NOTE: QIcon depend of X11 under Linux which is not re-rentrant. 0052 * Load this image here in first from main thread. 0053 */ 0054 brokenImg = QIcon::fromTheme(QLatin1String("view-preview")).pixmap(VidSlideSettings::videoSizeFromType(type)).toImage(); 0055 endImg = QIcon::fromTheme(QLatin1String("window-close")).pixmap(VidSlideSettings::videoSizeFromType(type)).toImage(); 0056 } 0057 0058 MjpegStreamSettings settings; ///< The MJPEG stream settings. 0059 QImage brokenImg; ///< Image to push as frame if current item from list cannot be loaded. 0060 QImage endImg; ///< Image to push as frame when stream is complete. 0061 bool failedToLoad; ///< determinate if image is loaded 0062 }; 0063 0064 MjpegFrameTask::MjpegFrameTask(const MjpegStreamSettings& settings) 0065 : ActionJob(nullptr), 0066 d (new Private(settings)) 0067 { 0068 } 0069 0070 MjpegFrameTask::~MjpegFrameTask() 0071 { 0072 delete d; 0073 } 0074 0075 QByteArray MjpegFrameTask::imageToJPEGArray(const QImage& frame) const 0076 { 0077 QByteArray outbuf; 0078 QBuffer buffer(&outbuf); 0079 buffer.open(QIODevice::WriteOnly); 0080 frame.save(&buffer, "JPEG", d->settings.quality); 0081 0082 return outbuf; 0083 } 0084 0085 QImage MjpegFrameTask::loadImageFromPreviewCache(const QString& path) const 0086 { 0087 QImage qimg; 0088 qCDebug(DIGIKAM_GENERAL_LOG) << "MjpegStream: Generate frame for" << path; 0089 0090 DImg dimg = PreviewLoadThread::loadHighQualitySynchronously(path); 0091 0092 if (dimg.isNull()) 0093 { 0094 // Generate an error frame. 0095 0096 qimg = d->brokenImg; 0097 d->failedToLoad = true; 0098 qCWarning(DIGIKAM_GENERAL_LOG) << "MjpegStream: Failed to load" << path; 0099 } 0100 else 0101 { 0102 // Generate real preview frame. 0103 0104 qimg = dimg.copyQImage(); 0105 } 0106 0107 // Resize output image to the wanted dimensions. 0108 0109 VidSlideSettings::VidType type = (VidSlideSettings::VidType)d->settings.outSize; 0110 qimg = FrameUtils::makeScaledImage(qimg, VidSlideSettings::videoSizeFromType(type)); 0111 0112 return qimg; 0113 } 0114 0115 void MjpegFrameTask::run() 0116 { 0117 QImage qiimg; // Current image in stream. 0118 QImage qtimg; // Current transition image. 0119 QImage qoimg; // Next image in stream. 0120 0121 FrameOsd osd; 0122 0123 VidSlideSettings::VidType type = (VidSlideSettings::VidType)d->settings.outSize; 0124 QSize JPEGsize = VidSlideSettings::videoSizeFromType(type); 0125 int imgFrames = d->settings.delay * d->settings.rate; 0126 bool oneLoopDone = false; 0127 0128 TransitionMngr transmngr; 0129 transmngr.setOutputSize(JPEGsize); 0130 0131 EffectMngr effmngr; 0132 effmngr.setOutputSize(JPEGsize); 0133 effmngr.setFrames(imgFrames); // Ex: 30 frames at 10 img/s => 3 s of effect 0134 0135 do 0136 { 0137 // To stream in loop forever. 0138 0139 for (int i = 0 ; ((i < d->settings.inputImages.count()) && !m_cancel) ; ++i) 0140 { 0141 // One loop stream all items one by one from the ordered list 0142 0143 if ((i == 0) && !oneLoopDone) 0144 { 0145 // If we use transition, the first item at the first loop must be merged from a black image. 0146 0147 qiimg = FrameUtils::makeFramedImage(QString(), JPEGsize); 0148 } 0149 0150 QString ofile; 0151 0152 if (i < d->settings.inputImages.count()) 0153 { 0154 // The current item to pass to the next stage from a transition 0155 0156 ofile = d->settings.inputImages[i].toLocalFile(); 0157 } 0158 0159 qoimg = loadImageFromPreviewCache(ofile); 0160 0161 // Apply transition between images 0162 0163 transmngr.setInImage(qiimg); 0164 transmngr.setOutImage(qoimg); 0165 transmngr.setTransition(d->settings.transition); 0166 0167 int ttmout = 0; 0168 0169 do 0170 { 0171 // Loop over all stages to make the transition 0172 0173 qtimg = transmngr.currentFrame(ttmout); 0174 0175 Q_EMIT signalFrameChanged(imageToJPEGArray(qtimg)); 0176 0177 QThread::msleep(lround(1000.0 / d->settings.rate)); 0178 } 0179 while ((ttmout != -1) && !m_cancel); 0180 0181 // Append OSD overlay on frame generated by effect. 0182 0183 QUrl itemUrl = d->settings.inputImages[i]; 0184 0185 // Apply effect on frame 0186 0187 int count = 0; 0188 int itmout = 0; 0189 effmngr.setImage(qoimg); 0190 effmngr.setEffect(d->settings.effect); 0191 0192 do 0193 { 0194 // Loop over all stages to make the effect 0195 0196 qiimg = effmngr.currentFrame(itmout); 0197 0198 if (!d->failedToLoad) 0199 { 0200 if ((JPEGsize.width() >= 1024) && (JPEGsize.height() >= 576)) 0201 { 0202 osd.insertOsdToFrame(qiimg, 0203 itemUrl, 0204 d->settings.osdSettings, 0205 d->settings.iface); 0206 } 0207 } 0208 else 0209 { 0210 osd.insertMessageOsdToFrame(qiimg, 0211 JPEGsize, 0212 QLatin1String("Failed to load image")); 0213 } 0214 0215 Q_EMIT signalFrameChanged(imageToJPEGArray(qiimg)); 0216 0217 count++; 0218 0219 QThread::msleep(lround(1000.0 / d->settings.rate)); 0220 } 0221 while ((count < imgFrames) && !m_cancel); 0222 0223 d->failedToLoad = false; 0224 oneLoopDone = true; // At least one loop is done. 0225 } 0226 } 0227 while (!m_cancel && d->settings.loop); 0228 0229 osd.insertMessageOsdToFrame(d->endImg, 0230 JPEGsize, 0231 QLatin1String("End of stream")); 0232 0233 Q_EMIT signalFrameChanged(imageToJPEGArray(d->endImg)); 0234 qCDebug(DIGIKAM_GENERAL_LOG) << "MjpegStream: end of stream"; 0235 0236 Q_EMIT signalDone(); 0237 } 0238 0239 } // namespace DigikamGenericMjpegStreamPlugin 0240 0241 #include "moc_mjpegframetask.cpp"