An analysis of how the PS4/PS5 Remote Play protocol (Takion) handles packet loss and rate adaptation, as implemented by this client, with a comparison against Parsec's BUD protocol and Sunshine/Moonlight.
Scope and method. Everything below was read from the client implementation in lib/. The
console side is closed source, so where the protocol carries a value the console chooses, that
is stated explicitly rather than guessed at. Line references are to the tree at the time of
writing.
One-line summary. Takion is architecturally a Sunshine-class design: unreliable media protected by Reed-Solomon FEC, no NACK, no retransmission of video, and no congestion control of any kind on the client. It is not a Parsec-class design.
On the FEC ratio specifically: the client cannot set it, directly or indirectly by any
supported means (§7). The console chooses k and m per frame, and captured traffic shows it
choosing well — 33-40% redundancy is typical under load, with 50% observed (§2.4). The poor
behaviour on lossy links is therefore not an under-provisioned FEC ratio. It is block FEC
meeting burst loss with a block of five to ten packets emitted inside one millisecond, plus a
rate loop that reports at 5 Hz into closed firmware (§9).
Takion multiplexes every channel onto one UDP socket. A datagram is dispatched on the low
nibble of byte 0 (lib/src/takion.c:62-71):
| Type | Channel | Reliability |
|---|---|---|
| 0 | CONTROL — SCTP-shaped: INIT/INIT_ACK/COOKIE/COOKIE_ACK/DATA/DATA_ACK |
reliable, cumulative ACK + fixed RTO |
| 1 | FEEDBACK_HISTORY — discrete input events (button edges, touch) |
unreliable + repetition |
| 2 | VIDEO |
unreliable, FEC only |
| 3 | AUDIO |
unreliable, repetition only |
| 4 | HANDSHAKE |
setup |
| 5 | CONGESTION — client to console loss report |
unreliable, fire and forget |
| 6 | FEEDBACK_STATE — full analog controller snapshot |
unreliable, idempotent |
| 8 | CLIENT_INFO |
setup |
Application-level control messages are nanopb-encoded protobuf (lib/protobuf/takion.proto)
carried inside type 0.
lib/src/fec.c is Cauchy Reed-Solomon over GF(2^8) (CHIAKI_FEC_WORDSIZE 8) via jerasure.
Each frame is split into k source units plus m parity units; any k of the k+m units
reconstructs the frame by erasure decode.
k and m are chosen by the console, per frame. The client reads them straight out of the
AV packet header and never negotiates or requests a ratio:
/* lib/src/frameprocessor.c:76-77 */
frame_processor->units_source_expected = packet->units_in_frame_total - packet->units_in_frame_fec;
frame_processor->units_fec_expected = packet->units_in_frame_fec;The frame is flushed as soon as enough units have arrived, rather than waiting for the tail
(chiaki_frame_processor_flush_possible, lib/include/chiaki/frameprocessor.h:67):
return frame_processor->units_source_received + frame_processor->units_fec_received
>= frame_processor->units_source_expected;UNIT_SLOTS_MAX is 512, raised from 256 because high bitrates overflowed it.
Nothing in the AV path acknowledges anything. There is no NACK, no selective repeat, no retransmit timer, no per-packet sequence tracking used for repair. Video sequence numbers exist only for reordering and loss accounting.
- FEC decode. Covers up to
mlost units per frame. - Reference-frame remap. If a P-slice names a reference frame that was never successfully
decoded, the client rewrites
ref_idxin the bitstream to point at the nearest surviving reference (lib/src/videoreceiver.c:271-290, viachiaki_bitstream_slice_set_reference_frame). This is decoder-side concealment added by chiaki-ng, not part of the protocol. - Give up on the frame. Send
CORRUPTFRAME(start, end)on the reliable control channel (lib/src/streamconnection.c:1263), drop the frame, and count it lost. - Optionally send
IDRREQUEST(lib/src/streamconnection.c:1285) and skip P-frames until an I-slice arrives.
Step 4 is opt-in and off by default. enable_idr_on_fec_failure defaults to false
(gui/include/settings.h:302). Redundant requests are further suppressed by a
waiting_for_idr latch. This is a deliberately conservative default and the right one for a
lossy link: at 60 fps, a client-driven IDR per failed frame is worse than the artifact it
repairs.
Step 3 is unconditional, and it is not free. CORRUPTFRAME is always sent, both on FEC
failure and on a detected frame-index gap (lib/src/videoreceiver.c:165). What the console does
with it is console-side policy — very likely a keyframe. So IDR-driven flicker can still occur
with enable_idr_on_fec_failure off; the client simply is not the one asking. There is no
client-side way to suppress CORRUPTFRAME.
The FEC decode test vectors in test/fec_test_cases.inl are 64 real captured frames. Their
(k, m) distribution is direct evidence of the console's protection policy:
k,m |
Count | Redundancy m/(k+m) |
|---|---|---|
| 3,2 | 31 | 40.0% |
| 4,2 | 20 | 33.3% |
| 5,2 | 4 | 28.6% |
| 8,1 | 2 | 11.1% |
| 7,2 | 1 | 22.2% |
| 6,4 | 1 | 40.0% |
| 6,2 | 1 | 25.0% |
| 6,1 | 1 | 14.3% |
| 5,3 | 1 | 37.5% |
| 4,4 | 1 | 50.0% |
| 4,3 | 1 | 42.9% |
Plus one live-parsed packet in test/takion.c:44-45 at k=7, m=1 (12.5%).
The console does adapt m, and it goes high — the mode is 40% redundancy and the ceiling
observed is 50%. This is at or above what Sunshine users configure by hand. Any claim that
Remote Play performs poorly on lossy links because it under-provisions FEC is not supported
by this data.
Caveat on the sample. These are FEC decode vectors, captured precisely at moments when
units were missing. They are biased toward the lossy regime — which is the regime of interest
here — and are not representative of steady state on a clean link, where m is presumably
lower. Note also that k is small throughout (3 to 8), which matters a great deal for burst
loss; see §9.
Video and audio each get a reorder queue with a 16 ms head-of-line timeout
(TAKION_AV_REORDER_TIMEOUT_US 16000, ~1 frame at 60 fps,
lib/src/takion.c:942-1025). On timeout it skips directly to the first buffered packet, so a
loss burst pays one timeout rather than one per missing packet. This absorbs Wi-Fi jitter
without stalling on genuine loss.
Audio is not Reed-Solomon. An audio AV packet carries source_units_count fresh Opus frames
followed by fec_units_count copies of previous frames
(lib/src/audioreceiver.c:119-134):
frame_index = packet->frame_index - fec_units_count + fec_index;Plain repetition with an offset. Losing one packet is covered by the next. Note the packed
header: units_in_frame_fec holds unit size in the high byte, FEC count in bits 4-7, source
count in bits 0-3 (lib/include/chiaki/takion.h:55-57).
Input uses two complementary schemes:
FEEDBACK_STATE(type 6): a full analog snapshot sent every 8-200 ms on change (FEEDBACK_STATE_TIMEOUT_MIN_MS 8/MAX_MS 200). Unreliable and never retransmitted — state is idempotent, so a lost packet self-heals on the next one.FEEDBACK_HISTORY(type 1): discrete events, which are not idempotent. Each packet re-includes the last 4 events (FEEDBACK_HISTORY_RESEND_EVENT_COUNT 0x4,lib/src/feedbacksender.c:189), so four consecutive losses are needed to drop a button press.
DATA_ACK is SCTP-shaped: cumulative TSN, a_rwnd, gap_ack_blocks_count, dup_tsns_count.
The gap ack blocks are parsed only to validate the packet length, then discarded. Compare
lib/src/takion.c:1430, which reads gap_ack_blocks_count, with :1447, which passes only
cumulative_seq_num to the send buffer. The result is cumulative-only acknowledgement: no SACK,
no fast retransmit, no duplicate-ACK signal.
Retransmission (lib/src/takionsendbuffer.c:12-15):
| Constant | Value |
|---|---|
TAKION_DATA_RESEND_TIMEOUT_MS |
200, flat |
TAKION_DATA_RESEND_TRIES_MAX |
25 (~5 s, then the packet is faked as acked and dropped) |
TAKION_SEND_BUFFER_SIZE |
16 slots |
No RTT estimate, no exponential backoff, no window management. a_rwnd is the constant
0x19000 and is also used verbatim as the socket SO_RCVBUF.
This channel carries protobuf control messages, rumble, trigger effects, and pad info — never
audio or video. It rarely bites. But CORRUPTFRAME and IDRREQUEST ride it, which is exactly
the path under stress at the moment the link is already dropping packets.
lib/src/congestioncontrol.c is 81 lines and this is the entire algorithm:
every 200 ms (CONGESTION_CONTROL_INTERVAL_MS):
(received, lost) = chiaki_packet_stats_get(reset = true)
loss = lost / (received + lost)
if loss > packet_loss_max:
clamp: lost = total * packet_loss_max; received = total - lost
send CONGESTION { word_0, received:u16, lost:u16 }
No congestion window. No send rate. No AIMD. No pacing. No in-session RTT measurement. A grep
of lib/ for cwnd, AIMD, pacing, BBR, GCC, or rate control returns nothing.
The control loop lives entirely inside closed console firmware. The client is a passive
reporter with a 5 Hz update cadence and a two-uint16 signal.
lib/src/packetstats.c merges two sources:
- Video: generation-based. Per frame,
expected = k + mandreceivedis the actual count, so loss is measured, not inferred (chiaki_frame_processor_report_packet_stats). - Audio: sequence-based.
seq_max - seq_min - seq_received.
Both are summed into one ratio (lib/src/packetstats.c:70-79).
The console sends CONNECTIONQUALITY, carrying target_bitrate, upstream_bitrate,
upstream_loss, disable_upstream_audio, rtt, and loss. The client logs it at verbose
level and nothing else (lib/src/streamconnection.c:693-706); the only side effect is
snapshotting measured bitrate for the on-screen overlay.
The PACKETLOSS protobuf message type is defined and never sent.
- Resolution.
"adaptiveStreamMode": "resize"in the launch spec (lib/src/launchspec.c:77), plusadaptive_stream_indexon every AV packet selecting among the profiles advertised inSTREAMINFO. The client just follows the switch and re-emits the matching video header. - FEC ratio, per frame, as covered in §2.1.
- Bitrate, presumably, reported back as
target_bitrate.
All three are console-side decisions driven by the 5 Hz loss report.
packet_loss_max caps how much loss the client is willing to admit to. Set it low and the
client under-reports, so the console keeps quality high and you eat the artifacts; set it to 1.0
and the console sees the truth and downshifts sooner.
Upstream chiaki-ng defaults to 0.05. Commit 79a8e611 ("honest congestion control") changed
the default in this tree to 1.0. That commit is a tacit acknowledgement that the console's
loop is the only loop, and that misreporting is the only client-side lever over it.
Exposed in the GUI as an integer percentage (QmlSettings::setPacketLossReportedMax).
Senkusha runs before the stream starts (lib/src/senkusha.c:252-272) and never again:
- RTT — 10 pings, arithmetic mean (
SENKUSHA_PING_COUNT_DEFAULT 10). - MTU — binary search 576-1454, inbound then outbound, 3 retries, timeout
5 * rttclamped to 5-500 ms.
That is the whole of it. No re-probing on path change, Wi-Fi roam, or congestion.
There is no bandwidth probe. SenkushaBandwidthCommand is defined in the protobuf
(lib/protobuf/takion.proto:444) and the client never sends it. The bwKbpsSent field in the
launch spec is simply the user's configured bitrate (lib/src/streamconnection.c:1009, from the
per-preset defaults at lib/src/session.c:100-120) — a static declaration, not a measurement.
There is no protocol message for requesting a FEC ratio, and no client-side knob that sets one.
k and m arrive in the header of every AV packet (units_in_frame_total,
units_in_frame_fec) and are strictly read-only inputs to the decoder. chiaki_fec_encode()
exists in lib/src/fec.c but has no caller anywhere in the library — it is there for symmetry
and for the test suite. The console decides, per frame, and the client complies.
| Lever | Where | Current value | Assessment |
|---|---|---|---|
bwLoss in the launch spec |
lib/src/launchspec.c:25 |
hardcoded 0.001000 |
The client's declared expected path loss, sent once at session setup. It is a string literal in the format template — ChiakiLaunchSpec has no field for it, and nothing measured ever reaches it. Most promising untested lever. |
packet_loss_max |
lib/src/congestioncontrol.c:28 |
1.0 in this tree, 0.05 upstream |
Caps reported loss. Raising reported loss should push the console toward more parity and lower rate. Mediated by console policy, updated at 5 Hz. |
bwKbpsSent (bitrate) |
lib/src/streamconnection.c:1009 |
user setting | Fewer bytes per frame means smaller k. For a given m that is higher fractional redundancy. Crude, but it is the one lever with a reliable mechanism behind it. |
mtu |
Senkusha probe | measured, 576-1454 | Sets unit size, hence k for a given frame size. Not directly settable; only reachable by making the probe fail. |
rtt |
Senkusha probe | measured | Reported once. Effect on FEC policy unknown. |
minFps, minBandwidth, score |
lib/src/launchspec.c:20,32-33 |
hardcoded 30, 0, 10 |
Constrain the console's adaptation envelope. Untested. |
bwLoss deserves a specific experiment. The client currently tells every console, on every
session, that the path loses 0.1% of packets — on a link that may be losing 10%. If the console
seeds its initial FEC ratio from this field, then raising it is a one-line change with a
directly observable outcome: log units_in_frame_fec on the first hundred frames of a session
at 0.001000 and again at, say, 0.050000, and compare. This is a hypothesis, not a finding:
the console is closed source and the field's actual effect is unverified.
m = 0 is silently rewritten to m = 1 (lib/src/frameprocessor.c:77-79):
frame_processor->units_fec_expected = packet->units_in_frame_fec;
if(frame_processor->units_fec_expected < 1)
frame_processor->units_fec_expected = 1;Frame assembly is unaffected — the flush loop iterates only over units_source_expected, and
flush_possible still fires at k units. But chiaki_frame_processor_report_packet_stats()
computes expected = units_source_expected + units_fec_expected, so a frame the console sent
with no parity is accounted as one unit short. If the console ever sends m = 0, the client
reports a phantom loss on every such frame — at k = 7 that is a fabricated 12.5% loss floor
fed straight into the congestion report, which would drive the console to degrade a link that
is in fact clean. Whether m = 0 ever occurs on the wire is unverified; every observed capture
has m >= 1.
congestion_control_interval from STREAMINFO is ignored. The console can specify the
reporting interval (lib/protobuf/takion.proto:270), and
stream_connection_takion_data_expect_streaminfo() decodes only audio_header and
resolution from that message. The field is never read. The client hardcodes
CONGESTION_CONTROL_INTERVAL_MS 200 regardless of what the console asked for.
| Takion (Remote Play) | BUD (Parsec) | Sunshine / Moonlight | |
|---|---|---|---|
| Video transport | unreliable + RS FEC | fully reliable ordered ring | unreliable + RS FEC |
| ACK for video | none | group ack, 10 ms / gap floors | none |
| NACK | none | yes (0x10), fast retransmit with per-fragment latch |
no |
| Video retransmission | none | yes, clamp(2*(retries+1)*srtt, 50-1000 ms) + 30 ms |
no |
| Loss recovery | FEC, then ref-frame remap, then IDR request | retransmit until delivered, furthest-slot stall escape | FEC, then IDR request |
| Reliable channel | cumulative ACK only, flat 200 ms RTO, 16 slots | per-fragment RTO, NACK, 100-fragment cap | control stream |
| Congestion control | none on client; 5 Hz loss report to closed firmware | local AIMD, MD x0.7, AI +step*0.15 per 30 clean ticks, actuates encoder bitrate | none in protocol |
| Controller cadence | 200 ms timer | per encoded frame (~16.7 ms at 60 fps) | n/a |
| Congestion signal | loss ratio only | stale-fragment ratio, partly delay-derived (srtt*1.1 + 20 ms) |
n/a |
| RTT tracking | once, pre-stream | continuous EWMA (0.9 / 0.1) | stats only |
| Bandwidth probe | defined in protocol, never used | throughput measured per tick, feeds peak |
no |
| MTU | one binary search, then fixed | continuous path probing | fixed |
BUD reference: 01-protocol.md §5.2, §9, §10, §13.
This section explains the empirically observed ordering on lossy links — Parsec good, Sunshine and Remote Play poor — from the mechanisms above.
A frame of k source and m parity units survives if and only if at most m of the n = k+m
units are lost. Under independent per-packet loss p:
P_fail = 1 - sum(i=0..m) C(n,i) * p^i * (1-p)^(n-i)
For the configurations actually observed on the wire (§2.4), plus two larger blocks for contrast:
k,m |
n |
Redundancy | p=2% |
p=5% |
p=10% |
Failed frames/s at 60 fps, p=10% |
|---|---|---|---|---|---|---|
| 3,2 | 5 | 40.0% | 0.01% | 0.12% | 0.86% | 0.5 |
| 4,2 | 6 | 33.3% | 0.02% | 0.22% | 1.58% | 1.0 |
| 5,2 | 7 | 28.6% | 0.03% | 0.38% | 2.57% | 1.5 |
| 7,2 | 9 | 22.2% | 0.06% | 0.84% | 5.30% | 3.2 |
| 6,1 | 7 | 14.3% | 0.79% | 4.44% | 14.97% | 9.0 |
| 8,1 | 9 | 11.1% | 1.31% | 7.12% | 22.52% | 13.5 |
| 12,4 | 16 | 25.0% | 0.00% | 0.09% | 1.70% | 1.0 |
| 6,4 | 10 | 40.0% | 0.00% | 0.01% | 0.16% | 0.1 |
Two things follow. First, m = 1 is catastrophic at these loss rates — 9 to 13.5 failed frames
per second — which is why the console raising m under load is the single most important thing
it does. Second, at the console's typical 33-40% redundancy, independent loss is not the
problem. 0.12% to 1.58% frame failure at 5-10% loss would be a perfectly usable stream.
The observed behaviour is much worse than that. So independent loss is the wrong model.
Real loss on a congested path is bursty: a tail-drop event discards a run of consecutive packets. Two properties of this codebase make block FEC nearly defenceless against it.
A frame's units are emitted back to back. They are not paced and not interleaved with anything else. If the bottleneck queue overflows during that emission, the losses are not merely correlated — they are guaranteed consecutive within one FEC block. The independence assumption that the table in §9.1 rests on is violated in the worst available way.
k is small. Every observed capture has k between 3 and 8, so n is 5 to 10. A frame is
five to ten packets sent in a burst lasting well under a millisecond. Under burst loss the block
size stops mattering and only m does:
A burst of
m+1consecutive lost packets destroys the frame, regardless ofk.
At the modal k=3, m=2, a three-packet drop — entirely routine on a congested link — is fatal.
No increase in redundancy percentage helps, because the redundancy is spent inside the same
sub-millisecond window that the burst covers. Larger blocks would average bursts out; frames
this small cannot produce them. There is no interleaving across frames either: each frame is its
own independent FEC block, so a burst spanning a frame boundary takes the tail of one and the
head of the next, killing both.
This is the mechanism behind the "20-30% FEC and it still flickers" experience in Sunshine, and it applies to Remote Play identically. You pay the bandwidth and still fail, because the failure mode is burst-shaped and the block is too small and too brief to average over it.
Retransmission is indifferent to the loss pattern. A lost fragment is detected by a gap in the cumulative acknowledgement and resent; whether it was lost alone or as part of a run of twelve changes the amount of retransmission, not whether recovery is possible. Concretely:
- Cost scales linearly with loss rate, not with burst structure. 5% loss costs ~5% extra traffic. There is no cliff and no exponent.
- Repair latency is bounded and small. NACK-driven fast retransmit fires without waiting for the timer (§5.2 of BUD), and the RTO floor is 50 ms + 30 ms grace. The affected frame is late; it is never absent.
- Nothing becomes undecodable, so nothing requests a keyframe. No IDR, no flicker. This is the decisive difference at 60 fps — the visible symptom of FEC failure is not the dropped frame, it is the keyframe that follows it.
- The rate loop closes locally, per frame. BUD's AIMD sees the congestion that is causing the burst loss and reduces the encoder bitrate within a frame or two, which reduces the queue overflow that produced the bursts. Takion reports loss at 5 Hz to a closed controller and waits.
The trade is real and worth stating plainly: on a clean path, FEC repairs in zero added latency where retransmission costs at least one RTT. BUD is paying a latency premium that Remote Play and Sunshine do not. That premium is a good deal at 5-10% loss and a bad one at 0.1%.
Better than Sunshine in two respects:
- The FEC ratio adapts. Sunshine's is a static config value; the console moves
mbetween 1 and 4 and reaches 40-50% redundancy under load (§2.4). It is slow — 5 Hz, closed loop — but it is not fixed. - There is a concealment step Sunshine lacks. The reference-frame remap (§2.3 step 2)
rewrites a P-slice's
ref_idxto the nearest surviving reference instead of giving up, andenable_idr_on_fec_failuredefaults tofalse. Some failures that would be a keyframe elsewhere become a brief artifact here.
Worse than Sunshine in one respect that matters:
- You cannot set the ratio. In Sunshine, 20-30% FEC is a number you type. Here the console
decides from a 5 Hz loss report and a
bwLossfield hardcoded to 0.1% (§7.2). If its policy is wrong for your path, there is no override.
- Lower the bitrate. Fewer bytes per frame means smaller
k, and for a givenmthat is strictly higher fractional redundancy. It also reduces the queue pressure producing the bursts in the first place. This is the only lever with a mechanism that is certain. - Keep
packet_loss_maxat1.0so the console sees the real loss rate and raisesm. The upstream default of0.05actively suppresses the adaptation you want here. - Leave
enable_idr_on_fec_failureoff (the default). Client-driven IDR per failed frame at 60 fps trades one bad frame for a visible flash. Note this does not eliminate keyframes —CORRUPTFRAMEis still sent unconditionally and the console may answer it with one (§2.3). - Experiment with
bwLoss(§7.2). One-line change, directly measurable by loggingunits_in_frame_fec, and currently untested by anyone as far as this tree shows. - Reduce the resolution rather than only the bitrate.
adaptiveStreamMode: "resize"means the console will do this itself, but starting lower avoids the transient.
None of these change the architecture. At sustained 5-10% burst loss, a retransmitting protocol is the right tool and a block-FEC protocol is not, and no amount of tuning closes that gap.
- FEC is the right primitive for 60 fps interactive video. A retransmission costs at least one RTT; a parity unit costs bandwidth and zero latency. On a 35 ms path, FEC repairs in zero frames what a retransmit-based scheme needs 80 ms+ — three to five frames — to fix. On this axis Takion beats BUD.
- The FEC ratio is per frame, so the console can adapt protection within a frame time rather than on a control-message round trip.
- Loss accounting is exact for video, not inferred from sequence gaps.
- Resolution downshift is a better degradation axis than bitrate alone for a fixed-profile hardware encoder.
- The 16 ms AV reorder window absorbs Wi-Fi microbursts without stalling on real loss.
- Loss-only congestion signal. The congestion packet carries two
uint16s. No one-way delay, no queueing delay, no jitter. On a bufferbloated last mile — most consumer internet — loss appears only after the queue has already added 100-300 ms of latency. A loss-only controller structurally cannot see that arriving. BUD's staleness classifier is partly delay-derived and reacts to RTT growth; Takion does not measure RTT during the session at all. - 5 Hz control loop. BUD ticks its controller once per encoded frame and acknowledges on a 10 ms floor. A congestion episode is 200 ms stale before the console hears about it.
- Open loop. The controller cannot be inspected, tuned, or fixed. With BUD the constants are in the spec and can be changed.
- No bandwidth probe. The initial rate is a user guess. Configure 30 Mbps on a 15 Mbps link and the session self-congests until the console's loop walks it down on its own schedule.
- Burst loss defeats block FEC, and this is the dominant failure mode on a lossy path.
Units of one frame go out back to back with no cross-frame interleaving, and
kis only 3 to 8, so a tail-drop burst removes consecutive units of one small generation. Under bursts onlymmatters:m+1consecutive losses are fatal regardless of redundancy percentage. Quantified in §9.1-9.2. Sunshine shares this exactly; BUD is immune because it retransmits. - FEC failure is a cliff, not a slope. Below
kunits the frame is gone: report corrupt, and wait for whatever keyframe the console decides to send over a link that has just demonstrated it is lossy. There is no intermediate degradation between "fine" and "frozen". The reference-frame remap softens some cases; it does not change the shape of the curve. - The reliable channel would be bad if it mattered. A flat 200 ms RTO with no SACK on a
150 ms path costs 200-400 ms per lost control message, and
IDRREQUESTis a control message.
On a wired, low-jitter path with real headroom — under ~40 ms, fiber or off-peak cable — Remote Play is genuinely good, and its FEC-not-retransmit choice actually beats BUD on latency, because the console's rate loop rarely has to act and the weak parts never engage.
On a path with variable capacity or bufferbloat — LTE, congested evening cable, a distant Wi-Fi AP — it degrades as a cliff rather than a ramp: FEC failures, freezes, keyframe flicker, where BUD's AIMD would have walked the bitrate down smoothly a second earlier.
At sustained 5-10% loss the gap is structural and not closable by tuning. Two mechanisms compound: block FEC over a five-to-ten-packet block emitted in under a millisecond has no defence against burst loss (§9.2), and a 5 Hz loss-only report into closed firmware cannot back the rate off fast enough to stop producing the bursts (§5). A retransmitting protocol is simply the right tool in that regime, and Remote Play is not one.
Available client-side levers: the bitrate slider, packet_loss_max, and
enable_idr_on_fec_failure — plus bwLoss, if someone patches it (§7.2). That is all of them.
| File | Role |
|---|---|
lib/src/fec.c |
Cauchy Reed-Solomon encode/decode via jerasure |
lib/src/frameprocessor.c |
Frame reassembly from units, erasure list construction, FEC invocation |
lib/src/videoreceiver.c |
Frame sequencing, corrupt-frame reporting, IDR request, reference-frame remap |
lib/src/audioreceiver.c |
Audio repetition redundancy extraction |
lib/src/congestioncontrol.c |
The entire congestion "control": a 200 ms loss reporter |
lib/src/packetstats.c |
Generation-based and sequence-based loss accounting |
lib/src/takion.c |
Transport, packet dispatch, DATA_ACK handling, AV reorder queues |
lib/src/takionsendbuffer.c |
Reliable-channel retransmission, flat 200 ms RTO |
lib/src/feedbacksender.c |
Input: idempotent state plus 4-event repetition history |
lib/src/senkusha.c |
Pre-stream RTT and MTU probing |
lib/src/streamconnection.c |
Protobuf control messages, CONNECTIONQUALITY handling |
lib/src/launchspec.c |
Launch spec JSON, including the static bwKbpsSent |
lib/protobuf/takion.proto |
Full control-message schema, including unused capabilities |
test/fec_test_cases.inl |
64 captured frames; the (k, m) evidence in §2.4 |
test/takion.c |
Real captured AV packet parse, k=7, m=1 |