@blockcast/mmt-transport 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/abr-controller.d.ts +94 -0
  2. package/dist/abr-controller.d.ts.map +1 -0
  3. package/dist/abr-controller.js +174 -0
  4. package/dist/abr-controller.js.map +1 -0
  5. package/dist/amt-gateway.d.ts +160 -0
  6. package/dist/amt-gateway.d.ts.map +1 -0
  7. package/dist/amt-gateway.js +390 -0
  8. package/dist/amt-gateway.js.map +1 -0
  9. package/dist/clock.d.ts +104 -0
  10. package/dist/clock.d.ts.map +1 -0
  11. package/dist/clock.js +183 -0
  12. package/dist/clock.js.map +1 -0
  13. package/dist/driad-discovery.d.ts +50 -0
  14. package/dist/driad-discovery.d.ts.map +1 -0
  15. package/dist/driad-discovery.js +170 -0
  16. package/dist/driad-discovery.js.map +1 -0
  17. package/dist/fec-client.d.ts +442 -0
  18. package/dist/fec-client.d.ts.map +1 -0
  19. package/dist/fec-client.js +784 -0
  20. package/dist/fec-client.js.map +1 -0
  21. package/dist/fec-client.test.d.ts +8 -0
  22. package/dist/fec-client.test.d.ts.map +1 -0
  23. package/dist/fec-client.test.js +112 -0
  24. package/dist/fec-client.test.js.map +1 -0
  25. package/dist/index.d.ts +34 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +43 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/transport-manager.d.ts +114 -0
  30. package/dist/transport-manager.d.ts.map +1 -0
  31. package/dist/transport-manager.js +396 -0
  32. package/dist/transport-manager.js.map +1 -0
  33. package/dist/types.d.ts +356 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +85 -0
  36. package/dist/types.js.map +1 -0
  37. package/package.json +60 -0
  38. package/src/abr-controller.ts +227 -0
  39. package/src/amt-gateway.ts +511 -0
  40. package/src/clock.ts +212 -0
  41. package/src/driad-discovery.ts +193 -0
  42. package/src/fec-client.test.ts +140 -0
  43. package/src/fec-client.ts +1097 -0
  44. package/src/index.ts +122 -0
  45. package/src/transport-manager.ts +460 -0
  46. package/src/types.ts +420 -0
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @blockcast/transport - Adaptive Bitrate Controller
3
+ *
4
+ * Progressive adaptive bitrate increase based on network conditions.
5
+ * Monitors received bandwidth and packet loss to adjust quality levels.
6
+ *
7
+ * Per draft-ramadan-moq-fec-00, ABR decisions should use the **effective loss rate**
8
+ * after FEC recovery, not the raw transport-level loss rate. This allows FEC to
9
+ * mask burst losses without unnecessarily downgrading quality.
10
+ */
11
+
12
+ import type { QualityLevel } from "./types.js";
13
+
14
+ /**
15
+ * FEC statistics for ABR decisions
16
+ */
17
+ export interface FecStatsForAbr {
18
+ /** Raw loss rate before FEC recovery (0.0-1.0) */
19
+ rawLossRate: number;
20
+ /** Effective loss rate after FEC recovery (0.0-1.0) */
21
+ effectiveLossRate: number;
22
+ /** FEC recovery rate (percentage of lost packets recovered) */
23
+ recoveryRate: number;
24
+ /** Whether FEC is enabled */
25
+ fecEnabled: boolean;
26
+ }
27
+
28
+ /**
29
+ * Standard quality levels for progressive ABR
30
+ */
31
+ export const QUALITY_LEVELS: QualityLevel[] = [
32
+ { name: "240p", bitrate: 300_000, width: 426, height: 240, fps: 15 },
33
+ { name: "360p", bitrate: 500_000, width: 640, height: 360, fps: 24 },
34
+ { name: "480p", bitrate: 1_000_000, width: 854, height: 480, fps: 30 },
35
+ { name: "720p", bitrate: 2_500_000, width: 1280, height: 720, fps: 30 },
36
+ { name: "1080p", bitrate: 5_000_000, width: 1920, height: 1080, fps: 30 },
37
+ { name: "1440p", bitrate: 8_000_000, width: 2560, height: 1440, fps: 30 },
38
+ { name: "4K", bitrate: 15_000_000, width: 3840, height: 2160, fps: 30 },
39
+ ];
40
+
41
+ /**
42
+ * Progressive Adaptive Bitrate Controller
43
+ *
44
+ * Features:
45
+ * - Monitors bandwidth via received bytes over time window
46
+ * - Tracks packet loss rate (raw and effective after FEC)
47
+ * - Uses effective loss rate for ABR decisions per draft-ramadan-moq-fec-00
48
+ * - Progressive quality upgrades (waits for stability)
49
+ * - Fast quality downgrades on congestion
50
+ */
51
+ export class AdaptiveBitrateController {
52
+ private currentQualityIndex: number;
53
+ private history: { timestamp: number; bytesReceived: number; packetsLost: number }[] = [];
54
+ private readonly historyWindow = 10_000; // 10 second window
55
+ private readonly upgradeThreshold = 0.8; // 80% of target bitrate sustained
56
+ private readonly downgradeThreshold = 0.5; // 50% of target bitrate triggers downgrade
57
+ private readonly stabilityPeriod = 5_000; // Wait 5s before upgrading
58
+
59
+ private lastQualityChange = 0;
60
+ private onQualityChange?: (quality: QualityLevel) => void;
61
+
62
+ // FEC stats for effective loss rate calculation
63
+ private fecStats: FecStatsForAbr = {
64
+ rawLossRate: 0,
65
+ effectiveLossRate: 0,
66
+ recoveryRate: 1,
67
+ fecEnabled: false,
68
+ };
69
+
70
+ constructor(initialQuality?: QualityLevel) {
71
+ this.currentQualityIndex = initialQuality ? QUALITY_LEVELS.findIndex((q) => q.name === initialQuality.name) : 0;
72
+
73
+ if (this.currentQualityIndex < 0) this.currentQualityIndex = 0;
74
+ }
75
+
76
+ setCallback(callback: (quality: QualityLevel) => void) {
77
+ this.onQualityChange = callback;
78
+ }
79
+
80
+ getCurrentQuality(): QualityLevel {
81
+ return QUALITY_LEVELS[this.currentQualityIndex];
82
+ }
83
+
84
+ /**
85
+ * Record network sample for bitrate adaptation
86
+ *
87
+ * @param bytesReceived - Bytes received in this sample
88
+ * @param packetsLost - Packets lost (raw, before FEC recovery)
89
+ */
90
+ recordSample(bytesReceived: number, packetsLost: number) {
91
+ const now = Date.now();
92
+
93
+ this.history.push({ timestamp: now, bytesReceived, packetsLost });
94
+
95
+ // Prune old samples
96
+ this.history = this.history.filter((s) => now - s.timestamp < this.historyWindow);
97
+
98
+ // Calculate metrics
99
+ const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
100
+ const currentQuality = this.getCurrentQuality();
101
+
102
+ // Per draft-ramadan-moq-fec-00: Use effective loss rate after FEC for ABR decisions
103
+ // This allows FEC to mask burst losses without triggering unnecessary downgrades
104
+ const effectiveLossRate = this.fecStats.fecEnabled
105
+ ? this.fecStats.effectiveLossRate
106
+ : rawLossRate;
107
+
108
+ // Decision logic
109
+ if (now - this.lastQualityChange < this.stabilityPeriod) {
110
+ return; // Wait for stability
111
+ }
112
+
113
+ // Check for downgrade - use effective loss rate (post-FEC)
114
+ // Only downgrade if FEC cannot recover the losses
115
+ if (effectiveLossRate > 0.05 || measuredBitrate < currentQuality.bitrate * this.downgradeThreshold) {
116
+ this.downgrade();
117
+ return;
118
+ }
119
+
120
+ // Check for upgrade (progressive increase) - use effective loss rate
121
+ if (
122
+ measuredBitrate > currentQuality.bitrate * this.upgradeThreshold &&
123
+ effectiveLossRate < 0.01 &&
124
+ this.currentQualityIndex < QUALITY_LEVELS.length - 1
125
+ ) {
126
+ this.upgrade();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Update FEC statistics for ABR decisions
132
+ *
133
+ * Per draft-ramadan-moq-fec-00, ABR should use the effective loss rate
134
+ * (after FEC recovery) rather than raw transport loss rate.
135
+ *
136
+ * @param stats - FEC statistics from the FEC decoder
137
+ */
138
+ setFecStats(stats: FecStatsForAbr) {
139
+ this.fecStats = stats;
140
+ }
141
+
142
+ /**
143
+ * Get current FEC statistics
144
+ */
145
+ getFecStats(): FecStatsForAbr {
146
+ return { ...this.fecStats };
147
+ }
148
+
149
+ private calculateMetrics() {
150
+ if (this.history.length < 2) {
151
+ return { measuredBitrate: 0, lossRate: 0 };
152
+ }
153
+
154
+ const totalBytes = this.history.reduce((sum, s) => sum + s.bytesReceived, 0);
155
+ const totalLost = this.history.reduce((sum, s) => sum + s.packetsLost, 0);
156
+ const duration = (this.history[this.history.length - 1].timestamp - this.history[0].timestamp) / 1000;
157
+
158
+ const measuredBitrate = duration > 0 ? (totalBytes * 8) / duration : 0;
159
+ const lossRate = totalLost / (this.history.length + totalLost);
160
+
161
+ return { measuredBitrate, lossRate };
162
+ }
163
+
164
+ private upgrade() {
165
+ if (this.currentQualityIndex >= QUALITY_LEVELS.length - 1) return;
166
+
167
+ this.currentQualityIndex++;
168
+ this.lastQualityChange = Date.now();
169
+
170
+ const newQuality = this.getCurrentQuality();
171
+ console.log(`[ABR] Upgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);
172
+
173
+ this.onQualityChange?.(newQuality);
174
+ }
175
+
176
+ private downgrade() {
177
+ if (this.currentQualityIndex <= 0) return;
178
+
179
+ this.currentQualityIndex--;
180
+ this.lastQualityChange = Date.now();
181
+
182
+ const newQuality = this.getCurrentQuality();
183
+ console.log(`[ABR] Downgrading to ${newQuality.name} (${newQuality.bitrate / 1_000_000}Mbps)`);
184
+
185
+ this.onQualityChange?.(newQuality);
186
+ }
187
+
188
+ /**
189
+ * Force quality level (for testing)
190
+ */
191
+ setQuality(name: string) {
192
+ const index = QUALITY_LEVELS.findIndex((q) => q.name === name);
193
+ if (index >= 0) {
194
+ this.currentQualityIndex = index;
195
+ this.lastQualityChange = Date.now();
196
+ this.onQualityChange?.(this.getCurrentQuality());
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get current metrics including both raw and effective loss rates
202
+ */
203
+ getMetrics(): {
204
+ measuredBitrate: number;
205
+ rawLossRate: number;
206
+ effectiveLossRate: number;
207
+ fecEnabled: boolean;
208
+ sampleCount: number;
209
+ } {
210
+ const { measuredBitrate, lossRate: rawLossRate } = this.calculateMetrics();
211
+ return {
212
+ measuredBitrate,
213
+ rawLossRate,
214
+ effectiveLossRate: this.fecStats.fecEnabled ? this.fecStats.effectiveLossRate : rawLossRate,
215
+ fecEnabled: this.fecStats.fecEnabled,
216
+ sampleCount: this.history.length,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Reset history and start fresh
222
+ */
223
+ reset() {
224
+ this.history = [];
225
+ this.lastQualityChange = 0;
226
+ }
227
+ }