Browse Source

fix segmenter timestamp continuity (#1610)

pull/1611/head
Jason Dove 2 years ago committed by GitHub
parent
commit
1f6e843a26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Channels/Mapper.cs
  3. 3
      ErsatzTV.Application/Channels/Queries/GetChannelResolution.cs
  4. 25
      ErsatzTV.Application/Channels/Queries/GetChannelResolutionHandler.cs
  5. 3
      ErsatzTV.Application/Channels/ResolutionViewModel.cs
  6. 3
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  7. 30
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  8. 1
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  9. 21
      ErsatzTV/Controllers/IptvController.cs

2
CHANGELOG.md

@ -34,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -34,6 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Automatically generate JWT tokens to allow channel previews of protected streams
- Fix bug applying music video fallback metadata
- Fix playback of media items with no audio streams
- Fix timestamp continuity in `HLS Segmenter` sessions
- This should make *some* clients happier
### Changed
- Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date

3
ErsatzTV.Application/Channels/Mapper.cs

@ -34,6 +34,9 @@ internal static class Mapper @@ -34,6 +34,9 @@ internal static class Mapper
channel.PreferredAudioLanguageCode,
GetStreamingMode(channel));
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
new(resolution.Height, resolution.Width);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);

3
ErsatzTV.Application/Channels/Queries/GetChannelResolution.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelResolution(string ChannelNumber) : IRequest<Option<ResolutionViewModel>>;

25
ErsatzTV.Application/Channels/Queries/GetChannelResolutionHandler.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetChannelResolutionHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetChannelResolution, Option<ResolutionViewModel>>
{
public async Task<Option<ResolutionViewModel>> Handle(
GetChannelResolution request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Channel> maybeChannel = await dbContext.Channels
.AsNoTracking()
.Include(c => c.FFmpegProfile)
.ThenInclude(ff => ff.Resolution)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber);
return maybeChannel.Map(c => Mapper.ProjectToViewModel(c.FFmpegProfile.Resolution));
}
}

3
ErsatzTV.Application/Channels/ResolutionViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record ResolutionViewModel(int Height, int Width);

3
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -181,7 +181,6 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -181,7 +181,6 @@ public class HlsSessionWorker : IHlsSessionWorker
_logger.LogError("Transcode folder is NOT empty!");
}
Touch();
_transcodedUntil = DateTimeOffset.Now;
PlaylistStart = _transcodedUntil;
@ -577,7 +576,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -577,7 +576,7 @@ public class HlsSessionWorker : IHlsSessionWorker
foreach ((long pts, long duration) in queryResult.RightToSeq())
{
result = pts + duration;
result = pts + duration + 1;
}
return result;

30
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -8,6 +8,7 @@ public class OutputFormatHls : IPipelineStep @@ -8,6 +8,7 @@ public class OutputFormatHls : IPipelineStep
private readonly Option<string> _mediaFrameRate;
private readonly bool _oneSecondGop;
private readonly string _playlistPath;
private readonly bool _isFirstTranscode;
private readonly string _segmentTemplate;
public OutputFormatHls(
@ -15,12 +16,14 @@ public class OutputFormatHls : IPipelineStep @@ -15,12 +16,14 @@ public class OutputFormatHls : IPipelineStep
Option<string> mediaFrameRate,
string segmentTemplate,
string playlistPath,
bool oneSecondGop = false)
bool isFirstTranscode,
bool oneSecondGop)
{
_desiredState = desiredState;
_mediaFrameRate = mediaFrameRate;
_segmentTemplate = segmentTemplate;
_playlistPath = playlistPath;
_isFirstTranscode = isFirstTranscode;
_oneSecondGop = oneSecondGop;
}
@ -38,8 +41,8 @@ public class OutputFormatHls : IPipelineStep @@ -38,8 +41,8 @@ public class OutputFormatHls : IPipelineStep
int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS;
return new[]
{
List<string> result =
[
"-g", $"{gop}",
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
@ -48,11 +51,28 @@ public class OutputFormatHls : IPipelineStep @@ -48,11 +51,28 @@ public class OutputFormatHls : IPipelineStep
"-hls_list_size", "0",
"-segment_list_flags", "+live",
"-hls_segment_filename",
_segmentTemplate,
_segmentTemplate
];
if (_isFirstTranscode)
{
result.AddRange(
[
"-hls_flags", "program_date_time+append_list+omit_endlist+independent_segments",
_playlistPath
]);
}
else
{
result.AddRange(
[
"-hls_flags", "program_date_time+append_list+discont_start+omit_endlist+independent_segments",
"-mpegts_flags", "+initial_discontinuity",
_playlistPath
};
]);
}
return result.ToArray();
}
}

1
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -275,6 +275,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -275,6 +275,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
videoStream.FrameRate,
segmentTemplate,
playlistPath,
isFirstTranscode: ffmpegState.PtsOffset == 0,
ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv));
}
}

21
ErsatzTV/Controllers/IptvController.cs

@ -191,6 +191,7 @@ public class IptvController : ControllerBase @@ -191,6 +191,7 @@ public class IptvController : ControllerBase
switch (mode)
{
case "segmenter":
string multiVariantPlaylist = await GetMultiVariantPlaylist(channelNumber);
_logger.LogDebug("Maybe starting ffmpeg session for channel {Channel}", channelNumber);
Either<BaseError, Unit> result = await _mediator.Send(new StartFFmpegSession(channelNumber, false));
return result.Match<IActionResult>(
@ -199,7 +200,7 @@ public class IptvController : ControllerBase @@ -199,7 +200,7 @@ public class IptvController : ControllerBase
_logger.LogDebug(
"Session started; returning multi-variant playlist for channel {Channel}",
channelNumber);
return Content(GetMultiVariantPlaylist(channelNumber), "application/vnd.apple.mpegurl");
return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl");
// return Redirect($"~/iptv/session/{channelNumber}/hls.m3u8");
},
error =>
@ -210,7 +211,7 @@ public class IptvController : ControllerBase @@ -210,7 +211,7 @@ public class IptvController : ControllerBase
_logger.LogDebug(
"Session is already active; returning multi-variant playlist for channel {Channel}",
channelNumber);
return Content(GetMultiVariantPlaylist(channelNumber), "application/vnd.apple.mpegurl");
return Content(multiVariantPlaylist, "application/vnd.apple.mpegurl");
// return RedirectPreserveMethod($"iptv/session/{channelNumber}/hls.m3u8");
default:
_logger.LogWarning(
@ -246,12 +247,20 @@ public class IptvController : ControllerBase @@ -246,12 +247,20 @@ public class IptvController : ControllerBase
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
// TODO: include resolution here?
private string GetMultiVariantPlaylist(string channelNumber) =>
$@"#EXTM3U
private async Task<string> GetMultiVariantPlaylist(string channelNumber)
{
Option<ResolutionViewModel> maybeResolution = await _mediator.Send(new GetChannelResolution(channelNumber));
string resolution = string.Empty;
foreach (ResolutionViewModel res in maybeResolution)
{
resolution = $",RESOLUTION={res.Width}x{res.Height}";
}
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10000000
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10000000{resolution}
{Request.Scheme}://{Request.Host}/iptv/session/{channelNumber}/hls.m3u8{AccessTokenQuery()}";
}
private string AccessTokenQuery() => string.IsNullOrWhiteSpace(Request.Query["access_token"])
? string.Empty

Loading…
Cancel
Save