こんにちは。
Web・iOSエンジニアの三浦です。
今回は、HTTP_Live_Streaming(HLS)を使った音声ストリーミング再生機能を Docker 上で作ってみました。
ちなみに今回説明するコードは、以下のgithubで公開しています。
https://github.com/takayuki-miura0203/docker-hls-sample
準備
環境
Docker が実行できる必要があります。
また、 /etc/hosts
に以下を記述してください。
127.0.0.1 front storage
HTTP_Live_Streaming とは?
HTTP_Live_Streaming(HLS) は、 Apple が提供する動画・音声のストリーミング再生技術です。
https://developer.apple.com/streaming/
動画・音声ファイルに処理を加えて分割し、分割されたファイルを読み込むようにすることで、ストリーミング再生を実現します。
また分割されたファイルの読み込みについても特別なことをする必要はなく、通常の動画・音声ファイルを読み込む代わりにファイル分割時に生成されたメタファイルを読み込むようにすれば、通常の動画・音声ファイルを使うのと同じ方法で使用することができます。
Dockerコンテナの構成
今回は、ユーザーから処理を受け付けるフロントと、作成した音声分割ファイルを保存しておくストレージを分けたかったため、以下のようなコンテナ構成にしています。
web
nginx を使ったウェブサーバ。
ユーザーから受け付けた処理を、ドメインをもとにフロント・ストレージに振り分ける。
front
主にユーザーからのアクセスを受け付ける php のバックエンド。
音声ファイルを POST で受け取って分割してストレージに渡す機能と、ストレージに存在する音声ファイルを再生できる画面を GET で提供する機能を持つ。
storage
分割された音声データをフロントから受け取り、保持する。
php + apache で構成する。
実装してみる
web
Dockerfile
念の為 vim を入れていますが、基本的には nginx
だけで十分です。
FROM nginx:latest
RUN apt-get update && apt-get install -y \
vim
設定ファイル
今回は、ドメイン名が front
ならフロントに、 storage
ならストレージに、それぞれ飛ばすようにしました。
また、音声ファイルのサイズが大きい場合はデフォルトの POST 可能サイズでは足りなくなるため、 client_max_body_size
を大きめに指定しています。
# front 用
server {
index index.php index.html;
server_name front;
include /etc/nginx/mime.types;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /app/public;
client_max_body_size 20M;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass front:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
# storage 用
server {
index index.php index.html;
server_name storage;
include /etc/nginx/mime.types;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html;
client_max_body_size 20M;
location / {
try_files $uri $uri/ =404;
}
location ~ \.(php|m4a|m3u8|aac)$ {
proxy_pass http://storage:80;
}
}
front
Dockerfile
nginx と通信するため、 php:fpm
を入れています。
また、音声ファイルの分割に使用するため、 apt-get
で ffmpeg
を入れています。
FROM php:7.3.5-fpm
RUN apt-get update && apt-get install -y \
git \
unzip \
vim \
ffmpeg
php.ini
デフォルトの php.ini
では音声ファイルのサイズに対して POST での受け入れ可能サイズが小さい場合があるので、受け入れ可能サイズを変更します。
今回は php.ini
ごとこちらの管理配下におき、マウントするようにしています。
post_max_size = 20M
upload_max_filesize = 20M
php
GET
GET でアクセスが来たらストリーミング・オリジナル両方の再生ができるようにしています。
なお、 m3u8
の拡張子のファイルが、ファイル分割時に生成されるメタファイルです。
<?php
// show audio player when GET requested
if ($_SERVER["REQUEST_METHOD"] == "GET") {
$streamingUrl = "http://storage:8080/{$_GET['name']}/playlist.m3u8";
$originalUrl = "http://storage:8080/{$_GET['name']}/original.m4a";
?>
<span>streaming</span>
<br>
<audio src=<?php echo $streamingUrl ?> controls />
</audio>
<br>
<br>
<span>original</span>
<br>
<audio src=<?php echo $originalUrl ?> controls />
</audio>
POST
POST でアクセスが来た場合は、 body
にて渡された音声ファイルを分割し、ストレージに渡します。
音声ファイル分割には ffmpeg
コマンドを利用しています。
<?php
// create streaming file when POST requested
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// delete audio file if exists
$fileInfo = pathinfo($_FILES['audio']['name']);
if (file_exists($fileInfo['filename'])) {
exec("rm -rf {$fileInfo['filename']}");
}
// get audio file
exec("mkdir {$fileInfo['filename']}");
move_uploaded_file(
$_FILES['audio']['tmp_name'],
"./{$fileInfo['filename']}/original.{$fileInfo['extension']}"
);
// create streaming files
exec("
ffmpeg \
-i {$fileInfo['filename']}/original.{$fileInfo['extension']} \
-map 0 \
-f segment \
-acodec aac \
-segment_list {$fileInfo['filename']}/playlist.m3u8 \
-segment_time 5 \
{$fileInfo['filename']}/stream-%03d.aac
");
// post files to storage
$postFields = ['directory' => $fileInfo['filename']];
$audioFiles = array_filter(glob($fileInfo['filename'] . '/*'), function($audioFile) {
return is_file($audioFile);
});
$audioFiles = array_values($audioFiles);
foreach ($audioFiles as $key => $audioFile) {
$postFields += ["audio[{$key}]" => new CURLFile(htmlspecialchars($audioFile))];
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://storage:80/index.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_exec($ch);
// delete audio files
exec("rm -rf {$fileInfo['filename']}");
}
storage
Dockerfile
ストレージではフロントと異なり php
以外のファイルも通信したかったため、 php:apache
を入れています。
FROM php:7.3.5-apache
RUN apt-get update && apt-get install -y \
git \
unzip \
vim
php.ini
フロントと同じく、 POST 通信のサイズ上限を上げるため php.ini
に変更を加えています。
post_max_size = 20M
upload_max_filesize = 20M
php
フロントから受け取った音声ファイルを保存します。
<?php
// delete audio files if exists
if (file_exists($_POST['directory'])) {
exec("rm -rf {$_POST['directory']}");
}
// get and save audio files
exec("mkdir {$_POST['directory']}");
foreach ($_FILES['audio']['tmp_name'] as $key => $tmpName) {
move_uploaded_file(
$tmpName,
"./{$_POST['directory']}/{$_FILES['audio']['name'][$key]}"
);
}
docker-compose
docker-compose.yml にて、ウェブサーバとフロント・ストレージをリンクさせるようにします。
version: '3.7'
services:
web:
build: ./web
ports:
- 8080:80
links:
- front
- storage
volumes:
- ./web/conf.d:/etc/nginx/conf.d
front:
build: ./front
volumes:
- ./front/php.ini:/usr/local/etc/php/php.ini
- ./front/app:/app
storage:
build: ./storage
volumes:
- ./storage/php.ini:/usr/local/etc/php/php.ini
- ./storage/app/public:/var/www/html
実行!
コンテナ作成
まずはコンテナを作成します。
docker-compose up
音声ファイル分割
続いて音声ファイルを渡し、ストリーミング用の音声ファイルを作成します。
今回のコードではm4aファイルにのみ対応しているので、注意してください。
curl -i -X POST \
-H "Content-Type:multipart/form-data" \
-F "audio=@\"{audio_file}\";type=audio/x-m4a;filename=\"{audio_file_name}\"" \
'http://front:8080/index.php'
これで音声ファイルが分割され、準備ができました。
ストリーミングで聞いてみる
ブラウザで以下の URL を叩き、実際にストリーミングで聞いてみましょう。
ただし、 2019/05/25現在Chromeには対応していません。
safari で聞くようにしましょう。
iOSで聞いてみる
せっかくなので、 iOS アプリで聞けるか試してみましょう!
これには xcode が必要ですので、ご注意ください。
info.plist
で、 HTTP が使用できるようにしてください。
その上で StoryBoard上で AVPlayerViewController を作成し、以下のコードを紐づけてください。
import Foundation
import AVFoundation
import AVKit
class AVAudioPlayerViewController: AVPlayerViewController {
override func viewDidLoad() {
super.viewDidLoad()
// storage の URL を指定することで、直接ストリーミング用のメタファイルを fetch することが可能です
let url = URL(string: "http://storage:8080/{audio_file_name}/playlist.m3u8")
let avAsset = AVURLAsset(url: url!)
let playerItem = AVPlayerItem(asset: avAsset)
let player = AVPlayer(playerItem: playerItem)
self.player = player
self.player?.play()
}
}
シミュレータなどでこの画面を表示すれば、ストリーミングで音声が流れ出すはずです!
さいごに
ストリーミング再生には特殊な技術が必要で大変そう、何らかのサービスが必要なのでは…と思っていたのですが、やってみると案外簡単にできました。
Chromeで再生できないので Web サイトでどのくらい使えるかはまだ未知数ですが、アプリで使うのであれば十分使えそうです!
参考文献
以下のサイトを参考にさせていただきました。
ありがとうございました!
https://qiita.com/mochizukikotaro/items/b398076cb57492980447
https://qiita.com/bossunn24/items/85ca5c3bfbba07b4e0cc
https://qiita.com/okumurakengo/items/5627326ee833a3a5ea03
https://heartbeats.jp/hbblog/2012/04/nginx05.html
https://koni.hateblo.jp/entry/2017/01/28/150522
https://qiita.com/takecian/items/639deeae094466de6546
http://blogs.rastafactory.co.jp/art/2013/01/28/php-%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/
https://heartbeats.jp/hbblog/2012/04/nginx04.html
https://gray-code.com/php/get-kind-of-file/
https://www.javadrive.jp/phpappli/keijiban/index3.html
https://qiita.com/Quantum/items/7e6e3e7a3bdf605c306a
https://qiita.com/momoto/items/b34fb9b908ffb26c76a4
https://qiita.com/tukiyo3/items/4162bd793be47d8651a8
https://blog.sioyaki.com/entry/2018/04/20/102344
http://ysklog.net/php/2873.html
http://omega.lid-inc.com/%E3%80%90php%E3%80%91%E3%80%80%E6%8C%87%E5%AE%9A%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E3%82%92%E3%82%B5%E3%83%96%E3%83%87%E3%82%A3%E3%83%AC/
https://qiita.com/yakimeron/items/34a65397c1041c0b2a0c
https://teratail.com/questions/135190