# Example Configuration
## Full-Featured Example
This example demonstrates every available component and feature in the library: HLS streaming, keyboard shortcuts overlay, context menu, error overlay, cinema mode, settings panel, screenshot, download, configuration storage, chapters, subtitles, and all control bar components.
```html
```
```typescript
import { addEvaIcons } from 'ez-vid-ang';
import { evaAllIcons } from 'ez-vid-ang/icons';
addEvaIcons(evaAllIcons);
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
signal,
viewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Subscription } from 'rxjs';
import {
EvaActiveChapter,
EvaApi,
EvaBackward,
EvaBuffering,
EvaChapterList,
EvaChapterMarker,
EvaCinemaMode,
EvaContextMenu,
EvaContextMenuEvent,
EvaContextMenuItem,
EvaControlsContainer,
EvaControlsDivider,
EvaDownload,
EvaDownloadEvent,
EvaErrorOverlay,
EvaForward,
EvaFullscreen,
EvaHlsDirective,
EvaKeyboardShortcutsConfiguration,
EvaKeyboardShortcutsOverlay,
EvaLoop,
EvaMute,
EvaOverlayPlay,
EvaPictureInPicture,
EvaPlaybackSpeed,
EvaPlayer,
EvaPlayPause,
EvaQualitySelector,
EvaRemotePlayback,
EvaScreenshot,
EvaScreenshotEvent,
EvaScrubBar,
EvaScrubBarBufferingTime,
EvaScrubBarCurrentTime,
EvaSettingsMenuEvent,
EvaSettingsMenuItem,
EvaSettingsPanel,
EvaSubtitleDisplay,
EvaTimeDisplay,
EvaTrack,
EvaTrackSelector,
EvaUserInteractionEventsDirective,
EvaVideoElementConfiguration,
EvaVideoSource,
EvaVolume,
} from 'ez-vid-ang';
@Component({
selector: 'app-full-example',
templateUrl: './full-example.html',
styleUrl: './full-example.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
EvaActiveChapter,
EvaBackward,
EvaBuffering,
EvaChapterList,
EvaCinemaMode,
EvaContextMenu,
EvaControlsContainer,
EvaControlsDivider,
EvaDownload,
EvaErrorOverlay,
EvaForward,
EvaFullscreen,
EvaHlsDirective,
EvaKeyboardShortcutsOverlay,
EvaLoop,
EvaMute,
EvaOverlayPlay,
EvaPictureInPicture,
EvaPlaybackSpeed,
EvaPlayer,
EvaPlayPause,
EvaQualitySelector,
EvaRemotePlayback,
EvaScreenshot,
EvaScrubBar,
EvaScrubBarBufferingTime,
EvaScrubBarCurrentTime,
EvaSettingsPanel,
EvaSubtitleDisplay,
EvaTimeDisplay,
EvaTrackSelector,
EvaUserInteractionEventsDirective,
EvaVolume,
],
})
export class FullExampleComponent implements AfterViewInit, OnInit, OnDestroy {
private readonly player = viewChild.required('evaVideoPlayer');
private pipSub: Subscription | null = null;
private get api(): EvaApi {
return this.player().playerMainAPI;
}
// ─── Video sources ─────────────────────────────────────────────────────────
protected readonly videoSources = signal([
{ type: 'video/mp4', src: '/video-sample.mp4' },
]);
protected readonly hlsSource = signal('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
protected readonly thumbnailVtt = signal('assets/thumbnails.vtt');
protected readonly videoConfiguration = signal({
autoplay: false,
controls: false,
crossorigin: 'anonymous',
disablePictureInPicture: false,
loop: false,
preload: 'auto',
startingVolume: 0.5,
});
protected readonly keyboardConfig = signal({
backwardsKeyOne: 'J',
forwardKeyOne: 'L',
backwardsKeyTwo: 'ArrowLeft',
forwardKeyTwo: 'ArrowRight',
backwardSeconds: 10,
forwardSeconds: 10,
});
// ─── Tracks & chapters ─────────────────────────────────────────────────────
protected readonly videoTracks = signal([
{ kind: 'subtitles', srclang: 'EN', label: 'English', src: 'subs-en.vtt' },
{ kind: 'subtitles', srclang: 'HR', label: 'Croatian', src: 'subs-hr.vtt' },
{ kind: 'chapters', srclang: 'EN', label: 'Chapters', src: 'chapters.vtt' },
]);
protected readonly chapters = signal([
{ startTime: 0, endTime: 90, title: 'Intro' },
{ startTime: 90, endTime: 180, title: 'Background & Context' },
{ startTime: 180, endTime: 300, title: 'Main Topic' },
{ startTime: 300, endTime: 420, title: 'Deep Dive' },
{ startTime: 420, endTime: 540, title: 'Examples & Demo' },
{ startTime: 540, endTime: 600, title: 'Conclusion' },
]);
protected readonly isChapterListOpen = signal(false);
protected toggleChapterList(): void {
this.isChapterListOpen.update(v => !v);
}
// ─── Context menu ──────────────────────────────────────────────────────────
protected readonly contextMenuItems: EvaContextMenuItem[] = [
{ id: 'copy-url', label: 'Copy video URL' },
{ id: 'copy-time', label: 'Copy URL at current time' },
{ id: 'sep1', label: '', divider: true },
{ id: 'screenshot', label: 'Take screenshot' },
{ id: 'sep2', label: '', divider: true },
{ id: 'about', label: 'About EzVidAng' },
];
protected onContextMenuAction(event: EvaContextMenuEvent): void {
switch (event.itemId) {
case 'copy-url':
navigator.clipboard.writeText(event.currentSrc);
break;
case 'copy-time': {
const url = `${event.currentSrc}#t=${Math.floor(event.currentTime)}`;
navigator.clipboard.writeText(url);
break;
}
case 'screenshot':
this.api.captureScreenshot().then(result => {
if (result?.blob) {
navigator.clipboard.write([
new ClipboardItem({ [result.blob.type]: result.blob }),
]);
}
});
break;
case 'about':
window.open('https://github.com/nicoss01/ez-vid-ang', '_blank');
break;
}
}
// ─── Download & screenshot handlers ────────────────────────────────────────
protected onDownload(event: EvaDownloadEvent): void {
const a = document.createElement('a');
a.href = event.currentSrc;
a.download = '';
a.click();
}
protected onScreenshot(event: EvaScreenshotEvent): void {
if (event.dataUrl) {
const a = document.createElement('a');
a.href = event.dataUrl;
a.download = `screenshot-${event.currentTime.toFixed(1)}s.png`;
a.click();
}
}
protected onRetry(): void {
console.log('Retry clicked — video reloading');
}
// ─── Settings panel ────────────────────────────────────────────────────────
private isLooping = false;
private isCinemaMode = false;
protected readonly settingsItems = signal([
{
id: 'speed',
label: 'Playback speed',
currentValue: 'Normal',
options: [
{ id: '0.25', label: '0.25x' },
{ id: '0.5', label: '0.5x' },
{ id: '1', label: 'Normal', selected: true },
{ id: '1.5', label: '1.5x' },
{ id: '2', label: '2x' },
{ id: '4', label: '4x' },
],
},
{ id: 'loop', label: 'Loop', currentValue: 'Off' },
{ id: 'cinema', label: 'Cinema mode', currentValue: 'Off' },
{ id: 'pip', label: 'Picture-in-Picture', currentValue: 'Off' },
{ id: 'screenshot', label: 'Take screenshot' },
{ id: 'shortcuts', label: 'Keyboard shortcuts' },
]);
public ngOnInit(): void {
// Defer PiP subscription until after view init
}
public ngAfterViewInit(): void {
this.pipSub = this.api.pictureInPictureSubject.subscribe(active => {
this.settingsItems.update(items =>
items.map(item =>
item.id === 'pip'
? { ...item, currentValue: active ? 'On' : 'Off' }
: item,
),
);
});
}
public ngOnDestroy(): void {
this.pipSub?.unsubscribe();
}
protected onSettingChanged(event: EvaSettingsMenuEvent): void {
switch (event.itemId) {
case 'speed':
this.api.setPlaybackSpeed(Number(event.optionId));
this.updateSubMenu('speed', event);
break;
case 'loop':
this.isLooping = !this.isLooping;
if (this.api.assignedVideoElement) {
this.api.assignedVideoElement.loop = this.isLooping;
this.api.loopSubject.next(this.isLooping);
}
this.updateToggle('loop', this.isLooping);
break;
case 'cinema':
this.isCinemaMode = !this.isCinemaMode;
this.api.cinemaModeSubject.next(this.isCinemaMode);
this.updateToggle('cinema', this.isCinemaMode);
break;
case 'pip':
this.api.changePictureInPictureStatus();
break;
case 'screenshot':
this.api.captureScreenshot().then(result => {
if (result?.blob) {
const url = URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = url;
a.download = `screenshot-${result.currentTime.toFixed(1)}s.png`;
a.click();
URL.revokeObjectURL(url);
}
});
break;
case 'shortcuts':
this.api.keyboardShortcutsOverlaySubject.next(true);
this.api.controlsSelectorComponentActive.next(true);
break;
}
}
private updateSubMenu(itemId: string, event: EvaSettingsMenuEvent): void {
this.settingsItems.update(items =>
items.map(item =>
item.id === itemId
? {
...item,
currentValue: event.label,
options: item.options?.map(opt => ({
...opt,
selected: opt.id === event.optionId,
})),
}
: item,
),
);
}
private updateToggle(itemId: string, active: boolean): void {
this.settingsItems.update(items =>
items.map(item =>
item.id === itemId
? { ...item, currentValue: active ? 'On' : 'Off' }
: item,
),
);
}
}
```
```scss
:host {
display: block;
width: 100%;
height: 100%;
position: relative;
}
```
See also: [Simple Example](example-simple.md) for a minimal player setup.