ASP.NET Coreで「アップロード→自動トランスコード→HLS/DASHで配信→ブラウザ再生」までを一気通貫で実装するための実践ガイドです。管理画面から動画を受け取り、FFmpegでセグメント化し、静的ファイルとして配信、フロントはVideo.js/hls.js/dash.jsで再生します。IIS/Kestrelの制限、CORS、MIME、ABR(多ビットレート)やCDNキャッシュ、セキュリティ(署名付きURL)まで、現場でハマりがちなポイントを具体的なコードで解決します。
要件の整理と全体像
目的は「管理者が動画をアップロードし、クライアントではHLS(.m3u8)またはDASH(.mpd)でストリーミング再生」することです。基本設計は下記の3レイヤーに分けます。
- 受け取りレイヤー: ASP.NET Core APIが
IFormFileで動画を受信し、ストレージ(例:wwwroot/uploads/source)へ保存 - 変換レイヤー: FFmpegを外部プロセスで起動し、HLS(.m3u8/.ts or .m4s)/DASH(.mpd/.m4s)を生成(バックグラウンドジョブ)
- 配信レイヤー: 生成ファイルを静的ファイル+CORSで公開、CDNでキャッシュ最適化、フロントはVideo.js/hls.js/dash.jsで再生
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Upload UI │ POST │ ASP.NET │ Queue │ FFmpeg │ writes HLS/DASH
│ (Admin) ├──────▶│ API ├────────▶│ Worker ├───────────────┐
└──────────┘ └──────────┘ └──────────┘ │
│
┌──────▼───────┐
│ wwwroot │
│ /uploads/hls │
│ /uploads/dash│
└──────┬───────┘
│
┌───────▼───────┐
│ Browser/Player│
│ Video.js/hls.js│
│ dash.js/Shaka │
└───────────────┘
よくある疑問への先回り回答
- セグメント分割は必要? はい。HLS/DASHは通常、数秒単位のセグメント(HLS:
.tsもしくは CMAFの.m4s)に分割し、プレイリスト(.m3u8/.mpd)から順次取得します。 - どのツールで分割・変換する? サーバー自動処理の現実解はFFmpegです。HLS/DASH生成を1コマンドで完結できます。
- C#だけで完結できる? 完全な純C#コーデックは現実的ではありません。
FFmpeg.AutoGenやXabe.FFmpeg等のラッパーを使っても、最終的にFFmpegバイナリが必要になります。 - サーバー負荷とプレイヤー設定は? 変換はバックグラウンド処理へ分離し、配信は静的ファイル+CDN。プレイヤーはSafariはネイティブHLS、Chrome/Edgeはhls.js(またはVideo.js/VHS)、DASHはdash.jsやShaka Playerが無難です。
ステップ別の実装(ASP.NET Core + FFmpeg)
プロジェクト準備
- .NET 7/8のWeb APIテンプレートを作成
wwwroot/uploads配下にsource/hls/dashディレクトリを用意- サーバーにFFmpegを配置(パスを環境変数または設定ファイル)
// appsettings.json(抜粋)
{
"Transcoder": {
"FfmpegPath": "C:\\tools\\ffmpeg\\bin\\ffmpeg.exe", // Linuxなら /usr/bin/ffmpeg
"DefaultSegmentSeconds": 6,
"MaxParallelJobs": 1
},
"AllowedHosts": "*"
}
Program.cs(Kestrel/IIS制限・静的ファイル・MIME・CORS)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 大容量アップロード許可
builder.Services.Configure(o =>
{
o.MultipartBodyLengthLimit = long.MaxValue;
});
// CORS(再生クライアントのオリジンを許可)
builder.Services.AddCors(o => o.AddPolicy("HlsCors", p =>
p.SetIsOriginAllowed(_ => true) // 必要に応じて限定
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("Content-Length","Accept-Ranges")));
// 設定バインド
builder.Services.Configure(builder.Configuration.GetSection("Transcoder"));
builder.Services.AddSingleton();
builder.Services.AddHostedService();
var app = builder.Build();
// 静的ファイル(/uploads を公開)
var uploadsRoot = Path.Combine(app.Environment.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsRoot);
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".m3u8"] = "application/x-mpegURL";
provider.Mappings[".ts"] = "video/MP2T";
provider.Mappings[".mpd"] = "application/dash+xml";
provider.Mappings[".m4s"] = "video/iso.segment";
provider.Mappings[".cmfv"] = "video/mp4"; // CMAF init(環境により)
provider.Mappings[".cmfa"] = "audio/mp4";
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(uploadsRoot),
RequestPath = "/uploads",
ContentTypeProvider = provider,
ServeUnknownFileTypes = true
});
app.UseCors("HlsCors");
app.MapControllers();
app.Run();
// オプション/キュー/ジョブ定義
public record TranscoderOptions
{
public string FfmpegPath { get; init; } = "ffmpeg";
public int DefaultSegmentSeconds { get; init; } = 6;
public int MaxParallelJobs { get; init; } = 1;
}
VideoController(アップロードAPI)
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/videos")]
public class VideoController : ControllerBase
{
private readonly IWebHostEnvironment _env;
private readonly EncodeQueue _queue;
public VideoController(IWebHostEnvironment env, EncodeQueue queue)
{
_env = env;
_queue = queue;
}
[HttpPost("upload")]
[DisableRequestSizeLimit]
public async Task<IActionResult> Upload([FromForm] IFormFile file, [FromForm] string? title)
{
if (file == null || file.Length == 0) return BadRequest("ファイルがありません。");
var id = Guid.NewGuid().ToString("N");
var srcDir = Path.Combine(_env.WebRootPath, "uploads", "source", id);
Directory.CreateDirectory(srcDir);
var safeName = Path.GetFileName(file.FileName);
var srcPath = Path.Combine(srcDir, safeName);
await using (var fs = System.IO.File.Create(srcPath))
{
await file.CopyToAsync(fs);
}
await _queue.EnqueueAsync(new EncodeJob
{
Id = id,
SourcePath = srcPath,
Title = title ?? safeName,
// ここでHLS/DASH両方やるか、HLSのみ等を指定可能
DoHls = true,
DoDash = true
});
return Ok(new { id, message = "エンコードキューに投入しました。" });
}
}
エンコードキューとバックグラウンドワーカー
アップロード直後にFFmpegを同期実行するとAPIのレスポンスが遅くなります。バックグラウンドで処理して、完了後にプレイリストURLをフロントへ通知する構成が安定します。
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
public class EncodeJob
{
public string Id { get; init; } = default!;
public string SourcePath { get; init; } = default!;
public string Title { get; init; } = default!;
public bool DoHls { get; init; }
public bool DoDash { get; init; }
}
public class EncodeQueue
{
private readonly Channel<EncodeJob> _channel = Channel.CreateUnbounded<EncodeJob>();
public ValueTask EnqueueAsync(EncodeJob job) => _channel.Writer.WriteAsync(job);
public IAsyncEnumerable<EncodeJob> DequeueAllAsync(CancellationToken ct) => _channel.Reader.ReadAllAsync(ct);
}
public class EncodeWorker : BackgroundService
{
private readonly IWebHostEnvironment _env;
private readonly TranscoderOptions _opt;
private readonly EncodeQueue _queue;
private readonly ILogger<EncodeWorker> _logger;
public EncodeWorker(IWebHostEnvironment env, IOptions<TranscoderOptions> opt, EncodeQueue queue, ILogger<EncodeWorker> logger)
{
_env = env;
_opt = opt.Value;
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var job in _queue.DequeueAllAsync(stoppingToken))
{
try
{
if (job.DoHls) await RunHls(job, stoppingToken);
if (job.DoDash) await RunDash(job, stoppingToken);
_logger.LogInformation("Job {Id} done.", job.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Job {Id} failed.", job.Id);
}
}
}
private async Task RunHls(EncodeJob job, CancellationToken ct)
{
var outDir = Path.Combine(_env.WebRootPath, "uploads", "hls", job.Id);
Directory.CreateDirectory(outDir);
// ABR(3レンディション) + 独立セグメント + マスタープレイリスト
var seg = _opt.DefaultSegmentSeconds;
var segFmt = Path.Combine(outDir, "%v", "seg%03d.ts").Replace("\\", "/");
var outFmt = Path.Combine(outDir, "%v", "index.m3u8").Replace("\\", "/");
Directory.CreateDirectory(Path.Combine(outDir, "0"));
Directory.CreateDirectory(Path.Combine(outDir, "1"));
Directory.CreateDirectory(Path.Combine(outDir, "2"));
var args =
$"-y -i \"{job.SourcePath}\" " +
"-filter_complex " +
"\"[0:v]split=3[v1][v2][v3];" +
"[v1]scale=w=1280:h=720:force_original_aspect_ratio=decrease:eval=frame[v1o];" +
"[v2]scale=w=854:h=480:force_original_aspect_ratio=decrease:eval=frame[v2o];" +
"[v3]scale=w=640:h=360:force_original_aspect_ratio=decrease:eval=frame[v3o]\" " +
"-map \"[v1o]\" -map 0:a:0? -c:v:0 h264 -profile:v:0 high -preset veryfast -b:v:0 2500k -maxrate:v:0 2675k -bufsize:v:0 3750k -c:a aac -b:a 128k " +
"-map \"[v2o]\" -map 0:a:0? -c:v:1 h264 -profile:v:1 main -preset veryfast -b:v:1 1200k -maxrate:v:1 1280k -bufsize:v:1 1800k -c:a aac -b:a 128k " +
"-map \"[v3o]\" -map 0:a:0? -c:v:2 h264 -profile:v:2 main -preset veryfast -b:v:2 800k -maxrate:v:2 856k -bufsize:v:2 1200k -c:a aac -b:a 96k " +
$"-g 48 -keyint_min 48 -sc_threshold 0 " + // 約6秒(8fps×6=48を想定。実FPSに合わせて調整)
$"-hls_time {seg} -hls_playlist_type vod -hls_flags independent_segments " +
$"-hls_segment_filename \"{segFmt}\" -master_pl_name master.m3u8 " +
"-var_stream_map \"v:0,a:0 v:1,a:0 v:2,a:0\" " +
$"\"{outFmt}\"";
await SpawnFFmpeg(args, outDir, ct);
}
private async Task RunDash(EncodeJob job, CancellationToken ct)
{
var outDir = Path.Combine(_env.WebRootPath, "uploads", "dash", job.Id);
Directory.CreateDirectory(outDir);
var args =
$"-y -i \"{job.SourcePath}\" " +
"-filter_complex " +
"\"[0:v]split=3[v1][v2][v3];" +
"[v1]scale=w=1280:h=720:force_original_aspect_ratio=decrease:eval=frame[v1o];" +
"[v2]scale=w=854:h=480:force_original_aspect_ratio=decrease:eval=frame[v2o];" +
"[v3]scale=w=640:h=360:force_original_aspect_ratio=decrease:eval=frame[v3o]\" " +
"-map \"[v1o]\" -map 0:a:0? -c:v:0 h264 -b:v:0 2400k -c:a aac -b:a 128k " +
"-map \"[v2o]\" -map 0:a:0? -c:v:1 h264 -b:v:1 1200k -c:a aac -b:a 128k " +
"-map \"[v3o]\" -map 0:a:0? -c:v:2 h264 -b:v:2 800k -c:a aac -b:a 96k " +
"-g 48 -keyint_min 48 -sc_threshold 0 " +
"-use_timeline 1 -use_template 1 -seg_duration 4 " +
"-init_seg_name init-$RepresentationID$.m4s -media_seg_name chunk-$RepresentationID$-$Number%05d$.m4s " +
"-adaptation_sets \"id=0,streams=v id=1,streams=a\" " +
"-f dash manifest.mpd";
await SpawnFFmpeg(args, outDir, ct);
}
private async Task SpawnFFmpeg(string args, string workingDir, CancellationToken ct)
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = _opt.FfmpegPath,
Arguments = args,
WorkingDirectory = workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi)!;
var stdErr = new List<string>();
p.ErrorDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) stdErr.Add(e.Data); };
p.BeginErrorReadLine();
await p.WaitForExitAsync(ct);
if (p.ExitCode != 0)
{
throw new InvalidOperationException("FFmpeg failed: " + string.Join(Environment.NewLine, stdErr));
}
}
}
再生ページ(HLS/DASH)
Safari/iOSは<video>にtype="application/x-mpegURL"を指定すればネイティブ再生できます。Chrome/Edgeはhls.jsを利用します。DASHはdash.js(またはShaka Player)が手軽です。
HLS(Safariはネイティブ、他ブラウザはhls.js)
<video id="v" controls width="100%" playsinline></video>
<script src="/lib/hls.min.js"></script>
<script>
const src = "/uploads/hls/{動画ID}/master.m3u8";
const video = document.getElementById('v');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src; // Safari
} else if (Hls.isSupported()) {
const hls = new Hls({ enableWorker: true });
hls.loadSource(src);
hls.attachMedia(video);
} else {
video.innerHTML = "HLSを再生できないブラウザです。";
}
</script>
Video.js(VHS内蔵、HLSのsource指定でOK)
<link rel="stylesheet" href="/lib/video-js.css">
<video id="vjs" class="video-js" controls preload="auto" width="100%">
<source src="/uploads/hls/{動画ID}/master.m3u8" type="application/x-mpegURL">
</video>
<script src="/lib/video.min.js"></script>
<script>var player = videojs('vjs');</script>
DASH(dash.js)
<video id="dash" controls width="100%" playsinline></video>
<script src="/lib/dash.all.min.js"></script>
<script>
var player = dashjs.MediaPlayer().create();
player.initialize(document.getElementById("dash"), "/uploads/dash/{動画ID}/manifest.mpd", true);
</script>
IIS(Windows)でのサイズ制限とMIME追加
<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="2147483648" /> <!-- 2GB -->
</requestFiltering>
</security>
<staticContent>
<remove fileExtension=".m3u8" />
<mimeMap fileExtension=".m3u8" mimeType="application/x-mpegURL" />
<mimeMap fileExtension=".ts" mimeType="video/MP2T" />
<mimeMap fileExtension=".mpd" mimeType="application/dash+xml" />
<mimeMap fileExtension=".m4s" mimeType="video/iso.segment" />
</staticContent>
</system.webServer>
</configuration>
HLS/DASHの設計ポイントと実戦Tips
セグメント長とGOP整合
- 推奨セグメント長: 2〜10秒。VODなら6秒が無難。低遅延優先なら2〜4秒。
- GOP(-g) は
fps × セグメント秒に合わせるとキーフレームがセグメント境界に揃い seeking 精度と切替が安定します。 - -hls_flags independent_segments を付けるとセグメント先頭がIフレームで独立し、ABR切替が滑らか。
ABRラダー(多ビットレート)の例
| 解像度 | ビットレート(映像) | maxrate/bufsize | 用途 |
|---|---|---|---|
| 1280×720 | 2.5 Mbps | 2.675/3.75 Mbps | Wi‑Fi/PC向け |
| 854×480 | 1.2 Mbps | 1.28/1.8 Mbps | 標準回線 |
| 640×360 | 0.8 Mbps | 0.856/1.2 Mbps | モバイル回線 |
静止画中心ならビットレートを下げ、動きが激しいスポーツ等では上げるなど、コンテンツ特性で調整します。
ファイル配置と公開パス
wwwroot/
uploads/
source/{id}/source.mp4
hls/{id}/master.m3u8
hls/{id}/0/index.m3u8, seg001.ts...
hls/{id}/1/index.m3u8, ...
hls/{id}/2/index.m3u8, ...
dash/{id}/manifest.mpd
dash/{id}/chunk-...m4s / init-...m4s
キャッシュとCDN
.m3u8/.mpd(プレイリスト/マニフェスト)は頻繁に参照されるため短めのTTL(例:30〜120秒)。.ts/.m4s(セグメント)は長めのTTL(例:数時間〜数日)。VODは不変なので長めでOK。- リネーム/ベリー化(ファイル名にハッシュ)でキャッシュ破棄を制御する運用も有効。
セキュリティ(非公開配信)
会員限定などでは署名付きURL(有効期限付きトークン)で/uploads/配下のアクセスを制御します。ASP.NET Core Data Protectionで簡易的な署名方式を実装できます。
// 署名トークン生成(例)
public static class SignedUrl
{
public static string Create(string path, TimeSpan ttl, IDataProtector protector)
{
var expires = DateTimeOffset.UtcNow.Add(ttl).ToUnixTimeSeconds();
var payload = $"{path}|{expires}";
var token = Convert.ToBase64String(protector.Protect(System.Text.Encoding.UTF8.GetBytes(payload)));
return $"{path}?sig={Uri.EscapeDataString(token)}";
}
public static bool Validate(HttpRequest req, IDataProtector protector, out string path)
{
path = req.Path.Value ?? "";
if (!req.Query.TryGetValue("sig", out var sig)) return false;
try
{
var bytes = protector.Unprotect(Convert.FromBase64String(sig!));
var parts = System.Text.Encoding.UTF8.GetString(bytes).Split('|');
if (parts.Length != 2) return false;
path = parts[0];
var exp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(parts[1]));
return path == req.Path && DateTimeOffset.UtcNow < exp;
}
catch { return false; }
}
}
ミドルウェアで/uploads配下へのアクセス時にSignedUrl.Validateを通し、失敗時は401/403を返します。さらにRefererチェックやCookie認証を併用すれば流出耐性が上がります。
ジョブの同時実行数とスロットリング
CPU・I/O負荷が高いため、同時エンコード数(MaxParallelJobs)を制御してください。上の実装ではキューを単一ワーカーで処理していますが、SemaphoreSlimで並列度を指定する実装に差し替えるだけで対応可能です。
FFmpegコマンドのレシピ集
最小構成(単一ビットレートのHLS)
ffmpeg -y -i input.mp4 -c:v h264 -preset veryfast -b:v 1800k -c:a aac -b:a 128k ^
-g 48 -keyint_min 48 -sc_threshold 0 ^
-hls_time 6 -hls_playlist_type vod -hls_flags independent_segments ^
-hls_segment_filename ".\\out\\seg%03d.ts" ".\\out\\index.m3u8"
複数レンディション(HLS、マスタープレイリスト生成)
ffmpeg -y -i input.mp4 ^
-filter_complex "[0:v]split=3[v1][v2][v3]; \
[v1]scale=w=1280:h=720:force_original_aspect_ratio=decrease[v1o]; \
[v2]scale=w=854:h=480:force_original_aspect_ratio=decrease[v2o]; \
[v3]scale=w=640:h=360:force_original_aspect_ratio=decrease[v3o]" ^
-map "[v1o]" -map 0:a -c:v:0 h264 -b:v:0 2500k -c:a aac -b:a 128k ^
-map "[v2o]" -map 0:a -c:v:1 h264 -b:v:1 1200k -c:a aac -b:a 128k ^
-map "[v3o]" -map 0:a -c:v:2 h264 -b:v:2 800k -c:a aac -b:a 96k ^
-g 48 -keyint_min 48 -sc_threshold 0 ^
-hls_time 6 -hls_flags independent_segments -hls_playlist_type vod ^
-hls_segment_filename ".\\out\\%v\\seg%03d.ts" -master_pl_name master.m3u8 ^
-var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" ".\\out\\%v\\index.m3u8"
DASH(CMAFセグメント)
ffmpeg -y -i input.mp4 ^
-filter_complex "[0:v]split=2[v1][v2]; [v1]scale=1280:720[v1o]; [v2]scale=854:480[v2o]" ^
-map "[v1o]" -map 0:a -c:v:0 h264 -b:v:0 2400k -c:a aac -b:a 128k ^
-map "[v2o]" -map 0:a -c:v:1 h264 -b:v:1 1200k -c:a aac -b:a 128k ^
-g 48 -keyint_min 48 -sc_threshold 0 ^
-use_timeline 1 -use_template 1 -seg_duration 4 ^
-init_seg_name init-$RepresentationID$.m4s ^
-media_seg_name chunk-$RepresentationID$-$Number%05d$.m4s ^
-adaptation_sets "id=0,streams=v id=1,streams=a" ^
-f dash ".\\out\\manifest.mpd"
純C#志向の代替(FFmpegラッパーを使う)
外部プロセスの起動に抵抗がある場合でも、現実的にはFFmpegバイナリが必要です。ラッパーを使うとコマンド組み立てや進捗取得が楽になります。
Xabe.FFmpegの例
// Install-Package Xabe.FFmpeg
FFmpeg.SetExecutablesPath("C:\\tools\\ffmpeg\\bin");
var mediaInfo = await FFmpeg.GetMediaInfo(srcPath);
var conv720 = FFmpeg.Conversions.New()
.AddStream(mediaInfo.VideoStreams.First().SetSize(1280, 720).SetCodec(VideoCodec.h264).SetBitrate(2500_000))
.AddStream(mediaInfo.AudioStreams.First().SetCodec(AudioCodec.aac).SetBitrate(128_000))
.AddParameter($"-g 48 -keyint_min 48 -sc_threshold 0 -hls_time 6 -hls_playlist_type vod -hls_flags independent_segments")
.AddParameter($"-hls_segment_filename "{outDir}\0\seg%03d.ts"")
.AddParameter($""{outDir}\0\index.m3u8"");
await conv720.Start();
ラッパーでも最終的な生成物は同じです。ABRやマスタープレイリストを作るなら、複数のコンバージョンを走らせるか、AddParameterで-var_stream_map付きの高度なFFmpegコマンドを渡します。
サーバー負荷・運用・トラブル対策
現実運用のチェックリスト
| 観点 | 推奨 | 理由/補足 |
|---|---|---|
| 変換プロセス | バックグラウンドジョブ(HostedService/Hangfireなど) | APIレスポンスをブロックしない。失敗時の再試行・可視化が容易。 |
| 同時実行数 | CPU/ディスクに合わせて1〜N | 上げすぎるとスループットが下がる。メトリクスで要チューニング。 |
| 配信 | 静的ファイル+CDN | ASP.NETプロセスを配信に使わず軽量化。CDNでオリジン負荷軽減。 |
| MIME | .m3u8, .ts, .mpd, .m4s を明示 | 誤判定だとダウンロード扱いになる。 |
| CORS | プレイヤーを置くオリジンを許可 | プレイリスト/セグメント取得でCORSに引っかかりやすい。 |
| セキュリティ | 署名付きURL+期限+認証 | 共有リンク流出対策。権限チェックも忘れずに。 |
| ログ | FFmpeg標準エラー出力を収集 | 失敗時に原因特定しやすくなる(コーデック/フィルタ/入出力エラー)。 |
よくあるエラーと対処
- 「Unknown mime type」…MIME未設定。Program.csまたはIISに追加。
- 「CORSエラー」…
app.UseCors()の位置とポリシー、静的ファイルルートの確認。 - 「音ズレ/切替で真っ黒」…
-gとセグメント長の不一致。-sc_threshold 0でシーンサーチ抑制。 - 「重すぎる」…
-presetをveryfastに。ABR段数を減らす。並列数を絞る。 - 「DASH再生不可」…マニフェストのパスや相対URL誤り、CORS、または古いブラウザ。
プレイヤー設定の実践ノウハウ
- 自動再生…モバイルはミュートでないとブロックされがち(
muted autoplay playsinline)。 - 字幕…VTTを
<track kind="subtitles" src="/subs/xx.vtt" srclang="ja">で追加。 - スロー/倍速…
playbackRateやVideo.jsのControlBarを有効化。 - エラーハンドリング…hls.jsの
on(Hls.Events.ERROR,...)でフォールバック(低ビットレートへ切替など)。
管理UIの簡易例(アップロードフォーム)
<form id="up" enctype="multipart/form-data">
<input type="file" name="file" accept="video/*" required />
<input type="text" name="title" placeholder="タイトル" />
<button>アップロード</button>
</form>
<script>
document.getElementById('up').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const res = await fetch('/api/videos/upload', { method: 'POST', body: fd });
const js = await res.json();
alert('キュー投入: ' + js.id);
});
</script>
HLS優先でDASHは追加対応がおすすめ
主要ブラウザはHLS再生の道筋が明快(Safariはネイティブ、他はhls.js/VHS)。まずHLSを安定運用し、必要に応じてDASHを追加する二段構えが開発・運用コストの最適解です。
ベストプラクティスまとめ
- アップロード→FFmpeg変換→静的配信→プレイヤー再生の分離が安定稼働の鍵。
- ABR(720p/480p/360p)を用意し、GOPとセグメント長の整合を取る。
- MIME/CORS/Rangeを正しく設定。CDNでプレイリスト短命、セグメント長命キャッシュ。
- バックグラウンドジョブで変換。ログ収集・再試行・可視化を前提に。
- 非公開配信は署名URLや期限付きトークンで保護。必要に応じてDRMは別設計。
FAQ(重要ポイントの復習)
| 質問 | 要点 | 実装のヒント |
|---|---|---|
| セグメント化は必要? | 必要。HLS/DASHは小分けセグメント+プレイリストで配信。 | -hls_time 2〜10、DASHは-seg_duration。GOP整合。 |
| 何で変換する? | FFmpegが最有力。自動化に向く。 | 外部プロセス起動が最小コスト。パス同梱または環境変数。 |
| C#だけで可能? | 実質不可。ラッパー経由でもFFmpegバイナリが必要。 | FFmpeg.AutoGenやXabe.FFmpegで制御簡略化。 |
| サーバー負荷とプレイヤー設定は? | 変換はBGジョブ、配信は静的+CDN、プレイヤーはhls.js/dash.js。 | 同時実行数制限、veryfast、ABR段数調整。 |
チェック用URL例(完成後)
- HLS:
/uploads/hls/{id}/master.m3u8 - DASH:
/uploads/dash/{id}/manifest.mpd
ローカル/QA環境でCORSやMIME、キャッシュ、プレイヤーのイベントログ(バッファ・エラー)を確認し、本番はCDNを前段に置く構成に切り替えれば、スケールやすく安定した配信になります。
付録:運用の高度化アイデア
- フラグメント化MP4(CMAF): HLS/DASHをCMAFで統一し、ストレージ節約とキャッシュ効率を向上。
- 位置合わせ字幕:
ffmpeg -i input.srt output.vttでVTT化、ラグがある場合は-itsoffsetで微調整。 - サムネイル自動生成:
ffmpeg -ss 00:00:05 -i input -frames:v 1 thumb.jpgで早送りプレビューに活用。 - メタデータ: DBに
id/title/duration/variants/paths/statusを保存し、配信可否を切り替え。 - 監視: エンコードキュー長、失敗率、処理時間、CPU/IOをダッシュボード化。
この記事で構築したもの
- ASP.NET Core APIでの大容量動画アップロード
- FFmpegによるHLS/DASH自動生成(ABR、安定GOP、独立セグメント)
- 静的ファイル配信・CORS・MIME・IIS制限解除
- hls.js / Video.js / dash.js を使ったマルチブラウザ再生
- 署名付きURL、CDNキャッシュ戦略、運用ノウハウ
この最小構成を土台に、DRMや課金、ウォーターマーク、サーバーレス変換(Functions/Batch)などへ安全に拡張できます。
ステップ一覧(クイックリファレンス)
| ステップ | 内容 | 補足・ポイント |
|---|---|---|
| 受け取り | IFormFileでwwwroot/uploads/sourceへ保存 | MultipartBodyLengthLimit拡大、ファイル名のサニタイズ |
| HLS変換 | FFmpeg(-hls_time、-var_stream_map)で.m3u8/.ts生成 | ABR・独立セグメント・GOP整合 |
| DASH変換 | FFmpeg(-f dash、CMAF .m4s)で.mpd生成 | -use_template/-use_timeline、-seg_duration |
| 静的配信 | MIME登録、CORS許可、Range/Content-Lengthの露出 | CDNでプレイリスト短命・セグメント長命キャッシュ |
| 再生 | SafariはネイティブHLS、他はhls.js/Video.js、DASHはdash.js | エラーイベント監視、フォールバック実装 |
| 運用 | バックグラウンド処理、ログ、再試行、メトリクス | 同時実行数・プリセット・ABR段数のバランスを調整 |
| セキュリティ | 署名URL、有効期限、認証・権限 | Referer/Cookie併用で漏えい対策 |
結論
アップロード → FFmpegでHLS/DASHへ変換 → 静的配信 → hls.js/Video.js/dash.jsで再生が最短で確実な実装パスです。FFmpegは外部プロセス起動が最小コストで、C#ラッパーを使ってもバイナリは必要になります。再生互換性と運用コストを考えると、まずHLSから構築し、必要時にDASHを追加する段階的アプローチが最も安全で拡張しやすい設計です。IIS/Kestrelの制限解除、MIME/CORS、ABR/GOP、CDNキャッシュ、署名URLなどの周辺設定を正しく整えることで、実運用に耐える映像配信基盤が完成します。

コメント