/* * This file is part of vimix - video live mixer * * **Copyright** (C) 2019-2022 Bruno Herbelin * * 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 . **/ #include #include #include #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; 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 captureMixerSession(Session *se, tinyxml2::XMLDocument *doc, std::string node, std::string label) { if (se != nullptr) { // 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 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); } } 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, Mixer::manager().session(), &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::takeSnapshot(Session *se, const std::string &label, bool create_thread) { if (se !=nullptr) { // create snapshot id u_int64_t id = BaseToolkit::uniqueId(); // inform session of its new snapshot se->snapshots()->keys_.push_back(id); if (create_thread) // threaded capture state of current session std::thread(captureMixerSession, se, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), label).detach(); else captureMixerSession(se, se->snapshots()->xmlDoc_, SNAPSHOT_NODE(id), label); #ifdef ACTION_DEBUG Log::Info("Snapshot stored %d '%s'", id, label.c_str()); #endif } } void Action::snapshot(const std::string &label) { // ignore if locked if (locked_) return; // ensure label is unique std::string snap_label = BaseToolkit::uniqueName(label, labels()); // take the snapshot on current session takeSnapshot(Mixer::manager().session(), snap_label, true); } 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(); if (se) { se->snapshots()->xmlDoc_->DeleteChild( snapshot_node_ ); // threaded capture state of current session std::thread(captureMixerSession, se, 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 Action::snapshots() const { return Mixer::manager().session()->snapshots()->keys_; } std::list Action::labels() const { std::list 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_){ // ensure unique snapshot label std::string snap_label = BaseToolkit::uniqueName(label, labels()); // change attribute snapshot_node_->SetAttribute("label", snap_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(); } }