Skip to content

Commit

Permalink
[FEAT] Add basic ABR impl
Browse files Browse the repository at this point in the history
  • Loading branch information
nots1dd committed Feb 23, 2025
1 parent 295ff2a commit d9d3b1b
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Wavy is a **lightweight** and **efficient** solution for audio streaming within
It supports:
- **Lossless formats** (FLAC, ALAC, WAV)
- **Lossy formats** (MP3, AAC, Opus, Vorbis)
- **Adaptive bitrate streaming** using **HLS (HTTP Live Streaming)**.
- **Adaptive bitrate streaming** (Soon) using **HLS (HTTP Live Streaming)**.
- **Metadata extraction** and **TOML-based** configuration.
- **Transport stream decoding** via **FFmpeg** for real-time audio playback.

Expand Down
90 changes: 90 additions & 0 deletions include/abr/ABRManager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

#include "PlaylistParser.hpp"
#include "NetworkDiagnoser.hpp"
#include <vector>
#include <algorithm>

class ABRManager {
public:
ABRManager(boost::asio::io_context &ioc, const std::string &master_url)
: ioc_(ioc),
ssl_ctx_(boost::asio::ssl::context::sslv23),
parser_(ioc, ssl_ctx_, master_url),
network_(ioc, master_url) {}

void selectBestBitrate() {
if (!parser_.fetchMasterPlaylist()) {
std::cerr << "[ERROR] Failed to fetch master playlist.\n";
return;
}

NetworkStats stats = network_.diagnoseNetworkSpeed();
if (stats.latency < 0) {
std::cerr << "[ERROR] Network diagnosis failed.\n";
return;
}

std::map<int, std::string> playlists = parser_.getBitratePlaylists();
if (playlists.empty()) {
std::cerr << "[ERROR] No available bitrates in playlist.\n";
return;
}

// Extract bitrates and sort them in ascending order
std::vector<int> available_bitrates;
for (const auto &[bitrate, _] : playlists) {
available_bitrates.push_back(bitrate);
}
std::sort(available_bitrates.begin(), available_bitrates.end());

int selected_bitrate = determineBestBitrate(stats, available_bitrates);
std::cout << "[INFO] Selected Best Bitrate: " << selected_bitrate << " kbps\n";
}

private:
boost::asio::io_context &ioc_;
boost::asio::ssl::context ssl_ctx_;
PlaylistParser parser_;
NetworkDiagnoser network_;

/**
* Determines the best bitrate based on network statistics.
*/
int determineBestBitrate(const NetworkStats &stats, const std::vector<int> &bitrates) {
if (bitrates.empty()) return 64000; // Default minimum bitrate

// Define network thresholds
constexpr int LOW_LATENCY = 80;
constexpr int MEDIUM_LATENCY = 150;
constexpr int HIGH_LATENCY = 250;
constexpr double MAX_PACKET_LOSS = 20.0;
constexpr double MAX_JITTER = 50.0;

int selected_bitrate = bitrates.front(); // Start with the lowest bitrate

for (int bitrate : bitrates) {
if (stats.packet_loss > MAX_PACKET_LOSS || stats.jitter > MAX_JITTER) {
// High packet loss or jitter -> stick to lowest bitrate
break;
}
if (stats.latency < LOW_LATENCY) {
// Low latency -> Choose highest available bitrate
selected_bitrate = bitrate;
}
else if (stats.latency < MEDIUM_LATENCY) {
// Medium latency -> Choose medium bitrate
if (bitrate <= bitrates[bitrates.size() / 2]) {
selected_bitrate = bitrate;
}
}
else if (stats.latency < HIGH_LATENCY) {
// High latency -> Choose lowest available bitrate
selected_bitrate = bitrates.front();
break;
}
}

return selected_bitrate;
}
};
7 changes: 7 additions & 0 deletions include/abr/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CPP := g++
SRC := main.cpp
LIBS := -lboost_log -lboost_log_setup -DBOOST_LOG_DLL -lboost_filesystem -lboost_system -lssl -lcrypto
BIN := abr

abr:
$(CPP) -o $(BIN) $(SRC) $(LIBS)
105 changes: 105 additions & 0 deletions include/abr/NetworkDiagnoser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#pragma once

#include <boost/asio.hpp>
#include <iostream>
#include <chrono>
#include <random>

namespace net = boost::asio;
using tcp = net::ip::tcp;
using namespace std::chrono;

struct NetworkStats {
int latency; // In milliseconds
double jitter; // In milliseconds
double packet_loss; // Percentage (0-100%)
};

class NetworkDiagnoser {
public:
NetworkDiagnoser(net::io_context &ioc, const std::string &server_url)
: resolver_(ioc), socket_(ioc), server_url_(server_url) {}

NetworkStats diagnoseNetworkSpeed() {
std::string host, port, target;
parseUrl(server_url_, host, port, target);

NetworkStats stats = { -1, 0.0, 0.0 }; // Default invalid values

try {
std::cout << "[INFO] Diagnosing network speed to: " << host << "\n";
auto const results = resolver_.resolve(host, port);

std::vector<int> latencies;

for (int i = 0; i < 5; ++i) { // Send 5 probes to calculate jitter
auto start = high_resolution_clock::now();
socket_.connect(*results.begin());
auto end = high_resolution_clock::now();
int ping_time = duration_cast<milliseconds>(end - start).count();
latencies.push_back(ping_time);
socket_.close();
}

stats.latency = calculateAverage(latencies);
stats.jitter = calculateJitter(latencies);
stats.packet_loss = simulatePacketLoss(); // Simulating packet loss

std::cout << "[INFO] Network Latency: " << stats.latency << " ms\n";
std::cout << "[INFO] Network Jitter: " << stats.jitter << " ms\n";
std::cout << "[INFO] Packet Loss: " << stats.packet_loss << "%\n";

} catch (const std::exception &e) {
std::cerr << "[ERROR] Network speed diagnosis failed: " << e.what() << "\n";
}

return stats;
}

private:
tcp::resolver resolver_;
tcp::socket socket_;
std::string server_url_;

void parseUrl(const std::string &url, std::string &host, std::string &port, std::string &target) {
size_t pos = url.find("//");
size_t start = (pos == std::string::npos) ? 0 : pos + 2;
size_t end = url.find('/', start);

std::string full_host = url.substr(start, end - start);
size_t port_pos = full_host.find(':');

if (port_pos != std::string::npos) {
host = full_host.substr(0, port_pos);
port = full_host.substr(port_pos + 1);
} else {
host = full_host;
port = "443"; // Default HTTPS port
}

target = (end == std::string::npos) ? "/" : url.substr(end);
}

int calculateAverage(const std::vector<int>& values) {
int sum = 0;
for (int v : values) sum += v;
return values.empty() ? -1 : sum / values.size();
}

double calculateJitter(const std::vector<int>& latencies) {
if (latencies.size() < 2) return 0.0;

double sum = 0.0;
for (size_t i = 1; i < latencies.size(); ++i) {
sum += std::abs(latencies[i] - latencies[i - 1]);
}
return sum / (latencies.size() - 1);
}

double simulatePacketLoss() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dist(0.0, 10.0); // Simulating 0-10% loss
return dist(gen);
}
};
137 changes: 137 additions & 0 deletions include/abr/PlaylistParser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#pragma once

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast.hpp>
#include <iostream>
#include <map>
#include <sstream>
#include <string>

namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = net::ip::tcp;

class PlaylistParser {
public:
PlaylistParser(net::io_context &ioc, ssl::context &ssl_ctx, const std::string &url)
: resolver_(ioc), stream_(ioc, ssl_ctx), master_url_(url) {}

bool fetchMasterPlaylist() {
std::string host, port, target;
parseUrl(master_url_, host, port, target);

std::cout << "[INFO] Fetching master playlist from: " << master_url_ << "\n";

try {
std::cout << "[INFO] Resolving host: " << host << " on port " << port << "\n";
auto const results = resolver_.resolve(host, port);

std::cout << "[INFO] Connecting to: " << host << ":" << port << "\n";
beast::get_lowest_layer(stream_).connect(results);
stream_.handshake(ssl::stream_base::client);

http::request<http::string_body> req{http::verb::get, target, 11};
req.set(http::field::host, host);
req.set(http::field::user_agent, "Boost.Beast");

std::cout << "[INFO] Sending HTTP GET request...\n";
http::write(stream_, req);

beast::flat_buffer buffer;
http::response<http::dynamic_body> res;
http::read(stream_, buffer, res);

std::cout << "[INFO] Received HTTP Response. Status: " << res.result_int() << "\n";
beast::error_code ec;
stream_.shutdown(ec);
if (ec && ec != beast::errc::not_connected) {
std::cerr << "[WARN] SSL Shutdown failed: " << ec.message() << "\n";
}

std::string body = beast::buffers_to_string(res.body().data());

parsePlaylist(body);
return true;
} catch (const std::exception &e) {
std::cerr << "[ERROR] Error fetching master playlist: " << e.what() << "\n";
return false;
}
}

std::map<int, std::string> getBitratePlaylists() const {
return bitrate_playlists_;
}

private:
net::ip::tcp::resolver resolver_;
ssl::stream<beast::tcp_stream> stream_;
std::string master_url_;
std::map<int, std::string> bitrate_playlists_;

void parseUrl(const std::string &url, std::string &host, std::string &port, std::string &target) {
size_t pos = url.find("//");
size_t start = (pos == std::string::npos) ? 0 : pos + 2;
size_t end = url.find('/', start);

std::string full_host = url.substr(start, end - start);
size_t port_pos = full_host.find(':');

if (port_pos != std::string::npos) {
host = full_host.substr(0, port_pos);
port = full_host.substr(port_pos + 1);
} else {
host = full_host;
port = "443"; // Default HTTPS port
}

target = (end == std::string::npos) ? "/" : url.substr(end);
std::cout << "[DEBUG] Parsed Host: " << host << ", Port: " << port << ", Target: " << target << "\n";
}

void parsePlaylist(const std::string &playlist) {
std::istringstream ss(playlist);
std::string line;
int current_bitrate = 0;

while (std::getline(ss, line)) {
if (line.find("#EXT-X-STREAM-INF") != std::string::npos) {
size_t bitrate_pos = line.find("BANDWIDTH=");
if (bitrate_pos != std::string::npos) {
size_t start = bitrate_pos + 9; // Position after "BANDWIDTH="
size_t end = line.find(',', start); // Find next comma

std::string bitrate_str = line.substr(start, end - start);

// Remove any '=' sign if it appears (edge case)
bitrate_str.erase(std::remove(bitrate_str.begin(), bitrate_str.end(), '='), bitrate_str.end());

std::cout << "[DEBUG] Extracted Bitrate String: '" << bitrate_str << "'\n";

// Ensure it's numeric
if (!bitrate_str.empty() && std::all_of(bitrate_str.begin(), bitrate_str.end(), ::isdigit)) {
try {
current_bitrate = std::stoi(bitrate_str);
std::cout << "[INFO] Parsed Bitrate: " << current_bitrate << " kbps\n";
} catch (const std::exception &e) {
std::cerr << "[ERROR] Failed to parse bitrate: " << e.what() << "\n";
continue;
}
} else {
std::cerr << "[ERROR] Invalid bitrate format: '" << bitrate_str << "'\n";
continue;
}
}
} else if (!line.empty() && line[0] != '#') {
if (current_bitrate > 0) {
bitrate_playlists_[current_bitrate] = line;
std::cout << "[INFO] Added bitrate playlist: " << current_bitrate << " -> " << line << "\n";
} else {
std::cerr << "[ERROR] Playlist URL found but no valid bitrate!\n";
}
}
}
}
};
Binary file added include/abr/abr
Binary file not shown.
28 changes: 28 additions & 0 deletions include/abr/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "ABRManager.hpp"
#include <boost/asio.hpp>

auto main(int argc, char* argv[]) -> int
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <network-stream>" << std::endl;
return EXIT_FAILURE;
}
try
{
boost::asio::io_context ioc;

// Replace with your HLS master playlist URL
std::string master_url = argv[1];

ABRManager abr_manager(ioc, master_url);
abr_manager.selectBestBitrate();
}
catch (const std::exception& e)
{
std::cerr << "[ERROR] Exception: " << e.what() << "\n";
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}

0 comments on commit d9d3b1b

Please sign in to comment.