ASP.NET Coreで動画アップロードからHLS/DASH配信まで完全ガイド|FFmpeg・Video.js・hls.js対応

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&lt;EncodeJob&gt; _channel = Channel.CreateUnbounded&lt;EncodeJob&gt;();

    public ValueTask EnqueueAsync(EncodeJob job) =&gt; _channel.Writer.WriteAsync(job);
    public IAsyncEnumerable&lt;EncodeJob&gt; DequeueAllAsync(CancellationToken ct) =&gt; _channel.Reader.ReadAllAsync(ct);
}

public class EncodeWorker : BackgroundService
{
    private readonly IWebHostEnvironment _env;
    private readonly TranscoderOptions _opt;
    private readonly EncodeQueue _queue;
    private readonly ILogger&lt;EncodeWorker&gt; _logger;

    public EncodeWorker(IWebHostEnvironment env, IOptions&lt;TranscoderOptions&gt; opt, EncodeQueue queue, ILogger&lt;EncodeWorker&gt; 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&lt;string&gt;();
        p.ErrorDataReceived += (_, e) =&gt; { 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)

&lt;video id="v" controls width="100%" playsinline&gt;&lt;/video&gt;
&lt;script src="/lib/hls.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
  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を再生できないブラウザです。";
  }
&lt;/script&gt;

Video.js(VHS内蔵、HLSのsource指定でOK)

&lt;link rel="stylesheet" href="/lib/video-js.css"&gt;
&lt;video id="vjs" class="video-js" controls preload="auto" width="100%"&gt;
  &lt;source src="/uploads/hls/{動画ID}/master.m3u8" type="application/x-mpegURL"&gt;
&lt;/video&gt;
&lt;script src="/lib/video.min.js"&gt;&lt;/script&gt;
&lt;script&gt;var player = videojs('vjs');&lt;/script&gt;

DASH(dash.js)

&lt;video id="dash" controls width="100%" playsinline&gt;&lt;/video&gt;
&lt;script src="/lib/dash.all.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
  var player = dashjs.MediaPlayer().create();
  player.initialize(document.getElementById("dash"), "/uploads/dash/{動画ID}/manifest.mpd", true);
&lt;/script&gt;

IIS(Windows)でのサイズ制限とMIME追加

&lt;configuration&gt;
  &lt;system.webServer&gt;
    &lt;security&gt;
      &lt;requestFiltering&gt;
        &lt;requestLimits maxAllowedContentLength="2147483648" /&gt;  &lt;!-- 2GB --&gt;
      &lt;/requestFiltering&gt;
    &lt;/security&gt;
    &lt;staticContent&gt;
      &lt;remove fileExtension=".m3u8" /&gt;
      &lt;mimeMap fileExtension=".m3u8" mimeType="application/x-mpegURL" /&gt;
      &lt;mimeMap fileExtension=".ts" mimeType="video/MP2T" /&gt;
      &lt;mimeMap fileExtension=".mpd" mimeType="application/dash+xml" /&gt;
      &lt;mimeMap fileExtension=".m4s" mimeType="video/iso.segment" /&gt;
    &lt;/staticContent&gt;
  &lt;/system.webServer&gt;
&lt;/configuration&gt;

HLS/DASHの設計ポイントと実戦Tips

セグメント長とGOP整合

  • 推奨セグメント長: 2〜10秒。VODなら6秒が無難。低遅延優先なら2〜4秒。
  • GOP(-g) は fps × セグメント秒 に合わせるとキーフレームがセグメント境界に揃い seeking 精度と切替が安定します。
  • -hls_flags independent_segments を付けるとセグメント先頭がIフレームで独立し、ABR切替が滑らか。

ABRラダー(多ビットレート)の例

解像度ビットレート(映像)maxrate/bufsize用途
1280×7202.5 Mbps2.675/3.75 MbpsWi‑Fi/PC向け
854×4801.2 Mbps1.28/1.8 Mbps標準回線
640×3600.8 Mbps0.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 &amp;&amp; DateTimeOffset.UtcNow &lt; 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上げすぎるとスループットが下がる。メトリクスで要チューニング。
配信静的ファイル+CDNASP.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の簡易例(アップロードフォーム)

&lt;form id="up" enctype="multipart/form-data"&gt;
  &lt;input type="file" name="file" accept="video/*" required /&gt;
  &lt;input type="text" name="title" placeholder="タイトル" /&gt;
  &lt;button&gt;アップロード&lt;/button&gt;
&lt;/form&gt;
&lt;script&gt;
document.getElementById('up').addEventListener('submit', async (e) =&gt; {
  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);
});
&lt;/script&gt;

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などの周辺設定を正しく整えることで、実運用に耐える映像配信基盤が完成します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次