LoginSignup
6
7

More than 3 years have passed since last update.

Vue.jsと<input type=range>でSoundCloudみたいなシークバーを作る

Last updated at Posted at 2019-07-15

挨拶

こんにちはkaijiです。
最初に、初投稿かつ今までWebをほとんどやったことのない人なので、色々と間違っていたり不足している部分があるかと思います。もしそういった点があった時はコメントで優しく教えてもらえると嬉しいです。

経緯・注意

個人で開発しているサイトでオーディオを再生するためシークバーを実装しようとして色々調べてたのですが、HTML5のAudioタグで作っていたり、inputの標準のUIで作っているものだったり、オリジナルの見た目で作っていてもプログレスバーまで作っているものはほとんど見当たらなかったので、Vue.js上でJavascriptのWeb Audio APIとinputタグを使って自分で作ってみました。
ここでは、シークバーを作ることをメインにしているので、再生、停止、ボリューム調整等の基本的な動作に関しては他のサイトとかを参考にしてください。

全体的な流れ

  1. シークバーの見た目を作る
  2. 実際にシークさせてみる
  3. 現在の再生時間と、全体の再生時間を表示させる(分:秒表示)
  4. 再生済み用のプログレスバーを作り、元のinputタグに重ねる

といった感じです。

シークバーの見た目を作る

シークバーの見た目に関しては、基本好きなように作ってねっていう感じなんですが、いくつか注意点があるので、それについて書きます。

まずinputタグにはデフォルトで見た目が設定されています。(Mac版Chromeの場合はこんな感じ↓)
スクリーンショット 2019-07-15 23.06.19.png
ですが、これだとブラウザごとに見た目が変わってしまったり、サイトによってはデザイン的に浮いてしまうことがあるため(ちなみにこれはHTML5のオーディオタグにも言える)今回は自分でCSSを書いて見た目を作っていくのですが、その前にこのデフォルトの見た目を表示させないように<style>内に以下のCSSを書いてみましょう。

input[type=range] {
    -webkit-appearance: none;
}

input[type=range]::-webkit-slider-thumb {

}

input[type=range]::-ms-tooltip {
    display:none;

}

input[type=range]::-moz-range-track {

}

input[type=range]::-moz-range-thumb {

}

ここではinput[type=range]の見た目を表示させないでくださいと各ブラウザ用に書いています。
こうすることで、真っさらなinput[type=range]ができたので、ここからは実際に見た目を作っていきます。
なぜ-msだけthumbの設定がないのかや、-mozには何も書いていないのか等は以下のページにわかりやすく書かれていたので、そちらを参考にしてみてください。

input type=range タグをカスタマイズするために

実際にシークさせてみる

次は実際にシークをさせてみましょう。
ここではWeb Audio API上でオーディオファイルが再生できる状態になっていることを前提に話をするので、まだ実装できてない人は他の投稿やリファレンス等をみて実装してからもう一度来てください。

今回はVue.jsを使っているので双方向データバインディングのできるv-modelを現在の再生時間、v-bindを全体の再生時間を表示させるために使っていきます。

まず<script>内でオーディオのcurrentTime(現在の再生時間)とdurationTime(全体の再生時間)を取得します。

const audio = new Audio

export default {
    data() {
        return {
            currentTime: 0,
            durationTime: 0
        }
    },

    mehods:{
        play() {
            audio.src = //オーディオのURLとか

            audio.addEventListener("loadedmetadata", function () {
                return {
                    durationTime: audio.duration.toFixed(0)
                }
            });
            audio.addEventListener("timeupdate", function () {
                return {
                    currentTime: audio.currentTime.toFixed(0)
                }
            });
            audio.addEventListener("ended", function () {
                return {
                    currentTime: 0,
                    durationTime: 0
                }
            });
            audio.play();
        },
        seek() {
            audio.currentTime = this.currentTime;
        }
    }
} 

ここでやっていることはaudio.srcで取得したオーディオデータからaddEventListenerを使ってメタデータを抽出できたタイミングでaudio.durationTimeを取得し、その値を返させています。
それと同じように再生中にaudio.currrentTimeを取得しその値を返させています。
また再生が終了したら両方の値を0(初期値)にしています。

そして下に書かれているseek()というメソッドではv-modelの特性である双方向データバインディングを使ってthumbを移動させるたびにその位置(時間)まで実際に再生されている音源のaudio.currentTimeを移動させています。

またaudio.durationTimeaudio.currrentTimeを取得するときtoFixed(0)と書いていますが、これは取得してきた値を整数に変換しています。
なぜこんなことをするのかというと、標準で取得してくる再生時間はミリ秒(1/1000秒)で表されているため、今回作るようなプレイヤーの場合あまり適している形とは言えません。
そのため今回は整数で表すようにしていますが、もし作るプレイヤーがミリ秒まで表示できるものであって欲しいならtoFixed(n)を書く必要はありませんし、toFixed(n)はnの値を変えることによって0.1秒単位(その場合nは1になる)などもっと細かい値にすることも可能なので、自分の用途に合わせて調整してみてください。
詳細は下記のURLから見てください

Number.prototype.toFixed()

では次に<template>内に記述していきます。

<a v-on:click=play>再生</a>
<input type="range" v-model="currentTime" v-on:input="seek" v-bind:max="durationTime"/>

最初のaタグやinputのtype="range"は単に処理を呼び出したり、inputのタイプを指定しているだけなので気にしないでください。

まずv-modelを使ってcurrentTimeを取得しています。なんども言っていますが、v-modelは双方向データバインディングが可能なため、値が変化する度にリロード等の処理をせず、直接表示される値を変化させることが可能です。
そしてv-on:input="seek"はシークバーに触れる(thumbが動く)度に先ほど記述したseek()メソッドが呼び出されます。
最後にv-bind:max="durationTime"はシークの最大値をdurationTimeにしています。そのほか最小値やステップは記述していないためデフォルトの値が使われますので、これも必要に応じて設定してみてください。

現在の再生時間と、全体の再生時間を表示させる(分:秒表示)

v-modelを使ったcurrentTime(現在の再生時間)、v-bindを使ったdurationTime(全体の再生時間)を反映させる処理を見てきた皆さんであれば、おそらくinputに値を反映させたように、文字にも同じように反映させればいいとすぐにわかったと思いますが、今回は少し発展して分:秒(mm:ss)で時間を表示していきたいと思います。

const audio = new Audio

export default {
    data() {
        return {
            currentTime: 0,
            durationTime: 0,
            convertedDurationMin: "00",
            convertedDurationSec: "00",
            convertedCurrentMin: "00",
            convertedCurrentSec: "00"
        }
    },

    mehods:{
        play() {
            audio.src = //オーディオのURLとか

            audio.addEventListener("loadedmetadata", function () {
                const durationMin = Math.floor(audio.duration.toFixed(0) / 60);
                const durationSec = audio.duration.toFixed(0) % 60;

                return {
                    durationTime: audio.duration.toFixed(0),
                    convertedDurationMin: ("00" + durationMin).slice(-2),
                    convertedDurationSec: ("00" + durationSec).slice(-2)
                }
            });
            audio.addEventListener("timeupdate", function () {
                const currentMin = Math.floor(audio.currentTime.toFixed(0) / 60);
                const currentSec = audio.currentTime.toFixed(0) % 60;

                if ((currentMin > this.convertedCurrentMin 
                    && audio.currentTime !== 0)
                    || (currentMin < this.convertedCurrentMin)) {
                    return {
                        currentTime: audio.currentTime.toFixed(0),
                        convertedCurrentMin: ("00" + currentMin).slice(-2),
                        convertedCurrentSec: ("00" + currentSec).slice(-2)
                    }

                } else {
                    return {
                        convertedCurrentSec: ("00" + currentSec).slice(-2)
                    }
                }
            });
            audio.addEventListener("ended", function () {
                return {
                    currentTime: 0,
                    durationTime: 0,
                    convertedDurationMin: "00",
                    convertedDurationSec: "00",
                    convertedCurrentMin: "00",
                    convertedCurrentSec: "00"
                }
            });
            audio.play();
        }
    }
} 

オーディオから取得したdurationTimeを整数に変換し、その値を60で割った数をdurationMin、60で割った数の余りをdurationSecとします。
covertedDurationMinへは"00"にdurationMinを加算した要素から最後の2つを取り出して返し、
covertedDurationSecへは"00"にdurationSecを加算した要素から最後の2つを取り出して返しています。

わかっている人もいると思いますが、"00は"文字列のため、それにdurationMinを加算すると9分以下なら3桁(Ex.009)、10分以上なら4桁(Ex.0010)となってしまいmm表記にはなりませんが、最後の二つの要素のみを取り出せば、9分以下の時は10の位が0になり、10分以上の時はdurationMinと同じ値になるので、mm表記にすることができます。
covertedDurationSecにも同じことが言えるので、これでdurationTimeをmm:ss表記にすることができました。

currentTimeにも同じことが言えますが、currentTimeは値がどんどん変わっていくため、それも考慮してコードを書くと上記のようなコードになります。

currentMinconvertedCurrentMinより大きく、かつaudio.currentTimeが0でない場合というのはcurrentMinが加算されるタイミング(currentSecが0になるタイミングとも言える)の時呼び出されるものです。
thumbを移動させないシークであればこれで問題ありませんが、今回はthumbを移動するため、それに加えてcurrentMinconvertedCurrentMinより大きいときという条件を加えました。こうすることにより1分以上戻った時でも正常に表示できるようになりました。

次に<template>内に記述していきます。

<span v-text="convertedCurrentMin + ':' + convertedCurrentSec"></span>
<span v-text="convertedDurationMin + ':' + convertedDurationSec"></span>

正直そのまますぎて説明することがないので注釈を一つ。ここではspanタグを使っていますが、もちろん別のタグを使っても構いません。
例えばSoundCloudではdurationTimeはaタグになっていて、一度クリックすると残り時間の表示になり、もう一度クリックすると従来通り全体の再生時間を表示することができます。

再生済み用のプログレスバーを作り、元のinputタグに重ねる

まず↓のようなinputと同じサイズのプログレスバーを作ります。
<span class="progress" id="progress"></span>
もちろんタグはdiv等でも構いません。

次にstyleを書いていきます。

span.progress {
    width: /*width*/
    background: linear-gradient(/*YourFavoriteColor*/, /*YourFavoriteColor*/) no-repeat;
    background-size: 0;
    position: absolute;
    pointer-events: none;
}

まずプログレスバーにposition: absoluteを加える等をして、inputの上に重ねます。
background: linear-gradient(/*YourFavoriteColor*/, /*YourFavoriteColor*/) no-repeat;でプログレスバーの色をグラデーションで指定し、backgroud-sizeを使ってプログレスの度合いを表しています。
また最後のpointer-events: none;はプログレスバーの操作を無効化して、その下に配置されているシークバーの操作をできるようにしています。これを指定することにより、プログレスバーが真上に重なっても操作ができるようになりました。

ちなみにプログレスを表示する項目が1つであれば、spanを足さずに直接inputに書くこともできます。
しかしこういったWebサービスではストリーミング再生、つまり再生済みだけでなく読み込み済みのプログレスも作る必要があることが多いため、今回は別のタグにして複数のプログレスを重ねられるようにしました。

次に<script>のtimeUpdate内に記述していきます。

const percent = audio.currentTime.toFixed(0) / audio.duration.toFixed(0) * 100;
document.getElementById("progress").style.backgroundSize = percent + "%";

currentTimeをdurationTimeで割った数に100を掛けた数を定数percentとします。
定数percentの範囲は0 <= percent <= 100なので、background-sizeの値をwidthに対しての百分率とすることができます。そして、span.progressbackground-sizeに定数parcentに"%"を足した値を反映させます。そうするとthumbに従ってプログレスバーが動いているように見えるかと思います。

最後に

長々と書いてきましたが、これで終わりです。初めてな上に眠気と戦いながら書いたので、ミスや分かりづらいところがあったかもしれないですが、少しでも読んでくださった方の参考になれば幸いです。

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7