From 4452c35cc27cf388218b3360221234e2c04acfb6 Mon Sep 17 00:00:00 2001 From: Kostya Bats Date: Wed, 28 Feb 2024 19:25:12 +0300 Subject: [PATCH 1/4] Basic support hls player and HLSVideo as MediaType --- schemas/advanced.schema.json | 27 +++++ .../org/icpclive/cds/api/ContestInfo.kt | 3 +- src/frontend/generated/api.ts | 11 +- src/frontend/overlay/package.json | 2 + .../organisms/holder/ContestantViewHolder.tsx | 111 +++++++++++++++++- src/frontend/package-lock.json | 33 ++++++ 6 files changed, 184 insertions(+), 3 deletions(-) diff --git a/schemas/advanced.schema.json b/schemas/advanced.schema.json index cb53932aa..080a8be02 100644 --- a/schemas/advanced.schema.json +++ b/schemas/advanced.schema.json @@ -22,6 +22,30 @@ ], "title": "M2tsVideo" }, + "HLSVideo": { + "type": "object", + "properties": { + "type": { + "const": "HLSVideo", + "default": "HLSVideo" + }, + "url": { + "type": "string" + }, + "jwtToken": { + "type": "string" + }, + "isMedia": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + "type", + "url" + ], + "title": "HLSVideo" + }, "Object": { "type": "object", "properties": { @@ -165,6 +189,9 @@ }, "org.icpclive.cds.api.MediaType?>": { "oneOf": [ + { + "$ref": "#/$defs/HLSVideo" + }, { "$ref": "#/$defs/M2tsVideo" }, diff --git a/src/cds/core/src/main/kotlin/org/icpclive/cds/api/ContestInfo.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/api/ContestInfo.kt index 7193d7254..cd3e1bcb1 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/api/ContestInfo.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/api/ContestInfo.kt @@ -115,4 +115,5 @@ public data class ContestInfo( val organizations: Map by lazy { organizationList.associateBy { it.id } } val problems: Map by lazy { problemList.associateBy { it.id } } val scoreboardProblems: List by lazy { problemList.sortedBy { it.ordinal }.filterNot { it.isHidden } } -} \ No newline at end of file +} + diff --git a/src/frontend/generated/api.ts b/src/frontend/generated/api.ts index a4af68237..986888d3c 100644 --- a/src/frontend/generated/api.ts +++ b/src/frontend/generated/api.ts @@ -104,6 +104,7 @@ export type OrganizationId = string; export type GroupId = string; export type MediaType = + | MediaType.HLSVideo | MediaType.M2tsVideo | MediaType.Object | MediaType.Photo @@ -114,6 +115,7 @@ export type MediaType = export namespace MediaType { export enum Type { + HLSVideo = "HLSVideo", M2tsVideo = "M2tsVideo", Object = "Object", Photo = "Photo", @@ -123,12 +125,19 @@ export namespace MediaType { WebRTCProxyConnection = "WebRTCProxyConnection", } + export interface HLSVideo { + type: MediaType.Type.HLSVideo; + url: string; + jwtToken?: string | null; + isMedia?: boolean; + } + export interface M2tsVideo { type: MediaType.Type.M2tsVideo; url: string; isMedia?: boolean; } - + export interface Object { type: MediaType.Type.Object; url: string; diff --git a/src/frontend/overlay/package.json b/src/frontend/overlay/package.json index 4db0272ee..d71500f73 100644 --- a/src/frontend/overlay/package.json +++ b/src/frontend/overlay/package.json @@ -6,6 +6,7 @@ "@reduxjs/toolkit": "^1.9.7", "@stylelint/postcss-css-in-js": "^0.38.0", "@vitejs/plugin-react": "^4.0.3", + "hls.js": "^1.5.6", "lodash": "^4.17.21", "luxon": "^2.3.1", "mpegts.js": "^1.7.3", @@ -44,6 +45,7 @@ "lint": "concurrently --group \"npm run lint:*\"" }, "devDependencies": { + "@types/hls.js": "^1.0.0", "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/styled-components": "^5.1.30", diff --git a/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx b/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx index 5131408c2..1d1fac898 100644 --- a/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx +++ b/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx @@ -7,11 +7,13 @@ import { useAppDispatch } from "@/redux/hooks"; import { MediaType } from "@shared/api"; import c from "../../../config"; import mpegts from "mpegts.js"; +import Hls from "hls.js"; // export const TeamImageWrapper = styled.img /*` // // border-radius: ${({ borderRadius }) => borderRadius}; // `*/; +// https://usefulangle.com/post/142/css-video-aspect-ratio // export const TeamVideoWrapper = styled.video/*` // position: absolute; // width: 100%; @@ -147,6 +149,97 @@ export const TeamWebRTCGrabberVideoWrapper = ({ media: { url, peerName, streamTy {...props}/>); }; +interface HlsPlayerProps + extends React.VideoHTMLAttributes { + src: string; + jwtToken?: string; + // onCanPlay: () => void; + onError?: () => void; +} + +function HlsPlayer({ + src, + autoPlay, + jwtToken, + // onCanPlay, + ...props +}: HlsPlayerProps) { + const playerRef = useRef(); + useEffect(() => { + // onCanPlay(); + let hls: Hls; + + function _initPlayer() { + if (hls != null) { + hls.destroy(); + } + + const newHls = new Hls({ + enableWorker: false, + xhrSetup: (xhr) => { + if (jwtToken) { + xhr.setRequestHeader("Authorization", "Bearer " + jwtToken); + } + }, + }); + + if (playerRef.current != null) { + newHls.attachMedia(playerRef.current); + } + + newHls.on(Hls.Events.MEDIA_ATTACHED, () => { + newHls.loadSource(src); + + newHls.on(Hls.Events.MANIFEST_PARSED, () => { + if (autoPlay) { + playerRef?.current + ?.play() + .catch(() => + console.log( + "Unable to autoplay prior to user interaction with the dom." + ) + ); + } + }); + }); + + newHls.on(Hls.Events.ERROR, function (event, data) { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + newHls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + newHls.recoverMediaError(); + break; + default: + _initPlayer(); + break; + } + } + }); + + hls = newHls; + } + + // Check for Media Source support + if (Hls.isSupported()) { + _initPlayer(); + } + + return () => { + if (hls != null) { + hls.destroy(); + } + }; + }, [autoPlay, src, jwtToken]); + + // If Media Source is supported, use HLS.js to play video + if (Hls.isSupported()) return