Browse Source

rework mpeg-ts mode (#557)

pull/558/head
Jason Dove 4 years ago committed by GitHub
parent
commit
594ce437fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs
  3. 2
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs
  4. 21
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs
  5. 46
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  6. 5
      ErsatzTV.Core/Domain/StreamingMode.cs
  7. 2
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  8. 7
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  9. 18
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  10. 3
      ErsatzTV.Core/Hdhr/LineupItem.cs
  11. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  12. 4
      ErsatzTV.Core/Iptv/ChannelPlaylist.cs
  13. 3855
      ErsatzTV.Infrastructure/Migrations/20220105234915_Remove_HLSHybridStreamingMode.Designer.cs
  14. 22
      ErsatzTV.Infrastructure/Migrations/20220105234915_Remove_HLSHybridStreamingMode.cs
  15. 2
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  16. 2
      ErsatzTV/Controllers/InternalController.cs
  17. 17
      ErsatzTV/Controllers/IptvController.cs
  18. 6
      ErsatzTV/Pages/ChannelEditor.razor
  19. 4
      ErsatzTV/Pages/Channels.razor

6
CHANGELOG.md

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Changed
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
- This improves compatibility with many clients and also improves performance at program boundaries
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
- This mode will be removed in a future release
## [0.3.5-alpha] - 2022-01-05 ## [0.3.5-alpha] - 2022-01-05
### Fixed ### Fixed

5
ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs

@ -23,7 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries
public async Task<Either<BaseError, PlayoutItemProcessModel>> Handle(T request, CancellationToken cancellationToken) public async Task<Either<BaseError, PlayoutItemProcessModel>> Handle(T request, CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request); Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request);
return await validation.Match( return await validation.Match(
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2), tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2),
@ -56,7 +56,8 @@ namespace ErsatzTV.Application.Streaming.Queries
{ {
"hls-direct" => StreamingMode.HttpLiveStreamingDirect, "hls-direct" => StreamingMode.HttpLiveStreamingDirect,
"segmenter" => StreamingMode.HttpLiveStreamingSegmenter, "segmenter" => StreamingMode.HttpLiveStreamingSegmenter,
"ts" => StreamingMode.TransportStream, "ts" => StreamingMode.TransportStreamHybrid,
"ts-legacy" => StreamingMode.TransportStream,
_ => channel.StreamingMode _ => channel.StreamingMode
}; };

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

@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Streaming.Queries
{ {
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base( public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
channelNumber, channelNumber,
"ts", "ts-legacy",
DateTimeOffset.Now, DateTimeOffset.Now,
false, false,
true) true)

21
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs

@ -0,0 +1,21 @@
using System;
namespace ErsatzTV.Application.Streaming.Queries
{
public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest
{
public GetWrappedProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
channelNumber,
"ts",
DateTimeOffset.Now,
false,
true)
{
Scheme = scheme;
Host = host;
}
public string Scheme { get; }
public string Host { get; }
}
}

46
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -0,0 +1,46 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Streaming.Queries
{
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
{
private readonly IFFmpegProcessService _ffmpegProcessService;
public GetWrappedProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService)
: base(dbContextFactory)
{
_ffmpegProcessService = ffmpegProcessService;
}
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
TvContext dbContext,
GetWrappedProcessByChannelNumber request,
Channel channel,
string ffmpegPath)
{
bool saveReports = await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.WrapSegmenter(
ffmpegPath,
saveReports,
channel,
request.Scheme,
request.Host);
return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue);
}
}
}

5
ErsatzTV.Core/Domain/StreamingMode.cs

@ -4,7 +4,8 @@
{ {
TransportStream = 1, TransportStream = 1,
HttpLiveStreamingDirect = 2, HttpLiveStreamingDirect = 2,
HttpLiveStreamingHybrid = 3, // HttpLiveStreamingHybrid = 3,
HttpLiveStreamingSegmenter = 4 HttpLiveStreamingSegmenter = 4,
TransportStreamHybrid = 5
} }
} }

2
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -71,7 +71,7 @@ namespace ErsatzTV.Core.FFmpeg
result.VideoCodec = "copy"; result.VideoCodec = "copy";
result.Deinterlace = false; result.Deinterlace = false;
break; break;
case StreamingMode.HttpLiveStreamingHybrid: case StreamingMode.TransportStreamHybrid:
case StreamingMode.HttpLiveStreamingSegmenter: case StreamingMode.HttpLiveStreamingSegmenter:
case StreamingMode.TransportStream: case StreamingMode.TransportStream:
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration; result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;

7
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -202,6 +202,13 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegProcessBuilder WithCopyCodec()
{
_arguments.Add("-c");
_arguments.Add("copy");
return this;
}
public FFmpegProcessBuilder WithWatermark( public FFmpegProcessBuilder WithWatermark(
Option<WatermarkOptions> watermarkOptions, Option<WatermarkOptions> watermarkOptions,
IDisplaySize resolution) IDisplaySize resolution)

18
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -247,6 +247,24 @@ namespace ErsatzTV.Core.FFmpeg
.Build(); .Build();
} }
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(1)
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter")
.WithMap("0")
.WithCopyCodec()
.WithMetadata(channel, None)
.WithFormat("mpegts")
.WithPipe()
.Build();
}
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
{ {
return new FFmpegProcessBuilder(ffmpegPath, false, _logger) return new FFmpegProcessBuilder(ffmpegPath, false, _logger)

3
ErsatzTV.Core/Hdhr/LineupItem.cs

@ -22,8 +22,7 @@ namespace ErsatzTV.Core.Hdhr
public string URL => _channel.StreamingMode switch public string URL => _channel.StreamingMode switch
{ {
StreamingMode.HttpLiveStreamingDirect or StreamingMode.HttpLiveStreamingHybrid => StreamingMode.HttpLiveStreamingDirect => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
$"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts" _ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
}; };
} }

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

@ -38,6 +38,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile); Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile); Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);

4
ErsatzTV.Core/Iptv/ChannelPlaylist.cs

@ -43,9 +43,9 @@ namespace ErsatzTV.Core.Iptv
string format = channel.StreamingMode switch string format = channel.StreamingMode switch
{ {
StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct", StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct",
StreamingMode.HttpLiveStreamingHybrid => "m3u8",
StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter", StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter",
_ => "ts" StreamingMode.TransportStreamHybrid => "ts",
_ => "ts?mode=legacy"
}; };
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head(); string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();

3855
ErsatzTV.Infrastructure/Migrations/20220105234915_Remove_HLSHybridStreamingMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

22
ErsatzTV.Infrastructure/Migrations/20220105234915_Remove_HLSHybridStreamingMode.cs

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Remove_HLSHybridStreamingMode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// replace HLS Hybrid with HLS Segmenter
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 4 WHERE StreamingMode = 3");
// replace MPEG-TS (Legacy) with new MPEG-TS
migrationBuilder.Sql("UPDATE Channel SET StreamingMode = 5 WHERE StreamingMode = 1");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

2
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

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

2
ErsatzTV/Controllers/InternalController.cs

@ -27,7 +27,7 @@ namespace ErsatzTV.Controllers
public Task<IActionResult> GetConcatPlaylist(string channelNumber) => public Task<IActionResult> GetConcatPlaylist(string channelNumber) =>
_mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) _mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
.ToActionResult(); .ToActionResult();
[HttpGet("ffmpeg/stream/{channelNumber}")] [HttpGet("ffmpeg/stream/{channelNumber}")]
public Task<IActionResult> GetStream( public Task<IActionResult> GetStream(
string channelNumber, string channelNumber,

17
ErsatzTV/Controllers/IptvController.cs

@ -52,14 +52,24 @@ namespace ErsatzTV.Controllers
.Map<ChannelGuide, IActionResult>(Ok); .Map<ChannelGuide, IActionResult>(Ok);
[HttpGet("iptv/channel/{channelNumber}.ts")] [HttpGet("iptv/channel/{channelNumber}.ts")]
public Task<IActionResult> GetTransportStreamVideo(string channelNumber) => public async Task<IActionResult> GetTransportStreamVideo(
_mediator.Send(new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) string channelNumber,
[FromQuery]
string mode = null)
{
FFmpegProcessRequest request = mode switch
{
"legacy" => new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber),
_ => new GetWrappedProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)
};
return await _mediator.Send(request)
.Map( .Map(
result => result.Match<IActionResult>( result => result.Match<IActionResult>(
processModel => processModel =>
{ {
Process process = processModel.Process; Process process = processModel.Process;
_logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber); _logger.LogInformation("Starting ts stream for channel {ChannelNumber}", channelNumber);
// _logger.LogDebug( // _logger.LogDebug(
// "ffmpeg concat arguments {FFmpegArguments}", // "ffmpeg concat arguments {FFmpegArguments}",
@ -68,6 +78,7 @@ namespace ErsatzTV.Controllers
return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t"); return new FileStreamResult(process.StandardOutput.BaseStream, "video/mp2t");
}, },
error => BadRequest(error.Value))); error => BadRequest(error.Value)));
}
[HttpGet("iptv/session/{channelNumber}/hls.m3u8")] [HttpGet("iptv/session/{channelNumber}/hls.m3u8")]
public async Task<IActionResult> GetLivePlaylist(string channelNumber) public async Task<IActionResult> GetLivePlaylist(string channelNumber)

6
ErsatzTV/Pages/ChannelEditor.razor

@ -32,9 +32,9 @@
<MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/> <MudTextField Label="Number" @bind-Value="_model.Number" For="@(() => _model.Number)" Immediate="true"/>
<MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/> <MudTextField Class="mt-3" Label="Name" @bind-Value="_model.Name" For="@(() => _model.Name)"/>
<MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)"> <MudSelect Class="mt-3" Label="Streaming Mode" @bind-Value="_model.StreamingMode" For="@(() => _model.StreamingMode)">
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS</MudSelectItem> <MudSelectItem Value="@(StreamingMode.TransportStreamHybrid)">MPEG-TS</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.TransportStream)">MPEG-TS (Legacy)</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem> <MudSelectItem Value="@(StreamingMode.HttpLiveStreamingDirect)">HLS Direct</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingHybrid)">HLS Hybrid</MudSelectItem>
<MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem> <MudSelectItem Value="@(StreamingMode.HttpLiveStreamingSegmenter)">HLS Segmenter</MudSelectItem>
</MudSelect> </MudSelect>
<MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)" <MudSelect Class="mt-3" Label="FFmpeg Profile" @bind-Value="_model.FFmpegProfileId" For="@(() => _model.FFmpegProfileId)"
@ -154,7 +154,7 @@
_model.Number = (maxNumber + 1).ToString(); _model.Number = (maxNumber + 1).ToString();
_model.Name = "New Channel"; _model.Name = "New Channel";
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId; _model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingMode = StreamingMode.TransportStream; _model.StreamingMode = StreamingMode.TransportStreamHybrid;
} }
} }

4
ErsatzTV/Pages/Channels.razor

@ -140,9 +140,9 @@
private static string GetStreamingMode(StreamingMode streamingMode) => streamingMode switch { private static string GetStreamingMode(StreamingMode streamingMode) => streamingMode switch {
StreamingMode.HttpLiveStreamingDirect => "HLS Direct", StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingHybrid => "HLS Hybrid",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter", StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
_ => "MPEG-TS" StreamingMode.TransportStreamHybrid => "MPEG-TS",
_ => "MPEG-TS (Legacy)"
}; };
} }
Loading…
Cancel
Save