@ -8,7 +8,6 @@ using System.Text;
using System.Threading ;
using System.Threading.Tasks ;
using MediaBrowser.Common.Extensions ;
using MediaBrowser.Controller ;
using MediaBrowser.Controller.Configuration ;
using MediaBrowser.Controller.Devices ;
using MediaBrowser.Controller.Dlna ;
@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding ;
using MediaBrowser.Controller.Net ;
using MediaBrowser.Model.Configuration ;
using MediaBrowser.Model.Diagnostics ;
using MediaBrowser.Model.Dlna ;
using MediaBrowser.Model.Dto ;
using MediaBrowser.Model.Entities ;
@ -32,6 +30,8 @@ namespace MediaBrowser.Api.Playback
/// </summary>
public abstract class BaseStreamingService : BaseApiService
{
protected static readonly CultureInfo UsCulture = CultureInfo . ReadOnly ( new CultureInfo ( "en-US" ) ) ;
/// <summary>
/// Gets or sets the application paths.
/// </summary>
@ -65,15 +65,25 @@ namespace MediaBrowser.Api.Playback
protected IFileSystem FileSystem { get ; private set ; }
protected IDlnaManager DlnaManager { get ; private set ; }
protected IDeviceManager DeviceManager { get ; private set ; }
protected ISubtitleEncoder SubtitleEncoder { get ; private set ; }
protected IMediaSourceManager MediaSourceManager { get ; private set ; }
protected IJsonSerializer JsonSerializer { get ; private set ; }
protected IAuthorizationContext AuthorizationContext { get ; private set ; }
protected EncodingHelper EncodingHelper { get ; set ; }
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected abstract TranscodingJobType TranscodingJobType { get ; }
/// <summary>
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
@ -112,12 +122,6 @@ namespace MediaBrowser.Api.Playback
/// </summary>
protected abstract string GetCommandLineArguments ( string outputPath , EncodingOptions encodingOptions , StreamState state , bool isEncoding ) ;
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected abstract TranscodingJobType TranscodingJobType { get ; }
/// <summary>
/// Gets the output file extension.
/// </summary>
@ -133,31 +137,18 @@ namespace MediaBrowser.Api.Playback
/// </summary>
private string GetOutputFilePath ( StreamState state , EncodingOptions encodingOptions , string outputFileExtension )
{
var folder = ServerConfigurationManager . ApplicationPaths . TranscodingTempPath ;
var data = GetCommandLineArguments ( "dummy\\dummy" , encodingOptions , state , false ) ;
data + = "-" + ( state . Request . DeviceId ? ? string . Empty ) ;
data + = "-" + ( state . Request . PlaySessionId ? ? string . Empty ) ;
var dataHash = data . GetMD5 ( ) . ToString ( "N" ) ;
data + = "-" + ( state . Request . DeviceId ? ? string . Empty )
+ "-" + ( state . Request . PlaySessionId ? ? string . Empty ) ;
if ( EnableOutputInSubFolder )
{
return Path . Combine ( folder , dataHash , dataHash + ( outputFileExtension ? ? string . Empty ) . ToLowerInvariant ( ) ) ;
}
var filename = data . GetMD5 ( ) . ToString ( "N" ) + outputFileExtension . ToLowerInvariant ( ) ;
var folder = ServerConfigurationManager . ApplicationPaths . TranscodingTempPath ;
return Path . Combine ( folder , dataHash + ( outputFileExtension ? ? string . Empty ) . ToLowerInvariant ( ) ) ;
return Path . Combine ( folder , filename ) ;
}
protected virtual bool EnableOutputInSubFolder = > false ;
protected readonly CultureInfo UsCulture = new CultureInfo ( "en-US" ) ;
protected virtual string GetDefaultH264Preset ( )
{
return "superfast" ;
}
protected virtual string GetDefaultH264Preset ( ) = > "superfast" ;
private async Task AcquireResources ( StreamState state , CancellationTokenSource cancellationTokenSource )
{
@ -171,7 +162,6 @@ namespace MediaBrowser.Api.Playback
var liveStreamResponse = await MediaSourceManager . OpenLiveStream ( new LiveStreamRequest
{
OpenToken = state . MediaSource . OpenToken
} , cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
EncodingHelper . AttachMediaSourceInfo ( state , liveStreamResponse . MediaSource , state . RequestedUrl ) ;
@ -209,22 +199,16 @@ namespace MediaBrowser.Api.Playback
if ( state . VideoRequest ! = null & & ! string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
var auth = AuthorizationContext . GetAuthorizationInfo ( Request ) ;
if ( auth . User ! = null )
if ( auth . User ! = null & & ! auth . User . Policy . EnableVideoPlaybackTranscoding )
{
if ( ! auth . User . Policy . EnableVideoPlaybackTranscoding )
{
ApiEntryPoint . Instance . OnTranscodeFailedToStart ( outputPath , TranscodingJobType , state ) ;
ApiEntryPoint . Instance . OnTranscodeFailedToStart ( outputPath , TranscodingJobType , state ) ;
throw new ArgumentException ( "User does not have access to video transcoding" ) ;
}
throw new ArgumentException ( "User does not have access to video transcoding" ) ;
}
}
var encodingOptions = ApiEntryPoint . Instance . GetEncodingOptions ( ) ;
var transcodingId = Guid . NewGuid ( ) . ToString ( "N" ) ;
var commandLineArgs = GetCommandLineArguments ( outputPath , encodingOptions , state , true ) ;
var process = new Process ( )
{
StartInfo = new ProcessStartInfo ( )
@ -239,7 +223,7 @@ namespace MediaBrowser.Api.Playback
RedirectStandardInput = true ,
FileName = MediaEncoder . EncoderPath ,
Arguments = commandLineArgs ,
Arguments = GetCommandLineArguments( outputPath , encodingOptions , state , true ) ,
WorkingDirectory = string . IsNullOrWhiteSpace ( workingDirectory ) ? null : workingDirectory ,
ErrorDialog = false
@ -250,7 +234,7 @@ namespace MediaBrowser.Api.Playback
var transcodingJob = ApiEntryPoint . Instance . OnTranscodeBeginning ( outputPath ,
state . Request . PlaySessionId ,
state . MediaSource . LiveStreamId ,
transcodingId ,
Guid. NewGuid ( ) . ToString ( "N" ) ,
TranscodingJobType ,
process ,
state . Request . DeviceId ,
@ -261,27 +245,26 @@ namespace MediaBrowser.Api.Playback
Logger . LogInformation ( commandLineLogMessage ) ;
var logFilePrefix = "ffmpeg-transcode" ;
if ( state . VideoRequest ! = null )
if ( state . VideoRequest ! = null
& & string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
if ( string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase )
& & string . Equals ( state . OutputAudioCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
if ( string . Equals ( state . OutputAudioCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
logFilePrefix = "ffmpeg-directstream" ;
}
else if ( string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
else
{
logFilePrefix = "ffmpeg-remux" ;
}
}
var logFilePath = Path . Combine ( ServerConfigurationManager . ApplicationPaths . LogDirectoryPath , logFilePrefix + "-" + Guid . NewGuid ( ) + ".txt" ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( logFilePath ) ) ;
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
state. LogFile Stream = FileSystem . GetFileStream ( logFilePath , FileOpenMode . Create , FileAccessMode . Write , FileShareMode . Read , true ) ;
Stream log Stream = FileSystem . GetFileStream ( logFilePath , FileOpenMode . Create , FileAccessMode . Write , FileShareMode . Read , true ) ;
var commandLineLogMessageBytes = Encoding . UTF8 . GetBytes ( Request . AbsoluteUri + Environment . NewLine + Environment . NewLine + JsonSerializer . SerializeToString ( state . MediaSource ) + Environment . NewLine + Environment . NewLine + commandLineLogMessage + Environment . NewLine + Environment . NewLine ) ;
await state. LogFile Stream. WriteAsync ( commandLineLogMessageBytes , 0 , commandLineLogMessageBytes . Length , cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
await log Stream. WriteAsync ( commandLineLogMessageBytes , 0 , commandLineLogMessageBytes . Length , cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
process . Exited + = ( sender , args ) = > OnFfMpegProcessExited ( process , transcodingJob , state ) ;
@ -298,13 +281,10 @@ namespace MediaBrowser.Api.Playback
throw ;
}
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
//process.BeginOutputReadLine();
state . TranscodingJob = transcodingJob ;
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
new JobLogger ( Logger ) . StartStreamingLog ( state , process . StandardError . BaseStream , state. LogFile Stream) ;
_ = new JobLogger ( Logger ) . StartStreamingLog ( state , process . StandardError . BaseStream , log Stream) ;
// Wait for the file to exist before proceeeding
while ( ! File . Exists ( state . WaitForPath ? ? outputPath ) & & ! transcodingJob . HasExited )
@ -368,25 +348,16 @@ namespace MediaBrowser.Api.Playback
Logger . LogDebug ( "Disposing stream resources" ) ;
state . Dispose ( ) ;
try
if ( process . ExitCode = = 0 )
{
Logger . LogInformation ( "FFMpeg exited with code { 0} ", process . ExitCode ) ;
Logger . LogInformation ( "FFMpeg exited with code 0") ;
}
catch
else
{
Logger . LogError ( "FFMpeg exited with an error." ) ;
Logger . LogError ( "FFMpeg exited with code {0}", process . ExitCode ) ;
}
// This causes on exited to be called twice:
//try
//{
// // Dispose the process
// process.Dispose();
//}
//catch (Exception ex)
//{
// Logger.LogError(ex, "Error disposing ffmpeg.");
//}
process . Dispose ( ) ;
}
/// <summary>
@ -643,11 +614,19 @@ namespace MediaBrowser.Api.Playback
return null ;
}
if ( value . IndexOf ( "npt=" , StringComparison . OrdinalIgnoreCase ) ! = 0 )
if ( ! value . StartsWith ( "npt=" , StringComparison . OrdinalIgnoreCase ) )
{
throw new ArgumentException ( "Invalid timeseek header" ) ;
}
value = value . Substring ( 4 ) . Split ( new [ ] { '-' } , 2 ) [ 0 ] ;
int index = value . IndexOf ( '-' ) ;
if ( index = = - 1 )
{
value = value . Substring ( 4 ) ;
}
else
{
value = value . Substring ( 4 , index ) ;
}
if ( value . IndexOf ( ':' ) = = - 1 )
{
@ -728,13 +707,10 @@ namespace MediaBrowser.Api.Playback
// state.SegmentLength = 6;
//}
if ( state . VideoRequest ! = null )
if ( state . VideoRequest ! = null & & ! string . IsNullOrWhiteSpace ( state . VideoRequest . VideoCodec ) )
{
if ( ! string . IsNullOrWhiteSpace ( state . VideoRequest . VideoCodec ) )
{
state . SupportedVideoCodecs = state . VideoRequest . VideoCodec . Split ( ',' ) . Where ( i = > ! string . IsNullOrWhiteSpace ( i ) ) . ToArray ( ) ;
state . VideoRequest . VideoCodec = state . SupportedVideoCodecs . FirstOrDefault ( ) ;
}
state . SupportedVideoCodecs = state . VideoRequest . VideoCodec . Split ( ',' ) . Where ( i = > ! string . IsNullOrWhiteSpace ( i ) ) . ToArray ( ) ;
state . VideoRequest . VideoCodec = state . SupportedVideoCodecs . FirstOrDefault ( ) ;
}
if ( ! string . IsNullOrWhiteSpace ( request . AudioCodec ) )
@ -779,7 +755,7 @@ namespace MediaBrowser.Api.Playback
var mediaSources = ( await MediaSourceManager . GetPlayackMediaSources ( LibraryManager . GetItemById ( request . Id ) , null , false , false , cancellationToken ) . ConfigureAwait ( false ) ) . ToList ( ) ;
mediaSource = string . IsNullOrEmpty ( request . MediaSourceId )
? mediaSources .First ( )
? mediaSources [0 ]
: mediaSources . FirstOrDefault ( i = > string . Equals ( i . Id , request . MediaSourceId ) ) ;
if ( mediaSource = = null & & request . MediaSourceId . Equals ( request . Id ) )
@ -834,11 +810,11 @@ namespace MediaBrowser.Api.Playback
if ( state . OutputVideoBitrate . HasValue & & ! string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
var resolution = ResolutionNormalizer . Normalize (
state . VideoStream = = null ? ( int? ) null : state . VideoStream . BitRate ,
state . VideoStream = = null ? ( int? ) null : state . VideoStream . Width ,
state . VideoStream = = null ? ( int? ) null : state . VideoStream . Height ,
state . VideoStream ? . BitRate ,
state . VideoStream ? . Width ,
state . VideoStream ? . Height ,
state . OutputVideoBitrate . Value ,
state . VideoStream = = null ? null : state . VideoStream . Codec ,
state . VideoStream ? . Codec ,
state . OutputVideoCodec ,
videoRequest . MaxWidth ,
videoRequest . MaxHeight ) ;
@ -846,17 +822,13 @@ namespace MediaBrowser.Api.Playback
videoRequest . MaxWidth = resolution . MaxWidth ;
videoRequest . MaxHeight = resolution . MaxHeight ;
}
ApplyDeviceProfileSettings ( state ) ;
}
else
{
ApplyDeviceProfileSettings ( state ) ;
}
ApplyDeviceProfileSettings ( state ) ;
var ext = string . IsNullOrWhiteSpace ( state . OutputContainer )
? GetOutputFileExtension ( state )
: ( "." + state . OutputContainer ) ;
: ( '.' + state . OutputContainer ) ;
var encodingOptions = ApiEntryPoint . Instance . GetEncodingOptions ( ) ;
@ -970,18 +942,18 @@ namespace MediaBrowser.Api.Playback
responseHeaders [ "transferMode.dlna.org" ] = string . IsNullOrEmpty ( transferMode ) ? "Streaming" : transferMode ;
responseHeaders [ "realTimeInfo.dlna.org" ] = "DLNA.ORG_TLAG=*" ;
if ( string . Equals ( GetHeader ( "getMediaInfo.sec" ) , "1" , StringComparison . OrdinalIgnoreCase ) )
if ( state . RunTimeTicks . HasValue )
{
if ( state . RunTimeTicks . HasValue )
if ( string . Equals ( GetHeader ( "getMediaInfo.sec" ) , "1" , StringComparison . OrdinalIgnoreCase ) )
{
var ms = TimeSpan . FromTicks ( state . RunTimeTicks . Value ) . TotalMilliseconds ;
responseHeaders [ "MediaInfo.sec" ] = string . Format ( "SEC_Duration={0};" , Convert . ToInt32 ( ms ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
}
if ( state . RunTimeTicks . HasValue & & ! isStaticallyStreamed & & profile ! = null )
{
AddTimeSeekResponseHeaders ( state , responseHeaders ) ;
if ( ! isStaticallyStreamed & & profile ! = null )
{
AddTimeSeekResponseHeaders ( state , responseHeaders ) ;
}
}
if ( profile = = null )