339 lines
9.5 KiB
C++
339 lines
9.5 KiB
C++
/******************************************************************************
|
|
VideoCapture.cpp: description
|
|
Copyright (C) 2012-2016 Wang Bin <wbsecg1@gmail.com>
|
|
|
|
* This file is part of QtAV
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
******************************************************************************/
|
|
#include "VideoCapture.h"
|
|
#include <QtCore/QCoreApplication>
|
|
#include <QtCore/QDir>
|
|
#include <QtCore/QRunnable>
|
|
#include <QtCore/QThreadPool>
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
|
|
#include <QtGui/QDesktopServices>
|
|
#else
|
|
#include <QtCore/QStandardPaths>
|
|
#endif
|
|
#include "Logger.h"
|
|
|
|
#define CAPTURE_DEBUG 0
|
|
|
|
namespace FAV {
|
|
|
|
Q_GLOBAL_STATIC(QThreadPool, videoCaptureThreadPool)
|
|
static bool app_is_dieing = false;
|
|
// TODO: cancel if qapp is quit
|
|
class CaptureTask : public QRunnable
|
|
{
|
|
public:
|
|
CaptureTask(VideoCapture* c)
|
|
: cap(c)
|
|
, save(true)
|
|
, original_fmt(false)
|
|
, quality(-1)
|
|
, format(QStringLiteral("PNG"))
|
|
, qfmt(QImage::Format_ARGB32)
|
|
{
|
|
setAutoDelete(true);
|
|
}
|
|
virtual void run() {
|
|
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo() << "CaptureTask started";
|
|
#endif
|
|
if (app_is_dieing) {
|
|
qDebug("app is dieing. cancel capture task %p", this);
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("app is dieing. cancel capture task %p", this);
|
|
#endif
|
|
return;
|
|
}
|
|
QImage image(frame.toImage());
|
|
if (image.isNull()) {
|
|
qWarning("Failed to convert to QImage");
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo() << "Failed to convert to QImage";
|
|
#endif
|
|
return;
|
|
}
|
|
QMetaObject::invokeMethod(cap, "imageCaptured", Q_ARG(QImage, image));
|
|
if (!save)
|
|
{
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo() << "No need to save.";
|
|
#endif
|
|
return;
|
|
}
|
|
bool main_thread = QThread::currentThread() == qApp->thread();
|
|
qDebug("capture task running in thread %p [main thread=%d]", QThread::currentThreadId(), main_thread);
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("capture task running in thread %p [main thread=%d]", QThread::currentThreadId(), main_thread);
|
|
#endif
|
|
|
|
if (!QDir(dir).exists()) {
|
|
if (!QDir().mkpath(dir)) {
|
|
qWarning("Failed to create capture dir [%s]", qPrintable(dir));
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("Failed to create capture dir [%s]", qPrintable(dir));
|
|
#endif
|
|
QMetaObject::invokeMethod(cap, "failed");
|
|
return;
|
|
}
|
|
}
|
|
name += QString::number(frame.pts(), 'f', 3); // timestamp()
|
|
QString path(dir + QStringLiteral("/") + name + QStringLiteral("."));
|
|
if (original_fmt) {
|
|
if (!frame.constBits(0)) {
|
|
frame = frame.to(frame.format());
|
|
}
|
|
path.append(frame.format().name());
|
|
qDebug("Saving capture to %s", qPrintable(path));
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
qWarning("VideoCapture is failed to open file %s", qPrintable(path));
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("VideoCapture is failed to open file %s", qPrintable(path));
|
|
#endif
|
|
QMetaObject::invokeMethod(cap, "failed");
|
|
return;
|
|
}
|
|
if (file.write(frame.frameData()) <= 0) {
|
|
qWarning("VideoCapture is failed to write captured frame with original format");
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("VideoCapture is failed to write captured frame with original format");
|
|
#endif
|
|
QMetaObject::invokeMethod(cap, "failed");
|
|
file.close();
|
|
return;
|
|
}
|
|
file.close();
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("VideoCapture saved done");
|
|
#endif
|
|
QMetaObject::invokeMethod(cap, "saved", Q_ARG(QString, path));
|
|
return;
|
|
}
|
|
if (image.isNull())
|
|
{
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("VideoCapture iamge is null");
|
|
#endif
|
|
return;
|
|
}
|
|
path.append(format.toLower());
|
|
qDebug("Saving capture to %s", qPrintable(path));
|
|
#if (TB5000_INSPECTION_VERSION)
|
|
// 이미지를 40% 영역으로 CROP
|
|
int cw = (int)(image.size().width() * 0.4);
|
|
int ch = (int)(image.size().height() * 0.4);
|
|
int cxoffset = (image.size().width() - cw) / 2;
|
|
int cyoffset = (image.size().height() - ch) / 2;
|
|
QRect crop = QRect(cxoffset,cyoffset,cw,ch);
|
|
image = image.copy(crop);
|
|
#endif // TB5000_INSPECTION_VERSION
|
|
|
|
bool ok = image.save(path, format.toLatin1().constData(), quality);
|
|
if (!ok) {
|
|
qWarning("Failed to save capture");
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("Failed to save capture");
|
|
#endif
|
|
QMetaObject::invokeMethod(cap, "failed");
|
|
}
|
|
QMetaObject::invokeMethod(cap, "saved", Q_ARG(QString, path));
|
|
#if (CAPTURE_DEBUG)
|
|
qInfo("VideoCapture saved done");
|
|
#endif
|
|
}
|
|
|
|
VideoCapture *cap;
|
|
bool save;
|
|
bool original_fmt;
|
|
int quality;
|
|
QString format, dir, name;
|
|
QImage::Format qfmt;
|
|
VideoFrame frame;
|
|
};
|
|
|
|
VideoCapture::VideoCapture(QObject *parent) :
|
|
QObject(parent)
|
|
, async(true)
|
|
, auto_save(true)
|
|
, original_fmt(false)
|
|
, qfmt(QImage::Format_ARGB32)
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
|
|
dir = QDesktopServices::storageLocation(QDesktopServices::PicturesLocation);
|
|
#else
|
|
dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
|
#endif
|
|
if (dir.isEmpty())
|
|
dir = qApp->applicationDirPath() + QStringLiteral("/capture");
|
|
fmt = QStringLiteral("PNG");
|
|
qual = -1;
|
|
// seems no direct connection is fine too
|
|
connect(qApp, SIGNAL(aboutToQuit()), SLOT(handleAppQuit()), Qt::DirectConnection);
|
|
}
|
|
|
|
void VideoCapture::setAsync(bool value)
|
|
{
|
|
if (async == value)
|
|
return;
|
|
async = value;
|
|
Q_EMIT asyncChanged();
|
|
}
|
|
|
|
bool VideoCapture::isAsync() const
|
|
{
|
|
return async;
|
|
}
|
|
|
|
void VideoCapture::setAutoSave(bool value)
|
|
{
|
|
if (auto_save == value)
|
|
return;
|
|
auto_save = value;
|
|
Q_EMIT autoSaveChanged();
|
|
}
|
|
|
|
bool VideoCapture::autoSave() const
|
|
{
|
|
return auto_save;
|
|
}
|
|
|
|
void VideoCapture::setOriginalFormat(bool value)
|
|
{
|
|
if (original_fmt == value)
|
|
return;
|
|
original_fmt = value;
|
|
Q_EMIT originalFormatChanged();
|
|
}
|
|
|
|
bool VideoCapture::isOriginalFormat() const
|
|
{
|
|
return original_fmt;
|
|
}
|
|
|
|
void VideoCapture::handleAppQuit()
|
|
{
|
|
app_is_dieing = true;
|
|
// TODO: how to cancel? since qt5.2, we can use clear()
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)
|
|
videoCaptureThreadPool()->clear();
|
|
#endif
|
|
videoCaptureThreadPool()->setExpiryTimeout(0);
|
|
videoCaptureThreadPool()->waitForDone();
|
|
}
|
|
|
|
void VideoCapture::capture()
|
|
{
|
|
Q_EMIT requested();
|
|
}
|
|
|
|
void VideoCapture::start()
|
|
{
|
|
Q_EMIT frameAvailable(frame);
|
|
if (!frame.isValid() || !frame.constBits(0)) { // if frame is always cloned, then size is at least width*height
|
|
qDebug("Captured frame from hardware decoder surface.");
|
|
}
|
|
CaptureTask *task = new CaptureTask(this);
|
|
// copy properties so the task will not be affect even if VideoCapture properties changed
|
|
task->save = autoSave();
|
|
task->original_fmt = original_fmt;
|
|
task->quality = qual;
|
|
task->dir = dir;
|
|
task->name = name;
|
|
task->format = fmt;
|
|
task->qfmt = qfmt;
|
|
task->frame = frame; //copy here and it's safe in capture thread because start() is called immediatly after setVideoFrame
|
|
if (isAsync()) {
|
|
videoCaptureThreadPool()->start(task);
|
|
} else {
|
|
task->run();
|
|
delete task;
|
|
}
|
|
}
|
|
|
|
void VideoCapture::setSaveFormat(const QString &format)
|
|
{
|
|
if (format.toLower() == fmt.toLower())
|
|
return;
|
|
fmt = format;
|
|
Q_EMIT saveFormatChanged();
|
|
}
|
|
|
|
QString VideoCapture::saveFormat() const
|
|
{
|
|
return fmt;
|
|
}
|
|
|
|
void VideoCapture::setQuality(int value)
|
|
{
|
|
if (qual == value)
|
|
return;
|
|
qual = value;
|
|
Q_EMIT qualityChanged();
|
|
}
|
|
|
|
int VideoCapture::quality() const
|
|
{
|
|
return qual;
|
|
}
|
|
|
|
void VideoCapture::setCaptureName(const QString &value)
|
|
{
|
|
if (name == value)
|
|
return;
|
|
name = value;
|
|
Q_EMIT captureNameChanged();
|
|
}
|
|
|
|
QString VideoCapture::captureName() const
|
|
{
|
|
return name;
|
|
}
|
|
|
|
void VideoCapture::setCaptureDir(const QString &value)
|
|
{
|
|
if (dir == value)
|
|
return;
|
|
dir = value;
|
|
Q_EMIT captureDirChanged();
|
|
}
|
|
|
|
QString VideoCapture::captureDir() const
|
|
{
|
|
return dir;
|
|
}
|
|
|
|
/*
|
|
* If the frame is not created for direct rendering, then the frame data is already deep copied, so detach is enough.
|
|
* TODO: map frame from texture etc.
|
|
*/
|
|
void VideoCapture::setVideoFrame(const VideoFrame &frame)
|
|
{
|
|
// parameter in ready(FAV::VideoFrame) ensure we can access the frame without lock
|
|
/*
|
|
* clone here may block VideoThread. But if not clone here, the frame may be
|
|
* modified outside and is not safe.
|
|
*/
|
|
this->frame = frame.clone(); // TODO: no clone, use detach()
|
|
}
|
|
|
|
} //namespace FAV
|
|
|