Browse Source

feat: select audio streams for next engine during playout build (#2866)

pull/2867/head
Jason Dove 1 month ago committed by GitHub
parent
commit
e6496bbc83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 77
      ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
  2. 2
      ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs
  3. 1
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  4. 40
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  5. 1
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs
  6. 1
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

77
ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs

@ -6,7 +6,9 @@ using CliWrap;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
@ -14,7 +16,6 @@ using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PlayoutItem = ErsatzTV.Core.Domain.PlayoutItem;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
@ -24,6 +25,8 @@ public partial class SyncNextPlayoutHandler(
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService, IEmbyPathReplacementService embyPathReplacementService,
ICustomStreamSelector customStreamSelector,
IFFmpegStreamSelector ffmpegStreamSelector,
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILogger<SyncNextPlayoutHandler> logger) ILogger<SyncNextPlayoutHandler> logger)
: IRequestHandler<SyncNextPlayout> : IRequestHandler<SyncNextPlayout>
@ -223,6 +226,21 @@ public partial class SyncNextPlayoutHandler(
}; };
} }
maybeChannel = await dbContext.Channels
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Number == channelNumber, cancellationToken);
foreach (Channel channel in maybeChannel)
{
var audioVersion = new MediaItemAudioVersion(playoutItem.MediaItem, headVersion);
await SelectTracks(
channel,
audioVersion,
nextPlayoutItem,
playoutItem.PreferredAudioLanguageCode ?? channel.PreferredAudioLanguageCode,
playoutItem.PreferredAudioTitle ?? channel.PreferredAudioTitle,
cancellationToken);
}
playout.Items.Add(nextPlayoutItem); playout.Items.Add(nextPlayoutItem);
} }
@ -230,6 +248,63 @@ public partial class SyncNextPlayoutHandler(
} }
} }
private async Task SelectTracks(
Channel channel,
MediaItemAudioVersion audioVersion,
Core.Next.PlayoutItem nextPlayoutItem,
string preferredAudioLanguage,
string preferredAudioTitle,
CancellationToken cancellationToken)
{
// TODO: NEXT: support subtitles
List<Subtitle> allSubtitles = [];
Option<MediaStream> maybeAudioStream = Option<MediaStream>.None;
//Option<Subtitle> maybeSubtitle = Option<Subtitle>.None;
if (channel.StreamSelectorMode is ChannelStreamSelectorMode.Custom)
{
StreamSelectorResult result = await customStreamSelector.SelectStreams(
channel,
nextPlayoutItem.Start,
audioVersion,
allSubtitles);
maybeAudioStream = result.AudioStream;
//maybeSubtitle = result.Subtitle;
}
if (channel.StreamSelectorMode is ChannelStreamSelectorMode.Default || maybeAudioStream.IsNone)
{
maybeAudioStream =
await ffmpegStreamSelector.SelectAudioStream(
audioVersion,
channel.StreamingMode,
channel,
preferredAudioLanguage,
preferredAudioTitle,
shouldLogMessages: false,
cancellationToken);
// maybeSubtitle =
// await ffmpegStreamSelector.SelectSubtitleStream(
// allSubtitles.ToImmutableList(),
// channel,
// preferredSubtitleLanguage,
// subtitleMode,
// cancellationToken);
}
foreach (MediaStream audioStream in maybeAudioStream)
{
if (nextPlayoutItem.Tracks?.Audio?.StreamIndex is null)
{
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Audio ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Audio.StreamIndex = audioStream.Index;
}
}
}
private async Task<Option<Core.Next.Source>> SourceForItem( private async Task<Option<Core.Next.Source>> SourceForItem(
PlayoutItem playoutItem, PlayoutItem playoutItem,
CancellationToken cancellationToken) CancellationToken cancellationToken)

2
ErsatzTV.Core.Tests/FFmpeg/FFmpegStreamSelectorTests.cs

@ -72,6 +72,7 @@ public class FFmpegStreamSelectorTests
channel, channel,
"jpn", "jpn",
"Whatever", "Whatever",
shouldLogMessages: false,
cancellationToken); cancellationToken);
selectedStream.IsSome.ShouldBeTrue(); selectedStream.IsSome.ShouldBeTrue();
foreach (MediaStream stream in selectedStream) foreach (MediaStream stream in selectedStream)
@ -134,6 +135,7 @@ public class FFmpegStreamSelectorTests
channel, channel,
null, null,
channel.PreferredAudioTitle, channel.PreferredAudioTitle,
shouldLogMessages: false,
cancellationToken); cancellationToken);
selectedStream.IsSome.ShouldBeTrue(); selectedStream.IsSome.ShouldBeTrue();
foreach (MediaStream stream in selectedStream) foreach (MediaStream stream in selectedStream)

1
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -153,6 +153,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
channel, channel,
preferredAudioLanguage, preferredAudioLanguage,
preferredAudioTitle, preferredAudioTitle,
shouldLogMessages: true,
cancellationToken); cancellationToken);
maybeSubtitle = maybeSubtitle =

40
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -47,21 +47,30 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
Channel channel, Channel channel,
string preferredAudioLanguage, string preferredAudioLanguage,
string preferredAudioTitle, string preferredAudioTitle,
bool shouldLogMessages,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (streamingMode == StreamingMode.HttpLiveStreamingDirect && if (streamingMode == StreamingMode.HttpLiveStreamingDirect &&
string.IsNullOrWhiteSpace(preferredAudioLanguage) && string.IsNullOrWhiteSpace(preferredAudioTitle)) string.IsNullOrWhiteSpace(preferredAudioLanguage) && string.IsNullOrWhiteSpace(preferredAudioTitle))
{ {
_logger.LogDebug( if (shouldLogMessages)
"Channel {Number} is HLS Direct with no preferred audio language or title; using all audio streams", {
channel.Number); _logger.LogDebug(
"Channel {Number} is HLS Direct with no preferred audio language or title; using all audio streams",
channel.Number);
}
return None; return None;
} }
string language = (preferredAudioLanguage ?? string.Empty).ToLowerInvariant(); string language = (preferredAudioLanguage ?? string.Empty).ToLowerInvariant();
if (string.IsNullOrWhiteSpace(language)) if (string.IsNullOrWhiteSpace(language))
{ {
_logger.LogDebug("Channel {Number} has no preferred audio language code", channel.Number); if (shouldLogMessages)
{
_logger.LogDebug("Channel {Number} has no preferred audio language code", channel.Number);
}
Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>( Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>(
ConfigElementKey.FFmpegPreferredLanguageCode, ConfigElementKey.FFmpegPreferredLanguageCode,
cancellationToken); cancellationToken);
@ -69,7 +78,11 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
lang => language = lang.ToLowerInvariant(), lang => language = lang.ToLowerInvariant(),
() => () =>
{ {
_logger.LogDebug("FFmpeg has no preferred audio language code; falling back to {Code}", "eng"); if (shouldLogMessages)
{
_logger.LogDebug("FFmpeg has no preferred audio language code; falling back to {Code}", "eng");
}
language = "eng"; language = "eng";
}); });
} }
@ -78,7 +91,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language])); GetTwoAndThreeLetterLanguageCodes(_languageCodeService.GetAllLanguageCodes([language]));
if (allLanguageCodes.Count > 1) if (allLanguageCodes.Count > 1)
{ {
_logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes); if (shouldLogMessages)
{
_logger.LogDebug("Preferred audio language has multiple codes {Codes}", allLanguageCodes);
}
} }
try try
@ -93,7 +109,11 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
version.MediaItem.Id, version.MediaItem.Id,
version.MediaVersion); version.MediaVersion);
sw.Stop(); sw.Stop();
_logger.LogDebug("SelectAudioStream duration: {Duration}", sw.Elapsed); if (shouldLogMessages)
{
_logger.LogDebug("SelectAudioStream duration: {Duration}", sw.Elapsed);
}
if (result.IsSome) if (result.IsSome)
{ {
return result; return result;
@ -108,7 +128,11 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
version.MediaItem.Id, version.MediaItem.Id,
version.MediaVersion); version.MediaVersion);
sw2.Stop(); sw2.Stop();
_logger.LogDebug("SelectAudioStream duration: {Duration}", sw2.Elapsed); if (shouldLogMessages)
{
_logger.LogDebug("SelectAudioStream duration: {Duration}", sw2.Elapsed);
}
if (result2.IsSome) if (result2.IsSome)
{ {
return result2; return result2;

1
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegStreamSelector.cs

@ -14,6 +14,7 @@ public interface IFFmpegStreamSelector
Channel channel, Channel channel,
string preferredAudioLanguage, string preferredAudioLanguage,
string preferredAudioTitle, string preferredAudioTitle,
bool shouldLogMessages,
CancellationToken cancellationToken); CancellationToken cancellationToken);
Task<Option<Subtitle>> SelectSubtitleStream( Task<Option<Subtitle>> SelectSubtitleStream(

1
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -1186,6 +1186,7 @@ public class TranscodingTests
Channel channel, Channel channel,
string preferredAudioLanguage, string preferredAudioLanguage,
string preferredAudioTitle, string preferredAudioTitle,
bool shouldLogMessages,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Optional(version.MediaVersion.Streams.FirstOrDefault(s => s.MediaStreamKind == MediaStreamKind.Audio)) Optional(version.MediaVersion.Streams.FirstOrDefault(s => s.MediaStreamKind == MediaStreamKind.Audio))
.AsTask(); .AsTask();

Loading…
Cancel
Save