Skip to content

Commit 72130bd

Browse files
authored
build(npm): prepare recording player packages for NPM publishing (#1575)
- Sets up shadow-player and multi-video-player packages for standalone - NPM distribution using a dual package.json approach. - Gateway API client for fetching recording data - build.ps1 for both packages - shadow-player: Added disconnect for proper cleanup Issue: ARC-412
1 parent 1417db9 commit 72130bd

File tree

18 files changed

+410
-61
lines changed

18 files changed

+410
-61
lines changed

.github/workflows/publish-libraries.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,14 @@ jobs:
9595
strategy:
9696
fail-fast: false
9797
matrix:
98-
library: [ ts-angular-client ]
98+
library: [ ts-angular-client, multi-video-player, shadow-player ]
9999
include:
100100
- library: ts-angular-client
101101
libpath: ./devolutions-gateway/openapi/ts-angular-client
102+
- library: multi-video-player
103+
libpath: ./webapp/packages/multi-video-player
104+
- library: shadow-player
105+
libpath: ./webapp/packages/shadow-player
102106

103107
steps:
104108
- name: Check out ${{ github.repository }}

webapp/apps/recording-player/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@import url("./node_modules/@devolutions/multi-video-player/dist/multi-video-player.css");
1+
@import url("./node_modules/@devolutions/multi-video-player/dist/index.css");
22

33
html,
44
body {

webapp/apps/recording-player/src/players/webm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import '@devolutions/multi-video-player';
2-
import '@devolutions/multi-video-player/dist/multi-video-player.css';
2+
import '@devolutions/multi-video-player/index.css';
33
import { MultiVideoPlayer } from '@devolutions/multi-video-player';
44
import type { GatewayAccessApi } from '../gateway';
55

webapp/apps/recording-player/src/streamers/webm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import '@devolutions/shadow-player/src/streamer';
2-
import { ShadowPlayer } from '@devolutions/shadow-player/src/streamer';
1+
import '@devolutions/shadow-player';
2+
import type { ShadowPlayer } from '@devolutions/shadow-player';
33
import { GatewayAccessApi } from '../gateway';
44
import { t } from '../i18n';
55
import { showNotification } from '../notification';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env pwsh
2+
3+
$ErrorActionPreference = "Stop"
4+
5+
Push-Location -Path $PSScriptRoot
6+
7+
try
8+
{
9+
pnpm install
10+
11+
pnpm run build
12+
13+
Set-Location -Path ./dist/
14+
npm pack
15+
}
16+
finally
17+
{
18+
Pop-Location
19+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@devolutions/multi-video-player",
3+
"version": "0.1.0",
4+
"description": "Multi-segment video player with live streaming support",
5+
"type": "module",
6+
"main": "./index.js",
7+
"module": "./index.js",
8+
"types": "./index.d.ts",
9+
"exports": {
10+
".": {
11+
"import": "./index.js",
12+
"types": "./index.d.ts"
13+
},
14+
"./index.css": "./index.css"
15+
},
16+
"dependencies": {
17+
"@devolutions/shadow-player": "^0.1.0",
18+
"video.js": "^8.0.0"
19+
},
20+
"repository": {
21+
"type": "git",
22+
"url": "https://github.com/Devolutions/devolutions-gateway.git"
23+
},
24+
"keywords": [
25+
"video-player",
26+
"multi-segment",
27+
"live-streaming",
28+
"videojs"
29+
],
30+
"license": "MIT OR Apache-2.0",
31+
"publishConfig": {
32+
"access": "public"
33+
}
34+
}

webapp/packages/multi-video-player/package.json

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,42 @@
33
"private": true,
44
"version": "0.0.0",
55
"type": "module",
6-
"files": [
7-
"./dist"
8-
],
9-
"main": "./dist/multi-video-player.js",
10-
"module": "./dist/multi-video-player.js",
11-
"types": "./dist/multi-video-player.d.ts",
6+
"main": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"import": "./dist/index.js",
11+
"types": "./dist/index.d.ts"
12+
},
13+
"./index.css": "./dist/index.css"
14+
},
1215
"scripts": {
1316
"dev": "vite",
1417
"build": "tsc && vite build",
1518
"build:test": "tsc && vite build --mode test",
1619
"preview": "vite preview",
1720
"check:write": "biome check --write ./src"
1821
},
22+
"dependencies": {
23+
"@devolutions/shadow-player": "workspace:*",
24+
"video.js": "^8.21.0"
25+
},
1926
"devDependencies": {
2027
"@biomejs/biome": "^1.9.4",
2128
"@types/jest": "^29.5.14",
29+
"@types/jquery": "^3.5.32",
2230
"cross-env": "^7.0.3",
2331
"jest": "^29.7.0",
2432
"jest-environment-jsdom": "^29.7.0",
2533
"ts-jest": "^29.2.6",
2634
"typescript": "~5.7.2",
27-
"video.js": "^8.21.0",
2835
"vite": "^6.2.0",
2936
"vite-plugin-bundle-css": "^0.1.1",
30-
"@types/jquery": "^3.5.32"
31-
},
32-
"dependencies": {
3337
"vite-plugin-dts": "^4.5.3",
3438
"vite-plugin-static-copy": "^2.3.0"
3539
},
3640
"jest": {
3741
"preset": "ts-jest",
3842
"testEnvironment": "jsdom"
39-
},
40-
"publishConfig": {
41-
"access": "restricted"
4243
}
4344
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
export interface GatewayRecordingFile {
2+
fileName: string;
3+
duration: number;
4+
startTime: number;
5+
}
6+
7+
export interface GatewayRecordingFileConfig {
8+
sessionId: string;
9+
startTime: number;
10+
duration: number;
11+
files: GatewayRecordingFile[];
12+
}
13+
14+
interface Session {
15+
id: string;
16+
}
17+
18+
export class GatewayRecordingApi {
19+
constructor(
20+
public gatewayUrl: string,
21+
public sessionId: string,
22+
public token: string
23+
) {}
24+
25+
async fetchMetadata(): Promise<GatewayRecordingFileConfig> {
26+
const url = `${this.gatewayUrl}/jet/jrec/pull/${this.sessionId}/recording.json?token=${this.token}`;
27+
const response = await fetch(url);
28+
29+
if (!response.ok) {
30+
throw new Error(`Failed to fetch recording metadata: ${response.status} ${response.statusText}`);
31+
}
32+
33+
return await response.json();
34+
}
35+
36+
getSegmentUrl(fileName: string): string {
37+
return `${this.gatewayUrl}/jet/jrec/pull/${this.sessionId}/${fileName}?token=${this.token}`;
38+
}
39+
40+
getShadowUrl(): string {
41+
const wsUrl = this.gatewayUrl
42+
.replace('http://', 'ws://')
43+
.replace('https://', 'wss://');
44+
return `${wsUrl}/jet/jrec/shadow/${this.sessionId}?token=${this.token}`;
45+
}
46+
47+
async isSessionActive(): Promise<boolean> {
48+
try {
49+
const url = `${this.gatewayUrl}/jet/sessions`;
50+
51+
const response = await fetch(url, {
52+
headers: {
53+
'Authorization': `Bearer ${this.token}`
54+
}
55+
});
56+
57+
if (!response.ok) {
58+
return false;
59+
}
60+
61+
const sessions: Session[] = await response.json();
62+
const sessionExists = sessions.some((session) => session.id === this.sessionId);
63+
64+
return sessionExists;
65+
} catch (error) {
66+
console.error('Error checking session activity:', error);
67+
return false;
68+
}
69+
}
70+
71+
static fromPullUrl(pullUrl: string): GatewayRecordingApi {
72+
const url = new URL(pullUrl);
73+
const pathParts = url.pathname.split('/pull/');
74+
const gatewayUrl = `${url.protocol}//${url.host}${pathParts[0]}`;
75+
const sessionId = pathParts[1].split('/')[0];
76+
const token = url.searchParams.get('token') || '';
77+
78+
return new GatewayRecordingApi(gatewayUrl, sessionId, token);
79+
}
80+
}

webapp/packages/multi-video-player/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import './style.css';
33
import '../node_modules/video.js/dist/video-js.css';
44

55
export * from './video-player/player';
6+
export * from './gateway-api';
7+
export * from './live-streamer';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import '@devolutions/shadow-player';
2+
import type { ShadowPlayer, ShadowPlayerError } from '@devolutions/shadow-player';
3+
import type { MultiVideoPlayer } from './video-player/player';
4+
import type { GatewayRecordingApi } from './gateway-api';
5+
6+
export class LiveRecordingStreamer {
7+
private shadowPlayer: ShadowPlayer | null = null;
8+
private api: GatewayRecordingApi;
9+
private onSessionNotFoundCallback: (() => void) | null = null;
10+
11+
constructor(api: GatewayRecordingApi) {
12+
this.api = api;
13+
}
14+
15+
onSessionNotFound(callback: () => void): void {
16+
this.onSessionNotFoundCallback = callback;
17+
}
18+
19+
async stream(container: HTMLElement): Promise<ShadowPlayer> {
20+
const shadowUrl = this.api.getShadowUrl();
21+
22+
this.shadowPlayer = document.createElement('shadow-player') as ShadowPlayer;
23+
this.shadowPlayer.setAttribute('controls', '');
24+
this.shadowPlayer.setAttribute('width', '100%');
25+
this.shadowPlayer.setAttribute('height', '100%');
26+
27+
this.shadowPlayer.onError((error: ShadowPlayerError) => {
28+
if (error.type === 'session-not-found') {
29+
this.onSessionNotFoundCallback?.();
30+
}
31+
});
32+
33+
container.appendChild(this.shadowPlayer);
34+
35+
await customElements.whenDefined('shadow-player');
36+
await new Promise((resolve) => setTimeout(resolve, 0));
37+
38+
this.shadowPlayer.srcChange(shadowUrl);
39+
this.shadowPlayer.play();
40+
41+
return this.shadowPlayer;
42+
}
43+
44+
async streamAndTransition(
45+
container: HTMLElement,
46+
staticPlayer: MultiVideoPlayer
47+
): Promise<void> {
48+
const shadowPlayer = await this.stream(container);
49+
50+
shadowPlayer.onEnd(async () => {
51+
container.removeChild(shadowPlayer);
52+
53+
const metadata = await this.api.fetchMetadata();
54+
await staticPlayer.play(
55+
metadata.files.map(file => ({
56+
src: this.api.getSegmentUrl(file.fileName),
57+
type: 'video/webm',
58+
duration: file.duration
59+
}))
60+
);
61+
});
62+
}
63+
64+
disconnect(): void {
65+
if (this.shadowPlayer) {
66+
this.shadowPlayer.disconnect();
67+
68+
if (this.shadowPlayer.parentElement) {
69+
this.shadowPlayer.parentElement.removeChild(this.shadowPlayer);
70+
}
71+
}
72+
this.shadowPlayer = null;
73+
}
74+
75+
isConnected(): boolean {
76+
return this.shadowPlayer !== null && this.shadowPlayer.parentElement !== null;
77+
}
78+
}

0 commit comments

Comments
 (0)