RustのHLS配信サーバーの作成練習

# はじめに
RustでRTMPを受信してHLSで配信するサーバーの、HLSの部分を上手に作れなくて困っていたので、とりあえず簡単に動くHLS配信サーバーを作成して整理してみようということで作成してみました。

> https://github.com/Papillon6814/Eupnea

HLSの動作確認には、Safariを使用しました。普段はFirefoxを使っているのですが、Safariはm3u8ファイルを直接再生することができるので、localhostのアドレスにSafariから直接GETリクエストを送信して動画を再生していました。（Firefoxだとファイルをダウンロードするように表示されてしまう。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/7b9a33fa-001d-5882-92b0-02410014751a.png)


## m3u8ファイルとtsファイル
HLSのストリーミングには、どちらも聞き慣れない名前でしたが**tsファイル**と**m3u8ファイル**というものが登場します。

**tsファイル**は、動画を分割して小分けにしたファイルです。このtsファイルを連続で再生していくことで、一つの動画としてストリーミングすることができます。
**m3u8ファイル**は、tsファイルを正しい順序や方法で再生するためのプレイリストファイルのようなものです。

## MasterPlaylistとMediaPlaylist
そのm3u8ファイルにも、**MasterPlaylist**と**MediaPlaylist**の２種類があります。

**MediaPlaylist**は、tsファイルを再生する順序や方法を指定するファイルで、このような形になっています。

```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:60
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:40,
sample.ts
```

`#EXT-X-TARGETDURATION`は、そのプレイリスト（m3u8）に含まれているtsファイルの長さの最大値を示しています。そして`#EXTINF`はtsファイルの秒数を示しています。なので、`#EXT-X-TARGETDURATION`が`#EXTINF`よりも小さくなってしまうとストリーミングが正常に再生されません。ここを直すのに結構時間を食っちゃいました。

**MasterPlaylist**は、複数のMediaPlaylistをまとめておくことができるプレイリストです。
なんのために使うのかというと、通信速度や処理できるファイル形式、ビットレートごとにどのMediaPlaylistを再生すればいいかをクライアントに判断させるためにあります。
ざっくりとこのような形になっています。（今回は動くかどうかを確認しただけなのでビットレートごとに分かれたりしていないです。）

```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="1",NAME="sample"
#EXT-X-STREAM-INF:BANDWIDTH=640000
http://localhost:8001/stream/media/playlist.m3u8
```

## 実際のプログラム
Rustで配信サーバーを作成したので、もちろん言語はRustです。
`main.rs`に全部書いちゃったのですが、記述量はすごく多くはないと思います。

```rs:main.rs
#![feature(proc_macro_hygiene, decl_macro)]

extern crate m3u8_rs;
#[macro_use]
extern crate rocket;

use std::io::Cursor;
use std::fs;
use std::path::PathBuf;
use m3u8_rs::playlist::{
    MasterPlaylist, 
    MediaPlaylist, 
    MediaSegment,
    VariantStream,
    AlternativeMedia,
    AlternativeMediaType,
};
use rocket::http::{ContentType, Status};
use rocket::response::Response;
use tempfile;

#[get("/stream/media/<file_name>")]
fn play_file<'r>(file_name: String) -> Response<'r> {
    println!("play_file called");
    let path = format!("./assets/{}", file_name);
    let filepath = PathBuf::from(path);

    match fs::read(&filepath) {
        Ok(file) => {
            let response = Response::build()
                .status(Status::Ok)
                .header(ContentType::new("application", "x-mpegURL"))
                .raw_header("Accept-Ranges", "bytes")
                .raw_header("Connection", "keep-alive")
                .sized_body(Cursor::new(file))
                .finalize();
            response
        },
        Err(e) => {
            println!("{}", e);
            let response = Response::build()
                .status(Status::Ok)
                .header(ContentType::Plain)
                .sized_body(Cursor::new("Error"))
                .finalize();
            response
        }
    }
}

#[get("/stream/playlist.m3u8")]
fn stream<'r>() -> Response<'r> {
    println!("stream called");
    let path = "./assets/master_playlist.m3u8";
    let filepath = PathBuf::from(path);

    match fs::read(&filepath) {
        Ok(file) => {
            let response = Response::build()
                .status(Status::Ok)
                .header(ContentType::new("application", "x-mpegURL"))
                .raw_header("Accept-Ranges", "bytes")
                .raw_header("Connection", "keep-alive")
                .sized_body(Cursor::new(file))
                .finalize();
            response
        },
        Err(e) => {
            println!("{}", e);
            let response = Response::build()
                .status(Status::Ok)
                .header(ContentType::Plain)
                .sized_body(Cursor::new("Error"))
                .finalize();
            response
        }
    }
}

fn main() {
    let mut playlist = Playlist::new();
    playlist.add_media_segment();
    playlist.add_master_playlist();

    rocket::ignite()
        .mount("/", routes![play_file, stream])
        .launch();
}

struct Playlist {
    master_playlist: MasterPlaylist,
    playlist: MediaPlaylist,
}

impl Playlist {
    fn new() -> Self {
        let mut playlist = MediaPlaylist::default();
        playlist.version = 3;
        playlist.target_duration = 60.0;
        playlist.media_sequence = 0;

        let mut master_playlist = MasterPlaylist::default();
        master_playlist.version = 3;

        Self {
            master_playlist: master_playlist,
            playlist: playlist
        }
    }

    #[allow(unused_must_use)]
    fn add_media_segment(&mut self) {
        let mut segment = MediaSegment::empty();
        segment.duration = 40.0;
        segment.title = Some("".into());
        segment.uri = "sample.ts".to_string();
    
        self.playlist.segments.push(segment);
        self.playlist.media_sequence = 1;

        let hls_root = PathBuf::from("./assets");

        let mut tmp_file = tempfile::Builder::new()
            .prefix("playlist.m3u")
            .suffix(".tmp")
            .tempfile_in(hls_root)
            .unwrap();

        self.playlist.write_to(&mut tmp_file);
        fs::rename(&tmp_file.path(), "./assets/playlist.m3u8");
    }

    #[allow(unused_must_use)]
    fn add_master_playlist(&mut self) {
        let mut alt_media = AlternativeMedia::default();
        //alt_media.uri = Some("playlist.m3u8".to_string());
        alt_media.media_type = AlternativeMediaType::Video;
        alt_media.group_id = "1".to_string();
        alt_media.name = "sample".to_string();

        let mut variant_stream = VariantStream::default();
        variant_stream.is_i_frame = false;
        variant_stream.uri = "http://localhost:8001/stream/media/playlist.m3u8".to_string();
        variant_stream.alternatives.push(alt_media);
        variant_stream.bandwidth = "640000".to_string();

        self.master_playlist.variants.push(variant_stream);

        let hls_root = PathBuf::from("./assets");

        let mut tmp_file = tempfile::Builder::new()
            .prefix("master_playlist.m3u")
            .suffix(".tmp")
            .tempfile_in(hls_root)
            .unwrap();

        self.master_playlist.write_to(&mut tmp_file);
        fs::rename(&tmp_file.path(), "./assets/master_playlist.m3u8");
    }
}
```

### 流れ
ざっくりと解説しようと思います。
使ったクレートは、[tempfile](https://docs.rs/tempfile/3.1.0/tempfile/)というものと、有名な[rocket](https://docs.rs/rocket/0.4.5/rocket/)と、m3u8を作るための[m3u8-rs](https://docs.rs/m3u8-rs/1.0.1/m3u8_rs/)です。

まず最初に、`Playlist`構造体というものを用意しておきます。MasterPlaylistを保持するフィールドと、MediaPlaylistを保持するフィールドを持っています。そしてそれぞれのフィールドに色々と設定を書き加えたプレイリストを格納します。

次は、rocketを使ってHTTPのレスポンスを用意します。
`#[get(...)]`という属性が付いている関数が、ルーティングの関数になっています。２つ関数があり、それぞれMasterPlaylistとMediaPlaylistを扱うようになっています。どちらを使っても動きますが、`Accept-Ranges`のヘッダがないとSafariで再生できなかったので`raw_header()`を使ってヘッダをつけておくことに注意しておくといいかもしれません。

ざっくりとこんな感じの記述で、HLS配信を実現できました。

# さいごに
ヘッダやm3u8の中身をちゃんと設定していないとちゃんと再生されず、サーバー作成にけっこう時間がかかってしまいました。実際はRTMPを受信してHLSを配信するサーバーを作成しないといけないので、これを参考にしながら既存のRTMPサーバーに改良を加えていこうと思います。

# 参考リンク
> https://blog.n-t.jp/hls-m3u8-masterplaylist/