Browse Source

fix: subtitle playback fixes

pull/2960/head
Jason Dove 5 days ago
parent
commit
e6c37c28ca
No known key found for this signature in database
  1. 4
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  3. 14
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs
  4. 41
      ErsatzTV/Controllers/InternalController.cs

4
CHANGELOG.md

@ -40,8 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -40,8 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix case where block playouts would occasionally get stuck building forever
- Fix green line sometimes seen with NVIDIA and AMD/VAAPI encoding
- Both bugs were in ffmpeg, and ETV's patched ffmpeg 8.1.2 is required for the fixes
- Pass extracted subtitle paths to next engine; this should fix text subtitle burn-in when extraction is enabled
- Pass extracted subtitle paths to next engine; this fixes text subtitle burn-in when extraction is enabled
- Embedded text subtitles will otherwise be ignored and unused (when extraction is disabled)
- Fix next engine music video playback when music video credits are disabled
- Fix playback when seeking into content beyond final text subtitle cue
## [26.6.0] - 2026-07-09
### Added

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

@ -49,7 +49,9 @@ public class UpdateChannelHandler( @@ -49,7 +49,9 @@ public class UpdateChannelHandler(
bool hasPlayoutChange = hasEpgChange || c.WatermarkId != update.WatermarkId ||
c.PreferredAudioLanguageCode != update.PreferredAudioLanguageCode ||
c.PreferredAudioTitle != update.PreferredAudioTitle ||
c.PreferredSubtitleLanguageCode != update.PreferredSubtitleLanguageCode;
c.PreferredSubtitleLanguageCode != update.PreferredSubtitleLanguageCode ||
c.MusicVideoCreditsMode != update.MusicVideoCreditsMode ||
c.SubtitleMode != update.SubtitleMode;
c.Name = update.Name;
c.Number = update.Number;

14
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -362,7 +362,7 @@ public class PlayoutItemConverter( @@ -362,7 +362,7 @@ public class PlayoutItemConverter(
ChannelSubtitleMode subtitleMode,
CancellationToken cancellationToken)
{
List<Subtitle> allSubtitles = await GetSubtitles(audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint);
List<Subtitle> allSubtitles = await GetSubtitles(channel, audioVersion.MediaItem, playoutItem.Id, playoutItem.InPoint);
Option<MediaStream> maybeAudioStream = Option<MediaStream>.None;
Option<Subtitle> maybeSubtitle = Option<Subtitle>.None;
@ -469,8 +469,6 @@ public class PlayoutItemConverter( @@ -469,8 +469,6 @@ public class PlayoutItemConverter(
KeepAlive = false,
Reconnect = true
};
SetInOutPoints(playoutItem, nextPlayoutItem.Tracks.Subtitle.Source);
}
}
}
@ -558,6 +556,7 @@ public class PlayoutItemConverter( @@ -558,6 +556,7 @@ public class PlayoutItemConverter(
}
private static async Task<List<Subtitle>> GetSubtitles(
Channel channel,
MediaItem mediaItem,
int playoutItemId,
TimeSpan playoutItemInPoint)
@ -570,7 +569,7 @@ public class PlayoutItemConverter( @@ -570,7 +569,7 @@ public class PlayoutItemConverter(
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
MusicVideo => GetMusicVideoSubtitles(playoutItemId, playoutItemInPoint),
MusicVideo => GetMusicVideoSubtitles(channel, playoutItemId, playoutItemInPoint),
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
.Map(mm => mm.Subtitles ?? [])
.IfNoneAsync([]),
@ -592,8 +591,13 @@ public class PlayoutItemConverter( @@ -592,8 +591,13 @@ public class PlayoutItemConverter(
return allSubtitles;
}
private static List<Subtitle> GetMusicVideoSubtitles(int playoutItemId, TimeSpan playoutItemInPoint)
private static List<Subtitle> GetMusicVideoSubtitles(Channel channel, int playoutItemId, TimeSpan playoutItemInPoint)
{
if (channel.MusicVideoCreditsMode is not ChannelMusicVideoCreditsMode.GenerateSubtitles)
{
return [];
}
string seekToMs = playoutItemInPoint > TimeSpan.Zero
? $"?seekToMs={(long)playoutItemInPoint.TotalMilliseconds}"
: string.Empty;

41
ErsatzTV/Controllers/InternalController.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Text;
using CliWrap;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Jellyfin;
@ -63,7 +64,8 @@ public class InternalController : StreamingControllerBase @@ -63,7 +64,8 @@ public class InternalController : StreamingControllerBase
[HttpGet("ffmpeg/music-video-credits/{playoutItemId:int}")]
public async Task<IActionResult> GetMusicVideoCredits(
int playoutItemId,
[FromQuery] long? seekToMs,
[FromQuery]
long? seekToMs,
CancellationToken cancellationToken)
{
Option<string> maybeCreditsFile = await _mediator.Send(
@ -74,7 +76,7 @@ public class InternalController : StreamingControllerBase @@ -74,7 +76,7 @@ public class InternalController : StreamingControllerBase
return new PhysicalFileResult(creditsFile, "text/x-ssa");
}
return NotFound();
return File(Encoding.UTF8.GetBytes(EmptySubtitleDocument("text/x-ssa")), "text/x-ssa");
}
[HttpGet("ffmpeg/remote-stream/{remoteStreamId}")]
@ -218,9 +220,14 @@ public class InternalController : StreamingControllerBase @@ -218,9 +220,14 @@ public class InternalController : StreamingControllerBase
}
[HttpGet("/media/subtitle/{id:int}")]
public async Task<IActionResult> GetSubtitle(int id, [FromQuery] long? seekToMs)
public async Task<IActionResult> GetSubtitle(
int id,
[FromQuery] long? seekToMs,
CancellationToken cancellationToken)
{
Either<BaseError, SubtitlePathAndCodec> maybePath = await _mediator.Send(new GetSubtitlePathById(id));
Either<BaseError, SubtitlePathAndCodec> maybePath = await _mediator.Send(
new GetSubtitlePathById(id),
cancellationToken);
foreach (SubtitlePathAndCodec pathAndCodec in maybePath.RightToSeq())
{
@ -236,7 +243,8 @@ public class InternalController : StreamingControllerBase @@ -236,7 +243,8 @@ public class InternalController : StreamingControllerBase
if (seekToMs is > 0)
{
Either<BaseError, SeekTextSubtitleProcess> maybeProcess = await _mediator.Send(
new GetSeekTextSubtitleProcess(pathAndCodec, TimeSpan.FromMilliseconds(seekToMs.Value)));
new GetSeekTextSubtitleProcess(pathAndCodec, TimeSpan.FromMilliseconds(seekToMs.Value)),
cancellationToken);
foreach (SeekTextSubtitleProcess processModel in maybeProcess.RightToSeq())
{
Command command = processModel.Process;
@ -264,10 +272,20 @@ public class InternalController : StreamingControllerBase @@ -264,10 +272,20 @@ public class InternalController : StreamingControllerBase
}
process.Start();
return new FileStreamResult(process.StandardOutput.BaseStream, mimeType);
using var buffer = new MemoryStream();
await process.StandardOutput.BaseStream.CopyToAsync(buffer, cancellationToken);
await process.WaitForExitAsync(cancellationToken);
byte[] bytes = buffer.ToArray();
if (bytes.Length == 0)
{
return Content(EmptySubtitleDocument(mimeType), mimeType);
}
return File(bytes, mimeType);
}
return new NotFoundResult();
return Content(EmptySubtitleDocument(mimeType), mimeType);
}
if (pathAndCodec.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
@ -347,4 +365,13 @@ public class InternalController : StreamingControllerBase @@ -347,4 +365,13 @@ public class InternalController : StreamingControllerBase
return GetProcessResponse(result, channelNumber, StreamingMode.TransportStream);
}
private static string EmptySubtitleDocument(string mimeType) => mimeType switch
{
"text/x-ssa" => "[Script Info]\nScriptType: v4.00+\n\n" +
"[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20\n\n" +
"[Events]\nFormat: Layer, Start, End, Style, Text\n",
"text/vtt" => "WEBVTT\n\n",
_ => "1\n00:00:00,000 --> 00:00:00,001\n \n\n"
};
}

Loading…
Cancel
Save