mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-08-15 18:45:24 +00:00
Chromecast improvements (#1657)
This commit is contained in:
+10
-1
@@ -24,6 +24,8 @@ export type VideoMeta = {
|
||||
title: string;
|
||||
poster: string | null;
|
||||
blurhash: string | null;
|
||||
previousSlug: string | null;
|
||||
nextSlug: string | null;
|
||||
};
|
||||
|
||||
export const fetchVideoInfo = async (
|
||||
@@ -86,6 +88,8 @@ type ApiVideo = {
|
||||
name?: string;
|
||||
poster?: ApiImage | null;
|
||||
} | null;
|
||||
previous?: { video?: string | null } | null;
|
||||
next?: { video?: string | null } | null;
|
||||
};
|
||||
|
||||
const entryDisplayNumber = (entry: ApiEntry): string => {
|
||||
@@ -108,7 +112,10 @@ export const fetchVideoMeta = async (
|
||||
presign?: string,
|
||||
): Promise<VideoMeta> => {
|
||||
const res = await fetch(
|
||||
withPresign(`${apiUrl}/api/videos/${slug}?with=show`, presign),
|
||||
withPresign(
|
||||
`${apiUrl}/api/videos/${slug}?with=show,previous,next`,
|
||||
presign,
|
||||
),
|
||||
);
|
||||
if (!res.ok) throw new Error(`video request failed: ${res.status}`);
|
||||
const data = (await res.json()) as ApiVideo;
|
||||
@@ -135,5 +142,7 @@ export const fetchVideoMeta = async (
|
||||
? withPresign(`${apiUrl}/api/images/${poster.id}?quality=high`, presign)
|
||||
: null,
|
||||
blurhash: poster?.blurhash ?? null,
|
||||
previousSlug: data.previous?.video ?? null,
|
||||
nextSlug: data.next?.video ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
+98
-13
@@ -6,16 +6,24 @@ import { SubtitleManager } from "./subtitles";
|
||||
import { ReceiverUi } from "./ui";
|
||||
|
||||
const { EventType, DetailedErrorCode } = cast.framework.events;
|
||||
const { EventType: SystemEventType } = cast.framework.system;
|
||||
const {
|
||||
MessageType,
|
||||
Command,
|
||||
HlsSegmentFormat,
|
||||
HlsVideoSegmentFormat,
|
||||
GenericMediaMetadata,
|
||||
Track,
|
||||
TrackType,
|
||||
TextTrackType,
|
||||
TextTrackStyle,
|
||||
TextTrackEdgeType,
|
||||
TextTrackFontGenericFamily,
|
||||
} = cast.framework.messages;
|
||||
|
||||
// prev/next requests we forward to the senders (react-native-omni listens to it)
|
||||
const KYOO_NAMESPACE = "urn:x-cast:dev.zoriya.omni";
|
||||
|
||||
export const filterMasterPlaylist = (
|
||||
text: string,
|
||||
baseUrl: string,
|
||||
@@ -69,6 +77,7 @@ export class KyooReceiver {
|
||||
document.getElementById("subtitle-layer") as HTMLElement,
|
||||
);
|
||||
#progress = new ProgressObserver(this.#player);
|
||||
#idleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
start(): void {
|
||||
this.#ui.hideCafChrome();
|
||||
@@ -76,7 +85,35 @@ export class KyooReceiver {
|
||||
this.#progress.start();
|
||||
|
||||
this.#playbackConfig.initialBandwidth = 20_000_000;
|
||||
this.#playbackConfig.segmentRequestRetryLimit = 4;
|
||||
const retryParameters = {
|
||||
maxAttempts: 5,
|
||||
baseDelay: 1000,
|
||||
backoffFactor: 2,
|
||||
fuzzFactor: 0.5,
|
||||
connectionTimeout: 0,
|
||||
stallTimeout: 0,
|
||||
timeout: 90_000,
|
||||
};
|
||||
this.#playbackConfig.shakaConfig = {
|
||||
manifest: { retryParameters },
|
||||
streaming: { retryParameters },
|
||||
};
|
||||
this.#player.setMessageInterceptor(MessageType.LOAD, this.#onLoad);
|
||||
// we never queue anything, so caf has nothing to skip to: forward the
|
||||
// intent to the senders instead, they are the ones knowing the next entry
|
||||
// & they load it like they do for their own prev/next buttons.
|
||||
this.#player.setMessageInterceptor(MessageType.QUEUE_UPDATE, (request) => {
|
||||
const jump = request.jump ?? 0;
|
||||
if (jump || request.currentItemId !== undefined) {
|
||||
const action = jump < 0 ? "prev" : "next";
|
||||
console.log(
|
||||
`[kyoo-receiver] asking senders to play the ${action} entry`,
|
||||
);
|
||||
this.#context.sendCustomMessage(KYOO_NAMESPACE, undefined, { action });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
this.#player.setMessageInterceptor(
|
||||
MessageType.MEDIA_STATUS,
|
||||
this.#onStatus,
|
||||
@@ -87,7 +124,10 @@ export class KyooReceiver {
|
||||
});
|
||||
this.#player.addEventListener(EventType.MEDIA_FINISHED, (e) => {
|
||||
this.#subtitles.clear();
|
||||
if (e.endedReason === "ERROR") this.#player.stop();
|
||||
// if no new item is enqueued, reset the ui
|
||||
// (will be cancelled if a request comes in)
|
||||
if (e.endedReason === "END_OF_STREAM")
|
||||
this.#idleTimer = setTimeout(() => this.#ui.reset(), 5_000);
|
||||
});
|
||||
this.#player.addEventListener(EventType.ERROR, (e) => {
|
||||
if (e.detailedErrorCode === DetailedErrorCode.LOAD_INTERRUPTED) {
|
||||
@@ -98,9 +138,20 @@ export class KyooReceiver {
|
||||
this.#player.stop();
|
||||
});
|
||||
|
||||
// senders that lost the app can't answer a skip, don't offer it anymore
|
||||
this.#context.addEventListener(SystemEventType.SENDER_DISCONNECTED, () => {
|
||||
if (this.#context.getSenders().length === 0)
|
||||
this.#player.removeSupportedMediaCommands(
|
||||
Command.QUEUE_PREV | Command.QUEUE_NEXT,
|
||||
);
|
||||
});
|
||||
|
||||
const options = new cast.framework.CastReceiverOptions();
|
||||
options.playbackConfig = this.#playbackConfig;
|
||||
options.maxInactivity = 3600;
|
||||
options.customNamespaces = {
|
||||
[KYOO_NAMESPACE]: cast.framework.system.MessageType.JSON,
|
||||
};
|
||||
this.#context.start(options);
|
||||
}
|
||||
|
||||
@@ -122,6 +173,7 @@ export class KyooReceiver {
|
||||
copied.trackContentId = undefined;
|
||||
return copied;
|
||||
});
|
||||
stripped.textTrackStyle = undefined;
|
||||
return stripped;
|
||||
};
|
||||
|
||||
@@ -140,6 +192,7 @@ export class KyooReceiver {
|
||||
request: messages.LoadRequestData,
|
||||
): Promise<messages.LoadRequestData> => {
|
||||
const data = (asObject(request.media?.customData) as KyooCastData) ?? {};
|
||||
clearTimeout(this.#idleTimer);
|
||||
|
||||
this.#ui.clearError();
|
||||
this.#ui.dismissSplash();
|
||||
@@ -158,24 +211,48 @@ export class KyooReceiver {
|
||||
request.media.hlsVideoSegmentFormat = HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
|
||||
if (request.media) {
|
||||
const style = new TextTrackStyle();
|
||||
style.foregroundColor = "#FFFFFFFF";
|
||||
style.backgroundColor = "#00000000";
|
||||
style.edgeType = TextTrackEdgeType.OUTLINE;
|
||||
style.edgeColor = "#000000FF";
|
||||
style.fontFamily = "Poppins";
|
||||
style.fontGenericFamily = TextTrackFontGenericFamily.SANS_SERIF;
|
||||
request.media.textTrackStyle = style;
|
||||
}
|
||||
|
||||
// Fetch the master ourselves to:
|
||||
// - resolve the 302 (relative urls resolve against the pre-redirect url)
|
||||
// (https://github.com/shaka-project/shaka-player/issues/2679)
|
||||
// - drop variants the device can't decode
|
||||
// (https://github.com/shaka-project/shaka-player/issues/9211)
|
||||
if (request.media?.contentUrl) {
|
||||
try {
|
||||
const res = await fetch(request.media.contentUrl, {
|
||||
redirect: "follow",
|
||||
});
|
||||
const finalUrl =
|
||||
res.redirected && res.url ? res.url : request.media.contentUrl;
|
||||
const filtered = filterMasterPlaylist(await res.text(), finalUrl);
|
||||
request.media.contentUrl = filtered
|
||||
? `data:application/vnd.apple.mpegurl,${encodeURIComponent(filtered)}`
|
||||
: finalUrl;
|
||||
} catch (e) {
|
||||
console.error("[kyoo-receiver] manifest fetch/filter failed", e);
|
||||
const masterUrl = request.media.contentUrl;
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const res = await fetch(masterUrl, { redirect: "follow" });
|
||||
if (!res.ok) throw new Error(`master request failed: ${res.status}`);
|
||||
const finalUrl = res.redirected && res.url ? res.url : masterUrl;
|
||||
const filtered = filterMasterPlaylist(await res.text(), finalUrl);
|
||||
request.media.contentUrl = filtered
|
||||
? `data:application/vnd.apple.mpegurl,${encodeURIComponent(filtered)}`
|
||||
: finalUrl;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (attempt >= 4) {
|
||||
console.error("[kyoo-receiver] manifest fetch/filter failed", e);
|
||||
break;
|
||||
}
|
||||
console.warn(
|
||||
`[kyoo-receiver] manifest fetch failed (attempt ${attempt + 1}), retrying`,
|
||||
e,
|
||||
);
|
||||
const base = Math.min(1000 * 2 ** attempt, 10_000);
|
||||
await new Promise((r) =>
|
||||
setTimeout(r, base / 2 + Math.random() * (base / 2)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +284,14 @@ export class KyooReceiver {
|
||||
try {
|
||||
const meta = await fetchVideoMeta(data.apiUrl, data.slug, data.presign);
|
||||
this.#ui.setMetadata(meta);
|
||||
this.#player.setSupportedMediaCommands(
|
||||
Command.PAUSE |
|
||||
Command.SEEK |
|
||||
Command.STREAM_VOLUME |
|
||||
Command.STREAM_MUTE |
|
||||
(meta.previousSlug ? Command.QUEUE_PREV : 0) |
|
||||
(meta.nextSlug ? Command.QUEUE_NEXT : 0),
|
||||
);
|
||||
if (meta.videoId && meta.entryId) {
|
||||
this.#progress.load(data, {
|
||||
videoId: meta.videoId,
|
||||
|
||||
@@ -133,6 +133,14 @@ export class ReceiverUi {
|
||||
this.#el.splash.classList.add("gone");
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.#el.splash.classList.remove("gone");
|
||||
this.clearMetadata();
|
||||
this.setLoading(false);
|
||||
this.setPaused(false);
|
||||
this.show({ sticky: true });
|
||||
}
|
||||
|
||||
setMetadata({
|
||||
showName,
|
||||
entryName,
|
||||
|
||||
+2
-2
@@ -46,7 +46,7 @@
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^4.3.1",
|
||||
"react-native-nitro-modules": "^0.35.7",
|
||||
"react-native-omni": "^1.11.3",
|
||||
"react-native-omni": "^1.11.4",
|
||||
"react-native-pager-view": "8.0.2",
|
||||
"react-native-reanimated": "4.5.0",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
@@ -1279,7 +1279,7 @@
|
||||
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.35.10", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KsySOAIkbSTjNiX2GvXiNT9P5DMeZd99yvtE/+Lw7RXV7Ztxh9Z+YyAbYkBqadhN5J/KXXuPjhRYgyLOIZgsbQ=="],
|
||||
|
||||
"react-native-omni": ["react-native-omni@1.11.3", "", { "dependencies": { "@expo/config-plugins": "^57.0.3", "@videojs/core": "github:zoriya/v10#build26-core", "@videojs/media": "github:zoriya/v10#build26-media", "@videojs/react": "github:zoriya/v10#build26-react", "@videojs/spf": "github:zoriya/v10#build26-spf", "@videojs/store": "github:zoriya/v10#build26-store", "@videojs/utils": "github:zoriya/v10#build26-utils", "jassub": "^2.5.7", "libpgs": "^0.8.1" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.86.0", "react-native-nitro-modules": ">=0.36.0" } }, "sha512-A2Mw07v5pSF4wvxz/NFfQhvpu512AMrGazZ+MRm/oYCvSnJyonQXvJ0cBvtaBJeYTAq1rPlBrRX4MgCIChyF+g=="],
|
||||
"react-native-omni": ["react-native-omni@1.11.4", "", { "dependencies": { "@expo/config-plugins": "^57.0.3", "@videojs/core": "github:zoriya/v10#build26-core", "@videojs/media": "github:zoriya/v10#build26-media", "@videojs/react": "github:zoriya/v10#build26-react", "@videojs/spf": "github:zoriya/v10#build26-spf", "@videojs/store": "github:zoriya/v10#build26-store", "@videojs/utils": "github:zoriya/v10#build26-utils", "jassub": "^2.5.7", "libpgs": "^0.8.1" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.86.0", "react-native-nitro-modules": ">=0.36.0" } }, "sha512-os7HCoTJXC/iOjds3FkGx41EmkOWPGDfX4QISrYfZfty0Tw30ESLHOvsLpq0IPrvJ2bxm6cTdDKdjsj2JJ01Lw=="],
|
||||
|
||||
"react-native-pager-view": ["react-native-pager-view@8.0.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Y8All2BbjidI4ZIF9kBOIIoldkqrY/2FXapHZMdjwD+ByXhiOWTFenPs5rq9KRN5uAH/gOXtQCqHLxQDNBQRiQ=="],
|
||||
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^4.3.1",
|
||||
"react-native-nitro-modules": "^0.35.7",
|
||||
"react-native-omni": "^1.11.3",
|
||||
"react-native-omni": "^1.11.4",
|
||||
"react-native-pager-view": "8.0.2",
|
||||
"react-native-reanimated": "4.5.0",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
|
||||
@@ -102,3 +102,5 @@ require (
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/asticode/go-astisub => github.com/zoriya/go-astisub v0.42.1-0.20260810114112-a5151634cd4e
|
||||
|
||||
+2
-2
@@ -14,8 +14,6 @@ github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xbl
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.59.0 h1:tjbwDym+MTSxqkAhJoHRZmHMXK6Jv4vGx+97FptKH6k=
|
||||
github.com/asticode/go-astikit v0.59.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astisub v0.42.0 h1:IJuJJTQwCSFeY9bx1kTzdCwkIeG27wj4CAh6nHc+pvY=
|
||||
github.com/asticode/go-astisub v0.42.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
||||
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
|
||||
github.com/asticode/go-astits v1.15.0 h1:yRyCiUc8Jj4F7clt2GDxHghMpWuFL5rkaLuGUd2/0J4=
|
||||
github.com/asticode/go-astits v1.15.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
@@ -184,6 +182,8 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk=
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI=
|
||||
github.com/zoriya/go-astisub v0.42.1-0.20260810114112-a5151634cd4e h1:nlZ82WW9dGqfFtV+hmETkASo86dm0u0gaqSYpVJgafQ=
|
||||
github.com/zoriya/go-astisub v0.42.1-0.20260810114112-a5151634cd4e/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
||||
gitlab.com/opennota/screengen v1.0.2 h1:GxYTJdAPEzmg5v5CV4dgn45JVW+EcXXAvCxhE7w6UDw=
|
||||
gitlab.com/opennota/screengen v1.0.2/go.mod h1:4kED4yriw2zslwYmXFCa5qCvEKwleBA7l5OE+d94NTU=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
|
||||
Reference in New Issue
Block a user