Browse Source

add extracting attachments for ffmpeg to burn subs

Nils Fürniß 3 years ago
parent
commit
ab40554759

+ 1 - 0
CONTRIBUTORS.md

@@ -76,6 +76,7 @@
  - [mitchfizz05](https://github.com/mitchfizz05)
  - [mitchfizz05](https://github.com/mitchfizz05)
  - [MrTimscampi](https://github.com/MrTimscampi)
  - [MrTimscampi](https://github.com/MrTimscampi)
  - [n8225](https://github.com/n8225)
  - [n8225](https://github.com/n8225)
+ - [Nalsai](https://github.com/Nalsai)
  - [Narfinger](https://github.com/Narfinger)
  - [Narfinger](https://github.com/Narfinger)
  - [NathanPickard](https://github.com/NathanPickard)
  - [NathanPickard](https://github.com/NathanPickard)
  - [neilsb](https://github.com/neilsb)
  - [neilsb](https://github.com/neilsb)

+ 16 - 0
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -18,6 +18,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
@@ -42,6 +43,8 @@ namespace Jellyfin.Api.Helpers
         /// </summary>
         /// </summary>
         private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
         private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
 
 
+        private readonly IAttachmentExtractor _attachmentExtractor;
+        private readonly IApplicationPaths _appPaths;
         private readonly IAuthorizationContext _authorizationContext;
         private readonly IAuthorizationContext _authorizationContext;
         private readonly EncodingHelper _encodingHelper;
         private readonly EncodingHelper _encodingHelper;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
@@ -55,6 +58,8 @@ namespace Jellyfin.Api.Helpers
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
         /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
         /// </summary>
         /// </summary>
+        /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+        /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
@@ -65,6 +70,8 @@ namespace Jellyfin.Api.Helpers
         /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
         /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         public TranscodingJobHelper(
         public TranscodingJobHelper(
+            IAttachmentExtractor attachmentExtractor,
+            IApplicationPaths appPaths,
             ILogger<TranscodingJobHelper> logger,
             ILogger<TranscodingJobHelper> logger,
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
@@ -75,6 +82,8 @@ namespace Jellyfin.Api.Helpers
             EncodingHelper encodingHelper,
             EncodingHelper encodingHelper,
             ILoggerFactory loggerFactory)
             ILoggerFactory loggerFactory)
         {
         {
+            _attachmentExtractor = attachmentExtractor;
+            _appPaths = appPaths;
             _logger = logger;
             _logger = logger;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -513,6 +522,13 @@ namespace Jellyfin.Api.Helpers
                 throw new ArgumentException("FFmpeg path not set.");
                 throw new ArgumentException("FFmpeg path not set.");
             }
             }
 
 
+            // If subtitles get burned in fonts may need to be extracted from the media file
+            if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+            {
+                var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+                await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, CancellationToken.None).ConfigureAwait(false);
+            }
+
             var process = new Process
             var process = new Process
             {
             {
                 StartInfo = new ProcessStartInfo
                 StartInfo = new ProcessStartInfo

+ 14 - 3
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         private const string VideotoolboxAlias = "vt";
         private const string VideotoolboxAlias = "vt";
         private const string OpenclAlias = "ocl";
         private const string OpenclAlias = "ocl";
         private const string CudaAlias = "cu";
         private const string CudaAlias = "cu";
-
+        private readonly IApplicationPaths _appPaths;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ISubtitleEncoder _subtitleEncoder;
         private readonly ISubtitleEncoder _subtitleEncoder;
 
 
@@ -51,9 +52,11 @@ namespace MediaBrowser.Controller.MediaEncoding
         };
         };
 
 
         public EncodingHelper(
         public EncodingHelper(
+            IApplicationPaths appPaths,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
             ISubtitleEncoder subtitleEncoder)
             ISubtitleEncoder subtitleEncoder)
         {
         {
+            _appPaths = appPaths;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
             _subtitleEncoder = subtitleEncoder;
             _subtitleEncoder = subtitleEncoder;
         }
         }
@@ -1080,6 +1083,12 @@ namespace MediaBrowser.Controller.MediaEncoding
             var alphaParam = enableAlpha ? ":alpha=1" : string.Empty;
             var alphaParam = enableAlpha ? ":alpha=1" : string.Empty;
             var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
             var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
 
 
+            var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+            var fontParam = string.Format(
+                CultureInfo.InvariantCulture,
+                ":fontsdir={0}",
+                _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
+
             // TODO
             // TODO
             // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
             // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
             // string fallbackFontParam = string.Empty;
             // string fallbackFontParam = string.Empty;
@@ -1120,11 +1129,12 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // TODO: Perhaps also use original_size=1920x800 ??
                 // TODO: Perhaps also use original_size=1920x800 ??
                 return string.Format(
                 return string.Format(
                     CultureInfo.InvariantCulture,
                     CultureInfo.InvariantCulture,
-                    "subtitles=f='{0}'{1}{2}{3}{4}",
+                    "subtitles=f='{0}'{1}{2}{3}{4}{5}",
                     _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
                     _mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
                     charsetParam,
                     charsetParam,
                     alphaParam,
                     alphaParam,
                     sub2videoParam,
                     sub2videoParam,
+                    fontParam,
                     // fallbackFontParam,
                     // fallbackFontParam,
                     setPtsParam);
                     setPtsParam);
             }
             }
@@ -1133,11 +1143,12 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             return string.Format(
             return string.Format(
                 CultureInfo.InvariantCulture,
                 CultureInfo.InvariantCulture,
-                "subtitles='{0}:si={1}{2}{3}'{4}",
+                "subtitles='{0}:si={1}{2}{3}{4}'{5}",
                 _mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
                 _mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
                 state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
                 state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
                 alphaParam,
                 alphaParam,
                 sub2videoParam,
                 sub2videoParam,
+                fontParam,
                 // fallbackFontParam,
                 // fallbackFontParam,
                 setPtsParam);
                 setPtsParam);
         }
         }

+ 6 - 0
MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs

@@ -6,6 +6,7 @@ using System.IO;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Controller.MediaEncoding
 namespace MediaBrowser.Controller.MediaEncoding
@@ -17,5 +18,10 @@ namespace MediaBrowser.Controller.MediaEncoding
             string mediaSourceId,
             string mediaSourceId,
             int attachmentStreamIndex,
             int attachmentStreamIndex,
             CancellationToken cancellationToken);
             CancellationToken cancellationToken);
+        Task ExtractAllAttachments(
+            string inputFile,
+            MediaSourceInfo mediaSource,
+            string outputPath,
+            CancellationToken cancellationToken);
     }
     }
 }
 }

+ 124 - 0
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs

@@ -83,6 +83,130 @@ namespace MediaBrowser.MediaEncoding.Attachments
             return (mediaAttachment, attachmentStream);
             return (mediaAttachment, attachmentStream);
         }
         }
 
 
+        public async Task ExtractAllAttachments(
+            string inputFile,
+            MediaSourceInfo mediaSource,
+            string outputPath,
+            CancellationToken cancellationToken)
+        {
+            var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
+
+            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                if (!Directory.Exists(outputPath))
+                {
+                    await ExtractAllAttachmentsInternal(
+                        _mediaEncoder.GetInputArgument(inputFile, mediaSource),
+                        outputPath,
+                        cancellationToken).ConfigureAwait(false);
+                }
+            }
+            finally
+            {
+                semaphore.Release();
+            }
+        }
+
+        private async Task ExtractAllAttachmentsInternal(
+            string inputPath,
+            string outputPath,
+            CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrEmpty(inputPath))
+            {
+                throw new ArgumentNullException(nameof(inputPath));
+            }
+
+            if (string.IsNullOrEmpty(outputPath))
+            {
+                throw new ArgumentNullException(nameof(outputPath));
+            }
+
+            Directory.CreateDirectory(outputPath);
+
+            var processArgs = string.Format(
+                CultureInfo.InvariantCulture,
+                "-dump_attachment:t \"\" -i {0} -t 0 -f null null",
+                inputPath);
+
+            int exitCode;
+
+            using (var process = new Process
+                {
+                    StartInfo = new ProcessStartInfo
+                    {
+                        Arguments = processArgs,
+                        FileName = _mediaEncoder.EncoderPath,
+                        UseShellExecute = false,
+                        CreateNoWindow = true,
+                        WindowStyle = ProcessWindowStyle.Hidden,
+                        WorkingDirectory = outputPath,
+                        ErrorDialog = false
+                    },
+                    EnableRaisingEvents = true
+                })
+            {
+                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+                process.Start();
+
+                var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
+
+                if (!ranToCompletion)
+                {
+                    try
+                    {
+                        _logger.LogWarning("Killing ffmpeg attachment extraction process");
+                        process.Kill();
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Error killing attachment extraction process");
+                    }
+                }
+
+                exitCode = ranToCompletion ? process.ExitCode : -1;
+            }
+
+            var failed = false;
+
+            if (exitCode != 0)
+            {
+                failed = true;
+
+                _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode);
+                try
+                {
+                    if (Directory.Exists(outputPath))
+                    {
+                        Directory.Delete(outputPath);
+                    }
+                }
+                catch (IOException ex)
+                {
+                    _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath);
+                }
+            }
+            else if (!Directory.Exists(outputPath))
+            {
+                failed = true;
+            }
+
+            if (failed)
+            {
+                _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
+
+                throw new InvalidOperationException(
+                    string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
+            }
+            else
+            {
+                _logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
+            }
+        }
+
         private async Task<Stream> GetAttachmentStream(
         private async Task<Stream> GetAttachmentStream(
             MediaSourceInfo mediaSource,
             MediaSourceInfo mediaSource,
             MediaAttachment mediaAttachment,
             MediaAttachment mediaAttachment,