@torrent-tv/proxy 2.91.0 → 2.92.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/bin/cli.js +3 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
* construction, and the call sites are untouched — which is what keeps a change
|
|
10
10
|
* of this size reviewable.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Source handles retain their derived identity across calls:
|
|
13
13
|
*
|
|
14
|
-
* - **`prioritizeByteRange` stays synchronous.** It returns nothing the caller
|
|
15
|
-
* inspects, so the command is dispatched and not awaited. Awaiting it would
|
|
16
|
-
* mean touching every call site for no observable gain.
|
|
17
14
|
* - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
|
|
18
15
|
* thread, so the worker keys them. Callers that have one pass it; the rest
|
|
19
16
|
* get one derived from the source itself, so the identity stays stable
|
|
@@ -33,13 +30,17 @@ export class WorkerTorrentPool {
|
|
|
33
30
|
#client;
|
|
34
31
|
/** Stand-ins by source key, so repeat calls return the same object. */
|
|
35
32
|
#torrents = new Map();
|
|
33
|
+
#pending = new Map();
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
36
|
* @param {{ memoryBytes?: number, stateDir?: string }} [options]
|
|
39
37
|
* @param {object | null} [client] - The worker interface; tests supply a fake.
|
|
40
38
|
*/
|
|
41
39
|
constructor(options = {}, client = null) {
|
|
42
|
-
this.#client = client ?? new TorrentWorkerClient(options
|
|
40
|
+
this.#client = client ?? new TorrentWorkerClient({ ...options, onSourceForgotten: event => {
|
|
41
|
+
this.forget(event.sourceKey);
|
|
42
|
+
options.onSourceForgotten?.(event);
|
|
43
|
+
} });
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -55,9 +56,29 @@ export class WorkerTorrentPool {
|
|
|
55
56
|
if (existing) {
|
|
56
57
|
return existing;
|
|
57
58
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const joined = this.#pending.get(sourceKey);
|
|
60
|
+
if (joined) return joined;
|
|
61
|
+
const pending = this.#client.getTorrent({ sourceKey, sourceType, source }).then(torrent => {
|
|
62
|
+
if (this.#pending.get(sourceKey) !== pending) {
|
|
63
|
+
const error = new Error("Source was forgotten while its metadata was being read.");
|
|
64
|
+
error.code = "SOURCE_FORGOTTEN";
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
this.#torrents.set(sourceKey, torrent);
|
|
68
|
+
return torrent;
|
|
69
|
+
}).finally(() => { if (this.#pending.get(sourceKey) === pending) this.#pending.delete(sourceKey); });
|
|
70
|
+
this.#pending.set(sourceKey, pending);
|
|
71
|
+
return pending;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
forget(sourceKey) {
|
|
75
|
+
this.#torrents.delete(sourceKey);
|
|
76
|
+
this.#pending.delete(sourceKey);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Return an existing handle without adding, reviving, or waiting for it. */
|
|
80
|
+
knownTorrent(sourceKey) {
|
|
81
|
+
return this.#torrents.get(sourceKey) ?? null;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
/**
|
|
@@ -119,10 +140,9 @@ export class WorkerTorrentPool {
|
|
|
119
140
|
*
|
|
120
141
|
* @param {object} torrent
|
|
121
142
|
* @param {number | null} [fileIndex]
|
|
122
|
-
* @param {{ resumeAnchorByteStart?: number | null }} [options]
|
|
123
143
|
* @returns {Promise<object | null>}
|
|
124
144
|
*/
|
|
125
|
-
async getFileStats(torrent, fileIndex = null,
|
|
145
|
+
async getFileStats(torrent, fileIndex = null, { forecast = false } = {}) {
|
|
126
146
|
const sourceKey = torrent?.sourceKey;
|
|
127
147
|
if (!sourceKey) {
|
|
128
148
|
return null;
|
|
@@ -130,99 +150,10 @@ export class WorkerTorrentPool {
|
|
|
130
150
|
return this.#client.getFileStats({
|
|
131
151
|
sourceKey,
|
|
132
152
|
fileIndex,
|
|
133
|
-
|
|
153
|
+
forecast
|
|
134
154
|
});
|
|
135
155
|
}
|
|
136
156
|
|
|
137
|
-
/**
|
|
138
|
-
* Fetch one whole file using only the room the viewer's own reading leaves.
|
|
139
|
-
*
|
|
140
|
-
* For a soundtrack or subtitle file shipped beside the picture: small next to
|
|
141
|
-
* the film, and having it on disk is what turns a later switch into a local
|
|
142
|
-
* read instead of a wait on the swarm.
|
|
143
|
-
*
|
|
144
|
-
* @param {object} torrent
|
|
145
|
-
* @param {number} fileIndex
|
|
146
|
-
* @returns {Promise<boolean>} Whether a fill was started by this call.
|
|
147
|
-
*/
|
|
148
|
-
async fillFileInBackground(torrent, fileIndex) {
|
|
149
|
-
const sourceKey = torrent?.sourceKey;
|
|
150
|
-
if (!sourceKey) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
const answer = await this.#client.fillFile({ sourceKey, fileIndex });
|
|
154
|
-
return answer?.started === true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Ask the torrent to fetch all remaining files at conditional TAIL urgency.
|
|
159
|
-
*
|
|
160
|
-
* @param {object} torrent
|
|
161
|
-
* @returns {Promise<boolean>}
|
|
162
|
-
*/
|
|
163
|
-
async fillTorrent(torrent) {
|
|
164
|
-
const sourceKey = torrent?.sourceKey;
|
|
165
|
-
if (!sourceKey) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
const answer = await this.#client.fillTorrent(sourceKey);
|
|
169
|
-
return answer?.started === true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Start fetching the region a viewer is about to resume at. Named in seconds
|
|
177
|
-
* here; the worker turns it into bytes, where the file's duration is readable.
|
|
178
|
-
*
|
|
179
|
-
* @param {object} torrent
|
|
180
|
-
* @param {number} fileIndex
|
|
181
|
-
* @param {number} positionSeconds
|
|
182
|
-
* @returns {Promise<boolean>}
|
|
183
|
-
*/
|
|
184
|
-
async warmResumePosition(torrent, fileIndex, positionSeconds, durationSeconds) {
|
|
185
|
-
const sourceKey = torrent?.sourceKey;
|
|
186
|
-
if (!sourceKey) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds });
|
|
190
|
-
return answer?.started === true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Reorder piece selection around a read position.
|
|
200
|
-
*
|
|
201
|
-
* Synchronous by design — see the file header.
|
|
202
|
-
*
|
|
203
|
-
* @param {object} torrent
|
|
204
|
-
* @param {number} fileIndex
|
|
205
|
-
* @param {number} byteStart
|
|
206
|
-
* @param {number} [windowBytes]
|
|
207
|
-
* @param {{ wholeFileRead?: boolean }} [options]
|
|
208
|
-
* @returns {void}
|
|
209
|
-
*/
|
|
210
|
-
prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes, options) {
|
|
211
|
-
const sourceKey = torrent?.sourceKey;
|
|
212
|
-
if (!sourceKey) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
void this.#client
|
|
216
|
-
.prioritizeByteRange({
|
|
217
|
-
sourceKey,
|
|
218
|
-
fileIndex,
|
|
219
|
-
byteStart,
|
|
220
|
-
windowBytes,
|
|
221
|
-
wholeFileRead: options?.wholeFileRead === true
|
|
222
|
-
})
|
|
223
|
-
.catch(() => undefined);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
157
|
/**
|
|
227
158
|
* Hand the download the priority map for one file.
|
|
228
159
|
*
|
|
@@ -240,21 +171,6 @@ export class WorkerTorrentPool {
|
|
|
240
171
|
await this.#client.setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones });
|
|
241
172
|
}
|
|
242
173
|
|
|
243
|
-
/**
|
|
244
|
-
* Pre-fetch the head and tail the codec probe needs.
|
|
245
|
-
*
|
|
246
|
-
* Takes an options object, matching `TorrentPool.prefetchFileEdges` — this
|
|
247
|
-
* adapter exists to present that same interface. It previously declared
|
|
248
|
-
* positional parameters instead, so the planner's options object arrived as
|
|
249
|
-
* `headBytes` and only worked because it was passed along far enough to be
|
|
250
|
-
* destructured at the far end. Anyone calling it as documented got the
|
|
251
|
-
* defaults instead of the sizes they asked for.
|
|
252
|
-
*
|
|
253
|
-
* @param {object} torrent
|
|
254
|
-
* @param {number} fileIndex
|
|
255
|
-
* @param {{ headBytes?: number, tailBytes?: number, timeoutMs?: number }} [options]
|
|
256
|
-
* @returns {Promise<unknown>}
|
|
257
|
-
*/
|
|
258
174
|
/**
|
|
259
175
|
* One byte range of one file, as bytes, on THIS thread.
|
|
260
176
|
*
|
|
@@ -313,6 +229,11 @@ export class WorkerTorrentPool {
|
|
|
313
229
|
return sourceKey ? this.#client.readHeld({ sourceKey, fileIndex, start, end }) : null;
|
|
314
230
|
}
|
|
315
231
|
|
|
232
|
+
async readHeldRangesOf(torrent, fileIndex, ranges, maxBytes) {
|
|
233
|
+
const sourceKey = torrent?.sourceKey;
|
|
234
|
+
return sourceKey ? this.#client.readHeldRanges({ sourceKey, fileIndex, ranges, maxBytes }) : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
316
237
|
async readRangeOf(torrent, fileIndex, start, end) {
|
|
317
238
|
const sourceKey = torrent?.sourceKey;
|
|
318
239
|
if (!sourceKey || !(end >= start) || !(start >= 0)) {
|
|
@@ -337,14 +258,6 @@ export class WorkerTorrentPool {
|
|
|
337
258
|
return total > 0 ? Buffer.concat(chunks, total) : null;
|
|
338
259
|
}
|
|
339
260
|
|
|
340
|
-
async prefetchFileEdges(torrent, fileIndex, options = {}) {
|
|
341
|
-
const sourceKey = torrent?.sourceKey;
|
|
342
|
-
if (!sourceKey) {
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
-
return this.#client.prefetchFileEdges({ sourceKey, fileIndex, options });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
261
|
/**
|
|
349
262
|
* Which films this proxy holds right now, and how much of each.
|
|
350
263
|
*
|
|
@@ -366,6 +279,7 @@ export class WorkerTorrentPool {
|
|
|
366
279
|
*/
|
|
367
280
|
async destroyAll() {
|
|
368
281
|
this.#torrents.clear();
|
|
282
|
+
this.#pending.clear();
|
|
369
283
|
await this.#client.destroyAll();
|
|
370
284
|
}
|
|
371
285
|
}
|
|
@@ -55,8 +55,6 @@ export const Command = {
|
|
|
55
55
|
TORRENT_TOTALS: "torrent-totals",
|
|
56
56
|
/** Which films this proxy holds right now, for content affinity. */
|
|
57
57
|
HELD_TORRENTS: "held-torrents",
|
|
58
|
-
/** Reorder piece selection around a read position (seek prioritisation). */
|
|
59
|
-
PRIORITIZE: "prioritize",
|
|
60
58
|
/**
|
|
61
59
|
* The priority map for one file: seconds of film against a number.
|
|
62
60
|
*
|
|
@@ -71,21 +69,6 @@ export const Command = {
|
|
|
71
69
|
READ_RANGE: "read-range",
|
|
72
70
|
/** Abandon an in-flight READ_RANGE (viewer gone, seek superseded). */
|
|
73
71
|
CANCEL_READ: "cancel-read",
|
|
74
|
-
/** Pre-fetch the head and tail a codec probe needs. */
|
|
75
|
-
PREFETCH_EDGES: "prefetch-edges",
|
|
76
|
-
/**
|
|
77
|
-
* Fetch one whole file using only the room the viewer's own reading leaves —
|
|
78
|
-
* a soundtrack or subtitle file they may switch to later. Returns as soon as
|
|
79
|
-
* the work is under way.
|
|
80
|
-
*/
|
|
81
|
-
FILL_FILE: "fill-file",
|
|
82
|
-
/** Claim every remaining file as conditional TAIL demand for one torrent. */
|
|
83
|
-
FILL_TORRENT: "fill-torrent",
|
|
84
|
-
/**
|
|
85
|
-
* Start fetching the region a viewer is about to resume at, named in seconds
|
|
86
|
-
* and turned into bytes here, where the file's own duration can be read.
|
|
87
|
-
*/
|
|
88
|
-
WARM_POSITION: "warm-position",
|
|
89
72
|
/**
|
|
90
73
|
* The byte ranges of one file the torrent holds WHOLE, so the subtitle walk
|
|
91
74
|
* can decide what it may read without asking the swarm. A list rather than a
|
|
@@ -102,6 +85,8 @@ export const Command = {
|
|
|
102
85
|
* must pull nothing.
|
|
103
86
|
*/
|
|
104
87
|
READ_HELD: "read-held",
|
|
88
|
+
/** Acquire all source pieces of an admitted input before copying its ranges. */
|
|
89
|
+
READ_HELD_RANGES: "read-held-ranges",
|
|
105
90
|
|
|
106
91
|
/** Shut the client down, optionally deleting downloaded data. */
|
|
107
92
|
/**
|
|
@@ -158,6 +143,10 @@ export const Event = {
|
|
|
158
143
|
* which put the cue reading in the thread that owns the swarm.
|
|
159
144
|
*/
|
|
160
145
|
PIECES_ARRIVED: "pieces-arrived",
|
|
146
|
+
/** Available source bytes changed after verification or withdrawal. */
|
|
147
|
+
PIECES_CHANGED: "pieces-changed",
|
|
148
|
+
/** A resolved source handle closed; retained media readings must be released. */
|
|
149
|
+
SOURCE_FORGOTTEN: "source-forgotten",
|
|
161
150
|
|
|
162
151
|
/**
|
|
163
152
|
* A file has been downloaded whole and written out as a file.
|
|
@@ -34,6 +34,8 @@ export function wholeFileStats(length) {
|
|
|
34
34
|
fileProgress: 1,
|
|
35
35
|
fileDownloaded: fileLength,
|
|
36
36
|
fileLength,
|
|
37
|
+
fileOffset: 0,
|
|
38
|
+
fileAvailable: fileLength > 0,
|
|
37
39
|
residence: fileLength > 0 ? [{ start: 0, end: fileLength, location: "whole-file" }] : [],
|
|
38
40
|
resumeNeededBytes: null,
|
|
39
41
|
resumeDownloadedBytes: null,
|