diff --git a/.vscode/settings.json b/.vscode/settings.json index cad09eb087a905316b6a64f97397395f62999098..5ade3be30bc98eaff91ee89c367e108180dc4476 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,21 @@ "xtr1common": "cpp", "xtree": "cpp", "xutility": "cpp", - "thread": "cpp" + "thread": "cpp", + "*.ipp": "cpp", + "clocale": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cwctype": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "codecvt": "cpp", + "complex": "cpp", + "condition_variable": "cpp", + "future": "cpp", + "mutex": "cpp", + "numeric": "cpp", + "typeindex": "cpp" } } \ No newline at end of file diff --git a/include/rew/common/tokenizer.h b/include/rew/common/tokenizer.h new file mode 100644 index 0000000000000000000000000000000000000000..dc499144a70c0f68accfa8fef9b59f5340d041d7 --- /dev/null +++ b/include/rew/common/tokenizer.h @@ -0,0 +1,79 @@ +#ifndef REW_COMMON_TOKENIZER_H +#define REW_COMMON_TOKENIZER_H + +#include <string> +#include <vector> +#include "../config.h" + +REW_NAMESPACE { + template<class T, typename CharTrait = std::char_traits<T>, typename Allocator = std::allocator<T>> + class Tokenizer { + public: + Tokenizer(const std::basic_string<T, CharTrait, Allocator>& str, const std::basic_string<T, CharTrait, Allocator>& delim) :ptr(str), del(delim) { + pos = 0; + last = 0; + } + + bool getNext(std::basic_string<T, CharTrait, Allocator>* str) { + // Loop until all tokens found + while (del.size() > 0 && (pos = ptr.find(del, pos)) != std::basic_string<T, CharTrait, Allocator>::npos) { + // Do we have non-empty token? + if (last < pos && pos - last > 0) { + if (str != NULL)*str = ptr.substr(last, pos - last); + pos += del.size(); + last = pos; + return true; + } + pos += del.size(); + last = pos; + } + // add the last token (will also include a whole string when no delim was found) + if (last < ptr.size()) { + if (str != NULL)*str = ptr.substr(last, ptr.size() - last); + last = ptr.size(); + return true; + } + return false; + } + + std::basic_string<T, CharTrait, Allocator> getNext() { + std::basic_string<T, CharTrait, Allocator> str; + getNext(&str); + return str; + } + + bool skipNext() { + return getNext(NULL); + } + + bool hasNext() { + size_t tempLast = last; + size_t tempPos = pos; + bool result = getNext(NULL); + last = tempLast; + pos = tempPos; + return result; + } + + size_t getPos() { + return pos; + } + + private: + const std::basic_string<T, CharTrait, Allocator>& ptr; + const std::basic_string<T, CharTrait, Allocator> del; + size_t pos = 0; + size_t last = 0; + }; + + inline std::vector<std::string> getTokens(const std::string& str, const std::string& delim) { + std::vector<std::string> tokens; + Tokenizer<char> tokenizer(str, delim); + while(tokenizer.hasNext()) { + tokens.push_back(tokenizer.getNext()); + } + return tokens; + } +} + +#endif diff --git a/include/rew/decoder/http_connection.h b/include/rew/decoder/http_connection.h new file mode 100644 index 0000000000000000000000000000000000000000..c76e62df515ffc9277582b55686a0ba7a90dc4e6 --- /dev/null +++ b/include/rew/decoder/http_connection.h @@ -0,0 +1,28 @@ +#ifndef REW_HTTP_CONNECTION_H +#define REW_HTTP_CONNECTION_H + +#include "../config.h" + +REW_NAMESPACE { + class HttpServer; + + /*! + * @ingroup decoder + */ + class REW_API HttpConnection { + public: + HttpConnection(HttpServer& server); + virtual ~HttpConnection() = default; + + virtual void start() = 0; + virtual void stop() = 0; + void send(const std::string& contentType, int status, const std::string& body); + virtual void sendRaw(std::shared_ptr<std::string> payload) = 0; + protected: + void receive(const std::string& payload); + private: + HttpServer& server; + }; +} + +#endif diff --git a/include/rew/decoder/http_server.h b/include/rew/decoder/http_server.h new file mode 100644 index 0000000000000000000000000000000000000000..30a0e8f90c16006097df1697cc57a0a08f2fcec4 --- /dev/null +++ b/include/rew/decoder/http_server.h @@ -0,0 +1,40 @@ +#ifndef REW_HTTP_SERVER_H +#define REW_HTTP_SERVER_H + +#include <unordered_map> +#include <mutex> +#include <memory> +#include <string> +#include <vector> +#include <list> +#include "../common/input.h" +#include "../common/named_raw_file.h" + +REW_NAMESPACE { + class HttpConnection; + + /*! + * @ingroup decoder + */ + class REW_API HttpServer: public Input<NamedRawFile> { + public: + HttpServer() = default; + virtual ~HttpServer() = default; + + void process(const NamedRawFile* data, size_t length) override; + void serve(HttpConnection& con, const std::string& file); + void disconnect(const std::shared_ptr<HttpConnection>& connection); + virtual void start() = 0; + virtual void stop() = 0; + + protected: + void accept(const std::shared_ptr<HttpConnection>& connection); + + private: + std::unordered_map<std::string, std::string> files; + std::list<std::shared_ptr<HttpConnection>> connections; + std::mutex mutex; + }; +} + +#endif diff --git a/include/rew/decoder/tcp_http_connection.h b/include/rew/decoder/tcp_http_connection.h new file mode 100644 index 0000000000000000000000000000000000000000..bd1d98b980320e58faf2a59a302df105a6393b0f --- /dev/null +++ b/include/rew/decoder/tcp_http_connection.h @@ -0,0 +1,28 @@ +#ifndef REW_TCP_HTTP_CONNECTION_H +#define REW_TCP_HTTP_CONNECTION_H + +#include "http_connection.h" + +REW_NAMESPACE { + class TcpHttpServer; + + /*! + * @ingroup decoder + */ + class REW_API TcpHttpConnection: public HttpConnection, public std::enable_shared_from_this<TcpHttpConnection> { + public: + TcpHttpConnection(TcpHttpServer& server, void* socket); + virtual ~TcpHttpConnection(); + + void start() override; + void stop() override; + void sendRaw(std::shared_ptr<std::string> payload) override; + private: + void doRead(); + + class Impl; + std::shared_ptr<Impl> pimpl; + }; +} + +#endif diff --git a/include/rew/decoder/tcp_http_server.h b/include/rew/decoder/tcp_http_server.h new file mode 100644 index 0000000000000000000000000000000000000000..6a6e88711db010832d7c5d2872b97a92904e0e73 --- /dev/null +++ b/include/rew/decoder/tcp_http_server.h @@ -0,0 +1,24 @@ +#ifndef REW_TCP_HTTP_SERVER_H +#define REW_TCP_HTTP_SERVER_H + +#include "http_server.h" + +REW_NAMESPACE { + /*! + * @ingroup decoder + */ + class REW_API TcpHttpServer: public HttpServer { + public: + TcpHttpServer(const std::string& address, int port); + virtual ~TcpHttpServer(); + + void start() override; + void stop() override; + + private: + class Impl; + std::unique_ptr<Impl> pimpl; + }; +} + +#endif diff --git a/src/rew/decoder-cli/main.cpp b/src/rew/decoder-cli/main.cpp index a54e11bf829d1c4f695fad4ec0c5f793b75b8d6a..8c1ebef7a34593e5f2c4e45c5ba945f120aecfcf 100644 --- a/src/rew/decoder-cli/main.cpp +++ b/src/rew/decoder-cli/main.cpp @@ -8,18 +8,16 @@ #include <limits.h> #include <stdio.h> #include <stdlib.h> +#include <signal.h> #endif #include <argagg/argagg.hpp> #include <iostream> +#include <thread> +#include <future> #include <rew/decoder/decoder.h> #include <rew/common/named_raw_file.h> #include <rew/decoder/physical_audio_source.h> -#include <thread> -#include "rew/encoder/audio_sink.h" -#include "rew/encoder/wav_writer.h" -#include <future> - -static rew::PhysicalAudioSource* gSource = nullptr; +#include <rew/decoder/tcp_http_server.h> ///===================================================================================================================== std::string getFullPath(const std::string& path) { @@ -36,25 +34,6 @@ std::string getFullPath(const std::string& path) { return std::string(&buffer[0]); } -///===================================================================================================================== -#ifdef _WIN32 -BOOL WINAPI ctrlHandler(const DWORD fdwCtrlType) { - switch (fdwCtrlType) { - case CTRL_C_EVENT: - case CTRL_CLOSE_EVENT: - case CTRL_BREAK_EVENT: - case CTRL_LOGOFF_EVENT: - case CTRL_SHUTDOWN_EVENT: - if (gSource) { - gSource->close(); - } - return FALSE; - default: - return FALSE; - } -} -#endif - ///===================================================================================================================== size_t getFileSize(const std::string& path) { struct stat info = { 0 }; @@ -102,6 +81,11 @@ argagg::parser argparser{ { "output", {"-o", "--output"}, "The output folder", 1 + }, + { + "serve", + {"-s", "--serve"}, + "Serve the files over http server", 0 } } }; @@ -154,21 +138,45 @@ private: ///===================================================================================================================== int main (const int argc, char* argv[]) { + static bool terminate = false; #ifdef _WIN32 - SetConsoleCtrlHandler(ctrlHandler, TRUE); + SetConsoleCtrlHandler([](const DWORD fdwCtrlType) -> BOOL { + switch (fdwCtrlType) { + case CTRL_C_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + terminate = true; + return FALSE; + default: + return FALSE; + } + }, TRUE); +#else + struct sigaction sigIntHandler; + + sigIntHandler.sa_handler = [](int num) -> void { + (void)num; + terminate = true; + }; + sigemptyset(&sigIntHandler.sa_mask); + sigIntHandler.sa_flags = 0; + + sigaction(SIGINT, &sigIntHandler, NULL); #endif try { const auto args = argparser.parse(argc, argv); + std::shared_ptr<rew::Input<rew::NamedRawFile>> sink; + std::shared_ptr<rew::TcpHttpServer> server; if (args["help"]) { std::cerr << argparser << std::endl; return EXIT_SUCCESS; } - if (args["listen"]) { - if (!args["output"]) - throw std::runtime_error("Output argument is required!"); + if (args["output"]) { const auto outputPath = getFullPath(args["output"].as<std::string>()); if (!pathIsValid(outputPath)) @@ -177,17 +185,37 @@ int main (const int argc, char* argv[]) { if (!folderIsValid(outputPath)) throw std::runtime_error("Output path: \"" + outputPath + "\" is not a folder!"); - auto sink = std::make_shared<CustomSink>(outputPath); + sink = std::make_shared<CustomSink>(outputPath); + } - auto source = std::make_shared<rew::PhysicalAudioSource>(); - gSource = source.get(); + else if (args["serve"]) { + server = std::make_shared<rew::TcpHttpServer>("localhost", 80); + sink = server; + server->start(); + } - auto decoder = std::make_shared<rew::Decoder>(source, sink, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, DEFAULT_SAMPLE_LENGTH_MS); + else { + throw std::runtime_error("You need to specify either --output or --serve!"); + } + + if (args["listen"]) { + const auto audio = std::make_shared<rew::PhysicalAudioSource>(); - source->start(); - source->process(); - source->close(); + auto decoder = std::make_shared<rew::Decoder>(audio, sink, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, DEFAULT_SAMPLE_LENGTH_MS); + auto t = std::thread([=]() -> void { + audio->start(); + audio->process(); + }); + + while(!terminate) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + audio->close(); + t.join(); + + if (server) server->stop(); return EXIT_SUCCESS; } else if (args["listen-devices"]) { @@ -204,34 +232,33 @@ int main (const int argc, char* argv[]) { if (!found) { std::cerr << "No devices found" << std::endl; } + return EXIT_SUCCESS; } else if (args["input"]) { - if (!args["output"]) - throw std::runtime_error("Output argument is required!"); - - const auto outputPath = getFullPath(args["output"].as<std::string>()); const auto inputPath = args["input"].as<std::string>(); if (!pathIsValid(inputPath)) throw std::runtime_error("Input file: \"" + inputPath + "\" does not exist!"); - if (!pathIsValid(outputPath)) - throw std::runtime_error("Output path: \"" + outputPath + "\" does not exist!"); - - if (!folderIsValid(outputPath)) - throw std::runtime_error("Output path: \"" + outputPath + "\" is not a folder!"); - auto wav = std::make_shared<rew::WavReader>(); auto source = std::make_shared<rew::AudioSource>(wav); - auto sink = std::make_shared<CustomSink>(outputPath); auto decoder = std::make_shared<rew::Decoder>(source, sink, DEFAULT_LOW_TONE_FREQ, DEFAULT_HIGH_TONE_FREQ, DEFAULT_SAMPLE_LENGTH_MS); - source->open(inputPath); - source->process(); - source->close(); + auto t = std::thread([=]() -> void { + source->open(inputPath); + source->process(); + source->close(); + }); + + while(!terminate) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + t.join(); + if (server) server->stop(); return EXIT_SUCCESS; } else { throw std::runtime_error("You need to specify either --input or --listen"); diff --git a/src/rew/decoder/http_connection.cpp b/src/rew/decoder/http_connection.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5149579b4827310962ceb1888520f5be7d755166 --- /dev/null +++ b/src/rew/decoder/http_connection.cpp @@ -0,0 +1,44 @@ +#include <sstream> +#include <rew/decoder/http_connection.h> +#include <rew/decoder/http_server.h> +#include <rew/common/tokenizer.h> + +///===================================================================================================================== +rew::HttpConnection::HttpConnection(HttpServer& server) + :server(server) { + +} + +///===================================================================================================================== +void rew::HttpConnection::send(const std::string& contentType, int status, const std::string& body) { + std::stringstream ss; + ss << "HTTP/1.1 200 OK\n"; + ss << "Server: Decoder\n"; + ss << "Accept-Ranges: bytes\n"; + ss << "Content-Length: " << std::to_string(body.size()) << "\n"; + ss << "Connection: close\n"; + ss << "Content-Type: " << contentType << "\n"; + ss << "\n"; + ss << body; + sendRaw(std::make_shared<std::string>(ss.str())); +} + +///===================================================================================================================== +void rew::HttpConnection::receive(const std::string& payload) { + try { + const auto tokens = getTokens(payload, "\n"); + if (tokens.size() < 1) { + send("text/plain", 400, "Bad request"); + } else { + const auto words = getTokens(tokens[0], " "); + if (words.size() < 2 || words[0] != "GET") { + send("text/plain", 400, "Bad request"); + } else { + server.serve(*this, words[1]); + } + } + } catch (std::exception& e) { + send("text/plain", 500, "Internal server error"); + } +} + diff --git a/src/rew/decoder/http_server.cpp b/src/rew/decoder/http_server.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1da8da76beac39a57fbefa6dfd4e91c062cba4ac --- /dev/null +++ b/src/rew/decoder/http_server.cpp @@ -0,0 +1,58 @@ +#include <rew/decoder/http_server.h> +#include <rew/decoder/http_connection.h> + +///===================================================================================================================== +void rew::HttpServer::process(const NamedRawFile* data, size_t length) { + for (size_t i = 0; i < length; i++) { + const auto& namedFile = data[i]; + + auto terminator = namedFile.length; + for (size_t i = 0; i < namedFile.length; i++) { + if (namedFile.data[i] == '\0') { + terminator = i; + break; + } + } + + if (terminator != namedFile.length) { + const auto sptr = reinterpret_cast<const char*>(namedFile.data.get()); + const auto name = std::string(sptr, terminator); + + const auto contents = reinterpret_cast<const char*>(namedFile.data.get() + terminator + 1); + const auto total = namedFile.length - terminator - 1; + + std::cout << "Caching file: " << name << " of size: " << total << " bytes!" << std::endl; + std::string data; + data.resize(total); + std::memcpy(&data[0], contents, total); + + std::lock_guard<std::mutex> guard{mutex}; + files.insert(std::make_pair(name, std::move(data))); + } + } +} + +///===================================================================================================================== +void rew::HttpServer::serve(HttpConnection& con, const std::string& file) { + std::lock_guard<std::mutex> guard{mutex}; + const auto it = files.find(file); + if (it == files.end()) { + con.send("text/plain", 404, "Not found"); + } else { + con.send("text/plain", 200, it->second); + } +} + +///===================================================================================================================== +void rew::HttpServer::accept(const std::shared_ptr<HttpConnection>& connection){ + std::lock_guard<std::mutex> guard{mutex}; + connections.push_back(connection); + connection->start(); +} + +///===================================================================================================================== +void rew::HttpServer::disconnect(const std::shared_ptr<HttpConnection>& connection){ + std::lock_guard<std::mutex> guard{mutex}; + connection->stop(); + connections.remove(connection); +} diff --git a/src/rew/decoder/tcp_http_connection.cpp b/src/rew/decoder/tcp_http_connection.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6615294226011447d5db7b3824e6e90570b06fa7 --- /dev/null +++ b/src/rew/decoder/tcp_http_connection.cpp @@ -0,0 +1,76 @@ +#define ASIO_STANDALONE +#if defined(WIN32) && !defined(_WIN32_WINNT) +#define _WIN32_WINNT 0x0601 +#endif +#include <asio.hpp> +#include <sstream> +#include <rew/decoder/tcp_http_connection.h> +#include <rew/decoder/tcp_http_server.h> + +///===================================================================================================================== +class rew::TcpHttpConnection::Impl { +public: + Impl(TcpHttpServer& server, asio::ip::tcp::socket socket) + : server(server), + socket(std::move(socket)) { + + } + + TcpHttpServer& server; + asio::ip::tcp::socket socket; + std::array<char, 8192> buffer; +}; + +///===================================================================================================================== +rew::TcpHttpConnection::TcpHttpConnection(TcpHttpServer& server, void* socket) + : HttpConnection(server), + pimpl(new Impl(server, std::move(*reinterpret_cast<asio::ip::tcp::socket*>(socket)))) { + +} + +///===================================================================================================================== +rew::TcpHttpConnection::~TcpHttpConnection(){ + stop(); +} + +///===================================================================================================================== +void rew::TcpHttpConnection::start(){ + doRead(); +} + +///===================================================================================================================== +void rew::TcpHttpConnection::stop(){ + pimpl->socket.close(); +} + +///===================================================================================================================== +void rew::TcpHttpConnection::doRead() { + auto self(shared_from_this()); + pimpl->socket.async_read_some(asio::buffer(pimpl->buffer), [this, self](const std::error_code ec, const size_t length) { + if (!ec){ + const auto payload = std::string(reinterpret_cast<const char*>(pimpl->buffer.data()), length); + self->receive(payload); + } else if (ec != asio::error::operation_aborted) { + pimpl->server.disconnect(shared_from_this()); + } + }); +} + +///===================================================================================================================== +void rew::TcpHttpConnection::sendRaw(std::shared_ptr<std::string> payload) { + auto self(shared_from_this()); + auto buffer = asio::buffer(payload->data(), payload->size()); + asio::async_write(pimpl->socket, buffer, [this, self, payload](const std::error_code ec, const size_t){ + (void)payload; //Extend the life of the payload + + if (!ec) { + // Initiate graceful connection closure. + asio::error_code ignored_ec; + pimpl->socket.shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec); + } + + if (ec != asio::error::operation_aborted) { + pimpl->server.disconnect(shared_from_this()); + } + }); +} diff --git a/src/rew/decoder/tcp_http_server.cpp b/src/rew/decoder/tcp_http_server.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d3c701c2a6b0ca419240ff02427005d23c23739e --- /dev/null +++ b/src/rew/decoder/tcp_http_server.cpp @@ -0,0 +1,92 @@ +#define ASIO_STANDALONE +#if defined(WIN32) && !defined(_WIN32_WINNT) +#define _WIN32_WINNT 0x0601 +#endif +#include <asio.hpp> +#include <thread> +#include <rew/decoder/tcp_http_server.h> +#include <rew/decoder/tcp_http_connection.h> + +///===================================================================================================================== +class rew::TcpHttpServer::Impl { +public: + Impl(TcpHttpServer& server, const std::string& address, const int port) + : server(server), + acceptor(ioContext) { + + // Open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR). + asio::ip::tcp::resolver resolver(ioContext); + asio::ip::tcp::endpoint endpoint = + *resolver.resolve(address, std::to_string(port)).begin(); + acceptor.open(endpoint.protocol()); + acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true)); + acceptor.bind(endpoint); + acceptor.listen(); + + doAccept(); + + std::cout << "Starting http server at " << endpoint.address().to_string() << ":" << endpoint.port() << std::endl; + } + + ~Impl() { + stop(); + } + + void doAccept() { + acceptor.async_accept([this](const std::error_code ec, asio::ip::tcp::socket socket){ + if (!acceptor.is_open()){ + return; + } + + if (!ec) { + std::shared_ptr<HttpConnection> connection + = std::make_shared<TcpHttpConnection>(server, reinterpret_cast<void*>(&socket)); + server.accept(connection); + } else { + std::cerr << "Http server error: " << ec.message(); + } + + doAccept(); + }); + } + + void start() { + thread = std::thread([this]() -> void { + ioContext.run(); + }); + } + + void stop() { + if (thread.joinable()) { + ioContext.stop(); + thread.join(); + } + } + + TcpHttpServer& server; + asio::io_context ioContext; + asio::ip::tcp::acceptor acceptor; + std::thread thread; +}; + +///===================================================================================================================== +rew::TcpHttpServer::TcpHttpServer(const std::string& address, const int port) + :pimpl(new Impl(*this, address, port)) { + +} + +///===================================================================================================================== +rew::TcpHttpServer::~TcpHttpServer() { + +} + +///===================================================================================================================== +void rew::TcpHttpServer::start() { + pimpl->start(); +} + +///===================================================================================================================== +void rew::TcpHttpServer::stop() { + pimpl->stop(); +} + diff --git a/src/rew/encoder/wav_writer.cpp b/src/rew/encoder/wav_writer.cpp index 2cb3acee6ab9e11c265e6b0597c9ed1c3a895fa5..da7d0a5df48a1fc7f909f5a546bea2cdc1bcc18b 100644 --- a/src/rew/encoder/wav_writer.cpp +++ b/src/rew/encoder/wav_writer.cpp @@ -90,7 +90,6 @@ bool rew::WavWriter::write(const unsigned char* data, const size_t length) { output.write(reinterpret_cast<const char*>(data), length); size += length; - std::cout << "WavWriter size: " << size << std::endl; return true; }