Browse Source

fix some stream continuity issues (#2186)

pull/2187/head
Jason Dove 10 months ago committed by GitHub
parent
commit
260949893c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 14
      ErsatzTV.Application/Streaming/HlsSessionWorker.cs
  3. 19
      ErsatzTV.Application/Streaming/PtsAndDuration.cs
  4. 19
      ErsatzTV.Application/Streaming/PtsTime.cs
  5. 2
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs
  6. 2
      ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs
  7. 5
      ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs
  8. 5
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTime.cs
  9. 102
      ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs
  10. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs
  11. 4
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  12. 4
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  13. 10
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  14. 2
      ErsatzTV.FFmpeg/FFmpegState.cs
  15. 8
      ErsatzTV.FFmpeg/Filter/AudioFirstPtsFilter.cs
  16. 8
      ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs
  17. 2
      ErsatzTV.FFmpeg/Filter/AudioSetPtsFilter.cs
  18. 12
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  19. 19
      ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs
  20. 23
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

1
CHANGELOG.md

@ -161,6 +161,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -161,6 +161,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix searching for `collection` names with spaces or other special characters, e.g. `collection:"Movies - Action"`
- Fix QSV transcoding errors when scaling
- Fix QSV frame freezing in browser
- Fix some stream continuity issues, and some cases where audio sync is lost at transition
## [25.2.0] - 2025-06-24
### Added

14
ErsatzTV.Application/Streaming/HlsSessionWorker.cs

@ -397,7 +397,7 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -397,7 +397,7 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
long ptsOffset = await GetPtsOffset(_channelNumber, cancellationToken);
double ptsOffset = await GetPtsOffset(_channelNumber, cancellationToken);
// _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset);
_logger.LogDebug("HLS session state: {State}", _state);
@ -620,12 +620,12 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -620,12 +620,12 @@ public class HlsSessionWorker : IHlsSessionWorker
}
}
private async Task<long> GetPtsOffset(string channelNumber, CancellationToken cancellationToken)
private async Task<double> GetPtsOffset(string channelNumber, CancellationToken cancellationToken)
{
await _slim.WaitAsync(cancellationToken);
try
{
long result = 0;
double result = 0;
// if we haven't yet written any segments, start at zero
if (!_hasWrittenSegments)
@ -633,8 +633,8 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -633,8 +633,8 @@ public class HlsSessionWorker : IHlsSessionWorker
return result;
}
Either<BaseError, PtsAndDuration> queryResult = await _mediator.Send(
new GetLastPtsDuration(channelNumber),
Either<BaseError, PtsTime> queryResult = await _mediator.Send(
new GetLastPtsTime(channelNumber),
cancellationToken);
foreach (BaseError error in queryResult.LeftToSeq())
@ -642,9 +642,9 @@ public class HlsSessionWorker : IHlsSessionWorker @@ -642,9 +642,9 @@ public class HlsSessionWorker : IHlsSessionWorker
_logger.LogWarning("Unable to determine last pts offset - {Error}", error.ToString());
}
foreach ((long pts, long duration) in queryResult.RightToSeq())
foreach (PtsTime pts in queryResult.RightToSeq())
{
result = pts + duration + 1;
result = pts.Value + 0.01;
}
return result;

19
ErsatzTV.Application/Streaming/PtsAndDuration.cs

@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
using System.Globalization;
namespace ErsatzTV.Application.Streaming;
public record PtsAndDuration(long Pts, long Duration)
{
public static PtsAndDuration From(string ffprobeLine)
{
string[] split = ffprobeLine.Split("|");
var left = long.Parse(split[0], CultureInfo.InvariantCulture);
if (!long.TryParse(split[1], out long right))
{
// some durations are N/A, so we have to guess at something
right = 10_000;
}
return new PtsAndDuration(left, right);
}
}

19
ErsatzTV.Application/Streaming/PtsTime.cs

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

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

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

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

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

5
ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Streaming;
public record GetLastPtsDuration(string ChannelNumber) : IRequest<Either<BaseError, PtsAndDuration>>;

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

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

102
ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs → ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs

@ -12,20 +12,20 @@ using Newtonsoft.Json; @@ -12,20 +12,20 @@ using Newtonsoft.Json;
namespace ErsatzTV.Application.Streaming;
public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Either<BaseError, PtsAndDuration>>
public class GetLastPtsTimeHandler : IRequestHandler<GetLastPtsTime, Either<BaseError, PtsTime>>
{
private readonly IClient _client;
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetLastPtsDurationHandler> _logger;
private readonly ILogger<GetLastPtsTimeHandler> _logger;
private readonly ITempFilePool _tempFilePool;
public GetLastPtsDurationHandler(
public GetLastPtsTimeHandler(
IClient client,
ILocalFileSystem localFileSystem,
ITempFilePool tempFilePool,
IConfigElementRepository configElementRepository,
ILogger<GetLastPtsDurationHandler> logger)
ILogger<GetLastPtsTimeHandler> logger)
{
_client = client;
_localFileSystem = localFileSystem;
@ -34,68 +34,78 @@ public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Eit @@ -34,68 +34,78 @@ public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Eit
_logger = logger;
}
public async Task<Either<BaseError, PtsAndDuration>> Handle(
GetLastPtsDuration request,
public async Task<Either<BaseError, PtsTime>> Handle(
GetLastPtsTime request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request);
return await validation.Match(
parameters => Handle(parameters, cancellationToken),
error => Task.FromResult<Either<BaseError, PtsAndDuration>>(error.Join()));
error => Task.FromResult<Either<BaseError, PtsTime>>(error.Join()));
}
private async Task<Validation<BaseError, RequestParameters>> Validate(GetLastPtsDuration request) =>
private async Task<Validation<BaseError, RequestParameters>> Validate(GetLastPtsTime request) =>
await ValidateFFprobePath().MapT(ffprobePath => new RequestParameters(request.ChannelNumber, ffprobePath));
private async Task<Either<BaseError, PtsAndDuration>> Handle(
private async Task<Either<BaseError, PtsTime>> Handle(
RequestParameters parameters,
CancellationToken cancellationToken)
{
Option<FileInfo> maybeLastSegment = GetLastSegment(parameters.ChannelNumber);
foreach (FileInfo segment in maybeLastSegment)
{
string[] argumentList =
{
"-v", "0",
"-show_entries",
"packet=pts,duration",
"-of", "compact=p=0:nk=1",
// "-read_intervals", "999999", // read_intervals causes inconsistent behavior on windows
segment.FullName
};
string lastLine = string.Empty;
Action<string> replaceLine = s =>
{
if (!string.IsNullOrWhiteSpace(s))
{
lastLine = s.Trim();
}
};
CommandResult probe = await Cli.Wrap(parameters.FFprobePath)
.WithArguments(argumentList)
.WithValidation(CommandResultValidation.None)
.WithStandardOutputPipe(PipeTarget.ToDelegate(replaceLine))
.ExecuteAsync(cancellationToken);
if (probe.ExitCode != 0)
{
return BaseError.New($"FFprobe at {parameters.FFprobePath} exited with code {probe.ExitCode}");
}
PtsTime videoPts = await GetPts(parameters, segment, "v", cancellationToken).IfNoneAsync(PtsTime.Zero);
PtsTime audioPts = await GetPts(parameters, segment, "a", cancellationToken).IfNoneAsync(PtsTime.Zero);
return videoPts.Value > audioPts.Value ? videoPts : audioPts;
}
try
{
return PtsAndDuration.From(lastLine);
}
catch (Exception ex)
return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}");
}
private async Task<Option<PtsTime>> GetPts(RequestParameters parameters, FileInfo segment, string audioVideo, CancellationToken cancellationToken)
{
string[] argumentList =
{
"-v", "0",
"-select_streams", $"{audioVideo}:0",
"-show_entries",
"packet=pts_time,duration_time",
"-of", "compact=p=0:nk=1",
// "-read_intervals", "999999", // read_intervals causes inconsistent behavior on windows
segment.FullName
};
string lastLine = string.Empty;
Action<string> replaceLine = s =>
{
if (!string.IsNullOrWhiteSpace(s))
{
_client.Notify(ex);
await SaveTroubleshootingData(parameters.ChannelNumber, lastLine);
lastLine = s.Trim();
}
};
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 BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}");
try
{
return PtsTime.From(lastLine);
}
catch (Exception ex)
{
_client.Notify(ex);
await SaveTroubleshootingData(parameters.ChannelNumber, lastLine);
}
return Option<PtsTime>.None;
}
private static Option<FileInfo> GetLastSegment(string channelNumber)

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

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

4
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -70,7 +70,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -70,7 +70,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset,
double ptsOffset,
Option<int> targetFramerate,
bool disableWatermarks,
Option<string> customReportsFolder,
@ -455,7 +455,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -455,7 +455,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset,
double ptsOffset,
string vaapiDisplay,
VaapiDriver vaapiDriver,
string vaapiDevice,

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

@ -36,7 +36,7 @@ public interface IFFmpegProcessService @@ -36,7 +36,7 @@ public interface IFFmpegProcessService
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset,
double ptsOffset,
Option<int> targetFramerate,
bool disableWatermarks,
Option<string> customReportsFolder,
@ -48,7 +48,7 @@ public interface IFFmpegProcessService @@ -48,7 +48,7 @@ public interface IFFmpegProcessService
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset,
double ptsOffset,
string vaapiDisplay,
VaapiDriver vaapiDriver,
string vaapiDevice,

10
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -116,7 +116,7 @@ public class PipelineBuilderBaseTests @@ -116,7 +116,7 @@ public class PipelineBuilderBaseTests
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
command.ShouldBe(
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=N/SR/TB[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
}
[Test]
@ -213,7 +213,7 @@ public class PipelineBuilderBaseTests @@ -213,7 +213,7 @@ public class PipelineBuilderBaseTests
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
command.ShouldBe(
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=N/SR/TB[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
}
[Test]
@ -368,9 +368,9 @@ public class PipelineBuilderBaseTests @@ -368,9 +368,9 @@ public class PipelineBuilderBaseTests
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
// 0.4.0 reference: "-nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -re -ss 00:14:33.6195516 -i /tmp/whatever.mkv -map 0:0 -map 0:a -c:v copy -flags cgop -sc_threshold 0 -c:a copy -movflags +faststart -muxdelay 0 -muxpreload 0 -metadata service_provider="ErsatzTV" -metadata service_name="ErsatzTV" -t 00:06:39.6934484 -f mpegts -mpegts_flags +initial_discontinuity pipe:1"
// 0.4.0 reference: "-nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -re -ss 00:14:33.6195516 -i /tmp/whatever.mkv -map 0:0 -map 0:a -c:v copy -flags cgop -sc_threshold 0 -c:a copy -movflags +faststart -metadata service_provider="ErsatzTV" -metadata service_name="ErsatzTV" -t 00:06:39.6934484 -f mpegts -mpegts_flags +initial_discontinuity pipe:1"
command.ShouldBe(
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:1 -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1");
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:1 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1");
}
[Test]
@ -459,7 +459,7 @@ public class PipelineBuilderBaseTests @@ -459,7 +459,7 @@ public class PipelineBuilderBaseTests
string command = PrintCommand(videoInputFile, audioInputFile, None, None, result);
command.ShouldBe(
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:a -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1");
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:a -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1");
}
[Test]

2
ErsatzTV.FFmpeg/FFmpegState.cs

@ -19,7 +19,7 @@ public record FFmpegState( @@ -19,7 +19,7 @@ public record FFmpegState(
OutputFormatKind OutputFormat,
Option<string> HlsPlaylistPath,
Option<string> HlsSegmentTemplate,
long PtsOffset,
double PtsOffset,
Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames,
bool IsSongWithProgress,

8
ErsatzTV.FFmpeg/Filter/AudioFirstPtsFilter.cs

@ -1,12 +1,8 @@ @@ -1,12 +1,8 @@
namespace ErsatzTV.FFmpeg.Filter;
public class AudioFirstPtsFilter : BaseFilter
public class AudioFirstPtsFilter(int pts) : BaseFilter
{
private readonly int _pts;
public AudioFirstPtsFilter(int pts) => _pts = pts;
public override string Filter => $"aresample=async=1:first_pts={_pts}";
public override string Filter => $"aresample=async=1:first_pts={pts}";
public override FrameState NextState(FrameState currentState) => currentState;
}

8
ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
namespace ErsatzTV.FFmpeg.Filter;
using System.Globalization;
public class AudioPadFilter : BaseFilter
namespace ErsatzTV.FFmpeg.Filter;
public class AudioPadFilter(TimeSpan audioDuration) : BaseFilter
{
public override string Filter => "apad";
public override string Filter => $"apad=whole_dur={audioDuration.TotalMilliseconds.ToString(NumberFormatInfo.InvariantInfo)}ms";
public override FrameState NextState(FrameState currentState) => currentState;
}

2
ErsatzTV.FFmpeg/Filter/AudioSetPtsFilter.cs

@ -2,7 +2,7 @@ namespace ErsatzTV.FFmpeg.Filter; @@ -2,7 +2,7 @@ namespace ErsatzTV.FFmpeg.Filter;
public class AudioSetPtsFilter : BaseFilter
{
public override string Filter => "asetpts=PTS-STARTPTS";
public override string Filter => "asetpts=N/SR/TB";
public override FrameState NextState(FrameState currentState) => currentState;
}

12
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -90,20 +90,22 @@ public class OutputFormatHls : IPipelineStep @@ -90,20 +90,22 @@ public class OutputFormatHls : IPipelineStep
foreach (string rFrameRate in _mediaFrameRate)
{
if (!int.TryParse(rFrameRate, out int fr))
if (double.TryParse(rFrameRate, out double value))
{
frameRate = (int)Math.Round(value);
}
else if (!int.TryParse(rFrameRate, out int fr))
{
string[] split = (rFrameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
frameRate = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
frameRate = 24;
}
}
frameRate = fr;
}
return frameRate;

19
ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs

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

23
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -169,7 +169,6 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -169,7 +169,6 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
new NoStatsOption(),
new LoglevelErrorOption(),
new StandardFormatFlags(),
new NoDemuxDecodeDelayOutputOption(),
outputOption,
new ClosedGopOutputOption()
};
@ -205,7 +204,6 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -205,7 +204,6 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
SetSceneDetect(videoStream, ffmpegState, desiredState, pipelineSteps);
SetFFReport(ffmpegState, pipelineSteps);
SetStreamSeek(ffmpegState, videoInputFile, context, pipelineSteps);
SetTimeLimit(ffmpegState, pipelineSteps);
(FilterChain filterChain, ffmpegState) = BuildVideoPipeline(
videoInputFile,
@ -420,16 +418,13 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -420,16 +418,13 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
if (pipelineSteps.All(ps => ps is not EncoderCopyAudio))
{
audioInputFile.FilterSteps.Add(new AudioFirstPtsFilter(0));
audioInputFile.FilterSteps.Add(new AudioSetPtsFilter());
}
foreach (TimeSpan _ in audioInputFile.DesiredState.AudioDuration)
{
audioInputFile.FilterSteps.Add(new AudioPadFilter());
}
if (pipelineSteps.All(ps => ps is not EncoderCopyAudio))
foreach (TimeSpan audioDuration in audioInputFile.DesiredState.AudioDuration)
{
audioInputFile.FilterSteps.Add(new AudioSetPtsFilter());
audioInputFile.FilterSteps.Add(new AudioPadFilter(audioDuration));
pipelineSteps.Add(new ShortestOutputOption());
}
}
@ -535,6 +530,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -535,6 +530,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
SetVideoBufferSizeOutput(desiredState, pipelineSteps);
}
FilterChain filterChain = SetVideoFilters(
videoInputFile,
videoStream,
@ -636,10 +633,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -636,10 +633,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
if (ffmpegState.PtsOffset > 0)
{
foreach (int videoTrackTimeScale in desiredState.VideoTrackTimeScale)
{
pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset, videoTrackTimeScale));
}
pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset));
}
}
@ -835,8 +829,5 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -835,8 +829,5 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
}
}
private static void SetTimeLimit(FFmpegState ffmpegState, List<IPipelineStep> pipelineSteps) =>
pipelineSteps.AddRange(ffmpegState.Finish.Map(finish => new TimeLimitOutputOption(finish)));
private sealed record FilterChainAndState(FilterChain FilterChain, FFmpegState FFmpegState);
}

Loading…
Cancel
Save