Browse Source

extract attached fonts (#1050)

pull/1014/head
Jason Dove 4 years ago committed by GitHub
parent
commit
f84496b09d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 187
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

2
CHANGELOG.md

@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Add (required) bit depth normalization option to ffmpeg profile - Add (required) bit depth normalization option to ffmpeg profile
- This can help if your card only supports e.g. h264 encoding, normalizing to 8 bits will allow the hardware encoder to be used - This can help if your card only supports e.g. h264 encoding, normalizing to 8 bits will allow the hardware encoder to be used
- Extract font attachments after extracting text subtitles
- This should improve SubStation Alpha subtitle rendering
- Add audio stream selector scripts for episodes and movies - Add audio stream selector scripts for episodes and movies
- This will let you customize which audio stream is selected for playback - This will let you customize which audio stream is selected for playback
- Episodes are passed the following data: - Episodes are passed the following data:

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

@ -109,31 +109,31 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(pi => pi.Start <= until) .Filter(pi => pi.Start <= until)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// TODO: support other media kinds (movies, other videos, etc)
var mediaItemIds = playoutItems.Map(pi => pi.MediaItemId).ToList(); var mediaItemIds = playoutItems.Map(pi => pi.MediaItemId).ToList();
// filter for subtitles that need extraction // filter for items with text subtitles or font attachments
List<int> unextractedMediaItemIds = List<int> mediaItemIdsWithTextSubtitles =
await GetUnextractedMediaItemIds(dbContext, mediaItemIds, cancellationToken); await GetMediaItemIdsWithTextSubtitles(dbContext, mediaItemIds, cancellationToken);
if (unextractedMediaItemIds.Any()) if (mediaItemIdsWithTextSubtitles.Any())
{ {
_logger.LogDebug( _logger.LogDebug(
"Found media items {MediaItemIds} with text subtitles to extract for playouts {PlayoutIds}", "Checking media items {MediaItemIds} for text subtitles or fonts to extract for playouts {PlayoutIds}",
unextractedMediaItemIds, mediaItemIdsWithTextSubtitles,
playoutIdsToCheck); playoutIdsToCheck);
} }
else else
{ {
_logger.LogDebug("Found no text subtitles to extract for playouts {PlayoutIds}", playoutIdsToCheck); _logger.LogDebug(
"Found no text subtitles or fonts to extract for playouts {PlayoutIds}",
playoutIdsToCheck);
} }
// sort by start time // sort by start time
var toUpdate = playoutItems var toUpdate = playoutItems
.Filter(pi => pi.Finish >= DateTime.UtcNow) .Filter(pi => pi.Finish >= DateTime.UtcNow)
.DistinctBy(pi => pi.MediaItemId) .DistinctBy(pi => pi.MediaItemId)
.Filter(pi => unextractedMediaItemIds.Contains(pi.MediaItemId)) .Filter(pi => mediaItemIdsWithTextSubtitles.Contains(pi.MediaItemId))
.OrderBy(pi => pi.StartOffset) .OrderBy(pi => pi.StartOffset)
.Map(pi => pi.MediaItemId) .Map(pi => pi.MediaItemId)
.ToList(); .ToList();
@ -145,14 +145,13 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return Unit.Default; return Unit.Default;
} }
PlayoutItem pi = playoutItems.Find(pi => pi.MediaItemId == mediaItemId);
_logger.LogDebug("Extracting subtitles for item with start time {StartTime}", pi?.StartOffset);
// extract subtitles and fonts for each item and update db // extract subtitles and fonts for each item and update db
await ExtractSubtitles(dbContext, mediaItemId, ffmpegPath, cancellationToken); await ExtractSubtitles(dbContext, mediaItemId, ffmpegPath, cancellationToken);
// await ExtractFonts(dbContext, episodeId, ffmpegPath, cancellationToken); await ExtractFonts(dbContext, mediaItemId, ffmpegPath, cancellationToken);
} }
_logger.LogDebug("Done checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
return Unit.Default; return Unit.Default;
} }
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
@ -161,7 +160,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
} }
} }
private async Task<List<int>> GetUnextractedMediaItemIds( private async Task<List<int>> GetMediaItemIdsWithTextSubtitles(
TvContext dbContext, TvContext dbContext,
List<int> mediaItemIds, List<int> mediaItemIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -174,7 +173,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(em => mediaItemIds.Contains(em.EpisodeId)) .Filter(em => mediaItemIds.Contains(em.EpisodeId))
.Filter( .Filter(
em => em.Subtitles.Any( em => em.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
.Map(em => em.EpisodeId) .Map(em => em.EpisodeId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -184,7 +183,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(mm => mediaItemIds.Contains(mm.MovieId)) .Filter(mm => mediaItemIds.Contains(mm.MovieId))
.Filter( .Filter(
mm => mm.Subtitles.Any( mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
.Map(mm => mm.MovieId) .Map(mm => mm.MovieId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -194,7 +193,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(mm => mediaItemIds.Contains(mm.MusicVideoId)) .Filter(mm => mediaItemIds.Contains(mm.MusicVideoId))
.Filter( .Filter(
mm => mm.Subtitles.Any( mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
.Map(mm => mm.MusicVideoId) .Map(mm => mm.MusicVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -204,7 +203,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(ovm => mediaItemIds.Contains(ovm.OtherVideoId)) .Filter(ovm => mediaItemIds.Contains(ovm.OtherVideoId))
.Filter( .Filter(
ovm => ovm.Subtitles.Any( ovm => ovm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false && s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle")) s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
.Map(ovm => ovm.OtherVideoId) .Map(ovm => ovm.OtherVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@ -218,40 +217,13 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return result; return result;
} }
private async Task<Unit> ExtractSubtitles( private async Task ExtractSubtitles(
TvContext dbContext, TvContext dbContext,
int mediaItemId, int mediaItemId,
string ffmpegPath, string ffmpegPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId))
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as Movie).MovieMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.ThenInclude(em => em.Subtitles)
.SelectOneAsync(e => e.Id, e => e.Id == mediaItemId);
foreach (MediaItem mediaItem in maybeMediaItem)
{ {
foreach (List<Subtitle> allSubtitles in GetSubtitles(mediaItem)) foreach (List<Subtitle> allSubtitles in GetSubtitles(mediaItem))
{ {
@ -273,6 +245,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
} }
} }
if (subtitlesToExtract.Count == 0)
{
continue;
}
string mediaItemPath = await GetMediaItemPath(mediaItem); string mediaItemPath = await GetMediaItemPath(mediaItem);
ArgumentsBuilder args = new ArgumentsBuilder() ArgumentsBuilder args = new ArgumentsBuilder()
@ -316,10 +293,36 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
} }
} }
} }
return Unit.Default;
} }
private static async Task<Option<MediaItem>> GetMediaItem(TvContext dbContext, int mediaItemId) =>
await dbContext.MediaItems
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as Movie).MovieMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(em => em.Subtitles)
.Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mi => (mi as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(mi => (mi as OtherVideo).OtherVideoMetadata)
.ThenInclude(em => em.Subtitles)
.SelectOneAsync(e => e.Id, e => e.Id == mediaItemId);
private static Option<List<Subtitle>> GetSubtitles(MediaItem mediaItem) => private static Option<List<Subtitle>> GetSubtitles(MediaItem mediaItem) =>
mediaItem switch mediaItem switch
{ {
@ -330,44 +333,64 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
_ => None _ => None
}; };
private async Task<Unit> ExtractFonts( private async Task ExtractFonts(
TvContext dbContext, TvContext dbContext,
int mediaItemId, int mediaItemId,
string ffmpegPath, string ffmpegPath,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Option<Episode> maybeEpisode = await dbContext.Episodes foreach (MediaItem mediaItem in await GetMediaItem(dbContext, mediaItemId))
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(e => e.MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(e => e.EpisodeMetadata)
.ThenInclude(em => em.Subtitles)
.SelectOneAsync(e => e.Id, e => e.Id == mediaItemId);
foreach (Episode episode in maybeEpisode)
{ {
string mediaItemPath = episode.GetHeadVersion().MediaFiles.Head().Path; MediaVersion headVersion = mediaItem.GetHeadVersion();
var attachments = headVersion.Streams
var arguments = $"-nostdin -hide_banner -dump_attachment:t \"\" -i \"{mediaItemPath}\" -y"; .Filter(s => s.MediaStreamKind == MediaStreamKind.Attachment)
.OrderBy(s => s.Index)
BufferedCommandResult result = await Cli.Wrap(ffmpegPath) .ToList();
.WithWorkingDirectory(FileSystemLayout.FontsCacheFolder)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken);
// if (result.ExitCode == 0)
// {
// _logger.LogDebug("Successfully extracted attached fonts");
// }
// else
// {
// _logger.LogError("Failed to extract attached fonts. {Error}", result.StandardError);
// }
}
return Unit.Default; for (var attachmentIndex = 0; attachmentIndex < attachments.Count; attachmentIndex++)
{
MediaStream fontStream = attachments[attachmentIndex];
if (!(fontStream.MimeType ?? string.Empty).Contains("font") &&
!(fontStream.MimeType ?? string.Empty).Contains("opentype"))
{
// not a font
continue;
}
string fullOutputPath = Path.Combine(FileSystemLayout.FontsCacheFolder, fontStream.FileName);
if (_localFileSystem.FileExists(fullOutputPath))
{
// already extracted
continue;
}
string mediaItemPath = await GetMediaItemPath(mediaItem);
var arguments =
$"-nostdin -hide_banner -dump_attachment:t:{attachmentIndex} \"\" -i \"{mediaItemPath}\" -y";
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithWorkingDirectory(FileSystemLayout.FontsCacheFolder)
.WithArguments(arguments)
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync(cancellationToken);
// ffmpeg seems to return exit code 1 in all cases when dumping an attachment
// so ignore it and check success a different way
if (_localFileSystem.FileExists(fullOutputPath))
{
_logger.LogDebug("Successfully extracted font {Font}", fontStream.FileName);
}
else
{
_logger.LogError(
"Failed to extract attached font {Font}. {Error}",
fontStream.FileName,
result.StandardError);
}
}
}
} }
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) => private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
@ -442,6 +465,4 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
} }
private record SubtitleToExtract(Subtitle Subtitle, string OutputPath); private record SubtitleToExtract(Subtitle Subtitle, string OutputPath);
private record FontToExtract(MediaStream Stream, string OutputPath);
} }

Loading…
Cancel
Save