first
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
// svctx — TID-aware unequal-error-protection (UEP) injector.
|
||||
//
|
||||
// Reads a sequence of length-prefixed HEVC NAL units from stdin, classifies
|
||||
// each by its temporal_id / criticality (svc_tx.h), and injects it at the PHY
|
||||
// rate its layer deserves (robust MCS for base/IDR, fast MCS for enhancement).
|
||||
// The per-packet radiotap carries the rate, so each frame is an independent
|
||||
// PHY mode (radiotap-per-packet wins; see RtlJaguarDevice::send_packet).
|
||||
//
|
||||
// Input protocol (stdin): <u32_le len><len bytes of NAL> ... EOF
|
||||
// (Length-prefixed = the AVCC/HVCC/MP4 framing many depacketizers already
|
||||
// produce. A production link would feed Annex-B or RTP instead — same NAL
|
||||
// header parse, trivial framing adapter.)
|
||||
//
|
||||
// All records are read up front, then injected in a continuous loop (so a
|
||||
// receiver can capture a stable rate histogram). A live deployment would
|
||||
// inject each NAL as it arrives, once.
|
||||
//
|
||||
// Usage:
|
||||
// DEVOURER_PID=0x8812 DEVOURER_CHANNEL=6 ./build/svctx \
|
||||
// [--mtu N] [--gap-us US] < nals.bin
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX // keep <windows.h> from defining min()/max() macros (breaks std::min)
|
||||
#endif
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
/* libusb.h explicitly: the pre-seam RtlUsbAdapter.h used to pull it in
|
||||
* for every consumer; the bus-neutral RtlAdapter.h no longer does. */
|
||||
#include <libusb.h>
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#include <windows.h>
|
||||
typedef int pid_t;
|
||||
#define sleep(seconds) Sleep((seconds)*1000)
|
||||
#elif defined(__MINGW32__) || defined(__MINGW64__)
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#elif defined(__ANDROID__)
|
||||
#include <libusb.h>
|
||||
#include <unistd.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <unistd.h>
|
||||
#include <libusb.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#endif
|
||||
|
||||
#include "DeviceSession.h"
|
||||
#include "RadiotapBuilder.h"
|
||||
#include "RtlAdapter.h"
|
||||
#include "UsbOpen.h"
|
||||
#include "WiFiDriver.h"
|
||||
#include "env_config.h"
|
||||
#include "logger.h"
|
||||
#include "stream_stdin.h"
|
||||
#include "svc_tx.h"
|
||||
|
||||
#define USB_VENDOR_ID 0x0bda
|
||||
static constexpr uint16_t kRealtekProductIds[] = {
|
||||
0x8812, 0x0811, 0xa811, 0xb811, 0x8813,
|
||||
};
|
||||
static const uint8_t kCanonicalSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00};
|
||||
|
||||
static std::vector<uint8_t> build_dot11_probe_req() {
|
||||
std::vector<uint8_t> h = {0x40, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
|
||||
h.insert(h.end(), kCanonicalSa, kCanonicalSa + 6);
|
||||
h.insert(h.end(), kCanonicalSa, kCanonicalSa + 6);
|
||||
h.push_back(0x80);
|
||||
h.push_back(0x00);
|
||||
return h;
|
||||
}
|
||||
|
||||
static const char* mode_str(const devourer::TxMode& m) {
|
||||
static char buf[24];
|
||||
if (m.mode == devourer::TxMode::Mode::HT)
|
||||
std::snprintf(buf, sizeof(buf), "MCS%u/%u%s%s%s", m.ht_mcs, m.bw_mhz,
|
||||
m.sgi ? "/SGI" : "", m.ldpc ? "/LDPC" : "",
|
||||
m.stbc ? "/STBC" : "");
|
||||
else if (m.mode == devourer::TxMode::Mode::VHT)
|
||||
std::snprintf(buf, sizeof(buf), "VHT%uSS_MCS%u/%u", m.vht_nss, m.vht_mcs,
|
||||
m.bw_mhz);
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%uM", m.legacy_rate_500kbps / 2);
|
||||
return buf;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
auto logger = std::make_shared<Logger>();
|
||||
apply_logging_env(*logger); /* DEVOURER_LOG_LEVEL / DEVOURER_EVENTS / ... */
|
||||
|
||||
size_t mtu = 1400;
|
||||
long gap_us = 2000;
|
||||
long termux_fd = 0;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--mtu" && i + 1 < argc)
|
||||
mtu = static_cast<size_t>(std::strtoul(argv[++i], nullptr, 0));
|
||||
else if (a == "--gap-us" && i + 1 < argc)
|
||||
gap_us = std::strtol(argv[++i], nullptr, 0);
|
||||
else {
|
||||
char* end = nullptr;
|
||||
long v = std::strtol(a.c_str(), &end, 0);
|
||||
if (end && *end == '\0' && v > 0) termux_fd = v;
|
||||
}
|
||||
}
|
||||
if (mtu < 16) mtu = 16;
|
||||
stream_stdin::set_stdin_binary();
|
||||
|
||||
libusb_context* context = nullptr;
|
||||
libusb_device_handle* handle = nullptr;
|
||||
int rc;
|
||||
/* Owns the teardown order (device -> interface -> handle -> context; see
|
||||
* DeviceSession.h). Each early return from here on unwinds whatever has
|
||||
* been adopted so far. */
|
||||
devourer::DeviceSession session{logger};
|
||||
if (termux_fd > 0) {
|
||||
libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY);
|
||||
libusb_set_option(NULL, LIBUSB_OPTION_WEAK_AUTHORITY);
|
||||
libusb_init(&context);
|
||||
session.adopt_context(context);
|
||||
rc = libusb_wrap_sys_device(context, (intptr_t)termux_fd, &handle);
|
||||
if (rc < 0) { logger->error("wrap_sys_device: {}", rc); return 1; }
|
||||
} else {
|
||||
rc = libusb_init(&context);
|
||||
if (rc < 0) return rc;
|
||||
session.adopt_context(context);
|
||||
uint16_t target_pid = 0;
|
||||
if (const char* p = std::getenv("DEVOURER_PID"))
|
||||
target_pid = static_cast<uint16_t>(std::strtoul(p, nullptr, 0));
|
||||
uint16_t target_vid = USB_VENDOR_ID;
|
||||
if (const char* v = std::getenv("DEVOURER_VID"))
|
||||
target_vid = static_cast<uint16_t>(std::strtoul(v, nullptr, 0));
|
||||
for (uint16_t pid : kRealtekProductIds) {
|
||||
if (target_pid != 0 && pid != target_pid) continue;
|
||||
handle = libusb_open_device_with_vid_pid(context, target_vid, pid);
|
||||
if (handle != NULL) { logger->info("Opened {:04x}:{:04x}", target_vid, pid); break; }
|
||||
}
|
||||
if (handle == NULL && target_pid != 0)
|
||||
handle = libusb_open_device_with_vid_pid(context, target_vid, target_pid);
|
||||
if (handle == NULL) { logger->error("No supported device"); return 1; }
|
||||
}
|
||||
|
||||
/* Claim-before-reset (see src/UsbOpen.h): the exclusive claim is the primary
|
||||
* guard — a second devourer on this adapter gets BUSY here and bails before
|
||||
* the reset, so it can't re-enumerate the adapter out from under the owner. */
|
||||
std::shared_ptr<devourer::UsbDeviceLock> usb_lock;
|
||||
const int wifi_iface = devourer::find_wifi_interface(handle);
|
||||
rc = devourer::claim_interface_then_reset(handle, wifi_iface, logger,
|
||||
termux_fd == 0 && std::getenv("DEVOURER_SKIP_RESET") == nullptr, usb_lock);
|
||||
if (rc != 0) {
|
||||
/* The claim failed, so nothing owns the handle yet — hand it to the
|
||||
* session purely so the unwind closes it. */
|
||||
session.adopt_handle(handle, wifi_iface);
|
||||
return 1;
|
||||
}
|
||||
session.adopt_handle(handle, wifi_iface);
|
||||
session.adopt_lock(usb_lock);
|
||||
|
||||
WiFiDriver wifi_driver{logger};
|
||||
auto owned_device = wifi_driver.CreateRadio(handle, nullptr, usb_lock,
|
||||
devourer_config_from_env());
|
||||
/* The session owns the device from here: it is what guarantees the device
|
||||
* (and its in-flight TX) dies before libusb does. */
|
||||
session.adopt_device(std::move(owned_device));
|
||||
IRadio *const rtlDevice = session.device();
|
||||
|
||||
int channel = 6;
|
||||
if (const char* ch = std::getenv("DEVOURER_CHANNEL")) channel = std::atoi(ch);
|
||||
/* DEVOURER_TX_POWER: flat TXAGC index. Unset = the family's calibrated
|
||||
* default (SetTxPower is now a real flat override on every generation). */
|
||||
if (const char* p = std::getenv("DEVOURER_TX_POWER"))
|
||||
rtlDevice->SetTxPower(static_cast<uint8_t>(std::atoi(p)));
|
||||
rtlDevice->InitWrite(SelectedChannel{.Channel = static_cast<uint8_t>(channel),
|
||||
.ChannelOffset = 0,
|
||||
.ChannelWidth = CHANNEL_WIDTH_20});
|
||||
sleep(2);
|
||||
|
||||
// Policy (DEVOURER_SVC_LADDER or the built-in default) + precomputed
|
||||
// radiotaps (one per rung — built once, reused).
|
||||
const svc::LayerPolicy policy = svc::policy_from_env();
|
||||
std::vector<uint8_t> rt_crit = devourer::build_stream_radiotap(policy.critical);
|
||||
std::vector<std::vector<uint8_t>> rt_tid;
|
||||
for (const auto& m : policy.by_tid)
|
||||
rt_tid.push_back(devourer::build_stream_radiotap(m));
|
||||
logger->info("SVC UEP policy: critical={}", mode_str(policy.critical));
|
||||
for (size_t i = 0; i < policy.by_tid.size(); ++i)
|
||||
logger->info(" T{} -> {}", i, mode_str(policy.by_tid[i]));
|
||||
|
||||
const auto dot11 = build_dot11_probe_req();
|
||||
|
||||
// Read all length-prefixed NALs up front.
|
||||
std::vector<std::vector<uint8_t>> nals;
|
||||
while (true) {
|
||||
std::vector<uint8_t> nal;
|
||||
/* Any incomplete record ends the read-ahead: this loop drains a whole clip
|
||||
* before transmitting anything, so a truncated tail is simply where the
|
||||
* clip stops. */
|
||||
if (stream_stdin::read_record(stdin, nal, 200000) !=
|
||||
stream_stdin::RecordResult::Ok)
|
||||
break;
|
||||
nals.push_back(std::move(nal));
|
||||
}
|
||||
if (nals.empty()) { logger->error("no NALs on stdin"); return 2; }
|
||||
logger->info("svctx: {} NALs, mtu={}, ch{} — looping", nals.size(), mtu,
|
||||
channel);
|
||||
|
||||
long sent[9] = {0}; // [0..7] per TID, [8] = critical
|
||||
long frames = 0;
|
||||
while (true) {
|
||||
for (const auto& nal : nals) {
|
||||
svc::NalInfo info = svc::parse_hevc_nal(nal.data(), nal.size());
|
||||
const std::vector<uint8_t>& rt =
|
||||
info.critical ? rt_crit
|
||||
: rt_tid.empty() ? rt_crit
|
||||
: rt_tid[info.tid < rt_tid.size() ? info.tid : rt_tid.size() - 1];
|
||||
sent[info.critical ? 8 : (info.tid > 7 ? 7 : info.tid)]++;
|
||||
// Fragment the NAL to the radio MTU; every fragment carries the layer rate.
|
||||
for (size_t off = 0; off < nal.size(); off += mtu) {
|
||||
size_t n = std::min(mtu, nal.size() - off);
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(rt.size() + dot11.size() + n);
|
||||
frame.insert(frame.end(), rt.begin(), rt.end());
|
||||
frame.insert(frame.end(), dot11.begin(), dot11.end());
|
||||
frame.insert(frame.end(), nal.begin() + off, nal.begin() + off + n);
|
||||
rtlDevice->send_packet(frame.data(), frame.size());
|
||||
if (gap_us > 0)
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(gap_us));
|
||||
}
|
||||
/* Per-layer injection counters. JSON keys are lowercase (t3plus stands
|
||||
* in for "T3+": temporal layers 3..7 aggregated). */
|
||||
if (++frames % 200 == 0) {
|
||||
devourer::Ev(logger->events(), "svc.stats")
|
||||
.f("frames", frames)
|
||||
.f("crit", sent[8])
|
||||
.f("t0", sent[0])
|
||||
.f("t1", sent[1])
|
||||
.f("t2", sent[2])
|
||||
.f("t3plus", sent[3] + sent[4] + sent[5] + sent[6] + sent[7]);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Device, then interface, handle and context (DeviceSession.h). Explicit
|
||||
* only because the process has nothing left to do here — the destructor
|
||||
* does exactly the same on every other exit path. */
|
||||
session.close();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// TID -> TxMode unequal-error-protection (UEP) shim for SVC-T video over
|
||||
// devourer. Maps each HEVC NAL's temporal_id (and IRAP/parameter-set
|
||||
// criticality) to a PHY TxMode, so the most important layers fly at the most
|
||||
// robust rate and the enhancement layers ride the fast rate — a graceful
|
||||
// degradation staircase instead of a single MCS cliff.
|
||||
//
|
||||
// App-level (no WiFiDriver core changes): consumes devourer::TxMode +
|
||||
// build_stream_radiotap + RtlJaguarDevice::send_packet.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "RadiotapBuilder.h" // parse_tx_mode_str
|
||||
#include "TxMode.h"
|
||||
|
||||
namespace svc {
|
||||
|
||||
// Per-temporal-layer PHY policy. by_tid[i] = mode for temporal layer i; TIDs
|
||||
// past the end clamp to the last (least-protected) entry. `critical` overrides
|
||||
// for frames you cannot lose: IRAP/IDR slices and parameter sets.
|
||||
struct LayerPolicy {
|
||||
std::vector<devourer::TxMode> by_tid;
|
||||
devourer::TxMode critical;
|
||||
|
||||
const devourer::TxMode& mode_for(uint8_t tid, bool is_critical) const {
|
||||
if (is_critical || by_tid.empty()) return critical;
|
||||
return by_tid[tid < by_tid.size() ? tid : by_tid.size() - 1];
|
||||
}
|
||||
};
|
||||
|
||||
inline devourer::TxMode HT(uint8_t mcs, uint8_t bw = 20, bool ldpc = false,
|
||||
bool stbc = false, bool sgi = false) {
|
||||
devourer::TxMode m;
|
||||
m.mode = devourer::TxMode::Mode::HT;
|
||||
m.ht_mcs = mcs;
|
||||
m.bw_mhz = bw;
|
||||
m.ldpc = ldpc;
|
||||
m.stbc = stbc;
|
||||
m.sgi = sgi;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Default 3-temporal-layer ladder for an 8812AU long-range link.
|
||||
// critical (IDR / VPS/SPS/PPS) : MCS0 20MHz LDPC STBC — max range
|
||||
// T0 base : MCS1 20MHz LDPC STBC
|
||||
// T1 (->30fps) : MCS4 20MHz
|
||||
// T2 (->60fps) : MCS7 40MHz SGI — max throughput
|
||||
inline LayerPolicy default_policy() {
|
||||
LayerPolicy p;
|
||||
p.critical = HT(/*mcs*/ 0, /*bw*/ 20, /*ldpc*/ true, /*stbc*/ true);
|
||||
p.by_tid = {
|
||||
HT(1, 20, /*ldpc*/ true, /*stbc*/ true),
|
||||
HT(4, 20),
|
||||
HT(7, 40, /*ldpc*/ false, /*stbc*/ false, /*sgi*/ true),
|
||||
};
|
||||
return p;
|
||||
}
|
||||
|
||||
// HEVC NAL header is 2 bytes:
|
||||
// nal_unit_type = (b0 >> 1) & 0x3F
|
||||
// nuh_temporal_id_plus1 = b1 & 0x07 (TID = that - 1)
|
||||
// Critical = IRAP slices (16..23, incl. IDR/CRA/BLA) or parameter sets
|
||||
// (VPS=32 / SPS=33 / PPS=34) — losing any of these stalls the decoder.
|
||||
struct NalInfo {
|
||||
uint8_t tid = 0;
|
||||
bool critical = false;
|
||||
uint8_t type = 0;
|
||||
};
|
||||
|
||||
inline NalInfo parse_hevc_nal(const uint8_t* nal, size_t len) {
|
||||
NalInfo n;
|
||||
if (len < 2) return n; // malformed -> treat as base layer
|
||||
n.type = (nal[0] >> 1) & 0x3F;
|
||||
int tid = static_cast<int>(nal[1] & 0x07) - 1;
|
||||
n.tid = tid < 0 ? 0 : static_cast<uint8_t>(tid);
|
||||
n.critical = (n.type >= 16 && n.type <= 23) || (n.type >= 32 && n.type <= 34);
|
||||
return n;
|
||||
}
|
||||
|
||||
// Build a LayerPolicy from DEVOURER_SVC_LADDER, a ';'-separated list of
|
||||
// "<key>=<spec>" where <key> is CRIT or T0/T1/T2/... and <spec> is a
|
||||
// DEVOURER_TX_RATE token string (e.g. "MCS0/20/LDPC/STBC"). Tn rungs are
|
||||
// ordered by index into by_tid. Unset -> default_policy().
|
||||
// DEVOURER_SVC_LADDER="CRIT=MCS0/20/LDPC/STBC;T0=MCS1/20/LDPC/STBC;T1=MCS4;T2=MCS7/40/SGI"
|
||||
inline LayerPolicy policy_from_env() {
|
||||
const char* raw = std::getenv("DEVOURER_SVC_LADDER");
|
||||
if (raw == nullptr || *raw == '\0') return default_policy();
|
||||
|
||||
LayerPolicy p;
|
||||
p.critical = HT(0, 20, /*ldpc*/ true, /*stbc*/ true); // default if CRIT omitted
|
||||
std::vector<std::pair<int, devourer::TxMode>> tids;
|
||||
const std::string s(raw);
|
||||
size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
size_t sc = s.find(';', i);
|
||||
std::string tok = s.substr(i, sc == std::string::npos ? std::string::npos : sc - i);
|
||||
i = (sc == std::string::npos) ? s.size() : sc + 1;
|
||||
size_t eq = tok.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string key = tok.substr(0, eq), val = tok.substr(eq + 1);
|
||||
devourer::TxMode m = devourer::parse_tx_mode_str(val);
|
||||
if (key == "CRIT" || key == "crit") {
|
||||
p.critical = m;
|
||||
} else if (key.size() >= 2 && (key[0] == 'T' || key[0] == 't')) {
|
||||
tids.emplace_back(std::atoi(key.c_str() + 1), m);
|
||||
}
|
||||
}
|
||||
std::sort(tids.begin(), tids.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
for (const auto& tm : tids) p.by_tid.push_back(tm.second);
|
||||
if (p.by_tid.empty()) return default_policy();
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace svc
|
||||
Reference in New Issue
Block a user