/* * 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 "defines.h" #include "BaseToolkit.h" #include "Source.h" #include "Settings.h" #include "FrameBuffer.h" #include "FrameGrabber.h" #include "SessionCreator.h" #include "SessionVisitor.h" #include "ActionManager.h" #include "SessionSource.h" #include "RenderSource.h" #include "MixingGroup.h" #include "ControlManager.h" #include "SourceCallback.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), activation_threshold_(MIXING_MIN_THRESHOLD), filename_(""), failedSource_(nullptr), thumbnail_(nullptr) { config_[View::RENDERING] = new Group; config_[View::RENDERING]->scale_ = glm::vec3(0.f); config_[View::GEOMETRY] = new Group; config_[View::GEOMETRY]->scale_ = Settings::application.views[View::GEOMETRY].default_scale; config_[View::GEOMETRY]->translation_ = Settings::application.views[View::GEOMETRY].default_translation; config_[View::LAYER] = new Group; config_[View::LAYER]->scale_ = Settings::application.views[View::LAYER].default_scale; config_[View::LAYER]->translation_ = Settings::application.views[View::LAYER].default_translation; config_[View::MIXING] = new Group; config_[View::MIXING]->scale_ = Settings::application.views[View::MIXING].default_scale; config_[View::MIXING]->translation_ = Settings::application.views[View::MIXING].default_translation; config_[View::TEXTURE] = new Group; config_[View::TEXTURE]->scale_ = Settings::application.views[View::TEXTURE].default_scale; config_[View::TEXTURE]->translation_ = Settings::application.views[View::TEXTURE].default_translation; input_sync_.resize(INPUT_MAX, Metronome::SYNC_NONE); snapshots_.xmlDoc_ = new tinyxml2::XMLDocument; start_time_ = gst_util_get_timestamp (); } Session::~Session() { // TODO delete all mixing groups? auto group_iter = mixing_groups_.begin(); while ( group_iter != mixing_groups_.end() ){ delete (*group_iter); group_iter = mixing_groups_.erase(group_iter); } // delete all sources for(auto it = sources_.begin(); it != sources_.end(); ) { // erase this source from the list it = deleteSource(*it); } // delete all callbacks for (auto iter = input_callbacks_.begin(); iter != input_callbacks_.end(); iter = input_callbacks_.erase(iter)) { if ( iter->second.model_ != nullptr) delete iter->second.model_; if ( iter->second.reverse_ != nullptr) delete iter->second.reverse_; } delete config_[View::RENDERING]; delete config_[View::GEOMETRY]; delete config_[View::LAYER]; delete config_[View::MIXING]; delete config_[View::TEXTURE]; snapshots_.keys_.clear(); delete snapshots_.xmlDoc_; } uint64_t Session::runtime() const { return gst_util_get_timestamp () - start_time_; } void Session::setActive (bool on) { if (active_ != on) { active_ = on; for(auto it = sources_.begin(); it != sources_.end(); ++it) { (*it)->setActive(active_); } } } // update all sources void Session::update(float dt) { // no update until render view is initialized if ( render_.frame() == nullptr ) return; // listen to inputs for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ++k) { // get if the input is activated (e.g. key pressed) bool input_active = Control::manager().inputActive(k->first); // get the callback model for each input if ( k->second.model_ != nullptr) { // if the value referenced as pressed changed state // or repeat key if there is no reverse callback if ( input_active != k->second.active_ || k->second.reverse_ == nullptr) { // ON PRESS if (input_active) { // delete the reverse if was not released if (k->second.reverse_ != nullptr) delete k->second.reverse_; // generate a new callback from the model SourceCallback *C = k->second.model_->clone(); // apply value multiplyer from input C->multiply( Control::manager().inputValue(k->first) ); // add delay C->delay( Metronome::manager().timeToSync( (Metronome::Synchronicity) input_sync_[k->first] ) ); // add callback to the source (force override) k->second.source_->call( C, true ); // get the reverse if the callback, and remember it (can be null) k->second.reverse_ = C->reverse(k->second.source_); } // ON RELEASE else { // call the reverse (NB: invalid value tested in call) k->second.source_->call( k->second.reverse_, true ); // do not keep reference to reverse: will be deleted when terminated k->second.reverse_ = nullptr; } // remember state of callback k->second.active_ = input_active; } } } // 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 *rs = dynamic_cast( *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; // set inputs // update the source (*it)->setActive(activation_threshold_); (*it)->update(dt); // render the source (*it)->render(); } } // update session's mixing groups auto group_iter = mixing_groups_.begin(); while ( group_iter != mixing_groups_.end() ){ // update all valid groups if ((*group_iter)->size() > 1) { (*group_iter)->update(dt); group_iter++; } else // delete invalid groups (singletons) group_iter = deleteMixingGroup(group_iter); } // 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 render_.update(dt); // draw render view in Frame Buffer render_.draw(); // draw the thumbnail only after all sources are ready if (ready) render_.drawThumbnail(); } SourceList::iterator Session::addSource(Source *s) { // lock before change access_.lock(); // find the source SourceList::iterator its = find(s); // ok, its NOT in the list ! if (its == sources_.end()) { // 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_back(s); // return the iterator to the source created at the beginning its = --sources_.end(); } // unlock access access_.unlock(); return its; } SourceList::iterator Session::deleteSource(Source *s) { // lock before change access_.lock(); // find the source SourceList::iterator its = find(s); // ok, its in the list ! if (its != sources_.end()) { // remove Node from the rendering scene render_.scene.ws()->detach( s->group(View::RENDERING) ); // inform group if (s->mixingGroup() != nullptr) s->mixingGroup()->detach(s); // erase the source from the update list & get next element its = sources_.erase(its); // delete the source : safe now delete s; } // unlock access access_.unlock(); // return end of next element return its; } void Session::removeSource(Source *s) { // lock before change access_.lock(); // find the source SourceList::iterator its = find(s); // ok, its in the list ! if (its != sources_.end()) { // remove Node from the rendering scene render_.scene.ws()->detach( s->group(View::RENDERING) ); // inform group if (s->mixingGroup() != nullptr) s->mixingGroup()->detach(s); // erase the source from the update list & get next element sources_.erase(its); } // unlock access access_.unlock(); } Source *Session::popSource() { Source *s = nullptr; SourceList::iterator its = sources_.begin(); if (its != sources_.end()) { s = *its; // remove Node from the rendering scene render_.scene.ws()->detach( s->group(View::RENDERING) ); // erase the source from the update list & get next element sources_.erase(its); } 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 render_.setResolution( resolution, useAlpha ); // store the actual resolution set in the render view config_[View::RENDERING]->scale_ = render_.resolution(); } void Session::setFadingTarget(float f, float duration) { // 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() { return sources_.begin(); } SourceList::iterator Session::end() { return sources_.end(); } SourceList::iterator Session::find(Source *s) { return std::find(sources_.begin(), sources_.end(), s); } SourceList::iterator Session::find(uint64_t id) { return std::find_if(sources_.begin(), sources_.end(), Source::hasId(id)); } SourceList::iterator Session::find(std::string namesource) { return std::find_if(sources_.begin(), sources_.end(), Source::hasName(namesource)); } SourceList::iterator Session::find(Node *node) { return std::find_if(sources_.begin(), sources_.end(), Source::hasNode(node)); } SourceList::iterator Session::find(float depth_from, float depth_to) { return std::find_if(sources_.begin(), sources_.end(), Source::hasDepth(depth_from, depth_to)); } SourceList Session::getDepthSortedList() const { return depth_sorted(sources_); } uint Session::numSource() const { return sources_.size(); } SourceIdList Session::getIdList() const { return ids(sources_); } std::list Session::getNameList(uint64_t exceptid) const { std::list namelist; for( SourceList::const_iterator it = sources_.cbegin(); it != sources_.cend(); ++it) { if ( (*it)->id() != exceptid ) namelist.push_back( (*it)->name() ); } return namelist; } bool Session::empty() const { return sources_.empty(); } SourceList::iterator Session::at(int index) { if ( index < 0 || index > (int) sources_.size()) return sources_.end(); int i = 0; SourceList::iterator it = sources_.begin(); while ( i < index && it != sources_.end() ){ i++; ++it; } return it; } int Session::index(SourceList::iterator it) const { int index = -1; int count = 0; for(auto i = sources_.begin(); i != sources_.end(); ++i, ++count) { if ( i == it ) { index = count; break; } } return index; } void Session::move(int current_index, int target_index) { if ( current_index < 0 || current_index > (int) sources_.size() || target_index < 0 || target_index > (int) sources_.size() || target_index == current_index ) return; SourceList::iterator from = at(current_index); SourceList::iterator to = at(target_index); if ( target_index > current_index ) ++to; Source *s = (*from); sources_.erase(from); sources_.insert(to, s); } bool Session::canlink (SourceList sources) { bool canlink = true; // verify that all sources given are valid in the sesion validate(sources); for (auto it = sources.begin(); it != sources.end(); ++it) { // this source is linked if ( (*it)->mixingGroup() != nullptr ) { // askt its group to detach it canlink = false; } } return canlink; } void Session::link(SourceList sources, Group *parent) { // we need at least 2 sources to make a group if (sources.size() > 1) { unlink(sources); // create and add a new mixing group MixingGroup *g = new MixingGroup(sources); mixing_groups_.push_back(g); // if provided, attach the group to the parent if (g && parent != nullptr) g->attachTo( parent ); } } void Session::unlink (SourceList sources) { // verify that all sources given are valid in the sesion validate(sources); // brute force : detach all given sources for (auto it = sources.begin(); it != sources.end(); ++it) { // this source is linked if ( (*it)->mixingGroup() != nullptr ) { // askt its group to detach it (*it)->mixingGroup()->detach(*it); } } } void Session::addNote(SessionNote note) { notes_.push_back( note ); } std::list::iterator Session::beginNotes () { return notes_.begin(); } std::list::iterator Session::endNotes () { return notes_.end(); } std::list::iterator Session::deleteNote (std::list::iterator n) { if (n != notes_.end()) return notes_.erase(n); return notes_.end(); } std::list Session::getMixingGroups () const { std::list lmg; for (auto group_it = mixing_groups_.begin(); group_it!= mixing_groups_.end(); ++group_it) lmg.push_back( (*group_it)->getCopy() ); return lmg; } std::list::iterator Session::deleteMixingGroup (std::list::iterator g) { if (g != mixing_groups_.end()) { delete (*g); return mixing_groups_.erase(g); } return mixing_groups_.end(); } std::list::iterator Session::beginMixingGroup() { return mixing_groups_.begin(); } std::list::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::validate (SourceList &sources) { // verify that all sources given are valid in the sesion // and remove the invalid sources for (auto _it = sources.begin(); _it != sources.end(); ) { SourceList::iterator found = std::find(sources_.begin(), sources_.end(), *_it); if ( found == sources_.end() ) _it = sources.erase(_it); else _it++; } } Session *Session::load(const std::string& filename, uint recursion) { // create session SessionCreator creator(recursion); creator.load(filename); // return created session return creator.session(); } std::string Session::save(const std::string& filename, Session *session, const std::string& snapshot_name) { std::string ret; if (session) { // lock access while saving session->access_.lock(); // capture a snapshot of current version if requested (do not create thread) if (!snapshot_name.empty()) Action::takeSnapshot(session, snapshot_name, false ); // save file to disk if (SessionVisitor::saveSession(filename, session)) // return filename string on success ret = filename; // unlock access after saving session->access_.unlock(); } return ret; } void Session::assignSourceCallback(uint input, Source *source, SourceCallback *callback) { // find if this callback is already assigned auto k = input_callbacks_.begin(); for (; k != input_callbacks_.end(); ++k) { // yes, then just change the source pointer if ( k->second.model_ == callback) { if (k->second.reverse_) delete k->second.reverse_; k->second.source_ = source; break; } } // if this callback is not assigned yet (looped until end) if ( k == input_callbacks_.end() ) { // create new entry std::multimap::iterator added = input_callbacks_.emplace(input, InputSourceCallback() ); added->second.model_ = callback; added->second.source_ = source; } } void Session::swapSourceCallback(uint from, uint to) { std::multimap swapped_callbacks_; for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ++k) { if ( k->first == from ) swapped_callbacks_.emplace( to, k->second); else swapped_callbacks_.emplace( k->first, k->second); } input_callbacks_.swap(swapped_callbacks_); } void Session::copySourceCallback(uint from, uint to) { if ( input_callbacks_.count(from) > 0 ) { auto result = input_callbacks_.equal_range(from); for (auto it = result.first; it != result.second; ++it) { assignSourceCallback(to, it->second.source_, it->second.model_->clone() ); } } } std::list< std::pair > Session::getSourceCallbacks(uint input) { std::list< std::pair > ret; if ( input_callbacks_.count(input) > 0 ) { auto result = input_callbacks_.equal_range(input); for (auto it = result.first; it != result.second; ++it) ret.push_back( std::pair(it->second.source_, it->second.model_) ); } return ret; } void Session::deleteSourceCallback(SourceCallback *callback) { for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ++k) { if ( k->second.model_ == callback) { delete callback; if (k->second.reverse_) delete k->second.reverse_; input_callbacks_.erase(k); break; } } } void Session::deleteSourceCallbacks(uint input) { for (auto k = input_callbacks_.begin(); k != input_callbacks_.end();) { if ( k->first == input) { if (k->second.model_) delete k->second.model_; if (k->second.reverse_) delete k->second.reverse_; k = input_callbacks_.erase(k); } else ++k; } } void Session::deleteSourceCallbacks(Source *source) { for (auto k = input_callbacks_.begin(); k != input_callbacks_.end();) { if ( k->second.source_ == source) { if (k->second.model_) delete k->second.model_; if (k->second.reverse_) delete k->second.reverse_; k = input_callbacks_.erase(k); } else ++k; } } void Session::clearSourceCallbacks() { for (auto k = input_callbacks_.begin(); k != input_callbacks_.end(); ) { if (k->second.model_) delete k->second.model_; if (k->second.reverse_) delete k->second.reverse_; k = input_callbacks_.erase(k); } } std::list Session::assignedInputs() { std::list inputs; // fill with list of keys for(const auto& [key, value] : input_callbacks_) { inputs.push_back(key); } // remove duplicates inputs.unique(); return inputs; } bool Session::inputAssigned(uint input) { return input_callbacks_.find(input) != input_callbacks_.end(); } void Session::setInputSynchrony(uint input, Metronome::Synchronicity sync) { input_sync_[input] = sync; } std::vector Session::getInputSynchrony() { return input_sync_; } Metronome::Synchronicity Session::inputSynchrony(uint input) { return input_sync_[input]; }