Skip to content

Commit 5169f60

Browse files
feat(dgw): shadowing player web-component (#1075)
1 parent 4138b52 commit 5169f60

File tree

18 files changed

+2604
-0
lines changed

18 files changed

+2604
-0
lines changed

webapp/shadow-player/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Base URL of the API
2+
const TOKEN_SERVER_BASE_URL = 'http://localhost:8080';
3+
const GATEWAY_BASE_URL = 'http://localhost:7171';
4+
5+
// Common request fields
6+
interface CommonRequest {
7+
validity_duration?: string;
8+
kid?: string;
9+
delegation_key_path?: string;
10+
jet_gw_id?: string;
11+
}
12+
13+
// Response type
14+
interface TokenResponse {
15+
token: string;
16+
}
17+
18+
// Scope request interface
19+
interface ScopeRequest extends CommonRequest {
20+
scope: string;
21+
}
22+
23+
export async function requestScopeToken(data: ScopeRequest): Promise<TokenResponse> {
24+
try {
25+
const response = await fetch(`${TOKEN_SERVER_BASE_URL}/scope`, {
26+
method: 'POST',
27+
headers: {
28+
'Content-Type': 'application/json',
29+
},
30+
body: JSON.stringify(data),
31+
});
32+
33+
if (!response.ok) {
34+
const errorResponse = await response.json();
35+
throw new Error(`Error ${response.status}: ${JSON.stringify(errorResponse)}`);
36+
}
37+
38+
return await response.json();
39+
} catch (error) {
40+
throw new Error(`Error: ${error.message}`);
41+
}
42+
}
43+
44+
// Function to list recordings
45+
export async function listRealtimeRecordings(): Promise<string[]> {
46+
const res = await requestScopeToken({ scope: 'gateway.recordings.read' });
47+
try {
48+
const response = await fetch(`${GATEWAY_BASE_URL}/jet/jrec/list/realtime`, {
49+
method: 'GET',
50+
headers: {
51+
Authorization: `Bearer ${res.token}`,
52+
},
53+
});
54+
55+
if (!response.ok) {
56+
const errorResponse = await response.json();
57+
throw new Error(`Error ${response.status}: ${JSON.stringify(errorResponse)}`);
58+
}
59+
60+
return await response.json();
61+
} catch (error: any) {
62+
throw new Error(`Error: ${error.message}`);
63+
}
64+
}
65+
66+
interface PullTokenRequest extends CommonRequest {
67+
jet_rop: 'pull';
68+
jet_aid: string;
69+
}
70+
71+
export async function requestPullToken(data: PullTokenRequest): Promise<TokenResponse> {
72+
try {
73+
const response = await fetch(`${TOKEN_SERVER_BASE_URL}/jrec`, {
74+
method: 'POST',
75+
headers: {
76+
'Content-Type': 'application/json',
77+
},
78+
body: JSON.stringify(data),
79+
});
80+
81+
if (!response.ok) {
82+
const errorResponse = await response.json();
83+
throw new Error(`Error ${response.status}: ${JSON.stringify(errorResponse)}`);
84+
}
85+
86+
return await response.json();
87+
} catch (error: any) {
88+
throw new Error(`Error: ${error.message}`);
89+
}
90+
}
91+
92+
interface GetInfoFileResponse {
93+
duration: number;
94+
files: {
95+
fileName: string;
96+
startTime: number;
97+
duration: number;
98+
};
99+
sessionId: string;
100+
startTime: number;
101+
}
102+
103+
export async function getInfoFile(uid: string): Promise<GetInfoFileResponse> {
104+
const pullFileToken = await requestPullToken({ jet_rop: 'pull', jet_aid: uid });
105+
try {
106+
const response = await fetch(`${GATEWAY_BASE_URL}/jet/jrec/pull/${uid}/recording.json`, {
107+
method: 'GET',
108+
headers: {
109+
Authorization: `Bearer ${pullFileToken.token}`,
110+
},
111+
});
112+
113+
if (!response.ok) {
114+
const errorResponse = await response.json();
115+
throw new Error(`Error ${response.status}: ${JSON.stringify(errorResponse)}`);
116+
}
117+
118+
return await response.json();
119+
} catch (error: any) {
120+
throw new Error(`Error: ${error.message}`);
121+
}
122+
}
123+
124+
export async function getStreamingWebsocketUrl(uid: string): Promise<string> {
125+
const fileInfo = await getInfoFile(uid);
126+
const token = await requestPullToken({ jet_rop: 'pull', jet_aid: uid });
127+
return `${GATEWAY_BASE_URL}/jet/jrec/realtime/${uid}/${fileInfo.files[0].fileName}?token=${token.token}`;
128+
}

webapp/shadow-player/demo-src/paly.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { getInfoFile, getStreamingWebsocketUrl } from './apiClient';
2+
import { ShadowPlayer } from '../src/streamer';
3+
4+
// Function to play the selected stream
5+
export async function playStream(id: string) {
6+
const websocketUrl = await getStreamingWebsocketUrl(id);
7+
8+
const videoElement = document.getElementById('shadowPlayer') as ShadowPlayer;
9+
videoElement.srcChange(websocketUrl);
10+
}

webapp/shadow-player/demo-src/ui.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
import { listRealtimeRecordings } from './apiClient';
3+
import { playStream } from './paly';
4+
5+
// Function to populate the file list
6+
async function populateFileList() {
7+
const files = await listRealtimeRecordings();
8+
9+
const fileList = document.getElementById('fileList');
10+
if (!fileList) {
11+
console.error('File list not found');
12+
return;
13+
}
14+
15+
fileList.innerHTML = ''; // Clear existing items
16+
17+
files.forEach((file, index) => {
18+
const fileItem = document.createElement('div');
19+
fileItem.className = 'file-item';
20+
21+
const fileName = document.createElement('span');
22+
fileName.className = 'file-name';
23+
fileName.textContent = file;
24+
25+
const playButton = document.createElement('button');
26+
playButton.className = 'play-button';
27+
playButton.textContent = 'Play';
28+
playButton.onclick = () => playStream(file);
29+
30+
fileItem.appendChild(fileName);
31+
fileItem.appendChild(playButton);
32+
33+
fileList.appendChild(fileItem);
34+
});
35+
}
36+
37+
// Initialize the file list on page load
38+
window.onload = populateFileList;
39+
40+
export function refreshList() {
41+
// Logic to refresh the list can be added here
42+
// For now, we'll just re-populate the list
43+
populateFileList();
44+
}
45+
46+
export const getElementNotNull = (id: string) => {
47+
const element = document.getElementById(id);
48+
if (!element) {
49+
throw new Error(`Element with ID ${id} not found`);
50+
}
51+
return element;
52+
};
53+
54+
getElementNotNull('refreshButton').onclick = refreshList;

webapp/shadow-player/index.html

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Streaming Dashboard</title>
9+
<style>
10+
body {
11+
margin: 0;
12+
font-family: Arial, sans-serif;
13+
}
14+
15+
.container {
16+
display: flex;
17+
height: 100vh;
18+
}
19+
20+
.sidebar {
21+
width: 30%;
22+
background-color: #f4f4f4;
23+
border-right: 1px solid #ddd;
24+
display: flex;
25+
flex-direction: column;
26+
}
27+
28+
.sidebar-header {
29+
padding: 20px;
30+
background-color: #fff;
31+
border-bottom: 1px solid #ddd;
32+
display: flex;
33+
justify-content: space-between;
34+
align-items: center;
35+
}
36+
37+
.sidebar-header h2 {
38+
margin: 0;
39+
font-size: 18px;
40+
}
41+
42+
.refresh-button {
43+
padding: 8px 12px;
44+
background-color: #007bff;
45+
color: #fff;
46+
border: none;
47+
border-radius: 4px;
48+
cursor: pointer;
49+
}
50+
51+
.refresh-button:hover {
52+
background-color: #0056b3;
53+
}
54+
55+
.file-list {
56+
flex: 1;
57+
overflow-y: auto;
58+
padding: 10px;
59+
}
60+
61+
.file-item {
62+
padding: 10px;
63+
margin-bottom: 10px;
64+
background-color: #fff;
65+
border: 1px solid #ddd;
66+
border-radius: 4px;
67+
display: flex;
68+
justify-content: space-between;
69+
align-items: center;
70+
}
71+
72+
.file-name {
73+
font-size: 16px;
74+
}
75+
76+
.play-button {
77+
padding: 6px 10px;
78+
background-color: #28a745;
79+
color: #fff;
80+
border: none;
81+
border-radius: 4px;
82+
cursor: pointer;
83+
}
84+
85+
.play-button:hover {
86+
background-color: #218838;
87+
}
88+
89+
.player-container {
90+
flex: 1;
91+
display: flex;
92+
align-items: center;
93+
justify-content: center;
94+
background-color: #e9ecef;
95+
}
96+
97+
webm-stream-player {
98+
width: 80%;
99+
height: 80%;
100+
background-color: #000;
101+
}
102+
</style>
103+
</head>
104+
105+
<body>
106+
<div class="container">
107+
<!-- Sidebar -->
108+
<div class="sidebar">
109+
<div class="sidebar-header">
110+
<h2>Streaming Files</h2>
111+
<button class="refresh-button" id="refreshButton">Refresh</button>
112+
</div>
113+
<div class="file-list" id="fileList">
114+
<!-- File items will be populated here -->
115+
</div>
116+
</div>
117+
118+
<!-- Main Content -->
119+
<div class="player-container">
120+
<shawdow-player id="shadowPlayer"></shawdow-player>
121+
</div>
122+
</div>
123+
124+
<script type="module" src="/src/main.ts"></script>
125+
<script type="module" src="index.ts"></script>
126+
127+
</body>
128+
129+
</html>

webapp/shadow-player/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// Demo for using the webm-stream-player element.
2+
import './demo-src/ui';

0 commit comments

Comments
 (0)