@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.
- package/dist/abr-controller.d.ts +94 -0
- package/dist/abr-controller.d.ts.map +1 -0
- package/dist/abr-controller.js +174 -0
- package/dist/abr-controller.js.map +1 -0
- package/dist/amt-gateway.d.ts +160 -0
- package/dist/amt-gateway.d.ts.map +1 -0
- package/dist/amt-gateway.js +390 -0
- package/dist/amt-gateway.js.map +1 -0
- package/dist/clock.d.ts +104 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +183 -0
- package/dist/clock.js.map +1 -0
- package/dist/driad-discovery.d.ts +50 -0
- package/dist/driad-discovery.d.ts.map +1 -0
- package/dist/driad-discovery.js +170 -0
- package/dist/driad-discovery.js.map +1 -0
- package/dist/fec-client.d.ts +442 -0
- package/dist/fec-client.d.ts.map +1 -0
- package/dist/fec-client.js +784 -0
- package/dist/fec-client.js.map +1 -0
- package/dist/fec-client.test.d.ts +8 -0
- package/dist/fec-client.test.d.ts.map +1 -0
- package/dist/fec-client.test.js +112 -0
- package/dist/fec-client.test.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/transport-manager.d.ts +114 -0
- package/dist/transport-manager.d.ts.map +1 -0
- package/dist/transport-manager.js +396 -0
- package/dist/transport-manager.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
- package/src/abr-controller.ts +227 -0
- package/src/amt-gateway.ts +511 -0
- package/src/clock.ts +212 -0
- package/src/driad-discovery.ts +193 -0
- package/src/fec-client.test.ts +140 -0
- package/src/fec-client.ts +1097 -0
- package/src/index.ts +122 -0
- package/src/transport-manager.ts +460 -0
- 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
|
+
}
|