Browse Source

add optional ffmpeg reports (#81)

* log full exceptions in plex tv api client

* add optional ffmpeg reports
pull/82/head
Jason Dove 5 years ago committed by GitHub
parent
commit
a8e861abc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  2. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs
  3. 5
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs
  4. 10
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  5. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  6. 14
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  7. 7
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  8. 2
      ErsatzTV.Core/FileSystemLayout.cs
  9. 13
      ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs
  10. 6
      ErsatzTV/Pages/FFmpeg.razor
  11. 25
      ErsatzTV/Pages/MediaSources.razor

33
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
@ -70,12 +71,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request) private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{ {
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath); await _configElementRepository.Get(ConfigElementKey.FFmpegPath).Match(
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
Option<ConfigElement> defaultFFmpegProfileId =
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
ffmpegPath.Match(
ce => ce =>
{ {
ce.Value = request.Settings.FFmpegPath; ce.Value = request.Settings.FFmpegPath;
@ -88,7 +84,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
_configElementRepository.Add(ce); _configElementRepository.Add(ce);
}); });
ffprobePath.Match( await _configElementRepository.Get(ConfigElementKey.FFprobePath).Match(
ce => ce =>
{ {
ce.Value = request.Settings.FFprobePath; ce.Value = request.Settings.FFprobePath;
@ -101,7 +97,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
_configElementRepository.Add(ce); _configElementRepository.Add(ce);
}); });
defaultFFmpegProfileId.Match( await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId).Match(
ce => ce =>
{ {
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString(); ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
@ -117,6 +113,27 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
_configElementRepository.Add(ce); _configElementRepository.Add(ce);
}); });
await _configElementRepository.Get(ConfigElementKey.FFmpegSaveReports).Match(
ce =>
{
ce.Value = request.Settings.SaveReports.ToString();
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{
Key = ConfigElementKey.FFmpegSaveReports.Key,
Value = request.Settings.SaveReports.ToString()
};
_configElementRepository.Add(ce);
});
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
return Unit.Default; return Unit.Default;
} }
} }

1
ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs

@ -5,5 +5,6 @@
public string FFmpegPath { get; set; } public string FFmpegPath { get; set; }
public string FFprobePath { get; set; } public string FFprobePath { get; set; }
public int DefaultFFmpegProfileId { get; set; } public int DefaultFFmpegProfileId { get; set; }
public bool SaveReports { get; set; }
} }
} }

5
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs

@ -22,12 +22,15 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath); Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
Option<int> defaultFFmpegProfileId = Option<int> defaultFFmpegProfileId =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId); await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
Option<bool> saveReports =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
return new FFmpegSettingsViewModel return new FFmpegSettingsViewModel
{ {
FFmpegPath = ffmpegPath.IfNone(string.Empty), FFmpegPath = ffmpegPath.IfNone(string.Empty),
FFprobePath = ffprobePath.IfNone(string.Empty), FFprobePath = ffprobePath.IfNone(string.Empty),
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0) DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0),
SaveReports = saveReports.IfNone(false)
}; };
} }
} }

10
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -23,6 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger; private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IConfigElementRepository _configElementRepository;
private readonly IPlayoutRepository _playoutRepository; private readonly IPlayoutRepository _playoutRepository;
public GetPlayoutItemProcessByChannelNumberHandler( public GetPlayoutItemProcessByChannelNumberHandler(
@ -35,6 +36,7 @@ namespace ErsatzTV.Application.Streaming.Queries
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger) ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
: base(channelRepository, configElementRepository) : base(channelRepository, configElementRepository)
{ {
_configElementRepository = configElementRepository;
_playoutRepository = playoutRepository; _playoutRepository = playoutRepository;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessService = ffmpegProcessService;
@ -54,7 +56,7 @@ namespace ErsatzTV.Application.Streaming.Queries
.BindT(ValidatePlayoutItemPath); .BindT(ValidatePlayoutItemPath);
return await maybePlayoutItem.Match( return await maybePlayoutItem.Match(
playoutItemWithPath => async playoutItemWithPath =>
{ {
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch
{ {
@ -63,14 +65,18 @@ namespace ErsatzTV.Application.Streaming.Queries
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
}; };
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
return Right<BaseError, Process>( return Right<BaseError, Process>(
_ffmpegProcessService.ForPlayoutItem( _ffmpegProcessService.ForPlayoutItem(
ffmpegPath, ffmpegPath,
saveReports,
channel, channel,
version, version,
playoutItemWithPath.Path, playoutItemWithPath.Path,
playoutItemWithPath.PlayoutItem.StartOffset, playoutItemWithPath.PlayoutItem.StartOffset,
now)).AsTask(); now));
}, },
async error => async error =>
{ {

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -10,5 +10,6 @@
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id"); public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
} }
} }

14
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -21,6 +21,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Text; using System.Text;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
@ -39,9 +40,14 @@ namespace ErsatzTV.Core.FFmpeg
private readonly List<string> _arguments = new(); private readonly List<string> _arguments = new();
private readonly string _ffmpegPath; private readonly string _ffmpegPath;
private readonly bool _saveReports;
private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath; public FFmpegProcessBuilder(string ffmpegPath, bool saveReports)
{
_ffmpegPath = ffmpegPath;
_saveReports = saveReports;
}
public FFmpegProcessBuilder WithThreads(int threads) public FFmpegProcessBuilder WithThreads(int threads)
{ {
@ -365,6 +371,12 @@ namespace ErsatzTV.Core.FFmpeg
StandardOutputEncoding = Encoding.UTF8 StandardOutputEncoding = Encoding.UTF8
}; };
if (_saveReports)
{
string fileName = Path.Combine(FileSystemLayout.FFmpegReportsFolder, "%p-%t.log");
startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32");
}
foreach (string argument in _arguments) foreach (string argument in _arguments)
{ {
startInfo.ArgumentList.Add(argument); startInfo.ArgumentList.Add(argument);

7
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Core.FFmpeg
public Process ForPlayoutItem( public Process ForPlayoutItem(
string ffmpegPath, string ffmpegPath,
bool saveReports,
Channel channel, Channel channel,
MediaVersion version, MediaVersion version,
string path, string path,
@ -28,7 +29,7 @@ namespace ErsatzTV.Core.FFmpeg
start, start,
now); now);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports)
.WithThreads(playbackSettings.ThreadCount) .WithThreads(playbackSettings.ThreadCount)
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration) .WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
.WithQuiet() .WithQuiet()
@ -91,7 +92,7 @@ namespace ErsatzTV.Core.FFmpeg
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution; IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
@ -114,7 +115,7 @@ namespace ErsatzTV.Core.FFmpeg
{ {
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings; FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
return new FFmpegProcessBuilder(ffmpegPath) return new FFmpegProcessBuilder(ffmpegPath, false)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)

2
ErsatzTV.Core/FileSystemLayout.cs

@ -19,6 +19,8 @@ namespace ErsatzTV.Core
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json"); public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
public static readonly string FFmpegReportsFolder = Path.Combine(AppDataFolder, "ffmpeg-reports");
public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork"); public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters"); public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");

13
ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Plex; using ErsatzTV.Core.Plex;
using ErsatzTV.Infrastructure.Plex.Models; using ErsatzTV.Infrastructure.Plex.Models;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging;
using Refit; using Refit;
namespace ErsatzTV.Infrastructure.Plex namespace ErsatzTV.Infrastructure.Plex
@ -15,14 +16,16 @@ namespace ErsatzTV.Infrastructure.Plex
public class PlexTvApiClient : IPlexTvApiClient public class PlexTvApiClient : IPlexTvApiClient
{ {
private const string AppName = "ErsatzTV"; private const string AppName = "ErsatzTV";
private readonly ILogger<PlexTvApiClient> _logger;
private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexSecretStore _plexSecretStore;
private readonly IPlexTvApi _plexTvApi; private readonly IPlexTvApi _plexTvApi;
public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore) public PlexTvApiClient(IPlexTvApi plexTvApi, IPlexSecretStore plexSecretStore, ILogger<PlexTvApiClient> logger)
{ {
_plexTvApi = plexTvApi; _plexTvApi = plexTvApi;
_plexSecretStore = plexSecretStore; _plexSecretStore = plexSecretStore;
_logger = logger;
} }
public async Task<Either<BaseError, List<PlexMediaSource>>> GetServers() public async Task<Either<BaseError, List<PlexMediaSource>>> GetServers()
@ -85,11 +88,12 @@ namespace ErsatzTV.Infrastructure.Plex
{ {
await _plexSecretStore.DeleteAll(); await _plexSecretStore.DeleteAll();
} }
return BaseError.New(apiException.Message); return BaseError.New(apiException.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting plex servers");
return BaseError.New(ex.Message); return BaseError.New(ex.Message);
} }
} }
@ -104,6 +108,7 @@ namespace ErsatzTV.Infrastructure.Plex
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error starting plex pin flow");
return BaseError.New(ex.Message); return BaseError.New(ex.Message);
} }
} }
@ -130,9 +135,9 @@ namespace ErsatzTV.Infrastructure.Plex
return true; return true;
} }
} }
catch (Exception) catch (Exception ex)
{ {
// ignored _logger.LogError(ex, "Error completing plex pin flow");
} }
return false; return false;

6
ErsatzTV/Pages/FFmpeg.razor

@ -29,6 +29,12 @@
} }
</MudSelect> </MudSelect>
</MudElement> </MudElement>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch T="bool"
Label="Save troubleshooting reports to disk"
Color="Color.Primary"
@bind-Checked="@_ffmpegSettings.SaveReports" />
</MudElement>
</MudForm> </MudForm>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>

25
ErsatzTV/Pages/MediaSources.razor

@ -1,25 +0,0 @@
@* @page "/media/sources" *@
@* @page "/media/sources/{PanelIndex:int}" *@
@* @inject NavigationManager NavigationManager *@
@* *@
@* <MudContainer MaxWidth="MaxWidth.ExtraLarge"> *@
@* <MudTabs @ref="_tabs" Elevation="1"> *@
@* <MudTabPanel Text="Local" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/0"))"> *@
@* <LocalMediaSources></LocalMediaSources> *@
@* </MudTabPanel> *@
@* <MudTabPanel Text="Plex" OnClick="@(() => NavigationManager.NavigateTo("/media/sources/1"))"> *@
@* <PlexMediaSources></PlexMediaSources> *@
@* </MudTabPanel> *@
@* </MudTabs> *@
@* </MudContainer> *@
@* *@
@* @code { *@
@* *@
@* [Parameter] *@
@* public int PanelIndex { get; set; } *@
@* *@
@* private MudTabs _tabs; *@
@* *@
@* protected override void OnAfterRender(bool firstRender) => _tabs.ActivatePanel(PanelIndex); *@
@* *@
@* } *@
Loading…
Cancel
Save