2014-10-06 23:58:46 +00:00
using MediaBrowser.Common.Configuration ;
2013-09-23 15:37:50 +00:00
using MediaBrowser.Common.IO ;
using MediaBrowser.Common.Net ;
2013-09-25 00:54:51 +00:00
using MediaBrowser.Model.IO ;
2013-09-23 15:37:50 +00:00
using MediaBrowser.Model.Logging ;
2013-09-23 17:14:17 +00:00
using MediaBrowser.Model.Net ;
2014-10-06 23:58:46 +00:00
using Mono.Unix.Native ;
2013-09-23 15:37:50 +00:00
using System ;
2014-10-06 23:58:46 +00:00
using System.Collections.Generic ;
2013-09-23 15:37:50 +00:00
using System.IO ;
using System.Linq ;
using System.Text ;
2013-09-23 17:14:17 +00:00
using System.Threading ;
2013-09-23 15:37:50 +00:00
using System.Threading.Tasks ;
2014-11-09 18:24:57 +00:00
namespace MediaBrowser.Server.Startup.Common.FFMpeg
2013-09-23 15:37:50 +00:00
{
public class FFMpegDownloader
{
private readonly IHttpClient _httpClient ;
private readonly IApplicationPaths _appPaths ;
private readonly ILogger _logger ;
2013-09-25 00:54:51 +00:00
private readonly IZipClient _zipClient ;
2013-10-31 14:03:23 +00:00
private readonly IFileSystem _fileSystem ;
2014-11-23 23:10:41 +00:00
private readonly NativeEnvironment _environment ;
2013-09-23 15:37:50 +00:00
2014-07-22 01:29:06 +00:00
private readonly string [ ] _fontUrls =
{
2014-10-08 23:31:44 +00:00
"https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/ARIALUNI.7z"
2014-07-22 01:29:06 +00:00
} ;
2013-09-23 17:14:17 +00:00
2014-11-23 23:10:41 +00:00
public FFMpegDownloader ( ILogger logger , IApplicationPaths appPaths , IHttpClient httpClient , IZipClient zipClient , IFileSystem fileSystem , NativeEnvironment environment )
2013-09-23 15:37:50 +00:00
{
_logger = logger ;
_appPaths = appPaths ;
_httpClient = httpClient ;
2013-09-25 00:54:51 +00:00
_zipClient = zipClient ;
2013-10-31 14:03:23 +00:00
_fileSystem = fileSystem ;
2014-11-23 23:10:41 +00:00
_environment = environment ;
2013-09-23 15:37:50 +00:00
}
2014-11-09 18:24:57 +00:00
public async Task < FFMpegInfo > GetFFMpegInfo ( NativeEnvironment environment , StartupOptions options , IProgress < double > progress )
2013-09-23 15:37:50 +00:00
{
2014-09-14 15:26:33 +00:00
var customffMpegPath = options . GetOption ( "-ffmpeg" ) ;
var customffProbePath = options . GetOption ( "-ffprobe" ) ;
if ( ! string . IsNullOrWhiteSpace ( customffMpegPath ) & & ! string . IsNullOrWhiteSpace ( customffProbePath ) )
{
return new FFMpegInfo
{
ProbePath = customffProbePath ,
EncoderPath = customffMpegPath ,
Version = "custom"
} ;
}
2014-11-09 18:24:57 +00:00
var downloadInfo = FFMpegDownloadInfo . GetInfo ( environment ) ;
var version = downloadInfo . Version ;
2014-09-18 01:26:23 +00:00
if ( string . Equals ( version , "path" , StringComparison . OrdinalIgnoreCase ) )
{
return new FFMpegInfo
{
2014-11-09 18:24:57 +00:00
ProbePath = downloadInfo . FFProbeFilename ,
EncoderPath = downloadInfo . FFMpegFilename ,
2014-09-18 01:26:23 +00:00
Version = version
} ;
}
2014-05-07 02:28:19 +00:00
var rootEncoderPath = Path . Combine ( _appPaths . ProgramDataPath , "ffmpeg" ) ;
2014-09-18 01:26:23 +00:00
var versionedDirectoryPath = Path . Combine ( rootEncoderPath , version ) ;
2013-09-23 15:37:50 +00:00
2013-09-23 16:00:22 +00:00
var info = new FFMpegInfo
{
2014-11-09 18:24:57 +00:00
ProbePath = Path . Combine ( versionedDirectoryPath , downloadInfo . FFProbeFilename ) ,
EncoderPath = Path . Combine ( versionedDirectoryPath , downloadInfo . FFMpegFilename ) ,
2014-09-18 01:26:23 +00:00
Version = version
2013-09-23 16:00:22 +00:00
} ;
2013-09-23 15:37:50 +00:00
2015-09-13 21:32:02 +00:00
_fileSystem . CreateDirectory ( versionedDirectoryPath ) ;
2013-09-23 15:37:50 +00:00
2014-09-14 15:10:51 +00:00
var excludeFromDeletions = new List < string > { versionedDirectoryPath } ;
2015-09-13 21:32:02 +00:00
if ( ! _fileSystem . FileExists ( info . ProbePath ) | | ! _fileSystem . FileExists ( info . EncoderPath ) )
2013-09-23 16:00:22 +00:00
{
2014-05-07 02:28:19 +00:00
// ffmpeg not present. See if there's an older version we can start with
var existingVersion = GetExistingVersion ( info , rootEncoderPath ) ;
2013-12-13 15:48:35 +00:00
2014-05-07 02:28:19 +00:00
// No older version. Need to download and block until complete
if ( existingVersion = = null )
{
2014-11-09 18:24:57 +00:00
await DownloadFFMpeg ( downloadInfo , versionedDirectoryPath , progress ) . ConfigureAwait ( false ) ;
2014-05-07 02:28:19 +00:00
}
else
{
// Older version found.
// Start with that. Download new version in the background.
var newPath = versionedDirectoryPath ;
2014-11-09 18:24:57 +00:00
Task . Run ( ( ) = > DownloadFFMpegInBackground ( downloadInfo , newPath ) ) ;
2013-12-13 15:48:35 +00:00
2014-05-07 02:28:19 +00:00
info = existingVersion ;
versionedDirectoryPath = Path . GetDirectoryName ( info . EncoderPath ) ;
2014-09-14 15:10:51 +00:00
excludeFromDeletions . Add ( versionedDirectoryPath ) ;
2014-05-07 02:28:19 +00:00
}
2013-09-23 16:00:22 +00:00
}
2013-09-23 15:37:50 +00:00
2014-05-07 02:28:19 +00:00
await DownloadFonts ( versionedDirectoryPath ) . ConfigureAwait ( false ) ;
2014-09-14 15:10:51 +00:00
DeleteOlderFolders ( Path . GetDirectoryName ( versionedDirectoryPath ) , excludeFromDeletions ) ;
2014-05-07 02:28:19 +00:00
return info ;
}
2015-02-11 20:23:07 +00:00
private void DeleteOlderFolders ( string path , IEnumerable < string > excludeFolders )
2014-09-14 15:10:51 +00:00
{
var folders = Directory . GetDirectories ( path )
. Where ( i = > ! excludeFolders . Contains ( i , StringComparer . OrdinalIgnoreCase ) )
. ToList ( ) ;
foreach ( var folder in folders )
{
DeleteFolder ( folder ) ;
}
}
private void DeleteFolder ( string path )
{
try
{
2015-01-13 03:46:44 +00:00
_fileSystem . DeleteDirectory ( path , true ) ;
2014-09-14 15:10:51 +00:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error deleting {0}" , ex , path ) ;
}
}
2014-05-07 02:28:19 +00:00
private FFMpegInfo GetExistingVersion ( FFMpegInfo info , string rootEncoderPath )
{
var encoderFilename = Path . GetFileName ( info . EncoderPath ) ;
var probeFilename = Path . GetFileName ( info . ProbePath ) ;
foreach ( var directory in Directory . EnumerateDirectories ( rootEncoderPath , "*" , SearchOption . TopDirectoryOnly )
. ToList ( ) )
2013-12-13 15:48:35 +00:00
{
2014-05-07 02:28:19 +00:00
var allFiles = Directory . EnumerateFiles ( directory , "*" , SearchOption . AllDirectories ) . ToList ( ) ;
2013-12-13 15:48:35 +00:00
2014-05-07 02:28:19 +00:00
var encoder = allFiles . FirstOrDefault ( i = > string . Equals ( Path . GetFileName ( i ) , encoderFilename , StringComparison . OrdinalIgnoreCase ) ) ;
var probe = allFiles . FirstOrDefault ( i = > string . Equals ( Path . GetFileName ( i ) , probeFilename , StringComparison . OrdinalIgnoreCase ) ) ;
if ( ! string . IsNullOrWhiteSpace ( encoder ) & &
! string . IsNullOrWhiteSpace ( probe ) )
2013-12-13 15:48:35 +00:00
{
2014-05-07 02:28:19 +00:00
return new FFMpegInfo
{
2015-02-11 20:23:07 +00:00
EncoderPath = encoder ,
ProbePath = probe ,
Version = Path . GetFileName ( Path . GetDirectoryName ( probe ) )
2014-05-07 02:28:19 +00:00
} ;
2013-12-13 15:48:35 +00:00
}
2014-05-07 02:28:19 +00:00
}
2013-09-23 17:27:38 +00:00
2014-05-07 02:28:19 +00:00
return null ;
}
2013-09-23 16:00:22 +00:00
2014-11-09 18:24:57 +00:00
private async void DownloadFFMpegInBackground ( FFMpegDownloadInfo downloadinfo , string directory )
2014-05-07 02:28:19 +00:00
{
try
{
2014-11-09 18:24:57 +00:00
await DownloadFFMpeg ( downloadinfo , directory , new Progress < double > ( ) ) . ConfigureAwait ( false ) ;
2014-05-07 02:28:19 +00:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error downloading ffmpeg" , ex ) ;
}
2013-09-23 15:37:50 +00:00
}
2014-11-09 18:24:57 +00:00
private async Task DownloadFFMpeg ( FFMpegDownloadInfo downloadinfo , string directory , IProgress < double > progress )
2013-09-23 15:37:50 +00:00
{
2014-11-09 18:24:57 +00:00
foreach ( var url in downloadinfo . DownloadUrls )
2013-09-23 17:14:17 +00:00
{
2013-12-13 15:48:35 +00:00
progress . Report ( 0 ) ;
2013-09-23 17:14:17 +00:00
try
{
2013-12-13 15:48:35 +00:00
var tempFile = await _httpClient . GetTempFile ( new HttpRequestOptions
{
Url = url ,
CancellationToken = CancellationToken . None ,
Progress = progress
} ) . ConfigureAwait ( false ) ;
2013-09-23 15:37:50 +00:00
2014-11-09 18:24:57 +00:00
ExtractFFMpeg ( downloadinfo , tempFile , directory ) ;
2013-09-23 17:14:17 +00:00
return ;
}
2014-01-21 06:10:58 +00:00
catch ( Exception ex )
{
2014-07-30 03:31:35 +00:00
_logger . ErrorException ( "Error downloading {0}" , ex , url ) ;
2013-09-23 17:14:17 +00:00
}
}
2013-09-23 17:27:38 +00:00
2015-04-02 21:01:42 +00:00
if ( downloadinfo . DownloadUrls . Length = = 0 )
{
throw new ApplicationException ( "ffmpeg unvailable. Please install it and start the server with two command line arguments: -ffmpeg \"{PATH}\" and -ffprobe \"{PATH}\"" ) ;
}
else
{
throw new ApplicationException ( "Unable to download required components. Please try again later." ) ;
}
2013-09-23 17:14:17 +00:00
}
2014-11-09 18:24:57 +00:00
private void ExtractFFMpeg ( FFMpegDownloadInfo downloadinfo , string tempFile , string targetFolder )
2013-09-23 17:14:17 +00:00
{
2014-05-07 02:28:19 +00:00
_logger . Info ( "Extracting ffmpeg from {0}" , tempFile ) ;
2013-09-23 17:14:17 +00:00
var tempFolder = Path . Combine ( _appPaths . TempDirectory , Guid . NewGuid ( ) . ToString ( ) ) ;
2015-09-13 21:32:02 +00:00
_fileSystem . CreateDirectory ( tempFolder ) ;
2013-09-23 17:14:17 +00:00
try
{
2014-11-09 18:24:57 +00:00
ExtractArchive ( downloadinfo , tempFile , tempFolder ) ;
2013-10-13 03:39:22 +00:00
2015-02-11 20:23:07 +00:00
var files = Directory . EnumerateFiles ( tempFolder , "*" , SearchOption . AllDirectories )
. ToList ( ) ;
2013-09-23 17:14:17 +00:00
2013-10-13 03:39:22 +00:00
foreach ( var file in files . Where ( i = >
{
var filename = Path . GetFileName ( i ) ;
2013-09-23 17:14:17 +00:00
2013-10-13 03:39:22 +00:00
return
2014-11-09 18:24:57 +00:00
string . Equals ( filename , downloadinfo . FFProbeFilename , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( filename , downloadinfo . FFMpegFilename , StringComparison . OrdinalIgnoreCase ) ;
2013-10-13 03:39:22 +00:00
} ) )
2013-09-23 17:14:17 +00:00
{
2015-01-25 21:04:29 +00:00
var targetFile = Path . Combine ( targetFolder , Path . GetFileName ( file ) ) ;
2015-09-13 21:32:02 +00:00
_fileSystem . CopyFile ( file , targetFile , true ) ;
2015-01-25 21:04:29 +00:00
SetFilePermissions ( targetFile ) ;
2013-09-23 17:14:17 +00:00
}
}
finally
{
DeleteFile ( tempFile ) ;
}
}
2015-01-25 21:04:29 +00:00
private void SetFilePermissions ( string path )
2014-10-06 23:58:46 +00:00
{
// Linux: File permission to 666, and user's execute bit
2014-11-23 23:10:41 +00:00
if ( _environment . OperatingSystem = = OperatingSystem . Bsd | | _environment . OperatingSystem = = OperatingSystem . Linux | | _environment . OperatingSystem = = OperatingSystem . Osx )
2014-10-06 23:58:46 +00:00
{
2015-02-11 20:23:07 +00:00
_logger . Info ( "Syscall.chmod {0} FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH" , path ) ;
2015-01-28 04:17:35 +00:00
Syscall . chmod ( path , FilePermissions . DEFFILEMODE | FilePermissions . S_IRWXU | FilePermissions . S_IXGRP | FilePermissions . S_IXOTH ) ;
2014-10-06 23:58:46 +00:00
}
}
2014-11-09 18:24:57 +00:00
private void ExtractArchive ( FFMpegDownloadInfo downloadinfo , string archivePath , string targetPath )
2013-09-23 17:14:17 +00:00
{
2014-05-07 02:28:19 +00:00
_logger . Info ( "Extracting {0} to {1}" , archivePath , targetPath ) ;
2014-11-09 18:24:57 +00:00
if ( string . Equals ( downloadinfo . ArchiveType , "7z" , StringComparison . OrdinalIgnoreCase ) )
2013-10-13 03:39:22 +00:00
{
_zipClient . ExtractAllFrom7z ( archivePath , targetPath , true ) ;
}
2014-11-09 18:24:57 +00:00
else if ( string . Equals ( downloadinfo . ArchiveType , "gz" , StringComparison . OrdinalIgnoreCase ) )
2013-10-13 03:39:22 +00:00
{
_zipClient . ExtractAllFromTar ( archivePath , targetPath , true ) ;
}
2013-09-23 15:37:50 +00:00
}
2013-10-13 19:56:54 +00:00
private void Extract7zArchive ( string archivePath , string targetPath )
{
2014-05-07 02:28:19 +00:00
_logger . Info ( "Extracting {0} to {1}" , archivePath , targetPath ) ;
2013-10-13 19:56:54 +00:00
_zipClient . ExtractAllFrom7z ( archivePath , targetPath , true ) ;
}
2013-09-23 15:37:50 +00:00
2013-09-23 17:14:17 +00:00
private void DeleteFile ( string path )
{
try
{
2015-01-13 03:46:44 +00:00
_fileSystem . DeleteFile ( path ) ;
2013-09-23 17:14:17 +00:00
}
catch ( IOException ex )
{
_logger . ErrorException ( "Error deleting temp file {0}" , ex , path ) ;
}
}
2013-09-23 15:37:50 +00:00
/// <summary>
/// Extracts the fonts.
/// </summary>
/// <param name="targetPath">The target path.</param>
2014-05-07 02:28:19 +00:00
/// <returns>Task.</returns>
private async Task DownloadFonts ( string targetPath )
2013-09-23 15:37:50 +00:00
{
2013-09-23 17:27:38 +00:00
try
2013-09-23 15:37:50 +00:00
{
2013-09-23 17:27:38 +00:00
var fontsDirectory = Path . Combine ( targetPath , "fonts" ) ;
2013-09-23 15:37:50 +00:00
2015-09-13 21:32:02 +00:00
_fileSystem . CreateDirectory ( fontsDirectory ) ;
2013-09-23 17:27:38 +00:00
const string fontFilename = "ARIALUNI.TTF" ;
2013-09-23 15:37:50 +00:00
2013-09-23 17:27:38 +00:00
var fontFile = Path . Combine ( fontsDirectory , fontFilename ) ;
2015-09-13 21:32:02 +00:00
if ( _fileSystem . FileExists ( fontFile ) )
2013-09-23 17:27:38 +00:00
{
2014-05-07 02:28:19 +00:00
await WriteFontConfigFile ( fontsDirectory ) . ConfigureAwait ( false ) ;
}
else
{
// Kick this off, but no need to wait on it
Task . Run ( async ( ) = >
{
await DownloadFontFile ( fontsDirectory , fontFilename , new Progress < double > ( ) ) . ConfigureAwait ( false ) ;
2015-02-11 20:23:07 +00:00
2014-05-07 02:28:19 +00:00
await WriteFontConfigFile ( fontsDirectory ) . ConfigureAwait ( false ) ;
} ) ;
2013-09-23 17:27:38 +00:00
}
}
catch ( HttpException ex )
2013-09-23 15:37:50 +00:00
{
2013-09-23 17:27:38 +00:00
// Don't let the server crash because of this
_logger . ErrorException ( "Error downloading ffmpeg font files" , ex ) ;
}
catch ( Exception ex )
{
// Don't let the server crash because of this
_logger . ErrorException ( "Error writing ffmpeg font files" , ex ) ;
2013-09-23 15:37:50 +00:00
}
}
/// <summary>
/// Downloads the font file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <param name="fontFilename">The font filename.</param>
/// <returns>Task.</returns>
2013-12-13 15:48:35 +00:00
private async Task DownloadFontFile ( string fontsDirectory , string fontFilename , IProgress < double > progress )
2013-09-23 15:37:50 +00:00
{
var existingFile = Directory
. EnumerateFiles ( _appPaths . ProgramDataPath , fontFilename , SearchOption . AllDirectories )
. FirstOrDefault ( ) ;
if ( existingFile ! = null )
{
try
{
2015-09-13 21:32:02 +00:00
_fileSystem . CopyFile ( existingFile , Path . Combine ( fontsDirectory , fontFilename ) , true ) ;
2013-09-23 15:37:50 +00:00
return ;
}
catch ( IOException ex )
{
// Log this, but don't let it fail the operation
_logger . ErrorException ( "Error copying file" , ex ) ;
}
}
2013-09-23 18:14:07 +00:00
string tempFile = null ;
foreach ( var url in _fontUrls )
2013-09-23 15:37:50 +00:00
{
2013-12-13 15:48:35 +00:00
progress . Report ( 0 ) ;
2013-09-23 18:14:07 +00:00
try
{
tempFile = await _httpClient . GetTempFile ( new HttpRequestOptions
{
Url = url ,
2013-12-13 15:48:35 +00:00
Progress = progress
2013-09-23 18:14:07 +00:00
} ) . ConfigureAwait ( false ) ;
break ;
}
catch ( Exception ex )
{
// The core can function without the font file, so handle this
_logger . ErrorException ( "Failed to download ffmpeg font file from {0}" , ex , url ) ;
}
}
if ( string . IsNullOrEmpty ( tempFile ) )
{
return ;
}
2013-09-23 15:37:50 +00:00
2013-10-13 19:56:54 +00:00
Extract7zArchive ( tempFile , fontsDirectory ) ;
2015-02-11 20:23:07 +00:00
2013-09-23 15:37:50 +00:00
try
{
2015-01-13 03:46:44 +00:00
_fileSystem . DeleteFile ( tempFile ) ;
2013-09-23 15:37:50 +00:00
}
catch ( IOException ex )
{
// Log this, but don't let it fail the operation
_logger . ErrorException ( "Error deleting temp file {0}" , ex , tempFile ) ;
}
}
/// <summary>
/// Writes the font config file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <returns>Task.</returns>
private async Task WriteFontConfigFile ( string fontsDirectory )
{
const string fontConfigFilename = "fonts.conf" ;
var fontConfigFile = Path . Combine ( fontsDirectory , fontConfigFilename ) ;
2015-09-13 21:32:02 +00:00
if ( ! _fileSystem . FileExists ( fontConfigFile ) )
2013-09-23 15:37:50 +00:00
{
var contents = string . Format ( "<?xml version=\"1.0\"?><fontconfig><dir>{0}</dir><alias><family>Arial</family><prefer>Arial Unicode MS</prefer></alias></fontconfig>" , fontsDirectory ) ;
var bytes = Encoding . UTF8 . GetBytes ( contents ) ;
2013-10-31 14:03:23 +00:00
using ( var fileStream = _fileSystem . GetFileStream ( fontConfigFile , FileMode . Create , FileAccess . Write ,
FileShare . Read , true ) )
2013-09-23 15:37:50 +00:00
{
await fileStream . WriteAsync ( bytes , 0 , bytes . Length ) ;
}
}
}
}
}