Skip to content
Draft
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
9 changes: 9 additions & 0 deletions webapp/apps/recording-player/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

html,
body {
width: 100%;
height: 100%;
margin: 0;
background-color: black;
}

shadow-player {
display: block;
width: 100%;
height: 100%;
}

#terminal {
height: 100%;
}
Expand Down
11 changes: 11 additions & 0 deletions webapp/apps/recording-player/public/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
"unknownError": "Unbekannter Fehler, bitte versuchen Sie es erneut",
"protocolError": "Ein Fehler ist aufgetreten: {{error}}"
},
"controls": {
"play": "Wiedergabe",
"pause": "Pause",
"mute": "Stummschalten",
"unmute": "Stummschaltung aufheben",
"volume": "Lautstärke",
"timeline": "Aufzeichnungszeitachse",
"fullscreen": "Vollbild",
"exitFullscreen": "Vollbild beenden",
"clip": "Clip"
},
"ui": {
"close": "Schließen"
}
Expand Down
11 changes: 11 additions & 0 deletions webapp/apps/recording-player/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
"unknownError": "Unknown error, please try again",
"protocolError": "An error occurred: {{error}}"
},
"controls": {
"play": "Play",
"pause": "Pause",
"mute": "Mute",
"unmute": "Unmute",
"volume": "Volume",
"timeline": "Recording timeline",
"fullscreen": "Fullscreen",
"exitFullscreen": "Exit fullscreen",
"clip": "Clip"
},
"ui": {
"close": "Close"
}
Expand Down
11 changes: 11 additions & 0 deletions webapp/apps/recording-player/public/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
"unknownError": "Error desconocido, por favor intente de nuevo",
"protocolError": "Se produjo un error: {{error}}"
},
"controls": {
"play": "Reproducir",
"pause": "Pausar",
"mute": "Silenciar",
"unmute": "Activar sonido",
"volume": "Volumen",
"timeline": "Línea de tiempo de la grabación",
"fullscreen": "Pantalla completa",
"exitFullscreen": "Salir de pantalla completa",
"clip": "Clip"
},
"ui": {
"close": "Cerrar"
}
Expand Down
11 changes: 11 additions & 0 deletions webapp/apps/recording-player/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
"unknownError": "Erreur inconnue, veuillez réessayer",
"protocolError": "Une erreur s'est produite: {{error}}"
},
"controls": {
"play": "Lire",
"pause": "Pause",
"mute": "Couper le son",
"unmute": "Réactiver le son",
"volume": "Volume",
"timeline": "Chronologie de l'enregistrement",
"fullscreen": "Plein écran",
"exitFullscreen": "Quitter le plein écran",
"clip": "Séquence"
},
"ui": {
"close": "Fermer"
}
Expand Down
9 changes: 9 additions & 0 deletions webapp/apps/recording-player/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export type TranslationKeys =
| 'notifications.unauthorized'
| 'notifications.unknownError'
| 'notifications.protocolError'
| 'controls.play'
| 'controls.pause'
| 'controls.mute'
| 'controls.unmute'
| 'controls.volume'
| 'controls.timeline'
| 'controls.fullscreen'
| 'controls.exitFullscreen'
| 'controls.clip'
| 'ui.close';

/**
Expand Down
18 changes: 12 additions & 6 deletions webapp/apps/recording-player/src/streamers/webm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ import { t } from '../i18n';
import { showNotification } from '../notification';

export async function handleWebm(gatewayAccessApi: GatewayAccessApi) {
// Create element with correct spelling
const shadowPlayer = document.createElement('shadow-player') as ShadowPlayer;
shadowPlayer.setAttribute('controls', '');
shadowPlayer.setControlLabels({
play: t('controls.play'),
pause: t('controls.pause'),
mute: t('controls.mute'),
unmute: t('controls.unmute'),
volume: t('controls.volume'),
timeline: t('controls.timeline'),
fullscreen: t('controls.fullscreen'),
exitFullscreen: t('controls.exitFullscreen'),
clip: t('controls.clip'),
});

// Append to DOM
document.body.appendChild(shadowPlayer);

// Wait for element to be initialized
await customElements.whenDefined('shadow-player');

// Wait for next microtask to ensure connectedCallback has run
await new Promise((resolve) => setTimeout(resolve, 0));

// Now safe to call methods
shadowPlayer.srcChange(gatewayAccessApi.sessionShadowingUrl());
shadowPlayer.play();

Expand Down
4 changes: 2 additions & 2 deletions webapp/packages/shadow-player/demo-src/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Base URL of the API
const TOKEN_SERVER_BASE_URL = 'http://localhost:8080';
const GATEWAY_BASE_URL = 'http://localhost:7171';
const TOKEN_SERVER_BASE_URL = import.meta.env.VITE_TOKEN_SERVER_BASE_URL ?? 'http://localhost:8080';
const GATEWAY_BASE_URL = import.meta.env.VITE_GATEWAY_BASE_URL ?? 'http://localhost:7171';

// Common request fields
interface CommonRequest {
Expand Down
4 changes: 2 additions & 2 deletions webapp/packages/shadow-player/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
background-color: #e9ecef;
}

webm-stream-player {
shadow-player {
width: 80%;
height: 80%;
background-color: #000;
Expand All @@ -120,7 +120,7 @@ <h2>Streaming Files</h2>

<!-- Main Content -->
<div class="player-container">
<shadow-player id="shadowPlayer"></shadow-player>
<shadow-player id="shadowPlayer" controls></shadow-player>
</div>
</div>

Expand Down
80 changes: 80 additions & 0 deletions webapp/packages/shadow-player/src/playbackClip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { SegmentStartedMessage } from './protocol';
import { ReactiveSourceBuffer } from './sourceBuffer';

export class PlaybackClip {
readonly video = document.createElement('video');

private readonly mediaSource = new MediaSource();
private readonly objectUrl = URL.createObjectURL(this.mediaSource);
private readonly opened: Promise<void>;
private sourceBuffer: ReactiveSourceBuffer | null = null;
private debug = false;
private complete = false;

constructor(readonly metadata: SegmentStartedMessage) {
this.video.src = this.objectUrl;
this.opened = new Promise<void>((resolve, reject) => {
const cleanup = () => {
this.mediaSource.removeEventListener('sourceopen', onOpen);
this.mediaSource.removeEventListener('sourceclose', onClose);
};
const onOpen = () => {
cleanup();
try {
this.sourceBuffer = new ReactiveSourceBuffer(this.mediaSource, metadata.codec);
this.sourceBuffer.setDebug(this.debug);
resolve();
} catch (error) {
reject(error);
}
};
const onClose = () => {
cleanup();
reject(new Error('MediaSource closed before it opened'));
};

this.mediaSource.addEventListener('sourceopen', onOpen);
this.mediaSource.addEventListener('sourceclose', onClose);
});
}

async open(): Promise<void> {
await this.opened;
}

async append(data: Uint8Array): Promise<void> {
await this.opened;
if (this.complete || !this.sourceBuffer) {
throw new Error('Cannot append to a completed clip');
}
await this.sourceBuffer.appendBuffer(data);
}

finish(): void {
if (this.complete) {
return;
}
if (this.mediaSource.readyState !== 'open') {
throw new Error('Cannot finish a MediaSource that is not open');
}
this.mediaSource.endOfStream();
this.complete = true;
}

setDebug(debug: boolean): void {
this.debug = debug;
this.sourceBuffer?.setDebug(debug);
}

downloadBufferedFile(): void {
this.sourceBuffer?.downloadBufferedFile();
}

dispose(): void {
this.video.pause();
this.video.removeAttribute('src');
this.video.load();
this.video.remove();
URL.revokeObjectURL(this.objectUrl);
}
}
149 changes: 149 additions & 0 deletions webapp/packages/shadow-player/src/playbackControls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
.control-bar {
position: absolute;
z-index: 3;
right: 0;
bottom: 0;
left: 0;
display: flex;
height: 30px;
align-items: stretch;
color: #fff;
background: rgba(43, 51, 63, 0.7);
font-family: Arial, Helvetica, sans-serif;
}

:host(:not([controls])) .control-bar {
display: none;
}

.control-button {
position: relative;
display: grid;
width: 40px;
min-width: 40px;
height: 30px;
padding: 7px 10px;
place-items: center;
color: inherit;
background: transparent;
border: 0;
cursor: pointer;
}

.control-button:hover,
.control-button:focus-visible {
color: #fff;
background: rgba(255, 255, 255, 0.12);
outline: none;
}

.control-button:focus-visible,
.timeline-segment:focus-visible,
.volume-input:focus-visible {
box-shadow: inset 0 0 0 2px #fff;
}

.control-button svg {
width: 16px;
height: 16px;
fill: currentColor;
}

.volume-control {
display: flex;
width: 40px;
min-width: 40px;
overflow: hidden;
align-items: center;
transition: width 120ms ease;
}

.volume-control:hover,
.volume-control:focus-within {
width: 105px;
}

.volume-input {
width: 0;
height: 3px;
margin: 0;
opacity: 0;
accent-color: #fff;
cursor: pointer;
transition:
width 120ms ease,
opacity 120ms ease;
}

.volume-control:hover .volume-input,
.volume-control:focus-within .volume-input {
width: 58px;
opacity: 1;
}

.timeline {
display: flex;
min-width: 4em;
flex: 1;
align-items: center;
touch-action: none;
}

.timeline-segment {
position: relative;
height: 3px;
min-width: 3px;
margin-left: 3px;
flex-basis: 0;
overflow: visible;
background: rgba(115, 133, 159, 0.5);
cursor: pointer;
transition: height 80ms ease;
}

.timeline-segment:hover,
.timeline-segment:focus-visible {
height: 10px;
outline: none;
}

.timeline-segment[aria-disabled="true"] {
cursor: wait;
}

.timeline-progress {
position: absolute;
inset: 0 auto 0 0;
width: 0;
background: #fff;
pointer-events: none;
}

.time-tooltip {
position: absolute;
bottom: 15px;
left: 0;
visibility: hidden;
padding: 5px 8px;
color: #fff;
background: rgba(0, 0, 0, 0.8);
border-radius: 2px;
font-size: 12px;
line-height: 1;
pointer-events: none;
transform: translateX(-50%);
white-space: nowrap;
}

.timeline-segment:hover .time-tooltip,
.timeline-segment:focus-visible .time-tooltip {
visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
.volume-control,
.volume-input,
.timeline-segment {
transition: none;
}
}
Loading
Loading