Browse Source

fix management of fmp4 init segments (#2546)

pull/2548/head
Jason Dove 10 months ago committed by GitHub
parent
commit
889904e70d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  3. 83
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  4. 23
      ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs
  5. 2
      ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs
  6. 48
      ErsatzTV.Core/FFmpeg/HlsInitSegmentCache.cs
  7. 91
      ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs
  8. 12
      ErsatzTV.Core/FFmpeg/IHlsInitSegmentCache.cs
  9. 9
      ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs
  10. 1
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  11. 12
      ErsatzTV.Core/Metadata/LocalFileSystem.cs
  12. 1
      ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs
  13. 2
      ErsatzTV/Startup.cs

1
CHANGELOG.md

@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix Trakt List sync
- Fix QSV audio sync
- Fix QSV capability detection on Linux using non-drm displays (e.g. wayland)
- Fix playlist filtering bug that made HLS Segmenter more likely to fail when streaming for multiple hours
### Changed
- Do not use graphics engine for single, permanent watermark

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

@ -25,6 +25,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -25,6 +25,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IGraphicsEngine _graphicsEngine;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IHlsInitSegmentCache _hlsInitSegmentCache;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<StartFFmpegSessionHandler> _logger;
@ -35,6 +36,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -35,6 +36,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
public StartFFmpegSessionHandler(
IHlsPlaylistFilter hlsPlaylistFilter,
IHlsInitSegmentCache hlsInitSegmentCache,
IServiceScopeFactory serviceScopeFactory,
IMediator mediator,
IClient client,
@ -48,6 +50,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -48,6 +50,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_hlsPlaylistFilter = hlsPlaylistFilter;
_hlsInitSegmentCache = hlsInitSegmentCache;
_serviceScopeFactory = serviceScopeFactory;
_mediator = mediator;
_client = client;
@ -124,6 +127,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -124,6 +127,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_graphicsEngine,
_client,
_hlsPlaylistFilter,
_hlsInitSegmentCache,
_configElementRepository,
_localFileSystem,
_sessionWorkerLogger,

83
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Pipelines;
@ -27,6 +26,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -27,6 +26,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
private static int _workAheadCount;
private readonly IClient _client;
private readonly IHlsInitSegmentCache _hlsInitSegmentCache;
private readonly IConfigElementRepository _configElementRepository;
private readonly OutputFormatKind _outputFormat;
private readonly IGraphicsEngine _graphicsEngine;
@ -49,7 +49,6 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -49,7 +49,6 @@ public class HlsSessionWorker : IHlsSessionWorker
private Timer _timer;
private DateTimeOffset _transcodedUntil;
private string _workingDirectory;
private ConcurrentDictionary<string, DateTimeOffset> _initTouches = new();
public HlsSessionWorker(
IServiceScopeFactory serviceScopeFactory,
@ -57,6 +56,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -57,6 +56,7 @@ public class HlsSessionWorker : IHlsSessionWorker
IGraphicsEngine graphicsEngine,
IClient client,
IHlsPlaylistFilter hlsPlaylistFilter,
IHlsInitSegmentCache hlsInitSegmentCache,
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
ILogger<HlsSessionWorker> logger,
@ -67,6 +67,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -67,6 +67,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_outputFormat = outputFormat;
_graphicsEngine = graphicsEngine;
_client = client;
_hlsInitSegmentCache = hlsInitSegmentCache;
_hlsPlaylistFilter = hlsPlaylistFilter;
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
@ -99,12 +100,6 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -99,12 +100,6 @@ public class HlsSessionWorker : IHlsSessionWorker
_lastAccess = DateTimeOffset.Now;
foreach (string init in fileName.Where(f => f.Contains("init.mp4")))
{
//_logger.LogDebug("Keep alive init {Init} for channel {ChannelNumber}", init, _channelNumber);
_initTouches.AddOrUpdate(init, DateTimeOffset.Now, (_, _) => DateTimeOffset.Now);
}
_timer?.Stop();
_timer?.Start();
}
@ -123,12 +118,15 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -123,12 +118,15 @@ public class HlsSessionWorker : IHlsSessionWorker
Option<string[]> maybeLines = await ReadPlaylistLines(cancellationToken);
foreach (string[] input in maybeLines)
{
await RefreshInits();
TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylist(
_outputFormat,
PlaylistStart,
filterBefore,
GetAllInits(),
input);
_hlsInitSegmentCache,
input,
maybeMaxSegments: 10);
if (DateTimeOffset.Now > _lastDelete.AddSeconds(30))
{
DeleteOldSegments(trimResult);
@ -271,7 +269,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -271,7 +269,7 @@ public class HlsSessionWorker : IHlsSessionWorker
try
{
_localFileSystem.EmptyFolder(_workingDirectory);
//_localFileSystem.EmptyFolder(_workingDirectory);
}
catch
{
@ -647,12 +645,14 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -647,12 +645,14 @@ public class HlsSessionWorker : IHlsSessionWorker
Option<string[]> maybeLines = await ReadPlaylistLines(cancellationToken);
foreach (string[] lines in maybeLines)
{
await RefreshInits();
// trim playlist and insert discontinuity before appending with new ffmpeg process
TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity(
_outputFormat,
PlaylistStart,
DateTimeOffset.Now.AddMinutes(-1),
GetAllInits(),
_hlsInitSegmentCache,
lines);
await WritePlaylist(trimResult.Playlist, cancellationToken);
@ -693,14 +693,9 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -693,14 +693,9 @@ public class HlsSessionWorker : IHlsSessionWorker
.ToList();
var allInits = Directory.GetFiles(_workingDirectory, "*init.mp4")
.Map(file =>
{
string fileName = Path.GetFileName(file);
// only consider deleting inits that have no segments left on disk
return long.TryParse(fileName.Split('_')[0], out long generatedAt) && !generatedAtHash.Contains(generatedAt)
? new Segment(file, 0, generatedAt)
: Option<Segment>.None;
})
.Map(file => long.TryParse(Path.GetFileName(file).Split('_')[0], out long generatedAt) && !generatedAtHash.Contains(generatedAt)
? new Segment(file, 0, generatedAt)
: Option<Segment>.None)
.Somes()
.ToList();
@ -714,21 +709,22 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -714,21 +709,22 @@ public class HlsSessionWorker : IHlsSessionWorker
// trimResult.Sequence);
}
DateTimeOffset toKeep = DateTimeOffset.Now.AddMinutes(-10);
foreach (var init in allInits)
{
string fileName = Path.GetFileName(init.File) ?? string.Empty;
if (!_initTouches.TryGetValue(fileName, out DateTimeOffset touchedAt))
// only consider deleting inits that have no segments left on disk, no segments in ffmpeg playlist
if (generatedAtHash.Contains(init.GeneratedAt) || init.GeneratedAt >= trimResult.GeneratedAt)
{
continue;
}
if (touchedAt < toKeep)
string fileName = Path.GetFileName(init.File);
if (_hlsInitSegmentCache.IsEarliestByHash(fileName))
{
toDelete.Add(init);
_initTouches.TryRemove(fileName, out _);
continue;
}
toDelete.Add(init);
_hlsInitSegmentCache.DeleteSegment(fileName);
}
foreach (Segment segment in toDelete)
@ -746,16 +742,27 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -746,16 +742,27 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
private List<long> GetAllInits() =>
_outputFormat is OutputFormatKind.HlsMp4
? Directory.GetFiles(_workingDirectory, "*init.mp4")
.Map(file =>
{
string fileName = Path.GetFileName(file);
return long.TryParse(fileName.Split('_')[0], out long generatedAt) ? generatedAt : long.MaxValue;
})
.ToList()
: [];
private async Task RefreshInits()
{
if (_outputFormat is not OutputFormatKind.HlsMp4)
{
return;
}
var allSegments = Directory.GetFiles(_workingDirectory, "live*.m4s")
.Map(Path.GetFileName)
.Map(s => s.Split("_")[1])
.ToHashSet();
foreach (string file in Directory.GetFiles(_workingDirectory, "*init.mp4"))
{
string key = Path.GetFileName(file).Split("_")[0];
if (allSegments.Contains(key))
{
await _hlsInitSegmentCache.AddSegment(file);
}
}
}
private async Task<long> GetPtsOffset(string channelNumber, CancellationToken cancellationToken)
{

23
ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs

@ -45,8 +45,9 @@ live001139.ts").Split(Environment.NewLine); @@ -45,8 +45,9 @@ live001139.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(-30),
[],
input);
Substitute.For<IHlsInitSegmentCache>(),
input,
maybeMaxSegments: 10);
result.PlaylistStart.ShouldBe(start);
result.Sequence.ShouldBe(1137);
@ -95,7 +96,7 @@ live001139.ts").Split(Environment.NewLine); @@ -95,7 +96,7 @@ live001139.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(-30),
[],
Substitute.For<IHlsInitSegmentCache>(),
input,
2);
@ -143,7 +144,7 @@ live001139.ts").Split(Environment.NewLine); @@ -143,7 +144,7 @@ live001139.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(-30),
[],
Substitute.For<IHlsInitSegmentCache>(),
input,
int.MaxValue,
true);
@ -196,7 +197,7 @@ live001139.ts").Split(Environment.NewLine); @@ -196,7 +197,7 @@ live001139.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(6),
[],
Substitute.For<IHlsInitSegmentCache>(),
input,
1);
@ -242,8 +243,9 @@ live001139.ts").Split(Environment.NewLine); @@ -242,8 +243,9 @@ live001139.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(6),
[],
input);
Substitute.For<IHlsInitSegmentCache>(),
input,
maybeMaxSegments: 10);
result.PlaylistStart.ShouldBe(start);
result.Sequence.ShouldBe(1137);
@ -535,8 +537,9 @@ live000082.ts").Split(Environment.NewLine); @@ -535,8 +537,9 @@ live000082.ts").Split(Environment.NewLine);
OutputFormatKind.Hls,
start,
start.AddSeconds(220),
[],
input);
Substitute.For<IHlsInitSegmentCache>(),
input,
maybeMaxSegments: 10);
// result.PlaylistStart.ShouldBe(start);
result.Sequence.ShouldBe(56);
@ -611,7 +614,7 @@ live000048.ts @@ -611,7 +614,7 @@ live000048.ts
OutputFormatKind.Hls,
start,
filterBefore,
[],
Substitute.For<IHlsInitSegmentCache>(),
input,
2);

2
ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs

@ -70,6 +70,8 @@ public class FakeLocalFileSystem : ILocalFileSystem @@ -70,6 +70,8 @@ public class FakeLocalFileSystem : ILocalFileSystem
.IfNoneAsync(string.Empty)
.Map(s => s.Split(Environment.NewLine));
public Task<byte[]> GetHash(string path) => throw new NotSupportedException();
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
private static List<DirectoryInfo> Split(DirectoryInfo path)

48
ErsatzTV.Core/FFmpeg/HlsInitSegmentCache.cs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
using System.Collections.Concurrent;
using ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Core.FFmpeg;
public class HlsInitSegmentCache(ILocalFileSystem localFileSystem) : IHlsInitSegmentCache
{
private readonly ConcurrentDictionary<string, string> _knownSegments = [];
private readonly ConcurrentDictionary<string, string> _earliestSegmentsByHash = [];
public async Task AddSegment(string segment)
{
string fileName = Path.GetFileName(segment);
if (!_knownSegments.ContainsKey(fileName))
{
byte[] hash = await localFileSystem.GetHash(segment);
string hashString = Convert.ToHexStringLower(hash);
//logger.LogDebug("Adding segment {Segment} to cache", fileName);
_knownSegments.TryAdd(fileName, hashString);
if (!_earliestSegmentsByHash.TryAdd(hashString, fileName))
{
//logger.LogDebug("An earlier segment with the same hash was already in the cache");
}
}
}
public string EarliestSegmentByHash(long generatedAt)
{
var fileName = $"{generatedAt}_init.mp4";
string hashString = _knownSegments[fileName];
return _earliestSegmentsByHash[hashString];
}
public bool IsEarliestByHash(string fileName)
{
string hashString = _knownSegments[fileName];
return _earliestSegmentsByHash[hashString] == fileName;
}
public void DeleteSegment(string segment)
{
//logger.LogDebug("Segment {Segment} is no longer needed; deleting cached hash", segment);
_knownSegments.TryRemove(segment, out _);
}
}

91
ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs

@ -6,24 +6,15 @@ using Microsoft.Extensions.Logging; @@ -6,24 +6,15 @@ using Microsoft.Extensions.Logging;
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(
OutputFormatKind outputFormat,
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
List<long> inits,
IHlsInitSegmentCache hlsInitSegmentCache,
string[] lines,
int maxSegments = 10,
Option<int> maybeMaxSegments,
bool endWithDiscontinuity = false)
{
try
@ -107,11 +98,11 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -107,11 +98,11 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
GeneratePlaylist(
outputFormat,
items,
inits,
hlsInitSegmentCache,
filterBefore,
targetDuration,
discontinuitySequence,
maxSegments);
maybeMaxSegments);
return new TrimPlaylistResult(nextPlaylistStart, startSequence, generatedAt, playlist, segments);
}
@ -119,10 +110,10 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -119,10 +110,10 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
{
try
{
string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist);
string file = tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist);
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?
return new TrimPlaylistResult(playlistStart, 0, 0, string.Empty, 0);
@ -140,18 +131,18 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -140,18 +131,18 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
OutputFormatKind outputFormat,
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
List<long> inits,
IHlsInitSegmentCache hlsInitSegmentCache,
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(
OutputFormatKind outputFormat,
List<PlaylistItem> items,
List<long> inits,
IHlsInitSegmentCache hlsInitSegmentCache,
DateTimeOffset filterBefore,
int targetDuration,
int discontinuitySequence,
int maxSegments)
Option<int> maybeMaxSegments)
{
if (items.Count != 0 && items[0] is PlaylistDiscontinuity)
{
@ -164,16 +155,26 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -164,16 +155,26 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
}
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
// otherwise return the last maxSegments
allSegments = afterFilter.Count >= maxSegments
? afterFilter.Take(maxSegments).ToList()
: allSegments.TakeLast(maxSegments).ToList();
foreach (int maxSegments in maybeMaxSegments)
{
// only filter if we have more than requested
if (allSegments.Count > maxSegments)
{
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;
@ -184,12 +185,6 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -184,12 +185,6 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
generatedAt = startSegment.GeneratedAt;
}
long minGeneratedAt = 0;
foreach (var firstSegment in allSegments.HeadOrNone())
{
minGeneratedAt = firstSegment.GeneratedAt;
}
// count all discontinuities that were filtered out
if (allSegments.Count != 0)
{
@ -208,12 +203,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -208,12 +203,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
if (outputFormat is OutputFormatKind.HlsMp4)
{
Option<long> maybeStartInit = Optional(inits.Find(init => init > 0 && init <= minGeneratedAt));
foreach (long init in maybeStartInit)
{
output.AppendLine(CultureInfo.InvariantCulture, $"#EXT-X-MAP:URI=\"{init}_init.mp4\"");
inits.Remove(init);
}
output.AppendLine(
CultureInfo.InvariantCulture,
$"#EXT-X-MAP:URI=\"{hlsInitSegmentCache.EarliestSegmentByHash(generatedAt)}\"");
}
for (var i = 0; i < items.Count; i++)
@ -229,15 +221,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -229,15 +221,9 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
if (outputFormat is OutputFormatKind.HlsMp4)
{
Option<long> maybeInit = Optional(
inits.Find(init => init > 0 && init <= nextSegment.GeneratedAt));
foreach (long init in maybeInit)
{
output.AppendLine(
CultureInfo.InvariantCulture,
$"#EXT-X-MAP:URI=\"{init}_init.mp4\"");
inits.Remove(init);
}
output.AppendLine(
CultureInfo.InvariantCulture,
$"#EXT-X-MAP:URI=\"{hlsInitSegmentCache.EarliestSegmentByHash(nextSegment.GeneratedAt)}\"");
}
}
}
@ -269,7 +255,12 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter @@ -269,7 +255,12 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter
.Map(s => s.StartTime)
.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;

12
ErsatzTV.Core/FFmpeg/IHlsInitSegmentCache.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.FFmpeg;
public interface IHlsInitSegmentCache
{
Task AddSegment(string segment);
string EarliestSegmentByHash(long generatedAt);
bool IsEarliestByHash(string fileName);
void DeleteSegment(string segment);
}

9
ErsatzTV.Core/FFmpeg/IHlsPlaylistFilter.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.OutputFormat;
namespace ErsatzTV.Core.FFmpeg;
@ -9,15 +8,15 @@ public interface IHlsPlaylistFilter @@ -9,15 +8,15 @@ public interface IHlsPlaylistFilter
OutputFormatKind outputFormat,
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
List<long> inits,
IHlsInitSegmentCache hlsInitSegmentCache,
string[] lines,
int maxSegments = 10,
Option<int> maybeMaxSegments,
bool endWithDiscontinuity = false);
TrimPlaylistResult TrimPlaylistWithDiscontinuity(
OutputFormatKind outputFormat,
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
List<long> inits,
IHlsInitSegmentCache hlsInitSegmentCache,
string[] lines);
}

1
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -16,4 +16,5 @@ public interface ILocalFileSystem @@ -16,4 +16,5 @@ public interface ILocalFileSystem
Unit EmptyFolder(string folder);
Task<string> ReadAllText(string path);
Task<string[]> ReadAllLines(string path);
Task<byte[]> GetHash(string path);
}

12
ErsatzTV.Core/Metadata/LocalFileSystem.cs

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
using Bugsnag;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using Bugsnag;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
@ -158,4 +160,12 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) : @@ -158,4 +160,12 @@ public class LocalFileSystem(IClient client, ILogger<LocalFileSystem> logger) :
public Task<string> ReadAllText(string path) => File.ReadAllTextAsync(path);
public Task<string[]> ReadAllLines(string path) => File.ReadAllLinesAsync(path);
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
public async Task<byte[]> GetHash(string path)
{
using var md5 = MD5.Create();
await using var stream = File.OpenRead(path);
return await md5.ComputeHashAsync(stream);
}
}

1
ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs

@ -59,6 +59,7 @@ public class FakeLocalFileSystem : ILocalFileSystem @@ -59,6 +59,7 @@ public class FakeLocalFileSystem : ILocalFileSystem
public Unit EmptyFolder(string folder) => Unit.Default;
public Task<string> ReadAllText(string path) => throw new NotImplementedException();
public Task<string[]> ReadAllLines(string path) => throw new NotImplementedException();
public Task<byte[]> GetHash(string path) => throw new NotImplementedException();
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();

2
ErsatzTV/Startup.cs

@ -14,7 +14,6 @@ using ErsatzTV.Core; @@ -14,7 +14,6 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Graphics;
using ErsatzTV.Core.Health;
using ErsatzTV.Core.Health.Checks;
using ErsatzTV.Core.Images;
@ -813,6 +812,7 @@ public class Startup @@ -813,6 +812,7 @@ public class Startup
services.AddScoped<IDecoSelector, DecoSelector>();
services.AddScoped<IWatermarkSelector, WatermarkSelector>();
services.AddScoped<IGraphicsElementSelector, GraphicsElementSelector>();
services.AddScoped<IHlsInitSegmentCache, HlsInitSegmentCache>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();

Loading…
Cancel
Save