Files
ersatztv/ErsatzTV/Controllers/StreamingControllerBase.cs
T
2026-07-04 19:57:50 +02:00

101 lines
3.4 KiB
C#

using System.IO.Pipelines;
using System.Text;
using CliWrap;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers;
public abstract class StreamingControllerBase(IGraphicsEngine graphicsEngine, ILogger logger)
: ControllerBase
{
protected IActionResult GetProcessResponse(
Either<BaseError, PlayoutItemProcessModel> result,
string channelNumber,
StreamingMode mode,
IDirectStreamSessionTracker directStreamSessionTracker = null)
{
foreach (BaseError error in result.LeftToSeq())
{
logger.LogError(
"Failed to create stream for channel {ChannelNumber}: {Error}",
channelNumber,
error.Value);
return BadRequest(error.Value);
}
foreach (PlayoutItemProcessModel processModel in result.RightToSeq())
{
return StartPlayout(processModel, channelNumber, mode, directStreamSessionTracker);
}
// this will never happen
return new NotFoundResult();
}
private FileStreamResult StartPlayout(
PlayoutItemProcessModel processModel,
string channelNumber,
StreamingMode mode,
IDirectStreamSessionTracker directStreamSessionTracker)
{
// for process counter
var ffmpegProcess = new FFmpegProcess();
Command process = processModel.Process;
logger.LogDebug("ffmpeg arguments {FFmpegArguments}", process.Arguments);
var cts = new CancellationTokenSource();
// do not use 'using' here; the token needs to live longer than this method scope
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token,
HttpContext.RequestAborted);
var pipe = new Pipe();
var stdErrBuffer = new StringBuilder();
Command processWithPipe = process;
foreach (GraphicsEngineContext graphicsEngineContext in processModel.GraphicsEngineContext)
{
var gePipe = new Pipe();
processWithPipe = process.WithStandardInputPipe(PipeSource.FromStream(gePipe.Reader.AsStream()));
// fire and forget graphics engine task
_ = graphicsEngine.Run(
graphicsEngineContext,
gePipe.Writer,
linkedCts.Token);
}
CommandTask<CommandResult> task = processWithPipe
.WithStandardOutputPipe(PipeTarget.ToStream(pipe.Writer.AsStream()))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(linkedCts.Token);
// ensure cleanup happens when ffmpeg exits (either naturally or via cancellation)
_ = task.Task.ContinueWith(
(t, _) =>
{
pipe.Writer.Complete(t.Exception);
ffmpegProcess.Dispose();
linkedCts.Dispose();
cts.Dispose();
},
null,
TaskScheduler.Default);
Stream stream = pipe.Reader.AsStream();
return directStreamSessionTracker is null
? new FileStreamResult(stream, "video/mp2t")
: new TrackedFileStreamResult(stream, "video/mp2t", directStreamSessionTracker, channelNumber, mode);
}
}