Browse Source

fix: playout item converter subtitles

pull/3014/head
Jason Dove 5 days ago
parent
commit
2b670947bf
No known key found for this signature in database
  1. 3
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  3. 15
      ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs
  4. 2
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  5. 297
      ErsatzTV.Infrastructure.Tests/Scheduling/PlayoutItemConverterTests.cs
  6. 3
      ErsatzTV.Infrastructure/Extensions/PlayoutItemQueryableExtensions.cs
  7. 160
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

3
CHANGELOG.md

@ -23,10 +23,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -23,10 +23,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Optimize QSV pipelines by merging consecutive vpp_qsv filters as much as possible (e.g. tonemap, scale and format using a single filter)
### Fixed
- Next engine:
- Fix many issues with subtitles that caused black/silence (error output from next)
- Fix health checks causing a flood of (harmless) logged errors when quickly navigating away from home page
- Health check results will now be cached for 5 minutes by default; a refresh button has been added to immediately re-run all checks
- Fix `/api/sessions` response when channels use Next streaming engine
- Save and restore sequential schedule mid-roll, post-roll, and graphics state between builds
- Fix subtitle playback with Plex other video libraries
## [26.9.0] - 2026-09-06
### Fixed

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

@ -444,7 +444,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -444,7 +444,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
Option<TimeSpan> maybeDuration = maybeNextStart.Map(s => s - now);
// limit working ahead on errors to 1 minute
if (!request.HlsRealtime && await maybeDuration.IfNoneAsync(TimeSpan.FromMinutes(2)) > TimeSpan.FromMinutes(1))
if (!request.HlsRealtime &&
await maybeDuration.IfNoneAsync(TimeSpan.FromMinutes(2)) > TimeSpan.FromMinutes(1))
{
maybeNextStart = now.AddMinutes(1);
maybeDuration = TimeSpan.FromMinutes(1);
@ -569,8 +570,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -569,8 +570,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
_ => []
};
bool isMediaServer = playoutItemWithPath.PlayoutItem.MediaItem is PlexMovie or PlexEpisode or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
bool isMediaServer = playoutItemWithPath.PlayoutItem.MediaItem is PlexMovie or PlexEpisode or PlexOtherVideo
or JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)
{

15
ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs

@ -82,6 +82,21 @@ public class GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFa @@ -82,6 +82,21 @@ public class GetSubtitlePathByIdHandler(IDbContextFactory<TvContext> dbContextFa
.Map(Optional);
}
// check for plex other video
if (maybePlexId.IsNone)
{
maybePlexId = await dbContext.Connection.QuerySingleOrDefaultAsync<int?>(
@"select PMS.Id from PlexMediaSource PMS
inner join Library L on PMS.Id = L.MediaSourceId
inner join LibraryPath LP on L.Id = LP.LibraryId
inner join MediaItem MI on LP.Id = MI.LibraryPathId
inner join OtherVideoMetadata OVM on OVM.OtherVideoId = MI.Id
inner join Subtitle S on OVM.Id = S.OtherVideoMetadataId
where S.Id = @SubtitleId",
new { SubtitleId = subtitleId })
.Map(Optional);
}
foreach (int plexMediaSourceId in maybePlexId)
{
foreach (string subtitlePath in maybeSubtitle.Map(s => s.Path))

2
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -542,7 +542,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -542,7 +542,7 @@ public class PrepareTroubleshootingPlaybackHandler(
_ => []
};
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or PlexOtherVideo or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)

297
ErsatzTV.Infrastructure.Tests/Scheduling/PlayoutItemConverterTests.cs

@ -0,0 +1,297 @@ @@ -0,0 +1,297 @@
using System.Collections.Immutable;
using System.Text.Json;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Security;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Scheduling;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
using static LanguageExt.Prelude;
using Next = ErsatzTV.Core.Next;
namespace ErsatzTV.Infrastructure.Tests.Scheduling;
[TestFixture]
public class PlayoutItemConverterTests
{
private const string VideoPath = "/media/movie.mkv";
private MockFileSystem _fileSystem = null!;
private PlayoutItemConverter _converter = null!;
private Channel _channel = null!;
private DateTimeOffset _start;
[SetUp]
public void SetUp()
{
_start = DateTimeOffset.UtcNow.AddDays(1);
_fileSystem = new MockFileSystem();
_fileSystem.Directory.CreateDirectory("/media");
_fileSystem.File.WriteAllText(VideoPath, string.Empty);
var plex = Substitute.For<IPlexPathReplacementService>();
plex.GetReplacementPlexPath(default, default!, default, default).ReturnsForAnyArgs(VideoPath);
var jellyfin = Substitute.For<IJellyfinPathReplacementService>();
jellyfin.GetReplacementJellyfinPath(default, default!, default, default).ReturnsForAnyArgs(VideoPath);
var emby = Substitute.For<IEmbyPathReplacementService>();
emby.GetReplacementEmbyPath(default, default!, default, default).ReturnsForAnyArgs(VideoPath);
var selector = Substitute.For<IFFmpegStreamSelector>();
selector.SelectSubtitleStream(default!, default!, default!, default, default, default)
.ReturnsForAnyArgs(call => Task.FromResult(
call.Arg<ImmutableList<Subtitle>>().HeadOrNone()));
var watermarks = Substitute.For<IWatermarkSelector>();
watermarks.SelectWatermarks(default, default!, default!, default, default).ReturnsForAnyArgs([]);
var graphics = Substitute.For<IGraphicsElementSelector>();
graphics.SelectGraphicsElements(default!, default!, default, default).ReturnsForAnyArgs([]);
_converter = new PlayoutItemConverter(
_fileSystem, plex, jellyfin, emby,
Substitute.For<ICustomStreamSelector>(), selector, watermarks, graphics,
Substitute.For<IDbContextFactory<TvContext>>(),
Substitute.For<ILogger<PlayoutItemConverter>>());
_channel = new Channel(Guid.NewGuid()) { StreamSelectorMode = ChannelStreamSelectorMode.Default };
}
[TestCase("jellyfin", null)]
[TestCase("plex", "/library/streams/123")]
[TestCase("emby", "media-source-id")]
[TestCase("jellyfin-episode", null)]
[TestCase("plex-episode", "/library/streams/123")]
[TestCase("emby-episode", "media-source-id")]
public async Task Server_sidecars_use_signed_subtitle_proxy(string server, string? path)
{
MediaItem movie = server switch
{
"jellyfin" => new JellyfinMovie(),
"plex" => new PlexMovie(),
"jellyfin-episode" => new JellyfinEpisode(),
"plex-episode" => new PlexEpisode(),
"emby-episode" => new EmbyEpisode(),
_ => new EmbyMovie()
};
var subtitle = new Subtitle
{
Id = 9873, SubtitleKind = SubtitleKind.Sidecar, Codec = "subrip",
StreamIndex = 100000, Path = path!
};
Next.PlayoutItem result = await Convert(movie, [subtitle]);
Next.Source source = result.Tracks!.Subtitle!.Source!;
source.SourceType.ShouldBe(Next.SourceType.Http);
source.Path.ShouldBeNull();
var uri = new Uri(source.Uri!);
uri.AbsolutePath.ShouldBe("/internal/media/subtitle/9873");
var query = uri.Query.TrimStart('?').Split('&')
.Select(part => part.Split('=', 2)).ToDictionary(part => part[0], part => part[1]);
InternalUrlSigner.Verify(query["exp"], query["sig"], "subtitle", "9873").ShouldBeTrue();
long.Parse(query["exp"]).ShouldBe(_start.AddSeconds(30).AddHours(2).ToUnixTimeSeconds());
query.ContainsKey("seekToMs").ShouldBeFalse();
AssertTiming(source);
subtitle.Path.ShouldBe(path);
using JsonDocument json = JsonDocument.Parse(Next.Serialize.ToJson(new Next.Playout
{
Version = "https://ersatztv.org/playout/version/0.0.4", Items = [result]
}));
JsonElement serialized = json.RootElement.GetProperty("items")[0]
.GetProperty("tracks").GetProperty("subtitle").GetProperty("source");
serialized.GetProperty("source_type").GetString().ShouldBe("http");
serialized.GetProperty("uri").GetString().ShouldBe(source.Uri);
}
[Test]
public async Task Persisted_local_sidecar_uses_proxy_after_checking_file_exists()
{
_fileSystem.File.WriteAllText("/media/movie.srt", string.Empty);
Next.PlayoutItem result = await Convert(new Movie(),
[new Subtitle { Id = 123, SubtitleKind = SubtitleKind.Sidecar, Path = "/media/movie.srt" }]);
Next.Source source = result.Tracks!.Subtitle!.Source!;
source.SourceType.ShouldBe(Next.SourceType.Http);
new Uri(source.Uri!).AbsolutePath.ShouldBe("/internal/media/subtitle/123");
AssertTiming(source);
}
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
[TestCase("/media/missing.srt")]
public async Task Unavailable_local_sidecars_do_not_emit_subtitle_tracks(string? path)
{
var subtitle = new Subtitle { Id = 123, SubtitleKind = SubtitleKind.Sidecar, Path = path! };
Next.PlayoutItem result = await Convert(new Movie(), [subtitle]);
result.Tracks?.Subtitle.ShouldBeNull();
}
[TestCase("jellyfin", 0, null)]
[TestCase("plex", 123, null)]
[TestCase("emby", 123, "")]
public async Task Unresolvable_server_sidecars_do_not_emit_subtitle_tracks(string server, int id, string? path)
{
Movie movie = server switch
{
"jellyfin" => new JellyfinMovie(),
"plex" => new PlexMovie(),
_ => new EmbyMovie()
};
Next.PlayoutItem result = await Convert(movie,
[new Subtitle { Id = id, SubtitleKind = SubtitleKind.Sidecar, Path = path! }]);
result.Tracks?.Subtitle.ShouldBeNull();
}
[TestCase(false)]
[TestCase(true)]
public async Task Extracted_subtitle_requires_existing_cache_file(bool exists)
{
string path = Path.Combine(FileSystemLayout.SubtitleCacheFolder, "movie.srt");
if (exists)
{
_fileSystem.Directory.CreateDirectory(FileSystemLayout.SubtitleCacheFolder);
_fileSystem.File.WriteAllText(path, string.Empty);
}
Next.PlayoutItem result = await Convert(new Movie(),
[new Subtitle { SubtitleKind = SubtitleKind.Embedded, IsExtracted = true, Path = "movie.srt" }]);
if (exists)
{
Next.Source source = result.Tracks!.Subtitle!.Source!;
source.Path.ShouldBe(path);
source.SourceType.ShouldBe(Next.SourceType.Local);
AssertTiming(source);
}
else
{
result.Tracks?.Subtitle.ShouldBeNull();
}
}
[TestCase("http://localhost:8409/subtitle.srt")]
[TestCase("http://127.0.0.1:8409/subtitle.srt")]
[TestCase("https://example.com/subtitle.srt")]
public async Task Remote_subtitles_preserve_uri_and_media_timing(string uri)
{
Next.PlayoutItem result = await Convert(new Movie(),
[new Subtitle { SubtitleKind = SubtitleKind.Sidecar, Path = uri }]);
Next.Source source = result.Tracks!.Subtitle!.Source!;
source.SourceType.ShouldBe(Next.SourceType.Http);
source.Uri.ShouldBe(uri);
AssertTiming(source);
}
[TestCase(true)]
[TestCase(false)]
public async Task Embedded_image_subtitle_has_effective_video_source(bool hasAudio)
{
Next.PlayoutItem result = await Convert(new Movie(),
[new Subtitle { SubtitleKind = SubtitleKind.Embedded, Codec = "hdmv_pgs_subtitle", StreamIndex = 4 }],
hasAudio);
Next.TrackSelection track = result.Tracks!.Subtitle!;
track.StreamIndex.ShouldBe(4);
Next.Source source = (track.Source ?? result.Source)!;
source.ShouldNotBeNull();
source.Path.ShouldBe(VideoPath);
AssertTiming(source);
if (!hasAudio)
{
result.Source.ShouldBeNull();
track.Source.ShouldBeSameAs(result.Tracks.Video!.Source);
}
}
[Test]
public async Task Music_video_uses_metadata_subtitles_when_credits_are_disabled()
{
_fileSystem.File.WriteAllText("/media/music.srt", string.Empty);
Next.PlayoutItem result = await Convert(new MusicVideo(),
[new Subtitle { SubtitleKind = SubtitleKind.Sidecar, Path = "/media/music.srt" }]);
result.Tracks!.Subtitle!.Source!.Path.ShouldBe("/media/music.srt");
}
[Test]
public async Task Generated_credits_do_not_apply_in_point_twice()
{
_channel.MusicVideoCreditsMode = ChannelMusicVideoCreditsMode.GenerateSubtitles;
Next.PlayoutItem result = await Convert(new MusicVideo(), []);
Next.Source source = result.Tracks!.Subtitle!.Source!;
source.SourceType.ShouldBe(Next.SourceType.Http);
source.Uri.ShouldEndWith("/internal/ffmpeg/music-video-credits/42?seekToMs=10000");
source.InPointMs.ShouldBeNull();
source.OutPointMs.ShouldBeNull();
}
[Test]
public async Task Filtering_does_not_mutate_metadata_or_supplied_subtitles()
{
List<Subtitle> subtitles =
[
new() { SubtitleKind = SubtitleKind.Embedded, Codec = "eia_608" },
new() { SubtitleKind = SubtitleKind.Sidecar, Codec = "dvd_subtitle" }
];
await Convert(new JellyfinMovie(), subtitles);
subtitles.Count.ShouldBe(2);
await Convert(new JellyfinMovie(), subtitles, suppliedSubtitles: Some(subtitles));
subtitles.Count.ShouldBe(2);
}
private async Task<Next.PlayoutItem> Convert(
MediaItem mediaItem,
List<Subtitle> subtitles,
bool hasAudio = true,
Option<List<Subtitle>> suppliedSubtitles = default)
{
var version = new MediaVersion
{
Duration = TimeSpan.FromMinutes(2),
MediaFiles = [new MediaFile { Path = VideoPath }],
Streams = [new MediaStream { MediaStreamKind = MediaStreamKind.Video, Index = 0 }]
};
if (hasAudio)
{
version.Streams.Add(new MediaStream { MediaStreamKind = MediaStreamKind.Audio, Index = 1 });
}
switch (mediaItem)
{
case Episode episode:
episode.MediaVersions = [version];
episode.EpisodeMetadata = [new EpisodeMetadata { Subtitles = subtitles }];
break;
case Movie movie:
movie.MediaVersions = [version];
movie.MovieMetadata = [new MovieMetadata { Subtitles = subtitles }];
break;
case MusicVideo musicVideo:
musicVideo.MediaVersions = [version];
musicVideo.MusicVideoMetadata = [new MusicVideoMetadata { Subtitles = subtitles }];
break;
}
var playoutItem = new PlayoutItem
{
Id = 42,
MediaItem = mediaItem,
Start = _start.UtcDateTime,
Finish = _start.AddSeconds(30).UtcDateTime,
InPoint = TimeSpan.FromSeconds(10),
OutPoint = TimeSpan.FromSeconds(40)
};
Option<Next.PlayoutItem> result = await _converter.ToNext(
Some(_channel), None, TimeSpan.Zero, playoutItem, suppliedSubtitles, true, CancellationToken.None);
result.IsSome.ShouldBeTrue();
return result.Match(item => item, () => throw new InvalidOperationException("No playout item"));
}
private static void AssertTiming(Next.Source source)
{
source.InPointMs.ShouldBe(10000);
source.OutPointMs.ShouldBe(40000);
}
}

3
ErsatzTV.Infrastructure/Extensions/PlayoutItemQueryableExtensions.cs

@ -92,6 +92,9 @@ public static class PlayoutItemQueryableExtensions @@ -92,6 +92,9 @@ public static class PlayoutItemQueryableExtensions
.ThenInclude(i => (i as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Subtitles)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as RemoteStream).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)

160
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -15,6 +15,7 @@ using ErsatzTV.Core.Security; @@ -15,6 +15,7 @@ using ErsatzTV.Core.Security;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
using PlayoutItem = ErsatzTV.Core.Domain.PlayoutItem;
@ -29,7 +30,8 @@ public class PlayoutItemConverter( @@ -29,7 +30,8 @@ public class PlayoutItemConverter(
IFFmpegStreamSelector ffmpegStreamSelector,
IWatermarkSelector watermarkSelector,
IGraphicsElementSelector graphicsElementSelector,
IDbContextFactory<TvContext> dbContextFactory) : IPlayoutItemConverter
IDbContextFactory<TvContext> dbContextFactory,
ILogger<PlayoutItemConverter> logger) : IPlayoutItemConverter
{
public async Task<Option<Core.Next.PlayoutItem>> ToNext(
string channelNumber,
@ -400,8 +402,8 @@ public class PlayoutItemConverter( @@ -400,8 +402,8 @@ public class PlayoutItemConverter(
bool shouldLogMessages,
CancellationToken cancellationToken)
{
List<Subtitle> allSubtitles = await subtitles.IfNoneAsync(
await GetSubtitles(channel, audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint));
List<Subtitle> allSubtitles = (await subtitles.IfNoneAsync(
() => GetSubtitles(channel, audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint))).ToList();
// TODO: external image subtitles
allSubtitles.RemoveAll(s => s.IsImage && s.SubtitleKind is not SubtitleKind.Embedded);
@ -455,65 +457,107 @@ public class PlayoutItemConverter( @@ -455,65 +457,107 @@ public class PlayoutItemConverter(
foreach (Subtitle subtitle in maybeSubtitle)
{
if (subtitle.SubtitleKind is SubtitleKind.Embedded)
if (subtitle.SubtitleKind is SubtitleKind.Embedded && subtitle.IsImage)
{
if (subtitle.IsImage)
{
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;
}
}
// next only supports sidecar text subtitles at the moment; ignore non-extracted text subs
else if (subtitle.IsExtracted && !string.IsNullOrWhiteSpace(subtitle.Path))
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle = new Core.Next.TrackSelection
{
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 = Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path),
};
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
}
StreamIndex = subtitle.StreamIndex,
// Without audio, the video source is on tracks.video rather than the item.
Source = nextPlayoutItem.Source is null ? nextPlayoutItem.Tracks.Video?.Source : null
};
continue;
}
else if (!IsRemoteUri(subtitle.Path))
foreach (Core.Next.Source source in SubtitleSource(playoutItem, subtitle, shouldLogMessages))
{
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,
};
nextPlayoutItem.Tracks ??= new Core.Next.PlayoutItemTracks();
nextPlayoutItem.Tracks.Subtitle = new Core.Next.TrackSelection { Source = source };
}
}
}
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
private Option<Core.Next.Source> SubtitleSource(
PlayoutItem playoutItem,
Subtitle subtitle,
bool shouldLogMessages)
{
string path = subtitle.Path;
bool isMediaServer = playoutItem.MediaItem is PlexMovie or PlexEpisode or PlexOtherVideo or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
bool isServerSidecar = isMediaServer && subtitle.SubtitleKind is SubtitleKind.Sidecar;
if (subtitle.SubtitleKind is SubtitleKind.Embedded)
{
// burned text subtitles must have been extracted to a sidecar file
if (!subtitle.IsExtracted || string.IsNullOrWhiteSpace(path))
{
return None;
}
else if (subtitle.Path.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase))
path = Path.Combine(FileSystemLayout.SubtitleCacheFolder, path);
}
// jellyfin sidecars have no path; plex and emby store item ids
bool isJellyfin = playoutItem.MediaItem is JellyfinMovie or JellyfinEpisode;
bool isRemote = IsRemoteUri(path);
bool isAvailable = isServerSidecar
? subtitle.Id > 0 && (isJellyfin || !string.IsNullOrWhiteSpace(path))
: !string.IsNullOrWhiteSpace(path) && (isRemote || fileSystem.File.Exists(path));
if (!isAvailable)
{
if (shouldLogMessages)
{
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.Http,
Uri = subtitle.Path,
KeepAlive = false,
Reconnect = true
};
}
logger.LogWarning(
"Ignoring unavailable subtitle {SubtitleId} for media item {MediaItemId}",
subtitle.Id,
playoutItem.MediaItemId);
}
return None;
}
Core.Next.Source source;
if (subtitle.SubtitleKind is SubtitleKind.Sidecar && subtitle.Id > 0)
{
DateTimeOffset exp = playoutItem.FinishOffset + TimeSpan.FromHours(2);
string sig = InternalUrlSigner.Sign(exp, "subtitle", $"{subtitle.Id}");
source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Http,
Uri =
$"http://localhost:{Settings.StreamingPort}/internal/media/subtitle/{subtitle.Id}?exp={exp.ToUnixTimeSeconds()}&sig={sig}",
KeepAlive = false,
Reconnect = true
};
}
else if (isRemote)
{
source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Http,
Uri = path,
KeepAlive = false,
Reconnect = true
};
}
else
{
source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Local,
Path = path
};
}
// generated http credits already apply the item's in-point at the endpoint.
// other subtitles retain media timestamps, and next applies the seek
if (subtitle.SubtitleKind is not SubtitleKind.Generated || !isRemote)
{
SetInOutPoints(playoutItem, source);
}
return source;
}
private void SelectGraphics(
@ -601,20 +645,24 @@ public class PlayoutItemConverter( @@ -601,20 +645,24 @@ public class PlayoutItemConverter(
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
MusicVideo => GetMusicVideoSubtitles(channel, playoutItemId, playoutItemInPoint),
MusicVideo musicVideo => channel.MusicVideoCreditsMode is ChannelMusicVideoCreditsMode.GenerateSubtitles
? GetMusicVideoSubtitles(channel, playoutItemId, playoutItemInPoint)
: await Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
_ => []
};
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or
bool isMediaServer = mediaItem is PlexMovie or PlexEpisode or PlexOtherVideo or
JellyfinMovie or JellyfinEpisode or EmbyMovie or EmbyEpisode;
if (isMediaServer)
{
// closed captions are currently unsupported
allSubtitles.RemoveAll(s => s.Codec == "eia_608");
allSubtitles = allSubtitles.Where(s => s.Codec != "eia_608").ToList();
}
return allSubtitles;

Loading…
Cancel
Save