mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
import { sampleBandwidth } from "./bandwidth-estimator.js";
|
|
import { ChunkedStreamIterable } from "./chunked-stream-iterable.js";
|
|
//#region src/network/fetch.ts
|
|
/**
|
|
* HTTP Fetch Wrapper
|
|
*
|
|
* Composable building blocks:
|
|
* - fetchResolvable() — fetch a Resource (handles byte ranges); returns Response
|
|
* - getResponseText() — extract text from Response
|
|
* - fetchResolvableStream() — single-stage async generator over body chunks
|
|
* - fetchStream() — two-stage: await connection establishment, then lazily
|
|
* iterate body chunks. Use when timing the connection start independently
|
|
* of body consumption matters (e.g., observable fetch timing for ABR).
|
|
* - createTrackedFetch() — factory for a fetchStream-shape function that
|
|
* samples bandwidth (via EWMA) per chunk and notifies via callback.
|
|
*/
|
|
/**
|
|
* Fetch resolvable from a Resource.
|
|
*
|
|
* Handles byte range requests if byteRange is present.
|
|
* Returns native fetch Response for composability (can extract text, stream, etc.).
|
|
*
|
|
* @param addressable - Resource to fetch (url + optional byteRange)
|
|
* @returns Promise resolving to Response
|
|
*
|
|
* @example
|
|
* const response = await fetchResolvable({ url: 'https://example.com/segment.m4s' });
|
|
* const text = await getResponseText(response);
|
|
*
|
|
* @example
|
|
* // With byte range
|
|
* const response = await fetchResolvable({
|
|
* url: 'https://example.com/file.mp4',
|
|
* byteRange: { start: 1000, end: 1999 }
|
|
* });
|
|
*/
|
|
async function fetchResolvable(addressable, options) {
|
|
const headers = new Headers(options?.headers);
|
|
if (addressable.byteRange) {
|
|
const { start, end } = addressable.byteRange;
|
|
headers.set("Range", `bytes=${start}-${end}`);
|
|
}
|
|
const request = new Request(addressable.url, {
|
|
method: "GET",
|
|
headers,
|
|
...options
|
|
});
|
|
return fetch(request);
|
|
}
|
|
/**
|
|
* Extract text from Response.
|
|
*
|
|
* Accepts minimal Response-like object (just needs text() method).
|
|
* Returns promise from response.text().
|
|
*
|
|
* @param response - Response-like object with text() method
|
|
* @returns Promise resolving to text content
|
|
*
|
|
* @example
|
|
* const response = await fetchResolvable(addressable);
|
|
* const text = await getResponseText(response);
|
|
*/
|
|
function getResponseText(response) {
|
|
return response.text();
|
|
}
|
|
/** Default {@link FetchText}: fetch the resource, reject on non-OK, return text. */
|
|
const fetchResolvableText = async (addressable, options) => {
|
|
const response = await fetchResolvable(addressable, options);
|
|
if (!response.ok) throw new Error(`fetchResolvableText: ${response.status} ${response.statusText} for ${addressable.url}`);
|
|
return getResponseText(response);
|
|
};
|
|
async function fetchStream(addressable, options) {
|
|
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
const response = await fetchResolvable(addressable, fetchOptions);
|
|
if (!response.body) throw new Error("Response has no body");
|
|
return new ChunkedStreamIterable(response.body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : []);
|
|
}
|
|
/**
|
|
* Returns a {@link FetchBytes} function that samples bandwidth via EWMA
|
|
* per body chunk. The factory captures the running bandwidth state
|
|
* internally; per chunk it computes the next state and notifies the
|
|
* supplied `onSample` callback.
|
|
*
|
|
* The factory's internal accumulator is seeded from `initial` and updated
|
|
* on every chunk; callers don't need to thread it back in. `onSample`
|
|
* receives the *new* state after each chunk — typical use is to bridge
|
|
* samples back into engine state for ABR consumers.
|
|
*
|
|
* @param initial - Starting `BandwidthState` (commonly zeros or the
|
|
* engine's current accumulator).
|
|
* @param onSample - Called with the new `BandwidthState` after each chunk.
|
|
*/
|
|
function createTrackedFetch(initial, onSample) {
|
|
let state = initial;
|
|
return async (addressable, options) => {
|
|
const { minChunkSize, ...fetchOptions } = options ?? {};
|
|
const response = await fetchResolvable(addressable, fetchOptions);
|
|
if (!response.body) throw new Error("Response has no body");
|
|
const body = response.body;
|
|
return { [Symbol.asyncIterator]: async function* () {
|
|
let chunkStart = performance.now();
|
|
for await (const chunk of new ChunkedStreamIterable(body, ...minChunkSize !== void 0 ? [{ minChunkSize }] : [])) {
|
|
const elapsed = performance.now() - chunkStart;
|
|
state = sampleBandwidth(state, elapsed, chunk.byteLength);
|
|
onSample(state);
|
|
yield chunk;
|
|
chunkStart = performance.now();
|
|
}
|
|
} };
|
|
};
|
|
}
|
|
//#endregion
|
|
export { createTrackedFetch, fetchResolvable, fetchResolvableText, fetchStream, getResponseText };
|
|
|
|
//# sourceMappingURL=fetch.js.map
|