diff options
31 files changed, 1373 insertions, 192 deletions
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 18559a68d..c03eddf99 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -103,6 +103,7 @@ <Compile Include="Playback\Hls\DynamicHlsService.cs" /> <Compile Include="Playback\Hls\HlsSegmentService.cs" /> <Compile Include="Playback\Hls\VideoHlsService.cs" /> + <Compile Include="Playback\ProgressiveStreamService.cs" /> <Compile Include="Playback\Progressive\AudioService.cs" /> <Compile Include="Playback\Progressive\BaseProgressiveStreamingService.cs" /> <Compile Include="Playback\BaseStreamingService.cs" /> diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index 9b9df3a92..c39811bb6 100644 --- a/MediaBrowser.Api/Music/InstantMixService.cs +++ b/MediaBrowser.Api/Music/InstantMixService.cs @@ -1,9 +1,9 @@ using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; using ServiceStack; -using System; using System.Collections.Generic; using System.Linq; @@ -36,103 +36,74 @@ namespace MediaBrowser.Api.Music public class InstantMixService : BaseApiService { private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; + private readonly IMusicManager _musicManager; - public InstantMixService(IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService) + public InstantMixService(IUserManager userManager, IDtoService dtoService, IMusicManager musicManager) { _userManager = userManager; - _libraryManager = libraryManager; _dtoService = dtoService; + _musicManager = musicManager; } public object Get(GetInstantMixFromSong request) { - var item = _dtoService.GetItemByDtoId(request.Id); + var item = (Audio)_dtoService.GetItemByDtoId(request.Id); - var result = GetInstantMixResult(request, item.Genres); + var user = _userManager.GetUserById(request.UserId.Value); - return ToOptimizedSerializedResultUsingCache(result); + var items = _musicManager.GetInstantMixFromSong(item, user); + + return GetResult(items, user, request); } public object Get(GetInstantMixFromAlbum request) { var album = (MusicAlbum)_dtoService.GetItemByDtoId(request.Id); - var genres = album - .RecursiveChildren - .OfType<Audio>() - .SelectMany(i => i.Genres) - .Concat(album.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, genres); + var items = _musicManager.GetInstantMixFromAlbum(album, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } public object Get(GetInstantMixFromMusicGenre request) { - var genre = GetMusicGenre(request.Name, _libraryManager); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, new[] { genre.Name }); + var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } public object Get(GetInstantMixFromArtist request) { - var artist = GetArtist(request.Name, _libraryManager); - - var genres = _libraryManager.RootFolder - .RecursiveChildren - .OfType<Audio>() - .Where(i => i.HasArtist(artist.Name)) - .SelectMany(i => i.Genres) - .Concat(artist.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, genres); + var items = _musicManager.GetInstantMixFromArtist(request.Name, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } - private ItemsResult GetInstantMixResult(BaseGetSimilarItems request, IEnumerable<string> genres) + private object GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request) { - var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; - var fields = request.GetItemFields().ToList(); - var inputItems = user == null - ? _libraryManager.RootFolder.RecursiveChildren - : user.RootFolder.GetRecursiveChildren(user); - - var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var limit = request.Limit.HasValue ? request.Limit.Value * 2 : 100; - - var items = inputItems - .OfType<Audio>() - .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey))) - .OrderByDescending(i => i.Item2) - .ThenBy(i => Guid.NewGuid()) - .Select(i => i.Item1) - .Take(limit) - .OrderBy(i => Guid.NewGuid()) - .ToList(); + var list = items.ToList(); var result = new ItemsResult { - TotalRecordCount = items.Count + TotalRecordCount = list.Count }; - var dtos = items.Take(request.Limit ?? items.Count) + var dtos = list.Take(request.Limit ?? list.Count) .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); result.Items = dtos.ToArray(); - return result; + return ToOptimizedResult(result); } } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 519ff7947..9b126420a 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -835,11 +835,6 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected string GetInputArgument(StreamState state) { - if (state.SendInputOverStandardInput) - { - return "-"; - } - var type = InputType.File; var inputPath = new[] { state.MediaPath }; @@ -898,9 +893,7 @@ namespace MediaBrowser.Api.Playback Arguments = commandLineArgs, WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false, - - RedirectStandardInput = state.SendInputOverStandardInput + ErrorDialog = false }, EnableRaisingEvents = true @@ -933,11 +926,6 @@ namespace MediaBrowser.Api.Playback throw; } - if (state.SendInputOverStandardInput) - { - StreamToStandardInput(process, state); - } - // MUST read both stdout and stderr asynchronously or a deadlock may occurr process.BeginOutputReadLine(); @@ -965,32 +953,6 @@ namespace MediaBrowser.Api.Playback } } - private async void StreamToStandardInput(Process process, StreamState state) - { - try - { - await StreamToStandardInputInternal(process, state).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Logger.Debug("Stream to standard input closed normally."); - } - catch (Exception ex) - { - Logger.ErrorException("Error writing to standard input", ex); - } - } - - private async Task StreamToStandardInputInternal(Process process, StreamState state) - { - state.StandardInputCancellationTokenSource = new CancellationTokenSource(); - - using (var fileStream = FileSystem.GetFileStream(state.MediaPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) - { - await new EndlessStreamCopy().CopyStream(fileStream, process.StandardInput.BaseStream, state.StandardInputCancellationTokenSource.Token).ConfigureAwait(false); - } - } - protected int? GetVideoBitrateParamValue(StreamState state) { var bitrate = state.VideoRequest.VideoBitRate; @@ -1315,11 +1277,6 @@ namespace MediaBrowser.Api.Playback ParseParams(request); } - if (request.ThrowDebugError) - { - throw new InvalidOperationException("You asked for a debug error, you got one."); - } - var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager); var url = Request.PathInfo; @@ -1369,8 +1326,6 @@ namespace MediaBrowser.Api.Playback { state.MediaPath = path; state.IsRemote = false; - - state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress; } else if (!string.IsNullOrEmpty(mediaUrl)) { @@ -1378,7 +1333,8 @@ namespace MediaBrowser.Api.Playback state.IsRemote = true; } - //state.RunTimeTicks = recording.RunTimeTicks; + state.RunTimeTicks = recording.RunTimeTicks; + if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsRemote) { await Task.Delay(1000, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Api/Playback/ProgressiveStreamService.cs b/MediaBrowser.Api/Playback/ProgressiveStreamService.cs new file mode 100644 index 000000000..531f79a22 --- /dev/null +++ b/MediaBrowser.Api/Playback/ProgressiveStreamService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Api.Playback.Progressive; + +namespace MediaBrowser.Api.Playback +{ + //public class GetProgressiveAudioStream : StreamRequest + //{ + + //} + + //public class ProgressiveStreamService : BaseApiService + //{ + // public object Get(GetProgressiveAudioStream request) + // { + // return ProcessRequest(request, false); + // } + + // /// <summary> + // /// Gets the specified request. + // /// </summary> + // /// <param name="request">The request.</param> + // /// <returns>System.Object.</returns> + // public object Head(GetProgressiveAudioStream request) + // { + // return ProcessRequest(request, true); + // } + + // protected object ProcessRequest(StreamRequest request, bool isHeadRequest) + // { + // var state = GetState(request, CancellationToken.None).Result; + + // var responseHeaders = new Dictionary<string, string>(); + + // if (request.Static && state.IsRemote) + // { + // AddDlnaHeaders(state, responseHeaders, true); + + // return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result; + // } + + // var outputPath = GetOutputFilePath(state); + // var outputPathExists = File.Exists(outputPath); + + // var isStatic = request.Static || + // (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)); + + // AddDlnaHeaders(state, responseHeaders, isStatic); + + // if (request.Static) + // { + // var contentType = state.GetMimeType(state.MediaPath); + + // return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest); + // } + + // if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)) + // { + // var contentType = state.GetMimeType(outputPath); + + // return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest); + // } + + // return GetStreamResult(state, responseHeaders, isHeadRequest).Result; + // } + + //} +} diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 0eb2984fb..add517b5d 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -67,11 +67,6 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceProfileId { get; set; } - - /// <summary> - /// For testing purposes - /// </summary> - public bool ThrowDebugError { get; set; } public string Params { get; set; } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 504d7d921..48285a4b1 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -51,8 +51,6 @@ namespace MediaBrowser.Api.Playback public bool HasMediaStreams { get; set; } - public bool SendInputOverStandardInput { get; set; } - public CancellationTokenSource StandardInputCancellationTokenSource { get; set; } public string LiveTvStreamId { get; set; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index a02851a9f..2704959e4 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -86,6 +86,13 @@ namespace MediaBrowser.Controller.Dto ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item); /// <summary> + /// Gets the media sources. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>List{MediaSourceInfo}.</returns> + List<MediaSourceInfo> GetMediaSources(BaseItem item); + + /// <summary> /// Gets the item by name dto. /// </summary> /// <param name="item">The item.</param> diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs new file mode 100644 index 000000000..192ce2e83 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + public interface IMusicManager + { + /// <summary> + /// Gets the instant mix from song. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user); + /// <summary> + /// Gets the instant mix from artist. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromArtist(string name, User user); + /// <summary> + /// Gets the instant mix from album. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user); + /// <summary> + /// Gets the instant mix from genre. + /// </summary> + /// <param name="genres">The genres.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user); + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs index edf86cae8..f8cdc6eee 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs @@ -1,8 +1,8 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Library; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Library; namespace MediaBrowser.Controller.LiveTv { @@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.LiveTv RecordingInfo RecordingInfo { get; set; } + long? RunTimeTicks { get; set; } + string GetClientTypeName(); string GetUserDataKey(); diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 9915ac044..16834a945 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -134,6 +134,7 @@ <Compile Include="Library\DeleteOptions.cs" /> <Compile Include="Library\ILibraryPostScanTask.cs" /> <Compile Include="Library\IMetadataSaver.cs" /> + <Compile Include="Library\IMusicManager.cs" /> <Compile Include="Library\ItemUpdateType.cs" /> <Compile Include="Library\IUserDataManager.cs" /> <Compile Include="Library\UserDataSaveEventArgs.cs" /> @@ -155,11 +156,14 @@ <Compile Include="LiveTv\SeriesTimerInfo.cs" /> <Compile Include="LiveTv\TimerInfo.cs" /> <Compile Include="Localization\ILocalizationManager.cs" /> + <Compile Include="MediaEncoding\EncodingOptions.cs" /> <Compile Include="MediaEncoding\ChapterImageRefreshOptions.cs" /> + <Compile Include="MediaEncoding\EncodingResult.cs" /> <Compile Include="MediaEncoding\IEncodingManager.cs" /> <Compile Include="MediaEncoding\ImageEncodingOptions.cs" /> <Compile Include="MediaEncoding\IMediaEncoder.cs" /> <Compile Include="MediaEncoding\InternalMediaInfoResult.cs" /> + <Compile Include="MediaEncoding\VideoEncodingOptions.cs" /> <Compile Include="Net\IHasResultFactory.cs" /> <Compile Include="Net\IHttpResultFactory.cs" /> <Compile Include="Net\IHttpServer.cs" /> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs new file mode 100644 index 000000000..74235becd --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs @@ -0,0 +1,79 @@ +using MediaBrowser.Controller.Dlna; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingOptions + { + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public string ItemId { get; set; } + + /// <summary> + /// Gets or sets the media source identifier. + /// </summary> + /// <value>The media source identifier.</value> + public string MediaSourceId { get; set; } + + /// <summary> + /// Gets or sets the device profile. + /// </summary> + /// <value>The device profile.</value> + public DeviceProfile DeviceProfile { get; set; } + + /// <summary> + /// Gets or sets the output path. + /// </summary> + /// <value>The output path.</value> + public string OutputPath { get; set; } + + /// <summary> + /// Gets or sets the container. + /// </summary> + /// <value>The container.</value> + public string Container { get; set; } + + /// <summary> + /// Gets or sets the audio codec. + /// </summary> + /// <value>The audio codec.</value> + public string AudioCodec { get; set; } + + /// <summary> + /// Gets or sets the start time ticks. + /// </summary> + /// <value>The start time ticks.</value> + public long? StartTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the maximum channels. + /// </summary> + /// <value>The maximum channels.</value> + public int? MaxAudioChannels { get; set; } + + /// <summary> + /// Gets or sets the channels. + /// </summary> + /// <value>The channels.</value> + public int? AudioChannels { get; set; } + + /// <summary> + /// Gets or sets the sample rate. + /// </summary> + /// <value>The sample rate.</value> + public int? AudioSampleRate { get; set; } + + /// <summary> + /// Gets or sets the bit rate. + /// </summary> + /// <value>The bit rate.</value> + public int? AudioBitRate { get; set; } + + /// <summary> + /// Gets or sets the maximum audio bit rate. + /// </summary> + /// <value>The maximum audio bit rate.</value> + public int? MaxAudioBitRate { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs b/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs new file mode 100644 index 000000000..75ee90e42 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingResult + { + public string OutputPath { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs new file mode 100644 index 000000000..773f0ea46 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs @@ -0,0 +1,26 @@ + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class VideoEncodingOptions : EncodingOptions + { + public string VideoCodec { get; set; } + + public string VideoProfile { get; set; } + + public double? VideoLevel { get; set; } + + public int? VideoStreamIndex { get; set; } + + public int? AudioStreamIndex { get; set; } + + public int? SubtitleStreamIndex { get; set; } + + public int? MaxWidth { get; set; } + + public int? MaxHeight { get; set; } + + public int? Height { get; set; } + + public int? Width { get; set; } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs new file mode 100644 index 000000000..08b7fbe49 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -0,0 +1,91 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class AudioEncoder + { + private readonly string _ffmpegPath; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly IIsoManager _isoManager; + private readonly ILiveTvManager _liveTvManager; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public AudioEncoder(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager) + { + _ffmpegPath = ffmpegPath; + _logger = logger; + _fileSystem = fileSystem; + _appPaths = appPaths; + _isoManager = isoManager; + _liveTvManager = liveTvManager; + } + + public Task BeginEncoding(InternalEncodingTask task) + { + return new FFMpegProcess(_ffmpegPath, _logger, _fileSystem, _appPaths, _isoManager, _liveTvManager).Start(task, GetArguments); + } + + private string GetArguments(InternalEncodingTask task, string mountedPath) + { + var options = task.Request; + + return string.Format("{0} -i {1} {2} -id3v2_version 3 -write_id3v1 1 \"{3}\"", + GetInputModifier(task), + GetInputArgument(task), + GetOutputModifier(task), + options.OutputPath).Trim(); + } + + private string GetInputModifier(InternalEncodingTask task) + { + return EncodingUtils.GetInputModifier(task); + } + + private string GetInputArgument(InternalEncodingTask task) + { + return EncodingUtils.GetInputArgument(new List<string> { task.MediaPath }, task.IsInputRemote); + } + + private string GetOutputModifier(InternalEncodingTask task) + { + var options = task.Request; + + var audioTranscodeParams = new List<string> + { + "-threads " + EncodingUtils.GetNumberOfThreads(task, false).ToString(_usCulture), + "-vn" + }; + + var bitrate = EncodingUtils.GetAudioBitrateParam(task); + + if (bitrate.HasValue) + { + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture)); + } + + var channels = EncodingUtils.GetNumAudioChannelsParam(options, task.AudioStream); + + if (channels.HasValue) + { + audioTranscodeParams.Add("-ac " + channels.Value); + } + + if (options.AudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + options.AudioSampleRate.Value); + } + + return string.Join(" ", audioTranscodeParams.ToArray()); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs new file mode 100644 index 000000000..79d512dc1 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -0,0 +1,233 @@ +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public static class EncodingUtils + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public static string GetInputArgument(List<string> inputFiles, bool isRemote) + { + if (isRemote) + { + return GetHttpInputArgument(inputFiles); + } + + return GetConcatInputArgument(inputFiles); + } + + /// <summary> + /// Gets the concat input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <returns>System.String.</returns> + private static string GetConcatInputArgument(List<string> inputFiles) + { + // Get all streams + // If there's more than one we'll need to use the concat command + if (inputFiles.Count > 1) + { + var files = string.Join("|", inputFiles); + + return string.Format("concat:\"{0}\"", files); + } + + // Determine the input path for video files + return GetFileInputArgument(inputFiles[0]); + } + + /// <summary> + /// Gets the file input argument. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>System.String.</returns> + private static string GetFileInputArgument(string path) + { + return string.Format("file:\"{0}\"", path); + } + + /// <summary> + /// Gets the HTTP input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <returns>System.String.</returns> + private static string GetHttpInputArgument(IEnumerable<string> inputFiles) + { + var url = inputFiles.First(); + + return string.Format("\"{0}\"", url); + } + + public static string GetAudioInputModifier(InternalEncodingTask options) + { + return GetCommonInputModifier(options); + } + + public static string GetInputModifier(InternalEncodingTask options) + { + var inputModifier = GetCommonInputModifier(options); + + //if (state.VideoRequest != null) + //{ + // inputModifier += " -fflags genpts"; + //} + + //if (!string.IsNullOrEmpty(state.InputVideoCodec)) + //{ + // inputModifier += " -vcodec " + state.InputVideoCodec; + //} + + //if (!string.IsNullOrEmpty(state.InputVideoSync)) + //{ + // inputModifier += " -vsync " + state.InputVideoSync; + //} + + return inputModifier; + } + + private static string GetCommonInputModifier(InternalEncodingTask options) + { + var inputModifier = string.Empty; + + if (options.EnableDebugLogging) + { + inputModifier += "-loglevel debug"; + } + + var probeSize = GetProbeSizeArgument(options.InputVideoType.HasValue && options.InputVideoType.Value == VideoType.Dvd); + inputModifier += " " + probeSize; + inputModifier = inputModifier.Trim(); + + if (!string.IsNullOrWhiteSpace(options.UserAgent)) + { + inputModifier += " -user-agent \"" + options.UserAgent + "\""; + } + + inputModifier += " " + GetFastSeekValue(options.Request); + inputModifier = inputModifier.Trim(); + + if (!string.IsNullOrEmpty(options.InputFormat)) + { + inputModifier += " -f " + options.InputFormat; + } + + if (!string.IsNullOrEmpty(options.InputAudioCodec)) + { + inputModifier += " -acodec " + options.InputAudioCodec; + } + + if (!string.IsNullOrEmpty(options.InputAudioSync)) + { + inputModifier += " -async " + options.InputAudioSync; + } + + if (options.ReadInputAtNativeFramerate) + { + inputModifier += " -re"; + } + + return inputModifier; + } + + private static string GetFastSeekValue(EncodingOptions options) + { + var time = options.StartTimeTicks; + + if (time.HasValue) + { + var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds; + + if (seconds > 0) + { + return string.Format("-ss {0}", seconds.ToString(UsCulture)); + } + } + + return string.Empty; + } + + public static string GetProbeSizeArgument(bool isDvd) + { + return isDvd ? "-probesize 1G -analyzeduration 200M" : string.Empty; + } + + public static int? GetAudioBitrateParam(InternalEncodingTask task) + { + if (task.Request.AudioBitRate.HasValue) + { + // Make sure we don't request a bitrate higher than the source + var currentBitrate = task.AudioStream == null ? task.Request.AudioBitRate.Value : task.AudioStream.BitRate ?? task.Request.AudioBitRate.Value; + + return Math.Min(currentBitrate, task.Request.AudioBitRate.Value); + } + + return null; + } + + /// <summary> + /// Gets the number of audio channels to specify on the command line + /// </summary> + /// <param name="request">The request.</param> + /// <param name="audioStream">The audio stream.</param> + /// <returns>System.Nullable{System.Int32}.</returns> + public static int? GetNumAudioChannelsParam(EncodingOptions request, MediaStream audioStream) + { + if (audioStream != null) + { + if (audioStream.Channels > 2 && string.Equals(request.AudioCodec, "wma", StringComparison.OrdinalIgnoreCase)) + { + // wmav2 currently only supports two channel output + return 2; + } + } + + if (request.MaxAudioChannels.HasValue) + { + if (audioStream != null && audioStream.Channels.HasValue) + { + return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value); + } + + return request.MaxAudioChannels.Value; + } + + return request.AudioChannels; + } + + public static int GetNumberOfThreads(InternalEncodingTask state, bool isWebm) + { + // Use more when this is true. -re will keep cpu usage under control + if (state.ReadInputAtNativeFramerate) + { + if (isWebm) + { + return Math.Max(Environment.ProcessorCount - 1, 1); + } + + return 0; + } + + // Webm: http://www.webmproject.org/docs/encoder-parameters/ + // The decoder will usually automatically use an appropriate number of threads according to how many cores are available but it can only use multiple threads + // for the coefficient data if the encoder selected --token-parts > 0 at encode time. + + switch (state.QualitySetting) + { + case EncodingQuality.HighSpeed: + return 2; + case EncodingQuality.HighQuality: + return 2; + case EncodingQuality.MaxQuality: + return isWebm ? 2 : 0; + default: + throw new Exception("Unrecognized MediaEncodingQuality value."); + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs b/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs new file mode 100644 index 000000000..05733aef0 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs @@ -0,0 +1,168 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class FFMpegProcess : IDisposable + { + private readonly string _ffmpegPath; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly IIsoManager _isoManager; + private readonly ILiveTvManager _liveTvManager; + + private Stream _logFileStream; + private InternalEncodingTask _task; + private IIsoMount _isoMount; + + public FFMpegProcess(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager) + { + _ffmpegPath = ffmpegPath; + _logger = logger; + _fileSystem = fileSystem; + _appPaths = appPaths; + _isoManager = isoManager; + _liveTvManager = liveTvManager; + } + + public async Task Start(InternalEncodingTask task, Func<InternalEncodingTask,string,string> argumentsFactory) + { + _task = task; + if (!File.Exists(_ffmpegPath)) + { + throw new InvalidOperationException("ffmpeg was not found at " + _ffmpegPath); + } + + Directory.CreateDirectory(Path.GetDirectoryName(task.Request.OutputPath)); + + string mountedPath = null; + if (task.InputVideoType.HasValue && task.InputVideoType == VideoType.Iso && task.IsoType.HasValue) + { + if (_isoManager.CanMount(task.MediaPath)) + { + _isoMount = await _isoManager.Mount(task.MediaPath, CancellationToken.None).ConfigureAwait(false); + mountedPath = _isoMount.MountedPath; + } + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + + // Must consume both stdout and stderr or deadlocks may occur + RedirectStandardOutput = true, + RedirectStandardError = true, + + FileName = _ffmpegPath, + WorkingDirectory = Path.GetDirectoryName(_ffmpegPath), + Arguments = argumentsFactory(task, mountedPath), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + + EnableRaisingEvents = true + }; + + _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-" + task.Id + ".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. + _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); + + process.Exited += process_Exited; + + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.ErrorException("Error starting ffmpeg", ex); + + task.OnError(); + + DisposeLogFileStream(); + + process.Dispose(); + + throw; + } + + task.OnBegin(); + + // MUST read both stdout and stderr asynchronously or a deadlock may occurr + process.BeginOutputReadLine(); + +#pragma warning disable 4014 + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + process.StandardError.BaseStream.CopyToAsync(_logFileStream); +#pragma warning restore 4014 + } + + async void process_Exited(object sender, EventArgs e) + { + var process = (Process)sender; + + if (_isoMount != null) + { + _isoMount.Dispose(); + _isoMount = null; + } + + DisposeLogFileStream(); + + try + { + _logger.Info("FFMpeg exited with code {0} for {1}", process.ExitCode, _task.Request.OutputPath); + } + catch + { + _logger.Info("FFMpeg exited with an error for {0}", _task.Request.OutputPath); + } + + _task.OnCompleted(); + + if (!string.IsNullOrEmpty(_task.LiveTvStreamId)) + { + try + { + await _liveTvManager.CloseLiveStream(_task.LiveTvStreamId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live tv stream", ex); + } + } + } + + public void Dispose() + { + DisposeLogFileStream(); + } + + private void DisposeLogFileStream() + { + if (_logFileStream != null) + { + _logFileStream.Dispose(); + _logFileStream = null; + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs new file mode 100644 index 000000000..826525aef --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs @@ -0,0 +1,95 @@ +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class InternalEncodingTask + { + public string Id { get; set; } + + public CancellationTokenSource CancellationTokenSource { get; set; } + + public double ProgressPercentage { get; set; } + + public EncodingOptions Request { get; set; } + + public VideoEncodingOptions VideoRequest + { + get { return Request as VideoEncodingOptions; } + } + + public string MediaPath { get; set; } + public List<string> StreamFileNames { get; set; } + public bool IsInputRemote { get; set; } + + public VideoType? InputVideoType { get; set; } + public IsoType? IsoType { get; set; } + public long? InputRunTimeTicks; + + public string AudioSync = "1"; + public string VideoSync = "vfr"; + + public string InputAudioSync { get; set; } + public string InputVideoSync { get; set; } + + public bool DeInterlace { get; set; } + + public bool ReadInputAtNativeFramerate { get; set; } + + public string InputFormat { get; set; } + + public string InputVideoCodec { get; set; } + + public string InputAudioCodec { get; set; } + + public string LiveTvStreamId { get; set; } + + public MediaStream AudioStream { get; set; } + public MediaStream VideoStream { get; set; } + public MediaStream SubtitleStream { get; set; } + public bool HasMediaStreams { get; set; } + + public int SegmentLength = 10; + public int HlsListSize; + + public string MimeType { get; set; } + public string OrgPn { get; set; } + public bool EnableMpegtsM2TsMode { get; set; } + + /// <summary> + /// Gets or sets the user agent. + /// </summary> + /// <value>The user agent.</value> + public string UserAgent { get; set; } + + public EncodingQuality QualitySetting { get; set; } + + public InternalEncodingTask() + { + Id = Guid.NewGuid().ToString("N"); + CancellationTokenSource = new CancellationTokenSource(); + StreamFileNames = new List<string>(); + } + + public bool EnableDebugLogging { get; set; } + + internal void OnBegin() + { + + } + + internal void OnCompleted() + { + + } + + internal void OnError() + { + + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs new file mode 100644 index 000000000..fa9b87906 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs @@ -0,0 +1,323 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class InternalEncodingTaskFactory + { + private readonly ILibraryManager _libraryManager; + private readonly ILiveTvManager _liveTvManager; + private readonly IItemRepository _itemRepo; + private readonly IServerConfigurationManager _config; + + public InternalEncodingTaskFactory(ILibraryManager libraryManager, ILiveTvManager liveTvManager, IItemRepository itemRepo, IServerConfigurationManager config) + { + _libraryManager = libraryManager; + _liveTvManager = liveTvManager; + _itemRepo = itemRepo; + _config = config; + } + + public async Task<InternalEncodingTask> Create(EncodingOptions request, CancellationToken cancellationToken) + { + ValidateInput(request); + + var state = new InternalEncodingTask + { + Request = request + }; + + var item = string.IsNullOrEmpty(request.MediaSourceId) ? + _libraryManager.GetItemById(new Guid(request.ItemId)) : + _libraryManager.GetItemById(new Guid(request.MediaSourceId)); + + if (item is ILiveTvRecording) + { + var recording = await _liveTvManager.GetInternalRecording(request.ItemId, cancellationToken).ConfigureAwait(false); + + if (string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + state.InputVideoType = VideoType.VideoFile; + } + + var path = recording.RecordingInfo.Path; + var mediaUrl = recording.RecordingInfo.Url; + + if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl)) + { + var streamInfo = await _liveTvManager.GetRecordingStream(request.ItemId, cancellationToken).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + path = streamInfo.Path; + mediaUrl = streamInfo.Url; + } + + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + state.MediaPath = path; + state.IsInputRemote = false; + } + else if (!string.IsNullOrEmpty(mediaUrl)) + { + state.MediaPath = mediaUrl; + state.IsInputRemote = true; + } + + state.InputRunTimeTicks = recording.RunTimeTicks; + if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsInputRemote) + { + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + + state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress; + state.AudioSync = "1000"; + state.DeInterlace = true; + state.InputVideoSync = "-1"; + state.InputAudioSync = "1"; + } + else if (item is LiveTvChannel) + { + var channel = _liveTvManager.GetInternalChannel(request.ItemId); + + if (string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + state.InputVideoType = VideoType.VideoFile; + } + + var streamInfo = await _liveTvManager.GetChannelStream(request.ItemId, cancellationToken).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + if (!string.IsNullOrEmpty(streamInfo.Path) && File.Exists(streamInfo.Path)) + { + state.MediaPath = streamInfo.Path; + state.IsInputRemote = false; + + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + else if (!string.IsNullOrEmpty(streamInfo.Url)) + { + state.MediaPath = streamInfo.Url; + state.IsInputRemote = true; + } + + state.ReadInputAtNativeFramerate = true; + state.AudioSync = "1000"; + state.DeInterlace = true; + state.InputVideoSync = "-1"; + state.InputAudioSync = "1"; + } + else + { + state.MediaPath = item.Path; + state.IsInputRemote = item.LocationType == LocationType.Remote; + + var video = item as Video; + + if (video != null) + { + state.InputVideoType = video.VideoType; + state.IsoType = video.IsoType; + + state.StreamFileNames = video.PlayableStreamFileNames.ToList(); + } + + state.InputRunTimeTicks = item.RunTimeTicks; + } + + var videoRequest = request as VideoEncodingOptions; + + var mediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery + { + ItemId = item.Id + + }).ToList(); + + if (videoRequest != null) + { + state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video); + state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false); + state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio); + + if (state.VideoStream != null && state.VideoStream.IsInterlaced) + { + state.DeInterlace = true; + } + } + else + { + state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); + } + + state.HasMediaStreams = mediaStreams.Count > 0; + + state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10; + state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440; + + state.QualitySetting = GetQualitySetting(); + + ApplyDeviceProfileSettings(state); + + return state; + } + + private void ValidateInput(EncodingOptions request) + { + if (string.IsNullOrWhiteSpace(request.ItemId)) + { + throw new ArgumentException("ItemId is required."); + } + if (string.IsNullOrWhiteSpace(request.OutputPath)) + { + throw new ArgumentException("OutputPath is required."); + } + if (string.IsNullOrWhiteSpace(request.Container)) + { + throw new ArgumentException("Container is required."); + } + if (string.IsNullOrWhiteSpace(request.AudioCodec)) + { + throw new ArgumentException("AudioCodec is required."); + } + + var videoRequest = request as VideoEncodingOptions; + + if (videoRequest == null) + { + return; + } + } + + /// <summary> + /// Determines which stream will be used for playback + /// </summary> + /// <param name="allStream">All stream.</param> + /// <param name="desiredIndex">Index of the desired.</param> + /// <param name="type">The type.</param> + /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param> + /// <returns>MediaStream.</returns> + private MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true) + { + var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList(); + + if (desiredIndex.HasValue) + { + var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value); + + if (stream != null) + { + return stream; + } + } + + if (returnFirstIfNoIndex && type == MediaStreamType.Audio) + { + return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ?? + streams.FirstOrDefault(); + } + + // Just return the first one + return returnFirstIfNoIndex ? streams.FirstOrDefault() : null; + } + + private void ApplyDeviceProfileSettings(InternalEncodingTask state) + { + var profile = state.Request.DeviceProfile; + + if (profile == null) + { + // Don't use settings from the default profile. + // Only use a specific profile if it was requested. + return; + } + + var container = state.Request.Container; + + var audioCodec = state.Request.AudioCodec; + + if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null) + { + audioCodec = state.AudioStream.Codec; + } + + var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec; + + if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.VideoStream != null) + { + videoCodec = state.VideoStream.Codec; + } + + var mediaProfile = state.VideoRequest == null ? + profile.GetAudioMediaProfile(container, audioCodec, state.AudioStream) : + profile.GetVideoMediaProfile(container, audioCodec, videoCodec, state.AudioStream, state.VideoStream); + + if (mediaProfile != null) + { + state.MimeType = mediaProfile.MimeType; + state.OrgPn = mediaProfile.OrgPn; + } + + var transcodingProfile = state.VideoRequest == null ? + profile.GetAudioTranscodingProfile(container, audioCodec) : + profile.GetVideoTranscodingProfile(container, audioCodec, videoCodec); + + if (transcodingProfile != null) + { + //state.EstimateContentLength = transcodingProfile.EstimateContentLength; + state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; + //state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + + foreach (var setting in transcodingProfile.Settings) + { + switch (setting.Name) + { + case TranscodingSettingType.VideoProfile: + { + if (state.VideoRequest != null && string.IsNullOrWhiteSpace(state.VideoRequest.VideoProfile)) + { + state.VideoRequest.VideoProfile = setting.Value; + } + break; + } + default: + throw new ArgumentException("Unrecognized TranscodingSettingType"); + } + } + } + } + + private EncodingQuality GetQualitySetting() + { + var quality = _config.Configuration.MediaEncodingQuality; + + if (quality == EncodingQuality.Auto) + { + var cpuCount = Environment.ProcessorCount; + + if (cpuCount >= 4) + { + //return EncodingQuality.HighQuality; + } + + return EncodingQuality.HighSpeed; + } + + return quality; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index fac54ecff..93df0c8b9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -6,10 +6,10 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; -using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -122,35 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <exception cref="System.ArgumentException">Unrecognized InputType</exception> public string GetInputArgument(string[] inputFiles, InputType type) { - string inputPath; - - switch (type) - { - case InputType.Bluray: - case InputType.Dvd: - case InputType.File: - inputPath = GetConcatInputArgument(inputFiles); - break; - case InputType.Url: - inputPath = GetHttpInputArgument(inputFiles); - break; - default: - throw new ArgumentException("Unrecognized InputType"); - } - - return inputPath; - } - - /// <summary> - /// Gets the HTTP input argument. - /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <returns>System.String.</returns> - private string GetHttpInputArgument(string[] inputFiles) - { - var url = inputFiles[0]; - - return string.Format("\"{0}\"", url); + return EncodingUtils.GetInputArgument(inputFiles.ToList(), type == InputType.Url); } /// <summary> @@ -160,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>System.String.</returns> public string GetProbeSizeArgument(InputType type) { - return type == InputType.Dvd ? "-probesize 1G -analyzeduration 200M" : string.Empty; + return EncodingUtils.GetProbeSizeArgument(type == InputType.Dvd); } /// <summary> @@ -879,36 +851,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return memoryStream; } - /// <summary> - /// Gets the file input argument. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>System.String.</returns> - private string GetFileInputArgument(string path) - { - return string.Format("file:\"{0}\"", path); - } - - /// <summary> - /// Gets the concat input argument. - /// </summary> - /// <param name="playableStreamFiles">The playable stream files.</param> - /// <returns>System.String.</returns> - private string GetConcatInputArgument(string[] playableStreamFiles) - { - // Get all streams - // If there's more than one we'll need to use the concat command - if (playableStreamFiles.Length > 1) - { - var files = string.Join("|", playableStreamFiles); - - return string.Format("concat:\"{0}\"", files); - } - - // Determine the input path for video files - return GetFileInputArgument(playableStreamFiles[0]); - } - public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken) { return new ImageEncoder(FFMpegPath, _logger, _fileSystem, _appPaths).EncodeImage(options, cancellationToken); diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index fb1041f89..ee1658ddd 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -48,7 +48,12 @@ </ItemGroup> <ItemGroup> <Compile Include="BdInfo\BdInfoExaminer.cs" /> + <Compile Include="Encoder\AudioEncoder.cs" /> + <Compile Include="Encoder\EncodingUtils.cs" /> + <Compile Include="Encoder\FFMpegProcess.cs" /> <Compile Include="Encoder\ImageEncoder.cs" /> + <Compile Include="Encoder\InternalEncodingTask.cs" /> + <Compile Include="Encoder\InternalEncodingTaskFactory.cs" /> <Compile Include="Encoder\MediaEncoder.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> diff --git a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs index d17e2a7f9..fe6faf363 100644 --- a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs @@ -33,6 +33,8 @@ namespace MediaBrowser.Model.LiveTv /// <value>The external identifier.</value> public string ExternalId { get; set; } + public List<MediaSourceInfo> MediaSources { get; set; } + /// <summary> /// Gets or sets the image tags. /// </summary> @@ -112,6 +114,7 @@ namespace MediaBrowser.Model.LiveTv public ChannelInfoDto() { ImageTags = new Dictionary<ImageType, Guid>(); + MediaSources = new List<MediaSourceInfo>(); } public event PropertyChangedEventHandler PropertyChanged; diff --git a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs index 40aa5710e..de07382c0 100644 --- a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs @@ -1,11 +1,11 @@ -using System.Diagnostics; -using System.Runtime.Serialization; -using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Library; using System; using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Library; +using System.Diagnostics; +using System.Runtime.Serialization; namespace MediaBrowser.Model.LiveTv { @@ -248,10 +248,13 @@ namespace MediaBrowser.Model.LiveTv /// <value>The type.</value> public string Type { get; set; } + public List<MediaSourceInfo> MediaSources { get; set; } + public RecordingInfoDto() { Genres = new List<string>(); ImageTags = new Dictionary<ImageType, Guid>(); + MediaSources = new List<MediaSourceInfo>(); } public event PropertyChangedEventHandler PropertyChanged; diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 2e0b3cb17..02da776bd 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; @@ -1064,7 +1065,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary); } - dto.MediaSources = GetMediaSources(audio); + dto.MediaSources = GetAudioMediaSources(audio); dto.MediaSourceCount = 1; } @@ -1100,7 +1101,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.MediaSources)) { - dto.MediaSources = GetMediaSources(video); + dto.MediaSources = GetVideoMediaSources(video); } if (fields.Contains(ItemFields.Chapters)) @@ -1266,9 +1267,48 @@ namespace MediaBrowser.Server.Implementations.Dto { SetBookProperties(dto, book); } + + var tvChannel = item as LiveTvChannel; + + if (tvChannel != null) + { + dto.MediaSources = GetMediaSources(tvChannel); + } + } + + public List<MediaSourceInfo> GetMediaSources(BaseItem item) + { + var video = item as Video; + + if (video != null) + { + return GetVideoMediaSources(video); + } + + var audio = item as Audio; + + if (audio != null) + { + return GetAudioMediaSources(audio); + } + + var result = new List<MediaSourceInfo> + { + new MediaSourceInfo + { + Id = item.Id.ToString("N"), + LocationType = item.LocationType, + Name = item.Name, + Path = GetMappedPath(item), + MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { ItemId = item.Id }).ToList(), + RunTimeTicks = item.RunTimeTicks + } + }; + + return result; } - private List<MediaSourceInfo> GetMediaSources(Video item) + private List<MediaSourceInfo> GetVideoMediaSources(Video item) { var result = item.GetAlternateVersions().Select(GetVersionInfo).ToList(); @@ -1293,7 +1333,7 @@ namespace MediaBrowser.Server.Implementations.Dto .ToList(); } - private List<MediaSourceInfo> GetMediaSources(Audio item) + private List<MediaSourceInfo> GetAudioMediaSources(Audio item) { var result = new List<MediaSourceInfo> { diff --git a/MediaBrowser.Server.Implementations/Library/MusicManager.cs b/MediaBrowser.Server.Implementations/Library/MusicManager.cs new file mode 100644 index 000000000..9d5826454 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Library/MusicManager.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.Library +{ + public class MusicManager : IMusicManager + { + private readonly ILibraryManager _libraryManager; + + public MusicManager(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user) + { + return GetInstantMixFromGenres(item.Genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromArtist(string name, User user) + { + var artist = _libraryManager.GetArtist(name); + + var genres = _libraryManager.RootFolder + .RecursiveChildren + .OfType<Audio>() + .Where(i => i.HasArtist(name)) + .SelectMany(i => i.Genres) + .Concat(artist.Genres) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return GetInstantMixFromGenres(genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user) + { + var genres = item + .RecursiveChildren + .OfType<Audio>() + .SelectMany(i => i.Genres) + .Concat(item.Genres) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return GetInstantMixFromGenres(genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user) + { + var inputItems = user.RootFolder.GetRecursiveChildren(user); + + var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + return inputItems + .OfType<Audio>() + .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey))) + .OrderByDescending(i => i.Item2) + .ThenBy(i => Guid.NewGuid()) + .Select(i => i.Item1) + .Take(100) + .OrderBy(i => Guid.NewGuid()); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 67c011a1f..b92e82385 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -34,8 +34,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) || - string.IsNullOrEmpty(collectionType)) + if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)) { return new Controller.Entities.Audio.Audio(); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs index 4e1bd860b..cf7358cce 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -222,13 +222,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv RunTimeTicks = (info.EndDate - info.StartDate).Ticks, OriginalAirDate = info.OriginalAirDate, - MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery - { - ItemId = recording.Id - - }).ToList() + MediaSources = _dtoService.GetMediaSources((BaseItem)recording) }; + dto.MediaStreams = dto.MediaSources.SelectMany(i => i.MediaStreams).ToList(); + if (info.Status == RecordingStatus.InProgress) { var now = DateTime.UtcNow.Ticks; @@ -317,7 +315,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv Type = info.GetClientTypeName(), Id = info.Id.ToString("N"), MediaType = info.MediaType, - ExternalId = info.ExternalId + ExternalId = info.ExternalId, + MediaSources = _dtoService.GetMediaSources(info) }; if (user != null) diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index ea7ef2ed6..115bc0136 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -141,6 +141,7 @@ <Compile Include="IO\LibraryMonitor.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> <Compile Include="Library\LibraryManager.cs" /> + <Compile Include="Library\MusicManager.cs" /> <Compile Include="Library\Resolvers\PhotoResolver.cs" /> <Compile Include="Library\SearchEngine.cs" /> <Compile Include="Library\ResolverHelper.cs" /> diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 4f748a6a8..00d2aa992 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -3,8 +3,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; @@ -43,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.Session private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; + private readonly IMusicManager _musicManager; /// <summary> /// Gets or sets the configuration manager. @@ -688,9 +687,22 @@ namespace MediaBrowser.Server.Implementations.Session var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null; - var items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user)) - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); + List<BaseItem> items; + + if (command.PlayCommand == PlayCommand.PlayInstantMix) + { + items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user)) + .Where(i => i.LocationType != LocationType.Virtual) + .ToList(); + + command.PlayCommand = PlayCommand.PlayNow; + } + else + { + items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user)) + .Where(i => i.LocationType != LocationType.Virtual) + .ToList(); + } if (command.PlayCommand == PlayCommand.PlayShuffle) { @@ -741,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Session { var folder = (Folder)item; - var items = user == null ? folder.RecursiveChildren: + var items = user == null ? folder.RecursiveChildren : folder.GetRecursiveChildren(user); items = items.Where(i => !i.IsFolder); @@ -754,6 +766,41 @@ namespace MediaBrowser.Server.Implementations.Session return new[] { item }; } + private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user) + { + var item = _libraryManager.GetItemById(new Guid(id)); + + var audio = item as Audio; + + if (audio != null) + { + return _musicManager.GetInstantMixFromSong(audio, user); + } + + var artist = item as MusicArtist; + + if (artist != null) + { + return _musicManager.GetInstantMixFromArtist(artist.Name, user); + } + + var album = item as MusicAlbum; + + if (album != null) + { + return _musicManager.GetInstantMixFromAlbum(album, user); + } + + var genre = item as MusicGenre; + + if (genre != null) + { + return _musicManager.GetInstantMixFromGenres(new[] { genre.Name }, user); + } + + return new BaseItem[] { }; + } + public Task SendBrowseCommand(Guid controllingSessionId, Guid sessionId, BrowseRequest command, CancellationToken cancellationToken) { var session = GetSessionForRemoteControl(sessionId); diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index d9d5e007e..4c75b6502 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -449,6 +449,8 @@ namespace MediaBrowser.ServerApplication LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager); RegisterSingleInstance(LibraryManager); + RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager)); + LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager); RegisterSingleInstance(LibraryMonitor); diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index ea1905bad..f268c44ec 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -4058,7 +4058,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi options.ForceTitle = true; } - var url = self.getUrl("Packages/" + packageId + "Reviews", options); + var url = self.getUrl("Packages/" + packageId + "/Reviews", options); return self.ajax({ type: "GET", diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index a5074d3c8..a874d5f6f 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="MediaBrowser.ApiClient.Javascript" version="3.0.248" targetFramework="net45" /> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.249" targetFramework="net45" /> </packages>
\ No newline at end of file |
