Latest changes to session recording with support for new webGUI record/playback controls (#840)

* Added separate directory for session recording files
* Changed recording & playback paths to use RECORDINGS dir and prevent absolute or relative paths in filename.
* Updated .gitignore so that recordings directory is ignored.
* Added session recording topic for synchronization of rec/play state with web gui.
* Added support to sessionRecording for providing a list of available playback files to web GUI.
* Fixed problem with playback filenames in list
* Fixed problem with occasional large jump in camera pos/rotation after playback finishes.
* Fixed the remaining post-playback problem that was causing a jump in position and rotation.
* Fix path issue on mac
* Fixed bug with bad scale values in recordings saved in ascii format.
This commit is contained in:
Gene Payne
2019-05-17 06:05:03 -06:00
committed by Emil Axelsson
parent c4781b01de
commit 32ebea9e06
16 changed files with 341 additions and 58 deletions
+1
View File
@@ -29,6 +29,7 @@ Thumbs.db
/documentation/
/logs/
/screenshots/
/recordings/
/sync/
# Customization is not supposed to be committed
customization.lua
+6
View File
@@ -318,6 +318,12 @@ void mainInitFunc() {
);
}
std::string sessionRecordingPath = "${RECORDINGS}";
FileSys.registerPathToken(
"${RECORDINGS}",
absPath(sessionRecordingPath),
ghoul::filesystem::FileSystem::Override::Yes
);
for (size_t i = 0; i < nWindows; ++i) {
sgct::SGCTWindow* w = SgctEngine->getWindowPtr(i);
@@ -56,6 +56,8 @@ public:
glm::dvec2 localRollVelocity() const;
glm::dvec2 globalRollVelocity() const;
void resetVelocities();
protected:
struct InteractionState {
InteractionState(double scaleFactor);
@@ -54,6 +54,7 @@ public:
void updateStatesFromInput(const InputState& inputState, double deltaTime);
void updateCameraStateFromStates(double deltaTime);
void resetVelocities();
Camera* camera() const;
void setCamera(Camera* camera);
@@ -66,6 +67,7 @@ public:
void startRetargetAim();
float retargetInterpolationTime() const;
void setRetargetInterpolationTime(float durationInSeconds);
void resetNodeMovements();
JoystickCameraStates& joystickStates();
@@ -148,7 +150,6 @@ private:
glm::dquat _previousAnchorNodeRotation;
glm::dvec3 _previousAimNodePosition;
glm::dquat _previousAimNodeRotation;
double _currentCameraToSurfaceDistance = 0.0;
bool _directlySetStereoDistance = false;
@@ -47,6 +47,15 @@ public:
Binary
};
enum class SessionState {
Idle = 0,
Recording = 1,
Playback = 2
};
using CallbackHandle = int;
using StateChangeCallback = std::function<void()>;
SessionRecording();
~SessionRecording();
/**
@@ -115,6 +124,12 @@ public:
*/
bool isPlayingBack() const;
/**
* Used to obtain the state of idle/recording/playback.
* \returns int value of state as defined by struct SessionState.
*/
SessionState state() const;
/**
* Used to trigger a save of the camera states (position, rotation, focus node,
* whether it is following the rotation of a node, and timestamp). The data will
@@ -141,12 +156,26 @@ public:
*/
static openspace::scripting::LuaLibrary luaLibrary();
/**
* Used to request a callback for notification of playback state change.
* \param cb function handle for callback.
* \returns CallbackHandle value of callback number.
*/
CallbackHandle addStateChangeCallback(StateChangeCallback cb);
/**
* Removes the callback for notification of playback state change.
* \param callback function handle for the callback.
*/
void removeStateChangeCallback(CallbackHandle handle);
/**
* Provides list of available playback files.
* \returns string of newline-delimited filenames in recordings dir.
*/
std::string playbackList();
private:
enum class SessionState {
Idle = 0,
Recording,
Playback
};
enum class RecordedType {
Camera = 0,
Time,
@@ -206,6 +235,7 @@ private:
bool isDataModeBinary();
unsigned int findIndexOfLastCameraKeyframeInTimeline();
bool doesTimelineEntryContainCamera(unsigned int index) const;
std::vector<std::pair<CallbackHandle, StateChangeCallback>> _stateChangeCallbacks;
RecordedType getNextKeyframeType();
RecordedType getPrevKeyframeType();
@@ -222,6 +252,7 @@ private:
RecordedDataMode _recordingDataMode = RecordedDataMode::Binary;
SessionState _state = SessionState::Idle;
SessionState _lastState = SessionState::Idle;
std::string _playbackFilename;
std::ifstream _playbackFile;
std::string _playbackLineParsing;
@@ -260,6 +291,8 @@ private:
unsigned int _idxTimeline_cameraFirstInTimeline = 0;
double _cameraFirstInTimeline_timestamp = 0;
int _nextCallbackHandle = 0;
};
} // namespace openspace
+2
View File
@@ -36,6 +36,7 @@ set(HEADER_FILES
include/topics/documentationtopic.h
include/topics/getpropertytopic.h
include/topics/luascripttopic.h
include/topics/sessionrecordingtopic.h
include/topics/setpropertytopic.h
include/topics/shortcuttopic.h
include/topics/subscriptiontopic.h
@@ -57,6 +58,7 @@ set(SOURCE_FILES
src/topics/documentationtopic.cpp
src/topics/getpropertytopic.cpp
src/topics/luascripttopic.cpp
src/topics/sessionrecordingtopic.cpp
src/topics/setpropertytopic.cpp
src/topics/shortcuttopic.cpp
src/topics/subscriptiontopic.cpp
@@ -0,0 +1,54 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2019 *
* *
* 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. *
****************************************************************************************/
#ifndef __OPENSPACE_MODULE_SERVER___SESSION_RECORDING_TOPIC___H__
#define __OPENSPACE_MODULE_SERVER___SESSION_RECORDING_TOPIC___H__
#include <modules/server/include/topics/topic.h>
#include <openspace/interaction/sessionrecording.h>
namespace openspace {
class SessionRecordingTopic : public Topic {
public:
SessionRecordingTopic();
virtual ~SessionRecordingTopic();
void handleJson(const nlohmann::json& json) override;
bool isDone() const override;
private:
const int UnsetOnChangeHandle = -1;
//Provides the idle/recording/playback state int value in json message
nlohmann::json state();
int _stateCallbackHandle = UnsetOnChangeHandle;
bool _isDone = false;
interaction::SessionRecording::SessionState _lastState;
};
} // namespace openspace
#endif // __OPENSPACE_MODULE_SERVER___SESSION_RECORDING_TOPIC___H__
+3
View File
@@ -29,6 +29,7 @@
#include <modules/server/include/topics/documentationtopic.h>
#include <modules/server/include/topics/getpropertytopic.h>
#include <modules/server/include/topics/luascripttopic.h>
#include <modules/server/include/topics/sessionrecordingtopic.h>
#include <modules/server/include/topics/setpropertytopic.h>
#include <modules/server/include/topics/shortcuttopic.h>
#include <modules/server/include/topics/subscriptiontopic.h>
@@ -56,6 +57,7 @@ namespace {
constexpr const char* DocumentationTopicKey = "documentation";
constexpr const char* GetPropertyTopicKey = "get";
constexpr const char* LuaScriptTopicKey = "luascript";
constexpr const char* SessionRecordingTopicKey = "sessionRecording";
constexpr const char* SetPropertyTopicKey = "set";
constexpr const char* ShortcutTopicKey = "shortcuts";
constexpr const char* SubscriptionTopicKey = "subscribe";
@@ -86,6 +88,7 @@ Connection::Connection(std::unique_ptr<ghoul::io::Socket> s,
_topicFactory.registerClass<DocumentationTopic>(DocumentationTopicKey);
_topicFactory.registerClass<GetPropertyTopic>(GetPropertyTopicKey);
_topicFactory.registerClass<LuaScriptTopic>(LuaScriptTopicKey);
_topicFactory.registerClass<SessionRecordingTopic>(SessionRecordingTopicKey);
_topicFactory.registerClass<SetPropertyTopic>(SetPropertyTopicKey);
_topicFactory.registerClass<ShortcutTopic>(ShortcutTopicKey);
_topicFactory.registerClass<SubscriptionTopic>(SubscriptionTopicKey);
@@ -31,6 +31,7 @@
#include <openspace/engine/virtualpropertymanager.h>
#include <openspace/engine/windowdelegate.h>
#include <openspace/interaction/navigationhandler.h>
#include <openspace/interaction/sessionrecording.h>
#include <openspace/network/parallelpeer.h>
#include <openspace/query/query.h>
#include <openspace/rendering/luaconsole.h>
@@ -48,6 +49,7 @@ const char* AllNodesValue = "__allNodes";
const char* AllScreenSpaceRenderablesValue = "__screenSpaceRenderables";
const char* PropertyKey = "property";
const char* RootPropertyOwner = "__rootOwner";
const char* SessionRecordingPlaybackList = "playbackList";
}
namespace openspace {
@@ -70,6 +72,11 @@ void GetPropertyTopic::handleJson(const nlohmann::json& json) {
else if (requestedKey == RootPropertyOwner) {
response = wrappedPayload(global::rootPropertyOwner);
}
else if (requestedKey == SessionRecordingPlaybackList) {
std::string fileList = global::sessionRecording.playbackList();
nlohmann::json getJson = { { SessionRecordingPlaybackList, fileList } };
response = wrappedPayload(getJson);
}
else {
response = propertyFromKey(requestedKey);
}
@@ -0,0 +1,95 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2019 *
* *
* 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 "modules/server/include/topics/sessionrecordingtopic.h"
#include <modules/server/include/connection.h>
#include <openspace/engine/globals.h>
#include <openspace/query/query.h>
#include <openspace/interaction/sessionrecording.h>
#include <ghoul/logging/logmanager.h>
namespace {
constexpr const char* _loggerCat = "SessionRecordingTopic";
constexpr const char* PropertyKey = "property";
constexpr const char* EventKey = "event";
constexpr const char* UnsubscribeEvent = "stop_subscription";
constexpr const char* StateKey = "recState";
} // namespace
using nlohmann::json;
namespace openspace {
SessionRecordingTopic::SessionRecordingTopic()
: _lastState(interaction::SessionRecording::SessionState::Idle)
{
LDEBUG("Starting new SessionRecording state subscription");
}
SessionRecordingTopic::~SessionRecordingTopic() {
if (_stateCallbackHandle != UnsetOnChangeHandle) {
global::sessionRecording.removeStateChangeCallback(_stateCallbackHandle);
}
}
bool SessionRecordingTopic::isDone() const {
return _isDone;
}
void SessionRecordingTopic::handleJson(const nlohmann::json& json) {
std::string event = json.at(EventKey).get<std::string>();
if (event == UnsubscribeEvent) {
_isDone = true;
return;
}
std::string requestedKey = json.at(PropertyKey).get<std::string>();
LDEBUG("Subscribing to " + requestedKey);
if (requestedKey == StateKey) {
_stateCallbackHandle = global::sessionRecording.addStateChangeCallback(
[this]() {
openspace::interaction::SessionRecording::SessionState nowState =
global::sessionRecording.state();
if (nowState != _lastState) {
_connection->sendJson(state());
_lastState = nowState;
}
}
);
_connection->sendJson(state());
}
else {
LWARNING("Cannot get " + requestedKey);
_isDone = true;
}
}
json SessionRecordingTopic::state() {
json statJson = { { "state", static_cast<int>(global::sessionRecording.state()) } };
return wrappedPayload(statJson);
}
} // namespace openspace
+1
View File
@@ -62,6 +62,7 @@ Paths = {
SYNC = "${BASE}/sync",
SCREENSHOTS = "${BASE}/screenshots",
WEB = "${DATA}/web",
RECORDINGS = "${BASE}/recordings",
CACHE = "${BASE}/cache",
CONFIG = "${BASE}/config",
@@ -78,6 +78,14 @@ void CameraInteractionStates::setVelocityScaleFactor(double scaleFactor) {
_globalRollState.setVelocityScaleFactor(scaleFactor);
}
void CameraInteractionStates::resetVelocities() {
_globalRotationState.velocity.setHard({ 0.0, 0.0 });
_localRotationState.velocity.setHard({ 0.0, 0.0 });
_truckMovementState.velocity.setHard({ 0.0, 0.0 });
_localRollState.velocity.setHard({ 0.0, 0.0 });
_globalRollState.velocity.setHard({ 0.0, 0.0 });
}
glm::dvec2 CameraInteractionStates::globalRotationVelocity() const{
return _globalRotationState.velocity.get();
}
+21 -40
View File
@@ -35,6 +35,12 @@
#include <glm/gtx/quaternion.hpp>
#ifdef INTERPOLATION_DEBUG_PRINT
namespace {
constexpr const char* _loggerCat = "KeyframeNavigator";
} // namespace
#endif
namespace openspace::interaction {
bool KeyframeNavigator::updateCamera(Camera& camera, bool ignoreFutureKeyframes) {
@@ -134,13 +140,15 @@ bool KeyframeNavigator::updateCamera(Camera* camera, const CameraPose prevPose,
// Linear interpolation
t = std::max(0.0, std::min(1.0, t));
camera->setPositionVec3(
prevKeyframeCameraPosition * (1 - t) + nextKeyframeCameraPosition * t
);
camera->setRotation(
glm::slerp(prevKeyframeCameraRotation, nextKeyframeCameraRotation, t)
glm::dvec3 nowCameraPosition = prevKeyframeCameraPosition * (1 - t) +
nextKeyframeCameraPosition * t;
glm::dquat nowCameraRotation = glm::slerp(prevKeyframeCameraRotation,
nextKeyframeCameraRotation, t
);
camera->setPositionVec3(nowCameraPosition);
camera->setRotation(nowCameraRotation);
// We want to affect view scaling, such that we achieve
// logarithmic interpolation of distance to an imagined focus node.
// To do this, we interpolate the scale reciprocal logarithmically.
@@ -155,42 +163,15 @@ bool KeyframeNavigator::updateCamera(Camera* camera, const CameraPose prevPose,
#ifdef INTERPOLATION_DEBUG_PRINT
LINFO(fmt::format(
"Cam pos prev={}, next={}",
std::to_string(prevKeyframeCameraPosition),
std::to_string(nextKeyframeCameraPosition)
"Cam pos = {} {} {} rot = {} {} {} {}",
nowCameraPosition.x,
nowCameraPosition.y,
nowCameraPosition.z,
nowCameraRotation.x,
nowCameraRotation.y,
nowCameraRotation.z,
nowCameraRotation.w
));
LINFO(fmt::format(
"Cam rot prev={} {} {} {} next={} {} {} {}",
prevKeyframeCameraRotation.x,
prevKeyframeCameraRotation.y,
prevKeyframeCameraRotation.z,
prevKeyframeCameraRotation.w,
nextKeyframeCameraRotation.x,
nextKeyframeCameraRotation.y,
nextKeyframeCameraRotation.z,
nextKeyframeCameraRotation.w
));
LINFO(fmt::format("Cam interp = {}", t));
LINFO(fmt::format(
"camera {} {} {} {} {} {}",
global::windowDelegate.applicationTime(),
global::windowDelegate.applicationTime() - _timestampPlaybackStarted_application,
global::timeManager.time().j2000Seconds(),
interpolatedCamera.x,
interpolatedCamera.y,
interpolatedCamera.z
));
// Following is for direct print to save & compare camera positions against recorded
// file
printf(
"camera %8.4f %8.4f %13.3f %16.7f %16.7f %16.7f\n",
global::windowDelegate.applicationTime(),
global::windowDelegate.applicationTime() - _timestampPlaybackStarted_application,
global::timeManager.time().j2000Seconds(),
interpolatedCamera.x,
interpolatedCamera.y,
interpolatedCamera.z
);
#endif
return true;
+12 -8
View File
@@ -124,14 +124,16 @@ void NavigationHandler::updateCamera(double deltaTime) {
if (_cameraUpdatedFromScript) {
_cameraUpdatedFromScript = false;
}
else if ( ! _playbackModeEnabled ) {
if (_camera) {
if (_useKeyFrameInteraction) {
_keyframeNavigator->updateCamera(*_camera, _playbackModeEnabled);
}
else {
_orbitalNavigator->updateStatesFromInput(*_inputState, deltaTime);
_orbitalNavigator->updateCameraStateFromStates(deltaTime);
else {
if ( ! _playbackModeEnabled ) {
if (_camera) {
if (_useKeyFrameInteraction) {
_keyframeNavigator->updateCamera(*_camera, _playbackModeEnabled);
}
else {
_orbitalNavigator->updateStatesFromInput(*_inputState, deltaTime);
_orbitalNavigator->updateCameraStateFromStates(deltaTime);
}
}
}
}
@@ -150,6 +152,8 @@ void NavigationHandler::triggerPlaybackStart() {
}
void NavigationHandler::stopPlayback() {
_orbitalNavigator->resetVelocities();
_orbitalNavigator->resetNodeMovements();
_playbackModeEnabled = false;
}
+20
View File
@@ -345,6 +345,11 @@ glm::quat OrbitalNavigator::anchorNodeToCameraRotation() const {
}
void OrbitalNavigator::resetVelocities() {
_mouseStates.resetVelocities();
_joystickStates.resetVelocities();
}
void OrbitalNavigator::updateStatesFromInput(const InputState& inputState,
double deltaTime)
{
@@ -580,6 +585,21 @@ void OrbitalNavigator::setAimNode(const std::string& aimNode) {
_aim.set(aimNode);
}
void OrbitalNavigator::resetNodeMovements() {
if (_anchorNode) {
_previousAnchorNodePosition = _anchorNode->worldPosition();
_previousAnchorNodeRotation = glm::quat_cast(_anchorNode->worldRotationMatrix());
} else {
_previousAnchorNodePosition = glm::dvec3(0.0);
_previousAnchorNodeRotation = glm::dquat();
}
if (_aimNode) {
_previousAimNodePosition = _aimNode->worldPosition();
} else {
_previousAimNodePosition = glm::dvec3(0.0);
}
}
void OrbitalNavigator::startRetargetAnchor() {
if (!_anchorNode) {
return;
+69 -4
View File
@@ -74,7 +74,17 @@ void SessionRecording::setRecordDataFormat(RecordedDataMode dataMode) {
}
bool SessionRecording::startRecording(const std::string& filename) {
const std::string absFilename = absPath(filename);
if (filename.find("/") != std::string::npos) {
LERROR("Recording filename must not contain path (/) elements");
return false;
}
if (!FileSys.directoryExists(absPath("${RECORDINGS}"))) {
FileSys.createDirectory(
absPath("${RECORDINGS}"),
ghoul::filesystem::FileSystem::Recursive::Yes
);
}
const std::string absFilename = absPath("${RECORDINGS}/" + filename);
if (_state == SessionState::Playback) {
_playbackFile.close();
@@ -123,7 +133,11 @@ void SessionRecording::stopRecording() {
bool SessionRecording::startPlayback(const std::string& filename,
KeyframeTimeRef timeMode, bool forceSimTimeAtStart)
{
const std::string absFilename = absPath(filename);
if (filename.find("/") != std::string::npos) {
LERROR("Playback filename must not contain path (/) elements");
return false;
}
const std::string absFilename = absPath("${RECORDINGS}/" + filename);
if (_state == SessionState::Recording) {
LERROR("Unable to start playback while in session recording mode");
@@ -299,7 +313,6 @@ void SessionRecording::cleanUpPlayback() {
if (node) {
global::navigationHandler.orbitalNavigator().setFocusNode(node->identifier());
}
}
global::scriptScheduler.stopPlayback();
@@ -477,7 +490,7 @@ void SessionRecording::saveCameraKeyframe() {
<< std::fixed << std::setprecision(7) << kf._rotation.y << " "
<< std::fixed << std::setprecision(7) << kf._rotation.z << " "
<< std::fixed << std::setprecision(7) << kf._rotation.w << " ";
keyframeLine << std::fixed << std::setprecision(7) << kf._scale << " ";
keyframeLine << std::scientific << kf._scale << " ";
if (kf._followNodeRotation) {
keyframeLine << "F ";
}
@@ -586,6 +599,16 @@ void SessionRecording::preSynchronization() {
else if (_cleanupNeeded) {
cleanUpPlayback();
}
//Handle callback(s) for change in idle/record/playback state
if (_state != _lastState) {
using K = const CallbackHandle;
using V = StateChangeCallback;
for (const std::pair<K, V>& it : _stateChangeCallbacks) {
it.second();
}
}
_lastState = _state;
}
bool SessionRecording::isRecording() const {
@@ -596,6 +619,10 @@ bool SessionRecording::isPlayingBack() const {
return (_state == SessionState::Playback);
}
SessionRecording::SessionState SessionRecording::state() const {
return _state;
}
bool SessionRecording::playbackAddEntriesToTimeline() {
bool parsingErrorsFound = false;
@@ -1255,6 +1282,44 @@ void SessionRecording::saveKeyframeToFile(std::string entry) {
_recordFile << 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<CallbackHandle, std::function<void()>>& cb) {
return cb.first == handle;
}
);
ghoul_assert(
it != _stateChangeCallbacks.end(),
"handle must be a valid callback handle"
);
_stateChangeCallbacks.erase(it);
}
std::string SessionRecording::playbackList() {
std::string fileList;
const std::string recordingsPath = absPath("${RECORDINGS}");
ghoul::filesystem::Directory currentDir(recordingsPath);
std::vector<std::string> allInputFiles = currentDir.readFiles();
for (std::string f : allInputFiles) {
//Remove path and keep only the filename, and add newline after
fileList.append(f.substr(recordingsPath.length() + 1, (f.length() - recordingsPath.length()) - 1));
fileList.append("\n");
}
//Remove the final trailing newline from the list and return it.
return fileList.substr(0, fileList.size() - 1);
}
scripting::LuaLibrary SessionRecording::luaLibrary() {
return {
"sessionRecording",