Twitterに要件定義が転がっていたのでIntersectionObserverを用いて実装しました。
以下のコードを記事のどこかに貼り付けると、画面右下に自動再生ボタンが表示されます。自動再生ボタンを押すと、画面で見えている「iTunes商品紹介」を自動で再生するようになります。
<span></span><style> #autoplay-btn{color:gainsboro;background-color:#fff;background-size:cover;position:fixed;bottom:5%;right:10%;width:60px;height:60px;border:5px solid currentColor;border-radius:100%;cursor:pointer;z-index:10000}#autoplay-btn.disabled{opacity:.3;cursor:not-allowed}#autoplay-btn:before,#autoplay-btn:after{content:"";position:absolute;top:10px;left:30%;border-top:20px solid transparent;border-left:35px solid currentColor;border-bottom:20px solid transparent}#autoplay-btn.autoplay{border-style:inset}#autoplay-btn.autoplay:before,#autoplay-btn.autoplay:after{opacity:.5;height:40px;border-width:0 6px 0 6px;border-color:transparent currentColor transparent currentColor}#autoplay-btn.autoplay:after{left:60%} </style> <div id="autoplay-btn" class="disabled"></div> <script> let observer=null,autoplay=!1;document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("autoplay-btn");e.classList.remove("disabled"),e.addEventListener("click",e=>{autoplay?autoplayOff(e.target):autoplayOn(e.target)});let t=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting?e.style.display="block":(autoplayOff(e),e.style.display="none")})});t.observe(e.parentElement)});const autoplayOn=e=>{autoplay=!0,e.classList.add("autoplay");let t=e.parentNode.getElementsByTagName("audio");for(let a of t)a.load(),a.loop=!0;for(let o of(observer=new IntersectionObserver(a=>{let o=null;if(a.forEach(e=>{(!e.target.paused||e.isIntersecting)&&(o=e.target)}),null!==o){for(let l of t)l.isSameNode(o)||l.pause();o.play();let s=o.closest(".itunes-embed").querySelector("img");e.style.backgroundImage=s?`url(${s.getAttribute("src")})`:"none"}},{rootMargin:"-30% 0px"}),t))o.load(),observer.observe(o)},autoplayOff=e=>{autoplay=!1,e.classList.remove("autoplay"),e.style.backgroundImage="none",observer&&observer.disconnect();let t=e.parentNode.getElementsByTagName("audio");for(let a of t)a.pause()}; </script>
俺のPCと俺のスマホの俺のブラウザで動作します。また、はてなブログの埋め込み機能に依存しているため、そのうち動かなくなるかもしれません。
この記事には合間に楽曲を配置しておきます。せっかくなので自動再生してみてね。
- ぶっ壊れてもいいから導入を楽にする
- CSSとHTML
- ボタンの活性化
- ブラウザによるaudio要素の自動再生ブロック
- IntersectionObserver
- 停止処理
- 実装はおしまい!
- 2023/2/13:追記
ぶっ壊れてもいいから導入を楽にする
「iTunes商品紹介」に依存している理由は導入時のハードルを下げるためです。
例えばdivで囲むとかめんどうじゃないですか?
Distortion!! / 結束バンド <div class="autoplay"> [https://music.apple.com/jp/album/distortion/1646030812?i=1646030817&uo=4:embed] </div> Distortion!!は素晴らしいので素晴らしいですね。Distortion!!は〜
共有の埋め込みコードに手を加えるなんてもってのほか。「コードをコピペすれば自動再生ができるようになる」というのが一番楽だなと思ってこの方針にしました。
次節以降に実装の詳細は載せますが、再生コンテンツはaudio要素で特定しています。そうすると余計なaudio要素が混入する可能性があるので、自動再生ボタンの親ノード配下のみを対象とすることで、お気持程度ですが意図せぬコンテンツの再生を防ぎます。
const audios = btn.parentNode.getElementsByTagName("audio");
CSSとHTML
ふいんきで書いたのでとくに触れることはありません。
<span></span><style> #autoplay-btn { color: gainsboro; background-color: white; background-size: cover; position: fixed; bottom: 5%; right: 10%; width: 60px; height: 60px; border: 5px solid currentColor; border-radius: 100%; cursor: pointer; z-index: 10000; } #autoplay-btn.disabled { opacity: 0.3; cursor: not-allowed; } #autoplay-btn:before, #autoplay-btn:after { content: ""; position: absolute; top: 10px; left: 30%; border-top: 20px solid transparent; border-left: 35px solid currentColor; border-bottom: 20px solid transparent; } #autoplay-btn.autoplay { border-style: inset; } #autoplay-btn.autoplay:before, #autoplay-btn.autoplay:after { opacity: 0.5; height: 40px; border-width: 0 6px 0 6px; border-color: transparent currentColor transparent currentColor; } #autoplay-btn.autoplay:after { left: 60%; } </style>
<div id="autoplay-btn" class="disabled"></div> <script> <!-- ここにJavaScriptを書く --> </script>
CSSにコメントを書いたり空行を混入させたりすると、意図せぬ動作となることがあったので省いています。先頭の謎のspan要素も記事内でstyleタグを読み込ませるために必要です。このようなはてなブログ独自のものはとくに深掘りしません。
ボタンの活性化
let observer = null; let autoplay = false; document.addEventListener("DOMContentLoaded", () => { const autoplayBtn = document.getElementById("autoplay-btn"); autoplayBtn.classList.remove("disabled"); autoplayBtn.addEventListener("click", (e) => { autoplay ? autoplayOff(e.target) : autoplayOn(e.target); }); // 記事がビューポートから出たら自動再生ボタンを非表示にする const autoplayBtnObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { autoplayBtn.style.display = "block"; } else { autoplayOff(autoplayBtn); autoplayBtn.style.display = "none"; } }); }); autoplayBtnObserver.observe(autoplayBtn.parentElement); })
生のJavaScriptで状態をどのように保持したらいいのかよく分かりません。Reactがいないと私は何もできない…Reactがダイスキ。
ボタンの活性化はaudio要素のマウントを待ちたいので、DOMContentLoaded
が発火してからeventListenerを付与し、disabled状態を解除します。
あとで出てくるIntersectionObserverをここでも利用して、記事がビューポートを出たら自動再生ボタンを非表示としています。
ブラウザによるaudio要素の自動再生ブロック
autoplayOn
の途中まで。
const autoplayOn = (btn) => { autoplay = true; btn.classList.add("autoplay"); // ユーザーアクションからloadすることで、JSでのaudio.play()を有効にする const audios = btn.parentNode.getElementsByTagName("audio"); for (const audio of audios) { audio.load(); audio.loop = true; };
ブラウザによっては、audio要素のautolplay機能およびJavaScriptからのplay
メソッドによる自動再生を制限しています。
メディアおよびウェブ音声 API の自動再生ガイド - ウェブメディア技術 | MDN
Chromeでのエラー。
Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.
これを避けるために「自動再生ボタンをクリック」というユーザーアクションをトリガーにすべてのaudio要素をloadしておくことで、以降のIntersectionObserverのコールバック内でplay
メソッドを利用できるようにしています。
ループ処理は久々にfor...of
で書きました。querySelectorAll
の戻り値はNodeList
であり、配列ではないけどforEach
が利用できるのに対して、getElementsByTagName
の戻り値はHTMLCollection
であり、forEach
が実装されていないので渋々です。
IntersectionObserver
バグではなく普通に2回目のDistortion!!です。autoplayOn
の続き。
observer = new IntersectionObserver((entries) => { let activeAudio = null; entries.forEach((entry) => { // 再生中または交差中のうち最後尾の要素を再生対象とする if (!entry.target.paused || entry.isIntersecting) { activeAudio = entry.target; } }); if (activeAudio !== null) { for (const audio of audios) !audio.isSameNode(activeAudio) && audio.pause(); activeAudio.play(); // 自動再生ボタンの背景画像を設定する const img = activeAudio.closest(".itunes-embed").querySelector("img"); btn.style.backgroundImage = img ? `url(${img.getAttribute("src")})` : "none"; } }, { rootMargin: "-30% 0px" }); for (const audio of audios) { audio.load(); observer.observe(audio); };
今回の主役です。
IntersectionObserver - Web API | MDN
第二引数を渡さない場合、ビューポート内にaudio要素が出現するとコールバックしますが、それだとフライングしてしまう可能性があります。
そこで、rootMargin: "-30% 0px"
を指定することで、ビューポートを3割超過したあたりでコールバックするよう判定ラインを調整しています。ただ、上から読んでいたらフライング問題はあまり発生しないかもしれません。
このoptionsですが、はてなブログプレビュー画面では有効にならなかったため、めちゃくちゃ時間を溶かしました。疲れたので原因は調べていません。プレビュー画面はiframeで埋め込まれているので動作が違うのでしょう。公開後はちゃんと機能しているのでもういいです。*1
停止処理
ただの停止処理です。
const autoplayOff = (btn) => { autoplay = false; btn.classList.remove("autoplay"); btn.style.backgroundImage = "none"; observer && observer.disconnect(); const audios = btn.parentNode.getElementsByTagName("audio"); for (const audio of audios) audio.pause(); };
実装はおしまい!
実際に導入するのであれば
- 複数記事が同時に1ページに表示されないようにする
- コードを記事ではなくブログのheadに追加する
などの対応をした方がいいです。
前者。例えば私のはてなブログ記事のトップページには複数の記事が表示されますが、ボタンのidが一意とならないため、ぶっ壊れるかも知れません。はてなブログProだとトップページに表示する記事を1つにできた気がします。
後者。ぶっ壊れたときにすべての記事のコードを修正するのはありえないので、head内にぶちこむなどを検討しなければいけません。はてなブログProだとPC・スマホのどちらでも共有スクリプトを配置できた気がします。
そういえば今日は私の誕生日です、はてなブログProのプレゼントを心よりお待ちしております。
2023/2/13:追記
要件定義元のめがねこさんが、私が勝手に実装した自動再生ボタンを自身のブログに配置してくれました。なんて心の広い。こちらも見てね。
あと、せっかくなので私のブログにも配置しました。過去の音楽関連の記事はなるたけiTunes商品紹介に差し替えておきます。こちらも見てね。
*1:バグがあるシステムもリリースしたらなおってるかも…?