@ -22,6 +22,8 @@ using System.Text;
using System.Threading ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Threading.Tasks ;
using CommonIO ;
using CommonIO ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Controller ;
namespace MediaBrowser.Api.Playback
namespace MediaBrowser.Api.Playback
{
{
@ -69,6 +71,9 @@ namespace MediaBrowser.Api.Playback
protected IZipClient ZipClient { get ; private set ; }
protected IZipClient ZipClient { get ; private set ; }
protected IJsonSerializer JsonSerializer { get ; private set ; }
protected IJsonSerializer JsonSerializer { get ; private set ; }
public static IServerApplicationHost AppHost ;
public static IHttpClient HttpClient ;
/// <summary>
/// <summary>
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
/// </summary>
@ -286,28 +291,46 @@ namespace MediaBrowser.Api.Playback
protected string GetH264Encoder ( StreamState state )
protected string GetH264Encoder ( StreamState state )
{
{
var defaultEncoder = "libx264" ;
// Only use alternative encoders for video files.
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
if ( state . VideoType = = VideoType . VideoFile )
if ( state . VideoType = = VideoType . VideoFile )
{
{
if ( string . Equals ( ApiEntryPoint . Instance . GetEncodingOptions ( ) . HardwareAccelerationType , "qsv" , StringComparison . OrdinalIgnoreCase ) | |
var encodingOptions = ApiEntryPoint . Instance . GetEncodingOptions ( ) ;
string . Equals ( ApiEntryPoint . Instance . GetEncodingOptions ( ) . HardwareAccelerationType , "h264_qsv" , StringComparison . OrdinalIgnoreCase ) )
var hwType = encodingOptions . HardwareAccelerationType ;
if ( string . Equals ( hwType , "qsv" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( hwType , "h264_qsv" , StringComparison . OrdinalIgnoreCase ) )
{
{
return "h264_qsv" ;
return GetAvailableEncoder ( "h264_qsv" , defaultEncoder ) ;
}
}
if ( string . Equals ( ApiEntryPoint . Instance . GetEncodingOptions ( ) . HardwareAccelerationType , "nvenc" , StringComparison . OrdinalIgnoreCase ) )
if ( string . Equals ( hwType , "nvenc" , StringComparison . OrdinalIgnoreCase ) )
{
return GetAvailableEncoder ( "h264_nvenc" , defaultEncoder ) ;
}
if ( string . Equals ( hwType , "h264_omx" , StringComparison . OrdinalIgnoreCase ) )
{
{
return "h264_nvenc" ;
return GetAvailableEncoder ( "h264_omx" , defaultEncoder ) ;
}
}
if ( string . Equals ( ApiEntryPoint . Instance . GetEncodingOptions ( ) . HardwareAccelerationType , "h264_omx" , StringComparison . OrdinalIgnoreCase ) )
if ( string . Equals ( hwType, "vaapi ", StringComparison . OrdinalIgnoreCas e) & & ! string . IsNullOrWhiteSpace ( encodingOptions . VaapiDevic e) )
{
{
return "h264_omx" ;
return GetAvailableEncoder ( "h264_vaapi" , defaultEncoder ) ;
}
}
}
}
return "libx264" ;
return defaultEncoder ;
}
private string GetAvailableEncoder ( string preferredEncoder , string defaultEncoder )
{
if ( MediaEncoder . SupportsEncoder ( preferredEncoder ) )
{
return preferredEncoder ;
}
return defaultEncoder ;
}
}
/// <summary>
/// <summary>
@ -409,7 +432,8 @@ namespace MediaBrowser.Api.Playback
if ( ! string . IsNullOrEmpty ( state . VideoRequest . Profile ) )
if ( ! string . IsNullOrEmpty ( state . VideoRequest . Profile ) )
{
{
if ( ! string . Equals ( videoCodec , "h264_omx" , StringComparison . OrdinalIgnoreCase ) )
if ( ! string . Equals ( videoCodec , "h264_omx" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoCodec , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
{
// not supported by h264_omx
// not supported by h264_omx
param + = " -profile:v " + state . VideoRequest . Profile ;
param + = " -profile:v " + state . VideoRequest . Profile ;
@ -464,7 +488,8 @@ namespace MediaBrowser.Api.Playback
if ( ! string . Equals ( videoCodec , "h264_omx" , StringComparison . OrdinalIgnoreCase ) & &
if ( ! string . Equals ( videoCodec , "h264_omx" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoCodec , "h264_qsv" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoCodec , "h264_qsv" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoCodec , "h264_nvenc" , StringComparison . OrdinalIgnoreCase ) )
! string . Equals ( videoCodec , "h264_nvenc" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoCodec , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
{
param = "-pix_fmt yuv420p " + param ;
param = "-pix_fmt yuv420p " + param ;
}
}
@ -530,11 +555,48 @@ namespace MediaBrowser.Api.Playback
var filters = new List < string > ( ) ;
var filters = new List < string > ( ) ;
if ( state . DeInterlace )
if ( string . Equals ( outputVideoCodec , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
filters . Add ( "format=nv12|vaapi" ) ;
filters . Add ( "hwupload" ) ;
}
else if ( state . DeInterlace & & ! string . Equals ( outputVideoCodec , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
{
filters . Add ( "yadif=0:-1:0" ) ;
filters . Add ( "yadif=0:-1:0" ) ;
}
}
if ( string . Equals ( outputVideoCodec , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
// Work around vaapi's reduced scaling features
var scaler = "scale_vaapi" ;
// Given the input dimensions (inputWidth, inputHeight), determine the output dimensions
// (outputWidth, outputHeight). The user may request precise output dimensions or maximum
// output dimensions. Output dimensions are guaranteed to be even.
decimal inputWidth = Convert . ToDecimal ( state . VideoStream . Width ) ;
decimal inputHeight = Convert . ToDecimal ( state . VideoStream . Height ) ;
decimal outputWidth = request . Width . HasValue ? Convert . ToDecimal ( request . Width . Value ) : inputWidth ;
decimal outputHeight = request . Height . HasValue ? Convert . ToDecimal ( request . Height . Value ) : inputHeight ;
decimal maximumWidth = request . MaxWidth . HasValue ? Convert . ToDecimal ( request . MaxWidth . Value ) : outputWidth ;
decimal maximumHeight = request . MaxHeight . HasValue ? Convert . ToDecimal ( request . MaxHeight . Value ) : outputHeight ;
if ( outputWidth > maximumWidth | | outputHeight > maximumHeight )
{
var scale = Math . Min ( maximumWidth / outputWidth , maximumHeight / outputHeight ) ;
outputWidth = Math . Min ( maximumWidth , Math . Truncate ( outputWidth * scale ) ) ;
outputHeight = Math . Min ( maximumHeight , Math . Truncate ( outputHeight * scale ) ) ;
}
outputWidth = 2 * Math . Truncate ( outputWidth / 2 ) ;
outputHeight = 2 * Math . Truncate ( outputHeight / 2 ) ;
if ( outputWidth ! = inputWidth | | outputHeight ! = inputHeight )
{
filters . Add ( string . Format ( "{0}=w={1}:h={2}" , scaler , outputWidth . ToString ( UsCulture ) , outputHeight . ToString ( UsCulture ) ) ) ;
}
}
else
{
// If fixed dimensions were supplied
// If fixed dimensions were supplied
if ( request . Width . HasValue & & request . Height . HasValue )
if ( request . Width . HasValue & & request . Height . HasValue )
{
{
@ -582,7 +644,8 @@ namespace MediaBrowser.Api.Playback
{
{
var maxHeightParam = request . MaxHeight . Value . ToString ( UsCulture ) ;
var maxHeightParam = request . MaxHeight . Value . ToString ( UsCulture ) ;
filters . Add ( string . Format ( "scale=trunc(oh*a/2)*2:min(ih\\,{0})" , maxHeightParam ) ) ;
filters . Add ( string . Format ( "scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})" , maxHeightParam ) ) ;
}
}
}
var output = string . Empty ;
var output = string . Empty ;
@ -917,6 +980,15 @@ namespace MediaBrowser.Api.Playback
}
}
}
}
if ( state . VideoRequest ! = null )
{
var encodingOptions = ApiEntryPoint . Instance . GetEncodingOptions ( ) ;
if ( GetVideoEncoder ( state ) . IndexOf ( "vaapi" , StringComparison . OrdinalIgnoreCase ) ! = - 1 )
{
arg = "-hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device " + encodingOptions . VaapiDevice + " " + arg ;
}
}
return arg . Trim ( ) ;
return arg . Trim ( ) ;
}
}
@ -1042,14 +1114,14 @@ namespace MediaBrowser.Api.Playback
var commandLineLogMessage = process . StartInfo . FileName + " " + process . StartInfo . Arguments ;
var commandLineLogMessage = process . StartInfo . FileName + " " + process . StartInfo . Arguments ;
Logger . Info ( commandLineLogMessage ) ;
Logger . Info ( commandLineLogMessage ) ;
var logFilePrefix = " transcode";
var logFilePrefix = " ffmpeg- transcode";
if ( state . VideoRequest ! = null & & string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) & & string . Equals ( state . OutputAudioCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
if ( state . VideoRequest ! = null & & string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) & & string . Equals ( state . OutputAudioCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
{
logFilePrefix = " directstream";
logFilePrefix = " ffmpeg- directstream";
}
}
else if ( state . VideoRequest ! = null & & string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
else if ( state . VideoRequest ! = null & & string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
{
logFilePrefix = " remux";
logFilePrefix = " ffmpeg- remux";
}
}
var logFilePath = Path . Combine ( ServerConfigurationManager . ApplicationPaths . LogDirectoryPath , logFilePrefix + "-" + Guid . NewGuid ( ) + ".txt" ) ;
var logFilePath = Path . Combine ( ServerConfigurationManager . ApplicationPaths . LogDirectoryPath , logFilePrefix + "-" + Guid . NewGuid ( ) + ".txt" ) ;
@ -1080,7 +1152,7 @@ namespace MediaBrowser.Api.Playback
//process.BeginOutputReadLine();
//process.BeginOutputReadLine();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
Task . Run ( ( ) = > StartStreamingLog ( transcodingJob , state , process . StandardError . BaseStream , state . LogFileStream ) ) ;
var task = Task . Run ( ( ) = > StartStreamingLog ( transcodingJob , state , process . StandardError . BaseStream , state . LogFileStream ) ) ;
// Wait for the file to exist before proceeeding
// Wait for the file to exist before proceeeding
while ( ! FileSystem . FileExists ( state . WaitForPath ? ? outputPath ) & & ! transcodingJob . HasExited )
while ( ! FileSystem . FileExists ( state . WaitForPath ? ? outputPath ) & & ! transcodingJob . HasExited )
@ -1099,28 +1171,30 @@ namespace MediaBrowser.Api.Playback
}
}
StartThrottler ( state , transcodingJob ) ;
StartThrottler ( state , transcodingJob ) ;
ReportUsage ( state ) ;
return transcodingJob ;
return transcodingJob ;
}
}
private void StartThrottler ( StreamState state , TranscodingJob transcodingJob )
private void StartThrottler ( StreamState state , TranscodingJob transcodingJob )
{
{
if ( EnableThrottling ( state ) & & state . InputProtocol = = MediaProtocol . File & &
if ( EnableThrottling ( state ) & & ! string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
state . RunTimeTicks . HasValue & &
state . VideoType = = VideoType . VideoFile & &
! string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
if ( state . RunTimeTicks . Value > = TimeSpan . FromMinutes ( 5 ) . Ticks & & state . IsInputVideo )
{
{
transcodingJob . TranscodingThrottler = state . TranscodingThrottler = new TranscodingThrottler ( transcodingJob , Logger , ServerConfigurationManager ) ;
transcodingJob . TranscodingThrottler = state . TranscodingThrottler = new TranscodingThrottler ( transcodingJob , Logger , ServerConfigurationManager ) ;
state . TranscodingThrottler . Start ( ) ;
state . TranscodingThrottler . Start ( ) ;
}
}
}
}
}
protected virtual bool EnableThrottling ( StreamState state )
protected virtual bool EnableThrottling ( StreamState state )
{
{
return true ;
// do not use throttling with hardware encoders
return state . InputProtocol = = MediaProtocol . File & &
state . RunTimeTicks . HasValue & &
state . RunTimeTicks . Value > = TimeSpan . FromMinutes ( 5 ) . Ticks & &
state . IsInputVideo & &
state . VideoType = = VideoType . VideoFile & &
! string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) & &
string . Equals ( GetVideoEncoder ( state ) , "libx264" , StringComparison . OrdinalIgnoreCase ) ;
}
}
private async Task StartStreamingLog ( TranscodingJob transcodingJob , StreamState state , Stream source , Stream target )
private async Task StartStreamingLog ( TranscodingJob transcodingJob , StreamState state , Stream source , Stream target )
@ -1158,6 +1232,7 @@ namespace MediaBrowser.Api.Playback
double? percent = null ;
double? percent = null ;
TimeSpan ? transcodingPosition = null ;
TimeSpan ? transcodingPosition = null ;
long? bytesTranscoded = null ;
long? bytesTranscoded = null ;
int? bitRate = null ;
var parts = line . Split ( ' ' ) ;
var parts = line . Split ( ' ' ) ;
@ -1221,11 +1296,32 @@ namespace MediaBrowser.Api.Playback
}
}
}
}
}
}
else if ( part . StartsWith ( "bitrate=" , StringComparison . OrdinalIgnoreCase ) )
{
var rate = part . Split ( new [ ] { '=' } , 2 ) . Last ( ) ;
int? scale = null ;
if ( rate . IndexOf ( "kbits/s" , StringComparison . OrdinalIgnoreCase ) ! = - 1 )
{
scale = 1024 ;
rate = rate . Replace ( "kbits/s" , string . Empty , StringComparison . OrdinalIgnoreCase ) ;
}
if ( scale . HasValue )
{
float val ;
if ( float . TryParse ( rate , NumberStyles . Any , UsCulture , out val ) )
{
bitRate = ( int ) Math . Ceiling ( val * scale . Value ) ;
}
}
}
}
}
if ( framerate . HasValue | | percent . HasValue )
if ( framerate . HasValue | | percent . HasValue )
{
{
ApiEntryPoint . Instance . ReportTranscodingProgress ( transcodingJob , state , transcodingPosition , framerate , percent , bytesTranscoded ) ;
ApiEntryPoint . Instance . ReportTranscodingProgress ( transcodingJob , state , transcodingPosition , framerate , percent , bytesTranscoded , bitRate );
}
}
}
}
@ -1547,13 +1643,6 @@ namespace MediaBrowser.Api.Playback
}
}
}
}
else if ( i = = 25 )
else if ( i = = 25 )
{
if ( videoRequest ! = null )
{
videoRequest . ForceLiveStream = string . Equals ( "true" , val , StringComparison . OrdinalIgnoreCase ) ;
}
}
else if ( i = = 26 )
{
{
if ( ! string . IsNullOrWhiteSpace ( val ) & & videoRequest ! = null )
if ( ! string . IsNullOrWhiteSpace ( val ) & & videoRequest ! = null )
{
{
@ -1564,17 +1653,21 @@ namespace MediaBrowser.Api.Playback
}
}
}
}
}
}
else if ( i = = 2 7 )
else if ( i = = 2 6 )
{
{
request . TranscodingMaxAudioChannels = int . Parse ( val , UsCulture ) ;
request . TranscodingMaxAudioChannels = int . Parse ( val , UsCulture ) ;
}
}
else if ( i = = 2 8 )
else if ( i = = 2 7 )
{
{
if ( videoRequest ! = null )
if ( videoRequest ! = null )
{
{
videoRequest . EnableSubtitlesInManifest = string . Equals ( "true" , val , StringComparison . OrdinalIgnoreCase ) ;
videoRequest . EnableSubtitlesInManifest = string . Equals ( "true" , val , StringComparison . OrdinalIgnoreCase ) ;
}
}
}
}
else if ( i = = 28 )
{
request . Tag = val ;
}
}
}
}
}
@ -1788,6 +1881,19 @@ namespace MediaBrowser.Api.Playback
{
{
state . OutputAudioCodec = "copy" ;
state . OutputAudioCodec = "copy" ;
}
}
else
{
// If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
var auth = AuthorizationContext . GetAuthorizationInfo ( Request ) ;
if ( ! string . IsNullOrWhiteSpace ( auth . UserId ) )
{
var user = UserManager . GetUserById ( auth . UserId ) ;
if ( ! user . Policy . EnableAudioPlaybackTranscoding )
{
state . OutputAudioCodec = "copy" ;
}
}
}
}
}
private void AttachMediaSourceInfo ( StreamState state ,
private void AttachMediaSourceInfo ( StreamState state ,
@ -2159,13 +2265,127 @@ namespace MediaBrowser.Api.Playback
if ( state . VideoRequest ! = null )
if ( state . VideoRequest ! = null )
{
{
state . VideoRequest . CopyTimestamps = transcodingProfile . CopyTimestamps ;
state . VideoRequest . CopyTimestamps = transcodingProfile . CopyTimestamps ;
state . VideoRequest . ForceLiveStream = transcodingProfile . ForceLiveStream ;
state . VideoRequest . EnableSubtitlesInManifest = transcodingProfile . EnableSubtitlesInManifest ;
state . VideoRequest . EnableSubtitlesInManifest = transcodingProfile . EnableSubtitlesInManifest ;
}
}
}
}
}
}
}
}
private async void ReportUsage ( StreamState state )
{
try
{
await ReportUsageInternal ( state ) . ConfigureAwait ( false ) ;
}
catch
{
}
}
private Task ReportUsageInternal ( StreamState state )
{
if ( ! ServerConfigurationManager . Configuration . EnableAnonymousUsageReporting )
{
return Task . FromResult ( true ) ;
}
if ( ! MediaEncoder . IsDefaultEncoderPath )
{
return Task . FromResult ( true ) ;
}
var dict = new Dictionary < string , string > ( ) ;
var outputAudio = GetAudioEncoder ( state ) ;
if ( ! string . IsNullOrWhiteSpace ( outputAudio ) )
{
dict [ "outputAudio" ] = outputAudio ;
}
var outputVideo = GetVideoEncoder ( state ) ;
if ( ! string . IsNullOrWhiteSpace ( outputVideo ) )
{
dict [ "outputVideo" ] = outputVideo ;
}
if ( ServerConfigurationManager . Configuration . CodecsUsed . Contains ( outputAudio ? ? string . Empty , StringComparer . OrdinalIgnoreCase ) & &
ServerConfigurationManager . Configuration . CodecsUsed . Contains ( outputVideo ? ? string . Empty , StringComparer . OrdinalIgnoreCase ) )
{
return Task . FromResult ( true ) ;
}
dict [ "id" ] = AppHost . SystemId ;
dict [ "type" ] = state . VideoRequest = = null ? "Audio" : "Video" ;
var audioStream = state . AudioStream ;
if ( audioStream ! = null & & ! string . IsNullOrWhiteSpace ( audioStream . Codec ) )
{
dict [ "inputAudio" ] = audioStream . Codec ;
}
var videoStream = state . VideoStream ;
if ( videoStream ! = null & & ! string . IsNullOrWhiteSpace ( videoStream . Codec ) )
{
dict [ "inputVideo" ] = videoStream . Codec ;
}
var cert = GetType ( ) . Assembly . GetModules ( ) . First ( ) . GetSignerCertificate ( ) ;
if ( cert ! = null )
{
dict [ "assemblySig" ] = cert . GetCertHashString ( ) ;
dict [ "certSubject" ] = cert . Subject ? ? string . Empty ;
dict [ "certIssuer" ] = cert . Issuer ? ? string . Empty ;
}
else
{
return Task . FromResult ( true ) ;
}
if ( state . SupportedAudioCodecs . Count > 0 )
{
dict [ "supportedAudioCodecs" ] = string . Join ( "," , state . SupportedAudioCodecs . ToArray ( ) ) ;
}
var auth = AuthorizationContext . GetAuthorizationInfo ( Request ) ;
dict [ "appName" ] = auth . Client ? ? string . Empty ;
dict [ "appVersion" ] = auth . Version ? ? string . Empty ;
dict [ "device" ] = auth . Device ? ? string . Empty ;
dict [ "deviceId" ] = auth . DeviceId ? ? string . Empty ;
dict [ "context" ] = "streaming" ;
//Logger.Info(JsonSerializer.SerializeToString(dict));
if ( ! ServerConfigurationManager . Configuration . CodecsUsed . Contains ( outputAudio ? ? string . Empty , StringComparer . OrdinalIgnoreCase ) )
{
var list = ServerConfigurationManager . Configuration . CodecsUsed . ToList ( ) ;
list . Add ( outputAudio ) ;
ServerConfigurationManager . Configuration . CodecsUsed = list . ToArray ( ) ;
}
if ( ! ServerConfigurationManager . Configuration . CodecsUsed . Contains ( outputVideo ? ? string . Empty , StringComparer . OrdinalIgnoreCase ) )
{
var list = ServerConfigurationManager . Configuration . CodecsUsed . ToList ( ) ;
list . Add ( outputVideo ) ;
ServerConfigurationManager . Configuration . CodecsUsed = list . ToArray ( ) ;
}
ServerConfigurationManager . SaveConfiguration ( ) ;
//Logger.Info(JsonSerializer.SerializeToString(dict));
var options = new HttpRequestOptions ( )
{
Url = "https://mb3admin.com/admin/service/transcoding/report" ,
CancellationToken = CancellationToken . None ,
LogRequest = false ,
LogErrors = false
} ;
options . RequestContent = JsonSerializer . SerializeToString ( dict ) ;
options . RequestContentType = "application/json" ;
return HttpClient . Post ( options ) ;
}
/// <summary>
/// <summary>
/// Adds the dlna headers.
/// Adds the dlna headers.
/// </summary>
/// </summary>