diff --git a/package.json b/package.json index 388054df..3148f860 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@ladjs/country-language": "^1.0.3", "@movie-web/providers": "^2.0.5", "@noble/hashes": "^1.3.3", + "@plasmohq/messaging": "^0.6.1", "@react-spring/web": "^9.7.3", "@scure/bip39": "^1.2.2", "@sozialhelden/ietf-language-tags": "^5.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6bb9bd..5c1d1c9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ dependencies: '@noble/hashes': specifier: ^1.3.3 version: 1.3.3 + '@plasmohq/messaging': + specifier: ^0.6.1 + version: 0.6.1(react@18.2.0) '@react-spring/web': specifier: ^9.7.3 version: 9.7.3(react-dom@18.2.0)(react@18.2.0) @@ -1980,6 +1983,18 @@ packages: tslib: 2.6.2 dev: true + /@plasmohq/messaging@0.6.1(react@18.2.0): + resolution: {integrity: sha512-/nn1k8SG5z++o/NnZu+byHWcC9MhPLxfmvj+AP3buqMn7uwfYDcYWURLuMW2Knw08HBg+wku2v1Ltt4evN0nzA==} + peerDependencies: + react: ^16.8.6 || ^17 || ^18 + peerDependenciesMeta: + react: + optional: true + dependencies: + nanoid: 5.0.3 + react: 18.2.0 + dev: false + /@react-spring/animated@9.7.3(react@18.2.0): resolution: {integrity: sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==} peerDependencies: @@ -5156,6 +5171,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.3: + resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /nanoid@5.0.4: resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==} engines: {node: ^18 || >=20} diff --git a/src/@types/plasmo.d.ts b/src/@types/plasmo.d.ts new file mode 100644 index 00000000..4570cae4 --- /dev/null +++ b/src/@types/plasmo.d.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import "@plasmohq/messaging"; + +export interface PlasmoRequestBody { + ruleId: number; + domain: string; + requestHeaders?: Record; + responseHeaders?: Record; +} + +export type PlasmoResponseBody = + | { + success: true; + ruleId: number; + } + | { + success: false; + error: string; + }; + +interface MmMetadata { + "declarative-net-request": { + req: PlasmoRequestBody; + res: PlasmoResponseBody; + }; + "proxy-request": { + req: PlasmoRequestBody; + res: PlasmoResponseBody; + }; +} + +interface MpMetadata {} + +declare module "@plasmohq/messaging" { + interface MessagesMetadata extends MmMetadata {} + interface PortsMetadata extends MpMetadata {} +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 7a9ce041..11a0799c 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -1,6 +1,8 @@ +import { sendToBackgroundViaRelay } from "@plasmohq/messaging"; import fscreen from "fscreen"; import Hls, { Level } from "hls.js"; +import { PlasmoRequestBody, PlasmoResponseBody } from "@/@types/plasmo"; import { DisplayInterface, DisplayInterfaceEvents, @@ -100,65 +102,75 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } function setupSource(vid: HTMLVideoElement, src: LoadableSource) { - if (src.type === "hls") { - if (canPlayHlsNatively(vid)) { - vid.src = processCdnLink(src.url); + // TODO: Add check whether the extension is installed + sendToBackgroundViaRelay({ + name: "declarative-net-request", + body: { + ruleId: 1, + domain: src.type === "hls" ? new URL(src.url).hostname : src.url, + requestHeaders: src.preferredHeaders, + }, + }).then(() => { + if (src.type === "hls") { + if (canPlayHlsNatively(vid)) { + vid.src = processCdnLink(src.url); + vid.currentTime = startAt; + return; + } + + if (!Hls.isSupported()) throw new Error("HLS not supported"); + if (!hls) { + hls = new Hls({ + maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once + fragLoadPolicy: { + default: { + maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin + maxTimeToFirstByteMs: 30 * 1000, + errorRetry: { + maxNumRetry: 2, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + timeoutRetry: { + maxNumRetry: 3, + maxRetryDelayMs: 0, + retryDelayMs: 0, + }, + }, + }, + }); + hls.on(Hls.Events.ERROR, (event, data) => { + console.error("HLS error", data); + if (data.fatal) { + emit("error", { + message: data.error.message, + stackTrace: data.error.stack, + errorName: data.error.name, + type: "hls", + }); + } + }); + hls.on(Hls.Events.MANIFEST_LOADED, () => { + if (!hls) return; + reportLevels(); + setupQualityForHls(); + }); + hls.on(Hls.Events.LEVEL_SWITCHED, () => { + if (!hls) return; + const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); + emit("changedquality", quality); + }); + } + + hls.attachMedia(vid); + hls.loadSource(processCdnLink(src.url)); vid.currentTime = startAt; return; } - if (!Hls.isSupported()) throw new Error("HLS not supported"); - if (!hls) { - hls = new Hls({ - maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once - fragLoadPolicy: { - default: { - maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin - maxTimeToFirstByteMs: 30 * 1000, - errorRetry: { - maxNumRetry: 2, - retryDelayMs: 1000, - maxRetryDelayMs: 8000, - }, - timeoutRetry: { - maxNumRetry: 3, - maxRetryDelayMs: 0, - retryDelayMs: 0, - }, - }, - }, - }); - hls.on(Hls.Events.ERROR, (event, data) => { - console.error("HLS error", data); - if (data.fatal) { - emit("error", { - message: data.error.message, - stackTrace: data.error.stack, - errorName: data.error.name, - type: "hls", - }); - } - }); - hls.on(Hls.Events.MANIFEST_LOADED, () => { - if (!hls) return; - reportLevels(); - setupQualityForHls(); - }); - hls.on(Hls.Events.LEVEL_SWITCHED, () => { - if (!hls) return; - const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); - emit("changedquality", quality); - }); - } - - hls.attachMedia(vid); - hls.loadSource(processCdnLink(src.url)); + vid.src = processCdnLink(src.url); vid.currentTime = startAt; - return; - } - - vid.src = processCdnLink(src.url); - vid.currentTime = startAt; + }); } function setSource() { diff --git a/src/components/player/utils/convertRunoutputToSource.ts b/src/components/player/utils/convertRunoutputToSource.ts index fba59e63..f54c5396 100644 --- a/src/components/player/utils/convertRunoutputToSource.ts +++ b/src/components/player/utils/convertRunoutputToSource.ts @@ -28,6 +28,7 @@ export function convertRunoutputToSource(out: { return { type: "hls", url: out.stream.playlist, + preferredHeaders: out.stream.preferredHeaders, }; } if (out.stream.type === "file") { @@ -49,6 +50,7 @@ export function convertRunoutputToSource(out: { return { type: "file", qualities, + preferredHeaders: out.stream.preferredHeaders, }; } throw new Error("unrecognized type"); diff --git a/src/stores/player/utils/qualities.ts b/src/stores/player/utils/qualities.ts index dbd84b5c..e5140d53 100644 --- a/src/stores/player/utils/qualities.ts +++ b/src/stores/player/utils/qualities.ts @@ -1,4 +1,4 @@ -import { Qualities } from "@movie-web/providers"; +import { Qualities, Stream } from "@movie-web/providers"; import { QualityStore } from "@/stores/quality"; @@ -14,16 +14,19 @@ export type SourceFileStream = { export type LoadableSource = { type: StreamType; url: string; + preferredHeaders?: Stream["preferredHeaders"]; }; export type SourceSliceSource = | { type: "file"; qualities: Partial>; + preferredHeaders?: Stream["preferredHeaders"]; } | { type: "hls"; url: string; + preferredHeaders?: Stream["preferredHeaders"]; }; const qualitySorting: Record = { diff --git a/src/utils/providers.ts b/src/utils/providers.ts index e5c8503c..73c95662 100644 --- a/src/utils/providers.ts +++ b/src/utils/providers.ts @@ -62,5 +62,7 @@ function makeLoadBalancedSimpleProxyFetcher() { export const providers = makeProviders({ fetcher: makeStandardFetcher(fetch), proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(), - target: targets.BROWSER, + // TODO: Add check whether the extension is installed + // target: targets.BROWSER, + target: targets.BROWSER_EXTENSION, }) as any as ProviderControls;