101 lines
3.4 KiB
C#
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);
|
|
}
|
|
}
|