Browse Source

consolidate streaming modes (#2544)

* consolidate segmenters

* let old segmenter mode query params continue to work
pull/2545/head
Jason Dove 3 months ago committed by GitHub
parent
commit
ffe15629cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Channels/Mapper.cs
  3. 18
      ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs
  4. 26
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  5. 4
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  6. 400
      ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs
  7. 22
      ErsatzTV.Application/Streaming/Queries/GetConcatSegmenterProcessByChannelNumber.cs
  8. 43
      ErsatzTV.Application/Streaming/Queries/GetConcatSegmenterProcessByChannelNumberHandler.cs
  9. 2
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs
  10. 4
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  11. 4
      ErsatzTV.Core/Domain/StreamingMode.cs
  12. 177
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  13. 50
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  14. 7
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  15. 2
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  16. 43
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  17. 6855
      ErsatzTV.Infrastructure.MySql/Migrations/20251018150417_Remove_HlsSegmenterV2.Designer.cs
  18. 21
      ErsatzTV.Infrastructure.MySql/Migrations/20251018150417_Remove_HlsSegmenterV2.cs
  19. 2
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  20. 6682
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251018145348_Remove_HlsSegmenterV2.Designer.cs
  21. 21
      ErsatzTV.Infrastructure.Sqlite/Migrations/20251018145348_Remove_HlsSegmenterV2.cs
  22. 2
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  23. 3
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  24. 2
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  25. 31
      ErsatzTV/Controllers/InternalController.cs
  26. 41
      ErsatzTV/Controllers/IptvController.cs
  27. 8
      ErsatzTV/Controllers/StreamingControllerBase.cs
  28. 2
      ErsatzTV/Pages/ChannelEditor.razor
  29. 4
      ErsatzTV/Pages/Channels.razor
  30. 11
      ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

3
CHANGELOG.md

@ -36,11 +36,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -36,11 +36,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Playback troubleshooting: wait for at least 2 initial segments (up to configured initial segment count) to reduce stalls
- Fix Trakt List sync
- Fix QSV audio sync
- Fix QSV capability detection on Linux using non-drm displays (e.g. wayland)
### Changed
- Do not use graphics engine for single, permanent watermark
- Rename `YAML Validation` tool to `Sequential Schedule Validation`
- Greatly reduce debug log spam during playout builds by logging summaries of certain warnings at the end
- Remove *experimental* `HLS Segmenter V2` streaming mode; it is not possible to maintain quality output using this mode
- Remove original `HLS Segmenter` streaming mode and rename `HLS Segmenter (fmp4)` to `HLS Segmenter`
## [25.7.1] - 2025-10-09
### Added

2
ErsatzTV.Application/Channels/Mapper.cs

@ -81,8 +81,6 @@ internal static class Mapper @@ -81,8 +81,6 @@ internal static class Mapper
StreamingMode.TransportStreamHybrid => "MPEG-TS",
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
StreamingMode.HttpLiveStreamingSegmenterFmp4 => "HLS Segmenter (fmp4)",
StreamingMode.HttpLiveStreamingSegmenterV2 => "HLS Segmenter V2",
_ => throw new ArgumentOutOfRangeException(nameof(channel))
};
}

18
ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs

@ -4,15 +4,11 @@ using ErsatzTV.Core.Iptv; @@ -4,15 +4,11 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels;
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
public class GetChannelPlaylistHandler(IChannelRepository channelRepository)
: IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
{
private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll(cancellationToken)
channelRepository.GetAll(cancellationToken)
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(
request.Scheme,
@ -38,14 +34,6 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha @@ -38,14 +34,6 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "segmenter-fmp4":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenterFmp4;
result.Add(channel);
break;
case "segmenter-v2":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenterV2;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);

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

@ -31,7 +31,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -31,7 +31,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
private readonly IMediator _mediator;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<HlsSessionWorker> _sessionWorkerLogger;
private readonly ILogger<HlsSessionWorkerV2> _sessionWorkerV2Logger;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public StartFFmpegSessionHandler(
@ -42,7 +41,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -42,7 +41,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
ILocalFileSystem localFileSystem,
ILogger<StartFFmpegSessionHandler> logger,
ILogger<HlsSessionWorker> sessionWorkerLogger,
ILogger<HlsSessionWorkerV2> sessionWorkerV2Logger,
IFFmpegSegmenterService ffmpegSegmenterService,
IConfigElementRepository configElementRepository,
IGraphicsEngine graphicsEngine,
@ -56,7 +54,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -56,7 +54,6 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_localFileSystem = localFileSystem;
_logger = logger;
_sessionWorkerLogger = sessionWorkerLogger;
_sessionWorkerV2Logger = sessionWorkerV2Logger;
_ffmpegSegmenterService = ffmpegSegmenterService;
_configElementRepository = configElementRepository;
_graphicsEngine = graphicsEngine;
@ -92,7 +89,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -92,7 +89,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
await _mediator.Send(new RefreshGraphicsElements(), cancellationToken);
IHlsSessionWorker worker = GetSessionWorker(request, targetFramerate);
HlsSessionWorker worker = GetSessionWorker(request, targetFramerate);
_ffmpegSegmenterService.AddOrUpdateWorker(request.ChannelNumber, worker);
@ -118,29 +115,12 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -118,29 +115,12 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
return Unit.Default;
}
private IHlsSessionWorker GetSessionWorker(StartFFmpegSession request, Option<int> targetFramerate) =>
private HlsSessionWorker GetSessionWorker(StartFFmpegSession request, Option<int> targetFramerate) =>
request.Mode switch
{
"segmenter-v2" => new HlsSessionWorkerV2(
_serviceScopeFactory,
_localFileSystem,
_sessionWorkerV2Logger,
targetFramerate,
request.Scheme,
request.Host),
"segmenter-fmp4" => new HlsSessionWorker(
_serviceScopeFactory,
OutputFormatKind.HlsMp4,
_graphicsEngine,
_client,
_hlsPlaylistFilter,
_configElementRepository,
_localFileSystem,
_sessionWorkerLogger,
targetFramerate),
_ => new HlsSessionWorker(
_serviceScopeFactory,
OutputFormatKind.Hls,
OutputFormatKind.HlsMp4,
_graphicsEngine,
_client,
_hlsPlaylistFilter,

4
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -453,7 +453,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -453,7 +453,7 @@ public class HlsSessionWorker : IHlsSessionWorker
var request = new GetPlayoutItemProcessByChannelNumber(
_channelNumber,
_outputFormat is OutputFormatKind.HlsMp4 ? StreamingMode.HttpLiveStreamingSegmenterFmp4 : StreamingMode.HttpLiveStreamingSegmenter,
StreamingMode.HttpLiveStreamingSegmenter,
now,
startAtZero,
realtime,
@ -544,7 +544,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -544,7 +544,7 @@ public class HlsSessionWorker : IHlsSessionWorker
Either<BaseError, PlayoutItemProcessModel> maybeOfflineProcess = await _mediator.Send(
new GetErrorProcess(
_channelNumber,
_outputFormat is OutputFormatKind.HlsMp4 ? StreamingMode.HttpLiveStreamingSegmenterFmp4 : StreamingMode.HttpLiveStreamingSegmenter,
StreamingMode.HttpLiveStreamingSegmenter,
realtime,
ptsOffset,
processModel.MaybeDuration,

400
ErsatzTV.Application/Streaming/HlsSessionWorkerV2.cs

@ -1,400 +0,0 @@ @@ -1,400 +0,0 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Timers;
using CliWrap;
using CliWrap.Buffered;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Timer = System.Timers.Timer;
namespace ErsatzTV.Application.Streaming;
public class HlsSessionWorkerV2 : IHlsSessionWorker
{
//private static int _workAheadCount;
private readonly string _host;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<HlsSessionWorkerV2> _logger;
private readonly IMediator _mediator;
private readonly string _scheme;
private readonly SemaphoreSlim _slim = new(1, 1);
private readonly Lock _sync = new();
private readonly Option<int> _targetFramerate;
private CancellationTokenSource _cancellationTokenSource;
private string _channelNumber;
private DateTimeOffset _channelStart;
private bool _disposedValue;
private DateTimeOffset _lastAccess;
private Option<PlayoutItemProcessModel> _lastProcessModel;
private IServiceScope _serviceScope;
private HlsSessionState _state;
private Timer _timer;
private DateTimeOffset _transcodedUntil;
public HlsSessionWorkerV2(
IServiceScopeFactory serviceScopeFactory,
ILocalFileSystem localFileSystem,
ILogger<HlsSessionWorkerV2> logger,
Option<int> targetFramerate,
string scheme,
string host)
{
_serviceScope = serviceScopeFactory.CreateScope();
_mediator = _serviceScope.ServiceProvider.GetRequiredService<IMediator>();
_localFileSystem = localFileSystem;
_logger = logger;
_targetFramerate = targetFramerate;
_scheme = scheme;
_host = host;
}
public DateTimeOffset PlaylistStart { get; private set; }
public async Task Cancel(CancellationToken cancellationToken)
{
_logger.LogInformation("API termination request for HLS session for channel {Channel}", _channelNumber);
await _slim.WaitAsync(cancellationToken);
try
{
await _cancellationTokenSource.CancelAsync();
}
finally
{
_slim.Release();
}
}
public void Touch()
{
lock (_sync)
{
//_logger.LogDebug("Keep alive - session worker v2 for channel {ChannelNumber}", _channelNumber);
_lastAccess = DateTimeOffset.Now;
_timer?.Stop();
_timer?.Start();
}
}
public Task<Option<TrimPlaylistResult>> TrimPlaylist(
DateTimeOffset filterBefore,
CancellationToken cancellationToken) =>
Task.FromResult(Option<TrimPlaylistResult>.None);
public void PlayoutUpdated() => _state = HlsSessionState.PlayoutUpdated;
public HlsSessionModel GetModel() => new(_channelNumber, _state.ToString(), _transcodedUntil, _lastAccess);
void IDisposable.Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public async Task Run(
string channelNumber,
Option<TimeSpan> idleTimeout,
CancellationToken incomingCancellationToken)
{
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken);
try
{
_channelNumber = channelNumber;
foreach (TimeSpan timeout in idleTimeout)
{
lock (_sync)
{
_timer = new Timer(timeout.TotalMilliseconds) { AutoReset = false };
_timer.Elapsed += CancelRun;
}
}
CancellationToken cancellationToken = _cancellationTokenSource.Token;
_logger.LogInformation("Starting HLS V2 session for channel {Channel}", channelNumber);
if (_localFileSystem.ListFiles(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber)).Any())
{
_logger.LogError("Transcode folder is NOT empty!");
}
Touch();
_transcodedUntil = DateTimeOffset.Now;
PlaylistStart = _transcodedUntil;
_channelStart = _transcodedUntil;
Option<int> maybePlayoutId = await _mediator.Send(
new GetPlayoutIdByChannelNumber(_channelNumber),
cancellationToken);
// time shift on-demand playout if needed
foreach (int playoutId in maybePlayoutId)
{
await _mediator.Send(
new TimeShiftOnDemandPlayout(playoutId, _transcodedUntil, true),
cancellationToken);
}
// start concat/segmenter process
// other transcode processes will be started by incoming requests from concat/segmenter process
var request = new GetConcatSegmenterProcessByChannelNumber(_scheme, _host, _channelNumber);
Either<BaseError, PlayoutItemProcessModel> maybeSegmenterProcess =
await _mediator.Send(request, cancellationToken);
foreach (BaseError error in maybeSegmenterProcess.LeftToSeq())
{
_logger.LogError(
"Failed to start concat segmenter for channel {ChannelNumber}: {Error}",
_channelNumber,
error.ToString());
return;
}
foreach (PlayoutItemProcessModel processModel in maybeSegmenterProcess.RightAsEnumerable())
{
Command process = processModel.Process;
_logger.LogDebug("ffmpeg concat segmenter arguments {FFmpegArguments}", process.Arguments);
try
{
BufferedCommandResult commandResult = await process
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
// TODO: handle result? this will probably *always* be canceled
_logger.LogDebug("ffmpeg concat segmenter finished (canceled)");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unexpected error in HLS Session Worker V2");
}
finally
{
if (_timer is not null)
{
lock (_sync)
{
_timer.Elapsed -= CancelRun;
}
}
try
{
await _mediator.Send(
new UpdateOnDemandCheckpoint(_channelNumber, DateTimeOffset.Now),
CancellationToken.None);
}
catch (Exception)
{
// do nothing
}
try
{
_localFileSystem.EmptyFolder(Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber));
}
catch
{
// do nothing
}
}
return;
[SuppressMessage("Usage", "VSTHRD100:Avoid async void methods")]
async void CancelRun(object o, ElapsedEventArgs e)
{
try
{
await _cancellationTokenSource.CancelAsync();
}
catch (Exception)
{
// do nothing
}
}
}
public async Task WaitForPlaylistSegments(
int initialSegmentCount,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
try
{
DateTimeOffset start = DateTimeOffset.Now;
DateTimeOffset finish = start.AddSeconds(8);
string segmentFolder = Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber);
string playlistFileName = Path.Combine(segmentFolder, "live.m3u8");
_logger.LogDebug("Waiting for playlist to exist");
while (!_localFileSystem.FileExists(playlistFileName))
{
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
}
_logger.LogDebug("Playlist exists");
var segmentCount = 0;
int lastSegmentCount = -1;
while (DateTimeOffset.Now < finish && segmentCount < initialSegmentCount)
{
if (segmentCount != lastSegmentCount)
{
lastSegmentCount = segmentCount;
_logger.LogDebug(
"Segment count {SegmentCount} of {InitialSegmentCount}",
segmentCount,
initialSegmentCount);
}
await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);
segmentCount = _localFileSystem.ListFiles(segmentFolder, "*.ts").Count();
}
}
finally
{
sw.Stop();
_logger.LogDebug("WaitForPlaylistSegments took {Duration}", sw.Elapsed);
}
}
public async Task<Either<BaseError, PlayoutItemProcessModel>> GetNextPlayoutItemProcess()
{
foreach (PlayoutItemProcessModel processModel in _lastProcessModel)
{
_state = NextState(_state, processModel);
}
// if we're at least 30 seconds ahead, drop to realtime
var transcodedBuffer = TimeSpan.FromSeconds(
Math.Max(0, _transcodedUntil.Subtract(DateTimeOffset.Now).TotalSeconds));
if (transcodedBuffer >= TimeSpan.FromSeconds(30))
{
// throttle to realtime if needed
HlsSessionState nextState = _state switch
{
HlsSessionState.SeekAndWorkAhead => HlsSessionState.SeekAndRealtime,
HlsSessionState.ZeroAndWorkAhead => HlsSessionState.ZeroAndRealtime,
_ => _state
};
if (nextState != _state)
{
_logger.LogDebug("HLS session state throttling {Last} => {Next}", _state, nextState);
_state = nextState;
}
}
_logger.LogDebug("Getting next playout item process with state {@State}", _state);
//long ptsOffset = await GetPtsOffset(_channelNumber, CancellationToken.None);
bool startAtZero = _state is HlsSessionState.ZeroAndRealtime or HlsSessionState.ZeroAndWorkAhead;
bool realtime = _state is HlsSessionState.ZeroAndRealtime or HlsSessionState.SeekAndRealtime;
var request = new GetPlayoutItemProcessByChannelNumber(
_channelNumber,
StreamingMode.HttpLiveStreamingSegmenterV2,
_transcodedUntil,
startAtZero,
realtime,
_channelStart,
0,
_targetFramerate);
Either<BaseError, PlayoutItemProcessModel> result = await _mediator.Send(request);
foreach (PlayoutItemProcessModel processModel in result.RightToSeq())
{
_logger.LogDebug("Next playout item process will transcode until {Until}", processModel.Until);
_transcodedUntil = processModel.Until;
_lastProcessModel = processModel;
}
if (result.IsLeft)
{
_lastProcessModel = Option<PlayoutItemProcessModel>.None;
}
return result;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
if (_timer is not null)
{
_timer.Dispose();
_timer = null;
}
_serviceScope.Dispose();
_serviceScope = null;
}
_disposedValue = true;
}
}
private HlsSessionState NextState(HlsSessionState state, PlayoutItemProcessModel processModel)
{
bool isComplete = processModel?.IsComplete == true;
HlsSessionState result = state switch
{
// playout updates should have the channel start over, transcode method will throttle if needed
HlsSessionState.PlayoutUpdated => HlsSessionState.SeekAndWorkAhead,
// after seeking and NOT completing the item, seek again, transcode method will throttle if needed
HlsSessionState.SeekAndWorkAhead when !isComplete => HlsSessionState.SeekAndWorkAhead,
// after seeking and completing the item, start at zero
HlsSessionState.SeekAndWorkAhead => HlsSessionState.ZeroAndWorkAhead,
// after starting and zero and NOT completing the item, seek, transcode method will throttle if needed
HlsSessionState.ZeroAndWorkAhead when !isComplete => HlsSessionState.SeekAndWorkAhead,
// after starting at zero and completing the item, start at zero again, transcode method will throttle if needed
HlsSessionState.ZeroAndWorkAhead => HlsSessionState.ZeroAndWorkAhead,
// realtime will always complete items, so start next at zero
HlsSessionState.SeekAndRealtime => HlsSessionState.ZeroAndRealtime,
// realtime will always complete items, so start next at zero
HlsSessionState.ZeroAndRealtime => HlsSessionState.ZeroAndRealtime,
// this will never happen with the enum
_ => throw new InvalidOperationException()
};
_logger.LogDebug("HLS session state {Last} => {Next}", state, result);
return result;
}
}

22
ErsatzTV.Application/Streaming/Queries/GetConcatSegmenterProcessByChannelNumber.cs

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Streaming;
public record GetConcatSegmenterProcessByChannelNumber : FFmpegProcessRequest
{
public GetConcatSegmenterProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
channelNumber,
StreamingMode.TransportStream,
DateTimeOffset.Now,
false,
true,
DateTimeOffset.Now, // unused
0)
{
Scheme = scheme;
Host = host;
}
public string Scheme { get; }
public string Host { get; }
}

43
ErsatzTV.Application/Streaming/Queries/GetConcatSegmenterProcessByChannelNumberHandler.cs

@ -1,43 +0,0 @@ @@ -1,43 +0,0 @@
using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Streaming;
public class GetConcatSegmenterProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService)
: FFmpegProcessHandler<GetConcatSegmenterProcessByChannelNumber>(dbContextFactory)
{
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
TvContext dbContext,
GetConcatSegmenterProcessByChannelNumber request,
Channel channel,
string ffmpegPath,
string ffprobePath,
CancellationToken cancellationToken)
{
bool saveReports = await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports, cancellationToken)
.Map(result => result.IfNone(false));
Command process = await ffmpegProcessService.ConcatSegmenterChannel(
ffmpegPath,
saveReports,
channel,
request.Scheme,
request.Host);
return new PlayoutItemProcessModel(
process,
Option<GraphicsEngineContext>.None,
Option<TimeSpan>.None,
DateTimeOffset.MaxValue,
true);
}
}

2
ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs

@ -39,7 +39,7 @@ public class GetHlsPlaylistByChannelNumberHandler : @@ -39,7 +39,7 @@ public class GetHlsPlaylistByChannelNumberHandler :
{
string mode = request.Mode switch
{
"segmenter" or "segmenter-fmp4" or "segmenter-v2" or "ts-legacy" or "ts" => $"&mode={request.Mode}",
"segmenter" or "ts-legacy" or "ts" => $"&mode={request.Mode}",
// "hls-direct" => string.Empty,
_ => string.Empty
};

4
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -42,7 +42,7 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -42,7 +42,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings(
StreamingMode.HttpLiveStreamingSegmenterFmp4,
StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile,
TestVersion,
new MediaStream(),
@ -106,7 +106,7 @@ public class FFmpegPlaybackSettingsCalculatorTests @@ -106,7 +106,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings(
StreamingMode.HttpLiveStreamingSegmenterFmp4,
StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile,
TestVersion,
new MediaStream(),

4
ErsatzTV.Core/Domain/StreamingMode.cs

@ -5,10 +5,8 @@ public enum StreamingMode @@ -5,10 +5,8 @@ public enum StreamingMode
TransportStream = 1,
HttpLiveStreamingDirect = 2,
// HttpLiveStreamingHybrid = 3,
HttpLiveStreamingSegmenter = 4,
TransportStreamHybrid = 5,
HttpLiveStreamingSegmenterV2 = 6,
HttpLiveStreamingSegmenterFmp4 = 100
// HttpLiveStreamingSegmenterLegacy = 999
}

177
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -244,14 +244,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -244,14 +244,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
switch (channel.StreamingMode)
{
case StreamingMode.HttpLiveStreamingSegmenter:
outputFormat = OutputFormatKind.Hls;
break;
case StreamingMode.HttpLiveStreamingSegmenterFmp4:
outputFormat = OutputFormatKind.HlsMp4;
break;
case StreamingMode.HttpLiveStreamingSegmenterV2:
outputFormat = OutputFormatKind.Nut;
break;
case StreamingMode.HttpLiveStreamingDirect:
{
// use mpeg-ts by default
@ -624,14 +618,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -624,14 +618,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
switch (channel.StreamingMode)
{
case StreamingMode.HttpLiveStreamingSegmenter:
outputFormat = OutputFormatKind.Hls;
break;
case StreamingMode.HttpLiveStreamingSegmenterFmp4:
outputFormat = OutputFormatKind.HlsMp4;
break;
case StreamingMode.HttpLiveStreamingSegmenterV2:
outputFormat = OutputFormatKind.Nut;
break;
}
Option<string> hlsPlaylistPath = outputFormat is OutputFormatKind.Hls or OutputFormatKind.HlsMp4
@ -642,7 +630,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -642,7 +630,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<string> hlsSegmentTemplate = outputFormat switch
{
OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"),
OutputFormatKind.HlsMp4 => Path.Combine(
FileSystemLayout.TranscodeFolder,
channel.Number,
@ -771,170 +758,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -771,170 +758,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
return GetCommand(ffmpegPath, None, None, None, concatInputFile, None, pipeline);
}
public async Task<Command> ConcatSegmenterChannel(
string ffmpegPath,
bool saveReports,
Channel channel,
string scheme,
string host)
{
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
var concatInputFile = new ConcatInputFile(
$"http://localhost:{Settings.StreamingPort}/ffmpeg/concat/{channel.Number}?mode=segmenter-v2",
resolution);
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateConcatSegmenterSettings(
channel.FFmpegProfile,
Option<int>.None);
playbackSettings.PadAudio = false;
string audioFormat = playbackSettings.AudioFormat switch
{
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
FFmpegProfileAudioFormat.Copy => AudioFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}")
};
var audioState = new AudioState(
audioFormat,
playbackSettings.AudioChannels,
playbackSettings.AudioBitrate,
playbackSettings.AudioBufferSize,
playbackSettings.AudioSampleRate,
false,
playbackSettings.NormalizeLoudnessMode switch
{
// TODO: NormalizeLoudnessMode.LoudNorm => AudioFilter.LoudNorm,
_ => AudioFilter.None
});
IPixelFormat pixelFormat = channel.FFmpegProfile.BitDepth switch
{
FFmpegProfileBitDepth.TenBit => new PixelFormatYuv420P10Le(),
_ => new PixelFormatYuv420P()
};
var ffmpegVideoStream = new VideoStream(
0,
VideoFormat.Raw,
string.Empty,
Some(pixelFormat),
ColorParams.Default,
resolution,
"1:1",
string.Empty,
Option<string>.None,
false,
ScanKind.Progressive);
var videoInputFile = new VideoInputFile(concatInputFile.Url, new List<VideoStream> { ffmpegVideoStream });
var ffmpegAudioStream = new AudioStream(1, string.Empty, channel.FFmpegProfile.AudioChannels);
Option<AudioInputFile> audioInputFile = new AudioInputFile(
concatInputFile.Url,
new List<AudioStream> { ffmpegAudioStream },
audioState);
Option<SubtitleInputFile> subtitleInputFile = Option<SubtitleInputFile>.None;
Option<WatermarkInputFile> watermarkInputFile = Option<WatermarkInputFile>.None;
Option<GraphicsEngineInput> graphicsEngineInput = Option<GraphicsEngineInput>.None;
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None);
string videoFormat = GetVideoFormat(playbackSettings);
Option<string> maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile);
Option<string> maybeVideoPreset = GetVideoPreset(
hwAccel,
videoFormat,
channel.FFmpegProfile.VideoPreset,
FFmpegLibraryHelper.MapBitDepth(channel.FFmpegProfile.BitDepth));
Option<string> hlsPlaylistPath = Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8");
Option<string> hlsSegmentTemplate = videoFormat switch
{
// hls/hevc needs mp4
VideoFormat.Hevc => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.m4s"),
// hls is otherwise fine with ts
_ => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
};
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
true,
videoFormat,
maybeVideoProfile,
maybeVideoPreset,
channel.FFmpegProfile.AllowBFrames,
Optional(playbackSettings.PixelFormat),
resolution,
resolution,
Option<FrameSize>.None,
false,
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
playbackSettings.VideoTrackTimeScale,
playbackSettings.Deinterlace);
Option<string> vaapiDisplay = VaapiDisplayName(hwAccel, channel.FFmpegProfile.VaapiDisplay);
Option<string> vaapiDriver = VaapiDriverName(hwAccel, channel.FFmpegProfile.VaapiDriver);
Option<string> vaapiDevice = VaapiDeviceName(hwAccel, channel.FFmpegProfile.VaapiDevice);
var ffmpegState = new FFmpegState(
saveReports,
HardwareAccelerationMode.None,
hwAccel,
vaapiDriver,
vaapiDevice,
playbackSettings.StreamSeek,
Option<TimeSpan>.None,
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
"ErsatzTV",
channel.Name,
Option<string>.None,
Option<string>.None,
Option<string>.None,
OutputFormatKind.Hls,
hlsPlaylistPath,
hlsSegmentTemplate,
Option<string>.None,
0,
playbackSettings.ThreadCount,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
false,
false,
GetTonemapAlgorithm(playbackSettings),
channel.UniqueId == Guid.Empty);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
hwAccel,
videoInputFile,
audioInputFile,
watermarkInputFile,
subtitleInputFile,
concatInputFile,
graphicsEngineInput,
vaapiDisplay,
vaapiDriver,
vaapiDevice,
FileSystemLayout.FFmpegReportsFolder,
FileSystemLayout.FontsCacheFolder,
ffmpegPath);
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
// copy video input options to concat input
concatInputFile.InputOptions.AddRange(videoInputFile.InputOptions);
return GetCommand(ffmpegPath, None, None, None, concatInputFile, None, pipeline);
}
public async Task<Command> WrapSegmenter(
string ffmpegPath,
bool saveReports,

50
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -29,18 +29,18 @@ namespace ErsatzTV.Core.FFmpeg; @@ -29,18 +29,18 @@ namespace ErsatzTV.Core.FFmpeg;
public static class FFmpegPlaybackSettingsCalculator
{
private static readonly List<string> CommonFormatFlags = new()
{
private static readonly List<string> CommonFormatFlags =
[
"+genpts",
"+discardcorrupt",
"+igndts"
};
];
private static readonly List<string> SegmenterFormatFlags = new()
{
private static readonly List<string> SegmenterFormatFlags =
[
"+discardcorrupt",
"+igndts"
};
];
public static FFmpegPlaybackSettings CalculateSettings(
StreamingMode streamingMode,
@ -59,14 +59,11 @@ public static class FFmpegPlaybackSettingsCalculator @@ -59,14 +59,11 @@ public static class FFmpegPlaybackSettingsCalculator
FormatFlags = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => SegmenterFormatFlags,
StreamingMode.HttpLiveStreamingSegmenterFmp4 => SegmenterFormatFlags,
_ => CommonFormatFlags
},
RealtimeOutput = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime,
StreamingMode.HttpLiveStreamingSegmenterFmp4 => hlsRealtime,
StreamingMode.HttpLiveStreamingSegmenterV2 => hlsRealtime,
_ => true
},
ThreadCount = ffmpegProfile.ThreadCount
@ -86,8 +83,6 @@ public static class FFmpegPlaybackSettingsCalculator @@ -86,8 +83,6 @@ public static class FFmpegPlaybackSettingsCalculator
break;
case StreamingMode.TransportStreamHybrid:
case StreamingMode.HttpLiveStreamingSegmenter:
case StreamingMode.HttpLiveStreamingSegmenterFmp4:
case StreamingMode.HttpLiveStreamingSegmenterV2:
case StreamingMode.TransportStream:
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
@ -179,37 +174,6 @@ public static class FFmpegPlaybackSettingsCalculator @@ -179,37 +174,6 @@ public static class FFmpegPlaybackSettingsCalculator
return result;
}
public static FFmpegPlaybackSettings CalculateConcatSegmenterSettings(
FFmpegProfile ffmpegProfile,
Option<int> targetFramerate) =>
new()
{
FormatFlags = CommonFormatFlags,
RealtimeOutput = false,
ThreadCount = ffmpegProfile.ThreadCount,
HardwareAcceleration = ffmpegProfile.HardwareAcceleration,
FrameRate = targetFramerate,
VideoTrackTimeScale = 90000,
VideoFormat = ffmpegProfile.VideoFormat,
VideoBitrate = ffmpegProfile.VideoBitrate,
VideoBufferSize = ffmpegProfile.VideoBufferSize,
TonemapAlgorithm = ffmpegProfile.TonemapAlgorithm,
VideoDecoder = null,
PixelFormat = ffmpegProfile.BitDepth switch
{
FFmpegProfileBitDepth.TenBit when ffmpegProfile.VideoFormat != FFmpegProfileVideoFormat.Mpeg2Video
=> new PixelFormatYuv420P10Le(),
_ => new PixelFormatYuv420P()
},
AudioFormat = ffmpegProfile.AudioFormat,
AudioBitrate = ffmpegProfile.AudioBitrate,
AudioBufferSize = ffmpegProfile.AudioBufferSize,
AudioChannels = ffmpegProfile.AudioChannels,
AudioSampleRate = ffmpegProfile.AudioSampleRate,
NormalizeLoudnessMode = ffmpegProfile.NormalizeLoudnessMode,
Deinterlace = false
};
public static FFmpegPlaybackSettings CalculateErrorSettings(
StreamingMode streamingMode,
FFmpegProfile ffmpegProfile,
@ -229,8 +193,6 @@ public static class FFmpegPlaybackSettingsCalculator @@ -229,8 +193,6 @@ public static class FFmpegPlaybackSettingsCalculator
RealtimeOutput = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime,
StreamingMode.HttpLiveStreamingSegmenterFmp4 => hlsRealtime,
StreamingMode.HttpLiveStreamingSegmenterV2 => hlsRealtime,
_ => true
},
VideoTrackTimeScale = 90000,

7
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -57,13 +57,6 @@ public interface IFFmpegProcessService @@ -57,13 +57,6 @@ public interface IFFmpegProcessService
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Task<Command> ConcatSegmenterChannel(
string ffmpegPath,
bool saveReports,
Channel channel,
string scheme,
string host);
Task<Command> WrapSegmenter(
string ffmpegPath,
bool saveReports,

2
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -79,8 +79,6 @@ public class ChannelPlaylist @@ -79,8 +79,6 @@ public class ChannelPlaylist
{
StreamingMode.HttpLiveStreamingDirect => $"m3u8?mode=hls-direct{accessTokenUriAmp}",
StreamingMode.HttpLiveStreamingSegmenter => $"m3u8?mode=segmenter{accessTokenUriAmp}",
StreamingMode.HttpLiveStreamingSegmenterFmp4 => $"m3u8?mode=segmenter-fmp4{accessTokenUriAmp}",
StreamingMode.HttpLiveStreamingSegmenterV2 => $"m3u8?mode=segmenter-v2{accessTokenUriAmp}",
StreamingMode.TransportStreamHybrid => $"ts{accessTokenUri}",
_ => $"ts?mode=ts-legacy{accessTokenUriAmp}"
};

43
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -486,27 +486,38 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -486,27 +486,38 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux))
{
Option<string> vaapiOutput = await GetVaapiOutput("drm", Option<string>.None, device);
if (vaapiOutput.IsNone)
if (!_memoryCache.TryGetValue("ffmpeg.vaapi_displays", out List<string>? vaapiDisplays))
{
_logger.LogWarning("Unable to determine QSV capabilities; please install vainfo");
return new DefaultHardwareCapabilities();
vaapiDisplays = ["drm"];
}
foreach (string o in vaapiOutput)
{
profileEntrypoints = VaapiCapabilityParser.ParseFull(o);
}
vaapiDisplays ??= [];
vaapiDisplays = vaapiDisplays.OrderBy(s => s).ToList();
if (profileEntrypoints is not null && profileEntrypoints.Count != 0)
foreach (string vaapiDisplay in vaapiDisplays)
{
_logger.LogDebug(
"Detected {Count} VAAPI profile entrypoints using QSV device {Device}",
profileEntrypoints.Count,
device);
_memoryCache.Set(cacheKey, profileEntrypoints);
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
Option<string> vaapiOutput = await GetVaapiOutput(vaapiDisplay, Option<string>.None, device);
if (vaapiOutput.IsNone)
{
_logger.LogWarning("Unable to determine QSV capabilities; please install vainfo");
return new DefaultHardwareCapabilities();
}
foreach (string o in vaapiOutput)
{
profileEntrypoints = VaapiCapabilityParser.ParseFull(o);
}
if (profileEntrypoints is not null && profileEntrypoints.Count != 0)
{
_logger.LogDebug(
"Detected {Count} VAAPI profile entrypoints using QSV device {Device}",
profileEntrypoints.Count,
device);
_memoryCache.Set(cacheKey, profileEntrypoints);
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
}
}
}

6855
ErsatzTV.Infrastructure.MySql/Migrations/20251018150417_Remove_HlsSegmenterV2.Designer.cs generated

File diff suppressed because it is too large Load Diff

21
ErsatzTV.Infrastructure.MySql/Migrations/20251018150417_Remove_HlsSegmenterV2.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Remove_HlsSegmenterV2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 4 WHERE StreamingMode IN (6, 100)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

2
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.9")
.HasAnnotation("ProductVersion", "9.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);

6682
ErsatzTV.Infrastructure.Sqlite/Migrations/20251018145348_Remove_HlsSegmenterV2.Designer.cs generated

File diff suppressed because it is too large Load Diff

21
ErsatzTV.Infrastructure.Sqlite/Migrations/20251018145348_Remove_HlsSegmenterV2.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Remove_HlsSegmenterV2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 4 WHERE StreamingMode IN (6, 100)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

2
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.HasAnnotation("ProductVersion", "9.0.10");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{

3
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -1109,8 +1109,7 @@ public class TranscodingTests @@ -1109,8 +1109,7 @@ public class TranscodingTests
// NUT doesn't set this properly
if (profileAcceleration != HardwareAccelerationKind.Amf &&
profileVideoFormat != FFmpegProfileVideoFormat.Mpeg2Video &&
(profileAcceleration != HardwareAccelerationKind.Vaapi || vaapiDriver != VaapiDriver.RadeonSI) &&
streamingMode != StreamingMode.HttpLiveStreamingSegmenterV2)
(profileAcceleration != HardwareAccelerationKind.Vaapi || vaapiDriver != VaapiDriver.RadeonSI))
{
colorParams.IsBt709.ShouldBeTrue($"{colorParams}");
}

2
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -119,7 +119,7 @@ public class TroubleshootController( @@ -119,7 +119,7 @@ public class TroubleshootController(
string[] segmentFiles = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenterFmp4 => Directory.GetFiles(
StreamingMode.HttpLiveStreamingSegmenter => Directory.GetFiles(
FileSystemLayout.TranscodeTroubleshootingFolder,
"*.m4s"),
_ => Directory.GetFiles(FileSystemLayout.TranscodeTroubleshootingFolder, "*.ts")

31
ErsatzTV/Controllers/InternalController.cs

@ -10,7 +10,6 @@ using ErsatzTV.Application.Subtitles.Queries; @@ -10,7 +10,6 @@ using ErsatzTV.Application.Subtitles.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Extensions;
using Flurl;
@ -23,18 +22,15 @@ namespace ErsatzTV.Controllers; @@ -23,18 +22,15 @@ namespace ErsatzTV.Controllers;
[ApiExplorerSettings(IgnoreApi = true)]
public class InternalController : StreamingControllerBase
{
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly ILogger<InternalController> _logger;
private readonly IMediator _mediator;
public InternalController(
IFFmpegSegmenterService ffmpegSegmenterService,
IGraphicsEngine graphicsEngine,
IMediator mediator,
ILogger<InternalController> logger)
: base(graphicsEngine, logger)
{
_ffmpegSegmenterService = ffmpegSegmenterService;
_mediator = mediator;
_logger = logger;
}
@ -46,19 +42,7 @@ public class InternalController : StreamingControllerBase @@ -46,19 +42,7 @@ public class InternalController : StreamingControllerBase
.ToActionResult();
[HttpGet("ffmpeg/stream/{channelNumber}")]
public async Task<IActionResult> GetStream(
string channelNumber,
[FromQuery]
string mode = "mixed")
{
switch (mode)
{
case "segmenter-v2":
return await GetSegmenterV2Stream(channelNumber);
default:
return await GetTsLegacyStream(channelNumber);
}
}
public Task<IActionResult> GetStream(string channelNumber) => GetTsLegacyStream(channelNumber);
[HttpGet("ffmpeg/remote-stream/{remoteStreamId}")]
public async Task<IActionResult> GetRemoteStream(int remoteStreamId, CancellationToken cancellationToken)
@ -262,19 +246,6 @@ public class InternalController : StreamingControllerBase @@ -262,19 +246,6 @@ public class InternalController : StreamingControllerBase
return new NotFoundResult();
}
private async Task<IActionResult> GetSegmenterV2Stream(string channelNumber)
{
if (_ffmpegSegmenterService.TryGetWorker(channelNumber, out IHlsSessionWorker worker) &&
worker is HlsSessionWorkerV2 v2)
{
Either<BaseError, PlayoutItemProcessModel> result = await v2.GetNextPlayoutItemProcess();
return GetProcessResponse(result, channelNumber, StreamingMode.HttpLiveStreamingSegmenterV2);
}
_logger.LogWarning("Unable to locate session worker for channel {Channel}", channelNumber);
return new NotFoundResult();
}
private async Task<IActionResult> GetTsLegacyStream(string channelNumber)
{
var request = new GetPlayoutItemProcessByChannelNumber(

41
ErsatzTV/Controllers/IptvController.cs

@ -208,12 +208,6 @@ public class IptvController : StreamingControllerBase @@ -208,12 +208,6 @@ public class IptvController : StreamingControllerBase
case StreamingMode.HttpLiveStreamingSegmenter:
mode = "segmenter";
break;
case StreamingMode.HttpLiveStreamingSegmenterFmp4:
mode = "segmenter-fmp4";
break;
case StreamingMode.HttpLiveStreamingSegmenterV2:
mode = "segmenter-v2";
break;
default:
return Redirect($"~/iptv/channel/{channelNumber}.ts{AccessTokenQuery()}");
}
@ -223,15 +217,15 @@ public class IptvController : StreamingControllerBase @@ -223,15 +217,15 @@ public class IptvController : StreamingControllerBase
switch (mode)
{
case "segmenter":
case "segmenter-fmp4":
case "segmenter-v2":
case "segmenter-fmp4":
_logger.LogDebug(
"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, mode);
string multiVariantPlaylist = await GetMultiVariantPlaylist(channelNumber);
return result.Match<IActionResult>(
_ =>
{
@ -291,37 +285,10 @@ public class IptvController : StreamingControllerBase @@ -291,37 +285,10 @@ public class IptvController : StreamingControllerBase
public async Task<IActionResult> GetStream(string channelNumber) =>
await GetHlsDirectStream(channelNumber);
private async Task<string> GetMultiVariantPlaylist(string channelNumber, string mode)
private async Task<string> GetMultiVariantPlaylist(string channelNumber)
{
string file = mode switch
{
// this serves the unmodified playlist from disk
"segmenter-v2" => "live.m3u8",
_ => "hls.m3u8"
};
var variantPlaylist =
$"{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/{file}{AccessTokenQuery()}";
try
{
if (mode == "segmenter-v2")
{
string fileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "playlist.m3u8");
if (System.IO.File.Exists(fileName))
{
string text = await System.IO.File.ReadAllTextAsync(fileName, Encoding.UTF8);
return text.Replace("live.m3u8", variantPlaylist);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Failed to return ffmpeg multi-variant playlist; falling back to generated playlist");
}
$"{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/hls.m3u8{AccessTokenQuery()}";
Option<ChannelStreamingSpecsViewModel> maybeStreamingSpecs =
await _mediator.Send(new GetChannelStreamingSpecs(channelNumber));

8
ErsatzTV/Controllers/StreamingControllerBase.cs

@ -77,13 +77,7 @@ public abstract class StreamingControllerBase(IGraphicsEngine graphicsEngine, IL @@ -77,13 +77,7 @@ public abstract class StreamingControllerBase(IGraphicsEngine graphicsEngine, IL
pipe.Writer,
TaskScheduler.Default);
string contentType = mode switch
{
StreamingMode.HttpLiveStreamingSegmenterV2 => "video/x-matroska",
_ => "video/mp2t"
};
return new FileStreamResult(pipe.Reader.AsStream(), contentType);
return new FileStreamResult(pipe.Reader.AsStream(), "video/mp2t");
}
// this will never happen

2
ErsatzTV/Pages/ChannelEditor.razor

@ -140,8 +140,6 @@ else @@ -140,8 +140,6 @@ else
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenterFmp4)">HLS Segmenter (fmp4)</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenterV2)">HLS Segmenter V2</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

4
ErsatzTV/Pages/Channels.razor

@ -246,8 +246,6 @@ @@ -246,8 +246,6 @@
uri.Path = uri.Path.Replace("/channels", $"/iptv/channel/{channel.Number}.m3u8");
uri.Query = channel.StreamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenterV2 => "?mode=segmenter-v2",
StreamingMode.HttpLiveStreamingSegmenterFmp4 => "?mode=segmenter-fmp4",
_ => "?mode=segmenter"
};
@ -329,8 +327,6 @@ @@ -329,8 +327,6 @@
{
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
StreamingMode.HttpLiveStreamingSegmenterFmp4 => "HLS Segmenter (fmp4)",
StreamingMode.HttpLiveStreamingSegmenterV2 => "HLS Segmenter V2",
StreamingMode.TransportStreamHybrid => "MPEG-TS",
_ => "MPEG-TS (Legacy)"
};

11
ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

@ -48,15 +48,6 @@ @@ -48,15 +48,6 @@
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Playback Settings</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Streaming Mode</MudText>
</div>
<MudSelect @bind-Value="_streamingMode" For="@(() => _streamingMode)">
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenterFmp4)">HLS Segmenter (fmp4)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>FFmpeg Profile</MudText>
@ -183,7 +174,7 @@ @@ -183,7 +174,7 @@
private readonly List<SubtitleViewModel> _subtitleStreams = [];
private readonly List<GraphicsElementViewModel> _graphicsElements = [];
private MediaItemInfo _info;
private StreamingMode _streamingMode = StreamingMode.HttpLiveStreamingSegmenter;
private readonly StreamingMode _streamingMode = StreamingMode.HttpLiveStreamingSegmenter;
private int _ffmpegProfileId;
private string _streamSelector;
private IEnumerable<string> _watermarkNames = new System.Collections.Generic.HashSet<string>();

Loading…
Cancel
Save