Browse Source

fix: subtitles with next engine

pull/2938/head
Jason Dove 1 month ago
parent
commit
1c2cab69b8
No known key found for this signature in database
  1. 3
      CHANGELOG.md
  2. 6
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  3. 136
      ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs
  4. 30
      ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs
  5. 3
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  6. 3
      ErsatzTV.Core/Interfaces/FFmpeg/ICustomStreamSelector.cs
  7. 31
      ErsatzTV.Core/Next/Playout.cs
  8. 11
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

3
CHANGELOG.md

@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- A config file named `{channel_number}.json` (e.g. `1.json`) will apply to the channel with that number - A config file named `{channel_number}.json` (e.g. `1.json`) will apply to the channel with that number
- Channel overlays will override values from the default overlay which will override values from the FFmpeg Profile - Channel overlays will override values from the default overlay which will override values from the FFmpeg Profile
- Add `epg_entries` support to image graphics elements - Add `epg_entries` support to image graphics elements
- Add (date-range based) alternate schedule system for sequential schedules
### Fixed ### Fixed
- Fix HLS Direct playback when JWT auth is also used - Fix HLS Direct playback when JWT auth is also used
@ -26,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use `und` language tag with sidecar subtitles that have no language in the file name - Use `und` language tag with sidecar subtitles that have no language in the file name
- Automatically remove already-played playout items from all on demand channels - Automatically remove already-played playout items from all on demand channels
- Automatically remove already-played playout items from (continuous) scripted schedules - Automatically remove already-played playout items from (continuous) scripted schedules
- Extract embedded text subtitles (when enabled in settings) on channels that use custom stream selectors
- Fix subtitle playback using Next streaming engine
### Changed ### Changed
- Upgrade Intel driver in docker containers to support latest Battlemage devices (e.g. B70) - Upgrade Intel driver in docker containers to support latest Battlemage devices (e.g. B70)

6
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -91,7 +91,8 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
.AsNoTracking() .AsNoTracking()
.Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None || .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => p.ProgramSchedule.Items.Any(psi =>
psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None)) psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None)
|| p.Channel.StreamSelectorMode == ChannelStreamSelectorMode.Custom)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1), cancellationToken); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1), cancellationToken);
playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id)); playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id));
@ -103,7 +104,8 @@ public class ExtractEmbeddedSubtitlesHandler : ExtractEmbeddedSubtitlesHandlerBa
.AsNoTracking() .AsNoTracking()
.Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None || .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p.ProgramSchedule.Items.Any(psi => p.ProgramSchedule.Items.Any(psi =>
psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None)) psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None)
|| p.Channel.StreamSelectorMode == ChannelStreamSelectorMode.Custom)
.Map(p => p.Id) .Map(p => p.Id)
.ToList(); .ToList();
} }

136
ErsatzTV.Core.Tests/FFmpeg/CustomStreamSelectorTests.cs

@ -104,7 +104,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -134,7 +135,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -164,7 +166,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -200,7 +203,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -231,7 +235,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -263,7 +268,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -295,7 +301,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeFalse(); result.Subtitle.IsSome.ShouldBeFalse();
} }
@ -322,7 +329,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -355,7 +363,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -397,7 +406,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -433,7 +443,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -471,7 +482,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -514,7 +526,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -551,7 +564,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM DateTimeOffset.Now.LocalDateTime.Date.AddHours(11).AddMinutes(59), // 11:59 AM
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -594,7 +608,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 11, 0, 0, 0, DateTimeKind.Unspecified); // sunday var start = new DateTime(2026, 1, 11, 0, 0, 0, DateTimeKind.Unspecified); // sunday
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -631,7 +650,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 11, 0, 0, 0, DateTimeKind.Unspecified); // sunday var start = new DateTime(2026, 1, 11, 0, 0, 0, DateTimeKind.Unspecified); // sunday
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -651,7 +675,8 @@ public class CustomStreamSelectorTests
} }
[Test] [Test]
public async Task Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_Before() public async Task
Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_Before()
{ {
// saturday from 9pm-11pm // saturday from 9pm-11pm
const string YAML = const string YAML =
@ -675,7 +700,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 10, 20, 59, 59, DateTimeKind.Unspecified); // saturday at 8:59:59pm var start = new DateTime(2026, 1, 10, 20, 59, 59, DateTimeKind.Unspecified); // saturday at 8:59:59pm
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -689,7 +719,8 @@ public class CustomStreamSelectorTests
} }
[Test] [Test]
public async Task Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_After() public async Task
Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_After()
{ {
// saturday from 9pm-11pm // saturday from 9pm-11pm
const string YAML = const string YAML =
@ -713,7 +744,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 10, 23, 0, 0, DateTimeKind.Unspecified); // saturday at 11:00pm var start = new DateTime(2026, 1, 10, 23, 0, 0, DateTimeKind.Unspecified); // saturday at 11:00pm
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -727,7 +763,8 @@ public class CustomStreamSelectorTests
} }
[Test] [Test]
public async Task Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_Wrong_Day() public async Task
Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Fail_Wrong_Day()
{ {
// saturday from 9pm-11pm // saturday from 9pm-11pm
const string YAML = const string YAML =
@ -751,7 +788,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 11, 22, 0, 0, DateTimeKind.Unspecified); // sunday at 10:00pm var start = new DateTime(2026, 1, 11, 22, 0, 0, DateTimeKind.Unspecified); // sunday at 10:00pm
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -789,7 +831,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 10, 22, 0, 0, DateTimeKind.Unspecified); // saturday at 10:00pm var start = new DateTime(2026, 1, 10, 22, 0, 0, DateTimeKind.Unspecified); // saturday at 10:00pm
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -810,7 +857,8 @@ public class CustomStreamSelectorTests
[Test] [Test]
[SetCulture("fr-FR")] [SetCulture("fr-FR")]
public async Task Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Match_France() public async Task
Should_Select_English_Audio_No_Subtitles_Day_Of_Week_Time_Of_Day_Content_Condition_Match_France()
{ {
// saturday from 9pm-11pm // saturday from 9pm-11pm
const string YAML = const string YAML =
@ -834,7 +882,12 @@ public class CustomStreamSelectorTests
var start = new DateTime(2026, 1, 10, 22, 0, 0, DateTimeKind.Unspecified); // saturday at 10:00pm var start = new DateTime(2026, 1, 10, 22, 0, 0, DateTimeKind.Unspecified); // saturday at 10:00pm
var dto = new DateTimeOffset(start, tz.GetUtcOffset(start)); var dto = new DateTimeOffset(start, tz.GetUtcOffset(start));
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, dto, _audioVersion, _subtitles); StreamSelectorResult result = await streamSelector.SelectStreams(
_channel,
dto,
_audioVersion,
_subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -875,7 +928,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -908,7 +962,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -943,7 +998,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -978,7 +1034,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -1010,7 +1067,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -1042,7 +1100,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -1074,7 +1133,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -1106,7 +1166,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -1137,7 +1198,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeTrue(); result.AudioStream.IsSome.ShouldBeTrue();
@ -1169,7 +1231,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.Subtitle.IsSome.ShouldBeTrue(); result.Subtitle.IsSome.ShouldBeTrue();
@ -1202,7 +1265,8 @@ public class CustomStreamSelectorTests
_channel, _channel,
DateTimeOffset.Now, DateTimeOffset.Now,
_audioVersion, _audioVersion,
_subtitles); _subtitles,
shouldLogMessages: true);
result.AudioStream.IsSome.ShouldBeFalse(); result.AudioStream.IsSome.ShouldBeFalse();
result.Subtitle.IsSome.ShouldBeFalse(); result.Subtitle.IsSome.ShouldBeFalse();

30
ErsatzTV.Core/FFmpeg/CustomStreamSelector.cs

@ -5,6 +5,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg.Selector; using ErsatzTV.Core.FFmpeg.Selector;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NCalc; using NCalc;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
@ -18,8 +19,11 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
Channel channel, Channel channel,
DateTimeOffset contentStartTime, DateTimeOffset contentStartTime,
MediaItemAudioVersion audioVersion, MediaItemAudioVersion audioVersion,
List<Subtitle> allSubtitles) List<Subtitle> allSubtitles,
bool shouldLogMessages)
{ {
ILogger<CustomStreamSelector> log = shouldLogMessages ? logger : NullLogger<CustomStreamSelector>.Instance;
try try
{ {
string streamSelectorFile = Path.Combine( string streamSelectorFile = Path.Combine(
@ -28,11 +32,11 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
if (!fileSystem.File.Exists(streamSelectorFile)) if (!fileSystem.File.Exists(streamSelectorFile))
{ {
logger.LogWarning("YAML stream selector file {File} does not exist; aborting.", channel.StreamSelector); log.LogWarning("YAML stream selector file {File} does not exist; aborting.", channel.StreamSelector);
return StreamSelectorResult.None; return StreamSelectorResult.None;
} }
StreamSelector streamSelector = await LoadStreamSelector(streamSelectorFile); StreamSelector streamSelector = await LoadStreamSelector(streamSelectorFile, log);
var audioStreams = audioVersion.MediaVersion.Streams var audioStreams = audioVersion.MediaVersion.Streams
.Where(s => s.MediaStreamKind == MediaStreamKind.Audio) .Where(s => s.MediaStreamKind == MediaStreamKind.Audio)
@ -44,7 +48,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
{ {
if (!ContentMatchesCondition(channel, contentStartTime, streamSelectorItem.ContentCondition)) if (!ContentMatchesCondition(channel, contentStartTime, streamSelectorItem.ContentCondition))
{ {
logger.LogDebug( log.LogDebug(
"Content does not match selector item {@SelectorItem}", "Content does not match selector item {@SelectorItem}",
streamSelectorItem); streamSelectorItem);
continue; continue;
@ -122,7 +126,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
{ {
candidateAudioStreams.Remove(audioStream); candidateAudioStreams.Remove(audioStream);
logger.LogDebug( log.LogDebug(
"Audio stream {@Stream} does not match selector item {@SelectorItem}", "Audio stream {@Stream} does not match selector item {@SelectorItem}",
new { Language = safeLanguage, Title = safeTitle }, new { Language = safeLanguage, Title = safeTitle },
streamSelectorItem); streamSelectorItem);
@ -131,7 +135,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
{ {
passesAudio = true; passesAudio = true;
logger.LogDebug( log.LogDebug(
"Audio stream {@Stream} matches selector item {@SelectorItem}", "Audio stream {@Stream} matches selector item {@SelectorItem}",
new { Language = safeLanguage, Title = safeTitle }, new { Language = safeLanguage, Title = safeTitle },
streamSelectorItem); streamSelectorItem);
@ -206,13 +210,15 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
} }
} }
// HLS Direct doesn't need to extract; Next streaming engine doesn't need to extract
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect &&
channel.StreamingEngine != StreamingEngine.Next &&
subtitle.SubtitleKind is SubtitleKind.Embedded && !subtitle.IsImage && subtitle.SubtitleKind is SubtitleKind.Embedded && !subtitle.IsImage &&
!subtitle.IsExtracted) !subtitle.IsExtracted)
{ {
candidateSubtitles.Remove(subtitle); candidateSubtitles.Remove(subtitle);
logger.LogDebug( log.LogDebug(
"Subtitle {@Subtitle} is embedded text subtitle and NOT extracted; ignoring", "Subtitle {@Subtitle} is embedded text subtitle and NOT extracted; ignoring",
new { Language = safeLanguage, Title = safeTitle }); new { Language = safeLanguage, Title = safeTitle });
} }
@ -220,7 +226,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
{ {
candidateSubtitles.Remove(subtitle); candidateSubtitles.Remove(subtitle);
logger.LogDebug( log.LogDebug(
"Subtitle {@Subtitle} does not match selector item {@SelectorItem}", "Subtitle {@Subtitle} does not match selector item {@SelectorItem}",
new { Language = safeLanguage, Title = safeTitle }, new { Language = safeLanguage, Title = safeTitle },
streamSelectorItem); streamSelectorItem);
@ -229,7 +235,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
{ {
passesSubtitles = true; passesSubtitles = true;
logger.LogDebug( log.LogDebug(
"Subtitle {@Subtitle} matches selector item {@SelectorItem}", "Subtitle {@Subtitle} matches selector item {@SelectorItem}",
new { Language = safeLanguage, Title = safeTitle }, new { Language = safeLanguage, Title = safeTitle },
streamSelectorItem); streamSelectorItem);
@ -255,7 +261,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "Unexpected error selecting streams"); log.LogError(ex, "Unexpected error selecting streams");
} }
return StreamSelectorResult.None; return StreamSelectorResult.None;
@ -325,7 +331,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
return expression.Evaluate() as bool? == true; return expression.Evaluate() as bool? == true;
} }
private async Task<StreamSelector> LoadStreamSelector(string streamSelectorFile) private async Task<StreamSelector> LoadStreamSelector(string streamSelectorFile, ILogger<CustomStreamSelector> log)
{ {
try try
{ {
@ -339,7 +345,7 @@ public class CustomStreamSelector(IFileSystem fileSystem, ILogger<CustomStreamSe
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogWarning(ex, "Error loading YAML stream selector"); log.LogWarning(ex, "Error loading YAML stream selector");
throw; throw;
} }
} }

3
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -132,7 +132,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
channel, channel,
start, start,
audioVersion, audioVersion,
allSubtitles); allSubtitles,
shouldLogMessages: true);
maybeAudioStream = result.AudioStream; maybeAudioStream = result.AudioStream;
maybeSubtitle = result.Subtitle; maybeSubtitle = result.Subtitle;

3
ErsatzTV.Core/Interfaces/FFmpeg/ICustomStreamSelector.cs

@ -9,5 +9,6 @@ public interface ICustomStreamSelector
Channel channel, Channel channel,
DateTimeOffset contentStartTime, DateTimeOffset contentStartTime,
MediaItemAudioVersion audioVersion, MediaItemAudioVersion audioVersion,
List<Subtitle> allSubtitles); List<Subtitle> allSubtitles,
bool shouldLogMessages);
} }

31
ErsatzTV.Core/Next/Playout.cs

@ -248,7 +248,8 @@ namespace ErsatzTV.Core.Next
/// ///
/// The hint fully replaces probing: values are not validated up front, so incorrect metadata /// The hint fully replaces probing: values are not validated up front, so incorrect metadata
/// surfaces as an ffmpeg error during playback rather than a probe failure. Provide an entry /// surfaces as an ffmpeg error during playback rather than a probe failure. Provide an entry
/// for every stream the pipeline needs to select — typically one video and one audio. /// for every stream the pipeline needs to select — typically one video and one audio, plus
/// any subtitle stream a track selects.
/// </summary> /// </summary>
public partial class ProbeHint public partial class ProbeHint
{ {
@ -272,6 +273,14 @@ namespace ErsatzTV.Core.Next
[JsonProperty("format_name")] [JsonProperty("format_name")]
public string FormatName { get; set; } public string FormatName { get; set; }
/// <summary>
/// Subtitle streams in the source. Omit (or use `[]`) for sources without subtitles;
/// defaults to empty. Provide an entry for any subtitle stream a track selects, otherwise
/// the requested subtitle stream cannot be located and subtitles are dropped.
/// </summary>
[JsonProperty("subtitle", NullValueHandling = NullValueHandling.Ignore)]
public List<SubtitleHint> Subtitle { get; set; }
/// <summary> /// <summary>
/// Video (and still-image) streams in the source. Omit (or use `[]`) for audio-only sources; /// Video (and still-image) streams in the source. Omit (or use `[]`) for audio-only sources;
/// defaults to empty. /// defaults to empty.
@ -304,6 +313,26 @@ namespace ErsatzTV.Core.Next
public long StreamIndex { get; set; } public long StreamIndex { get; set; }
} }
/// <summary>
/// Probe metadata for a single subtitle stream.
/// </summary>
public partial class SubtitleHint
{
/// <summary>
/// Codec name as reported by ffprobe (e.g. "subrip", "ass", "hdmv_pgs_subtitle",
/// "dvd_subtitle"). Compared case-insensitively; image-based codecs are handled differently
/// from text-based ones.
/// </summary>
[JsonProperty("codec")]
public string Codec { get; set; }
/// <summary>
/// Zero-based index of this stream within the source.
/// </summary>
[JsonProperty("stream_index")]
public long StreamIndex { get; set; }
}
/// <summary> /// <summary>
/// Probe metadata for a single video (or still-image) stream. Optional fields left out /// Probe metadata for a single video (or still-image) stream. Optional fields left out
/// assume progressive, square-pixel, SDR content at 24 fps — the same fallbacks used when /// assume progressive, square-pixel, SDR content at 24 fps — the same fallbacks used when

11
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -150,11 +150,19 @@ public class PlayoutItemConverter(
Codec = s.Codec, Codec = s.Codec,
Channels = s.Channels Channels = s.Channels
}).ToList(); }).ToList();
var sourceSubtitleHints = headVersion.Streams
.Where(s => s.MediaStreamKind is MediaStreamKind.Subtitle)
.Select(s => new Core.Next.SubtitleHint
{
StreamIndex = s.Index,
Codec = s.Codec
}).ToList();
nextPlayoutItem.Source.ProbeHint = new Core.Next.ProbeHint nextPlayoutItem.Source.ProbeHint = new Core.Next.ProbeHint
{ {
Audio = sourceAudioHints, Audio = sourceAudioHints,
Video = sourceVideoHints, Video = sourceVideoHints,
Subtitle = sourceSubtitleHints,
DurationMs = (long)headVersion.Duration.TotalMilliseconds DurationMs = (long)headVersion.Duration.TotalMilliseconds
}; };
@ -378,7 +386,8 @@ public class PlayoutItemConverter(
channel, channel,
nextPlayoutItem.Start, nextPlayoutItem.Start,
audioVersion, audioVersion,
allSubtitles); allSubtitles,
shouldLogMessages: false);
maybeAudioStream = result.AudioStream; maybeAudioStream = result.AudioStream;
maybeSubtitle = result.Subtitle; maybeSubtitle = result.Subtitle;
} }

Loading…
Cancel
Save