1use crate::rlc::RepairSchedule;
2use crate::{
3 ContinuousEncoder, CuStreamTx, CuStreamTxError, Error, FiniteObjectEncoder,
4 FiniteObjectSenderConfig, Lane, PACKET_HEADER_LEN, RecordKind, Result, StreamIdentity,
5 decode_record, encode_copperlist_record_into, encode_keyframe_and_recovery_point,
6};
7use alloc::{boxed::Box, string::ToString, vec, vec::Vec};
8use core::{fmt::Debug, marker::PhantomData};
9use cu_fec::{DensityThreshold, EncodingSymbolId, RlcConfig};
10use cu29_runtime::{copperlist::CopperList, curuntime::KeyFrame};
11use cu29_traits::{CopperListTuple, CuError, CuResult, WriteStream};
12
13pub const DEFAULT_MAX_SYMBOL_SIZE: usize = 1128;
15pub const DEFAULT_MAX_WINDOW_SYMBOLS: usize = 64;
17
18pub type DefaultContinuousCopperListSink<P, T> =
20 ContinuousCopperListSink<P, T, DEFAULT_MAX_SYMBOL_SIZE, DEFAULT_MAX_WINDOW_SYMBOLS>;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ContinuousSenderConfig {
25 pub identity: StreamIdentity,
26 pub lane: Lane,
27 pub fec: RlcConfig,
28 pub max_record_bytes: usize,
29 pub initial_esi: EncodingSymbolId,
30 pub repair_every_source_symbols: usize,
32 pub first_repair_key: u16,
33 pub repair_density: DensityThreshold,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct RecoverySenderConfig {
39 pub finite: FiniteObjectSenderConfig,
40 pub manifest_record: Vec<u8>,
41 pub recovery_interval: u32,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct LogStreamSenderConfig {
48 pub feedback: Option<crate::feedback::FeedbackPolicy>,
50 pub pacing: crate::PacingConfig,
51 pub continuous: ContinuousSenderConfig,
52 pub recovery: RecoverySenderConfig,
53}
54
55impl LogStreamSenderConfig {
56 pub fn validate(&self) -> Result<()> {
57 if self.continuous.identity != self.recovery.finite.identity {
58 return Err(Error::InvalidConfig(
59 "continuous and recovery sender identities differ",
60 ));
61 }
62 if self.continuous.lane == self.recovery.finite.lane {
63 return Err(Error::InvalidConfig(
64 "continuous and finite objects require distinct lanes",
65 ));
66 }
67 let manifest = decode_record(&self.recovery.manifest_record)?;
68 if manifest.kind != RecordKind::Manifest {
69 return Err(Error::InvalidConfig(
70 "recovery configuration requires a manifest record",
71 ));
72 }
73 if self.recovery.recovery_interval == 0 {
74 return Err(Error::InvalidConfig(
75 "recovery point interval must be nonzero",
76 ));
77 }
78 Ok(())
79 }
80}
81
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct ContinuousSenderStats {
85 pub records_encoded: u64,
86 pub source_datagrams: u64,
87 pub repair_datagrams: u64,
88 pub datagrams_sent: u64,
89 pub datagrams_dropped: u64,
90}
91
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct RecoverySenderStats {
95 pub keyframes_encoded: u64,
96 pub object_datagrams: u64,
97 pub datagrams_sent: u64,
98 pub datagrams_dropped: u64,
99}
100
101pub struct RecoveryPointSink<T: CuStreamTx> {
105 transport: T,
106 encoder: FiniteObjectEncoder,
107 manifest_record: Vec<u8>,
108 manifest_object_id: u64,
109 manifest_record_digest: [u8; 32],
110 recovery_interval: u32,
111 stats: RecoverySenderStats,
112}
113
114impl<T: CuStreamTx> Debug for RecoveryPointSink<T> {
115 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116 formatter
117 .debug_struct("RecoveryPointSink")
118 .field("transport", &self.transport)
119 .field("config", &self.encoder.config())
120 .field("stats", &self.stats)
121 .finish_non_exhaustive()
122 }
123}
124
125impl<T: CuStreamTx> RecoveryPointSink<T> {
126 pub fn new(transport: T, config: RecoverySenderConfig) -> Result<Self> {
127 if config.recovery_interval == 0 {
128 return Err(Error::InvalidConfig(
129 "recovery point interval must be nonzero",
130 ));
131 }
132 let manifest = decode_record(&config.manifest_record)?;
133 if manifest.kind != RecordKind::Manifest {
134 return Err(Error::InvalidConfig(
135 "recovery configuration requires a manifest record",
136 ));
137 }
138 let manifest_object_id = manifest.object_id;
139 let manifest_record_digest = manifest.digest;
140 Ok(Self {
141 transport,
142 encoder: FiniteObjectEncoder::new(config.finite)?,
143 manifest_record: config.manifest_record,
144 manifest_object_id,
145 manifest_record_digest,
146 recovery_interval: config.recovery_interval,
147 stats: RecoverySenderStats::default(),
148 })
149 }
150
151 pub const fn stats(&self) -> RecoverySenderStats {
152 self.stats
153 }
154
155 pub fn transport(&self) -> &T {
156 &self.transport
157 }
158
159 pub fn transport_mut(&mut self) -> &mut T {
160 &mut self.transport
161 }
162
163 pub fn into_transport(self) -> T {
164 self.transport
165 }
166
167 fn emit_record(&mut self, record: &[u8]) -> Result<()> {
168 emit_finite_record(
169 &mut self.encoder,
170 &mut self.transport,
171 &mut self.stats,
172 record,
173 )
174 }
175}
176
177fn emit_finite_record<T: CuStreamTx>(
178 encoder: &mut FiniteObjectEncoder,
179 transport: &mut T,
180 stats: &mut RecoverySenderStats,
181 record: &[u8],
182) -> Result<()> {
183 let datagrams = encoder.push_record_with(record, |datagram| {
184 stats.object_datagrams = stats.object_datagrams.saturating_add(1);
185 match transport.try_send(datagram) {
186 Ok(()) => stats.datagrams_sent = stats.datagrams_sent.saturating_add(1),
187 Err(CuStreamTxError::WouldBlock) => {
188 stats.datagrams_dropped = stats.datagrams_dropped.saturating_add(1);
189 }
190 Err(CuStreamTxError::Failed(message)) => return Err(Error::Transport(message)),
191 }
192 Ok(())
193 })?;
194 debug_assert!(datagrams > 0);
195 Ok(())
196}
197
198impl<T: CuStreamTx> WriteStream<KeyFrame> for RecoveryPointSink<T> {
199 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
200 if !keyframe
201 .culistid
202 .is_multiple_of(u64::from(self.recovery_interval))
203 {
204 return Ok(());
205 }
206 let (keyframe_record, recovery_point_record) = encode_keyframe_and_recovery_point(
207 keyframe,
208 self.manifest_object_id,
209 self.manifest_record_digest,
210 )
211 .map_err(|error| CuError::from(error.to_string()))?;
212 emit_finite_record(
213 &mut self.encoder,
214 &mut self.transport,
215 &mut self.stats,
216 &self.manifest_record,
217 )
218 .and_then(|()| self.emit_record(&keyframe_record))
219 .and_then(|()| self.emit_record(&recovery_point_record))
220 .map_err(|error| CuError::from(error.to_string()))?;
221 self.stats.keyframes_encoded = self.stats.keyframes_encoded.saturating_add(1);
222 Ok(())
223 }
224}
225
226pub struct ContinuousCopperListSink<
229 P,
230 T,
231 const MAX_SYMBOL_SIZE: usize,
232 const MAX_WINDOW_SYMBOLS: usize,
233> where
234 P: CopperListTuple,
235 T: CuStreamTx,
236{
237 transport: T,
238 encoder: Box<ContinuousEncoder<MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>>,
239 record: Vec<u8>,
240 datagram: Vec<u8>,
241 repair_schedule: RepairSchedule,
242 stats: ContinuousSenderStats,
243 _payload: PhantomData<fn() -> P>,
244}
245
246impl<P, T, const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize> Debug
247 for ContinuousCopperListSink<P, T, MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>
248where
249 P: CopperListTuple,
250 T: CuStreamTx,
251{
252 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253 formatter
254 .debug_struct("ContinuousCopperListSink")
255 .field("transport", &self.transport)
256 .field("config", &self.encoder.config())
257 .field("stats", &self.stats)
258 .finish_non_exhaustive()
259 }
260}
261
262impl<P, T, const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize>
263 ContinuousCopperListSink<P, T, MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>
264where
265 P: CopperListTuple,
266 T: CuStreamTx,
267{
268 pub fn new(transport: T, config: ContinuousSenderConfig) -> Result<Self> {
269 if config.repair_every_source_symbols == 0 {
270 return Err(Error::InvalidConfig(
271 "repair interval must contain at least one source symbol",
272 ));
273 }
274 let datagram_bytes = PACKET_HEADER_LEN
275 .checked_add(config.fec.symbol_size())
276 .ok_or(Error::InvalidConfig("datagram length overflow"))?;
277 let encoder = ContinuousEncoder::new(
278 config.identity,
279 config.lane,
280 config.fec,
281 config.max_record_bytes,
282 config.initial_esi,
283 )?;
284 Ok(Self {
285 transport,
286 encoder: Box::new(encoder),
287 record: vec![0; config.max_record_bytes],
288 datagram: vec![0; datagram_bytes],
289 repair_schedule: RepairSchedule::new(
290 config.repair_every_source_symbols,
291 config.first_repair_key,
292 config.repair_density,
293 ),
294 stats: ContinuousSenderStats::default(),
295 _payload: PhantomData,
296 })
297 }
298
299 pub const fn stats(&self) -> ContinuousSenderStats {
300 self.stats
301 }
302
303 pub fn transport(&self) -> &T {
304 &self.transport
305 }
306
307 pub fn transport_mut(&mut self) -> &mut T {
308 &mut self.transport
309 }
310
311 pub fn into_transport(self) -> T {
312 self.transport
313 }
314
315 fn emit_source_record(&mut self, record_len: usize) -> Result<()> {
316 let transport = &mut self.transport;
317 let stats = &mut self.stats;
318 let (source_datagrams, repair_datagrams) = self.encoder.push_record_with_repairs(
319 &self.record[..record_len],
320 &mut self.datagram,
321 &mut |datagram| submit_datagram(transport, stats, datagram),
322 &mut self.repair_schedule,
323 )?;
324 stats.source_datagrams = stats
325 .source_datagrams
326 .saturating_add(source_datagrams as u64);
327 stats.repair_datagrams = stats
328 .repair_datagrams
329 .saturating_add(repair_datagrams as u64);
330 Ok(())
331 }
332}
333
334impl<P, T, const MAX_SYMBOL_SIZE: usize, const MAX_WINDOW_SYMBOLS: usize> WriteStream<CopperList<P>>
335 for ContinuousCopperListSink<P, T, MAX_SYMBOL_SIZE, MAX_WINDOW_SYMBOLS>
336where
337 P: CopperListTuple + Send + Sync,
338 T: CuStreamTx,
339{
340 fn log(&mut self, copperlist: &CopperList<P>) -> CuResult<()> {
341 let record_len = encode_copperlist_record_into(copperlist, &mut self.record)
342 .map_err(|error| CuError::from(error.to_string()))?;
343 self.emit_source_record(record_len)
344 .map_err(|error| CuError::from(error.to_string()))?;
345 self.stats.records_encoded = self.stats.records_encoded.saturating_add(1);
346 Ok(())
347 }
348}
349
350fn submit_datagram<T: CuStreamTx>(
351 transport: &mut T,
352 stats: &mut ContinuousSenderStats,
353 datagram: &[u8],
354) -> Result<()> {
355 match transport.try_send(datagram) {
356 Ok(()) => stats.datagrams_sent = stats.datagrams_sent.saturating_add(1),
357 Err(CuStreamTxError::WouldBlock) => {
358 stats.datagrams_dropped = stats.datagrams_dropped.saturating_add(1);
359 }
360 Err(CuStreamTxError::Failed(message)) => return Err(Error::Transport(message)),
361 }
362 Ok(())
363}