From 1c7537e042223cb1797db7bcb2df93a5563754d1 Mon Sep 17 00:00:00 2001
From: Jens Koenen <koenen@vr.rwth-aachen.de>
Date: Thu, 1 Dec 2022 15:17:26 +0100
Subject: [PATCH] When setting the command line flag --write-video, the raw
 video stream of an remote headset is stored on disk

---
 src/headset/remote_headset.cpp | 110 ++++++++++++++++++++++++---------
 src/headset/remote_headset.hpp |   5 +-
 src/utility/command_parser.cpp |  14 +++++
 src/utility/command_parser.hpp |   2 +
 src/vr_application.cpp         |   2 +-
 5 files changed, 102 insertions(+), 31 deletions(-)

diff --git a/src/headset/remote_headset.cpp b/src/headset/remote_headset.cpp
index 72456cbe..f7e4b94c 100644
--- a/src/headset/remote_headset.cpp
+++ b/src/headset/remote_headset.cpp
@@ -1,8 +1,9 @@
 #include "remote_headset.hpp"
 #include "vr_application.hpp"
-#include <imgui.h>
 
-#include <glm/gtx/string_cast.hpp>
+#include <imgui.h>
+#include <filesystem>
+#include <chrono>
 
 RemoteHeadset::RemoteHeadset()
 {
@@ -119,33 +120,6 @@ bool RemoteHeadset::on_interface()
 
     ImGui::Separator();
 
-    if(ImGui::Checkbox("Overlay", &this->overlay_enable))
-    {
-        this->transport->send_overlay_config(this->overlay_enable);
-    }
-
-    std::vector<std::string> graph_names;
-    std::vector<const char*> graph_name_pointers;
-
-    std::unique_lock<std::mutex> lock(this->transport_mutex);
-    for (const Statistic::Ptr& statistic : this->statistic_list)
-    {
-        if (!statistic->is_log_only())
-        {
-            graph_names.push_back(statistic->get_display_name());
-        }
-    }
-    lock.unlock();
-
-    for (const std::string& graph_name : graph_names)
-    {
-        graph_name_pointers.push_back(graph_name.c_str());
-    }
-
-    ImGui::Combo("Overlay Graph", (int32_t*) &this->overlay_graph, graph_name_pointers.data(), graph_name_pointers.size());
-    
-    ImGui::Separator();
-
     Encoder::Ptr encoder = this->encoders.front();
 
     if (encoder->is_supported(ENCODER_SETTING_MODE_CONSTANT_BITRATE) && encoder->is_supported(ENCODER_SETTING_MODE_CONSTANT_QUALITY))
@@ -229,6 +203,33 @@ bool RemoteHeadset::on_interface()
 
     ImGui::Separator();
 
+    if(ImGui::Checkbox("Overlay", &this->overlay_enable))
+    {
+        this->transport->send_overlay_config(this->overlay_enable);
+    }
+
+    std::vector<std::string> graph_names;
+    std::vector<const char*> graph_name_pointers;
+
+    std::unique_lock<std::mutex> lock(this->transport_mutex);
+    for (const Statistic::Ptr& statistic : this->statistic_list)
+    {
+        if (!statistic->is_log_only())
+        {
+            graph_names.push_back(statistic->get_display_name());
+        }
+    }
+    lock.unlock();
+
+    for (const std::string& graph_name : graph_names)
+    {
+        graph_name_pointers.push_back(graph_name.c_str());
+    }
+
+    ImGui::Combo("Overlay Graph", (int32_t*) &this->overlay_graph, graph_name_pointers.data(), graph_name_pointers.size());
+    
+    ImGui::Separator();
+
     lock.lock();
     for (Statistic::Ptr statistic : this->statistic_list)
     {
@@ -301,6 +302,11 @@ void RemoteHeadset::submit_frame(VkCommandBuffer command_buffer, VkImageLayout f
         {
             this->transport->send_frame_nal(frame_number, frame_id, transform_id, content);
         }
+
+        if (!this->encoder_files.empty())
+        {
+            this->encoder_files[frame_id].write((const char*)content.data(), content.size());
+        }
     });
     pass_timer->end_pass(command_buffer);
 
@@ -498,11 +504,40 @@ bool RemoteHeadset::create_encoders()
         this->encoders[index] = encoder;
     }
 
+    if (this->get_application()->get_command_parser().should_write_video())
+    {
+        if (!std::filesystem::exists(this->video_directory))
+        {
+            std::filesystem::create_directory(this->video_directory);
+        }
+
+        this->encoder_files.resize(this->frame_id_count);
+
+        for (uint32_t index = 0; index < this->encoder_files.size(); index++)
+        {
+            std::string file_name = this->video_directory + "/" + build_file_name(index) + ".h264";
+
+            this->encoder_files[index].open(file_name, std::ios::out | std::ios::binary);
+
+            if (!this->encoder_files[index].good())
+            {
+                lava::log()->error("Can't create file '" + file_name + "' for video capture!");
+
+                return false;
+            }
+        }
+    }
+
     return true;
 }
 
 void RemoteHeadset::destroy_encoders()
 {
+    for (uint32_t index = 0; index < this->encoder_files.size(); index++)
+    {
+        this->encoder_files[index].close();
+    }
+
     for (uint32_t index = 0; index < this->encoders.size(); index++)
     {
         if (this->encoders[index] != nullptr)
@@ -719,6 +754,23 @@ void RemoteHeadset::on_encode_error()
     lava::log()->error("Remote Headset: Encode error detected!");
 }
 
+std::string RemoteHeadset::build_file_name(uint32_t frame_id)
+{
+    time_t system_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+    tm local_time = *localtime(&system_time);
+
+    std::string name = "video";
+    name += std::to_string(frame_id);
+    name += "_" + std::to_string(local_time.tm_mday);
+    name += "-" + std::to_string(local_time.tm_mon + 1);
+    name += "-" + std::to_string(local_time.tm_year + 1900);
+    name += "_" + std::to_string(local_time.tm_hour);
+    name += "-" + std::to_string(local_time.tm_min);
+    name += "-" + std::to_string(local_time.tm_sec);
+
+    return name;
+}
+
 bool RemoteHeadset::convert_strategy(StereoStrategyType strategy_type, RemoteStrategy& remote_strategy)
 {
     switch (strategy_type)
diff --git a/src/headset/remote_headset.hpp b/src/headset/remote_headset.hpp
index 4ba84800..02d97c42 100644
--- a/src/headset/remote_headset.hpp
+++ b/src/headset/remote_headset.hpp
@@ -1,11 +1,11 @@
 #pragma once
 #include <liblava/lava.hpp>
 #include <glm/glm.hpp>
+#include <fstream>
 #include <mutex>
 #include <vector>
 #include <array>
 #include <memory>
-#include <chrono>
 
 #include "headset.hpp"
 #include "strategy/stereo_strategy.hpp"
@@ -72,10 +72,12 @@ private:
     void on_transport_error();
     void on_encode_error();
 
+    static std::string build_file_name(uint32_t frame_id);
     static bool convert_strategy(StereoStrategyType strategy_type, RemoteStrategy& remote_strategy);
 
 private:
     const uint32_t server_port = 4000;
+    const std::string video_directory = "video_captures";
 
     const float near_plane = 0.1f;
     const float far_plane = 1000.0f;
@@ -110,6 +112,7 @@ private:
     std::vector<lava::image::ptr> framebuffers;
 
     std::vector<Encoder::Ptr> encoders;
+    std::vector<std::fstream> encoder_files;
     uint32_t encoder_mode = ENCODER_MODE_CONSTANT_QUALITY;
     uint32_t encoder_input_rate = 90;
     uint32_t encoder_key_rate = 90;
diff --git a/src/utility/command_parser.cpp b/src/utility/command_parser.cpp
index b1647bf8..77366016 100644
--- a/src/utility/command_parser.cpp
+++ b/src/utility/command_parser.cpp
@@ -20,6 +20,11 @@ bool CommandParser::parse_command(const argh::parser& cmd_line)
             this->write_frames = true;
         }
 
+        else if (flag == "write-video")
+        {
+            this->write_video = true;
+        }
+
         else if (flag == "disable-indirect-lighting")
         {
             this->disable_indirect_lighting = true;
@@ -315,6 +320,11 @@ bool CommandParser::should_write_frames() const
     return this->write_frames;
 }
 
+bool CommandParser::should_write_video() const
+{
+    return this->write_video;
+}
+
 bool CommandParser::should_disable_indirect_lighting() const
 {
     return this->disable_indirect_lighting;
@@ -340,8 +350,10 @@ void CommandParser::show_help()
     std::cout << "   --stereo-strategy={strategy}         The stereo strategy that should be used." << std::endl;
     std::cout << "                                        Options: native-forward (default), native-deferred, multi-view, dpr" << std::endl;
     std::cout << "   --transport={transport}              The transport method that should be used." << std::endl;
+    std::cout << "                                        This parameter should only be used when the parameter headset is set to remote." << std::endl;
     std::cout << "                                        Options: udp (default)" << std::endl;
     std::cout << "   --encoder={encoder}                  The encoder that should be used when a remote headset is used." << std::endl;
+    std::cout << "                                        This parameter should only be used when the parameter headset is set to remote." << std::endl;
     std::cout << "                                        Options: vulkan (default), nvidia" << std::endl;
     std::cout << "   --benchmark                          Play animation once and close program after completion." << std::endl;
     std::cout << "                                        If not set, the application runs indefinitely and the interface is enabled." << std::endl;
@@ -359,6 +371,8 @@ void CommandParser::show_help()
     std::cout << "   --show-companion-window              Show companion window. If not set, writes to the companion window are ignored." << std::endl;
     std::cout << "   --write-frames                       Write frames to files. If not set, frames will not be written to files." << std::endl;
     std::cout << "                                        This parameter is only allowed during a benchmark." << std::endl;
+    std::cout << "   --write-video                        Write video stream to file." << std::endl;
+    std::cout << "                                        This parameter should only be used when the parameter headset is set to remote." << std::endl;
     std::cout << "   --disable-indirect-lighting          Disable indirect lighting and the allocation and computation of an indirect light cache." << std::endl;
     std::cout << "   --disable-shadows                    Disable shadows and the allocation and computation of shadow maps." << std::endl;
     std::cout << "   --help                               Show help information." << std::endl;
diff --git a/src/utility/command_parser.hpp b/src/utility/command_parser.hpp
index 77454760..6507df74 100644
--- a/src/utility/command_parser.hpp
+++ b/src/utility/command_parser.hpp
@@ -41,6 +41,7 @@ public:
     bool should_benchmark() const;
     bool should_show_companion_window() const;
     bool should_write_frames() const;
+    bool should_write_video() const;
     bool should_disable_indirect_lighting() const;
     bool should_disbale_shadows() const;
 
@@ -74,6 +75,7 @@ private:
     bool benchmark = false;
     bool show_companion_window = false;
     bool write_frames = false;
+    bool write_video = false;
     bool disable_indirect_lighting = false;
     bool disable_shadows = false;
 };
\ No newline at end of file
diff --git a/src/vr_application.cpp b/src/vr_application.cpp
index ffb8fa3e..e387839c 100644
--- a/src/vr_application.cpp
+++ b/src/vr_application.cpp
@@ -416,7 +416,7 @@ bool VRApplication::on_create()
         return false;
     }
 
-    this->frame_capture = make_frame_capture("captures");
+    this->frame_capture = make_frame_capture("frame_captures");
     this->frame_capture->set_enabled(this->command_parser.should_write_frames());
 
     if (!this->frame_capture->create(this->get_frame_count()))
-- 
GitLab