LoginSignup
5
7

More than 3 years have passed since last update.

Dockerで、音声ストリーミング再生機能を作ってみた話

Last updated at Posted at 2019-05-25

こんにちは。
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-getffmpeg を入れています。

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

5
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
5
7