クラウド東北きりたん その3 ~HLSでライブストリーミング~

HLSを使ったライブストリーミングを試してみます

前々回前回の続きです。

あらすじ

前々回はPythonからWin32APIをバシバシ叩いてきりたん好きなコトを喋らせることができるようになったのでした。
クラウド東北きりたん その1 ~Win32APIでVOICEROIDを操作~

前回はAzureのWindowsServerにHTTPリクエストを送ってきりたん好きなコトを喋らせるサーバができたのでした。
クラウド東北きりたん その2 ~AzureのWindowsServerでVOICEROIDを動かす~

今回は、HTTP Live Streaming(HLS)を用いてきりたんボイスをライブ配信してみようと思います!

HTTP Live Streaming

HTTP Live Streamingとは、Appleが開発したHTTPベースのストリーミング配信プロトコルです。
静的な動画ファイルのストリーミング配信はもちろん、ライブ配信(生放送)もできたり、
アダプティブストリーミングと呼ばれる回線速度に応じて配信するビットレートを変更する技術も利用可能です。

最近話題のAbemaTVなんかでも、HLSで配信を行っています。
ちなみに、Twitterにアップされた動画もHLSで配信されています。

ストリーミング配信プロトコルと聞くと、複雑そうな気がしてきますが、HLSはHTTPベースで非常に単純です。
ザックリと説明を書いてみます。

HLSのしくみ

HLSでの配信は、.tsファイルと.m3u8ファイルによって行われます。

ts

.tsファイルは、MPEG-2 TSと呼ばれる形式で、配信される映像・音声そのものが格納されます。

配信されるデータは一定の秒数ごとに分割し、このMPEG-2 TS形式で保存しておきます。
分割された.tsファイルは、HTTPでダウンロードできるようにしておきます。

ちなみに、日本のデジタルテレビ放送もこのMPEG-2 TSで配信されています。

m3u8

.m3u8ファイルは、配信ファイルのインデックスです。
先述した.tsに分割された映像・音声データのURLが列記されています。

AbemaTVから配信されている.m3u8の例

1
2
3
4
5
6
7
8
9
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000
240/playlist.m3u8?t=3i87VhR5nuXMsjxJRGBiEYSNPdfggGQtr9LjXNx1fr5Dufac7cEaEKMyo2UAv77B63hAvVewach5eaPjFGK3EU22fcpcFD4RAeNAE7nisDwZguUqvp&mq=720&lanceId=c99528aa-0c3c-4987-ab6c-ce5cd1430223
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=900000
360/playlist.m3u8?t=3i87VhR5nuXMsjxJRGBiEYSNPdfggGQtr9LjXNx1fr5Dufac7cEaEKMyo2UAv77B63hAvVewach5eaPjFGK3EU22fcpcFD4RAeNAE7nisDwZguUqvp&mq=720&lanceId=c99528aa-0c3c-4987-ab6c-ce5cd1430223
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1400000
480/playlist.m3u8?t=3i87VhR5nuXMsjxJRGBiEYSNPdfggGQtr9LjXNx1fr5Dufac7cEaEKMyo2UAv77B63hAvVewach5eaPjFGK3EU22fcpcFD4RAeNAE7nisDwZguUqvp&mq=720&lanceId=c99528aa-0c3c-4987-ab6c-ce5cd1430223
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2200000
720/playlist.m3u8?t=3i87VhR5nuXMsjxJRGBiEYSNPdfggGQtr9LjXNx1fr5Dufac7cEaEKMyo2UAv77B63hAvVewach5eaPjFGK3EU22fcpcFD4RAeNAE7nisDwZguUqvp&mq=720&lanceId=c99528aa-0c3c-4987-ab6c-ce5cd1430223

これはMaster Playlistと呼ばれるデータで、
回線速度によって異なるビットレートでの配信を行うアダプティブストリーミングのためのファイルです。
次に示すMedia PlaylistのURLと想定する回線速度が列記されています。

AbemaTVから配信されている.m3u8の例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:4
#EXT-X-DISCONTINUITY-SEQUENCE:1
#EXT-X-KEY:METHOD=AES-128,URI="abematv://v2/abema-news/abema-news/DUjoiyL1pJGkADZotyiXDn5",IV=0xaccca4b41de3d9afb029070eb564be40
#EXTINF:5.005000,
https://abematv.akamaized.net/tsnews/abema-news/h264/720/5BPWe1D8Hu9yCC8HaA3oHS.ts
#EXTINF:5.005000,
https://abematv.akamaized.net/tsnews/abema-news/h264/720/5SphyMY1TTLvYkFo7B5JuM.ts
#EXTINF:5.005000,
https://abematv.akamaized.net/tsnews/abema-news/h264/720/2kxyGFo9sH9zUUfKj5USUk.ts
#EXTINF:5.005000,
https://abematv.akamaized.net/tsnews/abema-news/h264/720/Cz43TVWLgUgqskzvWBBnjA.ts

これはMedia Playlistと呼ばれるデータで、
配信されている映像・音声が格納された.tsファイルのURLが列記されています。

再生の方法

クライアントは、まず.m3u8ファイルを取得します。
それがMaster Playlistであれば、回線速度によって適切な.m3u8を読みに行きます。
それがMedia Playlistであれば、.tsファイルを取得して再生します。

クライアントは、.m3u8内のタグと呼ばれるデータ(#EXTで始まる行)に従って、.m3u8を再読込します。
ライブ配信を行う場合は、クライアントが再読込した際に新しい配信データが追加されていれば良いわけです。

以下に、主要なタグの説明を示します。

EXT-X-TARGETDURATION

分割された.tsの中で最大の長さに最も近い整数値を指定します。
クライアントは、およそこの秒数ごとに.m3u8を再読込します。

https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1

EXT-X-MEDIA-SEQUENCE

その.m3u8にかかれている一番最初の.tsが、放送全体で何番目の.tsであるかの値を指定します。
クライアントが分割された.tsを正しく連続再生する上で必要になります。

https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.2

EXTINF

分割された.ts1つの秒数。小数で指定できる。

https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1

HLSを再生したい

HLSはブラウザ上で再生できるのが強いです。
https://caniuse.com/#search=HLS

ん?????なんか赤いな……

FirefoxとChromeが対応してないやんけ!!!!!!!!!
珍しくEdgeが優秀だ……

悲しいですね。
でもMesia Source Extensions(MSE)という機能を使うとそれっぽくHLSを再生できるので安心です。
https://caniuse.com/#search=MSE

MSEを使ったHLS再生は、Video.jsとかhls.jsとかのライブラリを使うと簡単です。

ちなみに、AbemaTVはTHEOplayerという有償のプレーヤーを使ってるみたい。

HLSで生配信

HLSをなんとな~くわかった気になったので、ライブ配信をやってみます。

HLSで生配信をするにはどうすればよいのかというと、つまり

  • データをMPEG-2 TSにエンコードする
  • .m3u8.tsへのリンクを追加する

を繰り返すだけです。

.tsへのを追加していくだけだとドンドン.m3u8がでっかくなってしまうので、
過去の.tsへのリンクはある程度時間が立ったら消してしまいましょう。
.tsへのリンクを消したら、#EXT-X-MEDIA-SEQUENCEを増やさないとクライアントが困ってしまうので注意です。

とっても単純ですね!
さて、先述したことをやるだけでライブ配信サーバが書けてしまいます。

今回は、Twitterからタイムラインを取得して、ツイートをいい感じにきりたんに読んでもらい、
HLSを用いてリアルタイムでその音声データを配信してみます。

音声ファイルを分割してMPEG-2 TSにするのを自分で書くのは流石にしんどいので、
FFMPEGさんにお願いしました。
https://www.ffmpeg.org/ffmpeg-formats.html#hls-1

やること

twitter.listen()

  • UserStreamでツイート取得
  • kiritan.pyにジョブを投げる
  • encoder.pyのキューに読み上げたWAVファイルを蓄積

encoder.livestreaming()

  • キューにファイルがなければ無音データをプレイリストに追加
  • キューにファイルがあればTSに分割してプレイリストに追加
  • プレイリストの先頭のTSの再生時間分だけ待って、プレイリストから削除

やりました

方針が定まったら書くだけ……

コード

全コード
https://github.com/kaz/kiritan-server

HLS関係の処理はたったコレだけです!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# FFMPEGでファイルをMPEG-TSにエンコード(中身はMP3)
def ts(file):
logging.info("Encoding WAV to MPEG-TS")

data = subprocess.run(
[
"ffmpeg",
"-i", file, "-vn",
"-acodec", "libmp3lame",
"-ab", "128k",
"-ac", "2",
"-ar", "44100",
"-f", "hls",
"-hls_time", "2",
"-hls_list_size", "0",
"-start_number", str(int(time.time() * 1000)),
"-hls_segment_filename", "static/live%d.ts",
"pipe:1.m3u8"
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)

# 出力されたプレイリストをパースして返す
playlist = data.stdout.decode("utf-8")
playlist = playlist[playlist.rfind("#EXTM3U"):]

# Tuple (再生時間, ファイルパス)
return re.findall(r"#EXTINF:([\d.]+),\s+(\S+)", playlist)

# ライブストリーミングキューに追加
que = []
def enqueue(f):
que.append(f)

# ライブプレイリストを更新
tsl = []
seq = 0
def __livecasting():
global seq

while True:
try:
if len(que) != 0:
# キューにデータがあればプレイリストに追加
tsl.extend(ts(que.pop(0)))
else:
# キューが空なら無音ファイルを配信
while len(tsl) < 3:
tsl.append(("2.04", "silent.ts"))

# TS 1つ分だけ休憩する
time.sleep(float(tsl[0][0]))
tsl.pop(0)
seq += 1
except:
logging.error(traceback.format_exc())

# サーバ起動
def livecasting():
# 古い配信データを削除
for f in glob.glob("static/live*"):
os.remove(f)

threading.Thread(target=__livecasting).start()

# ライブプレイリストを生成
def playlist():
pl = [
"#EXTM3U",
"#EXT-X-VERSION:3",
"#EXT-X-TARGETDURATION:3",
"#EXT-X-MEDIA-SEQUENCE:%d" % seq
]

for ts in tsl[:5]:
pl.append("#EXTINF:%s," % ts[0])
pl.append("#EXT-X-DISCONTINUITY")
pl.append("/static/%s" % ts[1])

return "\n".join(pl)

ffmpegを使っているので、別途用意が必要です。
必要なPythonのライブラリはpypiwin32flasktweepyです

1
pip install pypiwin32 flask tweepy

動作検証

大体のブラウザでhls.jsを介した再生ができました。

ネイティブでHLSに対応しているブラウザ(Safari, Edge, iOS Safari, Android Chrome)は、
.m3u8に直接アクセスしても再生できました。

なんかAndroidだとちょっとプツプツしちゃってるかも???

ハマりそうなポイント

  • TS1つの長さ、プレイリスト全体の長さ、#EXT-X-TARGETDURATIONをうまく調整しないと再生されなかったりプツプツなったりする
    • このへんどうするのが最適なのかがわからないので今回は試行錯誤した
  • TSが切り替わる(別のメディアから生成したものになる)時に#EXT-X-DISCONTINUITYを付けないと再生が止まる
    • Appleのソフトウェアはうまくやってくれるけど、その他は上手く行かない
  • TwitterのUserStreamはPCの時計かズレてると認証失敗する

おしまい

ということで、AzureのWindowsServerでWin32APIを使ってVOICEROIDを操作してTwitterのTLを読み上げた音声をHLSでライブ配信できました!

Win32APIとかHLSとか、まだわからないことがたくさんなので、それはおかしいだろ!って思ったら鉞おねがいします><

それにしても、きりたんはかわいいですね!

おしまい

このエントリーをはてなブックマークに追加