2020-07-22 13:34:51 +02:00
#pragma warning disable CS1591
2019-01-13 21:02:23 +01:00
using System ;
2018-12-14 10:40:55 +01:00
using System.Collections.Concurrent ;
2020-03-27 00:10:16 +01:00
using System.Diagnostics ;
2018-12-14 10:40:55 +01:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
2020-08-31 19:07:40 +02:00
using System.Net.Http ;
2018-12-14 10:40:55 +01:00
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
2019-01-06 20:59:13 +01:00
using MediaBrowser.Common.Configuration ;
using MediaBrowser.Common.Extensions ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.MediaEncoding ;
2018-12-14 10:40:55 +01:00
using MediaBrowser.Model.Dto ;
2019-01-06 20:59:13 +01:00
using MediaBrowser.Model.Entities ;
2019-01-13 20:26:04 +01:00
using MediaBrowser.Model.IO ;
2019-01-06 20:59:13 +01:00
using MediaBrowser.Model.MediaInfo ;
using Microsoft.Extensions.Logging ;
2019-01-16 20:50:40 +01:00
using UtfUnknown ;
2018-12-14 10:40:55 +01:00
namespace MediaBrowser.MediaEncoding.Subtitles
{
public class SubtitleEncoder : ISubtitleEncoder
{
private readonly ILibraryManager _libraryManager ;
2020-06-06 02:15:56 +02:00
private readonly ILogger < SubtitleEncoder > _logger ;
2018-12-14 10:40:55 +01:00
private readonly IApplicationPaths _appPaths ;
private readonly IFileSystem _fileSystem ;
private readonly IMediaEncoder _mediaEncoder ;
2020-08-31 19:07:40 +02:00
private readonly IHttpClientFactory _httpClientFactory ;
2018-12-14 10:40:55 +01:00
private readonly IMediaSourceManager _mediaSourceManager ;
2020-08-04 17:08:09 +02:00
/// <summary>
/// The _semaphoreLocks.
/// </summary>
private readonly ConcurrentDictionary < string , SemaphoreSlim > _semaphoreLocks =
new ConcurrentDictionary < string , SemaphoreSlim > ( ) ;
2019-01-17 23:55:05 +01:00
public SubtitleEncoder (
ILibraryManager libraryManager ,
2019-10-26 22:53:53 +02:00
ILogger < SubtitleEncoder > logger ,
2019-01-04 18:46:52 +01:00
IApplicationPaths appPaths ,
IFileSystem fileSystem ,
IMediaEncoder mediaEncoder ,
2020-08-31 19:07:40 +02:00
IHttpClientFactory httpClientFactory ,
2020-03-27 00:45:48 +01:00
IMediaSourceManager mediaSourceManager )
2018-12-14 10:40:55 +01:00
{
_libraryManager = libraryManager ;
2019-10-26 22:53:53 +02:00
_logger = logger ;
2018-12-14 10:40:55 +01:00
_appPaths = appPaths ;
_fileSystem = fileSystem ;
_mediaEncoder = mediaEncoder ;
2020-08-31 19:07:40 +02:00
_httpClientFactory = httpClientFactory ;
2019-01-04 18:46:52 +01:00
_mediaSourceManager = mediaSourceManager ;
2018-12-14 10:40:55 +01:00
}
2019-01-13 21:31:14 +01:00
private string SubtitleCachePath = > Path . Combine ( _appPaths . DataPath , "subtitles" ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
private Stream ConvertSubtitles (
Stream stream ,
2018-12-14 10:40:55 +01:00
string inputFormat ,
string outputFormat ,
long startTimeTicks ,
2019-01-20 08:17:31 +01:00
long endTimeTicks ,
2018-12-14 10:40:55 +01:00
bool preserveOriginalTimestamps ,
CancellationToken cancellationToken )
{
var ms = new MemoryStream ( ) ;
try
{
var reader = GetReader ( inputFormat , true ) ;
var trackInfo = reader . Parse ( stream , cancellationToken ) ;
FilterEvents ( trackInfo , startTimeTicks , endTimeTicks , preserveOriginalTimestamps ) ;
var writer = GetWriter ( outputFormat ) ;
writer . Write ( trackInfo , ms , cancellationToken ) ;
ms . Position = 0 ;
}
catch
{
ms . Dispose ( ) ;
throw ;
}
return ms ;
}
2019-01-20 08:17:31 +01:00
private void FilterEvents ( SubtitleTrackInfo track , long startPositionTicks , long endTimeTicks , bool preserveTimestamps )
2018-12-14 10:40:55 +01:00
{
// Drop subs that are earlier than what we're looking for
track . TrackEvents = track . TrackEvents
. SkipWhile ( i = > ( i . StartPositionTicks - startPositionTicks ) < 0 | | ( i . EndPositionTicks - startPositionTicks ) < 0 )
. ToArray ( ) ;
2019-01-20 08:17:31 +01:00
if ( endTimeTicks > 0 )
2018-12-14 10:40:55 +01:00
{
track . TrackEvents = track . TrackEvents
2019-01-20 17:06:40 +01:00
. TakeWhile ( i = > i . StartPositionTicks < = endTimeTicks )
2018-12-14 10:40:55 +01:00
. ToArray ( ) ;
}
if ( ! preserveTimestamps )
{
foreach ( var trackEvent in track . TrackEvents )
{
trackEvent . EndPositionTicks - = startPositionTicks ;
trackEvent . StartPositionTicks - = startPositionTicks ;
}
}
}
async Task < Stream > ISubtitleEncoder . GetSubtitles ( BaseItem item , string mediaSourceId , int subtitleStreamIndex , string outputFormat , long startTimeTicks , long endTimeTicks , bool preserveOriginalTimestamps , CancellationToken cancellationToken )
{
if ( item = = null )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( item ) ) ;
2018-12-14 10:40:55 +01:00
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . IsNullOrWhiteSpace ( mediaSourceId ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( mediaSourceId ) ) ;
2018-12-14 10:40:55 +01:00
}
2019-12-04 21:39:27 +01:00
var mediaSources = await _mediaSourceManager . GetPlaybackMediaSources ( item , null , true , false , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
var mediaSource = mediaSources
. First ( i = > string . Equals ( i . Id , mediaSourceId , StringComparison . OrdinalIgnoreCase ) ) ;
var subtitleStream = mediaSource . MediaStreams
. First ( i = > i . Type = = MediaStreamType . Subtitle & & i . Index = = subtitleStreamIndex ) ;
var subtitle = await GetSubtitleStream ( mediaSource , subtitleStream , cancellationToken )
. ConfigureAwait ( false ) ;
2019-01-06 20:59:13 +01:00
var inputFormat = subtitle . format ;
2018-12-14 10:40:55 +01:00
var writer = TryGetWriter ( outputFormat ) ;
// Return the original if we don't have any way of converting it
if ( writer = = null )
{
2019-01-06 20:59:13 +01:00
return subtitle . stream ;
2018-12-14 10:40:55 +01:00
}
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
if ( string . Equals ( inputFormat , outputFormat , StringComparison . OrdinalIgnoreCase ) )
{
2019-01-06 20:59:13 +01:00
return subtitle . stream ;
2018-12-14 10:40:55 +01:00
}
2019-01-06 20:59:13 +01:00
using ( var stream = subtitle . stream )
2018-12-14 10:40:55 +01:00
{
2019-01-06 20:59:13 +01:00
return ConvertSubtitles ( stream , inputFormat , outputFormat , startTimeTicks , endTimeTicks , preserveOriginalTimestamps , cancellationToken ) ;
2018-12-14 10:40:55 +01:00
}
}
2019-01-06 20:59:13 +01:00
private async Task < ( Stream stream , string format ) > GetSubtitleStream (
MediaSourceInfo mediaSource ,
2018-12-14 10:40:55 +01:00
MediaStream subtitleStream ,
CancellationToken cancellationToken )
{
2019-01-06 20:59:13 +01:00
string [ ] inputFiles ;
2018-12-14 10:40:55 +01:00
2019-01-06 20:59:13 +01:00
if ( mediaSource . VideoType . HasValue
& & ( mediaSource . VideoType . Value = = VideoType . BluRay | | mediaSource . VideoType . Value = = VideoType . Dvd ) )
2018-12-14 10:40:55 +01:00
{
2019-01-06 20:59:13 +01:00
var mediaSourceItem = ( Video ) _libraryManager . GetItemById ( new Guid ( mediaSource . Id ) ) ;
2019-10-26 22:53:53 +02:00
inputFiles = mediaSourceItem . GetPlayableStreamFileNames ( ) ;
2019-01-06 20:59:13 +01:00
}
else
{
inputFiles = new [ ] { mediaSource . Path } ;
2018-12-14 10:40:55 +01:00
}
2020-07-25 12:44:31 +02:00
var protocol = mediaSource . Protocol ;
2020-07-25 13:41:04 +02:00
if ( subtitleStream . IsExternal )
{
2020-07-25 12:44:31 +02:00
protocol = _mediaSourceManager . GetPathProtocol ( subtitleStream . Path ) ;
}
2020-07-25 13:41:04 +02:00
2020-07-25 12:44:31 +02:00
var fileInfo = await GetReadableFile ( mediaSource . Path , inputFiles , protocol , subtitleStream , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
var stream = await GetSubtitleStream ( fileInfo . Path , fileInfo . Protocol , fileInfo . IsExternal , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
2019-01-06 20:59:13 +01:00
return ( stream , fileInfo . Format ) ;
2018-12-14 10:40:55 +01:00
}
2019-10-26 22:53:53 +02:00
private async Task < Stream > GetSubtitleStream ( string path , MediaProtocol protocol , bool requiresCharset , CancellationToken cancellationToken )
2018-12-14 10:40:55 +01:00
{
2020-03-23 17:39:12 +01:00
if ( requiresCharset )
2018-12-14 10:40:55 +01:00
{
2020-03-23 17:39:12 +01:00
using ( var stream = await GetStream ( path , protocol , cancellationToken ) . ConfigureAwait ( false ) )
2018-12-14 10:40:55 +01:00
{
2019-10-26 22:53:53 +02:00
var result = CharsetDetector . DetectFromStream ( stream ) . Detected ;
2020-03-12 17:18:49 +01:00
stream . Position = 0 ;
2019-10-26 22:53:53 +02:00
if ( result ! = null )
2018-12-14 10:40:55 +01:00
{
2019-10-26 22:53:53 +02:00
_logger . LogDebug ( "charset {CharSet} detected for {Path}" , result . EncodingName , path ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
using var reader = new StreamReader ( stream , result . Encoding ) ;
var text = await reader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
return new MemoryStream ( Encoding . UTF8 . GetBytes ( text ) ) ;
2018-12-14 10:40:55 +01:00
}
}
2020-03-12 17:18:49 +01:00
}
2020-03-23 17:39:12 +01:00
return File . OpenRead ( path ) ;
2018-12-14 10:40:55 +01:00
}
2019-01-06 20:59:13 +01:00
private async Task < SubtitleInfo > GetReadableFile (
string mediaPath ,
2018-12-14 10:40:55 +01:00
string [ ] inputFiles ,
MediaProtocol protocol ,
MediaStream subtitleStream ,
CancellationToken cancellationToken )
{
if ( ! subtitleStream . IsExternal )
{
string outputFormat ;
string outputCodec ;
if ( string . Equals ( subtitleStream . Codec , "ass" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( subtitleStream . Codec , "ssa" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( subtitleStream . Codec , "srt" , StringComparison . OrdinalIgnoreCase ) )
{
2019-01-06 20:59:13 +01:00
// Extract
2018-12-14 10:40:55 +01:00
outputCodec = "copy" ;
outputFormat = subtitleStream . Codec ;
}
else if ( string . Equals ( subtitleStream . Codec , "subrip" , StringComparison . OrdinalIgnoreCase ) )
{
2019-01-06 20:59:13 +01:00
// Extract
2018-12-14 10:40:55 +01:00
outputCodec = "copy" ;
outputFormat = "srt" ;
}
else
{
2019-01-06 20:59:13 +01:00
// Extract
2018-12-14 10:40:55 +01:00
outputCodec = "srt" ;
outputFormat = "srt" ;
}
2019-01-06 20:59:13 +01:00
// Extract
2018-12-14 10:40:55 +01:00
var outputPath = GetSubtitleCachePath ( mediaPath , protocol , subtitleStream . Index , "." + outputFormat ) ;
await ExtractTextSubtitle ( inputFiles , protocol , subtitleStream . Index , outputCodec , outputPath , cancellationToken )
. ConfigureAwait ( false ) ;
2019-01-06 20:59:13 +01:00
return new SubtitleInfo ( outputPath , MediaProtocol . File , outputFormat , false ) ;
2018-12-14 10:40:55 +01:00
}
var currentFormat = ( Path . GetExtension ( subtitleStream . Path ) ? ? subtitleStream . Codec )
. TrimStart ( '.' ) ;
if ( GetReader ( currentFormat , false ) = = null )
{
2019-01-06 20:59:13 +01:00
// Convert
2018-12-14 10:40:55 +01:00
var outputPath = GetSubtitleCachePath ( mediaPath , protocol , subtitleStream . Index , ".srt" ) ;
await ConvertTextSubtitleToSrt ( subtitleStream . Path , subtitleStream . Language , protocol , outputPath , cancellationToken ) . ConfigureAwait ( false ) ;
2019-01-06 20:59:13 +01:00
return new SubtitleInfo ( outputPath , MediaProtocol . File , "srt" , true ) ;
}
return new SubtitleInfo ( subtitleStream . Path , protocol , currentFormat , true ) ;
}
2018-12-14 10:40:55 +01:00
private ISubtitleParser GetReader ( string format , bool throwIfMissing )
{
if ( string . IsNullOrEmpty ( format ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( format ) ) ;
2018-12-14 10:40:55 +01:00
}
if ( string . Equals ( format , SubtitleFormat . SRT , StringComparison . OrdinalIgnoreCase ) )
{
return new SrtParser ( _logger ) ;
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . Equals ( format , SubtitleFormat . SSA , StringComparison . OrdinalIgnoreCase ) )
{
return new SsaParser ( ) ;
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . Equals ( format , SubtitleFormat . ASS , StringComparison . OrdinalIgnoreCase ) )
{
return new AssParser ( ) ;
}
if ( throwIfMissing )
{
throw new ArgumentException ( "Unsupported format: " + format ) ;
}
return null ;
}
private ISubtitleWriter TryGetWriter ( string format )
{
if ( string . IsNullOrEmpty ( format ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( format ) ) ;
2018-12-14 10:40:55 +01:00
}
if ( string . Equals ( format , "json" , StringComparison . OrdinalIgnoreCase ) )
{
2019-10-26 22:53:53 +02:00
return new JsonWriter ( ) ;
2018-12-14 10:40:55 +01:00
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . Equals ( format , SubtitleFormat . SRT , StringComparison . OrdinalIgnoreCase ) )
{
return new SrtWriter ( ) ;
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . Equals ( format , SubtitleFormat . VTT , StringComparison . OrdinalIgnoreCase ) )
{
return new VttWriter ( ) ;
}
2020-06-15 23:43:52 +02:00
2018-12-14 10:40:55 +01:00
if ( string . Equals ( format , SubtitleFormat . TTML , StringComparison . OrdinalIgnoreCase ) )
{
return new TtmlWriter ( ) ;
}
return null ;
}
private ISubtitleWriter GetWriter ( string format )
{
var writer = TryGetWriter ( format ) ;
if ( writer ! = null )
{
return writer ;
}
throw new ArgumentException ( "Unsupported format: " + format ) ;
}
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock ( string filename )
{
2020-07-22 13:34:51 +02:00
return _semaphoreLocks . GetOrAdd ( filename , _ = > new SemaphoreSlim ( 1 , 1 ) ) ;
2018-12-14 10:40:55 +01:00
}
/// <summary>
/// Converts the text subtitle to SRT.
/// </summary>
/// <param name="inputPath">The input path.</param>
2020-08-04 16:20:52 +02:00
/// <param name="language">The language.</param>
2018-12-14 10:40:55 +01:00
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task ConvertTextSubtitleToSrt ( string inputPath , string language , MediaProtocol inputProtocol , string outputPath , CancellationToken cancellationToken )
{
var semaphore = GetLock ( outputPath ) ;
await semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
try
{
2019-01-26 22:59:53 +01:00
if ( ! File . Exists ( outputPath ) )
2018-12-14 10:40:55 +01:00
{
await ConvertTextSubtitleToSrtInternal ( inputPath , language , inputProtocol , outputPath , cancellationToken ) . ConfigureAwait ( false ) ;
}
}
finally
{
semaphore . Release ( ) ;
}
}
/// <summary>
/// Converts the text subtitle to SRT internal.
/// </summary>
/// <param name="inputPath">The input path.</param>
2020-08-04 16:20:52 +02:00
/// <param name="language">The language.</param>
2018-12-14 10:40:55 +01:00
/// <param name="inputProtocol">The input protocol.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2019-01-13 21:37:13 +01:00
/// <exception cref="ArgumentNullException">
2020-08-04 17:08:09 +02:00
/// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
2018-12-14 10:40:55 +01:00
/// </exception>
private async Task ConvertTextSubtitleToSrtInternal ( string inputPath , string language , MediaProtocol inputProtocol , string outputPath , CancellationToken cancellationToken )
{
if ( string . IsNullOrEmpty ( inputPath ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( inputPath ) ) ;
2018-12-14 10:40:55 +01:00
}
if ( string . IsNullOrEmpty ( outputPath ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( outputPath ) ) ;
2018-12-14 10:40:55 +01:00
}
2019-01-26 22:08:04 +01:00
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
2018-12-14 10:40:55 +01:00
var encodingParam = await GetSubtitleFileCharacterSet ( inputPath , language , inputProtocol , cancellationToken ) . ConfigureAwait ( false ) ;
2019-07-14 16:05:45 +02:00
// FFmpeg automatically convert character encoding when it is UTF-16
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
2020-08-20 12:16:24 +02:00
if ( ( inputPath . EndsWith ( ".smi" , StringComparison . Ordinal ) | | inputPath . EndsWith ( ".sami" , StringComparison . Ordinal ) ) & &
2020-04-15 19:30:23 +02:00
( encodingParam . Equals ( "UTF-16BE" , StringComparison . OrdinalIgnoreCase ) | |
encodingParam . Equals ( "UTF-16LE" , StringComparison . OrdinalIgnoreCase ) ) )
2019-07-14 16:05:45 +02:00
{
2020-08-04 17:08:09 +02:00
encodingParam = string . Empty ;
2019-07-14 16:05:45 +02:00
}
else if ( ! string . IsNullOrEmpty ( encodingParam ) )
2018-12-14 10:40:55 +01:00
{
encodingParam = " -sub_charenc " + encodingParam ;
}
2020-03-27 01:53:08 +01:00
int exitCode ;
2018-12-14 10:40:55 +01:00
2020-04-11 19:25:50 +02:00
using ( var process = new Process
{
2020-04-11 19:46:31 +02:00
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true ,
UseShellExecute = false ,
FileName = _mediaEncoder . EncoderPath ,
2020-08-07 19:26:28 +02:00
Arguments = string . Format ( CultureInfo . InvariantCulture , "{0} -i \"{1}\" -c:s srt \"{2}\"" , encodingParam , inputPath , outputPath ) ,
2020-04-11 19:46:31 +02:00
WindowStyle = ProcessWindowStyle . Hidden ,
ErrorDialog = false
} ,
EnableRaisingEvents = true
} )
2018-12-14 10:40:55 +01:00
{
2020-03-27 01:53:08 +01:00
_logger . LogInformation ( "{0} {1}" , process . StartInfo . FileName , process . StartInfo . Arguments ) ;
2018-12-14 10:40:55 +01:00
try
{
2020-03-27 01:53:08 +01:00
process . Start ( ) ;
2018-12-14 10:40:55 +01:00
}
catch ( Exception ex )
{
2020-03-27 01:53:08 +01:00
_logger . LogError ( ex , "Error starting ffmpeg" ) ;
throw ;
2018-12-14 10:40:55 +01:00
}
2020-03-27 01:53:08 +01:00
var ranToCompletion = await process . WaitForExitAsync ( TimeSpan . FromMinutes ( 5 ) ) . ConfigureAwait ( false ) ;
if ( ! ranToCompletion )
{
try
{
_logger . LogInformation ( "Killing ffmpeg subtitle conversion process" ) ;
process . Kill ( ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error killing subtitle conversion process" ) ;
}
}
exitCode = ranToCompletion ? process . ExitCode : - 1 ;
}
2018-12-14 10:40:55 +01:00
var failed = false ;
if ( exitCode = = - 1 )
{
failed = true ;
2019-01-26 22:59:53 +01:00
if ( File . Exists ( outputPath ) )
2018-12-14 10:40:55 +01:00
{
try
{
2018-12-15 00:48:06 +01:00
_logger . LogInformation ( "Deleting converted subtitle due to failure: " , outputPath ) ;
2018-12-14 10:40:55 +01:00
_fileSystem . DeleteFile ( outputPath ) ;
}
catch ( IOException ex )
{
2019-01-06 20:59:13 +01:00
_logger . LogError ( ex , "Error deleting converted subtitle {Path}" , outputPath ) ;
2018-12-14 10:40:55 +01:00
}
}
}
2019-01-26 22:59:53 +01:00
else if ( ! File . Exists ( outputPath ) )
2018-12-14 10:40:55 +01:00
{
failed = true ;
}
if ( failed )
{
2019-09-20 12:42:08 +02:00
_logger . LogError ( "ffmpeg subtitle conversion failed for {Path}" , inputPath ) ;
2018-12-14 10:40:55 +01:00
2019-09-20 12:42:08 +02:00
throw new Exception (
string . Format ( CultureInfo . InvariantCulture , "ffmpeg subtitle conversion failed for {0}" , inputPath ) ) ;
2018-12-14 10:40:55 +01:00
}
2019-09-20 12:42:08 +02:00
2020-08-20 12:16:24 +02:00
await SetAssFont ( outputPath , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
2019-01-06 20:59:13 +01:00
_logger . LogInformation ( "ffmpeg subtitle conversion succeeded for {Path}" , inputPath ) ;
2018-12-14 10:40:55 +01:00
}
/// <summary>
/// Extracts the text subtitle.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
/// <param name="outputCodec">The output codec.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2020-08-04 17:08:09 +02:00
/// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
2019-01-06 20:59:13 +01:00
private async Task ExtractTextSubtitle (
string [ ] inputFiles ,
MediaProtocol protocol ,
int subtitleStreamIndex ,
string outputCodec ,
string outputPath ,
CancellationToken cancellationToken )
2018-12-14 10:40:55 +01:00
{
var semaphore = GetLock ( outputPath ) ;
await semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
try
{
2019-01-26 22:59:53 +01:00
if ( ! File . Exists ( outputPath ) )
2018-12-14 10:40:55 +01:00
{
2019-10-26 22:53:53 +02:00
await ExtractTextSubtitleInternal (
_mediaEncoder . GetInputArgument ( inputFiles , protocol ) ,
subtitleStreamIndex ,
outputCodec ,
outputPath ,
cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
}
}
finally
{
semaphore . Release ( ) ;
}
}
2019-01-06 20:59:13 +01:00
private async Task ExtractTextSubtitleInternal (
string inputPath ,
int subtitleStreamIndex ,
string outputCodec ,
string outputPath ,
CancellationToken cancellationToken )
2018-12-14 10:40:55 +01:00
{
if ( string . IsNullOrEmpty ( inputPath ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( inputPath ) ) ;
2018-12-14 10:40:55 +01:00
}
if ( string . IsNullOrEmpty ( outputPath ) )
{
2019-01-06 21:50:43 +01:00
throw new ArgumentNullException ( nameof ( outputPath ) ) ;
2018-12-14 10:40:55 +01:00
}
2019-01-26 22:08:04 +01:00
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
var processArgs = string . Format (
CultureInfo . InvariantCulture ,
"-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"" ,
inputPath ,
subtitleStreamIndex ,
outputCodec ,
outputPath ) ;
2018-12-14 10:40:55 +01:00
2020-03-27 01:53:08 +01:00
int exitCode ;
2018-12-14 10:40:55 +01:00
2020-04-11 19:25:50 +02:00
using ( var process = new Process
{
2020-04-11 19:46:31 +02:00
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true ,
UseShellExecute = false ,
FileName = _mediaEncoder . EncoderPath ,
Arguments = processArgs ,
WindowStyle = ProcessWindowStyle . Hidden ,
ErrorDialog = false
} ,
EnableRaisingEvents = true
} )
2018-12-14 10:40:55 +01:00
{
2020-03-27 01:53:08 +01:00
_logger . LogInformation ( "{File} {Arguments}" , process . StartInfo . FileName , process . StartInfo . Arguments ) ;
2018-12-14 10:40:55 +01:00
try
{
2020-03-27 01:53:08 +01:00
process . Start ( ) ;
2018-12-14 10:40:55 +01:00
}
catch ( Exception ex )
{
2020-03-27 01:53:08 +01:00
_logger . LogError ( ex , "Error starting ffmpeg" ) ;
throw ;
2018-12-14 10:40:55 +01:00
}
2020-03-27 01:53:08 +01:00
var ranToCompletion = await process . WaitForExitAsync ( TimeSpan . FromMinutes ( 5 ) ) . ConfigureAwait ( false ) ;
if ( ! ranToCompletion )
{
try
{
_logger . LogWarning ( "Killing ffmpeg subtitle extraction process" ) ;
process . Kill ( ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error killing subtitle extraction process" ) ;
}
}
exitCode = ranToCompletion ? process . ExitCode : - 1 ;
}
2018-12-14 10:40:55 +01:00
var failed = false ;
if ( exitCode = = - 1 )
{
failed = true ;
try
{
2019-01-06 20:59:13 +01:00
_logger . LogWarning ( "Deleting extracted subtitle due to failure: {Path}" , outputPath ) ;
2018-12-14 10:40:55 +01:00
_fileSystem . DeleteFile ( outputPath ) ;
}
catch ( FileNotFoundException )
{
}
catch ( IOException ex )
{
2019-01-06 20:59:13 +01:00
_logger . LogError ( ex , "Error deleting extracted subtitle {Path}" , outputPath ) ;
2018-12-14 10:40:55 +01:00
}
}
2019-01-26 22:59:53 +01:00
else if ( ! File . Exists ( outputPath ) )
2018-12-14 10:40:55 +01:00
{
failed = true ;
}
if ( failed )
{
2019-01-06 20:59:13 +01:00
var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}" ;
2018-12-14 10:40:55 +01:00
2018-12-15 00:48:06 +01:00
_logger . LogError ( msg ) ;
2018-12-14 10:40:55 +01:00
throw new Exception ( msg ) ;
}
else
{
2019-01-06 20:59:13 +01:00
var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}" ;
2018-12-14 10:40:55 +01:00
2018-12-15 00:48:06 +01:00
_logger . LogInformation ( msg ) ;
2018-12-14 10:40:55 +01:00
}
if ( string . Equals ( outputCodec , "ass" , StringComparison . OrdinalIgnoreCase ) )
{
2020-08-20 12:16:24 +02:00
await SetAssFont ( outputPath , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
}
}
/// <summary>
/// Sets the ass font.
/// </summary>
/// <param name="file">The file.</param>
2020-08-20 12:16:24 +02:00
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>System.Threading.CancellationToken.None</c>.</param>
2018-12-14 10:40:55 +01:00
/// <returns>Task.</returns>
2020-08-20 12:16:24 +02:00
private async Task SetAssFont ( string file , CancellationToken cancellationToken = default )
2018-12-14 10:40:55 +01:00
{
2019-01-06 20:59:13 +01:00
_logger . LogInformation ( "Setting ass font within {File}" , file ) ;
2018-12-14 10:40:55 +01:00
string text ;
Encoding encoding ;
2019-01-26 22:31:59 +01:00
using ( var fileStream = File . OpenRead ( file ) )
2019-01-06 20:59:13 +01:00
using ( var reader = new StreamReader ( fileStream , true ) )
2018-12-14 10:40:55 +01:00
{
2019-01-06 20:59:13 +01:00
encoding = reader . CurrentEncoding ;
2018-12-14 10:40:55 +01:00
2019-01-06 20:59:13 +01:00
text = await reader . ReadToEndAsync ( ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
}
2020-08-20 12:16:24 +02:00
var newText = text . Replace ( ",Arial," , ",Arial Unicode MS," , StringComparison . Ordinal ) ;
2018-12-14 10:40:55 +01:00
2020-08-20 12:16:24 +02:00
if ( ! string . Equals ( text , newText , StringComparison . Ordinal ) )
2018-12-14 10:40:55 +01:00
{
2020-01-08 17:52:50 +01:00
using ( var fileStream = new FileStream ( file , FileMode . Create , FileAccess . Write , FileShare . Read ) )
2018-12-15 00:48:06 +01:00
using ( var writer = new StreamWriter ( fileStream , encoding ) )
2018-12-14 10:40:55 +01:00
{
2020-08-20 12:16:24 +02:00
await writer . WriteAsync ( newText . AsMemory ( ) , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-14 10:40:55 +01:00
}
}
}
private string GetSubtitleCachePath ( string mediaPath , MediaProtocol protocol , int subtitleStreamIndex , string outputSubtitleExtension )
{
if ( protocol = = MediaProtocol . File )
{
var ticksParam = string . Empty ;
var date = _fileSystem . GetLastWriteTimeUtc ( mediaPath ) ;
2020-07-29 13:17:01 +02:00
ReadOnlySpan < char > filename = ( mediaPath + "_" + subtitleStreamIndex . ToString ( CultureInfo . InvariantCulture ) + "_" + date . Ticks . ToString ( CultureInfo . InvariantCulture ) + ticksParam ) . GetMD5 ( ) + outputSubtitleExtension ;
2018-12-14 10:40:55 +01:00
2020-07-29 13:17:01 +02:00
var prefix = filename . Slice ( 0 , 1 ) ;
2018-12-14 10:40:55 +01:00
2020-07-29 13:17:01 +02:00
return Path . Join ( SubtitleCachePath , prefix , filename ) ;
2018-12-14 10:40:55 +01:00
}
else
{
2020-07-29 13:17:01 +02:00
ReadOnlySpan < char > filename = ( mediaPath + "_" + subtitleStreamIndex . ToString ( CultureInfo . InvariantCulture ) ) . GetMD5 ( ) + outputSubtitleExtension ;
2018-12-14 10:40:55 +01:00
2020-07-29 13:17:01 +02:00
var prefix = filename . Slice ( 0 , 1 ) ;
2018-12-14 10:40:55 +01:00
2020-07-29 13:17:01 +02:00
return Path . Join ( SubtitleCachePath , prefix , filename ) ;
2018-12-14 10:40:55 +01:00
}
}
2019-10-26 22:53:53 +02:00
/// <inheritdoc />
2018-12-14 10:40:55 +01:00
public async Task < string > GetSubtitleFileCharacterSet ( string path , string language , MediaProtocol protocol , CancellationToken cancellationToken )
{
2019-10-26 22:53:53 +02:00
using ( var stream = await GetStream ( path , protocol , cancellationToken ) . ConfigureAwait ( false ) )
{
var charset = CharsetDetector . DetectFromStream ( stream ) . Detected ? . EncodingName ;
2018-12-14 10:40:55 +01:00
2020-03-30 08:46:05 +02:00
// UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
2020-08-20 12:16:24 +02:00
if ( ( path . EndsWith ( ".ass" , StringComparison . Ordinal ) | | path . EndsWith ( ".ssa" , StringComparison . Ordinal ) | | path . EndsWith ( ".srt" , StringComparison . Ordinal ) )
2020-03-30 08:46:05 +02:00
& & ( string . Equals ( charset , "utf-16le" , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( charset , "utf-16be" , StringComparison . OrdinalIgnoreCase ) ) )
{
2020-08-04 17:08:09 +02:00
charset = string . Empty ;
2020-03-30 08:46:05 +02:00
}
2019-10-26 22:53:53 +02:00
_logger . LogDebug ( "charset {0} detected for {Path}" , charset ? ? "null" , path ) ;
2018-12-14 10:40:55 +01:00
2019-10-26 22:53:53 +02:00
return charset ;
}
2018-12-14 10:40:55 +01:00
}
2020-08-31 19:07:40 +02:00
private async Task < Stream > GetStream ( string path , MediaProtocol protocol , CancellationToken cancellationToken )
2018-12-14 10:40:55 +01:00
{
2019-10-26 22:53:53 +02:00
switch ( protocol )
2018-12-14 10:40:55 +01:00
{
2019-10-26 22:53:53 +02:00
case MediaProtocol . Http :
2020-08-31 19:07:40 +02:00
{
using var response = await _httpClientFactory . CreateClient ( NamedClient . Default )
. GetAsync ( path , cancellationToken )
. ConfigureAwait ( false ) ;
return await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ;
}
2018-12-14 10:40:55 +01:00
2020-03-24 16:12:06 +01:00
case MediaProtocol . File :
2020-08-31 19:07:40 +02:00
return File . OpenRead ( path ) ;
2020-03-24 16:12:06 +01:00
default :
throw new ArgumentOutOfRangeException ( nameof ( protocol ) ) ;
2019-10-26 22:53:53 +02:00
}
2018-12-14 10:40:55 +01:00
}
2020-08-04 17:08:09 +02:00
private struct SubtitleInfo
{
public SubtitleInfo ( string path , MediaProtocol protocol , string format , bool isExternal )
{
Path = path ;
Protocol = protocol ;
Format = format ;
IsExternal = isExternal ;
}
public string Path { get ; set ; }
public MediaProtocol Protocol { get ; set ; }
public string Format { get ; set ; }
public bool IsExternal { get ; set ; }
}
2018-12-14 10:40:55 +01:00
}
2018-12-15 00:48:06 +01:00
}