mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-08 00:40:02 +01:00
Compare commits
331 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
109e6f590a | ||
|
|
8f0491ea57 | ||
|
|
e0522608a4 | ||
|
|
4b9a230803 | ||
|
|
fc5246efaa | ||
|
|
4eebfbb89f | ||
|
|
353d2c4744 | ||
|
|
2718e83132 | ||
|
|
7547d1179d | ||
|
|
61e89286bc | ||
|
|
53ae715816 | ||
|
|
8cb37dba36 | ||
|
|
4426f70de7 | ||
|
|
f0ca13150f | ||
|
|
780a20689c | ||
|
|
28f9ed1d8d | ||
|
|
2b5b8ad02c | ||
|
|
d5092b1765 | ||
|
|
fda62314f9 | ||
|
|
17018c137f | ||
|
|
8838c19c39 | ||
|
|
f02a99a4e2 | ||
|
|
7b26b0f23e | ||
|
|
0e9984827a | ||
|
|
033d41863a | ||
|
|
bc540044ac | ||
|
|
76a2535da3 | ||
|
|
ff48877d16 | ||
|
|
4b8efabc5f | ||
|
|
c79be090df | ||
|
|
626eab7e8f | ||
|
|
c103b7d883 | ||
|
|
cde055e29b | ||
|
|
1cb448c42e | ||
|
|
3d05444f30 | ||
|
|
7a551189d9 | ||
|
|
b885e70fed | ||
|
|
0a27c14041 | ||
|
|
eb8e33e311 | ||
|
|
2d44a60b90 | ||
|
|
135b6a5702 | ||
|
|
706c72fda8 | ||
|
|
fb7bdba388 | ||
|
|
733d08638d | ||
|
|
cb3cca8a64 | ||
|
|
a3a581794e | ||
|
|
f921e7610c | ||
|
|
8deb364025 | ||
|
|
3a9c6f56bf | ||
|
|
a612154123 | ||
|
|
bbc5e50491 | ||
|
|
a7689a8f54 | ||
|
|
731a1af1a6 | ||
|
|
f53ebd4389 | ||
|
|
baa6ddb401 | ||
|
|
315a8534d5 | ||
|
|
d77bd4034d | ||
|
|
fa71797ed2 | ||
|
|
8c63552573 | ||
|
|
a18d53c637 | ||
|
|
ffe05368e8 | ||
|
|
923d84f378 | ||
|
|
e5334aae0a | ||
|
|
4675be7e2a | ||
|
|
bf3fc61ef7 | ||
|
|
ebd9fab312 | ||
|
|
d359cf33d1 | ||
|
|
14bab1e299 | ||
|
|
4c4ad144b9 | ||
|
|
1157c0b1c5 | ||
|
|
68b2c5e0c1 | ||
|
|
b97fd06f2a | ||
|
|
51f0f5bd66 | ||
|
|
66f445997d | ||
|
|
73d4f7c1ea | ||
|
|
25fc5562db | ||
|
|
4d52bcb5b3 | ||
|
|
3d2de560b0 | ||
|
|
809e30d906 | ||
|
|
ef9e41f20d | ||
|
|
1b4849f214 | ||
|
|
e123d139e4 | ||
|
|
a8abd52afb | ||
|
|
091e99f21b | ||
|
|
b6593c2a83 | ||
|
|
5ac7887360 | ||
|
|
ed7627af6f | ||
|
|
1ea9fc54b2 | ||
|
|
3819571ec0 | ||
|
|
94f131fc57 | ||
|
|
3c20314aab | ||
|
|
1506d36407 | ||
|
|
aa4b2967c7 | ||
|
|
a42881d31f | ||
|
|
6a3ff2f235 | ||
|
|
fc4e3dc362 | ||
|
|
d6c689c5bb | ||
|
|
8676e9b900 | ||
|
|
c271cad9aa | ||
|
|
8e3bf786c0 | ||
|
|
a6ba694fbd | ||
|
|
790ccc320e | ||
|
|
26e951e59b | ||
|
|
a97581f5d7 | ||
|
|
5bf280ca4d | ||
|
|
fe00baa701 | ||
|
|
d3cb1d7f42 | ||
|
|
593363732a | ||
|
|
d00f4cf715 | ||
|
|
cac31dbb21 | ||
|
|
eb1fa6ca04 | ||
|
|
0ac515ea5a | ||
|
|
190e2a4952 | ||
|
|
0857b1bab6 | ||
|
|
f3e42fdc95 | ||
|
|
d617f3308a | ||
|
|
27cec85443 | ||
|
|
63f7cab508 | ||
|
|
ce0ac1bee1 | ||
|
|
2c2584c8df | ||
|
|
14fd4d96c3 | ||
|
|
dd7a63413c | ||
|
|
6d0c2301c1 | ||
|
|
8bf8f05add | ||
|
|
bd773d54c6 | ||
|
|
f4c52b7ed3 | ||
|
|
5b1504c8f6 | ||
|
|
06187b9a1a | ||
|
|
7fb6e57829 | ||
|
|
5ec954dbb5 | ||
|
|
a6bc30cf62 | ||
|
|
df165252fa | ||
|
|
da9c94f675 | ||
|
|
031cef6357 | ||
|
|
ef5f3efd2e | ||
|
|
bc8c4e3c7b | ||
|
|
f5da4c8bc2 | ||
|
|
644741a1ab | ||
|
|
09f46e7a27 | ||
|
|
db4e1d214f | ||
|
|
79433dd45c | ||
|
|
fe72c9b829 | ||
|
|
b37d22ba47 | ||
|
|
7fb08e618f | ||
|
|
63c6f1169b | ||
|
|
0eff8fd24d | ||
|
|
818e554d35 | ||
|
|
5a18dbaf37 | ||
|
|
ddd9bb4e99 | ||
|
|
38a2aa90e0 | ||
|
|
139137770c | ||
|
|
789bf1bd00 | ||
|
|
563e762dde | ||
|
|
28da5f8f39 | ||
|
|
5c42061fd9 | ||
|
|
843224ca35 | ||
|
|
e47e76962b | ||
|
|
2f0e4e3212 | ||
|
|
fb3e1d0d25 | ||
|
|
e9b7e55570 | ||
|
|
8c206898f0 | ||
|
|
a9c9683b8b | ||
|
|
4f43ddf088 | ||
|
|
a9c8b67975 | ||
|
|
d1b7073ff9 | ||
|
|
58afcacab9 | ||
|
|
5eddfcf196 | ||
|
|
9a87764949 | ||
|
|
fc4e40fba3 | ||
|
|
e8acfc1c26 | ||
|
|
eaadc210ae | ||
|
|
8002f3164c | ||
|
|
48f92bc52b | ||
|
|
d1e833e0a1 | ||
|
|
c5f0be2b32 | ||
|
|
dbcf3bb0ea | ||
|
|
e7a79f6cdc | ||
|
|
63b043dc4b | ||
|
|
d2a576c99c | ||
|
|
fc91e7cbdd | ||
|
|
0555361a57 | ||
|
|
c923815a01 | ||
|
|
7a4d2ac027 | ||
|
|
442e1096be | ||
|
|
6eaf8852ae | ||
|
|
3612fca707 | ||
|
|
4736d403a1 | ||
|
|
a18fd3177c | ||
|
|
5930b0f8fe | ||
|
|
1c7c64db59 | ||
|
|
9bc780bcda | ||
|
|
b3f89e0464 | ||
|
|
1de4822c67 | ||
|
|
c846e4072a | ||
|
|
c4f26bd500 | ||
|
|
041c01135a | ||
|
|
aa904f26ad | ||
|
|
ff99d37eb6 | ||
|
|
e8a500dc99 | ||
|
|
9f4f247cd2 | ||
|
|
e1ac930dd6 | ||
|
|
c20ed94f46 | ||
|
|
4efe754a8d | ||
|
|
bb83f7fcb7 | ||
|
|
79fa6082b0 | ||
|
|
f2ecc88955 | ||
|
|
7253c1ec1a | ||
|
|
cf32c9fc12 | ||
|
|
3086735be1 | ||
|
|
5a54e84dd8 | ||
|
|
1ef26c0c95 | ||
|
|
887142079b | ||
|
|
b75ea00c0d | ||
|
|
319fbfa84d | ||
|
|
3874252797 | ||
|
|
5dfc45af5f | ||
|
|
1f203801db | ||
|
|
291410a2b3 | ||
|
|
dfc4937688 | ||
|
|
a0b763ab71 | ||
|
|
ad36ac5cd9 | ||
|
|
cd40d6d7e8 | ||
|
|
ec4214ebf8 | ||
|
|
a403d40b6c | ||
|
|
7dcfc97f33 | ||
|
|
5ea056a483 | ||
|
|
cd1702bb53 | ||
|
|
6ff266581a | ||
|
|
6b7d108407 | ||
|
|
473e24bcd7 | ||
|
|
1f5056bf15 | ||
|
|
61fa062794 | ||
|
|
0e48cf4505 | ||
|
|
b606f479e9 | ||
|
|
2ccbf1ec12 | ||
|
|
ac6e84bb1c | ||
|
|
11d12c1f29 | ||
|
|
c9707e7335 | ||
|
|
2c0be68a3c | ||
|
|
2add317106 | ||
|
|
60ec11982a | ||
|
|
b2284cf1b4 | ||
|
|
e87ef2774b | ||
|
|
7a9fcaefd6 | ||
|
|
2333a7a11a | ||
|
|
2a7857c499 | ||
|
|
e892dc1eb5 | ||
|
|
9c8d1f31f6 | ||
|
|
048db7a44b | ||
|
|
1d94d494b6 | ||
|
|
c6ac35addb | ||
|
|
9ec279754b | ||
|
|
bdcf28c5da | ||
|
|
edf0f8074a | ||
|
|
c83a946cbd | ||
|
|
8604babeb6 | ||
|
|
7252b74539 | ||
|
|
08fbaa039f | ||
|
|
30f9fb50eb | ||
|
|
e3578df8a0 | ||
|
|
37445b8857 | ||
|
|
99ea14fab0 | ||
|
|
1717c143b2 | ||
|
|
53223d0876 | ||
|
|
096bcb4132 | ||
|
|
05cc70bdbd | ||
|
|
45653b52b5 | ||
|
|
d1841f2863 | ||
|
|
f85de11711 | ||
|
|
9d81a105ee | ||
|
|
deb6af9dea | ||
|
|
ab512b76aa | ||
|
|
bcbeee7247 | ||
|
|
bcdc94c3b9 | ||
|
|
6ebcf49758 | ||
|
|
a936ab6851 | ||
|
|
95378660dd | ||
|
|
e49bdac3e8 | ||
|
|
48380fab7e | ||
|
|
ce92529a84 | ||
|
|
8e29a555c8 | ||
|
|
a1b6ec066b | ||
|
|
fb59bf491f | ||
|
|
86aec7d2ba | ||
|
|
579f7d5609 | ||
|
|
c87b1ac363 | ||
|
|
543648112b | ||
|
|
daa3b9e978 | ||
|
|
fb8da181da | ||
|
|
6cc5a8af9e | ||
|
|
1a1956962a | ||
|
|
e422a1b403 | ||
|
|
295ece79ae | ||
|
|
08f8ee159a | ||
|
|
e2d416b3fb | ||
|
|
16ed97b4cb | ||
|
|
82739702bd | ||
|
|
0010c9e3d5 | ||
|
|
f59d4af92b | ||
|
|
a7df619a05 | ||
|
|
f3759c2ef5 | ||
|
|
d547f3a8a8 | ||
|
|
583e53d8a8 | ||
|
|
a27bf08ce0 | ||
|
|
060fb5ad2d | ||
|
|
ffc00d9035 | ||
|
|
83a9d281c2 | ||
|
|
b6c853c308 | ||
|
|
552c09d377 | ||
|
|
61164e627b | ||
|
|
3c71ee1ff2 | ||
|
|
07f610e84a | ||
|
|
faf344bc03 | ||
|
|
e9482a3dfc | ||
|
|
d4a7ce3487 | ||
|
|
3ef2737d82 | ||
|
|
582b67f4e1 | ||
|
|
5dd6c0af78 | ||
|
|
893e4f4723 | ||
|
|
d137e87f0e | ||
|
|
7f152077e5 | ||
|
|
74a9b229d0 | ||
|
|
80cf57a979 | ||
|
|
19ba943075 | ||
|
|
ba0e25a272 | ||
|
|
575c487fa6 | ||
|
|
1e91b2aa29 | ||
|
|
e10cf40f38 | ||
|
|
23defd2117 | ||
|
|
1227b87565 | ||
|
|
6a96c91fe1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ osx/.DS_Store
|
||||
.DS_Store
|
||||
|
||||
osx/runvimix
|
||||
|
||||
*.autosave
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -19,3 +19,6 @@
|
||||
[submodule "ext/glm"]
|
||||
path = ext/glm
|
||||
url = https://github.com/g-truc/glm.git
|
||||
[submodule "ext/link"]
|
||||
path = ext/link
|
||||
url = https://github.com/Ableton/link.git
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
#include "Log.h"
|
||||
#include "View.h"
|
||||
@@ -12,6 +32,7 @@
|
||||
#include "Settings.h"
|
||||
#include "BaseToolkit.h"
|
||||
#include "Interpolator.h"
|
||||
#include "SystemToolkit.h"
|
||||
|
||||
#include "ActionManager.h"
|
||||
|
||||
@@ -26,32 +47,36 @@ using namespace tinyxml2;
|
||||
|
||||
void captureMixerSession(tinyxml2::XMLDocument *doc, std::string node, std::string label)
|
||||
{
|
||||
|
||||
// create node
|
||||
XMLElement *sessionNode = doc->NewElement( node.c_str() );
|
||||
doc->InsertEndChild(sessionNode);
|
||||
// label describes the action
|
||||
sessionNode->SetAttribute("label", label.c_str() );
|
||||
// label describes the action
|
||||
sessionNode->SetAttribute("date", SystemToolkit::date_time_string().c_str() );
|
||||
// view indicates the view when this action occured
|
||||
sessionNode->SetAttribute("view", (int) Mixer::manager().view()->mode());
|
||||
|
||||
// get session to operate on
|
||||
Session *se = Mixer::manager().session();
|
||||
se->lock();
|
||||
|
||||
// get the thumbnail (requires one opengl update to render)
|
||||
FrameBufferImage *thumbnail = se->thumbnail();
|
||||
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, doc);
|
||||
if (imageelement)
|
||||
sessionNode->InsertEndChild(imageelement);
|
||||
delete thumbnail;
|
||||
FrameBufferImage *thumbnail = se->renderThumbnail();
|
||||
if (thumbnail) {
|
||||
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, doc);
|
||||
if (imageelement)
|
||||
sessionNode->InsertEndChild(imageelement);
|
||||
delete thumbnail;
|
||||
}
|
||||
|
||||
// save session attributes
|
||||
sessionNode->SetAttribute("activationThreshold", se->activationThreshold());
|
||||
|
||||
// save all sources using source visitor
|
||||
SessionVisitor sv(doc, sessionNode);
|
||||
for (auto iter = se->begin(); iter != se->end(); ++iter, sv.setRoot(sessionNode) )
|
||||
(*iter)->accept(sv);
|
||||
|
||||
se->unlock();
|
||||
}
|
||||
|
||||
|
||||
@@ -184,7 +209,7 @@ void Action::restore(uint target)
|
||||
|
||||
|
||||
|
||||
void Action::snapshot(const std::string &label)
|
||||
void Action::snapshot(const std::string &label, bool threaded)
|
||||
{
|
||||
// ignore if locked
|
||||
if (locked_)
|
||||
@@ -199,8 +224,11 @@ void Action::snapshot(const std::string &label)
|
||||
Session *se = Mixer::manager().session();
|
||||
se->snapshots()->keys_.push_back(id);
|
||||
|
||||
// threaded capture state of current session
|
||||
std::thread(captureMixerSession, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label).detach();
|
||||
if (threaded)
|
||||
// threaded capture state of current session
|
||||
std::thread(captureMixerSession, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label).detach();
|
||||
else
|
||||
captureMixerSession(se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), snap_label);
|
||||
|
||||
#ifdef ACTION_DEBUG
|
||||
Log::Info("Snapshot stored %d '%s'", id, snap_label.c_str());
|
||||
@@ -280,6 +308,23 @@ std::string Action::label(uint64_t snapshotid) const
|
||||
return label;
|
||||
}
|
||||
|
||||
std::string Action::date(uint64_t snapshotid) const
|
||||
{
|
||||
std::string date = "";
|
||||
|
||||
// get snapshot node of target in current session
|
||||
Session *se = Mixer::manager().session();
|
||||
const XMLElement *snap = se->snapshots()->xmlDoc_->FirstChildElement( SNAPSHOT_NODE(snapshotid).c_str() );
|
||||
|
||||
if (snap){
|
||||
const char *d = snap->Attribute("date");
|
||||
if (d)
|
||||
date = std::string(d);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
void Action::setLabel (uint64_t snapshotid, const std::string &label)
|
||||
{
|
||||
open(snapshotid);
|
||||
@@ -303,6 +348,13 @@ FrameBufferImage *Action::thumbnail(uint64_t snapshotid) const
|
||||
return img;
|
||||
}
|
||||
|
||||
void Action::clearSnapshots()
|
||||
{
|
||||
Session *se = Mixer::manager().session();
|
||||
while (!se->snapshots()->keys_.empty())
|
||||
remove(se->snapshots()->keys_.front());
|
||||
}
|
||||
|
||||
void Action::remove(uint64_t snapshotid)
|
||||
{
|
||||
if (snapshotid > 0)
|
||||
@@ -396,3 +448,68 @@ void Action::interpolate(float val, uint64_t snapshotid)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// static multithreaded version saving
|
||||
static void saveSnapshot(const std::string& filename, tinyxml2::XMLElement *snapshot_node)
|
||||
{
|
||||
if (!snapshot_node){
|
||||
Log::Warning("Invalid version.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
const char *l = snapshot_node->Attribute("label");
|
||||
if (!l) {
|
||||
Log::Warning("Invalid version.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// load the file: is it a session?
|
||||
tinyxml2::XMLDocument xmlDoc;
|
||||
XMLError eResult = xmlDoc.LoadFile(filename.c_str());
|
||||
if ( XMLResultError(eResult)){
|
||||
Log::Warning("%s could not be openned for re-export.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
XMLElement *header = xmlDoc.FirstChildElement(APP_NAME);
|
||||
if (header == nullptr) {
|
||||
Log::Warning("%s is not a %s session file.", filename.c_str(), APP_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all snapshots
|
||||
XMLElement* snapshotNode = xmlDoc.FirstChildElement("Snapshots");
|
||||
xmlDoc.DeleteChild(snapshotNode);
|
||||
|
||||
// swap "Session" node with version_node
|
||||
XMLElement *sessionNode = xmlDoc.FirstChildElement("Session");
|
||||
xmlDoc.DeleteChild(sessionNode);
|
||||
sessionNode = snapshot_node->DeepClone(&xmlDoc)->ToElement();
|
||||
sessionNode->SetName("Session");
|
||||
xmlDoc.InsertEndChild(sessionNode);
|
||||
|
||||
// we got a session, set a new export filename
|
||||
std::string newfilename = filename;
|
||||
newfilename.insert(filename.size()-4, "_" + std::string(l));
|
||||
|
||||
// save new file to disk
|
||||
if ( XMLSaveDoc(&xmlDoc, newfilename) )
|
||||
Log::Notify("Version exported to %s.", newfilename.c_str());
|
||||
else
|
||||
// error
|
||||
Log::Warning("Failed to export Session file %s.", newfilename.c_str());
|
||||
}
|
||||
|
||||
void Action::saveas(const std::string& filename, uint64_t snapshotid)
|
||||
{
|
||||
// ignore if locked or if no label is given
|
||||
if (locked_)
|
||||
return;
|
||||
|
||||
if (snapshotid > 0)
|
||||
open(snapshotid);
|
||||
|
||||
if (snapshot_node_) {
|
||||
// launch a thread to save the session
|
||||
std::thread (saveSnapshot, filename, snapshot_node_).detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define ACTIONMANAGER_H
|
||||
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
|
||||
#include <tinyxml2.h>
|
||||
@@ -38,17 +39,19 @@ public:
|
||||
FrameBufferImage *thumbnail (uint s) const;
|
||||
|
||||
// Snapshots
|
||||
void snapshot (const std::string &label = "");
|
||||
|
||||
void snapshot (const std::string &label = "", bool threaded = false);
|
||||
void clearSnapshots ();
|
||||
std::list<uint64_t> snapshots () const;
|
||||
uint64_t currentSnapshot () const { return snapshot_id_; }
|
||||
|
||||
void open (uint64_t snapshotid);
|
||||
void open (uint64_t snapshotid);
|
||||
void replace (uint64_t snapshotid = 0);
|
||||
void restore (uint64_t snapshotid = 0);
|
||||
void remove (uint64_t snapshotid = 0);
|
||||
void saveas (const std::string& filename, uint64_t snapshotid = 0);
|
||||
|
||||
std::string label (uint64_t snapshotid) const;
|
||||
std::string date (uint64_t snapshotid) const;
|
||||
std::list<std::string> labels () const;
|
||||
void setLabel (uint64_t snapshotid, const std::string &label);
|
||||
FrameBufferImage *thumbnail (uint64_t snapshotid) const;
|
||||
|
||||
118
BaseToolkit.cpp
118
BaseToolkit.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "BaseToolkit.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -7,6 +26,7 @@
|
||||
#include <iomanip>
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <map>
|
||||
|
||||
#include <locale>
|
||||
#include <unicode/ustream.h>
|
||||
@@ -52,26 +72,45 @@ std::string BaseToolkit::uniqueName(const std::string &basename, std::list<std::
|
||||
|
||||
std::string BaseToolkit::transliterate(const std::string &input)
|
||||
{
|
||||
auto ucs = icu::UnicodeString::fromUTF8(input);
|
||||
// because icu::Transliterator is slow, we keep a dictionnary of already
|
||||
// transliterated texts to be faster during repeated calls (update of user interface)
|
||||
static std::map<std::string, std::string> dictionnary_;
|
||||
std::map<std::string, std::string>::const_iterator existingentry = dictionnary_.find(input);
|
||||
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
icu::Transliterator *firstTrans = icu::Transliterator::createInstance(
|
||||
"any-NFKD ; [:Nonspacing Mark:] Remove; NFKC; Latin", UTRANS_FORWARD, status);
|
||||
firstTrans->transliterate(ucs);
|
||||
delete firstTrans;
|
||||
if (existingentry == dictionnary_.cend()) {
|
||||
|
||||
icu::Transliterator *secondTrans = icu::Transliterator::createInstance(
|
||||
"any-NFKD ; [:Nonspacing Mark:] Remove; [@!#$*%~] Remove; NFKC", UTRANS_FORWARD, status);
|
||||
secondTrans->transliterate(ucs);
|
||||
delete secondTrans;
|
||||
auto ucs = icu::UnicodeString::fromUTF8(input);
|
||||
|
||||
std::ostringstream output;
|
||||
output << ucs;
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
icu::Transliterator *firstTrans = icu::Transliterator::createInstance(
|
||||
"any-NFKD ; [:Nonspacing Mark:] Remove; NFKC; Latin", UTRANS_FORWARD, status);
|
||||
firstTrans->transliterate(ucs);
|
||||
delete firstTrans;
|
||||
|
||||
return output.str();
|
||||
icu::Transliterator *secondTrans = icu::Transliterator::createInstance(
|
||||
"any-NFKD ; [:Nonspacing Mark:] Remove; [@!#$*%~] Remove; NFKC", UTRANS_FORWARD, status);
|
||||
secondTrans->transliterate(ucs);
|
||||
delete secondTrans;
|
||||
|
||||
std::ostringstream output;
|
||||
output << ucs;
|
||||
|
||||
// remember for future
|
||||
dictionnary_[input] = output.str();
|
||||
}
|
||||
|
||||
// return remembered transliterated text
|
||||
return dictionnary_[input];
|
||||
}
|
||||
|
||||
|
||||
std::string BaseToolkit::unspace(const std::string &input)
|
||||
{
|
||||
std::string output = input;
|
||||
std::replace( output.begin(), output.end(), ' ', '_');
|
||||
return output;
|
||||
}
|
||||
|
||||
std::string BaseToolkit::byte_to_string(long b)
|
||||
{
|
||||
double numbytes = static_cast<double>(b);
|
||||
@@ -85,7 +124,8 @@ std::string BaseToolkit::byte_to_string(long b)
|
||||
++i;
|
||||
numbytes /= 1024.0;
|
||||
}
|
||||
oss << std::fixed << std::setprecision(2) << numbytes << *i;
|
||||
oss << std::fixed << std::setprecision(2) << numbytes;
|
||||
if (i != list.end()) oss << *i;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
@@ -102,21 +142,59 @@ std::string BaseToolkit::bits_to_string(long b)
|
||||
++i;
|
||||
numbytes /= 1000.0;
|
||||
}
|
||||
oss << std::fixed << std::setprecision(2) << numbytes << *i;
|
||||
oss << std::fixed << std::setprecision(2) << numbytes;
|
||||
if (i != list.end()) oss << *i;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
|
||||
std::string BaseToolkit::trunc_string(const std::string& path, int N)
|
||||
std::string BaseToolkit::truncated(const std::string& str, int N)
|
||||
{
|
||||
std::string trunc = path;
|
||||
int l = path.size();
|
||||
std::string trunc = str;
|
||||
int l = str.size();
|
||||
if ( l > N ) {
|
||||
trunc = std::string("...") + path.substr( l - N + 3 );
|
||||
trunc = std::string("...") + str.substr( l - N + 3 );
|
||||
}
|
||||
return trunc;
|
||||
}
|
||||
|
||||
std::list<std::string> BaseToolkit::splitted(const std::string& str, char delim)
|
||||
{
|
||||
std::list<std::string> strings;
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while ((start = str.find_first_not_of(delim, end)) != std::string::npos) {
|
||||
end = str.find(delim, start);
|
||||
size_t delta = start > 0 ? 1 : 0;
|
||||
strings.push_back(str.substr( start -delta, end - start + delta));
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
|
||||
std::string BaseToolkit::joinned(std::list<std::string> strlist, char separator)
|
||||
{
|
||||
std::string str;
|
||||
for (auto it = strlist.cbegin(); it != strlist.cend(); ++it)
|
||||
str += (*it) + separator;
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
bool BaseToolkit::is_a_number(const std::string& str, int *val)
|
||||
{
|
||||
bool isanumber = false;
|
||||
|
||||
try {
|
||||
*val = std::stoi(str);
|
||||
isanumber = true;
|
||||
}
|
||||
catch (const std::invalid_argument&) {
|
||||
// avoids crash
|
||||
}
|
||||
|
||||
return isanumber;
|
||||
}
|
||||
|
||||
std::string BaseToolkit::common_prefix( const std::list<std::string> & allStrings )
|
||||
{
|
||||
@@ -221,7 +299,7 @@ std::string BaseToolkit::common_numbered_pattern(const std::list<std::string> &a
|
||||
*min = std::min(*min, std::atoi(s.c_str()) );
|
||||
if (n < 0)
|
||||
n = s.size();
|
||||
else if ( n != s.size() ) {
|
||||
else if ( n != (int) s.size() ) {
|
||||
n = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,26 @@ std::string uniqueName(const std::string &basename, std::list<std::string> exist
|
||||
// get a transliteration to Latin of any string
|
||||
std::string transliterate(const std::string &input);
|
||||
|
||||
// replaces spaces by underscores in a string
|
||||
std::string unspace(const std::string &input);
|
||||
|
||||
// get a string to display memory size with unit KB, MB, GB, TB
|
||||
std::string byte_to_string(long b);
|
||||
|
||||
// get a string to display bit size with unit Kbit, MBit, Gbit, Tbit
|
||||
std::string bits_to_string(long b);
|
||||
|
||||
// Truncate a string to display the right most N characters (e.g. ./home/me/toto.mpg -> ...ome/me/toto.mpg)
|
||||
std::string trunc_string(const std::string& path, int N);
|
||||
// cut a string to display the right most N characters (e.g. /home/me/toto.mpg -> ...ome/me/toto.mpg)
|
||||
std::string truncated(const std::string& str, int N);
|
||||
|
||||
// split a string into list of strings separated by delimitor (e.g. /home/me/toto.mpg -> {home, me, toto.mpg} )
|
||||
std::list<std::string> splitted(const std::string& str, char delim);
|
||||
|
||||
// rebuilds a splitted string
|
||||
std::string joinned(std::list<std::string> strlist, char separator = ' ');
|
||||
|
||||
// returns true if the string
|
||||
bool is_a_number(const std::string& str, int *val = nullptr);
|
||||
|
||||
// find common parts in a list of strings
|
||||
std::string common_prefix(const std::list<std::string> &allStrings);
|
||||
|
||||
266
CMakeLists.txt
266
CMakeLists.txt
@@ -20,15 +20,18 @@ if(GIT_EXECUTABLE)
|
||||
add_definitions(-DVIMIX_VERSION_MAJOR=${VIMIX_VERSION_MAJOR})
|
||||
add_definitions(-DVIMIX_VERSION_MINOR=${VIMIX_VERSION_MINOR})
|
||||
add_definitions(-DVIMIX_VERSION_PATCH=${VIMIX_VERSION_PATCH})
|
||||
message(STATUS "Compiling vimix version ${VIMIX_VERSION_MAJOR}.${VIMIX_VERSION_MINOR}.${VIMIX_VERSION_PATCH}")
|
||||
else()
|
||||
message(STATUS "Compiling vimix (unknown version)")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
message(STATUS "Compiling vimix version ${VIMIX_VERSION_MAJOR}.${VIMIX_VERSION_MINOR}.${VIMIX_VERSION_PATCH}")
|
||||
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "ON")
|
||||
set(CMAKE_INCLUDE_CURRENTDIR ON)
|
||||
|
||||
# Find the cmake modules
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules )
|
||||
include(MacroLogFeature)
|
||||
include(MacroFindGStreamerLibrary)
|
||||
|
||||
if(UNIX)
|
||||
if (APPLE)
|
||||
@@ -37,16 +40,19 @@ if(UNIX)
|
||||
# the RPATH to be used when installing
|
||||
set(CMAKE_SKIP_RPATH TRUE)
|
||||
set(OpenGL_DIR /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/)
|
||||
set(CMAKE_OSX_ARCHITECTURES x86_64)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
|
||||
# set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X version to target for deployment")
|
||||
set (ICU_ROOT /usr/local/Cellar/icu4c/67.1)
|
||||
set(CMAKE_OSX_ARCHITECTURES "x86_64")
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14")
|
||||
|
||||
# find icu4c in OSX (pretty well hidden...)
|
||||
set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/opt/icu4c/lib/pkgconfig")
|
||||
|
||||
else()
|
||||
add_definitions(-DLINUX)
|
||||
|
||||
# linux opengl
|
||||
set(OpenGL_GL_PREFERENCE "GLVND")
|
||||
|
||||
# linux dialogs use GTK
|
||||
find_package(GTK 3.0 REQUIRED)
|
||||
macro_log_feature(GTK_FOUND "GTK" "GTK cross-platform widget toolkit" "http://www.gtk.org" TRUE)
|
||||
|
||||
@@ -71,9 +77,6 @@ macro_log_feature(GOBJECT_FOUND "GObject" "GTK object-oriented framework" "http:
|
||||
find_package(PNG REQUIRED)
|
||||
macro_log_feature(PNG_FOUND "PNG" "Portable Network Graphics" "http://www.libpng.org" TRUE)
|
||||
|
||||
find_package(ICU REQUIRED COMPONENTS i18n io uc)
|
||||
macro_log_feature(ICU_FOUND "ICU" "International Components for Unicode" "http://site.icu-project.org" TRUE)
|
||||
|
||||
#
|
||||
# GSTREAMER
|
||||
#
|
||||
@@ -82,49 +85,46 @@ find_package(GStreamer 1.0.0 COMPONENTS base)
|
||||
macro_log_feature(GSTREAMER_FOUND "GStreamer"
|
||||
"Open Source Multiplatform Multimedia Framework"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
macro_log_feature(GSTREAMER_BASE_LIBRARY_FOUND "GStreamer base library"
|
||||
"${GSTREAMER_BASE_LIBRARY}"
|
||||
"http://gstreamer.freedesktop.org/" FALSE "1.0.0")
|
||||
|
||||
find_package(GStreamerPluginsBase 1.0.0 COMPONENTS app audio video pbutils gl)
|
||||
macro_log_feature(GSTREAMER_APP_LIBRARY_FOUND "GStreamer app library"
|
||||
"${GSTREAMER_APP_LIBRARY}"
|
||||
macro_log_feature(GSTREAMER_APP_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer app library"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
macro_log_feature(GSTREAMER_AUDIO_LIBRARY_FOUND "GStreamer audio library"
|
||||
"${GSTREAMER_AUDIO_LIBRARY}"
|
||||
macro_log_feature(GSTREAMER_AUDIO_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer audio library"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
macro_log_feature(GSTREAMER_VIDEO_LIBRARY_FOUND "GStreamer video library"
|
||||
"${GSTREAMER_VIDEO_LIBRARY}"
|
||||
macro_log_feature(GSTREAMER_VIDEO_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer video library"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
macro_log_feature(GSTREAMER_PBUTILS_LIBRARY_FOUND "GStreamer pbutils library"
|
||||
"${GSTREAMER_PBUTILS_LIBRARY}"
|
||||
macro_log_feature(GSTREAMER_PBUTILS_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer pbutils library"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
macro_log_feature(GSTREAMER_GL_LIBRARY_FOUND "GStreamer opengl library"
|
||||
"${GSTREAMER_GL_LIBRARY}"
|
||||
macro_log_feature(GSTREAMER_GL_LIBRARY_FOUND "GStreamerPluginsBase" "GStreamer opengl library"
|
||||
"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
#find_package(GStreamerPluginsBad 1.0.0 COMPONENTS player)
|
||||
#macro_log_feature(GSTREAMER_PLAYER_LIBRARY_FOUND "GStreamer player library"
|
||||
#"${GSTREAMER_PLAYER_LIBRARY}"
|
||||
#"http://gstreamer.freedesktop.org/" TRUE "1.0.0")
|
||||
|
||||
|
||||
# Various preprocessor definitions for GST
|
||||
add_definitions(-DGST_DISABLE_XML -DGST_DISABLE_LOADSAVE)
|
||||
|
||||
|
||||
#
|
||||
# ICU4C
|
||||
#
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(ICU REQUIRED icu-i18n icu-uc icu-io)
|
||||
else ()
|
||||
find_package(ICU REQUIRED COMPONENTS i18n io uc)
|
||||
endif ()
|
||||
macro_log_feature(ICU_FOUND "ICU" "International Components for Unicode" "http://site.icu-project.org" TRUE)
|
||||
|
||||
#
|
||||
# GLFW3
|
||||
# NB: set glfw3_PATH to /usr/local/Cellar/glfw/3.3.2/lib/cmake/glfw3
|
||||
#
|
||||
find_package(glfw3 3.2 REQUIRED)
|
||||
macro_log_feature(glfw3_FOUND "GLFW3" "Open Source multi-platform library for OpenGL" "http://www.glfw.org" TRUE)
|
||||
set(GLFW_LIBRARY glfw)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(GLFW3 REQUIRED glfw3>=3.2)
|
||||
else ()
|
||||
find_package(glfw3 3.2 REQUIRED)
|
||||
endif()
|
||||
macro_log_feature(GLFW3_FOUND "glfw3" "Open Source multi-platform library for OpenGL" "http://www.glfw.org" TRUE)
|
||||
|
||||
|
||||
macro_display_feature_log()
|
||||
|
||||
@@ -135,14 +135,21 @@ set(BUILD_STATIC_LIBS ON)
|
||||
# GLM
|
||||
#
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/glm)
|
||||
message(STATUS "Compiling 'GLM' OpenGL mathematics https://glm.g-truc.net")
|
||||
message(STATUS "Compiling 'GLM' OpenGL mathematics https://glm.g-truc.net -- ${CMAKE_CURRENT_SOURCE_DIR}/ext/glm")
|
||||
|
||||
#
|
||||
# Ableton LINK
|
||||
#
|
||||
#add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/link)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/ext/link/AbletonLinkConfig.cmake)
|
||||
message(STATUS "Compiling Ableton 'Link' https://github.com/Ableton/link -- ${CMAKE_CURRENT_SOURCE_DIR}/ext/link")
|
||||
|
||||
#
|
||||
# GLAD
|
||||
#
|
||||
set(GLAD_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/glad/include)
|
||||
add_library(GLAD "${CMAKE_CURRENT_SOURCE_DIR}/ext/glad/src/glad.c")
|
||||
message(STATUS "Compiling 'GLAD' Open source multi-language OpenGL loader https://glad.dav1d.de -- ${GLAD_INCLUDE_DIR}.")
|
||||
message(STATUS "Compiling 'GLAD' Open source multi-language OpenGL loader https://glad.dav1d.de -- ${GLAD_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# DEAR IMGUI
|
||||
@@ -158,23 +165,14 @@ set(IMGUI_SRCS
|
||||
set(IMGUI_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/imgui)
|
||||
add_library(IMGUI "${IMGUI_SRCS}")
|
||||
target_compile_definitions(IMGUI PRIVATE "IMGUI_IMPL_OPENGL_LOADER_GLAD")
|
||||
message(STATUS "Compiling 'Dear ImGui' from https://github.com/ocornut/imgui.git -- ${IMGUI_INCLUDE_DIR}.")
|
||||
|
||||
#
|
||||
# ImGui Color Text Editor
|
||||
#
|
||||
set(IMGUITEXTEDIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit)
|
||||
set(IMGUITEXTEDIT_SRC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit/TextEditor.cpp
|
||||
)
|
||||
message(STATUS "Including 'ImGuiColorTextEdit' from https://github.com/BalazsJako/ImGuiColorTextEdit -- ${IMGUITEXTEDIT_INCLUDE_DIR}.")
|
||||
message(STATUS "Compiling 'Dear ImGui' from https://github.com/ocornut/imgui.git -- ${IMGUI_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# TINY XML 2
|
||||
#
|
||||
set(TINYXML2_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/tinyxml2)
|
||||
add_library(TINYXML2 "${CMAKE_CURRENT_SOURCE_DIR}/ext/tinyxml2/tinyxml2.cpp")
|
||||
message(STATUS "Compiling 'TinyXML2' from https://github.com/leethomason/tinyxml2.git -- ${TINYXML2_INCLUDE_DIR}.")
|
||||
message(STATUS "Compiling 'TinyXML2' from https://github.com/leethomason/tinyxml2.git -- ${TINYXML2_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# OSCPack
|
||||
@@ -195,22 +193,7 @@ set(OSCPACK_SRCS
|
||||
)
|
||||
set(OSCPACK_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/OSCPack)
|
||||
add_library(OSCPACK "${OSCPACK_SRCS}")
|
||||
message(STATUS "Compiling 'OSCPack' from http://www.rossbencina.com/code/oscpack -- ${OSCPACK_INCLUDE_DIR}.")
|
||||
|
||||
#
|
||||
# STB
|
||||
#
|
||||
set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/stb)
|
||||
add_definitions(-DIMGUI_USE_STB_SPRINTF)
|
||||
message(STATUS "Including 'STB Nothings' from https://github.com/nothings/stb -- ${STB_INCLUDE_DIR}.")
|
||||
|
||||
#
|
||||
# DIRENT
|
||||
#
|
||||
if(WIN32)
|
||||
set(DIRENT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/Dirent/include)
|
||||
message(STATUS "Including 'Dirent' from https://github.com/tronkko/dirent -- ${DIRENT_INCLUDE_DIR}.")
|
||||
endif( WIN32 )
|
||||
message(STATUS "Compiling 'OSCPack' from http://www.rossbencina.com/code/oscpack -- ${OSCPACK_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# FILE DIALOG: use tinyfiledialog for all except Linux
|
||||
@@ -230,11 +213,27 @@ else()
|
||||
endif()
|
||||
|
||||
#
|
||||
# OBJ LOADER - not used
|
||||
# ImGui Color Text Editor
|
||||
#
|
||||
#set(OBJLOADER_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/obj)
|
||||
#add_library(OBJLOADER "${CMAKE_CURRENT_SOURCE_DIR}/ext/obj/ObjLoader.cpp")
|
||||
#message(STATUS "Compiling 'ObjLoader' from https://github.com/mortennobel/OpenGL_3_2_Utils -- ${OBJLOADER_INCLUDE_DIR}.")
|
||||
set(IMGUITEXTEDIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit)
|
||||
set(IMGUITEXTEDIT_SRC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ext/ImGuiColorTextEdit/TextEditor.cpp
|
||||
)
|
||||
message(STATUS "Including 'ImGuiColorTextEdit' from https://github.com/BalazsJako/ImGuiColorTextEdit -- ${IMGUITEXTEDIT_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# STB
|
||||
#
|
||||
set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/stb)
|
||||
add_definitions(-DIMGUI_USE_STB_SPRINTF)
|
||||
message(STATUS "Including 'STB Nothings' from https://github.com/nothings/stb -- ${STB_INCLUDE_DIR}")
|
||||
|
||||
#
|
||||
# DIRENT (windows only)
|
||||
if(WIN32)
|
||||
set(DIRENT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext/Dirent/include)
|
||||
message(STATUS "Including 'Dirent' from https://github.com/tronkko/dirent -- ${DIRENT_INCLUDE_DIR}.")
|
||||
endif()
|
||||
|
||||
#
|
||||
# Application
|
||||
@@ -249,9 +248,11 @@ include_directories(
|
||||
${GSTREAMER_APP_INCLUDE_DIR}
|
||||
${GSTREAMER_PBUTILS_INCLUDE_DIR}
|
||||
${GSTREAMER_GL_INCLUDE_DIR}
|
||||
${GLFW3_INCLUDE_DIRS}
|
||||
${ICU_INCLUDE_DIRS}
|
||||
${GLM_INCLUDE_DIRS}
|
||||
${GLIB2_INCLUDE_DIR}
|
||||
${GLAD_INCLUDE_DIR}
|
||||
${GLM_INCLUDE_DIRS}
|
||||
${IMGUI_INCLUDE_DIR}
|
||||
${IMGUI_INCLUDE_DIR}/examples
|
||||
${IMGUITEXTEDIT_INCLUDE_DIR}
|
||||
@@ -260,7 +261,12 @@ include_directories(
|
||||
${STB_INCLUDE_DIR}
|
||||
${DIRENT_INCLUDE_DIR}
|
||||
${OSCPACK_INCLUDE_DIR}
|
||||
${ICU_INCLUDE_DIRS}
|
||||
${link_HEADERS}
|
||||
)
|
||||
|
||||
link_directories(
|
||||
${GLFW3_LIBRARY_DIRS}
|
||||
${ICU_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
|
||||
@@ -286,6 +292,7 @@ set(VMIX_SRCS
|
||||
TextureView.cpp
|
||||
TransitionView.cpp
|
||||
Source.cpp
|
||||
SourceCallback.cpp
|
||||
SourceList.cpp
|
||||
Session.cpp
|
||||
Selection.cpp
|
||||
@@ -293,6 +300,7 @@ set(VMIX_SRCS
|
||||
SessionVisitor.cpp
|
||||
Interpolator.cpp
|
||||
SessionCreator.cpp
|
||||
SessionParser.cpp
|
||||
Mixer.cpp
|
||||
FrameGrabber.cpp
|
||||
Recorder.cpp
|
||||
@@ -319,6 +327,7 @@ set(VMIX_SRCS
|
||||
SearchVisitor.cpp
|
||||
ImGuiToolkit.cpp
|
||||
ImGuiVisitor.cpp
|
||||
InfoVisitor.cpp
|
||||
GstToolkit.cpp
|
||||
GlmToolkit.cpp
|
||||
SystemToolkit.cpp
|
||||
@@ -328,6 +337,8 @@ set(VMIX_SRCS
|
||||
Connection.cpp
|
||||
ActionManager.cpp
|
||||
Overlay.cpp
|
||||
Metronome.cpp
|
||||
ControlManager.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -404,6 +415,7 @@ set(VMIX_RSC_FILES
|
||||
./rsc/mesh/shadow_perspective.ply
|
||||
./rsc/mesh/point.ply
|
||||
./rsc/mesh/square_point.ply
|
||||
./rsc/mesh/triangle_point.ply
|
||||
./rsc/mesh/icon_video.ply
|
||||
./rsc/mesh/icon_image.ply
|
||||
./rsc/mesh/icon_render.ply
|
||||
@@ -453,7 +465,6 @@ IF(APPLE)
|
||||
# create the application
|
||||
add_executable(${VMIX_BINARY} MACOSX_BUNDLE
|
||||
${VMIX_SRCS}
|
||||
./osx/CustomDelegate.m
|
||||
${IMGUITEXTEDIT_SRC}
|
||||
${MACOSX_BUNDLE_ICON_FILE}
|
||||
)
|
||||
@@ -469,7 +480,7 @@ IF(APPLE)
|
||||
|
||||
ELSE(APPLE)
|
||||
|
||||
link_directories (${GTK3_LIBRARY_DIRS})
|
||||
link_directories (${GTK3_LIBRARY_DIRS})
|
||||
|
||||
add_executable(${VMIX_BINARY}
|
||||
${VMIX_SRCS}
|
||||
@@ -490,11 +501,15 @@ set_property(TARGET ${VMIX_BINARY} PROPERTY C_STANDARD 11)
|
||||
target_compile_definitions(${VMIX_BINARY} PUBLIC "IMGUI_IMPL_OPENGL_LOADER_GLAD")
|
||||
|
||||
target_link_libraries(${VMIX_BINARY} LINK_PRIVATE
|
||||
vmix::rc
|
||||
glm::glm
|
||||
GLAD
|
||||
TINYXML2
|
||||
IMGUI
|
||||
OSCPACK
|
||||
${GLFW_LIBRARY}
|
||||
${TINYFD_LIBRARY}
|
||||
${GLFW3_LIBRARIES}
|
||||
${ICU_LIBRARIES}
|
||||
${CMAKE_DL_LIBS}
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GSTREAMER_LIBRARY}
|
||||
@@ -504,27 +519,21 @@ target_link_libraries(${VMIX_BINARY} LINK_PRIVATE
|
||||
${GSTREAMER_VIDEO_LIBRARY}
|
||||
${GSTREAMER_PBUTILS_LIBRARY}
|
||||
${GSTREAMER_GL_LIBRARY}
|
||||
${GSTREAMER_PLAYER_LIBRARY}
|
||||
${TINYFD_LIBRARY}
|
||||
Threads::Threads
|
||||
PNG::PNG
|
||||
glm::glm
|
||||
ICU::i18n
|
||||
ICU::io
|
||||
ICU::uc
|
||||
vmix::rc
|
||||
Ableton::Link
|
||||
${PLATFORM_LIBS}
|
||||
)
|
||||
|
||||
|
||||
|
||||
### DEFINE THE PACKAGING (all OS)
|
||||
|
||||
SET(CPACK_PACKAGE_NAME "vimix")
|
||||
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "vimix\nReal-time video mixing for live performance.")
|
||||
SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md")
|
||||
SET(CPACK_PACKAGE_CONTACT "bruno.herbelin@gmail.com")
|
||||
SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING.txt")
|
||||
SET(CPACK_PACKAGE_CONTACT "bruno.herbelin@gmail.com")
|
||||
SET(CPACK_PACKAGE_HOMEPAGE_URL "https://brunoherbelin.github.io/vimix")
|
||||
SET(CPACK_PACKAGE_VERSION_MAJOR "${VIMIX_VERSION_MAJOR}")
|
||||
SET(CPACK_PACKAGE_VERSION_MINOR "${VIMIX_VERSION_MINOR}")
|
||||
SET(CPACK_PACKAGE_VERSION_PATCH "${VIMIX_VERSION_PATCH}")
|
||||
@@ -536,23 +545,19 @@ SET(CPACK_SOURCE_IGNORE_FILES
|
||||
)
|
||||
|
||||
|
||||
# optimize size ?
|
||||
SET(CPACK_STRIP_FILES TRUE)
|
||||
|
||||
### DEFINE THE PACKAGING (OS specific)
|
||||
|
||||
IF(APPLE)
|
||||
|
||||
# include( InstallRequiredSystemLibraries )
|
||||
|
||||
# Bundle target
|
||||
set(CPACK_GENERATOR "DragNDrop")
|
||||
set(CPACK_GENERATOR DragNDrop)
|
||||
set(CPACK_BINARY_DRAGNDROP ON)
|
||||
|
||||
# OSX cpack info
|
||||
set(CPACK_SYSTEM_NAME "OSX_${CMAKE_OSX_DEPLOYMENT_TARGET}_${CMAKE_OSX_ARCHITECTURES}")
|
||||
set(CPACK_BUNDLE_NAME ${CPACK_PACKAGE_NAME})
|
||||
set(CPACK_BUNDLE_ICON ${MACOSX_BUNDLE_ICON_FILE})
|
||||
set(CPACK_BUNDLE_PLIST ${MACOSX_BUNDLE_PLIST_FILE})
|
||||
|
||||
set(APPS "\${CMAKE_INSTALL_PREFIX}/vimix.app")
|
||||
# set( APPS "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/vimix${CMAKE_EXECUTABLE_SUFFIX}")
|
||||
|
||||
install(TARGETS ${VMIX_BINARY}
|
||||
CONFIGURATIONS Release RelWithDebInfo
|
||||
@@ -565,34 +570,56 @@ IF(APPLE)
|
||||
|
||||
### TODO configure auto to find installation dir of gst
|
||||
|
||||
message(STATUS "install gst-plugins ${PKG_GSTREAMER_PLUGIN_DIR}")
|
||||
message(STATUS "install gst-plugins-base ${PKG_GSTREAMER_BASE_PLUGIN_DIR}")
|
||||
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PKG_GSTREAMER_PLUGINS_BAD gstreamer-plugins-bad-${GSTREAMER_ABI_VERSION})
|
||||
set(PKG_GSTREAMER_BAD_PLUGIN_DIR ${PKG_GSTREAMER_PLUGINS_BAD_LIBDIR}/gstreamer-${GSTREAMER_ABI_VERSION})
|
||||
message(STATUS "install gst-plugins-bad ${PKG_GSTREAMER_BAD_PLUGIN_DIR}")
|
||||
endif()
|
||||
|
||||
|
||||
# intall the gst-plugin-scanner program (used by plugins at load time)
|
||||
install(FILES "/usr/local/Cellar/gstreamer/1.18.1/libexec/gstreamer-1.0/gst-plugin-scanner"
|
||||
set(PKG_GSTREAMER_SCANNER "${PKG_GSTREAMER_PREFIX}/libexec/gstreamer-1.0/gst-plugin-scanner")
|
||||
message(STATUS "install gst-plugin-scanner ${PKG_GSTREAMER_SCANNER}")
|
||||
install(FILES "${PKG_GSTREAMER_SCANNER}"
|
||||
DESTINATION "${plugin_dest_dir}"
|
||||
PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
|
||||
COMPONENT Runtime
|
||||
)
|
||||
|
||||
# ICU DATA LIB GST dependency : undocumented and hacked here : seems to work
|
||||
install(FILES "/usr/local/Cellar/icu4c/67.1/lib/libicudata.67.1.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" RENAME "libicudata.67.dylib" COMPONENT Runtime)
|
||||
|
||||
# Install the gst-plugins (all those installed with brew )
|
||||
install(DIRECTORY "${PKG_GSTREAMER_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-base/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-good/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-bad/1.18.1_1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-ugly/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-libav/1.18.1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PKG_GSTREAMER_BASE_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PKG_GSTREAMER_BAD_PLUGIN_DIR}" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
|
||||
# install locally recompiled gst-plugins (because not included in brew package)
|
||||
set(LOCAL_BUILD_BAD "/Users/herbelin/Development/gst/gst-plugins-bad-1.18.0/build")
|
||||
install(FILES "${LOCAL_BUILD_BAD}/sys/applemedia/libgstapplemedia.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
|
||||
install(FILES "${LOCAL_BUILD_BAD}/ext/libde265/libgstde265.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
|
||||
install(FILES "${LOCAL_BUILD_BAD}/ext/x265/libgstx265.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-good/1.18.4/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-plugins-ugly/1.18.4_1/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
install(DIRECTORY "/usr/local/Cellar/gst-libav/1.18.4/lib/gstreamer-1.0" DESTINATION "${plugin_dest_dir}" COMPONENT Runtime)
|
||||
|
||||
# install locally recompiled & installed gst-plugins (because not included in brew package)
|
||||
install(FILES "/usr/local/lib/gstreamer-1.0/libgstapplemedia.dylib"
|
||||
"/usr/local/lib/gstreamer-1.0/libgstde265.dylib"
|
||||
"/usr/local/lib/gstreamer-1.0/libgstx265.dylib"
|
||||
DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
|
||||
|
||||
# install frei0r plugins (dependencies of gstreamer-1.0/libgstfrei0r.dylib plugin)
|
||||
install(FILES "/usr/local/Cellar/frei0r/1.7.0/lib/frei0r-1/lissajous0r.so"
|
||||
"/usr/local/Cellar/frei0r/1.7.0/lib/frei0r-1/rgbnoise.so"
|
||||
DESTINATION "${plugin_dest_dir}/frei0r-1" COMPONENT Runtime)
|
||||
|
||||
|
||||
# ICU DATA LIB GST dependency : undocumented and hacked here : seems to work
|
||||
# install(FILES "${ICU_LINK_LIBRARIES}" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" COMPONENT Runtime)
|
||||
install(FILES "/usr/local/Cellar/icu4c/69.1/lib/libicudata.69.1.dylib" DESTINATION "${plugin_dest_dir}/gstreamer-1.0" RENAME "libicudata.69.dylib" COMPONENT Runtime)
|
||||
message(STATUS "install ${ICU_LINK_LIBRARIES} from ${ICU_LIBRARY_DIRS}")
|
||||
|
||||
# package runtime fixup bundle
|
||||
set(APPS "\${CMAKE_INSTALL_PREFIX}/vimix.app")
|
||||
install(CODE "
|
||||
file(GLOB_RECURSE GSTPLUGINS \"\${CMAKE_INSTALL_PREFIX}/${plugin_dest_dir}/gstreamer-1.0/*.dylib\")
|
||||
list(APPEND LIBS_PATH \"/usr/local/Cellar/icu4c/67.1/lib\")
|
||||
list(APPEND LIBS_PATH \"\${ICU_LIBRARY_DIRS}\")
|
||||
include(BundleUtilities)
|
||||
set(BU_CHMOD_BUNDLE_ITEMS TRUE)
|
||||
fixup_bundle(\"${APPS}\" \"\${GSTPLUGINS}\" \"${LIBS_PATH}\")
|
||||
@@ -600,18 +627,39 @@ IF(APPLE)
|
||||
COMPONENT Runtime
|
||||
)
|
||||
|
||||
set(CPACK_BINARY_DRAGNDROP ON)
|
||||
set(APPLE_CODESIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/osx/entitlements.plist")
|
||||
set(APPLE_CODESIGN_IDENTITY "" CACHE STRING "")
|
||||
string(LENGTH "${APPLE_CODESIGN_IDENTITY}" APPLE_CODESIGN_IDENTITY_LENGHT)
|
||||
if( ${APPLE_CODESIGN_IDENTITY_LENGHT} LESS 40 )
|
||||
message(STATUS "Not signing bundle. Specify APPLE_CODESIGN_IDENTITY to cmake before running cpack to sign")
|
||||
else()
|
||||
install(CODE "
|
||||
execute_process(COMMAND
|
||||
codesign --verbose=4 --deep --force
|
||||
--entitlements \"${APPLE_CODESIGN_ENTITLEMENTS}\"
|
||||
--sign \"${APPLE_CODESIGN_IDENTITY}\"
|
||||
\"${APPS}\" )
|
||||
"
|
||||
COMPONENT Runtime
|
||||
)
|
||||
endif()
|
||||
|
||||
# # package runtime fixup bundle and codesign
|
||||
# set(BUNDLE_NAME "vimix.app")
|
||||
# set(BUNDLE_LIBS_DIR "${plugin_dest_dir}/gstreamer-1.0")
|
||||
# set(BUNDLE_DIRS "${ICU_LIBRARY_DIRS}")
|
||||
# set(APPLE_CODESIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/osx/entitlements.plist")
|
||||
|
||||
# configure_file(cmake/modules/BundleInstall.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/BundleInstall.cmake" @ONLY)
|
||||
# install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/BundleInstall.cmake" COMPONENT Runtime)
|
||||
|
||||
ELSE(APPLE)
|
||||
|
||||
set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/vimix")
|
||||
|
||||
install(TARGETS ${VMIX_BINARY}
|
||||
CONFIGURATIONS Release RelWithDebInfo
|
||||
RUNTIME DESTINATION bin COMPONENT Runtime
|
||||
)
|
||||
|
||||
|
||||
ENDIF(APPLE)
|
||||
|
||||
# Package full name
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -1,28 +1,44 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
#include "osc/OscOutboundPacketStream.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "Connection.h"
|
||||
#include "Settings.h"
|
||||
#include "Streamer.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "Connection.h"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define CONNECTION_DEBUG
|
||||
#endif
|
||||
|
||||
|
||||
Connection::Connection()
|
||||
Connection::Connection() : receiver_(nullptr)
|
||||
{
|
||||
receiver_ = nullptr;
|
||||
}
|
||||
|
||||
|
||||
Connection::~Connection()
|
||||
{
|
||||
if (receiver_!=nullptr) {
|
||||
@@ -147,7 +163,7 @@ int Connection::index(ConnectionInfo i) const
|
||||
|
||||
void Connection::print()
|
||||
{
|
||||
for(int i = 0; i<connections_.size(); i++) {
|
||||
for(size_t i = 0; i<connections_.size(); i++) {
|
||||
Log::Info(" - %s %s:%d", connections_[i].name.c_str(), connections_[i].address.c_str(), connections_[i].port_handshake);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +173,8 @@ void Connection::listen()
|
||||
#ifdef CONNECTION_DEBUG
|
||||
Log::Info("Accepting handshake on port %d", Connection::manager().connections_[0].port_handshake);
|
||||
#endif
|
||||
Connection::manager().receiver_->Run();
|
||||
if (Connection::manager().receiver_)
|
||||
Connection::manager().receiver_->Run();
|
||||
}
|
||||
|
||||
void Connection::ask()
|
||||
@@ -207,7 +224,7 @@ void Connection::ask()
|
||||
|
||||
}
|
||||
|
||||
void ConnectionRequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
void Connection::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint )
|
||||
{
|
||||
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
|
||||
|
||||
27
Connection.h
27
Connection.h
@@ -1,22 +1,17 @@
|
||||
#ifndef CONNECTION_H
|
||||
#define CONNECTION_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "osc/OscReceivedElements.h"
|
||||
#include "osc/OscPacketListener.h"
|
||||
#include "ip/UdpSocket.h"
|
||||
|
||||
#include "NetworkToolkit.h"
|
||||
|
||||
#define MAX_HANDSHAKE 20
|
||||
#define HANDSHAKE_PORT 71310
|
||||
#define STREAM_REQUEST_PORT 71510
|
||||
#define OSC_DIALOG_PORT 71010
|
||||
#define ALIVE 3
|
||||
|
||||
class ConnectionRequestListener : public osc::OscPacketListener {
|
||||
|
||||
protected:
|
||||
virtual void ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint );
|
||||
};
|
||||
|
||||
struct ConnectionInfo {
|
||||
|
||||
@@ -58,8 +53,6 @@ struct ConnectionInfo {
|
||||
|
||||
class Connection
|
||||
{
|
||||
friend class ConnectionRequestListener;
|
||||
|
||||
// Private Constructor
|
||||
Connection();
|
||||
Connection(Connection const& copy) = delete;
|
||||
@@ -82,11 +75,19 @@ public:
|
||||
int index(const std::string &name) const;
|
||||
ConnectionInfo info(int index = 0); // index 0 for self
|
||||
|
||||
protected:
|
||||
class RequestListener : public osc::OscPacketListener {
|
||||
|
||||
protected:
|
||||
virtual void ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint );
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
static void ask();
|
||||
static void listen();
|
||||
ConnectionRequestListener listener_;
|
||||
RequestListener listener_;
|
||||
UdpListeningReceiveSocket *receiver_;
|
||||
|
||||
std::vector< ConnectionInfo > connections_;
|
||||
|
||||
756
ControlManager.cpp
Normal file
756
ControlManager.cpp
Normal file
@@ -0,0 +1,756 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "osc/OscOutboundPacketStream.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
#include "Settings.h"
|
||||
#include "BaseToolkit.h"
|
||||
#include "Mixer.h"
|
||||
#include "Source.h"
|
||||
#include "ActionManager.h"
|
||||
#include "SystemToolkit.h"
|
||||
#include "tinyxml2Toolkit.h"
|
||||
|
||||
#include "ControlManager.h"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define CONTROL_DEBUG
|
||||
#endif
|
||||
|
||||
#define CONTROL_OSC_MSG "OSC: "
|
||||
|
||||
|
||||
void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint )
|
||||
{
|
||||
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
|
||||
remoteEndpoint.AddressAndPortAsString(sender);
|
||||
|
||||
try{
|
||||
#ifdef CONTROL_DEBUG
|
||||
Log::Info(CONTROL_OSC_MSG "received '%s' from %s", FullMessage(m).c_str(), sender);
|
||||
#endif
|
||||
// Preprocessing with Translator
|
||||
std::string address_pattern = Control::manager().translate(m.AddressPattern());
|
||||
|
||||
// structured OSC address
|
||||
std::list<std::string> address = BaseToolkit::splitted(address_pattern, OSC_SEPARATOR);
|
||||
//
|
||||
// A wellformed OSC address is in the form '/vimix/target/attribute {arguments}'
|
||||
// First test: should have 3 elements and start with APP_NAME ('vimix')
|
||||
//
|
||||
if (address.size() > 2 && address.front().compare(OSC_PREFIX) == 0 ){
|
||||
// done with the first part of the OSC address
|
||||
address.pop_front();
|
||||
// next part of the OSC message is the target
|
||||
std::string target = address.front();
|
||||
// next part of the OSC message is the attribute
|
||||
address.pop_front();
|
||||
std::string attribute = address.front();
|
||||
// Log target: just print text in log window
|
||||
if ( target.compare(OSC_INFO) == 0 )
|
||||
{
|
||||
if ( attribute.compare(OSC_INFO_NOTIFY) == 0) {
|
||||
Log::Notify(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
|
||||
}
|
||||
else if ( attribute.compare(OSC_INFO_LOG) == 0) {
|
||||
Log::Info(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
|
||||
}
|
||||
}
|
||||
// Output target: concerns attributes of the rendering output
|
||||
else if ( target.compare(OSC_OUTPUT) == 0 )
|
||||
{
|
||||
if ( Control::manager().receiveOutputAttribute(attribute, m.ArgumentStream())) {
|
||||
// send the global status
|
||||
Control::manager().sendOutputStatus(remoteEndpoint);
|
||||
}
|
||||
}
|
||||
// Session target: concerns attributes of the session
|
||||
else if ( target.compare(OSC_SESSION) == 0 )
|
||||
{
|
||||
if ( Control::manager().receiveSessionAttribute(attribute, m.ArgumentStream()) ) {
|
||||
// send the global status
|
||||
Control::manager().sendOutputStatus(remoteEndpoint);
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
||||
}
|
||||
}
|
||||
// ALL sources target: apply attribute to all sources of the session
|
||||
else if ( target.compare(OSC_ALL) == 0 )
|
||||
{
|
||||
// Loop over selected sources
|
||||
for (SourceList::iterator it = Mixer::manager().session()->begin(); it != Mixer::manager().session()->end(); ++it) {
|
||||
// apply attributes
|
||||
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
|
||||
// and send back feedback if needed
|
||||
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
||||
}
|
||||
}
|
||||
// Selected sources target: apply attribute to all sources of the selection
|
||||
else if ( target.compare(OSC_SELECTED) == 0 )
|
||||
{
|
||||
// Loop over selected sources
|
||||
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
|
||||
// apply attributes
|
||||
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
|
||||
// and send back feedback if needed
|
||||
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
||||
}
|
||||
}
|
||||
// Current source target: apply attribute to the current sources
|
||||
else if ( target.compare(OSC_CURRENT) == 0 )
|
||||
{
|
||||
int sourceid = -1;
|
||||
if ( attribute.compare(OSC_SYNC) == 0) {
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
||||
}
|
||||
else if ( attribute.compare(OSC_NEXT) == 0) {
|
||||
// set current to NEXT
|
||||
Mixer::manager().setCurrentNext();
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
||||
}
|
||||
else if ( attribute.compare(OSC_PREVIOUS) == 0) {
|
||||
// set current to PREVIOUS
|
||||
Mixer::manager().setCurrentPrevious();
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
||||
}
|
||||
else if ( BaseToolkit::is_a_number( attribute.substr(1), &sourceid) ){
|
||||
// set current to given INDEX
|
||||
Mixer::manager().setCurrentIndex(sourceid);
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
||||
}
|
||||
// all other attributes operate on current source
|
||||
else {
|
||||
// apply attributes to current source
|
||||
if ( Control::manager().receiveSourceAttribute( Mixer::manager().currentSource(), attribute, m.ArgumentStream()) )
|
||||
// and send back feedback if needed
|
||||
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
||||
}
|
||||
}
|
||||
// General case: try to identify the target
|
||||
else {
|
||||
// try to find source by index
|
||||
Source *s = nullptr;
|
||||
int sourceid = -1;
|
||||
if ( BaseToolkit::is_a_number(target.substr(1), &sourceid) )
|
||||
s = Mixer::manager().sourceAtIndex(sourceid);
|
||||
|
||||
// if failed, try to find source by name
|
||||
if (s == nullptr)
|
||||
s = Mixer::manager().findSource(target.substr(1));
|
||||
|
||||
// if a source with the given target name or index was found
|
||||
if (s) {
|
||||
// apply attributes to source
|
||||
if ( Control::manager().receiveSourceAttribute(s, attribute, m.ArgumentStream()) )
|
||||
// and send back feedback if needed
|
||||
Control::manager().sendSourceAttibutes(remoteEndpoint, target, s);
|
||||
}
|
||||
else
|
||||
Log::Info(CONTROL_OSC_MSG "Unknown target '%s' requested by %s.", target.c_str(), sender);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log::Info(CONTROL_OSC_MSG "Unknown osc message '%s' sent by %s.", m.AddressPattern(), sender);
|
||||
}
|
||||
}
|
||||
catch( osc::Exception& e ){
|
||||
// any parsing errors such as unexpected argument types, or
|
||||
// missing arguments get thrown as exceptions.
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s' from %s : %s", m.AddressPattern(), sender, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::string Control::RequestListener::FullMessage( const osc::ReceivedMessage& m )
|
||||
{
|
||||
// build a string with the address pattern of the message
|
||||
std::ostringstream message;
|
||||
message << m.AddressPattern() << " ";
|
||||
|
||||
// try to fill the string with the arguments
|
||||
std::ostringstream arguments;
|
||||
try{
|
||||
// loop over all arguments
|
||||
osc::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();
|
||||
while (arg != m.ArgumentsEnd()) {
|
||||
if( arg->IsBool() ){
|
||||
bool a = (arg++)->AsBoolUnchecked();
|
||||
message << (a ? "T" : "F");
|
||||
}
|
||||
else if( arg->IsInt32() ){
|
||||
int a = (arg++)->AsInt32Unchecked();
|
||||
message << "i";
|
||||
arguments << " " << a;
|
||||
}
|
||||
else if( arg->IsFloat() ){
|
||||
float a = (arg++)->AsFloatUnchecked();
|
||||
message << "f";
|
||||
arguments << " " << std::fixed << std::setprecision(2) << a;
|
||||
}
|
||||
else if( arg->IsString() ){
|
||||
const char *a = (arg++)->AsStringUnchecked();
|
||||
message << "s";
|
||||
arguments << " " << a;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( osc::Exception& e ){
|
||||
// any parsing errors such as unexpected argument types, or
|
||||
// missing arguments get thrown as exceptions.
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s': %s", m.AddressPattern(), e.what());
|
||||
}
|
||||
|
||||
// append list of arguments to the message string
|
||||
message << arguments.str();
|
||||
|
||||
// returns the full message
|
||||
return message.str();
|
||||
}
|
||||
|
||||
|
||||
Control::Control() : receiver_(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
Control::~Control()
|
||||
{
|
||||
terminate();
|
||||
}
|
||||
|
||||
|
||||
std::string Control::translate (std::string addresspattern)
|
||||
{
|
||||
auto it_translation = translation_.find(addresspattern);
|
||||
if ( it_translation != translation_.end() )
|
||||
return it_translation->second;
|
||||
else
|
||||
return addresspattern;
|
||||
}
|
||||
|
||||
void Control::loadOscConfig()
|
||||
{
|
||||
// reset translations
|
||||
translation_.clear();
|
||||
|
||||
// load osc config file
|
||||
tinyxml2::XMLDocument xmlDoc;
|
||||
tinyxml2::XMLError eResult = xmlDoc.LoadFile(Settings::application.control.osc_filename.c_str());
|
||||
|
||||
// the only reason to return false is if the file does not exist or is empty
|
||||
if (eResult == tinyxml2::XML_ERROR_FILE_NOT_FOUND
|
||||
| eResult == tinyxml2::XML_ERROR_FILE_COULD_NOT_BE_OPENED
|
||||
| eResult == tinyxml2::XML_ERROR_FILE_READ_ERROR
|
||||
| eResult == tinyxml2::XML_ERROR_EMPTY_DOCUMENT )
|
||||
resetOscConfig();
|
||||
|
||||
// found the file, could open and read it
|
||||
else if (eResult != tinyxml2::XML_SUCCESS)
|
||||
Log::Warning(CONTROL_OSC_MSG "Error while parsing Translator: %s", xmlDoc.ErrorIDToName(eResult));
|
||||
|
||||
// no XML parsing error
|
||||
else {
|
||||
// parse all entries 'osc'
|
||||
tinyxml2::XMLElement* osc = xmlDoc.FirstChildElement("osc");
|
||||
for( ; osc ; osc=osc->NextSiblingElement()) {
|
||||
// get the 'from' entry
|
||||
tinyxml2::XMLElement* from = osc->FirstChildElement("from");
|
||||
if (from) {
|
||||
const char *str_from = from->GetText();
|
||||
if (str_from) {
|
||||
// get the 'to' entry
|
||||
tinyxml2::XMLElement* to = osc->FirstChildElement("to");
|
||||
if (to) {
|
||||
const char *str_to = to->GetText();
|
||||
// if could get both; add to translator
|
||||
if (str_to)
|
||||
translation_[str_from] = str_to;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::Info(CONTROL_OSC_MSG "Loaded %d translation%s.", translation_.size(), translation_.size()>1?"s":"");
|
||||
}
|
||||
|
||||
void Control::resetOscConfig()
|
||||
{
|
||||
// generate a template xml translation dictionnary
|
||||
tinyxml2::XMLDocument xmlDoc;
|
||||
tinyxml2::XMLDeclaration *pDec = xmlDoc.NewDeclaration();
|
||||
xmlDoc.InsertFirstChild(pDec);
|
||||
tinyxml2::XMLComment *pComment = xmlDoc.NewComment("The OSC translator converts OSC address patterns into other ones.\n"
|
||||
"Complete the dictionnary by adding as many <osc> translations as you want.\n"
|
||||
"Each <osc> should contain a <from> pattern to translate into a <to> pattern.\n"
|
||||
"More at https://github.com/brunoherbelin/vimix/wiki/Open-Sound-Control-API.");
|
||||
xmlDoc.InsertEndChild(pComment);
|
||||
tinyxml2::XMLElement *from = xmlDoc.NewElement( "from" );
|
||||
from->InsertFirstChild( xmlDoc.NewText("/example/osc/message") );
|
||||
tinyxml2::XMLElement *to = xmlDoc.NewElement( "to" );
|
||||
to->InsertFirstChild( xmlDoc.NewText("/vimix/info/log") );
|
||||
tinyxml2::XMLElement *osc = xmlDoc.NewElement("osc");
|
||||
osc->InsertEndChild(from);
|
||||
osc->InsertEndChild(to);
|
||||
xmlDoc.InsertEndChild(osc);
|
||||
|
||||
// save xml in osc config file
|
||||
xmlDoc.SaveFile(Settings::application.control.osc_filename.c_str());
|
||||
|
||||
// reset and fill translation with default example
|
||||
translation_.clear();
|
||||
translation_["/example/osc/message"] = "/vimix/info/log";
|
||||
}
|
||||
|
||||
bool Control::init()
|
||||
{
|
||||
//
|
||||
// terminate before init (allows calling init() multiple times)
|
||||
//
|
||||
terminate();
|
||||
|
||||
//
|
||||
// load OSC Translator
|
||||
//
|
||||
loadOscConfig();
|
||||
|
||||
//
|
||||
// launch OSC listener
|
||||
//
|
||||
try {
|
||||
// try to create listenning socket
|
||||
// through exception runtime if fails
|
||||
receiver_ = new UdpListeningReceiveSocket( IpEndpointName( IpEndpointName::ANY_ADDRESS,
|
||||
Settings::application.control.osc_port_receive ), &listener_ );
|
||||
// listen for answers in a separate thread
|
||||
std::thread(listen).detach();
|
||||
|
||||
// inform user
|
||||
IpEndpointName ip = receiver_->LocalEndpointFor( IpEndpointName( NetworkToolkit::hostname().c_str(),
|
||||
Settings::application.control.osc_port_receive ));
|
||||
static char *addresseip = (char *)malloc(IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH);
|
||||
ip.AddressAndPortAsString(addresseip);
|
||||
Log::Info(CONTROL_OSC_MSG "Listening to UDP messages sent to %s", addresseip);
|
||||
}
|
||||
catch (const std::runtime_error &e) {
|
||||
// arg, the receiver could not be initialized
|
||||
// (often because the port was not available)
|
||||
receiver_ = nullptr;
|
||||
Log::Warning(CONTROL_OSC_MSG "Failed to init listener on port %d; %s", Settings::application.control.osc_port_receive, e.what());
|
||||
}
|
||||
|
||||
return receiver_ != nullptr;
|
||||
}
|
||||
|
||||
void Control::listen()
|
||||
{
|
||||
if (Control::manager().receiver_)
|
||||
Control::manager().receiver_->Run();
|
||||
|
||||
Control::manager().receiver_end_.notify_all();
|
||||
}
|
||||
|
||||
void Control::terminate()
|
||||
{
|
||||
if ( receiver_ != nullptr ) {
|
||||
|
||||
// request termination of receiver
|
||||
receiver_->AsynchronousBreak();
|
||||
|
||||
// wait for the receiver_end_ notification
|
||||
std::mutex mtx;
|
||||
std::unique_lock<std::mutex> lck(mtx);
|
||||
// if waited more than 2 seconds, its dead :(
|
||||
if ( receiver_end_.wait_for(lck,std::chrono::seconds(2)) == std::cv_status::timeout)
|
||||
Log::Warning(CONTROL_OSC_MSG "Failed to terminate; try again.");
|
||||
|
||||
// delete receiver and ready to initialize
|
||||
delete receiver_;
|
||||
receiver_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool Control::receiveOutputAttribute(const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments)
|
||||
{
|
||||
bool need_feedback = false;
|
||||
|
||||
try {
|
||||
if ( attribute.compare(OSC_SYNC) == 0) {
|
||||
need_feedback = true;
|
||||
}
|
||||
/// e.g. '/vimix/output/enable' or '/vimix/output/enable 1.0' or '/vimix/output/enable 0.0'
|
||||
else if ( attribute.compare(OSC_OUTPUT_ENABLE) == 0) {
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
Settings::application.render.disabled = on < 0.5f;
|
||||
}
|
||||
/// e.g. '/vimix/output/disable' or '/vimix/output/disable 1.0' or '/vimix/output/disable 0.0'
|
||||
else if ( attribute.compare(OSC_OUTPUT_DISABLE) == 0) {
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
Settings::application.render.disabled = on > 0.5f;
|
||||
}
|
||||
/// e.g. '/vimix/output/fading f 0.2' or '/vimix/output/fading ff 1.0 300.f'
|
||||
else if ( attribute.compare(OSC_OUTPUT_FADING) == 0) {
|
||||
float f = 0.f, d = 0.f;
|
||||
// first argument is fading value
|
||||
arguments >> f;
|
||||
if (arguments.Eos())
|
||||
arguments >> osc::EndMessage;
|
||||
// if a second argument is given, it is a duration
|
||||
else
|
||||
arguments >> d >> osc::EndMessage;
|
||||
Mixer::manager().session()->setFadingTarget(f, d);
|
||||
}
|
||||
/// e.g. '/vimix/output/fadein' or '/vimix/output/fadein f 300.f'
|
||||
else if ( attribute.compare(OSC_OUTPUT_FADE_IN) == 0) {
|
||||
float f = 0.f;
|
||||
// if argument is given, it is a duration
|
||||
if (!arguments.Eos())
|
||||
arguments >> f >> osc::EndMessage;
|
||||
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() - f * 0.01);
|
||||
need_feedback = true;
|
||||
}
|
||||
else if ( attribute.compare(OSC_OUTPUT_FADE_OUT) == 0) {
|
||||
float f = 0.f;
|
||||
// if argument is given, it is a duration
|
||||
if (!arguments.Eos())
|
||||
arguments >> f >> osc::EndMessage;
|
||||
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() + f * 0.01);
|
||||
need_feedback = true;
|
||||
}
|
||||
#ifdef CONTROL_DEBUG
|
||||
else {
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'output'", attribute.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
catch (osc::MissingArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'output'", attribute.c_str());
|
||||
}
|
||||
catch (osc::ExcessArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'output'", attribute.c_str());
|
||||
}
|
||||
catch (osc::WrongArgumentTypeException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'output'", attribute.c_str());
|
||||
}
|
||||
|
||||
return need_feedback;
|
||||
}
|
||||
|
||||
bool Control::receiveSourceAttribute(Source *target, const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments)
|
||||
{
|
||||
bool send_feedback = false;
|
||||
|
||||
if (target == nullptr)
|
||||
return send_feedback;
|
||||
|
||||
try {
|
||||
/// e.g. '/vimix/current/play' or '/vimix/current/play T' or '/vimix/current/play F'
|
||||
if ( attribute.compare(OSC_SOURCE_PLAY) == 0) {
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
target->call( new SetPlay(on > 0.5f) );
|
||||
}
|
||||
/// e.g. '/vimix/current/pause' or '/vimix/current/pause T' or '/vimix/current/pause F'
|
||||
else if ( attribute.compare(OSC_SOURCE_PAUSE) == 0) {
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
target->call( new SetPlay(on < 0.5f) );
|
||||
}
|
||||
/// e.g. '/vimix/current/replay'
|
||||
else if ( attribute.compare(OSC_SOURCE_REPLAY) == 0) {
|
||||
target->call( new RePlay() );
|
||||
}
|
||||
/// e.g. '/vimix/current/alpha f 0.3'
|
||||
else if ( attribute.compare(OSC_SOURCE_LOCK) == 0) {
|
||||
float x = 1.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new SetLock(x > 0.5f ? true : false) );
|
||||
}
|
||||
/// e.g. '/vimix/current/alpha f 0.3'
|
||||
else if ( attribute.compare(OSC_SOURCE_ALPHA) == 0) {
|
||||
float x = 1.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new SetAlpha(x), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/alpha f 0.3'
|
||||
else if ( attribute.compare(OSC_SOURCE_LOOM) == 0) {
|
||||
float x = 1.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new Loom(x), true );
|
||||
// this will require to send feedback status about source
|
||||
send_feedback = true;
|
||||
}
|
||||
/// e.g. '/vimix/current/transparency f 0.7'
|
||||
else if ( attribute.compare(OSC_SOURCE_TRANSPARENCY) == 0) {
|
||||
float x = 0.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new SetAlpha(1.f - x), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/depth f 5.0'
|
||||
else if ( attribute.compare(OSC_SOURCE_DEPTH) == 0) {
|
||||
float x = 0.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new SetDepth(x), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/translation ff 10.0 2.2'
|
||||
else if ( attribute.compare(OSC_SOURCE_GRAB) == 0) {
|
||||
float x = 0.f, y = 0.f;
|
||||
arguments >> x >> y >> osc::EndMessage;
|
||||
target->call( new Grab( x, y), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/scale ff 10.0 2.2'
|
||||
else if ( attribute.compare(OSC_SOURCE_RESIZE) == 0) {
|
||||
float x = 0.f, y = 0.f;
|
||||
arguments >> x >> y >> osc::EndMessage;
|
||||
target->call( new Resize( x, y), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/turn f 1.0'
|
||||
else if ( attribute.compare(OSC_SOURCE_TURN) == 0) {
|
||||
float x = 0.f, y = 0.f;
|
||||
arguments >> x;
|
||||
if (arguments.Eos())
|
||||
arguments >> osc::EndMessage;
|
||||
else // ignore second argument
|
||||
arguments >> y >> osc::EndMessage;
|
||||
target->call( new Turn( x ), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/reset'
|
||||
else if ( attribute.compare(OSC_SOURCE_RESET) == 0) {
|
||||
target->call( new ResetGeometry(), true );
|
||||
}
|
||||
#ifdef CONTROL_DEBUG
|
||||
else {
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
// overwrite value if source locked
|
||||
if (target->locked())
|
||||
send_feedback = true;
|
||||
|
||||
}
|
||||
catch (osc::MissingArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
||||
}
|
||||
catch (osc::ExcessArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
||||
}
|
||||
catch (osc::WrongArgumentTypeException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
||||
}
|
||||
|
||||
return send_feedback;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Control::receiveSessionAttribute(const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments)
|
||||
{
|
||||
bool send_feedback = false;
|
||||
|
||||
try {
|
||||
if ( attribute.compare(OSC_SYNC) == 0) {
|
||||
send_feedback = true;
|
||||
}
|
||||
else if ( attribute.compare(OSC_SESSION_VERSION) == 0) {
|
||||
float v = 0.f;
|
||||
arguments >> v >> osc::EndMessage;
|
||||
size_t id = (int) ceil(v);
|
||||
std::list<uint64_t> snapshots = Action::manager().snapshots();
|
||||
if ( id < snapshots.size() ) {
|
||||
for (size_t i = 0; i < id; ++i)
|
||||
snapshots.pop_back();
|
||||
uint64_t snap = snapshots.back();
|
||||
Action::manager().restore(snap);
|
||||
}
|
||||
send_feedback = true;
|
||||
}
|
||||
#ifdef CONTROL_DEBUG
|
||||
else {
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'session'", attribute.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
catch (osc::MissingArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'session'", attribute.c_str());
|
||||
}
|
||||
catch (osc::ExcessArgumentException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'session'", attribute.c_str());
|
||||
}
|
||||
catch (osc::WrongArgumentTypeException &e) {
|
||||
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'session'", attribute.c_str());
|
||||
}
|
||||
|
||||
return send_feedback;
|
||||
}
|
||||
|
||||
void Control::sendSourceAttibutes(const IpEndpointName &remoteEndpoint, std::string target, Source *s)
|
||||
{
|
||||
// default values
|
||||
char name[21] = {"\0"};
|
||||
float lock = 0.f;
|
||||
float play = 0.f;
|
||||
float depth = 0.f;
|
||||
float alpha = 0.f;
|
||||
|
||||
// get source or current source
|
||||
Source *_s = s;
|
||||
if ( target.compare(OSC_CURRENT) == 0 )
|
||||
_s = Mixer::manager().currentSource();
|
||||
|
||||
// fill values if the source is valid
|
||||
if (_s!=nullptr) {
|
||||
strncpy(name, _s->name().c_str(), 20);
|
||||
lock = _s->locked() ? 1.f : 0.f;
|
||||
play = _s->playing() ? 1.f : 0.f;
|
||||
depth = _s->depth();
|
||||
alpha = _s->alpha();
|
||||
}
|
||||
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
// create bundle
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
/// name
|
||||
std::string address = std::string(OSC_PREFIX) + target + OSC_SOURCE_NAME;
|
||||
p << osc::BeginMessage( address.c_str() ) << name << osc::EndMessage;
|
||||
/// Play status
|
||||
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_LOCK;
|
||||
p << osc::BeginMessage( address.c_str() ) << lock << osc::EndMessage;
|
||||
/// Play status
|
||||
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_PLAY;
|
||||
p << osc::BeginMessage( address.c_str() ) << play << osc::EndMessage;
|
||||
/// Depth
|
||||
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_DEPTH;
|
||||
p << osc::BeginMessage( address.c_str() ) << depth << osc::EndMessage;
|
||||
/// Alpha
|
||||
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_ALPHA;
|
||||
p << osc::BeginMessage( address.c_str() ) << alpha << osc::EndMessage;
|
||||
|
||||
// send bundle
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
}
|
||||
|
||||
|
||||
void Control::sendSourcesStatus(const IpEndpointName &remoteEndpoint, osc::ReceivedMessageArgumentStream arguments)
|
||||
{
|
||||
// (if an argument is given, it indicates the number of sources to update)
|
||||
float N = 0.f;
|
||||
if ( !arguments.Eos())
|
||||
arguments >> N >> osc::EndMessage;
|
||||
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
int i = 0;
|
||||
char oscaddr[128];
|
||||
int index_current = Mixer::manager().indexCurrentSource();
|
||||
for (; i < Mixer::manager().count(); ++i) {
|
||||
// send status of currently selected
|
||||
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
||||
p << osc::BeginMessage( oscaddr ) << (index_current == i ? 1.f : 0.f) << osc::EndMessage;
|
||||
|
||||
// send status of alpha
|
||||
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
||||
p << osc::BeginMessage( oscaddr ) << Mixer::manager().sourceAtIndex(i)->alpha() << osc::EndMessage;
|
||||
}
|
||||
|
||||
for (; i < (int) N ; ++i) {
|
||||
// reset status of currently selected
|
||||
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
||||
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
||||
|
||||
// reset status of alpha
|
||||
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
||||
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
||||
}
|
||||
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
|
||||
// send status of current source
|
||||
sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
||||
}
|
||||
|
||||
|
||||
void Control::sendOutputStatus(const IpEndpointName &remoteEndpoint)
|
||||
{
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
/// output attributes
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_ENABLE );
|
||||
p << (Settings::application.render.disabled ? 0.f : 1.f);
|
||||
p << osc::EndMessage;
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_FADING );
|
||||
p << Mixer::manager().session()->fading();
|
||||
p << osc::EndMessage;
|
||||
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
}
|
||||
105
ControlManager.h
Normal file
105
ControlManager.h
Normal file
@@ -0,0 +1,105 @@
|
||||
#ifndef CONTROL_H
|
||||
#define CONTROL_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <condition_variable>
|
||||
#include "NetworkToolkit.h"
|
||||
|
||||
#define OSC_SYNC "/sync"
|
||||
|
||||
#define OSC_INFO "/info"
|
||||
#define OSC_INFO_LOG "/log"
|
||||
#define OSC_INFO_NOTIFY "/notify"
|
||||
|
||||
#define OSC_OUTPUT "/output"
|
||||
#define OSC_OUTPUT_ENABLE "/enable"
|
||||
#define OSC_OUTPUT_DISABLE "/disable"
|
||||
#define OSC_OUTPUT_FADING "/fading"
|
||||
#define OSC_OUTPUT_FADE_IN "/fade-in"
|
||||
#define OSC_OUTPUT_FADE_OUT "/fade-out"
|
||||
|
||||
#define OSC_ALL "/all"
|
||||
#define OSC_SELECTED "/selected"
|
||||
#define OSC_CURRENT "/current"
|
||||
#define OSC_NEXT "/next"
|
||||
#define OSC_PREVIOUS "/previous"
|
||||
|
||||
#define OSC_SOURCE_NAME "/name"
|
||||
#define OSC_SOURCE_LOCK "/lock"
|
||||
#define OSC_SOURCE_PLAY "/play"
|
||||
#define OSC_SOURCE_PAUSE "/pause"
|
||||
#define OSC_SOURCE_REPLAY "/replay"
|
||||
#define OSC_SOURCE_ALPHA "/alpha"
|
||||
#define OSC_SOURCE_LOOM "/loom"
|
||||
#define OSC_SOURCE_TRANSPARENCY "/transparency"
|
||||
#define OSC_SOURCE_DEPTH "/depth"
|
||||
#define OSC_SOURCE_GRAB "/grab"
|
||||
#define OSC_SOURCE_RESIZE "/resize"
|
||||
#define OSC_SOURCE_TURN "/turn"
|
||||
#define OSC_SOURCE_RESET "/reset"
|
||||
|
||||
#define OSC_SESSION "/session"
|
||||
#define OSC_SESSION_VERSION "/version"
|
||||
|
||||
class Session;
|
||||
class Source;
|
||||
|
||||
class Control
|
||||
{
|
||||
// Private Constructor
|
||||
Control();
|
||||
Control(Control const& copy) = delete;
|
||||
Control& operator=(Control const& copy) = delete;
|
||||
|
||||
public:
|
||||
|
||||
static Control& manager ()
|
||||
{
|
||||
// The only instance
|
||||
static Control _instance;
|
||||
return _instance;
|
||||
}
|
||||
~Control();
|
||||
|
||||
bool init();
|
||||
void terminate();
|
||||
|
||||
std::string translate (std::string addresspattern);
|
||||
|
||||
protected:
|
||||
|
||||
class RequestListener : public osc::OscPacketListener {
|
||||
protected:
|
||||
virtual void ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint );
|
||||
std::string FullMessage( const osc::ReceivedMessage& m );
|
||||
};
|
||||
|
||||
bool receiveOutputAttribute(const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments);
|
||||
|
||||
bool receiveSourceAttribute(Source *target, const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments);
|
||||
|
||||
bool receiveSessionAttribute(const std::string &attribute,
|
||||
osc::ReceivedMessageArgumentStream arguments);
|
||||
|
||||
void sendSourceAttibutes(const IpEndpointName& remoteEndpoint, std::string target, Source *s = nullptr);
|
||||
void sendSourcesStatus(const IpEndpointName& remoteEndpoint, osc::ReceivedMessageArgumentStream arguments);
|
||||
void sendOutputStatus(const IpEndpointName& remoteEndpoint);
|
||||
|
||||
private:
|
||||
|
||||
static void listen();
|
||||
RequestListener listener_;
|
||||
std::condition_variable receiver_end_;
|
||||
UdpListeningReceiveSocket *receiver_;
|
||||
|
||||
std::map<std::string, std::string> translation_;
|
||||
void loadOscConfig();
|
||||
void resetOscConfig();
|
||||
|
||||
};
|
||||
|
||||
#endif // CONTROL_H
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - Live video mixer
|
||||
*
|
||||
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "GlmToolkit.h"
|
||||
#include "Scene.h"
|
||||
#include "Source.h"
|
||||
|
||||
135
Decorations.cpp
135
Decorations.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
#include <glm/gtx/component_wise.hpp>
|
||||
@@ -11,6 +30,8 @@
|
||||
#include "Resource.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include <glad/glad.h>
|
||||
|
||||
Frame::Frame(CornerType corner, BorderType border, ShadowType shadow) : Node(),
|
||||
right_(nullptr), left_(nullptr), top_(nullptr), shadow_(nullptr), square_(nullptr)
|
||||
@@ -249,7 +270,7 @@ void Handles::draw(glm::mat4 modelview, glm::mat4 projection)
|
||||
init();
|
||||
}
|
||||
|
||||
if ( visible_ ) {
|
||||
if ( visible_ && handle_) {
|
||||
static Mesh *handle_active = new Mesh("mesh/border_handles_overlay_filled.ply");
|
||||
|
||||
// set color
|
||||
@@ -526,11 +547,6 @@ Disk::Disk() : Node()
|
||||
color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
|
||||
}
|
||||
|
||||
Disk::~Disk()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void Disk::draw(glm::mat4 modelview, glm::mat4 projection)
|
||||
{
|
||||
if ( !initialized() ) {
|
||||
@@ -556,55 +572,88 @@ void Disk::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
Surface *Glyph::font_ = nullptr;
|
||||
|
||||
//SelectionBox::SelectionBox()
|
||||
//{
|
||||
//// color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
|
||||
// color = glm::vec4( 1.f, 0.f, 0.f, 1.f);
|
||||
// square_ = new LineSquare( 3 );
|
||||
Glyph::Glyph(int imgui_font_index) : Node(), character_(' '), font_index_(imgui_font_index), baseline_(0.f), shape_(1.f, 1.f)
|
||||
{
|
||||
if (Glyph::font_ == nullptr)
|
||||
Glyph::font_ = new Surface;
|
||||
|
||||
//}
|
||||
uvTransform_ = glm::identity<glm::mat4>();
|
||||
color = glm::vec4( 1.f, 1.f, 1.f, 1.f);
|
||||
}
|
||||
|
||||
//void SelectionBox::draw (glm::mat4 modelview, glm::mat4 projection)
|
||||
//{
|
||||
// if ( !initialized() ) {
|
||||
// square_->init();
|
||||
// init();
|
||||
// }
|
||||
void Glyph::draw(glm::mat4 modelview, glm::mat4 projection)
|
||||
{
|
||||
if ( !initialized() ) {
|
||||
|
||||
// if (visible_) {
|
||||
if (!Glyph::font_->initialized()) {
|
||||
|
||||
// // use a visitor bounding box to calculate extend of all selected nodes
|
||||
// BoundingBoxVisitor vbox;
|
||||
uint tex = (uint)(intptr_t)ImGui::GetIO().Fonts->TexID;
|
||||
if ( tex > 0) {
|
||||
Glyph::font_->init();
|
||||
font_->setTextureIndex(tex);
|
||||
}
|
||||
}
|
||||
|
||||
// // visit every child of the selection
|
||||
// for (NodeSet::iterator node = children_.begin();
|
||||
// node != children_.end(); node++) {
|
||||
// // reset the transform before
|
||||
// vbox.setModelview(glm::identity<glm::mat4>());
|
||||
// (*node)->accept(vbox);
|
||||
// }
|
||||
if (Glyph::font_->initialized()) {
|
||||
init();
|
||||
setChar(character_);
|
||||
}
|
||||
}
|
||||
|
||||
// // get the bounding box
|
||||
// bbox_ = vbox.bbox();
|
||||
if ( visible_ ) {
|
||||
|
||||
//// Log::Info(" -------- visitor box (%f, %f)-(%f, %f)", bbox_.min().x, bbox_.min().y, bbox_.max().x, bbox_.max().y);
|
||||
// set color
|
||||
Glyph::font_->shader()->color = color;
|
||||
|
||||
// // set color
|
||||
// square_->shader()->color = color;
|
||||
// modify the shader iTransform to select UVs of the desired glyph
|
||||
Glyph::font_->shader()->iTransform = uvTransform_;
|
||||
|
||||
// // compute transformation from bounding box
|
||||
//// glm::mat4 ctm = modelview * GlmToolkit::transform(glm::vec3(0.f), glm::vec3(0.f), glm::vec3(1.f));
|
||||
// glm::mat4 ctm = modelview * GlmToolkit::transform(bbox_.center(), glm::vec3(0.f), bbox_.scale());
|
||||
// rebuild a matrix with rotation (see handles) and translation from modelview + translation_
|
||||
// and define scale to be 1, 1
|
||||
glm::mat4 ctm;
|
||||
glm::vec3 rot(0.f);
|
||||
glm::vec4 vec = modelview * glm::vec4(1.f, 0.f, 0.f, 0.f);
|
||||
rot.z = glm::orientedAngle( glm::vec3(1.f, 0.f, 0.f), glm::normalize(glm::vec3(vec)), glm::vec3(0.f, 0.f, 1.f) );
|
||||
// extract scaling
|
||||
ctm = glm::rotate(glm::identity<glm::mat4>(), -rot.z, glm::vec3(0.f, 0.f, 1.f)) * modelview ;
|
||||
vec = ctm * glm::vec4(1.f, 1.f, 0.f, 0.f);
|
||||
glm::vec2 sca = glm::vec2(vec.y) * glm::vec2(scale_.y) * shape_;
|
||||
// extract translation
|
||||
glm::vec3 tran = glm::vec3(modelview[3][0], modelview[3][1], modelview[3][2]) ;
|
||||
tran += (translation_ + glm::vec3(0.f, baseline_ * scale_.y, 0.f) ) * glm::vec3(vec);
|
||||
// apply local rotation
|
||||
rot.z += rotation_.z;
|
||||
// generate matrix
|
||||
ctm = GlmToolkit::transform(tran, rot, glm::vec3(sca, 1.f));
|
||||
|
||||
// // draw bbox
|
||||
//// square_->draw( modelview, projection);
|
||||
// square_->draw( ctm, projection);
|
||||
Glyph::font_->draw( ctm, projection);
|
||||
}
|
||||
}
|
||||
|
||||
// // DEBUG
|
||||
//// visible_=false;
|
||||
// }
|
||||
void Glyph::setChar(char c)
|
||||
{
|
||||
character_ = c;
|
||||
|
||||
//}
|
||||
if (initialized()) {
|
||||
// get from imgui the UVs of the given char
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
ImFont* myfont = io.Fonts->Fonts[0];
|
||||
if (font_index_ > 0 && font_index_ < io.Fonts->Fonts.size() )
|
||||
myfont = io.Fonts->Fonts[font_index_];
|
||||
const ImFontGlyph* glyph = myfont->FindGlyph(character_);
|
||||
|
||||
if (glyph) {
|
||||
// create a texture UV transform to get the UV coordinates of the glyph
|
||||
const glm::vec3 uv_t = glm::vec3( glyph->U0, glyph->V0, 0.f);
|
||||
const glm::vec3 uv_s = glm::vec3( glyph->U1 - glyph->U0, glyph->V1 - glyph->V0, 1.f);
|
||||
const glm::vec3 uv_r = glm::vec3(0.f, 0.f, 0.f);
|
||||
uvTransform_ = GlmToolkit::transform(uv_t, uv_r, uv_s);
|
||||
|
||||
// remember glyph shape
|
||||
shape_ = glm::vec2(glyph->X1 - glyph->X0, glyph->Y1 - glyph->Y0) / myfont->FontSize;
|
||||
baseline_ = -glyph->Y0 / myfont->FontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ class Disk : public Node
|
||||
{
|
||||
public:
|
||||
Disk();
|
||||
~Disk();
|
||||
|
||||
void draw (glm::mat4 modelview, glm::mat4 projection) override;
|
||||
void accept (Visitor& v) override;
|
||||
@@ -94,20 +93,26 @@ protected:
|
||||
static Mesh *disk_;
|
||||
};
|
||||
|
||||
//class SelectionBox : public Group
|
||||
//{
|
||||
//public:
|
||||
// SelectionBox();
|
||||
class Glyph : public Node
|
||||
{
|
||||
public:
|
||||
Glyph(int imgui_font_index = 0);
|
||||
void setChar(char c);
|
||||
|
||||
// void draw (glm::mat4 modelview, glm::mat4 projection) override;
|
||||
void draw (glm::mat4 modelview, glm::mat4 projection) override;
|
||||
|
||||
// glm::vec4 color;
|
||||
glm::vec4 color;
|
||||
|
||||
//protected:
|
||||
// LineSquare *square_;
|
||||
// GlmToolkit::AxisAlignedBoundingBox bbox_;
|
||||
protected:
|
||||
|
||||
//};
|
||||
char character_;
|
||||
int font_index_;
|
||||
float baseline_;
|
||||
glm::vec2 shape_;
|
||||
glm::mat4 uvTransform_;
|
||||
static Surface *font_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif // DECORATIONS_H
|
||||
|
||||
185
DeviceSource.cpp
185
DeviceSource.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
@@ -19,7 +38,7 @@
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define DEVICE_DEBUG
|
||||
//#define GST_DEVICE_DEBUG
|
||||
#define GST_DEVICE_DEBUG
|
||||
#endif
|
||||
|
||||
|
||||
@@ -101,69 +120,82 @@ gboolean
|
||||
Device::callback_device_monitor (GstBus *, GstMessage * message, gpointer )
|
||||
{
|
||||
GstDevice *device;
|
||||
gchar *name;
|
||||
|
||||
switch (GST_MESSAGE_TYPE (message)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
case GST_MESSAGE_DEVICE_ADDED:
|
||||
{
|
||||
gst_message_parse_device_added (message, &device);
|
||||
name = gst_device_get_display_name (device);
|
||||
|
||||
// ignore if already in the list
|
||||
if ( std::find(manager().src_name_.begin(), manager().src_name_.end(), name) != manager().src_name_.end())
|
||||
break;
|
||||
|
||||
manager().src_name_.push_back(name);
|
||||
#ifdef GST_DEVICE_DEBUG
|
||||
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
|
||||
g_print("\nDevice %s plugged : %s\n", name, stru);
|
||||
g_free (stru);
|
||||
#endif
|
||||
g_free (name);
|
||||
|
||||
std::string p = pipelineForDevice(device, manager().src_description_.size());
|
||||
manager().src_description_.push_back(p);
|
||||
|
||||
DeviceConfigSet confs = getDeviceConfigs(p);
|
||||
manager().src_config_.push_back(confs);
|
||||
|
||||
manager().list_uptodate_ = false;
|
||||
|
||||
manager().add(device);
|
||||
gst_object_unref (device);
|
||||
|
||||
}
|
||||
break;
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
case GST_MESSAGE_DEVICE_REMOVED:
|
||||
{
|
||||
gst_message_parse_device_removed (message, &device);
|
||||
name = gst_device_get_display_name (device);
|
||||
manager().remove(name);
|
||||
#ifdef GST_DEVICE_DEBUG
|
||||
g_print("\nDevice %s unplugged\n", name);
|
||||
#endif
|
||||
g_free (name);
|
||||
|
||||
manager().list_uptodate_ = false;
|
||||
|
||||
manager().remove(device);
|
||||
gst_object_unref (device);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
void Device::remove(const char *device)
|
||||
|
||||
void Device::add(GstDevice *device)
|
||||
{
|
||||
if (device==nullptr)
|
||||
return;
|
||||
|
||||
gchar *device_name = gst_device_get_display_name (device);
|
||||
|
||||
if ( std::find(manager().src_name_.begin(), manager().src_name_.end(), device_name) == manager().src_name_.end()) {
|
||||
|
||||
std::string p = pipelineForDevice(device, manager().src_description_.size());
|
||||
DeviceConfigSet confs = getDeviceConfigs(p);
|
||||
|
||||
// add if not in the list and valid
|
||||
if (!p.empty() && !confs.empty()) {
|
||||
src_name_.push_back(device_name);
|
||||
src_description_.push_back(p);
|
||||
src_config_.push_back(confs);
|
||||
#ifdef GST_DEVICE_DEBUG
|
||||
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
|
||||
g_print("\nDevice %s plugged : %s\n", device_name, stru);
|
||||
g_free (stru);
|
||||
#endif
|
||||
}
|
||||
list_uptodate_ = false;
|
||||
}
|
||||
g_free (device_name);
|
||||
}
|
||||
|
||||
void Device::remove(GstDevice *device)
|
||||
{
|
||||
if (device==nullptr)
|
||||
return;
|
||||
|
||||
gchar *device_name = gst_device_get_display_name (device);
|
||||
|
||||
std::vector< std::string >::iterator nameit = src_name_.begin();
|
||||
std::vector< std::string >::iterator descit = src_description_.begin();
|
||||
std::vector< DeviceConfigSet >::iterator coit = src_config_.begin();
|
||||
while (nameit != src_name_.end()){
|
||||
|
||||
if ( (*nameit).compare(device) == 0 )
|
||||
if ( (*nameit).compare(device_name) == 0 )
|
||||
{
|
||||
src_name_.erase(nameit);
|
||||
src_description_.erase(descit);
|
||||
src_config_.erase(coit);
|
||||
|
||||
list_uptodate_ = false;
|
||||
|
||||
#ifdef GST_DEVICE_DEBUG
|
||||
g_print("\nDevice %s unplugged\n", device_name);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -171,10 +203,16 @@ void Device::remove(const char *device)
|
||||
++descit;
|
||||
++coit;
|
||||
}
|
||||
g_free (device_name);
|
||||
}
|
||||
|
||||
|
||||
Device::Device()
|
||||
Device::Device(): list_uptodate_(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void Device::init()
|
||||
{
|
||||
GstBus *bus;
|
||||
GstCaps *caps;
|
||||
@@ -198,24 +236,9 @@ Device::Device()
|
||||
GList *devices = gst_device_monitor_get_devices(monitor_);
|
||||
GList *tmp;
|
||||
for (tmp = devices; tmp ; tmp = tmp->next ) {
|
||||
|
||||
GstDevice *device = (GstDevice *) tmp->data;
|
||||
|
||||
gchar *name = gst_device_get_display_name (device);
|
||||
src_name_.push_back(name);
|
||||
g_free (name);
|
||||
|
||||
#ifdef GST_DEVICE_DEBUG
|
||||
gchar *stru = gst_structure_to_string( gst_device_get_properties(device) );
|
||||
g_print("\nDevice %s already plugged : %s", name, stru);
|
||||
g_free (stru);
|
||||
#endif
|
||||
|
||||
std::string p = pipelineForDevice(device, src_description_.size());
|
||||
src_description_.push_back(p);
|
||||
|
||||
DeviceConfigSet confs = getDeviceConfigs(p);
|
||||
src_config_.push_back(confs);
|
||||
add(device);
|
||||
gst_object_unref (device);
|
||||
}
|
||||
g_list_free(devices);
|
||||
|
||||
@@ -344,7 +367,6 @@ DeviceSource::~DeviceSource()
|
||||
void DeviceSource::setDevice(const std::string &devicename)
|
||||
{
|
||||
device_ = devicename;
|
||||
Log::Notify("Creating Source with device '%s'", device_.c_str());
|
||||
|
||||
int index = Device::manager().index(device_);
|
||||
if (index > -1) {
|
||||
@@ -365,28 +387,34 @@ void DeviceSource::setDevice(const std::string &devicename)
|
||||
Log::Info(" - %s %s %d x %d %.1f fps", (*it).stream.c_str(), (*it).format.c_str(), (*it).width, (*it).height, fps);
|
||||
}
|
||||
#endif
|
||||
DeviceConfig best = *confs.rbegin();
|
||||
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
|
||||
Log::Info("Device %s selected its optimal config: %s %s %dx%d@%.1ffps", device_.c_str(), best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
|
||||
if (!confs.empty()) {
|
||||
DeviceConfig best = *confs.rbegin();
|
||||
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
|
||||
Log::Info("Device %s selected its optimal config: %s %s %dx%d@%.1ffps", device_.c_str(), best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
|
||||
|
||||
pipeline << " ! " << best.stream;
|
||||
if (!best.format.empty())
|
||||
pipeline << ",format=" << best.format;
|
||||
pipeline << ",framerate=" << best.fps_numerator << "/" << best.fps_denominator;
|
||||
pipeline << ",width=" << best.width;
|
||||
pipeline << ",height=" << best.height;
|
||||
pipeline << " ! " << best.stream;
|
||||
if (!best.format.empty())
|
||||
pipeline << ",format=" << best.format;
|
||||
pipeline << ",framerate=" << best.fps_numerator << "/" << best.fps_denominator;
|
||||
pipeline << ",width=" << best.width;
|
||||
pipeline << ",height=" << best.height;
|
||||
|
||||
if ( best.stream.find("jpeg") != std::string::npos )
|
||||
pipeline << " ! jpegdec";
|
||||
if ( best.stream.find("jpeg") != std::string::npos )
|
||||
pipeline << " ! jpegdec";
|
||||
|
||||
if ( device_.find("Screen") != std::string::npos )
|
||||
pipeline << " ! videoconvert ! video/x-raw,format=RGB ! queue max-size-buffers=3";
|
||||
if ( device_.find("Screen") != std::string::npos )
|
||||
pipeline << " ! videoconvert ! video/x-raw,format=RGB ! queue max-size-buffers=3";
|
||||
|
||||
pipeline << " ! videoconvert";
|
||||
pipeline << " ! videoconvert";
|
||||
|
||||
// open gstreamer
|
||||
stream_->open( pipeline.str(), best.width, best.height);
|
||||
stream_->play(true);
|
||||
// resize render buffer
|
||||
if (renderbuffer_)
|
||||
renderbuffer_->resize(best.width, best.height);
|
||||
|
||||
// open gstreamer
|
||||
stream_->open( pipeline.str(), best.width, best.height);
|
||||
stream_->play(true);
|
||||
}
|
||||
|
||||
// will be ready after init and one frame rendered
|
||||
ready_ = false;
|
||||
@@ -561,9 +589,14 @@ DeviceConfigSet Device::getDeviceConfigs(const std::string &src_description)
|
||||
glm::ivec2 DeviceSource::icon() const
|
||||
{
|
||||
if ( device_.find("Screen") != std::string::npos )
|
||||
return glm::ivec2(19, 1);
|
||||
return glm::ivec2(ICON_SOURCE_DEVICE_SCREEN);
|
||||
else
|
||||
return glm::ivec2(2, 14);
|
||||
return glm::ivec2(ICON_SOURCE_DEVICE);
|
||||
}
|
||||
|
||||
std::string DeviceSource::info() const
|
||||
{
|
||||
return std::string("device '") + device_ + "'";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
inline std::string device() const { return device_; }
|
||||
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
private:
|
||||
std::string device_;
|
||||
@@ -99,6 +100,7 @@ public:
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void init();
|
||||
int numDevices () const;
|
||||
std::string name (int index) const;
|
||||
std::string description (int index) const;
|
||||
@@ -115,7 +117,8 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
void remove(const char *device);
|
||||
void remove(GstDevice *device);
|
||||
void add(GstDevice *device);
|
||||
|
||||
std::vector< std::string > src_name_;
|
||||
std::vector< std::string > src_description_;
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
|
||||
// multiplatform implementation of system dialogs
|
||||
//
|
||||
@@ -6,16 +25,17 @@
|
||||
// because of 'zenity' access rights nightmare :(
|
||||
// Thus this re-implementation of native GTK+ dialogs for linux
|
||||
|
||||
#include "defines.h"
|
||||
#include "Settings.h"
|
||||
#include "SystemToolkit.h"
|
||||
#include "DialogToolkit.h"
|
||||
|
||||
#if defined(LINUX)
|
||||
#define USE_TINYFILEDIALOG 0
|
||||
#else
|
||||
#define USE_TINYFILEDIALOG 1
|
||||
#endif
|
||||
|
||||
#include "defines.h"
|
||||
#include "SystemToolkit.h"
|
||||
#include "DialogToolkit.h"
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
#include "tinyfiledialogs.h"
|
||||
#else
|
||||
@@ -40,8 +60,10 @@ void add_filter_file_dialog( GtkWidget *dialog, int const countfilterPatterns, c
|
||||
GtkFileFilter *filter;
|
||||
filter = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name( filter, filterDescription );
|
||||
for (int i = 0; i < countfilterPatterns; i++ )
|
||||
gtk_file_filter_add_pattern( filter, filterPatterns[i] );
|
||||
for (int i = 0; i < countfilterPatterns; i++ ) {
|
||||
gtk_file_filter_add_pattern( filter, g_ascii_strdown(filterPatterns[i], -1) );
|
||||
gtk_file_filter_add_pattern( filter, g_ascii_strup(filterPatterns[i], -1) );
|
||||
}
|
||||
gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter );
|
||||
}
|
||||
|
||||
@@ -61,25 +83,156 @@ bool gtk_init()
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string DialogToolkit::saveSessionFileDialog(const std::string &path)
|
||||
|
||||
// globals
|
||||
const std::chrono::milliseconds timeout = std::chrono::milliseconds(4);
|
||||
bool DialogToolkit::FileDialog::busy_ = false;
|
||||
|
||||
//
|
||||
// FileDialog common functions
|
||||
//
|
||||
DialogToolkit::FileDialog::FileDialog(const std::string &name) : id_(name)
|
||||
{
|
||||
if ( Settings::application.dialogRecentFolder.count(id_) == 0 )
|
||||
Settings::application.dialogRecentFolder[id_] = SystemToolkit::home_path();
|
||||
}
|
||||
|
||||
bool DialogToolkit::FileDialog::closed()
|
||||
{
|
||||
if ( !promises_.empty() ) {
|
||||
// check that file dialog thread finished
|
||||
if (promises_.back().wait_for(timeout) == std::future_status::ready ) {
|
||||
// get the filename from this file dialog
|
||||
path_ = promises_.back().get();
|
||||
if (!path_.empty()) {
|
||||
// save path location
|
||||
Settings::application.dialogRecentFolder[id_] = SystemToolkit::path_filename(path_);
|
||||
}
|
||||
// done with this file dialog
|
||||
promises_.pop_back();
|
||||
busy_ = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// type specific implementations
|
||||
//
|
||||
std::string openImageFileDialog(const std::string &label, const std::string &path);
|
||||
void DialogToolkit::OpenImageDialog::open()
|
||||
{
|
||||
if ( !busy_ && promises_.empty() ) {
|
||||
promises_.emplace_back( std::async(std::launch::async, openImageFileDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string openSessionFileDialog(const std::string &label, const std::string &path);
|
||||
void DialogToolkit::OpenSessionDialog::open()
|
||||
{
|
||||
if ( !busy_ && promises_.empty() ) {
|
||||
promises_.emplace_back( std::async(std::launch::async, openSessionFileDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string openMediaFileDialog(const std::string &label, const std::string &path);
|
||||
void DialogToolkit::OpenMediaDialog::open()
|
||||
{
|
||||
if ( !busy_ && promises_.empty() ) {
|
||||
promises_.emplace_back( std::async(std::launch::async, openMediaFileDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string saveSessionFileDialog(const std::string &label, const std::string &path);
|
||||
void DialogToolkit::SaveSessionDialog::open()
|
||||
{
|
||||
if ( !busy_ && promises_.empty() ) {
|
||||
promises_.emplace_back( std::async(std::launch::async, saveSessionFileDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string openFolderDialog(const std::string &label, const std::string &path);
|
||||
void DialogToolkit::OpenFolderDialog::open()
|
||||
{
|
||||
if ( !busy_ && promises_.empty() ) {
|
||||
promises_.emplace_back( std::async(std::launch::async, openFolderDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::list<std::string> selectImagesFileDialog(const std::string &label,const std::string &path);
|
||||
void DialogToolkit::MultipleImagesDialog::open()
|
||||
{
|
||||
if ( !busy_ && promisedlist_.empty() ) {
|
||||
promisedlist_.emplace_back( std::async(std::launch::async, selectImagesFileDialog, id_,
|
||||
Settings::application.dialogRecentFolder[id_]) );
|
||||
busy_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool DialogToolkit::MultipleImagesDialog::closed()
|
||||
{
|
||||
if ( !promisedlist_.empty() ) {
|
||||
// check that file dialog thread finished
|
||||
if (promisedlist_.back().wait_for(timeout) == std::future_status::ready ) {
|
||||
// get the filename from this file dialog
|
||||
std::list<std::string> list = promisedlist_.back().get();
|
||||
if (!list.empty()) {
|
||||
// selected a filenames
|
||||
pathlist_ = list;
|
||||
path_ = list.front();
|
||||
// save path location
|
||||
Settings::application.dialogRecentFolder[id_] = SystemToolkit::path_filename(path_);
|
||||
}
|
||||
else {
|
||||
pathlist_.clear();
|
||||
path_.clear();
|
||||
}
|
||||
// done with this file dialog
|
||||
promisedlist_.pop_back();
|
||||
busy_ = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// CALLBACKS
|
||||
//
|
||||
//
|
||||
|
||||
std::string saveSessionFileDialog(const std::string &label, const std::string &path)
|
||||
{
|
||||
std::string filename = "";
|
||||
char const * save_pattern[1] = { "*.mix" };
|
||||
char const * save_pattern[1] = { VIMIX_FILES_PATTERN };
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * save_file_name;
|
||||
|
||||
save_file_name = tinyfd_saveFileDialog( "Save a session file", path.c_str(), 1, save_pattern, "vimix session");
|
||||
save_file_name = tinyfd_saveFileDialog( label.c_str(), path.c_str(), 1, save_pattern, "vimix session");
|
||||
|
||||
if (save_file_name)
|
||||
filename = std::string(save_file_name);
|
||||
#else
|
||||
if (!gtk_init()) {
|
||||
ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return filename;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Save Session File", NULL,
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_SAVE,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Save", GTK_RESPONSE_ACCEPT, NULL );
|
||||
@@ -115,32 +268,32 @@ std::string DialogToolkit::saveSessionFileDialog(const std::string &path)
|
||||
#endif
|
||||
|
||||
std::string extension = filename.substr(filename.find_last_of(".") + 1);
|
||||
if (extension != "mix")
|
||||
if (!filename.empty() && extension != "mix")
|
||||
filename += ".mix";
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
std::string DialogToolkit::openSessionFileDialog(const std::string &path)
|
||||
std::string openSessionFileDialog(const std::string &label, const std::string &path)
|
||||
{
|
||||
std::string filename = "";
|
||||
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
|
||||
char const * open_pattern[1] = { "*.mix" };
|
||||
char const * open_pattern[1] = { VIMIX_FILES_PATTERN };
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * open_file_name;
|
||||
open_file_name = tinyfd_openFileDialog( "Import a file", startpath.c_str(), 1, open_pattern, "vimix session", 0);
|
||||
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 1, open_pattern, "vimix session", 0);
|
||||
|
||||
if (open_file_name)
|
||||
filename = std::string(open_file_name);
|
||||
#else
|
||||
if (!gtk_init()) {
|
||||
ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return filename;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open Session File", NULL,
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL );
|
||||
@@ -178,36 +331,32 @@ std::string DialogToolkit::openSessionFileDialog(const std::string &path)
|
||||
}
|
||||
|
||||
|
||||
std::string DialogToolkit::openMediaFileDialog(const std::string &path)
|
||||
std::string openMediaFileDialog(const std::string &label, const std::string &path)
|
||||
{
|
||||
std::string filename = "";
|
||||
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
|
||||
char const * open_pattern[18] = { "*.mix", "*.mp4", "*.mpg",
|
||||
"*.avi", "*.mov", "*.mkv",
|
||||
"*.webm", "*.mod", "*.wmv",
|
||||
"*.mxf", "*.ogg", "*.flv",
|
||||
"*.asf", "*.jpg", "*.png",
|
||||
"*.gif", "*.tif", "*.svg" };
|
||||
char const * open_pattern[26] = { MEDIA_FILES_PATTERN };
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * open_file_name;
|
||||
open_file_name = tinyfd_openFileDialog( "Open Media File", startpath.c_str(), 18, open_pattern, "All supported formats", 0);
|
||||
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 26, open_pattern, "All supported formats", 0);
|
||||
|
||||
if (open_file_name)
|
||||
filename = std::string(open_file_name);
|
||||
#else
|
||||
|
||||
if (!gtk_init()) {
|
||||
ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return filename;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Open Media File", NULL,
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL );
|
||||
|
||||
// set file filters
|
||||
add_filter_file_dialog(dialog, 18, open_pattern, "All supported formats");
|
||||
add_filter_file_dialog(dialog, 26, open_pattern, "Supported formats (videos, images, sessions)");
|
||||
add_filter_any_file_dialog(dialog);
|
||||
|
||||
// Set the default path
|
||||
@@ -238,24 +387,82 @@ std::string DialogToolkit::openMediaFileDialog(const std::string &path)
|
||||
return filename;
|
||||
}
|
||||
|
||||
std::string DialogToolkit::openFolderDialog(const std::string &path)
|
||||
|
||||
std::string openImageFileDialog(const std::string &label, const std::string &path)
|
||||
{
|
||||
std::string filename = "";
|
||||
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
|
||||
char const * open_pattern[5] = { IMAGES_FILES_PATTERN };
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * open_file_name;
|
||||
open_file_name = tinyfd_openFileDialog( label.c_str(), startpath.c_str(), 5, open_pattern, "Image (JPG, PNG, BMP, PPM, GIF)", 0);
|
||||
|
||||
if (open_file_name)
|
||||
filename = std::string(open_file_name);
|
||||
#else
|
||||
|
||||
if (!gtk_init()) {
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return filename;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL );
|
||||
|
||||
// set file filters
|
||||
add_filter_file_dialog(dialog, 5, open_pattern, "Image (JPG, PNG, BMP, PPM, GIF)");
|
||||
add_filter_any_file_dialog(dialog);
|
||||
|
||||
// Set the default path
|
||||
gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER(dialog), startpath.c_str() );
|
||||
|
||||
// ensure front and centered
|
||||
gtk_window_set_keep_above( GTK_WINDOW(dialog), TRUE );
|
||||
if (window_x > 0 && window_y > 0)
|
||||
gtk_window_move( GTK_WINDOW(dialog), window_x, window_y);
|
||||
|
||||
// display and get filename
|
||||
if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT ) {
|
||||
|
||||
char *open_file_name = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
|
||||
if (open_file_name)
|
||||
filename = std::string(open_file_name);
|
||||
g_free( open_file_name );
|
||||
}
|
||||
|
||||
// remember position
|
||||
gtk_window_get_position( GTK_WINDOW(dialog), &window_x, &window_y);
|
||||
|
||||
// done
|
||||
gtk_widget_destroy(dialog);
|
||||
wait_for_event();
|
||||
#endif
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
std::string openFolderDialog(const std::string &label, const std::string &path)
|
||||
{
|
||||
std::string foldername = "";
|
||||
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * open_folder_name;
|
||||
open_folder_name = tinyfd_selectFolderDialog("Select folder", startpath.c_str());
|
||||
open_folder_name = tinyfd_selectFolderDialog(label.c_str(), startpath.c_str());
|
||||
|
||||
if (open_folder_name)
|
||||
foldername = std::string(open_folder_name);
|
||||
#else
|
||||
if (!gtk_init()) {
|
||||
ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return foldername;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Select folder", NULL,
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Select", GTK_RESPONSE_ACCEPT, NULL );
|
||||
@@ -290,16 +497,16 @@ std::string DialogToolkit::openFolderDialog(const std::string &path)
|
||||
}
|
||||
|
||||
|
||||
std::list<std::string> DialogToolkit::selectImagesFileDialog(const std::string &path)
|
||||
std::list<std::string> selectImagesFileDialog(const std::string &label,const std::string &path)
|
||||
{
|
||||
std::list<std::string> files;
|
||||
|
||||
std::string startpath = SystemToolkit::file_exists(path) ? path : SystemToolkit::home_path();
|
||||
char const * open_pattern[3] = { "*.tif", "*.jpg", "*.png" };
|
||||
char const * open_pattern[6] = { "*.jpg", "*.png", "*.tif" };
|
||||
|
||||
#if USE_TINYFILEDIALOG
|
||||
char const * open_file_names;
|
||||
open_file_names = tinyfd_openFileDialog( "Select images", startpath.c_str(), 3, open_pattern, "Images", 1);
|
||||
open_file_names = tinyfd_openFileDialog(label.c_str(), startpath.c_str(), 3, open_pattern, "Images (JPG, PNG, TIF)", 1);
|
||||
|
||||
if (open_file_names) {
|
||||
|
||||
@@ -325,17 +532,17 @@ std::list<std::string> DialogToolkit::selectImagesFileDialog(const std::string &
|
||||
#else
|
||||
|
||||
if (!gtk_init()) {
|
||||
ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
DialogToolkit::ErrorDialog("Could not initialize GTK+ for dialog");
|
||||
return files;
|
||||
}
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( "Select images", NULL,
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new( label.c_str(), NULL,
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL );
|
||||
|
||||
// set file filters
|
||||
add_filter_file_dialog(dialog, 3, open_pattern, "All supported formats");
|
||||
add_filter_file_dialog(dialog, 3, open_pattern, "Images (JPG, PNG, TIF)");
|
||||
add_filter_any_file_dialog(dialog);
|
||||
|
||||
// multiple files
|
||||
|
||||
@@ -3,23 +3,86 @@
|
||||
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <future>
|
||||
|
||||
#define VIMIX_FILES_PATTERN "*.mix"
|
||||
#define MEDIA_FILES_PATTERN "*.mix", "*.mp4", "*.mpg", "*.mpeg", "*.m2v", "*.m4v", "*.avi", "*.mov",\
|
||||
"*.mkv", "*.webm", "*.mod", "*.wmv", "*.mxf", "*.ogg",\
|
||||
"*.flv", "*.hevc", "*.asf", "*.jpg", "*.png", "*.gif",\
|
||||
"*.tif", "*.tiff", "*.webp", "*.bmp", "*.ppm", "*.svg,"
|
||||
#define IMAGES_FILES_PATTERN "*.jpg", "*.png", "*.bmp", "*.ppm", "*.gif"
|
||||
|
||||
namespace DialogToolkit
|
||||
{
|
||||
|
||||
std::string saveSessionFileDialog(const std::string &path);
|
||||
|
||||
std::string openSessionFileDialog(const std::string &path);
|
||||
|
||||
std::string openMediaFileDialog(const std::string &path);
|
||||
|
||||
std::string openFolderDialog(const std::string &path);
|
||||
|
||||
std::list<std::string> selectImagesFileDialog(const std::string &path);
|
||||
|
||||
void ErrorDialog(const char* message);
|
||||
|
||||
class FileDialog
|
||||
{
|
||||
protected:
|
||||
std::string id_;
|
||||
std::string directory_;
|
||||
std::string path_;
|
||||
std::vector< std::future<std::string> >promises_;
|
||||
static bool busy_;
|
||||
|
||||
public:
|
||||
FileDialog(const std::string &name);
|
||||
|
||||
virtual void open() = 0;
|
||||
virtual bool closed();
|
||||
inline std::string path() const { return path_; }
|
||||
|
||||
static bool busy() { return busy_; }
|
||||
};
|
||||
|
||||
class OpenImageDialog : public FileDialog
|
||||
{
|
||||
public:
|
||||
OpenImageDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open();
|
||||
};
|
||||
|
||||
class OpenSessionDialog : public FileDialog
|
||||
{
|
||||
public:
|
||||
OpenSessionDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open();
|
||||
};
|
||||
|
||||
class OpenMediaDialog : public FileDialog
|
||||
{
|
||||
public:
|
||||
OpenMediaDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open();
|
||||
};
|
||||
|
||||
class SaveSessionDialog : public FileDialog
|
||||
{
|
||||
public:
|
||||
SaveSessionDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open();
|
||||
};
|
||||
|
||||
class OpenFolderDialog : public FileDialog
|
||||
{
|
||||
public:
|
||||
OpenFolderDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open();
|
||||
};
|
||||
|
||||
class MultipleImagesDialog : public FileDialog
|
||||
{
|
||||
std::list<std::string> pathlist_;
|
||||
std::vector< std::future< std::list<std::string> > > promisedlist_;
|
||||
public:
|
||||
MultipleImagesDialog(const std::string &name) : FileDialog(name) {}
|
||||
void open() override;
|
||||
bool closed() override;
|
||||
inline std::list<std::string> images() const { return pathlist_; }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
@@ -17,8 +36,8 @@ DrawVisitor::DrawVisitor(Node *nodetodraw, glm::mat4 projection, bool force): fo
|
||||
}
|
||||
|
||||
DrawVisitor::DrawVisitor(const std::vector<Node *> &nodestodraw, glm::mat4 projection, bool force):
|
||||
force_(force), targets_(nodestodraw), modelview_(glm::identity<glm::mat4>()),
|
||||
projection_(projection), num_duplicat_(1), transform_duplicat_(glm::identity<glm::mat4>())
|
||||
modelview_(glm::identity<glm::mat4>()), projection_(projection), targets_(nodestodraw), force_(force),
|
||||
num_duplicat_(1), transform_duplicat_(glm::identity<glm::mat4>())
|
||||
{
|
||||
|
||||
}
|
||||
@@ -93,6 +112,6 @@ void DrawVisitor::visit(Switch &n)
|
||||
modelview_ = mv;
|
||||
}
|
||||
|
||||
void DrawVisitor::visit(Primitive &n)
|
||||
void DrawVisitor::visit(Primitive &)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
|
||||
void visit(Scene& n) override;
|
||||
void visit(Node& n) override;
|
||||
void visit(Primitive& n) override;
|
||||
void visit(Primitive& ) override;
|
||||
void visit(Group& n) override;
|
||||
void visit(Switch& n) override;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - Live video mixer
|
||||
*
|
||||
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
109
FrameBuffer.cpp
109
FrameBuffer.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "FrameBuffer.h"
|
||||
@@ -14,13 +33,14 @@
|
||||
|
||||
const char* FrameBuffer::aspect_ratio_name[5] = { "4:3", "3:2", "16:10", "16:9", "21:9" };
|
||||
glm::vec2 FrameBuffer::aspect_ratio_size[5] = { glm::vec2(4.f,3.f), glm::vec2(3.f,2.f), glm::vec2(16.f,10.f), glm::vec2(16.f,9.f) , glm::vec2(21.f,9.f) };
|
||||
const char* FrameBuffer::resolution_name[4] = { "720", "1080", "1440", "2160" };
|
||||
float FrameBuffer::resolution_height[4] = { 720.f, 1080.f, 1440.f, 2160.f };
|
||||
const char* FrameBuffer::resolution_name[5] = { "720", "1080", "1200", "1440", "2160" };
|
||||
float FrameBuffer::resolution_height[5] = { 720.f, 1080.f, 1200.f, 1440.f, 2160.f };
|
||||
|
||||
|
||||
glm::vec3 FrameBuffer::getResolutionFromParameters(int ar, int h)
|
||||
{
|
||||
float width = aspect_ratio_size[ar].x * resolution_height[h] / aspect_ratio_size[ar].y;
|
||||
width -= (int)width % 2;
|
||||
glm::vec3 res = glm::vec3( width, resolution_height[h] , 0.f);
|
||||
|
||||
return res;
|
||||
@@ -171,6 +191,31 @@ glm::vec3 FrameBuffer::resolution() const
|
||||
return glm::vec3(attrib_.viewport.x, attrib_.viewport.y, 0.f);
|
||||
}
|
||||
|
||||
void FrameBuffer::resize(int width, int height)
|
||||
{
|
||||
if (framebufferid_) {
|
||||
if (attrib_.viewport.x != width || attrib_.viewport.y != height)
|
||||
{
|
||||
// de-init
|
||||
glDeleteFramebuffers(1, &framebufferid_);
|
||||
framebufferid_ = 0;
|
||||
|
||||
if (intermediate_framebufferid_)
|
||||
glDeleteFramebuffers(1, &intermediate_framebufferid_);
|
||||
intermediate_framebufferid_ = 0;
|
||||
if (textureid_)
|
||||
glDeleteTextures(1, &textureid_);
|
||||
textureid_ = 0;
|
||||
if (intermediate_textureid_)
|
||||
glDeleteTextures(1, &intermediate_textureid_);
|
||||
intermediate_textureid_ = 0;
|
||||
|
||||
// change resolution
|
||||
attrib_.viewport = glm::ivec2(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FrameBuffer::begin(bool clear)
|
||||
{
|
||||
if (!framebufferid_)
|
||||
@@ -277,7 +322,7 @@ void FrameBuffer::checkFramebufferStatus()
|
||||
break;
|
||||
case GL_FRAMEBUFFER_COMPLETE:
|
||||
#ifndef NDEBUG
|
||||
Log::Info("Framebuffer created %d x %d.", width(), height());
|
||||
g_print("Framebuffer created %d x %d\n", width(), height());
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
@@ -317,7 +362,16 @@ FrameBufferImage::FrameBufferImage(jpegBuffer jpgimg) :
|
||||
rgb = stbi_load_from_memory(jpgimg.buffer, jpgimg.len, &width, &height, &c, 3);
|
||||
}
|
||||
|
||||
FrameBufferImage::~FrameBufferImage() {
|
||||
FrameBufferImage::FrameBufferImage(const std::string &filename) :
|
||||
rgb(nullptr), width(0), height(0)
|
||||
{
|
||||
int c = 0;
|
||||
if (!filename.empty())
|
||||
rgb = stbi_load(filename.c_str(), &width, &height, &c, 3);
|
||||
}
|
||||
|
||||
FrameBufferImage::~FrameBufferImage()
|
||||
{
|
||||
if (rgb!=nullptr)
|
||||
delete rgb;
|
||||
}
|
||||
@@ -352,6 +406,10 @@ FrameBufferImage *FrameBuffer::image(){
|
||||
if (!framebufferid_)
|
||||
return img;
|
||||
|
||||
// only compatible for RGB FrameBuffers
|
||||
if (use_alpha_ || use_multi_sampling_)
|
||||
return img;
|
||||
|
||||
// allocate image
|
||||
img = new FrameBufferImage(attrib_.viewport.x, attrib_.viewport.y);
|
||||
|
||||
@@ -367,22 +425,49 @@ bool FrameBuffer::fill(FrameBufferImage *image)
|
||||
if (!framebufferid_)
|
||||
init();
|
||||
|
||||
// not compatible for RGB
|
||||
// only compatible for RGB FrameBuffers
|
||||
if (use_alpha_ || use_multi_sampling_)
|
||||
return false;
|
||||
|
||||
// invalid image
|
||||
if ( image == nullptr ||
|
||||
image->rgb==nullptr ||
|
||||
image->width !=attrib_.viewport.x ||
|
||||
image->height!=attrib_.viewport.y )
|
||||
image->width < 1 ||
|
||||
image->height < 1 )
|
||||
return false;
|
||||
|
||||
// fill texture with image
|
||||
glBindTexture(GL_TEXTURE_2D, textureid_);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height,
|
||||
GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
// is it same size ?
|
||||
if (image->width == attrib_.viewport.x && image->height == attrib_.viewport.y ) {
|
||||
// directly fill texture with image
|
||||
glBindTexture(GL_TEXTURE_2D, textureid_);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image->width, image->height,
|
||||
GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
else {
|
||||
uint textureimage, framebufferimage;
|
||||
// generate texture
|
||||
glGenTextures(1, &textureimage);
|
||||
glBindTexture(GL_TEXTURE_2D, textureimage);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, image->width, image->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->rgb);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// create a framebuffer object
|
||||
glGenFramebuffers(1, &framebufferimage);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebufferimage);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureimage, 0);
|
||||
|
||||
// blit to the frame buffer object with interpolation
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebufferimage);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferid_);
|
||||
glBlitFramebuffer(0, 0, image->width, image->height,
|
||||
0, 0, attrib_.viewport.x, attrib_.viewport.y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
// cleanup
|
||||
glDeleteFramebuffers(1, &framebufferimage);
|
||||
glDeleteTextures(1, &textureimage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
|
||||
FrameBufferImage(int w, int h);
|
||||
FrameBufferImage(jpegBuffer jpgimg);
|
||||
FrameBufferImage(const std::string &filename);
|
||||
// non assignable class
|
||||
FrameBufferImage(FrameBufferImage const&) = delete;
|
||||
FrameBufferImage& operator=(FrameBufferImage const&) = delete;
|
||||
@@ -39,8 +40,8 @@ public:
|
||||
// size descriptions
|
||||
static const char* aspect_ratio_name[5];
|
||||
static glm::vec2 aspect_ratio_size[5];
|
||||
static const char* resolution_name[4];
|
||||
static float resolution_height[4];
|
||||
static const char* resolution_name[5];
|
||||
static float resolution_height[5];
|
||||
static glm::vec3 getResolutionFromParameters(int ar, int h);
|
||||
static glm::ivec2 getParametersFromResolution(glm::vec3 res);
|
||||
// unbind any framebuffer object
|
||||
@@ -69,6 +70,7 @@ public:
|
||||
inline uint width() const { return attrib_.viewport.x; }
|
||||
inline uint height() const { return attrib_.viewport.y; }
|
||||
glm::vec3 resolution() const;
|
||||
void resize(int width, int height);
|
||||
float aspectRatio() const;
|
||||
std::string info() const;
|
||||
|
||||
@@ -78,6 +80,7 @@ public:
|
||||
void setProjectionArea(glm::vec2 c);
|
||||
|
||||
// internal pixel format
|
||||
inline uint opengl_id() const { return framebufferid_; }
|
||||
inline bool use_alpha() const { return use_alpha_; }
|
||||
inline bool use_multisampling() const { return use_multi_sampling_; }
|
||||
|
||||
|
||||
310
FrameGrabber.cpp
310
FrameGrabber.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
// Desktop OpenGL function loader
|
||||
@@ -41,12 +60,31 @@ void FrameGrabbing::add(FrameGrabber *rec)
|
||||
grabbers_.push_back(rec);
|
||||
}
|
||||
|
||||
void FrameGrabbing::chain(FrameGrabber *rec, FrameGrabber *next_rec)
|
||||
{
|
||||
if (rec != nullptr && next_rec != nullptr)
|
||||
{
|
||||
// add grabber if not yet
|
||||
if ( std::find(grabbers_.begin(), grabbers_.end(), rec) == grabbers_.end() )
|
||||
grabbers_.push_back(rec);
|
||||
|
||||
grabbers_chain_[next_rec] = rec;
|
||||
}
|
||||
}
|
||||
|
||||
void FrameGrabbing::verify(FrameGrabber **rec)
|
||||
{
|
||||
if ( std::find(grabbers_.begin(), grabbers_.end(), *rec) == grabbers_.end() &&
|
||||
grabbers_chain_.find(*rec) == grabbers_chain_.end() )
|
||||
*rec = nullptr;
|
||||
}
|
||||
|
||||
FrameGrabber *FrameGrabbing::front()
|
||||
{
|
||||
if (grabbers_.empty())
|
||||
return nullptr;
|
||||
else
|
||||
return grabbers_.front();
|
||||
|
||||
return grabbers_.front();
|
||||
}
|
||||
|
||||
struct fgId: public std::unary_function<FrameGrabber*, bool>
|
||||
@@ -85,13 +123,17 @@ void FrameGrabbing::clearAll()
|
||||
{
|
||||
FrameGrabber *rec = *iter;
|
||||
rec->stop();
|
||||
iter = grabbers_.erase(iter);
|
||||
delete rec;
|
||||
if (rec->finished()) {
|
||||
iter = grabbers_.erase(iter);
|
||||
delete rec;
|
||||
}
|
||||
else
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
|
||||
void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer)
|
||||
{
|
||||
if (frame_buffer == nullptr)
|
||||
return;
|
||||
@@ -128,7 +170,6 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
|
||||
"format", G_TYPE_STRING, use_alpha_ ? "RGBA" : "RGB",
|
||||
"width", G_TYPE_INT, width_,
|
||||
"height", G_TYPE_INT, height_,
|
||||
"framerate", GST_TYPE_FRACTION, 30, 1,
|
||||
NULL);
|
||||
}
|
||||
|
||||
@@ -181,15 +222,16 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
|
||||
pbo_index_ = (pbo_index_ + 1) % 2;
|
||||
|
||||
// a frame was successfully grabbed
|
||||
if (buffer != nullptr) {
|
||||
if ( buffer != nullptr && gst_buffer_get_size(buffer) > 0) {
|
||||
|
||||
// give the frame to all recorders
|
||||
std::list<FrameGrabber *>::iterator iter = grabbers_.begin();
|
||||
while (iter != grabbers_.end())
|
||||
{
|
||||
FrameGrabber *rec = *iter;
|
||||
rec->addFrame(buffer, caps_, dt);
|
||||
rec->addFrame(buffer, caps_);
|
||||
|
||||
// remove finished recorders
|
||||
if (rec->finished()) {
|
||||
iter = grabbers_.erase(iter);
|
||||
delete rec;
|
||||
@@ -198,6 +240,27 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
|
||||
++iter;
|
||||
}
|
||||
|
||||
// manage the list of chainned recorder
|
||||
std::map<FrameGrabber *, FrameGrabber *>::iterator chain = grabbers_chain_.begin();
|
||||
while (chain != grabbers_chain_.end())
|
||||
{
|
||||
// update frame grabber of chain list
|
||||
chain->first->addFrame(buffer, caps_);
|
||||
|
||||
// if the chained recorder is now active
|
||||
if (chain->first->active_ && chain->first->accept_buffer_){
|
||||
// add it to main grabbers,
|
||||
grabbers_.push_back(chain->first);
|
||||
// stop the replaced grabber
|
||||
chain->second->stop();
|
||||
// loop in chain list: done with this chain
|
||||
chain = grabbers_chain_.erase(chain);
|
||||
}
|
||||
else
|
||||
// loop in chain list
|
||||
++chain;
|
||||
}
|
||||
|
||||
// unref / free the frame
|
||||
gst_buffer_unref(buffer);
|
||||
}
|
||||
@@ -208,14 +271,14 @@ void FrameGrabbing::grabFrame(FrameBuffer *frame_buffer, float dt)
|
||||
|
||||
|
||||
|
||||
FrameGrabber::FrameGrabber(): finished_(false), active_(false), accept_buffer_(false),
|
||||
pipeline_(nullptr), src_(nullptr), caps_(nullptr), timestamp_(0)
|
||||
FrameGrabber::FrameGrabber(): finished_(false), initialized_(false), active_(false), endofstream_(false), accept_buffer_(false), buffering_full_(false),
|
||||
pipeline_(nullptr), src_(nullptr), caps_(nullptr), timer_(nullptr), timer_firstframe_(0),
|
||||
timestamp_(0), duration_(0), frame_count_(0), buffering_size_(MIN_BUFFER_SIZE), timestamp_on_clock_(false)
|
||||
{
|
||||
// unique id
|
||||
id_ = BaseToolkit::uniqueId();
|
||||
// configure fix parameter
|
||||
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 30); // 30 FPS
|
||||
timeframe_ = 2 * frame_duration_;
|
||||
// configure default parameter
|
||||
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, DEFAULT_GRABBER_FPS); // 25 FPS by default
|
||||
}
|
||||
|
||||
FrameGrabber::~FrameGrabber()
|
||||
@@ -224,8 +287,13 @@ FrameGrabber::~FrameGrabber()
|
||||
gst_object_unref (src_);
|
||||
if (caps_ != nullptr)
|
||||
gst_caps_unref (caps_);
|
||||
if (timer_)
|
||||
gst_object_unref (timer_);
|
||||
|
||||
if (pipeline_ != nullptr) {
|
||||
gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||||
GstState state = GST_STATE_NULL;
|
||||
gst_element_set_state (pipeline_, state);
|
||||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||||
gst_object_unref (pipeline_);
|
||||
}
|
||||
}
|
||||
@@ -243,24 +311,28 @@ bool FrameGrabber::busy() const
|
||||
return false;
|
||||
}
|
||||
|
||||
double FrameGrabber::duration() const
|
||||
uint64_t FrameGrabber::duration() const
|
||||
{
|
||||
return gst_guint64_to_gdouble( GST_TIME_AS_MSECONDS(timestamp_) ) / 1000.0;
|
||||
return GST_TIME_AS_MSECONDS(duration_);
|
||||
}
|
||||
|
||||
void FrameGrabber::stop ()
|
||||
{
|
||||
// send end of stream
|
||||
gst_app_src_end_of_stream (src_);
|
||||
// TODO if not initialized wait for initializer
|
||||
|
||||
// stop recording
|
||||
active_ = false;
|
||||
|
||||
// send end of stream
|
||||
gst_app_src_end_of_stream (src_);
|
||||
}
|
||||
|
||||
std::string FrameGrabber::info() const
|
||||
{
|
||||
if (!initialized_)
|
||||
return "Initializing";
|
||||
if (active_)
|
||||
return GstToolkit::time_to_string(timestamp_);
|
||||
return GstToolkit::time_to_string(duration_);
|
||||
else
|
||||
return "Inactive";
|
||||
}
|
||||
@@ -277,83 +349,163 @@ void FrameGrabber::callback_need_data (GstAppSrc *, guint , gpointer p)
|
||||
void FrameGrabber::callback_enough_data (GstAppSrc *, gpointer p)
|
||||
{
|
||||
FrameGrabber *grabber = static_cast<FrameGrabber *>(p);
|
||||
if (grabber)
|
||||
if (grabber) {
|
||||
grabber->accept_buffer_ = false;
|
||||
#ifndef NDEBUG
|
||||
Log::Info("Frame capture : Buffer full");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void FrameGrabber::addFrame (GstBuffer *buffer, GstCaps *caps, float dt)
|
||||
GstPadProbeReturn FrameGrabber::callback_event_probe(GstPad *, GstPadProbeInfo * info, gpointer p)
|
||||
{
|
||||
GstEvent *event = GST_PAD_PROBE_INFO_EVENT(info);
|
||||
if (GST_EVENT_TYPE (event) == GST_EVENT_EOS)
|
||||
{
|
||||
FrameGrabber *grabber = static_cast<FrameGrabber *>(p);
|
||||
if (grabber)
|
||||
grabber->endofstream_ = true;
|
||||
}
|
||||
|
||||
return GST_PAD_PROBE_OK;
|
||||
}
|
||||
|
||||
|
||||
std::string FrameGrabber::initialize(FrameGrabber *rec, GstCaps *caps)
|
||||
{
|
||||
return rec->init(caps);
|
||||
}
|
||||
|
||||
void FrameGrabber::addFrame (GstBuffer *buffer, GstCaps *caps)
|
||||
{
|
||||
// ignore
|
||||
if (buffer == nullptr)
|
||||
return;
|
||||
|
||||
// first time initialization
|
||||
if (pipeline_ == nullptr)
|
||||
init(caps);
|
||||
|
||||
// cancel if finished
|
||||
if (finished_)
|
||||
return;
|
||||
|
||||
// stop if an incompatilble frame buffer given
|
||||
if ( !gst_caps_is_equal( caps_, caps ))
|
||||
{
|
||||
stop();
|
||||
// Log::Warning("FrameGrabber interrupted: new session (%s)\nincompatible with recording (%s)", gst_caps_to_string(frame.caps), gst_caps_to_string(caps_));
|
||||
Log::Warning("FrameGrabber interrupted because the resolution changed.");
|
||||
if (pipeline_ == nullptr) {
|
||||
initializer_ = std::async( FrameGrabber::initialize, this, caps);
|
||||
}
|
||||
|
||||
// store a frame if recording is active
|
||||
if (active_)
|
||||
{
|
||||
// calculate dt in ns
|
||||
timeframe_ += gst_gdouble_to_guint64( dt * 1000000.f );
|
||||
|
||||
// if time is passed one frame duration (with 10% margin)
|
||||
// and if the encoder accepts data
|
||||
if ( timeframe_ > frame_duration_ - 3000000 && accept_buffer_) {
|
||||
|
||||
// set timing of buffer
|
||||
buffer->pts = timestamp_;
|
||||
buffer->duration = frame_duration_;
|
||||
|
||||
// increment ref counter to make sure the frame remains available
|
||||
gst_buffer_ref(buffer);
|
||||
|
||||
// push
|
||||
gst_app_src_push_buffer (src_, buffer);
|
||||
// NB: buffer will be unrefed by the appsrc
|
||||
|
||||
accept_buffer_ = false;
|
||||
|
||||
// next timestamp
|
||||
timestamp_ += frame_duration_;
|
||||
|
||||
// restart frame counter
|
||||
timeframe_ = 0;
|
||||
}
|
||||
}
|
||||
// did the recording terminate with sink receiving end-of-stream ?
|
||||
else {
|
||||
|
||||
if (!finished_)
|
||||
// initializer ongoing in separate thread
|
||||
if (initializer_.valid()) {
|
||||
// try to get info from initializer
|
||||
if (initializer_.wait_for( std::chrono::milliseconds(4) ) == std::future_status::ready )
|
||||
{
|
||||
// Wait for EOS message
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
|
||||
GstMessage *msg = gst_bus_poll(bus, GST_MESSAGE_EOS, GST_TIME_AS_USECONDS(1));
|
||||
// received EOS
|
||||
if (msg) {
|
||||
// stop the pipeline
|
||||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
Log::Warning("FrameGrabber Could not stop.");
|
||||
// done initialization
|
||||
std::string msg = initializer_.get();
|
||||
|
||||
// if initialization succeeded
|
||||
if (initialized_) {
|
||||
// attach EOS detector
|
||||
GstPad *pad = gst_element_get_static_pad (gst_bin_get_by_name (GST_BIN (pipeline_), "sink"), "sink");
|
||||
gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, FrameGrabber::callback_event_probe, this, NULL);
|
||||
gst_object_unref (pad);
|
||||
// start recording
|
||||
active_ = true;
|
||||
// inform
|
||||
Log::Info("%s", msg.c_str());
|
||||
}
|
||||
// else show warning
|
||||
else {
|
||||
// end frame grabber
|
||||
finished_ = true;
|
||||
// inform
|
||||
Log::Warning("%s", msg.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (finished_)
|
||||
terminate();
|
||||
// stop if an incompatilble frame buffer given after initialization
|
||||
if (initialized_ && !gst_caps_is_subset( caps_, caps ))
|
||||
{
|
||||
stop();
|
||||
Log::Warning("Frame capture interrupted because the resolution changed.");
|
||||
}
|
||||
|
||||
// store a frame if recording is active and if the encoder accepts data
|
||||
if (active_)
|
||||
{
|
||||
if (accept_buffer_) {
|
||||
GstClockTime t = 0;
|
||||
|
||||
// initialize timer on first occurence
|
||||
if (timer_ == nullptr) {
|
||||
timer_ = gst_pipeline_get_clock ( GST_PIPELINE(pipeline_) );
|
||||
timer_firstframe_ = gst_clock_get_time(timer_);
|
||||
}
|
||||
else
|
||||
// time since timer starts (first frame registered)
|
||||
t = gst_clock_get_time(timer_) - timer_firstframe_;
|
||||
|
||||
// if time is zero (first frame) or if delta time is passed one frame duration (with a margin)
|
||||
if ( t == 0 || (t - duration_) > (frame_duration_ - 3000) ) {
|
||||
|
||||
// count frames
|
||||
frame_count_++;
|
||||
|
||||
// set duration to an exact multiples of frame duration
|
||||
duration_ = ( t / frame_duration_) * frame_duration_;
|
||||
|
||||
if (timestamp_on_clock_)
|
||||
// automatic frame presentation time stamp
|
||||
// set time to actual time
|
||||
// & round t to a multiples of frame duration
|
||||
timestamp_ = duration_;
|
||||
else {
|
||||
// monotonic time increment to keep fixed FPS
|
||||
timestamp_ += frame_duration_;
|
||||
// force frame presentation time stamp
|
||||
buffer->pts = timestamp_;
|
||||
// set frame duration
|
||||
buffer->duration = frame_duration_;
|
||||
}
|
||||
|
||||
// when buffering is (almost) full, refuse buffer 1 frame over 2
|
||||
if (buffering_full_)
|
||||
accept_buffer_ = frame_count_%2;
|
||||
else
|
||||
{
|
||||
// enter buffering_full_ mode if the space left in buffering is for only few frames
|
||||
// (this prevents filling the buffer entirely)
|
||||
if ( buffering_size_ - gst_app_src_get_current_level_bytes(src_) < MIN_BUFFER_SIZE ) {
|
||||
#ifndef NDEBUG
|
||||
Log::Info("Frame capture : Using %s of %s Buffer.",
|
||||
BaseToolkit::byte_to_string(gst_app_src_get_current_level_bytes(src_)).c_str(),
|
||||
BaseToolkit::byte_to_string(buffering_size_).c_str());
|
||||
#endif
|
||||
buffering_full_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// increment ref counter to make sure the frame remains available
|
||||
gst_buffer_ref(buffer);
|
||||
|
||||
// push frame
|
||||
gst_app_src_push_buffer (src_, buffer);
|
||||
// NB: buffer will be unrefed by the appsrc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we received and end of stream (from callback_event_probe)
|
||||
if (endofstream_)
|
||||
{
|
||||
// try to stop properly when interrupted
|
||||
if (active_) {
|
||||
// de-activate and re-send EOS
|
||||
stop();
|
||||
// inform
|
||||
Log::Info("Frame capture : Unnexpected EOF signal (no space left on drive? File deleted?)");
|
||||
Log::Warning("Frame capture : Failed after %s.", GstToolkit::time_to_string(duration_, GstToolkit::TIME_STRING_READABLE).c_str());
|
||||
}
|
||||
// terminate properly if finished
|
||||
else
|
||||
{
|
||||
terminate();
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#define FRAMEGRABBER_H
|
||||
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <gst/gst.h>
|
||||
@@ -13,6 +15,8 @@
|
||||
// read pixels & pbo should be the fastest
|
||||
// https://stackoverflow.com/questions/38140527/glreadpixels-vs-glgetteximage
|
||||
#define USE_GLREADPIXEL
|
||||
#define DEFAULT_GRABBER_FPS 30
|
||||
#define MIN_BUFFER_SIZE 33177600 // 33177600 bytes = 1 frames 4K, 9 frames 720p
|
||||
|
||||
class FrameBuffer;
|
||||
|
||||
@@ -39,36 +43,49 @@ public:
|
||||
|
||||
virtual void stop();
|
||||
virtual std::string info() const;
|
||||
virtual double duration() const;
|
||||
virtual uint64_t duration() const;
|
||||
virtual bool finished() const;
|
||||
virtual bool busy() const;
|
||||
|
||||
protected:
|
||||
|
||||
// only FrameGrabbing manager can add frame
|
||||
virtual void addFrame(GstBuffer *buffer, GstCaps *caps, float dt);
|
||||
virtual void addFrame(GstBuffer *buffer, GstCaps *caps);
|
||||
|
||||
// only addFrame method shall call those
|
||||
virtual void init(GstCaps *caps) = 0;
|
||||
virtual std::string init(GstCaps *caps) = 0;
|
||||
virtual void terminate() = 0;
|
||||
|
||||
// thread-safe testing termination
|
||||
std::atomic<bool> finished_;
|
||||
std::atomic<bool> initialized_;
|
||||
std::atomic<bool> active_;
|
||||
std::atomic<bool> endofstream_;
|
||||
std::atomic<bool> accept_buffer_;
|
||||
std::atomic<bool> buffering_full_;
|
||||
|
||||
// gstreamer pipeline
|
||||
GstElement *pipeline_;
|
||||
GstAppSrc *src_;
|
||||
GstCaps *caps_;
|
||||
GstClockTime timeframe_;
|
||||
|
||||
GstClock *timer_;
|
||||
GstClockTime timer_firstframe_;
|
||||
GstClockTime timestamp_;
|
||||
GstClockTime duration_;
|
||||
GstClockTime frame_duration_;
|
||||
guint64 frame_count_;
|
||||
guint64 buffering_size_;
|
||||
bool timestamp_on_clock_;
|
||||
|
||||
// async threaded initializer
|
||||
std::future<std::string> initializer_;
|
||||
static std::string initialize(FrameGrabber *rec, GstCaps *caps);
|
||||
|
||||
// gstreamer callbacks
|
||||
static void callback_need_data (GstAppSrc *, guint, gpointer user_data);
|
||||
static void callback_enough_data (GstAppSrc *, gpointer user_data);
|
||||
|
||||
static void callback_enough_data (GstAppSrc *, gpointer user_data);
|
||||
static GstPadProbeReturn callback_event_probe(GstPad *, GstPadProbeInfo *info, gpointer user_data);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -100,6 +117,8 @@ public:
|
||||
inline uint height() const { return height_; }
|
||||
|
||||
void add(FrameGrabber *rec);
|
||||
void chain(FrameGrabber *rec, FrameGrabber *new_rec);
|
||||
void verify(FrameGrabber **rec);
|
||||
FrameGrabber *front();
|
||||
FrameGrabber *get(uint64_t id);
|
||||
void stopAll();
|
||||
@@ -108,10 +127,11 @@ public:
|
||||
protected:
|
||||
|
||||
// only for friend Session
|
||||
void grabFrame(FrameBuffer *frame_buffer, float dt);
|
||||
void grabFrame(FrameBuffer *frame_buffer);
|
||||
|
||||
private:
|
||||
std::list<FrameGrabber *> grabbers_;
|
||||
std::map<FrameGrabber *, FrameGrabber *> grabbers_chain_;
|
||||
guint pbo_[2];
|
||||
guint pbo_index_;
|
||||
guint pbo_next_index_;
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - Live video mixer
|
||||
*
|
||||
* **Copyright** (C) 2020-2021 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
116
GeometryView.cpp
116
GeometryView.cpp
@@ -1,18 +1,34 @@
|
||||
// Opengl
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiToolkit.h"
|
||||
|
||||
// memmove
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "Mixer.h"
|
||||
#include "defines.h"
|
||||
#include "Source.h"
|
||||
@@ -170,7 +186,7 @@ void GeometryView::resize ( int scale )
|
||||
scene.root()->scale_.y = z;
|
||||
|
||||
// Clamp translation to acceptable area
|
||||
glm::vec3 border(scene.root()->scale_.x * 1.5, scene.root()->scale_.y * 1.5, 0.f);
|
||||
glm::vec3 border(2.f * Mixer::manager().session()->frame()->aspectRatio(), 2.f, 0.f);
|
||||
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border);
|
||||
}
|
||||
|
||||
@@ -224,7 +240,7 @@ void GeometryView::draw()
|
||||
scene.accept(draw_overlays);
|
||||
|
||||
// 4. Draw control overlays of current source on top (if selectable)
|
||||
if (canSelect(s)) {
|
||||
if (s!=nullptr && canSelect(s)) {
|
||||
s->setMode(Source::CURRENT);
|
||||
DrawVisitor dv(s->overlays_[mode_], projection);
|
||||
scene.accept(dv);
|
||||
@@ -411,39 +427,40 @@ std::pair<Node *, glm::vec2> GeometryView::pick(glm::vec2 P)
|
||||
if (current->workspace() != Settings::application.current_workspace){
|
||||
current = nullptr;
|
||||
}
|
||||
|
||||
// find if the current source was picked
|
||||
auto itp = pv.rbegin();
|
||||
for (; itp != pv.rend(); ++itp){
|
||||
// test if source contains this node
|
||||
Source::hasNode is_in_source((*itp).first );
|
||||
if ( is_in_source( current ) ){
|
||||
// a node in the current source was clicked !
|
||||
pick = *itp;
|
||||
break;
|
||||
else {
|
||||
// find if the current source was picked
|
||||
auto itp = pv.rbegin();
|
||||
for (; itp != pv.rend(); ++itp){
|
||||
// test if source contains this node
|
||||
Source::hasNode is_in_source((*itp).first );
|
||||
if ( is_in_source( current ) ){
|
||||
// a node in the current source was clicked !
|
||||
pick = *itp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// not found: the current source was not clicked
|
||||
if (itp == pv.rend()) {
|
||||
current = nullptr;
|
||||
}
|
||||
// picking on the menu handle: show context menu
|
||||
else if ( pick.first == current->handles_[mode_][Handles::MENU] ) {
|
||||
openContextMenu(MENU_SOURCE);
|
||||
}
|
||||
// pick on the lock icon; unlock source
|
||||
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->lock_ ) {
|
||||
lock(current, false);
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
// pick on the open lock icon; lock source and cancel pick
|
||||
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->unlock_ ) {
|
||||
lock(current, true);
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
// pick a locked source ; cancel pick
|
||||
else if ( current->locked() ) {
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
}
|
||||
// not found: the current source was not clicked
|
||||
if (itp == pv.rend()) {
|
||||
current = nullptr;
|
||||
}
|
||||
// picking on the menu handle: show context menu
|
||||
else if ( pick.first == current->handles_[mode_][Handles::MENU] ) {
|
||||
openContextMenu(MENU_SOURCE);
|
||||
}
|
||||
// pick on the lock icon; unlock source
|
||||
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->lock_ ) {
|
||||
lock(current, false);
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
// pick on the open lock icon; lock source and cancel pick
|
||||
else if ( UserInterface::manager().ctrlModifier() && pick.first == current->unlock_ ) {
|
||||
lock(current, true);
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
// pick a locked source ; cancel pick
|
||||
else if ( current->locked() ) {
|
||||
pick = { nullptr, glm::vec2(0.f) };
|
||||
}
|
||||
}
|
||||
// the clicked source changed (not the current source)
|
||||
@@ -516,7 +533,7 @@ std::pair<Node *, glm::vec2> GeometryView::pick(glm::vec2 P)
|
||||
|
||||
bool GeometryView::canSelect(Source *s) {
|
||||
|
||||
return ( View::canSelect(s) && s->ready() && s->active() && s->workspace() == Settings::application.current_workspace);
|
||||
return ( s!=nullptr && View::canSelect(s) && s->ready() && s->active() && s->workspace() == Settings::application.current_workspace);
|
||||
}
|
||||
|
||||
|
||||
@@ -927,10 +944,10 @@ View::Cursor GeometryView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::p
|
||||
sourceNode->rotation_.z = glm::radians( float(degrees) );
|
||||
overlay_rotation_clock_->visible_ = true;
|
||||
overlay_rotation_clock_->copyTransform(overlay_rotation_);
|
||||
info << "Angle " << degrees << "\u00b0"; // degree symbol
|
||||
info << "Angle " << degrees << UNICODE_DEGREE;
|
||||
}
|
||||
else
|
||||
info << "Angle " << std::fixed << std::setprecision(1) << glm::degrees(sourceNode->rotation_.z) << "\u00b0"; // degree symbol
|
||||
info << "Angle " << std::fixed << std::setprecision(1) << glm::degrees(sourceNode->rotation_.z) << UNICODE_DEGREE;
|
||||
|
||||
overlay_rotation_clock_hand_->visible_ = true;
|
||||
overlay_rotation_clock_hand_->translation_.x = s->stored_status_->translation_.x;
|
||||
@@ -1033,8 +1050,8 @@ void GeometryView::terminate()
|
||||
|
||||
void GeometryView::arrow (glm::vec2 movement)
|
||||
{
|
||||
static int accumulator = 0;
|
||||
accumulator++;
|
||||
static float accumulator = 0.f;
|
||||
accumulator += dt_;
|
||||
|
||||
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
|
||||
glm::vec3 gl_Position_to = Rendering::manager().unProject(movement, scene.root()->transform_);
|
||||
@@ -1057,18 +1074,19 @@ void GeometryView::arrow (glm::vec2 movement)
|
||||
|
||||
// + ALT : discrete displacement
|
||||
if (UserInterface::manager().altModifier()) {
|
||||
if (accumulator > 10) {
|
||||
if (accumulator > 100.f) {
|
||||
dest_translation += glm::sign(gl_delta) * 0.11f;
|
||||
dest_translation.x = ROUND(dest_translation.x, 10.f);
|
||||
dest_translation.y = ROUND(dest_translation.y, 10.f);
|
||||
accumulator = 0;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// normal case: dest += delta
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
|
||||
// store action in history
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "GlmToolkit.h"
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
#include <glm/gtc/random.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include "GlmToolkit.h"
|
||||
|
||||
|
||||
glm::mat4 GlmToolkit::transform(glm::vec3 translation, glm::vec3 rotation, glm::vec3 scale)
|
||||
{
|
||||
@@ -57,6 +76,16 @@ GlmToolkit::AxisAlignedBoundingBox::AxisAlignedBoundingBox() :
|
||||
{
|
||||
}
|
||||
|
||||
GlmToolkit::AxisAlignedBoundingBox::AxisAlignedBoundingBox(const GlmToolkit::AxisAlignedBoundingBox &D) :
|
||||
mMin(D.mMin), mMax(D.mMax)
|
||||
{
|
||||
}
|
||||
|
||||
void GlmToolkit::AxisAlignedBoundingBox::operator = (const GlmToolkit::AxisAlignedBoundingBox &D ) {
|
||||
mMin = D.mMin;
|
||||
mMax = D.mMax;
|
||||
}
|
||||
|
||||
void GlmToolkit::AxisAlignedBoundingBox::extend(const glm::vec3& point)
|
||||
{
|
||||
if (isNull()) {
|
||||
|
||||
@@ -15,11 +15,8 @@ class AxisAlignedBoundingBox
|
||||
{
|
||||
public:
|
||||
AxisAlignedBoundingBox();
|
||||
|
||||
inline void operator = (const AxisAlignedBoundingBox &D ) {
|
||||
mMin = D.mMin;
|
||||
mMax = D.mMax;
|
||||
}
|
||||
AxisAlignedBoundingBox(const AxisAlignedBoundingBox &D);
|
||||
void operator = (const AxisAlignedBoundingBox &D );
|
||||
|
||||
// test
|
||||
inline bool isNull() const { return mMin.x > mMax.x || mMin.y > mMax.y || mMin.z > mMax.z;}
|
||||
|
||||
135
GstToolkit.cpp
135
GstToolkit.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
using namespace std;
|
||||
@@ -14,6 +33,8 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
|
||||
return "00:00:00.00";
|
||||
case TIME_STRING_MINIMAL:
|
||||
return "0.0";
|
||||
case TIME_STRING_READABLE:
|
||||
return "0 second";
|
||||
default:
|
||||
return "00.00";
|
||||
}
|
||||
@@ -23,23 +44,52 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
|
||||
guint s = ms / 1000;
|
||||
ostringstream oss;
|
||||
|
||||
// MINIMAL: keep only the 2 higher values (most significant)
|
||||
if (m == TIME_STRING_MINIMAL) {
|
||||
// READABLE : long format
|
||||
if (m == TIME_STRING_READABLE) {
|
||||
int count = 0;
|
||||
if (s / 3600) {
|
||||
oss << s / 3600 << ':';
|
||||
oss << s / 3600 << " h ";
|
||||
count++;
|
||||
}
|
||||
if ((s % 3600) / 60) {
|
||||
oss << (s % 3600) / 60 << ':';
|
||||
oss << (s % 3600) / 60 << " min ";
|
||||
count++;
|
||||
}
|
||||
if (count < 2) {
|
||||
oss << setw(count > 0 ? 2 : 1) << setfill('0') << (s % 3600) % 60;
|
||||
count++;
|
||||
|
||||
if (count < 2 )
|
||||
oss << '.'<< setw(1) << setfill('0') << (ms % 1000) / 100 << " sec";
|
||||
else
|
||||
oss << " s";
|
||||
}
|
||||
if (count < 2 )
|
||||
oss << '.'<< setw(1) << setfill('0') << (ms % 1000) / 10;
|
||||
}
|
||||
// MINIMAL: keep only the 2 higher values (most significant)
|
||||
else if (m == TIME_STRING_MINIMAL) {
|
||||
int count = 0;
|
||||
// hours
|
||||
if (s / 3600) {
|
||||
oss << s / 3600 << ':';
|
||||
count++;
|
||||
}
|
||||
// minutes
|
||||
if (count > 0) {
|
||||
oss << setw(2) << setfill('0') << (s % 3600) / 60 << ':';
|
||||
count++;
|
||||
}
|
||||
else if ((s % 3600) / 60)
|
||||
{
|
||||
oss << (s % 3600) / 60 << ':';
|
||||
count++;
|
||||
}
|
||||
// seconds
|
||||
{
|
||||
oss << setw(count > 0 ? 2 : 1) << setfill('0') << (s % 3600) % 60;
|
||||
count++;
|
||||
}
|
||||
if (count < 2)
|
||||
oss << '.'<< setw((ms % 1000) / 100 ? 2 : 1) << setfill('0') << (ms % 1000) / 10;
|
||||
}
|
||||
else {
|
||||
// TIME_STRING_FIXED : fixed length string (11 chars) HH:mm:ss.ii"
|
||||
@@ -58,11 +108,13 @@ string GstToolkit::time_to_string(guint64 t, time_string_mode m)
|
||||
|
||||
std::string GstToolkit::filename_to_uri(std::string path)
|
||||
{
|
||||
if (path.empty())
|
||||
return path;
|
||||
|
||||
// set uri to open
|
||||
gchar *uritmp = gst_filename_to_uri(path.c_str(), NULL);
|
||||
std::string uri( uritmp );
|
||||
g_free(uritmp);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -119,6 +171,31 @@ bool GstToolkit::enable_feature (string name, bool enable) {
|
||||
}
|
||||
|
||||
gst_registry_add_feature (registry, GST_PLUGIN_FEATURE (factory));
|
||||
gst_object_unref (factory);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GstToolkit::has_feature (string name)
|
||||
{
|
||||
if (name.empty())
|
||||
return false;
|
||||
|
||||
GstRegistry *registry = NULL;
|
||||
GstElementFactory *factory = NULL;
|
||||
|
||||
registry = gst_registry_get();
|
||||
if (!registry) return false;
|
||||
|
||||
factory = gst_element_factory_find (name.c_str());
|
||||
if (!factory) return false;
|
||||
|
||||
GstElement *elem = gst_element_factory_create (factory, NULL);
|
||||
gst_object_unref (factory);
|
||||
|
||||
if (!elem) return false;
|
||||
|
||||
gst_object_unref (elem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,11 +213,11 @@ string GstToolkit::gst_version()
|
||||
|
||||
#if GST_GL_HAVE_PLATFORM_GLX
|
||||
// https://gstreamer.freedesktop.org/documentation/nvcodec/index.html?gi-language=c#plugin-nvcodec
|
||||
const char *plugins[10] = { "omxmpeg4videodec", "omxmpeg2dec", "omxh264dec", "vdpaumpegdec",
|
||||
"nvh264dec", "nvh265dec", "nvmpeg2videodec",
|
||||
"nvmpeg4videodec", "nvvp8dec", "nvvp9dec"
|
||||
};
|
||||
const int N = 10;
|
||||
// list ordered with higher priority first (e.g. nvidia proprietary before vaapi)
|
||||
const char *plugins[11] = { "nvh264dec", "nvh265dec", "nvmpeg2videodec", "nvmpeg4videodec", "nvvp8dec", "nvvp9dec",
|
||||
"vaapidecodebin", "omxmpeg4videodec", "omxmpeg2dec", "omxh264dec", "vdpaumpegdec",
|
||||
};
|
||||
const int N = 11;
|
||||
#elif GST_GL_HAVE_PLATFORM_CGL
|
||||
const char *plugins[2] = { "vtdec_hw", "vtdechw" };
|
||||
const int N = 2;
|
||||
@@ -166,7 +243,7 @@ std::list<std::string> GstToolkit::enable_gpu_decoding_plugins(bool enable)
|
||||
GstPluginFeature* feature = gst_registry_lookup_feature(plugins_register, plugins[i]);
|
||||
if(feature != NULL) {
|
||||
plugins_list_.push_front( string( plugins[i] ) );
|
||||
gst_plugin_feature_set_rank(feature, enable ? GST_RANK_PRIMARY + 1 : GST_RANK_MARGINAL);
|
||||
gst_plugin_feature_set_rank(feature, enable ? GST_RANK_PRIMARY + (N-i) : GST_RANK_MARGINAL);
|
||||
gst_object_unref(feature);
|
||||
}
|
||||
}
|
||||
@@ -188,12 +265,9 @@ std::string GstToolkit::used_gpu_decoding_plugins(GstElement *gstbin)
|
||||
{
|
||||
GstElement *e = static_cast<GstElement*>(g_value_peek_pointer(&value));
|
||||
if (e) {
|
||||
gchar *name = gst_element_get_name(e);
|
||||
// g_print(" - %s", name);
|
||||
std::string e_name(name);
|
||||
g_free(name);
|
||||
const gchar *name = gst_element_get_name(e);
|
||||
for (int i = 0; i < N; i++) {
|
||||
if (e_name.find(plugins[i]) != std::string::npos) {
|
||||
if (std::string(name).find(plugins[i]) != std::string::npos) {
|
||||
found = plugins[i];
|
||||
break;
|
||||
}
|
||||
@@ -207,3 +281,28 @@ std::string GstToolkit::used_gpu_decoding_plugins(GstElement *gstbin)
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::string GstToolkit::used_decoding_plugins(GstElement *gstbin)
|
||||
{
|
||||
std::string found = "";
|
||||
|
||||
GstIterator* it = gst_bin_iterate_recurse(GST_BIN(gstbin));
|
||||
GValue value = G_VALUE_INIT;
|
||||
for(GstIteratorResult r = gst_iterator_next(it, &value); r != GST_ITERATOR_DONE; r = gst_iterator_next(it, &value))
|
||||
{
|
||||
if ( r == GST_ITERATOR_OK )
|
||||
{
|
||||
GstElement *e = static_cast<GstElement*>(g_value_peek_pointer(&value));
|
||||
if (e) {
|
||||
const gchar *name = gst_element_get_name(e);
|
||||
found += std::string(name) + ", ";
|
||||
}
|
||||
}
|
||||
g_value_unset(&value);
|
||||
}
|
||||
gst_iterator_free(it);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace GstToolkit
|
||||
typedef enum {
|
||||
TIME_STRING_FIXED = 0,
|
||||
TIME_STRING_ADJUSTED,
|
||||
TIME_STRING_MINIMAL
|
||||
TIME_STRING_MINIMAL,
|
||||
TIME_STRING_READABLE
|
||||
} time_string_mode;
|
||||
|
||||
std::string time_to_string(guint64 t, time_string_mode m = TIME_STRING_ADJUSTED);
|
||||
@@ -23,8 +24,10 @@ std::string gst_version();
|
||||
std::list<std::string> all_plugins();
|
||||
std::list<std::string> enable_gpu_decoding_plugins(bool enable = true);
|
||||
std::string used_gpu_decoding_plugins(GstElement *gstbin);
|
||||
std::string used_decoding_plugins(GstElement *gstbin);
|
||||
|
||||
std::list<std::string> all_plugin_features(std::string pluginname);
|
||||
bool has_feature (std::string name);
|
||||
bool enable_feature (std::string name, bool enable);
|
||||
|
||||
}
|
||||
|
||||
987
ImGuiToolkit.cpp
987
ImGuiToolkit.cpp
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,14 @@
|
||||
|
||||
#include <glib.h>
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "rsc/fonts/IconsFontAwesome5.h"
|
||||
|
||||
namespace ImGuiToolkit
|
||||
@@ -13,30 +18,41 @@ namespace ImGuiToolkit
|
||||
// Icons from resource icon.dds
|
||||
void Icon (int i, int j, bool enabled = true);
|
||||
bool IconButton (int i, int j, const char *tooltips = nullptr);
|
||||
bool IconButton (const char* icon = ICON_FA_EXCLAMATION_CIRCLE, const char *tooltips = nullptr);
|
||||
bool IconButton (const char* icon, const char *tooltips = nullptr);
|
||||
bool IconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltips[] = nullptr);
|
||||
void ShowIconsWindow(bool* p_open);
|
||||
|
||||
// icon buttons
|
||||
// buttons and gui items with icon
|
||||
bool ButtonIcon (int i, int j, const char* tooltip = nullptr);
|
||||
bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle);
|
||||
bool ButtonIconMultistate (std::vector<std::pair<int, int> > icons, int* state);
|
||||
bool ButtonIconToggle (int i, int j, int i_toggle, int j_toggle, bool* toggle, const char *tooltip = nullptr);
|
||||
bool ButtonIconMultistate (std::vector<std::pair<int, int> > icons, int* state, const char* tooltip = nullptr);
|
||||
bool MenuItemIcon (int i, int j, const char* label, bool selected = false, bool enabled = true);
|
||||
bool SelectableIcon(const char* label, int i, int j, bool selected = false);
|
||||
bool ComboIcon (std::vector<std::pair<int, int> > icons, std::vector<std::string> labels, int* state);
|
||||
bool ComboIcon (const char* label, std::vector<std::pair<int, int> > icons, std::vector<std::string> items, int* i);
|
||||
|
||||
// utility buttons
|
||||
bool ButtonToggle (const char* label, bool* toggle);
|
||||
bool ButtonSwitch (const char* label, bool* toggle , const char *help = nullptr);
|
||||
// buttons
|
||||
bool ButtonToggle (const char* label, bool* toggle, const char *tooltip = nullptr);
|
||||
bool ButtonSwitch (const char* label, bool* toggle, const char *shortcut = nullptr);
|
||||
void ButtonOpenUrl (const char* label, const char* url, const ImVec2& size_arg = ImVec2(0,0));
|
||||
|
||||
// tooltip and mouse over help
|
||||
void setToolTipsEnabled (bool on);
|
||||
bool toolTipsEnabled ();
|
||||
void ToolTip (const char* desc, const char* shortcut = nullptr);
|
||||
void HelpMarker (const char* desc, const char* icon = ICON_FA_QUESTION_CIRCLE, const char* shortcut = nullptr);
|
||||
void HelpIcon (const char* desc, int i = 19, int j = 5, const char* shortcut = nullptr);
|
||||
void HelpToolTip(const char* desc, const char* shortcut = nullptr);
|
||||
void Indication (const char* desc, const char* icon, const char* shortcut = nullptr);
|
||||
void Indication (const char* desc, int i, int j, const char* shortcut = nullptr);
|
||||
|
||||
// utility sliders
|
||||
bool TimelineSlider (const char* label, guint64 *time, guint64 start, guint64 end, guint64 step, const float width);
|
||||
// sliders
|
||||
bool SliderTiming (const char* label, uint *ms, uint v_min, uint v_max, uint v_step, const char* text_max = nullptr);
|
||||
bool TimelineSlider (const char* label, guint64 *time, guint64 begin, guint64 first, guint64 end, guint64 step, const float width, double tempo = 0, double quantum = 0);
|
||||
void RenderTimeline (struct ImGuiWindow* window, struct ImRect timeline_bbox, guint64 begin, guint64 end, guint64 step, bool verticalflip = false);
|
||||
void RenderTimelineBPM (struct ImGuiWindow* window, struct ImRect timeline_bbox, double tempo, double quantum, guint64 begin, guint64 end, guint64 step, bool verticalflip = false);
|
||||
bool InvisibleSliderInt(const char* label, uint *index, uint min, uint max, const ImVec2 size);
|
||||
bool EditPlotLines(const char* label, float *array, int values_count, float values_min, float values_max, const ImVec2 size);
|
||||
bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, bool *released, const ImVec2 size);
|
||||
bool EditPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, guint64 begin, guint64 end, bool cut, bool *released, const ImVec2 size);
|
||||
void ShowPlotHistoLines(const char* label, float *histogram_array, float *lines_array, int values_count, float values_min, float values_max, const ImVec2 size);
|
||||
|
||||
// fonts from ressources 'fonts/'
|
||||
typedef enum {
|
||||
@@ -48,13 +64,18 @@ namespace ImGuiToolkit
|
||||
} font_style;
|
||||
void SetFont (font_style type, const std::string &ttf_font_name, int pointsize, int oversample = 2);
|
||||
void PushFont (font_style type);
|
||||
void ImageGlyph(font_style type, char c, float h = 60);
|
||||
void Spacing();
|
||||
|
||||
void WindowText(const char* window_name, ImVec2 window_pos, const char* text);
|
||||
bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text);
|
||||
void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format);
|
||||
// text input
|
||||
bool InputText(const char* label, std::string* str);
|
||||
bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0));
|
||||
void TextMultiline(const char* label, const std::string &str, float width);
|
||||
|
||||
bool InputCodeMultiline(const char* label, std::string *str, const ImVec2& size = ImVec2(0, 0), int *numline = NULL);
|
||||
void CodeMultiline(const char* label, const std::string &str, float width);
|
||||
|
||||
// color of gui items
|
||||
// accent color of UI
|
||||
typedef enum {
|
||||
ACCENT_BLUE =0,
|
||||
ACCENT_ORANGE,
|
||||
@@ -63,8 +84,11 @@ namespace ImGuiToolkit
|
||||
void SetAccentColor (accent_color color);
|
||||
struct ImVec4 HighlightColor (bool active = true);
|
||||
|
||||
bool InputText(const char* label, std::string* str);
|
||||
bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), int linesize = 0);
|
||||
// varia
|
||||
void WindowText(const char* window_name, ImVec2 window_pos, const char* text);
|
||||
bool WindowButton(const char* window_name, ImVec2 window_pos, const char* text);
|
||||
void WindowDragFloat(const char* window_name, ImVec2 window_pos, float* v, float v_speed, float v_min, float v_max, const char* format);
|
||||
|
||||
}
|
||||
|
||||
#endif // __IMGUI_TOOLKIT_H_
|
||||
|
||||
329
ImGuiVisitor.cpp
329
ImGuiVisitor.cpp
@@ -1,4 +1,22 @@
|
||||
#include "ImGuiVisitor.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
@@ -38,13 +56,14 @@
|
||||
#include "UserInterfaceManager.h"
|
||||
#include "SystemToolkit.h"
|
||||
|
||||
#include "ImGuiVisitor.h"
|
||||
|
||||
ImGuiVisitor::ImGuiVisitor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit(Node &n)
|
||||
void ImGuiVisitor::visit(Node &)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -62,7 +81,7 @@ void ImGuiVisitor::visit(Group &n)
|
||||
n.scale_.y = 1.f;
|
||||
Action::manager().store("Geometry Reset");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Geometry");
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(6, 15)) {
|
||||
@@ -70,7 +89,7 @@ void ImGuiVisitor::visit(Group &n)
|
||||
n.translation_.y = 0.f;
|
||||
Action::manager().store("Position 0.0, 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
float translation[2] = { n.translation_.x, n.translation_.y};
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if ( ImGui::SliderFloat2("Position", translation, -5.0, 5.0) )
|
||||
@@ -88,7 +107,7 @@ void ImGuiVisitor::visit(Group &n)
|
||||
n.scale_.y = 1.f;
|
||||
Action::manager().store("Scale 1.0 x 1.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
float scale[2] = { n.scale_.x, n.scale_.y} ;
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if ( ImGui::SliderFloat2("Scale", scale, -MAX_SCALE, MAX_SCALE, "%.2f") )
|
||||
@@ -106,7 +125,7 @@ void ImGuiVisitor::visit(Group &n)
|
||||
n.rotation_.z = 0.f;
|
||||
Action::manager().store("Angle 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderAngle("Angle", &(n.rotation_.z), -180.f, 180.f) ;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
@@ -147,20 +166,12 @@ void ImGuiVisitor::visit(Primitive &n)
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit(FrameBufferSurface &n)
|
||||
void ImGuiVisitor::visit(FrameBufferSurface &)
|
||||
{
|
||||
ImGui::Text("Framebuffer");
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit(MediaSurface &n)
|
||||
{
|
||||
ImGui::Text("%s", n.path().c_str());
|
||||
|
||||
if (n.mediaPlayer())
|
||||
n.mediaPlayer()->accept(*this);
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit(MediaPlayer &n)
|
||||
void ImGuiVisitor::visit(MediaPlayer &)
|
||||
{
|
||||
ImGui::Text("Media Player");
|
||||
}
|
||||
@@ -174,7 +185,7 @@ void ImGuiVisitor::visit(Shader &n)
|
||||
// n.blending = Shader::BLEND_OPACITY;
|
||||
// n.color = glm::vec4(1.f, 1.f, 1.f, 1.f);
|
||||
// }
|
||||
// ImGui::SameLine(0, 10);
|
||||
// ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
// ImGui::ColorEdit3("Color", glm::value_ptr(n.color), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel ) ;
|
||||
// ImGui::SameLine(0, 5);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
@@ -226,7 +237,7 @@ void ImGuiVisitor::visit(Shader &n)
|
||||
// // get index of the mask used in this ImageShader
|
||||
// int item_current = n.mask;
|
||||
//// if (ImGuiToolkit::ButtonIcon(10, 3)) n.mask = 0;
|
||||
//// ImGui::SameLine(0, 10);
|
||||
//// ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
// // combo list of masks
|
||||
// if ( ImGui::Combo("Mask", &item_current, ImageShader::mask_names, IM_ARRAYSIZE(ImageShader::mask_names) ) )
|
||||
@@ -246,14 +257,14 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
ImGui::PushID(std::to_string(n.id()).c_str());
|
||||
|
||||
ImGuiToolkit::Icon(6, 2);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Filters");
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(6, 4)) {
|
||||
if (ImGuiToolkit::IconButton(6, 4)) {
|
||||
n.gamma = glm::vec4(1.f, 1.f, 1.f, 1.f);
|
||||
Action::manager().store("Gamma & Color");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::ColorEdit3("Gamma Color", glm::value_ptr(n.gamma), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel) ;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
Action::manager().store("Gamma Color changed");
|
||||
@@ -270,12 +281,12 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
// ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
// ImGui::SliderFloat4("Levels", glm::value_ptr(n.levels), 0.0, 1.0);
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(5, 16)) {
|
||||
if (ImGuiToolkit::IconButton(5, 16)) {
|
||||
n.brightness = 0.f;
|
||||
n.contrast = 0.f;
|
||||
Action::manager().store("B & C 0.0 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
float bc[2] = { n.brightness, n.contrast};
|
||||
if ( ImGui::SliderFloat2("B & C", bc, -1.0, 1.0) )
|
||||
@@ -289,11 +300,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(9, 16)) {
|
||||
if (ImGuiToolkit::IconButton(9, 16)) {
|
||||
n.saturation = 0.f;
|
||||
Action::manager().store("Saturation 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderFloat("Saturation", &n.saturation, -1.0, 1.0);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
@@ -302,11 +313,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(12, 4)) {
|
||||
if (ImGuiToolkit::IconButton(12, 4)) {
|
||||
n.hueshift = 0.f;
|
||||
Action::manager().store("Hue shift 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderFloat("Hue shift", &n.hueshift, 0.0, 1.0);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
@@ -315,11 +326,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(18, 1)) {
|
||||
if (ImGuiToolkit::IconButton(18, 1)) {
|
||||
n.nbColors = 0;
|
||||
Action::manager().store("Posterize None");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderInt("Posterize", &n.nbColors, 0, 16, n.nbColors == 0 ? "None" : "%d colors");
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
@@ -329,11 +340,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(8, 1)) {
|
||||
if (ImGuiToolkit::IconButton(8, 1)) {
|
||||
n.threshold = 0.f;
|
||||
Action::manager().store("Threshold None");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderFloat("Threshold", &n.threshold, 0.0, 1.0, n.threshold < 0.001 ? "None" : "%.2f");
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
@@ -343,11 +354,11 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(3, 1)) {
|
||||
if (ImGuiToolkit::IconButton(3, 1)) {
|
||||
n.lumakey = 0.f;
|
||||
Action::manager().store("Lumakey 0.0");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
ImGui::SliderFloat("Lumakey", &n.lumakey, 0.0, 1.0);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
@@ -356,12 +367,12 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(13, 4)) {
|
||||
if (ImGuiToolkit::IconButton(13, 4)) {
|
||||
n.chromakey = glm::vec4(0.f, 0.8f, 0.f, 1.f);
|
||||
n.chromadelta = 0.f;
|
||||
Action::manager().store("Chromakey & Color Reset");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::ColorEdit3("Chroma color", glm::value_ptr(n.chromakey), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel ) ;
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
Action::manager().store("Chroma color changed");
|
||||
@@ -375,20 +386,20 @@ void ImGuiVisitor::visit(ImageProcessingShader &n)
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(6, 16)) {
|
||||
if (ImGuiToolkit::IconButton(6, 16)) {
|
||||
n.invert = 0;
|
||||
Action::manager().store("Invert None");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if (ImGui::Combo("Invert", &n.invert, "None\0Invert Color\0Invert Luminance\0"))
|
||||
Action::manager().store("Invert " + std::string(n.invert<1 ? "None": (n.invert>1 ? "Luminance" : "Color")));
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(1, 7)) {
|
||||
if (ImGuiToolkit::IconButton(1, 7)) {
|
||||
n.filterid = 0;
|
||||
Action::manager().store("Filter None");
|
||||
}
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if (ImGui::Combo("Filter", &n.filterid, ImageProcessingShader::filter_names, IM_ARRAYSIZE(ImageProcessingShader::filter_names) ) )
|
||||
Action::manager().store("Filter " + std::string(ImageProcessingShader::filter_names[n.filterid]));
|
||||
@@ -424,21 +435,21 @@ void ImGuiVisitor::visit (Source& s)
|
||||
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y ) );
|
||||
if (s.active()) {
|
||||
if (s.blendingShader()->color.a > 0.f)
|
||||
ImGuiToolkit::HelpMarker("Visible", ICON_FA_EYE);
|
||||
ImGuiToolkit::Indication("Visible", ICON_FA_EYE);
|
||||
else
|
||||
ImGuiToolkit::HelpMarker("Not visible", ICON_FA_EYE_SLASH);
|
||||
ImGuiToolkit::Indication("Not visible", ICON_FA_EYE_SLASH);
|
||||
}
|
||||
else
|
||||
ImGuiToolkit::HelpMarker("Inactive", ICON_FA_SNOWFLAKE);
|
||||
ImGuiToolkit::Indication("Inactive", ICON_FA_SNOWFLAKE);
|
||||
|
||||
// Inform on workspace
|
||||
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y + ImGui::GetFrameHeightWithSpacing()) );
|
||||
if (s.workspace() == Source::BACKGROUND)
|
||||
ImGuiToolkit::HelpIcon("in Background",10, 16);
|
||||
ImGuiToolkit::Indication("in Background",10, 16);
|
||||
else if (s.workspace() == Source::FOREGROUND)
|
||||
ImGuiToolkit::HelpIcon("in Foreground",12, 16);
|
||||
ImGuiToolkit::Indication("in Foreground",12, 16);
|
||||
else
|
||||
ImGuiToolkit::HelpIcon("in Workspace",11, 16);
|
||||
ImGuiToolkit::Indication("in Workspace",11, 16);
|
||||
|
||||
// locking
|
||||
ImGui::SetCursorPos( ImVec2(preview_width + 20, pos.y + 2.f * ImGui::GetFrameHeightWithSpacing()) );
|
||||
@@ -459,7 +470,7 @@ void ImGuiVisitor::visit (Source& s)
|
||||
// toggle enable/disable image processing
|
||||
bool on = s.imageProcessingEnabled();
|
||||
ImGui::SetCursorPos( ImVec2(preview_width + 15, pos.y + 3.5f * ImGui::GetFrameHeightWithSpacing()) );
|
||||
if ( ImGuiToolkit::ButtonToggle(ICON_FA_MAGIC, &on) ){
|
||||
if ( ImGuiToolkit::ButtonIconToggle(6, 2, 6, 2, &on, "Filters") ){
|
||||
std::ostringstream oss;
|
||||
oss << s.name() << ": " << ( on ? "Enable Filter" : "Disable Filter");
|
||||
Action::manager().store(oss.str());
|
||||
@@ -530,7 +541,7 @@ void ImGuiVisitor::visit (Source& s)
|
||||
|
||||
if (s.processingshader_link_.connected()) {
|
||||
ImGuiToolkit::Icon(6, 2);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Filters");
|
||||
Source *target = s.processingshader_link_.source();
|
||||
ImGui::Text("Following");
|
||||
@@ -547,17 +558,31 @@ void ImGuiVisitor::visit (Source& s)
|
||||
void ImGuiVisitor::visit (MediaSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
if ( s.mediaplayer()->isImage() )
|
||||
ImGui::Text("Image File");
|
||||
else
|
||||
ImGui::Text("Video File");
|
||||
|
||||
if ( ImGui::Button(IMGUI_TITLE_MEDIAPLAYER, ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
UserInterface::manager().showMediaPlayer( s.mediaplayer());
|
||||
// Media info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
// folder
|
||||
std::string path = SystemToolkit::path_filename(s.path());
|
||||
std::string label = BaseToolkit::trunc_string(path, 25);
|
||||
std::string label = BaseToolkit::truncated(path, 25);
|
||||
label = BaseToolkit::transliterate(label);
|
||||
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
|
||||
|
||||
@@ -571,36 +596,53 @@ void ImGuiVisitor::visit (SessionFileSource& s)
|
||||
return;
|
||||
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Session File");
|
||||
// ImGui::Text("%s", SystemToolkit::base_filename(s.path()).c_str());
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(3, 2)) s.session()->setFading(0.f);
|
||||
// info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
if ( ImGui::Button( ICON_FA_FILE_EXPORT " Import", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
Mixer::manager().import( &s );
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Sources");
|
||||
|
||||
if (ImGuiToolkit::ButtonIcon(3, 2)) s.session()->setFadingTarget(0.f);
|
||||
float f = s.session()->fading();
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if (ImGui::SliderFloat("Fading", &f, 0.0, 1.0, f < 0.001 ? "None" : "%.2f") )
|
||||
s.session()->setFading(f);
|
||||
s.session()->setFadingTarget(f);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()){
|
||||
std::ostringstream oss;
|
||||
oss << s.name() << ": Fading " << std::setprecision(2) << f;
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
if ( ImGui::Button( ICON_FA_FILE_UPLOAD " Open Session", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
if ( ImGui::Button( ICON_FA_FILE_UPLOAD " Open", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
Mixer::manager().set( s.detach() );
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("File");
|
||||
|
||||
std::string path = SystemToolkit::path_filename(s.path());
|
||||
std::string label = BaseToolkit::trunc_string(path, 25);
|
||||
std::string label = BaseToolkit::truncated(path, 25);
|
||||
label = BaseToolkit::transliterate(label);
|
||||
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Folder");
|
||||
|
||||
ImGui::Text("Contains %d sources.", s.session()->numSource());
|
||||
if ( ImGui::Button( ICON_FA_FILE_EXPORT " Import", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
Mixer::manager().import( &s );
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit (SessionGroupSource& s)
|
||||
@@ -609,20 +651,34 @@ void ImGuiVisitor::visit (SessionGroupSource& s)
|
||||
return;
|
||||
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Flat Sesion group");
|
||||
ImGui::Text("Contains %d sources.", s.session()->numSource());
|
||||
|
||||
// info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, 10.f + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
if ( ImGui::Button( ICON_FA_UPLOAD " Expand", ImVec2(IMGUI_RIGHT_ALIGN, 0)) ){
|
||||
Mixer::manager().import( &s );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit (RenderSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Rendering Output");
|
||||
if ( ImGui::Button(IMGUI_TITLE_PREVIEW, ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
Settings::application.widget.preview = true;
|
||||
@@ -631,7 +687,7 @@ void ImGuiVisitor::visit (RenderSource& s)
|
||||
void ImGuiVisitor::visit (CloneSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Clone");
|
||||
if ( ImGui::Button(s.origin()->name().c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
Mixer::manager().setCurrentSource(s.origin());
|
||||
@@ -642,17 +698,34 @@ void ImGuiVisitor::visit (CloneSource& s)
|
||||
void ImGuiVisitor::visit (PatternSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Pattern");
|
||||
|
||||
// stream info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if (ImGui::BeginCombo("##Patterns", Pattern::pattern_types[s.pattern()->type()].c_str()) )
|
||||
if (ImGui::BeginCombo("##Patterns", Pattern::get(s.pattern()->type()).label.c_str()) )
|
||||
{
|
||||
for (uint p = 0; p < Pattern::pattern_types.size(); ++p){
|
||||
if (ImGui::Selectable( Pattern::pattern_types[p].c_str() )) {
|
||||
for (uint p = 0; p < Pattern::count(); ++p){
|
||||
if (ImGui::Selectable( Pattern::get(p).label.c_str() )) {
|
||||
s.setPattern(p, s.pattern()->resolution());
|
||||
info.reset();
|
||||
std::ostringstream oss;
|
||||
oss << s.name() << ": Pattern " << Pattern::pattern_types[p];
|
||||
oss << s.name() << ": Pattern " << Pattern::get(p).label;
|
||||
Action::manager().store(oss.str());
|
||||
}
|
||||
}
|
||||
@@ -665,9 +738,24 @@ void ImGuiVisitor::visit (PatternSource& s)
|
||||
void ImGuiVisitor::visit (DeviceSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Device");
|
||||
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(IMGUI_RIGHT_ALIGN);
|
||||
if (ImGui::BeginCombo("##Hardware", s.device().c_str()))
|
||||
{
|
||||
@@ -675,6 +763,7 @@ void ImGuiVisitor::visit (DeviceSource& s)
|
||||
std::string namedev = Device::manager().name(d);
|
||||
if (ImGui::Selectable( namedev.c_str() )) {
|
||||
s.setDevice(namedev);
|
||||
info.reset();
|
||||
std::ostringstream oss;
|
||||
oss << s.name() << " Device " << namedev;
|
||||
Action::manager().store(oss.str());
|
||||
@@ -682,48 +771,65 @@ void ImGuiVisitor::visit (DeviceSource& s)
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
DeviceConfigSet confs = Device::manager().config( Device::manager().index(s.device().c_str()));
|
||||
if ( !confs.empty()) {
|
||||
DeviceConfig best = *confs.rbegin();
|
||||
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
|
||||
ImGui::Text("%s %s %dx%d@%.1ffps", best.stream.c_str(), best.format.c_str(), best.width, best.height, fps);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit (NetworkSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Network stream");
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(IMGUI_COLOR_STREAM, 0.9f));
|
||||
ImGui::Text("%s", s.connection().c_str());
|
||||
ImGui::PopStyleColor(1);
|
||||
NetworkStream *ns = s.networkStream();
|
||||
ImGui::Text(" - %s (%dx%d)\n - Server address %s", NetworkToolkit::protocol_name[ns->protocol()],
|
||||
ns->resolution().x, ns->resolution().y, ns->serverAddress().c_str());
|
||||
|
||||
// network info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
if ( ImGui::Button( ICON_FA_REPLY " Reconnect", ImVec2(IMGUI_RIGHT_ALIGN, 0)) )
|
||||
{
|
||||
// TODO : reload ?
|
||||
s.setConnection(s.connection());
|
||||
info.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void ImGuiVisitor::visit (MultiFileSource& s)
|
||||
{
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Images sequence");
|
||||
static int64_t id = s.id();
|
||||
static uint64_t id = 0;
|
||||
|
||||
// information text
|
||||
std::ostringstream msg;
|
||||
msg << "Sequence of " << s.sequence().max - s.sequence().min + 1 << " ";
|
||||
msg << s.sequence().codec << " images";
|
||||
ImGui::Text("%s", msg.str().c_str());
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
// change range
|
||||
static int _begin = -1;
|
||||
@@ -758,11 +864,50 @@ void ImGuiVisitor::visit (MultiFileSource& s)
|
||||
|
||||
// offer to open file browser at location
|
||||
std::string path = SystemToolkit::path_filename(s.sequence().location);
|
||||
std::string label = BaseToolkit::trunc_string(path, 25);
|
||||
std::string label = BaseToolkit::truncated(path, 25);
|
||||
label = BaseToolkit::transliterate(label);
|
||||
ImGuiToolkit::ButtonOpenUrl( label.c_str(), path.c_str(), ImVec2(IMGUI_RIGHT_ALIGN, 0) );
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Folder");
|
||||
|
||||
id = s.id();
|
||||
if (id != s.id())
|
||||
id = s.id();
|
||||
}
|
||||
|
||||
void ImGuiVisitor::visit (GenericStreamSource& s)
|
||||
{
|
||||
float w = ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN;
|
||||
|
||||
ImGuiToolkit::Icon(s.icon().x, s.icon().y);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::Text("Custom");
|
||||
|
||||
// stream info
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + w);
|
||||
s.accept(info);
|
||||
ImGui::Text("%s", info.str().c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
// icon (>) to open player
|
||||
if ( s.playable() ) {
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
ImGui::SameLine(0, 0);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE + ImGui::GetContentRegionAvail().x IMGUI_RIGHT_ALIGN);
|
||||
if (ImGuiToolkit::IconButton(ICON_FA_PLAY_CIRCLE, "Open in Player"))
|
||||
UserInterface::manager().showSourceEditor(&s);
|
||||
ImGui::SetCursorPos(pos);
|
||||
}
|
||||
|
||||
// Prepare display pipeline text
|
||||
static int numlines = 0;
|
||||
const ImGuiContext& g = *GImGui;
|
||||
ImVec2 fieldsize(w, MAX(3, numlines) * g.FontSize + g.Style.ItemSpacing.y + g.Style.FramePadding.y);
|
||||
|
||||
// Editor
|
||||
std::string _description = s.description();
|
||||
if ( ImGuiToolkit::InputCodeMultiline("Pipeline", &_description, fieldsize, &numlines) ) {
|
||||
s.setDescription(_description);
|
||||
Action::manager().store( s.name() + ": Change pipeline");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
#define IMGUIVISITOR_H
|
||||
|
||||
#include "Visitor.h"
|
||||
#include "InfoVisitor.h"
|
||||
|
||||
class ImGuiVisitor: public Visitor
|
||||
{
|
||||
InfoVisitor info;
|
||||
|
||||
public:
|
||||
ImGuiVisitor();
|
||||
|
||||
@@ -14,7 +17,6 @@ public:
|
||||
void visit (Group& n) override;
|
||||
void visit (Switch& n) override;
|
||||
void visit (Primitive& n) override;
|
||||
void visit (MediaSurface& n) override;
|
||||
void visit (FrameBufferSurface& n) override;
|
||||
|
||||
// Elements with attributes
|
||||
@@ -31,6 +33,7 @@ public:
|
||||
void visit (DeviceSource& s) override;
|
||||
void visit (NetworkSource& s) override;
|
||||
void visit (MultiFileSource& s) override;
|
||||
void visit (GenericStreamSource& s) override;
|
||||
};
|
||||
|
||||
#endif // IMGUIVISITOR_H
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "defines.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "ImageProcessingShader.h"
|
||||
|
||||
ShadingProgram imageProcessingShadingProgram("shaders/image.vs", "shaders/imageprocessing.fs");
|
||||
@@ -12,7 +32,7 @@ const char* ImageProcessingShader::filter_names[12] = { "None", "Blur", "Sharpen
|
||||
ImageProcessingShader::ImageProcessingShader(): Shader()
|
||||
{
|
||||
program_ = &imageProcessingShadingProgram;
|
||||
reset();
|
||||
ImageProcessingShader::reset();
|
||||
}
|
||||
|
||||
void ImageProcessingShader::use()
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include "defines.h"
|
||||
@@ -27,7 +46,7 @@ ImageShader::ImageShader(): Shader(), stipple(0.f), mask_texture(0)
|
||||
// static program shader
|
||||
program_ = &imageShadingProgram;
|
||||
// reset instance
|
||||
reset();
|
||||
ImageShader::reset();
|
||||
}
|
||||
|
||||
void ImageShader::use()
|
||||
@@ -85,7 +104,7 @@ AlphaShader::AlphaShader(): ImageShader()
|
||||
MaskShader::MaskShader(): Shader(), mode(0)
|
||||
{
|
||||
// reset instance
|
||||
reset();
|
||||
MaskShader::reset();
|
||||
// static program shader
|
||||
program_ = &maskPrograms[0];
|
||||
}
|
||||
|
||||
298
InfoVisitor.cpp
Normal file
298
InfoVisitor.cpp
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
|
||||
#include <tinyxml2.h>
|
||||
#include "tinyxml2Toolkit.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
#include "Scene.h"
|
||||
#include "Primitives.h"
|
||||
#include "ImageShader.h"
|
||||
#include "ImageProcessingShader.h"
|
||||
#include "MediaPlayer.h"
|
||||
#include "MediaSource.h"
|
||||
#include "SessionSource.h"
|
||||
#include "PatternSource.h"
|
||||
#include "DeviceSource.h"
|
||||
#include "NetworkSource.h"
|
||||
#include "MultiFileSource.h"
|
||||
#include "SessionCreator.h"
|
||||
#include "SessionVisitor.h"
|
||||
#include "Settings.h"
|
||||
#include "Mixer.h"
|
||||
#include "ActionManager.h"
|
||||
#include "BaseToolkit.h"
|
||||
#include "UserInterfaceManager.h"
|
||||
#include "SystemToolkit.h"
|
||||
|
||||
#include "InfoVisitor.h"
|
||||
|
||||
|
||||
|
||||
InfoVisitor::InfoVisitor() : brief_(true), current_id_(0)
|
||||
{
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Node &)
|
||||
{
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Group &)
|
||||
{
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Switch &)
|
||||
{
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Scene &)
|
||||
{
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Primitive &)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
void InfoVisitor::visit(MediaPlayer &mp)
|
||||
{
|
||||
// do not ask twice
|
||||
if (current_id_ == mp.id())
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
if (brief_) {
|
||||
oss << SystemToolkit::filename(mp.filename()) << std::endl;
|
||||
oss << mp.width() << " x " << mp.height() << ", ";
|
||||
oss << mp.media().codec_name.substr(0, mp.media().codec_name.find_first_of(" (,"));
|
||||
if (!mp.isImage())
|
||||
oss << ", " << std::fixed << std::setprecision(1) << mp.frameRate() << " fps";
|
||||
}
|
||||
else {
|
||||
oss << mp.filename() << std::endl;
|
||||
oss << mp.media().codec_name << std::endl;
|
||||
oss << mp.width() << " x " << mp.height() ;
|
||||
if (!mp.isImage())
|
||||
oss << ", " << std::fixed << std::setprecision(1) << mp.frameRate() << " fps";
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
|
||||
// remember (except if codec was not identified yet)
|
||||
if ( !mp.media().codec_name.empty() )
|
||||
current_id_ = mp.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit(Stream &n)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
if (brief_) {
|
||||
oss << BaseToolkit::splitted(n.description(), '!').front();
|
||||
}
|
||||
else {
|
||||
oss << n.description();
|
||||
}
|
||||
information_ = oss.str();
|
||||
}
|
||||
|
||||
|
||||
void InfoVisitor::visit (MediaSource& s)
|
||||
{
|
||||
s.mediaplayer()->accept(*this);
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (SessionFileSource& s)
|
||||
{
|
||||
if (current_id_ == s.id() || s.session() == nullptr)
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
if (brief_) {
|
||||
oss << SystemToolkit::filename(s.path()) << " (";
|
||||
oss << s.session()->numSource() << " sources)" << std::endl;
|
||||
}
|
||||
else
|
||||
oss << s.path() << std::endl;
|
||||
|
||||
if (s.session()->frame()){
|
||||
oss << s.session()->frame()->width() << " x " << s.session()->frame()->height() << ", ";
|
||||
oss << "RGB";
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (SessionGroupSource& s)
|
||||
{
|
||||
if (current_id_ == s.id() || s.session() == nullptr)
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << s.session()->numSource() << " sources in group" << std::endl;
|
||||
if (s.session()->frame()){
|
||||
oss << s.session()->frame()->width() << " x " << s.session()->frame()->height() << ", ";
|
||||
oss << "RGB";
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (RenderSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
information_ = "Rendering Output";
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (CloneSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
information_ = "Clone of " + s.origin()->name();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (PatternSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << Pattern::get(s.pattern()->type()).label << std::endl;
|
||||
if (s.pattern()) {
|
||||
oss << s.pattern()->width() << " x " << s.pattern()->height();
|
||||
oss << ", RGB";
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (DeviceSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
DeviceConfigSet confs = Device::manager().config( Device::manager().index(s.device().c_str()));
|
||||
if ( !confs.empty()) {
|
||||
DeviceConfig best = *confs.rbegin();
|
||||
float fps = static_cast<float>(best.fps_numerator) / static_cast<float>(best.fps_denominator);
|
||||
|
||||
if (brief_) {
|
||||
oss << best.width << " x " << best.height << ", ";
|
||||
oss << best.stream << " " << best.format << ", ";
|
||||
oss << std::fixed << std::setprecision(1) << fps << " fps";
|
||||
}
|
||||
else {
|
||||
oss << s.device() << std::endl;
|
||||
oss << best.width << " x " << best.height << ", ";
|
||||
oss << best.stream << " " << best.format << ", ";
|
||||
oss << std::fixed << std::setprecision(1) << fps << " fps";
|
||||
}
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (NetworkSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
NetworkStream *ns = s.networkStream();
|
||||
|
||||
std::ostringstream oss;
|
||||
if (brief_) {
|
||||
oss << ns->resolution().x << " x " << ns->resolution().y << ", ";
|
||||
oss << NetworkToolkit::protocol_name[ns->protocol()] << std::endl;
|
||||
oss << "IP " << ns->serverAddress();
|
||||
}
|
||||
else {
|
||||
oss << s.connection() << " (IP " << ns->serverAddress() << ")" << std::endl;
|
||||
oss << ns->resolution().x << " x " << ns->resolution().y << ", ";
|
||||
oss << NetworkToolkit::protocol_name[ns->protocol()];
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
|
||||
void InfoVisitor::visit (MultiFileSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
if (brief_) {
|
||||
oss << s.sequence().width << " x " << s.sequence().height << ", ";
|
||||
oss << s.sequence().codec << std::endl;
|
||||
oss << s.sequence().max - s.sequence().min + 1 << " images [";
|
||||
oss << s.sequence().min << " - " << s.sequence().max << "]";
|
||||
}
|
||||
else {
|
||||
oss << s.sequence().location << " [";
|
||||
oss << s.sequence().min << " - " << s.sequence().max << "]" << std::endl;
|
||||
oss << s.sequence().width << " x " << s.sequence().height << ", ";
|
||||
oss << s.sequence().codec << " (";
|
||||
oss << s.sequence().max - s.sequence().min + 1 << " images)";
|
||||
}
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
|
||||
void InfoVisitor::visit (GenericStreamSource& s)
|
||||
{
|
||||
if (current_id_ == s.id())
|
||||
return;
|
||||
|
||||
std::ostringstream oss;
|
||||
if (s.stream()) {
|
||||
std::string src_element = s.gstElements().front();
|
||||
src_element = src_element.substr(0, src_element.find(" "));
|
||||
oss << "gstreamer '" << src_element << "'" << std::endl;
|
||||
oss << s.stream()->width() << " x " << s.stream()->height();
|
||||
oss << ", RGB";
|
||||
}
|
||||
else
|
||||
oss << "Undefined";
|
||||
|
||||
information_ = oss.str();
|
||||
current_id_ = s.id();
|
||||
}
|
||||
41
InfoVisitor.h
Normal file
41
InfoVisitor.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#ifndef INFOVISITOR_H
|
||||
#define INFOVISITOR_H
|
||||
|
||||
#include "Visitor.h"
|
||||
|
||||
class InfoVisitor : public Visitor
|
||||
{
|
||||
std::string information_;
|
||||
bool brief_;
|
||||
uint64_t current_id_;
|
||||
|
||||
public:
|
||||
InfoVisitor();
|
||||
inline void setBriefStringMode () { brief_ = true; current_id_ = 0; }
|
||||
inline void setExtendedStringMode () { brief_ = false; current_id_ = 0; }
|
||||
inline void reset () { current_id_ = 0; }
|
||||
inline std::string str () const { return information_; }
|
||||
|
||||
// Elements of Scene
|
||||
void visit (Scene& n) override;
|
||||
void visit (Node& n) override;
|
||||
void visit (Group& n) override;
|
||||
void visit (Switch& n) override;
|
||||
void visit (Primitive& n) override;
|
||||
|
||||
// Elements with attributes
|
||||
void visit (Stream& n) override;
|
||||
void visit (MediaPlayer& n) override;
|
||||
void visit (MediaSource& s) override;
|
||||
void visit (SessionFileSource& s) override;
|
||||
void visit (SessionGroupSource& s) override;
|
||||
void visit (RenderSource& s) override;
|
||||
void visit (CloneSource& s) override;
|
||||
void visit (PatternSource& s) override;
|
||||
void visit (DeviceSource& s) override;
|
||||
void visit (NetworkSource& s) override;
|
||||
void visit (MultiFileSource& s) override;
|
||||
void visit (GenericStreamSource& s) override;
|
||||
};
|
||||
|
||||
#endif // INFOVISITOR_H
|
||||
@@ -1,6 +1,24 @@
|
||||
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
@@ -132,14 +150,19 @@ Interpolator::Interpolator()
|
||||
}
|
||||
|
||||
Interpolator::~Interpolator()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
void Interpolator::clear()
|
||||
{
|
||||
for (auto i = interpolators_.begin(); i != interpolators_.end(); ) {
|
||||
delete *i;
|
||||
i = interpolators_.erase(i);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void Interpolator::add (Source *s, const SourceCore &target)
|
||||
{
|
||||
SourceInterpolator *i = new SourceInterpolator(s, target);
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
Interpolator();
|
||||
~Interpolator();
|
||||
|
||||
void clear ();
|
||||
void add (Source *s, const SourceCore &target );
|
||||
|
||||
void apply (float percent);
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
// Opengl
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiToolkit.h"
|
||||
|
||||
#include <string>
|
||||
@@ -121,10 +138,9 @@ void LayerView::draw()
|
||||
float depth_inc = (dsl.back()->depth() - depth) / static_cast<float>(Mixer::selection().size()-1);
|
||||
for (++it; it != dsl.end(); ++it) {
|
||||
depth += depth_inc;
|
||||
(*it)->setDepth(depth);
|
||||
(*it)->call( new SetDepth(depth, 80.f) );
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Layer Distribute"));
|
||||
++View::need_deep_update_;
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_RULER_HORIZONTAL " Compress" )){
|
||||
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
|
||||
@@ -132,20 +148,18 @@ void LayerView::draw()
|
||||
float depth = (*it)->depth();
|
||||
for (++it; it != dsl.end(); ++it) {
|
||||
depth += LAYER_STEP;
|
||||
(*it)->setDepth(depth);
|
||||
(*it)->call( new SetDepth(depth, 80.f) );
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Layer Compress"));
|
||||
++View::need_deep_update_;
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_EXCHANGE_ALT " Reverse order" )){
|
||||
SourceList dsl = depth_sorted(Mixer::selection().getCopy());
|
||||
SourceList::iterator it = dsl.begin();
|
||||
SourceList::reverse_iterator rit = dsl.rbegin();
|
||||
for (; it != dsl.end(); ++it, ++rit) {
|
||||
(*it)->setDepth((*rit)->depth());
|
||||
(*it)->call( new SetDepth((*rit)->depth(), 80.f) );
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Layer Reverse order"));
|
||||
++View::need_deep_update_;
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor(2);
|
||||
@@ -200,9 +214,8 @@ void LayerView::resize ( int scale )
|
||||
scene.root()->scale_.y = z;
|
||||
|
||||
// Clamp translation to acceptable area
|
||||
glm::vec3 border_left(scene.root()->scale_.x * -2.f, scene.root()->scale_.y * -1.f, 0.f);
|
||||
glm::vec3 border_right(scene.root()->scale_.x * 8.f, scene.root()->scale_.y * 8.f, 0.f);
|
||||
scene.root()->translation_ = glm::clamp(scene.root()->translation_, border_left, border_right);
|
||||
glm::vec3 border(2.f, 1.f, 0.f);
|
||||
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border * 2.f);
|
||||
}
|
||||
|
||||
int LayerView::size ()
|
||||
@@ -292,7 +305,7 @@ float LayerView::setDepth(Source *s, float d)
|
||||
return sourceNode->translation_.z;
|
||||
}
|
||||
|
||||
View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2> pick)
|
||||
View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair<Node *, glm::vec2>)
|
||||
{
|
||||
if (!s)
|
||||
return Cursor();
|
||||
@@ -311,11 +324,9 @@ View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair
|
||||
// apply change
|
||||
float d = setDepth( s, MAX( -dest_translation.x, 0.f) );
|
||||
|
||||
// store action in history
|
||||
std::ostringstream info;
|
||||
info << "Depth " << std::fixed << std::setprecision(2) << d << " ";
|
||||
// info << (s->locked() ? ICON_FA_LOCK : ICON_FA_LOCK_OPEN); // TODO static not locked
|
||||
|
||||
// store action in history
|
||||
current_action_ = s->name() + ": " + info.str();
|
||||
|
||||
return Cursor(Cursor_ResizeNESW, info.str() );
|
||||
@@ -323,8 +334,8 @@ View::Cursor LayerView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pair
|
||||
|
||||
void LayerView::arrow (glm::vec2 movement)
|
||||
{
|
||||
static int accumulator = 0;
|
||||
accumulator++;
|
||||
static float accumulator = 0.f;
|
||||
accumulator += dt_;
|
||||
|
||||
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
|
||||
glm::vec3 gl_Position_to = Rendering::manager().unProject(glm::vec2(movement.x-movement.y, 0.f), scene.root()->transform_);
|
||||
@@ -347,17 +358,18 @@ void LayerView::arrow (glm::vec2 movement)
|
||||
|
||||
// + ALT : discrete displacement
|
||||
if (UserInterface::manager().altModifier()) {
|
||||
if (accumulator > 10) {
|
||||
if (accumulator > 100.f) {
|
||||
dest_translation += glm::sign(gl_delta) * 0.21f;
|
||||
dest_translation.x = ROUND(dest_translation.x, 10.f);
|
||||
accumulator = 0;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// normal case: dest += delta
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
|
||||
// store action in history
|
||||
|
||||
171
Log.cpp
171
Log.cpp
@@ -1,14 +1,27 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
using namespace std;
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "ImGuiToolkit.h"
|
||||
#include "DialogToolkit.h"
|
||||
@@ -66,7 +79,7 @@ struct AppLog
|
||||
// window
|
||||
ImGui::SameLine(0, 0);
|
||||
static bool numbering = true;
|
||||
ImGuiToolkit::ButtonToggle( ICON_FA_SORT_NUMERIC_DOWN, &numbering );
|
||||
ImGuiToolkit::ButtonIconToggle(4, 12, 4, 12, &numbering );
|
||||
ImGui::SameLine();
|
||||
bool clear = ImGui::Button( ICON_FA_BACKSPACE " Clear");
|
||||
ImGui::SameLine();
|
||||
@@ -75,76 +88,82 @@ struct AppLog
|
||||
Filter.Draw("Filter", -60.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
if ( ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) )
|
||||
if ( !ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_AlwaysHorizontalScrollbar) )
|
||||
{
|
||||
if (clear)
|
||||
Clear();
|
||||
if (copy)
|
||||
ImGui::LogToClipboard();
|
||||
|
||||
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
|
||||
mtx.lock();
|
||||
|
||||
const char* buf = Buf.begin();
|
||||
const char* buf_end = Buf.end();
|
||||
if (Filter.IsActive())
|
||||
{
|
||||
// In this example we don't use the clipper when Filter is enabled.
|
||||
// This is because we don't have a random access on the result on our filter.
|
||||
// A real application processing logs with ten of thousands of entries may want to store the result of search/filter.
|
||||
// especially if the filtering function is not trivial (e.g. reg-exp).
|
||||
for (int line_no = 0; line_no < LineOffsets.Size; line_no++)
|
||||
{
|
||||
const char* line_start = buf + LineOffsets[line_no];
|
||||
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
|
||||
if (Filter.PassFilter(line_start, line_end))
|
||||
ImGui::TextUnformatted(line_start, line_end);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The simplest and easy way to display the entire buffer:
|
||||
// ImGui::TextUnformatted(buf_begin, buf_end);
|
||||
// And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines.
|
||||
// Here we instead demonstrate using the clipper to only process lines that are within the visible area.
|
||||
// If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended.
|
||||
// Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height,
|
||||
// both of which we can handle since we an array pointing to the beginning of each line of text.
|
||||
// When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper.
|
||||
// Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries)
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(LineOffsets.Size);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++)
|
||||
{
|
||||
const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6);
|
||||
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
|
||||
ImGui::TextUnformatted(line_start, line_end);
|
||||
}
|
||||
}
|
||||
clipper.End();
|
||||
}
|
||||
|
||||
mtx.unlock();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
|
||||
// Auto scroll
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (clear)
|
||||
Clear();
|
||||
if (copy)
|
||||
ImGui::LogToClipboard();
|
||||
|
||||
ImGuiToolkit::PushFont(ImGuiToolkit::FONT_MONO);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
|
||||
mtx.lock();
|
||||
|
||||
const char* buf = Buf.begin();
|
||||
const char* buf_end = Buf.end();
|
||||
if (Filter.IsActive())
|
||||
{
|
||||
// In this example we don't use the clipper when Filter is enabled.
|
||||
// This is because we don't have a random access on the result on our filter.
|
||||
// A real application processing logs with ten of thousands of entries may want to store the result of search/filter.
|
||||
// especially if the filtering function is not trivial (e.g. reg-exp).
|
||||
for (int line_no = 0; line_no < LineOffsets.Size; line_no++)
|
||||
{
|
||||
const char* line_start = buf + LineOffsets[line_no];
|
||||
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
|
||||
if (Filter.PassFilter(line_start, line_end))
|
||||
ImGui::TextUnformatted(line_start, line_end);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The simplest and easy way to display the entire buffer:
|
||||
// ImGui::TextUnformatted(buf_begin, buf_end);
|
||||
// And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines.
|
||||
// Here we instead demonstrate using the clipper to only process lines that are within the visible area.
|
||||
// If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended.
|
||||
// Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height,
|
||||
// both of which we can handle since we an array pointing to the beginning of each line of text.
|
||||
// When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper.
|
||||
// Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries)
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(LineOffsets.Size);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++)
|
||||
{
|
||||
const char* line_start = buf + LineOffsets[line_no] + (numbering?0:6);
|
||||
const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end;
|
||||
ImGui::TextUnformatted(line_start, line_end);
|
||||
}
|
||||
}
|
||||
clipper.End();
|
||||
}
|
||||
|
||||
mtx.unlock();
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopFont();
|
||||
|
||||
// Auto scroll
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
}
|
||||
};
|
||||
|
||||
static AppLog logs;
|
||||
AppLog logs;
|
||||
list<string> notifications;
|
||||
list<string> warnings;
|
||||
float notifications_timeout = 0.f;
|
||||
|
||||
void Log::Info(const char* fmt, ...)
|
||||
{
|
||||
@@ -157,12 +176,9 @@ void Log::Info(const char* fmt, ...)
|
||||
void Log::ShowLogWindow(bool* p_open)
|
||||
{
|
||||
ImGui::SetNextWindowSize(ImVec2(700, 600), ImGuiCond_FirstUseEver);
|
||||
logs.Draw( ICON_FA_LIST_UL " Logs", p_open);
|
||||
logs.Draw( IMGUI_TITLE_LOGS, p_open);
|
||||
}
|
||||
|
||||
static list<string> notifications;
|
||||
static float notifications_timeout = 0.f;
|
||||
|
||||
void Log::Notify(const char* fmt, ...)
|
||||
{
|
||||
ImGuiTextBuffer buf;
|
||||
@@ -177,12 +193,9 @@ void Log::Notify(const char* fmt, ...)
|
||||
notifications_timeout = 0.f;
|
||||
|
||||
// always log
|
||||
Log::Info("%s", buf.c_str());
|
||||
Log::Info(ICON_FA_INFO_CIRCLE " %s", buf.c_str());
|
||||
}
|
||||
|
||||
|
||||
static list<string> warnings;
|
||||
|
||||
void Log::Warning(const char* fmt, ...)
|
||||
{
|
||||
ImGuiTextBuffer buf;
|
||||
@@ -196,7 +209,7 @@ void Log::Warning(const char* fmt, ...)
|
||||
warnings.push_back(buf.c_str());
|
||||
|
||||
// always log
|
||||
Log::Info("Warning - %s\n", buf.c_str());
|
||||
Log::Info(ICON_FA_EXCLAMATION_TRIANGLE " Warning - %s", buf.c_str());
|
||||
}
|
||||
|
||||
void Log::Render(bool *showWarnings)
|
||||
@@ -248,7 +261,7 @@ void Log::Render(bool *showWarnings)
|
||||
if (ImGui::BeginPopupModal("Warning", NULL, ImGuiWindowFlags_AlwaysAutoResize))
|
||||
{
|
||||
ImGuiToolkit::Icon(9, 4);
|
||||
ImGui::SameLine(0, 10);
|
||||
ImGui::SameLine(0, IMGUI_SAME_LINE);
|
||||
ImGui::SetNextItemWidth(width);
|
||||
ImGui::TextColored(ImVec4(1.0f,0.6f,0.0f,1.0f), "%ld error(s) occured.\n\n", warnings.size());
|
||||
ImGui::Dummy(ImVec2(width, 0));
|
||||
|
||||
79
Loopback.cpp
79
Loopback.cpp
@@ -1,4 +1,21 @@
|
||||
#include <thread>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
// Desktop OpenGL function loader
|
||||
#include <glad/glad.h>
|
||||
@@ -151,20 +168,17 @@ bool Loopback::systemLoopbackInitialized()
|
||||
|
||||
Loopback::Loopback() : FrameGrabber()
|
||||
{
|
||||
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 60);
|
||||
|
||||
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, 30); // fixed 30 FPS
|
||||
}
|
||||
|
||||
void Loopback::init(GstCaps *caps)
|
||||
std::string Loopback::init(GstCaps *caps)
|
||||
{
|
||||
// ignore
|
||||
if (caps == nullptr)
|
||||
return;
|
||||
return std::string("Invalid caps");
|
||||
|
||||
if (!Loopback::systemLoopbackInitialized()){
|
||||
Log::Warning("Loopback system shall be initialized first.");
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("Loopback system shall be initialized first.");
|
||||
}
|
||||
|
||||
// create a gstreamer pipeline
|
||||
@@ -174,10 +188,9 @@ void Loopback::init(GstCaps *caps)
|
||||
GError *error = NULL;
|
||||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||||
if (error != NULL) {
|
||||
Log::Warning("Loopback Could not construct pipeline %s:\n%s", description.c_str(), error->message);
|
||||
std::string msg = std::string("Loopback : Could not construct pipeline ") + description + "\n" + std::string(error->message);
|
||||
g_clear_error (&error);
|
||||
finished_ = true;
|
||||
return;
|
||||
return msg;
|
||||
}
|
||||
|
||||
// setup device sink
|
||||
@@ -190,49 +203,51 @@ void Loopback::init(GstCaps *caps)
|
||||
if (src_) {
|
||||
|
||||
g_object_set (G_OBJECT (src_),
|
||||
"stream-type", GST_APP_STREAM_TYPE_STREAM,
|
||||
"is-live", TRUE,
|
||||
"format", GST_FORMAT_TIME,
|
||||
// "do-timestamp", TRUE,
|
||||
NULL);
|
||||
|
||||
// Direct encoding (no buffering)
|
||||
gst_app_src_set_max_bytes( src_, 0 );
|
||||
// configure stream
|
||||
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
|
||||
gst_app_src_set_latency( src_, -1, 0);
|
||||
|
||||
// instruct src to use the required caps
|
||||
caps_ = gst_caps_copy( caps );
|
||||
// Set buffer size
|
||||
gst_app_src_set_max_bytes( src_, buffering_size_ );
|
||||
|
||||
// specify streaming framerate in the given caps
|
||||
GstCaps *tmp = gst_caps_copy( caps );
|
||||
GValue v = { 0, };
|
||||
g_value_init (&v, GST_TYPE_FRACTION);
|
||||
gst_value_set_fraction (&v, 30, 1); // fixed 30 FPS
|
||||
gst_caps_set_value(tmp, "framerate", &v);
|
||||
g_value_unset (&v);
|
||||
|
||||
// instruct src to use the caps
|
||||
caps_ = gst_caps_copy( tmp );
|
||||
gst_app_src_set_caps (src_, caps_);
|
||||
gst_caps_unref (tmp);
|
||||
|
||||
// setup callbacks
|
||||
GstAppSrcCallbacks callbacks;
|
||||
callbacks.need_data = FrameGrabber::callback_need_data;
|
||||
callbacks.enough_data = FrameGrabber::callback_enough_data;
|
||||
callbacks.seek_data = NULL; // stream type is not seekable
|
||||
gst_app_src_set_callbacks (src_, &callbacks, this, NULL);
|
||||
gst_app_src_set_callbacks( src_, &callbacks, this, NULL);
|
||||
|
||||
}
|
||||
else {
|
||||
Log::Warning("Loopback Could not configure source");
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("Loopback : Could not configure source.");
|
||||
}
|
||||
|
||||
// start recording
|
||||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
Log::Warning("Loopback Could not open %s", Loopback::system_loopback_name.c_str());
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("Loopback : Could not open ") + Loopback::system_loopback_name;
|
||||
}
|
||||
|
||||
// all good
|
||||
#if defined(LINUX)
|
||||
Log::Notify("Loopback started (v4l2loopback on %s)", Loopback::system_loopback_name.c_str());
|
||||
#else
|
||||
Log::Notify("Loopback started (%s)", Loopback::system_loopback_name.c_str());
|
||||
#endif
|
||||
// start
|
||||
active_ = true;
|
||||
initialized_ = true;
|
||||
|
||||
return std::string("Loopback started on ") + Loopback::system_loopback_name;
|
||||
}
|
||||
|
||||
void Loopback::terminate()
|
||||
|
||||
@@ -15,7 +15,7 @@ class Loopback : public FrameGrabber
|
||||
static std::string system_loopback_name;
|
||||
static bool system_loopback_initialized;
|
||||
|
||||
void init(GstCaps *caps) override;
|
||||
std::string init(GstCaps *caps) override;
|
||||
void terminate() override;
|
||||
|
||||
public:
|
||||
|
||||
317
MediaPlayer.cpp
317
MediaPlayer.cpp
@@ -1,12 +1,25 @@
|
||||
#include <thread>
|
||||
|
||||
using namespace std;
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
// Desktop OpenGL function loader
|
||||
#include <glad/glad.h>
|
||||
|
||||
|
||||
// vmix
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
#include "Resource.h"
|
||||
@@ -15,6 +28,7 @@ using namespace std;
|
||||
#include "BaseToolkit.h"
|
||||
#include "GstToolkit.h"
|
||||
#include "RenderingManager.h"
|
||||
#include "Metronome.h"
|
||||
|
||||
#include "MediaPlayer.h"
|
||||
|
||||
@@ -36,9 +50,13 @@ MediaPlayer::MediaPlayer()
|
||||
desired_state_ = GST_STATE_PAUSED;
|
||||
|
||||
failed_ = false;
|
||||
pending_ = false;
|
||||
metro_sync_ = Metronome::SYNC_NONE;
|
||||
force_update_ = false;
|
||||
seeking_ = false;
|
||||
rewind_on_disable_ = false;
|
||||
force_software_decoding_ = false;
|
||||
hardware_decoder_ = "";
|
||||
decoder_name_ = "";
|
||||
rate_ = 1.0;
|
||||
position_ = GST_CLOCK_TIME_NONE;
|
||||
loop_ = LoopMode::LOOP_REWIND;
|
||||
@@ -166,7 +184,8 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
|
||||
video_stream_info.framerate_n = gst_discoverer_video_info_get_framerate_num(vinfo);
|
||||
video_stream_info.framerate_d = gst_discoverer_video_info_get_framerate_denom(vinfo);
|
||||
if (video_stream_info.framerate_n == 0 || video_stream_info.framerate_d == 0) {
|
||||
video_stream_info.framerate_n = 25;
|
||||
Log::Info("'%s': No framerate indicated in the file; using default 30fps", uri.c_str());
|
||||
video_stream_info.framerate_n = 30;
|
||||
video_stream_info.framerate_d = 1;
|
||||
}
|
||||
video_stream_info.dt = ( (GST_SECOND * static_cast<guint64>(video_stream_info.framerate_d)) / (static_cast<guint64>(video_stream_info.framerate_n)) );
|
||||
@@ -186,7 +205,7 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
|
||||
if ( tags ) {
|
||||
gchar *container = NULL;
|
||||
if ( gst_tag_list_get_string (tags, GST_TAG_CONTAINER_FORMAT, &container) )
|
||||
video_stream_info.codec_name += " " + std::string(container);
|
||||
video_stream_info.codec_name += ", " + std::string(container);
|
||||
if (container)
|
||||
g_free(container);
|
||||
}
|
||||
@@ -220,7 +239,7 @@ MediaInfo MediaPlayer::UriDiscoverer(const std::string &uri)
|
||||
return video_stream_info;
|
||||
}
|
||||
|
||||
void MediaPlayer::open (const std::string & filename, const string &uri)
|
||||
void MediaPlayer::open (const std::string & filename, const std::string &uri)
|
||||
{
|
||||
// set path
|
||||
filename_ = BaseToolkit::transliterate( filename );
|
||||
@@ -231,6 +250,9 @@ void MediaPlayer::open (const std::string & filename, const string &uri)
|
||||
else
|
||||
uri_ = uri;
|
||||
|
||||
if (uri_.empty())
|
||||
failed_ = true;
|
||||
|
||||
// close before re-openning
|
||||
if (isOpen())
|
||||
close();
|
||||
@@ -260,13 +282,13 @@ void MediaPlayer::reopen()
|
||||
}
|
||||
}
|
||||
|
||||
void MediaPlayer::execute_open()
|
||||
{
|
||||
void MediaPlayer::execute_open()
|
||||
{
|
||||
// Create gstreamer pipeline :
|
||||
// "uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! appsink "
|
||||
// equivalent to command line
|
||||
// "gst-launch-1.0 uridecodebin uri=file:///path_to_file/filename.mp4 ! videoconvert ! ximagesink"
|
||||
string description = "uridecodebin name=decoder uri=" + uri_ + " ! queue max-size-time=0 ! ";
|
||||
std::string description = "uridecodebin name=decoder uri=" + uri_ + " ! queue max-size-time=0 ! ";
|
||||
// NB: queue adds some control over the buffer, thereby limiting the frame delay. zero size means no buffering
|
||||
|
||||
// string description = "uridecodebin name=decoder uri=" + uri_ + " decoder. ! ";
|
||||
@@ -319,8 +341,8 @@ void MediaPlayer::execute_open()
|
||||
g_object_set(G_OBJECT(pipeline_), "name", std::to_string(id_).c_str(), NULL);
|
||||
gst_pipeline_set_auto_flush_bus( GST_PIPELINE(pipeline_), true);
|
||||
|
||||
// GstCaps *caps = gst_static_caps_get (&frame_render_caps);
|
||||
string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
|
||||
// GstCaps *caps = gst_static_caps_get (&frame_render_caps);
|
||||
std::string capstring = "video/x-raw,format=RGBA,width="+ std::to_string(media_.width) +
|
||||
",height=" + std::to_string(media_.height);
|
||||
GstCaps *caps = gst_caps_from_string(capstring.c_str());
|
||||
if (!gst_video_info_from_caps (&v_frame_video_info_, caps)) {
|
||||
@@ -381,7 +403,7 @@ void MediaPlayer::execute_open()
|
||||
gst_caps_unref (caps);
|
||||
|
||||
#ifdef USE_GST_OPENGL_SYNC_HANDLER
|
||||
// capture bus signals to force a unique opengl context for all GST elements
|
||||
// capture bus signals to force a unique opengl context for all GST elements
|
||||
Rendering::LinkPipeline(GST_PIPELINE (pipeline_));
|
||||
#endif
|
||||
|
||||
@@ -402,7 +424,7 @@ void MediaPlayer::execute_open()
|
||||
|
||||
// all good
|
||||
Log::Info("MediaPlayer %s Opened '%s' (%s %d x %d)", std::to_string(id_).c_str(),
|
||||
uri_.c_str(), media_.codec_name.c_str(), media_.width, media_.height);
|
||||
SystemToolkit::filename(uri_).c_str(), media_.codec_name.c_str(), media_.width, media_.height);
|
||||
|
||||
Log::Info("MediaPlayer %s Timeline [%ld %ld] %ld frames, %d gaps", std::to_string(id_).c_str(),
|
||||
timeline_.begin(), timeline_.end(), timeline_.numFrames(), timeline_.numGaps());
|
||||
@@ -448,14 +470,13 @@ void MediaPlayer::close()
|
||||
if (pipeline_ != nullptr) {
|
||||
|
||||
// force flush
|
||||
GstState state;
|
||||
gst_element_send_event(pipeline_, gst_event_new_seek (1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH,
|
||||
GST_SEEK_TYPE_NONE, 0, GST_SEEK_TYPE_NONE, 0) );
|
||||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||||
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
|
||||
|
||||
// end pipeline
|
||||
gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||||
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
|
||||
|
||||
gst_object_unref (pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
@@ -513,15 +534,19 @@ void MediaPlayer::enable(bool on)
|
||||
|
||||
if ( enabled_ != on ) {
|
||||
|
||||
// option to automatically rewind each time the player is disabled
|
||||
if (!on && rewind_on_disable_ && desired_state_ == GST_STATE_PLAYING)
|
||||
rewind(true);
|
||||
|
||||
// apply change
|
||||
enabled_ = on;
|
||||
|
||||
// default to pause
|
||||
GstState requested_state = GST_STATE_PAUSED;
|
||||
|
||||
// unpause only if enabled
|
||||
if (enabled_) {
|
||||
if (enabled_)
|
||||
requested_state = desired_state_;
|
||||
}
|
||||
|
||||
// apply state change
|
||||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, requested_state);
|
||||
@@ -543,9 +568,18 @@ bool MediaPlayer::isImage() const
|
||||
return media_.isimage;
|
||||
}
|
||||
|
||||
std::string MediaPlayer::hardwareDecoderName()
|
||||
std::string MediaPlayer::decoderName()
|
||||
{
|
||||
return hardware_decoder_;
|
||||
// decoder_name_ not initialized
|
||||
if (decoder_name_.empty()) {
|
||||
// try to know if it is a hardware decoder
|
||||
decoder_name_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
|
||||
// nope, then it is a sofware decoder
|
||||
if (decoder_name_.empty())
|
||||
decoder_name_ = "software";
|
||||
}
|
||||
|
||||
return decoder_name_;
|
||||
}
|
||||
|
||||
bool MediaPlayer::softwareDecodingForced()
|
||||
@@ -559,19 +593,16 @@ void MediaPlayer::setSoftwareDecodingForced(bool on)
|
||||
|
||||
// set parameter
|
||||
force_software_decoding_ = on;
|
||||
decoder_name_ = "";
|
||||
|
||||
// changing state requires reload
|
||||
if (need_reload)
|
||||
reopen();
|
||||
}
|
||||
|
||||
void MediaPlayer::play(bool on)
|
||||
void MediaPlayer::execute_play_command(bool on)
|
||||
{
|
||||
// ignore if disabled, and cannot play an image
|
||||
if (!enabled_ || media_.isimage)
|
||||
return;
|
||||
|
||||
// request state
|
||||
// request state
|
||||
GstState requested_state = on ? GST_STATE_PLAYING : GST_STATE_PAUSED;
|
||||
|
||||
// ignore if requesting twice same state
|
||||
@@ -587,9 +618,10 @@ void MediaPlayer::play(bool on)
|
||||
|
||||
// requesting to play, but stopped at end of stream : rewind first !
|
||||
if ( desired_state_ == GST_STATE_PLAYING) {
|
||||
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|
||||
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
|
||||
rewind();
|
||||
if (rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()))
|
||||
execute_seek_command(timeline_.next(0));
|
||||
else if ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|
||||
execute_seek_command(timeline_.previous(timeline_.last()));
|
||||
}
|
||||
|
||||
// all ready, apply state change immediately
|
||||
@@ -597,17 +629,37 @@ void MediaPlayer::play(bool on)
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
Log::Warning("MediaPlayer %s Failed to play", std::to_string(id_).c_str());
|
||||
failed_ = true;
|
||||
}
|
||||
}
|
||||
#ifdef MEDIA_PLAYER_DEBUG
|
||||
else if (on)
|
||||
Log::Info("MediaPlayer %s Start", std::to_string(id_).c_str());
|
||||
else
|
||||
Log::Info("MediaPlayer %s Stop [%ld]", std::to_string(id_).c_str(), position());
|
||||
#endif
|
||||
}
|
||||
|
||||
// reset time counter
|
||||
timecount_.reset();
|
||||
void MediaPlayer::play(bool on)
|
||||
{
|
||||
// ignore if disabled, and cannot play an image
|
||||
if (!enabled_ || media_.isimage || pending_)
|
||||
return;
|
||||
|
||||
// Metronome
|
||||
if (metro_sync_ > Metronome::SYNC_NONE) {
|
||||
// busy with this delayed action
|
||||
pending_ = true;
|
||||
// delayed execution function
|
||||
std::function<void()> playlater = std::bind([](MediaPlayer *p, bool o) {
|
||||
p->execute_play_command(o); p->pending_=false; }, this, on);
|
||||
// Execute: sync to Metronome
|
||||
if (metro_sync_ > Metronome::SYNC_BEAT)
|
||||
Metronome::manager().executeAtPhase( playlater );
|
||||
else
|
||||
Metronome::manager().executeAtBeat( playlater );
|
||||
}
|
||||
else
|
||||
// execute immediately
|
||||
execute_play_command( on );
|
||||
}
|
||||
|
||||
bool MediaPlayer::isPlaying(bool testpipeline) const
|
||||
@@ -631,44 +683,77 @@ MediaPlayer::LoopMode MediaPlayer::loop() const
|
||||
{
|
||||
return loop_;
|
||||
}
|
||||
|
||||
|
||||
void MediaPlayer::setLoop(MediaPlayer::LoopMode mode)
|
||||
{
|
||||
loop_ = mode;
|
||||
}
|
||||
|
||||
void MediaPlayer::rewind()
|
||||
//void
|
||||
|
||||
void MediaPlayer::rewind(bool force)
|
||||
{
|
||||
if (!enabled_ || !media_.seekable)
|
||||
if (!enabled_ || !media_.seekable || pending_)
|
||||
return;
|
||||
|
||||
// playing forward, loop to begin
|
||||
if (rate_ > 0.0) {
|
||||
// begin is the end of a gab which includes the first PTS (if exists)
|
||||
// normal case, begin is zero
|
||||
execute_seek_command( timeline_.next(0) );
|
||||
}
|
||||
// playing forward, loop to begin;
|
||||
// begin is the end of a gab which includes the first PTS (if exists)
|
||||
// normal case, begin is zero
|
||||
// playing backward, loop to endTimeInterval gap;
|
||||
else {
|
||||
// end is the start of a gab which includes the last PTS (if exists)
|
||||
// normal case, end is last frame
|
||||
execute_seek_command( timeline_.previous(timeline_.last()) );
|
||||
// end is the start of a gab which includes the last PTS (if exists)
|
||||
// normal case, end is last frame
|
||||
GstClockTime target = (rate_ > 0.0) ? timeline_.next(0) : timeline_.previous(timeline_.last());
|
||||
|
||||
// Metronome
|
||||
if (metro_sync_) {
|
||||
// busy with this delayed action
|
||||
pending_ = true;
|
||||
// delayed execution function
|
||||
std::function<void()> rewindlater = std::bind([](MediaPlayer *p, GstClockTime t, bool f) {
|
||||
p->execute_seek_command( t, f ); p->pending_=false; }, this, target, force);
|
||||
// Execute: sync to Metronome
|
||||
if (metro_sync_ > Metronome::SYNC_BEAT)
|
||||
Metronome::manager().executeAtPhase( rewindlater );
|
||||
else
|
||||
Metronome::manager().executeAtBeat( rewindlater );
|
||||
}
|
||||
else
|
||||
// execute immediately
|
||||
execute_seek_command( target, force );
|
||||
}
|
||||
|
||||
|
||||
void MediaPlayer::step()
|
||||
{
|
||||
// useful only when Paused
|
||||
if (!enabled_ || isPlaying())
|
||||
if (!enabled_ || isPlaying() || pending_)
|
||||
return;
|
||||
|
||||
if ( ( rate_ < 0.0 && position_ <= timeline_.next(0) )
|
||||
|| ( rate_ > 0.0 && position_ >= timeline_.previous(timeline_.last()) ) )
|
||||
rewind();
|
||||
else {
|
||||
// step event
|
||||
GstEvent *stepevent = gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE);
|
||||
|
||||
// step
|
||||
gst_element_send_event (pipeline_, gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS(rate_), TRUE, FALSE));
|
||||
// Metronome
|
||||
if (metro_sync_) {
|
||||
// busy with this delayed action
|
||||
pending_ = true;
|
||||
// delayed execution function
|
||||
std::function<void()> steplater = std::bind([](MediaPlayer *p, GstEvent *e) {
|
||||
gst_element_send_event(p->pipeline_, e); p->pending_=false; }, this, stepevent) ;
|
||||
// Execute: sync to Metronome
|
||||
if (metro_sync_ > Metronome::SYNC_BEAT)
|
||||
Metronome::manager().executeAtPhase( steplater );
|
||||
else
|
||||
Metronome::manager().executeAtBeat( steplater );
|
||||
|
||||
}
|
||||
else
|
||||
// execute immediately
|
||||
gst_element_send_event (pipeline_, stepevent);
|
||||
}
|
||||
}
|
||||
|
||||
bool MediaPlayer::go_to(GstClockTime pos)
|
||||
@@ -679,7 +764,7 @@ bool MediaPlayer::go_to(GstClockTime pos)
|
||||
|
||||
GstClockTime jumpPts = pos;
|
||||
|
||||
if (timeline_.gapAt(pos, gap)) {
|
||||
if (timeline_.getGapAt(pos, gap)) {
|
||||
// if in a gap, find closest seek target
|
||||
if (gap.is_valid()) {
|
||||
// jump in one or the other direction
|
||||
@@ -765,14 +850,10 @@ void MediaPlayer::init_texture(guint index)
|
||||
pbo_index_ = 0;
|
||||
pbo_next_index_ = 1;
|
||||
|
||||
#ifdef MEDIA_PLAYER_DEBUG
|
||||
Log::Info("MediaPlayer %s Using Pixel Buffer Object texturing.", std::to_string(id_).c_str());
|
||||
#endif
|
||||
|
||||
// now that a frame is ready, and once only, browse into the pipeline
|
||||
// for possible hadrware decoding plugins used. Empty string means none.
|
||||
hardware_decoder_ = GstToolkit::used_gpu_decoding_plugins(pipeline_);
|
||||
// initialize decoderName once
|
||||
Log::Info("MediaPlayer %s Uses %s decoding and OpenGL PBO texturing.", std::to_string(id_).c_str(), decoderName().c_str());
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
@@ -800,19 +881,21 @@ void MediaPlayer::fill_texture(guint index)
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, media_.width, media_.height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
|
||||
// bind the next PBO to write pixels
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo_[pbo_next_index_]);
|
||||
#ifdef USE_GL_BUFFER_SUBDATA
|
||||
glBufferSubData(GL_PIXEL_UNPACK_BUFFER, 0, pbo_size_, frame_[index].vframe.data[0]);
|
||||
#else
|
||||
// update data directly on the mapped buffer
|
||||
// NB : equivalent but faster than glBufferSubData (memmove instead of memcpy ?)
|
||||
// See http://www.songho.ca/opengl/gl_pbo.html#map for more details
|
||||
glBufferData(GL_PIXEL_UNPACK_BUFFER, pbo_size_, 0, GL_STREAM_DRAW);
|
||||
// map the buffer object into client's memory
|
||||
GLubyte* ptr = (GLubyte*) glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
|
||||
if (ptr) {
|
||||
// update data directly on the mapped buffer
|
||||
// NB : equivalent but faster (memmove instead of memcpy ?) than
|
||||
// glNamedBufferSubData(pboIds[nextIndex], 0, imgsize, vp->getBuffer())
|
||||
memmove(ptr, frame_[index].vframe.data[0], pbo_size_);
|
||||
|
||||
// release pointer to mapping buffer
|
||||
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
|
||||
}
|
||||
#endif
|
||||
// done with PBO
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
}
|
||||
@@ -855,7 +938,7 @@ void MediaPlayer::update()
|
||||
}
|
||||
|
||||
// prevent unnecessary updates: disabled or already filled image
|
||||
if (!enabled_ || (media_.isimage && textureindex_>0 ) )
|
||||
if ( (!enabled_ && !force_update_) || (media_.isimage && textureindex_>0 ) )
|
||||
return;
|
||||
|
||||
// local variables before trying to update
|
||||
@@ -866,13 +949,6 @@ void MediaPlayer::update()
|
||||
index_lock_.lock();
|
||||
// get the last frame filled from fill_frame()
|
||||
read_index = last_index_;
|
||||
// // Do NOT miss and jump directly (after seek) to a pre-roll
|
||||
// for (guint i = 0; i < N_VFRAME; ++i) {
|
||||
// if (frame_[i].status == PREROLL) {
|
||||
// read_index = i;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// unlock access to index change
|
||||
index_lock_.unlock();
|
||||
|
||||
@@ -915,8 +991,7 @@ void MediaPlayer::update()
|
||||
// if already seeking (asynch)
|
||||
if (seeking_) {
|
||||
// request status update to pipeline (re-sync gst thread)
|
||||
GstState state;
|
||||
gst_element_get_state (pipeline_, &state, NULL, GST_CLOCK_TIME_NONE);
|
||||
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
|
||||
// seek should be resolved next frame
|
||||
seeking_ = false;
|
||||
// do NOT do another seek yet
|
||||
@@ -925,13 +1000,18 @@ void MediaPlayer::update()
|
||||
else {
|
||||
// manage timeline: test if position falls into a gap
|
||||
TimeInterval gap;
|
||||
if (position_ != GST_CLOCK_TIME_NONE && timeline_.gapAt(position_, gap)) {
|
||||
if (position_ != GST_CLOCK_TIME_NONE && timeline_.getGapAt(position_, gap)) {
|
||||
// if in a gap, seek to next section
|
||||
if (gap.is_valid()) {
|
||||
// jump in one or the other direction
|
||||
GstClockTime jumpPts = (rate_>0.f) ? gap.end : gap.begin;
|
||||
// seek to next valid time (if not beginnig or end of timeline)
|
||||
GstClockTime jumpPts = timeline_.step(); // round jump time to frame pts
|
||||
if ( rate_ > 0.f )
|
||||
jumpPts *= ( gap.end / timeline_.step() ) + 1; // FWD: go to end of gap
|
||||
else
|
||||
jumpPts *= ( gap.begin / timeline_.step() ); // BWD: go to begin of gap
|
||||
// (if not beginnig or end of timeline)
|
||||
if (jumpPts > timeline_.first() && jumpPts < timeline_.last())
|
||||
// seek to jump PTS time
|
||||
seek( jumpPts );
|
||||
// otherwise, we should loop
|
||||
else
|
||||
@@ -944,13 +1024,15 @@ void MediaPlayer::update()
|
||||
if (need_loop) {
|
||||
execute_loop_command();
|
||||
}
|
||||
|
||||
force_update_ = false;
|
||||
}
|
||||
|
||||
void MediaPlayer::execute_loop_command()
|
||||
{
|
||||
if (loop_==LOOP_REWIND) {
|
||||
rewind();
|
||||
}
|
||||
}
|
||||
else if (loop_==LOOP_BIDIRECTIONAL) {
|
||||
rate_ *= - 1.f;
|
||||
execute_seek_command();
|
||||
@@ -960,7 +1042,7 @@ void MediaPlayer::execute_loop_command()
|
||||
}
|
||||
}
|
||||
|
||||
void MediaPlayer::execute_seek_command(GstClockTime target)
|
||||
void MediaPlayer::execute_seek_command(GstClockTime target, bool force)
|
||||
{
|
||||
if ( pipeline_ == nullptr || !media_.seekable )
|
||||
return;
|
||||
@@ -969,7 +1051,7 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
|
||||
GstClockTime seek_pos = target;
|
||||
|
||||
// no target given
|
||||
if (target == GST_CLOCK_TIME_NONE)
|
||||
if (target == GST_CLOCK_TIME_NONE)
|
||||
// create seek event with current position (rate changed ?)
|
||||
seek_pos = position_;
|
||||
// target is given but useless
|
||||
@@ -980,9 +1062,12 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
|
||||
|
||||
// seek with flush (always)
|
||||
int seek_flags = GST_SEEK_FLAG_FLUSH;
|
||||
|
||||
// seek with trick mode if fast speed
|
||||
if ( ABS(rate_) > 1.0 )
|
||||
if ( ABS(rate_) > 1.5 )
|
||||
seek_flags |= GST_SEEK_FLAG_TRICKMODE;
|
||||
else
|
||||
seek_flags |= GST_SEEK_FLAG_ACCURATE;
|
||||
|
||||
// create seek event depending on direction
|
||||
GstEvent *seek_event = nullptr;
|
||||
@@ -1005,6 +1090,12 @@ void MediaPlayer::execute_seek_command(GstClockTime target)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Force update
|
||||
if (force) {
|
||||
gst_element_get_state (pipeline_, NULL, NULL, GST_CLOCK_TIME_NONE);
|
||||
force_update_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MediaPlayer::setPlaySpeed(double s)
|
||||
@@ -1012,12 +1103,12 @@ void MediaPlayer::setPlaySpeed(double s)
|
||||
if (media_.isimage)
|
||||
return;
|
||||
|
||||
// bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
|
||||
// bound to interval [-MAX_PLAY_SPEED MAX_PLAY_SPEED]
|
||||
rate_ = CLAMP(s, -MAX_PLAY_SPEED, MAX_PLAY_SPEED);
|
||||
// skip interval [-MIN_PLAY_SPEED MIN_PLAY_SPEED]
|
||||
if (ABS(rate_) < MIN_PLAY_SPEED)
|
||||
rate_ = SIGN(rate_) * MIN_PLAY_SPEED;
|
||||
|
||||
|
||||
// apply with seek
|
||||
execute_seek_command();
|
||||
}
|
||||
@@ -1096,7 +1187,9 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
|
||||
// get the frame from buffer
|
||||
if ( !gst_video_frame_map (&frame_[write_index_].vframe, &v_frame_video_info_, buf, GST_MAP_READ ) )
|
||||
{
|
||||
#ifdef MEDIA_PLAYER_DEBUG
|
||||
Log::Info("MediaPlayer %s Failed to map the video buffer", std::to_string(id_).c_str());
|
||||
#endif
|
||||
// free access to frame & exit
|
||||
frame_[write_index_].status = INVALID;
|
||||
frame_[write_index_].access.unlock();
|
||||
@@ -1113,7 +1206,7 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
|
||||
frame_[write_index_].position = buf->pts;
|
||||
|
||||
// set the start position (i.e. pts of first frame we got)
|
||||
if (timeline_.begin() == GST_CLOCK_TIME_NONE) {
|
||||
if (timeline_.first() == GST_CLOCK_TIME_NONE) {
|
||||
timeline_.setFirst(buf->pts);
|
||||
}
|
||||
}
|
||||
@@ -1123,6 +1216,7 @@ bool MediaPlayer::fill_frame(GstBuffer *buf, FrameStatus status)
|
||||
#ifdef MEDIA_PLAYER_DEBUG
|
||||
Log::Info("MediaPlayer %s Received an Invalid frame", std::to_string(id_).c_str());
|
||||
#endif
|
||||
// free access to frame & exit
|
||||
frame_[write_index_].status = INVALID;
|
||||
frame_[write_index_].access.unlock();
|
||||
return false;
|
||||
@@ -1233,54 +1327,27 @@ GstFlowReturn MediaPlayer::callback_new_sample (GstAppSink *sink, gpointer p)
|
||||
|
||||
|
||||
|
||||
MediaPlayer::TimeCounter::TimeCounter() {
|
||||
MediaPlayer::TimeCounter::TimeCounter()
|
||||
{
|
||||
timer = g_timer_new ();
|
||||
}
|
||||
|
||||
reset();
|
||||
MediaPlayer::TimeCounter::~TimeCounter()
|
||||
{
|
||||
g_free(timer);
|
||||
}
|
||||
|
||||
void MediaPlayer::TimeCounter::tic ()
|
||||
{
|
||||
// how long since last time
|
||||
GstClockTime t = gst_util_get_timestamp ();
|
||||
GstClockTime dt = t - last_time;
|
||||
const double dt = g_timer_elapsed (timer, NULL) * 1000.0;
|
||||
|
||||
// one more frame since last time
|
||||
nbFrames++;
|
||||
|
||||
// calculate instantaneous framerate
|
||||
// Exponential moving averate with previous framerate to filter jitter (50/50)
|
||||
// The divition of frame/time is done on long integer GstClockTime, counting in microsecond
|
||||
// NB: factor 100 to get 0.01 precision
|
||||
fps = 0.5 * fps + 0.005 * static_cast<double>( ( 100 * GST_SECOND * nbFrames ) / dt );
|
||||
|
||||
// reset counter every second
|
||||
if ( dt >= GST_SECOND)
|
||||
{
|
||||
last_time = t;
|
||||
nbFrames = 0;
|
||||
// ignore refresh after too little time
|
||||
if (dt > 3.0){
|
||||
// restart timer
|
||||
g_timer_start(timer);
|
||||
// calculate instantaneous framerate
|
||||
// Exponential moving averate with previous framerate to filter jitter
|
||||
fps = CLAMP( 0.5 * fps + 500.0 / dt, 0.0, 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
GstClockTime MediaPlayer::TimeCounter::dt ()
|
||||
{
|
||||
GstClockTime t = gst_util_get_timestamp ();
|
||||
GstClockTime dt = t - tic_time;
|
||||
tic_time = t;
|
||||
|
||||
// return the instantaneous delta t
|
||||
return dt;
|
||||
}
|
||||
|
||||
void MediaPlayer::TimeCounter::reset ()
|
||||
{
|
||||
last_time = gst_util_get_timestamp ();;
|
||||
tic_time = last_time;
|
||||
nbFrames = 0;
|
||||
fps = 0.0;
|
||||
}
|
||||
|
||||
double MediaPlayer::TimeCounter::frameRate() const
|
||||
{
|
||||
return fps;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <gst/app/gstappsink.h>
|
||||
|
||||
#include "Timeline.h"
|
||||
#include "Metronome.h"
|
||||
|
||||
// Forward declare classes referenced
|
||||
class Visitor;
|
||||
@@ -42,7 +43,7 @@ struct MediaInfo {
|
||||
bitrate = 0;
|
||||
framerate_n = 1;
|
||||
framerate_d = 25;
|
||||
codec_name = "unknown";
|
||||
codec_name = "";
|
||||
isimage = false;
|
||||
interlaced = false;
|
||||
seekable = false;
|
||||
@@ -187,7 +188,15 @@ public:
|
||||
/**
|
||||
* Seek to zero
|
||||
* */
|
||||
void rewind();
|
||||
void rewind(bool force = false);
|
||||
/**
|
||||
* pending
|
||||
* */
|
||||
bool pending() const { return pending_; }
|
||||
/**
|
||||
* Get position time
|
||||
* */
|
||||
GstClockTime position();
|
||||
/**
|
||||
* go to a valid position in media timeline
|
||||
* pos in nanoseconds.
|
||||
@@ -210,10 +219,6 @@ public:
|
||||
void setTimeline(const Timeline &tl);
|
||||
|
||||
float currentTimelineFading();
|
||||
/**
|
||||
* Get position time
|
||||
* */
|
||||
GstClockTime position();
|
||||
/**
|
||||
* Get framerate of the media
|
||||
* */
|
||||
@@ -232,7 +237,7 @@ public:
|
||||
* */
|
||||
guint height() const;
|
||||
/**
|
||||
* Get frames displayt aspect ratio
|
||||
* Get frames display aspect ratio
|
||||
* NB: can be different than width() / height()
|
||||
* */
|
||||
float aspectRatio() const;
|
||||
@@ -242,19 +247,31 @@ public:
|
||||
* */
|
||||
guint texture() const;
|
||||
/**
|
||||
* Get the name of the hardware decoder used
|
||||
* Empty string if none (i.e. software decoding)
|
||||
* Get the name of the decoder used,
|
||||
* return 'software' if no hardware decoder is used
|
||||
* NB: perform request on pipeline on first call
|
||||
* */
|
||||
std::string hardwareDecoderName();
|
||||
std::string decoderName();
|
||||
/**
|
||||
* Forces open using software decoding
|
||||
* (i.e. without hadrware decoding)
|
||||
* NB: this reopens the video and reset decoder name
|
||||
* */
|
||||
void setSoftwareDecodingForced(bool on);
|
||||
bool softwareDecodingForced();
|
||||
/**
|
||||
* Option to automatically rewind each time the player is disabled
|
||||
* (i.e. when enable(false) is called )
|
||||
* */
|
||||
inline void setRewindOnDisabled(bool on) { rewind_on_disable_ = on; }
|
||||
inline bool rewindOnDisabled() const { return rewind_on_disable_; }
|
||||
/**
|
||||
* Option to synchronize with metronome
|
||||
* */
|
||||
inline void setSyncToMetronome(Metronome::Synchronicity s) { metro_sync_ = s; }
|
||||
inline Metronome::Synchronicity syncToMetronome() const { return metro_sync_; }
|
||||
/**
|
||||
* Accept visitors
|
||||
* Used for saving session file
|
||||
* */
|
||||
void accept(Visitor& v);
|
||||
/**
|
||||
@@ -289,24 +306,24 @@ private:
|
||||
GstVideoInfo v_frame_video_info_;
|
||||
std::atomic<bool> opened_;
|
||||
std::atomic<bool> failed_;
|
||||
bool force_update_;
|
||||
bool pending_;
|
||||
bool seeking_;
|
||||
bool enabled_;
|
||||
bool rewind_on_disable_;
|
||||
bool force_software_decoding_;
|
||||
std::string hardware_decoder_;
|
||||
std::string decoder_name_;
|
||||
Metronome::Synchronicity metro_sync_;
|
||||
|
||||
// fps counter
|
||||
struct TimeCounter {
|
||||
|
||||
GstClockTime last_time;
|
||||
GstClockTime tic_time;
|
||||
int nbFrames;
|
||||
GTimer *timer;
|
||||
gdouble fps;
|
||||
public:
|
||||
TimeCounter();
|
||||
GstClockTime dt();
|
||||
~TimeCounter();
|
||||
void tic();
|
||||
void reset();
|
||||
gdouble frameRate() const;
|
||||
inline gdouble frameRate() const { return fps; }
|
||||
};
|
||||
TimeCounter timecount_;
|
||||
|
||||
@@ -344,8 +361,9 @@ private:
|
||||
|
||||
// gst pipeline control
|
||||
void execute_open();
|
||||
void execute_play_command(bool on);
|
||||
void execute_loop_command();
|
||||
void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE);
|
||||
void execute_seek_command(GstClockTime target = GST_CLOCK_TIME_NONE, bool force = false);
|
||||
|
||||
// gst frame filling
|
||||
void init_texture(guint index);
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "MediaSource.h"
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "defines.h"
|
||||
#include "ImageShader.h"
|
||||
@@ -11,6 +29,8 @@
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "MediaSource.h"
|
||||
|
||||
MediaSource::MediaSource(uint64_t id) : Source(id), path_("")
|
||||
{
|
||||
// create media player
|
||||
@@ -26,7 +46,6 @@ MediaSource::~MediaSource()
|
||||
void MediaSource::setPath(const std::string &p)
|
||||
{
|
||||
path_ = p;
|
||||
Log::Notify("Creating Source with media '%s'", path_.c_str());
|
||||
|
||||
// open gstreamer
|
||||
mediaplayer_->open(path_);
|
||||
@@ -49,9 +68,14 @@ MediaPlayer *MediaSource::mediaplayer() const
|
||||
glm::ivec2 MediaSource::icon() const
|
||||
{
|
||||
if (mediaplayer_->isImage())
|
||||
return glm::ivec2(4, 9);
|
||||
return glm::ivec2(ICON_SOURCE_IMAGE);
|
||||
else
|
||||
return glm::ivec2(18, 13);
|
||||
return glm::ivec2(ICON_SOURCE_VIDEO);
|
||||
}
|
||||
|
||||
std::string MediaSource::info() const
|
||||
{
|
||||
return std::string("media '") + path_ + "'";
|
||||
}
|
||||
|
||||
bool MediaSource::failed() const
|
||||
@@ -108,14 +132,48 @@ void MediaSource::setActive (bool on)
|
||||
{
|
||||
bool was_active = active_;
|
||||
|
||||
// try to activate (may fail if source is cloned)
|
||||
Source::setActive(on);
|
||||
|
||||
// change status of media player (only if status changed)
|
||||
if ( active_ != was_active ) {
|
||||
if ( active_ != was_active )
|
||||
mediaplayer_->enable(active_);
|
||||
|
||||
// change visibility of active surface (show preview of media when inactive)
|
||||
if (activesurface_) {
|
||||
if (active_)
|
||||
activesurface_->setTextureIndex(Resource::getTextureTransparent());
|
||||
else
|
||||
activesurface_->setTextureIndex(mediaplayer_->texture());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool MediaSource::playing () const
|
||||
{
|
||||
return mediaplayer_->isPlaying();
|
||||
}
|
||||
|
||||
void MediaSource::play (bool on)
|
||||
{
|
||||
mediaplayer_->play(on);
|
||||
}
|
||||
|
||||
bool MediaSource::playable () const
|
||||
{
|
||||
return !mediaplayer_->isImage();
|
||||
}
|
||||
|
||||
void MediaSource::replay ()
|
||||
{
|
||||
mediaplayer_->rewind();
|
||||
}
|
||||
|
||||
guint64 MediaSource::playtime () const
|
||||
{
|
||||
return mediaplayer_->position();
|
||||
}
|
||||
|
||||
void MediaSource::update(float dt)
|
||||
{
|
||||
Source::update(dt);
|
||||
|
||||
@@ -14,6 +14,11 @@ public:
|
||||
// implementation of source API
|
||||
void update (float dt) override;
|
||||
void setActive (bool on) override;
|
||||
bool playing () const override;
|
||||
void play (bool) override;
|
||||
bool playable () const override;
|
||||
void replay () override;
|
||||
guint64 playtime () const override;
|
||||
void render() override;
|
||||
bool failed() const override;
|
||||
uint texture() const override;
|
||||
@@ -25,6 +30,7 @@ public:
|
||||
MediaPlayer *mediaplayer() const;
|
||||
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
protected:
|
||||
|
||||
|
||||
27
Mesh.cpp
27
Mesh.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <istream>
|
||||
@@ -16,9 +35,10 @@
|
||||
#include "Resource.h"
|
||||
#include "ImageShader.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
#include "Mesh.h"
|
||||
#include "GlmToolkit.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "Mesh.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace glm;
|
||||
@@ -227,7 +247,6 @@ bool parsePLY(string ascii,
|
||||
// a numerical property
|
||||
if ( ! prop.is_list ) {
|
||||
|
||||
float value;
|
||||
switch ( prop.name[0] ) {
|
||||
case 'x':
|
||||
point.x = parseValue<float>(stringstream);
|
||||
@@ -263,7 +282,7 @@ bool parsePLY(string ascii,
|
||||
break;
|
||||
default:
|
||||
// ignore normals or other types
|
||||
value = parseValue<float>(stringstream);
|
||||
parseValue<float>(stringstream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
253
Metronome.cpp
Normal file
253
Metronome.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
#include <atomic>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
/// Ableton Link is a technology that synchronizes musical beat, tempo,
|
||||
/// and phase across multiple applications running on one or more devices.
|
||||
/// Applications on devices connected to a local network discover each other
|
||||
/// automatically and form a musical session in which each participant can
|
||||
/// perform independently: anyone can start or stop while still staying in time.
|
||||
/// Anyone can change the tempo, the others will follow.
|
||||
/// Anyone can join or leave without disrupting the session.
|
||||
///
|
||||
/// https://ableton.github.io/link/
|
||||
///
|
||||
#include <ableton/Link.hpp>
|
||||
|
||||
#include "Settings.h"
|
||||
#include "Metronome.h"
|
||||
#include "Log.h"
|
||||
|
||||
|
||||
namespace ableton
|
||||
{
|
||||
|
||||
/// Inspired from Dummy audio platform example
|
||||
/// https://github.com/Ableton/link/blob/master/examples/linkaudio/AudioPlatform_Dummy.hpp
|
||||
class Engine
|
||||
{
|
||||
public:
|
||||
Engine(Link& link)
|
||||
: mLink(link)
|
||||
, mQuantum(4.)
|
||||
{
|
||||
}
|
||||
|
||||
void startPlaying()
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
sessionState.setIsPlayingAndRequestBeatAtTime(true, now(), 0., mQuantum);
|
||||
mLink.commitAppSessionState(sessionState);
|
||||
}
|
||||
|
||||
void stopPlaying()
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
sessionState.setIsPlaying(false, now());
|
||||
mLink.commitAppSessionState(sessionState);
|
||||
}
|
||||
|
||||
bool isPlaying() const
|
||||
{
|
||||
return mLink.captureAppSessionState().isPlaying();
|
||||
}
|
||||
|
||||
double beatTime() const
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
return sessionState.beatAtTime(now(), mQuantum);
|
||||
}
|
||||
|
||||
std::chrono::microseconds timeNextBeat() const
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
double beat = ceil(sessionState.beatAtTime(now(), mQuantum));
|
||||
return sessionState.timeAtBeat(beat, mQuantum);
|
||||
}
|
||||
|
||||
double phaseTime() const
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
return sessionState.phaseAtTime(now(), mQuantum);
|
||||
}
|
||||
|
||||
std::chrono::microseconds timeNextPhase() const
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
double phase = ceil(sessionState.phaseAtTime(now(), mQuantum));
|
||||
double beat = ceil(sessionState.beatAtTime(now(), mQuantum));
|
||||
return sessionState.timeAtBeat(beat + (mQuantum-phase), mQuantum);
|
||||
}
|
||||
|
||||
double tempo() const
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
return sessionState.tempo();
|
||||
}
|
||||
|
||||
double setTempo(double tempo)
|
||||
{
|
||||
auto sessionState = mLink.captureAppSessionState();
|
||||
sessionState.setTempo(tempo, now());
|
||||
mLink.commitAppSessionState(sessionState);
|
||||
return sessionState.tempo();
|
||||
}
|
||||
|
||||
double quantum() const
|
||||
{
|
||||
return mQuantum;
|
||||
}
|
||||
|
||||
void setQuantum(double quantum)
|
||||
{
|
||||
mQuantum = quantum;
|
||||
}
|
||||
|
||||
bool isStartStopSyncEnabled() const
|
||||
{
|
||||
return mLink.isStartStopSyncEnabled();
|
||||
}
|
||||
|
||||
void setStartStopSyncEnabled(bool enabled)
|
||||
{
|
||||
mLink.enableStartStopSync(enabled);
|
||||
}
|
||||
|
||||
std::chrono::microseconds now() const
|
||||
{
|
||||
return mLink.clock().micros();
|
||||
}
|
||||
|
||||
private:
|
||||
Link& mLink;
|
||||
double mQuantum;
|
||||
};
|
||||
|
||||
|
||||
} // namespace ableton
|
||||
|
||||
|
||||
ableton::Link link_(120.);
|
||||
ableton::Engine engine_(link_);
|
||||
|
||||
Metronome::Metronome()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool Metronome::init()
|
||||
{
|
||||
// set parameters
|
||||
setEnabled(Settings::application.timer.link_enabled);
|
||||
setTempo(Settings::application.timer.link_tempo);
|
||||
setQuantum(Settings::application.timer.link_quantum);
|
||||
setStartStopSync(Settings::application.timer.link_start_stop_sync);
|
||||
|
||||
// no reason for failure?
|
||||
return true;
|
||||
}
|
||||
|
||||
void Metronome::terminate()
|
||||
{
|
||||
// save current tempo
|
||||
Settings::application.timer.link_tempo = tempo();
|
||||
|
||||
// disconnect
|
||||
link_.enable(false);
|
||||
}
|
||||
|
||||
void Metronome::setEnabled (bool on)
|
||||
{
|
||||
link_.enable(on);
|
||||
Settings::application.timer.link_enabled = link_.isEnabled();
|
||||
Log::Info("Metronome Ableton Link %s", Settings::application.timer.link_enabled ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
bool Metronome::enabled () const
|
||||
{
|
||||
return link_.isEnabled();
|
||||
}
|
||||
|
||||
double Metronome::beats() const
|
||||
{
|
||||
return engine_.beatTime();
|
||||
}
|
||||
|
||||
double Metronome::phase() const
|
||||
{
|
||||
return engine_.phaseTime();
|
||||
}
|
||||
|
||||
void Metronome::setQuantum(double q)
|
||||
{
|
||||
engine_.setQuantum(q);
|
||||
Settings::application.timer.link_quantum = engine_.quantum();
|
||||
}
|
||||
|
||||
double Metronome::quantum() const
|
||||
{
|
||||
return engine_.quantum();
|
||||
}
|
||||
|
||||
void Metronome::setTempo(double t)
|
||||
{
|
||||
// set the tempo to t
|
||||
// OR
|
||||
// adopt the last tempo value that have been proposed on the network
|
||||
Settings::application.timer.link_tempo = engine_.setTempo(t);
|
||||
}
|
||||
|
||||
double Metronome::tempo() const
|
||||
{
|
||||
return engine_.tempo();
|
||||
}
|
||||
|
||||
|
||||
void Metronome::setStartStopSync (bool on)
|
||||
{
|
||||
engine_.setStartStopSyncEnabled(on);
|
||||
Settings::application.timer.link_start_stop_sync = engine_.isStartStopSyncEnabled();
|
||||
Log::Info("Metronome Ableton Link start & stop sync %s", Settings::application.timer.link_start_stop_sync ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
bool Metronome::startStopSync () const
|
||||
{
|
||||
return engine_.isStartStopSyncEnabled();
|
||||
}
|
||||
|
||||
void Metronome::restart()
|
||||
{
|
||||
engine_.startPlaying();
|
||||
}
|
||||
|
||||
std::chrono::microseconds Metronome::timeToBeat()
|
||||
{
|
||||
return engine_.timeNextBeat() - engine_.now();
|
||||
}
|
||||
|
||||
std::chrono::microseconds Metronome::timeToPhase()
|
||||
{
|
||||
return engine_.timeNextPhase() - engine_.now();
|
||||
}
|
||||
|
||||
void delay(std::function<void()> f, std::chrono::microseconds us)
|
||||
{
|
||||
std::this_thread::sleep_for(us);
|
||||
f();
|
||||
}
|
||||
|
||||
void Metronome::executeAtBeat( std::function<void()> f )
|
||||
{
|
||||
std::thread( delay, f, timeToBeat() ).detach();
|
||||
}
|
||||
|
||||
void Metronome::executeAtPhase( std::function<void()> f )
|
||||
{
|
||||
std::thread( delay, f, timeToPhase() ).detach();
|
||||
}
|
||||
|
||||
size_t Metronome::peers() const
|
||||
{
|
||||
return link_.numPeers();
|
||||
}
|
||||
72
Metronome.h
Normal file
72
Metronome.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#ifndef METRONOME_H
|
||||
#define METRONOME_H
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
|
||||
class Metronome
|
||||
{
|
||||
// Private Constructor
|
||||
Metronome();
|
||||
Metronome(Metronome const& copy) = delete;
|
||||
Metronome& operator=(Metronome const& copy) = delete;
|
||||
|
||||
public:
|
||||
|
||||
typedef enum {
|
||||
SYNC_NONE = 0,
|
||||
SYNC_BEAT,
|
||||
SYNC_PHASE
|
||||
} Synchronicity;
|
||||
|
||||
static Metronome& manager ()
|
||||
{
|
||||
// The only instance
|
||||
static Metronome _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
bool init ();
|
||||
void terminate ();
|
||||
|
||||
void setEnabled (bool on);
|
||||
bool enabled () const;
|
||||
|
||||
void setTempo (double t);
|
||||
double tempo () const;
|
||||
|
||||
void setQuantum (double q);
|
||||
double quantum () const;
|
||||
|
||||
void setStartStopSync (bool on);
|
||||
bool startStopSync () const;
|
||||
void restart();
|
||||
|
||||
// get beat and phase
|
||||
double beats () const;
|
||||
double phase () const;
|
||||
|
||||
// mechanisms to delay execution to next beat
|
||||
std::chrono::microseconds timeToBeat();
|
||||
void executeAtBeat( std::function<void()> f );
|
||||
|
||||
// mechanisms to delay execution to next phase
|
||||
std::chrono::microseconds timeToPhase();
|
||||
void executeAtPhase( std::function<void()> f );
|
||||
|
||||
size_t peers () const;
|
||||
|
||||
};
|
||||
|
||||
/// Example calls to executeAtBeat
|
||||
///
|
||||
/// With a Lamda function calling a member function of an object
|
||||
/// - without parameter
|
||||
/// Metronome::manager().executeAtBeat( std::bind([](MediaPlayer *p) { p->rewind(); }, mediaplayer_) );
|
||||
///
|
||||
/// - with parameter
|
||||
/// Metronome::manager().executeAtBeat( std::bind([](MediaPlayer *p, bool o) { p->play(o); }, mediaplayer_, on) );
|
||||
///
|
||||
|
||||
|
||||
#endif // METRONOME_H
|
||||
131
Mixer.cpp
131
Mixer.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
@@ -36,15 +55,19 @@
|
||||
#include "Mixer.h"
|
||||
|
||||
#define THREADED_LOADING
|
||||
static std::vector< std::future<Session *> > sessionLoaders_;
|
||||
static std::vector< std::future<Session *> > sessionImporters_;
|
||||
static std::vector< SessionSource * > sessionSourceToImport_;
|
||||
std::vector< std::future<Session *> > sessionLoaders_;
|
||||
std::vector< std::future<Session *> > sessionImporters_;
|
||||
std::vector< SessionSource * > sessionSourceToImport_;
|
||||
const std::chrono::milliseconds timeout_ = std::chrono::milliseconds(4);
|
||||
|
||||
|
||||
// static multithreaded session saving
|
||||
static void saveSession(const std::string& filename, Session *session)
|
||||
static void saveSession(const std::string& filename, Session *session, bool with_version)
|
||||
{
|
||||
// capture a snapshot of current version if requested
|
||||
if (with_version)
|
||||
Action::manager().snapshot( SystemToolkit::date_time_string());
|
||||
|
||||
// lock access while saving
|
||||
session->lock();
|
||||
|
||||
@@ -54,7 +77,7 @@ static void saveSession(const std::string& filename, Session *session)
|
||||
// set session filename
|
||||
session->setFilename(filename);
|
||||
// cosmetics saved ok
|
||||
Rendering::manager().mainWindow().setTitle(filename);
|
||||
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(filename));
|
||||
Settings::application.recentSessions.push(filename);
|
||||
Log::Notify("Session %s saved.", filename.c_str());
|
||||
|
||||
@@ -67,7 +90,8 @@ static void saveSession(const std::string& filename, Session *session)
|
||||
session->unlock();
|
||||
}
|
||||
|
||||
Mixer::Mixer() : session_(nullptr), back_session_(nullptr), current_view_(nullptr), dt_(0.f), dt__(0.f)
|
||||
Mixer::Mixer() : session_(nullptr), back_session_(nullptr), sessionSwapRequested_(false),
|
||||
current_view_(nullptr), dt_(16.f), dt__(16.f)
|
||||
{
|
||||
// unsused initial empty session
|
||||
session_ = new Session;
|
||||
@@ -116,7 +140,8 @@ void Mixer::update()
|
||||
// check status of loader: did it finish ?
|
||||
if (sessionLoaders_.back().wait_for(timeout_) == std::future_status::ready ) {
|
||||
// get the session loaded by this loader
|
||||
set( sessionLoaders_.back().get() );
|
||||
if (sessionLoaders_.back().valid())
|
||||
set( sessionLoaders_.back().get() );
|
||||
// done with this session loader
|
||||
sessionLoaders_.pop_back();
|
||||
}
|
||||
@@ -141,9 +166,13 @@ void Mixer::update()
|
||||
// swap front and back sessions
|
||||
swap();
|
||||
++View::need_deep_update_;
|
||||
// set session filename
|
||||
Rendering::manager().mainWindow().setTitle(session_->filename());
|
||||
Settings::application.recentSessions.push(session_->filename());
|
||||
// inform new session filename
|
||||
if (session_->filename().empty()) {
|
||||
Rendering::manager().setMainWindowTitle(Settings::application.windows[0].name);
|
||||
} else {
|
||||
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(session_->filename()));
|
||||
Settings::application.recentSessions.push(session_->filename());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +195,7 @@ void Mixer::update()
|
||||
session_->update(dt_);
|
||||
|
||||
// grab frames to recorders & streamers
|
||||
FrameGrabbing::manager().grabFrame(session_->frame(), dt_);
|
||||
FrameGrabbing::manager().grabFrame(session_->frame());
|
||||
|
||||
// delete sources which failed update (one by one)
|
||||
Source *failure = session()->failedSource();
|
||||
@@ -229,11 +258,6 @@ Source * Mixer::createSourceFile(const std::string &path)
|
||||
ms->setPath(path);
|
||||
s = ms;
|
||||
}
|
||||
|
||||
// remember in recent media
|
||||
Settings::application.recentImport.push(path);
|
||||
Settings::application.recentImport.path = SystemToolkit::path_filename(path);
|
||||
|
||||
// propose a new name based on uri
|
||||
s->setName(SystemToolkit::base_filename(path));
|
||||
|
||||
@@ -263,9 +287,6 @@ Source * Mixer::createSourceMultifile(const std::list<std::string> &list_files,
|
||||
mfs->setSequence(sequence, fps);
|
||||
s = mfs;
|
||||
|
||||
// remember in recent media
|
||||
Settings::application.recentImport.path = SystemToolkit::path_filename(list_files.front());
|
||||
|
||||
// propose a new name
|
||||
s->setName( SystemToolkit::base_filename( BaseToolkit::common_prefix(list_files) ) );
|
||||
}
|
||||
@@ -311,7 +332,7 @@ Source * Mixer::createSourcePattern(uint pattern, glm::ivec2 res)
|
||||
s->setPattern(pattern, res);
|
||||
|
||||
// propose a new name based on pattern name
|
||||
std::string name = Pattern::pattern_types[pattern];
|
||||
std::string name = Pattern::get(pattern).label;
|
||||
name = name.substr(0, name.find(" "));
|
||||
s->setName(name);
|
||||
|
||||
@@ -400,7 +421,13 @@ void Mixer::insertSource(Source *s, View::Mode m)
|
||||
attach(s);
|
||||
|
||||
// new state in history manager
|
||||
Action::manager().store(s->name() + std::string(" source inserted"));
|
||||
Action::manager().store(s->name() + std::string(": source inserted"));
|
||||
|
||||
// notify creation of source
|
||||
Log::Notify("Added source '%s' with %s", s->name().c_str(), s->info().c_str());
|
||||
MediaSource *ms = dynamic_cast<MediaSource *>(s);
|
||||
if (ms)
|
||||
Settings::application.recentImport.push(ms->path());
|
||||
|
||||
// if requested to show the source in a given view
|
||||
// (known to work for View::MIXING et TRANSITION: other views untested)
|
||||
@@ -678,9 +705,10 @@ void Mixer::groupSelection()
|
||||
info << sessiongroup->name() << " inserted: " << sessiongroup->session()->numSource() << " sources flatten.";
|
||||
Action::manager().store(info.str());
|
||||
|
||||
Log::Notify("Added source '%s' with %s", sessiongroup->name().c_str(), sessiongroup->info().c_str());
|
||||
|
||||
// give the hand to the user
|
||||
Mixer::manager().setCurrentSource(sessiongroup);
|
||||
Log::Notify(info.str().c_str());
|
||||
}
|
||||
|
||||
void Mixer::renameSource(Source *s, const std::string &newname)
|
||||
@@ -770,6 +798,17 @@ SourceList Mixer::findSources (float depth_from, float depth_to)
|
||||
return found;
|
||||
}
|
||||
|
||||
SourceList Mixer::validate (const SourceList &list)
|
||||
{
|
||||
SourceList sl;
|
||||
for( auto sit = list.begin(); sit != list.end(); ++sit) {
|
||||
SourceList::iterator it = session_->find( *sit );
|
||||
if (it != session_->end())
|
||||
sl.push_back(*sit);
|
||||
}
|
||||
return sl;
|
||||
}
|
||||
|
||||
void Mixer::setCurrentSource(uint64_t id)
|
||||
{
|
||||
setCurrentSource( session_->find(id) );
|
||||
@@ -792,6 +831,14 @@ void Mixer::setCurrentSource(Source *s)
|
||||
setCurrentSource( session_->find(s) );
|
||||
}
|
||||
|
||||
Source *Mixer::sourceAtIndex (int index)
|
||||
{
|
||||
SourceList::iterator s = session_->at(index);
|
||||
if (s!=session_->end())
|
||||
return *s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Mixer::setCurrentIndex(int index)
|
||||
{
|
||||
setCurrentSource( session_->at(index) );
|
||||
@@ -862,11 +909,16 @@ void Mixer::unsetCurrentSource()
|
||||
}
|
||||
}
|
||||
|
||||
int Mixer::indexCurrentSource()
|
||||
int Mixer::indexCurrentSource() const
|
||||
{
|
||||
return current_source_index_;
|
||||
}
|
||||
|
||||
int Mixer::count() const
|
||||
{
|
||||
return (int) session_->numSource();
|
||||
}
|
||||
|
||||
Source *Mixer::currentSource()
|
||||
{
|
||||
if ( current_source_ != session_->end() )
|
||||
@@ -886,7 +938,7 @@ void Mixer::setView(View::Mode m)
|
||||
if ( se != nullptr )
|
||||
set ( se );
|
||||
else
|
||||
Log::Info("Transition interrupted: Session source added.");
|
||||
Log::Info("Transition interrupted.");
|
||||
}
|
||||
|
||||
switch (m) {
|
||||
@@ -939,13 +991,13 @@ View *Mixer::view(View::Mode m)
|
||||
}
|
||||
}
|
||||
|
||||
void Mixer::save()
|
||||
void Mixer::save(bool with_version)
|
||||
{
|
||||
if (!session_->filename().empty())
|
||||
saveas(session_->filename());
|
||||
saveas(session_->filename(), with_version);
|
||||
}
|
||||
|
||||
void Mixer::saveas(const std::string& filename)
|
||||
void Mixer::saveas(const std::string& filename, bool with_version)
|
||||
{
|
||||
// optional copy of views config
|
||||
session_->config(View::MIXING)->copyTransform( mixing_.scene.root() );
|
||||
@@ -954,7 +1006,7 @@ void Mixer::saveas(const std::string& filename)
|
||||
session_->config(View::TEXTURE)->copyTransform( appearance_.scene.root() );
|
||||
|
||||
// launch a thread to save the session
|
||||
std::thread (saveSession, filename, session_).detach();
|
||||
std::thread (saveSession, filename, session_, with_version).detach();
|
||||
}
|
||||
|
||||
void Mixer::load(const std::string& filename)
|
||||
@@ -1094,7 +1146,7 @@ void Mixer::merge(SessionSource *source)
|
||||
SourceList::iterator it = to_be_moved.begin();
|
||||
for (; it != to_be_moved.end(); ++it) {
|
||||
float scale_depth = (MAX_DEPTH-(*it)->depth()) / (MAX_DEPTH-next_depth);
|
||||
(*it)->setDepth( (*it)->depth() + scale_depth );
|
||||
(*it)->call( new SetDepth( (*it)->depth() + scale_depth ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1106,10 +1158,10 @@ void Mixer::merge(SessionSource *source)
|
||||
renameSource(s);
|
||||
|
||||
// scale alpha
|
||||
s->setAlpha( s->alpha() * source->alpha() );
|
||||
s->call( new SetAlpha(s->alpha() * source->alpha()));
|
||||
|
||||
// set depth (proportional to depth of s, adjusted by needed space)
|
||||
s->setDepth( target_depth + ( (s->depth()-start_depth)/ need_depth) );
|
||||
s->call( new SetDepth( target_depth + ( (s->depth()-start_depth)/ need_depth) ) );
|
||||
|
||||
// set location
|
||||
// a. transform of node to import
|
||||
@@ -1154,7 +1206,7 @@ void Mixer::merge(SessionSource *source)
|
||||
|
||||
void Mixer::swap()
|
||||
{
|
||||
if (!back_session_)
|
||||
if (!back_session_ || !session_)
|
||||
return;
|
||||
|
||||
if (session_) {
|
||||
@@ -1188,7 +1240,7 @@ void Mixer::swap()
|
||||
session_->setResolution( session_->config(View::RENDERING)->scale_ );
|
||||
|
||||
// transfer fading
|
||||
session_->setFading( MAX(back_session_->fading(), session_->fading()), true );
|
||||
session_->setFadingTarget( MAX(back_session_->fadingTarget(), session_->fadingTarget()));
|
||||
|
||||
// no current source
|
||||
current_source_ = session_->end();
|
||||
@@ -1237,6 +1289,7 @@ void Mixer::clear()
|
||||
// need to deeply update view to apply eventual changes
|
||||
++View::need_deep_update_;
|
||||
|
||||
Settings::application.recentSessions.front_is_valid = false;
|
||||
Log::Info("New session ready.");
|
||||
}
|
||||
|
||||
@@ -1256,6 +1309,17 @@ void Mixer::set(Session *s)
|
||||
sessionSwapRequested_ = true;
|
||||
}
|
||||
|
||||
void Mixer::setResolution(glm::vec3 res)
|
||||
{
|
||||
if (session_) {
|
||||
session_->setResolution(res);
|
||||
++View::need_deep_update_;
|
||||
std::ostringstream info;
|
||||
info << "Session resolution changed to " << res.x << "x" << res.y;
|
||||
Log::Info("%s", info.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Mixer::paste(const std::string& clipboard)
|
||||
{
|
||||
tinyxml2::XMLDocument xmlDoc;
|
||||
@@ -1283,7 +1347,6 @@ void Mixer::restore(tinyxml2::XMLElement *sessionNode)
|
||||
//
|
||||
// source lists
|
||||
//
|
||||
|
||||
// sessionsources contains list of ids of all sources currently in the session (before loading)
|
||||
SourceIdList session_sources = session_->getIdList();
|
||||
// for( auto it = sessionsources.begin(); it != sessionsources.end(); it++)
|
||||
|
||||
14
Mixer.h
14
Mixer.h
@@ -40,7 +40,7 @@ public:
|
||||
|
||||
// update session and all views
|
||||
void update();
|
||||
inline float dt() const { return dt_;}
|
||||
inline float dt() const { return dt_;} // in miliseconds
|
||||
inline int fps() const { return int(roundf(1000.f/dt__));}
|
||||
|
||||
// draw session and current view
|
||||
@@ -77,15 +77,18 @@ public:
|
||||
void setCurrentPrevious ();
|
||||
void unsetCurrentSource ();
|
||||
|
||||
Source *sourceAtIndex (int index);
|
||||
void setCurrentIndex (int index);
|
||||
void moveIndex (int current_index, int target_index);
|
||||
int indexCurrentSource ();
|
||||
int indexCurrentSource () const;
|
||||
int count() const;
|
||||
|
||||
// browsing into sources
|
||||
Source * findSource (Node *node);
|
||||
Source * findSource (std::string name);
|
||||
Source * findSource (uint64_t id);
|
||||
SourceList findSources (float depth_from, float depth_to);
|
||||
SourceList validate(const SourceList &list);
|
||||
|
||||
// management of view
|
||||
View *view (View::Mode m = View::INVALID);
|
||||
@@ -98,14 +101,15 @@ public:
|
||||
// manipulate, load and save sessions
|
||||
inline Session *session () const { return session_; }
|
||||
void clear ();
|
||||
void save ();
|
||||
void saveas (const std::string& filename);
|
||||
void save (bool with_version = false);
|
||||
void saveas (const std::string& filename, bool with_version = false);
|
||||
void load (const std::string& filename);
|
||||
void import (const std::string& filename);
|
||||
void import (SessionSource *source);
|
||||
void merge (Session *session);
|
||||
void merge (SessionSource *source);
|
||||
void set (Session *session);
|
||||
void setResolution(glm::vec3 res);
|
||||
|
||||
// operations depending on transition mode
|
||||
void close (bool smooth = false);
|
||||
@@ -113,6 +117,8 @@ public:
|
||||
|
||||
// create sources if clipboard contains well-formed xml text
|
||||
void paste (const std::string& clipboard);
|
||||
|
||||
// version and undo management
|
||||
void restore(tinyxml2::XMLElement *sessionNode);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
// non assignable class
|
||||
MixingGroup(MixingGroup const&) = delete;
|
||||
MixingGroup& operator=(MixingGroup const&) = delete;
|
||||
~MixingGroup ();
|
||||
virtual ~MixingGroup ();
|
||||
|
||||
// Get unique id
|
||||
inline uint64_t id () const { return id_; }
|
||||
|
||||
217
MixingView.cpp
217
MixingView.cpp
@@ -1,17 +1,34 @@
|
||||
// Opengl
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiToolkit.h"
|
||||
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "Mixer.h"
|
||||
#include "defines.h"
|
||||
#include "Source.h"
|
||||
@@ -31,7 +48,7 @@ uint textureMixingQuadratic();
|
||||
|
||||
|
||||
|
||||
MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
|
||||
MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_MIN_THRESHOLD)
|
||||
{
|
||||
scene.root()->scale_ = glm::vec3(MIXING_DEFAULT_SCALE, MIXING_DEFAULT_SCALE, 1.0f);
|
||||
scene.root()->translation_ = glm::vec3(0.0f, 0.0f, 0.0f);
|
||||
@@ -45,10 +62,30 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
|
||||
restoreSettings();
|
||||
|
||||
// Mixing scene background
|
||||
Mesh *tmp = new Mesh("mesh/disk.ply");
|
||||
tmp->scale_ = glm::vec3(limbo_scale_, limbo_scale_, 1.f);
|
||||
tmp->shader()->color = glm::vec4( COLOR_LIMBO_CIRCLE, 0.6f );
|
||||
scene.bg()->attach(tmp);
|
||||
limbo_ = new Mesh("mesh/disk.ply");
|
||||
limbo_->scale_ = glm::vec3(limbo_scale_, limbo_scale_, 1.f);
|
||||
limbo_->shader()->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
|
||||
scene.bg()->attach(limbo_);
|
||||
|
||||
// interactive limbo scaling slider
|
||||
limbo_slider_root_ = new Group;
|
||||
limbo_slider_root_->translation_ = glm::vec3(0.0f, limbo_scale_, 0.f);
|
||||
scene.bg()->attach(limbo_slider_root_);
|
||||
limbo_slider_ = new Disk();
|
||||
limbo_slider_->translation_ = glm::vec3(0.f, -0.01f, 0.f);
|
||||
limbo_slider_->scale_ = glm::vec3(0.1f, 0.1f, 1.f);
|
||||
limbo_slider_->color = glm::vec4( COLOR_LIMBO_CIRCLE, 1.f );
|
||||
limbo_slider_root_->attach(limbo_slider_);
|
||||
limbo_up_ = new Mesh("mesh/triangle_point.ply");
|
||||
limbo_up_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
|
||||
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
|
||||
limbo_slider_root_->attach(limbo_up_);
|
||||
limbo_down_ = new Mesh("mesh/triangle_point.ply");
|
||||
limbo_down_->translation_ = glm::vec3(0.f, -0.02f, 0.f);
|
||||
limbo_down_->scale_ = glm::vec3(0.8f, 0.8f, 1.f);
|
||||
limbo_down_->rotation_ = glm::vec3(0.f, 0.f, M_PI);
|
||||
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
|
||||
limbo_slider_root_->attach(limbo_down_);
|
||||
|
||||
mixingCircle_ = new Mesh("mesh/disk.ply");
|
||||
mixingCircle_->shader()->color = glm::vec4( 1.f, 1.f, 1.f, 1.f );
|
||||
@@ -60,8 +97,8 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
|
||||
|
||||
// Mixing scene foreground
|
||||
|
||||
// button frame
|
||||
tmp = new Mesh("mesh/disk.ply");
|
||||
// button frame (non interactive; no picking detected on Mesh)
|
||||
Mesh *tmp = new Mesh("mesh/disk.ply");
|
||||
tmp->scale_ = glm::vec3(0.033f, 0.033f, 1.f);
|
||||
tmp->translation_ = glm::vec3(0.f, 1.f, 0.f);
|
||||
tmp->shader()->color = glm::vec4( COLOR_CIRCLE, 0.9f );
|
||||
@@ -101,10 +138,10 @@ MixingView::MixingView() : View(MIXING), limbo_scale_(MIXING_LIMBO_SCALE)
|
||||
slider_root_->attach(tmp);
|
||||
|
||||
|
||||
stashCircle_ = new Disk();
|
||||
stashCircle_->scale_ = glm::vec3(0.5f, 0.5f, 1.f);
|
||||
stashCircle_->translation_ = glm::vec3(2.f, -1.0f, 0.f);
|
||||
stashCircle_->color = glm::vec4( COLOR_STASH_CIRCLE, 0.6f );
|
||||
// stashCircle_ = new Disk();
|
||||
// stashCircle_->scale_ = glm::vec3(0.5f, 0.5f, 1.f);
|
||||
// stashCircle_->translation_ = glm::vec3(2.f, -1.0f, 0.f);
|
||||
// stashCircle_->color = glm::vec4( COLOR_STASH_CIRCLE, 0.6f );
|
||||
// scene.bg()->attach(stashCircle_);
|
||||
|
||||
}
|
||||
@@ -175,7 +212,21 @@ void MixingView::draw()
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Center"));
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_HAYKAL " Distribute" )){
|
||||
if (ImGui::Selectable( ICON_FA_HAYKAL " Dispatch" )){
|
||||
glm::vec2 center = glm::vec2(0.f, 0.f);
|
||||
// distribute with equal angle
|
||||
float angle = 0.f;
|
||||
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
|
||||
|
||||
glm::vec2 P = center + glm::rotate(glm::vec2(0.f, 1.2), angle);
|
||||
(*it)->group(View::MIXING)->translation_.x = P.x;
|
||||
(*it)->group(View::MIXING)->translation_.y = P.y;
|
||||
(*it)->touch();
|
||||
angle -= glm::two_pi<float>() / float(Mixer::selection().size());
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Dispatch"));
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_FAN " Distribute" )){
|
||||
SourceList list;
|
||||
glm::vec2 center = glm::vec2(0.f, 0.f);
|
||||
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
|
||||
@@ -202,19 +253,40 @@ void MixingView::draw()
|
||||
(*it)->touch();
|
||||
angle -= glm::two_pi<float>() / float(list.size());
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Distribute"));
|
||||
Action::manager().store(std::string("Selection: Mixing Distribute in circle"));
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_ELLIPSIS_V " Align & Distribute" )){
|
||||
SourceList list;
|
||||
glm::vec2 center = glm::vec2(0.f, 0.f);
|
||||
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
|
||||
list.push_back(*it);
|
||||
// compute barycenter (1)
|
||||
center += glm::vec2((*it)->group(View::MIXING)->translation_);
|
||||
}
|
||||
// compute barycenter (2)
|
||||
center /= list.size();
|
||||
// distribute with equal angle
|
||||
float i = 0.f;
|
||||
float sign = -1.f;
|
||||
for (SourceList::iterator it = list.begin(); it != list.end(); ++it, sign*=-1.f) {
|
||||
(*it)->group(View::MIXING)->translation_.x = center.x;
|
||||
(*it)->group(View::MIXING)->translation_.y = center.y + sign * i;
|
||||
if (sign<0) i+=0.32f;
|
||||
(*it)->touch();
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Align & Distribute"));
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_CLOUD_SUN " Expand & hide" )){
|
||||
SourceList::iterator it = Mixer::selection().begin();
|
||||
for (; it != Mixer::selection().end(); ++it) {
|
||||
(*it)->setAlpha(0.f);
|
||||
(*it)->call( new SetAlpha(0.f) );
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Expand & hide"));
|
||||
}
|
||||
if (ImGui::Selectable( ICON_FA_SUN " Compress & show" )){
|
||||
SourceList::iterator it = Mixer::selection().begin();
|
||||
for (; it != Mixer::selection().end(); ++it) {
|
||||
(*it)->setAlpha(0.99f);
|
||||
(*it)->call( new SetAlpha(0.999f) );
|
||||
}
|
||||
Action::manager().store(std::string("Selection: Mixing Compress & show"));
|
||||
}
|
||||
@@ -233,7 +305,8 @@ void MixingView::resize ( int scale )
|
||||
scene.root()->scale_.y = z;
|
||||
|
||||
// Clamp translation to acceptable area
|
||||
glm::vec3 border(scene.root()->scale_.x * 1.f, scene.root()->scale_.y * 1.f, 0.f);
|
||||
glm::vec2 res = resolution();
|
||||
glm::vec3 border(2.3f * res.x/res.y, 2.3f, 0.f);
|
||||
scene.root()->translation_ = glm::clamp(scene.root()->translation_, -border, border);
|
||||
}
|
||||
|
||||
@@ -276,43 +349,34 @@ void MixingView::update(float dt)
|
||||
if (View::need_deep_update_ > 0) {
|
||||
|
||||
//
|
||||
// Set slider to match the actual fading of the session
|
||||
// Set limbo scale according to session
|
||||
//
|
||||
float f = Mixer::manager().session()->empty() ? 0.f : Mixer::manager().session()->fading();
|
||||
|
||||
// reverse calculate angle from fading & move slider
|
||||
slider_root_->rotation_.z = SIGN(slider_root_->rotation_.z) * asin(f) * 2.f;
|
||||
|
||||
// visual feedback on mixing circle
|
||||
f = 1.f - f;
|
||||
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
|
||||
const float p = CLAMP(Mixer::manager().session()->activationThreshold(), MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
|
||||
limbo_slider_root_->translation_.y = p;
|
||||
limbo_->scale_ = glm::vec3(p, p, 1.f);
|
||||
|
||||
//
|
||||
// prevent invalid scaling
|
||||
//
|
||||
float s = CLAMP(scene.root()->scale_.x, MIXING_MIN_SCALE, MIXING_MAX_SCALE);
|
||||
scene.root()->scale_.x = s;
|
||||
scene.root()->scale_.y = s;
|
||||
}
|
||||
|
||||
// the current view is the mixing view
|
||||
if (Mixer::manager().view() == this )
|
||||
{
|
||||
if (Mixer::manager().view() == this ){
|
||||
|
||||
//
|
||||
// Set session fading to match the slider angle (during animation)
|
||||
// Set slider to match the actual fading of the session
|
||||
//
|
||||
float f = Mixer::manager().session()->fading();
|
||||
|
||||
// calculate fading from angle
|
||||
float f = sin( ABS(slider_root_->rotation_.z) * 0.5f);
|
||||
// reverse calculate angle from fading & move slider
|
||||
slider_root_->rotation_.z = SIGN(-slider_root_->rotation_.z) * asin(f) * -2.f;
|
||||
|
||||
// apply fading
|
||||
if ( ABS_DIFF( f, Mixer::manager().session()->fading()) > EPSILON )
|
||||
{
|
||||
// apply fading to session
|
||||
Mixer::manager().session()->setFading(f);
|
||||
|
||||
// visual feedback on mixing circle
|
||||
f = 1.f - f;
|
||||
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
|
||||
}
|
||||
// visual feedback on mixing circle
|
||||
f = 1.f - f;
|
||||
mixingCircle_->shader()->color = glm::vec4(f, f, f, 1.f);
|
||||
|
||||
// update the selection overlay
|
||||
updateSelectionOverlay();
|
||||
@@ -329,18 +393,14 @@ std::pair<Node *, glm::vec2> MixingView::pick(glm::vec2 P)
|
||||
// deal with internal interactive objects
|
||||
if ( pick.first == button_white_ || pick.first == button_black_ ) {
|
||||
|
||||
RotateToCallback *anim = nullptr;
|
||||
if (pick.first == button_white_)
|
||||
anim = new RotateToCallback(0.f, 500.f);
|
||||
else
|
||||
anim = new RotateToCallback(SIGN(slider_root_->rotation_.z) * M_PI, 500.f);
|
||||
|
||||
// animate clic
|
||||
pick.first->update_callbacks_.push_back(new BounceScaleCallback(0.3f));
|
||||
|
||||
// reset & start animation
|
||||
slider_root_->update_callbacks_.clear();
|
||||
slider_root_->update_callbacks_.push_back(anim);
|
||||
// animated fading in session
|
||||
if (pick.first == button_white_)
|
||||
Mixer::manager().session()->setFadingTarget(0.f, 500.f);
|
||||
else
|
||||
Mixer::manager().session()->setFadingTarget(1.f, 500.f);
|
||||
|
||||
}
|
||||
else if ( overlay_selection_icon_ != nullptr && pick.first == overlay_selection_icon_ ) {
|
||||
@@ -425,10 +485,30 @@ View::Cursor MixingView::grab (Source *s, glm::vec2 from, glm::vec2 to, std::pai
|
||||
// animate slider (rotation angle on its parent)
|
||||
slider_root_->rotation_.z = angle;
|
||||
|
||||
// calculate fading from angle
|
||||
float f = sin( ABS(angle) * 0.5f);
|
||||
Mixer::manager().session()->setFadingTarget(f);
|
||||
|
||||
// cursor feedback
|
||||
slider_->color = glm::vec4( COLOR_CIRCLE_OVER, 0.9f );
|
||||
std::ostringstream info;
|
||||
info << "Global opacity " << 100 - int(Mixer::manager().session()->fading() * 100.0) << " %";
|
||||
info << "Output " << 100 - int(f * 100.0) << " %";
|
||||
return Cursor(Cursor_Hand, info.str() );
|
||||
}
|
||||
else if (pick.first == limbo_slider_) {
|
||||
|
||||
// move slider scaling limbo area
|
||||
const float p = CLAMP(gl_Position_to.y, MIXING_MIN_THRESHOLD, MIXING_MAX_THRESHOLD);
|
||||
limbo_slider_root_->translation_.y = p;
|
||||
limbo_->scale_ = glm::vec3(p, p, 1.f);
|
||||
Mixer::manager().session()->setActivationThreshold(p);
|
||||
|
||||
// color change of arrow indicators
|
||||
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p < MIXING_MAX_THRESHOLD ? 0.15f : 0.01f );
|
||||
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, p > MIXING_MIN_THRESHOLD ? 0.15f : 0.01f );
|
||||
|
||||
std::ostringstream info;
|
||||
info << ICON_FA_SNOWFLAKE " Deactivation limit";
|
||||
return Cursor(Cursor_Hand, info.str() );
|
||||
}
|
||||
|
||||
@@ -514,14 +594,24 @@ View::Cursor MixingView::over (glm::vec2 pos)
|
||||
else
|
||||
slider_->color = glm::vec4( COLOR_CIRCLE, 0.9f );
|
||||
|
||||
if ( pick.first == limbo_slider_ ) {
|
||||
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y < MIXING_MAX_THRESHOLD ? 0.1f : 0.01f );
|
||||
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, limbo_slider_root_->translation_.y > MIXING_MIN_THRESHOLD ? 0.1f : 0.01f );
|
||||
ret.type = Cursor_Hand;
|
||||
}
|
||||
else {
|
||||
limbo_up_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
|
||||
limbo_down_->shader()->color = glm::vec4( COLOR_CIRCLE_OVER, 0.01f );
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MixingView::arrow (glm::vec2 movement)
|
||||
{
|
||||
static int accumulator = 0;
|
||||
accumulator++;
|
||||
static float accumulator = 0.f;
|
||||
accumulator += dt_;
|
||||
|
||||
glm::vec3 gl_Position_from = Rendering::manager().unProject(glm::vec2(0.f), scene.root()->transform_);
|
||||
glm::vec3 gl_Position_to = Rendering::manager().unProject(movement, scene.root()->transform_);
|
||||
@@ -544,18 +634,19 @@ void MixingView::arrow (glm::vec2 movement)
|
||||
|
||||
// + ALT : discrete displacement
|
||||
if (UserInterface::manager().altModifier()) {
|
||||
if (accumulator > 10) {
|
||||
dest_translation += glm::sign(gl_delta) * 0.11f;
|
||||
if (accumulator > 100.f) {
|
||||
dest_translation += glm::sign(gl_delta) * 0.1f;
|
||||
dest_translation.x = ROUND(dest_translation.x, 10.f);
|
||||
dest_translation.y = ROUND(dest_translation.y, 10.f);
|
||||
accumulator = 0;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// normal case: dest += delta
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR;
|
||||
dest_translation += gl_delta * ARROWS_MOVEMENT_FACTOR * dt_;
|
||||
accumulator = 0.f;
|
||||
}
|
||||
|
||||
// store action in history
|
||||
@@ -625,7 +716,7 @@ void MixingView::updateSelectionOverlay()
|
||||
}
|
||||
|
||||
#define CIRCLE_PIXELS 64
|
||||
#define CIRCLE_PIXEL_RADIUS 1024.0
|
||||
#define CIRCLE_PIXEL_RADIUS 1024.f
|
||||
//#define CIRCLE_PIXELS 256
|
||||
//#define CIRCLE_PIXEL_RADIUS 16384.0
|
||||
//#define CIRCLE_PIXELS 1024
|
||||
|
||||
@@ -26,7 +26,6 @@ public:
|
||||
void arrow (glm::vec2) override;
|
||||
|
||||
void setAlpha (Source *s);
|
||||
inline float limboScale() { return limbo_scale_; }
|
||||
|
||||
private:
|
||||
void updateSelectionOverlay() override;
|
||||
@@ -37,9 +36,13 @@ private:
|
||||
Disk *slider_;
|
||||
Disk *button_white_;
|
||||
Disk *button_black_;
|
||||
Disk *stashCircle_;
|
||||
// Disk *stashCircle_;
|
||||
Mesh *mixingCircle_;
|
||||
Mesh *circle_;
|
||||
Mesh *limbo_;
|
||||
Group *limbo_slider_root_;
|
||||
Mesh *limbo_up_, *limbo_down_;
|
||||
Disk *limbo_slider_;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
|
||||
@@ -35,7 +54,8 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
|
||||
// sanity check: the location pattern looks like a filename and seems consecutive numbered
|
||||
if ( SystemToolkit::extension_filename(location).empty() ||
|
||||
SystemToolkit::path_filename(location) != SystemToolkit::path_filename(list_files.front()) ||
|
||||
list_files.size() != max - min + 1 ) {
|
||||
list_files.size() != (size_t) (max - min) + 1 ) {
|
||||
Log::Info("MultiFileSequence '%s' invalid.", location.c_str());
|
||||
location.clear();
|
||||
}
|
||||
|
||||
@@ -47,6 +67,8 @@ MultiFileSequence::MultiFileSequence(const std::list<std::string> &list_files)
|
||||
width = media.width;
|
||||
height = media.height;
|
||||
}
|
||||
else
|
||||
Log::Info("MultiFileSequence '%s' does not list images.", location.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +137,22 @@ void MultiFile::close ()
|
||||
Stream::close();
|
||||
}
|
||||
|
||||
void MultiFile::setIndex(int val)
|
||||
{
|
||||
if (src_) {
|
||||
g_object_set (src_, "index", val, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
int MultiFile::index()
|
||||
{
|
||||
int val = 0;
|
||||
if (src_) {
|
||||
g_object_get (src_, "index", &val, NULL);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
void MultiFile::setProperties (int begin, int end, int loop)
|
||||
{
|
||||
if (src_) {
|
||||
@@ -187,6 +225,27 @@ void MultiFileSource::setRange (int begin, int end)
|
||||
multifile()->setProperties (begin_, end_, loop_);
|
||||
}
|
||||
|
||||
void MultiFileSource::replay ()
|
||||
{
|
||||
if (multifile()) {
|
||||
multifile()->setIndex (begin_);
|
||||
stream_->rewind();
|
||||
}
|
||||
}
|
||||
|
||||
guint64 MultiFileSource::playtime () const
|
||||
{
|
||||
guint64 time = 0;
|
||||
|
||||
if (multifile())
|
||||
time += multifile()->index();
|
||||
|
||||
time *= GST_SECOND;
|
||||
time /= framerate_;
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
void MultiFileSource::accept (Visitor& v)
|
||||
{
|
||||
Source::accept(v);
|
||||
@@ -200,3 +259,14 @@ MultiFile *MultiFileSource::multifile () const
|
||||
}
|
||||
|
||||
|
||||
glm::ivec2 MultiFileSource::icon () const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_SEQUENCE);
|
||||
}
|
||||
|
||||
std::string MultiFileSource::info() const
|
||||
{
|
||||
return std::string("sequence '") + sequence_.location + "'";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ public:
|
||||
// dynamic change of gstreamer multifile source properties
|
||||
void setProperties(int begin, int end, int loop);
|
||||
|
||||
// image index
|
||||
int index();
|
||||
void setIndex(int val);
|
||||
|
||||
protected:
|
||||
GstElement *src_ ;
|
||||
};
|
||||
@@ -42,10 +46,13 @@ public:
|
||||
|
||||
// Source interface
|
||||
void accept (Visitor& v) override;
|
||||
void replay () override;
|
||||
guint64 playtime () const override;
|
||||
|
||||
// StreamSource interface
|
||||
Stream *stream () const override { return stream_; }
|
||||
glm::ivec2 icon () const override { return glm::ivec2(3, 9); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
// specific interface
|
||||
void setFiles (const std::list<std::string> &list_files, uint framerate);
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
@@ -5,17 +24,17 @@
|
||||
#include <future>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "osc/OscOutboundPacketStream.h"
|
||||
|
||||
#include "SystemToolkit.h"
|
||||
#include "defines.h"
|
||||
#include "Stream.h"
|
||||
#include "Decorations.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
#include "Connection.h"
|
||||
|
||||
#include "NetworkSource.h"
|
||||
|
||||
@@ -25,7 +44,7 @@
|
||||
|
||||
|
||||
// this is called when receiving an answer for streaming request
|
||||
void StreamerResponseListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
void NetworkStream::ResponseListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint )
|
||||
{
|
||||
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
|
||||
@@ -242,22 +261,34 @@ void NetworkStream::update()
|
||||
// general case : create pipeline and open
|
||||
if (!failed_) {
|
||||
// build the pipeline depending on stream info
|
||||
std::ostringstream pipeline;
|
||||
// get generic pipeline string
|
||||
std::string pipelinestring = NetworkToolkit::protocol_receive_pipeline[config_.protocol];
|
||||
// find placeholder for PORT
|
||||
int xxxx = pipelinestring.find("XXXX");
|
||||
// keep beginning of pipeline
|
||||
pipeline << pipelinestring.substr(0, xxxx);
|
||||
// Replace 'XXXX' by info on port config
|
||||
pipeline << parameter;
|
||||
// keep ending of pipeline
|
||||
pipeline << pipelinestring.substr(xxxx + 4);
|
||||
// add a videoconverter
|
||||
pipeline << " ! videoconvert";
|
||||
|
||||
// find placeholder for PORT or SHH socket
|
||||
size_t xxxx = pipelinestring.find("XXXX");
|
||||
if (xxxx != std::string::npos)
|
||||
// Replace 'XXXX' by info on port config
|
||||
pipelinestring.replace(xxxx, 4, parameter);
|
||||
|
||||
// find placeholder for WIDTH
|
||||
size_t wwww = pipelinestring.find("WWWW");
|
||||
if (wwww != std::string::npos)
|
||||
// Replace 'WWWW' by width
|
||||
pipelinestring.replace(wwww, 4, std::to_string(config_.width) );
|
||||
|
||||
// find placeholder for HEIGHT
|
||||
size_t hhhh = pipelinestring.find("HHHH");
|
||||
if (hhhh != std::string::npos)
|
||||
// Replace 'WWWW' by height
|
||||
pipelinestring.replace(hhhh, 4, std::to_string(config_.height) );
|
||||
|
||||
// add a videoconverter
|
||||
pipelinestring.append(" ! videoconvert ");
|
||||
|
||||
#ifdef NETWORK_DEBUG
|
||||
Log::Info("Openning pipeline %s", pipelinestring.c_str());
|
||||
#endif
|
||||
// open the pipeline with generic stream class
|
||||
Stream::open(pipeline.str(), config_.width, config_.height);
|
||||
Stream::open(pipelinestring, config_.width, config_.height);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -292,7 +323,6 @@ NetworkStream *NetworkSource::networkStream() const
|
||||
void NetworkSource::setConnection(const std::string &nameconnection)
|
||||
{
|
||||
connection_name_ = nameconnection;
|
||||
Log::Notify("Network Source connecting to '%s'", connection_name_.c_str());
|
||||
|
||||
// open network stream
|
||||
networkStream()->connect( connection_name_ );
|
||||
@@ -315,5 +345,13 @@ void NetworkSource::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
glm::ivec2 NetworkSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_NETWORK);
|
||||
}
|
||||
|
||||
std::string NetworkSource::info() const
|
||||
{
|
||||
return std::string("connected to '") + connection_name_ + "'";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
#ifndef NETWORKSOURCE_H
|
||||
#define NETWORKSOURCE_H
|
||||
|
||||
#include "osc/OscReceivedElements.h"
|
||||
#include "osc/OscPacketListener.h"
|
||||
#include "osc/OscOutboundPacketStream.h"
|
||||
#include "ip/UdpSocket.h"
|
||||
|
||||
#include "NetworkToolkit.h"
|
||||
#include "Connection.h"
|
||||
#include "StreamSource.h"
|
||||
|
||||
class NetworkStream;
|
||||
|
||||
class StreamerResponseListener : public osc::OscPacketListener
|
||||
{
|
||||
protected:
|
||||
class NetworkStream *parent_;
|
||||
virtual void ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint );
|
||||
public:
|
||||
inline void setParent(NetworkStream *s) { parent_ = s; }
|
||||
StreamerResponseListener() : parent_(nullptr) {}
|
||||
};
|
||||
|
||||
|
||||
class NetworkStream : public Stream
|
||||
{
|
||||
friend class StreamerResponseListener;
|
||||
|
||||
public:
|
||||
|
||||
NetworkStream();
|
||||
@@ -43,10 +23,22 @@ public:
|
||||
std::string clientAddress() const;
|
||||
std::string serverAddress() const;
|
||||
|
||||
protected:
|
||||
class ResponseListener : public osc::OscPacketListener
|
||||
{
|
||||
protected:
|
||||
class NetworkStream *parent_;
|
||||
virtual void ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint );
|
||||
public:
|
||||
inline void setParent(NetworkStream *s) { parent_ = s; }
|
||||
ResponseListener() : parent_(nullptr) {}
|
||||
};
|
||||
|
||||
private:
|
||||
// connection information
|
||||
ConnectionInfo streamer_;
|
||||
StreamerResponseListener listener_;
|
||||
ResponseListener listener_;
|
||||
UdpListeningReceiveSocket *receiver_;
|
||||
std::atomic<bool> received_config_;
|
||||
std::atomic<bool> connected_;
|
||||
@@ -74,7 +66,8 @@ public:
|
||||
void setConnection(const std::string &nameconnection);
|
||||
std::string connection() const;
|
||||
|
||||
glm::ivec2 icon() const override { return glm::ivec2(18, 11); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -60,32 +78,48 @@
|
||||
* RCV
|
||||
* gst-launch-1.0 shmsrc is-live=true socket-path=/tmp/blah ! video/x-raw, format=RGB, framerate=30/1, width=320, height=240 ! videoconvert ! autovideosink
|
||||
*
|
||||
* RTP UDP JPEG
|
||||
*
|
||||
* SND
|
||||
* gst-launch-1.0 videotestsrc is-live=true ! video/x-raw, format=RGB, framerate=30/1 ! videoconvert ! video/x-raw, format=I420 ! jpegenc quality=95 ! rtpjpegpay ! udpsink port=5000 host=127.0.0
|
||||
* RCV
|
||||
* gst-launch-1.0 udpsrc buffer-size=200000 port=5000 ! application/x-rtp,encoding-name=JPEG ! rtpjpegdepay ! queue max-size-buffers=10 ! jpegdec ! videoconvert ! video/x-raw, format=RGB ! autovideosink
|
||||
*
|
||||
* */
|
||||
|
||||
const char* NetworkToolkit::protocol_name[NetworkToolkit::DEFAULT] = {
|
||||
"Shared Memory",
|
||||
"RTP JPEG Stream",
|
||||
"RTP H264 Stream",
|
||||
"RTP JPEG Broadcast",
|
||||
"RTP H264 Broadcast"
|
||||
"RAW Images",
|
||||
"JPEG Stream",
|
||||
"H264 Stream",
|
||||
"JPEG Broadcast",
|
||||
"H264 Broadcast",
|
||||
"RGB Shared Memory"
|
||||
};
|
||||
|
||||
const std::vector<std::string> NetworkToolkit::protocol_send_pipeline {
|
||||
|
||||
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10 ! shmsink buffer-time=100000 wait-for-connection=true name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! jpegenc ! rtpjpegpay ! udpsink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! udpsink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! jpegenc ! rtpjpegpay ! rtpstreampay ! tcpserversink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! rtpstreampay ! tcpserversink name=sink"
|
||||
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=3 ! rtpvrawpay ! application/x-rtp,sampling=RGB ! udpsink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! jpegenc quality=85 ! rtpjpegpay ! udpsink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! rtph264pay aggregate-mode=1 ! udpsink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! jpegenc idct-method=float ! rtpjpegpay ! rtpstreampay ! tcpserversink name=sink",
|
||||
"video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=3 ! x264enc tune=\"zerolatency\" threads=2 ! rtph264pay ! rtpstreampay ! tcpserversink name=sink",
|
||||
"video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10 ! shmsink buffer-time=100000 wait-for-connection=true name=sink"
|
||||
};
|
||||
|
||||
const std::vector<std::string> NetworkToolkit::protocol_receive_pipeline {
|
||||
|
||||
"udpsrc buffer-size=200000 port=XXXX caps=\"application/x-rtp,media=(string)video,encoding-name=(string)RAW,sampling=(string)RGB,width=(string)WWWW,height=(string)HHHH\" ! rtpvrawdepay ! queue max-size-buffers=10",
|
||||
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=JPEG ! rtpjpegdepay ! queue max-size-buffers=10 ! jpegdec",
|
||||
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=H264 ! rtph264depay ! queue max-size-buffers=10 ! avdec_h264",
|
||||
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=JPEG ! rtpstreamdepay ! rtpjpegdepay ! jpegdec",
|
||||
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=H264 ! rtpstreamdepay ! rtph264depay ! avdec_h264",
|
||||
"shmsrc socket-path=XXXX ! video/x-raw, format=RGB, framerate=30/1 ! queue max-size-buffers=10",
|
||||
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=JPEG,payload=26,clock-rate=90000 ! queue max-size-buffers=10 ! rtpjpegdepay ! jpegdec",
|
||||
"udpsrc buffer-size=200000 port=XXXX ! application/x-rtp,encoding-name=H264,payload=96,clock-rate=90000 ! queue ! rtph264depay ! avdec_h264",
|
||||
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=JPEG,payload=26,clock-rate=90000 ! rtpstreamdepay ! rtpjpegdepay ! jpegdec",
|
||||
"tcpclientsrc timeout=1 port=XXXX ! queue max-size-buffers=30 ! application/x-rtp-stream,media=video,encoding-name=H264,payload=96,clock-rate=90000 ! rtpstreamdepay ! rtph264depay ! avdec_h264"
|
||||
};
|
||||
|
||||
const std::vector< std::pair<std::string, std::string> > NetworkToolkit::protocol_h264_send_pipeline {
|
||||
// {"vtenc_h264_hw", "video/x-raw, format=I420, framerate=30/1 ! queue max-size-buffers=10 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! rtph264pay aggregate-mode=1 ! udpsink name=sink"},
|
||||
{"nvh264enc", "video/x-raw, format=RGBA, framerate=30/1 ! queue max-size-buffers=10 ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! rtph264pay aggregate-mode=1 ! udpsink name=sink"},
|
||||
{"vaapih264enc", "video/x-raw, format=NV12, framerate=30/1 ! queue max-size-buffers=10 ! vaapih264enc rate-control=cqp init-qp=26 ! video/x-h264, profile=(string)main ! rtph264pay aggregate-mode=1 ! udpsink name=sink"}
|
||||
};
|
||||
|
||||
bool initialized_ = false;
|
||||
@@ -99,8 +133,7 @@ void add_interface(int fd, const char *name) {
|
||||
strncpy(ifreq.ifr_name, name, IFNAMSIZ);
|
||||
if(ioctl(fd, SIOCGIFADDR, &ifreq)==0) {
|
||||
char host[128];
|
||||
int family;
|
||||
switch(family=ifreq.ifr_addr.sa_family) {
|
||||
switch(ifreq.ifr_addr.sa_family) {
|
||||
case AF_INET:
|
||||
case AF_INET6:
|
||||
getnameinfo(&ifreq.ifr_addr, sizeof ifreq.ifr_addr, host, sizeof host, 0, 0, NI_NUMERICHOST);
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "osc/OscReceivedElements.h"
|
||||
#include "osc/OscPacketListener.h"
|
||||
#include "ip/UdpSocket.h"
|
||||
|
||||
#define OSC_SEPARATOR '/'
|
||||
#define OSC_PREFIX "/vimix"
|
||||
#define OSC_PING "/ping"
|
||||
#define OSC_PONG "/pong"
|
||||
@@ -12,22 +17,18 @@
|
||||
#define OSC_STREAM_REJECT "/reject"
|
||||
#define OSC_STREAM_DISCONNECT "/disconnect"
|
||||
|
||||
|
||||
#define MAX_HANDSHAKE 20
|
||||
#define HANDSHAKE_PORT 71310
|
||||
#define STREAM_REQUEST_PORT 71510
|
||||
#define OSC_DIALOG_PORT 71010
|
||||
#define IP_MTU_SIZE 1536
|
||||
|
||||
namespace NetworkToolkit
|
||||
{
|
||||
|
||||
typedef enum {
|
||||
SHM_RAW = 0,
|
||||
UDP_RAW = 0,
|
||||
UDP_JPEG,
|
||||
UDP_H264,
|
||||
TCP_JPEG,
|
||||
TCP_H264,
|
||||
SHM_RAW,
|
||||
DEFAULT
|
||||
} Protocol;
|
||||
|
||||
@@ -66,6 +67,7 @@ struct StreamConfig {
|
||||
|
||||
extern const char* protocol_name[DEFAULT];
|
||||
extern const std::vector<std::string> protocol_send_pipeline;
|
||||
extern const std::vector< std::pair<std::string, std::string> > protocol_h264_send_pipeline;
|
||||
extern const std::vector<std::string> protocol_receive_pipeline;
|
||||
|
||||
std::string hostname();
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <sstream>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "PatternSource.h"
|
||||
|
||||
#include "defines.h"
|
||||
#include "ImageShader.h"
|
||||
#include "Resource.h"
|
||||
@@ -10,94 +27,70 @@
|
||||
#include "Stream.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
#include "GstToolkit.h"
|
||||
|
||||
#define MAX_PATTERN 24
|
||||
#include "PatternSource.h"
|
||||
|
||||
// smpte (0) – SMPTE 100%% color bars
|
||||
// snow (1) – Random (television snow)
|
||||
// black (2) – 100%% Black
|
||||
// white (3) – 100%% White
|
||||
// red (4) – Red
|
||||
// green (5) – Green
|
||||
// blue (6) – Blue
|
||||
// checkers-1 (7) – Checkers 1px
|
||||
// checkers-2 (8) – Checkers 2px
|
||||
// checkers-4 (9) – Checkers 4px
|
||||
// checkers-8 (10) – Checkers 8px
|
||||
// circular (11) – Circular
|
||||
// blink (12) – Blink
|
||||
// smpte75 (13) – SMPTE 75%% color bars
|
||||
// zone-plate (14) – Zone plate
|
||||
// gamut (15) – Gamut checkers
|
||||
// chroma-zone-plate (16) – Chroma zone plate
|
||||
// solid-color (17) – Solid color
|
||||
// ball (18) – Moving ball
|
||||
// smpte100 (19) – SMPTE 100%% color bars
|
||||
// bar (20) – Bar
|
||||
// pinwheel (21) – Pinwheel
|
||||
// spokes (22) – Spokes
|
||||
// gradient (23) – Gradient
|
||||
// colors (24) – Colors
|
||||
const char* pattern_internal_[MAX_PATTERN] = { "videotestsrc pattern=black",
|
||||
"videotestsrc pattern=white",
|
||||
"videotestsrc pattern=gradient",
|
||||
"videotestsrc pattern=checkers-1 ! video/x-raw,format=GRAY8 ! videoconvert",
|
||||
"videotestsrc pattern=checkers-8 ! video/x-raw,format=GRAY8 ! videoconvert",
|
||||
"videotestsrc pattern=circular",
|
||||
"frei0r-src-lissajous0r ratiox=0.001 ratioy=0.999 ! videoconvert",
|
||||
"videotestsrc pattern=pinwheel",
|
||||
"videotestsrc pattern=spokes",
|
||||
"videotestsrc pattern=red",
|
||||
"videotestsrc pattern=green",
|
||||
"videotestsrc pattern=blue",
|
||||
"videotestsrc pattern=smpte100",
|
||||
"videotestsrc pattern=colors",
|
||||
"videotestsrc pattern=smpte",
|
||||
"videotestsrc pattern=snow",
|
||||
"videotestsrc pattern=blink",
|
||||
"videotestsrc pattern=zone-plate",
|
||||
"videotestsrc pattern=chroma-zone-plate",
|
||||
"videotestsrc pattern=bar horizontal-speed=5",
|
||||
"videotestsrc pattern=ball",
|
||||
"frei0r-src-ising0r",
|
||||
"videotestsrc pattern=black ! timeoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ",
|
||||
"videotestsrc pattern=black ! clockoverlay halignment=center valignment=center font-desc=\"Sans, 72\" "
|
||||
};
|
||||
//
|
||||
// Fill the list of patterns videotestsrc
|
||||
//
|
||||
// Label (for display), feature (for test), pipeline (for gstreamer), animated (true/false), available (false by default)
|
||||
std::vector<pattern_descriptor> Pattern::patterns_ = {
|
||||
{ "Black", "videotestsrc", "videotestsrc pattern=black", false, false },
|
||||
{ "White", "videotestsrc", "videotestsrc pattern=white", false, false },
|
||||
{ "Gradient", "videotestsrc", "videotestsrc pattern=gradient", false, false },
|
||||
{ "Checkers 1x1 px", "videotestsrc", "videotestsrc pattern=checkers-1 ! videobalance saturation=0 contrast=1.5", false, false },
|
||||
{ "Checkers 8x8 px", "videotestsrc", "videotestsrc pattern=checkers-8 ! videobalance saturation=0 contrast=1.5", false, false },
|
||||
{ "Circles", "videotestsrc", "videotestsrc pattern=circular", false, false },
|
||||
{ "Lissajous", "frei0r-src-lissajous0r", "frei0r-src-lissajous0r ratiox=0.001 ratioy=0.999 ! videoconvert", false, false },
|
||||
{ "Pinwheel", "videotestsrc", "videotestsrc pattern=pinwheel", false, false },
|
||||
{ "Spokes", "videotestsrc", "videotestsrc pattern=spokes", false, false },
|
||||
{ "Red", "videotestsrc", "videotestsrc pattern=red", false, false },
|
||||
{ "Green", "videotestsrc", "videotestsrc pattern=green", false, false },
|
||||
{ "Blue", "videotestsrc", "videotestsrc pattern=blue", false, false },
|
||||
{ "Color bars", "videotestsrc", "videotestsrc pattern=smpte100", false, false },
|
||||
{ "RGB grid", "videotestsrc", "videotestsrc pattern=colors", false, false },
|
||||
{ "SMPTE test pattern", "videotestsrc", "videotestsrc pattern=smpte", true, false },
|
||||
{ "Television snow", "videotestsrc", "videotestsrc pattern=snow", true, false },
|
||||
{ "Blink", "videotestsrc", "videotestsrc pattern=blink", true, false },
|
||||
{ "Fresnel zone plate", "videotestsrc", "videotestsrc pattern=zone-plate kx2=XXX ky2=YYY kt=4", true, false },
|
||||
{ "Chroma zone plate", "videotestsrc", "videotestsrc pattern=chroma-zone-plate kx2=XXX ky2=YYY kt=4", true, false },
|
||||
{ "Bar moving", "videotestsrc", "videotestsrc pattern=bar horizontal-speed=5", true, false },
|
||||
{ "Ball bouncing", "videotestsrc", "videotestsrc pattern=ball", true, false },
|
||||
{ "Blob", "frei0r-src-ising0r", "frei0r-src-ising0r", true, false },
|
||||
{ "Timer", "timeoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! timeoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ", true, false },
|
||||
{ "Clock", "clockoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! clockoverlay halignment=center valignment=center font-desc=\"Sans, 72\" ", true, false },
|
||||
{ "Resolution", "textoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! textoverlay text=\"XXXX x YYYY px\" halignment=center valignment=center font-desc=\"Sans, 52\" ", false, false },
|
||||
{ "Frame", "videobox", "videotestsrc pattern=solid-color foreground-color=0 ! videobox fill=white top=-10 bottom=-10 left=-10 right=-10", false, false },
|
||||
{ "Cross", "textoverlay", "videotestsrc pattern=solid-color foreground-color=0 ! textoverlay text=\"+\" halignment=center valignment=center font-desc=\"Sans, 22\" ", false, false },
|
||||
{ "Grid", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.35", false, false },
|
||||
{ "Point Grid", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.4", false, false },
|
||||
{ "Ruler", "frei0r-src-test-pat-g", "frei0r-src-test-pat-g type=0.9", false, false },
|
||||
{ "RGB noise", "frei0r-filter-rgbnoise", "videotestsrc pattern=black ! frei0r-filter-rgbnoise noise=0.6", true, false },
|
||||
{ "Philips test pattern", "frei0r-src-test-pat-b", "frei0r-src-test-pat-b type=0.7 ", false, false }
|
||||
};
|
||||
|
||||
std::vector<std::string> Pattern::pattern_types = { "Black",
|
||||
"White",
|
||||
"Gradient",
|
||||
"Checkers 1x1 px",
|
||||
"Checkers 8x8 px",
|
||||
"Circles",
|
||||
"Lissajous",
|
||||
"Pinwheel",
|
||||
"Spokes",
|
||||
"Red",
|
||||
"Green",
|
||||
"Blue",
|
||||
"Color bars",
|
||||
"RGB grid",
|
||||
"SMPTE test pattern",
|
||||
"Television snow",
|
||||
"Blink",
|
||||
"Fresnel zone plate",
|
||||
"Chroma zone plate",
|
||||
"Bar moving",
|
||||
"Ball bouncing"
|
||||
#if GST_VERSION_MINOR > 17
|
||||
,
|
||||
"Blob",
|
||||
"Timer",
|
||||
"Clock"
|
||||
#endif
|
||||
};
|
||||
|
||||
Pattern::Pattern() : Stream(), type_(MAX_PATTERN) // invalid pattern
|
||||
Pattern::Pattern() : Stream(), type_(UINT_MAX) // invalid pattern
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
pattern_descriptor Pattern::get(uint type)
|
||||
{
|
||||
// check availability of feature to use this pattern
|
||||
if (!patterns_[type].available)
|
||||
patterns_[type].available = GstToolkit::has_feature(patterns_[type].feature);
|
||||
|
||||
// return struct
|
||||
return patterns_[type];
|
||||
}
|
||||
|
||||
uint Pattern::count()
|
||||
{
|
||||
return patterns_.size();
|
||||
}
|
||||
|
||||
glm::ivec2 Pattern::resolution()
|
||||
{
|
||||
return glm::ivec2( width_, height_);
|
||||
@@ -106,26 +99,34 @@ glm::ivec2 Pattern::resolution()
|
||||
|
||||
void Pattern::open( uint pattern, glm::ivec2 res )
|
||||
{
|
||||
type_ = MIN(pattern, MAX_PATTERN-1);
|
||||
std::string gstreamer_pattern = pattern_internal_[type_];
|
||||
// clamp type to be sure
|
||||
type_ = MIN(pattern, Pattern::patterns_.size()-1);
|
||||
std::string gstreamer_pattern = Pattern::patterns_[type_].pipeline;
|
||||
|
||||
// there is always a special case...
|
||||
switch(type_)
|
||||
{
|
||||
case 18: // zone plates
|
||||
case 17:
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << " kx2=" << (int)(res.x * 10.f / res.y) << " ky2=10 kt=4";
|
||||
gstreamer_pattern += oss.str(); // Zone plate
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
//
|
||||
// pattern string post-processing: replace placeholders by resolution values
|
||||
// XXXX, YYYY = resolution x and y
|
||||
// XXX, YYY = resolution x and y / 10
|
||||
//
|
||||
// if there is a XXXX parameter to enter
|
||||
std::string::size_type xxxx = gstreamer_pattern.find("XXXX");
|
||||
if (xxxx != std::string::npos)
|
||||
gstreamer_pattern = gstreamer_pattern.replace(xxxx, 4, std::to_string(res.x));
|
||||
// if there is a YYYY parameter to enter
|
||||
std::string::size_type yyyy = gstreamer_pattern.find("YYYY");
|
||||
if (yyyy != std::string::npos)
|
||||
gstreamer_pattern = gstreamer_pattern.replace(yyyy, 4, std::to_string(res.y));
|
||||
// if there is a XXX parameter to enter
|
||||
std::string::size_type xxx = gstreamer_pattern.find("XXX");
|
||||
if (xxx != std::string::npos)
|
||||
gstreamer_pattern = gstreamer_pattern.replace(xxx, 3, std::to_string(res.x/10));
|
||||
// if there is a YYY parameter to enter
|
||||
std::string::size_type yyy = gstreamer_pattern.find("YYY");
|
||||
if (yyy != std::string::npos)
|
||||
gstreamer_pattern = gstreamer_pattern.replace(yyy, 3, std::to_string(res.y/10));
|
||||
|
||||
// all patterns before 'SMPTE test pattern' are single frames (not animated)
|
||||
single_frame_ = type_ < 14;
|
||||
// remember if the pattern is to be updated once or animated
|
||||
single_frame_ = !Pattern::patterns_[type_].animated;
|
||||
|
||||
// (private) open stream
|
||||
Stream::open(gstreamer_pattern, res.x, res.y);
|
||||
@@ -143,10 +144,17 @@ PatternSource::PatternSource(uint64_t id) : StreamSource(id)
|
||||
|
||||
void PatternSource::setPattern(uint type, glm::ivec2 resolution)
|
||||
{
|
||||
Log::Notify("Creating Source with pattern '%s'", Pattern::pattern_types[type].c_str());
|
||||
// open gstreamer with pattern
|
||||
if ( Pattern::get(type).available) {
|
||||
pattern()->open( (uint) type, resolution );
|
||||
}
|
||||
// revert to pattern Black if not available
|
||||
else {
|
||||
pattern()->open( 0, resolution );
|
||||
Log::Warning("Pattern '%s' is not available in this version of vimix.", Pattern::get(type).label.c_str());
|
||||
}
|
||||
|
||||
// open gstreamer
|
||||
pattern()->open( (uint) type, resolution );
|
||||
// play gstreamer
|
||||
stream_->play(true);
|
||||
|
||||
// will be ready after init and one frame rendered
|
||||
@@ -166,3 +174,12 @@ Pattern *PatternSource::pattern() const
|
||||
}
|
||||
|
||||
|
||||
glm::ivec2 PatternSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_PATTERN);
|
||||
}
|
||||
|
||||
std::string PatternSource::info() const
|
||||
{
|
||||
return std::string("pattern '") + Pattern::get(pattern()->type()).label + "'";
|
||||
}
|
||||
|
||||
@@ -5,10 +5,22 @@
|
||||
|
||||
#include "StreamSource.h"
|
||||
|
||||
typedef struct pattern_ {
|
||||
std::string label;
|
||||
std::string feature;
|
||||
std::string pipeline;
|
||||
bool animated;
|
||||
bool available;
|
||||
} pattern_descriptor;
|
||||
|
||||
|
||||
class Pattern : public Stream
|
||||
{
|
||||
static std::vector<pattern_descriptor> patterns_;
|
||||
|
||||
public:
|
||||
static std::vector<std::string> pattern_types;
|
||||
static pattern_descriptor get(uint type);
|
||||
static uint count();
|
||||
|
||||
Pattern();
|
||||
void open( uint pattern, glm::ivec2 res);
|
||||
@@ -35,7 +47,8 @@ public:
|
||||
Pattern *pattern() const;
|
||||
void setPattern(uint type, glm::ivec2 resolution);
|
||||
|
||||
glm::ivec2 icon() const override { return glm::ivec2(11, 5); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
#include "PickingVisitor.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "Log.h"
|
||||
#include "Decorations.h"
|
||||
|
||||
#include "GlmToolkit.h"
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/string_cast.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
|
||||
#include "Log.h"
|
||||
#include "Decorations.h"
|
||||
#include "GlmToolkit.h"
|
||||
|
||||
#include "PickingVisitor.h"
|
||||
|
||||
|
||||
PickingVisitor::PickingVisitor(glm::vec3 coordinates, bool force) : Visitor(),
|
||||
force_(force), modelview_(glm::mat4(1.f))
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
*/
|
||||
class PickingVisitor: public Visitor
|
||||
{
|
||||
bool force_;
|
||||
std::vector<glm::vec3> points_;
|
||||
glm::mat4 modelview_;
|
||||
std::vector< std::pair<Node *, glm::vec2> > nodes_;
|
||||
bool force_;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
#include "Primitives.h"
|
||||
#include "ImageShader.h"
|
||||
#include "Resource.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "MediaPlayer.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
|
||||
#include <glad/glad.h>
|
||||
|
||||
@@ -13,11 +25,17 @@
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/vector_angle.hpp>
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
#include <glm/gtx/rotate_vector.hpp>
|
||||
|
||||
#include "ImageShader.h"
|
||||
#include "Resource.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "MediaPlayer.h"
|
||||
#include "Visitor.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "Primitives.h"
|
||||
|
||||
Surface::Surface(Shader *s) : Primitive(s), textureindex_(0), mirror_(true)
|
||||
{
|
||||
@@ -121,52 +139,6 @@ void ImageSurface::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
MediaSurface::MediaSurface(const std::string& p, Shader *s) : Surface(s), path_(p)
|
||||
{
|
||||
mediaplayer_ = new MediaPlayer;
|
||||
}
|
||||
|
||||
MediaSurface::~MediaSurface()
|
||||
{
|
||||
delete mediaplayer_;
|
||||
}
|
||||
|
||||
void MediaSurface::init()
|
||||
{
|
||||
Surface::init();
|
||||
|
||||
mediaplayer_->open(path_);
|
||||
mediaplayer_->play(true);
|
||||
}
|
||||
|
||||
void MediaSurface::draw(glm::mat4 modelview, glm::mat4 projection)
|
||||
{
|
||||
if ( !initialized() ) {
|
||||
init();
|
||||
// set the texture to the media player once openned
|
||||
if ( mediaplayer_->isOpen() )
|
||||
textureindex_ = mediaplayer_->texture();
|
||||
}
|
||||
|
||||
Surface::draw(modelview, projection);
|
||||
}
|
||||
|
||||
void MediaSurface::update( float dt )
|
||||
{
|
||||
if ( mediaplayer_->isOpen() ) {
|
||||
mediaplayer_->update();
|
||||
scale_.x = mediaplayer_->aspectRatio();
|
||||
}
|
||||
|
||||
Primitive::update( dt );
|
||||
}
|
||||
|
||||
void MediaSurface::accept(Visitor& v)
|
||||
{
|
||||
Surface::accept(v);
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
FrameBufferSurface::FrameBufferSurface(FrameBuffer *fb, Shader *s) : Surface(s), frame_buffer_(fb)
|
||||
{
|
||||
}
|
||||
@@ -401,6 +373,24 @@ LineSquare::LineSquare(float linewidth) : Group()
|
||||
}
|
||||
|
||||
|
||||
LineSquare::LineSquare(const LineSquare &square)
|
||||
{
|
||||
top_ = new HLine(square.top_->width);
|
||||
top_->translation_ = glm::vec3(0.f, 1.f, 0.f);
|
||||
attach(top_);
|
||||
bottom_ = new HLine(square.bottom_->width);
|
||||
bottom_->translation_ = glm::vec3(0.f, -1.f, 0.f);
|
||||
attach(bottom_);
|
||||
left_ = new VLine(square.left_->width);
|
||||
left_->translation_ = glm::vec3(-1.f, 0.f, 0.f);
|
||||
attach(left_);
|
||||
right_ = new VLine(square.right_->width);
|
||||
right_->translation_ = glm::vec3(1.f, 0.f, 0.f);
|
||||
attach(right_);
|
||||
|
||||
setColor(square.color());
|
||||
}
|
||||
|
||||
void LineSquare::setLineWidth(float v)
|
||||
{
|
||||
top_->width = v;
|
||||
@@ -417,7 +407,6 @@ void LineSquare::setColor(glm::vec4 c)
|
||||
right_->color = c;
|
||||
}
|
||||
|
||||
|
||||
LineStrip::LineStrip(const std::vector<glm::vec2> &path, float linewidth) : Primitive(new Shader),
|
||||
arrayBuffer_(0), path_(path)
|
||||
{
|
||||
|
||||
29
Primitives.h
29
Primitives.h
@@ -62,31 +62,6 @@ protected:
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief The MediaSurface class is a Surface to draw a video
|
||||
*
|
||||
* URI is passed to a Media Player to handle the video playback
|
||||
* Height = 1.0, Width is set by the aspect ratio of the image
|
||||
*/
|
||||
class MediaSurface : public Surface {
|
||||
|
||||
public:
|
||||
MediaSurface(const std::string& p, Shader *s = new ImageShader);
|
||||
~MediaSurface();
|
||||
|
||||
void init () override;
|
||||
void draw (glm::mat4 modelview, glm::mat4 projection) override;
|
||||
void accept (Visitor& v) override;
|
||||
void update (float dt) override;
|
||||
|
||||
inline std::string path() const { return path_; }
|
||||
inline MediaPlayer *mediaPlayer() const { return mediaplayer_; }
|
||||
|
||||
protected:
|
||||
std::string path_;
|
||||
MediaPlayer *mediaplayer_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The FrameBufferSurface class is a Surface to draw a framebuffer
|
||||
*
|
||||
@@ -101,7 +76,8 @@ public:
|
||||
void draw (glm::mat4 modelview, glm::mat4 projection) override;
|
||||
void accept (Visitor& v) override;
|
||||
|
||||
inline FrameBuffer *getFrameBuffer() const { return frame_buffer_; }
|
||||
inline void setFrameBuffer(FrameBuffer *fb) { frame_buffer_ = fb; }
|
||||
inline FrameBuffer *frameBuffer() const { return frame_buffer_; }
|
||||
|
||||
protected:
|
||||
FrameBuffer *frame_buffer_;
|
||||
@@ -166,6 +142,7 @@ class LineSquare : public Group {
|
||||
|
||||
public:
|
||||
LineSquare(float linewidth = 1.f);
|
||||
LineSquare(const LineSquare &square);
|
||||
|
||||
void setLineWidth(float v);
|
||||
inline float lineWidth() const { return top_->width; }
|
||||
|
||||
@@ -12,6 +12,11 @@ monitor or a projector, but can be recorded live (no audio).
|
||||
|
||||
vimix is the successor for GLMixer - https://sourceforge.net/projects/glmixer/
|
||||
|
||||
# License
|
||||
|
||||
GPL-3.0-or-later
|
||||
See [LICENSE](https://github.com/brunoherbelin/vimix/blob/master/LICENSE)
|
||||
|
||||
# Install
|
||||
|
||||
Check the [Quick Installation Guide](https://github.com/brunoherbelin/vimix/wiki/Quick-Installation-Guide)
|
||||
@@ -64,7 +69,7 @@ Compile (or re-compile after pull):
|
||||
**Libraries:**
|
||||
|
||||
- gstreamer
|
||||
- gst-plugins : base, good, bad & ugly
|
||||
- gst-plugins : libav, base, good, bad & ugly
|
||||
- libglfw3
|
||||
- libicu
|
||||
|
||||
@@ -72,7 +77,7 @@ Compile (or re-compile after pull):
|
||||
|
||||
**Ubuntu**
|
||||
|
||||
$ apt-get install build-essential cmake libpng-dev libglfw3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libicu-dev libgtk-3-dev
|
||||
$ apt-get install build-essential cmake libpng-dev libglfw3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-libav libicu-dev libgtk-3-dev
|
||||
|
||||
**OSX with Brew**
|
||||
|
||||
|
||||
319
Recorder.cpp
319
Recorder.cpp
@@ -1,4 +1,25 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <thread>
|
||||
#include<algorithm> // for copy() and assign()
|
||||
#include<iterator> // for back_inserter
|
||||
|
||||
// Desktop OpenGL function loader
|
||||
#include <glad/glad.h>
|
||||
@@ -24,11 +45,11 @@ PNGRecorder::PNGRecorder() : FrameGrabber()
|
||||
{
|
||||
}
|
||||
|
||||
void PNGRecorder::init(GstCaps *caps)
|
||||
std::string PNGRecorder::init(GstCaps *caps)
|
||||
{
|
||||
// ignore
|
||||
if (caps == nullptr)
|
||||
return;
|
||||
return std::string("Invalid caps");
|
||||
|
||||
// create a gstreamer pipeline
|
||||
std::string description = "appsrc name=src ! videoconvert ! pngenc ! filesink name=sink";
|
||||
@@ -37,10 +58,9 @@ void PNGRecorder::init(GstCaps *caps)
|
||||
GError *error = NULL;
|
||||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||||
if (error != NULL) {
|
||||
Log::Warning("PNG Capture Could not construct pipeline %s:\n%s", description.c_str(), error->message);
|
||||
std::string msg = std::string("PNG Capture Could not construct pipeline ") + description + "\n" + std::string(error->message);
|
||||
g_clear_error (&error);
|
||||
finished_ = true;
|
||||
return;
|
||||
return msg;
|
||||
}
|
||||
|
||||
// verify location path (path is always terminated by the OS dependent separator)
|
||||
@@ -60,12 +80,13 @@ void PNGRecorder::init(GstCaps *caps)
|
||||
if (src_) {
|
||||
|
||||
g_object_set (G_OBJECT (src_),
|
||||
"stream-type", GST_APP_STREAM_TYPE_STREAM,
|
||||
"is-live", TRUE,
|
||||
"format", GST_FORMAT_TIME,
|
||||
// "do-timestamp", TRUE,
|
||||
NULL);
|
||||
|
||||
// configure stream
|
||||
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
|
||||
gst_app_src_set_latency( src_, -1, 0);
|
||||
|
||||
// Direct encoding (no buffering)
|
||||
gst_app_src_set_max_bytes( src_, 0 );
|
||||
|
||||
@@ -82,38 +103,35 @@ void PNGRecorder::init(GstCaps *caps)
|
||||
|
||||
}
|
||||
else {
|
||||
Log::Warning("PNG Capture Could not configure source");
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("PNG Capture : Failed to configure frame grabber.");
|
||||
}
|
||||
|
||||
// start pipeline
|
||||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
Log::Warning("PNG Capture Could not record %s", filename_.c_str());
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("PNG Capture : Failed to start frame grabber.");
|
||||
}
|
||||
|
||||
// all good
|
||||
Log::Info("PNG Capture started.");
|
||||
initialized_ = true;
|
||||
|
||||
// start recording !!
|
||||
active_ = true;
|
||||
return std::string("PNG Capture started ");
|
||||
}
|
||||
|
||||
void PNGRecorder::terminate()
|
||||
{
|
||||
// remember and inform
|
||||
Settings::application.recentRecordings.push(filename_);
|
||||
Log::Notify("PNG Capture %s is ready.", filename_.c_str());
|
||||
}
|
||||
|
||||
void PNGRecorder::addFrame(GstBuffer *buffer, GstCaps *caps, float dt)
|
||||
void PNGRecorder::addFrame(GstBuffer *buffer, GstCaps *caps)
|
||||
{
|
||||
FrameGrabber::addFrame(buffer, caps, dt);
|
||||
FrameGrabber::addFrame(buffer, caps);
|
||||
|
||||
// PNG Recorder specific :
|
||||
// stop after one frame
|
||||
if (timestamp_ > 0) {
|
||||
if (frame_count_ > 0) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
@@ -123,12 +141,13 @@ const char* VideoRecorder::profile_name[VideoRecorder::DEFAULT] = {
|
||||
"H264 (Realtime)",
|
||||
"H264 (High 4:4:4)",
|
||||
"H265 (Realtime)",
|
||||
"H265 (HQ Animation)",
|
||||
"H265 (HQ)",
|
||||
"ProRes (Standard)",
|
||||
"ProRes (HQ 4444)",
|
||||
"WebM VP8 (2MB/s)",
|
||||
"WebM VP8 (Realtime)",
|
||||
"Multiple JPEG"
|
||||
};
|
||||
|
||||
const std::vector<std::string> VideoRecorder::profile_description {
|
||||
// Control x264 encoder quality :
|
||||
// pass
|
||||
@@ -138,19 +157,17 @@ const std::vector<std::string> VideoRecorder::profile_description {
|
||||
// The total range is from 0 to 51, where 0 is lossless, 18 can be considered ‘visually lossless’,
|
||||
// and 51 is terrible quality. A sane range is 18-26, and the default is 23.
|
||||
// speed-preset
|
||||
// ultrafast (1)
|
||||
// superfast (2)
|
||||
// veryfast (3)
|
||||
// faster (4)
|
||||
// fast (5)
|
||||
#ifndef APPLE
|
||||
// "video/x-raw, format=I420 ! x264enc pass=4 quantizer=26 speed-preset=3 threads=4 ! video/x-h264, profile=baseline ! h264parse ! ",
|
||||
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" threads=4 ! video/x-h264, profile=baseline ! h264parse ! ",
|
||||
#else
|
||||
"video/x-raw, format=I420 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! h264parse ! ",
|
||||
#endif
|
||||
"video/x-raw, format=Y444_10LE ! x264enc pass=4 quantizer=16 speed-preset=4 threads=4 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
|
||||
"video/x-raw, format=I420 ! x264enc tune=\"zerolatency\" pass=4 quantizer=22 speed-preset=2 ! video/x-h264, profile=baseline ! h264parse ! ",
|
||||
"video/x-raw, format=Y444_10LE ! x264enc pass=4 quantizer=18 speed-preset=3 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
|
||||
// Control x265 encoder quality :
|
||||
// NB: apparently x265 only accepts I420 format :(
|
||||
// speed-preset
|
||||
// superfast (2)
|
||||
// veryfast (3)
|
||||
// faster (4)
|
||||
// fast (5)
|
||||
@@ -162,56 +179,176 @@ const std::vector<std::string> VideoRecorder::profile_description {
|
||||
// fastdecode (5)
|
||||
// animation (6) optimize the encode quality for animation content without impacting the encode speed
|
||||
// crf Quality-controlled variable bitrate [0 51]
|
||||
// default 28
|
||||
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
|
||||
"video/x-raw, format=I420 ! x265enc tune=4 speed-preset=3 ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||||
"video/x-raw, format=I420 ! x265enc tune=6 speed-preset=4 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||||
// default 28
|
||||
// 24 for x265 should be visually transparent; anything lower will probably just waste file size
|
||||
"video/x-raw, format=I420 ! x265enc tune=2 speed-preset=2 option-string=\"crf=24\" ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||||
"video/x-raw, format=I420 ! x265enc tune=6 speed-preset=2 option-string=\"crf=12\" ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||||
// Apple ProRes encoding parameters
|
||||
// pass
|
||||
// cbr (0) – Constant Bitrate Encoding
|
||||
// quant (2) – Constant Quantizer
|
||||
// pass1 (512) – VBR Encoding - Pass 1
|
||||
// profile
|
||||
// 0 ‘proxy’
|
||||
// 1 ‘lt’
|
||||
// 2 ‘standard’
|
||||
// 3 ‘hq’
|
||||
// 4 ‘4444’
|
||||
"avenc_prores_ks pass=2 profile=2 quantizer=26 ! ",
|
||||
"video/x-raw, format=Y444_10LE ! avenc_prores_ks pass=2 profile=4 quantizer=12 ! ",
|
||||
// 0 ‘proxy’ 45Mbps YUV 4:2:2
|
||||
// 1 ‘lt’ 102Mbps YUV 4:2:2
|
||||
// 2 ‘standard’ 147Mbps YUV 4:2:2
|
||||
// 3 ‘hq’ 220Mbps YUV 4:2:2
|
||||
// 4 ‘4444’ 330Mbps YUVA 4:4:4:4
|
||||
// quant-mat
|
||||
// -1 auto
|
||||
// 0 proxy
|
||||
// 2 lt
|
||||
// 3 standard
|
||||
// 4 hq
|
||||
// 6 default
|
||||
"video/x-raw, format=I422_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=2 quant-mat=6 quantizer=8 ! ",
|
||||
"video/x-raw, format=Y444_10LE ! avenc_prores_ks pass=2 bits_per_mb=8000 profile=4 quant-mat=6 quantizer=4 ! ",
|
||||
// VP8 WebM encoding
|
||||
"vp8enc end-usage=vbr cpu-used=8 max-quantizer=35 deadline=100000 target-bitrate=200000 keyframe-max-dist=360 token-partitions=2 static-threshold=100 ! ",
|
||||
"jpegenc ! "
|
||||
// deadline per frame (usec)
|
||||
// 0=best,
|
||||
// 1=realtime
|
||||
// see https://www.webmproject.org/docs/encoder-parameters/
|
||||
// "vp8enc end-usage=cbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 undershoot=95 "
|
||||
// "buffer-size=6000 buffer-initial-size=4000 buffer-optimal-size=5000 "
|
||||
// "keyframe-max-dist=999999 min-quantizer=4 max-quantizer=50 ! ",
|
||||
"vp8enc end-usage=vbr deadline=1 cpu-used=8 threads=4 target-bitrate=400000 keyframe-max-dist=360 "
|
||||
"token-partitions=2 static-threshold=1000 min-quantizer=4 max-quantizer=20 ! ",
|
||||
// JPEG encoding
|
||||
"jpegenc idct-method=float ! "
|
||||
};
|
||||
|
||||
// Too slow
|
||||
//// WebM VP9 encoding parameters
|
||||
//// https://www.webmproject.org/docs/encoder-parameters/
|
||||
//// https://developers.google.com/media/vp9/settings/vod/
|
||||
//"vp9enc end-usage=vbr end-usage=vbr cpu-used=3 max-quantizer=35 target-bitrate=200000 keyframe-max-dist=360 token-partitions=2 static-threshold=1000 ! "
|
||||
|
||||
// FAILED
|
||||
// x265 encoder quality
|
||||
// string description = "appsrc name=src ! videoconvert ! "
|
||||
// "x265enc tune=4 speed-preset=2 option-string='crf=28' ! h265parse ! "
|
||||
// "qtmux ! filesink name=sink";
|
||||
#if GST_GL_HAVE_PLATFORM_GLX
|
||||
|
||||
// under GLX (Linux), gstreamer might have nvidia or vaapi encoders
|
||||
// the hardware encoder will be filled at first instanciation of VideoRecorder
|
||||
std::vector<std::string> VideoRecorder::hardware_encoder;
|
||||
std::vector<std::string> VideoRecorder::hardware_profile_description;
|
||||
|
||||
std::vector<std::string> nvidia_encoder = {
|
||||
"nvh264enc",
|
||||
"nvh264enc",
|
||||
"nvh265enc",
|
||||
"nvh265enc",
|
||||
"", "", "", ""
|
||||
};
|
||||
|
||||
std::vector<std::string> nvidia_profile_description {
|
||||
// qp-const Constant quantizer (-1 = from NVENC preset)
|
||||
// Range: -1 - 51 Default: -1
|
||||
// rc-mode Rate Control Mode
|
||||
// (0): default - Default
|
||||
// (1): constqp - Constant Quantization
|
||||
// (2): cbr - Constant Bit Rate
|
||||
// (3): vbr - Variable Bit Rate
|
||||
// (4): vbr-minqp - Variable Bit Rate (with minimum quantization parameter, DEPRECATED)
|
||||
// (5): cbr-ld-hq - Low-Delay CBR, High Quality
|
||||
// (6): cbr-hq - CBR, High Quality (slower)
|
||||
// (7): vbr-hq - VBR, High Quality (slower)
|
||||
// Control nvh264enc encoder
|
||||
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 zerolatency=true ! video/x-h264, profile=(string)main ! h264parse ! ",
|
||||
"video/x-raw, format=RGBA ! nvh264enc rc-mode=1 qp-const=18 ! video/x-h264, profile=(string)high-4:4:4 ! h264parse ! ",
|
||||
// Control nvh265enc encoder
|
||||
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 zerolatency=true ! video/x-h265, profile=(string)main-10 ! h265parse ! ",
|
||||
"video/x-raw, format=RGBA ! nvh265enc rc-mode=1 qp-const=18 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
|
||||
"", "", "", ""
|
||||
};
|
||||
|
||||
std::vector<std::string> vaapi_encoder = {
|
||||
"vaapih264enc",
|
||||
"vaapih264enc",
|
||||
"vaapih265enc",
|
||||
"vaapih265enc",
|
||||
"", "", "", ""
|
||||
};
|
||||
|
||||
std::vector<std::string> vaapi_profile_description {
|
||||
|
||||
// Control vaapih264enc encoder
|
||||
"video/x-raw, format=NV12 ! vaapih264enc rate-control=cqp init-qp=26 ! video/x-h264, profile=(string)main ! h264parse ! ",
|
||||
"video/x-raw, format=NV12 ! vaapih264enc rate-control=cqp init-qp=14 quality-level=4 keyframe-period=0 max-bframes=2 ! video/x-h264, profile=(string)high ! h264parse ! ",
|
||||
// Control vaapih265enc encoder
|
||||
"video/x-raw, format=NV12 ! vaapih265enc ! video/x-h265, profile=(string)main ! h265parse ! ",
|
||||
"video/x-raw, format=NV12 ! vaapih265enc rate-control=cqp init-qp=14 quality-level=4 keyframe-period=0 max-bframes=2 ! video/x-h265, profile=(string)main-444 ! h265parse ! ",
|
||||
"", "", "", ""
|
||||
};
|
||||
|
||||
#elif GST_GL_HAVE_PLATFORM_CGL
|
||||
// under CGL (Mac), gstreamer might have the VideoToolbox
|
||||
std::vector<std::string> VideoRecorder::hardware_encoder = {
|
||||
"vtenc_h264_hw",
|
||||
"vtenc_h264_hw",
|
||||
"", "", "", "", "", ""
|
||||
};
|
||||
|
||||
std::vector<std::string> VideoRecorder::hardware_profile_description {
|
||||
// Control vtenc_h264_hw encoder
|
||||
"video/x-raw, format=I420 ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 ! h264parse ! ",
|
||||
"video/x-raw, format=UYVY ! vtenc_h264_hw realtime=1 allow-frame-reordering=0 quality=0.9 ! h264parse ! ",
|
||||
"", "", "", "", "", ""
|
||||
};
|
||||
|
||||
#else
|
||||
// in other platforms, no hardware encoder
|
||||
std::vector<std::string> VideoRecorder::hardware_encoder;
|
||||
std::vector<std::string> VideoRecorder::hardware_profile_description;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
const char* VideoRecorder::buffering_preset_name[6] = { "Minimum", "100 MB", "200 MB", "500 MB", "1 GB", "2 GB" };
|
||||
const guint64 VideoRecorder::buffering_preset_value[6] = { MIN_BUFFER_SIZE, 104857600, 209715200, 524288000, 1073741824, 2147483648 };
|
||||
|
||||
const char* VideoRecorder::framerate_preset_name[3] = { "15 FPS", "25 FPS", "30 FPS" };
|
||||
const gint VideoRecorder::framerate_preset_value[3] = { 15, 25, 30 };
|
||||
|
||||
|
||||
VideoRecorder::VideoRecorder() : FrameGrabber()
|
||||
{
|
||||
// first run initialization of hardware encoders in linux
|
||||
#if GST_GL_HAVE_PLATFORM_GLX
|
||||
if (hardware_encoder.size() < 1) {
|
||||
// test nvidia encoder
|
||||
if ( GstToolkit::has_feature(nvidia_encoder[0] ) ) {
|
||||
// consider that if first nvidia encoder is valid, all others should also be available
|
||||
hardware_encoder.assign(nvidia_encoder.begin(), nvidia_encoder.end());
|
||||
hardware_profile_description.assign(nvidia_profile_description.begin(), nvidia_profile_description.end());
|
||||
}
|
||||
// test vaapi encoder
|
||||
else if ( GstToolkit::has_feature(vaapi_encoder[0] ) ) {
|
||||
hardware_encoder.assign(vaapi_encoder.begin(), vaapi_encoder.end());
|
||||
hardware_profile_description.assign(vaapi_profile_description.begin(), vaapi_profile_description.end());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void VideoRecorder::init(GstCaps *caps)
|
||||
std::string VideoRecorder::init(GstCaps *caps)
|
||||
{
|
||||
// ignore
|
||||
if (caps == nullptr)
|
||||
return;
|
||||
return std::string("Invalid caps");
|
||||
|
||||
// apply settings
|
||||
buffering_size_ = MAX( MIN_BUFFER_SIZE, buffering_preset_value[Settings::application.record.buffering_mode]);
|
||||
frame_duration_ = gst_util_uint64_scale_int (1, GST_SECOND, framerate_preset_value[Settings::application.record.framerate_mode]);
|
||||
timestamp_on_clock_ = Settings::application.record.priority_mode < 1;
|
||||
|
||||
// create a gstreamer pipeline
|
||||
std::string description = "appsrc name=src ! videoconvert ! ";
|
||||
if (Settings::application.record.profile < 0 || Settings::application.record.profile >= DEFAULT)
|
||||
Settings::application.record.profile = H264_STANDARD;
|
||||
description += profile_description[Settings::application.record.profile];
|
||||
|
||||
// test for a hardware accelerated encoder
|
||||
if (Settings::application.render.gpu_decoding && (int) hardware_encoder.size() > 0 &&
|
||||
GstToolkit::has_feature(hardware_encoder[Settings::application.record.profile]) ) {
|
||||
|
||||
description += hardware_profile_description[Settings::application.record.profile];
|
||||
Log::Info("Video Recording using hardware accelerated encoder (%s)", hardware_encoder[Settings::application.record.profile].c_str());
|
||||
}
|
||||
// revert to software encoder
|
||||
else
|
||||
description += profile_description[Settings::application.record.profile];
|
||||
|
||||
// verify location path (path is always terminated by the OS dependent separator)
|
||||
std::string path = SystemToolkit::path_directory(Settings::application.record.path);
|
||||
@@ -238,10 +375,9 @@ void VideoRecorder::init(GstCaps *caps)
|
||||
GError *error = NULL;
|
||||
pipeline_ = gst_parse_launch (description.c_str(), &error);
|
||||
if (error != NULL) {
|
||||
Log::Warning("VideoRecorder Could not construct pipeline %s:\n%s", description.c_str(), error->message);
|
||||
std::string msg = std::string("Video Recording : Could not construct pipeline ") + description + "\n" + std::string(error->message);
|
||||
g_clear_error (&error);
|
||||
finished_ = true;
|
||||
return;
|
||||
return msg;
|
||||
}
|
||||
|
||||
// setup file sink
|
||||
@@ -255,18 +391,32 @@ void VideoRecorder::init(GstCaps *caps)
|
||||
if (src_) {
|
||||
|
||||
g_object_set (G_OBJECT (src_),
|
||||
"stream-type", GST_APP_STREAM_TYPE_STREAM,
|
||||
"is-live", TRUE,
|
||||
"format", GST_FORMAT_TIME,
|
||||
// "do-timestamp", TRUE,
|
||||
NULL);
|
||||
|
||||
// Direct encoding (no buffering)
|
||||
gst_app_src_set_max_bytes( src_, 0 );
|
||||
if (timestamp_on_clock_)
|
||||
g_object_set (G_OBJECT (src_),"do-timestamp", TRUE,NULL);
|
||||
|
||||
// instruct src to use the required caps
|
||||
caps_ = gst_caps_copy( caps );
|
||||
// configure stream
|
||||
gst_app_src_set_stream_type( src_, GST_APP_STREAM_TYPE_STREAM);
|
||||
gst_app_src_set_latency( src_, -1, 0);
|
||||
|
||||
// Set buffer size
|
||||
gst_app_src_set_max_bytes( src_, buffering_size_);
|
||||
|
||||
// specify recorder framerate in the given caps
|
||||
GstCaps *tmp = gst_caps_copy( caps );
|
||||
GValue v = { 0, };
|
||||
g_value_init (&v, GST_TYPE_FRACTION);
|
||||
gst_value_set_fraction (&v, framerate_preset_value[Settings::application.record.framerate_mode], 1);
|
||||
gst_caps_set_value(tmp, "framerate", &v);
|
||||
g_value_unset (&v);
|
||||
|
||||
// instruct src to use the caps
|
||||
caps_ = gst_caps_copy( tmp );
|
||||
gst_app_src_set_caps (src_, caps_);
|
||||
gst_caps_unref (tmp);
|
||||
|
||||
// setup callbacks
|
||||
GstAppSrcCallbacks callbacks;
|
||||
@@ -277,35 +427,52 @@ void VideoRecorder::init(GstCaps *caps)
|
||||
|
||||
}
|
||||
else {
|
||||
Log::Warning("VideoRecorder Could not configure source");
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("Video Recording : Failed to configure frame grabber.");
|
||||
}
|
||||
|
||||
// start recording
|
||||
GstStateChangeReturn ret = gst_element_set_state (pipeline_, GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
Log::Warning("VideoRecorder Could not record %s", filename_.c_str());
|
||||
finished_ = true;
|
||||
return;
|
||||
return std::string("Video Recording : Failed to start frame grabber.");
|
||||
}
|
||||
|
||||
// all good
|
||||
Log::Info("Video Recording started (%s)", profile_name[Settings::application.record.profile]);
|
||||
initialized_ = true;
|
||||
|
||||
return std::string("Video Recording started ") + profile_name[Settings::application.record.profile];
|
||||
|
||||
// start recording !!
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void VideoRecorder::terminate()
|
||||
{
|
||||
// stop the pipeline (again)
|
||||
gst_element_set_state (pipeline_, GST_STATE_NULL);
|
||||
|
||||
// statistics on expected number of frames
|
||||
guint64 N = MAX( (guint64) duration_ / (guint64) frame_duration_, frame_count_);
|
||||
float loss = 100.f * ((float) (N - frame_count_) ) / (float) N;
|
||||
Log::Info("Video Recording : %ld frames captured in %s (aming for %ld, %.0f%% lost)",
|
||||
frame_count_, GstToolkit::time_to_string(duration_, GstToolkit::TIME_STRING_READABLE).c_str(), N, loss);
|
||||
|
||||
// warn user if more than 10% lost
|
||||
if (loss > 10.f) {
|
||||
if (timestamp_on_clock_)
|
||||
Log::Warning("Video Recording lost %.0f%% of frames: framerate could not be maintained at %ld FPS.", loss, GST_SECOND / frame_duration_);
|
||||
else
|
||||
Log::Warning("Video Recording lost %.0f%% of frames: video is only %s long.",
|
||||
loss, GstToolkit::time_to_string(timestamp_, GstToolkit::TIME_STRING_READABLE).c_str());
|
||||
Log::Info("Video Recording : try a lower resolution / a lower framerate / a larger buffer size / a faster codec.");
|
||||
}
|
||||
|
||||
// remember and inform
|
||||
Settings::application.recentRecordings.push(filename_);
|
||||
Log::Notify("Video Recording %s is ready.", filename_.c_str());
|
||||
}
|
||||
|
||||
std::string VideoRecorder::info() const
|
||||
{
|
||||
if (active_)
|
||||
return GstToolkit::time_to_string(timestamp_);
|
||||
else
|
||||
if (initialized_ && !active_ && !endofstream_)
|
||||
return "Saving file...";
|
||||
|
||||
return FrameGrabber::info();
|
||||
}
|
||||
|
||||
25
Recorder.h
25
Recorder.h
@@ -2,6 +2,8 @@
|
||||
#define RECORDER_H
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
#include <gst/app/gstappsrc.h>
|
||||
@@ -10,26 +12,26 @@
|
||||
|
||||
class PNGRecorder : public FrameGrabber
|
||||
{
|
||||
std::string filename_;
|
||||
std::string filename_;
|
||||
|
||||
public:
|
||||
|
||||
PNGRecorder();
|
||||
std::string filename() const { return filename_; }
|
||||
|
||||
protected:
|
||||
|
||||
void init(GstCaps *caps) override;
|
||||
std::string init(GstCaps *caps) override;
|
||||
void terminate() override;
|
||||
void addFrame(GstBuffer *buffer, GstCaps *caps, float dt) override;
|
||||
void addFrame(GstBuffer *buffer, GstCaps *caps) override;
|
||||
|
||||
};
|
||||
|
||||
|
||||
class VideoRecorder : public FrameGrabber
|
||||
{
|
||||
std::string filename_;
|
||||
std::string filename_;
|
||||
|
||||
void init(GstCaps *caps) override;
|
||||
std::string init(GstCaps *caps) override;
|
||||
void terminate() override;
|
||||
|
||||
public:
|
||||
@@ -45,12 +47,19 @@ public:
|
||||
JPEG_MULTI,
|
||||
DEFAULT
|
||||
} Profile;
|
||||
static const char* profile_name[DEFAULT];
|
||||
static const char* profile_name[DEFAULT];
|
||||
static const std::vector<std::string> profile_description;
|
||||
static std::vector<std::string> hardware_encoder;
|
||||
static std::vector<std::string> hardware_profile_description;
|
||||
|
||||
static const char* buffering_preset_name[6];
|
||||
static const guint64 buffering_preset_value[6];
|
||||
static const char* framerate_preset_name[3];
|
||||
static const int framerate_preset_value[3];
|
||||
|
||||
VideoRecorder();
|
||||
std::string info() const override;
|
||||
|
||||
std::string filename() const { return filename_; }
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <thread>
|
||||
|
||||
// Opengl
|
||||
#include <glad/glad.h>
|
||||
#include <glm/glm.hpp>
|
||||
@@ -123,18 +144,22 @@ FrameBufferImage *RenderView::thumbnail ()
|
||||
// by default null image
|
||||
FrameBufferImage *img = nullptr;
|
||||
|
||||
// this function is always called from a parallel thread
|
||||
// So we wait for a few frames of rendering before trying to capture a thumbnail
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// create and store a promise for a FrameBufferImage
|
||||
thumbnailer_.emplace_back( std::promise<FrameBufferImage *>() );
|
||||
|
||||
// future will return the primised FrameBufferImage
|
||||
std::future<FrameBufferImage *> t = thumbnailer_.back().get_future();
|
||||
// future will return the promised FrameBufferImage
|
||||
std::future<FrameBufferImage *> ft = thumbnailer_.back().get_future();
|
||||
|
||||
try {
|
||||
// wait for valid return value from promise
|
||||
img = t.get();
|
||||
// wait for a valid return value from promise
|
||||
img = ft.get();
|
||||
}
|
||||
// catch any failed promise
|
||||
catch (std::runtime_error&){
|
||||
catch (const std::exception&){
|
||||
}
|
||||
|
||||
return img;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
class RenderView : public View
|
||||
{
|
||||
friend class Session;
|
||||
|
||||
// rendering FBO
|
||||
FrameBuffer *frame_buffer_;
|
||||
Surface *fading_overlay_;
|
||||
@@ -26,12 +28,15 @@ public:
|
||||
void setResolution (glm::vec3 resolution = glm::vec3(0.f), bool useAlpha = false);
|
||||
glm::vec3 resolution() const { return frame_buffer_->resolution(); }
|
||||
|
||||
void setFading(float f = 0.f);
|
||||
float fading() const;
|
||||
|
||||
// current frame
|
||||
inline FrameBuffer *frame () const { return frame_buffer_; }
|
||||
|
||||
protected:
|
||||
|
||||
void setFading(float f = 0.f);
|
||||
float fading() const;
|
||||
|
||||
// get a thumbnail outside of opengl context; wait for a promise to be fullfiled after draw
|
||||
void drawThumbnail();
|
||||
FrameBufferImage *thumbnail ();
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
#include <cstring>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
@@ -38,7 +57,7 @@
|
||||
// standalone image loader
|
||||
#include <stb_image.h>
|
||||
|
||||
// vmix
|
||||
// vimix
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
#include "Resource.h"
|
||||
@@ -48,6 +67,7 @@
|
||||
#include "SystemToolkit.h"
|
||||
#include "GstToolkit.h"
|
||||
#include "UserInterfaceManager.h"
|
||||
|
||||
#include "RenderingManager.h"
|
||||
|
||||
#ifdef USE_GST_OPENGL_SYNC_HANDLER
|
||||
@@ -100,7 +120,7 @@ void Rendering::LinkPipeline( GstPipeline *pipeline )
|
||||
#endif
|
||||
|
||||
|
||||
static std::map<GLFWwindow *, RenderingWindow*> GLFW_window_;
|
||||
std::map<GLFWwindow *, RenderingWindow*> GLFW_window_;
|
||||
|
||||
static void glfw_error_callback(int error, const char* description)
|
||||
{
|
||||
@@ -188,7 +208,6 @@ bool Rendering::init()
|
||||
// additional window callbacks for main window
|
||||
glfwSetWindowRefreshCallback( main_.window(), WindowRefreshCallback );
|
||||
glfwSetDropCallback( main_.window(), Rendering::FileDropped);
|
||||
glfwSetWindowSizeLimits( main_.window(), 800, 500, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
||||
|
||||
//
|
||||
// Gstreamer setup
|
||||
@@ -200,6 +219,11 @@ bool Rendering::init()
|
||||
g_setenv ("GST_PLUGIN_SYSTEM_PATH", plugins_path.c_str(), TRUE);
|
||||
g_setenv ("GST_PLUGIN_SCANNER", plugins_scanner.c_str(), TRUE);
|
||||
}
|
||||
std::string frei0r_path = SystemToolkit::cwd_path() + "frei0r-1" ;
|
||||
if ( SystemToolkit::file_exists(frei0r_path)) {
|
||||
Log::Info("Found Frei0r plugins in %s", frei0r_path.c_str());
|
||||
g_setenv ("FREI0R_PATH", frei0r_path.c_str(), TRUE);
|
||||
}
|
||||
g_setenv ("GST_GL_API", "opengl3", TRUE);
|
||||
gst_init (NULL, NULL);
|
||||
|
||||
@@ -207,9 +231,10 @@ bool Rendering::init()
|
||||
std::list<std::string> gpuplugins = GstToolkit::enable_gpu_decoding_plugins(Settings::application.render.gpu_decoding);
|
||||
if (Settings::application.render.gpu_decoding) {
|
||||
if (gpuplugins.size() > 0) {
|
||||
Log::Info("Fond the following GPU decoding plugin(s):");
|
||||
for(auto it = gpuplugins.begin(); it != gpuplugins.end(); it++)
|
||||
Log::Info(" - %s", (*it).c_str());
|
||||
Log::Info("Found the following GPU decoding plugin(s):");
|
||||
int i = 1;
|
||||
for(auto it = gpuplugins.rbegin(); it != gpuplugins.rend(); it++, ++i)
|
||||
Log::Info("%d. %s", i, (*it).c_str());
|
||||
}
|
||||
else {
|
||||
Log::Info("No GPU decoding plugin found.");
|
||||
@@ -278,24 +303,26 @@ void Rendering::pushBackDrawCallback(RenderingCallback function)
|
||||
|
||||
void Rendering::draw()
|
||||
{
|
||||
// guint64 _time = gst_util_get_timestamp ();
|
||||
// change windows fullscreen mode if requested
|
||||
main_.toggleFullscreen_();
|
||||
output_.toggleFullscreen_();
|
||||
|
||||
// change main window title if requested
|
||||
if (!main_new_title_.empty()) {
|
||||
main_.setTitle(main_new_title_);
|
||||
main_new_title_.clear();
|
||||
}
|
||||
|
||||
// operate on main window context
|
||||
main_.makeCurrent();
|
||||
|
||||
// User Interface step 1
|
||||
UserInterface::manager().NewFrame();
|
||||
|
||||
// Custom draw
|
||||
// draw
|
||||
std::list<Rendering::RenderingCallback>::iterator iter;
|
||||
for (iter=draw_callbacks_.begin(); iter != draw_callbacks_.end(); ++iter)
|
||||
{
|
||||
(*iter)();
|
||||
}
|
||||
|
||||
// User Interface step 2
|
||||
UserInterface::manager().Render();
|
||||
|
||||
// perform screenshot if requested
|
||||
if (request_screenshot_) {
|
||||
// glfwMakeContextCurrent(main_window_);
|
||||
@@ -303,28 +330,11 @@ void Rendering::draw()
|
||||
request_screenshot_ = false;
|
||||
}
|
||||
|
||||
// draw output window (and swap buffer output)
|
||||
output_.draw( Mixer::manager().session()->frame() );
|
||||
|
||||
// swap GL buffers
|
||||
glfwSwapBuffers(main_.window());
|
||||
glfwSwapBuffers(output_.window());
|
||||
|
||||
// Poll and handle events (inputs, window resize, etc.)
|
||||
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
|
||||
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
|
||||
glfwPollEvents();
|
||||
|
||||
// change windows
|
||||
main_.toggleFullscreen_();
|
||||
output_.toggleFullscreen_();
|
||||
|
||||
#ifndef USE_GST_APPSINK_CALLBACKS
|
||||
// no g_main_loop_run(loop) : update global GMainContext
|
||||
g_main_context_iteration(NULL, FALSE);
|
||||
#endif
|
||||
// draw output window (and swap buffer output)
|
||||
output_.draw( Mixer::manager().session()->frame() );
|
||||
|
||||
// software framerate limiter 60FPS if not v-sync
|
||||
if ( Settings::application.render.vsync < 1 ) {
|
||||
@@ -335,6 +345,16 @@ void Rendering::draw()
|
||||
g_timer_start(timer);
|
||||
}
|
||||
|
||||
// Poll and handle events (inputs, window resize, etc.)
|
||||
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application.
|
||||
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
|
||||
glfwPollEvents();
|
||||
|
||||
// no g_main_loop_run(loop) : update global GMainContext
|
||||
g_main_context_iteration(NULL, FALSE);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -485,9 +505,11 @@ RenderingWindow::~RenderingWindow()
|
||||
|
||||
void RenderingWindow::setTitle(const std::string &title)
|
||||
{
|
||||
std::string fulltitle = Settings::application.windows[index_].name;
|
||||
if ( !title.empty() )
|
||||
fulltitle += " -- " + title;
|
||||
std::string fulltitle;
|
||||
if ( title.empty() )
|
||||
fulltitle = Settings::application.windows[index_].name;
|
||||
else
|
||||
fulltitle = title + std::string(" - " APP_NAME);
|
||||
|
||||
glfwSetWindowTitle(window_, fulltitle.c_str());
|
||||
}
|
||||
@@ -583,7 +605,7 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
|
||||
// done request
|
||||
request_toggle_fullscreen_ = false;
|
||||
|
||||
// if in fullscreen mode
|
||||
// disable fullscreen mode
|
||||
if (mo == nullptr) {
|
||||
// store fullscreen mode
|
||||
Settings::application.windows[index_].fullscreen = false;
|
||||
@@ -595,7 +617,7 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
|
||||
Settings::application.windows[index_].w,
|
||||
Settings::application.windows[index_].h, 0 );
|
||||
}
|
||||
// not in fullscreen mode
|
||||
// set fullscreen mode
|
||||
else {
|
||||
// store fullscreen mode
|
||||
Settings::application.windows[index_].fullscreen = true;
|
||||
@@ -605,12 +627,11 @@ void RenderingWindow::setFullscreen_(GLFWmonitor *mo)
|
||||
const GLFWvidmode * mode = glfwGetVideoMode(mo);
|
||||
glfwSetInputMode( window_, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
|
||||
glfwSetWindowMonitor( window_, mo, 0, 0, mode->width, mode->height, mode->refreshRate);
|
||||
|
||||
// Enable vsync on output window only (i.e. not 0 if has a master)
|
||||
// Workaround for disabled vsync in fullscreen (https://github.com/glfw/glfw/issues/1072)
|
||||
glfwSwapInterval( nullptr == master_ ? 0 : Settings::application.render.vsync);
|
||||
}
|
||||
|
||||
// Enable vsync on output window only (i.e. not 0 if has a master)
|
||||
// Workaround for disabled vsync in fullscreen (https://github.com/glfw/glfw/issues/1072)
|
||||
glfwSwapInterval( nullptr == master_ ? 0 : Settings::application.render.vsync);
|
||||
|
||||
}
|
||||
|
||||
@@ -689,23 +710,25 @@ bool RenderingWindow::init(int index, GLFWwindow *share)
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_AUTO_ICONIFY, GLFW_FALSE);
|
||||
|
||||
// create the window normal
|
||||
// create the window
|
||||
window_ = glfwCreateWindow(winset.w, winset.h, winset.name.c_str(), NULL, master_);
|
||||
if (window_ == NULL){
|
||||
Log::Error("Failed to create GLFW Window %d", index_);
|
||||
return false;
|
||||
}
|
||||
|
||||
// set position
|
||||
// ensure minimal window size
|
||||
glfwSetWindowSizeLimits(window_, 800, 500, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
||||
|
||||
// set initial position
|
||||
glfwSetWindowPos(window_, winset.x, winset.y);
|
||||
|
||||
/// CALLBACKS
|
||||
// store global ref to pointers (used by callbacks)
|
||||
GLFW_window_[window_] = this;
|
||||
// window position and resize callbacks
|
||||
glfwSetWindowSizeCallback( window_, WindowResizeCallback );
|
||||
// glfwSetFramebufferSizeCallback( window_, WindowResizeCallback );
|
||||
glfwSetWindowPosCallback( window_, WindowMoveCallback );
|
||||
glfwSetWindowSizeCallback( window_, WindowResizeCallback );
|
||||
|
||||
// take opengl context ownership
|
||||
glfwMakeContextCurrent(window_);
|
||||
@@ -826,39 +849,45 @@ void RenderingWindow::draw(FrameBuffer *fb)
|
||||
|
||||
// attach the 2D texture to local FBO
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureid_, 0);
|
||||
|
||||
#ifndef NDEBUG
|
||||
Log::Info("Blit to output window enabled.");
|
||||
#endif
|
||||
}
|
||||
|
||||
// calculate scaling factor of frame buffer inside window
|
||||
int rx, ry, rw, rh;
|
||||
float renderingAspectRatio = fb->aspectRatio();
|
||||
if (aspectRatio() < renderingAspectRatio) {
|
||||
int nh = (int)( float(window_attributes_.viewport.x) / renderingAspectRatio);
|
||||
rx = 0;
|
||||
ry = (window_attributes_.viewport.y - nh) / 2;
|
||||
rw = window_attributes_.viewport.x;
|
||||
rh = (window_attributes_.viewport.y + nh) / 2;
|
||||
} else {
|
||||
int nw = (int)( float(window_attributes_.viewport.y) * renderingAspectRatio );
|
||||
rx = (window_attributes_.viewport.x - nw) / 2;
|
||||
ry = 0;
|
||||
rw = (window_attributes_.viewport.x + nw) / 2;
|
||||
rh = window_attributes_.viewport.y;
|
||||
// if not disabled
|
||||
if (!Settings::application.render.disabled) {
|
||||
|
||||
// calculate scaling factor of frame buffer inside window
|
||||
int rx, ry, rw, rh;
|
||||
float renderingAspectRatio = fb->aspectRatio();
|
||||
if (aspectRatio() < renderingAspectRatio) {
|
||||
int nh = (int)( float(window_attributes_.viewport.x) / renderingAspectRatio);
|
||||
rx = 0;
|
||||
ry = (window_attributes_.viewport.y - nh) / 2;
|
||||
rw = window_attributes_.viewport.x;
|
||||
rh = (window_attributes_.viewport.y + nh) / 2;
|
||||
} else {
|
||||
int nw = (int)( float(window_attributes_.viewport.y) * renderingAspectRatio );
|
||||
rx = (window_attributes_.viewport.x - nw) / 2;
|
||||
ry = 0;
|
||||
rw = (window_attributes_.viewport.x + nw) / 2;
|
||||
rh = window_attributes_.viewport.y;
|
||||
}
|
||||
|
||||
// select fbo texture read target
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_);
|
||||
|
||||
// select screen target
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
|
||||
// blit operation from fbo (containing texture) to screen
|
||||
glBlitFramebuffer(0, fb->height(), fb->width(), 0, rx, ry, rw, rh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
|
||||
}
|
||||
|
||||
// select fbo texture read target
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_);
|
||||
|
||||
// select screen target
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
|
||||
// blit operation from fbo (containing texture) to screen
|
||||
glBlitFramebuffer(0, fb->height(), fb->width(), 0, rx, ry, rw, rh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
|
||||
}
|
||||
|
||||
// draw geometry
|
||||
else
|
||||
else if (!Settings::application.render.disabled)
|
||||
{
|
||||
// VAO is not shared between multiple contexts of different windows
|
||||
// so we have to create a new VAO for rendering the surface in this window
|
||||
@@ -888,11 +917,13 @@ void RenderingWindow::draw(FrameBuffer *fb)
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// restore attribs
|
||||
Rendering::manager().popAttrib();
|
||||
|
||||
// swap buffer
|
||||
glfwSwapBuffers(window_);
|
||||
}
|
||||
|
||||
// restore attribs
|
||||
Rendering::manager().popAttrib();
|
||||
|
||||
// give back context ownership
|
||||
glfwMakeContextCurrent(master_);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class RenderingWindow
|
||||
|
||||
GLFWwindow *window_, *master_;
|
||||
RenderingAttrib window_attributes_;
|
||||
std::string title_changed_;
|
||||
int index_;
|
||||
float dpi_scale_;
|
||||
|
||||
@@ -106,9 +107,8 @@ public:
|
||||
|
||||
// Initialization OpenGL and GLFW window creation
|
||||
bool init();
|
||||
|
||||
// show windows and reset views
|
||||
void show();
|
||||
|
||||
// true if active rendering window
|
||||
bool isActive();
|
||||
// draw one frame
|
||||
@@ -131,6 +131,7 @@ public:
|
||||
// get hold on the windows
|
||||
inline RenderingWindow& mainWindow() { return main_; }
|
||||
inline RenderingWindow& outputWindow() { return output_; }
|
||||
inline void setMainWindowTitle(const std::string t) { main_new_title_ = t; }
|
||||
|
||||
// request screenshot
|
||||
void requestScreenshot();
|
||||
@@ -160,6 +161,7 @@ private:
|
||||
std::list<RenderingCallback> draw_callbacks_;
|
||||
|
||||
RenderingWindow main_;
|
||||
std::string main_new_title_;
|
||||
RenderingWindow output_;
|
||||
|
||||
// file drop callback
|
||||
|
||||
38
Resource.cpp
38
Resource.cpp
@@ -1,6 +1,21 @@
|
||||
#include "defines.h"
|
||||
#include "Resource.h"
|
||||
#include "Log.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -17,6 +32,10 @@
|
||||
#include <cmrc/cmrc.hpp>
|
||||
CMRC_DECLARE(vmix);
|
||||
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
#include "Resource.h"
|
||||
|
||||
|
||||
std::map<std::string, uint> textureIndex;
|
||||
std::map<std::string, float> textureAspectRatio;
|
||||
@@ -162,9 +181,8 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
|
||||
uint mipMapCount = *(uint*)&(header[24]);
|
||||
uint fourCC = *(uint*)&(header[80]);
|
||||
|
||||
// how big is it going to be including all mipmaps?
|
||||
uint bufsize;
|
||||
bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
|
||||
// how big is it going to be including all mipmaps?
|
||||
uint bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
|
||||
|
||||
// get the buffer = bytes [128 - ]
|
||||
const char *buffer = fp + 128;
|
||||
@@ -188,7 +206,7 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
|
||||
}
|
||||
}
|
||||
|
||||
if (height == 0){
|
||||
if (height == 0 || bufsize == 0){
|
||||
Log::Error("Invalid image in ressource %s", std::string(path).c_str());
|
||||
return 0;
|
||||
}
|
||||
@@ -237,10 +255,9 @@ uint Resource::getTextureDDS(const std::string& path, float *aspect_ratio)
|
||||
uint Resource::getTextureImage(const std::string& path, float *aspect_ratio)
|
||||
{
|
||||
std::string ext = path.substr(path.find_last_of(".") + 1);
|
||||
if (ext=="dds")
|
||||
if (ext=="dds"){
|
||||
return getTextureDDS(path, aspect_ratio);
|
||||
|
||||
GLuint textureID = 0;
|
||||
}
|
||||
|
||||
// return previously openned resource if already openned before
|
||||
if (textureIndex.count(path) > 0) {
|
||||
@@ -248,6 +265,7 @@ uint Resource::getTextureImage(const std::string& path, float *aspect_ratio)
|
||||
return textureIndex[path];
|
||||
}
|
||||
|
||||
GLuint textureID = 0;
|
||||
float ar = 1.0;
|
||||
int w, h, n;
|
||||
unsigned char* img = nullptr;
|
||||
|
||||
30
Scene.cpp
30
Scene.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <glm/gtc/matrix_access.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
@@ -20,7 +39,9 @@
|
||||
#include "Scene.h"
|
||||
|
||||
#define DEBUG_SCENE 0
|
||||
#if DEBUG_SCENE
|
||||
static int num_nodes_ = 0;
|
||||
#endif
|
||||
|
||||
// Node
|
||||
Node::Node() : initialized_(false), visible_(true), refcount_(0)
|
||||
@@ -70,21 +91,18 @@ void Node::copyTransform(const Node *other)
|
||||
|
||||
void Node::update( float dt)
|
||||
{
|
||||
std::list<UpdateCallback *>::iterator iter;
|
||||
for (iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
||||
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
||||
{
|
||||
UpdateCallback *callback = *iter;
|
||||
|
||||
if (callback->enabled())
|
||||
callback->update(this, dt);
|
||||
callback->update(this, dt);
|
||||
|
||||
if (callback->finished()) {
|
||||
iter = update_callbacks_.erase(iter);
|
||||
delete callback;
|
||||
}
|
||||
else {
|
||||
else
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
// update transform matrix from attributes
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
#include "Screenshot.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <memory.h>
|
||||
#include <assert.h>
|
||||
@@ -11,6 +28,7 @@
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_write.h>
|
||||
|
||||
#include "Screenshot.h"
|
||||
|
||||
|
||||
Screenshot::Screenshot()
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
#include <algorithm>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "SearchVisitor.h"
|
||||
#include <algorithm>
|
||||
|
||||
#include "Scene.h"
|
||||
#include "MediaSource.h"
|
||||
#include "Session.h"
|
||||
#include "SessionSource.h"
|
||||
|
||||
#include "SearchVisitor.h"
|
||||
|
||||
SearchVisitor::SearchVisitor(Node *node) : Visitor(), node_(node), found_(false)
|
||||
{
|
||||
|
||||
@@ -52,7 +71,7 @@ SearchFileVisitor::SearchFileVisitor() : Visitor()
|
||||
|
||||
}
|
||||
|
||||
void SearchFileVisitor::visit(Node &n)
|
||||
void SearchFileVisitor::visit(Node &)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "defines.h"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef SELECTION_H
|
||||
#define SELECTION_H
|
||||
|
||||
#include <string>
|
||||
#include "SourceList.h"
|
||||
|
||||
class Selection
|
||||
|
||||
172
Session.cpp
172
Session.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "defines.h"
|
||||
@@ -5,20 +24,21 @@
|
||||
#include "Source.h"
|
||||
#include "Settings.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "Session.h"
|
||||
#include "FrameGrabber.h"
|
||||
#include "SessionCreator.h"
|
||||
#include "SessionSource.h"
|
||||
#include "MixingGroup.h"
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
#include "Session.h"
|
||||
|
||||
SessionNote::SessionNote(const std::string &t, bool l, int s): label(std::to_string(BaseToolkit::uniqueId())),
|
||||
text(t), large(l), stick(s), pos(glm::vec2(520.f, 30.f)), size(glm::vec2(220.f, 220.f))
|
||||
{
|
||||
}
|
||||
|
||||
Session::Session() : active_(true), filename_(""), failedSource_(nullptr), fading_target_(0.f)
|
||||
Session::Session() : active_(true), activation_threshold_(MIXING_MIN_THRESHOLD),
|
||||
filename_(""), failedSource_(nullptr), thumbnail_(nullptr)
|
||||
{
|
||||
config_[View::RENDERING] = new Group;
|
||||
config_[View::RENDERING]->scale_ = glm::vec3(0.f);
|
||||
@@ -40,6 +60,7 @@ Session::Session() : active_(true), filename_(""), failedSource_(nullptr), fadin
|
||||
config_[View::TEXTURE]->translation_ = Settings::application.views[View::TEXTURE].default_translation;
|
||||
|
||||
snapshots_.xmlDoc_ = new tinyxml2::XMLDocument;
|
||||
start_time_ = gst_util_get_timestamp ();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +89,11 @@ Session::~Session()
|
||||
delete snapshots_.xmlDoc_;
|
||||
}
|
||||
|
||||
uint64_t Session::runtime() const
|
||||
{
|
||||
return gst_util_get_timestamp () - start_time_;
|
||||
}
|
||||
|
||||
void Session::setActive (bool on)
|
||||
{
|
||||
if (active_ != on) {
|
||||
@@ -85,25 +111,28 @@ void Session::update(float dt)
|
||||
if ( render_.frame() == nullptr )
|
||||
return;
|
||||
|
||||
// pre-render of all sources
|
||||
// pre-render all sources
|
||||
failedSource_ = nullptr;
|
||||
bool ready = true;
|
||||
for( SourceList::iterator it = sources_.begin(); it != sources_.end(); ++it){
|
||||
|
||||
// ensure the RenderSource is rendering this session
|
||||
RenderSource *s = dynamic_cast<RenderSource *>( *it );
|
||||
if ( s!= nullptr && s->session() != this )
|
||||
s->setSession(this);
|
||||
// ensure the RenderSource is rendering *this* session
|
||||
RenderSource *rs = dynamic_cast<RenderSource *>( *it );
|
||||
if ( rs!= nullptr && rs->session() != this )
|
||||
rs->setSession(this);
|
||||
|
||||
// discard failed source
|
||||
if ( (*it)->failed() ) {
|
||||
failedSource_ = (*it);
|
||||
}
|
||||
// render normally
|
||||
else {
|
||||
if ( !(*it)->ready() )
|
||||
ready = false;
|
||||
// render the source
|
||||
(*it)->render();
|
||||
// update the source
|
||||
(*it)->setActive(activation_threshold_);
|
||||
(*it)->update(dt);
|
||||
}
|
||||
}
|
||||
@@ -121,10 +150,28 @@ void Session::update(float dt)
|
||||
group_iter = deleteMixingGroup(group_iter);
|
||||
}
|
||||
|
||||
// apply fading (smooth dicotomic reaching)
|
||||
float f = render_.fading();
|
||||
if ( ABS_DIFF(f, fading_target_) > EPSILON) {
|
||||
render_.setFading( f + ( fading_target_ - f ) / 2.f);
|
||||
// update fading requested
|
||||
if (fading_.active) {
|
||||
|
||||
// animate
|
||||
fading_.progress += dt;
|
||||
|
||||
// update animation
|
||||
if ( fading_.duration > 0.f && fading_.progress < fading_.duration ) {
|
||||
// interpolation
|
||||
float f = fading_.progress / fading_.duration;
|
||||
f = ( 1.f - f ) * fading_.start + f * fading_.target;
|
||||
render_.setFading( f );
|
||||
}
|
||||
// arrived at target
|
||||
else {
|
||||
// set precise value
|
||||
render_.setFading( fading_.target );
|
||||
// fading finished
|
||||
fading_.active = false;
|
||||
fading_.start = fading_.target;
|
||||
fading_.duration = fading_.progress = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// update the scene tree
|
||||
@@ -151,9 +198,9 @@ SourceList::iterator Session::addSource(Source *s)
|
||||
// insert the source in the rendering
|
||||
render_.scene.ws()->attach(s->group(View::RENDERING));
|
||||
// insert the source to the beginning of the list
|
||||
sources_.push_front(s);
|
||||
sources_.push_back(s);
|
||||
// return the iterator to the source created at the beginning
|
||||
its = sources_.begin();
|
||||
its = sources_.end()--;
|
||||
}
|
||||
|
||||
// unlock access
|
||||
@@ -228,6 +275,33 @@ Source *Session::popSource()
|
||||
return s;
|
||||
}
|
||||
|
||||
static void replaceThumbnail(Session *s)
|
||||
{
|
||||
if (s != nullptr) {
|
||||
FrameBufferImage *t = s->renderThumbnail();
|
||||
if (t != nullptr) // avoid recursive infinite loop
|
||||
s->setThumbnail(t);
|
||||
}
|
||||
}
|
||||
|
||||
void Session::setThumbnail(FrameBufferImage *t)
|
||||
{
|
||||
resetThumbnail();
|
||||
// replace with given image
|
||||
if (t != nullptr)
|
||||
thumbnail_ = t;
|
||||
// no thumbnail image given: capture from rendering in a parallel thread
|
||||
else
|
||||
std::thread( replaceThumbnail, this ).detach();
|
||||
}
|
||||
|
||||
void Session::resetThumbnail()
|
||||
{
|
||||
if (thumbnail_ != nullptr)
|
||||
delete thumbnail_;
|
||||
thumbnail_ = nullptr;
|
||||
}
|
||||
|
||||
void Session::setResolution(glm::vec3 resolution, bool useAlpha)
|
||||
{
|
||||
// setup the render view: if not specified the default config resulution will be used
|
||||
@@ -236,12 +310,17 @@ void Session::setResolution(glm::vec3 resolution, bool useAlpha)
|
||||
config_[View::RENDERING]->scale_ = render_.resolution();
|
||||
}
|
||||
|
||||
void Session::setFading(float f, bool forcenow)
|
||||
void Session::setFadingTarget(float f, float duration)
|
||||
{
|
||||
if (forcenow)
|
||||
render_.setFading( f );
|
||||
|
||||
fading_target_ = CLAMP(f, 0.f, 1.f);
|
||||
// targetted fading value
|
||||
fading_.target = CLAMP(f, 0.f, 1.f);
|
||||
// starting point for interpolation
|
||||
fading_.start = fading();
|
||||
// initiate animation
|
||||
fading_.progress = 0.f;
|
||||
fading_.duration = duration;
|
||||
// activate update
|
||||
fading_.active = true;
|
||||
}
|
||||
|
||||
SourceList::iterator Session::begin()
|
||||
@@ -314,7 +393,7 @@ bool Session::empty() const
|
||||
|
||||
SourceList::iterator Session::at(int index)
|
||||
{
|
||||
if (index<0)
|
||||
if ( index < 0 || index > (int) sources_.size())
|
||||
return sources_.end();
|
||||
|
||||
int i = 0;
|
||||
@@ -459,6 +538,59 @@ std::list<MixingGroup *>::iterator Session::endMixingGroup()
|
||||
return mixing_groups_.end();
|
||||
}
|
||||
|
||||
|
||||
size_t Session::numPlayGroups() const
|
||||
{
|
||||
return play_groups_.size();
|
||||
}
|
||||
|
||||
void Session::addPlayGroup(const SourceIdList &ids)
|
||||
{
|
||||
play_groups_.push_back( ids );
|
||||
}
|
||||
|
||||
void Session::addToPlayGroup(size_t i, Source *s)
|
||||
{
|
||||
if (i < play_groups_.size() )
|
||||
{
|
||||
if ( std::find(play_groups_[i].begin(), play_groups_[i].end(), s->id()) == play_groups_[i].end() )
|
||||
play_groups_[i].push_back(s->id());
|
||||
}
|
||||
}
|
||||
|
||||
void Session::removeFromPlayGroup(size_t i, Source *s)
|
||||
{
|
||||
if (i < play_groups_.size() )
|
||||
{
|
||||
if ( std::find(play_groups_[i].begin(), play_groups_[i].end(), s->id()) != play_groups_[i].end() )
|
||||
play_groups_[i].remove( s->id() );
|
||||
}
|
||||
}
|
||||
|
||||
void Session::deletePlayGroup(size_t i)
|
||||
{
|
||||
if (i < play_groups_.size() )
|
||||
play_groups_.erase( play_groups_.begin() + i);
|
||||
}
|
||||
|
||||
SourceList Session::playGroup(size_t i) const
|
||||
{
|
||||
SourceList list;
|
||||
|
||||
if (i < play_groups_.size() )
|
||||
{
|
||||
for (auto sid = play_groups_[i].begin(); sid != play_groups_[i].end(); ++sid){
|
||||
|
||||
SourceList::const_iterator it = std::find_if(sources_.begin(), sources_.end(), Source::hasId( *sid));;
|
||||
if ( it != sources_.end())
|
||||
list.push_back( *it);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void Session::lock()
|
||||
{
|
||||
access_.lock();
|
||||
|
||||
52
Session.h
52
Session.h
@@ -74,6 +74,7 @@ public:
|
||||
|
||||
// update all sources and mark sources which failed
|
||||
void update (float dt);
|
||||
uint64_t runtime() const;
|
||||
|
||||
// update mode (active or not)
|
||||
void setActive (bool on);
|
||||
@@ -85,15 +86,25 @@ public:
|
||||
// get frame result of render
|
||||
inline FrameBuffer *frame () const { return render_.frame(); }
|
||||
|
||||
// get thumbnail image
|
||||
inline FrameBufferImage *thumbnail () { return render_.thumbnail(); }
|
||||
// get an newly rendered thumbnail
|
||||
inline FrameBufferImage *renderThumbnail () { return render_.thumbnail(); }
|
||||
|
||||
// get / set thumbnail image
|
||||
inline FrameBufferImage *thumbnail () const { return thumbnail_; }
|
||||
void setThumbnail(FrameBufferImage *t = nullptr);
|
||||
void resetThumbnail();
|
||||
|
||||
// configure rendering resolution
|
||||
void setResolution (glm::vec3 resolution, bool useAlpha = false);
|
||||
|
||||
// manipulate fading of output
|
||||
void setFading (float f, bool forcenow = false);
|
||||
inline float fading () const { return fading_target_; }
|
||||
void setFadingTarget (float f, float duration = 0.f);
|
||||
inline float fadingTarget () const { return fading_.target; }
|
||||
inline float fading () const { return render_.fading(); }
|
||||
|
||||
// activation threshold for source (mixing distance)
|
||||
inline void setActivationThreshold(float t) { activation_threshold_ = t; }
|
||||
inline float activationThreshold() const { return activation_threshold_;}
|
||||
|
||||
// configuration for group nodes of views
|
||||
inline Group *config (View::Mode m) const { return config_.at(m); }
|
||||
@@ -124,7 +135,16 @@ public:
|
||||
std::list<MixingGroup *>::iterator deleteMixingGroup (std::list<MixingGroup *>::iterator g);
|
||||
|
||||
// snapshots
|
||||
SessionSnapshots * const snapshots () { return &snapshots_; }
|
||||
SessionSnapshots * snapshots () { return &snapshots_; }
|
||||
|
||||
// playlists
|
||||
void addPlayGroup(const SourceIdList &ids);
|
||||
void deletePlayGroup(size_t i);
|
||||
size_t numPlayGroups() const;
|
||||
SourceList playGroup(size_t i) const;
|
||||
void addToPlayGroup(size_t i, Source *s);
|
||||
void removeFromPlayGroup(size_t i, Source *s);
|
||||
std::vector<SourceIdList> getPlayGroups() { return play_groups_; }
|
||||
|
||||
// lock and unlock access (e.g. while saving)
|
||||
void lock ();
|
||||
@@ -132,6 +152,7 @@ public:
|
||||
|
||||
protected:
|
||||
bool active_;
|
||||
float activation_threshold_;
|
||||
RenderView render_;
|
||||
std::string filename_;
|
||||
Source *failedSource_;
|
||||
@@ -141,9 +162,28 @@ protected:
|
||||
std::list<MixingGroup *> mixing_groups_;
|
||||
std::map<View::Mode, Group*> config_;
|
||||
SessionSnapshots snapshots_;
|
||||
float fading_target_;
|
||||
std::vector<SourceIdList> play_groups_;
|
||||
std::mutex access_;
|
||||
FrameBufferImage *thumbnail_;
|
||||
uint64_t start_time_;
|
||||
|
||||
struct Fading
|
||||
{
|
||||
bool active;
|
||||
float start;
|
||||
float target;
|
||||
float duration;
|
||||
float progress;
|
||||
|
||||
Fading() {
|
||||
active = false;
|
||||
start = 0.f;
|
||||
target = 0.f;
|
||||
duration = 0.f;
|
||||
progress = 0.f;
|
||||
}
|
||||
};
|
||||
Fading fading_;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "Log.h"
|
||||
@@ -13,6 +32,7 @@
|
||||
#include "DeviceSource.h"
|
||||
#include "NetworkSource.h"
|
||||
#include "MultiFileSource.h"
|
||||
#include "StreamSource.h"
|
||||
#include "Session.h"
|
||||
#include "ImageShader.h"
|
||||
#include "ImageProcessingShader.h"
|
||||
@@ -54,7 +74,15 @@ SessionInformation SessionCreator::info(const std::string& filename)
|
||||
}
|
||||
const XMLElement *session = doc.FirstChildElement("Session");
|
||||
if (session != nullptr ) {
|
||||
ret.thumbnail = XMLToImage(session);
|
||||
const XMLElement *thumbnailelement = session->FirstChildElement("Thumbnail");
|
||||
// if there is a user defined thumbnail, get it
|
||||
if (thumbnailelement) {
|
||||
ret.thumbnail = XMLToImage(thumbnailelement);
|
||||
ret.user_thumbnail_ = true;
|
||||
}
|
||||
// otherwise get the default saved thumbnail in session
|
||||
else
|
||||
ret.thumbnail = XMLToImage(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +127,9 @@ void SessionCreator::load(const std::string& filename)
|
||||
loadConfig( xmlDoc_.FirstChildElement("Views") );
|
||||
|
||||
// ready to read sources
|
||||
SessionLoader::load( xmlDoc_.FirstChildElement("Session") );
|
||||
sessionFilePath_ = SystemToolkit::path_filename(filename);
|
||||
XMLElement *sessionNode = xmlDoc_.FirstChildElement("Session");
|
||||
SessionLoader::load( sessionNode );
|
||||
|
||||
// create groups
|
||||
std::list< SourceList > groups = getMixingGroups();
|
||||
@@ -112,6 +142,18 @@ void SessionCreator::load(const std::string& filename)
|
||||
// load notes
|
||||
loadNotes( xmlDoc_.FirstChildElement("Notes") );
|
||||
|
||||
// load playlists
|
||||
loadPlayGroups( xmlDoc_.FirstChildElement("PlayGroups") );
|
||||
|
||||
// thumbnail
|
||||
const XMLElement *thumbnailelement = sessionNode->FirstChildElement("Thumbnail");
|
||||
// if there is a user-defined thumbnail, get it
|
||||
if (thumbnailelement) {
|
||||
FrameBufferImage *thumbnail = XMLToImage(thumbnailelement);
|
||||
if (thumbnail != nullptr)
|
||||
session_->setThumbnail( thumbnail );
|
||||
}
|
||||
|
||||
// all good
|
||||
session_->setFilename(filename);
|
||||
}
|
||||
@@ -163,7 +205,8 @@ void SessionCreator::loadNotes(XMLElement *notesNode)
|
||||
XMLElement *sizeNode = note->FirstChildElement("size");
|
||||
if (sizeNode) tinyxml2::XMLElementToGLM( sizeNode->FirstChildElement("vec2"), N.size);
|
||||
XMLElement* contentNode = note->FirstChildElement("text");
|
||||
if (contentNode) N.text = std::string ( contentNode->GetText() );
|
||||
if (contentNode && contentNode->GetText())
|
||||
N.text = std::string ( contentNode->GetText() );
|
||||
|
||||
session_->addNote(N);
|
||||
}
|
||||
@@ -171,6 +214,29 @@ void SessionCreator::loadNotes(XMLElement *notesNode)
|
||||
}
|
||||
}
|
||||
|
||||
void SessionCreator::loadPlayGroups(tinyxml2::XMLElement *playgroupNode)
|
||||
{
|
||||
if (playgroupNode != nullptr && session_ != nullptr) {
|
||||
|
||||
XMLElement* playgroup = playgroupNode->FirstChildElement("PlayGroup");
|
||||
for( ; playgroup ; playgroup = playgroup->NextSiblingElement())
|
||||
{
|
||||
SourceIdList playgroup_sources;
|
||||
|
||||
XMLElement* playgroupSourceNode = playgroup->FirstChildElement("source");
|
||||
for ( ; playgroupSourceNode ; playgroupSourceNode = playgroupSourceNode->NextSiblingElement()) {
|
||||
uint64_t id__ = 0;
|
||||
playgroupSourceNode->QueryUnsigned64Attribute("id", &id__);
|
||||
|
||||
if (sources_id_.count(id__) > 0)
|
||||
playgroup_sources.push_back( id__ );
|
||||
|
||||
}
|
||||
session_->addPlayGroup( playgroup_sources );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SessionLoader::SessionLoader(): Visitor(),
|
||||
session_(nullptr), xmlCurrent_(nullptr), recursion_(0)
|
||||
{
|
||||
@@ -226,8 +292,18 @@ void SessionLoader::load(XMLElement *sessionNode)
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionNode != nullptr && session_ != nullptr) {
|
||||
if (sessionNode != nullptr && session_ != nullptr)
|
||||
{
|
||||
//
|
||||
// session attributes
|
||||
//
|
||||
float t = MIXING_MIN_THRESHOLD;
|
||||
sessionNode->QueryFloatAttribute("activationThreshold", &t);
|
||||
session_->setActivationThreshold(t);
|
||||
|
||||
//
|
||||
// source lists
|
||||
//
|
||||
XMLElement* sourceNode = sessionNode->FirstChildElement("Source");
|
||||
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
|
||||
{
|
||||
@@ -271,6 +347,9 @@ void SessionLoader::load(XMLElement *sessionNode)
|
||||
else if ( std::string(pType) == "MultiFileSource") {
|
||||
load_source = new MultiFileSource(id_xml_);
|
||||
}
|
||||
else if ( std::string(pType) == "GenericStreamSource") {
|
||||
load_source = new GenericStreamSource(id_xml_);
|
||||
}
|
||||
|
||||
// skip failed (including clones)
|
||||
if (!load_source)
|
||||
@@ -317,8 +396,13 @@ void SessionLoader::load(XMLElement *sessionNode)
|
||||
SourceList::iterator origin;
|
||||
if (id_origin_ > 0)
|
||||
origin = session_->find(id_origin_);
|
||||
else
|
||||
origin = session_->find( std::string ( originNode->GetText() ) );
|
||||
else {
|
||||
const char *text = originNode->GetText();
|
||||
if (text)
|
||||
origin = session_->find( std::string(text) );
|
||||
else
|
||||
origin = session_->end();
|
||||
}
|
||||
// found the orign source
|
||||
if (origin != session_->end()) {
|
||||
// create a new source of type Clone
|
||||
@@ -391,6 +475,9 @@ Source *SessionLoader::createSource(tinyxml2::XMLElement *sourceNode, Mode mode)
|
||||
else if ( std::string(pType) == "MultiFileSource") {
|
||||
load_source = new MultiFileSource(id__);
|
||||
}
|
||||
else if ( std::string(pType) == "GenericStreamSource") {
|
||||
load_source = new GenericStreamSource(id__);
|
||||
}
|
||||
else if ( std::string(pType) == "CloneSource") {
|
||||
// clone from given origin
|
||||
XMLElement* originNode = xmlCurrent_->FirstChildElement("origin");
|
||||
@@ -652,6 +739,14 @@ void SessionLoader::visit(MediaPlayer &n)
|
||||
mediaplayerNode->QueryBoolAttribute("software_decoding", &gpudisable);
|
||||
n.setSoftwareDecodingForced(gpudisable);
|
||||
|
||||
bool rewind_on_disabled = false;
|
||||
mediaplayerNode->QueryBoolAttribute("rewind_on_disabled", &rewind_on_disabled);
|
||||
n.setRewindOnDisabled(rewind_on_disabled);
|
||||
|
||||
int sync_to_metronome = 0;
|
||||
mediaplayerNode->QueryIntAttribute("sync_to_metronome", &sync_to_metronome);
|
||||
n.setSyncToMetronome( (Metronome::Synchronicity) sync_to_metronome);
|
||||
|
||||
bool play = true;
|
||||
mediaplayerNode->QueryBoolAttribute("play", &play);
|
||||
n.play(play);
|
||||
@@ -802,12 +897,27 @@ void SessionLoader::visit (Source& s)
|
||||
void SessionLoader::visit (MediaSource& s)
|
||||
{
|
||||
// set uri
|
||||
XMLElement* uriNode = xmlCurrent_->FirstChildElement("uri");
|
||||
if (uriNode) {
|
||||
std::string uri = std::string ( uriNode->GetText() );
|
||||
// load only new files
|
||||
if ( uri != s.path() )
|
||||
s.setPath(uri);
|
||||
XMLElement* pathNode = xmlCurrent_->FirstChildElement("uri"); // TODO change to "path" but keep backward compatibility
|
||||
if (pathNode) {
|
||||
const char * text = pathNode->GetText();
|
||||
if (text) {
|
||||
std::string path(text);
|
||||
// load only new files
|
||||
if ( path.compare(s.path()) != 0 ) {
|
||||
if ( !SystemToolkit::file_exists(path)){
|
||||
const char * relative;
|
||||
if ( pathNode->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
|
||||
std::string rel = SystemToolkit::path_absolute_from_path(std::string( relative ), sessionFilePath_);
|
||||
Log::Info("File %s not found; Trying %s instead.", path.c_str(), rel.c_str());
|
||||
path = rel;
|
||||
}
|
||||
}
|
||||
s.setPath(path);
|
||||
}
|
||||
}
|
||||
// ensures the source is initialized even if no valid path is given
|
||||
else
|
||||
s.setPath("");
|
||||
}
|
||||
|
||||
// set config media player
|
||||
@@ -819,14 +929,26 @@ void SessionLoader::visit (SessionFileSource& s)
|
||||
// set fading
|
||||
float f = 0.f;
|
||||
xmlCurrent_->QueryFloatAttribute("fading", &f);
|
||||
s.session()->setFading(f);
|
||||
s.session()->setFadingTarget(f);
|
||||
// set uri
|
||||
XMLElement* pathNode = xmlCurrent_->FirstChildElement("path");
|
||||
if (pathNode) {
|
||||
std::string path = std::string ( pathNode->GetText() );
|
||||
// load only new files
|
||||
if ( path != s.path() )
|
||||
s.load(path, recursion_ + 1);
|
||||
const char * text = pathNode->GetText();
|
||||
if (text) {
|
||||
std::string path(text);
|
||||
// load only new files
|
||||
if ( path != s.path() ) {
|
||||
if ( !SystemToolkit::file_exists(path)){
|
||||
const char * relative;
|
||||
if ( pathNode->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
|
||||
std::string rel = SystemToolkit::path_absolute_from_path(std::string( relative ), sessionFilePath_);
|
||||
Log::Info("File %s not found; Trying %s instead.", path.c_str(), rel.c_str());
|
||||
path = rel;
|
||||
}
|
||||
}
|
||||
s.load(path, recursion_ + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -894,40 +1016,68 @@ void SessionLoader::visit (MultiFileSource& s)
|
||||
if (seq) {
|
||||
|
||||
MultiFileSequence sequence;
|
||||
sequence.location = std::string ( seq->GetText() );
|
||||
seq->QueryIntAttribute("min", &sequence.min);
|
||||
seq->QueryIntAttribute("max", &sequence.max);
|
||||
seq->QueryUnsignedAttribute("width", &sequence.width);
|
||||
seq->QueryUnsignedAttribute("height", &sequence.height);
|
||||
const char *codec = seq->Attribute("codec");
|
||||
if (codec)
|
||||
sequence.codec = std::string(codec);
|
||||
|
||||
uint fps = 0;
|
||||
seq->QueryUnsignedAttribute("fps", &fps);
|
||||
const char *text = seq->GetText();
|
||||
if (text) {
|
||||
sequence.location = std::string (text);
|
||||
|
||||
// different sequence
|
||||
if ( sequence != s.sequence() ) {
|
||||
s.setSequence( sequence, fps);
|
||||
// fix path if absolute path is not found
|
||||
std::string folder = SystemToolkit::path_filename(sequence.location);
|
||||
std::string dir = SystemToolkit::path_directory(folder);
|
||||
if ( dir.empty() ){
|
||||
const char * relative;
|
||||
if ( seq->QueryStringAttribute("relative", &relative) == XML_SUCCESS) {
|
||||
std::string rel = SystemToolkit::path_absolute_from_path(std::string(relative), sessionFilePath_);
|
||||
Log::Info("Folder %s not found; Trying %s instead.", folder.c_str(), rel.c_str());
|
||||
sequence.location = rel;
|
||||
}
|
||||
}
|
||||
|
||||
// set sequence parameters
|
||||
seq->QueryIntAttribute("min", &sequence.min);
|
||||
seq->QueryIntAttribute("max", &sequence.max);
|
||||
seq->QueryUnsignedAttribute("width", &sequence.width);
|
||||
seq->QueryUnsignedAttribute("height", &sequence.height);
|
||||
const char *codec = seq->Attribute("codec");
|
||||
if (codec)
|
||||
sequence.codec = std::string(codec);
|
||||
|
||||
uint fps = 0;
|
||||
seq->QueryUnsignedAttribute("fps", &fps);
|
||||
|
||||
// different sequence
|
||||
if ( sequence != s.sequence() ) {
|
||||
s.setSequence( sequence, fps);
|
||||
}
|
||||
// same sequence, different framerate
|
||||
else if ( fps != s.framerate() ) {
|
||||
s.setFramerate( fps );
|
||||
}
|
||||
|
||||
int begin = -1;
|
||||
seq->QueryIntAttribute("begin", &begin);
|
||||
int end = INT_MAX;
|
||||
seq->QueryIntAttribute("end", &end);
|
||||
if ( begin != s.begin() || end != s.end() )
|
||||
s.setRange(begin, end);
|
||||
|
||||
bool loop = true;
|
||||
seq->QueryBoolAttribute("loop", &loop);
|
||||
if ( loop != s.loop() )
|
||||
s.setLoop(loop);
|
||||
}
|
||||
// same sequence, different framerate
|
||||
else if ( fps != s.framerate() ) {
|
||||
s.setFramerate( fps );
|
||||
}
|
||||
|
||||
int begin = -1;
|
||||
seq->QueryIntAttribute("begin", &begin);
|
||||
int end = INT_MAX;
|
||||
seq->QueryIntAttribute("end", &end);
|
||||
if ( begin != s.begin() || end != s.end() )
|
||||
s.setRange(begin, end);
|
||||
|
||||
bool loop = true;
|
||||
seq->QueryBoolAttribute("loop", &loop);
|
||||
if ( loop != s.loop() )
|
||||
s.setLoop(loop);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SessionLoader::visit (GenericStreamSource& s)
|
||||
{
|
||||
XMLElement* desc = xmlCurrent_->FirstChildElement("Description");
|
||||
|
||||
if (desc) {
|
||||
const char * text = desc->GetText();
|
||||
if (text)
|
||||
s.setDescription(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ public:
|
||||
void visit (DeviceSource& s) override;
|
||||
void visit (NetworkSource& s) override;
|
||||
void visit (MultiFileSource& s) override;
|
||||
void visit (GenericStreamSource& s) override;
|
||||
|
||||
static void XMLToNode(const tinyxml2::XMLElement *xml, Node &n);
|
||||
static void XMLToSourcecore(tinyxml2::XMLElement *xml, SourceCore &s);
|
||||
@@ -67,6 +68,7 @@ public:
|
||||
protected:
|
||||
// result created session
|
||||
Session *session_;
|
||||
std::string sessionFilePath_;
|
||||
// parsing current xml
|
||||
tinyxml2::XMLElement *xmlCurrent_;
|
||||
// level of loading recursion
|
||||
@@ -81,9 +83,11 @@ protected:
|
||||
struct SessionInformation {
|
||||
std::string description;
|
||||
FrameBufferImage *thumbnail;
|
||||
bool user_thumbnail_;
|
||||
SessionInformation() {
|
||||
description = "";
|
||||
thumbnail = nullptr;
|
||||
user_thumbnail_ = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,6 +97,7 @@ class SessionCreator : public SessionLoader {
|
||||
|
||||
void loadConfig(tinyxml2::XMLElement *viewsNode);
|
||||
void loadNotes(tinyxml2::XMLElement *notesNode);
|
||||
void loadPlayGroups(tinyxml2::XMLElement *playlistsNode);
|
||||
void loadSnapshots(tinyxml2::XMLElement *snapshotNode);
|
||||
|
||||
public:
|
||||
|
||||
129
SessionParser.cpp
Normal file
129
SessionParser.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
|
||||
#include "SystemToolkit.h"
|
||||
#include "tinyxml2Toolkit.h"
|
||||
using namespace tinyxml2;
|
||||
|
||||
#include "SessionParser.h"
|
||||
|
||||
SessionParser::SessionParser()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool SessionParser::open(const std::string &filename)
|
||||
{
|
||||
// if the file exists
|
||||
if (filename.empty() || !SystemToolkit::file_exists(filename))
|
||||
return false;
|
||||
|
||||
// try to load the file
|
||||
xmlDoc_.Clear();
|
||||
XMLError eResult = xmlDoc_.LoadFile(filename.c_str());
|
||||
|
||||
// error
|
||||
if ( XMLResultError(eResult, false) )
|
||||
return false;
|
||||
|
||||
filename_ = filename;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SessionParser::save()
|
||||
{
|
||||
if (filename_.empty())
|
||||
return false;
|
||||
|
||||
// save file to disk
|
||||
return ( XMLSaveDoc(&xmlDoc_, filename_) );
|
||||
}
|
||||
|
||||
std::map< uint64_t, std::pair<std::string, bool> > SessionParser::pathList() const
|
||||
{
|
||||
std::map< uint64_t, std::pair<std::string, bool> > paths;
|
||||
|
||||
// fill path list
|
||||
const XMLElement *session = xmlDoc_.FirstChildElement("Session");
|
||||
if (session != nullptr ) {
|
||||
const XMLElement *sourceNode = session->FirstChildElement("Source");
|
||||
|
||||
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
|
||||
{
|
||||
// get id
|
||||
uint64_t sid = 0;
|
||||
sourceNode->QueryUnsigned64Attribute("id", &sid);
|
||||
|
||||
// get path
|
||||
const XMLElement* pathNode = nullptr;
|
||||
|
||||
pathNode = sourceNode->FirstChildElement("uri");
|
||||
if (!pathNode)
|
||||
pathNode = sourceNode->FirstChildElement("path");
|
||||
if (!pathNode)
|
||||
pathNode = sourceNode->FirstChildElement("Sequence");
|
||||
|
||||
if (pathNode) {
|
||||
const char *text = pathNode->GetText();
|
||||
if (text) {
|
||||
bool exists = SystemToolkit::file_exists(text);
|
||||
paths[sid] = std::pair<std::string, bool>(std::string(text), exists);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return path list
|
||||
return paths;
|
||||
}
|
||||
|
||||
void SessionParser::replacePath(uint64_t id, const std::string &path)
|
||||
{
|
||||
XMLElement *session = xmlDoc_.FirstChildElement("Session");
|
||||
if (session != nullptr ) {
|
||||
XMLElement *sourceNode = session->FirstChildElement("Source");
|
||||
|
||||
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
|
||||
{
|
||||
// get id
|
||||
uint64_t sid = 0;
|
||||
sourceNode->QueryUnsigned64Attribute("id", &sid);
|
||||
|
||||
if (sid == id) {
|
||||
|
||||
// get path
|
||||
XMLElement* pathNode = nullptr;
|
||||
|
||||
pathNode = sourceNode->FirstChildElement("uri");
|
||||
if (!pathNode)
|
||||
pathNode = sourceNode->FirstChildElement("path");
|
||||
if (!pathNode)
|
||||
pathNode = sourceNode->FirstChildElement("Sequence");
|
||||
|
||||
if (pathNode) {
|
||||
XMLText *text = xmlDoc_.NewText( path.c_str() );
|
||||
pathNode->InsertEndChild( text );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
SessionParser.h
Normal file
40
SessionParser.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef SESSIONPARSER_H
|
||||
#define SESSIONPARSER_H
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <tinyxml2.h>
|
||||
|
||||
|
||||
class Session;
|
||||
|
||||
//struct SessionInformation {
|
||||
// std::string description;
|
||||
// FrameBufferImage *thumbnail;
|
||||
// bool user_thumbnail_;
|
||||
// SessionInformation() {
|
||||
// description = "";
|
||||
// thumbnail = nullptr;
|
||||
// user_thumbnail_ = false;
|
||||
// }
|
||||
//};
|
||||
|
||||
class SessionParser
|
||||
{
|
||||
public:
|
||||
SessionParser();
|
||||
|
||||
bool open(const std::string& filename);
|
||||
bool save();
|
||||
|
||||
std::map<uint64_t, std::pair<std::string, bool> > pathList() const;
|
||||
void replacePath(uint64_t id, const std::string &path);
|
||||
|
||||
// static SessionInformation info(const std::string& filename);
|
||||
|
||||
private:
|
||||
tinyxml2::XMLDocument xmlDoc_;
|
||||
std::string filename_;
|
||||
};
|
||||
|
||||
#endif // SESSIONPARSER_H
|
||||
@@ -1,9 +1,25 @@
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "SessionSource.h"
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include "defines.h"
|
||||
#include "Log.h"
|
||||
@@ -17,8 +33,9 @@
|
||||
#include "SessionCreator.h"
|
||||
#include "Mixer.h"
|
||||
|
||||
#include "SessionSource.h"
|
||||
|
||||
SessionSource::SessionSource(uint64_t id) : Source(id), failed_(false)
|
||||
SessionSource::SessionSource(uint64_t id) : Source(id), failed_(false), timer_(0), paused_(false)
|
||||
{
|
||||
session_ = new Session;
|
||||
}
|
||||
@@ -62,12 +79,21 @@ uint SessionSource::texture() const
|
||||
}
|
||||
|
||||
void SessionSource::setActive (bool on)
|
||||
{
|
||||
{
|
||||
Source::setActive(on);
|
||||
|
||||
// change status of session (recursive change of internal sources)
|
||||
if (session_ != nullptr)
|
||||
if (session_) {
|
||||
session_->setActive(active_);
|
||||
|
||||
// change visibility of active surface (show preview of session when inactive)
|
||||
if (activesurface_) {
|
||||
if (active_)
|
||||
activesurface_->setTextureIndex(Resource::getTextureTransparent());
|
||||
else
|
||||
activesurface_->setTextureIndex(session_->frame()->texture());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SessionSource::update(float dt)
|
||||
@@ -76,8 +102,10 @@ void SessionSource::update(float dt)
|
||||
return;
|
||||
|
||||
// update content
|
||||
if (active_)
|
||||
if (active_ && !paused_) {
|
||||
session_->update(dt);
|
||||
timer_ += guint64(dt * 1000.f) * GST_USECOND;
|
||||
}
|
||||
|
||||
// delete a source which failed
|
||||
if (session_->failedSource() != nullptr) {
|
||||
@@ -90,6 +118,14 @@ void SessionSource::update(float dt)
|
||||
Source::update(dt);
|
||||
}
|
||||
|
||||
void SessionSource::replay ()
|
||||
{
|
||||
if (session_) {
|
||||
for( SourceList::iterator it = session_->begin(); it != session_->end(); ++it)
|
||||
(*it)->replay();
|
||||
timer_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
SessionFileSource::SessionFileSource(uint64_t id) : SessionSource(id), path_(""), initialized_(false), wait_for_sources_(false)
|
||||
{
|
||||
@@ -150,6 +186,7 @@ void SessionFileSource::load(const std::string &p, uint recursion)
|
||||
}
|
||||
|
||||
// will be ready after init and one frame rendered
|
||||
initialized_ = false;
|
||||
ready_ = false;
|
||||
}
|
||||
|
||||
@@ -242,6 +279,16 @@ void SessionFileSource::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
glm::ivec2 SessionFileSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_SESSION);
|
||||
}
|
||||
|
||||
std::string SessionFileSource::info() const
|
||||
{
|
||||
return std::string("session vimix '") + path_ + "'";
|
||||
}
|
||||
|
||||
|
||||
SessionGroupSource::SessionGroupSource(uint64_t id) : SessionSource(id), resolution_(glm::vec3(0.f))
|
||||
{
|
||||
@@ -298,7 +345,7 @@ void SessionGroupSource::init()
|
||||
++View::need_deep_update_;
|
||||
|
||||
// done init
|
||||
Log::Info("Source Group (%d x %d).", int(renderbuffer->resolution().x), int(renderbuffer->resolution().y) );
|
||||
Log::Info("Source Group created (%d x %d).", int(renderbuffer->resolution().x), int(renderbuffer->resolution().y) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +370,19 @@ void SessionGroupSource::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
glm::ivec2 SessionGroupSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_GROUP);
|
||||
}
|
||||
|
||||
std::string SessionGroupSource::info() const
|
||||
{
|
||||
if (session_)
|
||||
return std::string("group of ") + std::to_string(session_->numSource()) + " sources";
|
||||
else
|
||||
return std::string("undefined group.");
|
||||
}
|
||||
|
||||
RenderSource::RenderSource(uint64_t id) : Source(id), session_(nullptr)
|
||||
{
|
||||
// set symbol
|
||||
@@ -332,7 +392,7 @@ RenderSource::RenderSource(uint64_t id) : Source(id), session_(nullptr)
|
||||
|
||||
bool RenderSource::failed() const
|
||||
{
|
||||
if ( mode_ > Source::UNINITIALIZED && session_!=nullptr )
|
||||
if ( renderbuffer_ != nullptr && session_ != nullptr )
|
||||
return renderbuffer_->resolution() != session_->frame()->resolution();
|
||||
|
||||
return false;
|
||||
@@ -372,7 +432,7 @@ void RenderSource::init()
|
||||
|
||||
glm::vec3 RenderSource::resolution() const
|
||||
{
|
||||
if (mode_ > Source::UNINITIALIZED)
|
||||
if (renderbuffer_ != nullptr)
|
||||
return renderbuffer_->resolution();
|
||||
else if (session_ && session_->frame())
|
||||
return session_->frame()->resolution();
|
||||
@@ -386,3 +446,13 @@ void RenderSource::accept(Visitor& v)
|
||||
// if (!failed())
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
glm::ivec2 RenderSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_RENDER);
|
||||
}
|
||||
|
||||
std::string RenderSource::info() const
|
||||
{
|
||||
return std::string("Render loopback");
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ public:
|
||||
// implementation of source API
|
||||
void update (float dt) override;
|
||||
void setActive (bool on) override;
|
||||
bool playing () const override { return !paused_; }
|
||||
void play (bool on) override { paused_ = !on; }
|
||||
bool playable () const override { return true; }
|
||||
guint64 playtime () const override { return timer_; }
|
||||
void replay () override;
|
||||
bool failed () const override;
|
||||
uint texture () const override;
|
||||
|
||||
@@ -24,6 +29,8 @@ protected:
|
||||
|
||||
Session *session_;
|
||||
std::atomic<bool> failed_;
|
||||
guint64 timer_;
|
||||
bool paused_;
|
||||
};
|
||||
|
||||
class SessionFileSource : public SessionSource
|
||||
@@ -39,7 +46,9 @@ public:
|
||||
void load(const std::string &p = "", uint recursion = 0);
|
||||
|
||||
inline std::string path() const { return path_; }
|
||||
glm::ivec2 icon() const override { return glm::ivec2(19, 6); }
|
||||
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -65,7 +74,8 @@ public:
|
||||
// import a source
|
||||
bool import(Source *source);
|
||||
|
||||
glm::ivec2 icon() const override { return glm::ivec2(10, 6); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -80,6 +90,9 @@ public:
|
||||
RenderSource(uint64_t id = 0);
|
||||
|
||||
// implementation of source API
|
||||
bool playing () const override { return true; }
|
||||
void play (bool) override {}
|
||||
bool playable () const override { return false; }
|
||||
bool failed () const override;
|
||||
uint texture() const override;
|
||||
void accept (Visitor& v) override;
|
||||
@@ -89,7 +102,8 @@ public:
|
||||
|
||||
glm::vec3 resolution() const;
|
||||
|
||||
glm::ivec2 icon() const override { return glm::ivec2(0, 2); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
protected:
|
||||
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
#include "SessionVisitor.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
|
||||
#include <tinyxml2.h>
|
||||
using namespace tinyxml2;
|
||||
|
||||
#include "Log.h"
|
||||
#include "defines.h"
|
||||
@@ -19,11 +42,7 @@
|
||||
#include "SystemToolkit.h"
|
||||
#include "ActionManager.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
|
||||
#include <tinyxml2.h>
|
||||
using namespace tinyxml2;
|
||||
#include "SessionVisitor.h"
|
||||
|
||||
|
||||
bool SessionVisitor::saveSession(const std::string& filename, Session *session)
|
||||
@@ -46,75 +65,139 @@ bool SessionVisitor::saveSession(const std::string& filename, Session *session)
|
||||
XMLElement *sessionNode = xmlDoc.NewElement("Session");
|
||||
xmlDoc.InsertEndChild(sessionNode);
|
||||
SessionVisitor sv(&xmlDoc, sessionNode);
|
||||
sv.sessionFilePath_ = SystemToolkit::path_filename(filename);
|
||||
for (auto iter = session->begin(); iter != session->end(); ++iter, sv.setRoot(sessionNode) )
|
||||
// source visitor
|
||||
(*iter)->accept(sv);
|
||||
|
||||
// get the thumbnail
|
||||
// save session attributes
|
||||
sessionNode->SetAttribute("activationThreshold", session->activationThreshold());
|
||||
|
||||
// save the thumbnail
|
||||
FrameBufferImage *thumbnail = session->thumbnail();
|
||||
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
|
||||
if (imageelement)
|
||||
sessionNode->InsertEndChild(imageelement);
|
||||
delete thumbnail;
|
||||
if (thumbnail != nullptr && thumbnail->width > 0 && thumbnail->height > 0) {
|
||||
XMLElement *thumbnailelement = xmlDoc.NewElement("Thumbnail");
|
||||
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
|
||||
if (imageelement) {
|
||||
sessionNode->InsertEndChild(thumbnailelement);
|
||||
thumbnailelement->InsertEndChild(imageelement);
|
||||
}
|
||||
}
|
||||
// if no thumbnail is set by user, capture thumbnail now
|
||||
else {
|
||||
thumbnail = session->renderThumbnail();
|
||||
if (thumbnail) {
|
||||
XMLElement *imageelement = SessionVisitor::ImageToXML(thumbnail, &xmlDoc);
|
||||
if (imageelement)
|
||||
sessionNode->InsertEndChild(imageelement);
|
||||
delete thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. config of views
|
||||
XMLElement *views = xmlDoc.NewElement("Views");
|
||||
xmlDoc.InsertEndChild(views);
|
||||
{
|
||||
XMLElement *mixing = xmlDoc.NewElement( "Mixing" );
|
||||
mixing->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::MIXING), &xmlDoc));
|
||||
views->InsertEndChild(mixing);
|
||||
|
||||
XMLElement *geometry = xmlDoc.NewElement( "Geometry" );
|
||||
geometry->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::GEOMETRY), &xmlDoc));
|
||||
views->InsertEndChild(geometry);
|
||||
|
||||
XMLElement *layer = xmlDoc.NewElement( "Layer" );
|
||||
layer->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::LAYER), &xmlDoc));
|
||||
views->InsertEndChild(layer);
|
||||
|
||||
XMLElement *appearance = xmlDoc.NewElement( "Texture" );
|
||||
appearance->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::TEXTURE), &xmlDoc));
|
||||
views->InsertEndChild(appearance);
|
||||
|
||||
XMLElement *render = xmlDoc.NewElement( "Rendering" );
|
||||
render->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::RENDERING), &xmlDoc));
|
||||
views->InsertEndChild(render);
|
||||
}
|
||||
saveConfig( &xmlDoc, session );
|
||||
|
||||
// 3. snapshots
|
||||
XMLElement *snapshots = xmlDoc.NewElement("Snapshots");
|
||||
const XMLElement* N = session->snapshots()->xmlDoc_->FirstChildElement();
|
||||
for( ; N ; N=N->NextSiblingElement())
|
||||
snapshots->InsertEndChild( N->DeepClone( &xmlDoc ));
|
||||
xmlDoc.InsertEndChild(snapshots);
|
||||
saveSnapshots( &xmlDoc, session );
|
||||
|
||||
// 4. optional notes
|
||||
XMLElement *notes = xmlDoc.NewElement("Notes");
|
||||
xmlDoc.InsertEndChild(notes);
|
||||
for (auto nit = session->beginNotes(); nit != session->endNotes(); ++nit) {
|
||||
XMLElement *note = xmlDoc.NewElement( "Note" );
|
||||
note->SetAttribute("large", (*nit).large );
|
||||
note->SetAttribute("stick", (*nit).stick );
|
||||
XMLElement *pos = xmlDoc.NewElement("pos");
|
||||
pos->InsertEndChild( XMLElementFromGLM(&xmlDoc, (*nit).pos) );
|
||||
note->InsertEndChild(pos);
|
||||
XMLElement *size = xmlDoc.NewElement("size");
|
||||
size->InsertEndChild( XMLElementFromGLM(&xmlDoc, (*nit).size) );
|
||||
note->InsertEndChild(size);
|
||||
XMLElement *content = xmlDoc.NewElement("text");
|
||||
XMLText *text = xmlDoc.NewText( (*nit).text.c_str() );
|
||||
content->InsertEndChild( text );
|
||||
note->InsertEndChild(content);
|
||||
|
||||
notes->InsertEndChild(note);
|
||||
}
|
||||
saveNotes( &xmlDoc, session );
|
||||
|
||||
// 5. optional playlists
|
||||
savePlayGroups( &xmlDoc, session );
|
||||
|
||||
// save file to disk
|
||||
return ( XMLSaveDoc(&xmlDoc, filename) );
|
||||
}
|
||||
|
||||
void SessionVisitor::saveConfig(tinyxml2::XMLDocument *doc, Session *session)
|
||||
{
|
||||
if (doc != nullptr && session != nullptr)
|
||||
{
|
||||
XMLElement *views = doc->NewElement("Views");
|
||||
|
||||
XMLElement *mixing = doc->NewElement( "Mixing" );
|
||||
mixing->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::MIXING), doc));
|
||||
views->InsertEndChild(mixing);
|
||||
|
||||
XMLElement *geometry = doc->NewElement( "Geometry" );
|
||||
geometry->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::GEOMETRY), doc));
|
||||
views->InsertEndChild(geometry);
|
||||
|
||||
XMLElement *layer = doc->NewElement( "Layer" );
|
||||
layer->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::LAYER), doc));
|
||||
views->InsertEndChild(layer);
|
||||
|
||||
XMLElement *appearance = doc->NewElement( "Texture" );
|
||||
appearance->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::TEXTURE), doc));
|
||||
views->InsertEndChild(appearance);
|
||||
|
||||
XMLElement *render = doc->NewElement( "Rendering" );
|
||||
render->InsertEndChild( SessionVisitor::NodeToXML(*session->config(View::RENDERING), doc));
|
||||
views->InsertEndChild(render);
|
||||
|
||||
doc->InsertEndChild(views);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void SessionVisitor::saveSnapshots(tinyxml2::XMLDocument *doc, Session *session)
|
||||
{
|
||||
if (doc != nullptr && session != nullptr)
|
||||
{
|
||||
XMLElement *snapshots = doc->NewElement("Snapshots");
|
||||
const XMLElement* N = session->snapshots()->xmlDoc_->FirstChildElement();
|
||||
for( ; N ; N=N->NextSiblingElement())
|
||||
snapshots->InsertEndChild( N->DeepClone( doc ));
|
||||
doc->InsertEndChild(snapshots);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionVisitor::saveNotes(tinyxml2::XMLDocument *doc, Session *session)
|
||||
{
|
||||
if (doc != nullptr && session != nullptr)
|
||||
{
|
||||
XMLElement *notes = doc->NewElement("Notes");
|
||||
for (auto nit = session->beginNotes(); nit != session->endNotes(); ++nit) {
|
||||
XMLElement *note = doc->NewElement( "Note" );
|
||||
note->SetAttribute("large", (*nit).large );
|
||||
note->SetAttribute("stick", (*nit).stick );
|
||||
XMLElement *pos = doc->NewElement("pos");
|
||||
pos->InsertEndChild( XMLElementFromGLM(doc, (*nit).pos) );
|
||||
note->InsertEndChild(pos);
|
||||
XMLElement *size = doc->NewElement("size");
|
||||
size->InsertEndChild( XMLElementFromGLM(doc, (*nit).size) );
|
||||
note->InsertEndChild(size);
|
||||
XMLElement *content = doc->NewElement("text");
|
||||
XMLText *text = doc->NewText( (*nit).text.c_str() );
|
||||
content->InsertEndChild( text );
|
||||
note->InsertEndChild(content);
|
||||
|
||||
notes->InsertEndChild(note);
|
||||
}
|
||||
doc->InsertEndChild(notes);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionVisitor::savePlayGroups(tinyxml2::XMLDocument *doc, Session *session)
|
||||
{
|
||||
if (doc != nullptr && session != nullptr)
|
||||
{
|
||||
XMLElement *playlistNode = doc->NewElement("PlayGroups");
|
||||
std::vector<SourceIdList> pl = session->getPlayGroups();
|
||||
for (auto plit = pl.begin(); plit != pl.end(); ++plit) {
|
||||
XMLElement *list = doc->NewElement("PlayGroup");
|
||||
playlistNode->InsertEndChild(list);
|
||||
for (auto id = plit->begin(); id != plit->end(); ++id) {
|
||||
XMLElement *sour = doc->NewElement("source");
|
||||
sour->SetAttribute("id", *id);
|
||||
list->InsertEndChild(sour);
|
||||
}
|
||||
}
|
||||
doc->InsertEndChild(playlistNode);
|
||||
}
|
||||
}
|
||||
|
||||
SessionVisitor::SessionVisitor(tinyxml2::XMLDocument *doc,
|
||||
tinyxml2::XMLElement *root,
|
||||
bool recursive) : Visitor(), recursive_(recursive), xmlCurrent_(root)
|
||||
@@ -263,14 +346,6 @@ void SessionVisitor::visit(FrameBufferSurface &)
|
||||
xmlCurrent_->SetAttribute("type", "FrameBufferSurface");
|
||||
}
|
||||
|
||||
void SessionVisitor::visit(MediaSurface &n)
|
||||
{
|
||||
// Node of a different type
|
||||
xmlCurrent_->SetAttribute("type", "MediaSurface");
|
||||
|
||||
n.mediaPlayer()->accept(*this);
|
||||
}
|
||||
|
||||
void SessionVisitor::visit(MediaPlayer &n)
|
||||
{
|
||||
XMLElement *newelement = xmlDoc_->NewElement("MediaPlayer");
|
||||
@@ -281,6 +356,8 @@ void SessionVisitor::visit(MediaPlayer &n)
|
||||
newelement->SetAttribute("loop", (int) n.loop());
|
||||
newelement->SetAttribute("speed", n.playSpeed());
|
||||
newelement->SetAttribute("software_decoding", n.softwareDecodingForced());
|
||||
newelement->SetAttribute("rewind_on_disabled", n.rewindOnDisabled());
|
||||
newelement->SetAttribute("sync_to_metronome", (int) n.syncToMetronome());
|
||||
|
||||
// timeline
|
||||
XMLElement *timelineelement = xmlDoc_->NewElement("Timeline");
|
||||
@@ -511,6 +588,9 @@ void SessionVisitor::visit (MediaSource& s)
|
||||
XMLText *text = xmlDoc_->NewText( s.path().c_str() );
|
||||
uri->InsertEndChild( text );
|
||||
|
||||
if (!sessionFilePath_.empty())
|
||||
uri->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.path(), sessionFilePath_).c_str());
|
||||
|
||||
s.mediaplayer()->accept(*this);
|
||||
}
|
||||
|
||||
@@ -524,6 +604,9 @@ void SessionVisitor::visit (SessionFileSource& s)
|
||||
xmlCurrent_->InsertEndChild(path);
|
||||
XMLText *text = xmlDoc_->NewText( s.path().c_str() );
|
||||
path->InsertEndChild( text );
|
||||
|
||||
if (!sessionFilePath_.empty())
|
||||
path->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.path(), sessionFilePath_).c_str());
|
||||
}
|
||||
|
||||
void SessionVisitor::visit (SessionGroupSource& s)
|
||||
@@ -610,12 +693,29 @@ void SessionVisitor::visit (MultiFileSource& s)
|
||||
sequence->SetAttribute("width", s.sequence().width);
|
||||
sequence->SetAttribute("height", s.sequence().height);
|
||||
sequence->SetAttribute("codec", s.sequence().codec.c_str());
|
||||
|
||||
if (!sessionFilePath_.empty())
|
||||
sequence->SetAttribute("relative", SystemToolkit::path_relative_to_path(s.sequence().location, sessionFilePath_).c_str());
|
||||
|
||||
XMLText *location = xmlDoc_->NewText( s.sequence().location.c_str() );
|
||||
sequence->InsertEndChild( location );
|
||||
|
||||
xmlCurrent_->InsertEndChild(sequence);
|
||||
}
|
||||
|
||||
|
||||
void SessionVisitor::visit (GenericStreamSource& s)
|
||||
{
|
||||
xmlCurrent_->SetAttribute("type", "GenericStreamSource");
|
||||
|
||||
XMLElement *desc = xmlDoc_->NewElement("Description");
|
||||
|
||||
XMLText *text = xmlDoc_->NewText( s.description().c_str() );
|
||||
desc->InsertEndChild( text );
|
||||
|
||||
xmlCurrent_->InsertEndChild(desc);
|
||||
}
|
||||
|
||||
std::string SessionVisitor::getClipboard(const SourceList &list)
|
||||
{
|
||||
std::string x = "";
|
||||
|
||||
@@ -13,6 +13,12 @@ class SessionVisitor : public Visitor {
|
||||
bool recursive_;
|
||||
tinyxml2::XMLDocument *xmlDoc_;
|
||||
tinyxml2::XMLElement *xmlCurrent_;
|
||||
std::string sessionFilePath_;
|
||||
|
||||
static void saveConfig(tinyxml2::XMLDocument *doc, Session *session);
|
||||
static void saveSnapshots(tinyxml2::XMLDocument *doc, Session *session);
|
||||
static void saveNotes(tinyxml2::XMLDocument *doc, Session *session);
|
||||
static void savePlayGroups(tinyxml2::XMLDocument *doc, Session *session);
|
||||
|
||||
public:
|
||||
SessionVisitor(tinyxml2::XMLDocument *doc = nullptr,
|
||||
@@ -35,7 +41,6 @@ public:
|
||||
void visit (Primitive& n) override;
|
||||
void visit (Surface&) override;
|
||||
void visit (ImageSurface& n) override;
|
||||
void visit (MediaSurface& n) override;
|
||||
void visit (FrameBufferSurface&) override;
|
||||
void visit (LineStrip& n) override;
|
||||
void visit (LineSquare&) override;
|
||||
@@ -61,6 +66,7 @@ public:
|
||||
void visit (NetworkSource& s) override;
|
||||
void visit (MixingGroup& s) override;
|
||||
void visit (MultiFileSource& s) override;
|
||||
void visit (GenericStreamSource& s) override;
|
||||
|
||||
static tinyxml2::XMLElement *NodeToXML(const Node &n, tinyxml2::XMLDocument *doc);
|
||||
static tinyxml2::XMLElement *ImageToXML(const FrameBufferImage *img, tinyxml2::XMLDocument *doc);
|
||||
|
||||
290
Settings.cpp
290
Settings.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <locale>
|
||||
@@ -8,15 +27,33 @@ using namespace std;
|
||||
using namespace tinyxml2;
|
||||
|
||||
#include "defines.h"
|
||||
#include "Settings.h"
|
||||
#include "SystemToolkit.h"
|
||||
#include "Settings.h"
|
||||
|
||||
|
||||
Settings::Application Settings::application;
|
||||
string settingsFilename = "";
|
||||
|
||||
static string settingsFilename = "";
|
||||
|
||||
void Settings::Save()
|
||||
XMLElement *save_history(Settings::History &h, const char *nodename, XMLDocument &xmlDoc)
|
||||
{
|
||||
XMLElement *pElement = xmlDoc.NewElement( nodename );
|
||||
pElement->SetAttribute("path", h.path.c_str());
|
||||
pElement->SetAttribute("autoload", h.load_at_start);
|
||||
pElement->SetAttribute("autosave", h.save_on_exit);
|
||||
pElement->SetAttribute("valid", h.front_is_valid);
|
||||
for(auto it = h.filenames.cbegin();
|
||||
it != h.filenames.cend(); ++it) {
|
||||
XMLElement *fileNode = xmlDoc.NewElement("path");
|
||||
XMLText *text = xmlDoc.NewText( (*it).c_str() );
|
||||
fileNode->InsertEndChild( text );
|
||||
pElement->InsertFirstChild(fileNode);
|
||||
}
|
||||
return pElement;
|
||||
}
|
||||
|
||||
|
||||
void Settings::Save(uint64_t runtime)
|
||||
{
|
||||
// impose C locale for all app
|
||||
setlocale(LC_ALL, "C");
|
||||
@@ -31,6 +68,9 @@ void Settings::Save()
|
||||
pRoot->SetAttribute("minor", VIMIX_VERSION_MINOR);
|
||||
xmlDoc.InsertEndChild(pRoot);
|
||||
#endif
|
||||
// runtime
|
||||
if (runtime>0)
|
||||
pRoot->SetAttribute("runtime", runtime + application.total_runtime);
|
||||
|
||||
string comment = "Settings for " + application.name;
|
||||
XMLComment *pComment = xmlDoc.NewComment(comment.c_str());
|
||||
@@ -40,13 +80,13 @@ void Settings::Save()
|
||||
{
|
||||
XMLElement *windowsNode = xmlDoc.NewElement( "Windows" );
|
||||
|
||||
for (int i = 0; i < application.windows.size(); i++)
|
||||
for (int i = 0; i < (int) application.windows.size(); ++i)
|
||||
{
|
||||
const Settings::WindowConfig& w = application.windows[i];
|
||||
|
||||
XMLElement *window = xmlDoc.NewElement( "Window" );
|
||||
window->SetAttribute("id", i);
|
||||
window->SetAttribute("name", w.name.c_str());
|
||||
window->SetAttribute("id", i);
|
||||
window->SetAttribute("x", w.x);
|
||||
window->SetAttribute("y", w.y);
|
||||
window->SetAttribute("w", w.w);
|
||||
@@ -64,18 +104,24 @@ void Settings::Save()
|
||||
applicationNode->SetAttribute("scale", application.scale);
|
||||
applicationNode->SetAttribute("accent_color", application.accent_color);
|
||||
applicationNode->SetAttribute("smooth_transition", application.smooth_transition);
|
||||
applicationNode->SetAttribute("smooth_snapshot", application.smooth_snapshot);
|
||||
applicationNode->SetAttribute("save_snapshot", application.save_version_snapshot);
|
||||
applicationNode->SetAttribute("smooth_cursor", application.smooth_cursor);
|
||||
applicationNode->SetAttribute("action_history_follow_view", application.action_history_follow_view);
|
||||
applicationNode->SetAttribute("show_tooptips", application.show_tooptips);
|
||||
applicationNode->SetAttribute("accept_connections", application.accept_connections);
|
||||
applicationNode->SetAttribute("pannel_history_mode", application.pannel_history_mode);
|
||||
applicationNode->SetAttribute("pannel_history_mode", application.pannel_current_session_mode);
|
||||
applicationNode->SetAttribute("stream_low_bandwidth", application.stream_low_bandwidth);
|
||||
pRoot->InsertEndChild(applicationNode);
|
||||
|
||||
// Widgets
|
||||
XMLElement *widgetsNode = xmlDoc.NewElement( "Widgets" );
|
||||
widgetsNode->SetAttribute("preview", application.widget.preview);
|
||||
widgetsNode->SetAttribute("history", application.widget.history);
|
||||
widgetsNode->SetAttribute("preview_view", application.widget.preview_view);
|
||||
widgetsNode->SetAttribute("timer", application.widget.timer);
|
||||
widgetsNode->SetAttribute("timer_view", application.widget.timer_view);
|
||||
widgetsNode->SetAttribute("media_player", application.widget.media_player);
|
||||
widgetsNode->SetAttribute("media_player_view", application.widget.media_player_view);
|
||||
widgetsNode->SetAttribute("timeline_editmode", application.widget.timeline_editmode);
|
||||
widgetsNode->SetAttribute("shader_editor", application.widget.shader_editor);
|
||||
widgetsNode->SetAttribute("stats", application.widget.stats);
|
||||
widgetsNode->SetAttribute("stats_mode", application.widget.stats_mode);
|
||||
@@ -99,6 +145,11 @@ void Settings::Save()
|
||||
RecordNode->SetAttribute("path", application.record.path.c_str());
|
||||
RecordNode->SetAttribute("profile", application.record.profile);
|
||||
RecordNode->SetAttribute("timeout", application.record.timeout);
|
||||
RecordNode->SetAttribute("delay", application.record.delay);
|
||||
RecordNode->SetAttribute("resolution_mode", application.record.resolution_mode);
|
||||
RecordNode->SetAttribute("framerate_mode", application.record.framerate_mode);
|
||||
RecordNode->SetAttribute("buffering_mode", application.record.buffering_mode);
|
||||
RecordNode->SetAttribute("priority_mode", application.record.priority_mode);
|
||||
pRoot->InsertEndChild(RecordNode);
|
||||
|
||||
// Transition
|
||||
@@ -170,44 +221,51 @@ void Settings::Save()
|
||||
{
|
||||
XMLElement *recent = xmlDoc.NewElement( "Recent" );
|
||||
|
||||
XMLElement *recentsession = xmlDoc.NewElement( "Session" );
|
||||
recentsession->SetAttribute("path", application.recentSessions.path.c_str());
|
||||
recentsession->SetAttribute("autoload", application.recentSessions.load_at_start);
|
||||
recentsession->SetAttribute("autosave", application.recentSessions.save_on_exit);
|
||||
recentsession->SetAttribute("valid", application.recentSessions.front_is_valid);
|
||||
for(auto it = application.recentSessions.filenames.begin();
|
||||
it != application.recentSessions.filenames.end(); ++it) {
|
||||
XMLElement *fileNode = xmlDoc.NewElement("path");
|
||||
XMLText *text = xmlDoc.NewText( (*it).c_str() );
|
||||
fileNode->InsertEndChild( text );
|
||||
recentsession->InsertFirstChild(fileNode);
|
||||
};
|
||||
recent->InsertEndChild(recentsession);
|
||||
// recent session filenames
|
||||
recent->InsertEndChild( save_history(application.recentSessions, "Session", xmlDoc));
|
||||
|
||||
XMLElement *recentfolder = xmlDoc.NewElement( "Folder" );
|
||||
for(auto it = application.recentFolders.filenames.begin();
|
||||
it != application.recentFolders.filenames.end(); ++it) {
|
||||
XMLElement *fileNode = xmlDoc.NewElement("path");
|
||||
XMLText *text = xmlDoc.NewText( (*it).c_str() );
|
||||
fileNode->InsertEndChild( text );
|
||||
recentfolder->InsertFirstChild(fileNode);
|
||||
};
|
||||
recent->InsertEndChild(recentfolder);
|
||||
// recent session folders
|
||||
recent->InsertEndChild( save_history(application.recentFolders, "Folder", xmlDoc));
|
||||
|
||||
XMLElement *recentmedia = xmlDoc.NewElement( "Import" );
|
||||
recentmedia->SetAttribute("path", application.recentImport.path.c_str());
|
||||
for(auto it = application.recentImport.filenames.begin();
|
||||
it != application.recentImport.filenames.end(); ++it) {
|
||||
XMLElement *fileNode = xmlDoc.NewElement("path");
|
||||
XMLText *text = xmlDoc.NewText( (*it).c_str() );
|
||||
fileNode->InsertEndChild( text );
|
||||
recentmedia->InsertFirstChild(fileNode);
|
||||
// recent import media uri
|
||||
recent->InsertEndChild( save_history(application.recentImport, "Import", xmlDoc));
|
||||
|
||||
// recent import folders
|
||||
recent->InsertEndChild( save_history(application.recentImportFolders, "ImportFolder", xmlDoc));
|
||||
|
||||
// recent recordings
|
||||
recent->InsertEndChild( save_history(application.recentRecordings, "Record", xmlDoc));
|
||||
|
||||
// recent dialog path
|
||||
XMLElement *recentdialogpath = xmlDoc.NewElement( "Dialog" );
|
||||
for(auto it = application.dialogRecentFolder.cbegin();
|
||||
it != application.dialogRecentFolder.cend(); ++it) {
|
||||
XMLElement *pathNode = xmlDoc.NewElement("path");
|
||||
pathNode->SetAttribute("label", (*it).first.c_str() );
|
||||
XMLText *text = xmlDoc.NewText( (*it).second.c_str() );
|
||||
pathNode->InsertEndChild( text );
|
||||
recentdialogpath->InsertFirstChild(pathNode);
|
||||
}
|
||||
recent->InsertEndChild(recentmedia);
|
||||
recent->InsertEndChild(recentdialogpath);
|
||||
|
||||
pRoot->InsertEndChild(recent);
|
||||
}
|
||||
|
||||
// Metronome
|
||||
XMLElement *timerConfNode = xmlDoc.NewElement( "Timer" );
|
||||
timerConfNode->SetAttribute("mode", application.timer.mode);
|
||||
timerConfNode->SetAttribute("link_enabled", application.timer.link_enabled);
|
||||
timerConfNode->SetAttribute("link_tempo", application.timer.link_tempo);
|
||||
timerConfNode->SetAttribute("link_quantum", application.timer.link_quantum);
|
||||
timerConfNode->SetAttribute("link_start_stop_sync", application.timer.link_start_stop_sync);
|
||||
timerConfNode->SetAttribute("stopwatch_duration", application.timer.stopwatch_duration);
|
||||
pRoot->InsertEndChild(timerConfNode);
|
||||
|
||||
// Controller
|
||||
XMLElement *controlConfNode = xmlDoc.NewElement( "Control" );
|
||||
controlConfNode->SetAttribute("osc_port_receive", application.control.osc_port_receive);
|
||||
controlConfNode->SetAttribute("osc_port_send", application.control.osc_port_send);
|
||||
|
||||
|
||||
// First save : create filename
|
||||
if (settingsFilename.empty())
|
||||
@@ -218,14 +276,45 @@ void Settings::Save()
|
||||
|
||||
}
|
||||
|
||||
|
||||
void load_history(Settings::History &h, const char *nodename, XMLElement *root)
|
||||
{
|
||||
XMLElement * pElement = root->FirstChildElement(nodename);
|
||||
if (pElement)
|
||||
{
|
||||
// list of path
|
||||
h.filenames.clear();
|
||||
XMLElement* path = pElement->FirstChildElement("path");
|
||||
for( ; path ; path = path->NextSiblingElement())
|
||||
{
|
||||
const char *p = path->GetText();
|
||||
if (p)
|
||||
h.push( std::string (p) );
|
||||
}
|
||||
// path attribute
|
||||
const char *path_ = pElement->Attribute("path");
|
||||
if (path_)
|
||||
h.path = std::string(path_);
|
||||
else
|
||||
h.path = SystemToolkit::home_path();
|
||||
// other attritutes
|
||||
pElement->QueryBoolAttribute("autoload", &h.load_at_start);
|
||||
pElement->QueryBoolAttribute("autosave", &h.save_on_exit);
|
||||
pElement->QueryBoolAttribute("valid", &h.front_is_valid);
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::Load()
|
||||
{
|
||||
// impose C locale for all app
|
||||
setlocale(LC_ALL, "C");
|
||||
|
||||
// set filenames from settings path
|
||||
application.control.osc_filename = SystemToolkit::full_filename(SystemToolkit::settings_path(), OSC_CONFIG_FILE);
|
||||
settingsFilename = SystemToolkit::full_filename(SystemToolkit::settings_path(), APP_SETTINGS);
|
||||
|
||||
// try to load settings file
|
||||
XMLDocument xmlDoc;
|
||||
if (settingsFilename.empty())
|
||||
settingsFilename = SystemToolkit::full_filename(SystemToolkit::settings_path(), APP_SETTINGS);
|
||||
XMLError eResult = xmlDoc.LoadFile(settingsFilename.c_str());
|
||||
|
||||
// do not warn if non existing file
|
||||
@@ -235,8 +324,10 @@ void Settings::Load()
|
||||
else if (XMLResultError(eResult))
|
||||
return;
|
||||
|
||||
// first element should be called by the application name
|
||||
XMLElement *pRoot = xmlDoc.FirstChildElement(application.name.c_str());
|
||||
if (pRoot == nullptr) return;
|
||||
if (pRoot == nullptr)
|
||||
return;
|
||||
|
||||
// cancel on different root name
|
||||
if (application.name.compare( string( pRoot->Value() ) ) != 0 )
|
||||
@@ -250,25 +341,33 @@ void Settings::Load()
|
||||
if (version_major != VIMIX_VERSION_MAJOR || version_minor != VIMIX_VERSION_MINOR)
|
||||
return;
|
||||
#endif
|
||||
// runtime
|
||||
pRoot->QueryUnsigned64Attribute("runtime", &application.total_runtime);
|
||||
|
||||
XMLElement * applicationNode = pRoot->FirstChildElement("Application");
|
||||
if (applicationNode != nullptr) {
|
||||
applicationNode->QueryFloatAttribute("scale", &application.scale);
|
||||
applicationNode->QueryIntAttribute("accent_color", &application.accent_color);
|
||||
applicationNode->QueryBoolAttribute("smooth_transition", &application.smooth_transition);
|
||||
applicationNode->QueryBoolAttribute("smooth_snapshot", &application.smooth_snapshot);
|
||||
applicationNode->QueryBoolAttribute("save_snapshot", &application.save_version_snapshot);
|
||||
applicationNode->QueryBoolAttribute("smooth_cursor", &application.smooth_cursor);
|
||||
applicationNode->QueryBoolAttribute("action_history_follow_view", &application.action_history_follow_view);
|
||||
applicationNode->QueryBoolAttribute("show_tooptips", &application.show_tooptips);
|
||||
applicationNode->QueryBoolAttribute("accept_connections", &application.accept_connections);
|
||||
applicationNode->QueryIntAttribute("pannel_history_mode", &application.pannel_history_mode);
|
||||
applicationNode->QueryIntAttribute("pannel_history_mode", &application.pannel_current_session_mode);
|
||||
applicationNode->QueryBoolAttribute("stream_low_bandwidth", &application.stream_low_bandwidth);
|
||||
}
|
||||
|
||||
// Widgets
|
||||
XMLElement * widgetsNode = pRoot->FirstChildElement("Widgets");
|
||||
if (widgetsNode != nullptr) {
|
||||
widgetsNode->QueryBoolAttribute("preview", &application.widget.preview);
|
||||
widgetsNode->QueryBoolAttribute("history", &application.widget.history);
|
||||
widgetsNode->QueryIntAttribute("preview_view", &application.widget.preview_view);
|
||||
widgetsNode->QueryBoolAttribute("timer", &application.widget.timer);
|
||||
widgetsNode->QueryIntAttribute("timer_view", &application.widget.timer_view);
|
||||
widgetsNode->QueryBoolAttribute("media_player", &application.widget.media_player);
|
||||
widgetsNode->QueryIntAttribute("media_player_view", &application.widget.media_player_view);
|
||||
widgetsNode->QueryBoolAttribute("timeline_editmode", &application.widget.timeline_editmode);
|
||||
widgetsNode->QueryBoolAttribute("shader_editor", &application.widget.shader_editor);
|
||||
widgetsNode->QueryBoolAttribute("stats", &application.widget.stats);
|
||||
widgetsNode->QueryIntAttribute("stats_mode", &application.widget.stats_mode);
|
||||
@@ -292,7 +391,12 @@ void Settings::Load()
|
||||
XMLElement * recordnode = pRoot->FirstChildElement("Record");
|
||||
if (recordnode != nullptr) {
|
||||
recordnode->QueryIntAttribute("profile", &application.record.profile);
|
||||
recordnode->QueryFloatAttribute("timeout", &application.record.timeout);
|
||||
recordnode->QueryUnsignedAttribute("timeout", &application.record.timeout);
|
||||
recordnode->QueryIntAttribute("delay", &application.record.delay);
|
||||
recordnode->QueryIntAttribute("resolution_mode", &application.record.resolution_mode);
|
||||
recordnode->QueryIntAttribute("framerate_mode", &application.record.framerate_mode);
|
||||
recordnode->QueryIntAttribute("buffering_mode", &application.record.buffering_mode);
|
||||
recordnode->QueryIntAttribute("priority_mode", &application.record.priority_mode);
|
||||
|
||||
const char *path_ = recordnode->Attribute("path");
|
||||
if (path_)
|
||||
@@ -327,16 +431,18 @@ void Settings::Load()
|
||||
for( ; windowNode ; windowNode=windowNode->NextSiblingElement())
|
||||
{
|
||||
Settings::WindowConfig w;
|
||||
w.name = std::string(windowNode->Attribute("name"));
|
||||
windowNode->QueryIntAttribute("x", &w.x); // If this fails, original value is left as-is
|
||||
windowNode->QueryIntAttribute("y", &w.y);
|
||||
windowNode->QueryIntAttribute("w", &w.w);
|
||||
windowNode->QueryIntAttribute("h", &w.h);
|
||||
windowNode->QueryBoolAttribute("f", &w.fullscreen);
|
||||
w.monitor = std::string(windowNode->Attribute("m"));
|
||||
const char *text = windowNode->Attribute("m");
|
||||
if (text)
|
||||
w.monitor = std::string(text);
|
||||
|
||||
int i = 0;
|
||||
windowNode->QueryIntAttribute("id", &i);
|
||||
w.name = application.windows[i].name; // keep only original name
|
||||
application.windows[i] = w;
|
||||
}
|
||||
}
|
||||
@@ -399,60 +505,56 @@ void Settings::Load()
|
||||
if (pElement)
|
||||
{
|
||||
// recent session filenames
|
||||
XMLElement * pSession = pElement->FirstChildElement("Session");
|
||||
if (pSession)
|
||||
{
|
||||
const char *path_ = pSession->Attribute("path");
|
||||
if (path_)
|
||||
application.recentSessions.path = std::string(path_);
|
||||
else
|
||||
application.recentSessions.path = SystemToolkit::home_path();
|
||||
application.recentSessions.filenames.clear();
|
||||
XMLElement* path = pSession->FirstChildElement("path");
|
||||
for( ; path ; path = path->NextSiblingElement())
|
||||
{
|
||||
const char *p = path->GetText();
|
||||
if (p)
|
||||
application.recentSessions.push( std::string (p) );
|
||||
}
|
||||
pSession->QueryBoolAttribute("autoload", &application.recentSessions.load_at_start);
|
||||
pSession->QueryBoolAttribute("autosave", &application.recentSessions.save_on_exit);
|
||||
pSession->QueryBoolAttribute("valid", &application.recentSessions.front_is_valid);
|
||||
}
|
||||
load_history(application.recentSessions, "Session", pElement);
|
||||
|
||||
// recent session folders
|
||||
XMLElement * pFolder = pElement->FirstChildElement("Folder");
|
||||
if (pFolder)
|
||||
{
|
||||
application.recentFolders.filenames.clear();
|
||||
XMLElement* path = pFolder->FirstChildElement("path");
|
||||
for( ; path ; path = path->NextSiblingElement())
|
||||
{
|
||||
const char *p = path->GetText();
|
||||
if (p)
|
||||
application.recentFolders.push( std::string (p) );
|
||||
}
|
||||
}
|
||||
load_history(application.recentFolders, "Folder", pElement);
|
||||
|
||||
// recent media uri
|
||||
XMLElement * pImport = pElement->FirstChildElement("Import");
|
||||
if (pImport)
|
||||
load_history(application.recentImport, "Import", pElement);
|
||||
|
||||
// recent import folders
|
||||
load_history(application.recentImportFolders, "ImportFolder", pElement);
|
||||
|
||||
// recent recordings
|
||||
load_history(application.recentRecordings, "Record", pElement);
|
||||
|
||||
// recent dialog path
|
||||
XMLElement * pDialog = pElement->FirstChildElement("Dialog");
|
||||
if (pDialog)
|
||||
{
|
||||
const char *path_ = pImport->Attribute("path");
|
||||
if (path_)
|
||||
application.recentImport.path = std::string(path_);
|
||||
else
|
||||
application.recentImport.path = SystemToolkit::home_path();
|
||||
application.recentImport.filenames.clear();
|
||||
XMLElement* path = pImport->FirstChildElement("path");
|
||||
application.dialogRecentFolder.clear();
|
||||
XMLElement* path = pDialog->FirstChildElement("path");
|
||||
for( ; path ; path = path->NextSiblingElement())
|
||||
{
|
||||
const char *l = path->Attribute("label");
|
||||
const char *p = path->GetText();
|
||||
if (p)
|
||||
application.recentImport.push( std::string (p) );
|
||||
if (l && p)
|
||||
application.dialogRecentFolder[ std::string(l)] = std::string (p);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// bloc metronome
|
||||
XMLElement * timerconfnode = pRoot->FirstChildElement("Timer");
|
||||
if (timerconfnode != nullptr) {
|
||||
timerconfnode->QueryUnsigned64Attribute("mode", &application.timer.mode);
|
||||
timerconfnode->QueryBoolAttribute("link_enabled", &application.timer.link_enabled);
|
||||
timerconfnode->QueryDoubleAttribute("link_tempo", &application.timer.link_tempo);
|
||||
timerconfnode->QueryDoubleAttribute("link_quantum", &application.timer.link_quantum);
|
||||
timerconfnode->QueryBoolAttribute("link_start_stop_sync", &application.timer.link_start_stop_sync);
|
||||
timerconfnode->QueryUnsigned64Attribute("stopwatch_duration", &application.timer.stopwatch_duration);
|
||||
}
|
||||
|
||||
// bloc Controller
|
||||
XMLElement *controlconfnode = pRoot->FirstChildElement("Control");
|
||||
if (controlconfnode != nullptr) {
|
||||
controlconfnode->QueryIntAttribute("osc_port_receive", &application.control.osc_port_receive);
|
||||
controlconfnode->QueryIntAttribute("osc_port_send", &application.control.osc_port_send);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Settings::History::push(const string &filename)
|
||||
@@ -528,13 +630,11 @@ void Settings::Unlock()
|
||||
|
||||
void Settings::Check()
|
||||
{
|
||||
Settings::Save();
|
||||
|
||||
XMLDocument xmlDoc;
|
||||
XMLError eResult = xmlDoc.LoadFile(settingsFilename.c_str());
|
||||
if (XMLResultError(eResult))
|
||||
if (XMLResultError(eResult)) {
|
||||
return;
|
||||
|
||||
}
|
||||
xmlDoc.Print();
|
||||
}
|
||||
|
||||
|
||||
95
Settings.h
95
Settings.h
@@ -1,7 +1,9 @@
|
||||
#ifndef __SETTINGS_H_
|
||||
#define __SETTINGS_H_
|
||||
|
||||
#include "defines.h"
|
||||
#ifdef __APPLE__
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
@@ -9,6 +11,8 @@
|
||||
#include <list>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "defines.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
struct WidgetsConfig
|
||||
@@ -18,11 +22,15 @@ struct WidgetsConfig
|
||||
int stats_mode;
|
||||
bool logs;
|
||||
bool preview;
|
||||
bool history;
|
||||
int preview_view;
|
||||
bool media_player;
|
||||
bool media_player_view;
|
||||
int media_player_view;
|
||||
bool timer;
|
||||
int timer_view;
|
||||
bool timeline_editmode;
|
||||
bool shader_editor;
|
||||
bool toolbox;
|
||||
bool help;
|
||||
|
||||
WidgetsConfig() {
|
||||
stats = false;
|
||||
@@ -30,11 +38,15 @@ struct WidgetsConfig
|
||||
stats_corner = 1;
|
||||
logs = false;
|
||||
preview = false;
|
||||
history = false;
|
||||
preview_view = -1;
|
||||
media_player = false;
|
||||
media_player_view = true;
|
||||
media_player_view = -1;
|
||||
timeline_editmode = false;
|
||||
shader_editor = false;
|
||||
toolbox = false;
|
||||
help = false;
|
||||
timer = false;
|
||||
timer_view = -1;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,17 +74,26 @@ struct ViewConfig
|
||||
|
||||
};
|
||||
|
||||
#define RECORD_MAX_TIMEOUT 1800.f
|
||||
|
||||
struct RecordConfig
|
||||
{
|
||||
std::string path;
|
||||
int profile;
|
||||
float timeout;
|
||||
uint timeout;
|
||||
int delay;
|
||||
int resolution_mode;
|
||||
int framerate_mode;
|
||||
int buffering_mode;
|
||||
int priority_mode;
|
||||
|
||||
RecordConfig() : path("") {
|
||||
profile = 0;
|
||||
timeout = RECORD_MAX_TIMEOUT;
|
||||
delay = 0;
|
||||
resolution_mode = 1;
|
||||
framerate_mode = 1;
|
||||
buffering_mode = 2;
|
||||
priority_mode = 1;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -115,6 +136,7 @@ struct TransitionConfig
|
||||
|
||||
struct RenderConfig
|
||||
{
|
||||
bool disabled;
|
||||
bool blit;
|
||||
int vsync;
|
||||
int multisampling;
|
||||
@@ -124,6 +146,7 @@ struct RenderConfig
|
||||
bool gpu_decoding;
|
||||
|
||||
RenderConfig() {
|
||||
disabled = false;
|
||||
blit = false;
|
||||
vsync = 1;
|
||||
multisampling = 2;
|
||||
@@ -147,6 +170,36 @@ struct SourceConfig
|
||||
}
|
||||
};
|
||||
|
||||
struct TimerConfig
|
||||
{
|
||||
uint64_t mode;
|
||||
bool link_enabled;
|
||||
double link_tempo;
|
||||
double link_quantum;
|
||||
bool link_start_stop_sync;
|
||||
uint64_t stopwatch_duration;
|
||||
|
||||
TimerConfig() {
|
||||
mode = 0;
|
||||
link_enabled = true;
|
||||
link_tempo = 120.;
|
||||
link_quantum = 4.;
|
||||
link_start_stop_sync = true;
|
||||
stopwatch_duration = 60;
|
||||
}
|
||||
};
|
||||
|
||||
struct ControllerConfig
|
||||
{
|
||||
int osc_port_receive;
|
||||
int osc_port_send;
|
||||
std::string osc_filename;
|
||||
|
||||
ControllerConfig() {
|
||||
osc_port_receive = OSC_PORT_RECV_DEFAULT;
|
||||
osc_port_send = OSC_PORT_SEND_DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
struct Application
|
||||
{
|
||||
@@ -157,16 +210,18 @@ struct Application
|
||||
// Verification
|
||||
std::string name;
|
||||
std::string executable;
|
||||
uint64_t total_runtime;
|
||||
|
||||
// Global settings Application interface
|
||||
float scale;
|
||||
int accent_color;
|
||||
bool smooth_snapshot;
|
||||
bool save_version_snapshot;
|
||||
bool smooth_transition;
|
||||
bool smooth_cursor;
|
||||
bool action_history_follow_view;
|
||||
bool show_tooptips;
|
||||
|
||||
int pannel_history_mode;
|
||||
int pannel_current_session_mode;
|
||||
|
||||
// connection settings
|
||||
bool accept_connections;
|
||||
@@ -187,6 +242,7 @@ struct Application
|
||||
|
||||
// settings exporters
|
||||
RecordConfig record;
|
||||
bool stream_low_bandwidth;
|
||||
|
||||
// settings new source
|
||||
SourceConfig source;
|
||||
@@ -194,6 +250,9 @@ struct Application
|
||||
// settings transition
|
||||
TransitionConfig transition;
|
||||
|
||||
// settings controller
|
||||
ControllerConfig control;
|
||||
|
||||
// multiple windows handling
|
||||
std::vector<WindowConfig> windows;
|
||||
|
||||
@@ -201,24 +260,32 @@ struct Application
|
||||
History recentSessions;
|
||||
History recentFolders;
|
||||
History recentImport;
|
||||
History recentImportFolders;
|
||||
History recentRecordings;
|
||||
std::map< std::string, std::string > dialogRecentFolder;
|
||||
|
||||
// Metronome & stopwatch
|
||||
TimerConfig timer;
|
||||
|
||||
Application() : fresh_start(false), instance_id(0), name(APP_NAME), executable(APP_NAME) {
|
||||
scale = 1.f;
|
||||
accent_color = 0;
|
||||
smooth_transition = false;
|
||||
smooth_snapshot = false;
|
||||
save_version_snapshot = false;
|
||||
smooth_cursor = false;
|
||||
action_history_follow_view = false;
|
||||
show_tooptips = true;
|
||||
accept_connections = false;
|
||||
pannel_history_mode = 0;
|
||||
pannel_current_session_mode = 0;
|
||||
current_view = 1;
|
||||
current_workspace= 1;
|
||||
brush = glm::vec3(0.5f, 0.1f, 0.f);
|
||||
stream_low_bandwidth = false;
|
||||
windows = std::vector<WindowConfig>(3);
|
||||
windows[0].name = APP_NAME APP_TITLE;
|
||||
windows[0].name = APP_TITLE;
|
||||
windows[0].w = 1600;
|
||||
windows[0].h = 900;
|
||||
windows[1].name = APP_NAME " -- Output";
|
||||
windows[1].name = "Output " APP_TITLE;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -229,7 +296,7 @@ struct Application
|
||||
extern Application application;
|
||||
|
||||
// Save and Load store settings in XML file
|
||||
void Save();
|
||||
void Save(uint64_t runtime = 0);
|
||||
void Load();
|
||||
void Lock();
|
||||
void Unlock();
|
||||
|
||||
36
Shader.cpp
36
Shader.cpp
@@ -1,9 +1,21 @@
|
||||
#include "Shader.h"
|
||||
#include "Resource.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "Log.h"
|
||||
#include "Visitor.h"
|
||||
#include "RenderingManager.h"
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -14,11 +26,19 @@
|
||||
#include <glad/glad.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include "BaseToolkit.h"
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#define GLM_ENABLE_EXPERIMENTAL
|
||||
#include <glm/gtx/string_cast.hpp>
|
||||
|
||||
#include "Resource.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include "Log.h"
|
||||
#include "Visitor.h"
|
||||
#include "BaseToolkit.h"
|
||||
#include "RenderingManager.h"
|
||||
|
||||
#include "Shader.h"
|
||||
|
||||
// Globals
|
||||
ShadingProgram *ShadingProgram::currentProgram_ = nullptr;
|
||||
ShadingProgram simpleShadingProgram("shaders/simple.vs", "shaders/simple.fs");
|
||||
@@ -206,7 +226,7 @@ Shader::Shader() : blending(BLEND_OPACITY)
|
||||
id_ = BaseToolkit::uniqueId();
|
||||
|
||||
program_ = &simpleShadingProgram;
|
||||
reset();
|
||||
Shader::reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
449
Source.cpp
449
Source.cpp
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include <algorithm>
|
||||
#include <locale>
|
||||
#include <tinyxml2.h>
|
||||
@@ -97,7 +116,7 @@ SourceCore& SourceCore::operator= (SourceCore const& other)
|
||||
|
||||
|
||||
Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(nullptr),
|
||||
active_(true), locked_(false), need_update_(true), dt_(0), workspace_(STAGE)
|
||||
active_(true), locked_(false), need_update_(true), dt_(16.f), workspace_(STAGE)
|
||||
{
|
||||
// create unique id
|
||||
if (id_ == 0)
|
||||
@@ -124,6 +143,16 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
|
||||
frames_[View::MIXING]->attach(frame);
|
||||
groups_[View::MIXING]->attach(frames_[View::MIXING]);
|
||||
|
||||
// Glyphs show letters from the intials, with Font index 4 (LARGE)
|
||||
initial_0_ = new Glyph(4);
|
||||
initial_0_->translation_ = glm::vec3(0.2f, 0.8f, 0.1f);
|
||||
initial_0_->scale_.y = 0.2f;
|
||||
groups_[View::MIXING]->attach(initial_0_);
|
||||
initial_1_ = new Glyph(4);
|
||||
initial_1_->translation_ = glm::vec3(0.4f, 0.8f, 0.1f);
|
||||
initial_1_->scale_.y = 0.2f;
|
||||
groups_[View::MIXING]->attach(initial_1_);
|
||||
|
||||
overlays_[View::MIXING] = new Group;
|
||||
overlays_[View::MIXING]->translation_.z = 0.1;
|
||||
overlays_[View::MIXING]->visible_ = false;
|
||||
@@ -207,6 +236,9 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
|
||||
frames_[View::LAYER]->attach(frame);
|
||||
groups_[View::LAYER]->attach(frames_[View::LAYER]);
|
||||
|
||||
groups_[View::LAYER]->attach(initial_0_);
|
||||
groups_[View::LAYER]->attach(initial_1_);
|
||||
|
||||
overlays_[View::LAYER] = new Group;
|
||||
overlays_[View::LAYER]->translation_.z = 0.15;
|
||||
overlays_[View::LAYER]->visible_ = false;
|
||||
@@ -256,8 +288,10 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
|
||||
// locker switch button : locked / unlocked icons
|
||||
locker_ = new Switch;
|
||||
lock_ = new Handles(Handles::LOCKED);
|
||||
lock_->color.a = 0.6;
|
||||
locker_->attach(lock_);
|
||||
unlock_ = new Handles(Handles::UNLOCKED);
|
||||
unlock_->color.a = 0.6;
|
||||
locker_->attach(unlock_);
|
||||
|
||||
// simple image shader (with texturing) for blending
|
||||
@@ -268,7 +302,7 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
|
||||
|
||||
// for drawing in mixing view
|
||||
mixingshader_ = new ImageShader;
|
||||
mixingshader_->stipple = 1.0;
|
||||
mixingshader_->stipple = 1.0f;
|
||||
mixinggroup_ = nullptr;
|
||||
|
||||
// create media surface:
|
||||
@@ -281,6 +315,7 @@ Source::Source(uint64_t id) : SourceCore(), id_(id), ready_(false), symbol_(null
|
||||
renderbuffer_ = nullptr;
|
||||
rendersurface_ = nullptr;
|
||||
mixingsurface_ = nullptr;
|
||||
activesurface_ = nullptr;
|
||||
maskbuffer_ = nullptr;
|
||||
maskimage_ = nullptr;
|
||||
mask_need_update_ = false;
|
||||
@@ -313,15 +348,25 @@ Source::~Source()
|
||||
overlays_.clear();
|
||||
frames_.clear();
|
||||
handles_.clear();
|
||||
|
||||
// clear and delete callbacks
|
||||
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); ) {
|
||||
SourceCallback *callback = *iter;
|
||||
iter = update_callbacks_.erase(iter);
|
||||
delete callback;
|
||||
}
|
||||
}
|
||||
|
||||
void Source::setName (const std::string &name)
|
||||
{
|
||||
if (!name.empty())
|
||||
name_ = BaseToolkit::transliterate(name);
|
||||
name_ = BaseToolkit::unspace( BaseToolkit::transliterate(name) );
|
||||
|
||||
initials_[0] = std::toupper( name_.front(), std::locale("C") );
|
||||
initials_[1] = std::toupper( name_.back(), std::locale("C") );
|
||||
|
||||
initial_0_->setChar(initials_[0]);
|
||||
initial_1_->setChar(initials_[1]);
|
||||
}
|
||||
|
||||
void Source::accept(Visitor& v)
|
||||
@@ -352,6 +397,10 @@ void Source::setMode(Source::Mode m)
|
||||
for (auto o = overlays_.begin(); o != overlays_.end(); ++o)
|
||||
(*o).second->visible_ = (current && !locked_);
|
||||
|
||||
// the opacity of the initials changes if current
|
||||
initial_0_->color.w = current ? 1.0 : 0.7;
|
||||
initial_1_->color.w = current ? 1.0 : 0.7;
|
||||
|
||||
// the lock icon
|
||||
locker_->setActive( locked_ ? 0 : 1);
|
||||
|
||||
@@ -425,7 +474,6 @@ void Source::render()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Source::attach(FrameBuffer *renderbuffer)
|
||||
{
|
||||
// invalid argument
|
||||
@@ -437,12 +485,6 @@ void Source::attach(FrameBuffer *renderbuffer)
|
||||
delete renderbuffer_;
|
||||
renderbuffer_ = renderbuffer;
|
||||
|
||||
// if a symbol is available, add it to overlay
|
||||
if (symbol_) {
|
||||
overlays_[View::MIXING]->attach( symbol_ );
|
||||
overlays_[View::LAYER]->attach( symbol_ );
|
||||
}
|
||||
|
||||
// create the surfaces to draw the frame buffer in the views
|
||||
rendersurface_ = new FrameBufferSurface(renderbuffer_, blendingshader_);
|
||||
groups_[View::RENDERING]->attach(rendersurface_);
|
||||
@@ -455,18 +497,27 @@ void Source::attach(FrameBuffer *renderbuffer)
|
||||
groups_[View::LAYER]->attach(mixingsurface_);
|
||||
|
||||
// for views showing a scaled mixing surface, a dedicated transparent surface allows grabbing
|
||||
Surface *surfacetmp = new Surface();
|
||||
surfacetmp->setTextureIndex(Resource::getTextureTransparent());
|
||||
groups_[View::TEXTURE]->attach(surfacetmp);
|
||||
groups_[View::MIXING]->attach(surfacetmp);
|
||||
groups_[View::LAYER]->attach(surfacetmp);
|
||||
activesurface_ = new Surface();
|
||||
activesurface_->setTextureIndex(Resource::getTextureTransparent());
|
||||
groups_[View::TEXTURE]->attach(activesurface_);
|
||||
groups_[View::MIXING]->attach(activesurface_);
|
||||
groups_[View::LAYER]->attach(activesurface_);
|
||||
|
||||
// Transition group node is optionnal
|
||||
if (groups_[View::TRANSITION]->numChildren() > 0)
|
||||
groups_[View::TRANSITION]->attach(mixingsurface_);
|
||||
|
||||
// hack to place the symbols in the corner independently of aspect ratio
|
||||
symbol_->translation_.x += 0.1f * (renderbuffer_->aspectRatio()-1.f);
|
||||
// if a symbol is available, add it to overlay
|
||||
if (symbol_) {
|
||||
overlays_[View::MIXING]->attach( symbol_ );
|
||||
overlays_[View::LAYER]->attach( symbol_ );
|
||||
// hack to place the symbols in the corner independently of aspect ratio
|
||||
symbol_->translation_.x += 0.1f * (renderbuffer_->aspectRatio()-1.f);
|
||||
}
|
||||
|
||||
// hack to place the initials in the corner independently of aspect ratio
|
||||
initial_0_->translation_.x -= renderbuffer_->aspectRatio();
|
||||
initial_1_->translation_.x -= renderbuffer_->aspectRatio();
|
||||
|
||||
// add lock icon to views (displayed on front)
|
||||
groups_[View::LAYER]->attach( locker_ );
|
||||
@@ -499,6 +550,10 @@ void Source::attach(FrameBuffer *renderbuffer)
|
||||
|
||||
void Source::setActive (bool on)
|
||||
{
|
||||
// request update
|
||||
need_update_ |= active_ != on;
|
||||
|
||||
// activate
|
||||
active_ = on;
|
||||
|
||||
// do not disactivate if a clone depends on it
|
||||
@@ -513,6 +568,11 @@ void Source::setActive (bool on)
|
||||
groups_[View::LAYER]->visible_ = active_;
|
||||
}
|
||||
|
||||
void Source::setActive (float threshold)
|
||||
{
|
||||
setActive( glm::length( glm::vec2(groups_[View::MIXING]->translation_) ) < threshold );
|
||||
}
|
||||
|
||||
void Source::setLocked (bool on)
|
||||
{
|
||||
locked_ = on;
|
||||
@@ -522,21 +582,27 @@ void Source::setLocked (bool on)
|
||||
|
||||
// Transfer functions from coordinates to alpha (1 - transparency)
|
||||
|
||||
// linear distance
|
||||
float linear_(float x, float y) {
|
||||
return 1.f - CLAMP( sqrt( ( x * x ) + ( y * y ) ), 0.f, 1.f );
|
||||
}
|
||||
//// linear distance
|
||||
//float linear_(float x, float y) {
|
||||
// return 1.f - CLAMP( sqrt( ( x * x ) + ( y * y ) ), 0.f, 1.f );
|
||||
//}
|
||||
|
||||
// quadratic distance
|
||||
float quad_(float x, float y) {
|
||||
return 1.f - CLAMP( ( x * x ) + ( y * y ), 0.f, 1.f );
|
||||
}
|
||||
//// quadratic distance
|
||||
//float quad_(float x, float y) {
|
||||
// return 1.f - CLAMP( ( x * x ) + ( y * y ), 0.f, 1.f );
|
||||
//}
|
||||
|
||||
// best alpha transfer function: quadratic sinusoidal shape
|
||||
float sin_quad_(float x, float y) {
|
||||
//// best alpha transfer function: quadratic sinusoidal shape
|
||||
//float sin_quad_(float x, float y) {
|
||||
// float D = sqrt( ( x * x ) + ( y * y ) );
|
||||
// return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
|
||||
//// return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ), 0.f, 1.f ) );
|
||||
//}
|
||||
|
||||
float SourceCore::alphaFromCordinates(float x, float y)
|
||||
{
|
||||
float D = sqrt( ( x * x ) + ( y * y ) );
|
||||
return 0.5f + 0.5f * cos( M_PI * CLAMP( D * sqrt(D), 0.f, 1.f ) );
|
||||
// return 0.5f + 0.5f * cos( M_PI * CLAMP( ( ( x * x ) + ( y * y ) ), 0.f, 1.f ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -545,38 +611,41 @@ float Source::depth() const
|
||||
return group(View::RENDERING)->translation_.z;
|
||||
}
|
||||
|
||||
void Source::setDepth(float d)
|
||||
{
|
||||
groups_[View::LAYER]->translation_.z = CLAMP(d, MIN_DEPTH, MAX_DEPTH);
|
||||
touch();
|
||||
}
|
||||
|
||||
float Source::alpha() const
|
||||
{
|
||||
return blendingShader()->color.a;
|
||||
}
|
||||
|
||||
void Source::setAlpha(float a)
|
||||
void Source::call(SourceCallback *callback, bool override)
|
||||
{
|
||||
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
|
||||
glm::vec2 step = glm::normalize(glm::vec2(1.f, 1.f));// step in diagonal by default
|
||||
if (callback != nullptr) {
|
||||
|
||||
// step in direction of source translation if possible
|
||||
if ( glm::length(dist) > DELTA_ALPHA)
|
||||
step = glm::normalize(dist);
|
||||
// lock access to callbacks list
|
||||
access_callbacks_.lock();
|
||||
|
||||
// converge to reduce the difference of alpha
|
||||
// using dichotomic algorithm
|
||||
float delta = sin_quad_(dist.x, dist.y) - CLAMP(a, 0.f, 1.f);
|
||||
while ( glm::abs(delta) > DELTA_ALPHA ){
|
||||
dist += step * (delta / 2.f);
|
||||
delta = sin_quad_(dist.x, dist.y) - CLAMP(a, 0.f, 1.f);
|
||||
// remove similar callbacks if override
|
||||
if (override) {
|
||||
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
||||
{
|
||||
// remove and delete all callbacks of same type
|
||||
SourceCallback *c = *iter;
|
||||
if (callback->type() == c->type() ) {
|
||||
iter = update_callbacks_.erase(iter);
|
||||
delete c;
|
||||
}
|
||||
// iterate
|
||||
else
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
|
||||
// add callback to callbacks list
|
||||
update_callbacks_.push_back(callback);
|
||||
|
||||
// release access to callbacks list
|
||||
access_callbacks_.unlock();
|
||||
}
|
||||
|
||||
// apply new mixing coordinates
|
||||
groups_[View::MIXING]->translation_.x = dist.x;
|
||||
groups_[View::MIXING]->translation_.y = dist.y;
|
||||
touch();
|
||||
}
|
||||
|
||||
void Source::update(float dt)
|
||||
@@ -584,126 +653,153 @@ void Source::update(float dt)
|
||||
// keep delta-t
|
||||
dt_ = dt;
|
||||
|
||||
// update nodes if needed
|
||||
if (renderbuffer_ && mixingsurface_ && maskbuffer_ && need_update_)
|
||||
// if update is possible
|
||||
if (renderbuffer_ && mixingsurface_ && maskbuffer_)
|
||||
{
|
||||
// ADJUST alpha based on MIXING node
|
||||
// read position of the mixing node and interpret this as transparency of render output
|
||||
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
|
||||
// use the sinusoidal transfer function
|
||||
blendingshader_->color = glm::vec4(1.f, 1.f, 1.f, sin_quad_( dist.x, dist.y ));
|
||||
mixingshader_->color = blendingshader_->color;
|
||||
|
||||
// CHANGE update status based on limbo
|
||||
bool a = glm::length(dist) < MIXING_LIMBO_SCALE;
|
||||
setActive( a );
|
||||
// adjust scale of mixing icon : smaller if not active
|
||||
groups_[View::MIXING]->scale_ = glm::vec3(MIXING_ICON_SCALE) - ( a ? glm::vec3(0.f, 0.f, 0.f) : glm::vec3(0.03f, 0.03f, 0.f) );
|
||||
|
||||
// MODIFY geometry based on GEOMETRY node
|
||||
groups_[View::RENDERING]->translation_ = groups_[View::GEOMETRY]->translation_;
|
||||
groups_[View::RENDERING]->rotation_ = groups_[View::GEOMETRY]->rotation_;
|
||||
glm::vec3 s = groups_[View::GEOMETRY]->scale_;
|
||||
// avoid any null scale
|
||||
s.x = CLAMP_SCALE(s.x);
|
||||
s.y = CLAMP_SCALE(s.y);
|
||||
s.z = 1.f;
|
||||
groups_[View::GEOMETRY]->scale_ = s;
|
||||
groups_[View::RENDERING]->scale_ = s;
|
||||
|
||||
// MODIFY CROP projection based on GEOMETRY crop
|
||||
renderbuffer_->setProjectionArea( glm::vec2(groups_[View::GEOMETRY]->crop_) );
|
||||
|
||||
// Mixing and layer icons scaled based on GEOMETRY crop
|
||||
mixingsurface_->scale_ = groups_[View::GEOMETRY]->crop_;
|
||||
mixingsurface_->scale_.x *= renderbuffer_->aspectRatio();
|
||||
mixingsurface_->update(dt_);
|
||||
|
||||
// Layers icons are displayed in Perspective (diagonal)
|
||||
groups_[View::LAYER]->translation_.x = -groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::LAYER]->translation_.y = groups_[View::LAYER]->translation_.x / LAYER_PERSPECTIVE;
|
||||
|
||||
// Update workspace based on depth, and
|
||||
// adjust vertical position of icon depending on workspace
|
||||
if (groups_[View::LAYER]->translation_.x < -LAYER_FOREGROUND) {
|
||||
groups_[View::LAYER]->translation_.y -= 0.3f;
|
||||
workspace_ = Source::FOREGROUND;
|
||||
}
|
||||
else if (groups_[View::LAYER]->translation_.x < -LAYER_BACKGROUND) {
|
||||
groups_[View::LAYER]->translation_.y -= 0.15f;
|
||||
workspace_ = Source::STAGE;
|
||||
}
|
||||
else
|
||||
workspace_ = Source::BACKGROUND;
|
||||
|
||||
// MODIFY depth based on LAYER node
|
||||
groups_[View::MIXING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::GEOMETRY]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::RENDERING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
|
||||
// MODIFY texture projection based on APPEARANCE node
|
||||
// UV to node coordinates
|
||||
static glm::mat4 UVtoScene = GlmToolkit::transform(glm::vec3(1.f, -1.f, 0.f),
|
||||
glm::vec3(0.f, 0.f, 0.f),
|
||||
glm::vec3(-2.f, 2.f, 1.f));
|
||||
// Aspect Ratio correction transform : coordinates of Appearance Frame are scaled by render buffer width
|
||||
glm::mat4 Ar = glm::scale(glm::identity<glm::mat4>(), glm::vec3(renderbuffer_->aspectRatio(), 1.f, 1.f) );
|
||||
// Translation : same as Appearance Frame (modified by Ar)
|
||||
glm::mat4 Tra = glm::translate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->translation_);
|
||||
// Scaling : inverse scaling (larger UV when smaller Appearance Frame)
|
||||
glm::vec2 scale = glm::vec2(groups_[View::TEXTURE]->scale_.x,groups_[View::TEXTURE]->scale_.y);
|
||||
scale = glm::sign(scale) * glm::max( glm::vec2(glm::epsilon<float>()), glm::abs(scale));
|
||||
glm::mat4 Sca = glm::scale(glm::identity<glm::mat4>(), glm::vec3(scale, 1.f));
|
||||
// Rotation : same angle than Appearance Frame, inverted axis
|
||||
glm::mat4 Rot = glm::rotate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->rotation_.z, glm::vec3(0.f, 0.f, -1.f) );
|
||||
// Combine transformations (non transitive) in this order:
|
||||
// 1. switch to Scene coordinate system
|
||||
// 2. Apply the aspect ratio correction
|
||||
// 3. Apply the translation
|
||||
// 4. Apply the rotation (centered after translation)
|
||||
// 5. Revert aspect ration correction
|
||||
// 6. Apply the Scaling (independent of aspect ratio)
|
||||
// 7. switch back to UV coordinate system
|
||||
texturesurface_->shader()->iTransform = glm::inverse(UVtoScene) * glm::inverse(Sca) * glm::inverse(Ar) * Rot * Tra * Ar * UVtoScene;
|
||||
|
||||
// if a mask image was given to be updated
|
||||
if (mask_need_update_) {
|
||||
// fill the mask buffer (once)
|
||||
if (maskbuffer_->fill(maskimage_) )
|
||||
mask_need_update_ = false;
|
||||
}
|
||||
// otherwise, render the mask buffer
|
||||
else
|
||||
// lock access to callbacks list
|
||||
access_callbacks_.lock();
|
||||
// call all callbacks
|
||||
for (auto iter=update_callbacks_.begin(); iter != update_callbacks_.end(); )
|
||||
{
|
||||
// draw mask in mask frame buffer
|
||||
maskbuffer_->begin(false);
|
||||
// loopback maskbuffer texture for painting
|
||||
masksurface_->setTextureIndex(maskbuffer_->texture());
|
||||
// fill surface with mask texture
|
||||
masksurface_->draw(glm::identity<glm::mat4>(), maskbuffer_->projection());
|
||||
maskbuffer_->end();
|
||||
}
|
||||
SourceCallback *callback = *iter;
|
||||
|
||||
// set the rendered mask as mask for blending
|
||||
blendingshader_->mask_texture = maskbuffer_->texture();
|
||||
// call update for active callbacks
|
||||
if (callback->active()) {
|
||||
callback->update(this, dt);
|
||||
need_update_ = true;
|
||||
}
|
||||
|
||||
// inform mixing group
|
||||
if (mixinggroup_)
|
||||
mixinggroup_->setAction(MixingGroup::ACTION_UPDATE);
|
||||
|
||||
// do not update next frame
|
||||
need_update_ = false;
|
||||
}
|
||||
|
||||
if (processingshader_link_.connected() && imageProcessingEnabled()) {
|
||||
Source *ref_source = processingshader_link_.source();
|
||||
if (ref_source!=nullptr) {
|
||||
if (ref_source->imageProcessingEnabled())
|
||||
processingshader_->copy( *ref_source->processingShader() );
|
||||
// remove and delete finished callbacks
|
||||
if (callback->finished()) {
|
||||
iter = update_callbacks_.erase(iter);
|
||||
delete callback;
|
||||
}
|
||||
// iterate
|
||||
else
|
||||
processingshader_link_.disconnect();
|
||||
++iter;
|
||||
}
|
||||
// release access to callbacks list
|
||||
access_callbacks_.unlock();
|
||||
|
||||
// update nodes if needed
|
||||
if (need_update_)
|
||||
{
|
||||
// ADJUST alpha based on MIXING node
|
||||
// read position of the mixing node and interpret this as transparency of render output
|
||||
glm::vec2 dist = glm::vec2(groups_[View::MIXING]->translation_);
|
||||
// use the sinusoidal transfer function
|
||||
blendingshader_->color = glm::vec4(1.f, 1.f, 1.f, SourceCore::alphaFromCordinates( dist.x, dist.y ));
|
||||
mixingshader_->color = blendingshader_->color;
|
||||
// adjust scale of mixing icon : smaller if not active
|
||||
groups_[View::MIXING]->scale_ = glm::vec3(MIXING_ICON_SCALE) - ( active_ ? glm::vec3(0.f, 0.f, 0.f) : glm::vec3(0.03f, 0.03f, 0.f) );
|
||||
|
||||
// MODIFY geometry based on GEOMETRY node
|
||||
groups_[View::RENDERING]->translation_ = groups_[View::GEOMETRY]->translation_;
|
||||
groups_[View::RENDERING]->rotation_ = groups_[View::GEOMETRY]->rotation_;
|
||||
glm::vec3 s = groups_[View::GEOMETRY]->scale_;
|
||||
// avoid any null scale
|
||||
s.x = CLAMP_SCALE(s.x);
|
||||
s.y = CLAMP_SCALE(s.y);
|
||||
s.z = 1.f;
|
||||
groups_[View::GEOMETRY]->scale_ = s;
|
||||
groups_[View::RENDERING]->scale_ = s;
|
||||
|
||||
// MODIFY CROP projection based on GEOMETRY crop
|
||||
renderbuffer_->setProjectionArea( glm::vec2(groups_[View::GEOMETRY]->crop_) );
|
||||
|
||||
// Mixing and layer icons scaled based on GEOMETRY crop
|
||||
mixingsurface_->scale_ = groups_[View::GEOMETRY]->crop_;
|
||||
mixingsurface_->scale_.x *= renderbuffer_->aspectRatio();
|
||||
mixingsurface_->update(dt_);
|
||||
|
||||
// Layers icons are displayed in Perspective (diagonal)
|
||||
groups_[View::LAYER]->translation_.x = -groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::LAYER]->translation_.y = groups_[View::LAYER]->translation_.x / LAYER_PERSPECTIVE;
|
||||
|
||||
// Update workspace based on depth, and
|
||||
// adjust vertical position of icon depending on workspace
|
||||
if (groups_[View::LAYER]->translation_.x < -LAYER_FOREGROUND) {
|
||||
groups_[View::LAYER]->translation_.y -= 0.3f;
|
||||
workspace_ = Source::FOREGROUND;
|
||||
}
|
||||
else if (groups_[View::LAYER]->translation_.x < -LAYER_BACKGROUND) {
|
||||
groups_[View::LAYER]->translation_.y -= 0.15f;
|
||||
workspace_ = Source::STAGE;
|
||||
}
|
||||
else
|
||||
workspace_ = Source::BACKGROUND;
|
||||
|
||||
// MODIFY depth based on LAYER node
|
||||
groups_[View::MIXING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::GEOMETRY]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
groups_[View::RENDERING]->translation_.z = groups_[View::LAYER]->translation_.z;
|
||||
|
||||
// MODIFY texture projection based on APPEARANCE node
|
||||
// UV to node coordinates
|
||||
static glm::mat4 UVtoScene = GlmToolkit::transform(glm::vec3(1.f, -1.f, 0.f),
|
||||
glm::vec3(0.f, 0.f, 0.f),
|
||||
glm::vec3(-2.f, 2.f, 1.f));
|
||||
// Aspect Ratio correction transform : coordinates of Appearance Frame are scaled by render buffer width
|
||||
glm::mat4 Ar = glm::scale(glm::identity<glm::mat4>(), glm::vec3(renderbuffer_->aspectRatio(), 1.f, 1.f) );
|
||||
// Translation : same as Appearance Frame (modified by Ar)
|
||||
glm::mat4 Tra = glm::translate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->translation_);
|
||||
// Scaling : inverse scaling (larger UV when smaller Appearance Frame)
|
||||
glm::vec2 scale = glm::vec2(groups_[View::TEXTURE]->scale_.x,groups_[View::TEXTURE]->scale_.y);
|
||||
scale = glm::sign(scale) * glm::max( glm::vec2(glm::epsilon<float>()), glm::abs(scale));
|
||||
glm::mat4 Sca = glm::scale(glm::identity<glm::mat4>(), glm::vec3(scale, 1.f));
|
||||
// Rotation : same angle than Appearance Frame, inverted axis
|
||||
glm::mat4 Rot = glm::rotate(glm::identity<glm::mat4>(), groups_[View::TEXTURE]->rotation_.z, glm::vec3(0.f, 0.f, -1.f) );
|
||||
// Combine transformations (non transitive) in this order:
|
||||
// 1. switch to Scene coordinate system
|
||||
// 2. Apply the aspect ratio correction
|
||||
// 3. Apply the translation
|
||||
// 4. Apply the rotation (centered after translation)
|
||||
// 5. Revert aspect ration correction
|
||||
// 6. Apply the Scaling (independent of aspect ratio)
|
||||
// 7. switch back to UV coordinate system
|
||||
texturesurface_->shader()->iTransform = glm::inverse(UVtoScene) * glm::inverse(Sca) * glm::inverse(Ar) * Rot * Tra * Ar * UVtoScene;
|
||||
|
||||
// if a mask image was given to be updated
|
||||
if (mask_need_update_) {
|
||||
// fill the mask buffer (once)
|
||||
if (maskbuffer_->fill(maskimage_) )
|
||||
mask_need_update_ = false;
|
||||
}
|
||||
// otherwise, render the mask buffer
|
||||
else
|
||||
{
|
||||
// draw mask in mask frame buffer
|
||||
maskbuffer_->begin(false);
|
||||
// loopback maskbuffer texture for painting
|
||||
masksurface_->setTextureIndex(maskbuffer_->texture());
|
||||
// fill surface with mask texture
|
||||
masksurface_->draw(glm::identity<glm::mat4>(), maskbuffer_->projection());
|
||||
maskbuffer_->end();
|
||||
}
|
||||
|
||||
// set the rendered mask as mask for blending
|
||||
blendingshader_->mask_texture = maskbuffer_->texture();
|
||||
|
||||
// inform mixing group
|
||||
if (mixinggroup_)
|
||||
mixinggroup_->setAction(MixingGroup::ACTION_UPDATE);
|
||||
|
||||
// do not update next frame
|
||||
need_update_ = false;
|
||||
}
|
||||
|
||||
if (processingshader_link_.connected() && imageProcessingEnabled()) {
|
||||
Source *ref_source = processingshader_link_.source();
|
||||
if (ref_source!=nullptr) {
|
||||
if (ref_source->imageProcessingEnabled())
|
||||
processingshader_->copy( *ref_source->processingShader() );
|
||||
else
|
||||
processingshader_link_.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FrameBuffer *Source::frame() const
|
||||
@@ -713,7 +809,7 @@ FrameBuffer *Source::frame() const
|
||||
return renderbuffer_;
|
||||
}
|
||||
else {
|
||||
static FrameBuffer *black = new FrameBuffer(640,480);
|
||||
static FrameBuffer *black = new FrameBuffer(320,180);
|
||||
return black;
|
||||
}
|
||||
}
|
||||
@@ -867,10 +963,19 @@ void CloneSource::setActive (bool on)
|
||||
groups_[View::GEOMETRY]->visible_ = active_;
|
||||
groups_[View::LAYER]->visible_ = active_;
|
||||
|
||||
if ( mode_ > Source::UNINITIALIZED && origin_ != nullptr)
|
||||
origin_->touch();
|
||||
}
|
||||
if (origin_) {
|
||||
if ( mode_ > Source::UNINITIALIZED)
|
||||
origin_->touch();
|
||||
|
||||
// change visibility of active surface (show preview of origin when inactive)
|
||||
if (activesurface_) {
|
||||
if (active_)
|
||||
activesurface_->setTextureIndex(Resource::getTextureTransparent());
|
||||
else
|
||||
activesurface_->setTextureIndex(origin_->texture());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint CloneSource::texture() const
|
||||
{
|
||||
@@ -887,3 +992,13 @@ void CloneSource::accept(Visitor& v)
|
||||
v.visit(*this);
|
||||
}
|
||||
|
||||
glm::ivec2 CloneSource::icon() const
|
||||
{
|
||||
return glm::ivec2(ICON_SOURCE_CLONE);
|
||||
}
|
||||
|
||||
std::string CloneSource::info() const
|
||||
{
|
||||
return std::string("clone of '") + origin_->name() + "'";
|
||||
}
|
||||
|
||||
|
||||
57
Source.h
57
Source.h
@@ -4,12 +4,29 @@
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <list>
|
||||
|
||||
#include "View.h"
|
||||
#include "SourceCallback.h"
|
||||
|
||||
#define DEFAULT_MIXING_TRANSLATION -1.f, 1.f
|
||||
|
||||
#define ICON_SOURCE_VIDEO 18, 13
|
||||
#define ICON_SOURCE_IMAGE 4, 9
|
||||
#define ICON_SOURCE_DEVICE_SCREEN 19, 1
|
||||
#define ICON_SOURCE_DEVICE 2, 14
|
||||
#define ICON_SOURCE_SEQUENCE 3, 9
|
||||
#define ICON_SOURCE_NETWORK 18, 11
|
||||
#define ICON_SOURCE_PATTERN 11, 5
|
||||
#define ICON_SOURCE_SESSION 19, 6
|
||||
#define ICON_SOURCE_GROUP 10, 6
|
||||
#define ICON_SOURCE_RENDER 0, 2
|
||||
#define ICON_SOURCE_CLONE 9, 2
|
||||
#define ICON_SOURCE_GSTREAMER 16, 16
|
||||
#define ICON_SOURCE 13, 11
|
||||
|
||||
class SourceCallback;
|
||||
class ImageShader;
|
||||
class MaskShader;
|
||||
class ImageProcessingShader;
|
||||
@@ -18,6 +35,7 @@ class FrameBufferSurface;
|
||||
class Frame;
|
||||
class Handles;
|
||||
class Symbol;
|
||||
class Glyph;
|
||||
class CloneSource;
|
||||
class MixingGroup;
|
||||
|
||||
@@ -43,6 +61,9 @@ public:
|
||||
|
||||
void copy(SourceCore const& other);
|
||||
|
||||
// alpha transfer function
|
||||
static float alphaFromCordinates(float x, float y);
|
||||
|
||||
protected:
|
||||
// nodes
|
||||
std::map<View::Mode, Group*> groups_;
|
||||
@@ -123,9 +144,13 @@ public:
|
||||
// a Source shall be updated before displayed (Mixing, Geometry and Layer)
|
||||
virtual void update (float dt);
|
||||
|
||||
// add callback to each update
|
||||
void call(SourceCallback *callback, bool override = false);
|
||||
|
||||
// update mode
|
||||
inline bool active () const { return active_; }
|
||||
virtual void setActive (bool on);
|
||||
void setActive (float threshold);
|
||||
|
||||
// lock mode
|
||||
inline bool locked () const { return locked_; }
|
||||
@@ -139,6 +164,13 @@ public:
|
||||
} Workspace;
|
||||
inline Workspace workspace () const { return workspace_; }
|
||||
|
||||
// a Source shall define a way to play
|
||||
virtual bool playable () const = 0;
|
||||
virtual bool playing () const = 0;
|
||||
virtual void play (bool on) = 0;
|
||||
virtual void replay () {}
|
||||
virtual guint64 playtime () const { return 0; }
|
||||
|
||||
// a Source shall informs if the source failed (i.e. shall be deleted)
|
||||
virtual bool failed () const = 0;
|
||||
|
||||
@@ -158,13 +190,9 @@ public:
|
||||
void setMask (FrameBufferImage *img);
|
||||
void storeMask (FrameBufferImage *img = nullptr);
|
||||
|
||||
// operations on depth
|
||||
// get properties
|
||||
float depth () const;
|
||||
void setDepth (float d);
|
||||
|
||||
// operations on alpha
|
||||
float alpha () const;
|
||||
void setAlpha (float a);
|
||||
|
||||
// groups for mixing
|
||||
MixingGroup *mixingGroup() const { return mixinggroup_; }
|
||||
@@ -221,7 +249,10 @@ public:
|
||||
}
|
||||
|
||||
// class-dependent icon
|
||||
virtual glm::ivec2 icon () const { return glm::ivec2(12, 11); }
|
||||
virtual glm::ivec2 icon () const { return glm::ivec2(ICON_SOURCE); }
|
||||
|
||||
// class-dependent notification
|
||||
virtual std::string info () const { return "Undefined"; }
|
||||
|
||||
SourceLink processingshader_link_;
|
||||
|
||||
@@ -243,7 +274,11 @@ protected:
|
||||
// the rendersurface draws the renderbuffer in the scene
|
||||
// It is associated to the rendershader for mixing effects
|
||||
FrameBufferSurface *rendersurface_;
|
||||
|
||||
// for the mixer, we have a surface with stippling to show
|
||||
// the rendering, and a preview of the original texture
|
||||
FrameBufferSurface *mixingsurface_;
|
||||
Surface *activesurface_;
|
||||
|
||||
// blendingshader provides mixing controls
|
||||
ImageShader *blendingshader_;
|
||||
@@ -269,6 +304,7 @@ protected:
|
||||
Handles *lock_, *unlock_;
|
||||
Switch *locker_;
|
||||
Symbol *symbol_;
|
||||
Glyph *initial_0_, *initial_1_;
|
||||
|
||||
// update
|
||||
bool active_;
|
||||
@@ -276,6 +312,8 @@ protected:
|
||||
bool need_update_;
|
||||
float dt_;
|
||||
Workspace workspace_;
|
||||
std::list<SourceCallback *> update_callbacks_;
|
||||
std::mutex access_callbacks_;
|
||||
|
||||
// clones
|
||||
CloneList clones_;
|
||||
@@ -299,6 +337,10 @@ public:
|
||||
|
||||
// implementation of source API
|
||||
void setActive (bool on) override;
|
||||
bool playing () const override { return true; }
|
||||
void play (bool) override {}
|
||||
bool playable () const override { return false; }
|
||||
void replay () override {}
|
||||
uint texture() const override;
|
||||
bool failed() const override { return origin_ == nullptr; }
|
||||
void accept (Visitor& v) override;
|
||||
@@ -307,7 +349,8 @@ public:
|
||||
inline void detach() { origin_ = nullptr; }
|
||||
inline Source *origin() const { return origin_; }
|
||||
|
||||
glm::ivec2 icon() const override { return glm::ivec2(9, 2); }
|
||||
glm::ivec2 icon() const override;
|
||||
std::string info() const override;
|
||||
|
||||
protected:
|
||||
// only Source class can create new CloneSource via clone();
|
||||
|
||||
299
SourceCallback.cpp
Normal file
299
SourceCallback.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* This file is part of vimix - video live mixer
|
||||
*
|
||||
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
**/
|
||||
|
||||
#include "defines.h"
|
||||
#include "Source.h"
|
||||
#include "Log.h"
|
||||
|
||||
#include "SourceCallback.h"
|
||||
|
||||
SourceCallback::SourceCallback(): active_(true), finished_(false), initialized_(false)
|
||||
{
|
||||
}
|
||||
|
||||
void ResetGeometry::update(Source *s, float)
|
||||
{
|
||||
s->group(View::GEOMETRY)->scale_ = glm::vec3(1.f);
|
||||
s->group(View::GEOMETRY)->rotation_.z = 0;
|
||||
s->group(View::GEOMETRY)->crop_ = glm::vec3(1.f);
|
||||
s->group(View::GEOMETRY)->translation_ = glm::vec3(0.f);
|
||||
s->touch();
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
SetAlpha::SetAlpha(float alpha) : SourceCallback(), alpha_(CLAMP(alpha, 0.f, 1.f))
|
||||
{
|
||||
pos_ = glm::vec2();
|
||||
step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default
|
||||
}
|
||||
|
||||
void SetAlpha::update(Source *s, float)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// set start position on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// initial position
|
||||
pos_ = glm::vec2(s->group(View::MIXING)->translation_);
|
||||
// step in direction of source translation if possible
|
||||
if ( glm::length(pos_) > DELTA_ALPHA)
|
||||
step_ = glm::normalize(pos_);
|
||||
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// perform operation
|
||||
float delta = SourceCore::alphaFromCordinates(pos_.x, pos_.y) - alpha_;
|
||||
|
||||
// converge to reduce the difference of alpha using dichotomic algorithm
|
||||
if ( glm::abs(delta) > DELTA_ALPHA ){
|
||||
pos_ += step_ * (delta / 2.f);
|
||||
s->group(View::MIXING)->translation_ = glm::vec3(pos_, s->group(View::MIXING)->translation_.z);
|
||||
}
|
||||
// done
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
SetLock::SetLock(bool on) : SourceCallback(), lock_(on)
|
||||
{
|
||||
}
|
||||
|
||||
void SetLock::update(Source *s, float)
|
||||
{
|
||||
if (s)
|
||||
s->setLocked(lock_);
|
||||
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
Loom::Loom(float da, float duration) : SourceCallback(), speed_(da),
|
||||
duration_(duration), progress_(0.f)
|
||||
{
|
||||
pos_ = glm::vec2();
|
||||
step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default
|
||||
}
|
||||
|
||||
void Loom::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// reset on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// start animation
|
||||
progress_ = 0.f;
|
||||
// initial position
|
||||
pos_ = glm::vec2(s->group(View::MIXING)->translation_);
|
||||
// step in direction of source translation if possible
|
||||
if ( glm::length(pos_) > DELTA_ALPHA)
|
||||
step_ = glm::normalize(pos_);
|
||||
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// time passed
|
||||
progress_ += dt;
|
||||
|
||||
// move target by speed vector (in the direction of step_, amplitude of speed * time (in second))
|
||||
pos_ += step_ * ( speed_ * dt * 0.001f);
|
||||
|
||||
// apply alpha if valid in range [0 1]
|
||||
float alpha = SourceCore::alphaFromCordinates(pos_.x, pos_.y);
|
||||
if ( alpha > DELTA_ALPHA && alpha < 1.0 - DELTA_ALPHA )
|
||||
s->group(View::MIXING)->translation_ = glm::vec3(pos_, s->group(View::MIXING)->translation_.z);
|
||||
|
||||
// time-out
|
||||
if ( progress_ > duration_ ) {
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
SetDepth::SetDepth(float target, float duration) : SourceCallback(),
|
||||
duration_(duration), progress_(0.f), start_(0.f), target_(CLAMP(target, MIN_DEPTH, MAX_DEPTH))
|
||||
{
|
||||
}
|
||||
|
||||
void SetDepth::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// set start position on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
start_ = s->group(View::LAYER)->translation_.z;
|
||||
progress_ = 0.f;
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// time passed
|
||||
progress_ += dt;
|
||||
|
||||
// perform movement
|
||||
if ( ABS(duration_) > 0.f)
|
||||
s->group(View::LAYER)->translation_.z = start_ + (progress_/duration_) * (target_ - start_);
|
||||
|
||||
// time-out
|
||||
if ( progress_ > duration_ ) {
|
||||
// apply depth to target
|
||||
s->group(View::LAYER)->translation_.z = target_;
|
||||
// ensure reordering of view
|
||||
++View::need_deep_update_;
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
|
||||
SetPlay::SetPlay(bool on) : SourceCallback(), play_(on)
|
||||
{
|
||||
}
|
||||
|
||||
void SetPlay::update(Source *s, float)
|
||||
{
|
||||
if (s && s->playing() != play_) {
|
||||
// call play function
|
||||
s->play(play_);
|
||||
}
|
||||
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
RePlay::RePlay() : SourceCallback()
|
||||
{
|
||||
}
|
||||
|
||||
void RePlay::update(Source *s, float)
|
||||
{
|
||||
if (s) {
|
||||
// call replay function
|
||||
s->replay();
|
||||
}
|
||||
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
|
||||
Grab::Grab(float dx, float dy, float duration) : SourceCallback(), speed_(glm::vec2(dx,dy)),
|
||||
duration_(duration), progress_(0.f)
|
||||
{
|
||||
}
|
||||
|
||||
void Grab::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// reset on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// start animation
|
||||
progress_ = 0.f;
|
||||
// initial position
|
||||
start_ = glm::vec2(s->group(View::GEOMETRY)->translation_);
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// time passed
|
||||
progress_ += dt;
|
||||
|
||||
// move target by speed vector * time (in second)
|
||||
glm::vec2 pos = start_ + speed_ * ( dt * 0.001f);
|
||||
s->group(View::GEOMETRY)->translation_ = glm::vec3(pos, s->group(View::GEOMETRY)->translation_.z);
|
||||
|
||||
// time-out
|
||||
if ( progress_ > duration_ ) {
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
Resize::Resize(float dx, float dy, float duration) : SourceCallback(), speed_(glm::vec2(dx,dy)),
|
||||
duration_(duration), progress_(0.f)
|
||||
{
|
||||
}
|
||||
|
||||
void Resize::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// reset on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// start animation
|
||||
progress_ = 0.f;
|
||||
// initial position
|
||||
start_ = glm::vec2(s->group(View::GEOMETRY)->scale_);
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// time passed
|
||||
progress_ += dt;
|
||||
|
||||
// move target by speed vector * time (in second)
|
||||
glm::vec2 scale = start_ + speed_ * ( dt * 0.001f);
|
||||
s->group(View::GEOMETRY)->scale_ = glm::vec3(scale, s->group(View::GEOMETRY)->scale_.z);
|
||||
|
||||
// time-out
|
||||
if ( progress_ > duration_ ) {
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
Turn::Turn(float da, float duration) : SourceCallback(), speed_(da),
|
||||
duration_(duration), progress_(0.f)
|
||||
{
|
||||
}
|
||||
|
||||
void Turn::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// reset on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// start animation
|
||||
progress_ = 0.f;
|
||||
// initial position
|
||||
start_ = s->group(View::GEOMETRY)->rotation_.z;
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// calculate amplitude of movement
|
||||
progress_ += dt;
|
||||
|
||||
// perform movement
|
||||
s->group(View::GEOMETRY)->rotation_.z = start_ + speed_ * ( dt * -0.001f / M_PI);
|
||||
|
||||
// timeout
|
||||
if ( progress_ > duration_ ) {
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user