Skip to content

Video Player: Vue 3 Integration

The shallowRef pattern

The player instance must be wrapped in shallowRef, not ref. The player is a class instance with many methods, and deep reactivity (ref) will try to make every property reactive, which triggers proxies on internal state. Use shallowRef so Vue tracks the reference without proxying the instance.

TypeScript
// composables/useVideoPlayer.ts
import { shallowRef, onUnmounted } from 'vue';
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

export function useVideoPlayer(containerId: string) {
const player = shallowRef(
nmplayer(containerId)
.addPlugin(DesktopUiPlugin)
.setup({ playlist: [] }),
);

onUnmounted(() => {
player.value.dispose();
});

return { player };
}

Component example

Vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type { NMVideoPlayer } from '@nomercy-entertainment/nomercy-video-player';

let player: NMVideoPlayer | null = null;

const isPlaying = ref(false);
const currentTime = ref(0);

onMounted(async () => {
player = nmplayer('main')
.addPlugin(DesktopUiPlugin)
.setup({
playlist: [{ id: '1', url: '...', title: 'Demo' }],
});

player.on('play', () => {
isPlaying.value = true;
});
player.on('pause', () => {
isPlaying.value = false;
});
player.on('time', ({ time }) => {
currentTime.value = time;
});

await player.ready();
});

onUnmounted(() => {
player?.dispose();
});
</script>

<template>
<div>
<div id="main" style="width: 100%; aspect-ratio: 16/9;" />
<div>
<button @click="isPlaying ? player?.pause() : player?.play()">
{{ isPlaying ? 'Pause' : 'Play' }}
</button>
<span>{{ Math.floor(currentTime) }}s</span>
</div>
</div>
</template>

Shared player across components

Create the player once and export the ref:

TypeScript
// lib/player.ts
import { shallowRef } from 'vue';
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

export const player = shallowRef(nmplayer('global'));

// Call setup() once after the DOM is ready (e.g., in App.vue onMounted).
// The player mounts by finding the div with id 'global'.
export async function initPlayer() {
player.value
.addPlugin(DesktopUiPlugin)
.setup({ playlist: [] });
await player.value.ready();
}

Import in any component:

TypeScript
import { player } from '@/lib/player';

player.value.on('current', ({ item }) => {
/* ... */
});

Queue updates from Pinia / Vuex

TypeScript
import { watch } from 'vue';
import { usePlayerStore } from '@/stores/player';
import { player } from '@/lib/player';

const store = usePlayerStore();

watch(
() => store.queue,
(newQueue) => {
player.value.queue(newQueue);
},
{ deep: true },
);

Reactive playlist updates

To swap the entire queue while the player is running, call player.queue(newItems) to replace the list, then player.item(0, { autoplay: true }) to jump to the first item and start playback. queue() accepts VideoPlaylistItem[]; item() accepts an item, an id string, or a numeric index.

TypeScript
import type { VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

function loadNewPlaylist(items: VideoPlaylistItem[]) {
player.value?.queue(items);
player.value?.item(0, { autoplay: true });
}

Use this inside a Pinia action or a Vue watch to react to catalogue navigation:

TypeScript
import { watch } from 'vue';
import type { VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';
import { useLibraryStore } from '@/stores/library';
import { player } from '@/lib/player';

const store = useLibraryStore();

watch(
() => store.selectedPlaylist,
(items: VideoPlaylistItem[]) => {
player.value.queue(items);
player.value.item(0, { autoplay: true });
},
);

Example items using the NoMercy media catalogue:

TypeScript
const sintelPlaylist: VideoPlaylistItem[] = [
{
id: 'sintel',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: '/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
duration: 888,
subtitles: [
{ id: 'sintel-en', label: 'English', url: '/Sintel.(2010)/subtitles/Sintel.(2010).NoMercy.eng.full.vtt', language: 'eng', kind: 'subtitles' },
],
},
{
id: 'big-buck-bunny',
title: 'Big Buck Bunny',
url: '/Big.Buck.Bunny.(2008)/Big.Buck.Bunny.(2008).NoMercy.m3u8',
image: '/w780/uVEFQvFMMsg4e6yb03xOfVsDz0o.jpg',
duration: 596,
},
];

loadNewPlaylist(sintelPlaylist);

Note: Calling queue() replaces the list immediately but does not start loading. The item(0, { autoplay: true }) call triggers the load and playback. If you want to append items to a running queue instead, use player.queueAppend(items).

Nuxt / SSR

The player requires browser APIs. Wrap initialization in onMounted or use client:only:

TypeScript
// In onMounted, player will not be created during SSR
onMounted(() => {
player.value = nmplayer('main').setup({ ... });
});

Or use a <ClientOnly> wrapper component around the player component.