mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* properly track discontinuity sequences with fmp4 * update dependenciespull/2549/head
15 changed files with 611 additions and 435 deletions
@ -1,5 +0,0 @@ |
|||||||
using ErsatzTV.Core; |
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Streaming; |
|
||||||
|
|
||||||
public record GetLastPtsTime(string ChannelNumber) : IRequest<Either<BaseError, PtsTime>>; |
|
||||||
@ -1,170 +0,0 @@ |
|||||||
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 : IRequestHandler<GetLastPtsTime, Either<BaseError, PtsTime>> |
|
||||||
{ |
|
||||||
private readonly IClient _client; |
|
||||||
private readonly IConfigElementRepository _configElementRepository; |
|
||||||
private readonly ILocalFileSystem _localFileSystem; |
|
||||||
private readonly ILogger<GetLastPtsTimeHandler> _logger; |
|
||||||
private readonly ITempFilePool _tempFilePool; |
|
||||||
|
|
||||||
public GetLastPtsTimeHandler( |
|
||||||
IClient client, |
|
||||||
ILocalFileSystem localFileSystem, |
|
||||||
ITempFilePool tempFilePool, |
|
||||||
IConfigElementRepository configElementRepository, |
|
||||||
ILogger<GetLastPtsTimeHandler> logger) |
|
||||||
{ |
|
||||||
_client = client; |
|
||||||
_localFileSystem = localFileSystem; |
|
||||||
_tempFilePool = tempFilePool; |
|
||||||
_configElementRepository = configElementRepository; |
|
||||||
_logger = logger; |
|
||||||
} |
|
||||||
|
|
||||||
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.ChannelNumber, ffprobePath)); |
|
||||||
|
|
||||||
private async Task<Either<BaseError, PtsTime>> Handle( |
|
||||||
RequestParameters parameters, |
|
||||||
CancellationToken cancellationToken) |
|
||||||
{ |
|
||||||
Option<FileInfo> maybeLastSegment = GetLastSegment(parameters.ChannelNumber); |
|
||||||
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,duration", |
|
||||||
"-of", "compact=p=0:nk=1", |
|
||||||
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)); |
|
||||||
return Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault()); |
|
||||||
} |
|
||||||
|
|
||||||
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(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); |
|
||||||
} |
|
||||||
} |
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue