Browse Source

improve stream error handling

pull/72/head
Jason Dove 5 years ago
parent
commit
ff0e312fcc
  1. 139
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  2. 9
      ErsatzTV.Core/Errors/PlayoutItemDoesNotExistOnDisk.cs
  3. 9
      ErsatzTV.Core/Errors/UnableToLocatePlayoutItem.cs
  4. 4
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  5. 4
      ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs

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

@ -6,10 +6,13 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
@ -17,6 +20,7 @@ namespace ErsatzTV.Application.Streaming.Queries
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber> GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{ {
private readonly FFmpegProcessService _ffmpegProcessService; private readonly FFmpegProcessService _ffmpegProcessService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger; private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlayoutRepository _playoutRepository; private readonly IPlayoutRepository _playoutRepository;
@ -27,12 +31,14 @@ namespace ErsatzTV.Application.Streaming.Queries
IPlayoutRepository playoutRepository, IPlayoutRepository playoutRepository,
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
FFmpegProcessService ffmpegProcessService, FFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger) ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
: base(channelRepository, configElementRepository) : base(channelRepository, configElementRepository)
{ {
_playoutRepository = playoutRepository; _playoutRepository = playoutRepository;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessService = ffmpegProcessService;
_localFileSystem = localFileSystem;
_logger = logger; _logger = logger;
} }
@ -42,49 +48,124 @@ namespace ErsatzTV.Application.Streaming.Queries
string ffmpegPath) string ffmpegPath)
{ {
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
Option<PlayoutItem> maybePlayoutItem = await _playoutRepository.GetPlayoutItem(channel.Id, now); Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await _playoutRepository
return await maybePlayoutItem.Match<Task<Either<BaseError, Process>>>( .GetPlayoutItem(channel.Id, now)
async playoutItem => .Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
.BindT(ValidatePlayoutItemPath);
return await maybePlayoutItem.Match(
playoutItemWithPath =>
{ {
MediaVersion version = playoutItem.MediaItem switch MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch
{ {
Movie m => m.MediaVersions.Head(), Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(), Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
}; };
MediaFile file = version.MediaFiles.Head(); return Right<BaseError, Process>(
string path = file.Path; _ffmpegProcessService.ForPlayoutItem(
if (playoutItem.MediaItem is PlexMovie plexMovie) ffmpegPath,
{ channel,
path = await GetReplacementPlexPath(plexMovie.LibraryPathId, path); version,
} playoutItemWithPath.Path,
playoutItemWithPath.PlayoutItem.StartOffset,
return _ffmpegProcessService.ForPlayoutItem( now)).AsTask();
ffmpegPath,
channel,
version,
path,
playoutItem.StartOffset,
now);
}, },
async () => async error =>
{ {
if (channel.FFmpegProfile.Transcode) var offlineTranscodeMessage =
$"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'";
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
.Filter(transcode => transcode)
.Match(
_ => _playoutRepository.GetNextItemStart(channel.Id, now)
.MapT(nextStart => nextStart - now),
() => Option<TimeSpan>.None.AsTask());
switch (error)
{ {
Option<TimeSpan> maybeDuration = await _playoutRepository.GetNextItemStart(channel.Id, now) case UnableToLocatePlayoutItem:
.MapT(nextStart => nextStart - now); if (channel.FFmpegProfile.Transcode)
{
return _ffmpegProcessService.ForError(
ffmpegPath,
channel,
maybeDuration,
"Channel is Offline");
}
else
{
var message =
$"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}";
return _ffmpegProcessService.ForOfflineImage(ffmpegPath, channel, maybeDuration); return BaseError.New(message);
} }
case PlayoutItemDoesNotExistOnDisk:
if (channel.FFmpegProfile.Transcode)
{
return _ffmpegProcessService.ForError(ffmpegPath, channel, maybeDuration, error.Value);
}
else
{
var message =
$"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}";
var message = return BaseError.New(message);
$"Unable to locate playout item for channel {channel.Number}; offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'"; }
default:
if (channel.FFmpegProfile.Transcode)
{
return _ffmpegProcessService.ForError(
ffmpegPath,
channel,
maybeDuration,
"Channel is Offline");
}
else
{
var message =
$"Unexpected error locating playout item for channel {channel.Number}; {offlineTranscodeMessage}";
return BaseError.New(message); return BaseError.New(message);
}
}
}); });
} }
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(PlayoutItem playoutItem)
{
string path = await GetPlayoutItemPath(playoutItem);
// TODO: this won't work with url streaming from plex
if (_localFileSystem.FileExists(path))
{
return new PlayoutItemWithPath(playoutItem, path);
}
return new PlayoutItemDoesNotExistOnDisk(path);
}
private async Task<string> GetPlayoutItemPath(PlayoutItem playoutItem)
{
MediaVersion version = playoutItem.MediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem))
};
MediaFile file = version.MediaFiles.Head();
string path = file.Path;
if (playoutItem.MediaItem is PlexMovie plexMovie)
{
path = await GetReplacementPlexPath(plexMovie.LibraryPathId, path);
}
return path;
}
private async Task<string> GetReplacementPlexPath(int libraryPathId, string path) private async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
{ {
List<PlexPathReplacement> replacements = List<PlexPathReplacement> replacements =
@ -105,5 +186,7 @@ namespace ErsatzTV.Application.Streaming.Queries
}, },
() => path); () => path);
} }
private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path);
} }
} }

9
ErsatzTV.Core/Errors/PlayoutItemDoesNotExistOnDisk.cs

@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Errors
{
public class PlayoutItemDoesNotExistOnDisk : BaseError
{
public PlayoutItemDoesNotExistOnDisk(string path) : base($"Playout item does not exist on disk\n{path}")
{
}
}
}

9
ErsatzTV.Core/Errors/UnableToLocatePlayoutItem.cs

@ -0,0 +1,9 @@
namespace ErsatzTV.Core.Errors
{
public class UnableToLocatePlayoutItem : BaseError
{
public UnableToLocatePlayoutItem() : base("Unable to locate playout item")
{
}
}
}

4
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -84,7 +84,7 @@ namespace ErsatzTV.Core.FFmpeg
.Build(); .Build();
} }
public Process ForOfflineImage(string ffmpegPath, Channel channel, Option<TimeSpan> duration) public Process ForError(string ffmpegPath, Channel channel, Option<TimeSpan> duration, string errorMessage)
{ {
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
@ -99,7 +99,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithLoopedImage("Resources/background.png") .WithLoopedImage("Resources/background.png")
.WithLibavfilter() .WithLibavfilter()
.WithInput("anullsrc") .WithInput("anullsrc")
.WithErrorText(desiredResolution, "Channel is Offline") .WithErrorText(desiredResolution, errorMessage)
.WithPixfmt("yuv420p") .WithPixfmt("yuv420p")
.WithPlaybackArgs(playbackSettings) .WithPlaybackArgs(playbackSettings)
.WithMetadata(channel) .WithMetadata(channel)

4
ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs

@ -61,8 +61,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<DateTimeOffset>> GetNextItemStart(int channelId, DateTimeOffset now) => public Task<Option<DateTimeOffset>> GetNextItemStart(int channelId, DateTimeOffset now) =>
_dbContext.PlayoutItems _dbContext.PlayoutItems
.Where(pi => pi.Playout.ChannelId == channelId) .Where(pi => pi.Playout.ChannelId == channelId)
.Where(pi => pi.Finish > now.UtcDateTime) .Where(pi => pi.Start > now.UtcDateTime)
.OrderBy(pi => pi.Finish) .OrderBy(pi => pi.Start)
.FirstOrDefaultAsync() .FirstOrDefaultAsync()
.Map(Optional) .Map(Optional)
.MapT(pi => pi.StartOffset); .MapT(pi => pi.StartOffset);

Loading…
Cancel
Save