Skip to content

Vue Integration

NMMusicPlayer is framework-agnostic. In Vue 3, wrap it in a composable to expose reactive state.

Composable pattern

TypeScript
// composables/useMusicPlayer.ts
import { ref, onUnmounted } from 'vue';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AutoAdvancePlugin,
MediaSessionPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';

export function useMusicPlayer() {
const currentTrack = ref<MusicPlaylistItem | undefined>(undefined);
const isPlaying = ref(false);
const currentTime = ref(0);
const duration = ref(0);
const volume = ref(100);

const player = nmMPlayer('main')
.addPlugin(AutoAdvancePlugin)
.addPlugin(MediaSessionPlugin)
.setup({
playlist: [],
});

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

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

player.on('time', ({ time }) => {
currentTime.value = time;
});

player.on('duration', ({ duration: dur }) => {
duration.value = dur;
});

player.on('volume', ({ level }) => {
volume.value = level;
});

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

return {
player,
currentTrack,
isPlaying,
currentTime,
duration,
volume,
};
}

Using in a component

Vue
<script setup lang="ts">
import { computed } from 'vue';
import { useMusicPlayer } from '@/composables/useMusicPlayer';

const { player, currentTrack, isPlaying, currentTime, duration } = useMusicPlayer();

const progress = computed(() =>
duration.value > 0 ? (currentTime.value / duration.value) * 100 : 0,
);

function seek(event: MouseEvent) {
const bar = event.currentTarget as HTMLElement;
const ratio = event.offsetX / bar.clientWidth;
void player.time(ratio * player.duration());
}
</script>

<template>
<div class="player">
<p>{{ currentTrack?.name ?? 'Nothing playing' }}</p>
<p>{{ currentTrack?.artist }}</p>

<div class="progress" @click="seek">
<div class="fill" :style="{ width: `${progress}%` }" />
</div>

<button @click="player.togglePlayback()">
{{ isPlaying ? 'Pause' : 'Play' }}
</button>
<button @click="player.previous()">Prev</button>
<button @click="player.next()">Next</button>
</div>
</template>

Providing the player globally

For apps where multiple components need the same player, provide it at the app root:

TypeScript
// main.ts
import { createApp } from 'vue';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';

const player = nmMPlayer('global');
const app = createApp(App);
app.provide('player', player);
TypeScript
// any component
import { inject } from 'vue';
import type { IMusicPlayer } from '@nomercy-entertainment/nomercy-music-player';

const player = inject<IMusicPlayer>('player')!;

Pinia store pattern

When several components share the same player, a Pinia store is the cleanest way to centralise state and expose actions. Install Pinia first if it is not already in the project:

Shell
npm install pinia
TypeScript
// stores/musicPlayer.ts
import { defineStore } from 'pinia';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AutoAdvancePlugin,
MediaSessionPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
import {
RepeatState,
ShuffleState,
} from '@nomercy-entertainment/nomercy-music-player';
import type { MusicPlaylistItem, NMMusicPlayer } from '@nomercy-entertainment/nomercy-music-player';

function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}

export const useMusicPlayerStore = defineStore('musicPlayer', {
state: () => ({
player: null as NMMusicPlayer | null,
isPlaying: false,
currentTrack: null as MusicPlaylistItem | null,
currentTime: 0,
duration: 0,
volume: 100,
shuffle: ShuffleState.OFF,
repeat: RepeatState.OFF,
}),

getters: {
progress: (state) =>
state.duration > 0 ? (state.currentTime / state.duration) * 100 : 0,
hasNext: (state) => (state.player?.peekNext() !== undefined),
hasPrevious: (state) => (state.player?.peekPrevious() !== undefined),
formattedCurrentTime: (state) => formatTime(state.currentTime),
formattedDuration: (state) => formatTime(state.duration),
artistNames: (state) =>
state.currentTrack?.artist ?? '',
},

actions: {
init(baseUrl: string) {
const player = nmMPlayer('global')
.addPlugin(AutoAdvancePlugin)
.addPlugin(MediaSessionPlugin)
.setup({ baseUrl, playlist: [] });

player.on('current', ({ item }) => {
this.currentTrack = item ?? null;
});
player.on('play', () => { this.isPlaying = true; });
player.on('pause', () => { this.isPlaying = false; });
player.on('ended', () => { this.isPlaying = false; });
player.on('time', ({ time }) => { this.currentTime = time; });
player.on('duration', ({ duration }) => { this.duration = duration; });
player.on('volume', ({ level }) => { this.volume = level; });
player.on('shuffle', ({ state }) => { this.shuffle = state; });
player.on('repeat', ({ state }) => { this.repeat = state; });

this.player = player;
},

togglePlayback() { void this.player?.togglePlayback(); },
next() { void this.player?.next(); },
previous() { void this.player?.previous(); },

seekByPercentage(pct: number) {
this.player?.seekByPercentage(pct);
},

toggleShuffle() {
const next = this.shuffle === ShuffleState.OFF
? ShuffleState.ON
: ShuffleState.OFF;
this.player?.shuffleState(next);
},

cycleRepeat() {
const order = [RepeatState.OFF, RepeatState.ALL, RepeatState.ONE] as const;
const nextMode = order[(order.indexOf(this.repeat) + 1) % order.length];
this.player?.repeatState(nextMode);
},

dispose() {
this.player?.dispose();
this.player = null;
this.$reset();
},
},
});

Wire the store at app startup:

TypeScript
// main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import { useMusicPlayerStore } from '@/stores/musicPlayer';

const app = createApp(App);
app.use(createPinia());
app.mount('#app');

// Init after Pinia is installed so the store is ready.
useMusicPlayerStore().init('https://protected.cdn.your-domain.com');

Progress bar component

A click-to-seek progress bar using seekByPercentage from the store:

Vue
<!-- components/MusicProgressBar.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useMusicPlayerStore } from '@/stores/musicPlayer';

const musicStore = useMusicPlayerStore();
const progressBarRef = ref<HTMLElement>();

const {
progress,
formattedCurrentTime,
formattedDuration,
} = storeToRefs(musicStore);

function handleClick(event: MouseEvent) {
if (!progressBarRef.value) return;
const rect = progressBarRef.value.getBoundingClientRect();
const pct = ((event.clientX - rect.left) / rect.width) * 100;
musicStore.seekByPercentage(Math.max(0, Math.min(100, pct)));
}
</script>

<template>
<div class="progress-section">
<span>{{ formattedCurrentTime }}</span>
<div
ref="progressBarRef"
class="progress-track"
role="slider"
:aria-valuenow="progress"
aria-valuemin="0"
aria-valuemax="100"
tabindex="0"
@click="handleClick"
>
<div class="progress-played" :style="{ width: `${progress}%` }" />
</div>
<span>{{ formattedDuration }}</span>
</div>
</template>

usePlayerControls composable

Extract transport controls into a reusable composable so any component can call them without importing the store directly:

TypeScript
// composables/usePlayerControls.ts
import { storeToRefs } from 'pinia';
import { useMusicPlayerStore } from '@/stores/musicPlayer';

export function usePlayerControls() {
const musicStore = useMusicPlayerStore();

const {
isPlaying,
shuffle,
repeat,
hasNext,
hasPrevious,
} = storeToRefs(musicStore);

return {
isPlaying,
shuffle,
repeat,
hasNext,
hasPrevious,
togglePlayback: () => musicStore.togglePlayback(),
next: () => musicStore.next(),
previous: () => musicStore.previous(),
toggleShuffle: () => musicStore.toggleShuffle(),
cycleRepeat: () => musicStore.cycleRepeat(),
};
}

Use it in any controls component:

Vue
<!-- components/MusicControls.vue -->
<script setup lang="ts">
import { usePlayerControls } from '@/composables/usePlayerControls';
import { RepeatState, ShuffleState } from '@nomercy-entertainment/nomercy-music-player';

const {
isPlaying,
shuffle,
repeat,
hasNext,
hasPrevious,
togglePlayback,
next,
previous,
toggleShuffle,
cycleRepeat,
} = usePlayerControls();
</script>

<template>
<div class="controls">
<button :disabled="!hasPrevious" @click="previous">Prev</button>
<button @click="togglePlayback">{{ isPlaying ? 'Pause' : 'Play' }}</button>
<button :disabled="!hasNext" @click="next">Next</button>

<button
:class="{ active: shuffle === ShuffleState.ON }"
@click="toggleShuffle"
>
Shuffle
</button>

<button
:class="{ active: repeat !== RepeatState.OFF }"
@click="cycleRepeat"
>
{{ repeat === RepeatState.ONE ? 'Repeat one' : 'Repeat' }}
</button>
</div>
</template>

App-level plugin / provide-inject pattern

If you prefer a Vue plugin over Pinia, you can provide the player instance at the app root and inject it in components. This pattern suits small apps where full store infrastructure would be overkill.

TypeScript
// plugins/musicPlayer.ts
import type { App } from 'vue';
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AutoAdvancePlugin,
MediaSessionPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { NMMusicPlayer } from '@nomercy-entertainment/nomercy-music-player';

export const MusicPlayerKey = Symbol('MusicPlayer');

export default {
install(app: App, options: { baseUrl: string }) {
const player = nmMPlayer('global')
.addPlugin(AutoAdvancePlugin)
.addPlugin(MediaSessionPlugin)
.setup({ baseUrl: options.baseUrl, playlist: [] });

app.provide(MusicPlayerKey, player);

// Clean up when the app unmounts.
app.unmount = (function (original) {
return function () {
player.dispose();
return original.call(app);
};
})(app.unmount);
},
};

Register it in main.ts:

TypeScript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import MusicPlayerPlugin, { MusicPlayerKey } from './plugins/musicPlayer';

const app = createApp(App);
app.use(MusicPlayerPlugin, { baseUrl: 'https://protected.cdn.your-domain.com' });
app.mount('#app');

Inject in any component:

TypeScript
import { inject } from 'vue';
import type { NMMusicPlayer } from '@nomercy-entertainment/nomercy-music-player';
import { MusicPlayerKey } from '@/plugins/musicPlayer';

const player = inject<NMMusicPlayer>(MusicPlayerKey)!;

Note: MusicPlayerKey is a typed Symbol. Prefer it over the bare string key from the “Providing the player globally” example above — it prevents key collisions in larger apps.

See also