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.
// 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
<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:
// 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:
import { player } from '@/lib/player';
player.value.on('current', ({ item }) => {
/* ... */
});
Queue updates from Pinia / Vuex
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.
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:
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:
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. Theitem(0, { autoplay: true })call triggers the load and playback. If you want to append items to a running queue instead, useplayer.queueAppend(items).
Nuxt / SSR
The player requires browser APIs.
Wrap initialization in onMounted or use client:only:
// 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.