/***************************************************************************************** * * * 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 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 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) {} SessionRecording::SessionRecording(bool isGlobal) : properties::PropertyOwner({ "SessionRecording", "Session Recording" }) , _renderPlaybackInformation(RenderPlaybackInfo, false) { if (isGlobal) { auto fTask = FactoryManager::ref().factory(); ghoul_assert(fTask, "No task factory existed"); fTask->registerClass("ConvertRecFormatTask"); fTask->registerClass("ConvertRecFileVersionTask"); addProperty(_renderPlaybackInformation); } } 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::string absFilename = absPath("${RECORDINGS}/" + filenameIn); if (FileSys.fileExists(absFilename)) { LERROR(fmt::format( "Unable to start recording; file {} already exists.", absFilename.c_str() )); 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.c_str() )); return false; } return true; } bool SessionRecording::startRecording(const std::string& filename) { if (_state == SessionState::Recording) { LERROR("Unable to start recording while already in recording mode"); return false; } else if (_state == SessionState::Playback) { LERROR("Unable to start recording while in session playback mode"); return false; } if (!FileSys.directoryExists(absPath("${RECORDINGS}"))) { FileSys.createDirectory( absPath("${RECORDINGS}"), ghoul::filesystem::FileSystem::Recursive::Yes ); } bool recordingFileOK = handleRecordingFile(filename); if (recordingFileOK) { _state = SessionState::Recording; _playbackActive_camera = false; _playbackActive_time = false; _playbackActive_script = false; _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 so this is preserved in recording double currentDeltaTime = global::timeManager->deltaTime(); std::string scriptCommandForInitializingDeltaTime = "openspace.time.setDeltaTime(" + std::to_string(currentDeltaTime) + ")"; saveScriptKeyframe(scriptCommandForInitializingDeltaTime); LINFO("Session recording started"); } return recordingFileOK; } void SessionRecording::stopRecording() { if (_state == SessionState::Recording) { _state = SessionState::Idle; LINFO("Session recording stopped"); } // Close the recording file _recordFile.close(); } bool SessionRecording::startPlayback(std::string& filename, KeyframeTimeRef timeMode, bool forceSimTimeAtStart) { if (isPath(filename)) { LERROR("Playback filename must not contain path (/) elements"); return false; } 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 (FileSys.fileExists(filename)) { absFilename = filename; } else { absFilename = absPath("${RECORDINGS}/" + filename); } if (_state == SessionState::Recording) { LERROR("Unable to start playback while in session recording mode"); return false; } else if (_state == SessionState::Playback) { LERROR("Unable to start new playback while in session playback mode"); return false; } if (!FileSys.fileExists(absFilename)) { LERROR("Cannot find the specified playback file."); cleanUpPlayback(); return false; } _playbackLineNum = 1; _playbackFilename = absFilename; // 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; } //Set time reference mode double now = global::windowDelegate->applicationTime(); _timestampPlaybackStarted_application = now; _timestampPlaybackStarted_simulation = global::timeManager->time().j2000Seconds(); _timestampApplicationStarted_simulation = _timestampPlaybackStarted_simulation - now; _playbackTimeReferenceMode = timeMode; //Set playback flags to true for all modes _playbackActive_camera = true; _playbackActive_script = true; if (UsingTimeKeyframes) { _playbackActive_time = true; } global::navigationHandler->keyframeNavigator().setTimeReferenceMode(timeMode, now); global::scriptScheduler->setTimeReferenceMode(timeMode); _setSimulationTimeWithNextCameraKeyframe = forceSimTimeAtStart; if (!playbackAddEntriesToTimeline()) { cleanUpPlayback(); return false; } _hasHitEndOfCameraKeyframes = false; findFirstCameraKeyframeInTimeline(); LINFO(fmt::format( "Playback session started: ({:8.3f},0.0,{:13.3f}) with {}/{}/{} entries, " "forceTime={}", now, _timestampPlaybackStarted_simulation, _keyframesCamera.size(), _keyframesTime.size(), _keyframesScript.size(), (forceSimTimeAtStart ? 1 : 0) )); global::navigationHandler->triggerPlaybackStart(); global::scriptScheduler->triggerPlaybackStart(); global::timeManager->triggerPlaybackStart(); _state = SessionState::Playback; return true; } void 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 = _timeline[_idxTimeline_cameraFirstInTimeline].timestamp; foundCameraKeyframe = true; break; } } if (!foundCameraKeyframe) { signalPlaybackFinishedForComponent(RecordedType::Camera); } } 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) { _state = SessionState::Idle; _cleanupNeeded = true; LINFO("Playback session finished"); } } void SessionRecording::enableTakeScreenShotDuringPlayback(int fps) { _saveRenderingDuringPlayback = true; _saveRenderingDeltaTime = 1.0 / fps; } void SessionRecording::disableTakeScreenShotDuringPlayback() { _saveRenderingDuringPlayback = false; } void SessionRecording::stopPlayback() { if (_state == SessionState::Playback) { _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()) { unsigned int p = _timeline[_idxTimeline_cameraPtrPrev].idxIntoKeyframeTypeArray; 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(); _idxTimeline_nonCamera = 0; _idxTime = 0; _idxScript = 0; _idxTimeline_cameraPtrNext = 0; _idxTimeline_cameraPtrPrev = 0; _hasHitEndOfCameraKeyframes = false; _saveRenderingDuringPlayback = 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; } void SessionRecording::saveCameraKeyframe() { if (_state != SessionState::Recording) { return; } 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 = { kf._timestamp, kf._timestamp - _timestampRecordStarted, global::timeManager->time().j2000Seconds() }; saveSingleKeyframeCamera(kf, times, _recordingDataMode, _recordFile, _keyframeBuffer); } 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::saveTimeKeyframe() { if (_state != SessionState::Recording) { return; } //Create a time keyframe, then call to populate it with current time props datamessagestructures::TimeKeyframe kf = _externInteract.generateTimeKeyframe(); Timestamps times = { kf._timestamp, kf._timestamp - _timestampRecordStarted, global::timeManager->time().j2000Seconds() }; saveSingleKeyframeTime(kf, times, _recordingDataMode, _recordFile, _keyframeBuffer); } 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::saveScriptKeyframe(std::string scriptToSave) { if (_state != SessionState::Recording) { return; } datamessagestructures::ScriptMessage sm = _externInteract.generateScriptMessage(scriptToSave); Timestamps times = { sm._timestamp, sm._timestamp - _timestampRecordStarted, global::timeManager->time().j2000Seconds() }; saveSingleKeyframeScript( sm, times, _recordingDataMode, _recordFile, _keyframeBuffer ); } 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::preSynchronization() { ZoneScoped if (_state == SessionState::Recording) { saveCameraKeyframe(); if (UsingTimeKeyframes) { saveTimeKeyframe(); } } else if (_state == SessionState::Playback) { 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 text = std::to_string(currentTime()); ghoul::fontrendering::RenderFont(*font, penPosition, text, glm::vec4(1.f)); } bool SessionRecording::isRecording() const { return (_state == SessionState::Recording); } bool SessionRecording::isPlayingBack() const { return (_state == SessionState::Playback); } bool SessionRecording::isSavingFramesDuringPlayback() const { return (_state == SessionState::Playback && _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(double timeOs, double timeRec, double timeSim) { if (_playbackTimeReferenceMode == KeyframeTimeRef::Relative_recordedStart) { return timeRec; } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { return timeSim; } else { return 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); } else if (_playbackTimeReferenceMode == KeyframeTimeRef::Absolute_simTimeJ2000) { return global::timeManager->time().j2000Seconds(); } else { return global::windowDelegate->applicationTime(); } } 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; } } bool SessionRecording::playbackCamera() { Timestamps times; datamessagestructures::CameraKeyframe kf; bool success = readSingleKeyframeCamera( kf, times, _recordingDataMode, _playbackFile, _playbackLineParsing, _playbackLineNum ); if (_setSimulationTimeWithNextCameraKeyframe) { global::timeManager->setTimeNextFrame(Time(times.timeSim)); _setSimulationTimeWithNextCameraKeyframe = false; _saveRenderingCurrentRecordedTime = times.timeRec; } double timeRef = appropriateTimestamp(times.timeOs, times.timeRec, times.timeSim); interaction::KeyframeNavigator::CameraPose pbFrame(std::move(kf)); if (success) { success = addKeyframe(timeRef, 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; } times.timeOs = kf._timestamp; 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; //global::timeManager.addKeyframe(timeRef, pbFrame._timestamp); //_externInteract.timeInteraction(pbFrame); if (success) { success = addKeyframe(kf._timestamp, 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 ); double timeRef = appropriateTimestamp(times.timeOs, times.timeRec, times.timeSim); if (success) { success = addKeyframe(timeRef, kf._script, _playbackLineNum); } return success; } 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::addKeyframe(double timestamp, interaction::KeyframeNavigator::CameraPose keyframe, int lineNum) { size_t indexIntoCameraKeyframesFromMainTimeline = _keyframesCamera.size(); _keyframesCamera.push_back(std::move(keyframe)); return addKeyframeToTimeline( RecordedType::Camera, indexIntoCameraKeyframesFromMainTimeline, timestamp, lineNum ); } bool SessionRecording::addKeyframe(double timestamp, datamessagestructures::TimeKeyframe keyframe, int lineNum) { size_t indexIntoTimeKeyframesFromMainTimeline = _keyframesTime.size(); _keyframesTime.push_back(std::move(keyframe)); return addKeyframeToTimeline( RecordedType::Time, indexIntoTimeKeyframesFromMainTimeline, timestamp, lineNum ); } bool SessionRecording::addKeyframe(double timestamp, std::string scriptToQueue, int lineNum) { size_t indexIntoScriptKeyframesFromMainTimeline = _keyframesScript.size(); _keyframesScript.push_back(std::move(scriptToQueue)); return addKeyframeToTimeline( RecordedType::Script, indexIntoScriptKeyframesFromMainTimeline, timestamp, lineNum ); } bool SessionRecording::addKeyframeToTimeline(RecordedType type, size_t indexIntoTypeKeyframes, double timestamp, int lineNum) { try { _timeline.push_back({ type, static_cast(indexIntoTypeKeyframes), timestamp }); } 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; } void SessionRecording::moveAheadInTime() { double currTime = currentTime(); lookForNonCameraKeyframesThatHaveComeDue(currTime); updateCameraWithOrWithoutNewKeyframes(currTime); 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()) { _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 = _timeline[seekAheadIndex].timestamp; 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 = _timeline[_idxTimeline_cameraPtrNext].timestamp; 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 = _timeline[_idxTimeline_cameraPtrPrev].timestamp; // getNextTimestamp(); double nextTime = _timeline[_idxTimeline_cameraPtrNext].timestamp; 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, false ); } 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 _timeline[_idxTimeline_nonCamera].timestamp; } else { return _timeline.back().timestamp; } } double SessionRecording::getPrevTimestamp() { if (_timeline.empty()) { return 0.0; } else if (_idxTimeline_nonCamera == 0) { return _timeline.front().timestamp; } else if (_idxTimeline_nonCamera < _timeline.size()) { return _timeline[_idxTimeline_nonCamera - 1].timestamp; } else { return _timeline.back().timestamp; } } 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::string path = absPath("${RECORDINGS}"); std::vector fileList; ghoul::filesystem::Directory currentDir(path); std::vector allInputFiles = currentDir.readFiles(); for (const std::string& f : allInputFiles) { // Remove path and keep only the filename fileList.push_back(f.substr(path.length() + 1, f.length() - path.length() - 1)); } 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) { if (isPath(filename)) { throw ConversionError("Playback filename must not contain path (/) elements"); } std::string conversionInFilename = absPath("${RECORDINGS}/" + filename); if (!FileSys.fileExists(conversionInFilename)) { throw ConversionError(fmt::format( "Cannot find the specified playback file '{}' to convert.", conversionInFilename )); } DataMode mode = readModeFromHeader(conversionInFilename); 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); } 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", "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." }, { "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." }, { "startPlaybackRecordedTime", &luascriptfunctions::startPlaybackRecordedTime, {}, "string", "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." }, { "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." }, { "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." }, } }; } } // namespace openspace::interaction