This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
+262
View File
@@ -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;
}
+120
View File
@@ -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