Audio

Five tracks with three qualities each.
Download button is hidden.
The availability to change the playback speed is removed.
Previous and Next buttons tooltips and states are updated according to the current track.

HTML

<div class="audio-playlist-wrapper"> <div class="playlist-menu"> <!-- A beautiful icon --> <svg width="40" height="44" viewBox="0 0 100 110"> <ellipse cx="14.14" cy="98.26" rx="14.52" ry="11.26" transform="rotate(-21.12 14.15 98.275)"/> <rect x="24.52" y="18" width="3.77" height="79.4" rx="1"/> <ellipse cx="85.86" cy="84.92" rx="14.52" ry="11.26" transform="rotate(-21.12 85.883 84.921)"/> <rect x="96.23" y="4.66" width="3.77" height="79.4" rx="1"/> <path d="M100 15.98L24.52 33.59V17.02L100 0v15.98z"/> <rect x="46.91" y="37.46" width="28" height="6.49" rx="3.25"/> <rect x="46.91" y="49.55" width="28" height="6.49" rx="3.25"/> <rect x="46.91" y="61.64" width="28" height="6.49" rx="3.25"/> </svg> <ul><!-- The track list will be build by javascript --></ul> </div> <div class="audio-container"> <audio><!-- Our audio container --></audio> <div class="track-title"><!-- Will display the current track information --></div> </div> </div>

SCSS

//For the curious ! $bodycolor: #c9be9f; $red: #a10304; $orange: #cb852a; .audio-playlist-wrapper { display: flex; flex-direction: column; margin-top: 40px; .playlist-menu { display: flex; flex-wrap: wrap; flex-direction: row; svg { width: 20px; height: 30px; fill:$red; transition: fill 0.4s; margin-top: auto; margin-bottom: auto; margin-right: 10px; @media screen and (min-width: 450px) { width: 40px; height: 44px; margin-right: 20px; } } ul { overflow: hidden; transition: width 0.4s; li{ padding-bottom: 5px; span{ position: relative; cursor: pointer; } span:after{ content: ''; display: block; position: absolute; height: 1px; width: 0; left: 0; bottom: -2px; background: $bodycolor; transition: width 0.2s; } &.active{ color: $orange; span:before{ content:''; display: inline-block; } &:not(.playing) span:before{ height : 0; width : 0; border-top : 0.4em solid transparent; border-bottom : 0.4em solid transparent; border-left : 0.7em solid $orange; margin-right: 3px; } &.playing span:before{ width: 2px; height: 0.8em; box-shadow: 0.4em 0 0 0 $orange; background: $orange; margin-right: 10px; } span:after { width: 100%; background: $orange; } } @media screen and (hover:hover) { &:hover span:after{ width: 100%; } } } } } .audio-container { margin-top: 20px; position: relative; .amst__wrapper { border: 1px solid #383838; border-radius: 5px; box-shadow: 2px 2px 3px #383838; } .track-title{ position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 150%); padding-top: 5px; text-align: center; font-size: 0.9rem; } } } @mixin large-screens{ .audio-playlist-wrapper { flex-direction: row; align-items: center; .playlist-menu { cursor: pointer; flex-wrap: nowrap; padding: 20px 0; svg{ fill: $orange; } ul{ width: 0px; li{ white-space: nowrap; } } &:hover{ svg{ fill: $red; } ul { width: 320px; } } } .audio-container { flex-grow: 1; margin: auto; } } } body.menu-is-closed { @media screen and (hover: hover) and (min-width: 750px) { @include large-screens; } } body:not(.menu-is-closed) { @media screen and (hover: hover) and (min-width: 970px) { @include large-screens; } } body.ie{ @include large-screens; }

JAVASCRIPT

//Redefine the class options for audio and video players AmstramgramMediaPlayer.options = { //Next and previous buttons are hidden by default so we show them. next: { hidden: false }, previous: { hidden: false }, } /** * For the audio player, we hide the download button * and reset the playbackRates array of the settings panel * because we don't want to give the user the ability to change playback speed. * (by default : * playbackRates: [['0.25 x', 0.25], ['0.5 x', 0.5], ['0.75 x', 0.75], ['Normal', 1], ['1.5 x', 1.5], ['2 x', 2], ['4 x', 4]] * ) */ AmstramgramMediaPlayer.audioOptions = { download: { hidden: true },//Download button is visible by default settings: { playbackRates: [] }, } const audioWrapper = document.querySelector('.audio-playlist-wrapper'), audioPlaylist = [ { artist: 'Camille', title: 'Seeds', duration: 171, src: [ { src: 'assets/audio/Camille-Seeds-128.mp3', quality: 'MP3 128K', type: 'audio/mpeg', }, { src: 'assets/audio/Camille-Seeds-320.mp3', quality: 'MP3 320K', type: 'audio/mpeg', }, { src: 'assets/audio/Camille-Seeds.wav', quality: 'WAV', type: 'audio/wav', }, ] }, { artist: 'Claude Nougaro', title: "Déjeuner sur l'herbe", duration: 257, src: [ { src: "https://amp.onfaitdessites.fr/assets/audio/Claude_Nougaro-Dejeuner_sur_l'herbe-128.mp3", quality: 'MP3 128K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Claude_Nougaro-Dejeuner_sur_l'herbe-320.mp3", quality: 'MP3 320K', quality: 'MP3 128K', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Claude_Nougaro-Dejeuner_sur_l'herbe.wav", quality: 'WAV', type: 'audio/wav', }, ] }, { artist: 'Jacques Higelin', title: "Ici, c'est l'enfer", duration: 254, src: [ { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Higelin-Ici_c'est_l'enfer-128.mp3", quality: 'MP3 128K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Higelin-Ici_c'est_l'enfer-320.mp3", quality: 'MP3 320K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Higelin-Ici_c'est_l'enfer.wav", quality: 'WAV', type: 'audio/wav', }, ] }, { artist: 'Olivier Marguerit', title: "Plonge dans l'eau", duration: 222, src: [ { src: "https://amp.onfaitdessites.fr/assets/audio/Olivier_Marguerit-Plonge_dans_l'eau-128.mp3", quality: 'MP3 128K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Olivier_Marguerit-Plonge_dans_l'eau-320.mp3", quality: 'MP3 320K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Olivier_Marguerit-Plonge_dans_l'eau.wav", quality: 'WAV', type: 'audio/wav', }, ] }, { artist: 'Jacques Brel', title: "La ville s'endormait", duration: 254, src: [ { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Brel-La_ville_s'endormait-128.mp3", quality: 'MP3 128K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Brel-La_ville_s'endormait-320.mp3", quality: 'MP3 320K', type: 'audio/mpeg', }, { src: "https://amp.onfaitdessites.fr/assets/audio/Jacques_Brel-La_ville_s'endormait.wav", quality: 'WAV', type: 'audio/wav', }, ] }, ] //Building the menu track list let ulContent = '' audioPlaylist.forEach(el => { ulContent += `<li><span class="notranslate"><b>${el.artist}</b> - ${el.title}</span></li>` }) audioWrapper.querySelector('ul').innerHTML = ulContent let audioCurrentId//Store the current track id //Initializing the audio player const audioPlayer = new AmstramgramMediaPlayer( audioWrapper.querySelector('audio'),//target of the instance {},//No parameter function () {//Function call after player initialization //this = player instance this //listening to the play and pause events dispatched by the player //to update the track list menu .on('amst__play', _ => audioWrapper.querySelectorAll('li')[audioCurrentId].classList.add('playing')) .on('amst__pause', _ => audioWrapper.querySelectorAll('li')[audioCurrentId].classList.remove('playing')) //Change the source when click on Next or Previous button .on('amst__next-click', _ => setAudioSource(audioCurrentId + 1)) .on('amst__previous-click', _ => setAudioSource(audioCurrentId - 1)) //When a track ends, play the next one if there is one this.params.media.addEventListener('ended', _ => { if (audioCurrentId + 1 < audioPlaylist.length) { setAudioSource(audioCurrentId + 1) this.play() } }) //Set the first element of our audioPlaylist array as player primary source setAudioSource(0) } ) //Listening to the click event on the track list menu <li> items Array.from(audioWrapper.querySelectorAll('li')).forEach((li, id) => { li.addEventListener('click', _ => { if (li.classList.contains('active')) { //If the click occurs on active line, toggle the playback audioPlayer.toggle() } else { //else, set the src according to the line id setAudioSource(id) //and force the playback to start if (audioPlayer.paused) audioPlayer.play() } }) }) function setAudioSource(id) { if (id >= 0 && id < audioPlaylist.length && id != audioCurrentId) { //On first call, no line is active //If needed, reset the current active line if (audioWrapper.querySelector('li.active')) audioWrapper.querySelector('li.active').removeAttribute('class') audioWrapper.querySelectorAll('li')[id].classList.add('active') //Store the playback state const paused = audioPlayer.paused //Set the src with all relevant parameters audioPlayer.src = { src: audioPlaylist[id].src, duration: audioPlaylist[id].duration, previous: { label: (id == 0) ? 'Nothing' : audioPlaylist[id - 1].title, disabled: (id == 0) }, next: { label: (id == audioPlaylist.length - 1) ? 'Nothing' : audioPlaylist[id + 1].title, disabled: (id == audioPlaylist.length - 1) }, } //Update the current track id audioCurrentId = id //Update the track info audioWrapper.querySelector('.track-title').innerHTML = audioWrapper.querySelector('li.active').innerHTML //If the player was playing, start the playback if (!paused) audioPlayer.play() } }

Video

    All videos courtesy of my dear friend Jean-Baptiste Lévêque.
    More informations here or here...

    Four videos with four qualities each.
    Previous and Next buttons tooltips and states are updated according to the current track.
    First video comes with english and french subtitles. Unless your browser language is french, the default subtitles are in english.
    For the second video, Download button is disabled.
    The third doesn't get thumbnails.
    The fifth item, labelled Error, will throw an intentional error, just because there is no valid video associated to it.

    HTML

    <div class="video-playlist-wrapper"> <div class="playlist-menu"> <ul><!-- The track list will be build by javascript --></ul> </div> <div class="video-container"> <video><!-- Our video container --></video> <div class="track-title"><!-- Will display the current video information --></div> </div> </div>

    SCSS

    //For the curious ! $bodycolor: #c9be9f; $red: #a10304; $orange: #cb852a; .video-playlist-wrapper { display: flex; flex-direction: column; margin-top: 40px; .playlist-menu { display: flex; flex-wrap: wrap; flex-direction: row; ul { width: 160px; overflow: hidden; font-weight: bold; li{ padding-bottom: 5px; span{ position: relative; cursor: pointer; } span:after{ content: ''; display: block; position: absolute; height: 1px; width: 0; left: 0; bottom: -2px; background: $bodycolor; transition: width 0.2s; } &.active{ color: $orange; span:before{ content:''; display: inline-block; } &:not(.playing) span:before{ height : 0; width : 0; border-top : 0.4em solid transparent; border-bottom : 0.4em solid transparent; border-left : 0.7em solid $orange; margin-right: 3px; } &.playing span:before{ width: 2px; height: 0.8em; box-shadow: 0.4em 0 0 0 $orange; background: $orange; margin-right: 10px; } span:after { width: 100%; background: $orange; } } @media screen and (hover:hover) { &:hover span:after{ width: 100%; } } } } } .video-container { margin-top: 20px; position: relative; .track-title{ position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 150%); padding-top: 5px; text-align: center; font-size: 0.9rem; font-weight: bold; } } } @mixin large-screens{ .video-playlist-wrapper{ flex-direction: row; align-items: center; .video-container { flex-grow: 1; margin: auto; } } } body.menu-is-closed { @media screen and (hover: hover) and (min-width: 750px) { @include large-screens; } } body:not(.menu-is-closed) { @media screen and (hover: hover) and (min-width: 970px) { @include large-screens; } } body.ie{ @include large-screens; }

    JAVASCRIPT

    //Redefine the class options for audio and video players AmstramgramMediaPlayer.options = { //Next and previous buttons are hidden by default so we show them. next: { hidden: false }, previous: { hidden: false }, } //Custom the error message //and set the same poster for all the videos AmstramgramMediaPlayer.videoOptions = { errorMessage: "First video will play in 2 seconds", poster: 'assets/video/PosterPlaylist.jpg', } const videoWrapper = document.querySelector('.video-playlist-wrapper'), videoPlaylist = [ { title: 'Les castors', src: {//An object containing all the video parameters duration: 210, src: [ { src: 'assets/video/Castors-360p.mp4', quality: '360p' }, { src: 'assets/video/Castors-480p.mp4', quality: '480p' }, { src: 'assets/video/Castors-720p.mp4', quality: '720p', default: true }, { src: 'assets/video/Castors-1080p.mp4', quality: '1080p' }, ], thumbnails: { src: 'assets/video/Castors-thumbnails.jpg', int: 2.12 }, subtitles: { sources: [ { src: 'assets/video/Castors-fr.vtt', label: 'Français', srclang: 'fr', }, { src: 'assets/video/Castors-en.vtt', label: 'English', srclang: 'en', }, ], default: 'en' } } }, { title: 'France', src: { duration: 327, src: [ { src: 'assets/video/France-360p.mp4', quality: '360p' }, { src: 'assets/video/France-480p.mp4', quality: '480p' }, { src: 'assets/video/France-720p.mp4', quality: '720p', }, { src: 'assets/video/France-1080p.mp4', quality: '1080p' }, ], thumbnails: { src: 'assets/video/France-thumbnails.jpg', int: 3.28 }, download: {disabled: true}, } }, { title: 'Joyeuses Pâques', src: { duration: 40, format: 4 / 3,//Yep ! It's an old one ! src: [ { src: 'assets/video/Joyeuses_Paques-360p.mp4', quality: '360p' }, { src: 'assets/video/Joyeuses_Paques-480p.mp4', quality: '480p' }, { src: 'assets/video/Joyeuses_Paques-720p.mp4', quality: '720p', }, { src: 'assets/video/Joyeuses_Paques-1080p.mp4', quality: '1080p' }, ], } }, { title: 'Le plouc', src: { duration: 186, src: [ { src: 'assets/video/Le_plouc-360p.mp4', quality: '360p' }, { src: 'assets/video/Le_plouc-480p.mp4', quality: '480p' }, { src: 'assets/video/Le_plouc-720p.mp4', quality: '720p', }, { src: 'assets/video/Le_plouc-1080p.mp4', quality: '1080p' }, ], thumbnails: { src: 'assets/video/Le_plouc-thumbnails.jpg', int: 1.88 }, } }, { title: 'Error', src: { duration: 186, src: 'assets/video/noFile.mp4', } }, ] //Building the menu track list ulContent = '' videoPlaylist.forEach(el => { ulContent += `<li><span class="notranslate">${el.title}</span></li>` }) videoWrapper.querySelector('ul').innerHTML = ulContent let videoCurrentId,//Store the current track id errorTimeOut //Initializing the video player const videoPlayer = new AmstramgramMediaPlayer( videoWrapper.querySelector('video'),//target of the instance { volumeHorizontal: false,//Set a vertical volume slider volumeGroup: 1,//Volume will not be linked to other players volume }, function () {//Function call after player initialization //this = player instance this //listening to the play and pause events dispatched by the player //to update the track list menu .on('amst__play', _ => { clearTimeout(errorTimeOut) videoWrapper.querySelectorAll('li')[videoCurrentId].classList.add('playing') console.log('Player plays !') }) .on('amst__pause', _ => { videoWrapper.querySelectorAll('li')[videoCurrentId].classList.remove('playing') console.log('Player pauses !') }) .on('amst__error', _ => { //Should be dispatched when clicking on the Error line console.log("Media file can't be found !!!") //Launch the first video after 2 seconds errorTimeOut = setTimeout(_ => {setVideoSource(0); this.play()}, 2000) }) //Change the source when click on Next or Previous button .on('amst__previous-click', _ => setVideoSource(videoCurrentId - 1)) .on('amst__next-click', _ => setVideoSource(videoCurrentId + 1)) //Log buttons clicked events .on('amst__previous-click amst__playPause-click amst__next-click amst__mute-click amst__subtitles-click amst__fullscreen-click amst__pip-click amst__download-click', (button, event) => { //Log the button name console.log("The " + button.name + " button has been clicked !") //Log the button properties console.log('The button properties : ', button.params) //Log the original event console.log('The original event : ', event) }) .on('amst__fullscreen-enter', _ => { console.log('The player enters in fullscreen mode !') }) .on('amst__fullscreen-exit', _ => { console.log('The player exits fullscreen mode !') }) .on('amst__reset', _ => { console.log('The player has been reset !') }) .on('amst__resize', _ => { console.log('The player has been resized !') }) .on('amst__src-change', _ => { console.log('The player source has been changed !') }) //Log settings changed events .on('amst__settings-change', (setting, oldValue, newValue) => { setting = (setting == 'subs') ? 'Subtitles language' : setting.charAt(0).toUpperCase() + setting.slice(1) console.log('amst__settings-change event : ' + setting + ' has changed from ' + oldValue + ' to ' + newValue) }) .on('amst__playbackRate-change', (oldValue, newValue) => { console.log('amst__playbackRate-change event : Playback rate has changed from ' + oldValue + ' to ' + newValue) }) .on('amst__quality-change', (oldValue, newValue) => { console.log('amst__quality-change event : Quality has changed from ' + oldValue + ' to ' + newValue) }) .on('amst__subs-change', (oldValue, newValue) => { console.log('amst__subs-change event : Subtitles language has changed from ' + oldValue + ' to ' + newValue) }) //When a video ends, play the next one if there is one this.params.media.addEventListener('ended', _ => { if (videoCurrentId + 1 < videoPlaylist.length) { setVideoSource(videoCurrentId + 1) this.play() } }) //Set the first element of our audioPlaylist array as player primary source setVideoSource(0) } ) //Listening to the click event on the track list menu <li> items Array.from(videoWrapper.querySelectorAll('li')).forEach((li, id) => { li.addEventListener('click', _ => { if (li.classList.contains('active')) { videoPlayer.toggle() } else { setVideoSource(id) if (videoPlayer.paused) videoPlayer.play() } }) }) function setVideoSource(id) { if (id >= 0 && id < videoPlaylist.length && id != videoCurrentId) { //On first call, no line is active //If needed, reset the current active line if (videoWrapper.querySelector('li.active')) videoWrapper.querySelector('li.active').removeAttribute('class') videoWrapper.querySelectorAll('li')[id].classList.add('active') //Store the playback state const paused = videoPlayer.paused let mySrc = videoPlaylist[id].src //Update the previous button properties mySrc.previous = { label: (id == 0) ? 'Nothing' : videoPlaylist[id - 1].title, disabled: (id == 0) } //Update the next button properties mySrc.next = { label: (id == videoPlaylist.length - 1) ? 'Nothing' : videoPlaylist[id + 1].title, disabled: (id == videoPlaylist.length - 1) } //Set the src with all relevant parameters videoPlayer.src = mySrc //Update the current video id videoCurrentId = id //Update the video title videoWrapper.querySelector('.track-title').innerHTML = videoWrapper.querySelector('li.active').innerHTML //If the player was playing, start the playback if (!paused) videoPlayer.play() } }

    Notes

    Perhaps have you notice that the volumeGroup parameter of the video player has been set to 1, so it is not linked to the other player volume.
    The audio player volumeGroup remained to the 0 default value, as well as for the players created on the Home page : they all share the same volume. You can check it by changing its volume and then going back to the Home page.
    Each volumeGroup level is stored in the session storage, updated on change and recalled when refresh or navigating from page to page.