Skip to content

Commit

Permalink
Merge pull request #320 from Novage/resuming-segment-loading
Browse files Browse the repository at this point in the history
Resuming segment downloading.
  • Loading branch information
i-zolotarenko authored Dec 29, 2023
2 parents 2fd5fb0 + 04854d1 commit 0508d9f
Show file tree
Hide file tree
Showing 14 changed files with 534 additions and 232 deletions.
22 changes: 11 additions & 11 deletions p2p-media-loader-demo/index.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Vite + React + TS</title>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/clappr@latest"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/gh/clappr/clappr-level-selector-plugin@latest/dist/level-selector.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/npm/shaka-player@~4.6.0/dist/shaka-player.compiled.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/gh/clappr/dash-shaka-playback@latest/dist/dash-shaka-playback.external.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Vite + React + TS</title>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/clappr@latest"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/gh/clappr/clappr-level-selector-plugin@latest/dist/level-selector.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/npm/shaka-player@~4.6.0/dist/shaka-player.compiled.min.js"></script>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/gh/clappr/dash-shaka-playback@latest/dist/dash-shaka-playback.external.js"></script>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions p2p-media-loader-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const streamUrls = {
hlsBigBunnyBuck: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
hlsByteRangeVideo:
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8",
hlsOneLevelByteRangeVideo:
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8",
hlsBasicExample:
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8",
hlsAdvancedVideo:
Expand Down
2 changes: 2 additions & 0 deletions packages/p2p-media-loader-core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class Core<TStream extends Stream = Stream> {
p2pNotReceivingBytesTimeoutMs: 1000,
p2pLoaderDestroyTimeoutMs: 30 * 1000,
httpNotReceivingBytesTimeoutMs: 1000,
httpErrorRetries: 3,
p2pErrorRetries: 3,
};
private readonly bandwidthApproximator = new BandwidthApproximator();
private segmentStorage?: SegmentsMemoryStorage;
Expand Down
182 changes: 142 additions & 40 deletions packages/p2p-media-loader-core/src/http-loader.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,146 @@
import { Settings } from "./types";
import { Request, RequestError, HttpRequestErrorType } from "./request";

export async function fulfillHttpSegmentRequest(
request: Request,
settings: Pick<Settings, "httpNotReceivingBytesTimeoutMs">
) {
const headers = new Headers();
const { segment } = request;
const { url, byteRange } = segment;

if (byteRange) {
const { start, end } = byteRange;
const byteRangeString = `bytes=${start}-${end}`;
headers.set("Range", byteRangeString);
import {
Request,
RequestError,
HttpRequestErrorType,
RequestControls,
} from "./request";

type HttpSettings = Pick<Settings, "httpNotReceivingBytesTimeoutMs">;

export class HttpRequestExecutor {
private readonly requestControls: RequestControls;
private readonly requestHeaders = new Headers();
private readonly abortController = new AbortController();
private readonly expectedBytesLength?: number;
private readonly byteRange?: { start: number; end?: number };

constructor(
private readonly request: Request,
private readonly settings: HttpSettings
) {
const { byteRange } = this.request.segment;
if (byteRange) this.byteRange = { ...byteRange };

if (request.loadedBytes !== 0) {
this.byteRange = this.byteRange ?? { start: 0 };
this.byteRange.start = this.byteRange.start + request.loadedBytes;
}
if (this.request.totalBytes) {
this.expectedBytesLength =
this.request.totalBytes - this.request.loadedBytes;
}

if (this.byteRange) {
const { start, end } = this.byteRange;
this.requestHeaders.set("Range", `bytes=${start}-${end ?? ""}`);
}

const { httpNotReceivingBytesTimeoutMs } = this.settings;
this.requestControls = this.request.start(
{ type: "http" },
{
abort: () => this.abortController.abort("abort"),
notReceivingBytesTimeoutMs: httpNotReceivingBytesTimeoutMs,
}
);
void this.fetch();
}

const abortController = new AbortController();
const requestControls = request.start(
{ type: "http" },
{
abort: () => abortController.abort("abort"),
notReceivingBytesTimeoutMs: settings.httpNotReceivingBytesTimeoutMs,
private async fetch() {
const { segment } = this.request;
try {
const response = await window.fetch(segment.url, {
headers: this.requestHeaders,
signal: this.abortController.signal,
});
this.handleResponseHeaders(response);

if (!response.body) return;
const { requestControls } = this;
requestControls.firstBytesReceived();

const reader = response.body.getReader();
for await (const chunk of readStream(reader)) {
this.requestControls.addLoadedChunk(chunk);
}
requestControls.completeOnSuccess();
} catch (error) {
this.handleError(error);
}
);
try {
const fetchResponse = await window.fetch(url, {
headers,
signal: abortController.signal,
});
requestControls.firstBytesReceived();

if (!fetchResponse.ok) {
throw new RequestError("fetch-error", fetchResponse.statusText);
}

private handleResponseHeaders(response: Response) {
if (!response.ok) {
if (response.status === 406) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
} else {
throw new RequestError("http-error", response.statusText);
}
}

if (!fetchResponse.body) return;
const totalBytesString = fetchResponse.headers.get("Content-Length");
if (totalBytesString) request.setTotalBytes(+totalBytesString);
const { byteRange } = this;
if (byteRange) {
if (response.status === 200) {
if (this.request.segment.byteRange) {
throw new RequestError("http-unexpected-status-code");
} else {
this.request.clearLoadedBytes();
}
} else {
if (response.status !== 206) {
throw new RequestError(
"http-unexpected-status-code",
response.statusText
);
}
const contentLengthHeader = response.headers.get("Content-Length");
if (
contentLengthHeader &&
this.expectedBytesLength !== undefined &&
this.expectedBytesLength !== +contentLengthHeader
) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}

const contentRangeHeader = response.headers.get("Content-Range");
const contentRange = contentRangeHeader
? parseContentRangeHeader(contentRangeHeader)
: undefined;
if (contentRange) {
const { from, to, total } = contentRange;
if (
(total !== undefined && this.request.totalBytes !== total) ||
(from !== undefined && byteRange.start !== from) ||
(to !== undefined &&
byteRange.end !== undefined &&
byteRange.end !== to)
) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
}
}
}

const reader = fetchResponse.body.getReader();
for await (const chunk of readStream(reader)) {
requestControls.addLoadedChunk(chunk);
if (response.status === 200 && this.request.totalBytes === undefined) {
const contentLengthHeader = response.headers.get("Content-Length");
if (contentLengthHeader) this.request.setTotalBytes(+contentLengthHeader);
}
requestControls.completeOnSuccess();
} catch (error) {
}

private handleError(error: unknown) {
if (error instanceof Error) {
if (error.name !== "abort") return;

const httpLoaderError: RequestError<HttpRequestErrorType> = !(
error instanceof RequestError
)
? new RequestError("fetch-error", error.message)
? new RequestError("http-error", error.message)
: error;
requestControls.abortOnError(httpLoaderError);
this.requestControls.abortOnError(httpLoaderError);
}
}
}
Expand All @@ -66,3 +154,17 @@ async function* readStream(
yield value;
}
}

function parseContentRangeHeader(headerValue: string) {
const match = headerValue
.trim()
.match(/^bytes (?:(?:(\d+)|)-(?:(\d+)|)|\*)\/(?:(\d+)|\*)$/);
if (!match) return;

const [, from, to, total] = match;
return {
from: from ? parseInt(from) : undefined,
to: to ? parseInt(to) : undefined,
total: total ? parseInt(total) : undefined,
};
}
Loading

0 comments on commit 0508d9f

Please sign in to comment.