#FJMK blog

citrus+というバンドをやってます。 http://citrusplus.jp 平日昼間はインターネットの会社@紀尾井町でエンジニアやってます。

YouTubeをラジオ化するChrome Extensionを作りました

f:id:sfjmk:20170903205756p:plain YouTubeをラジオ化するChrome Extension(拡張機能) 「Listen Tube」 を作ってみたので、その流れを書こうと思います。



こんな風にYouTubeで動画を表示させずに音だけを流せるようになります。
f:id:sfjmk:20170903205737p:plain






公開したChrome Extensionはこちら。

Listen Tube - Chrome ウェブストア

作業用BGMを聴くためにYouTubeを開いたのに、気がついたら関係ない動画を観ちゃってる問題

作業中にBGMを聴きたい時はYouTubeをよく利用する方は結構多いと思います。
簡単に曲が探せますし、勝手に次の曲が流れてくれますし便利ですよね。

ただ、使う上で一個問題が…
それは、

_人人人人人人人人人人_
> 気が散ってしまう <
 ̄YYYYYYYYY

ということ。

BGMを探しにYouTubeを開いたつもりが、ちょっと気が緩んでプロモーションビデオやバラエティ番組の動画を開いてそのまま鑑賞してしまい、気がついたら◯時間経っていたという経験があるのは僕だけじゃないはずです。

あと、これは気が散るとは別の話ですが、BGMを聴くつもりでも、動画が大きく表示されてしまうので、会社で開くのは若干憚られるという場合もあるかもしれません。

動画を見られない(=音だけ聞こえる)YouTube、欲しいですよね。

調べてみた所、
SoundYouNeed.com - Music search engine & player
こんなサービスがあったので、しばらく使っていましたが、連続再生機能が微妙だったり、ちょいちょい挙動がおかしくなるので、自分で作る必要があると感じました。

最初はYoutubeAPI使ってWEBサービス的な感じにしようと思いましたが、APIリクエスト回数制限が結構厳し目で、作った所であまりスケールしなさそうだったので、本家YouTubeのインターフェース自体をいじるChrome Extensionを作ることにしました。
(YouTubeAPIの上限リクエスト数って申請すれば簡単に緩めてもらえるんでしょうかね? ご存知の方いたら教えてください)

Chrome Extensionの作り方

今回、Chrome Extensionを作ったのは初めてですが、かなり簡単に作ることができました。

今回の記事では細かくは説明しませんが、作り方とかは以下のページが參考になると思います。

公式はこの辺

ブログとかだとこの辺

やることは単純で、今回くらい簡単なExtensionでしたら、

  • manifest.jsonという名前の設定ファイル (Chrome Extension説明だったり設定を記述します)
  • イコン画像のファイル
  • 実装したcssファイル
  • 実装したjsファイル

を用意して、デベロッパー登録(US$5.00が必要)して、Chrome ウェブストアにアップロードすればもう公開が完了します。
Chrome ウェブストアで公開せず自分だけが使う場合はにはデベロッパー登録する必要すらありません。

作ってみた

というわけで早速Chrome Extensionを作っていきます。
まず、manifest.jsonファイルを用意します。

{
  "manifest_version": 2,
  "name": "Listen Tube",
  "version": "0.1.1",
  "description": "YouTubeをラジオ化する(=動画を表示させず音だけ流す)Chrome Extensionです。詳しくはこちら → http://fjmk.jp/blog/839",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "content_scripts": [
    {
      "css": ["style.css"],
      "js": ["main.js"],
      "matches": ["https://*.youtube.com/*"]
    }
  ]
}

設定・説明や読み込みファイルを記述するだけです。
content_scriptsのmatchesを指定することで、youtube内でのみ動作するようになっています。

ここからCSSJavaScriptを書いていくわけですが、始めに作る上で最低限のものしか作らないというルールを決めました。

意識したのは↓この辺です。

意識したこと

対象ページを絞る

あくまでYouTubeをBGMプレイヤーとして使うというユースケースを想定しています。
僕の場合、BGMを探すときはほとんど 検索→検索一覧→動画ページ という動線なので、通らないページ(チェンネルページとか)はそのままで問題ありません。埋込み機能にも対応しないことにしました。

デザイン面は多少妥協する

今回は元のYouTubeマークアップされているid,classを利用して見た目をいじるので、挙動がYouTube側のマークアップに依存してしてしまいます。
最初はミュージックプレイヤーっぽい見た目にしようかなとか思ってましたが、関係するclassが増えてしまうと保守性が著しく下がるので、あくまで気が散る要因を除外できれば良しとしました。

とはいいつつも、まあレイアウトがガタガタになってはテンションが下がるので、そのへんはバランスを考えつつという感じで。

作業の流れ

では何が我々の集中力を削いでいるんでしょう。
上でも書きましたが、自分がBGMを探す際は、

トップページを開く

検索ボックスにキーワード入力して検索

検索結果一覧で観たい動画(聴きたいBGM)を選ぶ

動画ページで動画を再生する

という順序の場合が殆どなので、上から順を追って対処していきます。
また、この時点で、デベロッパーモードでExtensionを読みこみ、以降ソースに変更がある度にリロードをして動作確認をしています。

CSSを幾つかに分けて書いていますが、ファイルとしては1つです。
ソースはGitHubに上げてあります→ sfujimaki/ListenTube

トップページ

まず、トップページの「おすすめ」エリア。

f:id:sfjmk:20170903205649p:plain

BGMを探すためにYouTubeを開いたのに、楽しげな動画を沢山出してきて、我々の集中力を削ごうとしてきます。 ここで動画をポチったら最期。そのまま数時間を失ってしまいます。

ということで、

消します。

#page.home #header, #page.home #content {
    display: none;
}

f:id:sfjmk:20170903205447p:plain これですっきりしました。気を散らさずに検索アクションに入ることができそうです。

検索結果一覧ページ

f:id:sfjmk:20170903205438p:plain

観たい動画を選ぶページですので、ここは消すわけにはいきません。
正直画像を小さくしたりしたいところではありますが…拘るとキリがないので、このままでよしとします。

動画ページ

f:id:sfjmk:20170903205544p:plain 問題はこのページです。気が散る要素で満ち溢れています。

関連動画

まず、問題はこのサイドバーです。
f:id:sfjmk:20170903205443p:plain トップページと同じく、楽しげな関連動画を表示して、我々の回遊率を高めようとしてきます。
無意識のうちに目に飛び込んでくるので、気を許すといつのまにか動画を楽しんでいる自分がいます。
削除…しようかと思いましたが、一応次のBGM選ぶときにあったほうがいい気がしたので、あまり目につかないページの下に追いやります。

/* メインカラム幅を100%にして中央寄せ*/
#page.watch .watch-main-col {
    width:100%;
    float: none;
}

/* サイドバー */
#page.watch .watch-sidebar {
    margin: 0 auto;
    top: 10px;
}

このようにCSSを当ててあげて、1カラムレイアウトにします。

f:id:sfjmk:20170903205702p:plain

うん。気が散りづらくなった気がします。

コメント

f:id:sfjmk:20170903205715p:plain

コメントもBGMを聴く上では必要ありませんね。 気が散るというほどのものでもないかもしれませんが、まあどちらかといえば気が散る要因になるので消しましょう。

/* コメント部分を隠す */
#page.watch #watch-discussion {
    display: none;
}

f:id:sfjmk:20170903205432p:plain

動画説明

f:id:sfjmk:20170903205616p:plain

動画説明は…まあいいか。そのまま残しましょう。

f:id:sfjmk:20170903205702p:plain

これでだいぶすっきりしました。

動画プレイヤー部分

最後に動画プレイヤー部分に手を加えます。

f:id:sfjmk:20170903205644p:plain

まず最初にCSSを貼っちゃいます。

/* プレイヤーの高さを設定 */
#page.watch .player-height {
    height: 150px;
}

/* 広告表示エリアの高さを設定 */
#page.watch .ad-container-single-media-element-annotations {
    height: 150px !important;
}

/* ビデオのシークバー&ボタン群を常に表示させる */
#page.watch .ytp-chrome-bottom {
    opacity: 1;
}

/* ビデオの中身を消す */
#page.watch .video-stream {
   display: none;
}

やってることはコメントに書いてある通りです。要するにプレイヤーの面積を小さくした上で、動画が表示されないようにしています。
プレイヤーを小さくしただけだと、広告動画が出た時に広告スキップボタンが押せなくなってしまうので、広告表示エリアも高さを合わせます。
(そういえば、広告ブロックするExtensionとかもあるらしいですよ。)

これでほぼ出来上がりました。
f:id:sfjmk:20170903205453p:plain

ウィンドウ幅を変えても崩れなさそうです。
f:id:sfjmk:20170903205558p:plain

サムネイルの挿入

ただ、このままではサムネイルが表示されません。
今回は最低限の物だけを作るポリシーで進めてきましたが、真っ黒のプレイヤーではちょっと寂しいので、サムネイルを表示する処理を書きます。

最初、「JSでog:imageを取得して表示すればいいかー」と思ってましたが、ここで罠が。
YouTube、動画間の遷移でPjax的なことをしているので、動画ページ内で別の動画をクリックして遷移した場合、Chrome ExtensionのJSを再読込してくれません。
なので、JSで普通にog:imageをサムネイル試してみたところ、別の動画に飛んでも最初に取得したサムネイルがそのまま残ってしまいました。
ソース上のog:image自体も書き換わらない模樣。

なので、何かしらの方法でページが遷移したことを検知して、画像URLを取得する必要があります。

今回はDOMの変更を検知できるMutationObserverを使って、head内の変化を検知し、変化があったら画像を取得&挿入することにしました。

MutationObserverの説明はこちら。
MutationObserver - Web API インターフェイス | MDN

タイトル等どこか一箇所の変更を検知するようにしたかったのですが、なんかうまくいかなかったので、ちょっと雑ですがhead全体をみてます。
ちょっと強引な気もしますが、プラグイン読み込んだりするのは面倒なので、あくまで素のJSぱぱっと書ける範囲に留めておきます。

遷移の度に何回か検知してしまうので厳密には「遷移を検知」とは違いますが…いいやり方ご存知でしたら教えてください。

画像はog:imageを取得しようと思ってましたが、どうやら変わらないようなので、開いている動画ページのURLから動画IDを抜き出して、画像URL(https://i.ytimg.com/vi/動画ID/hqdefault.jpg)を取得します。
遷移の度に、ビデオプレイヤー内にサムネイルを挿入します。

また、Pjaxではなく普通にページを開いた時も、DOM生成のタイミングのズレでうまくサムネイルが挿入されてくれなかったりするので、setIntervalを使って、サムネイルが取得できるまで取得をし続けるようにしました。

コードとしてはこんな感じです。

// サムネイル画像の作成
var image = new Image();
image.height = 100;
image.style.float = "left";

// サムネイルが取得できるまで取得し続ける
var timer = setInterval(function(){
    insertThumbnail();

    if (image.src) {
        clearInterval(timer);
    }
}, 1000);

// オブザーバインスタンスを作成
var observer = new MutationObserver(function(mutations) {
    insertThumbnail();
});

// 対象ノードとオブザーバの設定を渡す
observer.observe(document.querySelector('head'), {childList: true, subtree: true});

/**
 * 引数としてパラメータの名前を渡すと、URLのパラメータから値を取得して返す
 *
 * @param {string} name パラメータの名前
 */
function getParamValueByName(name) {
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0; i<vars.length; i++) {
        var pair = vars[i].split("=");
        if (pair[0] == name) {
            return pair[1];
        }
    }
}

/**
 * サムネイル画像のURLを取得して挿入
 */
function insertThumbnail() {
    image.src = 'https://i.ytimg.com/vi/'+getParamValueByName('v')+'/hqdefault.jpg';
    var playerContainerElement = document.getElementById("movie_player");
    console.log(playerContainerElement);
    playerContainerElement.insertBefore(image, playerContainerElement.firstChild);
}

これでサムネイルが表示されます。
f:id:sfjmk:20170903205607p:plain バランス等ちょっと惜しい感が否めませんが、その辺を解消しようとしたらコードが膨らんできてしまったので、一旦これで完成とします。

(一応)ミックスリストでも崩れません。
f:id:sfjmk:20170903205728p:plain

アイコンの準備

最後に、Chrome ウェブストアに登録する際に必要なアイコンを用意します。

YouTubeカラーの背景に、適当に見つけてきたフリー素材のヘッドフォン画像を組み合わせました。
f:id:sfjmk:20170903205638p:plain

公開

あとは、
デベロッパー ダッシュボード - Chrome ウェブストア
こちらから公開するだけです。

公開の仕方は、この辺を參考にしてみてください。

基本的に、作業フォルダをzipに固めてアップロードして、幾つか情報を入力するだけで公開できます。 どれくらい情報を作り込むかにもよりますが、今回は最低限の項目だけ埋めたので、10分もかからず公開まで完了しました。(後からいつでも編集できるっぽいです)

今回作ったChrome Extensionはこちらで公開されてるので是非使ってみて下さい。

Listen Tube - Chrome ウェブストア

拡張機能管理ページに行かないとOn/Offできませんが、どうしても動画を観たい時はシークレットウィンドウで観るか別ブラウザで観るといいと思います。

あと、自分でも登録してみましたが、特にデータ吸い上げたりしてなくてもこんな表示がでちゃうんですね…これは紛らわしい気が。

f:id:sfjmk:20170903205707p:plain データ吸い上げたりしてないので、安心してください。

最後に

実際に書いたコードは大した量じゃありませんが、一応GitHubにも置いておきました。
sfujimaki/ListenTube

以上、いかがでしたでしょうか。
作ったばかりなので僕自身まだそんなに使っていませんが、今のところ結構便利な気がしているので、皆さん是非使ってみてください。
使っていて、不具合とか、「こうしたらいいんじゃない?」というのがあったら@macky256宛に是非お知らせください!

こういう、「実装は楽だけど結構便利」的なものはどんどんつくっていきたいですね。