diff --git a/.github/workflows/release-lib.yml b/.github/workflows/release-lib.yml index 7ce7b8a2d..426f4aceb 100644 --- a/.github/workflows/release-lib.yml +++ b/.github/workflows/release-lib.yml @@ -30,7 +30,7 @@ jobs: uses: gradle/actions/setup-gradle@v3 - name: Publish package - run: ./gradlew publish --scan -Pnpm.download=false -Pbuild_version=${{ github.event.inputs.version }} -i + run: ./gradlew publish --scan -Pnpm.download=false -Pbuild_version=${{ github.event.inputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 853489ef3..0545326cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: uses: gradle/actions/setup-gradle@v3 - name: Build with gradle - run: ./gradlew release --scan -Pnpm.download=false -Pbuild_version=${{ github.event.inputs.version }} -i + run: ./gradlew release --scan -Pnpm.download=false -Pbuild_version=${{ github.event.inputs.version }} - name: Upload artifacts uses: actions/upload-artifact@master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3c221cf0..4e143c700 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: uses: gradle/actions/setup-gradle@v3 - name: Build with gradle - run: ./gradlew build release apiCheck --scan -Pnpm.download=false -i + run: ./gradlew build release apiCheck --scan -Pnpm.download=false - uses: actions/upload-artifact@v3 if: always() diff --git a/schemas/advanced.schema.json b/schemas/advanced.schema.json index cb53932aa..f21137b8f 100644 --- a/schemas/advanced.schema.json +++ b/schemas/advanced.schema.json @@ -1,6 +1,30 @@ { "$ref": "#/$defs/ICPC live advanced settings", "$defs": { + "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" + }, "M2tsVideo": { "type": "object", "properties": { @@ -165,6 +189,9 @@ }, "org.icpclive.cds.api.MediaType?>": { "oneOf": [ + { + "$ref": "#/$defs/HLSVideo" + }, { "$ref": "#/$defs/M2tsVideo" }, diff --git a/src/cds-converter/src/main/kotlin/org/icpclive/export/clics/ClicsExporter.kt b/src/cds-converter/src/main/kotlin/org/icpclive/export/clics/ClicsExporter.kt index f818d64e7..4c727c6bd 100644 --- a/src/cds-converter/src/main/kotlin/org/icpclive/export/clics/ClicsExporter.kt +++ b/src/cds-converter/src/main/kotlin/org/icpclive/export/clics/ClicsExporter.kt @@ -51,6 +51,7 @@ private fun MediaType.toClicsMedia() = when (this) { is MediaType.TaskStatus -> null is MediaType.Video -> Media("video", url) is MediaType.M2tsVideo -> Media("video/m2ts", url) + is MediaType.HLSVideo -> Media("application/vnd.apple.mpegurl", url) is MediaType.WebRTCGrabberConnection -> null is MediaType.WebRTCProxyConnection -> null } @@ -487,4 +488,4 @@ object ClicsExporter { )) } } -} \ No newline at end of file +} diff --git a/src/cds/core/api/core.api b/src/cds/core/api/core.api index 1214ae769..7dc73dd52 100644 --- a/src/cds/core/api/core.api +++ b/src/cds/core/api/core.api @@ -781,6 +781,38 @@ public final class org/icpclive/cds/api/MediaType$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class org/icpclive/cds/api/MediaType$HLSVideo : org/icpclive/cds/api/MediaType { + public static final field Companion Lorg/icpclive/cds/api/MediaType$HLSVideo$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Z + public final fun copy (Ljava/lang/String;Ljava/lang/String;Z)Lorg/icpclive/cds/api/MediaType$HLSVideo; + public static synthetic fun copy$default (Lorg/icpclive/cds/api/MediaType$HLSVideo;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lorg/icpclive/cds/api/MediaType$HLSVideo; + public fun equals (Ljava/lang/Object;)Z + public final fun getJwtToken ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun isMedia ()Z + public fun toString ()Ljava/lang/String; +} + +public synthetic class org/icpclive/cds/api/MediaType$HLSVideo$$serializer : kotlinx/serialization/internal/GeneratedSerializer { + public static final field INSTANCE Lorg/icpclive/cds/api/MediaType$HLSVideo$$serializer; + public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lorg/icpclive/cds/api/MediaType$HLSVideo; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/icpclive/cds/api/MediaType$HLSVideo;)V + public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; +} + +public final class org/icpclive/cds/api/MediaType$HLSVideo$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class org/icpclive/cds/api/MediaType$M2tsVideo : org/icpclive/cds/api/MediaType { public static final field Companion Lorg/icpclive/cds/api/MediaType$M2tsVideo$Companion; public fun (Ljava/lang/String;Z)V 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..7756c4107 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,4 @@ 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/cds/core/src/main/kotlin/org/icpclive/cds/api/MediaType.kt b/src/cds/core/src/main/kotlin/org/icpclive/cds/api/MediaType.kt index 4a85d1ed7..d5e84e824 100644 --- a/src/cds/core/src/main/kotlin/org/icpclive/cds/api/MediaType.kt +++ b/src/cds/core/src/main/kotlin/org/icpclive/cds/api/MediaType.kt @@ -23,6 +23,10 @@ public sealed class MediaType { @SerialName("M2tsVideo") public data class M2tsVideo(val url: String, override val isMedia: Boolean = true) : MediaType() + @Serializable + @SerialName("HLSVideo") + public data class HLSVideo(val url: String, val jwtToken: String? = null, override val isMedia: Boolean = true) : MediaType() + /** * WebRTC proxy connection * @see https://github.com/kbats183/webrtc-proxy @@ -65,4 +69,4 @@ public sealed class MediaType { is WebRTCGrabberConnection -> copy(isMedia = false) else -> this } -} \ No newline at end of file +} diff --git a/src/cds/plugins/clics/src/main/kotlin/org/icpclive/cds/plugins/clics/ClicsModel.kt b/src/cds/plugins/clics/src/main/kotlin/org/icpclive/cds/plugins/clics/ClicsModel.kt index b206276a8..1afa6441d 100644 --- a/src/cds/plugins/clics/src/main/kotlin/org/icpclive/cds/plugins/clics/ClicsModel.kt +++ b/src/cds/plugins/clics/src/main/kotlin/org/icpclive/cds/plugins/clics/ClicsModel.kt @@ -44,6 +44,9 @@ internal class ClicsModel(private val addTeamNames: Boolean) { if (mime.startsWith("video/m2ts")) { return MediaType.M2tsVideo(href) } + if (mime.startsWith("application/vnd.apple.mpegurl")) { + return MediaType.HLSVideo(href) + } if (mime.startsWith("video")) { return MediaType.Video(href) } diff --git a/src/frontend/generated/api.ts b/src/frontend/generated/api.ts index a4af68237..1bd8fd425 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,6 +125,13 @@ 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; 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/molecules/info/ContestantViewCorner.tsx b/src/frontend/overlay/src/components/molecules/info/ContestantViewCorner.tsx index ca9bb0934..e3db95a7c 100644 --- a/src/frontend/overlay/src/components/molecules/info/ContestantViewCorner.tsx +++ b/src/frontend/overlay/src/components/molecules/info/ContestantViewCorner.tsx @@ -31,7 +31,7 @@ const CornerContestantInfo = styled(ContestantInfo)` `; export const ContestantViewCorner = ({ teamId, isSmall = false, className = null }: { - teamId: number; + teamId: string; isSmall: boolean; className?: string; }) => { diff --git a/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx b/src/frontend/overlay/src/components/organisms/holder/ContestantViewHolder.tsx index 5131408c2..1e1521521 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%; @@ -50,7 +52,7 @@ export const TeamM2tsVideoWrapper = ({ url, setIsLoaded }) => { if (videoRef.current) { videoRef.current.srcObject = null; } - } + }; }, [url]); return (); }; +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 ; + + // Fallback to using a regular video player if HLS is supported by default in the user's browser + return ; +} export const FullWidthWrapper = styled.div` width: 100%; @@ -209,7 +302,22 @@ const teamViewComponentRender: { onError={() => onLoadStatus(false)} autoPlay loop - muted/> + muted + /> + ; + }, + HLSVideo: ({ onLoadStatus, media, ...props }) => { + return + onLoadStatus(true)} + onError={() => onLoadStatus(false)} + {...props} + /> ; }, M2tsVideo: ({ onLoadStatus, className, media }) => { diff --git a/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.tsx b/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.tsx index 57356890e..3fbbe48c8 100644 --- a/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.tsx +++ b/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.tsx @@ -115,7 +115,7 @@ export const ScoreboardTaskResultLabel = styled(TaskResultLabel)` interface ScoreboardRowProps { - teamId: number, + teamId: string, hideTasks?: boolean, optimismLevel: OptimismLevel } diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index cd8036b44..12732b9b1 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -5212,6 +5212,16 @@ "@types/node": "*" } }, + "node_modules/@types/hls.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/hls.js/-/hls.js-1.0.0.tgz", + "integrity": "sha512-EGY2QJefX+Z9XH4PAxI7RFoNqBlQEk16UpYR3kbr82CIgMX5SlMe0PjFdFV0JytRhyVPQCiwSyONuI6S1KdSag==", + "deprecated": "This is a stub types definition. hls.js provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "hls.js": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -10525,6 +10535,11 @@ "he": "bin/he" } }, + "node_modules/hls.js": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.6.tgz", + "integrity": "sha512-rmlaIEfLuSwqRtYLeTk30ebYli5qNK2urdkEcqYoBezRpV+MFHhZnMX77lHWW+EMjNlwr2sx2apfqq54E3yXnA==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -21620,6 +21635,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", @@ -21650,6 +21666,7 @@ "web-vitals": "^1.1.2" }, "devDependencies": { + "@types/hls.js": "^1.0.0", "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/styled-components": "^5.1.30", @@ -25659,6 +25676,15 @@ "@types/node": "*" } }, + "@types/hls.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/hls.js/-/hls.js-1.0.0.tgz", + "integrity": "sha512-EGY2QJefX+Z9XH4PAxI7RFoNqBlQEk16UpYR3kbr82CIgMX5SlMe0PjFdFV0JytRhyVPQCiwSyONuI6S1KdSag==", + "dev": true, + "requires": { + "hls.js": "*" + } + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -29598,6 +29624,11 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hls.js": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.6.tgz", + "integrity": "sha512-rmlaIEfLuSwqRtYLeTk30ebYli5qNK2urdkEcqYoBezRpV+MFHhZnMX77lHWW+EMjNlwr2sx2apfqq54E3yXnA==" + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -29859,11 +29890,13 @@ "requires": { "@reduxjs/toolkit": "^1.9.7", "@stylelint/postcss-css-in-js": "^0.38.0", + "@types/hls.js": "^1.0.0", "@types/lodash": "^4.14.202", "@types/luxon": "^3.4.2", "@types/styled-components": "^5.1.30", "@vitejs/plugin-react": "^4.0.3", "concurrently": "^8.2.2", + "hls.js": "^1.5.6", "lodash": "^4.17.21", "luxon": "^2.3.1", "mpegts.js": "^1.7.3",