diff --git a/ErsatzTV.Application/Channels/ChannelViewModel.cs b/ErsatzTV.Application/Channels/ChannelViewModel.cs
index e4f55ec05..e4693c7a8 100644
--- a/ErsatzTV.Application/Channels/ChannelViewModel.cs
+++ b/ErsatzTV.Application/Channels/ChannelViewModel.cs
@@ -22,6 +22,7 @@ public record ChannelViewModel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
+ NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannel.cs b/ErsatzTV.Application/Channels/Commands/CreateChannel.cs
index 9a5b138d1..feaa51f67 100644
--- a/ErsatzTV.Application/Channels/Commands/CreateChannel.cs
+++ b/ErsatzTV.Application/Channels/Commands/CreateChannel.cs
@@ -21,6 +21,7 @@ public record CreateChannel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
+ NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
index b6d24197b..37ce3c9b7 100644
--- a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
+++ b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
@@ -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,
diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
index 844893cda..9e8bee01a 100644
--- a/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
+++ b/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
@@ -22,6 +22,7 @@ public record UpdateChannel(
int? MirrorSourceChannelId,
TimeSpan? PlayoutOffset,
StreamingEngine StreamingEngine,
+ NextEngineTextSubtitleMode NextEngineTextSubtitleMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
index c05af5cdf..445b1d4bd 100644
--- a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
+++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
@@ -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;
diff --git a/ErsatzTV.Application/Channels/Mapper.cs b/ErsatzTV.Application/Channels/Mapper.cs
index 4c59ba569..401b9d7cc 100644
--- a/ErsatzTV.Application/Channels/Mapper.cs
+++ b/ErsatzTV.Application/Channels/Mapper.cs
@@ -25,6 +25,7 @@ internal static class Mapper
channel.MirrorSourceChannelId,
channel.PlayoutOffset,
channel.StreamingEngine,
+ channel.NextEngineTextSubtitleMode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj
index 00f38e17b..32369f792 100644
--- a/ErsatzTV.Application/ErsatzTV.Application.csproj
+++ b/ErsatzTV.Application/ErsatzTV.Application.csproj
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
index b57ccbac5..c459723e9 100644
--- a/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
+++ b/ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
@@ -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(
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(
//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;
}
diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
index d54eb5099..81bb2eaeb 100644
--- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
+++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
@@ -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;
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(
await mediator.Send(new RefreshGraphicsElements(), cancellationToken);
ChannelConfig config = await MapConfig(
- request.ChannelNumber,
+ validationResult.Channel,
validationResult.FfmpegProfile,
cancellationToken);
@@ -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 maybeStreamingSpecs =
await mediator.Send(new GetChannelStreamingSpecs(request.ChannelNumber));
string resolution = string.Empty;
@@ -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 MapConfig(
- string channelNumber,
+ ChannelViewModel channel,
FFmpegProfileViewModel ffmpegProfile,
CancellationToken cancellationToken)
{
@@ -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(
Normalization = new Normalization
{
Audio = audioNormalization,
- Video = videoNormalization
+ Video = videoNormalization,
+ Subtitle = subtitleNormalization
}
};
}
diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
index 1bd40e366..20287e7a8 100644
--- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
+++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
@@ -9,12 +9,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/ErsatzTV.Core/Domain/Channel.cs b/ErsatzTV.Core/Domain/Channel.cs
index fb7970e21..64c3ba362 100644
--- a/ErsatzTV.Core/Domain/Channel.cs
+++ b/ErsatzTV.Core/Domain/Channel.cs
@@ -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 Playouts { get; set; }
public List Artwork { get; set; }
diff --git a/ErsatzTV.Core/Domain/NextEngineSubtitleMode.cs b/ErsatzTV.Core/Domain/NextEngineSubtitleMode.cs
new file mode 100644
index 000000000..b387b6251
--- /dev/null
+++ b/ErsatzTV.Core/Domain/NextEngineSubtitleMode.cs
@@ -0,0 +1,7 @@
+namespace ErsatzTV.Core.Domain;
+
+public enum NextEngineTextSubtitleMode
+{
+ Burn = 0,
+ Convert = 1,
+}
diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj
index 7656ee9a2..98e6a64c3 100644
--- a/ErsatzTV.Core/ErsatzTV.Core.csproj
+++ b/ErsatzTV.Core/ErsatzTV.Core.csproj
@@ -16,10 +16,10 @@
-
-
-
-
+
+
+
+
@@ -27,10 +27,10 @@
-
+
-
+
diff --git a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
index 57d0fde00..e1e74f711 100644
--- a/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
+++ b/ErsatzTV.Core/FFmpeg/FFmpegStreamSelector.cs
@@ -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
channel,
allLanguageCodes,
version.MediaItem.Id,
- version.MediaVersion);
+ version.MediaVersion,
+ shouldLogMessages);
sw2.Stop();
if (shouldLogMessages)
{
@@ -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