mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Cast app receive videos
This commit is contained in:
parent
8a9a6a5f29
commit
a978ed6aeb
@ -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
11
chromecast/.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
.dockerignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.parcel-cache
|
||||
.git
|
||||
dist
|
9
chromecast/Dockerfile.dev
Normal file
9
chromecast/Dockerfile.dev
Normal 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
91
chromecast/src/api.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user