|
|
|
@ -6,24 +6,15 @@ using Microsoft.Extensions.Logging; |
|
|
|
|
|
|
|
|
|
|
|
namespace ErsatzTV.Core.FFmpeg; |
|
|
|
namespace ErsatzTV.Core.FFmpeg; |
|
|
|
|
|
|
|
|
|
|
|
public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
public class HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger<HlsPlaylistFilter> logger) : IHlsPlaylistFilter |
|
|
|
{ |
|
|
|
{ |
|
|
|
private readonly ILogger<HlsPlaylistFilter> _logger; |
|
|
|
|
|
|
|
private readonly ITempFilePool _tempFilePool; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger<HlsPlaylistFilter> logger) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
_tempFilePool = tempFilePool; |
|
|
|
|
|
|
|
_logger = logger; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public TrimPlaylistResult TrimPlaylist( |
|
|
|
public TrimPlaylistResult TrimPlaylist( |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
DateTimeOffset playlistStart, |
|
|
|
DateTimeOffset playlistStart, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
List<long> inits, |
|
|
|
IHlsInitSegmentCache hlsInitSegmentCache, |
|
|
|
string[] lines, |
|
|
|
string[] lines, |
|
|
|
int maxSegments = 10, |
|
|
|
Option<int> maybeMaxSegments, |
|
|
|
bool endWithDiscontinuity = false) |
|
|
|
bool endWithDiscontinuity = false) |
|
|
|
{ |
|
|
|
{ |
|
|
|
try |
|
|
|
try |
|
|
|
@ -107,11 +98,11 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
GeneratePlaylist( |
|
|
|
GeneratePlaylist( |
|
|
|
outputFormat, |
|
|
|
outputFormat, |
|
|
|
items, |
|
|
|
items, |
|
|
|
inits, |
|
|
|
hlsInitSegmentCache, |
|
|
|
filterBefore, |
|
|
|
filterBefore, |
|
|
|
targetDuration, |
|
|
|
targetDuration, |
|
|
|
discontinuitySequence, |
|
|
|
discontinuitySequence, |
|
|
|
maxSegments); |
|
|
|
maybeMaxSegments); |
|
|
|
|
|
|
|
|
|
|
|
return new TrimPlaylistResult(nextPlaylistStart, startSequence, generatedAt, playlist, segments); |
|
|
|
return new TrimPlaylistResult(nextPlaylistStart, startSequence, generatedAt, playlist, segments); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -119,10 +110,10 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
{ |
|
|
|
{ |
|
|
|
try |
|
|
|
try |
|
|
|
{ |
|
|
|
{ |
|
|
|
string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist); |
|
|
|
string file = tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist); |
|
|
|
File.WriteAllLines(file, lines); |
|
|
|
File.WriteAllLines(file, lines); |
|
|
|
|
|
|
|
|
|
|
|
_logger.LogError(ex, "Error filtering playlist. Bad playlist saved to {BadPlaylistFile}", file); |
|
|
|
logger.LogError(ex, "Error filtering playlist. Bad playlist saved to {BadPlaylistFile}", file); |
|
|
|
|
|
|
|
|
|
|
|
// TODO: better error result?
|
|
|
|
// TODO: better error result?
|
|
|
|
return new TrimPlaylistResult(playlistStart, 0, 0, string.Empty, 0); |
|
|
|
return new TrimPlaylistResult(playlistStart, 0, 0, string.Empty, 0); |
|
|
|
@ -140,18 +131,18 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
DateTimeOffset playlistStart, |
|
|
|
DateTimeOffset playlistStart, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
List<long> inits, |
|
|
|
IHlsInitSegmentCache hlsInitSegmentCache, |
|
|
|
string[] lines) => |
|
|
|
string[] lines) => |
|
|
|
TrimPlaylist(outputFormat, playlistStart, filterBefore, inits, lines, int.MaxValue, true); |
|
|
|
TrimPlaylist(outputFormat, playlistStart, filterBefore, hlsInitSegmentCache, lines, Option<int>.None, true); |
|
|
|
|
|
|
|
|
|
|
|
private static Tuple<string, DateTimeOffset, long, long, int> GeneratePlaylist( |
|
|
|
private static Tuple<string, DateTimeOffset, long, long, int> GeneratePlaylist( |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
OutputFormatKind outputFormat, |
|
|
|
List<PlaylistItem> items, |
|
|
|
List<PlaylistItem> items, |
|
|
|
List<long> inits, |
|
|
|
IHlsInitSegmentCache hlsInitSegmentCache, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
DateTimeOffset filterBefore, |
|
|
|
int targetDuration, |
|
|
|
int targetDuration, |
|
|
|
int discontinuitySequence, |
|
|
|
int discontinuitySequence, |
|
|
|
int maxSegments) |
|
|
|
Option<int> maybeMaxSegments) |
|
|
|
{ |
|
|
|
{ |
|
|
|
if (items.Count != 0 && items[0] is PlaylistDiscontinuity) |
|
|
|
if (items.Count != 0 && items[0] is PlaylistDiscontinuity) |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -164,16 +155,26 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var allSegments = items.OfType<PlaylistSegment>().ToList(); |
|
|
|
var allSegments = items.OfType<PlaylistSegment>().ToList(); |
|
|
|
// only filter if we have more than requested
|
|
|
|
|
|
|
|
if (allSegments.Count > maxSegments) |
|
|
|
// still need to filter old content
|
|
|
|
|
|
|
|
if (maybeMaxSegments.IsNone) |
|
|
|
{ |
|
|
|
{ |
|
|
|
var afterFilter = allSegments.Filter(s => s.StartTime >= filterBefore).ToList(); |
|
|
|
allSegments.RemoveAll(s => s.StartTime < filterBefore); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// if there are enough new segments after filtering, use those
|
|
|
|
foreach (int maxSegments in maybeMaxSegments) |
|
|
|
// otherwise return the last maxSegments
|
|
|
|
{ |
|
|
|
allSegments = afterFilter.Count >= maxSegments |
|
|
|
// only filter if we have more than requested
|
|
|
|
? afterFilter.Take(maxSegments).ToList() |
|
|
|
if (allSegments.Count > maxSegments) |
|
|
|
: allSegments.TakeLast(maxSegments).ToList(); |
|
|
|
{ |
|
|
|
|
|
|
|
var afterFilter = allSegments.Filter(s => s.StartTime >= filterBefore).ToList(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// if there are enough new segments after filtering, use those
|
|
|
|
|
|
|
|
// otherwise return the last maxSegments
|
|
|
|
|
|
|
|
allSegments = afterFilter.Count >= maxSegments |
|
|
|
|
|
|
|
? afterFilter.Take(maxSegments).ToList() |
|
|
|
|
|
|
|
: allSegments.TakeLast(maxSegments).ToList(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
long startSequence = 0; |
|
|
|
long startSequence = 0; |
|
|
|
@ -184,12 +185,6 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
generatedAt = startSegment.GeneratedAt; |
|
|
|
generatedAt = startSegment.GeneratedAt; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
long minGeneratedAt = 0; |
|
|
|
|
|
|
|
foreach (var firstSegment in allSegments.HeadOrNone()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
minGeneratedAt = firstSegment.GeneratedAt; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// count all discontinuities that were filtered out
|
|
|
|
// count all discontinuities that were filtered out
|
|
|
|
if (allSegments.Count != 0) |
|
|
|
if (allSegments.Count != 0) |
|
|
|
{ |
|
|
|
{ |
|
|
|
@ -208,12 +203,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
|
|
|
|
|
|
|
|
if (outputFormat is OutputFormatKind.HlsMp4) |
|
|
|
if (outputFormat is OutputFormatKind.HlsMp4) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Option<long> maybeStartInit = Optional(inits.Find(init => init > 0 && init <= minGeneratedAt)); |
|
|
|
output.AppendLine( |
|
|
|
foreach (long init in maybeStartInit) |
|
|
|
CultureInfo.InvariantCulture, |
|
|
|
{ |
|
|
|
$"#EXT-X-MAP:URI=\"{hlsInitSegmentCache.EarliestSegmentByHash(generatedAt)}\""); |
|
|
|
output.AppendLine(CultureInfo.InvariantCulture, $"#EXT-X-MAP:URI=\"{init}_init.mp4\""); |
|
|
|
|
|
|
|
inits.Remove(init); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < items.Count; i++) |
|
|
|
for (var i = 0; i < items.Count; i++) |
|
|
|
@ -229,15 +221,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
|
|
|
|
|
|
|
|
if (outputFormat is OutputFormatKind.HlsMp4) |
|
|
|
if (outputFormat is OutputFormatKind.HlsMp4) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Option<long> maybeInit = Optional( |
|
|
|
output.AppendLine( |
|
|
|
inits.Find(init => init > 0 && init <= nextSegment.GeneratedAt)); |
|
|
|
CultureInfo.InvariantCulture, |
|
|
|
foreach (long init in maybeInit) |
|
|
|
$"#EXT-X-MAP:URI=\"{hlsInitSegmentCache.EarliestSegmentByHash(nextSegment.GeneratedAt)}\""); |
|
|
|
{ |
|
|
|
|
|
|
|
output.AppendLine( |
|
|
|
|
|
|
|
CultureInfo.InvariantCulture, |
|
|
|
|
|
|
|
$"#EXT-X-MAP:URI=\"{init}_init.mp4\""); |
|
|
|
|
|
|
|
inits.Remove(init); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -269,7 +255,12 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter |
|
|
|
.Map(s => s.StartTime) |
|
|
|
.Map(s => s.StartTime) |
|
|
|
.IfNone(DateTimeOffset.MaxValue); |
|
|
|
.IfNone(DateTimeOffset.MaxValue); |
|
|
|
|
|
|
|
|
|
|
|
return Tuple(playlist, nextPlaylistStart, startSequence, generatedAt, allSegments.Count); |
|
|
|
return Tuple( |
|
|
|
|
|
|
|
playlist, |
|
|
|
|
|
|
|
nextPlaylistStart, |
|
|
|
|
|
|
|
startSequence, |
|
|
|
|
|
|
|
items.OfType<PlaylistSegment>().Min(s => s.GeneratedAt), |
|
|
|
|
|
|
|
allSegments.Count); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private abstract record PlaylistItem; |
|
|
|
private abstract record PlaylistItem; |
|
|
|
|