1 /* Webcamoid, webcam capture application.
2  * Copyright (C) 2015  Gonzalo Exequiel Pedone
3  *
4  * Webcamoid is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Webcamoid is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with Webcamoid. If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Web-Site: http://webcamoid.github.io/
18  */
19 
20 #include <QSize>
21 #include <QMutex>
22 #include <QQmlContext>
23 #include <QQuickItem>
24 #include <QQmlProperty>
25 #include <QQmlApplicationEngine>
26 #include <QSystemTrayIcon>
27 #include <QSettings>
28 #include <QFileInfo>
29 #include <QDateTime>
30 #include <QStandardPaths>
31 #include <QFileDialog>
32 #include <QApplication>
33 #include <ak.h>
34 #include <akcaps.h>
35 #include <akaudiocaps.h>
36 #include <akvideocaps.h>
37 
38 #include "mediatools.h"
39 #include "videodisplay.h"
40 #include "iconsprovider.h"
41 #include "pluginconfigs.h"
42 #include "mediasource.h"
43 #include "audiolayer.h"
44 #include "videoeffects.h"
45 #include "recording.h"
46 #include "updates.h"
47 #include "clioptions.h"
48 
49 #define COMMONS_PROJECT_URL "https://webcamoid.github.io/"
50 #define COMMONS_PROJECT_LICENSE_URL "https://raw.githubusercontent.com/webcamoid/webcamoid/master/COPYING"
51 #define COMMONS_PROJECT_DOWNLOADS_URL "https://webcamoid.github.io/#downloads"
52 #define COMMONS_PROJECT_ISSUES_URL "https://github.com/webcamoid/webcamoid/issues"
53 #define COMMONS_COPYRIGHT_NOTICE "Copyright (C) 2011-2018  Gonzalo Exequiel Pedone"
54 
55 class MediaToolsPrivate
56 {
57     public:
58         QQmlApplicationEngine *m_engine {nullptr};
59         PluginConfigsPtr m_pluginConfigs;
60         MediaSourcePtr m_mediaSource;
61         AudioLayerPtr m_audioLayer;
62         VideoEffectsPtr m_videoEffects;
63         RecordingPtr m_recording;
64         UpdatesPtr m_updates;
65         AkElementPtr m_virtualCamera;
66         QSystemTrayIcon *m_trayIcon {nullptr};
67         CliOptions m_cliOptions;
68         int m_windowWidth {0};
69         int m_windowHeight {0};
70         bool m_enableVirtualCamera {false};
71 
72         bool embedInterface(QQmlApplicationEngine *engine,
73                             QObject *ctrlInterface,
74                             const QString &where) const;
75 };
76 
MediaTools(QObject * parent)77 MediaTools::MediaTools(QObject *parent):
78     QObject(parent)
79 {
80     this->d = new MediaToolsPrivate;
81 
82     // Initialize environment.
83     this->d->m_trayIcon = new QSystemTrayIcon(QApplication::windowIcon(), this);
84     this->d->m_engine = new QQmlApplicationEngine();
85     this->d->m_engine->addImageProvider(QLatin1String("icons"), new IconsProvider);
86     this->d->m_engine->addImportPath(":/Ak/share/qml");
87     Ak::setQmlEngine(this->d->m_engine);
88     this->d->m_pluginConfigs =
89             PluginConfigsPtr(new PluginConfigs(this->d->m_cliOptions,
90                                                this->d->m_engine));
91     this->d->m_mediaSource = MediaSourcePtr(new MediaSource(this->d->m_engine));
92     this->d->m_audioLayer = AudioLayerPtr(new AudioLayer(this->d->m_engine));
93     this->d->m_videoEffects = VideoEffectsPtr(new VideoEffects(this->d->m_engine));
94     this->d->m_recording = RecordingPtr(new Recording(this->d->m_engine));
95     this->d->m_updates = UpdatesPtr(new Updates(this->d->m_engine));
96     this->d->m_virtualCamera = AkElement::create("VirtualCamera");
97 
98     if (this->d->m_virtualCamera) {
99         AkElement::link(this->d->m_videoEffects.data(),
100                         this->d->m_virtualCamera.data(),
101                         Qt::DirectConnection);
102         QObject::connect(this->d->m_virtualCamera.data(),
103                          SIGNAL(stateChanged(AkElement::ElementState)),
104                          this,
105                          SIGNAL(virtualCameraStateChanged(AkElement::ElementState)));
106     }
107 
108     AkElement::link(this->d->m_mediaSource.data(),
109                     this->d->m_videoEffects.data(),
110                     Qt::DirectConnection);
111     AkElement::link(this->d->m_mediaSource.data(),
112                     this->d->m_audioLayer.data(),
113                     Qt::DirectConnection);
114     AkElement::link(this->d->m_videoEffects.data(),
115                     this->d->m_recording.data(),
116                     Qt::DirectConnection);
117     AkElement::link(this->d->m_audioLayer.data(),
118                     this->d->m_recording.data(),
119                     Qt::DirectConnection);
120     QObject::connect(this->d->m_mediaSource.data(),
121                      &MediaSource::error,
122                      this,
123                      &MediaTools::error);
124     QObject::connect(this->d->m_mediaSource.data(),
125                      &MediaSource::stateChanged,
126                      this->d->m_videoEffects.data(),
127                      &VideoEffects::setState);
128     QObject::connect(this->d->m_mediaSource.data(),
129                      &MediaSource::stateChanged,
130                      this->d->m_audioLayer.data(),
131                      &AudioLayer::setOutputState);
132     QObject::connect(this->d->m_recording.data(),
133                      &Recording::stateChanged,
134                      this->d->m_audioLayer.data(),
135                      &AudioLayer::setInputState);
136     QObject::connect(this->d->m_mediaSource.data(),
137                      &MediaSource::audioCapsChanged,
138                      this->d->m_audioLayer.data(),
139                      &AudioLayer::setInputCaps);
140     QObject::connect(this->d->m_mediaSource.data(),
141                      &MediaSource::streamChanged,
142                      this->d->m_audioLayer.data(),
143                      [this] (const QString &stream)
144                      {
145                          this->d->m_audioLayer->setInputDescription(this->d->m_mediaSource->description(stream));
146                      });
147     QObject::connect(this->d->m_mediaSource.data(),
148                      &MediaSource::streamChanged,
149                      this,
150                      &MediaTools::updateVCamState);
151     QObject::connect(this->d->m_mediaSource.data(),
152                      &MediaSource::videoCapsChanged,
153                      this,
154                      &MediaTools::updateVCamCaps);
155     QObject::connect(this,
156                      &MediaTools::enableVirtualCameraChanged,
157                      this,
158                      &MediaTools::updateVCamState);
159     QObject::connect(this->d->m_pluginConfigs.data(),
160                      &PluginConfigs::pluginsChanged,
161                      this->d->m_videoEffects.data(),
162                      &VideoEffects::updateEffects);
163     QObject::connect(this->d->m_audioLayer.data(),
164                      &AudioLayer::outputCapsChanged,
165                      this->d->m_recording.data(),
166                      &Recording::setAudioCaps);
167     QObject::connect(this->d->m_mediaSource.data(),
168                      &MediaSource::videoCapsChanged,
169                      this->d->m_recording.data(),
170                      &Recording::setVideoCaps);
171     QObject::connect(qApp,
172                      &QCoreApplication::aboutToQuit,
173                      this->d->m_mediaSource.data(),
174                      [this] () {
175                         this->d->m_mediaSource->setState(AkElement::ElementStateNull);
176                      });
177 
178     this->loadConfigs();
179     this->updateVCamCaps(this->d->m_mediaSource->videoCaps());
180     this->d->m_recording->setVideoCaps(this->d->m_mediaSource->videoCaps());
181     this->d->m_recording->setAudioCaps(this->d->m_audioLayer->outputCaps());
182     this->d->m_audioLayer->setInputCaps(this->d->m_mediaSource->audioCaps());
183     this->d->m_audioLayer->setInputDescription(this->d->m_mediaSource->description(this->d->m_mediaSource->stream()));
184 }
185 
~MediaTools()186 MediaTools::~MediaTools()
187 {
188     this->saveConfigs();
189     delete this->d->m_engine;
190     delete this->d;
191 }
192 
windowWidth() const193 int MediaTools::windowWidth() const
194 {
195     return this->d->m_windowWidth;
196 }
197 
windowHeight() const198 int MediaTools::windowHeight() const
199 {
200     return this->d->m_windowHeight;
201 }
202 
enableVirtualCamera() const203 bool MediaTools::enableVirtualCamera() const
204 {
205     return this->d->m_enableVirtualCamera;
206 }
207 
virtualCameraState() const208 AkElement::ElementState MediaTools::virtualCameraState() const
209 {
210     if (this->d->m_virtualCamera)
211         return this->d->m_virtualCamera->state();
212 
213     return AkElement::ElementStateNull;
214 }
215 
applicationName() const216 QString MediaTools::applicationName() const
217 {
218     return QCoreApplication::applicationName();
219 }
220 
applicationVersion() const221 QString MediaTools::applicationVersion() const
222 {
223 #ifdef DAILY_BUILD
224     return QString(tr("Daily Build"));
225 #else
226     return QCoreApplication::applicationVersion();
227 #endif
228 }
229 
qtVersion() const230 QString MediaTools::qtVersion() const
231 {
232     return QT_VERSION_STR;
233 }
234 
copyrightNotice() const235 QString MediaTools::copyrightNotice() const
236 {
237     return COMMONS_COPYRIGHT_NOTICE;
238 }
239 
projectUrl() const240 QString MediaTools::projectUrl() const
241 {
242     return COMMONS_PROJECT_URL;
243 }
244 
projectLicenseUrl() const245 QString MediaTools::projectLicenseUrl() const
246 {
247     return COMMONS_PROJECT_LICENSE_URL;
248 }
249 
projectDownloadsUrl() const250 QString MediaTools::projectDownloadsUrl() const
251 {
252     return COMMONS_PROJECT_DOWNLOADS_URL;
253 }
254 
projectIssuesUrl() const255 QString MediaTools::projectIssuesUrl() const
256 {
257     return COMMONS_PROJECT_ISSUES_URL;
258 }
259 
fileNameFromUri(const QString & uri) const260 QString MediaTools::fileNameFromUri(const QString &uri) const
261 {
262     return QFileInfo(uri).baseName();
263 }
264 
matches(const QString & pattern,const QStringList & strings) const265 bool MediaTools::matches(const QString &pattern,
266                          const QStringList &strings) const
267 {
268     if (pattern.isEmpty())
269         return true;
270 
271     for (const QString &str: strings)
272         if (str.contains(QRegExp(pattern,
273                                  Qt::CaseInsensitive,
274                                  QRegExp::Wildcard)))
275             return true;
276 
277     return false;
278 }
279 
currentTime() const280 QString MediaTools::currentTime() const
281 {
282     return QDateTime::currentDateTime().toString("yyyy-MM-dd hh-mm-ss");
283 }
284 
standardLocations(const QString & type) const285 QStringList MediaTools::standardLocations(const QString &type) const
286 {
287     static const QMap<QString, QStandardPaths::StandardLocation> stdPaths = {
288         {"movies"  , QStandardPaths::MoviesLocation  },
289         {"pictures", QStandardPaths::PicturesLocation},
290     };
291 
292     if (stdPaths.contains(type))
293         return QStandardPaths::standardLocations(stdPaths[type]);
294 
295     return QStringList();
296 }
297 
saveFileDialog(const QString & caption,const QString & fileName,const QString & directory,const QString & suffix,const QString & filters) const298 QString MediaTools::saveFileDialog(const QString &caption,
299                                    const QString &fileName,
300                                    const QString &directory,
301                                    const QString &suffix,
302                                    const QString &filters) const
303 {
304     QFileDialog saveFileDialog(nullptr,
305                                caption,
306                                fileName,
307                                filters);
308 
309     saveFileDialog.setModal(true);
310     saveFileDialog.setDefaultSuffix(suffix);
311     saveFileDialog.setDirectory(directory);
312     saveFileDialog.setFileMode(QFileDialog::AnyFile);
313     saveFileDialog.setAcceptMode(QFileDialog::AcceptSave);
314 
315     if (saveFileDialog.exec() == QDialog::Accepted)
316         return saveFileDialog.selectedFiles().value(0);
317 
318     return QString();
319 }
320 
readFile(const QString & fileName) const321 QString MediaTools::readFile(const QString &fileName) const
322 {
323     QFile file(fileName);
324     file.open(QIODevice::ReadOnly | QIODevice::Text);
325     QString data = file.readAll();
326     file.close();
327 
328     return data;
329 }
330 
urlToLocalFile(const QUrl & url) const331 QString MediaTools::urlToLocalFile(const QUrl &url) const
332 {
333     return url.toLocalFile();
334 }
335 
embedVirtualCameraControls(const QString & where,const QString & name)336 bool MediaTools::embedVirtualCameraControls(const QString &where,
337                                             const QString &name)
338 {
339     if (!this->d->m_virtualCamera)
340         return false;
341 
342     auto ctrlInterface = this->d->m_virtualCamera->controlInterface(this->d->m_engine, "");
343 
344     if (!ctrlInterface)
345         return false;
346 
347     if (!name.isEmpty())
348         ctrlInterface->setObjectName(name);
349 
350     return this->d->embedInterface(this->d->m_engine, ctrlInterface, where);
351 }
352 
removeInterface(const QString & where,QQmlApplicationEngine * engine)353 void MediaTools::removeInterface(const QString &where,
354                                  QQmlApplicationEngine *engine)
355 {
356     if (!engine)
357         engine = this->d->m_engine;
358 
359     if (!engine)
360         return;
361 
362     for (auto &obj: engine->rootObjects()) {
363         auto item = obj->findChild<QQuickItem *>(where);
364 
365         if (!item)
366             continue;
367 
368         auto childItems = item->childItems();
369 
370         for (QQuickItem *child: childItems) {
371             child->setParentItem(nullptr);
372             child->setParent(nullptr);
373 
374             delete child;
375         }
376     }
377 }
378 
convertToAbsolute(const QString & path)379 QString MediaTools::convertToAbsolute(const QString &path)
380 {
381     if (!QDir::isRelativePath(path))
382         return QDir::cleanPath(path);
383 
384     static const QDir applicationDir(QCoreApplication::applicationDirPath());
385     QString absPath = applicationDir.absoluteFilePath(path);
386 
387     return QDir::cleanPath(absPath).replace('/', QDir::separator());
388 }
389 
embedInterface(QQmlApplicationEngine * engine,QObject * ctrlInterface,const QString & where) const390 bool MediaToolsPrivate::embedInterface(QQmlApplicationEngine *engine,
391                                        QObject *ctrlInterface,
392                                        const QString &where) const
393 {
394     if (!engine || !ctrlInterface)
395         return false;
396 
397     for (auto &obj: engine->rootObjects()) {
398         // First, find where to embed the UI.
399         auto item = obj->findChild<QQuickItem *>(where);
400 
401         if (!item)
402             continue;
403 
404         // Create an item with the plugin context.
405         auto interfaceItem = qobject_cast<QQuickItem *>(ctrlInterface);
406 
407         // Finally, embed the plugin item UI in the desired place.
408         interfaceItem->setParentItem(item);
409 
410         return true;
411     }
412 
413     return false;
414 }
415 
setWindowWidth(int windowWidth)416 void MediaTools::setWindowWidth(int windowWidth)
417 {
418     if (this->d->m_windowWidth == windowWidth)
419         return;
420 
421     this->d->m_windowWidth = windowWidth;
422     emit this->windowWidthChanged(windowWidth);
423 }
424 
setWindowHeight(int windowHeight)425 void MediaTools::setWindowHeight(int windowHeight)
426 {
427     if (this->d->m_windowHeight == windowHeight)
428         return;
429 
430     this->d->m_windowHeight = windowHeight;
431     emit this->windowHeightChanged(windowHeight);
432 }
433 
setEnableVirtualCamera(bool enableVirtualCamera)434 void MediaTools::setEnableVirtualCamera(bool enableVirtualCamera)
435 {
436     if (this->d->m_enableVirtualCamera == enableVirtualCamera)
437         return;
438 
439     this->d->m_enableVirtualCamera = enableVirtualCamera;
440     emit this->enableVirtualCameraChanged(enableVirtualCamera);
441 }
442 
setVirtualCameraState(AkElement::ElementState virtualCameraState)443 void MediaTools::setVirtualCameraState(AkElement::ElementState virtualCameraState)
444 {
445     if (this->d->m_virtualCamera) {
446         auto state = virtualCameraState;
447         auto vcamStream = this->d->m_virtualCamera->property("media").toString();
448 
449         if (this->d->m_enableVirtualCamera
450             && virtualCameraState == AkElement::ElementStatePlaying
451             && this->d->m_mediaSource->state() == AkElement::ElementStatePlaying
452             && this->d->m_mediaSource->stream() == vcamStream) {
453             // Prevents self blocking by pausing the virtual camera.
454             state = AkElement::ElementStatePaused;
455         }
456 
457         this->d->m_virtualCamera->setState(state);
458     }
459 }
460 
resetWindowWidth()461 void MediaTools::resetWindowWidth()
462 {
463     this->setWindowWidth(0);
464 }
465 
resetWindowHeight()466 void MediaTools::resetWindowHeight()
467 {
468     this->setWindowHeight(0);
469 }
470 
resetEnableVirtualCamera()471 void MediaTools::resetEnableVirtualCamera()
472 {
473     this->setEnableVirtualCamera(false);
474 }
475 
resetVirtualCameraState()476 void MediaTools::resetVirtualCameraState()
477 {
478     this->setVirtualCameraState(AkElement::ElementStateNull);
479 }
480 
loadConfigs()481 void MediaTools::loadConfigs()
482 {
483     QSettings config;
484 
485     config.beginGroup("GeneralConfigs");
486     QSize windowSize = config.value("windowSize", QSize(1024, 600)).toSize();
487     this->d->m_windowWidth = windowSize.width();
488     this->d->m_windowHeight = windowSize.height();
489     config.endGroup();
490 
491     config.beginGroup("VirtualCamera");
492     this->setEnableVirtualCamera(config.value("enable", false).toBool());
493     config.endGroup();
494 
495     auto optPaths =
496             this->d->m_cliOptions.value(this->d->m_cliOptions.vcamPathOpt()).split(';');
497 
498     QStringList driverPaths;
499 
500     for (auto path: optPaths) {
501         path = this->convertToAbsolute(path);
502 
503         if (QFileInfo::exists(path))
504             driverPaths << path;
505     }
506 
507     if (this->d->m_virtualCamera)
508         QMetaObject::invokeMethod(this->d->m_virtualCamera.data(),
509                                   "addDriverPaths",
510                                   Q_ARG(QStringList, driverPaths));
511 }
512 
saveConfigs()513 void MediaTools::saveConfigs()
514 {
515     QSettings config;
516 
517     config.beginGroup("GeneralConfigs");
518     config.setValue("windowSize", QSize(this->d->m_windowWidth,
519                                         this->d->m_windowHeight));
520     config.endGroup();
521 
522     config.beginGroup("VirtualCamera");
523     config.setValue("enable", this->enableVirtualCamera());
524     config.endGroup();
525 }
526 
show()527 void MediaTools::show()
528 {
529     // @uri Webcamoid
530     qmlRegisterType<VideoDisplay>("Webcamoid", 1, 0, "VideoDisplay");
531     this->d->m_engine->rootContext()->setContextProperty("Webcamoid", this);
532 
533     // Map tray icon to QML
534     this->d->m_engine->rootContext()->setContextProperty("trayIcon", this->d->m_trayIcon);
535 
536     // Map tray icon enums to QML
537     this->d->m_engine->rootContext()->setContextProperty("TrayIcon_NoIcon", QSystemTrayIcon::NoIcon);
538     this->d->m_engine->rootContext()->setContextProperty("TrayIcon_Information", QSystemTrayIcon::Information);
539     this->d->m_engine->rootContext()->setContextProperty("TrayIcon_Warning", QSystemTrayIcon::Warning);
540     this->d->m_engine->rootContext()->setContextProperty("TrayIcon_Critical", QSystemTrayIcon::Critical);
541 
542     this->d->m_engine->load(QUrl(QStringLiteral("qrc:/Webcamoid/share/qml/main.qml")));
543 
544     for (auto &obj: this->d->m_engine->rootObjects()) {
545         // First, find where to enbed the UI.
546         auto videoDisplay = obj->findChild<VideoDisplay *>("videoDisplay");
547 
548         if (!videoDisplay)
549             continue;
550 
551         AkElement::link(this->d->m_videoEffects.data(),
552                         videoDisplay,
553                         Qt::DirectConnection);
554         break;
555     }
556 
557     emit this->interfaceLoaded();
558 }
559 
updateVCamCaps(const AkCaps & videoCaps)560 void MediaTools::updateVCamCaps(const AkCaps &videoCaps)
561 {
562     if (!this->d->m_virtualCamera)
563         return;
564 
565     QMetaObject::invokeMethod(this->d->m_virtualCamera.data(),
566                               "clearStreams");
567     QMetaObject::invokeMethod(this->d->m_virtualCamera.data(),
568                               "addStream",
569                               Q_ARG(int, 0),
570                               Q_ARG(AkCaps, videoCaps));
571 }
572 
updateVCamState()573 void MediaTools::updateVCamState()
574 {
575     if (!this->d->m_virtualCamera)
576         return;
577 
578     if (this->d->m_enableVirtualCamera) {
579         if (this->d->m_mediaSource->state() == AkElement::ElementStatePlaying) {
580             auto vcamStream = this->d->m_virtualCamera->property("media").toString();
581 
582             // Prevents self blocking by pausing the virtual camera.
583             auto state = this->d->m_mediaSource->stream() == vcamStream?
584                              AkElement::ElementStatePaused:
585                              AkElement::ElementStatePlaying;
586             this->d->m_virtualCamera->setState(state);
587         }
588     } else
589         this->d->m_virtualCamera->setState(AkElement::ElementStateNull);
590 }
591 
592 #include "moc_mediatools.cpp"
593