This library is an upgraded version of Universal Player. The new library has below features:
- Built with Typescript
- Built in Vue 2.7 version (easy to migrate to Vue 3 when needed)
- Using Tailwind 3
- Can be integrated in Vue 2 (requires 2.7 version) and Vue 3 seamlessly
- Plug and play components that can be styled independently
- Treeshakable (get only the components needed and reduce the package size)
- Mindvalley Design System integration (icons, styles, fonts, etc)
- Browser Detection utility
(Note: Currenlty only Audio Player is supported. Video Player will be added later.)
git clone https://github.com/mindvalley/mv-universal-player.git
OR
git clone [email protected]:mindvalley/mv-universal-player.git
yarn install
yarn dev
The components are grouped in below categories. Each category has its own reason to exist. The components are built keeping in mind that they are loosely coupled and higly cohesive at the same time. This pattern allows us to extend the independent components based on requirements.
- Headless
- AudioPlayer
- AudioItem
- Raw
- AudioPlayButton
- AudioFastForwardButton
- AudioRewindButton
- AudioProgressBar
- AudioDescription
- Abstraction
- AudioResource
- MeditationMixer
- Carousel
- Utils
- Browser Detection
- Format Sources
Important: When above components are consumed, they are used with MV prefix. It means, AudioPlayer becomes MVAudioPlayer.
The components under this category play crucial role. Their main purpose is to provide player functionality (play, pause, seek, rewind, fastforward, etc) and leave styling for other components consuming them.
AudioPlayer
It is an instance of the VideoJs without UI. It exposes all the features of the VideoJS along with few custom features. This component can be used independently but it makes more sense when used with AudioItem.
Props
Name | Type | Default | Description |
---|---|---|---|
id | String |
mv-audio-player-3423423534543 |
The unique identifier of a player. The random number at the end is generated dynamically. If the id is passed, then there won't be 'mv-audio-player-' prefix. |
playbackRates | Array<Number> |
[0.5, 1, 2] |
Playback speed. |
loop | Boolean |
false |
Whether the player should continue playing the audio once it has reached the end. |
Usage
<MVAudioPlayer v-slot="{ state, play, pause, setVolume, setCurrentTime, setSources, setAudio, setPlaybackRate, setMixing }">
<!-- Other code -->
</MVAudioPlayer>
You can also reference the element and access all methods and the current state of the player.
<MVAudioPlayer ref="player">
<!-- Other code -->
</MVAudioPlayer>
Although you can use the state and all the actions directly, they are specially meant for AudioItem to consume when used together. E.g, set the audio sources through AudioItem instead of setting it through player.
AudioItem
This component represents the virtual instance of a player. It means, that all the communication to the player (AudioPlayer) happens through this component. It is dependent on AudioPlayer. You cannot use it standalone.
Props
Name | Type | Default | Description |
---|---|---|---|
id | String |
mv-audio-item-3423423534543 |
The unique identifier of an item. The random number at the end is generated dynamically. If the id is passed, then there won't be 'mv-audio-item-' prefix. |
sources | Array<Source> |
[] |
The audio sources that need to be played. Source represents { type?: string, src: string} interface. |
Usage
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources"
id="audioItem1"
@play="emitEvent('play')"
@pause="emitEvent('pause')"
@seeking="emitEvent('seeking', $event)"
@ended="emitEvent('ended', $event)"
@rewind="emitEvent('rewind', $event)"
@fastforward="emitEvent('fastforward', $event)"
@playbackSpeed="emitEvent('playbackSpeed', $event)"
@error="emitEvent('error', $event)"
>
<!-- Other code -->
</MVAudioItem>
</MVAudioPlayer>
Like AudioPlayer, you can also reference the AudioItem and access the state and methods of that particular instance. Here, the state
is different from the player state. This state represents the internal state of an audio item, not the player. It has below properites:
State | Description |
---|---|
currentTime |
The current time of the audio being played. |
playing |
Whether the audio is playing or paused. |
currentPlayingAudioItemId |
This is to know which audio item is active when there are multiple instances of the audioItem. Mainly used in Meditation Mixer context. |
mixing |
To know whether any other audio is also being played along with main audio. Mainly used in Meditation Mixer context. |
volume |
The volume of the sound. Mainly used in Meditation Mixer context. |
As said before, treat AudioItem as a virtual instance, and not the instance of VideoJS.
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources2"
id="audioItem2">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources3"
id="audioItem3">
<!-- Other code -->
</MVAudioItem>
</MVAudioPlayer>
In the above example, you can see that we have multiple AudioItem
instances. A good analogy to represent above structure is a Spotify player. There can be multiple song items in the list, but when you select any song, at a time only one song is being played. This means current playing song will reset and new song will start from the begining.
This also means there will be only one VideoJS instance (through AudioPlayer) and AudioItem is there just to read the state and call methods of AudioPlayer. If you want multiple VideoJS instances then you can do as shown below:
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources2"
id="audioItem2">
<!-- Other code -->
</MVAudioItem>
</MVAudioPlayer>
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources2"
id="audioItem2">
<!-- Other code -->
</MVAudioItem>
</MVAudioPlayer>
In above scenario, you can practically play two audios together. But any one audio from first player and any other from second player.
One interesting thing to note about these two components is that they can be nested inside one another.
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources2"
id="audioItem2">
<!-- Other code -->
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioItem
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources2"
id="audioItem2">
<!-- Other code -->
</MVAudioItem>
</MVAudioPlayer>
</MVAudioItem>
</MVAudioPlayer>
The above structure is useful in Meditation Mixer context where one audio can be mixed with another. Now, when we say that all the components are loosely coupled and higly cohesive, what we mean is that you can place AudioPlayer
anywhere in the application and have AudioItem
somewhere deeply nested in one of the components. AudioItem
doesn't need to be the immediate child of AudioPlayer
.
<MVAudioPlayer> <---- in App.vue component
<!-- many other components in between -->
<MVAudioItem <--- in some child component
v-slot="{ state, seek, play, pause, rewind, fastForward }"
:sources="sources1"
id="audioItem1">
<!-- Other code -->
</MVAudioItem>
<MVAudioPlayer>
Above structure is useful when you want only one player across the app.
The components under this category can be used independently without the dependency of Headless components. In other words, they are pure functional components that work on props and produce events. They have their own css styling and integration of MV Design System, but the styles can be overriden using the exposed classes.
AudioPlayButton
A play button for the player. Though it is named as 'play' button, it will also show pause icon when the audio is in paused state.
Props
Name | Type | Default | Description |
---|---|---|---|
playing | Boolean |
false |
Whether the audio is being played or not. When true , it will show pause icon and when false it will show play icon. |
Events
Name | Payload | Description |
---|---|---|
play | `` | It will emit when the play icon is clicked. |
pause | `` | It will emit when the pause icon is clicked. |
Usage
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, play, pause }"
:sources="sources"
id="audioItem1"
>
<MVAudioPlayButton @play="play" @pause="pause" :playing="state?.playing" />
</MVAudioItem>
</MVAudioPlayer>
AudioFastForwardButton
A fast-forward button for the player.
Props
Name | Type | Default | Description |
---|---|---|---|
seconds | Number |
15 |
The seconds by which the audio should be moved forward. |
tooltipText | String |
-Sec Forward |
The tooltip to be shown when user hovers the cursor. So the end result combining seconds and tooltipText will be 15-Sec Forward . Note: The tooltip won't be shown on mobile devices. |
Events
Name | Payload | Description |
---|---|---|
fastForward | 15 |
The seconds by which the audio was moved forward. The number of seconds would be the same that was passed in seconds prop. |
Usage
<MVAudioPlayer>
<MVAudioItem
v-slot="{ fastForward }"
:sources="sources"
id="audioItem1"
>
<MVAudioFastForwardButton @fastForward="fastForward($event)" />
</MVAudioItem>
</MVAudioPlayer>
AudioRewindButton
A rewind button for the player.
Props
Name | Type | Default | Description |
---|---|---|---|
seconds | Number |
15 |
The seconds by which the audio should be rewinded. |
tooltipText | String |
-Sec Rewind |
The tooltip to be shown when user hovers the cursor. So the end result combining seconds and tooltipText will be 15-Sec Rewind . Note: The tooltip won't be shown on mobile devices. |
Events
Name | Payload | Description |
---|---|---|
rewind | 15 |
The seconds by which the audio was rewinded. The number of seconds would be the same that was passed in seconds prop. |
Usage
<MVAudioPlayer>
<MVAudioItem
v-slot="{ rewind }"
:sources="sources"
id="audioItem1"
>
<MVAudioRewindButton @rewind="rewind($event)" />
</MVAudioItem>
</MVAudioPlayer>
AudioProgressBar
The progress bar showing the current status of an audio. User can also seek the audio.
Props
Name | Type | Default | Description |
---|---|---|---|
duration | Number |
0 |
It is required. It is the total seconds of an audio. The seconds will be converted to humanized format by the component. |
currentTime | Number |
0 |
It is required. It represents where the audio is currently. |
Events
Name | Payload | Description |
---|---|---|
seek | 0 |
It will emit the current time (where the user clicked) of the audio. |
Usage
<MVAudioPlayer>
<MVAudioItem
v-slot="{ state, seek }"
:sources="sources"
id="audioItem1"
>
<MVAudioProgressBar
:duration="props.duration"
class="text-white"
:current-time="state?.currentTime"
@seek="seek"
/>
</MVAudioItem>
</MVAudioPlayer>
AudioDescription
The progress bar showing the current status of an audio. User can also seek the audio.
Props
Name | Type | Default | Description |
---|---|---|---|
imageSrc | String |
`` | It is required. The image path. E.g, artist's image |
name | String |
`` | It is required. E.g, artist's name |
headline | String |
`` | It is required. E.g, some tagline for the audio |
description | String |
`` | It is required. E.g, some description for the audio. If the the length of the description is <=250, it will show 'Show More' text for user to expand else 'Show Less' to collapse the text . |
showMoreText | String |
`` | It is required. E.g, 'Show More' when the text is collapsed. |
showLessText | String |
`` | It is required. E.g, 'Show Less' when the text is expanded. |
Usage
<MVAudioPlayer>
<MVAudioDescription
:image-src="section?.media?.author?.portraitAsset?.url"
:name="section?.media?.author?.name"
:headline="section?.media?.author?.headline"
:description="section?.media?.description"
show-more-text="Show More"
show-less-text="Show Less"
class="pt-8"
/>
</MVAudioPlayer>
Headless and Raw components are enough to serve the needs of an audio player. But if you want to use readymade component which clubs all Headless and Raw components so that it is easy to integrate in the consuming application, you can use components under this category. It is most likely that you will use these components.
AudioResource
You can think of this component as an extension to AudioItem component because it encompasses all the necessary headless and raw components to provide good user experience. It manages the responsiveness of the player and allows you to inject MeditationMixer and AudioDescription components through '#meditation-mixer' and '#audio-description' slots respectively.
Props
Name | Type | Default | Description |
---|---|---|---|
assetID | String |
'' | It is required. The unique identifier of the resource (AudioItem). The assetID passed will be used to further pass it down to AudioItem and will also be used while emitting events so that user knows which AudioItem (virtual instance of a player) has emitted the event. |
sources | Array<Source> |
[] |
It is required. The audio sources that need to be played. Source represents { type?: string, src: string} interface. The sources are passed down to AudioItem. |
title | String |
`` | Audio title. |
artistName | String |
`` | Artist's name. |
duration | Number |
0 |
Total duration of the sound in seconds. It is passed down to AudioProgressBar. |
posterUrl | String |
`` | Poster of the audio. It is also used to give blur effect in the background. |
ratings | Number |
0 |
Ratings of the audio. |
totalRatings | Number |
0 |
Total ratings of the audio. E.g, 3/5 (here 3 is ratings and 5 is totalRatings) |
overlay | Boolean |
false |
Overlay effect in the background. |
blurEffect | Boolean |
false |
Blur effect in the background. |
showFavourite | Boolean |
false |
Whether to show favourite (heart icon) on the top right corner. |
isFavourite | Boolean |
false |
To show whether the audio is favourite or not. It is dependend on showFavourite . |
Events
Name | Payload | Description |
---|---|---|
timeupdate | {assetId: '...', currentTime: '...'} |
The current time of the audio being played. The currentTime will be in seconds. |
play | {assetId: '...'} |
It will emit when the audio has started to play. |
pause | {assetId: '...'} |
It will emit when the audio has paused. |
seeking | {assetId: '...', seeking: '...'} |
It will emit when the audio is seeked through progress bar. The seeking will be in seconds. |
ended | {assetId: '...'} |
It will emit the audio has ended. |
rewind | {assetId: '...', {previousTime: '...', currentTime: '...'}} |
It will emit when the audio is rewinded. |
fastforward | {assetId: '...', {previousTime: '...', currentTime: '...'}} |
It will emit when the audio is fast forwarded. |
Usage
<MVAudioPlayer>
<MVAudioResource
v-for="section in filteredSections"
:key="section?.media?.id"
:asset-id="section?.media?.id"
:sources="formatSources(section?.media?.mediaAsset?.renditions)"
:duration="section?.media?.mediaAsset.duration"
:poster-url="section?.media?.coverAsset?.url"
:title="section?.media?.title"
:artist-id="section?.media?.author.id"
:artist-name="section?.media?.author.name"
:ratings="section?.media?.averageRating"
:total-ratings="section?.media?.ratingsCount"
class="my-10 mx-auto inset-0 z-15 relative overflow-hidden p-6 lg:p-8 rounded-3xl bg-cover bg-center"
:style="`background-image: url(${section?.media?.coverAsset?.url});`"
blurEffect
overlay
showFavourite
@play="logEvent('play', $event)"
@pause="logEvent('pause', $event)"
@seeking="logEvent('seeking', $event)"
@ended="logEvent('ended', $event)"
@rewind="logEvent('rewind', $event)"
@fastforward="logEvent('fastforward', $event)"
@playbackSpeed="logEvent('playbackSpeed', $event)"
@favourite="logEvent('favourite', $event)"
@error="logEvent('error', $event)"
>
<!-- Other code-->
</MVAudioResource>
</MVAudioPlayer>
MeditationMixer
This is an optional component which you can use to mix sounds. It is a wrapper of an AudioPlayer component. Every usage of MeditationMixer will create new instance of VideoJS. This component cannot be used without AudioResource.
It accepts MeditationTrackItem
(which is nothing but again a wrapper of an AudioItem) and MeditationVolumeSlider
components as children components to provide mixing feature. It is important to note that both MeditationTrackItem
and MeditationVolumeSlider
cannot be used without MeditionMixer
component.
MeditationTrackItem Props
Name | Type | Default | Description |
---|---|---|---|
id | String |
mv-meditation-track-item-3423423534543 |
The unique identifier of the item. The random number at the end is generated dynamically. If the id is passed, then there won't be 'mv-meditation-track-item-' prefix. |
sources | Array<Source> |
[] |
The audio sources that need to be played. Source represents { type?: string, src: string} interface. If you don't pass the sources, it will return 'NO BG SOUND' item by default. |
isActive | Boolean |
false |
Whether current sound is active or not. It is mainly used to show selection of a background sound with blue border. It doesn't mean the audio (background sound) is being played. The audio is in play/pause state based on main audio's state. |
backgroundSrc | String |
`` | Background image for the sound. |
volume | Number |
0.5 |
Default volume of the sound. Rest of the volume would be of main audio. |
MeditationVolumeSlider Props
Name | Type | Default | Description |
---|---|---|---|
volume | Number |
0.5 |
The default indicator. |
min | Number |
0 |
Minimum volume the user can set. |
max | Number |
0 |
Maximum volume the user can set. |
step | Number |
0.01 |
The step by which the volume should be increased or decreased. |
leftText | String |
sound |
The text to be shown on the left hand side of the slider. |
rightText | String |
vocal |
The text to be shown on the right hand side of the slider. |
Usage
<MVAudioResource
v-for="section in filteredSections"
:key="section?.media?.id"
:asset-id="section?.media?.id"
:sources="formatSources(section?.media?.mediaAsset?.renditions)"
:duration="section?.media?.mediaAsset.duration"
:poster-url="section?.media?.coverAsset?.url"
:title="section?.media?.title"
:artist-id="section?.media?.author.id"
:artist-name="section?.media?.author.name"
:ratings="section?.media?.averageRating"
:total-ratings="section?.media?.ratingsCount"
:style="`background-image: url(${section?.media?.coverAsset?.url});`"
>
<template #meditation-mixer>
<MVMeditationMixer>
<div class="text-cool-grey-350 mb-2 text-xs">Mix Track</div>
<div class="gap-x-2 px-6">
<MVCarousel tagName="Slide">
<MVCarouselSlide :key="0">
<MVMeditationTrackItem :volume="0"></MVMeditationTrackItem>
</MVCarouselSlide>
<MVCarouselSlide v-for="(sound, index) in backgroundSounds" :key="index + 1">
<MVMeditationTrackItem
:sources="formatSources(sound?.item?.mediaAsset.renditions)"
:background-src="sound?.item?.coverAsset?.thumbnailUrl"
@play="logEvent('play', $event)"
@pause="logEvent('pause', $event)"
@timeupdate="logEvent('timeupdate', $event)"
@error="logEvent('error', $event)"
/>
</MVCarouselSlide>
</MVCarousel>
</div>
<div
class="flex w-full mt-4 items-center justify-center transition duration-300 ease-in"
>
<MVMeditationVolumeSlider leftText="sound" rightText="vocal" />
</div>
</MVMeditationMixer>
</template>
</MVAudioResource>
MVCarousel
This component uses Vue Carousel under the hood. Currently it is configured (e.g number of slides per breakpoint) keeping MeditationMixer in mind, but you can also use it elsewhere.
Props
Name | Type | Default | Description |
---|---|---|---|
tagName | String |
MVCarouselSlide |
This is to indicate what tag would act as a child component. This is nothing but to identify the slide component which Vue Carousel uses to count the no. of slides and react to responsiveness. If you are using whole mv-universal-plaer package (not individual components), then you don't need to pass the prop, but if you are using individual components (for tree-shaking) then you need to pass Slide as a prop. |
The component for the slide is Slide
. E.g, you can wrap the MeditationTrackItem in the Slide
component.
Usage
<MVMeditationMixer>
<div class="text-cool-grey-350 mb-2 text-xs">Mix Track</div>
<div class="gap-x-2 px-6">
<MVCarousel tagName="Slide">
<MVCarouselSlide :key="0">
<MVMeditationTrackItem :volume="0"></MVMeditationTrackItem>
</MVCarouselSlide>
<MVCarouselSlide v-for="(sound, index) in backgroundSounds" :key="index + 1">
<MVMeditationTrackItem
:sources="formatSources(sound?.item?.mediaAsset.renditions)"
:background-src="sound?.item?.coverAsset?.thumbnailUrl"
/>
</MVCarouselSlide>
</MVCarousel>
</div>
</MVMeditationMixer>
The components under this category are helpers.
useDetectBrowser
This composable is meant to identify what browser and device the user is using to access the application. For e.g, we can use it to hide MeditationVolumeSlider when the user is using iOS and Safari browser because iOS doesn't allow the user to control the volume programmatically other than physical buttons.
It exposes below properties:
- isiPhoneOriPadSafari
- isiPhone
- isiPad
- isTouchDevice
Usage:
import { useDetectBrowser } from "@mindvalley/mv-universal-player";
const { isiPhone, isiPad, isiPhoneOriPadSafari } = useDetectBrowser();
useGlobal
This composable clubs generic utitlity features. For example, it includes formatSources method to help format and priotize .hls (for video) and .mp4a (for audio) sources. So, let's say you pass a list of renditions which has .mp4a as source, then this will be the only source that would be considered. If not, then rest of the sources will be considered as fallback.
Usage:
import { useGlobal } from "@mindvalley/mv-universal-player";
const { formatSources } = useGlobal();
formatSources accepts two parameters:
Name | Type | Default | Description |
---|---|---|---|
sources | [{id: string, contentType: string, url: string}] | [] | The list of sources which contains renditions. |
isAudio | boolean | true | It is to identify whether the sources are of audio or video. For audio '.mp4a' is considered as priority and for video, .hls. |
Vue Version Support
The package uses Vue Demi library to support Vue 2.7 and Vue 3 versions. So, all the Vue components are imported from vue-demi
and not vue
.
Types
The types for each components are generated using vite-plugin-dts
library to enable TypeScript support.
Vite
For compatibility of Vue 2 with Vite, @vitejs/plugin-vue2
library is used.
Treeshaking
This option can be enabled/disabled using preserveModules
option in vite.config.ts
file. Setting true
enables treeshaking.
Build Package
yarn build
Publish Package
Right now the package is published on GitHub Package Registry. In future, if the package needs to be uploaded on NPM registry then update below url in package.json
file.
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
Publish the package using below command. Ensure that the version
number in package.json is incremented every time.
yarn library:publish
Tar Ball
If you just want to build the tar ball, you can use below command.
yarn library:pack
Build/Package/Publish
If you want to build the package, create tar ball and also publish the package, you can use below command.
yarn build:library
This package can be downloaded using usual package installation approach or through git url.
yarn add @mindvalley/mv-universal-player
Note: To download the package for Vue 2, download the package without '-next' appended. For example:
- 1.0.5 (for Vue 2)
- 1.0.5-next (for Vue 3)
IMPORTANT: In package.json, do NOT set the caret symbol in front of the version. Have exact version else it will download the latest version which might be of any version.
Whole Package
This will register all the components of the library globally and then you can use them in any of the internal components without importing them explicitly.
import MVUniversalPlayer from "@mindvalley/mv-universal-player";
import "@mindvalley/mv-universal-player/dist/style.css";
Vue.use(MVUniversalPlayer);
Individual Components (tree-shaking)
If you want to use only specific components to minimize the bundle size, it is suggested you use this approach.
import "@mindvalley/mv-universal-player/dist/style.css";
import {
useDetectBrowser,
MVAudioPlayer,
MVAudioResource,
MVMeditationMixer,
MVMeditationVolumeSlider,
MVMeditationTrackItem,
MVCarousel,
MVCarouselSlide,
MVAudioDescription
} from "@mindvalley/mv-universal-player";
Note: If you are already using vue-sprite
package for Mindvalley Design System icons, it is recommened you go with inidvidual components approach to reduce the package size.
Though most of the times you might use the player in VueJS environment (Vue 2 or Vue 3), you can also integrate the library in Phoenix LiveView. Let's go step by step.
1. Install Vue
Because the library is built in Vue, it needs to have Vue runtime. It can be Vue 2 (2.7) or Vue 3+.
2. Add Universal Player
Use either of the commands based on the Vue environment.
For Vue 2.7
yarn add @mindvalley/[email protected]
For Vue 3+
yarn add @mindvalley/[email protected]
3. Add Host Element
We need an HTML element to bin our universal player.
<div id="mv-universal-player" phx-hook="MVUniversalPlayer" phx-update="ignore">
</div>
The above element is where our universal player will be hosted dynamically. You can refer JS Hooks guide to know more about this.
We have added phx-update='ignore'
to prevent LiveView from managing this element whenever the liveview state changes. This is because the player manages its own state.
4. Create JS Hook
To bind our player we need to create JS Hook and pass it to LiveSocket constructor. So, let's create a new file and place below code in it. We are naming it sample.js
and then you can import it in another .js
file if required.
import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import Vue from "vue";
import "@mindvalley/mv-universal-player/dist/style.css";
import { MVAudioPlayer, MVAudioResource } from "@mindvalley/mv-universal-player";
import MVUniversalPlayer from "@mindvalley/mv-universal-player";
let Hooks = {};
Hooks.MVUniversalPlayer = {
mounted() {
this.pushEvent("load_data", {}, (reply, ref) => {
this.mvUniversalPlayer = mount(this.el.id, {
resource: reply.data.resource,
});
});
},
};
export function mount(id, opts) {
const player = document.getElementById(id);
const { resource } = opts;
const data = { resource: resource };
// If your code already has @mindvalley/design-system integrated, you can remove below line.
Vue.use(MVUniversalPlayer);
new Vue({
el: player,
components: { MVAudioPlayer, MVAudioResource },
data: data,
methods: {
formatSources(localSources = []) {
const audioSources = localSources?.filter(
(source) =>
source.id === "mp3" || source.id === "ogg" || source.id === "hls"
);
const updatedSources = [];
for (const i in audioSources) {
updatedSources.push({
type: localSources[i]?.contentType,
src: localSources[i]?.url,
});
}
return updatedSources;
},
},
template: `
<div class="mv-universal-player">
<MVAudioPlayer>
<MVAudioResource
:key="resource.assetId"
:asset-id="resource.assetId"
:sources="formatSources(resource.mediaAsset.renditions)"
:duration="resource.mediaAsset.duration"
:poster-url="resource.coverAsset.url"
:title="resource.title"
:artist-id="resource.author.id"
:artist-name="resource.author.name"
:ratings="resource.averageRating"
:total-ratings="resource.ratingsCount"
class="my-10 mx-auto inset-0 z-15 relative overflow-hidden p-6 lg:p-8 rounded-3xl bg-cover bg-center"
:style="{
'background-image': 'url(' + resource.coverAsset.url + ')'
}"
blurEffect
overlay
showFavourite
>
<!-- Other code-->
</MVAudioResource>
</MVAudioPlayer>
</div>`,
});
}
let csrfToken = document
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
params: {
_csrf_token: csrfToken,
},
});
liveSocket.connect();
You can split above code as per the needs. The above code will:
- mount the hook to the host element
- call
mounted
callback - send an event to LiveView to fetch the initial data
- pass the data to mount() which will further mount the Vue instance with the player attached to it to the host element
You should create matching event in LiveView.
def handle_event("load_data", _, socket) do
data = %{resource: socket.assigns.resource}
{:reply, %{data: data}, socket}
end
To develop components in isolation, we have integrated Storybook. Running below command will open up Storybook in your browser.
yarn storybook
To let other people review our components, we need to build and then deploy it on Chromatic.
yarn build-storybook
Replace TOKEN with Chromatic token.
npx chromatic --project-token=<TOKEN>
To access Chromatic, ask your colleague to add you.