MediaElement.js の使い方
HTML の audio 要素や video 要素を使えば特別なプラグインなしでメディア(音声や動画)を再生することができますが、ブラウザごとにスタイルが異なります。
JavaScript オープンソースのメディアプレーヤー MediaElement.js を使えば、 audio 要素や video 要素をどのブラウザでも同じスタイル(UI)で表示・操作することができ、IE11+, MS Edge, Chrome, Firefox, Safari, iOS 8+ 及び Android 4.0+ で動作可能です。
例えば、audio 要素の場合、デフォルトでは以下のようなプレーヤーが表示されます(再生ボタンをクリックすると音が出ます)。
スタイルやアイコンをカスタマイズすることも可能です。
以下は MediaElement.js(バージョンは 6.0.3)の基本的な使い方についての解説のような覚書です。
作成日:2023年5月27日
関連ページ
インストール
MediaElement.js はダウンロードや CDN、npm などでインストールすることができます。
ドキュメント: Installation
ダウンロード
https://github.com/mediaelement/mediaelement からダウンロードすることができます。
基本的な機能で使用するファイルは build フォルダ内の以下の3つです。
- mediaelement-and-player.min.js(JavaScript)
- mediaelementplayer.min.css(CSS)
- mejs-controls.svg(SVG アイコン)
CDN
CDN を使用する場合は、以下のページにリンクが記載されいます。
npm
npm や BOWER を使う場合は、以下のコマンドを実行します。
// npm npm install mediaelement // BOWER bower install mediaelement
スクリプトと CSS の読み込み
ダウンロードしたファイル(mediaelementplayer.min.css、mediaelement-and-player.min.js)を任意の場所に配置し、MediaElement.js を利用するページで読み込みます。
また、ダウンロードした mejs-controls.svg(SVG ファイル)も任意の場所に配置します。
※ mejs-controls.svg を、初期化を記述するスクリプトと異なる場所に配置する場合は、初期化の際に iconSprite プロパティを使ってファイルの場所を指定する必要があります。
CSS は head 内で読み込みます。
<head> <meta charset="UTF-8"> ... <!-- MediaElement の CSS ファイルの読み込み --> <link rel="stylesheet" href="mediaelementplayer.min.css" /> <!-- 独自の CSS ファイルの読み込み(任意) --> <link rel="stylesheet" href="style.css" /> ... </head>
JavaScript は body の閉じタグの前などで読み込みます。
<!-- JavaScript ファイルの読み込み --> <script src="mediaelement-and-player.min.js"></script> <!-- 初期化の処理など(後述) --> <script> const audioElems = document.querySelectorAll('audio'); audioElems.forEach((elem) => { new MediaElementPlayer(elem, { success: function (mediaElement, originalNode, instance) { ... } }); }); </script> </body>
CDN
以下は CDN の場合の例です。mejs-controls.svg は別途ダウンロードして任意の場所に保存します。
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/6.0.3/mediaelementplayer.css"> <!-- 独自の CSS ファイルの読み込み(任意) --> <link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mediaelement/6.0.3/mediaelement-and-player.js"></script> <script> const audioElems = document.querySelectorAll('audio'); audioElems.forEach((elem) => { new MediaElementPlayer(elem, { // mejs-controls.svg の位置を指定 iconSprite: '../mejs-controls.svg', }); }); const videoElems = document.querySelectorAll('video'); videoElems.forEach((elem) => { new MediaElementPlayer(elem, { // mejs-controls.svg の位置を指定 iconSprite: '../mejs-controls.svg', }); }); </script>
Initialize player(初期化)
MediaElement.js を利用するには、new MediaElementPlayer()
に対象の要素と設定オブジェクトを渡してプレーヤーを初期化します(Initialize player)。
以下が構文です。
- 第1引数:対象の要素
- 第2引数:設定オブジェクト(省略可能)
戻り値として MediaElementPlayer オブジェクトのインスタンスを返します(以下では player に代入)。
const player = new MediaElementPlayer(対象の要素, { //オプションの設定オブジェクト(省略可能) });
必要に応じて戻り値のインタンス(以下の場合 audioPlayer)を使ってプロパティやメソッドを利用できます。戻り値を利用しない場合は、変数への代入は省略可能です。
// audio 要素を取得 const audioElem = document.querySelector('audio'); // 上記で取得した audio 要素を渡して初期化 const audioPlayer = new MediaElementPlayer(audioElem, { //オプションの設定オブジェクト(省略可能なオプション) audioWidth : 320, // 幅を指定 audioHeight: 40, // 高さを指定 startVolume: 1.0, // 初期ボリュームを指定 iconSprite: 'mejs-controls.svg', // アイコンファイルの場所を指定 // 初期化が成功した場合に実行されるコールバック関数 success: function (mediaElement, originalNode, instance) { // メッセージをコンソールに出力(確認用) console.log('initialized successfully.'); } });
上記ではサイズや初期ボリュームなどの設定オプションのオブジェクトを指定しています。省略するとデフォルトが適用されます。
iconSprite: はプレーヤーのコントロールに表示する SVG アイコンファイルの場所を指定するオプションで、初期化を記述したスクリプトと同じ場所に配置した場合は省略可能です。
success: はメディアがロードされるとすぐに実行されるコールバック関数です。
通常、audio 要素は controls 属性が指定されていないとコントロールが表示されませんが、MediaElement.js が適用されると controls 属性が指定されていなくてもプレーヤーが表示されます。
上記の場合、querySelector('audio') で取得した audio 要素を初期化しているので、例えば以下が HTML に記述されているとオーディオプレーヤーが表示されます。
<audio src="myaudio.mp3"></audio>
開発ツールで確認すると、一番外側に mejs__container クラスが付与されたコンテナの div 要素があり、その中に audio 要素やコントロールがあるのが確認できます。
また、上記の例では success: のコールバック関数でメッセージをコンソールに出力しているので、コンソールタブを確認すると以下が出力されています。
以下は video の例です。この例では play() メソッドで自動的に再生するために要素に muted 属性を指定しています。
<div class="video-wrapper"> <video src="myvideo.mp4" width="1280" height="720" muted></video> </div>
const videoElem = document.querySelector('video'); const videoPlayer = new MediaElementPlayer(videoElem, { stretching: 'responsive', // レスポンシブに alwaysShowControls: true, // コントロールを常に表示 loop: true, // ループ(HTMLの loop 属性でも指定可能) poster: 'images/poster.jpg' , //代替画像(HTMLの poster 属性でも指定可能) }); // 戻り値のインスタンスで play() メソッドを使って再生 videoPlayer.play();
上記の場合、例えば以下のように表示されて自動的に再生されます。
自動再生は video 要素に autoplay 属性を指定してもできますし、コールバック関数で引数の originalNode(または mediaElement)の autoplay プロパティに true を設定することもできます。
const videoElem = document.querySelector('video'); const videoPlayer = new MediaElementPlayer(videoElem, { stretching: 'responsive', alwaysShowControls: true, loop: true, poster: 'images/poster.jpg' , success: function (mediaElement, originalNode, instance) { originalNode.autoplay = true; //または mediaElement.autoplay = true; } });
または以下のようにコールバック関数でインスタンスや video 要素(や audio 要素)のメソッドを使って自動再生することもできます。
const videoElem = document.querySelector('video'); const videoPlayer = new MediaElementPlayer(videoElem, { stretching: 'responsive', alwaysShowControls: true, loop: true, poster: 'images/poster.jpg' , success: function (mediaElement, originalNode, instance) { // コールバック関数で引数の instance とそのメソッドを使って再生 instance.play(); // originalNode.play(); で video 要素の play() メソッドを使っても可能 } });
class 属性
audio 要素や video 要素に class 属性を指定すると、その値(クラス名)はプレーヤーのコンテナ要素(.mejs__container)にも追加されます。
<div class="media-wrapper"> <audio class="my-audio" src="sample.mp3" width="460" height="40"></audio> <video class="my-video" src="sample.mp4" width="1280" height="720" muted></video> </div>
const audioElem = document.querySelector('audio'); const audioPlayer = new MediaElementPlayer(audioElem, { stretching: 'responsive' }); const videoElem = document.querySelector('video'); const videoPlayer = new MediaElementPlayer(videoElem, { stretching: 'responsive' });
以下のようにコンテナ要素に指定したクラスが追加されます。
追加されたクラスのセレクタを使ってスタイルや DOM を操作することができます。
.media-wrapper { max-width: 480px; } .my-audio { background-color: green; margin: 30px; } /* video のマージンは .mejs__container に指定 */ .my-video.mejs__container { background-color: blue; margin: 30px; }
const myAudios = document.querySelectorAll('.my-audio'); myAudios.forEach((elem) => { elem.addEventListener('mouseover', (e)=> { e.currentTarget.style.backgroundColor = 'red'; }) });
設定オプション
MediaElementPlayer では多数の設定オプションが用意されていて、初期化の際に指定することができます。以下のページで指定できるオプションを確認することができます。
API and Configuration(MediaElementPlayer)
Parameter | Type | Default | 説明 |
---|---|---|---|
iconSprite | string | mejs-controls.svg | SVG アイコンへのパス |
success | callback | 初期化が成功した場合に実行されるコールバック関数。 | |
features | array | [...] | プレーヤーで使用する機能/プラグインのリスト。 |
startVolume | number | 0.8 | 初期ボリュームの値 |
audioVolume | string | horizontal | audio 要素の音量スライダーの位置 |
videoVolume | string | vertical | video 要素の音量スライダーの位置 |
pauseOtherPlayers | boolean | true | 再生を開始すると、他の再生中のプレーヤーを一時停止する |
iconSprite(コントロールのアイコン)
mejs-controls.svg は再生ボタンなどのコントロールの部品(アイコン)の SVG ファイルです。ダウンロードした mediaelement-and-player.js や mediaelementplayer.css と同じフォルダに入っています。
デフォルトでは初期化を記述したスクリプトと同じフォルダを参照するので、mejs-controls.svg が異なる場所にある場合は iconSprite オプションを使ってパスを指定する必要があります。
初期化を記述したスクリプトと同じ場所にある場合は指定する必要はありません。
mejs-controls.svg を正しく参照できないと、以下のように再生ボタンなどが表示されません。
以下は mejs-controls.svg を images フォルダに配置した場合の例です。
const audioPlayer = new MediaElementPlayer(audioElem, { // 初期化を記述したスクリプトからの場所を指定 iconSprite: 'images/mejs-controls.svg' });
success(コールバック関数)
success には初期化が成功した場合に実行されるコールバック関数を指定することができます。
コールバック関数の引数には、以下を渡すことができます。
- 第1引数:mediaElement
- 第2引数:originalNode
- 第3引数:instance
引数に指定する値がどのようなものかは、例えば以下のようにすると確認することができます。
const audioElem = document.querySelector('audio'); const audioPlayer = new MediaElementPlayer(audioElem, { // 初期化が成功した場合に実行されるコールバック関数 success: function (mediaElement, originalNode, instance) { // 引数に指定した値をコンソールに出力 console.log(mediaElement); console.log(originalNode); console.log(instance); } }); //戻り値のオブジェクト(コールバック関数の第3引数 instance と同じ) console.log(audioPlayer);
<audio class="my-audio" src="audio/birds2.mp3" width="460" height="40"></audio>
上記の場合、例えば以下のようにコンソールに出力されます。
- mediaElement : audio 要素のラッパー要素
- originalNode : audio 要素
- instance : オブジェクトのインスタンス
第3引数の instance は戻り値の audioPlayer と同じです。
instance を展開すると以下のようになっています。例えば、container プロパティを使えば外側の要素にアクセスできますし、controls プロパティで div.mejs__controls にアクセスできます。
以下は instance.container でコンテナ要素(div.mejs__container)にアクセスして背景色を赤に設定し、instance.controls でコントロール部分の要素(div.mejs__controls)にアクセスしてその高さを取得して出力する例です。
※ 背景色は CSS で設定可能です。
const audioPlayer = new MediaElementPlayer(audioElem, { success: function (mediaElement, originalNode, instance) { // プレーヤーの背景色を赤に instance.container.style.backgroundColor = 'red'; // コントロール部分の高さを取得して出力 console.log(instance.controls.clientHeight); //40 } });
features(コントロールの表示順)
features にはコントロールに表示する項目を配列で指定することができます。
指定された項目が指定された順番で表示されます。デフォルトでは loop ボタンはありません。
const videoPlayer = new MediaElementPlayer(videoElem, { // 以下はデフォルトと同じ features: ['playpause', 'current', 'progress', 'duration', 'tracks', 'volume', 'fullscreen'] });
オプションの useDefaultControls に true を設定すると、features で指定した(変更した)内容は無視され、デフォルトのコントロールがデフォルトの並び順で表示されます。
volume
ボリュームの初期値は 0.8 に設定されています。startVolume オプションで変更することができます。
また、ボリュームのコントロールの位置(表示方向)は以下で指定することができます。
- audioVolume :オーディオのボリュームの位置(デフォルトは horizontal)
- videoVolume :ビデオのボリュームの位置(デフォルトは vertical)
const audioPlayer = new MediaElementPlayer(audioElem, { // 初期ボリュームの値を 1.0 に startVolume: 1.0, // ボリュームの表示を垂直方向(マウスオーバーで表示される)に audioVolume : 'vertical', });
pauseOtherPlayers
デフォルトでは、プレーヤーを再生すると、もし他のプレーヤーが再生中であれば一時停止します。
以下の場合、audio 要素を再生すると、他のプレーヤー(audio も video も)を一時停止しますが、video 要素を再生した場合は、現在再生中のプレーヤーを停止しません。
// audio 要素 const audioElems = document.querySelectorAll('audio'); audioElems.forEach((audioElem) => { // プレーヤーを開始すると、他のプレーヤーを一時停止(デフォルト) new MediaElementPlayer(audioElem); }); // video 要素 const videoElems = document.querySelectorAll('video'); videoElems.forEach((audioElem) => { new MediaElementPlayer(audioElem, { // プレーヤーを開始しても、他のプレーヤーを一時停止しない(同時に再生可能) pauseOtherPlayers: false }); })
Sizing(サイズ調整)
audio や video のサイズを設定オプションで指定することができます。
以下が設定できるオプションとデフォルト値です(設定オプションから抜粋)。
Parameter | Type | Default | 説明 |
---|---|---|---|
setDimensions | boolean | true | CSS ではなく JS を使用してサイズを設定する |
enableAutosize | boolean | true | メディアの寸法に合わせてサイズを変更する |
defaultVideoWidth | number | 480 | video 要素に width が指定されていない場合のデフォルトの幅 |
defaultVideoHeight | number | 270 | video 要素に height が指定されていない場合のデフォルトの高さ |
videoWidth | number | -1 | 設定すると video 要素の width の値を上書き |
videoHeight | number | -1 | 設定すると video 要素の height の値を上書き |
defaultAudioWidth | number | 400 | audio 要素に width が指定されていない場合のデフォルトの幅 |
defaultAudioHeight | number | 40 | audio 要素に height が指定されていない場合のデフォルトの高さ |
audioWidth | number | -1 | 設定すると audio 要素の width の値を上書き |
audioHeight | number | -1 | 設定すると audio 要素の height の値を上書き |
stretching | string | auto | 'auto', 'responsive', 'fill', 'none' のいずれかを指定できます。詳細は stretching modes |
audio の高さの変更
audio プレーヤーのデフォルトの高さは 40px です(ドキュメントでは 30 となっていますが、 mediaelement-and-player.js で確認すると 40 です)。
audio プレーヤーの高さは audioHeight オプションで数値で指定できます。
const audioPlayer = new MediaElementPlayer(audioElem, { // audio プレーヤーの高さを50pxに audioHeight: 50 });
但し、この設定だけではコントロールが垂直方向の中央に揃わないので、CSS で追加の設定を指定します。
コントロール部分(.mejs__controls)に同じ高さを設定し、align-items: center; を指定します。また、ボリューム部分(.mejs__horizontal-volume-slider)を調整します。
※もっと良い方法があるかも知れません。
.mejs__controls { align-items: center; height: 50px; } .mejs__horizontal-volume-slider { margin-top: -4px; }
以下はコントロール部分(.mejs__controls)に設定されているスタイルです。
.mejs__controls { bottom: 0; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; height: 2.5rem; left: 0; list-style-type: none; margin: 0; padding: 0 0.625rem; position: absolute; width: 100%; z-index: 3; }
レスポンシブ表示(stretching modes)
stretching modes では以下の4つのモードを指定することができます。
モード | 説明 |
---|---|
auto | CSS で max-width と max-height が設定されているかをチェックして、設定されていれば responsive モードにします。設定されていなければ none モードにします。 |
responsive | レスポンシブ表示します(親要素の大きさに合わせて伸縮します)。stretching: 'responsive' を設定するのと、videoWidth: '100%' と videoHeight: '100%' を設定するのは同じことですが、stretching: 'responsive' が推奨されています。 |
fill | fill モードを設定すると、親要素のサイズに合わせてトリミング(crop)されて表示されます。期待どおりに動作させるには、親要素の高さを 100% に設定することが推奨されています。 |
none | video/audio タグで指定された width 属性と height 属性の値を使用します。 |
以下は audio 要素と video 要素で stretching に responsive を指定してレスポンシブ表示にする例です。
audio 要素と video 要素はそれぞれ親要素で囲み、親要素で max-width を指定してサイズを調整します。
<div class="audio-wrapper"> <audio src="myAudio.mp3" width="420" height="40"></audio> </div> <div class="video-wrapper"> <video src="myVideo.mp4" width="1280" height="720" muted></video> </div>
const audioElem = document.querySelector('audio'); const audioPlayer = new MediaElementPlayer(audioElem, { // audio をレスポンシブに stretching: 'responsive' }); const videoElem = document.querySelector('video'); const videoPlayer = new MediaElementPlayer(videoElem, { // video をレスポンシブに stretching: 'responsive' });
以下でもほぼ同じです。
const audioPlayer = new MediaElementPlayer(audioElem, { // audio をレスポンシブに(幅のみ) audioWidth: '100%' }); const videoPlayer = new MediaElementPlayer(videoElem, { // video をレスポンシブに videoWidth: '100%' , videoHeight: '100%' });
基本的には CSS でラッパー(親要素)に max-width を指定します。
これにより max-width 以下では幅に合わせて伸縮します。※ video の場合は伸縮に合わせて高さと幅が変化しますが、audio の場合は幅は伸縮しますが、高さは一定です。
.audio-wrapper { max-width: 420px; } .video-wrapper { max-width: 600px; } .audio-wrapper, .video-wrapper { margin: 30px 0; }
audio 要素の場合は、audio 要素にクラスを指定していれば、そのクラスに max-width を指定することもできます。※ video 要素の場合は、ラッパー(親要素)に max-width を指定します。
<audio class="my-audio" src="sample.mp3" width="460" height="40"></audio>
audio 要素に指定したクラスは MediaElement のラッパー要素にも自動的に付与されます。
.my-audio { max-width: 480px; }
Styling(スタイル)
MediaElementPlayer のスタイルはダウンロードした mediaelementplayer.min.css に設定されていますが、ミニファイされていない mediaelementplayer.css で内容を確認することができます。
例えば、以下のような div.media-wrapper(ラッパー)で囲んだ audio 要素の場合、
<div class="media-wrapper"> <audio src="sample.mp3"></audio> </div>
MediaElementPlayer で初期化すると、以下のような HTML が生成されます。
プレーヤーは .mejs__container や .mejs__mediaelement、.mejs__controls(コントロール部分)などで構成されていることが確認できます。
この例では、audio 要素に width と height 属性を指定していないので、設定オプションの defaultAudioWidth と defaultAudioHeight の値 width: 400px; height: 40px; が設定されています。
<div class="media-wrapper"><!-- 上記 HTML で指定したラッパー --> <span class="mejs__offscreen">Audio Player</span> <div id="mep_0" class="mejs__container mejs__container-keyboard-inactive mejs__audio" tabindex="0" role="application" aria-label="Audio Player" style="width: 400px; height: 40px;"> <div class="mejs__inner"> <div class="mejs__mediaelement"> <div id="mejs_30821638592994316"> <audio src="http://webdesignleaves.localhost/pr/audio/birds2.mp3" id="mejs_30821638592994316_html5" preload="none" tabindex="-1"></audio></div> </div> <div class="mejs__layers">...</div> <div class="mejs__controls">...</div> </div> </div> </div>
mediaelementplayer.css には以下のような設定が記述されています。
.mejs__container には背景色が設定され、内側の要素 .mejs__mediaelement は絶対配置でコンテナいっぱいに広がるように設定されています。
.mejs__inner にはスタイルは設定されていません。
.mejs__container { background: #000; box-sizing: border-box; font-family: 'Helvetica', Arial, serif; position: relative; text-align: left; text-indent: 0; vertical-align: top; } /* 中略 */ .mejs__mediaelement { height: 100%; left: 0; position: absolute; top: 0; width: 100%; z-index: 0; } /* 中略 */ .mejs__controls { bottom: 0; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; height: 2.5rem; left: 0; list-style-type: none; margin: 0; padding: 0 0.625rem; position: absolute; width: 100%; z-index: 3; } .mejs__controls:not([style*='display: none']) { background: rgba(255, 0, 0, 0.7); background: -webkit-linear-gradient(transparent, rgba(0, 0, 0, 0.35)); background: linear-gradient(transparent, rgba(0, 0, 0, 0.35)); }
背景色
プレーヤーの背景色は .mejs__container に background: #000;(黒)が指定されています。
.mejs__container { background: #000; /* プレーヤーの背景色 */ box-sizing: border-box; font-family: 'Helvetica', Arial, serif; position: relative; text-align: left; text-indent: 0; vertical-align: top; }
背景色を変更するには、独自のスタイルシート(mediaelementplayer.min.css の後から読み込む)で .mejs__container に設定します。
例えば、独自の CSS で以下を記述するとプレーヤーの色を赤に変更することができます。
.mejs__container { background-color: red; /* または background: red; */ }
audio 要素や video 要素にクラスを指定している場合は、そのクラス名のセレクタを使ってスタイルを設定することもできます。例えば、audio 要素に my-audio というクラスを指定していれば、以下のように指定することもできます。
.my-audio { background-color: red; }
以下のようにプレーヤーの背景色が赤で表示されます。
よく見ると、プレーヤー全体にグラデーションが薄くかかっているのがわかります。
グラデーション
background-color: transparent; とすると、以下のようにグラデーションのみが表示されます。
このグラデーションは以下により設定されています。
.mejs__controls:not([style*='display: none']) { background: rgba(255, 0, 0, 0.7); background: -webkit-linear-gradient(transparent, rgba(0, 0, 0, 0.35)); background: linear-gradient(transparent, rgba(0, 0, 0, 0.35)); }
以下は背景色を変更して、グラデーションも削除する例です。
.mejs__controls:not([...]) には linear-gradient も指定されているので background で指定します(background-color: transparent ではグラデーションを除去できません)。
.mejs__container { background-color: #3e7b3e; } .mejs__controls:not([style*='display: none']) { /* background に指定 */ background: transparent; }
上記により以下のような表示になります。
タイトルを表示
以下はラッパー要素の背景色をプレーヤーと同じ色にしてタイトルを表示する(ように見せる)例です。
<div class="audio-with-title"> <div class="title">Birds Singing</div> <audio src="birds.mp3" width="460" height="40"></audio> </div>
プレーヤーのデフォルトの背景色は #000 なので、例えば以下のように設定すると、
/* ラッパー要素 */ .audio-with-title { /* 背景色をプレーヤーと同じ色に */ background-color: #000; /* ラッパー要素に角丸を設定 */ border-radius: 15px; padding: 10px; } .audio-with-title .title { color: #eee; font-size: 13px; padding: 0.25rem 1rem; }
以下のような表示になります。
以下はプレーヤーの背景色を変更する場合の例です。
/* プレーヤーの背景色 */ .mejs__container { background-color: #181866; } /* プレーヤーのグラデーションを削除(好みに応じて) */ .mejs__controls:not([style*='display: none']) { background: transparent; } /* ラッパーの背景色(プレーヤーと同じ色) */ .audio-with-title { background-color:#181866; } /* タイトル */ .audio-with-title .title { color: #eee; font-size: 13px; padding: 0.25rem 1rem; } /* フォーカス時のコンテナのアウトラインを非表示に */ .mejs__container:focus { outline: none; }
上記により以下のような表示になります。
コールバックでタイトルを表示
前述の例ではタイトルやラッパーを HTML に記述しましたが、以下は audio 要素に特定のクラスと title 属性が指定されている場合は、オプションの success でコールバックを使ってタイトルを表示する例です。
以下の例では、.audio-with-title が付与された audio 要素に title 属性が指定されている場合は、title 属性の値をタイトルとして表示します。
<audio class="audio-with-title" src="birds.mp3" title="Birds Singing"></audio>
success のコールバックでは、オーディオプレーヤーをラップする div 要素を作成して insertBefore() と appendChild() を使ってコンテナー(.mejs__container)をラップします。
audio 要素に title 属性が指定されていれば、その値をテキストとする div 要素の HTML を作成して、insertAdjacentHTML() でラッパーに追加します。
const myAudios = document.querySelectorAll('.audio-with-title'); myAudios.forEach((audio) => { new MediaElementPlayer(audio, { // mejs-controls.svg がスクリプトと異なる場所にある場合 iconSprite: 'path to /mejs-controls.svg', stretching: 'responsive', // コールバック success: function (mediaElement, originalNode, instance) { // オーディオプレーヤーをラップする div 要素(ラッパー)を作成 const audioWrapper = document.createElement('div'); // クラス属性を付与 audioWrapper.className = 'audio-wrapper'; // コンテナー(.mejs__container)の前にラッパーを挿入 instance.container.parentNode.insertBefore(audioWrapper, instance.container); // コンテナー(.mejs__container)をラッパーに追加(移動) audioWrapper.appendChild(instance.container); // audio 要素の title 属性の値 const title = originalNode.getAttribute('title'); if (title) { // audio 要素に title 属性が指定されていればタイトルとして表示 const titleDiv = `<div class="title">${title}</div>`; audioWrapper.insertAdjacentHTML('afterbegin', titleDiv); } } }); });
CSS ではプレーヤーとラッパーの背景色を同じ色に指定しています。また、この例では audio 要素にクラス属性を指定しているので、プレーヤーの背景色は audio 要素のクラスセレクタに指定しています(.mejs__container に指定することもできます)。
/* ラッパー */ .audio-wrapper { max-width: 360px; background-color: #181866; } /* プレーヤーのグラデーションを削除(好みに応じて) */ .audio-wrapper .mejs__controls:not([style*='display: none']) { background: transparent; } /* フォーカス時のコンテナのアウトラインを非表示に */ .audio-wrapper .mejs__container:focus { outline: none; } /* プレーヤーの背景色(ラッパーと同じ色) */ .audio-with-title { background-color: #181866; } /* タイトル */ .audio-wrapper .title { color: #eee; font-size: 13px; padding: 0.25rem 1rem; }
上記により以下のように表示されます。
角丸
プレーヤーに角丸を設定するには、.mejs__container と .mejs__controls:not([style*='display: none']) の両方に border-radius を設定します。
.mejs__container のみに指定すると、グラデーションの影が両端の下に残ってしまいます(または、グラデーションを削除すれば影も表示されません)。
.mejs__container { background-color: #aaa; border-radius: 20px; } .mejs__controls:not([style*='display: none']) { border-radius: 20px; }
上記により以下のような表示になります。
以下は角丸を設定し、グラデーションを削除した例です。
.mejs__container { background-color: #aaa; border-radius: 20px; } .mejs__controls:not([style*='display: none']) { /* background に transparent を指定してグラデーションを削除 */ background: transparent; }
コントロール
コントロールの部分も開発ツールなどで調べて各部品にスタイルを設定することができます。
以下はシークバーのハンドル(つまみ部分)を円形に表示して、シークバーの高さを小さくする例です(ダウンロードした demo の index.html に掲載されています)。
この例ではラッパーに thin-slider クラスが付与されているプレーヤーのシークバーにスタイルを適用しています。
<div class="thin-slider"> <audio src="sample.mp3"></audio> </div>
.thin-slider .mejs__time-buffering, .thin-slider .mejs__time-current, .thin-slider .mejs__time-handle, .thin-slider .mejs__time-loaded, .thin-slider .mejs__time-marker, .thin-slider .mejs__time-total, .thin-slider .mejs__time-hovered { height: 0.125rem; } .thin-slider .mejs__time-total { margin-top: 0.5625rem; } .thin-slider .mejs__time-handle { left: -0.3125rem; width: 0.75rem; height: 0.75rem; border-radius: 50%; background: #ffffff; top: -0.3125rem; cursor: pointer; display: block; position: absolute; z-index: 2; border: none; } .thin-slider .mejs__time-handle-content { top: 0; left: 0; width: 0.75rem; height: 0.75rem; }
色
以下はコントロールの色をカスタマイズする例です。
再生ボタンなどの SVG アイコンは button の color プロパティで色を設定(変更)することができます。
それぞれのコントロールの親要素(コンテナ)には、例えば .mejs__play や .mejs__pause などのコントロールを識別するためのクラスが指定されています。
/* play button と replay button(SVG アイコン)*/ .mejs__button.mejs__play button, .mejs__button.mejs__replay button { color: lightblue; } /* pause button(SVG アイコン) */ .mejs__button.mejs__pause button { color: lightsalmon; } /* currenttime(数字) */ .mejs__time.mejs__currenttime-container { color: yellow; } /* currenttime(バーの背景色) */ .mejs__time-current { background-color: pink; } /* duration(数字) */ .mejs__time.mejs__duration-container { color: orange; } /* unmute button(SVG アイコン) */ .mejs__button.mejs__unmute button { color: red; } /* current volume(バーの背景色) */ .mejs__horizontal-volume-current { background-color: lightgreen; }
上記の場合、以下のような表示になります。
アイコンの変更
コントロールのアイコンは mejs-controls.svg という SVG ファイルに記述されています。
以下は mejs-controls.svg をコピーして mejs-controls-custom.svg という名前で保存し、アイコンを変更する例です。
mejs-controls.svg を mejs-controls-custom.svg という名前で保存したら、初期化する際に iconSprite オプションで mejs-controls-custom.svg を参照するようにします。
const audioElem = document.querySelector('audio'); const audioPlayer = new MediaElementPlayer(audioElem, { // 別名で保存(コピー)した SVG ファイル mejs-controls-custom.svg を参照 iconSprite: 'mejs-controls-custom.svg' });
この例では、play ボタンと pause ボタン、及び replay ボタン(id が icon-play と icon-pause、及び icon-replay の symbol 要素の viewBox と path 要素の部分)を変更しています。
また、デフォルトの play ボタンと replay ボタンのアイコンは異なるアイコンですが、以下では play ボタンと replay ボタンを同じアイコンで表示するようにしています。
<?xml version="1.0" encoding="UTF-8"?> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> ・・・中略・・・ <!-- 変更 --> <symbol id="icon-play" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z"/></symbol> <!-- 変更 --> <symbol id="icon-pause" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M5 6.25a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0v-3.5zm3.5 0a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0v-3.5z"/></symbol> <!-- 変更( icon-play と同じ SVG) --> <symbol id="icon-replay" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z"/></symbol> ・・・中略・・・ </svg>
もとの mejs-controls.svg の play ボタン、pause ボタン、replay ボタンの symbol 要素及び配下の path 要素は以下のようになっています。
<symbol id="icon-play" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 8.5c.3.1.4.5.2.8-.1.1-.1.2-.2.2l-11.4 7c-.5.3-.8.1-.8-.5V2c0-.5.4-.8.8-.5l11.4 7z"/></symbol> <symbol id="icon-pause" viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg"><path d="M1,0H3.2a.94.94,0,0,1,1,1V15a.94.94,0,0,1-1,1H1a.94.94,0,0,1-1-1V1A1,1,0,0,1,1,0Zm9.8,0H13a.94.94,0,0,1,1,1V15a.94.94,0,0,1-1,1H10.8a.94.94,0,0,1-1-1V1A1,1,0,0,1,10.8,0Z"/></symbol> <symbol id="icon-replay" viewBox="0 0 17.52 15.97" xmlns="http://www.w3.org/2000/svg"><path d="M16.7,7.27a.81.81,0,0,1-.9.7c-.1,0-.2,0-.2-.1l-4.9-1.8c-.5-.2-.6-.6-.1-.8l6.2-3.6c.5-.3.8-.1.7.5l-.8,5.1Z"/><path d="M8.1,13.77a5.92,5.92,0,0,1-2.9-.7A5.77,5.77,0,0,1,2,7.87a6.21,6.21,0,0,1,6.3-6A6.15,6.15,0,0,1,13.9,6l.1-.1L16.1,7A8.1,8.1,0,0,0,7,.07,8.24,8.24,0,0,0,0,8a8.06,8.06,0,0,0,4.3,7,10.21,10.21,0,0,0,3.8,1,8.24,8.24,0,0,0,6.6-3.3l-1.8-.9a7,7,0,0,1-4.8,2Z"/></symbol>
<?xml version="1.0" encoding="UTF-8"?> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <symbol id="icon-captions" viewBox="0 0 18 14" xmlns="http://www.w3.org/2000/svg"> <g fill="#fff"> <path d="M-.67-4.08h14a2,2,0,0,1,2,2v10a2,2,0,0,1-2,2h-14a2,2,0,0,1-2-2v-10A2,2,0,0,1-.67-4.08Z" transform="translate(2.67 4.08)"/> </g> <g fill="#000"> <path d="M2.88,6.67a2.81,2.81,0,0,1-2.1-1A3.91,3.91,0,0,1,.88.87c.5-.6,2-1.7,4.6.2l-.6.8c-1.4-1-2.6-1.1-3.3-.3a3,3,0,0,0-.1,3.5c.7.9,1.9.8,3.4-.1l.5.9a5.28,5.28,0,0,1-2.5.8Zm7.5,0a2.81,2.81,0,0,1-2.1-1,3.91,3.91,0,0,1,.1-4.8c.5-.6,2-1.7,4.6.2l-.5.8c-1.4-1-2.6-1.1-3.3-.3a3,3,0,0,0-.1,3.5c.7.9,1.9.8,3.4-.1l.5.9a6.07,6.07,0,0,1-2.6.8Z" transform="translate(2.67 4.08)"/> </g> </symbol> <symbol id="icon-chapters" viewBox="0 0 16.6 13" xmlns="http://www.w3.org/2000/svg"><path d="M1.5,0A1.54,1.54,0,0,1,3,1.5,1.54,1.54,0,0,1,1.5,3,1.54,1.54,0,0,1,0,1.5,1.47,1.47,0,0,1,1.5,0ZM6.6,0h8.5a1.47,1.47,0,0,1,1.5,1.5A1.54,1.54,0,0,1,15.1,3H6.6A1.47,1.47,0,0,1,5.1,1.5,1.37,1.37,0,0,1,6.6,0ZM1.5,5A1.54,1.54,0,0,1,3,6.5,1.54,1.54,0,0,1,1.5,8,1.54,1.54,0,0,1,0,6.5,1.47,1.47,0,0,1,1.5,5ZM6.6,5h8.5a1.47,1.47,0,0,1,1.5,1.5A1.54,1.54,0,0,1,15.1,8H6.6A1.47,1.47,0,0,1,5.1,6.5,1.37,1.37,0,0,1,6.6,5ZM1.5,10a1.5,1.5,0,0,1,0,3A1.54,1.54,0,0,1,0,11.5,1.47,1.47,0,0,1,1.5,10Zm5.1,0h8.5a1.47,1.47,0,0,1,1.5,1.5A1.54,1.54,0,0,1,15.1,13H6.6a1.47,1.47,0,0,1-1.5-1.5A1.37,1.37,0,0,1,6.6,10Z"/></symbol> <symbol id="icon-fullscreen" viewBox="0 0 17.8 17.8" xmlns="http://www.w3.org/2000/svg"><path d="M0,1A.94.94,0,0,1,1,0H6.4c.6,0,.7.3.3.7l-6,6C.3,7.1,0,7,0,6.4ZM0,16.8a.94.94,0,0,0,1,1H6.4c.6,0,.7-.3.3-.7l-6-6c-.4-.4-.7-.3-.7.3ZM17.8,1a.94.94,0,0,0-1-1H11.4c-.6,0-.7.3-.3.7l6,6c.4.4.7.3.7-.3Zm0,15.8a.94.94,0,0,1-1,1H11.4c-.6,0-.7-.3-.3-.7l6-6c.4-.4.7-.3.7.3Z"/></symbol> <symbol id="icon-unfullscreen" viewBox="0 0 17.8 17.8" xmlns="http://www.w3.org/2000/svg"><path d="M11.74,4.64a.94.94,0,0,0,1,1h4.1c.6,0,.7-.3.3-.7L12.44.24c-.4-.4-.7-.3-.7.3Zm-7.1,1a.94.94,0,0,0,1-1V.54c0-.6-.3-.7-.7-.3L.24,4.94c-.4.4-.3.7.3.7Zm1,7.1a.94.94,0,0,0-1-1H.54c-.6,0-.7.3-.3.7l4.7,4.7c.4.4.7.3.7-.3Zm7.1-1a.94.94,0,0,0-1,1v4.1c0,.5.3.7.7.3l4.7-4.7c.4-.4.3-.7-.3-.7Z"/></symbol> <symbol id="icon-mute" viewBox="0 0 17.55 17.03" xmlns="http://www.w3.org/2000/svg"> <g stroke="none"> <path d="M6.21,4.36a3,3,0,0,1-1.8.6H1.21a.94.94,0,0,0-1,1v5.7a.94.94,0,0,0,1,1h4.2c.3.2.5.4.8.6l3.5,2.6a.47.47,0,0,0,.8-.4V2.06a.47.47,0,0,0-.8-.4Z"/> </g> <g fill="none" stroke-linecap="round" stroke-width="1.5px"> <path d="M13.11,1.18S17,.38,17,8.88s-3.9,7.8-3.9,7.8"/> <path d="M11.81,5.08s2.6-.4,2.6,3.8-2.6,3.9-2.6,3.9"/> </g> </symbol> <symbol id="icon-unmute" viewBox="0 0 18.2 17" xmlns="http://www.w3.org/2000/svg"> <g stroke="none"> <path d="M6.21,4.36a3,3,0,0,1-1.8.6H1.21a.94.94,0,0,0-1,1v5.7a.94.94,0,0,0,1,1h4.2c.3.2.5.4.8.6l3.5,2.6a.47.47,0,0,0,.8-.4V2.06a.47.47,0,0,0-.8-.4Z"/> </g> <g fill="none" stroke-linecap="round" stroke-width="2px"> <path d="M12,5.55l5.4,5.4M12,11l5.4-5.4"/> </g> </symbol> <!-- 変更 --> <symbol id="icon-play" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z"/></symbol> <!-- 変更 --> <symbol id="icon-pause" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M5 6.25a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0v-3.5zm3.5 0a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0v-3.5z"/></symbol> <!-- 変更( icon-play と同じ SVG) --> <symbol id="icon-replay" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z"/></symbol> <symbol id="icon-overlay-play" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"> <g fill="#333" stroke="#fff" stroke-width="5px"> <path d="M2.5,40A37.5,37.5,0,1,1,40,77.5,37.51,37.51,0,0,1,2.5,40Z"/> </g> <g fill="#fff"> <path d="M60.3,38a1,1,0,0,1,.6,1.4.9.9,0,0,1-.6.6L30,57.5c-1,.6-1.7.1-1.7-1v-35c0-1.1.8-1.5,1.7-1Z"/> </g> </symbol> <symbol id="icon-loading-spinner" viewBox="0 0 75.8 77.9" xmlns="http://www.w3.org/2000/svg"> <g fill="#fff"> <circle cx="38.8" cy="8.1" r="8.1"/> <g opacity="0.4"> <circle cx="70.8" cy="40" r="5"/> </g> <g opacity="0.6"> <circle cx="38.8" cy="71.9" r="6"/> </g> <g opacity="0.8"> <circle cx="7" cy="40" r="7"/> </g> <g opacity="0.9"> <circle cx="15.1" cy="17.3" r="7.5"/> </g> <g opacity="0.3"> <circle cx="63.2" cy="17.1" r="4.5"/> </g> <g opacity="0.5"> <circle cx="62.7" cy="63.8" r="5.5"/> </g> <g opacity="0.7"> <circle cx="15.1" cy="63.8" r="6.5"/> </g> </g> </symbol> </svg>
アイコンの大きさを調整
指定した SVG のアイコンの大きさを調整する例です。
SVG 側で viewBox や path の値で大きさを調整することもできますが、以下では CSS で button 要素に transform: scale() を適用して大きさを調整しています。
/* play button と replay button */ .mejs__button.mejs__play button, .mejs__button.mejs__replay button { transform: scale(1.2); } /* pause button */ .mejs__button.mejs__pause button { transform: scale(1.2); }
上記の場合、play ボタンと pause ボタン、及び replay ボタンのアイコンが変更されて以下のように表示されます。
変更後の play ボタン、pause ボタンの SVG アイコンは Bootstrap Icons の play-circle と pause-circle を利用させていただきました。
Plugins(追加機能)
MediaElement.js Plugins のページから追加機能(Plugins)をダウンロードすることができます。
CDN のリンクは https://cdnjs.com/libraries/mediaelement-plugins にあります。
ダウンロードして解凍したフォルダ(mediaelement-plugins-master)内の dist フォルダに各プラグインに使用するファイル(JavaScript と CSS など)が入っています。
また、demo フォルダ内の index.html でそれぞれのプラグインの内容を確認できます。
loop
デフォルトではループ機能のボタンはなく、ループの操作はプラグインとして提供されています。
loop プラグインを使うと、コントロールバーにループボタンを作成し、ループエフェクトのオン/オフを切り替えることができます。
mediaelement-plugins/blob/master/docs/loop.md
使用するには loop プラグインの CSS と JavaScript を読み込みます。
<link rel="stylesheet" href="mediaelementplayer.min.css" /> <link rel="stylesheet" href="dist/loop/loop.min.css"> <!-- 追加 -->
<script src="mediaelement-and-player.min.js"></script> <script src="dist/loop/loop.min.js"></script> <!-- 追加 -->
初期化の際に features に 'loop' を追加します。※ features への指定順でそれぞれのコントロールが表示(配置)されます。
const audioElems = document.querySelectorAll('audio'); audioElems.forEach((audioElem) => { new MediaElementPlayer(audioElem,{ // features に 'loop' を追加。任意の位置(順番)に指定可能 features: ['playpause', 'current', 'progress', 'duration', 'tracks', 'volume', 'loop'] }); });
上記の場合、'loop' を一番最後に記述したので右端にループボタンが表示されます。
アイコンの変更
loop.css を見ると以下のように記述されていて、ループボタンのアイコンは loop.svg を参照しています。また、loop-on と loop-off でポジションを変更して表示するようになっています。
.mejs__loop-button > button, .mejs-loop-button > button { background: url('loop.svg') no-repeat transparent; } .mejs__loop-off > button, .mejs-loop-off > button { background-position: -20px 1px; } .mejs__loop-on > button, .mejs-loop-on > button { background-position: 0 1px; }
loop.svg を見ると以下のようになっていて、g タグに id が指定されてグループ化されていたりするので、単純に SVG ファイルを置き換えるだけではうまく変更できませんでした。
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.48 15.65" x="0" y="0" width="37" height="16"><defs><style>.cls-1{fill:#fff;}.cls-2{opacity:0.5;}</style></defs><title>4</title><g id="loop"><g id="loop_on" data-name="loop on"><path class="cls-1" d="M52.5,5.7a5.61,5.61,0,0,1-.77,7.9l-0.13.1a5.61,5.61,0,0,1-7.9-.77l-0.1-.13a5.63,5.63,0,0,1,.2-7.4L42.2,3.8A8,8,0,0,0,53.9,14.7a7.84,7.84,0,0,0,.2-10.6Z" transform="translate(-40.24 -1.7)"/><path class="cls-1" d="M49.6,2.5a0.64,0.64,0,0,1,.46-0.78l0.14,0h0.3L56,2.5c0.5,0.1.7,0.5,0.3,0.8l-5,5c-0.4.4-.8,0.3-0.9-0.3Z" transform="translate(-40.24 -1.7)"/></g><g id="loop_off" data-name="loop off" class="cls-2"><path class="cls-1" d="M72.7,5.9a5.61,5.61,0,0,1-.77,7.9l-0.13.1a5.7,5.7,0,0,1-8-.9A5.63,5.63,0,0,1,64,5.6L62.4,4.1A8,8,0,0,0,74.1,15a7.84,7.84,0,0,0,.2-10.6Z" transform="translate(-40.24 -1.7)"/><path class="cls-1" d="M69.8,2.8A0.64,0.64,0,0,1,70.26,2L70.4,2h0.3l5.5,0.8c0.5,0.1.7,0.5,0.3,0.8l-5,5c-0.4.4-.8,0.3-0.8-0.3Z" transform="translate(-40.24 -1.7)"/></g></g></svg>
以下はループボタンのアイコンを擬似要素で背景画像として表示して変更する例です。
独自のスタイルシートに以下を記述し、background を非表示にして疑似要素で SVG を表示しています。サイズは環境により調整します。
.mejs__loop-button > button, .mejs-loop-button > button { /* background を非表示に */ background: none; } .mejs__loop-button.mejs__loop-on > button::before, .mejs-loop-button.mejs__loop-on > button::before { content: ""; display: inline-block; /* サイズは縦横同じ場合は 20px 前後が良さそうです */ height: 20px; width: 20px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='yellow' viewBox='0 0 16 16'%3E %3Cpath d='M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z'/%3E %3Cpath fill-rule='evenodd' d='M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z'/%3E%3C/svg%3E"); } .mejs__loop-button.mejs__loop-off > button::before, .mejs-loop-button.mejs__loop-off > button::before { content: ""; display: inline-block; height: 20px; width: 20px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23999999' viewBox='0 0 16 16'%3E %3Cpath d='M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z'/%3E %3Cpath fill-rule='evenodd' d='M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z'/%3E%3C/svg%3E"); }
アイコンの色は background-image: url() に指定した fill 属性の値で変更できます。「%23」は 「#」をエンコードしたものです。上記は以下のように表示されます。
以下は異なるループアイコンの例です(アイコンの SVG は Bootstrap Icons を利用)。
.mejs__loop-button > button, .mejs-loop-button > button { /* 非表示 */ background: none; } .mejs__loop-button.mejs__loop-on > button::before, .mejs-loop-button.mejs__loop-on > button::before { content: ""; display: inline-block; /* この場合、横長のアイコンなのでサイズが 20px だと途切れてしまう */ height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='lightgreen' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); } .mejs__loop-button.mejs__loop-off > button::before, .mejs-loop-button.mejs__loop-off > button::before { content: ""; display: inline-block; height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23cccccc' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); }
Playlist
Playlist プラグインを使用すると簡単にプレイリストを作成することができます。
- https://github.com/mediaelement/mediaelement-plugins/blob/master/docs/playlist.md
- https://github.com/mediaelement/mediaelement-plugins/blob/master/demo/playlist.html
Playlist プラグインを使用してプレイリストを作成するには Playlist プラグインの CSS(playlist.min.css) と JavaScript(playlist.min.js) を読み込みます。
<link rel="stylesheet" href="mediaelementplayer.min.css" /> <link rel="stylesheet" href="dist/playlist/playlist.min.css">
<script src="mediaelement-and-player.min.js"></script> <script src="dist/playlist/playlist.min.js"></script>
注意
Playlist プラグインにはループの機能があるので、Playlist プラグインを使用する場合は、Loop プラグインは必要ありません(使用しないことが推奨されています)。
HTML
HTML では source 要素に以下のよう属性を設定します。
- src:音声データの URL(必須)
- type:音声データのタイプ(必須)
- title:タイトル(必須)
- data-playlist-thumbnail:サムネイル画像
- data-playlist-description:説明文
<div class="media-wrapper"> <audio preload="none" controls width="450"> <source src="birds.mp3" type="audio/mp3" title="タイトル1" data-playlist-thumbnail="images/01.jpg" data-playlist-description="説明1"> <source src="stream.mp3" type="audio/mp3" title="タイトル2" data-playlist-thumbnail="images/02.jpg" data-playlist-description="説明2"> <source src="wave.mp3" type="audio/mp3" title="タイトル3" data-playlist-thumbnail="images/03.jpg" data-playlist-description="説明3"> </audio> </div>
初期化
初期化では、features に 'playlist'(プレイリストの表示に必須)と表示するコントロールを指定します。
const mejsPlayLists = document.querySelectorAll('.playlist'); mejsPlayLists.forEach((element) => { new MediaElementPlayer(element, { // features に配列で 'playlist' と表示するコントロールを指定します。 features: ['playlist', 'prevtrack', 'playpause', 'nexttrack', 'current', 'progress', 'duration', 'volume', 'loop'], }); });
以下のようなプレイリストが表示されます。リストは全部は表示されず、スクロールして見るようになっています。
他の audio 要素への影響
Playlist プラグインの CSS を読み込むと、Playlist を適用していない audio 要素にも以下のスタイルが適用されてしまいます。
.mejs__container.mejs__audio, .mejs-container.mejs-audio { min-height: 200px; }
例えば、他の audio 要素を通常の MediaElement として初期化している場合、以下のように高さが設定されてしまいます。
他の audio 要素に min-height を適用させないようにする1つの方法は、Playlist の要素以外のコンテナの min-height を 0 にします。
この例の場合、.playlist に Playlist を適用しているので、以下のように :not() を使ってそれ以外のコンテナの min-height を 0 にします。
.mejs__container.mejs__audio:not(.playlist) , .mejs-container.mejs-audio:not(.playlist) { min-height: 0px; }
リスト部分を非表示にする
以下のようにコンテナの min-height を 0 にするとリスト部分を非表示にすることができます。
.mejs__container.mejs__audio, .mejs-container.mejs-audio { min-height: 0px; }
上記を指定すると、以下のようにリスト部分が非表示になり、現在再生中のサムネイル画像とタイトルのみが表示されます。
※ コンテナの min-height の値でリストの高さを調整することができます。
リスト部分を全て表示する
リスト部分を全て表示する例です。
初期化の際に、success のコールバックで各要素の高さを取得してコンテナの min-height に設定しています(もっと良い方法があるかも知れません)。
// プレイリストの対象の要素 const mejsPlayLists = document.querySelectorAll('.playlist'); mejsPlayLists.forEach((element) => { new MediaElementPlayer(element, { features: ['prevtrack', 'playpause', 'nexttrack', 'current', 'progress', 'duration', 'volume', 'playlist', 'loop'], success: function (mediaElement, originalNode, instance) { // コンテナ(.mejs__container) const mejs_container = instance.container; // レイヤ(.mejs__layers) const mejs_layers = instance.layers; // リスト項目 const listItems = mejs_layers.querySelectorAll('li'); // リスト項目の高さを入れる変数の初期化 let listHeight = 0; // 各リスト項目の高さの合計を算出 listItems.forEach((item) => { listHeight += item.clientHeight; }) // .mejs__playlist-current の高さ const mejs_playlist_current_height = mejs_layers.querySelector('.mejs__playlist-current').clientHeight; // .mejs__controls の高さ const mejs_controls_height = instance.controls.clientHeight; // 全体の高さを算出してコンテナの min-height に適用 mejs_container.style.setProperty('min-height', `${listHeight + mejs_playlist_current_height + mejs_controls_height}px`); } }); });
例えば、以下のように表示されます。
リスト部分をクリックした際のエラー
コントロールの再生ボタンやスキップボタンをクリックした場合は問題ありませんが、リスト部分をクリックして再生すると以下のようなエラーが表示されてしまいます。
Uncaught (in promise) DOMException: The play() request was interrupted by a call to pause(). mediaelement-and-player.min.js:12
プレイリストの作成(サンプル)
以下は MediaElement を使用してプレイリストを独自に作成する例(サンプル)です。
再生ボタンをクリックすると音が出ます。
ループボタンを使用するので Loop プラグインの CSS(loop.min.css)と JavaScript(loop.min.js)を追加で読み込みます。
HTML ではカスタムデータ属性を使って以下の値を指定します。
- data-playlist-title:プレイリスト上部に表示するタイトル
- data-audio-src:各トラックのリストのタイトル
<ul class="audio-list" data-playlist-title="Sound of Nature"> <li class="track-title" data-audio-src="birds.mp3"> <a href="../birds.html">鳥のさえずり</a> </li> <li class="track-title" data-audio-src="stream.mp3"> <a href="../stream.html">小川のせせらぎ</a> </li> <li class="track-title" data-audio-src="wave.mp3"> <a href="../wave.html">波の音</a> </li> </ul>
JavaScript ではプレイリストを表示する関数を定義して、対象の要素に適用します。
document.addEventListener('DOMContentLoaded', () => { // 対象の要素を取得(この場合は .audio-list) const audioLists = document.querySelectorAll('.audio-list'); audioLists.forEach((list) => { //それぞれについて以下で定義した関数を適用 setupPlayList(list); }); // プレイリストを表示する関数(引数にはリストの要素を指定) function setupPlayList(targetList) { // オーディオプレーヤーをラップする div 要素(ラッパー)を作成 const playList = document.createElement('div'); // クラス属性を付与 playList.className = 'playlist'; // targetList の前にラッパーを挿入 targetList.parentNode.insertBefore(playList, targetList); // targetList をラッパーに追加 playList.appendChild(targetList); // プレイリストのタイトルや再生中のタイトル、コントロール部分(audio 要素)の HTML const htmls = `<h3 class="playlist-title"></h3> <div class="now-playing"> <div class="current-title"></div> <div class="audio-controls"> <audio controls></audio> </div> </div> <div class="tracks"></div>`; // 上記を insertAdjacentHTML で playList に追加 playList.insertAdjacentHTML('afterbegin', htmls); // targetList をトラック(.tracks)に追加 playList.querySelector('.tracks').appendChild(targetList); // プレイリストのタイトルを表示する要素を取得 const titleTag = playList.querySelector('.playlist-title'); // 引数 targetList の data-playlist-title の値をプレイリストのタイトルに titleTag.textContent = targetList.dataset.playlistTitle; // 全てのトラック(li 要素)を取得 const tracks = playList.querySelectorAll('.tracks li'); // 選択されたトラックを表す変数の初期化 let currentTrack = tracks[0]; // audio 要素 const audio = playList.querySelector('audio'); audio.src = currentTrack.dataset.audioSrc; // MediaElement で audio 要素を初期化 new MediaElementPlayer(audio, { stretching: 'responsive', iconSprite: '../audio/mediaelement/mejs-controls.svg', features: ['playpause', 'playlist', 'current', 'progress', 'duration', 'volume', 'loop'], }); const currentTitle = playList.querySelector('.now-playing .current-title'); currentTitle.textContent = currentTrack.textContent; // 各トラックで tracks.forEach((track) => { // ボタンを表示する要素をタイトルのテキストの前に追加 const playBtnElem = `<button class="play" type="button" aria-label="Play"></button>`; track.insertAdjacentHTML('afterbegin', playBtnElem); // 再生ボタン const playBtn = track.querySelector('button.play'); // 再生ボタンをクリックした際の処理 playBtn.addEventListener('click', (e) => { // 再生・一時停止を実行する関数を呼び出す toggleBtn(track); }, false); }); // 音声データを再生する非同期関数(Async Function) async function playAudio(track) { try { // 異なるトラック(項目)が選択された場合 if (track !== currentTrack) { audio.src = track.dataset.audioSrc; const btn = track.querySelector('button.play'); //currentTrack を更新(awaitの後ではplayイベントでcurrentTrackの検出が遅れる) currentTrack = btn.parentElement; } await audio.play(); // currentTitle を更新 currentTitle.textContent = currentTrack.textContent; } catch (err) { console.warn(err); } } // トグルボタンのリスナー function toggleBtn(track) { if (audio.paused) { // 停止中であれば setupAndPlay() を呼び出して再生 playAudio(track); } else { if (track === currentTrack) { // 再生中であれば停止 audio.pause(); } else { // 全てのトラックのボタンを初期状態にリセット reseAllBtns(); // 再生 playAudio(track); } } } // 全てのトラックのボタンを初期状態にリセットする関数 function reseAllBtns() { tracks.forEach((track) => { // リストのボタン const btn = track.querySelector('button.play'); btn.setAttribute('aria-label', 'Play'); btn.classList.remove('playing'); }); } // play イベントでボタンのクラスと aria-label を変更 audio.addEventListener('play', (e) => { const btn = currentTrack.querySelector('button.play'); btn.classList.add('playing'); btn.setAttribute('aria-label', 'Pause'); }, false); // pause イベントで全てのトラックのボタンを初期状態にリセット audio.addEventListener('pause', (e) => { reseAllBtns(); }, false); }; });
.mejs__container { background-color: #496096; } .mejs__container:focus { /* フォーカス時のコンテナのアウトラインを非表示に */ outline: none; } .mejs__controls:not([style*='display: none']) { background: transparent; } .mejs__loop-button>button, .mejs-loop-button>button { /* 独自のループアイコンを表示するため元のアイコンを非表示に */ background: none; } .mejs__loop-button.mejs__loop-on>button::before, .mejs-loop-button.mejs__loop-on>button::before { content: ""; display: inline-block; height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='lightgreen' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); } .mejs__loop-button.mejs__loop-off>button::before, .mejs-loop-button.mejs__loop-off>button::before { content: ""; display: inline-block; height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23cccccc' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); } .playlist { position: relative; margin: 50px 0; width: 100%; max-width: 480px; border: 1px solid #eee; padding: 0; } .playlist h3 { margin: 0 0 1px; font-size: 16px; background-color: #eee; padding: 0.5rem; } .playlist .tracks ul { list-style-type: none; margin: 0; padding: 0; } .playlist .tracks ul li { display: flex; flex-wrap: nowrap; align-items: center; margin: 2px 0; background-color: #eee; padding: 0.5rem 0.25rem; } .playlist .tracks ul li:last-of-type { margin-bottom: 0; } .playlist .tracks .track-title { font-size: 14px; } .playlist .tracks button { cursor: pointer; border: none; background-color: transparent; position: relative; } .playlist .tracks button.play::before { content: ""; display: inline-block; height: 30px; width: 30px; vertical-align: -10px; margin-right: 8px; background-repeat: no-repeat; } .playlist .tracks ul li .controls { display: flex; flex-wrap: nowrap; gap: 10px; margin-left: 20px; } .playlist .tracks ul li .controls .current-time, .playlist .tracks ul li .controls .duration { font-size: 13px; color: #999; } .playlist .now-playing { padding: 10px; background-color: #496096; color: #efefef; } .playlist .current-title { margin-left: 1rem; font-size: 12px; color: #4d6fbe; color: #fff; } /* Play ボタン*/ .playlist .tracks button.play::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E"); /* transition: background-image .1s; */ } /* Play ボタン :hover*/ .playlist .tracks button.play:hover::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E"); } /* Pause ボタン */ .playlist .tracks button.play.playing::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E"); } .audio-wrapper { max-width: 480px; margin: 30px 0; } a { text-decoration: none; }
詳細は以下を御覧ください。
以下は前後のトラックにスキップするボタンとリストのループを追加した例です。
HTML はプラグインの Playlist のように audio 要素と source 要素でマークアップします。
<audio class="audio-list2" data-playlist-title="自然の音"> <source src="birds.mp3" data-track-title="鳥のさえずり" data-href="#"> <source src="stream.mp3" data-track-title="小川のせせらぎ" data-href="#"> <source src="wave.mp3" data-track-title="波の音" data-href="#"> </audio>
但し、この場合、個別のループを指定しても機能しない(次のトラックに移動してしまう)ので個別のループボタンは features で表示しないようにしています。
また、前の例では、プレイリストを表示する関数の中で MediaElement を使って audio 要素を初期化していますが、この例では別途 audio 要素を MediaElement を使って初期化する関数を定義してプレイリストを表示する関数に渡すようにしています。
const playLists2 = document.querySelectorAll('.audio-list2'); playLists2.forEach((elem) => { // 対象の要素をプレイリストで表示 setupAudioPlayList2(elem, setupMejsAudio); }); /** * MediaElementPlayer で初期化する関数(setupAudioPlayList2 の第2引数に指定) * @param {HTMLElement} elem audio 要素 ※必須 */ function setupMejsAudio(elem) { const mejsPlayer = new MediaElementPlayer(elem, { stretching: 'responsive', // loop は指定しない(機能しないので) features: ['playpause', 'playlist', 'current', 'progress', 'duration', 'volume'], }); } /** * プレイリストの ul 要素を受け取りコントロールを生成する関数 * 別途定義した audio 要素をカスタマイズする関数を第2引数 func に指定 * @param {HTMLElement} audio 2つ以上の source 要素を持つ audio 要素 ※必須 * @param {Function} func audio 要素をカスタマイズする関数 */ function setupAudioPlayList2(audio, func) { // 第1引数の audio が存在しない場合やそれが audio 要素でなければ終了 if (!audio || audio.tagName !== 'AUDIO') { console.warn('Parameter 1 shoudl be audio element'); return; } // source 要素が2未満の場合は終了 if (audio.querySelectorAll('source').length < 2) { console.warn('Need at least two source element for the list'); return; } // オーディオプレーヤーをラップする div 要素(ラッパー)を作成 const playList = document.createElement('div'); playList.className = 'playlist'; // audio(引数に受け取った audio 要素)の前にラッパーを挿入 audio.parentNode.insertBefore(playList, audio); // audio をラッパーに追加 playList.appendChild(audio); // 再生中のタイトル、スキップボタン、リストのループボタン、audio 要素の HTML const htmls = `<h3 class="playlist-title"></h3> <div class="now-playing"> <div class="current-title"></div> <div class="audio-controls"> <div class="skip-controls"> <button class="skip-backward skip-backward-btn" type="button" aria-label="Skip Backward"></button> <button class="skip-forward skip-forward-btn" type="button" aria-label="Skip Forward"></button> </div> </div> <div class="list-control"><button class="list-loop list-loop-btn" type="button" aria-label="Loop List"></button></div> </div> <div class="tracks"> <ul></ul> </div>`; playList.insertAdjacentHTML('afterbegin', htmls); // .audio-controls に audio を追加(移動) playList.querySelector('.audio-controls').appendChild(audio); // controls 属性を true にして表示 audio.controls = true; //.tracks ul 要素を取得(上記 htmls で追加) const trackList = playList.querySelector('.tracks ul'); // source 要素を全て取得 const sources = audio.querySelectorAll('source'); // 各 source 要素について sources.forEach((source) => { // li 要素を作成 const li = document.createElement('li'); // li 要素の data-audio-src 属性に source の src を設定 li.setAttribute('data-audio-src', source.src); if (source.dataset.href) { // data-href 属性が指定されている場合はリンク(a 要素)を生成 const link = document.createElement('a'); // source の data-track-title のテキストをリンクで囲み li 要素の HTML とする li.innerHTML = `<a href="${source.dataset.href}">${source.dataset.trackTitle}</a>` } else { // li 要素のテキストに source の data-track-title を設定 li.textContent = source.dataset.trackTitle; } // リスト(.tracks ul)に li 要素を追加 trackList.appendChild(li); }); // プレイリストのタイトルを表示する要素を取得 const titleTag = playList.querySelector('.playlist-title'); // 引数 audio の data-playlist-title の値をプレイリストのタイトルに titleTag.textContent = audio.dataset.playlistTitle; // 全てのトラック(li 要素)を取得 const tracks = playList.querySelectorAll('.tracks li'); // 選択された(再生中の)トラックを表す変数の初期化 let currentTrack = tracks[0]; // audio 要素に最初のトラックの音声データを指定 audio.src = currentTrack.dataset.audioSrc; // 引数に audio 要素をカスタマイズする関数が指定されていれば実行 if (func) { // 引数を渡して関数を実行 func(audio); } // 現在再生中のタイトルを表示する要素 const currentTitle = playList.querySelector('.now-playing .current-title'); // currentTrack の更新などを行う関数を実行して初期化 updateTrack(currentTrack); // 各トラックで tracks.forEach((track) => { // ボタンを表示する要素をタイトルのテキストの前に追加 const playBtnElem = `<button class="play" type="button" aria-label="Play"></button>`; track.insertAdjacentHTML('afterbegin', playBtnElem); // 各トラックに追加した再生ボタン const playBtn = track.querySelector('button.play'); // 上記ボタンをクリックした際の処理 playBtn.addEventListener('click', (e) => { // 再生・一時停止を実行する関数を呼び出す toggleBtn(track); }, false); }); // 音声データを再生する非同期関数(引数にトラックの li 要素を受け取る) async function playAudio(track) { try { // 現在再生中と異なるトラック(項目)が選択された場合 if (track !== currentTrack) { audio.src = track.dataset.audioSrc; const btn = track.querySelector('button.play'); //currentTrack を更新 currentTrack = btn.parentElement; } await audio.play(); // currentTitle を更新 currentTitle.textContent = currentTrack.textContent; // トラックの情報を更新 updateTrack(track); } catch (err) { console.warn(err); } } // 各トラックに追加した再生ボタンの click イベントのリスナー function toggleBtn(track) { if (audio.paused) { // 停止中であれば playAudio() を呼び出して再生 playAudio(track); } else { if (track === currentTrack) { // 再生中であれば停止 audio.pause(); } else { // 全てのトラックのボタンを初期状態にリセット reseAllBtns(); // 再生 playAudio(track); } } } // 全てのトラックのボタンを初期状態にリセットする関数 function reseAllBtns() { tracks.forEach((track) => { // トラックのボタン const btn = track.querySelector('button.play'); btn.setAttribute('aria-label', 'Play'); btn.classList.remove('playing'); }); } // play イベントでボタンのクラスと aria-label を変更 audio.addEventListener('play', (e) => { const btn = currentTrack.querySelector('button.play'); btn.classList.add('playing'); btn.setAttribute('aria-label', 'Pause'); }, false); // pause イベントで全てのトラックのボタンを初期状態にリセット audio.addEventListener('pause', (e) => { reseAllBtns(); }, false); // currentTrack の更新と track の li 要素のクラスの着脱及びタイトルの更新 function updateTrack(li) { tracks.forEach((track) => { track.classList.remove('active'); }); currentTrack = li; li.classList.add('active'); currentTitle.textContent = li.textContent; } // リストのループボタン const ListLoopBtn = playList.querySelector('.list-loop'); // ループが有効かどうかのフラグを定義して初期化 let isLoopActive = false; // ループボタンにクリックイベントを設定 ListLoopBtn.addEventListener('click', () => { if (isLoopActive) { isLoopActive = false; ListLoopBtn.classList.remove('looped'); // aria-label 属性の値を更新 ListLoopBtn.setAttribute('aria-label', 'Loop List'); } else { isLoopActive = true; ListLoopBtn.classList.add('looped'); // aria-label 属性の値を更新 ListLoopBtn.setAttribute('aria-label', 'Unloop List'); } }); // Skip Forward ボタン(次のオーディオへ進むボタン) const skipForwardBtn = playList.querySelector('.skip-forward'); // Skip Forward ボタンにクリックイベントを設定 skipForwardBtn.addEventListener('click', skipForward, false); function skipForward() { // 次の兄弟要素を取得 const nextTrack = currentTrack.nextElementSibling; // 次のトラックが存在すれば(兄弟要素があれば) if (nextTrack) { audio.pause(); updateTrack(nextTrack) // src に次のトラックを設定 audio.src = nextTrack.dataset.audioSrc; reseAllBtns(); playAudio(nextTrack); } else { // 次のトラックが存在しなければ先頭のトラックを再生 updateTrack(tracks[0]) // src に最初のトラックを設定 audio.src = tracks[0].dataset.audioSrc; reseAllBtns(); playAudio(tracks[0]); } } // Skip Backward ボタン(前のオーディオへ進むボタン) const skipBackwardBtn = playList.querySelector('.skip-backward'); // Skip Backward ボタンにクリックイベントを設定 skipBackwardBtn.addEventListener('click', skipFBackward, false); function skipFBackward() { const prevTrack = currentTrack.previousElementSibling; // 前のトラックが存在すれば(前の兄弟要素があれば) if (prevTrack) { audio.pause(); updateTrack(prevTrack) // src に前のトラックを設定 audio.src = prevTrack.dataset.audioSrc; reseAllBtns(); playAudio(prevTrack); } else { updateTrack(tracks[tracks.length - 1]) // src に最後のトラックを設定 audio.src = tracks[tracks.length - 1].dataset.audioSrc; reseAllBtns(); playAudio(tracks[tracks.length - 1]); } } // .audio-player(音声プレーヤー)のトグルボタン(カスタマイズする関数が適用されている場合) const audioToggleBtn = playList.querySelector('.audio-player .controls .toggle'); // 再生終了時に発火するイベント audio.addEventListener('ended', audioEnded, false); function audioEnded() { const nextTrack = currentTrack.nextElementSibling; // 次のトラックが存在すれば if (nextTrack) { audio.pause(); updateTrack(nextTrack); // src に次のトラックを設定 audio.src = nextTrack.dataset.audioSrc; // 次のトラックを再生 playAudio(nextTrack); } else { if (!isLoopActive) { // カスタマイズする関数が適用されている場合 if (audioToggleBtn) { // .audio-player(音声プレーヤー)のトグルボタンのクラスの削除と aria-label の変更 audioToggleBtn.classList.remove('playing'); audioToggleBtn.setAttribute('aria-label', 'Play'); } } else { audio.pause(); updateTrack(tracks[0]); // src に次のトラック(先頭のトラック)を設定 audio.src = tracks[0].dataset.audioSrc; playAudio(tracks[0]); } } } };
/* 148行目までは前述の例と同じ */ .mejs__container { background-color: #496096; } .mejs__container:focus { outline: none; } .mejs__controls:not([style*='display: none']) { background: transparent; } .mejs__loop-button>button, .mejs-loop-button>button { background: none; } .mejs__loop-button.mejs__loop-on>button::before, .mejs-loop-button.mejs__loop-on>button::before { content: ""; display: inline-block; height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='lightgreen' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); } .mejs__loop-button.mejs__loop-off>button::before, .mejs-loop-button.mejs__loop-off>button::before { content: ""; display: inline-block; height: 18px; width: 18px; background-repeat: no-repeat; background-size: contain; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23cccccc' viewBox='0 0 16 16'%3E %3Cpath d='M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192Zm3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z'/%3E%3C/svg%3E"); } .playlist { position: relative; margin: 50px 0; width: 100%; max-width: 480px; border: 1px solid #eee; padding: 0; } .playlist h3 { margin: 0 0 1px; font-size: 16px; background-color: #eee; padding: 0.5rem; } .playlist .tracks ul { list-style-type: none; margin: 0; padding: 0; } .playlist .tracks ul li { display: flex; flex-wrap: nowrap; align-items: center; margin: 2px 0; background-color: #eee; padding: 0.5rem 0.25rem; } .playlist .tracks ul li:last-of-type { margin-bottom: 0; } .playlist .tracks .track-title { font-size: 14px; } .playlist .tracks button { cursor: pointer; border: none; background-color: transparent; position: relative; } .playlist .tracks button.play::before { content: ""; display: inline-block; height: 30px; width: 30px; vertical-align: -10px; margin-right: 8px; background-repeat: no-repeat; } .playlist .tracks ul li .controls { display: flex; flex-wrap: nowrap; gap: 10px; margin-left: 20px; } .playlist .tracks ul li .controls .current-time, .playlist .tracks ul li .controls .duration { font-size: 13px; color: #999; } .playlist .now-playing { padding: 10px; background-color: #496096; color: #efefef; } .playlist .current-title { margin-left: 1rem; font-size: 12px; color: #4d6fbe; color: #fff; } /* Play ボタン*/ .playlist .tracks button.play::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E %3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E %3Cpath d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445z'/%3E%3C/svg%3E"); /* transition: background-image .1s; */ } /* Play ボタン :hover*/ .playlist .tracks button.play:hover::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%234d6fbe' viewBox='0 0 16 16'%3E %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'/%3E%3C/svg%3E"); } /* Pause ボタン */ .playlist .tracks button.play.playing::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23eb213c' class='bi bi-pause-circle-fill' viewBox='0 0 16 16'%3E %3Cpath d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'/%3E%3C/svg%3E"); } .audio-wrapper { max-width: 480px; margin: 30px 0; } a { text-decoration: none; } /* 以下前述の例に追加した部分 */ @media screen and (min-width: 480px) { .playlist .audio-controls { display: flex; align-items: center; } } /* プレイリストの中の音声プレーヤー */ .playlist .now-playing .audio-player { padding: 0; } /* スキップボタンのラッパー */ .skip-controls { display: flex; } /* スキップボタン */ .playlist .skip-controls button, .playlist .list-control button { cursor: pointer; border: none; background-color: transparent; position: relative; } .playlist .skip-controls button::before, .playlist .list-control button::before { content: ""; display: inline-block; height: 20px; width: 20px; vertical-align: -10px; margin-right: 8px; background-repeat: no-repeat; } /* Skip Forward ボタン */ .playlist .now-playing button.skip-forward-btn::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' class='bi bi-skip-forward-fill' viewBox='0 0 16 16'%3E %3Cpath d='M15.5 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V8.753l-6.267 3.636c-.54.313-1.233-.066-1.233-.697v-2.94l-6.267 3.636C.693 12.703 0 12.324 0 11.693V4.308c0-.63.693-1.01 1.233-.696L7.5 7.248v-2.94c0-.63.693-1.01 1.233-.696L15 7.248V4a.5.5 0 0 1 .5-.5z'/%3E%3C/svg%3E"); } /* Skip Backward ボタン */ .playlist .now-playing button.skip-backward-btn::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' class='bi bi-skip-backward-fill' viewBox='0 0 16 16'%3E %3Cpath d='M.5 3.5A.5.5 0 0 0 0 4v8a.5.5 0 0 0 1 0V8.753l6.267 3.636c.54.313 1.233-.066 1.233-.697v-2.94l6.267 3.636c.54.314 1.233-.065 1.233-.696V4.308c0-.63-.693-1.01-1.233-.696L8.5 7.248v-2.94c0-.63-.692-1.01-1.233-.696L1 7.248V4a.5.5 0 0 0-.5-.5z'/%3E%3C/svg%3E"); } /* List Loop ボタン */ .playlist .list-control button.list-loop-btn::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E %3Cpath d='M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z'/%3E %3Cpath fill-rule='evenodd' d='M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z'/%3E%3C/svg%3E"); } .playlist .list-control button.list-loop-btn.looped::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f3a5d2' viewBox='0 0 16 16'%3E %3Cpath d='M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z'/%3E %3Cpath fill-rule='evenodd' d='M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z'/%3E%3C/svg%3E"); }
個別のループを有効にする
前の例の場合、単に features で loop を指定して loop ボタンを表示しても、個別のトラックのループは機能しません(loop プラグインのループは loop 属性の操作によるものではないため?)。
個別のトラックのループが機能するようにするには、audio 要素を MediaElement で初期化する際にコールバック関数で loop 属性を操作します。
前の例の audio 要素を MediaElement で初期化する関数を以下のように書き換えると、個別のループを有効にすることができます。
function setupMejsAudio(elem) { const mejsPlayer = new MediaElementPlayer(elem, { stretching: 'responsive', // loop を追加 features: ['playpause', 'playlist', 'current', 'progress', 'duration', 'volume', 'loop'], // コールバックを追加 success: function (mediaElement, originalNode, instance) { // プレーヤーのループボタン const loopBtn = instance.controls.querySelector('.mejs__loop-button button') // 現在トラックのループが有効化どうかのフラグ let isLooped = false; // ループボタンに click イベントを設定 loopBtn.addEventListener('click', ()=> { // フラグを反転 isLooped = !isLooped; // loop 属性をフラグの値に originalNode.loop = isLooped; }) } }); }