From a6c9b08e48db5e35f2ebb15d7e1789248fb2892f Mon Sep 17 00:00:00 2001 From: Thomas Jensen <32744171+Mast3rwaf1z@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:33:35 +0100 Subject: [PATCH] Better access (#19) * implemented a small web frontend to get a video file, next i just need to convert it to a stream of matrices and pass it to hyperwall * initial implementation of using hyperwall through web ui * finished implementing file uploading and added some more convenience scripts * depends * depends * depends --- .github/workflows/build-test.yml | 4 +- CMakeLists.txt | 7 +- flake.nix | 1 + include/Utils.hpp | 10 +++ include/Web/Server.hpp | 52 +++++++++++ scripts/ffplay-single.sh | 6 ++ scripts/tmux-ffplay.sh | 9 +- src/Receiver.cpp | 23 ++--- src/Sender.cpp | 18 +--- src/SenderWeb.cpp | 147 +++++++++++++++++++++++++++++++ 10 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 include/Web/Server.hpp create mode 100755 scripts/ffplay-single.sh create mode 100644 src/SenderWeb.cpp diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 979609d..59483c4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,4 +1,4 @@ -name: Build project +name: Build and test project on: @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: install dependencies - run: sudo apt-get update && sudo apt-get -y install cmake libopencv-dev libspdlog-dev libargparse-dev + run: sudo apt-get update && sudo apt-get -y install cmake libopencv-dev libspdlog-dev libargparse-dev libcpp-httplib-dev - uses: actions/cache@v4 with: path: build diff --git a/CMakeLists.txt b/CMakeLists.txt index 26a47fa..50c8312 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,11 @@ find_package(spdlog REQUIRED) # catch2 find_package(Catch2 REQUIRED) -foreach(exe Sender Receiver) +# httplib +find_package(httplib REQUIRED) + +# init all executables +foreach(exe Sender Receiver SenderWeb) add_executable(${exe} src/${exe}.cpp ${PROJECT_IMPLEMENTATIONS}) # Linking @@ -35,6 +39,7 @@ target_link_libraries(${exe} ${OpenCV_LIBS} argparse::argparse spdlog::spdlog + httplib::httplib ) endforeach() diff --git a/flake.nix b/flake.nix index b6276e6..8bb791c 100644 --- a/flake.nix +++ b/flake.nix @@ -19,6 +19,7 @@ spdlog argparse catch2_3 + httplib ]; }; }) { diff --git a/include/Utils.hpp b/include/Utils.hpp index c2591d0..2a7b863 100644 --- a/include/Utils.hpp +++ b/include/Utils.hpp @@ -1,4 +1,6 @@ +#pragma once +#include #include namespace Util { @@ -19,4 +21,12 @@ constexpr std::vector range(const int start, const int end) { return values; } +constexpr std::tuple split_resolution(const std::string res) { + return { + std::stoi(res.substr(0, res.find("x"))), + std::stoi(res.substr(res.find("x")+1, res.size())) + }; +} + + } diff --git a/include/Web/Server.hpp b/include/Web/Server.hpp new file mode 100644 index 0000000..7afe252 --- /dev/null +++ b/include/Web/Server.hpp @@ -0,0 +1,52 @@ +#include +#include + +namespace Web { + +class Server { + httplib::Server server; + public: + + Server() { + server.set_logger([](httplib::Request req, httplib::Response res){ + std::stringstream ss; + ss << "Method: " << req.method << " | "; + ss << "Path: " << req.path << " | "; + ss << "Status: " << res.status << " | "; + + switch(((res.status + 50) / 100) * 100) { + case 100: + case 300: + spdlog::warn(ss.str()); + break; + case 200: + spdlog::info(ss.str()); + break; + case 400: + case 500: + spdlog::error(ss.str()); + } + }); + } + void run(std::string host, int port) { + spdlog::info("Running server: {}:{}", host, port); + server.listen(host, port); + } + + void add(std::string path, std::string content) { + server.Get(path, [content](httplib::Request req, httplib::Response& res) { + res.set_content(content, "text/html"); + }); + } + void add_page(std::string path, std::ifstream file) { + std::stringstream ss; + ss << file.rdbuf(); + return add(path, ss.str()); + } + template + void Post (std::string path, F f) { + server.Post(path, f); + } +}; + +} diff --git a/scripts/ffplay-single.sh b/scripts/ffplay-single.sh new file mode 100755 index 0000000..00fe486 --- /dev/null +++ b/scripts/ffplay-single.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +while true; do + ffplay rtsp://${1}:8554/frame/${2}/${3} -fs -autoexit -window_title "frame-${2}x${3}" + sleep 1 +done diff --git a/scripts/tmux-ffplay.sh b/scripts/tmux-ffplay.sh index 05771ae..f3d0036 100755 --- a/scripts/tmux-ffplay.sh +++ b/scripts/tmux-ffplay.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash - -for ((x=0;x<$1;x++)); do - for ((y=0;y<$2;y++)); do - tmux new-session -d ./build/Receiver ${x}x${y} - done +for ((x=0;x<$2;x++)); do + for ((y=0;y<$3;y++)); do + tmux new-session -d ./scripts/ffplay-single.sh $1 $x $y + done done diff --git a/src/Receiver.cpp b/src/Receiver.cpp index 4c1e403..edd16a6 100644 --- a/src/Receiver.cpp +++ b/src/Receiver.cpp @@ -11,11 +11,12 @@ #include "Utils.hpp" -std::string split_res(std::string res, std::string direction) { - if (direction == "x") { - return res.substr(0, res.find("x")); +cv::VideoCapture await_capture(std::string path) { + while(true) { + cv::VideoCapture capture(path); + if (capture.isOpened()) + return capture; } - return res.substr(res.find("x")+1, res.size()); } int main(int argc, char* argv[]) { @@ -38,10 +39,8 @@ int main(int argc, char* argv[]) { exit(EXIT_FAILURE); } if(!parser.get("--test")) { - cv::VideoCapture capture( - std::format("rtsp://0.0.0.0:8554/frame/{}/{}", - split_res(parser.get("coordinate"), "x"), - split_res(parser.get("coordinate"), "y"))); + const auto [x, y] = Util::split_resolution(parser.get("coordinate")); + cv::VideoCapture capture = await_capture(std::format("rtsp://0.0.0.0:8554/frame/{}/{}", x, y)); while(true) { cv::Mat frame; capture.read(frame); @@ -57,11 +56,13 @@ int main(int argc, char* argv[]) { else { // not particularly good std::vector threads; - for(const auto& x : Util::range(stoi(split_res(parser.get("coordinate"), "x")))) { - for(const auto& y : Util::range(stoi(split_res(parser.get("coordinate"), "y")))) { + const auto [X, Y] = Util::split_resolution(parser.get("coordinate")); + for(const auto& x : Util::range(X)) { + + for(const auto& y : Util::range(Y)) { threads.push_back(std::thread{[x, y](){ spdlog::info("Running thread: {} {}", x, y); - auto capture = cv::VideoCapture(std::format("rtsp://0.0.0.0:8554/frame/{}/{}", x, y)); + auto capture = await_capture(std::format("rtsp://0.0.0.0:8554/frame/{}/{}", x, y)); while(true) { cv::Mat frame; capture.read(frame); diff --git a/src/Sender.cpp b/src/Sender.cpp index 7fbe2de..1ca7e04 100644 --- a/src/Sender.cpp +++ b/src/Sender.cpp @@ -10,13 +10,7 @@ #include "Hyperwall.hpp" #include "Sources/FileSource.hpp" #include "Sources/WebcamSource.hpp" - -std::string split_res(std::string res, std::string direction) { - if (direction == "x") { - return res.substr(0, res.find("x")); - } - return res.substr(res.find("x")+1, res.size()); -} +#include "Utils.hpp" int main(int argc, char* argv[]) { auto loglevel = (int)spdlog::level::info; @@ -61,14 +55,8 @@ int main(int argc, char* argv[]) { spdlog::debug("Generating settings"); Hyperwall::Settings settings( - { - stoi(split_res(parser.get("--resolution"), "x")), - stoi(split_res(parser.get("--resolution"), "y")) - }, - { - stoi(split_res(parser.get("--dimensions"), "x")), - stoi(split_res(parser.get("--dimensions"), "y")) - }, + Util::split_resolution(parser.get("--resolution")), + Util::split_resolution(parser.get("--dimensions")), parser.get("--rtsp-server"), parser.get("--bitrate"), parser.get("--framerate") diff --git a/src/SenderWeb.cpp b/src/SenderWeb.cpp new file mode 100644 index 0000000..90279cf --- /dev/null +++ b/src/SenderWeb.cpp @@ -0,0 +1,147 @@ +#include +#include +#include +#include + +#include "Hyperwall.hpp" +#include "Sources/FileSource.hpp" +#include "Utils.hpp" +#include "Web/Server.hpp" + +std::vector threads; +bool running; + +std::string layout(std::string input) { + std::string style = R"( + + )"; + return std::format( + R"( + + + {} + + Hyperwall + + + + + +

Hyperwall

+

+
+ {} + + + + )", style, input); +} + +int main(int argc, char* argv[]) { + Web::Server server; + + server.add("/", layout(R"( +
+
+ + +

+ + +

+ + +

+ +
+
+ )")); + + server.Post("/run", [](httplib::Request request, httplib::Response& response){ + if(!request.has_file("videoFile")) { + response.status = 400; + response.set_content("No file uploaded.", "text/plain"); + return; + } + std::tuple dimensions, resolution; + if(request.has_file("dimensions") && request.get_file_value("dimensions").content.contains("x")) { + auto dim = request.get_file_value("dimensions"); + dimensions = Util::split_resolution(dim.content); + } + else { + spdlog::warn("No dim provided"); + dimensions = {2, 2}; + } + if (request.has_file("resolution") && request.get_file_value("resolution").content.contains("x")) { + auto res = request.get_file_value("resolution"); + resolution = Util::split_resolution(res.content); + } + else { + spdlog::warn("No res provided"); + resolution = {1920, 1080}; + } + + + const auto& file = request.get_file_value("videoFile"); + spdlog::info("Got file: {}", file.filename); + + if (running) { + response.status = 400; + response.set_content("Failed, hyperwall is already running", "text/plain"); + return; + } + auto filename = std::format("./tmp/{}", file.filename); + + std::ofstream ofs("./tmp/" + file.filename, std::ios::binary); + if (ofs) { + ofs.write(file.content.data(), file.content.size()); + ofs.close(); + ofs.flush(); + } + + threads.push_back(std::thread{[&filename, dimensions, resolution](){ + running = true; + Hyperwall::FileSource source(filename); + Hyperwall::Settings settings( + resolution, + dimensions + ); + Hyperwall::Hyperwall hyperwall(source, settings); + hyperwall.run(); + std::filesystem::remove(filename); + running = false; + }}); + + response.set_content(layout(R"( + Successfully uploaded video!
+ Go back + )"), "text/html"); + }); + + server.run("0.0.0.0", 8000); +}