mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-08 16:59:59 +01:00
Mixing view handles to grab and scale limbo area. Saving of user defined limit in Session (and snapshot). Testing for source activation outside of update during session update loop.
516 lines
14 KiB
C++
516 lines
14 KiB
C++
/*
|
|
* This file is part of vimix - video live 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 <string>
|
|
#include <algorithm>
|
|
#include <thread>
|
|
|
|
#include "Log.h"
|
|
#include "View.h"
|
|
#include "Mixer.h"
|
|
#include "MixingGroup.h"
|
|
#include "tinyxml2Toolkit.h"
|
|
#include "ImageProcessingShader.h"
|
|
#include "SessionVisitor.h"
|
|
#include "SessionCreator.h"
|
|
#include "Settings.h"
|
|
#include "BaseToolkit.h"
|
|
#include "Interpolator.h"
|
|
#include "SystemToolkit.h"
|
|
|
|
#include "ActionManager.h"
|
|
|
|
#ifndef NDEBUG
|
|
#define ACTION_DEBUG
|
|
#endif
|
|
|
|
#define HISTORY_NODE(i) std::to_string(i).insert(0,1,'H')
|
|
#define SNAPSHOT_NODE(i) std::to_string(i).insert(0,1,'S')
|
|
|
|
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();
|
|
|
|
// get the thumbnail (requires one opengl update to render)
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
Action::Action(): history_step_(0), history_max_step_(0), locked_(false),
|
|
snapshot_id_(0), snapshot_node_(nullptr), interpolator_(nullptr), interpolator_node_(nullptr)
|
|
{
|
|
|
|
}
|
|
|
|
void Action::init()
|
|
{
|
|
// clean the history
|
|
history_doc_.Clear();
|
|
history_step_ = 0;
|
|
history_max_step_ = 0;
|
|
|
|
// reset snapshot
|
|
snapshot_id_ = 0;
|
|
snapshot_node_ = nullptr;
|
|
|
|
store("Session start");
|
|
}
|
|
|
|
void Action::store(const std::string &label)
|
|
{
|
|
// ignore if locked or if no label is given
|
|
if (locked_ || label.empty())
|
|
return;
|
|
|
|
// incremental naming of history nodes
|
|
history_step_++;
|
|
|
|
// erase future
|
|
for (uint e = history_step_; e <= history_max_step_; e++) {
|
|
XMLElement *node = history_doc_.FirstChildElement( HISTORY_NODE(e).c_str() );
|
|
if ( node )
|
|
history_doc_.DeleteChild(node);
|
|
}
|
|
history_max_step_ = history_step_;
|
|
|
|
// threaded capturing state of current session
|
|
std::thread(captureMixerSession, &history_doc_, HISTORY_NODE(history_step_), label).detach();
|
|
|
|
#ifdef ACTION_DEBUG
|
|
Log::Info("Action stored %d '%s'", history_step_, label.c_str());
|
|
// XMLSaveDoc(&history_doc_, "/home/bhbn/history.xml");
|
|
#endif
|
|
}
|
|
|
|
void Action::undo()
|
|
{
|
|
// not possible to go to 1 -1 = 0
|
|
if (history_step_ <= 1)
|
|
return;
|
|
|
|
// restore always changes step_ to step_ - 1
|
|
restore( history_step_ - 1);
|
|
}
|
|
|
|
void Action::redo()
|
|
{
|
|
// not possible to go to max_step_ + 1
|
|
if (history_step_ >= history_max_step_)
|
|
return;
|
|
|
|
// restore always changes step_ to step_ + 1
|
|
restore( history_step_ + 1);
|
|
}
|
|
|
|
|
|
void Action::stepTo(uint target)
|
|
{
|
|
// get reasonable target
|
|
uint t = CLAMP(target, 1, history_max_step_);
|
|
|
|
// ignore t == step_
|
|
if (t != history_step_)
|
|
restore(t);
|
|
}
|
|
|
|
std::string Action::label(uint s) const
|
|
{
|
|
std::string l = "";
|
|
|
|
if (s > 0 && s <= history_max_step_) {
|
|
const XMLElement *sessionNode = history_doc_.FirstChildElement( HISTORY_NODE(s).c_str());
|
|
if (sessionNode)
|
|
l = sessionNode->Attribute("label");
|
|
}
|
|
return l;
|
|
}
|
|
|
|
FrameBufferImage *Action::thumbnail(uint s) const
|
|
{
|
|
FrameBufferImage *img = nullptr;
|
|
|
|
if (s > 0 && s <= history_max_step_) {
|
|
const XMLElement *sessionNode = history_doc_.FirstChildElement( HISTORY_NODE(s).c_str());
|
|
if (sessionNode)
|
|
img = SessionLoader::XMLToImage(sessionNode);
|
|
}
|
|
|
|
return img;
|
|
}
|
|
|
|
void Action::restore(uint target)
|
|
{
|
|
// lock
|
|
locked_ = true;
|
|
|
|
// get history node of target step
|
|
history_step_ = CLAMP(target, 1, history_max_step_);
|
|
XMLElement *sessionNode = history_doc_.FirstChildElement( HISTORY_NODE(history_step_).c_str() );
|
|
|
|
if (sessionNode) {
|
|
|
|
// ask view to refresh, and switch to action view if user prefers
|
|
int view = Settings::application.current_view ;
|
|
if (Settings::application.action_history_follow_view)
|
|
sessionNode->QueryIntAttribute("view", &view);
|
|
Mixer::manager().setView( (View::Mode) view);
|
|
|
|
// actually restore
|
|
Mixer::manager().restore(sessionNode);
|
|
}
|
|
|
|
// free
|
|
locked_ = false;
|
|
}
|
|
|
|
|
|
|
|
void Action::snapshot(const std::string &label, bool threaded)
|
|
{
|
|
// ignore if locked
|
|
if (locked_)
|
|
return;
|
|
|
|
std::string snap_label = BaseToolkit::uniqueName(label, labels());
|
|
|
|
// create snapshot id
|
|
u_int64_t id = BaseToolkit::uniqueId();
|
|
|
|
// get session to operate on
|
|
Session *se = Mixer::manager().session();
|
|
se->snapshots()->keys_.push_back(id);
|
|
|
|
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());
|
|
#endif
|
|
}
|
|
|
|
void Action::open(uint64_t snapshotid)
|
|
{
|
|
if ( snapshot_id_ != snapshotid )
|
|
{
|
|
// get snapshot node of target in current session
|
|
Session *se = Mixer::manager().session();
|
|
snapshot_node_ = se->snapshots()->xmlDoc_->FirstChildElement( SNAPSHOT_NODE(snapshotid).c_str() );
|
|
|
|
if (snapshot_node_)
|
|
snapshot_id_ = snapshotid;
|
|
else
|
|
snapshot_id_ = 0;
|
|
|
|
interpolator_node_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void Action::replace(uint64_t snapshotid)
|
|
{
|
|
// ignore if locked or if no label is given
|
|
if (locked_)
|
|
return;
|
|
|
|
if (snapshotid > 0)
|
|
open(snapshotid);
|
|
|
|
if (snapshot_node_) {
|
|
// remember label
|
|
std::string label = snapshot_node_->Attribute("label");
|
|
|
|
// remove previous node
|
|
Session *se = Mixer::manager().session();
|
|
se->snapshots()->xmlDoc_->DeleteChild( snapshot_node_ );
|
|
|
|
// threaded capture state of current session
|
|
std::thread(captureMixerSession, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(snapshot_id_), label).detach();
|
|
|
|
#ifdef ACTION_DEBUG
|
|
Log::Info("Snapshot replaced %d '%s'", snapshot_id_, label.c_str());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
std::list<uint64_t> Action::snapshots() const
|
|
{
|
|
return Mixer::manager().session()->snapshots()->keys_;
|
|
}
|
|
|
|
std::list<std::string> Action::labels() const
|
|
{
|
|
std::list<std::string> names;
|
|
|
|
tinyxml2::XMLDocument *doc = Mixer::manager().session()->snapshots()->xmlDoc_;
|
|
for ( XMLElement *snap = doc->FirstChildElement(); snap ; snap = snap->NextSiblingElement() )
|
|
names.push_back( snap->Attribute("label"));
|
|
|
|
return names;
|
|
}
|
|
|
|
std::string Action::label(uint64_t snapshotid) const
|
|
{
|
|
std::string label = "";
|
|
|
|
// 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)
|
|
label = snap->Attribute("label");
|
|
|
|
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);
|
|
|
|
if (snapshot_node_)
|
|
snapshot_node_->SetAttribute("label", label.c_str());
|
|
}
|
|
|
|
FrameBufferImage *Action::thumbnail(uint64_t snapshotid) const
|
|
{
|
|
FrameBufferImage *img = nullptr;
|
|
|
|
// 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){
|
|
img = SessionLoader::XMLToImage(snap);
|
|
}
|
|
|
|
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)
|
|
open(snapshotid);
|
|
|
|
if (snapshot_node_) {
|
|
// remove
|
|
Session *se = Mixer::manager().session();
|
|
se->snapshots()->xmlDoc_->DeleteChild( snapshot_node_ );
|
|
se->snapshots()->keys_.remove( snapshot_id_ );
|
|
}
|
|
|
|
snapshot_node_ = nullptr;
|
|
snapshot_id_ = 0;
|
|
}
|
|
|
|
void Action::restore(uint64_t snapshotid)
|
|
{
|
|
// lock
|
|
locked_ = true;
|
|
|
|
if (snapshotid > 0)
|
|
open(snapshotid);
|
|
|
|
if (snapshot_node_)
|
|
// actually restore
|
|
Mixer::manager().restore(snapshot_node_);
|
|
|
|
// free
|
|
locked_ = false;
|
|
|
|
store("Snapshot " + label(snapshot_id_));
|
|
}
|
|
|
|
float Action::interpolation()
|
|
{
|
|
float ret = 0.f;
|
|
if ( interpolator_node_ == snapshot_node_ && interpolator_)
|
|
ret = interpolator_->current();
|
|
|
|
return ret;
|
|
}
|
|
|
|
void Action::interpolate(float val, uint64_t snapshotid)
|
|
{
|
|
if (snapshotid > 0)
|
|
open(snapshotid);
|
|
|
|
if (snapshot_node_) {
|
|
|
|
if ( interpolator_node_ != snapshot_node_ ) {
|
|
|
|
// change interpolator
|
|
if (interpolator_)
|
|
delete interpolator_;
|
|
|
|
// create new interpolator
|
|
interpolator_ = new Interpolator;
|
|
|
|
// current session
|
|
Session *se = Mixer::manager().session();
|
|
|
|
XMLElement* N = snapshot_node_->FirstChildElement("Source");
|
|
for( ; N ; N = N->NextSiblingElement()) {
|
|
|
|
// check if a source with the given id exists in the session
|
|
uint64_t id_xml_ = 0;
|
|
N->QueryUnsigned64Attribute("id", &id_xml_);
|
|
SourceList::iterator sit = se->find(id_xml_);
|
|
|
|
// a source with this id exists
|
|
if ( sit != se->end() ) {
|
|
// read target in the snapshot xml
|
|
SourceCore target;
|
|
SessionLoader::XMLToSourcecore(N, target);
|
|
|
|
// add an interpolator for this source
|
|
interpolator_->add(*sit, target);
|
|
}
|
|
}
|
|
|
|
// operate interpolation on opened snapshot
|
|
interpolator_node_ = snapshot_node_;
|
|
}
|
|
|
|
if (interpolator_) {
|
|
// Log::Info("Action::interpolate %f", val);
|
|
interpolator_->apply( val );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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();
|
|
}
|
|
}
|