Browse Source

return multi variant playlist from start ffmpeg session

pull/2854/head
Jason Dove 3 months ago
parent
commit
0b03436ad3
No known key found for this signature in database
  1. 10
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs
  2. 67
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  3. 6
      ErsatzTV.Core/Errors/ChannelSessionAlreadyActive.cs
  4. 76
      ErsatzTV/Controllers/IptvController.cs

10
ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs

@ -2,6 +2,12 @@ @@ -2,6 +2,12 @@
namespace ErsatzTV.Application.Streaming;
public record StartFFmpegSession(string ChannelNumber, string Mode, string Scheme, string Host) :
IRequest<Either<BaseError, Unit>>,
public record StartFFmpegSession(
string ChannelNumber,
string Mode,
string Scheme,
string Host,
string PathBase,
string AccessTokenQuery) :
IRequest<Either<BaseError, string>>,
IFFmpegWorkerRequest;

67
ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.IO.Abstractions;
using System.Globalization;
using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Graphics;
@ -19,7 +20,7 @@ using Microsoft.Extensions.Logging; @@ -19,7 +20,7 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, string>>
{
private readonly IFileSystem _fileSystem;
private readonly IConfigElementRepository _configElementRepository;
@ -65,15 +66,15 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -65,15 +66,15 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_workerChannel = workerChannel;
}
public Task<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
public Task<Either<BaseError, string>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => StartProcess(request, cancellationToken))
// this weirdness is needed to maintain the error type (.ToEitherAsync() just gives BaseError)
#pragma warning disable VSTHRD103
.Bind(v => v.ToEither().MapLeft(seq => seq.Head()).MapAsync<BaseError, Task<Unit>, Unit>(identity));
.Bind(v => v.ToEither().MapLeft(seq => seq.Head()).MapAsync<BaseError, Task<string>, string>(identity));
#pragma warning restore VSTHRD103
private async Task<Unit> StartProcess(StartFFmpegSession request, CancellationToken cancellationToken)
private async Task<string> StartProcess(StartFFmpegSession request, CancellationToken cancellationToken)
{
Option<TimeSpan> idleTimeout = await _configElementRepository
.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout, cancellationToken)
@ -116,7 +117,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -116,7 +117,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
await worker.WaitForPlaylistSegments(initialSegmentCount, cancellationToken);
return Unit.Default;
return await GetMultiVariantPlaylist(request);
}
private HlsSessionWorker GetSessionWorker(StartFFmpegSession request, Option<FrameRate> targetFramerate) =>
@ -139,12 +140,12 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -139,12 +140,12 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
SessionMustBeInactive(request)
.BindT(_ => FolderMustBeEmpty(request));
private Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
private async Task<Validation<BaseError, Unit>> SessionMustBeInactive(StartFFmpegSession request)
{
var result = Optional(_ffmpegSegmenterService.TryAddWorker(request.ChannelNumber, null))
.Where(success => success)
.Map(_ => Unit.Default)
.ToValidation<BaseError>(new ChannelSessionAlreadyActive());
.ToValidation<BaseError>(new ChannelSessionAlreadyActive(await GetMultiVariantPlaylist(request)));
if (result.IsFail && _ffmpegSegmenterService.TryGetWorker(
request.ChannelNumber,
@ -153,7 +154,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -153,7 +154,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
worker?.Touch(Option<string>.None);
}
return result.AsTask();
return result;
}
private Task<Validation<BaseError, Unit>> FolderMustBeEmpty(StartFFmpegSession request)
@ -166,4 +167,52 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -166,4 +167,52 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
return Task.FromResult<Validation<BaseError, Unit>>(Unit.Default);
}
private async Task<string> GetMultiVariantPlaylist(StartFFmpegSession request)
{
var variantPlaylist =
$"{request.Scheme}://{request.Host}{request.PathBase}/iptv/session/{request.ChannelNumber}/hls.m3u8{request.AccessTokenQuery}";
Option<ChannelStreamingSpecsViewModel> maybeStreamingSpecs =
await _mediator.Send(new GetChannelStreamingSpecs(request.ChannelNumber));
string resolution = string.Empty;
var bitrate = "10000000";
foreach (ChannelStreamingSpecsViewModel streamingSpecs in maybeStreamingSpecs)
{
string videoCodec = streamingSpecs.VideoFormat switch
{
FFmpegProfileVideoFormat.Av1 => "av01.0.01M.08",
FFmpegProfileVideoFormat.Hevc => "hvc1.1.6.L93.B0",
FFmpegProfileVideoFormat.H264 => "avc1.4D4028",
_ => string.Empty
};
string audioCodec = streamingSpecs.AudioFormat switch
{
FFmpegProfileAudioFormat.Ac3 => "ac-3",
FFmpegProfileAudioFormat.Aac or FFmpegProfileAudioFormat.AacLatm => "mp4a.40.2",
_ => string.Empty
};
List<string> codecStrings = [];
if (!string.IsNullOrWhiteSpace(videoCodec))
{
codecStrings.Add(videoCodec);
}
if (!string.IsNullOrWhiteSpace(audioCodec))
{
codecStrings.Add(audioCodec);
}
string codecs = codecStrings.Count > 0 ? $",CODECS=\"{string.Join(",", codecStrings)}\"" : string.Empty;
resolution = $",RESOLUTION={streamingSpecs.Width}x{streamingSpecs.Height}{codecs}";
bitrate = streamingSpecs.Bitrate.ToString(CultureInfo.InvariantCulture);
}
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH={bitrate}{resolution}
{variantPlaylist}";
}
}

6
ErsatzTV.Core/Errors/ChannelSessionAlreadyActive.cs

@ -1,8 +1,6 @@ @@ -1,8 +1,6 @@
namespace ErsatzTV.Core.Errors;
public class ChannelSessionAlreadyActive : BaseError
public class ChannelSessionAlreadyActive(string multiVariantPlaylist) : BaseError("Channel already has HLS session")
{
public ChannelSessionAlreadyActive() : base("Channel already has HLS session")
{
}
public string MultiVariantPlaylist { get; } = multiVariantPlaylist;
}

76
ErsatzTV/Controllers/IptvController.cs

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
using System.Diagnostics;
using System.Globalization;
using System.Text;
using CliWrap;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Images;
@ -93,6 +91,15 @@ public class IptvController : StreamingControllerBase @@ -93,6 +91,15 @@ public class IptvController : StreamingControllerBase
return NotFound();
}
foreach (ChannelViewModel channel in maybeChannel)
{
// NEXT: MPEG-TS streams are not (yet?) supported
if (!channel.IsEnabled || channel.StreamingEngine is StreamingEngine.Next)
{
return NotFound();
}
}
// if mode is "unspecified" - find the configured mode and set it or redirect
if (string.IsNullOrWhiteSpace(mode) || mode == "mixed")
{
@ -224,11 +231,16 @@ public class IptvController : StreamingControllerBase @@ -224,11 +231,16 @@ public class IptvController : StreamingControllerBase
"Maybe starting ffmpeg session for channel {Channel}, mode {Mode}",
channelNumber,
mode);
var request = new StartFFmpegSession(channelNumber, mode, Request.Scheme, Request.Host.ToString());
Either<BaseError, Unit> result = await _mediator.Send(request);
string multiVariantPlaylist = await GetMultiVariantPlaylist(channelNumber);
var request = new StartFFmpegSession(
channelNumber,
mode,
Request.Scheme,
Request.Host.ToString(),
Request.PathBase,
AccessTokenQuery());
Either<BaseError, string> result = await _mediator.Send(request);
return result.Match<IActionResult>(
_ =>
multiVariantPlaylist =>
{
_logger.LogDebug(
"Session started; returning multi-variant playlist for channel {Channel}",
@ -241,12 +253,12 @@ public class IptvController : StreamingControllerBase @@ -241,12 +253,12 @@ public class IptvController : StreamingControllerBase
{
switch (error)
{
case ChannelSessionAlreadyActive:
case ChannelSessionAlreadyActive active:
_logger.LogDebug(
"Session is already active; returning multi-variant playlist for channel {Channel}",
channelNumber);
return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl");
return Content(active.MultiVariantPlaylist, "application/vnd.apple.mpegurl");
// return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8");
default:
_logger.LogWarning(
@ -289,54 +301,6 @@ public class IptvController : StreamingControllerBase @@ -289,54 +301,6 @@ public class IptvController : StreamingControllerBase
public async Task<IActionResult> GetStream(string channelNumber) =>
await GetHlsDirectStream(channelNumber);
private async Task<string> GetMultiVariantPlaylist(string channelNumber)
{
var variantPlaylist =
$"{Request.Scheme}://{Request.Host}{Request.PathBase}/iptv/session/{channelNumber}/hls.m3u8{AccessTokenQuery()}";
Option<ChannelStreamingSpecsViewModel> maybeStreamingSpecs =
await _mediator.Send(new GetChannelStreamingSpecs(channelNumber));
string resolution = string.Empty;
var bitrate = "10000000";
foreach (ChannelStreamingSpecsViewModel streamingSpecs in maybeStreamingSpecs)
{
string videoCodec = streamingSpecs.VideoFormat switch
{
FFmpegProfileVideoFormat.Av1 => "av01.0.01M.08",
FFmpegProfileVideoFormat.Hevc => "hvc1.1.6.L93.B0",
FFmpegProfileVideoFormat.H264 => "avc1.4D4028",
_ => string.Empty
};
string audioCodec = streamingSpecs.AudioFormat switch
{
FFmpegProfileAudioFormat.Ac3 => "ac-3",
FFmpegProfileAudioFormat.Aac or FFmpegProfileAudioFormat.AacLatm => "mp4a.40.2",
_ => string.Empty
};
List<string> codecStrings = [];
if (!string.IsNullOrWhiteSpace(videoCodec))
{
codecStrings.Add(videoCodec);
}
if (!string.IsNullOrWhiteSpace(audioCodec))
{
codecStrings.Add(audioCodec);
}
string codecs = codecStrings.Count > 0 ? $",CODECS=\"{string.Join(",", codecStrings)}\"" : string.Empty;
resolution = $",RESOLUTION={streamingSpecs.Width}x{streamingSpecs.Height}{codecs}";
bitrate = streamingSpecs.Bitrate.ToString(CultureInfo.InvariantCulture);
}
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH={bitrate}{resolution}
{variantPlaylist}";
}
private async Task<IActionResult> GetHlsDirectStream(string channelNumber)
{
var request = new GetPlayoutItemProcessByChannelNumber(

Loading…
Cancel
Save