概要
背景
Webページ上で動画を再生できるようにしたい場合、Youtubeの動画を埋め込むのが主流だ。しかし、規約の問題やアクセス制御要件によっては、自前での配信が必要な場合もある。
Webサーバ上にmp4を配置して直配信する、というのが真っ先に思いつく方法だが、円滑なストリーミング配信・アダプティブビットレート対応を考えると、HLSに変換してから配信する方が良い。
高性能なPCがあれば都度ローカルで変換できるが、そうでない場合はクラウド上で変換することになる。今回はさくらのクラウドでその変換機構を構築した。
成果
ソース群はこちら。
主たる構成はいたってシンプルで、オブジェクトストレージに格納されたmp4をサーバー上でFFmpegを用いてHLS変換、成果物をまたオブジェクトストレージに格納するというもの。配信はさくらのウェブアクセラレータで行う。
変換済のmp4についてはS3互換APIでタグを付与しているため、再実行時は未処理のmp4だけを洗い出してHLS変換できる。
- サーバー(8コア・16GB)・ディスク(100GB)
- オブジェクトストレージ
- さくらのウェブアクセラレータ

変換用サーバーはTerraformで構築し、簡単に削除できるようにしている。動画変換に用いる高性能・高価格なものであるため、稼働時間を最小限にするのが目的だ。
環境構築
オブジェクトストレージ2バケットとさくらのウェブアクセラレータは設定済みと仮定する。さくらのクラウドマニュアルに丁寧な解説があるため参照されたし。
Terraformによるサーバー構築
サーバー構築をTerraform for さくらのクラウド v3を用いて行う。基本的にはサーバーを1台構築するだけだが、最低限のセキュリティ担保のため、KMSによるディスク暗号化とパケットフィルタを適用している。TLS鍵も作成し、公開鍵認証によるSSHログインに対応している。
また、スタートアップスクリプトでFFmpegとgolangも自動ダウンロードする。
data "sakura_archive" "ubuntu" {
os_type = "ubuntu2404"
}
resource "sakura_kms" "hls_converter_server_encryption_key" {
name = "hls_converter_server_encryption_key"
description = "Encryption key for the HLS converter server."
key_origin = "generated"
}
resource "sakura_disk" "hls_converter_disk" {
name = "hls_converter_disk"
description = "Disk for the HLS converter server."
connector = "virtio"
encryption_algorithm = "aes256_xts"
icon_id = var.ubuntu_icon
kms_key_id = sakura_kms.hls_converter_server_encryption_key.id
plan = "ssd"
size = 100
source_archive_id = data.sakura_archive.ubuntu.id
zone = var.zone
}
resource "sakura_packet_filter" "minimum_filter" {
name = "minimum_filter"
description = "Minimum packet filter for the HLS converter server."
zone = var.zone
}
resource "sakura_packet_filter_rules" "rules" {
packet_filter_id = sakura_packet_filter.minimum_filter.id
zone = var.zone
expression = [
{
description = "Allow SSH access. Limit source IP addresses, if needed."
destination_port = "22"
protocol = "tcp"
source_network = "0.0.0.0/0"
},
{
protocol = "udp"
source_port = "123"
source_network = "0.0.0.0/0"
},
{
protocol = "udp"
destination_port = "68"
},
{
protocol = "icmp"
},
{
protocol = "tcp"
destination_port = "32768-61000"
},
{
protocol = "udp"
destination_port = "32768-61000"
},
{
protocol = "fragment"
},
{
protocol = "ip"
allow = false
description = "Deny all except above rules."
}
]
}
resource "sakura_script" "ffmpeg_golang_install_script" {
name = "ffmpeg_golang_install_script"
class = "shell"
content = file("scripts/install_ffmpeg_golang.sh")
icon_id = var.ubuntu_icon
}
# Generate a temporary SSH key pair for the server.
resource "tls_private_key" "temporary_ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
}
# Save the private key to a local file. Please save it securely, as it will be needed to access the server.
resource "local_sensitive_file" "private_key_file" {
content = tls_private_key.temporary_ssh_key.private_key_pem
filename = ".ssh/id_rsa.pem"
}
resource "sakura_ssh_key" "hls_converter_server_sshkey" {
name = "hls_converter_server_sshkey"
description = "SSH key for the HLS converter server. Please save it in .ssh/ directory."
public_key = tls_private_key.temporary_ssh_key.public_key_openssh
}
resource "sakura_server" "hls_converter_server" {
name = "hls_converter_server"
description = "Server for the HLS converter."
core = 8
disks = [sakura_disk.hls_converter_disk.id]
icon_id = var.ubuntu_icon
interface_driver = "virtio"
memory = 16
tags = ["@keyboard-us"]
zone = var.zone
disk_edit_parameter = {
hostname = "ubuntuhost"
password_wo = var.os_password
password_wo_version = 1
disable_pw_auth = true
ssh_key_ids = [sakura_ssh_key.hls_converter_server_sshkey.id]
script = [{
id = sakura_script.ffmpeg_golang_install_script.id
}]
}
network_interface = [{
upstream = "shared"
packet_filter_id = sakura_packet_filter.minimum_filter.id
}]
}
おなじみのコマンドを叩く。
terraform init #初回だけ
terraform apply
Go実行準備
構築が完了したら、サーバーにmain.goを移す(先にコンパイルしても問題ない)。

ライブラリを追加(コンパイル済の場合は不要)
go mod init 任意の名前
go mod tidy
これで、実行の準備が整った。
実行
実際に処理を走らせてみる。
今回は1080p、720p、480pでアダプティブビットレートを適用する。
cmd := exec.Command("ffmpeg",
"-hide_banner",
"-y",
"-i", input_path,
"-filter_complex", "[0:v]split=3[v1][v2][v3];[v1]scale=1920:1080[v1out];[v2]scale=1280:720[v2out];[v3]scale=854:480[v3out]",
"-flags", "+cgop",
"-map", "[v1out]", "-c:v:0", "libx264", "-preset:v:0", "slow", "-b:v:0", "5000k", "-maxrate:v:0", "5350k", "-bufsize:v:0", "7500k", "-g:v:0", gop_size_str, "-keyint_min:v:0", gop_size_str, "-sc_threshold:v:0", "0",
"-map", "[v2out]", "-c:v:1", "libx264", "-preset:v:1", "slow", "-b:v:1", "2800k", "-maxrate:v:1", "2996k", "-bufsize:v:1", "4200k", "-g:v:1", gop_size_str, "-keyint_min:v:1", gop_size_str, "-sc_threshold:v:1", "0",
"-map", "[v3out]", "-c:v:2", "libx264", "-preset:v:2", "slow", "-b:v:2", "1400k", "-maxrate:v:2", "1498k", "-bufsize:v:2", "2100k", "-g:v:2", gop_size_str, "-keyint_min:v:2", gop_size_str, "-sc_threshold:v:2", "0",
"-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "192k",
"-map", "0:a:0", "-c:a:1", "aac", "-b:a:1", "128k",
"-map", "0:a:0", "-c:a:2", "aac", "-b:a:2", "96k",
"-f", "hls",
"-hls_time", hls_segment_seconds_str,
"-hls_playlist_type", "vod",
"-hls_flags", "independent_segments+temp_file",
"-master_pl_name", "master.m3u8",
"-var_stream_map", "v:0,a:0 v:1,a:1 v:2,a:2",
filepath.Join(output_dir, "stream_%v.m3u8"),
)
変換元の動画は、筆者が撮った映画を用いる。1080p、16:9、24fps、72分でおよそ8GBのmp4動画データである。
データ格納
まず、オブジェクトストレージのinputバケットに格納する。

出力バケットは言わずもがな、outputバケット。
変換実行
実際にコマンドを叩いて、実行する。以下のような引数で動くよう、Goを記述した。
go run main.go -endpoint="https://オブジェクトストレージのエンドポイント" -region="リージョン" -input-bucket="mp4を格納したバケット名" -output-bucket="HLSを出力するバケット名" -access-key="アクセスキーID" -secret-key="シークレットアクセスキー"
しばらくすると、処理が完了する。

それなりに高性能なサーバーで処理したが、処理に要した時間は元動画の尺とほぼ同一。さすがに重い処理である。
HLS再生
オブジェクトストレージの出力先バケットに.m3u8と.tsファイルが保存されたので、簡易的な検証用htmlを同バケットに用意して、さくらのウェブアクセラレータ経由で表示、再生できるか確認してみた。m3u8のパスをフォームに書くことで、HLSが再生できる。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>S3 HLS Player</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
font-family: sans-serif;
margin: 20px;
background-color: #000000;
}
h2 {
color: #d7003a;
}
.container {
max-width: 800px;
margin: 0 auto;
background: #0d0015;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #d7003a;
color: #fff;
border: none;
border-radius: 4px;
}
button:hover {
background-color: #a22041;
}
video {
width: 100%;
background-color: #000;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h2>S3 HLS Player</h2>
<div class="input-group">
<input type="text" id="m3u8-url" placeholder="path/master.m3u8" />
<button onclick="playVideo()">再生</button>
</div>
<video id="video" controls></video>
</div>
<script>
var hls = null;
function playVideo() {
var video = document.getElementById("video");
var url = document.getElementById("m3u8-url").value;
if (!url) {
alert("m3u8ファイルのURLを入力してください");
return;
}
// 既存のhlsインスタンスがある場合は破棄
if (hls) {
hls.destroy();
}
// hls.js がサポートされているブラウザの場合
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
}
// Safariの場合や、ネイティブでHLS再生が可能なブラウザの場合
else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
} else {
alert("お使いのブラウザはHLS再生に対応していません");
}
}
</script>
</body>
</html>
実際にパスを打ってみると……

問題なく再生できた。これで、mp4->HLS->配信が一気通貫で行えたことになる。
まとめ
今回はさくらのクラウドでmp4->HLS変換を実行してみた。
記事化のため簡易的な構成にしたが、CronやEventBusを用いて既存余剰サーバーでの深夜定期実行にしたり、逆に大規模構成化することも可能である。
閉域網で構築したい場合は、サーバーを共有セグメントではなくスイッチに接続して、スイッチのサービスエンドポイントゲートウェイからオブジェクトストレージに接続するのも手である。この場合、VPNルータとあわせて時間単価は+48円程度になる。

やり方はいろいろあるので、ぜひ試してみて欲しい。
最後にちょっとした所感
GPUは今や、主に推論向けの商品であるため、こうした動画処理に適したNVENC搭載GPUは話題に挙がりにくい。しかし、見ての通り8コア16GBでも相当な時間がかかる重い処理であるため、高額GPUに手が出せない私のような庶民でもオンデマンドで処理できるよう、引き続き普及が進んで欲しい。

