フリーソフトだけで動画配信システム構築

世界のインターネットトラフィックのうち動画が占める割合は、おおむね60%超に達すると各種統計で報告され¹²、社内外イベントのライブ配信やオンデマンド学習(VOD: Video on Demand)は企業の標準ワークフローになりました。一方で商用SaaSを前提にした設計は、視聴ピークや長時間配信でコストが雪だるま式に増えがちです。一般的な運用でも、ビットレートと同時視聴数に比例して帯域費用が跳ね上がり、配信プラットフォームの従量課金がROI(投資対効果)を圧迫する局面が見られます。そこで視点を変え、無料のフリーソフト(オープンソース)だけで、現実的に運用できる動画配信スタックを組むとどうなるかを、実装コード、性能目安、セキュリティ、運用の観点まで具体的に解きほぐします。前提は中級以上のLinux運用とネットワーク設計の知識です。クラウドでもオンプレでも、コモディティなx86サーバーが1台あれば、PoC(概念実証)はその日のうちに立ち上がります。
無料ソフトで実現する全体像と要件整理
無料のOSSで完結させるとき、構成はシンプルです。入力として配信者側からRTMP(リアルタイムメッセージングプロトコル)またはSRT(Secure Reliable Transport。損失に強いUDP系)で受け、サーバー側でFFmpegによるトランスコード(符号化し直し)とパッケージングを行い、出力としてHLS(HTTP Live Streaming)またはMPEG-DASHのセグメントをNginxで静的配信します。フロントではhls.jsやShaka Playerといった無料のプレイヤーをブラウザに組み込み、TLS(HTTPSの暗号化)はLet’s Encryptで自動化します。この流れを、ライブ、疑似ライブ、VODのいずれにも横展開できます。
非機能要件として重要になるのは、スループット、レイテンシ、可用性、観測性の四点です。スループットは視聴同時数とビットレートの積で上限を固定的に決めます。例えばフルHDの平均2.5Mbpsを前提に同時1,000視聴であれば、単純計算で2.5Gbpsの下り帯域が必要です。HTTPキャッシュ(CDNやプロキシ)を前段に置くとオリジンの負荷は下がりますが、無料スタックのみで構成する場合は、オリジンの10GbEインターフェースと十分なディスクIO、そしてカーネルのTCP送受信バッファなどのチューニングが現実解になります。レイテンシはHLSのセグメント長に直結するため、6秒×3セグメントのデフォルト運用ならおおよそ18~30秒の遅延、3秒セグメントなら10~15秒程度が目安です⁴⁵。可用性は水平方向のオリジン冗長と、配信URLのクライアント側フェイルオーバが鍵になります。観測性はNginxのアクセスログ、FFmpegのログ、node_exporterとPrometheusでのメトリクス収集、Grafanaでの可視化が無料で成立します。
ユースケース別に見る設計の勘所
ライブ配信は入力側のネットワーク品質とサーバーのトランスコード余力がボトルネックになりやすく、ABR(Adaptive Bitrate Streaming。複数ビットレート)を用意して再生側のネットワーク揺らぎに備えるのが基本です³。疑似ライブはオンデマンドファイルを時間同期で順次出す方式で、編成管理の実装が追加される代わりに入力側の不確実性がなくなります。VODはストレージコストとキャッシュ効率が勝負で、無料の範囲であってもファイルのチャンク配置とキャッシュキー設計で体感品質は大きく変わります。
スループットとリソースの目安
CPUはx264での1080p60トランスコードを複数本並列に回すとコアが飽和しやすく、エンコードプリセットをultrafast〜veryfastに寄せると遅延と画質のバランスが取りやすくなります。ハードウェアエンコードを使わずに無料で済ませる場合、8 vCPUで1080p30を2〜3本、720p30を3〜4本程度の同時トランスコードが現実的な上限の一例です。ネットワークはHTTP/1.1のキープアライブと送信バッファの拡張が有効で、ディスクはセグメントの短時間滞留が中心のためNVMeでなくとも安定した書き込み性能があれば十分に回ります。
実装ステップ:Nginx-RTMPとFFmpeg、hls.jsで組む
まずは最小構成でPoCを立ち上げます。Ubuntu 22.04 LTSのクリーン環境を想定し、NginxのOSS版にRTMPモジュールを組み込み、FFmpegを用意し、静的配信とTLS終端を同居させます。必要なのはパッケージマネージャでの導入と設定ファイルの配置、そしてサービスの永続化です。
RTMP取り込みとHLS/DASHの出力
Nginxの設定は、rtmpブロックでアプリケーションを定義し、execでFFmpegにパイプしてHLSのセグメントを切る方法が簡潔です。以下は最小の例で、RTMPの取り込みを1080pと720pの二段でHLSに変換し、オリジンからそのまま配信します。
# /etc/nginx/nginx.conf(抜粋)
worker_processes auto;
events { worker_connections 10240; }
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server {
listen 80;
server_name example.com;
location /hls/ {
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
add_header Cache-Control "no-cache";
root /var/www;
}
}
}
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
exec ffmpeg -i rtmp://localhost/live/$name \
-map 0:v -map 0:a -c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 -c:a aac -b:a 128k \
-s:v:0 1920x1080 -b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
-s:v:1 1280x720 -b:v:1 2800k -maxrate:v:1 2990k -bufsize:v:1 4200k \
-f hls -hls_time 3 -hls_list_size 6 -hls_segment_filename /var/www/hls/$name_%v_%03d.ts \
-master_pl_name /var/www/hls/$name.m3u8 -var_stream_map "v:0,a:0 v:1,a:0" /var/www/hls/$name_%v.m3u8;
}
}
}
DASHを併用したい場合は、FFmpegの出力をdashに変えるだけで成立します。hls.jsはHLSのみの対応ですが、Shaka Playerを使えばDASHも無料で再生できます。レイテンシを抑えるには3秒セグメントにし、playlistの数を短く維持します。低遅延HLS(LL-HLS)を厳密に実装するには追加のサーバー実装やCDN設定が必要なため、無料スタックの最初の一歩としては通常のHLSでの安定運用を優先するのが無難です⁵。
フロントエンドのプレイヤー実装
ブラウザ側はhls.jsを使います。MSE(Media Source Extensions)に対応していればSafari以外でもHLSを再生できます。以下は最小の埋め込み例で、例外処理と再試行も加えています。
<!doctype html>
<html>
<head><meta charset="utf-8"><title>OSS Player</title></head>
<body>
<video id="v" controls autoplay playsinline width="960" height="540"></video>
<script type="module">
import Hls from 'https://cdn.jsdelivr.net/npm/hls.js@latest';
const video = document.getElementById('v');
const src = 'https://example.com/hls/stream.m3u8';
if (Hls.isSupported()) {
const hls = new Hls({maxRetries: 3, enableWorker: true});
hls.on(Hls.Events.ERROR, (e, data) => {
console.warn('HLS error', data);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR: hls.startLoad(); break;
case Hls.ErrorTypes.MEDIA_ERROR: hls.recoverMediaError(); break;
default: hls.destroy();
}
}
});
hls.loadSource(src);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src;
} else {
document.body.insertAdjacentHTML('beforeend', '<p>HLS未対応のブラウザです。</p>');
}
</script>
</body>
</html>
この段階で、配信者は OBS や vMix から rtmp://example.com/live/stream のようなURLへ配信し、視聴者はブラウザでm3u8を再生できます。TLSはcertbotで無料更新し、HTTPからHTTPSへリダイレクトを設定すればひとまず完了です。
セキュリティと運用:トークン、ログ、スケール
無料の構成でも、アクセス制御と不正視聴対策は必須です。Nginxのsecure_linkやRTMPのon_publishコールバックと連動させ、短寿命トークンで流通を制御します。アプリケーション側は軽量なAPIだけで足り、外部データベースがなくてもHMAC(鍵付きハッシュ)による署名で保護できます。
配信開始の認証と再生の署名URL
まずは配信開始時の検証です。Nginx-RTMPはon_publishでHTTPコールバックが可能なので、Node.jsの小さなサービスで署名を検証します。
// server.js
import express from 'express';
import crypto from 'node:crypto';
const app = express();
app.use(express.urlencoded({ extended: true }));
const SECRET = process.env.STREAM_SECRET || 'change-me';
function verify(key, expires, sig) {
const msg = `${key}:${expires}`;
const mac = crypto.createHmac('sha256', SECRET).update(msg).digest('hex');
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig));
}
app.post('/auth/on_publish', (req, res) => {
try {
const key = req.body.name;
const expires = req.body.expires;
const sig = req.body.sig;
if (!key || !expires || !sig) return res.status(400).send('bad');
if (Date.now() > Number(expires)) return res.status(403).send('expired');
if (!verify(key, expires, sig)) return res.status(403).send('forbidden');
return res.status(200).send('ok');
} catch (e) {
console.error(e);
return res.status(500).send('error');
}
});
app.listen(8080, () => console.log('auth up'));
Nginx側ではapplication liveにon_publishを設定し、200が返らなければ配信を拒否します。再生側の保護にはsecure_link_md5を使うか、アプリケーションで署名付きURLを発行します。以下はPython FastAPIでm3u8の署名付きURLを返す例です。
# app.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import hmac, hashlib, time
from urllib.parse import quote
app = FastAPI()
SECRET = b"change-me"
def sign(path: str, exp: int) -> str:
msg = f"{path}{exp}".encode()
sig = hmac.new(SECRET, msg, hashlib.sha256).hexdigest()
return f"{path}?exp={exp}&sig={sig}"
@app.get("/play")
def get_url(stream: str):
if not stream:
raise HTTPException(400, "missing")
exp = int(time.time()) + 300
path = f"/hls/{quote(stream)}.m3u8"
url = sign(path, exp)
return JSONResponse({"url": url})
Nginxでsecure_linkを検証すると、短寿命のURL以外は403になります。静的配信でも十分に機能するため、無料の制約下であっても恒久的なホットリンク対策として有効です。
FFmpegプロセスの健全性と復旧
ライブでは入力の瞬断が一定の確率で起こります。FFmpegのexitコードや標準エラーを監視し、自動で再起動するのが安定運用の近道です。Goで最小のプロセスマネージャを書くと、外部のプロセス監視に頼らずに復旧を自動化できます。
// guardian.go
package main
import (
"bufio"
"context"
"log"
"os/exec"
"time"
)
func run(ctx context.Context, args []string) error {
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
stderr, _ := cmd.StderrPipe()
if err := cmd.Start(); err != nil { return err }
go func() {
s := bufio.NewScanner(stderr)
for s.Scan() { log.Printf("ffmpeg: %s", s.Text()) }
}()
return cmd.Wait()
}
func main() {
args := []string{"-re", "-i", "rtmp://localhost/live/stream", "-c:v", "libx264", "-f", "hls",
"-hls_time", "3", "-hls_list_size", "6", "/var/www/hls/stream.m3u8"}
for {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
err := run(ctx, args)
cancel()
if err != nil { log.Printf("ffmpeg exit: %v", err) }
time.Sleep(2 * time.Second)
log.Printf("restarting ffmpeg")
}
}
systemdでRestart=alwaysを指定してもよいですが、ログ整形や通知、段階的なバックオフなどをアプリに持たせると復旧条件を細かく制御できます。入力の瞬断を検知したら低画質プロファイルのみで再起動して帯域を確保するといった運用も可能です。
Nginxでの署名検証とCORS
secure_linkの検証設定は短い記述で導入できます。CORSヘッダを付けてフロントアプリからの再生を許可し、不要なメソッドは閉じます。
# /etc/nginx/snippets/secure.conf
set $secret 'change-me';
secure_link $arg_exp$arg_sig;
secure_link_md5 "$uri$arg_exp$secret";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 403; }
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
アプリ側でexpとsigを発行し、Nginx側で同じシークレットを使って検証します。トークンは短寿命にし、リクエスト単位での再発行を前提にします。
性能目安、チューニング、コスト試算
性能はハードウェア、プリセット、プロファイル本数に敏感です。例えば、8 vCPU(3.2GHzクラス)、32GBメモリ、SATA SSD、10GbEの構成といった一般的なx86環境の一例では、1080pと720pの二段ABRを持つ単一ライブチャンネルのトランスコードと、同時視聴でおおむね1,000〜1,500規模までのHLS配信をオリジン単体で捌けたという報告もあります。ネットワークスタックの送信バッファ拡張と、Nginxのworker_processes auto、worker_connections 10k前後の設定が効きやすい傾向です。もちろん視聴端末の分布やセグメント長、TLSオフロードの有無で数字は変動します。
チューニングでは、FFmpegのGOP(Group of Pictures)長をfpsの2倍から4倍程度に固定し、シーンチェンジ検出を抑制してABRのTS化でのスイッチングを安定させます。x264のpresetはveryfast付近から入り、ビットレートを実測で詰めます。Nginxはsendfileとtcp_nopushの併用、open_file_cacheの適切なTTL、TLSではECDHEとHTTP/2を有効にしつつセッション再利用を有効化します。観測面では、次のようなPrometheusのexporterとスクレイプを無料で組み合わせるだけで十分な可視化ができます。
# prometheus.yml(抜粋)
scrape_configs:
- job_name: 'node'
static_configs: [{ targets: ['localhost:9100'] }]
- job_name: 'nginx'
static_configs: [{ targets: ['localhost:9113'] }]
コストは無料のソフトウェアであるがゆえにライセンスはゼロですが、帯域とコンピュートは現実的な費目になります。自社データセンターで10GbE回線を既に保有しているなら、追加費用は主に電力と機器減価に限られます。クラウドを使う場合はegress課金が支配的になり、たとえば2.5MbpsのフルHDを1,000同時で1時間配信すると合計帯域は約1,125GBになり、一般的なクラウドの外向き料金を当てはめると数百ドル相当になります。ここを抑えるには、視聴を社内ネットワークに寄せる、ABRの初期ビットレートを保守的にする、セグメントのキャッシュヒットを高めるといった手筋が有効です。
導入期間とROIの考え方
PoCは半日から1日で成立し、ドキュメンテーションとプレイヤーブランディングを含めた内製のβ運用は1週間程度が現実的な目安です。SaaSの従量課金と比較すると、月間数百時間の配信と数千の視聴を超えたあたりから無料構成の固定費優位が見え始めることがあります。初期構築の工数は必要ですが、内製化によりコンテンツ保護の方針を柔軟に決められ、ログやメトリクスをもとに最適化ループを高速に回せる点がROIに効いてきます。
エッジキャッシュなしでの同時視聴の上限目安
オリジン単体での同時視聴上限は、ネットワークのスループットとカーネル設定で大きく動きます。10GbEでHTTP/1.1のキープアライブが効いている場合、2.5Mbpsのプロファイルなら理論上の帯域上限は同時3,500視聴前後になりますが、現実にはTCPオーバーヘッドやTLS、セグメントの断続的なピークがあるため、7割程度を上限目安に置くのが安全です。余裕を持たせたい場合は視聴を720p主体にする方針が実効的です。
VODパイプラインも無料で組む
VODではアップロードを受けて非同期でマルチプロファイルを生成し、静的に配信します。ここでもFFmpegとNginxだけで成立しますが、キューイングとリトライを小さなAPIで制御すると運用が安定します。TypeScriptでS3互換ストレージに配置する前処理を記述し、アップロード直後にHLSを作ります。
// worker.ts
import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'auto', endpoint: process.env.S3_ENDPOINT, forcePathStyle: true });
async function transcode(input: string, outDir: string) {
return new Promise((resolve, reject) => {
const args = ['-i', input, '-c:v', 'libx264', '-preset', 'veryfast', '-c:a', 'aac', '-f', 'hls', '-hls_time', '4', `${outDir}/master.m3u8`];
const p = spawn('ffmpeg', args);
p.stderr.on('data', d => process.stderr.write(d));
p.on('exit', code => code === 0 ? resolve(0) : reject(new Error('ffmpeg exit ' + code)));
});
}
async function upload(dir: string) {
// 省略: dir配下のm3u8/tsをS3へ並列アップロード
}
async function main() {
try { await transcode('/uploads/input.mp4', '/var/www/vod/vid1'); await upload('/var/www/vod/vid1'); }
catch (e) { console.error(e); process.exitCode = 1; }
}
main();
再生側はライブと同様のプレイヤーでm3u8を指すだけです。VODではキャッシュの効きがよいため、社内プロキシや無料のリバースプロキシでヒット率を高めるだけでも体感品質が安定します。章立てやクローズドキャプションを追加したい場合は、FFmpegのメタデータマップとWebVTTを併用します。
クライアント側の再生エラー処理を強化する
ネットワークの揺らぎを考慮して、hls.jsの設定でエラー時の振る舞いをきめ細かく制御します。インポート済みのモジュールに対して、致命エラーごとに復旧戦略を変えます。
// player-retry.js
import Hls from 'hls.js';
export function setup(video, src) {
const hls = new Hls({ backBufferLength: 30, maxBufferLength: 60 });
hls.on(Hls.Events.ERROR, (_, data) => {
if (!data.fatal) return;
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
else hls.destroy();
});
hls.loadSource(src); hls.attachMedia(video);
return hls;
}
この種のクライアントリカバリーは、無料スタックであっても体感品質を底上げする効果があり、視聴離脱を抑えます。
運用品質を上げるための実践知
無料構成をプロダクションに持ち込むとき、ボトルネックを先回りして潰すのが成功の鍵になります。入力側のOBSのキーフレーム間隔をGOP長と一致させ、音声サンプルレートを統一し、字幕やメタデータの扱いをあらかじめ決めておくと、録画とライブの両立がスムーズです。タイムベースの乱れはABRのスイッチングに影響するため、複数プロファイルで同一のGOP境界を維持する設計が重要です。ストレージは短命セグメントの書き込みが中心で、GCやファイルディスクリプタの枯渇を避けるために古いセグメントの削除を確実にスケジュールします。ログは1行に1イベント、時刻とストリーム名、プロファイル、エラーコードを含めると、障害解析のスピードが上がります。
組織面では、プラットフォームチームが配信のSLOを宣言し、視聴完了率、初回再生までのTTFP、リバッファ率といったKPIを観測する仕組みを先に用意すると、プロダクト側との合意形成が早まります。無料のソフトウェアであっても、合意したSLOに対してどの層をどう拡張するかをロードマップ化できれば、意思決定は十分にファクトドリブンになります。
よくある落とし穴と回避策
HLSのキャッシュキーにクエリパラメータを不用意に含めるとヒット率が落ち、オリジン負荷が跳ね上がります。署名はパスと期限だけに限定し、プレイヤー側の再試行で不要なクエリを増やさない方針が安定です。OBSの再接続時にストリーム名が変わると、m3u8が断片化して再生エラーの原因になります。同一イベントでは同一キーを維持し、ファイルの掃除はイベント終了後に行います。セグメント長を過度に短くすると遅延は下がりますが、ヘッダオーバーヘッドとTCPセッションの負荷が増えます。まずは3秒を中心にチューニングし、実測により調整します⁵。
Dockerでの再現性確保
ビルド再現性を保つならDockerに寄せます。完全に無料で環境を配りたい場合、ComposeにNginx-RTMP、certbot、auth APIを含めたひとつのリポジトリにまとめると、オンボーディングが速くなります。CIのコンテナビルドを走らせてタグを固定し、アーティファクトをリリースする運用なら、イベント前日の回帰事故を大幅に減らせます。
まとめ:無料でも、十分に戦える
フリーソフトだけで動画配信システムを構築するのは現実的か。答えはイエスです。Nginx-RTMPとFFmpeg、hls.jsを柱に、短寿命トークンと最小のAPIを添えるだけで、ライブ配信とVODの両方を安定運用できます。ピーク視聴が読めない大規模案件や厳密な低遅延が必須の競技配信では設計の追加が要りますが、社内イベント、ウェビナー、ラーニングの領域なら、無料スタックで十分に高い体感品質を実現できます。
次の一歩として、まずは単一チャンネルのライブをPoCとして流し、視聴ログとメトリクスを眺めながら、ABRの本数とセグメント長を調整してください。どの時点でCDNやハードウェアエンコードに踏み込むかは、視聴規模とコストの曲線が教えてくれます。あなたの組織では、どのKPIから改善しますか。配信のSLOを1つだけ選び、来週のイベントで小さく検証してみましょう。
参考文献
- ITWeb. Global internet traffic up as video dominates. https://www.itweb.co.za/article/global-internet-traffic-up-as-video-dominates/DZQ58vV8o2lMzXy2
- Cisco Newsroom. IP Video to Represent 80 Percent of Global IP Traffic, 2019. https://newsroom.cisco.com/press-release-content?articleId=1644203
- Cloudflare Learning Center. What is adaptive bitrate streaming (ABR)? https://www.cloudflare.com/learning/video/what-is-adaptive-bitrate-streaming
- AWS 公式ブログ. 映像ストリーミングにおけるレイテンシの基本 (2019-03-19). https://aws.amazon.com/jp/blogs/news/jp-blog-media-streaming-latency20190319/
- Wowza Blog. HLS latency sucks, but here’s how to fix it. https://www.wowza.com/blog/hls-latency-sucks-but-heres-how-to-fix-it