前回の記事の構成で今回は以下の機能を実現するアプリケーションをDotnet Core V9で作成します。
- バックグラウンドでMediaMTXの起動・停止(Windowsサービスプログラムの実装)
- ブラウザからライブ及び、録画映像の視聴(Webサーバの実装)
- 録画ファイルを1日分保持(期間外の録画ファイルの自動削除機能の実装)
録画ファイルの保持期間はffmpegのパラメータで制御します。
今回作成するプログラムでファイルの管理は行いません。
1.事前準備
今回は.NET SDK + VSCodeで開発を行う想定です。事前に以下のモジュールをインストールしてください。(Visual Studio 2022を利用する場合はインストール不要です)
- .NET SDK 9
- Visual Studio Code(VSCode)
- C# DevKit(VsCode拡張機能)
2.プロジェクトの作成
今回作成するアプリのファイル構成です。
.\RtspRec
| .gitignore
| RtspRec.sln
|
\---RtspRecService
| appsettings.Development.json
| appsettings.json
| mediamtx.yml
| Program.cs
| RtspRecService.csproj
| RtspRecService.csproj.user
| RtspRecSettings.cs
| Worker.cs
|
+---Proc
| MediaMTXManager.cs
|
\---wwwroot
index.html
プロジェクトのディレクトリを作成します
New-Item -Path .\RtspRec -Type Directory
プロジェクトのカレントディレクトリへ移動します
cd .\RtspRec
ソリューションファイルを作成します
dotnet new sln -n RtspRec
必要に応じてdotnet用.gitignoreファイルを作成します(任意)
dotnet new gitignore
3.Windowsサービスプロジェクト作成
Windowsサービスのプロジェクトを作成します
dotnet new worker -n RtspRecService
ソリューションにWindowsサービスのプロジェクトを追加します
dotnet sln add RtspRecService/RtspRecService.csproj
Windowsサービスプロジェクトのカレントディレクトリへ移動します
cd .\RtspRecService
利用パッケージを追加します
dotnet add package Microsoft.Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
dotnet add package NLog
dotnet add package NLog.Extensions.Logging
バックグラウンドで起動するアプリの為、調査用にNLogを利用してログ出力します。
4.プログラム作成
以降、VSCodeからプロジェクトのカレントディレクトリを開きファイルの作成、編集を行います。
appsettings.jsonを編集します
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"NLog": {
"throwConfigExceptions": true,
"targets": {
"async": true,
"console": {
"type": "ColoredConsole",
"layout": "${longdate} ${level:uppercase=true} [${logger}] - ${message}${exception:format=ToString,StackTrace}"
},
"file": {
"type": "File",
"archiveAboveSize": 5000000,
"maxArchiveFiles": 5,
"fileName": "./logs/app-${shortdate}.log",
"layout": "${longdate} ${level:uppercase=true} [${logger}] - ${message}${exception:format=ToString,StackTrace}"
}
},
"rules": [
{
"logger": "*",
"minLevel": "Info",
"writeTo": "console"
},
{
"logger": "*",
"minLevel": "Info",
"writeTo": "file"
}
]
},
"RtspRec": {
"MediaMTXConfFile": "mediamtx.yml"
}
}
NLogセクションのログフォーマット及び、ログレベルの設定は適宜変更してください。
appsettings.Development.jsonはデバッグ用の設定ファイルを想定しています。
デバッグ実行する場合は適宜編集し利用ください。
.\RtspRecSettings.csを作成します
namespace RtspRec.RtspRecService;
public class RtspRecSettings
{
public required string MediaMTXConfFile { get; set; }
}
.\Proc\MediaMTXManager.csを作成します
using Microsoft.Extensions.Options;
using System.Diagnostics;
namespace RtspRec.RtspRecService.Proc;
public class MediaMTXManager(ILogger<MediaMTXManager> logger, IOptions<RtspRecSettings> settings)
{
private readonly ILogger<MediaMTXManager> _logger = logger;
private readonly RtspRecSettings _settings = settings.Value;
private Process? _mediaMTX;
public bool IsAlived => _mediaMTX?.HasExited == false;
public async Task StartAsync(CancellationToken token)
{
if (IsAlived)
{
_logger.LogInformation("MediaMTXは既に起動しています");
return;
}
_logger.LogInformation("MediaMTXを起動します");
await StartMediaMTXProcess(token);
}
public async Task StopAsync()
{
if (!IsAlived)
{
_logger.LogInformation("MediaMTXは既に停止しています");
return;
}
try
{
_mediaMTX!.Kill(true);
await _mediaMTX.WaitForExitAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "MediaMTXの停止中に異常が発生しました");
}
finally
{
_mediaMTX?.Dispose();
_mediaMTX = null;
}
_logger.LogInformation("MediaMTXを停止しました");
}
private async Task StartMediaMTXProcess(CancellationToken token)
{
// コンフィグファイルの絶対パスを取得
var currentDir = Environment.CurrentDirectory;
var confPath = Path.Combine(currentDir, _settings.MediaMTXConfFile);
if (!File.Exists(confPath))
{
throw new FileNotFoundException($"MediaMTXのコンフィグファイルが見つかりません。: {confPath}");
}
// MediaMTXプロセスの起動情報を設定
_mediaMTX = new()
{
StartInfo = new ProcessStartInfo
{
FileName = "mediamtx",
Arguments = confPath,
WorkingDirectory = currentDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
// MediaMTXプロセスを起動
_mediaMTX.Start();
// エラーログの監視を開始
await MonitorErrorOutputAsync(token);
try
{
await _mediaMTX.WaitForExitAsync(token);
}
catch (OperationCanceledException)
{
_logger.LogInformation("MediaMTXのプロセスをKILLします。");
_mediaMTX.Kill();
}
_logger.LogInformation($"ExitCode: {_mediaMTX.ExitCode}");
}
private async Task MonitorErrorOutputAsync(CancellationToken token)
{
if (_mediaMTX?.StandardError == null || _mediaMTX?.StandardOutput == null)
{
return;
}
// 標準出力の監視タスク
var stdOutTask = Task.Run(async () =>
{
while (!_mediaMTX.StandardOutput.EndOfStream)
{
var line = await _mediaMTX.StandardOutput.ReadLineAsync(token);
if (line != null)
{
_logger.LogInformation("[MediaMTX:stdout] {Line}", line);
}
}
}, token);
// 標準エラー出力の監視タスク
var stdErrTask = Task.Run(async () =>
{
while (!_mediaMTX.StandardError.EndOfStream)
{
var line = await _mediaMTX.StandardError.ReadLineAsync();
if (line != null)
{
_logger.LogDebug("[MediaMTX:stderr] {Line}", line);
}
}
}, token);
// 両方の読み取りが完了するまで待機
await Task.WhenAll(stdOutTask, stdErrTask);
}
}
.\Worker.csを編集します
using RtspRec.RtspRecService.Proc;
namespace RtspRec.RtspRecService;
public class Worker(ILogger<Worker> logger, MediaMTXManager mediaMtx) : BackgroundService
{
private readonly ILogger<Worker> _logger = logger;
private readonly MediaMTXManager _mediaMtx = mediaMtx;
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker is starting: {time}", DateTimeOffset.UtcNow.ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss zzz"));
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.UtcNow.ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss zzz"));
await _mediaMtx.StartAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Worker encountered an error");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker is stopping: {time}", DateTimeOffset.UtcNow.ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss zzz"));
await _mediaMtx.StopAsync();
// ExecuteAsync のキャンセルと完了を待つ
await base.StopAsync(cancellationToken);
}
}
.\Program.csを編集します
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using NLog.Extensions.Logging;
using RtspRec.RtspRecService;
using RtspRec.RtspRecService.Proc;
var basePath = AppContext.BaseDirectory;
// カレントディレクトリを実行ファイルのディレクトリに設定する
Directory.SetCurrentDirectory(basePath);
// 環境変数から環境名を取得し、設定ファイルを読み込む
var environmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
var config = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true) // 環境ごとの設定ファイルを追加
.AddEnvironmentVariables().Build();
var builder = WebApplication.CreateBuilder(args);
// NLogの設定する
builder.Logging.ClearProviders();
builder.Logging.AddNLog();
// サービス名を設定する
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "RTSP Recorder Service";
});
// appsettings.jsonのMediaMTXセクションをRtspRecSettingsにバインドする
builder.Services.Configure<RtspRecSettings>(
builder.Configuration.GetSection("RtspRec")
);
builder.Services.AddOptions<RtspRecSettings>()
.ValidateOnStart(); // 起動時に検証する
// DIコンテナにバックグラウンドサービスを登録する
builder.Services.AddHostedService<Worker>();
// MediaTMXProcessManagerをシングルトンとして登録する
builder.Services.AddSingleton<MediaMTXManager>();
// CORSを設定する場合必要
builder.Services.AddControllers();
var host = builder.Build();
// 静的ファイルを有効化
host.UseDefaultFiles(); // index.html を自動的に返す
host.UseStaticFiles(); // wwwroot 配下のファイルを配信
// --- 仮想ディレクトリ "Contents" を /Contents パスで追加 ---
// basePath/Contents を物理ディレクトリとしてマッピングし、/Contents でアクセス可能にする
var contentsDir = Path.Combine(basePath, "Contents");
if (!Directory.Exists(contentsDir))
{
Directory.CreateDirectory(contentsDir);
}
// CORS設定(hls.js、ライブ配信用)
host.UseCors(policy =>
policy.WithOrigins(
"https://cdn.jsdelivr.net/npm/hls.js@latest",
"http://localhost:8888"
).AllowAnyHeader(
).AllowAnyMethod()
);
// MIME設定
var contentTypeProvider = new FileExtensionContentTypeProvider();
contentTypeProvider.Mappings[".ts"] = "video/mp2t";
contentTypeProvider.Mappings[".m3u8"] = "application/vnd.apple.mpegurl";
// /Contents にアクセスしたときに Contents/index.html を返すようにする
host.UseDefaultFiles(new DefaultFilesOptions
{
FileProvider = new PhysicalFileProvider(contentsDir),
RequestPath = "/Contents"
});
// /Contents 配下の静的ファイルを配信
host.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(contentsDir),
RequestPath = "/Contents",
ContentTypeProvider = contentTypeProvider,
ServeUnknownFileTypes = false // 不明な拡張子は明示的にマッピングで対応する
});
host.UseRouting();
//host.Run();
await host.RunAsync();
WebサーバーとしてKestrelサーバーを利用します。
.\RtspRecService.csprojを編集します
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-RtspRecService-ed43f9c7-62d3-49ef-9f2a-30014db4106f</UserSecretsId>
<RootNamespace>RtspRec.RtspRecService</RootNamespace>
<OutputType>exe</OutputType>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Configuration>Release</Configuration>
<SelfContained>true</SelfContained>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
<PublishDir>C:\RtspRec\</PublishDir>
<AssemblyName>RtspRecService</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Remove="mediamtx.yml" />
</ItemGroup>
<ItemGroup>
<Content Include="mediamtx.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
<PackageReference Include="NLog" Version="6.0.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.5" />
</ItemGroup>
</Project>
.\mediamtx.ymlを作成します
rtsp: yes
rtspTransports: [tcp]
hls: yes
hlsAddress: :8888
rtmp: no
webrtc: no
srt: no
paths:
Camera1:
source: rtsp://<ユーザ>:<パスワード>@<IPアドレス>:<ポート>/<識別子>
sourceOnDemand: no
# ffmpeg実行
runOnReady: |
ffmpeg -rtsp_transport tcp \
-i rtsp://localhost:8554/Camera1 \
-c:v libx264 -preset veryfast -vf scale=640:360 \
-r 15 -crf 23 -an \
-f hls -hls_time 300 -hls_list_size 288 \
-hls_flags delete_segments \
-hls_segment_filename .\\Contents\\Camera1\\%Y%m%d_%H%M%S.ts \
-strftime 1 -reset_timestamps 1 \
-avoid_negative_ts make_zero \
.\\Contents\\Camera1\\stream.m3u8
runOnReadyRestart: yes
- MediaMTXでカメラデバイスからRTSPのストリームを受信し、ライブ配信用としてRTSP(ffmpeg入力ソース用)、HLS(ライブ視聴用)ストリームを配信します
- MediaMTXから配信されたRTSPストリームをffmpegの入力ソースとして読み込み録画用HLSファイルを作成します
- 録画ファイルは今回実装したWebサーバから配信します
.\wwwroot\index.htmlを作成します
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>カメラ映像</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
background: #111;
color: #eee;
font-family: sans-serif;
text-align: center;
}
.player {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 640px;
margin: 20px auto 0;
gap: 8px;
}
/* select のスタイル */
#channelSelect {
align-self: flex-end;
}
/* video のスタイル */
#video {
border: 1px solid #ccc;
display: block;
width: 100%;
height: auto;
}
/* 小さい画面でも同様に機能する */
@media (max-width: 360px) {
.player {
padding: 0 8px;
}
}
</style>
</head>
<body>
<div class="player">
<select id="channelSelect">
<option value="http://localhost:8888/Camera1/index.m3u8">ライブ</option>
<option value="/Contents/Camera1/stream.m3u8">録画</option>
</select>
<video id="video" controls width="640" height="360">
Your browser does not support HTML5 video.
</video>
</div>
<script>
const video = document.getElementById('video');
const select = document.getElementById('channelSelect');
let hls;
function loadStream(url) {
if (hls) {
hls.destroy(); // 前のインスタンスを破棄
}
video.muted = true; // ミュート設定
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(error => {
console.warn('Autoplay failed:', error);
});
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
video.addEventListener('loadedmetadata', () => {
video.play().catch(error => {
console.warn('Autoplay failed:', error);
});
});
} else {
document.body.innerHTML += '<p>HLS not supported in this browser.</p>';
}
}
// 初期ロード
loadStream(select.value);
// 選択変更時に切り替え
select.addEventListener('change', () => {
loadStream(select.value);
});
</script>
</body>
</html>
- hls.jsを利用した視聴画面です
- ライブ、録画映像の切り替えが可能です
5.起動・視聴確認
RtspRecServiceプロジェクトのカレントディレクトリで実行します。
ビルドします
dotnet build -c Release
パブリッシュします
dotnet publish -r win-x64 -c Release
録画ファイルの保存ディレクトリを作成します
cd C:\RtspRec
New-Item -Path .\Contents\Camera1 -Type Directory
RtspRecServiceを起動します
.\RtspRecService
視聴ページにアクセスし、カメラ映像を視聴します
http://localhost:5000
今回は録画用ファイル作成の設定(ffmpegのコマンドパラメータの設定)は、300セグメント(5分)単位でファイルを作成しています。初回のファイルが作成されるまで録画映像は視聴できません。
6.サービス登録
Windowsサービスとして登録します
New-Service -Name "RtspRec Service" -BinaryPathName "C:\RtspRec\RtspRecService.exe" -DisplayName "RTSP Recorder" -StartupType Manual
Windowsサービスの実行ユーザはデフォルトLocal Userとなります。MediaMTX、ffmpegをWindowsの全ユーザで利用できるようにインストールしてください。
以下、インストール手順です
Windowsターミナルを管理者権限で起動します
1. 「Windows」+「R」キーをファイル名を指定して実行のダイアログを表示します
2. ダイアログの名前のテキストボックスに'wt'を入力後、「Ctrl」+「Shift」+「Enter」キーを実行します
WingetでMediaMTX,ffmpegをインストールします
Winget install -e --id bluenviron.mediamtx --scope machine
Winget install -e --id Gyan.FFmpeg --scope machine
コマンドを検索します
Get-Command mediamtx, ffmpeg
CommandType Name Version Source
----------- ---- ------- ------
Application mediamtx.exe 0.0.0.0 C:\Program Files\WinGet\Links\mediamtx.exe
Application ffmpeg.exe 0.0.0.0 C:\Program Files\WinGet\Links\ffmpeg.exe
次回は、WebRTCでのライブ視聴画面の作成を行います。