Skip to content

Recipe: Svelte Integration

Svelte’s stores are a close match for the player’s own event emitter, both are “subscribe, get pushed updates” primitives, so the wrapper is a writable store fed by the player’s events plus the player instance itself for imperative calls. A Svelte action handles mounting, since the container element needs to exist in the DOM before nmplayer() can bind to it.

The store

TypeScript
// stores/videoPlayer.ts
import { writable } from 'svelte/store';
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import type { IVideoPlayer, VideoPlayerConfig, VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

export interface VideoPlayerState {
player: IVideoPlayer | null;
currentItem: VideoPlaylistItem | undefined;
isPlaying: boolean;
currentTime: number;
duration: number;
}

const initialState: VideoPlayerState = {
player: null,
currentItem: undefined,
isPlaying: false,
currentTime: 0,
duration: 0,
};

function createVideoPlayerStore() {
const { subscribe, update, set } = writable<VideoPlayerState>(initialState);

return {
subscribe,

init(containerId: string, config: VideoPlayerConfig) {
const player = nmplayer(containerId).setup(config);

player.on('item', ({ item }) => update((state) => ({ ...state, currentItem: item })));
player.on('play', () => update((state) => ({ ...state, isPlaying: true })));
player.on('playing', () => update((state) => ({ ...state, isPlaying: true })));
player.on('pause', () => update((state) => ({ ...state, isPlaying: false })));
player.on('time', ({ time }) => update((state) => ({ ...state, currentTime: time })));
player.on('duration', ({ duration }) => update((state) => ({ ...state, duration })));

update((state) => ({ ...state, player }));
},

dispose() {
update((state) => {
void state.player?.dispose();
return initialState;
});
set(initialState);
},
};
}

export const videoPlayerStore = createVideoPlayerStore();

A mount action

Svelte’s use: actions run exactly once the element is attached to the DOM, the right moment to call nmplayer():

TypeScript
// actions/mountVideoPlayer.ts
import type { VideoPlayerConfig } from '@nomercy-entertainment/nomercy-video-player';
import { videoPlayerStore } from '../stores/videoPlayer';

export function mountVideoPlayer(node: HTMLElement, config: VideoPlayerConfig) {
if (!node.id) node.id = 'svelte-video-player';
videoPlayerStore.init(node.id, config);

return {
destroy() {
videoPlayerStore.dispose();
},
};
}

Using it in a component

Svelte
<!-- VideoPlayer.svelte -->
<script lang="ts">
import type { VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';
import { videoPlayerStore } from '../stores/videoPlayer';
import { mountVideoPlayer } from '../actions/mountVideoPlayer';

export let baseUrl: string;
export let playlist: VideoPlaylistItem[];

$: progress = $videoPlayerStore.duration > 0
? ($videoPlayerStore.currentTime / $videoPlayerStore.duration) * 100
: 0;

function seek(event: MouseEvent) {
const bar = event.currentTarget as HTMLElement;
const ratio = event.offsetX / bar.clientWidth;
$videoPlayerStore.player?.seekByPercentage(ratio * 100);
}
</script>

<div use:mountVideoPlayer={{ baseUrl, playlist }} />

<p>{$videoPlayerStore.currentItem?.title ?? 'Nothing playing'}</p>

<div
role="slider"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
tabindex="0"
on:click={seek}
>
<div style="width: {progress}%" />
</div>

<button type="button" on:click={() => $videoPlayerStore.player?.togglePlayback()}>
{$videoPlayerStore.isPlaying ? 'Pause' : 'Play'}
</button>

$videoPlayerStore auto-subscribes and auto-unsubscribes with the component’s own lifecycle, the $: reactive statement recomputes progress on every store update without a manual subscription. The action’s destroy() fires when the bound element leaves the DOM, disposing the player at exactly the point Svelte itself considers the component gone.

Using it with real data

baseUrl and playlist are props — the parent supplies the real nomercy-media fixture catalogue (the same three shorts the Quickstart and the testbed use):

Svelte
<!-- App.svelte -->
<script lang="ts">
import VideoPlayer from './VideoPlayer.svelte';

const baseUrl = 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films';
const playlist = [
{
id: 'sintel',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: '/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
},
{
id: 'cosmos-laundromat',
title: 'Cosmos Laundromat',
url: '/Cosmos.Laundromat.(2015)/Cosmos.Laundromat.(2015).NoMercy.m3u8',
image: '/w780/f2wABsgj2lIR2dkDEfBZX8p4Iyk.jpg',
},
{
id: 'big-buck-bunny',
title: 'Big Buck Bunny',
url: '/Big.Buck.Bunny.(2008)/Big.Buck.Bunny.(2008).NoMercy.m3u8',
image: '/original/xtdybjRRZ15mCrPOvEld305myys.jpg',
},
];
</script>

<VideoPlayer {baseUrl} {playlist} />

Sharing the instance

videoPlayerStore is a module-level singleton, exported once from stores/videoPlayer.ts. Every component that imports it, not only VideoPlayer.svelte above, reads the exact same reactive state and the exact same player instance, no provider and no second init() call:

Svelte
<!-- NowPlayingBadge.svelte -->
<script lang="ts">
import { videoPlayerStore } from '../stores/videoPlayer';
</script>

{#if $videoPlayerStore.currentItem}
<p class="badge">
{$videoPlayerStore.isPlaying ? 'Playing' : 'Paused'}{$videoPlayerStore.currentItem.title}
</p>
{/if}

Mount <NowPlayingBadge /> anywhere else in the tree, a header, a persistent mini-player, a route that never renders VideoPlayer.svelte itself, and it reflects the same playback state. Svelte’s module system is the shared-instance mechanism here, the same job Vue’s provide/inject and React’s context provider do explicitly elsewhere in this section.

Next steps