Compare commits

...

12 Commits

8 changed files with 578 additions and 14 deletions

46
README.md Normal file
View File

@@ -0,0 +1,46 @@
# Video Streaming Proof of Concept
This project is a demo for streaming video output from multiple "client" devices to one "server". This is a basic demo
of what sauron-cv seeks to accomplish.
## Building
This project uses [Meson](https://mesonbuild.com/) for build management. This project also optionally uses
[Nix](https://nix.dev/) for system dependency management.
### Using Nix
```shell
nix develop
```
### Install Dependencies
Install the following packages from your distribution package manager:
- `meson`
- `opencv`
- `boost`
A better procedure for this (hopefully involving Meson) will be added / documented at a later date.
### Setup Meson
```shell
meson setup build
```
*NOTE FOR JETBRAINS / CLION USERS: PLEASE SET YOUR MESON BUILD DIRECTORY TO `build` IN THE IDE SETTINGS UNDER "Build /
Execution / Deployment" -> "Meson"*
### Compiling
```shell
meson build client # for client only
meson build server # for server only
```
## Running
```shell
./build/client # for client application
./build/server # for server application
```

View File

@@ -1,8 +1,60 @@
#include <opencv2/videoio.hpp>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "transfer.h"
#include "logging.h"
#include <chrono>
#include <boost/asio.hpp>
using namespace std;
using boost::asio::ip::tcp;
int main() {
cout << "Hello World!" << endl;
const int FRAME_DELAY_MS = 1000 / 30;
// create video capture
cv::VideoCapture cap = cv::VideoCapture(0);
try {
boost::asio::io_context io_context;
tcp::resolver resolver(io_context);
tcp::resolver::results_type endpoints =
resolver.resolve("127.0.0.1", "8080");
cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
info("Ready to connect.");
// sending connection request
tcp::socket socket(io_context);
boost::asio::connect(socket, endpoints);
info("Connected.");
// create buffer for serialization
vector<uchar> imgbuf;
while (true) {
auto start_time = std::chrono::high_resolution_clock::now();
cap.read(image);
trace("Sending image");
sendImage(socket, image, imgbuf);
auto end_time = std::chrono::high_resolution_clock::now();
auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// Calculate the remaining time to sleep
auto sleep_duration = std::chrono::milliseconds(FRAME_DELAY_MS) - elapsed_time;
// Sleep for the remaining duration if positive
if (sleep_duration.count() > 0) {
std::this_thread::sleep_for(sleep_duration);
}
}
}
catch (std::exception& e) {
error(e.what());
}
return 0;
}

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1756386758,
"narHash": "sha256-1wxxznpW2CKvI9VdniaUnTT2Os6rdRJcRUf65ZK9OtE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "dfb2f12e899db4876308eba6d93455ab7da304cd",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

63
flake.nix Normal file
View File

@@ -0,0 +1,63 @@
{
description = "video-streaming-poc devShell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in with pkgs;
let
opencv-custom = pkgs.opencv.override {
enableGtk3 = true;
#enableCuda = true;
enablePython = true;
};
in {
devShells.default = mkShell rec {
buildInputs = [
# Meson
meson
pkg-config
ninja
# Boost
boost
# OpenCV
opencv-custom
];
};
packages.default = pkgs.stdenv.mkDerivation {
name = "video-streaming-poc";
src = ./.;
nativeBuildInputs = with pkgs; [
meson
ninja
pkg-config
];
buildInputs = with pkgs; [
boost
opencv-custom
];
buildPhase = ''
meson setup --wipe build
meson compile
'';
installPhase = ''
mkdir -p $out/bin
cp build/client $out/bin/
cp build/server $out/bin/
'';
};
}
);
}

170
logging.h Normal file
View File

@@ -0,0 +1,170 @@
#ifndef LOGGING_H
#define LOGGING_H
#include <iostream>
#include <iomanip>
#include <chrono>
#include <ctime>
// Logging levels
#define LOG_ERROR 0
#define LOG_WARN 1
#define LOG_INFO 2
#define LOG_DEBUG 3
#define LOG_TRACE 4
// Set logging arguments
// TODO: set these by compiler arguments or env vars or something
#define LOGGING LOG_DEBUG // logging level
#define LOGGING_COLOR true // enable color
#define LOGGING_TIMESTAMP true // enable timestamp
#define LOGGING_TIMESTAMP_FMT "%Y-%m-%dT%H:%M:%S%z" // timestamp format (local time)
#define LOGGING_POSITION false // display position (only works on C++20 or newer)
// Color codes
#define ANSI_RESET "\033[0m"
#define ANSI_BLACK "\033[30m" /* Black */
#define ANSI_RED "\033[31m" /* Red */
#define ANSI_GREEN "\033[32m" /* Green */
#define ANSI_YELLOW "\033[33m" /* Yellow */
#define ANSI_BLUE "\033[34m" /* Blue */
#define ANSI_MAGENTA "\033[35m" /* Magenta */
#define ANSI_CYAN "\033[36m" /* Cyan */
#define ANSI_WHITE "\033[37m" /* White */
#define ANSI_BOLD "\033[1m" /* Bold */
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args) << "\n";
}
inline void printTimestamp() {
#if LOGGING_TIMESTAMP
auto now = std::chrono::system_clock::now();
auto time_c = std::chrono::system_clock::to_time_t(now);
std::tm time_tm;
localtime_r(&time_c, &time_tm);
std::cout << std::put_time(&time_tm, LOGGING_TIMESTAMP_FMT) << ": ";
#endif
}
// if we're on C++20 or later, then use the source_location header and add source location to logs
#if __cplusplus >= 202002L && LOGGING_POSITION
#include <source_location>
inline void printPosition(std::source_location& location) {
std::cout << location.file_name() << ":" << location.function_name() << ":" << ANSI_CYAN << location.line() << ANSI_RESET << ": ";
}
inline void printHeader(std::string name, std::string color, std::source_location& location) {
#if LOGGING_COLOR
std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
printTimestamp();
printPosition(location);
#else
printHeader(name);
#endif
}
inline void printHeader(std::string name, std::source_location& location) {
std::cout << "[" << name << "] ";
printTimestamp();
printPosition(location);
}
template <typename... Args, typename Sl = std::source_location>
void trace(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_TRACE
printHeader("TRACE", ANSI_CYAN, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void debug(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_DEBUG
printHeader("DEBUG", ANSI_MAGENTA, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void info(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_INFO
printHeader("INFO", ANSI_GREEN, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void warn(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_WARN
printHeader("WARN", ANSI_YELLOW, location);
print(args...);
#endif
}
template <typename... Args, typename Sl = std::source_location>
void error(Args... args, Sl location = std::source_location::current()) {
#if LOGGING >= LOG_ERROR
printHeader("ERROR", ANSI_RED, location);
print(args...);
#endif
}
#else
inline void printHeader(std::string name, std::string color) {
#if LOGGING_COLOR
std::cout << ANSI_BOLD << color << "[" << name << "] " << ANSI_RESET;
printTimestamp();
#else
printHeader(name);
#endif
}
inline void printHeader(std::string name) {
std::cout << "[" << name << "] ";
printTimestamp();
}
template <typename... Args>
void trace(Args... args) {
#if LOGGING >= LOG_TRACE
printHeader("TRACE", ANSI_CYAN);
print(args...);
#endif
}
template <typename... Args>
void debug(Args... args) {
#if LOGGING >= LOG_DEBUG
printHeader("DEBUG", ANSI_MAGENTA);
print(args...);
#endif
}
template <typename... Args>
void info(Args... args) {
#if LOGGING >= LOG_INFO
printHeader("INFO", ANSI_GREEN);
print(args...);
#endif
}
template <typename... Args>
void warn(Args... args) {
#if LOGGING >= LOG_WARN
printHeader("WARN", ANSI_YELLOW);
print(args...);
#endif
}
template <typename... Args>
void error(Args... args) {
#if LOGGING >= LOG_ERROR
printHeader("ERROR", ANSI_RED);
print(args...);
#endif
}
#endif
#endif //LOGGING_H

View File

@@ -1,13 +1,37 @@
project('video-streaming-poc', 'cpp')
add_project_arguments('-Wall', '-Wextra', language : 'cpp')
#=======================================================================================================================
# PROJECT SETTINGS
#=======================================================================================================================
project('video-streaming-poc', 'cpp', version : '0.0.1-SNAPSHOT',
default_options : ['c_std=c17', 'cpp_std=c++20'])
#=======================================================================================================================
# DEPENDENCIES
#=======================================================================================================================
# opencv dependency
opencv = dependency('opencv4', version : '>=4.0.0')
opencv_incl_dir = opencv.get_variable(cmake : 'OpenCV_INCLUDE_DIRECTORIES', pkgconfig : 'includedir')
include = include_directories(opencv_incl_dir)
# boost dependency
boost = dependency('boost')
common = []
#=======================================================================================================================
# SOURCE FILES
#=======================================================================================================================
# common files between client / server
common = ['transfer.h', 'logging.h']
# client-only files
client = common + ['client.cpp']
# server-only files
server = common + ['server.cpp']
client_exe = executable('client', client, dependencies : opencv, include_directories : include)
server_exe = executable('server', server, dependencies : opencv, include_directories : include)
#=======================================================================================================================
# BUILD TARGETS
#=======================================================================================================================
# client executable
client_exe = executable('client', client,
dependencies : [opencv, boost],
include_directories : include)
# server executable
server_exe = executable('server', server,
dependencies : [opencv, boost],
include_directories : include)

View File

@@ -1,16 +1,56 @@
#include <opencv2/highgui.hpp>
#include <opencv2/core/mat.hpp>
#include <opencv2/imgproc.hpp>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "transfer.h"
#include "logging.h"
#include <boost/asio.hpp>
using namespace std;
using boost::asio::ip::tcp;
int main() {
cv::VideoCapture cap = cv::VideoCapture(0);
bool running = true;
cv::Mat image = cv::Mat::zeros(480, 640, CV_8UC3);
while (running) {
cap.read(image);
imshow("image", image);
running = cv::waitKey(30) != 27;
try {
boost::asio::io_context io_context;
// creating socket
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
tcp::socket socket(io_context);
info("Ready to accept connections.");
// accepting connection request
acceptor.accept(socket);
info("Client connected.");
// TODO: handle multiple images
cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
bool running = true;
// TODO: make this asynchronous. probably do that in tandem with setting up networking
while (running) {
// receive data
vector<uchar> buffer;
trace("Receiving image");
int latency = recvImage(socket, buffer);
trace("Applying new data to image");
applyImage(image, &buffer);
cv::putText(image, to_string(latency), cv::Point(0, 480), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(255, 0, 0), 1, cv::LINE_AA);
trace("Displaying image");
imshow("image", image);
running = cv::waitKey(30) != 27;
}
}
catch (std::exception& e) {
error(e.what());
}
return 0;
}
}

108
transfer.h Normal file
View File

@@ -0,0 +1,108 @@
#ifndef TRANSFER_H
#define TRANSFER_H
#include <chrono>
#include <opencv2/core/mat.hpp>
#include <opencv2/imgcodecs.hpp>
#include <sys/socket.h>
#include "logging.h"
#include <vector>
#include <boost/asio.hpp>
using namespace std;
using boost::asio::ip::tcp;
struct imageHeader {
size_t size;
chrono::milliseconds timestamp;
};
chrono::milliseconds getMillis() {
// Get current time
const auto now = chrono::system_clock::now();
// Get time since epoch in milliseconds
const chrono::duration ms = chrono::duration_cast<chrono::milliseconds>(now.time_since_epoch());
return chrono::milliseconds(ms.count());
}
inline void serializeImage(const cv::Mat& image, std::vector<uchar>& buffer) {
cv::imencode(".jpg", image, buffer);
}
void sendImage(tcp::socket& socket, const cv::Mat& image, std::vector<uchar>& buffer) {
boost::system::error_code e;
serializeImage(image, buffer);
size_t totalSent = 0;
// first send the size of the serialized image
const size_t size = buffer.size();
const chrono::milliseconds timestamp = getMillis();
imageHeader header;
header.size = size;
header.timestamp = timestamp;
trace("Buffer size: ", size);
if (const ssize_t sent = boost::asio::write(socket, boost::asio::buffer(&header, sizeof(header)), e); sent == -1) {
throw boost::system::system_error(e);
}
// then start sending the serialized image
while (totalSent < size) {
const ssize_t sent = boost::asio::write(socket, boost::asio::buffer(buffer), e);
if (sent == -1) {
throw boost::system::system_error(e);
}
totalSent += sent;
debug("Packet sent (", sent, " bytes, total ", totalSent, " / ", size, " bytes)");
}
}
int recvImage(tcp::socket& socket, std::vector<uchar>& buffer) {
boost::system::error_code e;
// first receive the size of the image
imageHeader header;
size_t len = 0;
while (len < sizeof(imageHeader)) {
len += socket.read_some(boost::asio::buffer(&header, sizeof(header)), e);
if (e.failed()) {
throw boost::system::system_error(e);
}
}
size_t dataSize = header.size;
chrono::milliseconds sentTime = header.timestamp;
trace("Buffer size: ", dataSize);
// resize the buffer to fit the whole image
buffer.resize(dataSize);
// start receiving the image until the whole thing arrives
size_t totalReceived = 0;
while (totalReceived < dataSize) {
size_t bytesReceived = boost::asio::read(socket, boost::asio::buffer(buffer), e);
if (e == boost::asio::error::eof) // Connection closed cleanly by peer.
throw boost::system::system_error(e);
else if (e)
throw boost::system::system_error(e);
totalReceived += bytesReceived;
debug("Packet received (", bytesReceived, " bytes, total ", totalReceived, " / ", dataSize, " bytes)");
}
chrono::milliseconds currentTime = getMillis();
chrono::milliseconds diff = currentTime - sentTime;
debug("Packet latency: ", diff.count(), "ms");
return diff.count();
}
bool applyImage(cv::Mat& image, std::vector<uchar> *src) {
// decode the image into an OpenCV Mat
cv::imdecode(*src, cv::IMREAD_UNCHANGED, &image);
return true;
}
#endif //TRANSFER_H