Browse Source

add hls segmenter initial segment count (#616)

pull/617/head
Jason Dove 4 years ago committed by GitHub
parent
commit
534e2c4512
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  3. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs
  4. 3
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs
  5. 25
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  6. 35
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  7. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  8. 9
      ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs
  9. 5
      ErsatzTV/Controllers/IptvController.cs
  10. 10
      ErsatzTV/Pages/Settings.razor

1
CHANGELOG.md

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Include `Series` category tag for all episodes in XMLTV
- Include movie, episode (show), music video (artist) genres as `category` tags in XMLTV
- Add framerate normalization to `HLS Segmenter` and `MPEG-TS` streaming modes
- Add `HLS Segmenter Initial Segment Count` setting to allow segmenter to work ahead before allowing client playback
### Changed
- Intermittent watermarks will now fade in and out

4
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -119,6 +119,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -119,6 +119,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
ConfigElementKey.FFmpegWorkAheadSegmenters,
request.Settings.WorkAheadSegmenterLimit);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegInitialSegmentCount,
request.Settings.InitialSegmentCount);
return Unit.Default;
}
}

1
ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs

@ -11,5 +11,6 @@ @@ -11,5 +11,6 @@
public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
}
}

3
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs

@ -34,6 +34,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -34,6 +34,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
var result = new FFmpegSettingsViewModel
{
@ -44,6 +46,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -44,6 +46,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(4)
};
foreach (int watermarkId in watermark)

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

@ -5,6 +5,7 @@ using System.Threading.Tasks; @@ -5,6 +5,7 @@ using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
@ -50,7 +51,7 @@ namespace ErsatzTV.Application.Streaming.Commands @@ -50,7 +51,7 @@ namespace ErsatzTV.Application.Streaming.Commands
TimeSpan idleTimeout = await _configElementRepository
.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout)
.Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1)));
using IServiceScope scope = _serviceScopeFactory.CreateScope();
HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService<HlsSessionWorker>();
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
@ -68,12 +69,32 @@ namespace ErsatzTV.Application.Streaming.Commands @@ -68,12 +69,32 @@ namespace ErsatzTV.Application.Streaming.Commands
request.ChannelNumber,
"live.m3u8");
IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
int initialSegmentCount = await repo.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount)
.Map(maybeCount => maybeCount.Match(identity, () => 4));
await WaitForPlaylistSegments(playlistFileName, initialSegmentCount, worker);
return Unit.Default;
}
private static async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker)
{
while (!File.Exists(playlistFileName))
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
}
return Unit.Default;
var segmentCount = 0;
while (segmentCount < initialSegmentCount)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30);
string[] input = await File.ReadAllLinesAsync(playlistFileName);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input);
segmentCount = result.SegmentCount;
}
}
private Task<Validation<BaseError, Unit>> Validate(StartFFmpegSession request) =>

35
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -231,16 +231,31 @@ namespace ErsatzTV.Application.Streaming @@ -231,16 +231,31 @@ namespace ErsatzTV.Application.Streaming
await File.WriteAllTextAsync(playlistFileName, trimResult.Playlist, cancellationToken);
// delete old segments
foreach (string file in Directory.GetFiles(
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber),
"*.ts"))
var allSegments = Directory.GetFiles(
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber),
"live*.ts")
.Map(
file =>
{
string fileName = Path.GetFileName(file);
var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]);
return new Segment(file, sequenceNumber);
})
.ToList();
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
// if (toDelete.Count > 0)
// {
// _logger.LogInformation(
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
// toDelete.Map(s => s.SequenceNumber).Min(),
// toDelete.Map(s => s.SequenceNumber).Max(),
// trimResult.Sequence);
// }
foreach (Segment segment in toDelete)
{
string fileName = Path.GetFileName(file);
if (fileName.StartsWith("live") && int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]) <
trimResult.Sequence)
{
File.Delete(file);
}
File.Delete(segment.File);
}
_playlistStart = trimResult.PlaylistStart;
@ -276,5 +291,7 @@ namespace ErsatzTV.Application.Streaming @@ -276,5 +291,7 @@ namespace ErsatzTV.Application.Streaming
return await repo.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
.Map(maybeCount => maybeCount.Match(identity, () => 1));
}
private record Segment(string File, int SequenceNumber);
}
}

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id");
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count");
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");

9
ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs

@ -95,12 +95,13 @@ namespace ErsatzTV.Core.FFmpeg @@ -95,12 +95,13 @@ namespace ErsatzTV.Core.FFmpeg
i += 3;
}
if (endWithDiscontinuity)
var playlist = output.ToString();
if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}"))
{
output.AppendLine("#EXT-X-DISCONTINUITY");
playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine;
}
return new TrimPlaylistResult(nextPlaylistStart, startSequence, output.ToString());
return new TrimPlaylistResult(nextPlaylistStart, startSequence, playlist, segments);
}
public static TrimPlaylistResult TrimPlaylistWithDiscontinuity(
@ -112,5 +113,5 @@ namespace ErsatzTV.Core.FFmpeg @@ -112,5 +113,5 @@ namespace ErsatzTV.Core.FFmpeg
}
}
public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist);
public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist, int SegmentCount);
}

5
ErsatzTV/Controllers/IptvController.cs

@ -92,6 +92,11 @@ namespace ErsatzTV.Controllers @@ -92,6 +92,11 @@ namespace ErsatzTV.Controllers
{
string[] input = await System.IO.File.ReadAllLinesAsync(fileName);
// _logger.LogInformation(
// "Trimming playlist: {PlaylistStart} {FilterBefore}",
// worker.PlaylistStart,
// now);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input);
return Content(result.Playlist, "application/vnd.apple.mpegurl");
}

10
ErsatzTV/Pages/Settings.razor

@ -94,6 +94,14 @@ @@ -94,6 +94,14 @@
Required="true"
RequiredError="Work-ahead HLS Segmenter limit is required!"/>
</MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField T="int"
Label="HLS Segmenter Initial Segment Count"
@bind-Value="_ffmpegSettings.InitialSegmentCount"
Validation="@(new Func<int, string>(ValidateInitialSegmentCount))"
Required="true"
RequiredError="HLS Segmenter initial segment count is required!"/>
</MudElement>
</MudForm>
</MudCardContent>
<MudCardActions>
@ -206,6 +214,8 @@ @@ -206,6 +214,8 @@
private static string ValidateWorkAheadSegmenterLimit(int limit) => limit < 0 ? "Work-Ahead HLS Segmenter limit must be greater than or equal to 0" : null;
private static string ValidateInitialSegmentCount(int count) => count < 1 ? "HLS Segmenter initial segment count must be greater than or equal to 1" : null;
private async Task LoadFFmpegProfilesAsync() =>
_ffmpegProfiles = await _mediator.Send(new GetAllFFmpegProfiles());

Loading…
Cancel
Save