こんにちは。
開発Div. エンジニアのセイです。 クラウドワークスでクラウドソーシングの開発に携わっています。
簡単な自己紹介ですが、中国の上海出身です。4歳の息子がいます。子どもと遊ぶのは大好きですが、コード書くよりは疲れます。。。
最近開発業務上でvideoタグを触ったチャンスが有ったので、色々試したこと調べてことをメモ帳としてまとめて共有したいと思います。
開発環境:
ブラウザ:
サンプルビデオ:
- vimeo.comにフリーで提供頂いたサンプル動画を使っています
まず何も考えずにvideoファイルをそのままレスポンスとして返すように実装してみました
index.html.erb
<%= video_tag(['video/Pollution-SD.mp4'], controls: true) %>
video_controller.rb
def show file_path = "file/#{safe_filename}" file_size = File.size?(file_path) render status: :not_found, text: 'file not found' and return unless file_size.present? send_file file_path, type: MIME::Types.type_for(file_path)[0].to_s end
テストしてみましたら、Chromeだと再生は問題無いですが、早送りなどの操作はできないようです(SafariとFirefoxはそういう問題はありません)
なんでだろう、調べてみましたら
Chromeはvideoタグのリソースに対するリクエストは自動的に Rangeヘッダー付き
になります。つまり、Chromeは ステータス206のパーシャルレスポンス
を求めていることです
(実際、SafariとFirefoxもRangeヘッダー付きでリクエストしますが、200レスポンスでも再生&早送りなどの操作はできます)
パーシャルレスポンス対応
video_controller.rb
def show file_path = "file/#{safe_filename}" file_size = File.size?(file_path) render status: :not_found, text: 'file not found' and return unless file_size.present? if request.headers['Range'].present? # ↓↓↓ 追加したコード file_range = request.headers['Range'].match /bytes=(?<start>\d+)-(?<end>\d*)/ file_start = file_range.present? ? file_range[:start].to_i : 0 file_end = file_range.present? && file_range[:end].present? ? file_range[:end].to_i : file_size - 1 if file_start < 0 || file_end >= file_size || file_end < file_start render status: :bad_request, text: 'bad request' and return end file_range_length = file_end - file_start + 1 response.header['Content-Range'] = "bytes #{file_start}-#{file_end}/#{file_size}" response.header['Content-Length'] = file_range_length.to_s send_data File.binread(file_path, file_range_length, file_start), disposition: 'inline', type: MIME::Types.type_for(file_path)[0].to_s , status: :partial_content # ↑↑↑ 追加したコード else send_file file_path, type: MIME::Types.type_for(file_path)[0].to_s end end
これで解決しました。
そこで新たな考えが出てきました。せっかくパーシャル対応しているので、大きなビデオファイルだと、複数回のリクエストに分割して、ダウンロードしながら再生すればサーバの負荷も分散できるし待ち時間も短縮できるではないかと考えていました
ストリームにサイズを制限する
# 該当ビデオファイルは1.2MBで、1リクエストには300KBの制限をかけ # リクエスト4、5回分で全部再生できると想定している VIDEO_READ_BUFFER_LIMIT = 300 * 1024 def show file_path = "file/#{safe_filename}" file_size = File.size?(file_path) render status: :not_found, text: 'file not found' and return unless file_size.present? if request.headers['Range'].present? file_range = request.headers['Range'].match /bytes=(?<start>\d+)-(?<end>\d*)/ file_start = file_range.present? ? file_range[:start].to_i : 0 file_end = file_range.present? && file_range[:end].present? ? file_range[:end].to_i : file_size - 1 if file_start < 0 || file_end >= file_size || file_end < file_start render status: :bad_request, text: 'bad request' and return end file_range_length = file_end - file_start + 1 # ↓↓↓ 追加したコード if file_range_length > VIDEO_READ_BUFFER_LIMIT file_range_length = VIDEO_READ_BUFFER_LIMIT file_end = file_range_length + file_start - 1 end # ↑↑↑ 追加したコード response.header['Content-Range'] = "bytes #{file_start}-#{file_end}/#{file_size}" response.header['Content-Length'] = file_range_length.to_s send_data File.binread(file_path, file_range_length, file_start), disposition: 'inline', type: MIME::Types.type_for(file_path)[0].to_s , status: :partial_content else send_file file_path, type: MIME::Types.type_for(file_path)[0].to_s end end
想定通り動いてくれた!!
と、思ったらFirefoxに怒られました
Firefoxだと、1回目のレスポンスの内容のみ再生してくれて次のリクエストは投げてくれないようです。 (ちなみに、SafariはChromeと同じくダウンロードしながら再生できます)
色々調べてみました。どうやらそれはFirefoxのバグか仕様か分からないが、2010年既に報告があって、まだ解決されてない状態です
570755 – 206 Partial content is ignored and mozilla does not seek for additional video content
このサイトのコメント履歴から見ると、Firefoxの開発者はそもそもその問題は対応すべきかどうかをずっと議論していて結局何もしませんでした
ちょっと残念ですが、その問題の解決策も考えてなくてとりあえず業務仕様として、ビデオファイルのサイズは大きくないためサイズ制限しない方法でリリースしました
Apache Killer
ApacheのRangeヘッダー対応によってDoS攻撃されたことが昔けっこう騒いてたが、2.2.21以後は対応されたため、必ずApacheのバージョンを確認してください。
各ブラウザの再生できるビデオファイル形式のサポート
単純にvideoタグを使うならば、再生できるビデオファイルの形式はかなり制限されているようです。現状はmp4(コーデック:H.264)とWebM(コーデック:VP8)2種類だけだそうです。
以上