Header image

クラッソーネの開発者がエンジニアリングに関することもそうでないことも綴っています!

Ruby on RailsでもHLS形式で動画を再生したい!

Ruby on RailsでもHLS形式で動画を再生したい!

この記事は Ruby on Rails Advent Calendar 2021 二日目の記事です。
昨日は kakudaisuke さんの「I18nを使いながらPCでもスマホでも狙ったところでレスポンシブに改行する」でした!

https://qiita.com/kakudaisuke/items/afc6e3f1544638812ec1

この記事で話すこと

この記事では、動画を再生するときの形式の一つである HLS(HTTP ライブストリーミング)について簡単にまとめつつ、Ruby on Rails 製のウェブサイトで実装をするための勘所をお伝えします。

HLS って?何が嬉しいの?

HLS とは Web 上で動画や音声のストリーミング配信・再生を行なうためのプロトコルの一つです。
元は Apple が開発をした形式でしたが、現在は Apple 製品にとどまらずさまざまなデバイス・ブラウザにて利用することができます。

HLS を利用した動画の再生は、大まかにはこのような仕組みになっています:

  • 配信側は、もとの動画を一定間隔で細かく分割した MPEG2-TS ファイル(.ts)と、その再生順などを記したインデックスファイル(.m3u8)を用意する。
  • 再生側はまず .m3u8 ファイルを受信し、その内容に従って順々に .ts ファイルを要求する。そして受信したものから順に再生する。

例えばサイトのファーストビューのメインビジュアルとして動画を利用するとき、素朴に MP4 の動画を読み込ませようとするとどうしても FCP スコア が下がってしまいます。
そのときに HLS を利用することで再生しながら動画の続きを読み込ませられるようになるため、ページの初期読み込み速度を抑えつつ動画を配信することができます。

HLS について、詳しくはこちらのサイトをご覧ください。

この記事ではいわゆるオンデマンドな動画の配信を対象にしています。
ライブ配信は扱っていませんのでご了承ください。

HLS ってどうやって実装できるの?

HLS を使って動画の配信を実装するためには、大きく分けて三つの作業が必要です。

  1. 配信したい動画をもとに .m3u8 ファイルや .ts ファイルを生成する
  2. .m3u8 ファイルや .ts ファイルを配信できるようにする
  3. HLS 形式で動画を配信する仕組みを導入する

それぞれ順に見ていきます。

1. 配信したい動画をもとに .m3u8 ファイルや .ts ファイルを生成する

HLS に必要なファイルたちの生成は ffmpeg を利用すると簡単です。

例えばローカルにある video.mp4 から video.m3u8 というインデックスファイルと video000.tsvideo001.ts、...のような動画ファイルを生成したいときは、このようなコマンドを実行すれば OK です。

ffmpeg -i video.mp4 -c:v copy -f hls -hls_time 5 -hls_playlist_type vod -hls_segment_filename "video%3d.ts" video.m3u8
オプションの意味
オプション 意味
-i video.mp4 元のファイルの指定。
-c:v copy コーデックの指定。copyとすると元のファイルと同じコーデックになります。
-f hls フォーマットの指定。
-hls_time 5 動画を分割する際の間隔の指定。単位は秒。
-hls_playlist_type vod HLSのタイプを指定します。
-hls_segment_filename "video%3d.ts" 小さく分割した動画ファイルの名前の指定。%3d と指定していることで 000001、...といった三桁のインデックスが割り振られます。
video.m3u8 インデックスファイルの名前の指定。

2. .m3u8 ファイルや .ts ファイルを配信できるようにする

Ruby on Rails において、静的なファイルの配信はさまざまな方法で行うことができます。
Sprockets を使った昔ながらの方法、Webpacker を使ったイマドキ(?)な方法、/public にあるファイルを参照する素朴な方法の三つが代表的なものでしょうか。

ここでは .m3u8 ファイル は Webpacker を使って配信し、.ts ファイルは /public に置いて配信してみます。

まずは Webpacker が動画ファイルを配信できるようにします。
app/assets/javascript/videos というディレクトリを作り、そこに video.m3u8 を置きつつ、application.js を次のようにします。

app/javascript/packs/application.js
// この行があることで `asset_pack_path` で動画ファイルを参照できるようになる。
require.context("../videos", true);

import Rails from "@rails/ujs"
Rails.start()

次に動画を表示するための HTML を作っていきます。
詳細は割愛しますが、Rails の View ファイルにて先ほど配置した video.m3u8 を参照してみます。

app/views/home/show.html.erb
<%= video_tag asset_pack_path('media/videos/video.m3u8') %>

これだけで iOS Safari などのブラウザでは HLS 形式の動画を読み込むことができます。

しかし実際に試してみると .ts ファイルが見つからずに動画を再生することができません。
ファイルを探すときに Asset Pipeline の仕組みを使って探索に行ってしまうため、/public にファイルを置いてあると見つけてくれないのです。

/public に置いてあるファイルを直接参照させるために、video.m3u8 に手を加えましょう。
.ts ファイルを呼び出している行の先頭全てに ↓ のように / をつけてください。

app/javascript/videos/video.m3u8
-video000.ts
+/video000.ts

以上で、特別な設定が不要な iOS Safari などのブラウザでは HLS 形式の動画を読み込めるようになりました!

3. HLS 形式で動画を配信する仕組みを導入する

最後に、ネイティブにはサポートされていないその他のブラウザでも HLS 形式の動画を読み込めるようにしていきます。
ここではこの用途に特化した video-dev/hls.js というライブラリを利用します。

https://github.com/video-dev/hls.js/

基本的には README の通りに実装すれば正しく動きます。
せっかくなので今回は hls.js がサポートしていないブラウザ向けのフォールバックも含めて実装してみます。

app/views/home/show.html.erb の変更

HLS をサポートしていないブラウザ向けに MP4 動画も配置しつつ、その動画のパスも指定しておきます。

app/views/home/show.html.erb
<%= video_tag '', class: 'js-hls-video', controls: true, data: { 'hls-src': asset_pack_path('media/videos/video.m3u8'), 'fallback-src': asset_pack_path('media/videos/video.mp4') } %>
package.json の変更

yarn add hls.js を実行してください。

package.json
  "dependencies": {
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "5.4.3",
+   "hls.js": "^1.1.1",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12"
  },
app/javascript/packs/application.js の変更
 require.context("../videos", true);

 import Rails from "@rails/ujs"
 Rails.start()
+
+import "../src/loadHls"
app/javascript/src/loadHls.js の追加
app/javascript/src/loadHls.js
import Hls from 'hls.js';

function loadHlsVideo (video) {
  const videoSrc = video.getAttribute('data-hls-src');
  const fallbackVideoSrc = video.getAttribute('data-fallback-src');

  // hls.jsがサポートしているブラウザの場合。
  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(videoSrc);
    hls.attachMedia(video);
  }
  // HLSをサポートしているブラウザの場合はsrc属性に直接m3u8ファイルを指定すればよい。
  else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = videoSrc;
  // どうしようもないときはフォールバック用の動画を再生する。
  } else {
    video.src = fallbackVideoSrc;
  }
}

document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('video.js-hls-video').forEach((video) => loadHlsVideo(video))
})

こうすることで、hls.js がサポートする様々なブラウザ にて HLS 形式で動画を再生できるようになりました!

実装してみたコードは公開しています

今回実装してみたこれらのコードも含んだ、実際に動く Rails アプリのソースコードを公開しています。
理解・実践の参考にしていただけたら嬉しいです!

https://github.com/yamat47/rails-hls-sample

また Heroku でこの Rails アプリをデプロイしています。
実機での動作を確認したいときにお使いください!

https://rails-hls-sample.herokuapp.com/

開発者ツールでネットワークの状況を見てみると、少しずつ動画の続きが読み込まれていっているのがわかると思います。

tsファイルが少しずつ読み込まれていっている様子

2022年2月20日追記:デモンストレーション用のアプリの公開を終了しました。
コードは readonly で公開し続けています。

感想:Rails にちょっと周辺領域の技術を組み合わせるのめっちゃ楽しい

この記事では、かなり枯れつつある Ruby on Rails に HLS というちょっと違った領域の技術を組み合わせてみました。
hls.js のおかげであまり複雑な実装をする必要はなくなっていますが、Rails ができることの広さをあらためて実感しました。

クラッソーネでは Ruby on Rails にいろんな技術を組み合わせるのが好きなエンジニアを大募集しています。
少しでもこの記事に興味を持っていただいた方、ぜひカジュアル面談でお話ししましょう!

https://www.crassone.co.jp/recruit/engineer/


クラッソーネで Ruby on Rails を使ってサービス開発をしています。週末はアメフトのコーチをしたり選手もしたり、たまに審判もしたりしてます。エビ中もすき。