Browse Source

feat: convert text subtitles using next engine (#2869)

* feat: convert text subtitles using next engine

* update dependencies
pull/2870/head
Jason Dove 3 weeks ago committed by GitHub
parent
commit
1b5d6af777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  2. 1
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  3. 1
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  4. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  5. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  6. 1
      ErsatzTV.Application/Channels/Mapper.cs
  7. 4
      ErsatzTV.Application/ErsatzTV.Application.csproj
  8. 29
      ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
  9. 26
      ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
  10. 12
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  11. 1
      ErsatzTV.Core/Domain/Channel.cs
  12. 7
      ErsatzTV.Core/Domain/NextEngineSubtitleMode.cs
  13. 12
      ErsatzTV.Core/ErsatzTV.Core.csproj
  14. 223
      ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
  15. 59
      ErsatzTV.Core/Next/Config/ChannelConfig.cs
  16. 6
      ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
  17. 4
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  18. 7051
      ErsatzTV.Infrastructure.MySql/Migrations/20260429142640_Add_ChannelNextEngineTextSubtitleMode.Designer.cs
  19. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20260429142640_Add_ChannelNextEngineTextSubtitleMode.cs
  20. 3
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  21. 6878
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260429142552_Add_ChannelNextEngineTextSubtitleMode.Designer.cs
  22. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260429142552_Add_ChannelNextEngineTextSubtitleMode.cs
  23. 3
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  24. 2
      ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
  25. 2
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  26. 2
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  27. 8
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  28. 18
      ErsatzTV/ErsatzTV.csproj
  29. 14
      ErsatzTV/Pages/ChannelEditor.razor
  30. 1
      ErsatzTV/Shared/ChannelPreviewDialog.razor
  31. 3
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

1
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -22,6 +22,7 @@ public record ChannelViewModel( @@ -22,6 +22,7 @@ public record ChannelViewModel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -21,6 +21,7 @@ public record CreateChannel( @@ -21,6 +21,7 @@ public record CreateChannel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -86,6 +86,7 @@ public class CreateChannelHandler( @@ -86,6 +86,7 @@ public class CreateChannelHandler(
MirrorSourceChannelId = request.MirrorSourceChannelId,
PlayoutOffset = request.PlayoutOffset,
StreamingEngine = request.StreamingEngine,
NextEngineTextSubtitleMode = request.NextEngineTextSubtitleMode,
StreamingMode = request.StreamingMode,
Artwork = artwork,
StreamSelectorMode = request.StreamSelectorMode,

1
ErsatzTV.Application/Channels/Commands/UpdateChannel.cs

@ -22,6 +22,7 @@ public record UpdateChannel( @@ -22,6 +22,7 @@ public record UpdateChannel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,

1
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -133,6 +133,7 @@ public class UpdateChannelHandler( @@ -133,6 +133,7 @@ public class UpdateChannelHandler(
c.MirrorSourceChannelId = update.MirrorSourceChannelId;
c.PlayoutOffset = update.PlayoutOffset;
c.StreamingEngine = update.StreamingEngine;
c.NextEngineTextSubtitleMode = update.NextEngineTextSubtitleMode;
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;

1
ErsatzTV.Application/Channels/Mapper.cs

@ -25,6 +25,7 @@ internal static class Mapper @@ -25,6 +25,7 @@ internal static class Mapper
channel.MirrorSourceChannelId,
channel.PlayoutOffset,
channel.StreamingEngine,
channel.NextEngineTextSubtitleMode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,

4
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -14,8 +14,8 @@ @@ -14,8 +14,8 @@
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.21.0" />

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

@ -17,6 +17,7 @@ using ErsatzTV.Infrastructure.Data; @@ -17,6 +17,7 @@ using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PlayoutItem = ErsatzTV.Core.Domain.PlayoutItem;
namespace ErsatzTV.Application.Playouts;
@ -320,11 +321,27 @@ public partial class SyncNextPlayoutHandler( @@ -320,11 +321,27 @@ public partial class SyncNextPlayoutHandler(
foreach (Subtitle subtitle in maybeSubtitle)
{
if (nextPlayoutItem.Tracks?.Subtitle?.StreamIndex is null)
if (subtitle.SubtitleKind is SubtitleKind.Embedded)
{
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Subtitle.StreamIndex = subtitle.StreamIndex;
if (nextPlayoutItem.Tracks?.Subtitle?.StreamIndex is null)
{
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Subtitle.StreamIndex = subtitle.StreamIndex;
}
}
else if (!subtitle.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
if (nextPlayoutItem.Tracks?.Subtitle?.Source is null)
{
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle ??= new Core.Next.TrackSelection();
nextPlayoutItem.Tracks.Subtitle.Source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Local,
Path = subtitle.Path,
};
}
}
}
}
@ -484,8 +501,8 @@ public partial class SyncNextPlayoutHandler( @@ -484,8 +501,8 @@ public partial class SyncNextPlayoutHandler(
//allSubtitles.RemoveAll(s => s.Codec == "eia_608");
}
// TODO: support text subtitles; external image subtitles
allSubtitles.RemoveAll(s => !s.IsImage || s.SubtitleKind is not SubtitleKind.Embedded);
// TODO: external image subtitles
allSubtitles.RemoveAll(s => s.IsImage && s.SubtitleKind is not SubtitleKind.Embedded);
return allSubtitles;
}

26
ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
using System.Globalization;
using System.IO.Abstractions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
@ -18,6 +17,7 @@ using ErsatzTV.Core.Next.Config; @@ -18,6 +17,7 @@ using ErsatzTV.Core.Next.Config;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Subtitle = ErsatzTV.Core.Next.Config.Subtitle;
namespace ErsatzTV.Application.Streaming;
@ -67,7 +67,7 @@ public class StartFFmpegNextSessionHandler( @@ -67,7 +67,7 @@ public class StartFFmpegNextSessionHandler(
await mediator.Send(new RefreshGraphicsElements(), cancellationToken);
ChannelConfig config = await MapConfig(
request.ChannelNumber,
validationResult.Channel,
validationResult.FfmpegProfile,
cancellationToken);
@ -230,6 +230,9 @@ public class StartFFmpegNextSessionHandler( @@ -230,6 +230,9 @@ public class StartFFmpegNextSessionHandler(
var variantPlaylist =
$"{request.Scheme}://{request.Host}{request.PathBase}/iptv/session/{request.ChannelNumber}/live.m3u8{request.AccessTokenQuery}";
var subtitlePlaylist =
$"{request.Scheme}://{request.Host}{request.PathBase}/iptv/session/{request.ChannelNumber}/live_sub.m3u8{request.AccessTokenQuery}";
Option<ChannelStreamingSpecsViewModel> maybeStreamingSpecs =
await mediator.Send(new GetChannelStreamingSpecs(request.ChannelNumber));
string resolution = string.Empty;
@ -268,13 +271,14 @@ public class StartFFmpegNextSessionHandler( @@ -268,13 +271,14 @@ public class StartFFmpegNextSessionHandler(
}
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-VERSION:6
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=""subs"",NAME=""English"",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE=""en"",URI=""{subtitlePlaylist}""
#EXT-X-STREAM-INF:BANDWIDTH={bitrate}{resolution}
{variantPlaylist}";
}
private async Task<ChannelConfig> MapConfig(
string channelNumber,
ChannelViewModel channel,
FFmpegProfileViewModel ffmpegProfile,
CancellationToken cancellationToken)
{
@ -355,7 +359,16 @@ public class StartFFmpegNextSessionHandler( @@ -355,7 +359,16 @@ public class StartFFmpegNextSessionHandler(
}
};
string playoutFolder = fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channelNumber, "current");
var subtitleNormalization = new Subtitle
{
Mode = channel.NextEngineTextSubtitleMode switch
{
NextEngineTextSubtitleMode.Convert => Mode.Convert,
_ => Mode.Burn
}
};
string playoutFolder = fileSystem.Path.Combine(FileSystemLayout.NextPlayoutsFolder, channel.Number, "current");
return new ChannelConfig
{
@ -367,7 +380,8 @@ public class StartFFmpegNextSessionHandler( @@ -367,7 +380,8 @@ public class StartFFmpegNextSessionHandler(
Normalization = new Normalization
{
Audio = audioNormalization,
Video = videoNormalization
Video = videoNormalization,
Subtitle = subtitleNormalization
}
};
}

12
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -9,12 +9,12 @@ @@ -9,12 +9,12 @@
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />

1
ErsatzTV.Core/Domain/Channel.cs

@ -23,6 +23,7 @@ public class Channel @@ -23,6 +23,7 @@ public class Channel
public int? FallbackFillerId { get; set; }
public FillerPreset FallbackFiller { get; set; }
public StreamingEngine StreamingEngine { get; set; }
public NextEngineTextSubtitleMode NextEngineTextSubtitleMode { get; set; }
public StreamingMode StreamingMode { get; set; }
public List<Playout> Playouts { get; set; }
public List<Artwork> Artwork { get; set; }

7
ErsatzTV.Core/Domain/NextEngineSubtitleMode.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum NextEngineTextSubtitleMode
{
Burn = 0,
Convert = 1,
}

12
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -16,10 +16,10 @@ @@ -16,10 +16,10 @@
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.8" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="NCalcSync" Version="5.12.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
@ -27,10 +27,10 @@ @@ -27,10 +27,10 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="SkiaSharp" Version="3.119.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.2" />
<PackageReference Include="System.CommandLine" Version="2.0.6" />
<PackageReference Include="System.CommandLine" Version="2.0.7" />
<PackageReference Include="Testably.Abstractions" Version="10.2.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="17.0.1" />
<PackageReference Include="YamlDotNet" Version="17.1.0" />
</ItemGroup>
<ItemGroup>

223
ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs

@ -107,7 +107,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -107,7 +107,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
channel,
allLanguageCodes,
version.MediaItem.Id,
version.MediaVersion);
version.MediaVersion,
shouldLogMessages);
sw.Stop();
if (shouldLogMessages)
{
@ -126,7 +127,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -126,7 +127,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
channel,
allLanguageCodes,
version.MediaItem.Id,
version.MediaVersion);
version.MediaVersion,
shouldLogMessages);
sw2.Stop();
if (shouldLogMessages)
{
@ -144,10 +146,13 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -144,10 +146,13 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to execute audio stream selector script; falling back to built-in logic");
if (shouldLogMessages)
{
_logger.LogError(ex, "Failed to execute audio stream selector script; falling back to built-in logic");
}
}
return DefaultSelectAudioStream(version.MediaVersion, allLanguageCodes, preferredAudioTitle);
return DefaultSelectAudioStream(version.MediaVersion, allLanguageCodes, preferredAudioTitle, shouldLogMessages);
}
public async Task<Option<Subtitle>> SelectSubtitleStream(
@ -176,47 +181,52 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -176,47 +181,52 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
var candidateSubtitles = subtitles.ToList();
bool useEmbeddedSubtitles = await _configElementRepository
.GetValue<bool>(ConfigElementKey.FFmpegUseEmbeddedSubtitles, cancellationToken)
.IfNoneAsync(true);
if (!useEmbeddedSubtitles)
// next engine doesn't need to specifically enable or pre-extract embedded subtitles
if (channel.StreamingEngine != StreamingEngine.Next)
{
if (shouldLogMessages)
bool useEmbeddedSubtitles = await _configElementRepository
.GetValue<bool>(ConfigElementKey.FFmpegUseEmbeddedSubtitles, cancellationToken)
.IfNoneAsync(true);
if (!useEmbeddedSubtitles)
{
_logger.LogDebug("Ignoring embedded subtitles for channel {Number}", channel.Number);
}
if (shouldLogMessages)
{
_logger.LogDebug("Ignoring embedded subtitles for channel {Number}", channel.Number);
}
candidateSubtitles = candidateSubtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList();
}
candidateSubtitles =
candidateSubtitles.Filter(s => s.SubtitleKind is not SubtitleKind.Embedded).ToList();
}
if (channel.StreamingMode is not StreamingMode.HttpLiveStreamingDirect)
{
foreach (Subtitle subtitle in candidateSubtitles
.Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage)
.ToList())
if (channel.StreamingMode is not StreamingMode.HttpLiveStreamingDirect)
{
if (!subtitle.IsExtracted)
foreach (Subtitle subtitle in candidateSubtitles
.Filter(s => s.SubtitleKind is SubtitleKind.Embedded && !s.IsImage)
.ToList())
{
if (shouldLogMessages)
if (!subtitle.IsExtracted)
{
_logger.LogDebug(
"Ignoring embedded subtitle with index {Index} that has not been extracted",
subtitle.StreamIndex);
if (shouldLogMessages)
{
_logger.LogDebug(
"Ignoring embedded subtitle with index {Index} that has not been extracted",
subtitle.StreamIndex);
}
candidateSubtitles.Remove(subtitle);
}
candidateSubtitles.Remove(subtitle);
}
else if (string.IsNullOrWhiteSpace(subtitle.Path))
{
if (shouldLogMessages)
else if (string.IsNullOrWhiteSpace(subtitle.Path))
{
_logger.LogDebug(
"BUG: ignoring embedded subtitle with index {Index} that is missing a path",
subtitle.StreamIndex);
if (shouldLogMessages)
{
_logger.LogDebug(
"BUG: ignoring embedded subtitle with index {Index} that is missing a path",
subtitle.StreamIndex);
}
candidateSubtitles.Remove(subtitle);
}
candidateSubtitles.Remove(subtitle);
}
}
}
@ -294,7 +304,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -294,7 +304,8 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
private Option<MediaStream> DefaultSelectAudioStream(
MediaVersion version,
IReadOnlyCollection<string> preferredLanguageCodes,
string preferredAudioTitle)
string preferredAudioTitle,
bool shouldLogMessages)
{
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
@ -304,26 +315,32 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -304,26 +315,32 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
if (correctLanguage.Count != 0)
{
_logger.LogDebug(
"Found {Count} audio streams with preferred audio language code(s) {Code}",
correctLanguage.Count,
preferredLanguageCodes);
if (shouldLogMessages)
{
_logger.LogDebug(
"Found {Count} audio streams with preferred audio language code(s) {Code}",
correctLanguage.Count,
preferredLanguageCodes);
}
return PrioritizeAudioTitle(correctLanguage, preferredAudioTitle ?? string.Empty);
return PrioritizeAudioTitle(correctLanguage, preferredAudioTitle ?? string.Empty, shouldLogMessages);
}
_logger.LogDebug(
"Unable to find audio stream with preferred audio language code(s) {Code}",
preferredLanguageCodes);
if (shouldLogMessages)
{
_logger.LogDebug(
"Unable to find audio stream with preferred audio language code(s) {Code}",
preferredLanguageCodes);
}
return PrioritizeAudioTitle(audioStreams, preferredAudioTitle ?? string.Empty);
return PrioritizeAudioTitle(audioStreams, preferredAudioTitle ?? string.Empty, shouldLogMessages);
}
private Option<MediaStream> PrioritizeAudioTitle(IReadOnlyCollection<MediaStream> streams, string title)
private Option<MediaStream> PrioritizeAudioTitle(IReadOnlyCollection<MediaStream> streams, string title, bool shouldLogMessages)
{
if (string.IsNullOrWhiteSpace(title))
{
return PrioritizeDefault(streams);
return PrioritizeDefault(streams, shouldLogMessages);
}
// prioritize matching titles
@ -332,30 +349,43 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -332,30 +349,43 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
.ToList();
if (matchingTitle.Count != 0)
{
_logger.LogDebug(
"Found {Count} audio streams with preferred title {Title}",
matchingTitle.Count,
title);
if (shouldLogMessages)
{
_logger.LogDebug(
"Found {Count} audio streams with preferred title {Title}",
matchingTitle.Count,
title);
}
return PrioritizeDefault(matchingTitle);
return PrioritizeDefault(matchingTitle, shouldLogMessages);
}
_logger.LogDebug("Unable to find audio stream with preferred title {Title}", title);
if (shouldLogMessages)
{
_logger.LogDebug("Unable to find audio stream with preferred title {Title}", title);
}
return PrioritizeDefault(streams);
return PrioritizeDefault(streams, shouldLogMessages);
}
private Option<MediaStream> PrioritizeDefault(IReadOnlyCollection<MediaStream> streams)
private Option<MediaStream> PrioritizeDefault(IReadOnlyCollection<MediaStream> streams, bool shouldLogMessages)
{
var sorted = streams.OrderByDescending(s => s.Channels).ToList();
Option<MediaStream> maybeDefault = Optional(sorted.Find(s => s.Default));
foreach (MediaStream stream in maybeDefault)
{
_logger.LogDebug("Found audio stream flagged as default");
if (shouldLogMessages)
{
_logger.LogDebug("Found audio stream flagged as default");
}
return stream;
}
_logger.LogDebug("Unable to find default audio stream; selecting stream with most channels");
if (shouldLogMessages)
{
_logger.LogDebug("Unable to find default audio stream; selecting stream with most channels");
}
return streams.HeadOrNone();
}
@ -364,20 +394,33 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -364,20 +394,33 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
Channel channel,
List<string> preferredLanguageCodes,
int episodeId,
MediaVersion version)
MediaVersion version,
bool shouldLogMessages)
{
string jsScriptPath = Path.ChangeExtension(
Path.Combine(FileSystemLayout.AudioStreamSelectorScriptsFolder, "episode"),
"js");
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (shouldLogMessages)
{
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
}
if (!_fileSystem.File.Exists(jsScriptPath))
{
_logger.LogDebug("Unable to locate episode audio stream selector script; falling back to built-in logic");
if (shouldLogMessages)
{
_logger.LogDebug(
"Unable to locate episode audio stream selector script; falling back to built-in logic");
}
return Option<MediaStream>.None;
}
_logger.LogDebug("Found JS Script at {Path}", jsScriptPath);
if (shouldLogMessages)
{
_logger.LogDebug("Found JS Script at {Path}", jsScriptPath);
}
await _scriptEngine.LoadAsync(jsScriptPath);
@ -397,28 +440,40 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -397,28 +440,40 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
preferredLanguageCodes.ToArray(),
audioStreams);
return ProcessScriptResult(version, result);
return ProcessScriptResult(version, result, shouldLogMessages);
}
private async Task<Option<MediaStream>> SelectMovieAudioStream(
Channel channel,
List<string> preferredLanguageCodes,
int movieId,
MediaVersion version)
MediaVersion version,
bool shouldLogMessages)
{
string jsScriptPath = Path.ChangeExtension(
Path.Combine(FileSystemLayout.AudioStreamSelectorScriptsFolder, "movie"),
"js");
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
if (shouldLogMessages)
{
_logger.LogDebug("Checking for JS Script at {Path}", jsScriptPath);
}
if (!_fileSystem.File.Exists(jsScriptPath))
{
_logger.LogDebug(
"Unable to locate movie audio stream selector script; falling back to built-in logic");
if (shouldLogMessages)
{
_logger.LogDebug(
"Unable to locate movie audio stream selector script; falling back to built-in logic");
}
return Option<MediaStream>.None;
}
_logger.LogDebug("Found JS Script at {Path}", jsScriptPath);
if (shouldLogMessages)
{
_logger.LogDebug("Found JS Script at {Path}", jsScriptPath);
}
await _scriptEngine.LoadAsync(jsScriptPath);
@ -435,10 +490,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -435,10 +490,10 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
preferredLanguageCodes.ToArray(),
audioStreams);
return ProcessScriptResult(version, result);
return ProcessScriptResult(version, result, shouldLogMessages);
}
private Option<MediaStream> ProcessScriptResult(MediaVersion version, object result)
private Option<MediaStream> ProcessScriptResult(MediaVersion version, object result, bool shouldLogMessages)
{
if (result is double d)
{
@ -446,22 +501,32 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector @@ -446,22 +501,32 @@ public class FFmpegStreamSelector : IFFmpegStreamSelector
Option<MediaStream> maybeStream = version.Streams.Find(s => s.Index == streamIndex);
foreach (MediaStream stream in maybeStream)
{
_logger.LogDebug(
"JS Script returned audio stream index {Index} with language {Language} and {Channels} audio channel(s)",
streamIndex,
stream.Language,
stream.Channels);
if (shouldLogMessages)
{
_logger.LogDebug(
"JS Script returned audio stream index {Index} with language {Language} and {Channels} audio channel(s)",
streamIndex,
stream.Language,
stream.Channels);
}
return stream;
}
_logger.LogWarning(
"JS Script returned audio stream index {Index} which does not exist",
streamIndex);
if (shouldLogMessages)
{
_logger.LogWarning(
"JS Script returned audio stream index {Index} which does not exist",
streamIndex);
}
}
else
{
_logger.LogInformation(
"JS Script did not return an audio stream index; falling back to built-in logic");
if (shouldLogMessages)
{
_logger.LogInformation(
"JS Script did not return an audio stream index; falling back to built-in logic");
}
}
return Option<MediaStream>.None;

59
ErsatzTV.Core/Next/Config/ChannelConfig.cs

@ -37,6 +37,9 @@ namespace ErsatzTV.Core.Next.Config @@ -37,6 +37,9 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("ffprobe_path")]
public string FfprobePath { get; set; }
[JsonProperty("preferred_filters", NullValueHandling = NullValueHandling.Ignore)]
public List<string> PreferredFilters { get; set; }
}
public partial class Normalization
@ -44,6 +47,9 @@ namespace ErsatzTV.Core.Next.Config @@ -44,6 +47,9 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("audio")]
public Audio Audio { get; set; }
[JsonProperty("subtitle", NullValueHandling = NullValueHandling.Ignore)]
public Subtitle Subtitle { get; set; }
[JsonProperty("video")]
public Video Video { get; set; }
}
@ -84,6 +90,12 @@ namespace ErsatzTV.Core.Next.Config @@ -84,6 +90,12 @@ namespace ErsatzTV.Core.Next.Config
public double? TruePeak { get; set; }
}
public partial class Subtitle
{
[JsonProperty("mode", NullValueHandling = NullValueHandling.Ignore)]
public Mode? Mode { get; set; }
}
public partial class Video
{
[JsonProperty("accel")]
@ -98,6 +110,9 @@ namespace ErsatzTV.Core.Next.Config @@ -98,6 +110,9 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("buffer_kbps")]
public long? BufferKbps { get; set; }
[JsonProperty("deinterlace", NullValueHandling = NullValueHandling.Ignore)]
public bool? Deinterlace { get; set; }
[JsonProperty("format")]
public VideoFormat? Format { get; set; }
@ -131,6 +146,8 @@ namespace ErsatzTV.Core.Next.Config @@ -131,6 +146,8 @@ namespace ErsatzTV.Core.Next.Config
public enum AudioFormat { Aac, Ac3 };
public enum Mode { Burn, Convert };
public enum AccelEnum { Cuda, Qsv, Vaapi, Videotoolbox, Vulkan };
public enum VideoFormat { H264, Hevc };
@ -156,6 +173,7 @@ namespace ErsatzTV.Core.Next.Config @@ -156,6 +173,7 @@ namespace ErsatzTV.Core.Next.Config
Converters =
{
AudioFormatConverter.Singleton,
ModeConverter.Singleton,
AccelEnumConverter.Singleton,
VideoFormatConverter.Singleton,
VaapiDriverEnumConverter.Singleton,
@ -205,6 +223,47 @@ namespace ErsatzTV.Core.Next.Config @@ -205,6 +223,47 @@ namespace ErsatzTV.Core.Next.Config
public static readonly AudioFormatConverter Singleton = new AudioFormatConverter();
}
internal class ModeConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(Mode) || t == typeof(Mode?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var value = serializer.Deserialize<string>(reader);
switch (value)
{
case "burn":
return Mode.Burn;
case "convert":
return Mode.Convert;
}
throw new Exception("Cannot unmarshal type Mode");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
if (untypedValue == null)
{
serializer.Serialize(writer, null);
return;
}
var value = (Mode)untypedValue;
switch (value)
{
case Mode.Burn:
serializer.Serialize(writer, "burn");
return;
case Mode.Convert:
serializer.Serialize(writer, "convert");
return;
}
throw new Exception("Cannot marshal type Mode");
}
public static readonly ModeConverter Singleton = new ModeConverter();
}
internal class AccelEnumConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(AccelEnum) || t == typeof(AccelEnum?);

6
ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj

@ -8,9 +8,9 @@ @@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="[8.0.2]" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />

4
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -15,8 +15,8 @@ @@ -15,8 +15,8 @@
<PackageReference Include="Hardware.Info" Version="101.1.1.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Lennox.NvEncSharp" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
</ItemGroup>

7051
ErsatzTV.Infrastructure.MySql/Migrations/20260429142640_Add_ChannelNextEngineTextSubtitleMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20260429142640_Add_ChannelNextEngineTextSubtitleMode.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelNextEngineTextSubtitleMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "NextEngineTextSubtitleMode",
table: "Channel",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NextEngineTextSubtitleMode",
table: "Channel");
}
}
}

3
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -331,6 +331,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -331,6 +331,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.HasColumnType("varchar(50)")
.UseCollation("utf8mb4_general_ci");
b.Property<int>("NextEngineTextSubtitleMode")
.HasColumnType("int");
b.Property<string>("Number")
.HasColumnType("varchar(255)");

6878
ErsatzTV.Infrastructure.Sqlite/Migrations/20260429142552_Add_ChannelNextEngineTextSubtitleMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20260429142552_Add_ChannelNextEngineTextSubtitleMode.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_ChannelNextEngineTextSubtitleMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "NextEngineTextSubtitleMode",
table: "Channel",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NextEngineTextSubtitleMode",
table: "Channel");
}
}
}

3
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -318,6 +318,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -318,6 +318,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasColumnType("varchar(50)")
.UseCollation("NOCASE");
b.Property<int>("NextEngineTextSubtitleMode")
.HasColumnType("INTEGER");
b.Property<string>("Number")
.HasColumnType("TEXT");

2
ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />

2
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="EFCore.BulkExtensions" Version="[9.0.2,10)" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.3.4" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.3.6" />
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="Jint" Version="4.8.0" />
<PackageReference Include="JsonSchema.Net" Version="9.2.0" />

2
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />

8
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -26,16 +26,16 @@ @@ -26,16 +26,16 @@
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.6" />
<PackageReference Include="System.CommandLine" Version="2.0.7" />
</ItemGroup>
<ItemGroup>

18
ErsatzTV/ErsatzTV.csproj

@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
<ItemGroup>
<!-- <PackageReference Include="EntityFrameworkProfiler.Appender" Version="6.0.6049" /> -->
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="BlazorSortable" Version="6.0.0" />
<PackageReference Include="BlazorSortable" Version="6.0.1" />
<PackageReference Include="Chronic.Core" Version="0.4.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
@ -39,23 +39,23 @@ @@ -39,23 +39,23 @@
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.6">
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="9.3.0" />
<PackageReference Include="MudBlazor" Version="9.4.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.4.1" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.1" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.6" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />

14
ErsatzTV/Pages/ChannelEditor.razor

@ -140,6 +140,18 @@ else @@ -140,6 +140,18 @@ else
<MudSelectItem Value="@(StreamingEngine.Next)">Next</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.StreamingEngine is StreamingEngine.Next)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Next Engine Text Subtitle Mode</MudText>
</div>
<MudSelect @bind-Value="_model.NextEngineTextSubtitleMode" For="@(() => _model.NextEngineTextSubtitleMode)">
<MudSelectItem Value="@(NextEngineTextSubtitleMode.Burn)">Burn</MudSelectItem>
<MudSelectItem Value="@(NextEngineTextSubtitleMode.Convert)">Convert If Possible</MudSelectItem>
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Streaming Mode</MudText>
@ -421,6 +433,7 @@ else @@ -421,6 +433,7 @@ else
_model.MirrorSourceChannelId = channelViewModel.MirrorSourceChannelId;
_model.PlayoutOffset = channelViewModel.PlayoutOffset;
_model.StreamingEngine = channelViewModel.StreamingEngine;
_model.NextEngineTextSubtitleMode = channelViewModel.NextEngineTextSubtitleMode;
_model.StreamingMode = channelViewModel.StreamingMode;
_model.StreamSelectorMode = channelViewModel.StreamSelectorMode;
_model.StreamSelector = channelViewModel.StreamSelector;
@ -453,6 +466,7 @@ else @@ -453,6 +466,7 @@ else
_model.Group = "ErsatzTV";
_model.FFmpegProfileId = ffmpegSettings.DefaultFFmpegProfileId;
_model.StreamingEngine = StreamingEngine.Legacy;
_model.NextEngineTextSubtitleMode = NextEngineTextSubtitleMode.Burn;
_model.StreamingMode = StreamingMode.TransportStreamHybrid;
_model.IsEnabled = true;
_model.ShowInEpg = true;

1
ErsatzTV/Shared/ChannelPreviewDialog.razor

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
<media-control-bar>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<media-captions-button></media-captions-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>

3
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -44,6 +44,7 @@ public class ChannelEditViewModel @@ -44,6 +44,7 @@ public class ChannelEditViewModel
}
}
public NextEngineTextSubtitleMode NextEngineTextSubtitleMode { get; set; }
public StreamingMode StreamingMode { get; set; }
public int? WatermarkId { get; set; }
public int? FallbackFillerId { get; set; }
@ -86,6 +87,7 @@ public class ChannelEditViewModel @@ -86,6 +87,7 @@ public class ChannelEditViewModel
MirrorSourceChannelId,
PlayoutOffset,
StreamingEngine,
NextEngineTextSubtitleMode,
StreamingMode,
WatermarkId,
FallbackFillerId,
@ -119,6 +121,7 @@ public class ChannelEditViewModel @@ -119,6 +121,7 @@ public class ChannelEditViewModel
MirrorSourceChannelId,
PlayoutOffset,
StreamingEngine,
NextEngineTextSubtitleMode,
StreamingMode,
WatermarkId,
FallbackFillerId,

Loading…
Cancel
Save