Browse Source

switch back from fmp4 to ts segments (#2554)

* restore pts offset calculation

* use ts segments again

* update changelog
pull/2556/head
Jason Dove 9 months ago committed by GitHub
parent
commit
2ef2b0299a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 2
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  3. 56
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  4. 10
      ErsatzTV.Application/Streaming/PtsTime.cs
  5. 2
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs
  6. 2
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs
  7. 2
      ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs
  8. 7
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs
  9. 211
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs
  10. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs
  11. 2
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs
  12. 2
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  13. 9
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  14. 1
      ErsatzTV.Core/FFmpeg/TempFileCategory.cs
  15. 4
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  16. 8
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  17. 4
      ErsatzTV.FFmpeg/FFmpegState.cs
  18. 4
      ErsatzTV.FFmpeg/Filter/ResetPtsFilter.cs
  19. 24
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  20. 4
      ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs
  21. 14
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  22. 2
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  23. 4
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  24. 6
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  25. 2
      ErsatzTV/Controllers/InternalController.cs
  26. 2
      ErsatzTV/Controllers/IptvController.cs

2
CHANGELOG.md

@ -50,7 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -50,7 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Rename `YAML Validation` tool to `Sequential Schedule Validation`
- Greatly reduce debug log spam during playout builds by logging summaries of certain warnings at the end
- Remove *experimental* `HLS Segmenter V2` streaming mode; it is not possible to maintain quality output using this mode
- Remove original `HLS Segmenter` streaming mode and rename `HLS Segmenter (fmp4)` to `HLS Segmenter`
- Remove *experimental* `HLS Segmenter (fmp4)` streaming mode; this mode only worked properly in a browser, many clients did not like it
## [25.7.1] - 2025-10-09
### Added

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

@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.FFmpeg.OutputFormat;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@ -124,6 +125,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit @@ -124,6 +125,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_serviceScopeFactory,
_graphicsEngine,
_client,
OutputFormatKind.Hls,
_hlsPlaylistFilter,
_hlsInitSegmentCache,
_configElementRepository,

56
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -26,6 +26,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -26,6 +26,7 @@ public class HlsSessionWorker : IHlsSessionWorker
{
private static int _workAheadCount;
private readonly IClient _client;
private readonly OutputFormatKind _outputFormatKind;
private readonly IHlsInitSegmentCache _hlsInitSegmentCache;
private readonly Dictionary<long, int> _discontinuityMap = [];
private readonly IConfigElementRepository _configElementRepository;
@ -42,6 +43,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -42,6 +43,7 @@ public class HlsSessionWorker : IHlsSessionWorker
private DateTimeOffset _channelStart;
private int _discontinuitySequence;
private bool _disposedValue;
private bool _hasWrittenSegments;
private DateTimeOffset _lastAccess;
private DateTimeOffset _lastDelete = DateTimeOffset.MinValue;
private IServiceScope _serviceScope;
@ -54,6 +56,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -54,6 +56,7 @@ public class HlsSessionWorker : IHlsSessionWorker
IServiceScopeFactory serviceScopeFactory,
IGraphicsEngine graphicsEngine,
IClient client,
OutputFormatKind outputFormatKind,
IHlsPlaylistFilter hlsPlaylistFilter,
IHlsInitSegmentCache hlsInitSegmentCache,
IConfigElementRepository configElementRepository,
@ -65,6 +68,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -65,6 +68,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_mediator = _serviceScope.ServiceProvider.GetRequiredService<IMediator>();
_graphicsEngine = graphicsEngine;
_client = client;
_outputFormatKind = outputFormatKind;
_hlsInitSegmentCache = hlsInitSegmentCache;
_hlsPlaylistFilter = hlsPlaylistFilter;
_configElementRepository = configElementRepository;
@ -120,7 +124,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -120,7 +124,7 @@ public class HlsSessionWorker : IHlsSessionWorker
TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylist(
_discontinuityMap,
OutputFormatKind.HlsMp4,
_outputFormatKind,
PlaylistStart,
filterBefore,
_hlsInitSegmentCache,
@ -442,8 +446,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -442,8 +446,7 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
// fmp4 doesn't require pts offsets because each new item gets a discontinuity AND a new init segment
const long PTS_OFFSET = 0;
TimeSpan ptsOffset = await GetPtsOffset(_channelNumber, cancellationToken);
_logger.LogDebug("HLS session state: {State}", _state);
@ -457,7 +460,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -457,7 +460,7 @@ public class HlsSessionWorker : IHlsSessionWorker
startAtZero,
realtime,
_channelStart,
PTS_OFFSET,
ptsOffset,
_targetFramerate);
// _logger.LogInformation("Request {@Request}", request);
@ -528,6 +531,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -528,6 +531,7 @@ public class HlsSessionWorker : IHlsSessionWorker
processModel.Until.Subtract(DateTimeOffset.Now).TotalSeconds);
_transcodedUntil = processModel.Until;
_state = NextState(_state, processModel);
_hasWrittenSegments = true;
return true;
}
else
@ -552,7 +556,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -552,7 +556,7 @@ public class HlsSessionWorker : IHlsSessionWorker
_channelNumber,
StreamingMode.HttpLiveStreamingSegmenter,
realtime,
PTS_OFFSET,
ptsOffset,
processModel.MaybeDuration,
processModel.Until,
errorMessage),
@ -577,6 +581,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -577,6 +581,8 @@ public class HlsSessionWorker : IHlsSessionWorker
_transcodedUntil = processModel.Until;
_state = NextState(_state, null);
_hasWrittenSegments = true;
return true;
}
}
@ -648,7 +654,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -648,7 +654,7 @@ public class HlsSessionWorker : IHlsSessionWorker
// trim playlist and insert discontinuity before appending with new ffmpeg process
TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity(
_discontinuityMap,
OutputFormatKind.HlsMp4,
_outputFormatKind,
PlaylistStart,
DateTimeOffset.Now.AddMinutes(-1),
_hlsInitSegmentCache,
@ -759,6 +765,44 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -759,6 +765,44 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
private async Task<TimeSpan> GetPtsOffset(string channelNumber, CancellationToken cancellationToken)
{
await _slim.WaitAsync(cancellationToken);
try
{
TimeSpan result = TimeSpan.Zero;
// if we haven't yet written any segments, start at zero
if (!_hasWrittenSegments)
{
return result;
}
await RefreshInits();
Either<BaseError, PtsTime> queryResult = await _mediator.Send(
new GetLastPtsTime(_hlsInitSegmentCache, channelNumber),
cancellationToken);
foreach (BaseError error in queryResult.LeftToSeq())
{
_logger.LogWarning("Unable to determine last pts offset - {Error}", error.ToString());
}
foreach (PtsTime pts in queryResult.RightToSeq())
{
_logger.LogWarning("Last pts offset is {Pts}", pts.Value);
result = pts.Value;
}
return result;
}
finally
{
_slim.Release();
}
}
private async Task<int> GetWorkAheadLimit(CancellationToken cancellationToken) =>
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters, cancellationToken)
.Map(maybeCount => maybeCount.Match(identity, () => 1));

10
ErsatzTV.Application/Streaming/PtsTime.cs

@ -2,19 +2,19 @@ @@ -2,19 +2,19 @@
namespace ErsatzTV.Application.Streaming;
public record PtsTime(long Value)
public record PtsTime(TimeSpan Value)
{
public static readonly PtsTime Zero = new(0);
public static readonly PtsTime Zero = new(TimeSpan.Zero);
public static PtsTime From(string ffprobeLine)
{
string[] split = ffprobeLine.Split("|");
var ptsTime = long.Parse(split[0], CultureInfo.InvariantCulture);
if (long.TryParse(split[1], CultureInfo.InvariantCulture, out long duration))
var ptsTime = double.Parse(split[0], CultureInfo.InvariantCulture);
if (double.TryParse(split[1], CultureInfo.InvariantCulture, out double duration))
{
ptsTime += duration;
}
return new PtsTime(ptsTime);
return new PtsTime(TimeSpan.FromSeconds(ptsTime));
}
}

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

@ -10,4 +10,4 @@ public record FFmpegProcessRequest( @@ -10,4 +10,4 @@ public record FFmpegProcessRequest(
bool StartAtZero,
bool HlsRealtime,
DateTimeOffset ChannelStartTime,
long PtsOffset) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;
TimeSpan PtsOffset) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;

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

@ -11,7 +11,7 @@ public record GetConcatProcessByChannelNumber : FFmpegProcessRequest @@ -11,7 +11,7 @@ public record GetConcatProcessByChannelNumber : FFmpegProcessRequest
false,
true,
DateTimeOffset.Now, // unused
0)
TimeSpan.Zero)
{
Scheme = scheme;
Host = host;

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

@ -6,7 +6,7 @@ public record GetErrorProcess( @@ -6,7 +6,7 @@ public record GetErrorProcess(
string ChannelNumber,
StreamingMode Mode,
bool HlsRealtime,
long PtsOffset,
TimeSpan PtsOffset,
Option<TimeSpan> MaybeDuration,
DateTimeOffset Until,
string ErrorMessage) : FFmpegProcessRequest(

7
ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.FFmpeg;
namespace ErsatzTV.Application.Streaming;
public record GetLastPtsTime(IHlsInitSegmentCache InitSegmentCache, string ChannelNumber)
: IRequest<Either<BaseError, PtsTime>>;

211
ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
using System.Text;
using Bugsnag;
using CliWrap;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace ErsatzTV.Application.Streaming;
public class GetLastPtsTimeHandler(
IClient client,
ILocalFileSystem localFileSystem,
ITempFilePool tempFilePool,
IConfigElementRepository configElementRepository,
ILogger<GetLastPtsTimeHandler> logger)
: IRequestHandler<GetLastPtsTime, Either<BaseError, PtsTime>>
{
public async Task<Either<BaseError, PtsTime>> Handle(
GetLastPtsTime request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request, cancellationToken);
return await validation.Match(
parameters => Handle(parameters, cancellationToken),
error => Task.FromResult<Either<BaseError, PtsTime>>(error.Join()));
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
GetLastPtsTime request,
CancellationToken cancellationToken) =>
await ValidateFFprobePath(cancellationToken)
.MapT(ffprobePath => new RequestParameters(request.InitSegmentCache, request.ChannelNumber, ffprobePath));
private async Task<Either<BaseError, PtsTime>> Handle(
RequestParameters parameters,
CancellationToken cancellationToken)
{
Option<FileInfo> maybeLastSegment = GetLastSegment(parameters);
foreach (FileInfo segment in maybeLastSegment)
{
return await GetPts(parameters, segment, cancellationToken).IfNoneAsync(PtsTime.Zero);
}
return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}");
}
private async Task<Option<PtsTime>> GetPts(
RequestParameters parameters,
FileInfo segment,
CancellationToken cancellationToken)
{
string[] argumentList =
{
"-v", "0",
"-show_entries",
"packet=pts_time,duration_time",
"-of", "compact=p=0:nk=1",
segment.FullName
};
PtsTime maxTime = PtsTime.Zero;
Action<string> replaceLine = s =>
{
if (!string.IsNullOrWhiteSpace(s))
{
try
{
var newPts = PtsTime.From(s.Trim());
if (newPts.Value > maxTime.Value)
{
maxTime = newPts;
}
}
catch
{
// do nothing
}
}
};
logger.LogDebug("ffprobe arguments {FFmpegArguments}", argumentList.ToList());
CommandResult probe = await Cli.Wrap(parameters.FFprobePath)
.WithArguments(argumentList)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToDelegate(replaceLine))
.ExecuteAsync(cancellationToken);
if (probe.ExitCode != 0)
{
return Option<PtsTime>.None;
}
return maxTime;
}
private Option<FileInfo> GetLastSegment(RequestParameters parameters)
{
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, parameters.ChannelNumber));
var allFiles = directory.GetFiles("*.ts").Append(directory.GetFiles("*.m4s")).ToList();
Option<FileInfo> maybeLastSegment = Optional(allFiles.OrderByDescending(f => f.Name).FirstOrDefault());
foreach (var lastSegment in maybeLastSegment)
{
if (lastSegment.Name.Contains("m4s"))
{
string[] split = lastSegment.Name.Split('_');
if (long.TryParse(split[1], out long generatedAt))
{
try
{
string init = parameters.InitSegmentCache.EarliestSegmentByHash(generatedAt);
string fullInit = Path.Combine(directory.FullName, init);
string combined = tempFilePool.GetNextTempFile(TempFileCategory.Fmp4LastSegment);
using (var output = File.OpenWrite(combined))
{
// copy init
using (var readInit = File.OpenRead(fullInit))
{
readInit.CopyTo(output);
}
// copy segment
using (var readSegment = File.OpenRead(lastSegment.FullName))
{
readSegment.CopyTo(output);
}
}
// return concatenated init + segment
return new FileInfo(combined);
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine($"Can't find init for last segment {lastSegment.FullName}");
foreach (var file in allFiles)
{
Console.WriteLine(file.FullName);
}
throw;
}
}
}
return lastSegment;
}
return Option<FileInfo>.None;
}
private Task<Validation<BaseError, string>> ValidateFFprobePath(CancellationToken cancellationToken) =>
configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath, cancellationToken)
.FilterT(File.Exists)
.Map(ffprobePath => ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private async Task SaveTroubleshootingData(string channelNumber, string output)
{
try
{
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
FileInfo[] allFiles = directory.GetFiles();
string playlistFileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8");
string playlistContents = string.Empty;
if (localFileSystem.FileExists(playlistFileName))
{
playlistContents = await File.ReadAllTextAsync(playlistFileName);
}
var data = new TroubleshootingData(allFiles, playlistContents, output);
string serialized = data.Serialize();
string file = tempFilePool.GetNextTempFile(TempFileCategory.BadTranscodeFolder);
await File.WriteAllTextAsync(file, serialized);
logger.LogWarning("Transcode folder is in bad state; troubleshooting info saved to {File}", file);
}
catch (Exception ex)
{
client.Notify(ex);
}
}
private sealed record RequestParameters(
IHlsInitSegmentCache InitSegmentCache,
string ChannelNumber,
string FFprobePath);
private sealed record TroubleshootingData(IEnumerable<FileInfo> Files, string Playlist, string ProbeOutput)
{
public string Serialize()
{
var data = new InternalData(
Files.Map(f => new FileData(f.FullName, f.Length, f.LastWriteTimeUtc)).ToList(),
Convert.ToBase64String(Encoding.UTF8.GetBytes(Playlist)),
Convert.ToBase64String(Encoding.UTF8.GetBytes(ProbeOutput)));
return JsonConvert.SerializeObject(data);
}
private sealed record FileData(string FileName, long Bytes, DateTime LastWriteTimeUtc);
private sealed record InternalData(List<FileData> Files, string EncodedPlaylist, string EncodedProbeOutput);
}
}

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

@ -9,7 +9,7 @@ public record GetPlayoutItemProcessByChannelNumber( @@ -9,7 +9,7 @@ public record GetPlayoutItemProcessByChannelNumber(
bool StartAtZero,
bool HlsRealtime,
DateTimeOffset ChannelStart,
long PtsOffset,
TimeSpan PtsOffset,
Option<int> TargetFramerate) : FFmpegProcessRequest(
ChannelNumber,
Mode,

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

@ -15,7 +15,7 @@ public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest @@ -15,7 +15,7 @@ public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest
false,
true,
DateTimeOffset.Now, // unused
0)
TimeSpan.Zero)
{
Scheme = scheme;
Host = host;

2
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -247,7 +247,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -247,7 +247,7 @@ public class PrepareTroubleshootingPlaybackHandler(
FillerKind.None,
inPoint,
channelStartTime: DateTimeOffset.Now,
0,
TimeSpan.Zero,
None,
FileSystemLayout.TranscodeTroubleshootingFolder,
_ => { },

9
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -82,7 +82,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -82,7 +82,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FillerKind fillerKind,
TimeSpan inPoint,
DateTimeOffset channelStartTime,
long ptsOffset,
TimeSpan ptsOffset,
Option<int> targetFramerate,
Option<string> customReportsFolder,
Action<FFmpegPipeline> pipelineAction,
@ -244,7 +244,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -244,7 +244,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
switch (channel.StreamingMode)
{
case StreamingMode.HttpLiveStreamingSegmenter:
outputFormat = OutputFormatKind.HlsMp4;
outputFormat = OutputFormatKind.Hls;
break;
case StreamingMode.HttpLiveStreamingDirect:
{
@ -554,7 +554,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -554,7 +554,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset,
TimeSpan ptsOffset,
string vaapiDisplay,
VaapiDriver vaapiDriver,
string vaapiDevice,
@ -619,7 +619,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -619,7 +619,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
switch (channel.StreamingMode)
{
case StreamingMode.HttpLiveStreamingSegmenter:
outputFormat = OutputFormatKind.HlsMp4;
outputFormat = OutputFormatKind.Hls;
break;
}
@ -631,6 +631,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -631,6 +631,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<string> hlsSegmentTemplate = outputFormat switch
{
OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"),
OutputFormatKind.HlsMp4 => Path.Combine(
FileSystemLayout.TranscodeFolder,
channel.Number,

1
ErsatzTV.Core/FFmpeg/TempFileCategory.cs

@ -7,6 +7,7 @@ public enum TempFileCategory @@ -7,6 +7,7 @@ public enum TempFileCategory
CoverArt = 2,
CachedArtwork = 3,
Fmp4LastSegment = 97,
BadTranscodeFolder = 98,
BadPlaylist = 99
}

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

@ -37,7 +37,7 @@ public interface IFFmpegProcessService @@ -37,7 +37,7 @@ public interface IFFmpegProcessService
FillerKind fillerKind,
TimeSpan inPoint,
DateTimeOffset channelStartTime,
long ptsOffset,
TimeSpan ptsOffset,
Option<int> targetFramerate,
Option<string> customReportsFolder,
Action<FFmpegPipeline> pipelineAction,
@ -50,7 +50,7 @@ public interface IFFmpegProcessService @@ -50,7 +50,7 @@ public interface IFFmpegProcessService
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset,
TimeSpan ptsOffset,
string vaapiDisplay,
VaapiDriver vaapiDriver,
string vaapiDevice,

8
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -91,7 +91,7 @@ public class PipelineBuilderBaseTests @@ -91,7 +91,7 @@ public class PipelineBuilderBaseTests
Option<string>.None,
Option<string>.None,
Option<string>.None,
0,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
@ -190,7 +190,7 @@ public class PipelineBuilderBaseTests @@ -190,7 +190,7 @@ public class PipelineBuilderBaseTests
Option<string>.None,
Option<string>.None,
Option<string>.None,
0,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
@ -347,7 +347,7 @@ public class PipelineBuilderBaseTests @@ -347,7 +347,7 @@ public class PipelineBuilderBaseTests
Option<string>.None,
Option<string>.None,
Option<string>.None,
0,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
@ -440,7 +440,7 @@ public class PipelineBuilderBaseTests @@ -440,7 +440,7 @@ public class PipelineBuilderBaseTests
Option<string>.None,
Option<string>.None,
Option<string>.None,
0,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,

4
ErsatzTV.FFmpeg/FFmpegState.cs

@ -20,7 +20,7 @@ public record FFmpegState( @@ -20,7 +20,7 @@ public record FFmpegState(
Option<string> HlsPlaylistPath,
Option<string> HlsSegmentTemplate,
Option<string> HlsInitTemplate,
long PtsOffset,
TimeSpan PtsOffset,
Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames,
bool IsSongWithProgress,
@ -49,7 +49,7 @@ public record FFmpegState( @@ -49,7 +49,7 @@ public record FFmpegState(
Option<string>.None,
Option<string>.None,
Option<string>.None,
0,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,

4
ErsatzTV.FFmpeg/Filter/Qsv/QsvResetPtsFilter.cs → ErsatzTV.FFmpeg/Filter/ResetPtsFilter.cs

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
namespace ErsatzTV.FFmpeg.Filter.Qsv;
namespace ErsatzTV.FFmpeg.Filter;
public class QsvResetPtsFilter : BaseFilter
public class ResetPtsFilter : BaseFilter
{
public override string Filter => "setpts=PTS-STARTPTS";

24
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -95,12 +95,24 @@ public class OutputFormatHls : IPipelineStep @@ -95,12 +95,24 @@ public class OutputFormatHls : IPipelineStep
}
else
{
result.AddRange(
[
"-hls_flags", $"{pdt}append_list+discont_start{independentSegments}",
"-mpegts_flags", "+initial_discontinuity",
_playlistPath
]);
switch (_outputFormat)
{
case OutputFormatKind.HlsMp4:
result.AddRange(
[
"-hls_flags", $"{pdt}append_list+discont_start{independentSegments}",
_playlistPath
]);
break;
default:
result.AddRange(
[
"-hls_flags", $"{pdt}append_list+discont_start{independentSegments}",
"-mpegts_flags", "+initial_discontinuity",
_playlistPath
]);
break;
}
}
return result.ToArray();

4
ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs

@ -2,12 +2,12 @@ @@ -2,12 +2,12 @@
namespace ErsatzTV.FFmpeg.OutputOption;
public class OutputTsOffsetOption(long ptsOffset, int videoTrackTimeScale) : OutputOption
public class OutputTsOffsetOption(TimeSpan ptsOffset) : OutputOption
{
public override string[] OutputOptions =>
[
"-output_ts_offset",
$"{(ptsOffset / (double)videoTrackTimeScale).ToString(NumberFormatInfo.InvariantInfo)}"
$"{ptsOffset.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo)}s"
];
// public override FrameState NextState(FrameState currentState) => currentState with { PtsOffset = _ptsOffset };

14
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -255,6 +255,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -255,6 +255,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
SetTimeLimit(ffmpegState, pipelineSteps);
(FilterChain filterChain, ffmpegState) = BuildVideoPipeline(
isFmp4Hls,
videoInputFile,
videoStream,
ffmpegState,
@ -386,7 +387,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -386,7 +387,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
segmentTemplate,
ffmpegState.HlsInitTemplate,
playlistPath,
ffmpegState.PtsOffset == 0,
ffmpegState.PtsOffset == TimeSpan.Zero,
oneSecondGop,
ffmpegState.IsTroubleshooting));
}
@ -539,6 +540,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -539,6 +540,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
ICollection<IPipelineStep> pipelineSteps);
private FilterChainAndState BuildVideoPipeline(
bool isFmp4Hls,
VideoInputFile videoInputFile,
VideoStream videoStream,
FFmpegState ffmpegState,
@ -593,7 +595,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -593,7 +595,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
_fontsFolder,
pipelineSteps);
SetOutputTsOffset(ffmpegState, desiredState, pipelineSteps);
SetOutputTsOffset(isFmp4Hls, ffmpegState, desiredState, pipelineSteps);
return new FilterChainAndState(filterChain, ffmpegState);
}
@ -674,6 +676,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -674,6 +676,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
}
private static void SetOutputTsOffset(
bool isFmp4Hls,
FFmpegState ffmpegState,
FrameState desiredState,
List<IPipelineStep> pipelineSteps)
@ -683,12 +686,9 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -683,12 +686,9 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
return;
}
if (ffmpegState.PtsOffset > 0)
if (ffmpegState.PtsOffset > TimeSpan.Zero && !isFmp4Hls)
{
foreach (int videoTrackTimeScale in desiredState.VideoTrackTimeScale)
{
pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset, videoTrackTimeScale));
}
pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset));
}
}

2
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -170,7 +170,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -170,7 +170,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
currentState = decoder.NextState(currentState);
}
videoInputFile.FilterSteps.Add(new QsvResetPtsFilter());
videoInputFile.FilterSteps.Add(new ResetPtsFilter());
// easier to use nv12 for overlay
if (context.HasSubtitleOverlay || context.HasWatermark || context.HasGraphicsEngine)

4
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -396,7 +396,7 @@ public class TranscodingTests @@ -396,7 +396,7 @@ public class TranscodingTests
FillerKind.None,
TimeSpan.Zero,
DateTimeOffset.Now,
0,
TimeSpan.Zero,
None,
Option<string>.None,
_ => { },
@ -707,7 +707,7 @@ public class TranscodingTests @@ -707,7 +707,7 @@ public class TranscodingTests
FillerKind.None,
TimeSpan.Zero,
DateTimeOffset.Now,
0,
TimeSpan.Zero,
None,
Option<string>.None,
PipelineAction,

6
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -119,9 +119,9 @@ public class TroubleshootController( @@ -119,9 +119,9 @@ public class TroubleshootController(
string[] segmentFiles = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => Directory.GetFiles(
FileSystemLayout.TranscodeTroubleshootingFolder,
"*.m4s"),
// StreamingMode.HttpLiveStreamingSegmenter => Directory.GetFiles(
// FileSystemLayout.TranscodeTroubleshootingFolder,
// "*.m4s"),
_ => Directory.GetFiles(FileSystemLayout.TranscodeTroubleshootingFolder, "*.ts")
};

2
ErsatzTV/Controllers/InternalController.cs

@ -255,7 +255,7 @@ public class InternalController : StreamingControllerBase @@ -255,7 +255,7 @@ public class InternalController : StreamingControllerBase
false,
true,
DateTimeOffset.Now,
0,
TimeSpan.Zero,
Option<int>.None);
Either<BaseError, PlayoutItemProcessModel> result = await _mediator.Send(request);

2
ErsatzTV/Controllers/IptvController.cs

@ -342,7 +342,7 @@ public class IptvController : StreamingControllerBase @@ -342,7 +342,7 @@ public class IptvController : StreamingControllerBase
false,
true,
DateTimeOffset.Now,
0,
TimeSpan.Zero,
Option<int>.None);
Either<BaseError, PlayoutItemProcessModel> result = await _mediator.Send(request);

Loading…
Cancel
Save