diff --git a/.gitignore b/.gitignore index 1f5659b..97aa154 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ testem.log # System Files .DS_Store Thumbs.db +/env_dev/data +/env_dev/dataMongo backPY/env @@ -62,3 +64,5 @@ __pycache__ .design/ .vscode/ +front/storybook-static +back/bin diff --git a/Dockerfile b/Dockerfile index e0379e6..0747509 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,23 +3,31 @@ ## buyilding-end install applications: ## ###################################################################################### -FROM archlinux:base-devel AS builder +FROM archlinux:base-devel AS common # update system RUN pacman -Syu --noconfirm && pacman-db-upgrade \ - && pacman -S --noconfirm jdk-openjdk maven npm pnpm \ + && pacman -S --noconfirm jdk-openjdk wget\ + && pacman -Scc --noconfirm + +WORKDIR /tmp + +FROM common AS builder +# update system +RUN pacman -Syu --noconfirm && pacman-db-upgrade \ + && pacman -S --noconfirm maven npm pnpm \ && pacman -Scc --noconfirm ENV PATH /tmp/node_modules/.bin:$PATH -WORKDIR /tmp ###################################################################################### ## ## Build back: ## ###################################################################################### -FROM builder AS buildBack -COPY back/pom.xml /tmp -COPY back/src /tmp/src/ +FROM builder AS build_back +COPY back/pom.xml ./ +COPY back/Formatter.xml ./ +COPY back/src ./src/ RUN mvn clean compile assembly:single ###################################################################################### @@ -27,27 +35,44 @@ RUN mvn clean compile assembly:single ## Build front: ## ###################################################################################### -FROM builder AS buildFront +FROM builder AS dependency_front RUN echo "@kangaroo-and-rabbit:registry=https://gitea.atria-soft.org/api/packages/kangaroo-and-rabbit/npm/" > /root/.npmrc ADD front/package.json \ - front/karma.conf.js \ - front/protractor.conf.js \ - /tmp/ + front/pnpm-lock.yaml \ + ./ +ADD front/src/theme ./src/theme # install and cache app dependencies -RUN pnpm install +RUN pnpm install --prod=false -ADD front/e2e \ - front/tsconfig.json \ - front/tslint.json \ - front/angular.json \ - /tmp/ -ADD front/src /tmp/src +############################################################### +## Install sources +############################################################### +FROM dependency_front AS load_sources_front -# generate build -RUN ng build --output-path=dist --configuration=production --base-href=/karideo/ --deploy-url=/karideo/ +# JUST to get the vertion of the application and his sha... +COPY \ + front/tsconfig.json \ + front/tsconfig.node.json \ + front/vite.config.mts \ + front/index.html \ + ./ + +COPY front/public ./public +COPY front/src ./src + +#We are not in prod mode ==> we need to overwrite the production env. +ARG env=front/.env.production +COPY ${env} .env + +############################################################### +## Build the sources +############################################################### +FROM load_sources_front AS build_front +# build in bundle mode all the application +RUN pnpm static:build ###################################################################################### ## @@ -55,17 +80,22 @@ RUN ng build --output-path=dist --configuration=production --base-href=/karideo/ ## ###################################################################################### -FROM bellsoft/liberica-openjdk-alpine:latest -# add wget to manage the health check... -RUN apk add --no-cache wget +#FROM bellsoft/liberica-openjdk-alpine:latest +## add wget to manage the health check... +#RUN apk add --no-cache wget +FROM common -ENV LANG=C.UTF-8 +ENV LANG C.UTF-8 -COPY --from=buildBack /tmp/out/maven/*.jar /application/application.jar -COPY --from=buildFront /tmp/dist /application/front/ +COPY --from=build_back /tmp/out/maven/*.jar /application/application.jar +COPY --from=build_front /tmp/dist /application/front/ WORKDIR /application/ EXPOSE 80 +# To verify health-check: docker inspect --format "{{json .State.Health }}" YOUR_SERVICE_NAME | jq +HEALTHCHECK --start-period=10s --start-interval=2s --interval=30s --timeout=5s --retries=10 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:80/api/health_check || exit 1 + CMD ["java", "-Xms64M", "-Xmx1G", "-cp", "/application/application.jar", "org.kar.karideo.WebLauncher"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6472bac --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +Karideo +======= + +**K**angaroo **A**nd **R**abbit (v)ideo is a simple framework to propose video streaming for personal network + +Run in local: +============= + +Start tools +----------- + +Start the server basic interfaces: (DB(mySQL), Adminer) + +```{.bash} +# start the Bdd interface (no big data > 50Mo) +docker compose -f env_dev/docker-compose.yaml up -d +``` + +Start the Back-end: +------------------- + +backend is developed in JAVA + +The first step is configuring your JAVA version (or select the JVM with the OS) +```bash +export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH +``` + +Install the dependency: +```bash +mvn install +``` + +Run the test +```bash +mvn test +``` + +Install it for external use +```bash +mvn install +``` + +Execute the local server: +```bash +mvn exec:java@dev-mode +``` + +Start the Front-end: +-------------------- + +backend is developed in JAVA +```bash +cd front +pnpm install +pnpm dev +``` + +Display the result: +------------------- + +[show the webpage: http://localhost:4203](http://localhost:4203) + +Some other dev tools: +===================== + +Format code: +------------ + +```bash +export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH +mvn formatter:format +mvn test +``` + +Tools in production mode +======================== + +Changing the Log Level +---------------------- + +In a production environment, you can adjust the log level to help diagnose bugs more effectively. + +The available log levels are: +| **Log Level Tag** | **org.kar.karideo** | **org.kar.archidata** | **other** | +| ----------------- | ------------------- | --------------------- | --------- | +| `prod` | INFO | INFO | INFO | +| `prod-debug` | DEBUG | INFO | INFO | +| `prod-trace` | TRACE | DEBUG | INFO | +| `prod-trace-full` | TRACE | TRACE | INFO | +| `dev` | TRACE | DEBUG | INFO | + + +Manual set in production: +========================= + +Connect on the registry +------------------------ + +To log-in and log-out from the registry: +```bash +export REGISTRY_ADDRESS=gitea.atria-soft.org +docker login -u <> ${REGISTRY_ADDRESS} +docker logout ${REGISTRY_ADDRESS} +``` + +pull the root image of dockers +------------------------------ + +```bash +docker pull archlinux:base-devel +docker pull bellsoft/liberica-openjdk-alpine:latest +``` + +Create the version +------------------ + +Execute in the local folder: (use ```dev``` for development and ```latest``` for production release) + +```bash +export TAG_DOCKER=latest +export REGISTRY_ADDRESS=gitea.atria-soft.org +docker build -t ${REGISTRY_ADDRESS}/kangaroo-and-rabbit/karideo:${TAG_DOCKER} . +docker push ${REGISTRY_ADDRESS}/kangaroo-and-rabbit/karideo:${TAG_DOCKER} +``` diff --git a/front/.env.production b/front/.env.production index 33f75f5..0681162 100644 --- a/front/.env.production +++ b/front/.env.production @@ -1,2 +1,2 @@ # URL for database connection -VITE_API_BASE_URL=karusic/api/ +VITE_API_BASE_URL=karideo/api/ diff --git a/front/src/App.tsx b/front/src/App.tsx index 5a221c0..73036c4 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -1,6 +1,6 @@ import { ErrorBoundary } from '@/errors/ErrorBoundary'; -import { AudioPlayer } from './components'; +import { VideoPlayer } from './components'; import { EnvDevelopment } from './components/EnvDevelopment/EnvDevelopment'; import { AppRoutes } from './scene/AppRoutes'; import { ServiceContextProvider } from './service/ServiceContext'; @@ -12,7 +12,7 @@ export const App = () => { - + ); }; diff --git a/front/src/app/back-api/model/data.ts b/front/src/app/back-api/model/data.ts deleted file mode 100644 index 9afc166..0000000 --- a/front/src/app/back-api/model/data.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Interface of the server (auto-generated code) - */ -import { z as zod } from "zod"; - -import {ZodLong} from "./long"; -import {ZodUUIDGenericDataSoftDelete, ZodUUIDGenericDataSoftDeleteWrite } from "./uuid-generic-data-soft-delete"; - -export const ZodData = ZodUUIDGenericDataSoftDelete.extend({ - /** - * Sha512 of the data - */ - sha512: zod.string().max(128), - /** - * Mime -type of the media - */ - mimeType: zod.string().max(128), - /** - * Size in Byte of the data - */ - size: ZodLong, - -}); - -export type Data = zod.infer; - -export function isData(data: any): data is Data { - try { - ZodData.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodData' error=${e}`); - return false; - } -} -export const ZodDataWrite = ZodUUIDGenericDataSoftDeleteWrite.extend({ - /** - * Sha512 of the data - */ - sha512: zod.string().max(128).optional(), - /** - * Mime -type of the media - */ - mimeType: zod.string().max(128).optional(), - /** - * Size in Byte of the data - */ - size: ZodLong.optional(), - -}); - -export type DataWrite = zod.infer; - -export function isDataWrite(data: any): data is DataWrite { - try { - ZodDataWrite.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodDataWrite' error=${e}`); - return false; - } -} diff --git a/front/src/app/back-api/model/uuid-generic-data-soft-delete.ts b/front/src/app/back-api/model/uuid-generic-data-soft-delete.ts deleted file mode 100644 index 0c50650..0000000 --- a/front/src/app/back-api/model/uuid-generic-data-soft-delete.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Interface of the server (auto-generated code) - */ -import { z as zod } from "zod"; - -import {ZodUUIDGenericData, ZodUUIDGenericDataWrite } from "./uuid-generic-data"; - -export const ZodUUIDGenericDataSoftDelete = ZodUUIDGenericData.extend({ - /** - * Deleted state - */ - deleted: zod.boolean().readonly().optional(), - -}); - -export type UUIDGenericDataSoftDelete = zod.infer; - -export function isUUIDGenericDataSoftDelete(data: any): data is UUIDGenericDataSoftDelete { - try { - ZodUUIDGenericDataSoftDelete.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodUUIDGenericDataSoftDelete' error=${e}`); - return false; - } -} -export const ZodUUIDGenericDataSoftDeleteWrite = ZodUUIDGenericDataWrite.extend({ - -}); - -export type UUIDGenericDataSoftDeleteWrite = zod.infer; - -export function isUUIDGenericDataSoftDeleteWrite(data: any): data is UUIDGenericDataSoftDeleteWrite { - try { - ZodUUIDGenericDataSoftDeleteWrite.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodUUIDGenericDataSoftDeleteWrite' error=${e}`); - return false; - } -} diff --git a/front/src/app/back-api/model/uuid-generic-data.ts b/front/src/app/back-api/model/uuid-generic-data.ts deleted file mode 100644 index de0bf6f..0000000 --- a/front/src/app/back-api/model/uuid-generic-data.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Interface of the server (auto-generated code) - */ -import { z as zod } from "zod"; - -import {ZodUUID} from "./uuid"; -import {ZodGenericTiming, ZodGenericTimingWrite } from "./generic-timing"; - -export const ZodUUIDGenericData = ZodGenericTiming.extend({ - /** - * Unique UUID of the object - */ - uuid: ZodUUID.readonly(), - -}); - -export type UUIDGenericData = zod.infer; - -export function isUUIDGenericData(data: any): data is UUIDGenericData { - try { - ZodUUIDGenericData.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodUUIDGenericData' error=${e}`); - return false; - } -} -export const ZodUUIDGenericDataWrite = ZodGenericTimingWrite.extend({ - -}); - -export type UUIDGenericDataWrite = zod.infer; - -export function isUUIDGenericDataWrite(data: any): data is UUIDGenericDataWrite { - try { - ZodUUIDGenericDataWrite.parse(data); - return true; - } catch (e: any) { - console.log(`Fail to parse data type='ZodUUIDGenericDataWrite' error=${e}`); - return false; - } -} diff --git a/front/src/components/Cover.tsx b/front/src/components/Cover.tsx index 5bff6da..93dcb41 100644 --- a/front/src/components/Cover.tsx +++ b/front/src/components/Cover.tsx @@ -1,7 +1,6 @@ import { ReactElement, useEffect, useState } from 'react'; -import { Box, BoxProps, Flex, FlexProps } from '@chakra-ui/react'; -import { Image } from '@chakra-ui/react'; +import { Box, BoxProps, Flex, Image } from '@chakra-ui/react'; import { ObjectId } from '@/back-api'; import { DataUrlAccess } from '@/utils/data-url-access'; @@ -17,6 +16,7 @@ export type CoversProps = Omit & { export const Covers = ({ data, + onClick, iconEmpty, size = '100px', slideshow = false, @@ -45,7 +45,7 @@ export const Covers = ({ if (!data || data.length < 1) { if (iconEmpty) { - return ; + return ; } else { return ( ); @@ -69,6 +70,7 @@ export const Covers = ({ src={url} maxWidth={size} boxSize={size} /*{...rest}*/ + onClick={onClick} /> ); } diff --git a/front/src/components/EmptyEnd.tsx b/front/src/components/EmptyEnd.tsx index b49ff24..4dedc91 100644 --- a/front/src/components/EmptyEnd.tsx +++ b/front/src/components/EmptyEnd.tsx @@ -5,7 +5,7 @@ export const EmptyEnd = () => { diff --git a/front/src/components/AudioPlayer.tsx b/front/src/components/VideoPlayer.tsx similarity index 61% rename from front/src/components/AudioPlayer.tsx rename to front/src/components/VideoPlayer.tsx index 90b7e2a..1b67058 100644 --- a/front/src/components/AudioPlayer.tsx +++ b/front/src/components/VideoPlayer.tsx @@ -1,18 +1,21 @@ import { useEffect, useRef, useState } from 'react'; -import { Box, Flex, IconButton, SliderTrack, Text } from '@chakra-ui/react'; +import { Box, chakra, Flex, IconButton, SliderTrack, Spacer, Text, useBreakpointValue } from '@chakra-ui/react'; import { MdFastForward, MdFastRewind, + MdFullscreen, MdLooksOne, MdNavigateBefore, MdNavigateNext, + MdOutlinePictureInPictureAlt, MdPause, + MdPictureInPictureAlt, MdPlayArrow, MdRepeat, MdRepeatOne, MdStop, - MdTrendingFlat, + MdTrendingFlat } from 'react-icons/md'; import { useColorModeValue } from '@/components/ui/color-mode'; @@ -59,10 +62,44 @@ const formatTime = (time) => { return '00:00'; }; -export const AudioPlayer = ({}: AudioPlayerProps) => { - const { playMediaList, MediaOffset, previous, next, first } = +export const VideoPlayer = ({}: AudioPlayerProps) => { + const [time, setTime] = useState(10); + const [isRunning, setIsRunning] = useState(true); + useEffect(() => { + if (!isRunning || time <= 0) { + console.log(`exit timer`); + setIsRunning(false); + return; + } + const timer = setInterval(() => { + setTime((prevTime) => { + console.log(`current time : ${prevTime}`); + return prevTime - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }, [time, isRunning]); + + const resetTimer = () => { + setTime(10); + setIsRunning(true); + }; + useEffect(() => { + const resetTimer = () => { + setTime(10); + setIsRunning(true); + }; + window.addEventListener("mousemove", resetTimer); + return () => { + window.removeEventListener("mousemove", resetTimer); + }; + }, []); + + const { playMediaList, MediaOffset, previous, next, first, clear } = useActivePlaylistService(); - const audioRef = useRef(null); + const containerRef = useRef(null); + const videoRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [timeProgress, setTimeProgress] = useState(0); const [playingMode, setPlayingMode] = useState(PlayMode.PLAY_ALL); @@ -74,6 +111,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { const { dataType } = useSpecificType(dataMedia?.typeId); const { dataSeries } = useSpecificSeries(dataMedia?.seriesId); + const isMobile = useBreakpointValue({ base: false, sm: true }); const [mediaSource, setMediaSource] = useState(''); useEffect(() => { setMediaSource( @@ -83,7 +121,16 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { ); }, [dataMedia, setMediaSource]); const backColor = useColorModeValue('back.100', 'back.800'); - const configButton = { + const configButton = isMobile ? { + borderRadius: 'full', + backgroundColor: 'transparent', + _hover: { + bgColor: 'brand.500', + }, + width: '35px', + height: '35px', + padding: '2px', + } : { borderRadius: 'full', backgroundColor: 'transparent', _hover: { @@ -95,15 +142,15 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { }; useEffect(() => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } if (isPlaying) { - audioRef.current.play(); + videoRef.current.play(); } else { - audioRef.current.pause(); + videoRef.current.pause(); } - }, [isPlaying, audioRef]); + }, [isPlaying, videoRef]); const onAudioEnded = () => { if (playMediaList.length === 0 || isNullOrUndefined(MediaOffset)) { @@ -124,46 +171,46 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { }; const onSeek = (newValue) => { console.log(`onSeek: ${newValue}`); - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } - audioRef.current.currentTime = newValue; + videoRef.current.currentTime = newValue; }; const onPlay = () => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } if (isPlaying) { - audioRef.current.pause(); + videoRef.current.pause(); } else { - audioRef.current.play(); + videoRef.current.play(); } }; const onStop = () => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } - if (audioRef.current.currentTime == 0 && audioRef.current.paused) { - // TODO remove current playing value + if (videoRef.current.currentTime <= 0.5 && videoRef.current.paused) { + clear(); } else { - audioRef.current.pause(); - audioRef.current.currentTime = 0; + videoRef.current.pause(); + videoRef.current.currentTime = 0; } }; const onNavigatePrevious = () => { previous(); }; const onFastRewind = () => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } - audioRef.current.currentTime -= 10; + videoRef.current.currentTime -= 10; }; const onFastForward = () => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } - audioRef.current.currentTime += 10; + videoRef.current.currentTime += 10; }; const onNavigateNext = () => { next(); @@ -185,17 +232,17 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { * Call when meta-data is updated */ function onChangeMetadata(): void { - const seconds = audioRef.current?.duration; + const seconds = videoRef.current?.duration; if (seconds !== undefined) { setDuration(seconds); } } const onTimeUpdate = () => { - if (!audioRef || !audioRef.current) { + if (!videoRef || !videoRef.current) { return; } - console.log(`onTimeUpdate ${audioRef.current.currentTime}`); - setTimeProgress(audioRef.current.currentTime); + //console.log(`onTimeUpdate ${videoRef.current.currentTime}`); + setTimeProgress(videoRef.current.currentTime); }; const onDurationChange = (event) => {}; const onChangeStateToPlay = () => { @@ -212,12 +259,57 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { } return result; }; + const onFullScreen = () => { + if (containerRef.current) { + if (!document.fullscreenElement) { + if (containerRef.current.requestFullscreen) { + resetTimer(); + containerRef.current.requestFullscreen(); + } + } else { + document.exitFullscreen(); + } + } + }; + const [isPiPSupported, setIsPiPSupported] = useState(false); + useEffect(() => { + setIsPiPSupported(!!document.pictureInPictureEnabled); + }, []); + const [isPiP, setIsPiP] = useState(false); + const onPictureInPicture = async () => { + if (videoRef.current) { + try { + if (!isPiP) { + await videoRef.current.requestPictureInPicture(); + setIsPiP(true); + } else { + document.exitPictureInPicture(); + setIsPiP(false); + } + } catch (error) { + console.error("Erreur avec Picture-in-Picture:", error); + } + } + }; + const [isFullScreen, setIsFullScreen] = useState(false); + useEffect(() => { + const handleFullScreenChange = () => { + console.log(`changeFullScreen: ${!!document.fullscreenElement}`) + setIsFullScreen(!!document.fullscreenElement); + }; + + document.addEventListener("fullscreenchange", handleFullScreenChange); + return () => { + document.removeEventListener("fullscreenchange", handleFullScreenChange); + }; + }, []); return ( <> {!isNullOrUndefined(MediaOffset) && ( + <> { zIndex={1000} borderWidth="1px" borderColor="brand.900" - bgColor={backColor} + bgColor={isFullScreen ? "0x000000" : backColor} borderTopRadius="10px" direction="column" - > - + + {(!isFullScreen || (!isMobile || isRunning)) && <> { marginRight="auto" overflow="hidden" // noOfLines={1} - > + > {dataMedia?.name ?? '???'} { marginRight="auto" overflow="hidden" // noOfLines={1} - > + > {dataSeries && dataSeries.name} {dataSeason && dataSeason?.name} {dataType && ` / ${dataType.name}`} + {isFullScreen && } { colorPalette="brand" marks={marks()} //focusCapture={false} - > + > { marginRight="auto" overflow="hidden" // noOfLines={1} - > + > {formatTime(timeProgress)} @@ -308,7 +420,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => { aria-label={'Stop'} onClick={onStop} variant="ghost" - > + > { onClick={onNavigatePrevious} marginLeft="auto" variant="ghost" - > + > {' '} { aria-label={'jump 15sec in past'} onClick={onFastRewind} variant="ghost" - > + > { aria-label={'jump 15sec in future'} onClick={onFastForward} variant="ghost" - > + > { marginRight="auto" onClick={onNavigateNext} variant="ghost" - > + > { aria-label={'continue to the end'} onClick={onTypePlay} variant="ghost" - > + > {playModeIcon[playingMode]} + + + + {isPiPSupported && !isMobile && + + {isPiP ? : } + + } + } + )} - -