/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2021 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WIN32 #include #endif // WIN32 namespace { constexpr const char* _loggerCat = "SessionRecording"; constexpr openspace::properties::Property::PropertyInfo RenderPlaybackInfo = { "RenderInfo", "Render Playback Information", "If enabled, information about a currently played back session " "recording is rendering to screen" }; constexpr openspace::properties::Property::PropertyInfo IgnoreRecordedScaleInfo = { "IgnoreRecordedScale", "Ignore Recorded Scale", "If this value is enabled, the scale value from a recording is ignored and the " "computed values are used instead" }; constexpr const bool UsingTimeKeyframes = false; } // namespace #include "sessionrecording_lua.inl" namespace openspace::interaction { ConversionError::ConversionError(std::string msg) : ghoul::RuntimeError(std::move(msg), "conversionError") {} SessionRecording::SessionRecording() : properties::PropertyOwner({ "SessionRecording", "Session Recording" }) , _renderPlaybackInformation(RenderPlaybackInfo, false) , _ignoreRecordedScale(IgnoreRecordedScaleInfo, false) {} SessionRecording::SessionRecording(bool isGlobal) : properties::PropertyOwner({ "SessionRecording", "Session Recording" }) , _renderPlaybackInformation(RenderPlaybackInfo, false) , _ignoreRecordedScale(IgnoreRecordedScaleInfo, false) { if (isGlobal) { auto fTask = FactoryManager::ref().factory(); ghoul_assert(fTask, "No task factory existed"); fTask->registerClass("ConvertRecFormatTask"); fTask->registerClass("ConvertRecFileVersionTask"); addProperty(_renderPlaybackInformation); addProperty(_ignoreRecordedScale); } } SessionRecording::~SessionRecording() { // NOLINT } void SessionRecording::deinitialize() { stopRecording(); stopPlayback(); } void SessionRecording::setRecordDataFormat(DataMode dataMode) { _recordingDataMode = dataMode; } bool SessionRecording::hasFileExtension(std::string filename, std::string extension) { if (filename.length() <= extension.length()) { return false; } else { return (filename.substr(filename.length() - extension.length()) == extension); } } bool SessionRecording::isPath(std::string& filename) { size_t unixDelimiter = filename.find("/"); size_t windowsDelimiter = filename.find("\\"); return (unixDelimiter != std::string::npos || windowsDelimiter != std::string::npos); } void SessionRecording::removeTrailingPathSlashes(std::string& filename) { while (filename.substr(filename.length() - 1, 1) == "/") { filename.pop_back(); } while (filename.substr(filename.length() - 1, 1) == "\\") { filename.pop_back(); } } void SessionRecording::extractFilenameFromPath(std::string& filename) { size_t unixDelimiter = filename.find_last_of("/"); if (unixDelimiter != std::string::npos) filename = filename.substr(unixDelimiter + 1); size_t windowsDelimiter = filename.find_last_of("\\"); if (windowsDelimiter != std::string::npos) filename = filename.substr(windowsDelimiter + 1); } bool SessionRecording::handleRecordingFile(std::string filenameIn) { if (isPath(filenameIn)) { LERROR("Recording filename must not contain path (/) elements"); return false; } if (_recordingDataMode == DataMode::Binary) { if (hasFileExtension(filenameIn, FileExtensionAscii)) { LERROR("Specified filename for binary recording has ascii file extension"); return false; } else if (!hasFileExtension(filenameIn, FileExtensionBinary)) { filenameIn += FileExtensionBinary; } } else if (_recordingDataMode == DataMode::Ascii) { if (hasFileExtension(filenameIn, FileExtensionBinary)) { LERROR("Specified filename for ascii recording has binary file extension"); return false; } else if (!hasFileExtension(filenameIn, FileExtensionAscii)) { filenameIn += FileExtensionAscii; } } std::filesystem::path absFilename = absPath("${RECORDINGS}/" + filenameIn); if (std::filesystem::is_regular_file(absFilename)) { LERROR(fmt::format( "Unable to start recording; file {} already exists.", absFilename )); return false; } if (_recordingDataMode == DataMode::Binary) { _recordFile.open(absFilename, std::ios::binary); } else { _recordFile.open(absFilename); } if (!_recordFile.is_open() || !_recordFile.good()) { LERROR(fmt::format("Unable to open file {} for keyframe recording", absFilename)); return false; } return true; } bool SessionRecording::startRecording(const std::string& filename) { _timeline.clear(); if (_state == SessionState::Recording) { LERROR("Unable to start recording while already in recording mode"); return false; } else if (_state == SessionState::Playback || _state == SessionState::PlaybackPaused) { LERROR("Unable to start recording while in session playback mode"); return false; } if (!std::filesystem::is_directory(absPath("${RECORDINGS}"))) { std::filesystem::create_directories(absPath("${RECORDINGS}")); } bool recordingFileOK = handleRecordingFile(filename); if (recordingFileOK) { _state = SessionState::Recording; _playbackActive_camera = false; _playbackActive_time = false; _playbackActive_script = false; _propertyBaselinesSaved.clear(); _keyframesSavePropertiesBaseline_scripts.clear(); _keyframesSavePropertiesBaseline_timeline.clear(); _recordingEntryNum = 1; _recordFile << FileHeaderTitle; _recordFile.write(FileHeaderVersion, FileHeaderVersionLength); if (_recordingDataMode == DataMode::Binary) { _recordFile << DataFormatBinaryTag; } else { _recordFile << DataFormatAsciiTag; } _recordFile << '\n'; _timestampRecordStarted = global::windowDelegate->applicationTime(); //Record the current delta time as the first property to save in the file. //This needs to be saved as a baseline whether or not it changes during recording. _timestamps3RecordStarted = { _timestampRecordStarted, 0.0, global::timeManager->time().j2000Seconds() }; recordCurrentTimePauseState(); recordCurrentTimeRate(); LINFO("Session recording started"); } return recordingFileOK; } void SessionRecording::recordCurrentTimePauseState() { bool isPaused = global::timeManager->isPaused(); std::string initialTimePausedCommand = "openspace.time.setPause(" + std::string(isPaused ? "true" : "false") + ")"; saveScriptKeyframeToPropertiesBaseline(initialTimePausedCommand); } void SessionRecording::recordCurrentTimeRate() { std::string initialTimeRateCommand = fmt::format( "openspace.time.setDeltaTime({})", global::timeManager->targetDeltaTime() ); saveScriptKeyframeToPropertiesBaseline(initialTimeRateCommand); } void SessionRecording::stopRecording() { if (_state == SessionState::Recording) { //Add all property baseline scripts to the beginning of the recording file datamessagestructures::ScriptMessage smTmp; for (timelineEntry initPropScripts : _keyframesSavePropertiesBaseline_timeline) { if (initPropScripts.keyframeType == RecordedType::Script) { smTmp._script = _keyframesSavePropertiesBaseline_scripts [initPropScripts.idxIntoKeyframeTypeArray]; saveSingleKeyframeScript( smTmp, _timestamps3RecordStarted, _recordingDataMode, _recordFile, _keyframeBuffer ); } } for (timelineEntry entry : _timeline) { switch (entry.keyframeType) { case RecordedType::Camera: { interaction::KeyframeNavigator::CameraPose kf = _keyframesCamera[entry.idxIntoKeyframeTypeArray]; glm::dquat tmpRotation( std::move(kf.rotation.w), std::move(kf.rotation.x), std::move(kf.rotation.y), std::move(kf.rotation.z) ); datamessagestructures::CameraKeyframe kfMsg( std::move(kf.position), std::move(tmpRotation), std::move(kf.focusNode), std::move(kf.followFocusNodeRotation), std::move(kf.scale) ); saveSingleKeyframeCamera( kfMsg, entry.t3stamps, _recordingDataMode, _recordFile, _keyframeBuffer ); break; } case RecordedType::Time: { datamessagestructures::TimeKeyframe tf = _keyframesTime[entry.idxIntoKeyframeTypeArray]; saveSingleKeyframeTime( tf, entry.t3stamps, _recordingDataMode, _recordFile, _keyframeBuffer ); break; } case RecordedType::Script: { smTmp._script = _keyframesScript[entry.idxIntoKeyframeTypeArray]; saveSingleKeyframeScript( smTmp, entry.t3stamps, _recordingDataMode, _recordFile, _keyframeBuffer ); break; } default: { break; } } } _state = SessionState::Idle; LINFO("Session recording stopped"); } // Close the recording file _recordFile.close(); _cleanupNeeded = true; } bool SessionRecording::startPlayback(std::string& filename, KeyframeTimeRef timeMode, bool forceSimTimeAtStart, bool loop) { std::string absFilename; // Run through conversion in case file is older. Does nothing if the file format // is up-to-date filename = convertFile(filename); if (std::filesystem::is_regular_file(filename)) { absFilename = filename; } else { absFilename = absPath("${RECORDINGS}/" + filename).string(); } if (_state == SessionState::Recording) { LERROR("Unable to start playback while in session recording mode"); return false; } else if (_state == SessionState::Playback || _state == SessionState::PlaybackPaused) { LERROR("Unable to start new playback while in session playback mode"); return false; } if (!std::filesystem::is_regular_file(absFilename)) { LERROR("Cannot find the specified playback file."); cleanUpPlayback(); return false; } _playbackLineNum = 1; _playbackFilename = absFilename; _playbackLoopMode = loop; // Open in ASCII first _playbackFile.open(_playbackFilename, std::ifstream::in); // Read header std::string readBackHeaderString = readHeaderElement( _playbackFile, FileHeaderTitle.length() ); if (readBackHeaderString != FileHeaderTitle) { LERROR("Specified playback file does not contain expected header."); cleanUpPlayback(); return false; } readHeaderElement(_playbackFile, FileHeaderVersionLength); std::string readDataMode = readHeaderElement(_playbackFile, 1); if (readDataMode[0] == DataFormatAsciiTag) { _recordingDataMode = DataMode::Ascii; } else if (readDataMode[0] == DataFormatBinaryTag) { _recordingDataMode = DataMode::Binary; } else { LERROR("Unknown data type in header (should be Ascii or Binary)"); cleanUpPlayback(); } // throwaway newline character readHeaderElement(_playbackFile, 1); if (_recordingDataMode == DataMode::Binary) { //Close & re-open the file, starting from the beginning, and do dummy read // past the header, version, and data type _playbackFile.close(); _playbackFile.open(_playbackFilename, std::ifstream::in | std::ios::binary); size_t headerSize = FileHeaderTitle.length() + FileHeaderVersionLength + sizeof(DataFormatBinaryTag) + sizeof('\n'); std::vector hBuffer; hBuffer.resize(headerSize); _playbackFile.read(hBuffer.data(), headerSize); } if (!_playbackFile.is_open() || !_playbackFile.good()) { LERROR(fmt::format( "Unable to open file {} for keyframe playback", absFilename.c_str() )); stopPlayback(); cleanUpPlayback(); return false; } _saveRendering_isFirstFrame = true; //Set time reference mode _playbackForceSimTimeAtStart = forceSimTimeAtStart; double now = global::windowDelegate->applicationTime(); _playbackTimeReferenceMode = timeMode; initializePlayback_time(now); global::scriptScheduler->setTimeReferenceMode(timeMode); _loadedNodes.clear(); populateListofLoadedSceneGraphNodes(); if (!playbackAddEntriesToTimeline()) { cleanUpPlayback(); return false; } initializePlayback_modeFlags(); if (!initializePlayback_timeline()) { cleanUpPlayback(); return false; } LINFO(fmt::format( "Playback session started: ({:8.3f},0.0,{:13.3f}) with {}/{}/{} entries, " "forceTime={}", now, _timestampPlaybackStarted_simulation, _keyframesCamera.size(), _keyframesTime.size(), _keyframesScript.size(), (_playbackForceSimTimeAtStart ? 1 : 0) )); initializePlayback_triggerStart(); return true; } void SessionRecording::initializePlayback_time(double now) { using namespace std::chrono; _timestampPlaybackStarted_application = now; _timestampPlaybackStarted_simulation = global::timeManager->time().j2000Seconds(); _timestampApplicationStarted_simulation = _timestampPlaybackStarted_simulation - now; _saveRenderingCurrentRecordedTime_interpolation = steady_clock::now(); _saveRenderingClockInterpolation_countsPerSec = system_clock::duration::period::den / system_clock::duration::period::num; _playbackPauseOffset = 0.0; global::navigationHandler->keyframeNavigator().setTimeReferenceMode( _playbackTimeReferenceMode, now); } void SessionRecording::initializePlayback_modeFlags() { _playbackActive_camera = true; _playbackActive_script = true; if (UsingTimeKeyframes) { _playbackActive_time = true; } _hasHitEndOfCameraKeyframes = false; } bool SessionRecording::initializePlayback_timeline() { if (!findFirstCameraKeyframeInTimeline()) { return false; } if (_playbackForceSimTimeAtStart) { Timestamps times = _timeline[_idxTimeline_cameraFirstInTimeline].t3stamps; global::timeManager->setTimeNextFrame(Time(times.timeSim)); _saveRenderingCurrentRecordedTime = times.timeRec; } _idxTimeline_nonCamera = 0; _idxTime = 0; _idxScript = 0; _idxTimeline_cameraPtrNext = 0; _idxTimeline_cameraPtrPrev = 0; return true; } void SessionRecording::initializePlayback_triggerStart() { global::navigationHandler->triggerPlaybackStart(); global::scriptScheduler->triggerPlaybackStart(); global::timeManager->triggerPlaybackStart(); _state = SessionState::Playback; } bool SessionRecording::isPlaybackPaused() { return (_state == SessionState::PlaybackPaused); } void SessionRecording::setPlaybackPause(bool pause) { if (pause && _state == SessionState::Playback) { _playbackPausedWithinDeltaTimePause = global::timeManager->isPaused(); if (!_playbackPausedWithinDeltaTimePause) { global::timeManager->setPause(true); } _state = SessionState::PlaybackPaused; } else if (!pause && _state == SessionState::PlaybackPaused) { if (!_playbackPausedWithinDeltaTimePause) { global::timeManager->setPause(false); } _state = SessionState::Playback; } } bool SessionRecording::findFirstCameraKeyframeInTimeline() { bool foundCameraKeyframe = false; for (unsigned int i = 0; i < _timeline.size(); i++) { if (doesTimelineEntryContainCamera(i)) { _idxTimeline_cameraFirstInTimeline = i; _idxTimeline_cameraPtrPrev = _idxTimeline_cameraFirstInTimeline; _idxTimeline_cameraPtrNext = _idxTimeline_cameraFirstInTimeline; _cameraFirstInTimeline_timestamp = appropriateTimestamp( _timeline[_idxTimeline_cameraFirstInTimeline].t3stamps); foundCameraKeyframe = true; break; } } if (!foundCameraKeyframe) { signalPlaybackFinishedForComponent(RecordedType::Camera); return true; } else { return checkIfInitialFocusNodeIsLoaded(_idxTimeline_cameraFirstInTimeline); } } void SessionRecording::signalPlaybackFinishedForComponent(RecordedType type) { if (type == RecordedType::Camera) { _playbackActive_camera = false; LINFO("Playback finished signal: camera"); } else if (type == RecordedType::Time) { _playbackActive_time = false; LINFO("Playback finished signal: time"); } else if (type == RecordedType::Script) { _playbackActive_script = false; LINFO("Playback finished signal: script"); } if (!_playbackActive_camera && !_playbackActive_time && !_playbackActive_script) { if (_playbackLoopMode) { //Loop back to the beginning to replay _saveRenderingDuringPlayback = false; initializePlayback_time(global::windowDelegate->applicationTime()); initializePlayback_modeFlags(); initializePlayback_timeline(); initializePlayback_triggerStart(); } else { _state = SessionState::Idle; _cleanupNeeded = true; LINFO("Playback session finished"); } } } void SessionRecording::enableTakeScreenShotDuringPlayback(int fps) { _saveRenderingDuringPlayback = true; _saveRenderingDeltaTime = 1.0 / fps; _saveRenderingDeltaTime_interpolation_usec = std::chrono::microseconds(static_cast(_saveRenderingDeltaTime * 1000000)); } void SessionRecording::disableTakeScreenShotDuringPlayback() { _saveRenderingDuringPlayback = false; } void SessionRecording::stopPlayback() { if (_state == SessionState::Playback || _state == SessionState::PlaybackPaused) { _state = SessionState::Idle; _cleanupNeeded = true; LINFO("Session playback stopped"); } } void SessionRecording::cleanUpPlayback() { global::navigationHandler->stopPlayback(); global::timeManager->stopPlayback(); Camera* camera = global::navigationHandler->camera(); ghoul_assert(camera != nullptr, "Camera must not be nullptr"); Scene* scene = camera->parent()->scene(); if (!_timeline.empty()) { if (_timeline.size() > 0) { unsigned int p = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; if (_keyframesCamera.size() > 0) { const SceneGraphNode* n = scene->sceneGraphNode( _keyframesCamera[p].focusNode); if (n) { global::navigationHandler->orbitalNavigator().setFocusNode( n->identifier()); } } } } global::scriptScheduler->stopPlayback(); _playbackFile.close(); // Clear all timelines and keyframes _timeline.clear(); _keyframesCamera.clear(); _keyframesTime.clear(); _keyframesScript.clear(); _keyframesSavePropertiesBaseline_scripts.clear(); _keyframesSavePropertiesBaseline_timeline.clear(); _propertyBaselinesSaved.clear(); _loadedNodes.clear(); _idxTimeline_nonCamera = 0; _idxTime = 0; _idxScript = 0; _idxTimeline_cameraPtrNext = 0; _idxTimeline_cameraPtrPrev = 0; _hasHitEndOfCameraKeyframes = false; _saveRenderingDuringPlayback = false; _saveRendering_isFirstFrame = true; _playbackPauseOffset = 0.0; _playbackLoopMode = false; _playbackForceSimTimeAtStart = false; _cleanupNeeded = false; } void SessionRecording::writeToFileBuffer(unsigned char* buf, size_t& idx, double src) { const size_t writeSize_bytes = sizeof(double); unsigned char const *p = reinterpret_cast(&src); memcpy((buf + idx), p, writeSize_bytes); idx += writeSize_bytes; } void SessionRecording::writeToFileBuffer(unsigned char* buf, size_t& idx, std::vector& cv) { const size_t writeSize_bytes = cv.size() * sizeof(char); memcpy((buf + idx), cv.data(), writeSize_bytes); idx += writeSize_bytes; } void SessionRecording::writeToFileBuffer(unsigned char* buf, size_t& idx, unsigned char c) { const size_t writeSize_bytes = sizeof(char); buf[idx] = c; idx += writeSize_bytes; } void SessionRecording::writeToFileBuffer(unsigned char* buf, size_t& idx, bool b) { buf[idx] = b ? 1 : 0; idx += sizeof(char); } void SessionRecording::saveStringToFile(const std::string& s, unsigned char* kfBuffer, size_t& idx, std::ofstream& file) { size_t strLen = s.size(); size_t writeSize_bytes = sizeof(size_t); idx = 0; unsigned char const *p = reinterpret_cast(&strLen); memcpy((kfBuffer + idx), p, writeSize_bytes); idx += static_cast(writeSize_bytes); saveKeyframeToFileBinary(kfBuffer, idx, file); file.write(s.c_str(), s.size()); } bool SessionRecording::hasCameraChangedFromPrev( datamessagestructures::CameraKeyframe kfNew) { constexpr const double threshold = 1e-2; bool hasChanged = false; glm::dvec3 positionDiff = kfNew._position - _prevRecordedCameraKeyframe._position; if (glm::length(positionDiff) > threshold) { hasChanged = true; } double rotationDiff = dot(kfNew._rotation, _prevRecordedCameraKeyframe._rotation); if (std::abs(rotationDiff - 1.0) > threshold) { hasChanged = true; } _prevRecordedCameraKeyframe = kfNew; return hasChanged; } SessionRecording::Timestamps SessionRecording::generateCurrentTimestamp3(double kfTime) { return { kfTime, kfTime - _timestampRecordStarted, global::timeManager->time().j2000Seconds() }; } void SessionRecording::saveCameraKeyframeToTimeline() { const SceneGraphNode* an = global::navigationHandler->orbitalNavigator().anchorNode(); if (!an) { return; } // Create a camera keyframe, then call to populate it with current position // & orientation of camera datamessagestructures::CameraKeyframe kf = _externInteract.generateCameraKeyframe(); Timestamps times = generateCurrentTimestamp3(kf._timestamp); interaction::KeyframeNavigator::CameraPose pbFrame(std::move(kf)); addKeyframe(times, pbFrame, _recordingEntryNum++); } void SessionRecording::saveHeaderBinary(Timestamps& times, char type, unsigned char* kfBuffer, size_t& idx) { kfBuffer[idx++] = type; writeToFileBuffer(kfBuffer, idx, times.timeOs); writeToFileBuffer(kfBuffer, idx, times.timeRec); writeToFileBuffer(kfBuffer, idx, times.timeSim); } void SessionRecording::saveHeaderAscii(Timestamps& times, const std::string& type, std::stringstream& line) { line << type << ' '; line << times.timeOs << ' '; line << times.timeRec << ' '; line << std::fixed << std::setprecision(3) << times.timeSim << ' '; } void SessionRecording::saveCameraKeyframeBinary(Timestamps& times, datamessagestructures::CameraKeyframe& kf, unsigned char* kfBuffer, std::ofstream& file) { // Writing to a binary session recording file size_t idx = 0; saveHeaderBinary(times, HeaderCameraBinary, kfBuffer, idx); // Writing to internal buffer, and then to file, for performance reasons std::vector writeBuffer; kf.serialize(writeBuffer); writeToFileBuffer(kfBuffer, idx, writeBuffer); saveKeyframeToFileBinary(kfBuffer, idx, file); } void SessionRecording::saveCameraKeyframeAscii(Timestamps& times, datamessagestructures::CameraKeyframe& kf, std::ofstream& file) { std::stringstream keyframeLine = std::stringstream(); saveHeaderAscii(times, HeaderCameraAscii, keyframeLine); kf.write(keyframeLine); saveKeyframeToFile(keyframeLine.str(), file); } void SessionRecording::saveTimeKeyframeToTimeline() { //Create a time keyframe, then call to populate it with current time props datamessagestructures::TimeKeyframe kf = _externInteract.generateTimeKeyframe(); Timestamps times = generateCurrentTimestamp3(kf._timestamp); addKeyframe(times, kf, _recordingEntryNum++); } void SessionRecording::saveTimeKeyframeBinary(Timestamps& times, datamessagestructures::TimeKeyframe& kf, unsigned char* kfBuffer, std::ofstream& file) { size_t idx = 0; saveHeaderBinary(times, HeaderTimeBinary, kfBuffer, idx); std::vector writeBuffer; kf.serialize(writeBuffer); writeToFileBuffer(kfBuffer, idx, writeBuffer); saveKeyframeToFileBinary(kfBuffer, idx, file); } void SessionRecording::saveTimeKeyframeAscii(Timestamps& times, datamessagestructures::TimeKeyframe& kf, std::ofstream& file) { std::stringstream keyframeLine = std::stringstream(); saveHeaderAscii(times, HeaderTimeAscii, keyframeLine); kf.write(keyframeLine); saveKeyframeToFile(keyframeLine.str(), file); } void SessionRecording::saveScriptKeyframeToTimeline(std::string script) { if (doesStartWithSubstring(script, scriptReturnPrefix)) { script = script.substr(scriptReturnPrefix.length()); } for (std::string reject : _scriptRejects) { if (doesStartWithSubstring(script, reject)) { return; } } trimCommandsFromScriptIfFound(script); replaceCommandsFromScriptIfFound(script); datamessagestructures::ScriptMessage sm = _externInteract.generateScriptMessage(script); Timestamps times = generateCurrentTimestamp3(sm._timestamp); addKeyframe(times, sm._script, _playbackLineNum); } bool SessionRecording::doesStartWithSubstring(const std::string& s, const std::string& matchSubstr) { return (s.substr(0, matchSubstr.length()).compare(matchSubstr) == 0); } void SessionRecording::saveScriptKeyframeToPropertiesBaseline(std::string script) { Timestamps times = generateCurrentTimestamp3(global::windowDelegate->applicationTime()); size_t indexIntoScriptKeyframesFromMainTimeline = _keyframesSavePropertiesBaseline_scripts.size(); _keyframesSavePropertiesBaseline_scripts.push_back(std::move(script)); addKeyframeToTimeline( _keyframesSavePropertiesBaseline_timeline, RecordedType::Script, indexIntoScriptKeyframesFromMainTimeline, times, 0 ); } void SessionRecording::trimCommandsFromScriptIfFound(std::string& script) { for (std::string trimSnippet : _scriptsToBeTrimmed) { auto findIdx = script.find(trimSnippet); if (findIdx != std::string::npos) { auto findClosingParens = script.find_first_of(')', findIdx); script.erase(findIdx, findClosingParens + 1); } } } void SessionRecording::replaceCommandsFromScriptIfFound(std::string& script) { for (ScriptSubstringReplace replacementSnippet : _scriptsToBeReplaced) { auto findIdx = script.find(replacementSnippet.substringFound); if (findIdx != std::string::npos) { script.erase(findIdx, replacementSnippet.substringFound.length()); script.insert(findIdx, replacementSnippet.substringReplacement); } } } void SessionRecording::saveScriptKeyframeBinary(Timestamps& times, datamessagestructures::ScriptMessage& sm, unsigned char* smBuffer, std::ofstream& file) { size_t idx = 0; saveHeaderBinary(times, HeaderScriptBinary, smBuffer, idx); // Writing to internal buffer, and then to file, for performance reasons std::vector writeBuffer; sm.serialize(writeBuffer); writeToFileBuffer(smBuffer, idx, writeBuffer); saveKeyframeToFileBinary(smBuffer, idx, file); } void SessionRecording::saveScriptKeyframeAscii(Timestamps& times, datamessagestructures::ScriptMessage& sm, std::ofstream& file) { std::stringstream keyframeLine = std::stringstream(); saveHeaderAscii(times, HeaderScriptAscii, keyframeLine); sm.write(keyframeLine); saveKeyframeToFile(keyframeLine.str(), file); } void SessionRecording::savePropertyBaseline(properties::Property& prop) { std::string propIdentifier = prop.fullyQualifiedIdentifier(); if (isPropertyAllowedForBaseline(propIdentifier)) { bool isPropAlreadySaved = ( std::find( _propertyBaselinesSaved.begin(), _propertyBaselinesSaved.end(), propIdentifier ) != _propertyBaselinesSaved.end() ); if (!isPropAlreadySaved) { std::string initialScriptCommand = fmt::format( "openspace.setPropertyValueSingle(\"{}\", {})", propIdentifier, prop.getStringValue() ); saveScriptKeyframeToPropertiesBaseline(initialScriptCommand); _propertyBaselinesSaved.push_back(propIdentifier); } } } bool SessionRecording::isPropertyAllowedForBaseline(const std::string& propString) { for (std::string reject : _propertyBaselineRejects) { if (doesStartWithSubstring(propString, reject)) { return false; } } return true; } void SessionRecording::preSynchronization() { ZoneScoped if (_state == SessionState::Recording) { saveCameraKeyframeToTimeline(); if (UsingTimeKeyframes) { saveTimeKeyframeToTimeline(); } } else if (_state == SessionState::Playback || _state == SessionState::PlaybackPaused) { moveAheadInTime(); } else if (_cleanupNeeded) { cleanUpPlayback(); } //Handle callback(s) for change in idle/record/playback state if (_state != _lastState) { using K = CallbackHandle; using V = StateChangeCallback; for (const std::pair& it : _stateChangeCallbacks) { it.second(); } } _lastState = _state; } void SessionRecording::render() { ZoneScoped if (!(_renderPlaybackInformation && isPlayingBack())) { return; } constexpr const char* FontName = "Mono"; constexpr const float FontSizeFrameinfo = 32.f; std::shared_ptr font = global::fontManager->font(FontName, FontSizeFrameinfo); glm::vec2 res = global::renderEngine->fontResolution(); glm::vec2 penPosition = glm::vec2( res.x / 2 - 150.f, res.y / 4 ); std::string text1 = std::to_string(currentTime()); ghoul::fontrendering::RenderFont( *font, penPosition, text1, glm::vec4(1.f), ghoul::fontrendering::CrDirection::Down ); std::string text2 = fmt::format( "Scale: {}", global::navigationHandler->camera()->scaling() ); ghoul::fontrendering::RenderFont(*font, penPosition, text2, glm::vec4(1.f)); } bool SessionRecording::isRecording() const { return (_state == SessionState::Recording); } bool SessionRecording::isPlayingBack() const { return (_state == SessionState::Playback || _state == SessionState::PlaybackPaused); } bool SessionRecording::isSavingFramesDuringPlayback() const { return ((_state == SessionState::Playback || _state == SessionState::PlaybackPaused) && _saveRenderingDuringPlayback); } SessionRecording::SessionState SessionRecording::state() const { return _state; } bool SessionRecording::playbackAddEntriesToTimeline() { bool parsingStatusOk = true; if (_recordingDataMode == DataMode::Binary) { unsigned char frameType; while (parsingStatusOk) { frameType = readFromPlayback(_playbackFile); // Check if have reached EOF if (!_playbackFile) { LINFO(fmt::format( "Finished parsing {} entries from playback file {}", _playbackLineNum - 1, _playbackFilename )); break; } if (frameType == HeaderCameraBinary) { parsingStatusOk = playbackCamera(); } else if (frameType == HeaderTimeBinary) { parsingStatusOk = playbackTimeChange(); } else if (frameType == HeaderScriptBinary) { parsingStatusOk = playbackScript(); } else { LERROR(fmt::format( "Unknown frame type {} @ index {} of playback file {}", frameType, _playbackLineNum - 1, _playbackFilename )); parsingStatusOk = false; break; } _playbackLineNum++; } } else { while (parsingStatusOk && std::getline(_playbackFile, _playbackLineParsing)) { _playbackLineNum++; std::istringstream iss(_playbackLineParsing); std::string entryType; if (!(iss >> entryType)) { LERROR(fmt::format( "Error reading entry type @ line {} of playback file {}", _playbackLineNum, _playbackFilename )); break; } if (entryType == HeaderCameraAscii) { parsingStatusOk = playbackCamera(); } else if (entryType == HeaderTimeAscii) { parsingStatusOk = playbackTimeChange(); } else if (entryType == HeaderScriptAscii) { parsingStatusOk = playbackScript(); } else if (entryType.substr(0, 1) == HeaderCommentAscii) { continue; } else { LERROR(fmt::format( "Unknown frame type {} @ line {} of playback file {}", entryType, _playbackLineNum, _playbackFilename )); parsingStatusOk = false; break; } } LINFO(fmt::format( "Finished parsing {} entries from playback file {}", _playbackLineNum, _playbackFilename )); } return parsingStatusOk; } double SessionRecording::appropriateTimestamp(Timestamps t3stamps) { if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { return t3stamps.timeRec; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { return t3stamps.timeSim; } else { return t3stamps.timeOs; } } double SessionRecording::equivalentSimulationTime(double timeOs, double timeRec, double timeSim) { if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { return _timestampPlaybackStarted_simulation + timeRec; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_applicationStart) { return _timestampApplicationStarted_simulation + timeOs; } else { return timeSim; } } double SessionRecording::equivalentApplicationTime(double timeOs, double timeRec, double timeSim) { if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { return _timestampPlaybackStarted_application + timeRec; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { return timeSim - _timestampApplicationStarted_simulation; } else { return timeOs; } } double SessionRecording::currentTime() const { if (isSavingFramesDuringPlayback()) { return _saveRenderingCurrentRecordedTime; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { return (global::windowDelegate->applicationTime() - _timestampPlaybackStarted_application) - _playbackPauseOffset; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { return global::timeManager->time().j2000Seconds(); } else { return global::windowDelegate->applicationTime() - _playbackPauseOffset; } } double SessionRecording::fixedDeltaTimeDuringFrameOutput() const { // Check if renderable in focus is still resolving tile loading // do not adjust time while we are doing this const SceneGraphNode* focusNode = global::navigationHandler->orbitalNavigator().anchorNode(); const Renderable* focusRenderable = focusNode->renderable(); if (!focusRenderable || focusRenderable->renderedWithDesiredData()) { return _saveRenderingDeltaTime; } else { return 0; } } std::chrono::steady_clock::time_point SessionRecording::currentPlaybackInterpolationTime() const { return _saveRenderingCurrentRecordedTime_interpolation; } bool SessionRecording::playbackCamera() { Timestamps times; datamessagestructures::CameraKeyframe kf; bool success = readSingleKeyframeCamera( kf, times, _recordingDataMode, _playbackFile, _playbackLineParsing, _playbackLineNum ); interaction::KeyframeNavigator::CameraPose pbFrame(std::move(kf)); if (success) { success = addKeyframe( {times.timeOs, times.timeRec, times.timeSim}, pbFrame, _playbackLineNum ); } return success; } bool SessionRecording::convertCamera(std::stringstream& inStream, DataMode mode, int lineNum, std::string& inputLine, std::ofstream& outFile, unsigned char* buffer) { Timestamps times; datamessagestructures::CameraKeyframe kf; bool success = readSingleKeyframeCamera( kf, times, mode, reinterpret_cast(inStream), inputLine, lineNum ); if (success) { saveSingleKeyframeCamera( kf, times, mode, outFile, buffer ); } return success; } bool SessionRecording::readSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, Timestamps& times, DataMode mode, std::ifstream& file, std::string& inLine, const int lineNum) { if (mode == DataMode::Binary) { return readCameraKeyframeBinary(times, kf, file, lineNum); } else { return readCameraKeyframeAscii(times, kf, inLine, lineNum); } } void SessionRecording::saveSingleKeyframeCamera(datamessagestructures::CameraKeyframe& kf, Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer) { if (mode == DataMode::Binary) { saveCameraKeyframeBinary(times, kf, buffer, file); } else { saveCameraKeyframeAscii(times, kf, file); } } bool SessionRecording::readCameraKeyframeBinary(Timestamps& times, datamessagestructures::CameraKeyframe& kf, std::ifstream& file, int lineN) { times.timeOs = readFromPlayback(file); times.timeRec = readFromPlayback(file); times.timeSim = readFromPlayback(file); try { kf.read(&file); } catch (std::bad_alloc&) { LERROR(fmt::format( "Allocation error with camera playback from keyframe entry {}", lineN - 1 )); return false; } catch (std::length_error&) { LERROR(fmt::format( "length_error with camera playback from keyframe entry {}", lineN - 1 )); return false; } if (!file) { LINFO(fmt::format( "Error reading camera playback from keyframe entry {}", lineN - 1 )); return false; } return true; } bool SessionRecording::readCameraKeyframeAscii(Timestamps& times, datamessagestructures::CameraKeyframe& kf, std::string currentParsingLine, int lineN) { std::string rotationFollowing; std::string entryType; std::istringstream iss(currentParsingLine); iss >> entryType; iss >> times.timeOs >> times.timeRec >> times.timeSim; kf.read(iss); //ASCII format does not contain trailing timestamp so add it here kf._timestamp = times.timeOs; if (iss.fail() || !iss.eof()) { LERROR(fmt::format("Error parsing camera line {} of playback file", lineN)); return false; } return true; } bool SessionRecording::playbackTimeChange() { Timestamps times; datamessagestructures::TimeKeyframe kf; bool success = readSingleKeyframeTime( kf, times, _recordingDataMode, _playbackFile, _playbackLineParsing, _playbackLineNum ); kf._timestamp = equivalentApplicationTime(times.timeOs, times.timeRec, times.timeSim); kf._time = kf._timestamp + _timestampApplicationStarted_simulation; if (success) { success = addKeyframe( {times.timeOs, times.timeRec, times.timeSim}, kf, _playbackLineNum ); } return success; } bool SessionRecording::convertTimeChange(std::stringstream& inStream, DataMode mode, int lineNum, std::string& inputLine, std::ofstream& outFile, unsigned char* buffer) { Timestamps times; datamessagestructures::TimeKeyframe kf; bool success = readSingleKeyframeTime( kf, times, mode, reinterpret_cast(inStream), inputLine, lineNum ); if (success) { saveSingleKeyframeTime( kf, times, mode, outFile, buffer ); } return success; } bool SessionRecording::readSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, Timestamps& times, DataMode mode, std::ifstream& file, std::string& inLine, const int lineNum) { if (mode == DataMode::Binary) { return readTimeKeyframeBinary(times, kf, file, lineNum); } else { return readTimeKeyframeAscii(times, kf, inLine, lineNum); } } void SessionRecording::saveSingleKeyframeTime(datamessagestructures::TimeKeyframe& kf, Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer) { if (mode == DataMode::Binary) { saveTimeKeyframeBinary(times, kf, buffer, file); } else { saveTimeKeyframeAscii(times, kf, file); } } bool SessionRecording::readTimeKeyframeBinary(Timestamps& times, datamessagestructures::TimeKeyframe& kf, std::ifstream& file, int lineN) { times.timeOs = readFromPlayback(file); times.timeRec = readFromPlayback(file); times.timeSim = readFromPlayback(file); try { kf.read(&file); } catch (std::bad_alloc&) { LERROR(fmt::format( "Allocation error with time playback from keyframe entry {}", lineN - 1 )); return false; } catch (std::length_error&) { LERROR(fmt::format( "length_error with time playback from keyframe entry {}", lineN - 1 )); return false; } if (!file) { LERROR(fmt::format( "Error reading time playback from keyframe entry {}", lineN - 1 )); return false; } return true; } bool SessionRecording::readTimeKeyframeAscii(Timestamps& times, datamessagestructures::TimeKeyframe& kf, std::string currentParsingLine, int lineN) { std::string entryType; std::istringstream iss(currentParsingLine); iss >> entryType; iss >> times.timeOs >> times.timeRec >> times.timeSim; kf.read(iss); if (iss.fail() || !iss.eof()) { LERROR(fmt::format( "Error parsing time line {} of playback file", lineN )); return false; } return true; } std::string SessionRecording::readHeaderElement(std::ifstream& stream, size_t readLen_chars) { std::vector readTemp(readLen_chars); stream.read(&readTemp[0], readLen_chars); return std::string(readTemp.begin(), readTemp.end()); } std::string SessionRecording::readHeaderElement(std::stringstream& stream, size_t readLen_chars) { std::vector readTemp(readLen_chars); stream.read(&readTemp[0], readLen_chars); return std::string(readTemp.begin(), readTemp.end()); } bool SessionRecording::playbackScript() { Timestamps times; datamessagestructures::ScriptMessage kf; bool success = readSingleKeyframeScript( kf, times, _recordingDataMode, _playbackFile, _playbackLineParsing, _playbackLineNum ); success = checkIfScriptUsesScenegraphNode(kf._script); if (success) { success = addKeyframe( {times.timeOs, times.timeRec, times.timeSim}, kf._script, _playbackLineNum ); } return success; } void SessionRecording::populateListofLoadedSceneGraphNodes() { std::vector nodes = global::renderEngine->scene()->allSceneGraphNodes(); for (SceneGraphNode* n : nodes) { _loadedNodes.push_back(n->identifier()); } } bool SessionRecording::checkIfScriptUsesScenegraphNode(std::string s) { if (s.rfind(scriptReturnPrefix, 0) == 0) { s.erase(0, scriptReturnPrefix.length()); } //This works for both setPropertyValue and setPropertyValueSingle if (s.rfind("openspace.setPropertyValue", 0) == 0) { std::string found; if (s.find("(\"") != std::string::npos) { std::string subj = s.substr(s.find("(\"") + 2); checkForScenegraphNodeAccess_Scene(subj, found); checkForScenegraphNodeAccess_Nav(subj, found); if (found.length() > 0) { auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), found); if (it == _loadedNodes.end()) { LERROR(fmt::format( "Playback file requires scenegraph node '{}', which is " "not currently loaded", found )); return false; } } } } return true; } void SessionRecording::checkForScenegraphNodeAccess_Scene(std::string& s, std::string& result) { const std::string scene = "Scene."; auto posScene = s.find(scene); if (posScene != std::string::npos) { auto posDot = s.find(".", posScene + scene.length() + 1); if (posDot > posScene && posDot != std::string::npos) { result = s.substr(posScene + scene.length(), posDot - (posScene + scene.length())); } } } void SessionRecording::checkForScenegraphNodeAccess_Nav(std::string& s, std::string& result) { const std::string nextTerm = "NavigationHandler.OrbitalNavigator."; auto posNav = s.find(nextTerm); if (posNav != std::string::npos) { for (std::string accessName : _navScriptsUsingNodes) { if (s.substr(posNav + nextTerm.length()).rfind(accessName) == 0) { std::string postName = s.substr(posNav + nextTerm.length() + accessName.length() + 2); eraseSpacesFromString(postName); result = getNameFromSurroundingQuotes(postName); } } } } void SessionRecording::eraseSpacesFromString(std::string& s) { s.erase(std::remove_if(s.begin(), s.end(), ::isspace), s.end()); } std::string SessionRecording::getNameFromSurroundingQuotes(std::string& s) { std::string result; char quote = s.at(0); //Handle either ' or " marks if (quote == '\'' || quote == '\"') { size_t quoteCount = std::count(s.begin(), s.end(), quote); //Must be an opening and closing quote char if (quoteCount == 2) { result = s.substr(1, s.rfind(quote) - 1); } } return result; } bool SessionRecording::checkIfInitialFocusNodeIsLoaded(unsigned int camIdx1) { if (_keyframesCamera.size() > 0) { std::string startFocusNode = _keyframesCamera[_timeline[camIdx1].idxIntoKeyframeTypeArray].focusNode; auto it = std::find(_loadedNodes.begin(), _loadedNodes.end(), startFocusNode); if (it == _loadedNodes.end()) { LERROR(fmt::format( "Playback file requires scenegraph node '{}', which is " "not currently loaded", startFocusNode )); return false; } } return true; } bool SessionRecording::convertScript(std::stringstream& inStream, DataMode mode, int lineNum, std::string& inputLine, std::ofstream& outFile, unsigned char* buffer) { Timestamps times; datamessagestructures::ScriptMessage kf; bool success = readSingleKeyframeScript( kf, times, mode, reinterpret_cast(inStream), inputLine, lineNum ); if (success) { saveSingleKeyframeScript( kf, times, mode, outFile, buffer ); } return success; } bool SessionRecording::readSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, Timestamps& times, DataMode mode, std::ifstream& file, std::string& inLine, const int lineNum) { if (mode == DataMode::Binary) { return readScriptKeyframeBinary(times, kf, file, lineNum); } else { return readScriptKeyframeAscii(times, kf, inLine, lineNum); } } void SessionRecording::saveSingleKeyframeScript(datamessagestructures::ScriptMessage& kf, Timestamps& times, DataMode mode, std::ofstream& file, unsigned char* buffer) { if (mode == DataMode::Binary) { saveScriptKeyframeBinary(times, kf, buffer, file); } else { saveScriptKeyframeAscii(times, kf, file); } } bool SessionRecording::readScriptKeyframeBinary(Timestamps& times, datamessagestructures::ScriptMessage& kf, std::ifstream& file, int lineN) { times.timeOs = readFromPlayback(file); times.timeRec = readFromPlayback(file); times.timeSim = readFromPlayback(file); try { kf.read(&file); } catch (std::bad_alloc&) { LERROR(fmt::format( "Allocation error with script playback from keyframe entry {}", lineN - 1 )); return false; } catch (std::length_error&) { LERROR(fmt::format( "length_error with script playback from keyframe entry {}", lineN - 1 )); return false; } if (!file) { LERROR(fmt::format( "Error reading script playback from keyframe entry {}", lineN - 1 )); return false; } return true; } bool SessionRecording::readScriptKeyframeAscii(Timestamps& times, datamessagestructures::ScriptMessage& kf, std::string currentParsingLine, int lineN) { std::string entryType; std::istringstream iss(currentParsingLine); iss >> entryType; iss >> times.timeOs >> times.timeRec >> times.timeSim; kf.read(iss); if (iss.fail()) { LERROR(fmt::format( "Error parsing script line {} of playback file", lineN )); return false; } else if (!iss.eof()) { LERROR(fmt::format( "Did not find an EOL at line {} of playback file", lineN )); return false; } return true; } bool SessionRecording::addKeyframeToTimeline(std::vector& timeline, RecordedType type, size_t indexIntoTypeKeyframes, Timestamps t3stamps, int lineNum) { try { timeline.push_back({ type, static_cast(indexIntoTypeKeyframes), t3stamps }); } catch(...) { LERROR(fmt::format( "Timeline memory allocation error trying to add keyframe {}. " "The playback file may be too large for system memory.", lineNum - 1 )); return false; } return true; } bool SessionRecording::addKeyframe(Timestamps t3stamps, interaction::KeyframeNavigator::CameraPose keyframe, int lineNum) { size_t indexIntoCameraKeyframesFromMainTimeline = _keyframesCamera.size(); _keyframesCamera.push_back(std::move(keyframe)); return addKeyframeToTimeline( _timeline, RecordedType::Camera, indexIntoCameraKeyframesFromMainTimeline, t3stamps, lineNum ); } bool SessionRecording::addKeyframe(Timestamps t3stamps, datamessagestructures::TimeKeyframe keyframe, int lineNum) { size_t indexIntoTimeKeyframesFromMainTimeline = _keyframesTime.size(); _keyframesTime.push_back(std::move(keyframe)); return addKeyframeToTimeline( _timeline, RecordedType::Time, indexIntoTimeKeyframesFromMainTimeline, t3stamps, lineNum ); } bool SessionRecording::addKeyframe(Timestamps t3stamps, std::string scriptToQueue, int lineNum) { size_t indexIntoScriptKeyframesFromMainTimeline = _keyframesScript.size(); _keyframesScript.push_back(std::move(scriptToQueue)); return addKeyframeToTimeline( _timeline, RecordedType::Script, indexIntoScriptKeyframesFromMainTimeline, t3stamps, lineNum ); } void SessionRecording::moveAheadInTime() { using namespace std::chrono; bool paused = global::timeManager->isPaused(); bool playbackPaused = (_state == SessionState::PlaybackPaused); if (playbackPaused) { _playbackPauseOffset += global::windowDelegate->applicationTime() - _previousTime; } _previousTime = global::windowDelegate->applicationTime(); double currTime = currentTime(); lookForNonCameraKeyframesThatHaveComeDue(currTime); updateCameraWithOrWithoutNewKeyframes(currTime); //Unfortunately the first frame is sometimes rendered because globebrowsing reports // that all chunks are rendered when they apparently are not. if (_saveRendering_isFirstFrame) { _saveRendering_isFirstFrame = false; return; } if (isSavingFramesDuringPlayback()) { // Check if renderable in focus is still resolving tile loading // do not adjust time while we are doing this, or take screenshot const SceneGraphNode* focusNode = global::navigationHandler->orbitalNavigator().anchorNode(); const Renderable* focusRenderable = focusNode->renderable(); if (!focusRenderable || focusRenderable->renderedWithDesiredData()) { if (!playbackPaused) { _saveRenderingCurrentRecordedTime_interpolation += _saveRenderingDeltaTime_interpolation_usec; _saveRenderingCurrentRecordedTime += _saveRenderingDeltaTime; global::renderEngine->takeScreenshot(); } } } } void SessionRecording::lookForNonCameraKeyframesThatHaveComeDue(double currTime) { while (isTimeToHandleNextNonCameraKeyframe(currTime)) { if (!processNextNonCameraKeyframeAheadInTime()) { break; } if (++_idxTimeline_nonCamera >= _timeline.size()) { _idxTimeline_nonCamera--; if (_playbackActive_time) { signalPlaybackFinishedForComponent(RecordedType::Time); } if (_playbackActive_script) { signalPlaybackFinishedForComponent(RecordedType::Script); } break; } } } void SessionRecording::updateCameraWithOrWithoutNewKeyframes(double currTime) { if (!_playbackActive_camera) { return; } bool didFindFutureCameraKeyframes = findNextFutureCameraIndex(currTime); bool isPrevAtFirstKeyframe = (_idxTimeline_cameraPtrPrev == _idxTimeline_cameraFirstInTimeline); bool isFirstTimelineCameraKeyframeInFuture = (currTime < _cameraFirstInTimeline_timestamp); if (! (isPrevAtFirstKeyframe && isFirstTimelineCameraKeyframeInFuture)) { processCameraKeyframe(currTime); } if (!didFindFutureCameraKeyframes) { signalPlaybackFinishedForComponent(RecordedType::Camera); } } bool SessionRecording::isTimeToHandleNextNonCameraKeyframe(double currTime) { bool isNonCameraPlaybackActive = (_playbackActive_time || _playbackActive_script); return (currTime > getNextTimestamp()) && isNonCameraPlaybackActive; } bool SessionRecording::findNextFutureCameraIndex(double currTime) { unsigned int seekAheadIndex = _idxTimeline_cameraPtrPrev; while (true) { seekAheadIndex++; if (seekAheadIndex >= static_cast(_timeline.size())) { seekAheadIndex = static_cast(_timeline.size()) - 1; } if (doesTimelineEntryContainCamera(seekAheadIndex)) { unsigned int indexIntoCameraKeyframes = _timeline[seekAheadIndex].idxIntoKeyframeTypeArray; double seekAheadKeyframeTimestamp = appropriateTimestamp(_timeline[seekAheadIndex].t3stamps); if (indexIntoCameraKeyframes >= (_keyframesCamera.size() - 1)) { _hasHitEndOfCameraKeyframes = true; } if (currTime < seekAheadKeyframeTimestamp) { if (seekAheadIndex > _idxTimeline_cameraPtrNext) { _idxTimeline_cameraPtrPrev = _idxTimeline_cameraPtrNext; _idxTimeline_cameraPtrNext = seekAheadIndex; } break; } else { // Force interpolation between consecutive keyframes _idxTimeline_cameraPtrPrev = seekAheadIndex; } } double interpolationUpperBoundTimestamp = appropriateTimestamp(_timeline[_idxTimeline_cameraPtrNext].t3stamps); if ((currTime > interpolationUpperBoundTimestamp) && _hasHitEndOfCameraKeyframes) { _idxTimeline_cameraPtrPrev = _idxTimeline_cameraPtrNext; return false; } if (seekAheadIndex == (_timeline.size() - 1)) { break; } } return true; } bool SessionRecording::doesTimelineEntryContainCamera(unsigned int index) const { return (_timeline[index].keyframeType == RecordedType::Camera); } bool SessionRecording::processNextNonCameraKeyframeAheadInTime() { switch (getNextKeyframeType()) { case RecordedType::Camera: // Just return true since this function no longer handles camera keyframes return true; case RecordedType::Time: _idxTime = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; if (_keyframesTime.empty()) { return false; } LINFO("Time keyframe type"); // TBD: the TimeManager restricts setting time directly return false; case RecordedType::Script: _idxScript = _timeline[_idxTimeline_nonCamera].idxIntoKeyframeTypeArray; return processScriptKeyframe(); default: LERROR(fmt::format( "Bad keyframe type encountered during playback at index {}.", _idxTimeline_nonCamera )); return false; } } //void SessionRecording::moveBackInTime() { } //for future use unsigned int SessionRecording::findIndexOfLastCameraKeyframeInTimeline() { unsigned int i = static_cast(_timeline.size()) - 1; for (; i > 0; i--) { if (_timeline[i].keyframeType == RecordedType::Camera) { break; } } return i; } bool SessionRecording::processCameraKeyframe(double now) { interaction::KeyframeNavigator::CameraPose nextPose; interaction::KeyframeNavigator::CameraPose prevPose; unsigned int prevIdx; unsigned int nextIdx; if (!_playbackActive_camera) { return false; } else if (_keyframesCamera.empty()) { return false; } else { prevIdx = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; prevPose = _keyframesCamera[prevIdx]; nextIdx = _timeline[_idxTimeline_cameraPtrNext].idxIntoKeyframeTypeArray; nextPose = _keyframesCamera[nextIdx]; } // getPrevTimestamp(); double prevTime = appropriateTimestamp(_timeline[_idxTimeline_cameraPtrPrev].t3stamps); // getNextTimestamp(); double nextTime = appropriateTimestamp(_timeline[_idxTimeline_cameraPtrNext].t3stamps); double t; if ((nextTime - prevTime) < 1e-7) { t = 0; } else { t = (now - prevTime) / (nextTime - prevTime); } #ifdef INTERPOLATION_DEBUG_PRINT LINFOC("prev", std::to_string(prevTime)); LINFOC("now", std::to_string(prevTime + t)); LINFOC("next", std::to_string(nextTime)); #endif // Need to activly update the focusNode position of the camera in relation to // the rendered objects will be unstable and actually incorrect Camera* camera = global::navigationHandler->camera(); Scene* scene = camera->parent()->scene(); const SceneGraphNode* n = scene->sceneGraphNode(_keyframesCamera[prevIdx].focusNode); if (n) { global::navigationHandler->orbitalNavigator().setFocusNode(n->identifier()); } return interaction::KeyframeNavigator::updateCamera( global::navigationHandler->camera(), prevPose, nextPose, t, _ignoreRecordedScale ); } bool SessionRecording::processScriptKeyframe() { if (!_playbackActive_script) { return false; } else if (_keyframesScript.empty()) { return false; } else { std::string nextScript = nextKeyframeObj( _idxScript, _keyframesScript, ([this]() { signalPlaybackFinishedForComponent(RecordedType::Script); }) ); global::scriptEngine->queueScript( nextScript, scripting::ScriptEngine::RemoteScripting::Yes ); } return true; } double SessionRecording::getNextTimestamp() { if (_timeline.empty()) { return 0.0; } else if (_idxTimeline_nonCamera < _timeline.size()) { return appropriateTimestamp(_timeline[_idxTimeline_nonCamera].t3stamps); } else { return appropriateTimestamp(_timeline.back().t3stamps); } } double SessionRecording::getPrevTimestamp() { if (_timeline.empty()) { return 0.0; } else if (_idxTimeline_nonCamera == 0) { return appropriateTimestamp(_timeline.front().t3stamps); } else if (_idxTimeline_nonCamera < _timeline.size()) { return appropriateTimestamp(_timeline[_idxTimeline_nonCamera - 1].t3stamps); } else { return appropriateTimestamp(_timeline.back().t3stamps); } } SessionRecording::RecordedType SessionRecording::getNextKeyframeType() { if (_timeline.empty()) { return RecordedType::Invalid; } else if (_idxTimeline_nonCamera < _timeline.size()) { return _timeline[_idxTimeline_nonCamera].keyframeType; } else { return _timeline.back().keyframeType; } } SessionRecording::RecordedType SessionRecording::getPrevKeyframeType() { if (_timeline.empty()) { return RecordedType::Invalid; } else if (_idxTimeline_nonCamera < _timeline.size()) { if (_idxTimeline_nonCamera > 0) { return _timeline[_idxTimeline_nonCamera - 1].keyframeType; } else { return _timeline.front().keyframeType; } } else { return _timeline.back().keyframeType; } } void SessionRecording::saveKeyframeToFileBinary(unsigned char* buffer, size_t size, std::ofstream& file) { file.write(reinterpret_cast(buffer), size); } void SessionRecording::saveKeyframeToFile(std::string entry, std::ofstream& file) { file << std::move(entry) << std::endl; } SessionRecording::CallbackHandle SessionRecording::addStateChangeCallback( StateChangeCallback cb) { CallbackHandle handle = _nextCallbackHandle++; _stateChangeCallbacks.emplace_back(handle, std::move(cb)); return handle; } void SessionRecording::removeStateChangeCallback(CallbackHandle handle) { const auto it = std::find_if( _stateChangeCallbacks.begin(), _stateChangeCallbacks.end(), [handle](const std::pair>& cb) { return cb.first == handle; } ); ghoul_assert( it != _stateChangeCallbacks.end(), "handle must be a valid callback handle" ); _stateChangeCallbacks.erase(it); } std::vector SessionRecording::playbackList() const { const std::filesystem::path path = absPath("${RECORDINGS}"); std::vector fileList; if (std::filesystem::is_directory(path)) { namespace fs = std::filesystem; for (const fs::directory_entry& e : fs::directory_iterator(path)) { if (!e.is_regular_file()) { continue; } // Remove path and keep only the filename const std::string filename = e.path().filename().string(); #ifdef WIN32 DWORD attributes = GetFileAttributes(e.path().string().c_str()); bool isHidden = attributes & FILE_ATTRIBUTE_HIDDEN; #else bool isHidden = filename.find(".") == 0; #endif // WIN32 if (!isHidden) { // Don't add hidden files fileList.push_back(filename); } } } return fileList; } void SessionRecording::readPlaybackHeader_stream(std::stringstream& conversionInStream, std::string& version, DataMode& mode) { // Read header std::string readBackHeaderString = readHeaderElement( conversionInStream, FileHeaderTitle.length() ); if (readBackHeaderString != FileHeaderTitle) { throw ConversionError("File to convert does not contain expected header."); } version = readHeaderElement(conversionInStream, FileHeaderVersionLength); std::string readDataMode = readHeaderElement(conversionInStream, 1); if (readDataMode[0] == DataFormatAsciiTag) { mode = DataMode::Ascii; } else if (readDataMode[0] == DataFormatBinaryTag) { mode = DataMode::Binary; } else { throw ConversionError("Unknown data type in header (needs Ascii or Binary)"); } //Read to throw out newline at end of header readHeaderElement(conversionInStream, 1); } SessionRecording::DataMode SessionRecording::readModeFromHeader(std::string filename) { DataMode mode; std::ifstream inputFile; // Open in ASCII first inputFile.open(filename, std::ifstream::in); // Read header std::string readBackHeaderString = readHeaderElement( inputFile, FileHeaderTitle.length() ); if (readBackHeaderString != FileHeaderTitle) { LERROR("Specified playback file does not contain expected header."); } readHeaderElement(inputFile, FileHeaderVersionLength); std::string readDataMode = readHeaderElement(inputFile, 1); if (readDataMode[0] == DataFormatAsciiTag) { mode = DataMode::Ascii; } else if (readDataMode[0] == DataFormatBinaryTag) { mode = DataMode::Binary; } else { throw ConversionError("Unknown data type in header (should be Ascii or Binary)"); } return mode; } void SessionRecording::readFileIntoStringStream(std::string filename, std::ifstream& inputFstream, std::stringstream& stream) { std::filesystem::path conversionInFilename = absPath("${RECORDINGS}/" + filename); if (!std::filesystem::is_regular_file(conversionInFilename)) { throw ConversionError(fmt::format( "Cannot find the specified playback file {} to convert", conversionInFilename )); } DataMode mode = readModeFromHeader(conversionInFilename.string()); stream.str(""); stream.clear(); inputFstream.close(); if (mode == DataMode::Binary) { inputFstream.open(conversionInFilename, std::ifstream::in | std::ios::binary); } else { inputFstream.open(conversionInFilename, std::ifstream::in); } stream << inputFstream.rdbuf(); if (!inputFstream.is_open() || !inputFstream.good()) { throw ConversionError(fmt::format( "Unable to open file {} for conversion", filename.c_str() )); } inputFstream.close(); } std::string SessionRecording::convertFile(std::string filename, int depth) { std::string conversionOutFilename = filename; std::ifstream conversionInFile; std::stringstream conversionInStream; if (depth >= _maximumRecursionDepth) { LERROR("Runaway recursion in session recording conversion of file version."); exit(EXIT_FAILURE); } std::string newFilename = filename; try { readFileIntoStringStream(filename, conversionInFile, conversionInStream); DataMode mode; std::string fileVersion; readPlaybackHeader_stream( conversionInStream, fileVersion, mode ); int conversionLineNum = 1; //If this instance of the SessionRecording class isn't the instance with the // correct version of the file to be converted, then call getLegacy() to recurse // to the next level down in the legacy subclasses until we get the right // version, then proceed with conversion from there. if (fileVersion.compare(fileFormatVersion()) != 0) { //conversionInStream.seekg(conversionInStream.beg); newFilename = getLegacyConversionResult(filename, depth + 1); removeTrailingPathSlashes(newFilename); if (isPath(newFilename)) { extractFilenameFromPath(newFilename); } if (filename == newFilename) { return filename; } readFileIntoStringStream(newFilename, conversionInFile, conversionInStream); readPlaybackHeader_stream( conversionInStream, fileVersion, mode ); } if (depth != 0) { conversionOutFilename = determineConversionOutFilename(filename, mode); LINFO(fmt::format( "Starting conversion on rec file {}, version {} in {} mode. " "Writing result to {}.", newFilename, fileVersion, (mode == DataMode::Ascii) ? "ascii" : "binary", conversionOutFilename )); std::ofstream conversionOutFile; if (mode == DataMode::Binary) { conversionOutFile.open(conversionOutFilename, std::ios::binary); } else { conversionOutFile.open(conversionOutFilename); } if (!conversionOutFile.is_open() || !conversionOutFile.good()) { LERROR(fmt::format( "Unable to open file {} for conversion result", conversionOutFilename.c_str() )); return ""; } conversionOutFile << FileHeaderTitle; conversionOutFile.write( targetFileFormatVersion().c_str(), FileHeaderVersionLength ); if (mode == DataMode::Binary) { conversionOutFile << DataFormatBinaryTag; } else { conversionOutFile << DataFormatAsciiTag; } conversionOutFile << '\n'; convertEntries( newFilename, conversionInStream, mode, conversionLineNum, conversionOutFile ); conversionOutFile.close(); } conversionInFile.close(); } catch (ConversionError& c) { LERROR(c.message); } if (depth == 0) { return newFilename; } else { return conversionOutFilename; } } bool SessionRecording::convertEntries(std::string& inFilename, std::stringstream& inStream, DataMode mode, int lineNum, std::ofstream& outFile) { bool conversionStatusOk = true; std::string lineParsing; if (mode == DataMode::Binary) { unsigned char frameType; while (conversionStatusOk) { frameType = readFromPlayback(inStream); // Check if have reached EOF if (!inStream) { LINFO(fmt::format( "Finished converting {} entries from playback file {}", lineNum - 1, inFilename )); break; } if (frameType == HeaderCameraBinary) { conversionStatusOk = convertCamera( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } else if (frameType == HeaderTimeBinary) { conversionStatusOk = convertTimeChange( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } else if (frameType == HeaderScriptBinary) { try { conversionStatusOk = convertScript( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } catch (ConversionError& c) { LERROR(c.message); conversionStatusOk = false; } } else { LERROR(fmt::format( "Unknown frame type {} @ index {} of conversion file {}", frameType, lineNum - 1, inFilename )); conversionStatusOk = false; } lineNum++; } } else { while (conversionStatusOk && std::getline(inStream, lineParsing)) { lineNum++; std::istringstream iss(lineParsing); std::string entryType; if (!(iss >> entryType)) { LERROR(fmt::format( "Error reading entry type @ line {} of conversion file {}", lineNum, inFilename )); break; } if (entryType == HeaderCameraAscii) { conversionStatusOk = convertCamera( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } else if (entryType == HeaderTimeAscii) { conversionStatusOk = convertTimeChange( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } else if (entryType == HeaderScriptAscii) { try { conversionStatusOk = convertScript( inStream, mode, lineNum, lineParsing, outFile, _keyframeBuffer ); } catch (ConversionError& c) { LERROR(c.message); conversionStatusOk = false; } } else if (entryType.substr(0, 1) == HeaderCommentAscii) { continue; } else { LERROR(fmt::format( "Unknown frame type {} @ line {} of conversion file {}", entryType, lineNum, inFilename )); conversionStatusOk = false; } } LINFO(fmt::format( "Finished parsing {} entries from conversion file {}", lineNum, inFilename )); } return conversionStatusOk; } std::string SessionRecording::getLegacyConversionResult(std::string filename, int depth) { SessionRecording_legacy_0085 legacy; return legacy.convertFile(filename, depth); } std::string SessionRecording_legacy_0085::getLegacyConversionResult(std::string filename, int) { //This method is overriden in each legacy subclass, but does nothing in this instance // as the oldest supported legacy version. LERROR(fmt::format( "Version 00.85 is the oldest supported legacy file format; no conversion " "can be made. It is possible that file {} has a corrupted header or an invalid " "file format version number.", filename )); return filename; } std::string SessionRecording::fileFormatVersion() { return std::string(FileHeaderVersion); } std::string SessionRecording::targetFileFormatVersion() { return std::string(FileHeaderVersion); } std::string SessionRecording::determineConversionOutFilename(const std::string filename, DataMode mode) { std::string filenameSansExtension = filename; std::string fileExtension = (mode == DataMode::Binary) ? FileExtensionBinary : FileExtensionAscii; if (filename.find_last_of(".") != std::string::npos) { filenameSansExtension = filename.substr(0, filename.find_last_of(".")); } filenameSansExtension += "_" + fileFormatVersion() + "-" + targetFileFormatVersion(); return absPath("${RECORDINGS}/" + filenameSansExtension + fileExtension).string(); } bool SessionRecording_legacy_0085::convertScript(std::stringstream& inStream, DataMode mode, int lineNum, std::string& inputLine, std::ofstream& outFile, unsigned char* buffer) { Timestamps times; ScriptMessage_legacy_0085 kf; bool success = readSingleKeyframeScript( kf, times, mode, reinterpret_cast(inStream), inputLine, lineNum ); if (success) { saveSingleKeyframeScript( kf, times, mode, outFile, buffer ); } return success; } scripting::LuaLibrary SessionRecording::luaLibrary() { return { "sessionRecording", { { "startRecording", &luascriptfunctions::startRecording, {}, "string", "Starts a recording session. The string argument is the filename used " "for the file where the recorded keyframes are saved. " "The file data format is binary." }, { "startRecordingAscii", &luascriptfunctions::startRecordingAscii, {}, "string", "Starts a recording session. The string argument is the filename used " "for the file where the recorded keyframes are saved. " "The file data format is ASCII." }, { "stopRecording", &luascriptfunctions::stopRecording, {}, "void", "Stops a recording session" }, { "startPlayback", &luascriptfunctions::startPlaybackDefault, {}, "string [, bool]", "Starts a playback session with keyframe times that are relative to " "the time since the recording was started (the same relative time " "applies to the playback). When playback starts, the simulation time " "is automatically set to what it was at recording time. The string " "argument is the filename to pull playback keyframes from (the file " "path is relative to the RECORDINGS variable specified in the config " "file). If a second input value of true is given, then playback will " "continually loop until it is manually stopped." }, { "startPlaybackApplicationTime", &luascriptfunctions::startPlaybackApplicationTime, {}, "string", "Starts a playback session with keyframe times that are relative to " "application time (seconds since OpenSpace application started). " "The string argument is the filename to pull playback keyframes from " "(the file path is relative to the RECORDINGS variable specified in the " "config file)." }, { "startPlaybackRecordedTime", &luascriptfunctions::startPlaybackRecordedTime, {}, "string [, bool]", "Starts a playback session with keyframe times that are relative to " "the time since the recording was started (the same relative time " "applies to the playback). The string argument is the filename to pull " "playback keyframes from (the file path is relative to the RECORDINGS " "variable specified in the config file). If a second input value of " "true is given, then playback will continually loop until it is " "manually stopped." }, { "startPlaybackSimulationTime", &luascriptfunctions::startPlaybackSimulationTime, {}, "string", "Starts a playback session with keyframe times that are relative to " "the simulated date & time. The string argument is the filename to pull " "playback keyframes from (the file path is relative to the RECORDINGS " "variable specified in the config file)." }, { "stopPlayback", &luascriptfunctions::stopPlayback, {}, "void", "Stops a playback session before playback of all keyframes is complete" }, { "enableTakeScreenShotDuringPlayback", &luascriptfunctions::enableTakeScreenShotDuringPlayback, {}, "[int]", "Enables that rendered frames should be saved during playback. The " "parameter determines the number of frames that are exported per second " "if this value is not provided, 60 frames per second will be exported." }, { "disableTakeScreenShotDuringPlayback", &luascriptfunctions::disableTakeScreenShotDuringPlayback, {}, "void", "Used to disable that renderings are saved during playback" }, { "fileFormatConversion", &luascriptfunctions::fileFormatConversion, {}, "string", "Performs a conversion of the specified file to the most most recent " "file format, creating a copy of the recording file." }, { "setPlaybackPause", &luascriptfunctions::setPlaybackPause, {}, "bool", "Pauses or resumes the playback progression through keyframes" }, { "togglePlaybackPause", &luascriptfunctions::togglePlaybackPause, {}, "", "Toggles the pause function, i.e. temporarily setting the delta time to 0" " and restoring it afterwards" }, { "isPlayingBack", & luascriptfunctions::isPlayingBack, {}, "", "Returns true if session recording is currently playing back a recording" } } }; } } // namespace openspace::interaction