From a978ed6aebbe9de926673eb23c1da4c2978b5908 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 23 Oct 2022 14:49:29 +0900 Subject: [PATCH] Cast app receive videos --- back/src/Kyoo.Core/CoreModule.cs | 10 ++- chromecast/.dockerignore | 11 +++ chromecast/Dockerfile.dev | 9 +++ chromecast/src/api.ts | 91 +++++++++++++++++++++++++ chromecast/src/index.html | 2 +- chromecast/src/index.ts | 39 +++-------- docker-compose.dev.yml | 13 ++++ docker-compose.yml | 1 + front/src/player/cast/cast-provider.tsx | 3 +- front/src/player/cast/state.tsx | 20 +++--- front/src/player/state.tsx | 8 +-- 11 files changed, 162 insertions(+), 45 deletions(-) create mode 100644 chromecast/.dockerignore create mode 100644 chromecast/Dockerfile.dev create mode 100644 chromecast/src/api.ts diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs index a3ec6761..48e23c18 100644 --- a/back/src/Kyoo.Core/CoreModule.cs +++ b/back/src/Kyoo.Core/CoreModule.cs @@ -121,7 +121,9 @@ namespace Kyoo.Core services.AddHttpContextAccessor(); services.AddTransient, JsonOptions>(); - services.AddMvcCore() + services + .AddMvcCore() + .AddCors() .AddNewtonsoftJson() .AddDataAnnotations() .AddControllersAsServices() @@ -167,6 +169,12 @@ namespace Kyoo.Core }, SA.Before), SA.New(app => app.UseResponseCompression(), SA.Routing + 1), SA.New(app => app.UseRouting(), SA.Routing), + SA.New(app => app.UseCors(x => x + .SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + ), SA.Routing + 2), SA.New(app => app.UseEndpoints(x => x.MapControllers()), SA.Endpoint) }; } diff --git a/chromecast/.dockerignore b/chromecast/.dockerignore new file mode 100644 index 00000000..32b837e1 --- /dev/null +++ b/chromecast/.dockerignore @@ -0,0 +1,11 @@ +Dockerfile +Dockerfile.dev +.dockerignore +.eslintrc.json +.gitignore +node_modules +npm-debug.log +README.md +.parcel-cache +.git +dist diff --git a/chromecast/Dockerfile.dev b/chromecast/Dockerfile.dev new file mode 100644 index 00000000..70088d13 --- /dev/null +++ b/chromecast/Dockerfile.dev @@ -0,0 +1,9 @@ +FROM node:16-alpine AS builder +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn --frozen-lockfile + +EXPOSE 1234 +ENV PORT 1234 +CMD ["yarn", "dev"] diff --git a/chromecast/src/api.ts b/chromecast/src/api.ts new file mode 100644 index 00000000..b7b4412d --- /dev/null +++ b/chromecast/src/api.ts @@ -0,0 +1,91 @@ +/* + * Kyoo - A portable and vast media library solution. + * Copyright (c) Kyoo. + * + * See AUTHORS.md and LICENSE file in the project root for full license information. + * + * Kyoo is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Kyoo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kyoo. If not, see . + */ + +export type Item = { + /** + * The slug of this episode. + */ + slug: string; + /** + * The title of the show containing this episode. + */ + showTitle?: string; + /** + * The slug of the show containing this episode + */ + showSlug?: string; + /** + * The season in witch this episode is in. + */ + seasonNumber?: number; + /** + * The number of this episode is it's season. + */ + episodeNumber?: number; + /** + * The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + */ + absoluteNumber?: number; + /** + * The title of this episode. + */ + name: string; + /** + * true if this is a movie, false otherwise. + */ + isMovie: boolean; + /** + * An url to the poster of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present. + */ + poster?: string | null; + /** + * An url to the thumbnail of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present. + */ + thumbnail?: string | null; + /** + * An url to the logo of this resource. If this resource does not have an image, the link will be null. If the kyoo's instance is not capable of handling this kind of image for the specific resource, this field won't be present. + */ + logo?: string | null; + /** + * The links to the videos of this watch item. + */ + link: { + direct: string, + transmux: string, + } +}; + +export const getItem = async (slug: string, apiUrl: string) => { + try { + const resp = await fetch(`${apiUrl}/watch/${slug}`); + if (!resp.ok) { + console.error(await resp.text()); + return null; + } + const ret = await resp.json() as Item; + if (!ret) return null; + ret.link.direct = `${apiUrl}/${ret.link.direct}`; + ret.link.transmux = `${apiUrl}/${ret.link.transmux}`; + return ret; + } catch(e) { + console.error("Fetch error", e); + return null + } +} diff --git a/chromecast/src/index.html b/chromecast/src/index.html index cb03255d..0aca6924 100644 --- a/chromecast/src/index.html +++ b/chromecast/src/index.html @@ -7,6 +7,6 @@ - + diff --git a/chromecast/src/index.ts b/chromecast/src/index.ts index 2ec61bb0..acebc9be 100644 --- a/chromecast/src/index.ts +++ b/chromecast/src/index.ts @@ -18,40 +18,23 @@ * along with Kyoo. If not, see . */ +import { getItem } from "./api"; + const context = cast.framework.CastReceiverContext.getInstance(); const playerManager = context.getPlayerManager(); -playerManager.setMessageInterceptor(cast.framework.messages.MessageType.LOAD, (loadRequestData) => { - console.log(loadRequestData) - const error = new cast.framework.messages.ErrorData( - cast.framework.messages.ErrorType.LOAD_FAILED, - ); - if (!loadRequestData.media) { - error.reason = cast.framework.messages.ErrorReason.INVALID_PARAMS; - return error; - } +playerManager.setMessageInterceptor(cast.framework.messages.MessageType.LOAD, async (loadRequestData) => { + if (loadRequestData.media.contentUrl && loadRequestData.media.metadata) return loadRequestData; - if (!loadRequestData.media.entity) { - return loadRequestData; + const item = await getItem(loadRequestData.media.contentId, loadRequestData.media.customData.serverUrl); + if (!item) { + return new cast.framework.messages.ErrorData( + cast.framework.messages.ErrorType.LOAD_FAILED, + ); } - + loadRequestData.media.contentUrl = item.link.direct; + loadRequestData.media.metadata = item; return loadRequestData; - /* return thirdparty */ - /* .fetchAssetAndAuth(loadRequestData.media.entity, loadRequestData.credentials) */ - /* .then((asset) => { */ - /* if (!asset) { */ - /* throw cast.framework.messages.ErrorReason.INVALID_REQUEST; */ - /* } */ - - /* loadRequestData.media.contentUrl = asset.url; */ - /* loadRequestData.media.metadata = asset.metadata; */ - /* loadRequestData.media.tracks = asset.tracks; */ - /* return loadRequestData; */ - /* }) */ - /* .catch((reason) => { */ - /* error.reason = reason; // cast.framework.messages.ErrorReason */ - /* return error; */ - /* }); */ }); context.start(); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9f3604a9..52626a7c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -36,6 +36,7 @@ services: restart: on-failure environment: - KYOO_URL=http://back:5000 + - NEXT_PUBLIC_BACK_URL=${PUBLIC_BACK_URL} ingress: image: nginx restart: on-failure @@ -59,6 +60,18 @@ services: - POSTGRES_DB=${POSTGRES_DB} volumes: - db:/var/lib/postgresql/data + chromecast: + build: + context: ./chromecast + dockerfile: Dockerfile.dev + volumes: + - ./chromecast:/app + - /app/node_modules/ + - /app/.parcel-cache/ + - /app/dist/ + ports: + - "1234:1234" + restart: on-failure volumes: kyoo: diff --git a/docker-compose.yml b/docker-compose.yml index ab15b80a..2c8a5dad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: - PORT=8901 - FRONT_URL=http://front:8901 - BACK_URL=http://back:5000 + - CAST_APPLICATION_ID=${CAST_APPLICATION_ID} volumes: - ./nginx.conf.template:/etc/nginx/templates/kyoo.conf.template:ro depends_on: diff --git a/front/src/player/cast/cast-provider.tsx b/front/src/player/cast/cast-provider.tsx index 872aa4cf..5c775707 100644 --- a/front/src/player/cast/cast-provider.tsx +++ b/front/src/player/cast/cast-provider.tsx @@ -34,8 +34,7 @@ export const CastProvider = () => { window.__onGCastApiAvailable = (isAvailable) => { if (!isAvailable) return; cast.framework.CastContext.getInstance().setOptions({ - receiverApplicationId: - process.env.CAST_APPLICATION_ID ?? chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + receiverApplicationId: process.env.CAST_APPLICATION_ID, autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, }); }; diff --git a/front/src/player/cast/state.tsx b/front/src/player/cast/state.tsx index 1dbf41df..64bda141 100644 --- a/front/src/player/cast/state.tsx +++ b/front/src/player/cast/state.tsx @@ -21,7 +21,7 @@ import { atom, useAtomValue, useSetAtom } from "jotai"; import { useEffect } from "react"; import { bakedAtom } from "~/utils/jotai-utils"; -import { stopAtom } from "../state"; +import { stopAtom, localMediaAtom } from "../state"; export type Media = { name: string; @@ -56,9 +56,10 @@ export const [_mediaAtom, mediaAtom] = bakedAtom( const session = cast.framework.CastContext.getInstance().getCurrentSession(); if (!session) return; const mediaInfo = new chrome.cast.media.MediaInfo( - value, - process.env.KYOO_URL ?? "http://localhost:5000", + value, "application/json" ); + if (!process.env.NEXT_PUBLIC_BACK_URL) console.error("PUBLIC_BACK_URL is not defined. Chromecast won't work."); + mediaInfo.customData = { serverUrl: process.env.NEXT_PUBLIC_BACK_URL }; session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo)); }, ); @@ -68,8 +69,9 @@ export const useCastController = () => { const setPlay = useSetAtom(_playAtom); const setDuration = useSetAtom(_durationAtom); const setMedia = useSetAtom(_mediaAtom); + const loadMedia = useSetAtom(mediaAtom); const stopPlayer = useAtomValue(stopAtom); - const media = useAtomValue(mediaAtom); + const localMedia = useAtomValue(localMediaAtom); useEffect(() => { const context = cast.framework.CastContext.getInstance(); @@ -87,9 +89,9 @@ export const useCastController = () => { ]; const sessionStateHandler = (event: cast.framework.SessionStateEventData) => { - if (event.sessionState === cast.framework.SessionState.SESSION_STARTED) { + if (event.sessionState === cast.framework.SessionState.SESSION_STARTED && localMedia) { stopPlayer[0](); - setMedia(media); + loadMedia(localMedia); } }; @@ -99,10 +101,10 @@ export const useCastController = () => { context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, sessionStateHandler); for (const [key, handler] of eventListeners) controller.removeEventListener(key, handler); }; - }, [player, controller, setPlay, setDuration, setMedia, stopPlayer, media]); + }, [player, controller, setPlay, setDuration, setMedia, stopPlayer, localMedia, loadMedia]); }; -export const CastController = (props: any) => { +export const CastController = () => { useCastController(); - return
; + return null; }; diff --git a/front/src/player/state.tsx b/front/src/player/state.tsx index 69ea6f84..625d393a 100644 --- a/front/src/player/state.tsx +++ b/front/src/player/state.tsx @@ -84,7 +84,7 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker) } } catch {} }); -export const mediaAtom = atom(null); +export const localMediaAtom = atom(null); // The tuple is only used to prevent jotai from thinking the function is a read func. export const stopAtom = atom<[() => void]>([() => {}]); @@ -102,7 +102,7 @@ export const useVideoController = (slug: string, links?: { direct: string; trans const setVolume = useSetAtom(_volumeAtom); const setMuted = useSetAtom(_mutedAtom); const setFullscreen = useSetAtom(fullscreenAtom); - const setMedia = useSetAtom(mediaAtom); + const setLocalMedia = useSetAtom(localMediaAtom); const [playMode, setPlayMode] = useAtom(playModeAtom); setPlayer(player); @@ -114,8 +114,8 @@ export const useVideoController = (slug: string, links?: { direct: string; trans useEffect(() => { setPlayMode(PlayMode.Direct); - setMedia(slug); - }, [slug, links, setPlayMode, setMedia]); + setLocalMedia(slug); + }, [slug, links, setPlayMode, setLocalMedia]); useEffect(() => { const src = playMode === PlayMode.Direct ? links?.direct : links?.transmux;