Cast app receive videos

This commit is contained in:
Zoe Roux 2022-10-23 14:49:29 +09:00
parent 8a9a6a5f29
commit a978ed6aeb
No known key found for this signature in database
GPG Key ID: B2AB52A2636E5C46
11 changed files with 162 additions and 45 deletions

View File

@ -121,7 +121,9 @@ namespace Kyoo.Core
services.AddHttpContextAccessor();
services.AddTransient<IConfigureOptions<MvcNewtonsoftJsonOptions>, JsonOptions>();
services.AddMvcCore()
services
.AddMvcCore()
.AddCors()
.AddNewtonsoftJson()
.AddDataAnnotations()
.AddControllersAsServices()
@ -167,6 +169,12 @@ namespace Kyoo.Core
}, SA.Before),
SA.New<IApplicationBuilder>(app => app.UseResponseCompression(), SA.Routing + 1),
SA.New<IApplicationBuilder>(app => app.UseRouting(), SA.Routing),
SA.New<IApplicationBuilder>(app => app.UseCors(x => x
.SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
), SA.Routing + 2),
SA.New<IApplicationBuilder>(app => app.UseEndpoints(x => x.MapControllers()), SA.Endpoint)
};
}

11
chromecast/.dockerignore Normal file
View File

@ -0,0 +1,11 @@
Dockerfile
Dockerfile.dev
.dockerignore
.eslintrc.json
.gitignore
node_modules
npm-debug.log
README.md
.parcel-cache
.git
dist

View File

@ -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"]

91
chromecast/src/api.ts Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View File

@ -7,6 +7,6 @@
</head>
<body>
<cast-media-player></cast-media-player>
<script src="index.ts"></script>
<script src="index.ts" type="module"></script>
</body>
</html>

View File

@ -18,40 +18,23 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
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();

View File

@ -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:

View File

@ -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:

View File

@ -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,
});
};

View File

@ -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<Media | null, string>(
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 <div></div>;
return null;
};

View File

@ -84,7 +84,7 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
}
} catch {}
});
export const mediaAtom = atom<string | null>(null);
export const localMediaAtom = atom<string | null>(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;