fix some stream continuity issues (#2186)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
_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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public record GetLastPtsDuration(string ChannelNumber) : IRequest<Either<BaseError, PtsAndDuration>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public record GetLastPtsTime(string ChannelNumber) : IRequest<Either<BaseError, PtsTime>>;
|
||||
+58
-48
@@ -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,70 +34,80 @@ 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}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return PtsAndDuration.From(lastLine);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Notify(ex);
|
||||
await SaveTroubleshootingData(parameters.ChannelNumber, lastLine);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
|
||||
@@ -6,7 +6,7 @@ public record GetPlayoutItemProcessByChannelNumber(
|
||||
DateTimeOffset Now,
|
||||
bool StartAtZero,
|
||||
bool HlsRealtime,
|
||||
long PtsOffset,
|
||||
double PtsOffset,
|
||||
Option<int> TargetFramerate) : FFmpegProcessRequest(
|
||||
ChannelNumber,
|
||||
Mode,
|
||||
|
||||
@@ -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
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime,
|
||||
long ptsOffset,
|
||||
double ptsOffset,
|
||||
string vaapiDisplay,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
|
||||
@@ -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
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime,
|
||||
long ptsOffset,
|
||||
double ptsOffset,
|
||||
string vaapiDisplay,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
SetSceneDetect(videoStream, ffmpegState, desiredState, pipelineSteps);
|
||||
SetFFReport(ffmpegState, pipelineSteps);
|
||||
SetStreamSeek(ffmpegState, videoInputFile, context, pipelineSteps);
|
||||
SetTimeLimit(ffmpegState, pipelineSteps);
|
||||
|
||||
(FilterChain filterChain, ffmpegState) = BuildVideoPipeline(
|
||||
videoInputFile,
|
||||
@@ -420,17 +418,14 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
if (pipelineSteps.All(ps => ps is not EncoderCopyAudio))
|
||||
{
|
||||
audioInputFile.FilterSteps.Add(new AudioFirstPtsFilter(0));
|
||||
}
|
||||
|
||||
foreach (TimeSpan _ in audioInputFile.DesiredState.AudioDuration)
|
||||
{
|
||||
audioInputFile.FilterSteps.Add(new AudioPadFilter());
|
||||
}
|
||||
|
||||
if (pipelineSteps.All(ps => ps is not EncoderCopyAudio))
|
||||
{
|
||||
audioInputFile.FilterSteps.Add(new AudioSetPtsFilter());
|
||||
}
|
||||
|
||||
foreach (TimeSpan audioDuration in audioInputFile.DesiredState.AudioDuration)
|
||||
{
|
||||
audioInputFile.FilterSteps.Add(new AudioPadFilter(audioDuration));
|
||||
pipelineSteps.Add(new ShortestOutputOption());
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAudioLoudness(AudioInputFile audioInputFile)
|
||||
@@ -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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user