Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic support hls player and HLSVideo as MediaType #148

Merged
merged 4 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-lib.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions schemas/advanced.schema.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -165,6 +189,9 @@
},
"org.icpclive.cds.api.MediaType?<kotlin.String,kotlinx.serialization.Sealed<MediaType>>": {
"oneOf": [
{
"$ref": "#/$defs/HLSVideo"
},
{
"$ref": "#/$defs/M2tsVideo"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -487,4 +488,4 @@ object ClicsExporter {
))
}
}
}
}
32 changes: 32 additions & 0 deletions src/cds/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Ljava/lang/String;Ljava/lang/String;Z)V
public synthetic fun <init> (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 <init> (Ljava/lang/String;Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,4 @@ public data class ContestInfo(
val organizations: Map<OrganizationId, OrganizationInfo> by lazy { organizationList.associateBy { it.id } }
val problems: Map<ProblemId, ProblemInfo> by lazy { problemList.associateBy { it.id } }
val scoreboardProblems: List<ProblemInfo> by lazy { problemList.sortedBy { it.ordinal }.filterNot { it.isHidden } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://github.com/kbats183/webrtc-proxy">https://github.com/kbats183/webrtc-proxy</a>
Expand Down Expand Up @@ -65,4 +69,4 @@ public sealed class MediaType {
is WebRTCGrabberConnection -> copy(isMedia = false)
else -> this
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export type OrganizationId = string;
export type GroupId = string;

export type MediaType =
| MediaType.HLSVideo
| MediaType.M2tsVideo
| MediaType.Object
| MediaType.Photo
Expand All @@ -114,6 +115,7 @@ export type MediaType =

export namespace MediaType {
export enum Type {
HLSVideo = "HLSVideo",
M2tsVideo = "M2tsVideo",
Object = "Object",
Photo = "Photo",
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/overlay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const CornerContestantInfo = styled(ContestantInfo)`
`;

export const ContestantViewCorner = ({ teamId, isSmall = false, className = null }: {
teamId: number;
teamId: string;
isSmall: boolean;
className?: string;
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down Expand Up @@ -50,7 +52,7 @@ export const TeamM2tsVideoWrapper = ({ url, setIsLoaded }) => {
if (videoRef.current) {
videoRef.current.srcObject = null;
}
}
};
}, [url]);
return (<VideoWrapper
ref={videoRef}
Expand Down Expand Up @@ -147,6 +149,97 @@ export const TeamWebRTCGrabberVideoWrapper = ({ media: { url, peerName, streamTy
{...props}/>);
};

interface HlsPlayerProps
extends React.VideoHTMLAttributes<HTMLVideoElement> {
src: string;
jwtToken?: string;
// onCanPlay: () => void;
onError?: () => void;
}

function HlsPlayer({
src,
autoPlay,
jwtToken,
// onCanPlay,
...props
}: HlsPlayerProps) {
const playerRef = useRef<HTMLVideoElement>();
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 <VideoWrapper ref={playerRef} autoPlay={autoPlay} controls={false} {...props} />;

// Fallback to using a regular video player if HLS is supported by default in the user's browser
return <VideoWrapper ref={playerRef} src={src} autoPlay={autoPlay} controls={false} {...props} />;
}

export const FullWidthWrapper = styled.div`
width: 100%;
Expand Down Expand Up @@ -209,7 +302,22 @@ const teamViewComponentRender: {
onError={() => onLoadStatus(false)}
autoPlay
loop
muted/>
muted
/>
</FullWidthWrapper>;
},
HLSVideo: ({ onLoadStatus, media, ...props }) => {
return <FullWidthWrapper>
<HlsPlayer
src={media.url}
jwtToken={media.jwtToken}
autoPlay
loop
muted
onCanPlay={() => onLoadStatus(true)}
onError={() => onLoadStatus(false)}
{...props}
/>
</FullWidthWrapper>;
},
M2tsVideo: ({ onLoadStatus, className, media }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const ScoreboardTaskResultLabel = styled(TaskResultLabel)`


interface ScoreboardRowProps {
teamId: number,
teamId: string,
hideTasks?: boolean,
optimismLevel: OptimismLevel
}
Expand Down
Loading
Loading