diff options
Diffstat (limited to 'MediaBrowser.Api')
38 files changed, 1655 insertions, 1252 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index ef415ec57..281f764b5 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -132,6 +132,7 @@ namespace MediaBrowser.Api /// Called when [transcode beginning]. /// </summary> /// <param name="path">The path.</param> + /// <param name="streamId">The stream identifier.</param> /// <param name="transcodingJobId">The transcoding job identifier.</param> /// <param name="type">The type.</param> /// <param name="process">The process.</param> @@ -140,6 +141,7 @@ namespace MediaBrowser.Api /// <param name="cancellationTokenSource">The cancellation token source.</param> /// <returns>TranscodingJob.</returns> public TranscodingJob OnTranscodeBeginning(string path, + string streamId, string transcodingJobId, TranscodingJobType type, Process process, @@ -157,7 +159,8 @@ namespace MediaBrowser.Api ActiveRequestCount = 1, DeviceId = deviceId, CancellationTokenSource = cancellationTokenSource, - Id = transcodingJobId + Id = transcodingJobId, + StreamId = streamId }; _activeTranscodingJobs.Add(job); @@ -316,17 +319,26 @@ namespace MediaBrowser.Api /// Kills the single transcoding job. /// </summary> /// <param name="deviceId">The device id.</param> + /// <param name="streamId">The stream identifier.</param> /// <param name="deleteFiles">The delete files.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">deviceId</exception> - internal void KillTranscodingJobs(string deviceId, Func<string, bool> deleteFiles) + internal void KillTranscodingJobs(string deviceId, string streamId, Func<string, bool> deleteFiles) { if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException("deviceId"); } - KillTranscodingJobs(j => string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase), deleteFiles); + KillTranscodingJobs(j => + { + if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)) + { + return string.IsNullOrWhiteSpace(streamId) || string.Equals(streamId, j.StreamId, StringComparison.OrdinalIgnoreCase); + } + + return false; + + }, deleteFiles); } /// <summary> @@ -335,7 +347,7 @@ namespace MediaBrowser.Api /// <param name="killJob">The kill job.</param> /// <param name="deleteFiles">The delete files.</param> /// <returns>Task.</returns> - internal void KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles) + private void KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles) { var jobs = new List<TranscodingJob>(); @@ -517,6 +529,11 @@ namespace MediaBrowser.Api public class TranscodingJob { /// <summary> + /// Gets or sets the stream identifier. + /// </summary> + /// <value>The stream identifier.</value> + public string StreamId { get; set; } + /// <summary> /// Gets or sets the path. /// </summary> /// <value>The path.</value> diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index dff433c9d..4465be97a 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Dto; +using System.Threading.Tasks; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -72,6 +73,29 @@ namespace MediaBrowser.Api return ResultFactory.GetOptimizedResultUsingCache(Request, cacheKey, lastDateModified, cacheDuration, factoryFn); } + protected void AssertCanUpdateUser(IUserManager userManager, string userId) + { + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + + var authenticatedUser = userManager.GetUserById(auth.UserId); + + // If they're going to update the record of another user, they must be an administrator + if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase)) + { + if (!authenticatedUser.Policy.IsAdministrator) + { + throw new SecurityException("Unauthorized access."); + } + } + else + { + if (!authenticatedUser.Policy.EnableUserPreferenceAccess) + { + throw new SecurityException("Unauthorized access."); + } + } + } + /// <summary> /// To the optimized serialized result using cache. /// </summary> @@ -88,9 +112,9 @@ namespace MediaBrowser.Api /// Gets the session. /// </summary> /// <returns>SessionInfo.</returns> - protected SessionInfo GetSession() + protected async Task<SessionInfo> GetSession() { - var session = SessionContext.GetSession(Request); + var session = await SessionContext.GetSession(Request).ConfigureAwait(false); if (session == null) { diff --git a/MediaBrowser.Api/ConnectService.cs b/MediaBrowser.Api/ConnectService.cs index 4bcd33d9e..bdd2eeaad 100644 --- a/MediaBrowser.Api/ConnectService.cs +++ b/MediaBrowser.Api/ConnectService.cs @@ -1,8 +1,10 @@ -using MediaBrowser.Common.Extensions; +using System; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Connect; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Connect; +using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; using System.Linq; @@ -73,6 +75,28 @@ namespace MediaBrowser.Api public string ConnectUserId { get; set; } } + [Route("/Connect/Supporters", "GET")] + [Authenticated(Roles = "Admin")] + public class GetConnectSupporterSummary : IReturn<ConnectSupporterSummary> + { + } + + [Route("/Connect/Supporters", "DELETE")] + [Authenticated(Roles = "Admin")] + public class RemoveConnectSupporter : IReturnVoid + { + [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string Id { get; set; } + } + + [Route("/Connect/Supporters", "POST")] + [Authenticated(Roles = "Admin")] + public class AddConnectSupporter : IReturnVoid + { + [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Id { get; set; } + } + public class ConnectService : BaseApiService { private readonly IConnectManager _connectManager; @@ -84,6 +108,35 @@ namespace MediaBrowser.Api _userManager = userManager; } + public async Task<object> Get(GetConnectSupporterSummary request) + { + var result = await _connectManager.GetConnectSupporterSummary().ConfigureAwait(false); + var existingConnectUserIds = result.Users.Select(i => i.Id).ToList(); + + result.EligibleUsers = _userManager.Users + .Where(i => !string.IsNullOrWhiteSpace(i.ConnectUserId)) + .Where(i => !existingConnectUserIds.Contains(i.ConnectUserId, StringComparer.OrdinalIgnoreCase)) + .OrderBy(i => i.Name) + .Select(i => _userManager.GetUserDto(i)) + .ToList(); + + return ToOptimizedResult(result); + } + + public void Delete(RemoveConnectSupporter request) + { + var task = _connectManager.RemoveConnectSupporter(request.Id); + + Task.WaitAll(task); + } + + public void Post(AddConnectSupporter request) + { + var task = _connectManager.AddConnectSupporter(request.Id); + + Task.WaitAll(task); + } + public object Post(CreateConnectLink request) { return _connectManager.LinkUser(request.Id, request.ConnectUsername); diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index e29bbf674..ec9b2571e 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Api.Images /// Class UpdateItemImageIndex /// </summary> [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")] - [Authenticated] + [Authenticated(Roles = "admin")] public class UpdateItemImageIndex : IReturnVoid { /// <summary> @@ -64,7 +64,7 @@ namespace MediaBrowser.Api.Images /// </summary> /// <value>The id.</value> [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } + public string Id { get; set; } /// <summary> /// Gets or sets the type of the image. @@ -143,7 +143,7 @@ namespace MediaBrowser.Api.Images /// </summary> /// <value>The id.</value> [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } + public string Id { get; set; } } /// <summary> @@ -151,7 +151,7 @@ namespace MediaBrowser.Api.Images /// </summary> [Route("/Items/{Id}/Images/{Type}", "DELETE")] [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")] - [Authenticated] + [Authenticated(Roles = "admin")] public class DeleteItemImage : DeleteImageRequest, IReturnVoid { /// <summary> @@ -159,7 +159,7 @@ namespace MediaBrowser.Api.Images /// </summary> /// <value>The id.</value> [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } + public string Id { get; set; } } /// <summary> @@ -175,7 +175,7 @@ namespace MediaBrowser.Api.Images /// </summary> /// <value>The id.</value> [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid Id { get; set; } + public string Id { get; set; } } /// <summary> @@ -191,7 +191,7 @@ namespace MediaBrowser.Api.Images /// </summary> /// <value>The id.</value> [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public Guid Id { get; set; } + public string Id { get; set; } /// <summary> /// The raw Http Request Input Stream @@ -206,7 +206,7 @@ namespace MediaBrowser.Api.Images [Route("/Items/{Id}/Images/{Type}", "POST")] [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")] [Api(Description = "Posts an item image")] - [Authenticated] + [Authenticated(Roles = "admin")] public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid { /// <summary> @@ -318,7 +318,7 @@ namespace MediaBrowser.Api.Images try { - var size = _imageProcessor.GetImageSize(info.Path, info.DateModified); + var size = _imageProcessor.GetImageSize(info); width = Convert.ToInt32(size.Width); height = Convert.ToInt32(size.Height); @@ -417,11 +417,12 @@ namespace MediaBrowser.Api.Images /// <param name="request">The request.</param> public void Post(PostUserImage request) { - var id = new Guid(GetPathValue(1)); + var userId = GetPathValue(1); + AssertCanUpdateUser(_userManager, userId); request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true); - var item = _userManager.GetUserById(id); + var item = _userManager.GetUserById(userId); var task = PostImage(item, request.RequestStream, request.Type, Request.ContentType); @@ -434,7 +435,7 @@ namespace MediaBrowser.Api.Images /// <param name="request">The request.</param> public void Post(PostItemImage request) { - var id = new Guid(GetPathValue(1)); + var id = GetPathValue(1); request.Type = (ImageType)Enum.Parse(typeof(ImageType), GetPathValue(3), true); @@ -451,7 +452,10 @@ namespace MediaBrowser.Api.Images /// <param name="request">The request.</param> public void Delete(DeleteUserImage request) { - var item = _userManager.GetUserById(request.Id); + var userId = request.Id; + AssertCanUpdateUser(_userManager, userId); + + var item = _userManager.GetUserById(userId); var task = item.DeleteImage(request.Type, request.Index ?? 0); @@ -492,7 +496,6 @@ namespace MediaBrowser.Api.Images /// <param name="currentIndex">Index of the current.</param> /// <param name="newIndex">The new index.</param> /// <returns>Task.</returns> - /// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception> private Task UpdateItemIndex(IHasImages item, ImageType type, int currentIndex, int newIndex) { return item.SwapImages(type, currentIndex, newIndex); diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index d6b4da8be..a0ba6e61f 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -200,24 +200,15 @@ namespace MediaBrowser.Api //} item.ProviderIds = request.ProviderIds; - var service = new ItemRefreshService(_libraryManager) + var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions { - Logger = Logger, - Request = Request, - ResultFactory = ResultFactory, - SessionContext = SessionContext, - AuthorizationContext = AuthorizationContext - }; - - service.Post(new RefreshItem - { - Id = request.Id, MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = ImageRefreshMode.FullRefresh, ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages, - Recursive = true - }); + ReplaceAllImages = request.ReplaceAllImages + + }, CancellationToken.None); + Task.WaitAll(task); } /// <summary> diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs index 78bc14ab0..419077f21 100644 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ b/MediaBrowser.Api/ItemRefreshService.cs @@ -1,13 +1,7 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using ServiceStack; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace MediaBrowser.Api { @@ -40,41 +34,12 @@ namespace MediaBrowser.Api public class ItemRefreshService : BaseApiService { private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; - public ItemRefreshService(ILibraryManager libraryManager) + public ItemRefreshService(ILibraryManager libraryManager, IProviderManager providerManager) { _libraryManager = libraryManager; - } - - private async Task RefreshArtist(RefreshItem request, MusicArtist item) - { - var cancellationToken = CancellationToken.None; - - var albums = _libraryManager.RootFolder - .GetRecursiveChildren() - .OfType<MusicAlbum>() - .Where(i => i.HasArtist(item.Name)) - .ToList(); - - var musicArtists = albums - .Select(i => i.Parent) - .OfType<MusicArtist>() - .ToList(); - - var options = GetRefreshOptions(request); - - var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), cancellationToken, options, true)); - - await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false); - - try - { - await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error refreshing library", ex); - } + _providerManager = providerManager; } /// <summary> @@ -85,68 +50,9 @@ namespace MediaBrowser.Api { var item = _libraryManager.GetItemById(request.Id); - var task = item is MusicArtist ? RefreshArtist(request, (MusicArtist)item) : RefreshItem(request, item); - - Task.WaitAll(task); - } - - /// <summary> - /// Refreshes the item. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>Task.</returns> - private async Task RefreshItem(RefreshItem request, BaseItem item) - { var options = GetRefreshOptions(request); - try - { - await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); - - if (item.IsFolder) - { - // Collection folders don't validate their children so we'll have to simulate that here - var collectionFolder = item as CollectionFolder; - - if (collectionFolder != null) - { - await RefreshCollectionFolderChildren(request, collectionFolder).ConfigureAwait(false); - } - else - { - var folder = (Folder)item; - - await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, options, request.Recursive).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - Logger.ErrorException("Error refreshing library", ex); - } - } - - /// <summary> - /// Refreshes the collection folder children. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="collectionFolder">The collection folder.</param> - /// <returns>Task.</returns> - private async Task RefreshCollectionFolderChildren(RefreshItem request, CollectionFolder collectionFolder) - { - var options = GetRefreshOptions(request); - - foreach (var child in collectionFolder.Children.ToList()) - { - await child.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); - - if (child.IsFolder) - { - var folder = (Folder)child; - - await folder.ValidateChildren(new Progress<double>(), CancellationToken.None, options, request.Recursive).ConfigureAwait(false); - } - } + _providerManager.QueueRefresh(item.Id, options); } private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request) diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 6517d738b..013838091 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -41,8 +41,8 @@ namespace MediaBrowser.Api [ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string ContentType { get; set; } } - - [Authenticated] + + [Authenticated(Roles = "admin")] public class ItemUpdateService : BaseApiService { private readonly ILibraryManager _libraryManager; @@ -61,7 +61,7 @@ namespace MediaBrowser.Api public object Get(GetMetadataEditorInfo request) { var item = _libraryManager.GetItemById(request.ItemId); - + var info = new MetadataEditorInfo { ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(), @@ -131,7 +131,7 @@ namespace MediaBrowser.Api Value = "" }); } - + list.Add(new NameValuePair { Name = "FolderTypeMovies", @@ -389,20 +389,33 @@ namespace MediaBrowser.Api game.PlayersSupported = request.Players; } - var song = item as Audio; + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null) + { + hasAlbumArtists.AlbumArtists = request + .AlbumArtists + .Select(i => i.Name) + .ToList(); + } + var hasArtists = item as IHasArtist; + if (hasArtists != null) + { + hasArtists.Artists = request + .ArtistItems + .Select(i => i.Name) + .ToList(); + } + + var song = item as Audio; if (song != null) { song.Album = request.Album; - song.AlbumArtists = string.IsNullOrWhiteSpace(request.AlbumArtist) ? new List<string>() : new List<string> { request.AlbumArtist }; - song.Artists = request.Artists.ToList(); } var musicVideo = item as MusicVideo; - if (musicVideo != null) { - musicVideo.Artists = request.Artists.ToList(); musicVideo.Album = request.Album; } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 85cc879f4..4d9afa260 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Activity; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -9,11 +8,9 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using ServiceStack; using System; diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 27944a4ea..f5fe921ce 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -212,24 +212,26 @@ namespace MediaBrowser.Api.Library File.Create(path); } - - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); } finally { - // No need to start if scanning the library because it will handle it - if (!request.RefreshLibrary) + Task.Run(() => { - _libraryMonitor.Start(); - } - } - - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + // No need to start if scanning the library because it will handle it + if (request.RefreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); } } @@ -279,24 +281,26 @@ namespace MediaBrowser.Api.Library } Directory.Move(currentPath, newPath); - - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); } finally { - // No need to start if scanning the library because it will handle it - if (!request.RefreshLibrary) + Task.Run(() => { - _libraryMonitor.Start(); - } - } - - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + // No need to start if scanning the library because it will handle it + if (request.RefreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); } } @@ -325,24 +329,26 @@ namespace MediaBrowser.Api.Library try { _fileSystem.DeleteDirectory(path, true); - - // Need to add a delay here or directory watchers may still pick up the changes - var delayTask = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(delayTask); } finally { - // No need to start if scanning the library because it will handle it - if (!request.RefreshLibrary) + Task.Run(() => { - _libraryMonitor.Start(); - } - } - - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + // No need to start if scanning the library because it will handle it + if (request.RefreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); } } @@ -362,24 +368,26 @@ namespace MediaBrowser.Api.Library try { LibraryHelpers.AddMediaPath(_fileSystem, request.Name, request.Path, _appPaths); - - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); } finally { - // No need to start if scanning the library because it will handle it - if (!request.RefreshLibrary) + Task.Run(() => { - _libraryMonitor.Start(); - } - } - - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + // No need to start if scanning the library because it will handle it + if (request.RefreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); } } @@ -399,24 +407,26 @@ namespace MediaBrowser.Api.Library try { LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, _appPaths); - - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); } finally { - // No need to start if scanning the library because it will handle it - if (!request.RefreshLibrary) + Task.Run(() => { - _libraryMonitor.Start(); - } - } - - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + // No need to start if scanning the library because it will handle it + if (request.RefreshLibrary) + { + _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitor.Start(); + } + }); } } } diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index f3dcf57e0..24c91e172 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -171,6 +171,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "MinStartDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] public string MinStartDate { get; set; } + [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? HasAired { get; set; } + [ApiMember(Name = "MaxStartDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] public string MaxStartDate { get; set; } @@ -179,6 +182,24 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "MaxEndDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] public string MaxEndDate { get; set; } + + [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")] + public bool? IsMovie { get; set; } + + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? StartIndex { get; set; } + + [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? Limit { get; set; } + + [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Name, StartDate", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string SortBy { get; set; } + + [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public SortOrder? SortOrder { get; set; } + + [ApiMember(Name = "Genres", Description = "The genres to return guide information for.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] + public string Genres { get; set; } } [Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")] @@ -196,6 +217,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? HasAired { get; set; } + + [ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsMovie { get; set; } } [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")] @@ -312,7 +336,7 @@ namespace MediaBrowser.Api.LiveTv private void AssertUserCanManageLiveTv() { - var user = SessionContext.GetUser(Request); + var user = SessionContext.GetUser(Request).Result; if (user == null) { @@ -368,8 +392,9 @@ namespace MediaBrowser.Api.LiveTv { var query = new ProgramQuery { - ChannelIdList = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(), - UserId = request.UserId + ChannelIds = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(), + UserId = request.UserId, + HasAired = request.HasAired }; if (!string.IsNullOrEmpty(request.MinStartDate)) @@ -392,6 +417,13 @@ namespace MediaBrowser.Api.LiveTv query.MaxEndDate = DateTime.Parse(request.MaxEndDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); } + query.StartIndex = request.StartIndex; + query.Limit = request.Limit; + query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + query.SortOrder = request.SortOrder; + query.IsMovie = request.IsMovie; + query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); @@ -404,7 +436,8 @@ namespace MediaBrowser.Api.LiveTv UserId = request.UserId, IsAiring = request.IsAiring, Limit = request.Limit, - HasAired = request.HasAired + HasAired = request.HasAired, + IsMovie = request.IsMovie }; var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 271305921..14d0f13fb 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -79,8 +79,10 @@ <Compile Include="FilterService.cs" /> <Compile Include="IHasDtoOptions.cs" /> <Compile Include="Library\ChapterService.cs" /> - <Compile Include="Playback\Hls\MpegDashService.cs" /> + <Compile Include="Playback\Dash\ManifestBuilder.cs" /> + <Compile Include="Playback\Dash\MpegDashService.cs" /> <Compile Include="Playback\MediaInfoService.cs" /> + <Compile Include="Playback\TranscodingThrottler.cs" /> <Compile Include="PlaylistService.cs" /> <Compile Include="Reports\ReportFieldType.cs" /> <Compile Include="Reports\ReportResult.cs" /> @@ -134,6 +136,7 @@ <Compile Include="SearchService.cs" /> <Compile Include="Session\SessionsService.cs" /> <Compile Include="SimilarItemsHelper.cs" /> + <Compile Include="Sync\SyncHelper.cs" /> <Compile Include="Sync\SyncJobWebSocketListener.cs" /> <Compile Include="Sync\SyncJobsWebSocketListener.cs" /> <Compile Include="Sync\SyncService.cs" /> diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index 76c6c5776..a1c98addb 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -77,15 +77,13 @@ namespace MediaBrowser.Api.Music var album1 = (MusicAlbum)item1; var album2 = (MusicAlbum)item2; - var artists1 = album1.GetRecursiveChildren(i => i is IHasArtist) - .Cast<IHasArtist>() - .SelectMany(i => i.AllArtists) + var artists1 = album1 + .AllArtists .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); - var artists2 = album2.GetRecursiveChildren(i => i is IHasArtist) - .Cast<IHasArtist>() - .SelectMany(i => i.AllArtists) + var artists2 = album2 + .AllArtists .Distinct(StringComparer.OrdinalIgnoreCase) .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 49d5f1c8d..7115ddffd 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1,9 +1,9 @@ -using MediaBrowser.Controller.Devices; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -14,6 +14,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using System; @@ -68,17 +69,21 @@ namespace MediaBrowser.Api.Playback protected ILiveTvManager LiveTvManager { get; private set; } protected IDlnaManager DlnaManager { get; private set; } protected IDeviceManager DeviceManager { get; private set; } - protected IChannelManager ChannelManager { get; private set; } protected ISubtitleEncoder SubtitleEncoder { get; private set; } + protected IProcessManager ProcessManager { get; private set; } + protected IMediaSourceManager MediaSourceManager { get; private set; } + protected IZipClient ZipClient { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="BaseStreamingService" /> class. /// </summary> - protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) + protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) { + ZipClient = zipClient; + MediaSourceManager = mediaSourceManager; + ProcessManager = processManager; DeviceManager = deviceManager; SubtitleEncoder = subtitleEncoder; - ChannelManager = channelManager; DlnaManager = dlnaManager; LiveTvManager = liveTvManager; FileSystem = fileSystem; @@ -129,9 +134,21 @@ namespace MediaBrowser.Api.Playback var data = GetCommandLineArguments("dummy\\dummy", "dummyTranscodingId", state, false); data += "-" + (state.Request.DeviceId ?? string.Empty); - data += "-" + (state.Request.ClientTime ?? string.Empty); + data += "-" + (state.Request.StreamId ?? state.Request.ClientTime ?? string.Empty); + + var dataHash = data.GetMD5().ToString("N"); + + if (EnableOutputInSubFolder) + { + return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLower()); + } + + return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLower()); + } - return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower()); + protected virtual bool EnableOutputInSubFolder + { + get { return false; } } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); @@ -877,14 +894,6 @@ namespace MediaBrowser.Api.Playback return "copy"; } - private bool SupportsThrottleWithStream - { - get - { - return false; - } - } - /// <summary> /// Gets the input argument. /// </summary> @@ -908,23 +917,15 @@ namespace MediaBrowser.Api.Playback private string GetInputPathArgument(string transcodingJobId, StreamState state) { - if (state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.VideoType == VideoType.VideoFile && - !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) - { - if (SupportsThrottleWithStream) - { - var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId; - - url += "&transcodingJobId=" + transcodingJobId; - - return string.Format("\"{0}\"", url); - } - } - } + //if (state.InputProtocol == MediaProtocol.File && + // state.RunTimeTicks.HasValue && + // state.VideoType == VideoType.VideoFile && + // !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + //{ + // if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) + // { + // } + //} var protocol = state.InputProtocol; @@ -1053,6 +1054,7 @@ namespace MediaBrowser.Api.Playback } var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, + state.Request.StreamId ?? state.Request.ClientTime, transcodingId, TranscodingJobType, process, @@ -1094,7 +1096,7 @@ namespace MediaBrowser.Api.Playback StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream); // Wait for the file to exist before proceeeding - while (!File.Exists(outputPath) && !transcodingJob.HasExited) + while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } @@ -1109,9 +1111,26 @@ namespace MediaBrowser.Api.Playback } } + StartThrottler(state, transcodingJob); + return transcodingJob; } + private void StartThrottler(StreamState state, TranscodingJob transcodingJob) + { + if (state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) + { + state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ProcessManager); + state.TranscodingThrottler.Start(); + } + } + } + private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) { try @@ -1505,7 +1524,7 @@ namespace MediaBrowser.Api.Playback } else if (i == 16) { - request.ClientTime = val; + request.StreamId = val; } else if (i == 17) { @@ -1640,6 +1659,9 @@ namespace MediaBrowser.Api.Playback List<MediaStream> mediaStreams = null; state.ItemType = item.GetType().Name; + state.ItemId = item.Id.ToString("N"); + var archivable = item as IArchivable; + state.IsInputArchive = archivable != null && archivable.IsArchive; if (item is ILiveTvRecording) { @@ -1653,7 +1675,7 @@ namespace MediaBrowser.Api.Playback var source = string.IsNullOrEmpty(request.MediaSourceId) ? recording.GetMediaSources(false).First() - : recording.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId)); + : MediaSourceManager.GetStaticMediaSource(recording, request.MediaSourceId, false); mediaStreams = source.MediaStreams; @@ -1692,25 +1714,13 @@ namespace MediaBrowser.Api.Playback // Just to prevent this from being null and causing other methods to fail state.MediaPath = string.Empty; } - else if (item is IChannelMediaItem) - { - var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false); - state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); - state.InputProtocol = mediaSource.Protocol; - state.MediaPath = mediaSource.Path; - state.RunTimeTicks = item.RunTimeTicks; - state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; - state.InputBitrate = mediaSource.Bitrate; - state.InputFileSize = mediaSource.Size; - state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; - mediaStreams = mediaSource.MediaStreams; - } else { - var hasMediaSources = (IHasMediaSources)item; + var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false); + var mediaSource = string.IsNullOrEmpty(request.MediaSourceId) - ? hasMediaSources.GetMediaSources(false).First() - : hasMediaSources.GetMediaSources(false).First(i => string.Equals(i.Id, request.MediaSourceId)); + ? mediaSources.First() + : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId)); mediaStreams = mediaSource.MediaStreams; @@ -1720,6 +1730,8 @@ namespace MediaBrowser.Api.Playback state.InputFileSize = mediaSource.Size; state.InputBitrate = mediaSource.Bitrate; state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; + state.RunTimeTicks = mediaSource.RunTimeTicks; + state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; var video = item as Video; @@ -1742,7 +1754,6 @@ namespace MediaBrowser.Api.Playback } } - state.RunTimeTicks = mediaSource.RunTimeTicks; } var videoRequest = request as VideoStreamRequest; @@ -1865,29 +1876,6 @@ namespace MediaBrowser.Api.Playback state.AllMediaStreams = mediaStreams; } - private async Task<MediaSourceInfo> GetChannelMediaInfo(string id, - string mediaSourceId, - CancellationToken cancellationToken) - { - var channelMediaSources = await ChannelManager.GetChannelItemMediaSources(id, true, cancellationToken) - .ConfigureAwait(false); - - var list = channelMediaSources.ToList(); - - if (!string.IsNullOrWhiteSpace(mediaSourceId)) - { - var source = list - .FirstOrDefault(i => string.Equals(mediaSourceId, i.Id)); - - if (source != null) - { - return source; - } - } - - return list.First(); - } - private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream) { if (videoStream.IsInterlaced) diff --git a/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs b/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs new file mode 100644 index 000000000..35e252a19 --- /dev/null +++ b/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs @@ -0,0 +1,224 @@ +using System; +using System.Globalization; +using System.Security; +using System.Text; + +namespace MediaBrowser.Api.Playback.Dash +{ + public class ManifestBuilder + { + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public string GetManifestText(StreamState state, string playlistUrl) + { + var builder = new StringBuilder(); + + var time = TimeSpan.FromTicks(state.RunTimeTicks.Value); + + var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S"; + + builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + + builder.AppendFormat( + "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" type=\"static\" mediaPresentationDuration=\"{0}\" minBufferTime=\"PT5.0S\">", + duration); + + builder.Append("<ProgramInformation>"); + builder.Append("</ProgramInformation>"); + + builder.Append("<Period start=\"PT0S\">"); + builder.Append(GetVideoAdaptationSet(state, playlistUrl)); + builder.Append(GetAudioAdaptationSet(state, playlistUrl)); + builder.Append("</Period>"); + + builder.Append("</MPD>"); + + return builder.ToString(); + } + + private string GetVideoAdaptationSet(StreamState state, string playlistUrl) + { + var builder = new StringBuilder(); + + builder.Append("<AdaptationSet id=\"video\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">"); + builder.Append(GetVideoRepresentationOpenElement(state)); + + AppendSegmentList(state, builder, "0", playlistUrl); + + builder.Append("</Representation>"); + builder.Append("</AdaptationSet>"); + + return builder.ToString(); + } + + private string GetAudioAdaptationSet(StreamState state, string playlistUrl) + { + var builder = new StringBuilder(); + + builder.Append("<AdaptationSet id=\"audio\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">"); + builder.Append(GetAudioRepresentationOpenElement(state)); + + builder.Append("<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"6\" />"); + + AppendSegmentList(state, builder, "1", playlistUrl); + + builder.Append("</Representation>"); + builder.Append("</AdaptationSet>"); + + return builder.ToString(); + } + + private string GetVideoRepresentationOpenElement(StreamState state) + { + var codecs = GetVideoCodecDescriptor(state); + + var mime = "video/mp4"; + + var xml = "<Representation id=\"0\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\""; + + if (state.OutputWidth.HasValue) + { + xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\""; + } + if (state.OutputHeight.HasValue) + { + xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\""; + } + if (state.OutputVideoBitrate.HasValue) + { + xml += " bandwidth=\"" + state.OutputVideoBitrate.Value.ToString(UsCulture) + "\""; + } + + xml += ">"; + + return xml; + } + + private string GetAudioRepresentationOpenElement(StreamState state) + { + var codecs = GetAudioCodecDescriptor(state); + + var mime = "audio/mp4"; + + var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\""; + + if (state.OutputAudioSampleRate.HasValue) + { + xml += " audioSamplingRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\""; + } + if (state.OutputAudioBitrate.HasValue) + { + xml += " bandwidth=\"" + state.OutputAudioBitrate.Value.ToString(UsCulture) + "\""; + } + + xml += ">"; + + return xml; + } + + private string GetVideoCodecDescriptor(StreamState state) + { + // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html + // http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/ + + var level = state.TargetVideoLevel ?? 0; + var profile = state.TargetVideoProfile ?? string.Empty; + + if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1) + { + if (level >= 4.1) + { + return "avc1.640028"; + } + + if (level >= 4) + { + return "avc1.640028"; + } + + return "avc1.64001f"; + } + + if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1) + { + if (level >= 4) + { + return "avc1.4d0028"; + } + + if (level >= 3.1) + { + return "avc1.4d001f"; + } + + return "avc1.4d001e"; + } + + if (level >= 3.1) + { + return "avc1.42001f"; + } + + return "avc1.42E01E"; + } + + private string GetAudioCodecDescriptor(StreamState state) + { + // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html + + if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return "mp4a.40.34"; + } + + // AAC 5ch + if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5) + { + return "mp4a.40.5"; + } + + // AAC 2ch + return "mp4a.40.2"; + } + + private void AppendSegmentList(StreamState state, StringBuilder builder, string type, string playlistUrl) + { + var extension = ".m4s"; + + var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds; + + var queryStringIndex = playlistUrl.IndexOf('?'); + var queryString = queryStringIndex == -1 ? string.Empty : playlistUrl.Substring(queryStringIndex); + + var index = 0; + var duration = 1000000 * state.SegmentLength; + builder.AppendFormat("<SegmentList timescale=\"1000000\" duration=\"{0}\" startNumber=\"1\">", duration.ToString(CultureInfo.InvariantCulture)); + + while (seconds > 0) + { + var filename = index == 0 + ? "init" + : (index - 1).ToString(UsCulture); + + var segmentUrl = string.Format("dash/{3}/{0}{1}{2}", + filename, + extension, + SecurityElement.Escape(queryString), + type); + + if (index == 0) + { + builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl); + } + else + { + builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl); + } + + seconds -= state.SegmentLength; + index++; + } + builder.Append("</SegmentList>"); + } + } +} diff --git a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs new file mode 100644 index 000000000..38b0d35d1 --- /dev/null +++ b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs @@ -0,0 +1,561 @@ +using MediaBrowser.Api.Playback.Hls; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.IO; +using ServiceStack; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MimeTypes = MediaBrowser.Model.Net.MimeTypes; + +namespace MediaBrowser.Api.Playback.Dash +{ + /// <summary> + /// Options is needed for chromecast. Threw Head in there since it's related + /// </summary> + [Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")] + [Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")] + public class GetMasterManifest : VideoStreamRequest + { + public bool EnableAdaptiveBitrateStreaming { get; set; } + + public GetMasterManifest() + { + EnableAdaptiveBitrateStreaming = true; + } + } + + [Route("/Videos/{Id}/dash/{RepresentationId}/{SegmentId}.m4s", "GET")] + public class GetDashSegment : VideoStreamRequest + { + /// <summary> + /// Gets or sets the segment id. + /// </summary> + /// <value>The segment id.</value> + public string SegmentId { get; set; } + + /// <summary> + /// Gets or sets the representation identifier. + /// </summary> + /// <value>The representation identifier.</value> + public string RepresentationId { get; set; } + } + + public class MpegDashService : BaseHlsService + { + public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient) + { + NetworkManager = networkManager; + } + + protected INetworkManager NetworkManager { get; private set; } + + public object Get(GetMasterManifest request) + { + var result = GetAsync(request, "GET").Result; + + return result; + } + + public object Head(GetMasterManifest request) + { + var result = GetAsync(request, "HEAD").Result; + + return result; + } + + protected override bool EnableOutputInSubFolder + { + get + { + return true; + } + } + + private async Task<object> GetAsync(GetMasterManifest request, string method) + { + if (string.IsNullOrEmpty(request.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required"); + } + + var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); + + var playlistText = string.Empty; + + if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) + { + playlistText = new ManifestBuilder().GetManifestText(state, Request.RawUrl); + } + + return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>()); + } + + public object Get(GetDashSegment request) + { + return GetDynamicSegment(request, request.SegmentId, request.RepresentationId).Result; + } + + private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId, string representationId) + { + if ((request.StartTimeTicks ?? 0) > 0) + { + throw new ArgumentException("StartTimeTicks is not allowed."); + } + + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var requestedIndex = string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase) ? + -1 : + int.Parse(segmentId, NumberStyles.Integer, UsCulture); + + var state = await GetState(request, cancellationToken).ConfigureAwait(false); + + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".mpd"); + + var segmentExtension = GetSegmentFileExtension(state); + + var segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); + var segmentLength = state.SegmentLength; + + TranscodingJob job = null; + + if (!string.IsNullOrWhiteSpace(segmentPath)) + { + job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); + } + + await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + try + { + segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); + if (!string.IsNullOrWhiteSpace(segmentPath)) + { + job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); + } + else + { + if (string.Equals(representationId, "0", StringComparison.OrdinalIgnoreCase)) + { + job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); + var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; + Logger.Debug("Current transcoding index is {0}. requestedIndex={1}. segmentGapRequiringTranscodingChange={2}", currentTranscodingIndex ?? -2, requestedIndex, segmentGapRequiringTranscodingChange); + if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange) + { + // If the playlist doesn't already exist, startup ffmpeg + try + { + ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, p => false); + + if (currentTranscodingIndex.HasValue) + { + DeleteLastTranscodedFiles(playlistPath, 0); + } + + var positionTicks = GetPositionTicks(state, requestedIndex); + request.StartTimeTicks = positionTicks; + + var startNumber = GetStartNumber(state); + + var workingDirectory = Path.Combine(Path.GetDirectoryName(playlistPath), (startNumber == -1 ? 0 : startNumber).ToString(CultureInfo.InvariantCulture)); + state.WaitForPath = Path.Combine(workingDirectory, Path.GetFileName(playlistPath)); + Directory.CreateDirectory(workingDirectory); + job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, workingDirectory).ConfigureAwait(false); + await WaitForMinimumDashSegmentCount(Path.Combine(workingDirectory, Path.GetFileName(playlistPath)), 1, cancellationTokenSource.Token).ConfigureAwait(false); + } + catch + { + state.Dispose(); + throw; + } + } + } + } + } + finally + { + ApiEntryPoint.Instance.TranscodingStartLock.Release(); + } + + while (string.IsNullOrWhiteSpace(segmentPath)) + { + segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + + Logger.Info("returning {0}", segmentPath); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType), cancellationToken).ConfigureAwait(false); + } + + private long GetPositionTicks(StreamState state, int requestedIndex) + { + if (requestedIndex <= 0) + { + return 0; + } + + var startSeconds = requestedIndex * state.SegmentLength; + return TimeSpan.FromSeconds(startSeconds).Ticks; + } + + protected Task WaitForMinimumDashSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) + { + return WaitForSegment(playlist, "stream0-" + segmentCount.ToString("00000", CultureInfo.InvariantCulture) + ".m4s", cancellationToken); + } + + private async Task<object> GetSegmentResult(string playlistPath, + string segmentPath, + int segmentIndex, + int segmentLength, + TranscodingJob transcodingJob, + CancellationToken cancellationToken) + { + // If all transcoding has completed, just return immediately + if (transcodingJob != null && transcodingJob.HasExited) + { + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + } + + // Wait for the file to stop being written to, then stream it + var length = new FileInfo(segmentPath).Length; + var eofCount = 0; + + while (eofCount < 10) + { + var info = new FileInfo(segmentPath); + + if (!info.Exists) + { + break; + } + + var newLength = info.Length; + + if (newLength == length) + { + eofCount++; + } + else + { + eofCount = 0; + } + + length = newLength; + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + } + + private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob) + { + var segmentEndingSeconds = (1 + index) * segmentLength; + var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks; + + return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + { + Path = segmentPath, + FileShare = FileShare.ReadWrite, + OnComplete = () => + { + if (transcodingJob != null) + { + transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); + } + + } + }); + } + + public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) + { + var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType); + + if (job == null || job.HasExited) + { + return null; + } + + var file = GetLastTranscodingFiles(playlist, segmentExtension, FileSystem, 1).FirstOrDefault(); + + if (file == null) + { + return null; + } + + return GetIndex(file.FullName); + } + + public int GetIndex(string segmentPath) + { + var indexString = Path.GetFileNameWithoutExtension(segmentPath).Split('-').LastOrDefault(); + + if (string.Equals(indexString, "init", StringComparison.OrdinalIgnoreCase)) + { + return -1; + } + var startNumber = int.Parse(Path.GetFileNameWithoutExtension(Path.GetDirectoryName(segmentPath)), NumberStyles.Integer, UsCulture); + + return startNumber + int.Parse(indexString, NumberStyles.Integer, UsCulture) - 1; + } + + private void DeleteLastTranscodedFiles(string playlistPath, int retryCount) + { + if (retryCount >= 5) + { + return; + } + } + + private static List<FileInfo> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count) + { + var folder = Path.GetDirectoryName(playlist); + + try + { + return new DirectoryInfo(folder) + .EnumerateFiles("*", SearchOption.AllDirectories) + .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(fileSystem.GetLastWriteTimeUtc) + .Take(count) + .ToList(); + } + catch (DirectoryNotFoundException) + { + return new List<FileInfo>(); + } + } + + private string FindSegment(string playlist, string representationId, string segmentExtension, int requestedIndex) + { + var folder = Path.GetDirectoryName(playlist); + + if (requestedIndex == -1) + { + var path = Path.Combine(folder, "0", "stream" + representationId + "-" + "init" + segmentExtension); + return File.Exists(path) ? path : null; + } + + try + { + foreach (var subfolder in new DirectoryInfo(folder).EnumerateDirectories().ToList()) + { + var subfolderName = Path.GetFileNameWithoutExtension(subfolder.FullName); + int startNumber; + if (int.TryParse(subfolderName, NumberStyles.Any, UsCulture, out startNumber)) + { + var segmentIndex = requestedIndex - startNumber + 1; + var path = Path.Combine(folder, subfolderName, "stream" + representationId + "-" + segmentIndex.ToString("00000", CultureInfo.InvariantCulture) + segmentExtension); + if (File.Exists(path)) + { + return path; + } + } + } + } + catch (DirectoryNotFoundException) + { + + } + + return null; + } + + protected override string GetAudioArguments(StreamState state) + { + var codec = state.OutputAudioCodec; + + if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + { + return "-codec:a:0 copy"; + } + + var args = "-codec:a:0 " + codec; + + var channels = state.OutputAudioChannels; + + if (channels.HasValue) + { + args += " -ac " + channels.Value; + } + + var bitrate = state.OutputAudioBitrate; + + if (bitrate.HasValue) + { + args += " -ab " + bitrate.Value.ToString(UsCulture); + } + + args += " " + GetAudioFilterParam(state, true); + + return args; + } + + protected override string GetVideoArguments(StreamState state) + { + var codec = state.OutputVideoCodec; + + var args = "-codec:v:0 " + codec; + + if (state.EnableMpegtsM2TsMode) + { + args += " -mpegts_m2ts_mode 1"; + } + + // See if we can save come cpu cycles by avoiding encoding + if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + { + return state.VideoStream != null && IsH264(state.VideoStream) ? + args + " -bsf:v h264_mp4toannexb" : + args; + } + + var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + state.SegmentLength.ToString(UsCulture)); + + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; + + args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg; + + // Add resolution params, if specified + if (!hasGraphicalSubs) + { + args += GetOutputSizeParam(state, codec, false); + } + + // This is for internal graphical subs + if (hasGraphicalSubs) + { + args += GetGraphicalSubtitleParam(state, codec); + } + + return args; + } + + protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) + { + // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3 + // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/ + + var threads = GetNumberOfThreads(state, false); + + var inputModifier = GetInputModifier(state); + + var initSegmentName = "stream$RepresentationID$-init.m4s"; + var segmentName = "stream$RepresentationID$-$Number%05d$.m4s"; + + var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"", + inputModifier, + GetInputArgument(transcodingJobId, state), + threads, + GetMapArgs(state), + GetVideoArguments(state), + GetAudioArguments(state), + initSegmentName, + segmentName, + (state.SegmentLength * 1000000).ToString(CultureInfo.InvariantCulture), + state.WaitForPath + ).Trim(); + + return args; + } + + protected override int GetStartNumber(StreamState state) + { + return GetStartNumber(state.VideoRequest); + } + + private int GetStartNumber(VideoStreamRequest request) + { + var segmentId = "0"; + + var segmentRequest = request as GetDashSegment; + if (segmentRequest != null) + { + segmentId = segmentRequest.SegmentId; + } + + if (string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase)) + { + return -1; + } + + return int.Parse(segmentId, NumberStyles.Integer, UsCulture); + } + + /// <summary> + /// Gets the segment file extension. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected override string GetSegmentFileExtension(StreamState state) + { + return ".m4s"; + } + + protected override TranscodingJobType TranscodingJobType + { + get + { + return TranscodingJobType.Dash; + } + } + + private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken) + { + var tmpPath = playlist + ".tmp"; + + var segmentFilename = Path.GetFileName(segment); + + Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist); + + while (true) + { + FileStream fileStream; + try + { + fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); + } + catch (IOException) + { + fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); + } + // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written + using (fileStream) + { + using (var reader = new StreamReader(fileStream)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + if (line.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + Logger.Debug("Finished waiting for {0} in {1}", segmentFilename, playlist); + return; + } + } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + } + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 2da5c33ce..8541a60ef 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -23,7 +23,7 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> public abstract class BaseHlsService : BaseStreamingService { - protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager) + protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient) { } @@ -181,7 +181,7 @@ namespace MediaBrowser.Api.Playback.Hls return builder.ToString(); } - protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) + protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) { Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist); diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index e639dbdfe..43a9db131 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -1,9 +1,8 @@ -using MediaBrowser.Controller.Devices; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -11,6 +10,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using ServiceStack; using System; @@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Hls public class DynamicHlsService : BaseHlsService { - public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager) + public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient) { NetworkManager = networkManager; } @@ -100,13 +100,13 @@ namespace MediaBrowser.Api.Playback.Hls var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture); + var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture); var state = await GetState(request, cancellationToken).ConfigureAwait(false); var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - var segmentPath = GetSegmentPath(playlistPath, index); + var segmentPath = GetSegmentPath(playlistPath, requestedIndex); var segmentLength = state.SegmentLength; var segmentExtension = GetSegmentFileExtension(state); @@ -115,7 +115,8 @@ namespace MediaBrowser.Api.Playback.Hls if (File.Exists(segmentPath)) { - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); + job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); } await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); @@ -123,26 +124,26 @@ namespace MediaBrowser.Api.Playback.Hls { if (File.Exists(segmentPath)) { - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); + job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); } else { var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - - if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4) + var segmentGapRequiringTranscodingChange = 24/state.SegmentLength; + if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange) { // If the playlist doesn't already exist, startup ffmpeg try { - ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase)); + ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId ?? request.ClientTime, p => false); if (currentTranscodingIndex.HasValue) { DeleteLastFile(playlistPath, segmentExtension, 0); } - var startSeconds = index * state.SegmentLength; - request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks; + request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex); job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } @@ -152,7 +153,7 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false); + await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); } } } @@ -169,11 +170,26 @@ namespace MediaBrowser.Api.Playback.Hls Logger.Info("returning {0}", segmentPath); job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); + return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); + } + + private long GetSeekPositionTicks(StreamState state, int requestedIndex) + { + var startSeconds = requestedIndex * state.SegmentLength; + var position = TimeSpan.FromSeconds(startSeconds).Ticks; + + return position; } public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) { + var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType); + + if (job == null || job.HasExited) + { + return null; + } + var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem); if (file == null) @@ -204,7 +220,7 @@ namespace MediaBrowser.Api.Playback.Hls { return; } - + try { FileSystem.DeleteFile(file.FullName); @@ -277,7 +293,7 @@ namespace MediaBrowser.Api.Playback.Hls CancellationToken cancellationToken) { // If all transcoding has completed, just return immediately - if (!IsTranscoding(playlistPath)) + if (transcodingJob != null && transcodingJob.HasExited) { return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); } @@ -288,12 +304,15 @@ namespace MediaBrowser.Api.Playback.Hls { using (var reader = new StreamReader(fileStream)) { - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - // If it appears in the playlist, it's done - if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + while (!reader.EndOfStream) { - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + var text = await reader.ReadLineAsync().ConfigureAwait(false); + + // If it appears in the playlist, it's done + if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); + } } } } @@ -356,13 +375,6 @@ namespace MediaBrowser.Api.Playback.Hls }); } - private bool IsTranscoding(string playlistPath) - { - var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - - return job != null && !job.HasExited; - } - private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method) { var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); @@ -681,20 +693,36 @@ namespace MediaBrowser.Api.Playback.Hls // If isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; - var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", - inputModifier, - GetInputArgument(transcodingJobId, state), - threads, - GetMapArgs(state), - GetVideoArguments(state), - GetAudioArguments(state), - state.SegmentLength.ToString(UsCulture), - startNumberParam, - state.HlsListSize.ToString(UsCulture), - outputPath - ).Trim(); - - return args; + if (state.EnableGenericHlsSegmenter) + { + var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts"; + + return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + inputModifier, + GetInputArgument(transcodingJobId, state), + threads, + GetMapArgs(state), + GetVideoArguments(state), + GetAudioArguments(state), + state.SegmentLength.ToString(UsCulture), + startNumberParam, + outputPath, + outputTsArg + ).Trim(); + } + + return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", + inputModifier, + GetInputArgument(transcodingJobId, state), + threads, + GetMapArgs(state), + GetVideoArguments(state), + GetAudioArguments(state), + state.SegmentLength.ToString(UsCulture), + startNumberParam, + state.HlsListSize.ToString(UsCulture), + outputPath + ).Trim(); } /// <summary> diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index 9f80fcd0a..da4ffb55d 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -47,6 +47,9 @@ namespace MediaBrowser.Api.Playback.Hls { [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] public string DeviceId { get; set; } + + [ApiMember(Name = "StreamId", Description = "The stream id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string StreamId { get; set; } } public class HlsSegmentService : BaseApiService @@ -69,7 +72,7 @@ namespace MediaBrowser.Api.Playback.Hls public void Delete(StopEncodingProcess request) { - ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, path => true); + ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.StreamId, path => true); } /// <summary> diff --git a/MediaBrowser.Api/Playback/Hls/MpegDashService.cs b/MediaBrowser.Api/Playback/Hls/MpegDashService.cs deleted file mode 100644 index 80451c0cc..000000000 --- a/MediaBrowser.Api/Playback/Hls/MpegDashService.cs +++ /dev/null @@ -1,675 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using ServiceStack; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace MediaBrowser.Api.Playback.Hls -{ - /// <summary> - /// Options is needed for chromecast. Threw Head in there since it's related - /// </summary> - [Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")] - [Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")] - public class GetMasterManifest : VideoStreamRequest - { - public bool EnableAdaptiveBitrateStreaming { get; set; } - - public GetMasterManifest() - { - EnableAdaptiveBitrateStreaming = true; - } - } - - [Route("/Videos/{Id}/dash/{SegmentId}.ts", "GET")] - [Route("/Videos/{Id}/dash/{SegmentId}.mp4", "GET")] - public class GetDashSegment : VideoStreamRequest - { - /// <summary> - /// Gets or sets the segment id. - /// </summary> - /// <value>The segment id.</value> - public string SegmentId { get; set; } - } - - public class MpegDashService : BaseHlsService - { - public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager) - { - NetworkManager = networkManager; - } - - protected INetworkManager NetworkManager { get; private set; } - - public object Get(GetMasterManifest request) - { - var result = GetAsync(request, "GET").Result; - - return result; - } - - public object Head(GetMasterManifest request) - { - var result = GetAsync(request, "HEAD").Result; - - return result; - } - - private async Task<object> GetAsync(GetMasterManifest request, string method) - { - if (string.Equals(request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("Audio codec copy is not allowed here."); - } - - if (string.Equals(request.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("Video codec copy is not allowed here."); - } - - if (string.IsNullOrEmpty(request.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required"); - } - - var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); - - var playlistText = string.Empty; - - if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) - { - playlistText = GetManifestText(state); - } - - return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>()); - } - - private string GetManifestText(StreamState state) - { - var builder = new StringBuilder(); - - var time = TimeSpan.FromTicks(state.RunTimeTicks.Value); - - var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S"; - - builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); - - var profile = string.Equals(GetSegmentFileExtension(state), ".ts", StringComparison.OrdinalIgnoreCase) - ? "urn:mpeg:dash:profile:mp2t-simple:2011" - : "urn:mpeg:dash:profile:mp2t-simple:2011"; - - builder.AppendFormat( - "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" minBufferTime=\"PT2.00S\" mediaPresentationDuration=\"{0}\" maxSegmentDuration=\"PT{1}S\" type=\"static\" profiles=\""+profile+"\">", - duration, - state.SegmentLength.ToString(CultureInfo.InvariantCulture)); - - builder.Append("<ProgramInformation moreInformationURL=\"http://gpac.sourceforge.net\">"); - builder.Append("</ProgramInformation>"); - - builder.AppendFormat("<Period start=\"PT0S\" duration=\"{0}\">", duration); - builder.Append("<AdaptationSet segmentAlignment=\"true\">"); - - builder.Append("<ContentComponent id=\"1\" contentType=\"video\"/>"); - - var lang = state.AudioStream != null ? state.AudioStream.Language : null; - if (string.IsNullOrWhiteSpace(lang)) lang = "und"; - - builder.AppendFormat("<ContentComponent id=\"2\" contentType=\"audio\" lang=\"{0}\"/>", lang); - - builder.Append(GetRepresentationOpenElement(state, lang)); - - AppendSegmentList(state, builder); - - builder.Append("</Representation>"); - builder.Append("</AdaptationSet>"); - builder.Append("</Period>"); - - builder.Append("</MPD>"); - - return builder.ToString(); - } - - private string GetRepresentationOpenElement(StreamState state, string language) - { - var codecs = GetVideoCodecDescriptor(state) + "," + GetAudioCodecDescriptor(state); - - var mime = string.Equals(GetSegmentFileExtension(state), ".ts", StringComparison.OrdinalIgnoreCase) - ? "video/mp2t" - : "video/mp4"; - - var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" startWithSAP=\"1\" codecs=\"" + codecs + "\""; - - if (state.OutputWidth.HasValue) - { - xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\""; - } - if (state.OutputHeight.HasValue) - { - xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\""; - } - if (state.OutputAudioSampleRate.HasValue) - { - xml += " sampleRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\""; - } - - if (state.TotalOutputBitrate.HasValue) - { - xml += " bandwidth=\"" + state.TotalOutputBitrate.Value.ToString(UsCulture) + "\""; - } - - xml += ">"; - - return xml; - } - - private string GetVideoCodecDescriptor(StreamState state) - { - // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html - // http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/ - - var level = state.TargetVideoLevel ?? 0; - var profile = state.TargetVideoProfile ?? string.Empty; - - if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1) - { - if (level >= 4.1) - { - return "avc1.640028"; - } - - if (level >= 4) - { - return "avc1.640028"; - } - - return "avc1.64001f"; - } - - if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1) - { - if (level >= 4) - { - return "avc1.4d0028"; - } - - if (level >= 3.1) - { - return "avc1.4d001f"; - } - - return "avc1.4d001e"; - } - - if (level >= 3.1) - { - return "avc1.42001f"; - } - - return "avc1.42E01E"; - } - - private string GetAudioCodecDescriptor(StreamState state) - { - // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html - - if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return "mp4a.40.34"; - } - - // AAC 5ch - if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5) - { - return "mp4a.40.5"; - } - - // AAC 2ch - return "mp4a.40.2"; - } - - public object Get(GetDashSegment request) - { - return GetDynamicSegment(request, request.SegmentId).Result; - } - - private void AppendSegmentList(StreamState state, StringBuilder builder) - { - var extension = GetSegmentFileExtension(state); - - var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds; - - var queryStringIndex = Request.RawUrl.IndexOf('?'); - var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex); - - var index = 0; - builder.Append("<SegmentList timescale=\"1000\" duration=\"10000\">"); - - - while (seconds > 0) - { - var segmentUrl = string.Format("dash/{0}{1}{2}", - index.ToString(UsCulture), - extension, - SecurityElement.Escape(queryString)); - - if (index == 0) - { - builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl); - } - else - { - builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl); - } - - seconds -= state.SegmentLength; - index++; - } - builder.Append("</SegmentList>"); - } - - private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId) - { - if ((request.StartTimeTicks ?? 0) > 0) - { - throw new ArgumentException("StartTimeTicks is not allowed."); - } - - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - - var index = int.Parse(segmentId, NumberStyles.Integer, UsCulture); - - var state = await GetState(request, cancellationToken).ConfigureAwait(false); - - var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - - var segmentExtension = GetSegmentFileExtension(state); - - var segmentPath = GetSegmentPath(playlistPath, segmentExtension, index); - var segmentLength = state.SegmentLength; - - TranscodingJob job = null; - - if (File.Exists(segmentPath)) - { - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); - } - - await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); - try - { - if (File.Exists(segmentPath)) - { - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); - } - else - { - var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - - if (currentTranscodingIndex == null || index < currentTranscodingIndex.Value || (index - currentTranscodingIndex.Value) > 4) - { - // If the playlist doesn't already exist, startup ffmpeg - try - { - ApiEntryPoint.Instance.KillTranscodingJobs(j => j.Type == TranscodingJobType && string.Equals(j.DeviceId, request.DeviceId, StringComparison.OrdinalIgnoreCase), p => !string.Equals(p, playlistPath, StringComparison.OrdinalIgnoreCase)); - - if (currentTranscodingIndex.HasValue) - { - DeleteLastFile(playlistPath, segmentExtension, 0); - } - - var startSeconds = index * state.SegmentLength; - request.StartTimeTicks = TimeSpan.FromSeconds(startSeconds).Ticks; - - job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, Path.GetDirectoryName(playlistPath)).ConfigureAwait(false); - } - catch - { - state.Dispose(); - throw; - } - - await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false); - } - } - } - finally - { - ApiEntryPoint.Instance.TranscodingStartLock.Release(); - } - - Logger.Info("waiting for {0}", segmentPath); - while (!File.Exists(segmentPath)) - { - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } - - Logger.Info("returning {0}", segmentPath); - job = job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false); - } - - private async Task<object> GetSegmentResult(string playlistPath, - string segmentPath, - int segmentIndex, - int segmentLength, - TranscodingJob transcodingJob, - CancellationToken cancellationToken) - { - // If all transcoding has completed, just return immediately - if (!IsTranscoding(playlistPath)) - { - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); - } - - var segmentFilename = Path.GetFileName(segmentPath); - - using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) - { - using (var reader = new StreamReader(fileStream)) - { - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - // If it appears in the playlist, it's done - if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) - { - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); - } - } - } - - // if a different file is encoding, it's done - //var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath); - //if (currentTranscodingIndex > segmentIndex) - //{ - //return GetSegmentResult(segmentPath, segmentIndex); - //} - - // Wait for the file to stop being written to, then stream it - var length = new FileInfo(segmentPath).Length; - var eofCount = 0; - - while (eofCount < 10) - { - var info = new FileInfo(segmentPath); - - if (!info.Exists) - { - break; - } - - var newLength = info.Length; - - if (newLength == length) - { - eofCount++; - } - else - { - eofCount = 0; - } - - length = newLength; - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); - } - - private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob) - { - var segmentEndingSeconds = (1 + index) * segmentLength; - var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks; - - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - Path = segmentPath, - FileShare = FileShare.ReadWrite, - OnComplete = () => - { - if (transcodingJob != null) - { - transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); - } - - } - }); - } - - private bool IsTranscoding(string playlistPath) - { - var job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - - return job != null && !job.HasExited; - } - - public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) - { - var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem); - - if (file == null) - { - return null; - } - - var playlistFilename = Path.GetFileNameWithoutExtension(playlist); - - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); - - return int.Parse(indexString, NumberStyles.Integer, UsCulture); - } - - private void DeleteLastFile(string path, string segmentExtension, int retryCount) - { - if (retryCount >= 5) - { - return; - } - - var file = GetLastTranscodingFile(path, segmentExtension, FileSystem); - - if (file != null) - { - try - { - FileSystem.DeleteFile(file.FullName); - } - catch (IOException ex) - { - Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName); - - Thread.Sleep(100); - DeleteLastFile(path, segmentExtension, retryCount + 1); - } - catch (Exception ex) - { - Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName); - } - } - } - - private static FileInfo GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) - { - var folder = Path.GetDirectoryName(playlist); - - try - { - return new DirectoryInfo(folder) - .EnumerateFiles("*", SearchOption.TopDirectoryOnly) - .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(fileSystem.GetLastWriteTimeUtc) - .FirstOrDefault(); - } - catch (DirectoryNotFoundException) - { - return null; - } - } - - protected override int GetStartNumber(StreamState state) - { - return GetStartNumber(state.VideoRequest); - } - - private int GetStartNumber(VideoStreamRequest request) - { - var segmentId = "0"; - - var segmentRequest = request as GetDynamicHlsVideoSegment; - if (segmentRequest != null) - { - segmentId = segmentRequest.SegmentId; - } - - return int.Parse(segmentId, NumberStyles.Integer, UsCulture); - } - - private string GetSegmentPath(string playlist, string segmentExtension, int index) - { - var folder = Path.GetDirectoryName(playlist); - - var filename = Path.GetFileNameWithoutExtension(playlist); - - return Path.Combine(folder, filename + index.ToString("000", UsCulture) + segmentExtension); - } - - protected override string GetAudioArguments(StreamState state) - { - var codec = state.OutputAudioCodec; - - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) - { - return "-codec:a:0 copy"; - } - - var args = "-codec:a:0 " + codec; - - var channels = state.OutputAudioChannels; - - if (channels.HasValue) - { - args += " -ac " + channels.Value; - } - - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) - { - args += " -ab " + bitrate.Value.ToString(UsCulture); - } - - args += " " + GetAudioFilterParam(state, true); - - return args; - } - - protected override string GetVideoArguments(StreamState state) - { - var codec = state.OutputVideoCodec; - - var args = "-codec:v:0 " + codec; - - if (state.EnableMpegtsM2TsMode) - { - args += " -mpegts_m2ts_mode 1"; - } - - // See if we can save come cpu cycles by avoiding encoding - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) - { - return state.VideoStream != null && IsH264(state.VideoStream) ? - args + " -bsf:v h264_mp4toannexb" : - args; - } - - var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", - state.SegmentLength.ToString(UsCulture)); - - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - - args+= " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg; - - args += " -r 24 -g 24"; - - // Add resolution params, if specified - if (!hasGraphicalSubs) - { - args += GetOutputSizeParam(state, codec, false); - } - - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, codec); - } - - return args; - } - - protected override string GetCommandLineArguments(string outputPath, string transcodingJobId, StreamState state, bool isEncoding) - { - // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3 - // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/ - - var threads = GetNumberOfThreads(state, false); - - var inputModifier = GetInputModifier(state); - - // If isEncoding is true we're actually starting ffmpeg - var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; - - var segmentFilename = Path.GetFileNameWithoutExtension(outputPath) + "%03d" + GetSegmentFileExtension(state); - - var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f ssegment -segment_time {6} -segment_list_size {8} -segment_list \"{9}\" {10}", - inputModifier, - GetInputArgument(transcodingJobId, state), - threads, - GetMapArgs(state), - GetVideoArguments(state), - GetAudioArguments(state), - state.SegmentLength.ToString(UsCulture), - startNumberParam, - state.HlsListSize.ToString(UsCulture), - outputPath, - segmentFilename - ).Trim(); - - return args; - } - - /// <summary> - /// Gets the segment file extension. - /// </summary> - /// <param name="state">The state.</param> - /// <returns>System.String.</returns> - protected override string GetSegmentFileExtension(StreamState state) - { - return ".mp4"; - } - - protected override TranscodingJobType TranscodingJobType - { - get - { - return TranscodingJobType.Dash; - } - } - } -} diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 8de52ea02..533764f64 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -57,7 +57,7 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> public class VideoHlsService : BaseHlsService { - public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager) + public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient) { } diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 77178c8cc..943d9fe48 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -1,7 +1,7 @@ -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; +using System; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.MediaInfo; using ServiceStack; @@ -22,51 +22,54 @@ namespace MediaBrowser.Api.Playback public string UserId { get; set; } } + [Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")] + public class GetPlaybackInfo : IReturn<LiveMediaInfoResult> + { + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Id { get; set; } + + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string UserId { get; set; } + } + [Authenticated] public class MediaInfoService : BaseApiService { - private readonly ILibraryManager _libraryManager; - private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; + private readonly IMediaSourceManager _mediaSourceManager; - public MediaInfoService(ILibraryManager libraryManager, IChannelManager channelManager, IUserManager userManager) + public MediaInfoService(IMediaSourceManager mediaSourceManager) { - _libraryManager = libraryManager; - _channelManager = channelManager; - _userManager = userManager; + _mediaSourceManager = mediaSourceManager; } - public async Task<object> Get(GetLiveMediaInfo request) + public Task<object> Get(GetPlaybackInfo request) { - var item = _libraryManager.GetItemById(request.Id); - IEnumerable<MediaSourceInfo> mediaSources; + return GetPlaybackInfo(request.Id, request.UserId); + } - var channelItem = item as IChannelMediaItem; - var user = _userManager.GetUserById(request.UserId); + public Task<object> Get(GetLiveMediaInfo request) + { + return GetPlaybackInfo(request.Id, request.UserId); + } - if (channelItem != null) + private async Task<object> GetPlaybackInfo(string id, string userId) + { + IEnumerable<MediaSourceInfo> mediaSources; + var result = new LiveMediaInfoResult(); + + try { - mediaSources = await _channelManager.GetChannelItemMediaSources(request.Id, true, CancellationToken.None) - .ConfigureAwait(false); + mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false); } - else + catch (PlaybackException ex) { - var hasMediaSources = (IHasMediaSources)item; - - if (user == null) - { - mediaSources = hasMediaSources.GetMediaSources(true); - } - else - { - mediaSources = hasMediaSources.GetMediaSources(true, user); - } + mediaSources = new List<MediaSourceInfo>(); + result.ErrorCode = ex.ErrorCode; } - return ToOptimizedResult(new LiveMediaInfoResult - { - MediaSources = mediaSources.ToList() - }); + result.MediaSources = mediaSources.ToList(); + + return ToOptimizedResult(result); } } } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 37155b8f9..f3193c954 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -1,8 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; @@ -32,7 +32,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> public class AudioService : BaseProgressiveStreamingService { - public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient) + public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient, imageProcessor, httpClient) { } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 9dbe3389e..baf6ab653 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -1,8 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; @@ -15,7 +15,6 @@ using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -29,7 +28,7 @@ namespace MediaBrowser.Api.Playback.Progressive protected readonly IImageProcessor ImageProcessor; protected readonly IHttpClient HttpClient; - protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager) + protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient) { ImageProcessor = imageProcessor; HttpClient = httpClient; @@ -153,49 +152,12 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { - var job = string.IsNullOrEmpty(request.TranscodingJobId) ? - null : - ApiEntryPoint.Instance.GetTranscodingJob(request.TranscodingJobId); - - var limits = new List<long>(); - if (state.InputBitrate.HasValue) - { - // Bytes per second - limits.Add((state.InputBitrate.Value / 8)); - } - if (state.InputFileSize.HasValue && state.RunTimeTicks.HasValue) - { - var totalSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds; - - if (totalSeconds > 1) - { - var timeBasedLimit = state.InputFileSize.Value / totalSeconds; - limits.Add(Convert.ToInt64(timeBasedLimit)); - } - } - - // Take the greater of the above to methods, just to be safe - var throttleLimit = limits.Count > 0 ? limits.First() : 0; - - // Pad to play it safe - var bytesPerSecond = Convert.ToInt64(1.05 * throttleLimit); - - // Don't even start evaluating this until at least two minutes have content have been consumed - var targetGap = throttleLimit * 120; - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, - Path = state.MediaPath, - Throttle = request.Throttle, - - ThrottleLimit = bytesPerSecond, - - MinThrottlePosition = targetGap, - - ThrottleCallback = (l1, l2) => ThrottleCallack(l1, l2, bytesPerSecond, job) + Path = state.MediaPath }); } } @@ -234,67 +196,6 @@ namespace MediaBrowser.Api.Playback.Progressive } } - private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(3).Ticks; - - private long ThrottleCallack(long currentBytesPerSecond, long bytesWritten, long originalBytesPerSecond, TranscodingJob job) - { - var bytesDownloaded = job.BytesDownloaded ?? 0; - var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; - var downloadPositionTicks = job.DownloadPositionTicks ?? 0; - - var path = job.Path; - - if (bytesDownloaded > 0 && transcodingPositionTicks > 0) - { - // Progressive Streaming - byte-based consideration - - try - { - var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length; - - // Estimate the bytes the transcoder should be ahead - double gapFactor = _gapLengthInTicks; - gapFactor /= transcodingPositionTicks; - var targetGap = bytesTranscoded * gapFactor; - - var gap = bytesTranscoded - bytesDownloaded; - - if (gap < targetGap) - { - //Logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); - return 0; - } - - //Logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); - } - catch - { - //Logger.Error("Error getting output size"); - } - } - else if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) - { - // HLS - time-based consideration - - var targetGap = _gapLengthInTicks; - var gap = transcodingPositionTicks - downloadPositionTicks; - - if (gap < targetGap) - { - //Logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); - return 0; - } - - //Logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); - } - else - { - //Logger.Debug("No throttle data for " + path); - } - - return originalBytesPerSecond; - } - /// <summary> /// Gets the static remote stream result. /// </summary> diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 7e86b867f..e7b90c820 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -1,8 +1,8 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Diagnostics; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; @@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> public class VideoService : BaseProgressiveStreamingService { - public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient) + public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, subtitleEncoder, deviceManager, processManager, mediaSourceManager, zipClient, imageProcessor, httpClient) { } diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index dc22fc754..b52260b50 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -71,8 +71,8 @@ namespace MediaBrowser.Api.Playback public string Params { get; set; } public string ClientTime { get; set; } + public string StreamId { get; set; } - public bool Throttle { get; set; } public string TranscodingJobId { get; set; } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 40e765f1a..1d4dd1aaf 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,17 +1,16 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; -using MediaBrowser.Model.Net; namespace MediaBrowser.Api.Playback { @@ -23,6 +22,7 @@ namespace MediaBrowser.Api.Playback public string RequestedUrl { get; set; } public StreamRequest Request { get; set; } + public TranscodingThrottler TranscodingThrottler { get; set; } public VideoStreamRequest VideoRequest { @@ -52,10 +52,12 @@ namespace MediaBrowser.Api.Playback public IIsoMount IsoMount { get; set; } public string MediaPath { get; set; } + public string WaitForPath { get; set; } public MediaProtocol InputProtocol { get; set; } public bool IsInputVideo { get; set; } + public bool IsInputArchive { get; set; } public VideoType VideoType { get; set; } public IsoType? IsoType { get; set; } @@ -64,8 +66,8 @@ namespace MediaBrowser.Api.Playback public string LiveTvStreamId { get; set; } - public int SegmentLength = 6; - + public int SegmentLength = 3; + public bool EnableGenericHlsSegmenter = false; public int HlsListSize { get @@ -112,6 +114,7 @@ namespace MediaBrowser.Api.Playback public long? EncodingDurationTicks { get; set; } public string ItemType { get; set; } + public string ItemId { get; set; } public string GetMimeType(string outputPath) { @@ -125,6 +128,7 @@ namespace MediaBrowser.Api.Playback public void Dispose() { + DisposeTranscodingThrottler(); DisposeLiveStream(); DisposeLogStream(); DisposeIsoMount(); @@ -147,6 +151,23 @@ namespace MediaBrowser.Api.Playback } } + private void DisposeTranscodingThrottler() + { + if (TranscodingThrottler != null) + { + try + { + TranscodingThrottler.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing TranscodingThrottler", ex); + } + + TranscodingThrottler = null; + } + } + private void DisposeIsoMount() { if (IsoMount != null) diff --git a/MediaBrowser.Api/Playback/TranscodingThrottler.cs b/MediaBrowser.Api/Playback/TranscodingThrottler.cs new file mode 100644 index 000000000..943a4d065 --- /dev/null +++ b/MediaBrowser.Api/Playback/TranscodingThrottler.cs @@ -0,0 +1,159 @@ +using MediaBrowser.Controller.Diagnostics; +using MediaBrowser.Model.Logging; +using System; +using System.IO; +using System.Threading; + +namespace MediaBrowser.Api.Playback +{ + public class TranscodingThrottler : IDisposable + { + private readonly TranscodingJob _job; + private readonly ILogger _logger; + private readonly IProcessManager _processManager; + private Timer _timer; + private bool _isPaused; + + private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(2).Ticks; + + public TranscodingThrottler(TranscodingJob job, ILogger logger, IProcessManager processManager) + { + _job = job; + _logger = logger; + _processManager = processManager; + } + + public void Start() + { + _timer = new Timer(TimerCallback, null, 5000, 5000); + } + + private void TimerCallback(object state) + { + if (_job.HasExited) + { + DisposeTimer(); + return; + } + + if (IsThrottleAllowed(_job)) + { + PauseTranscoding(); + } + else + { + UnpauseTranscoding(); + } + } + + private void PauseTranscoding() + { + if (!_isPaused) + { + _logger.Debug("Sending pause command to ffmpeg"); + + try + { + _job.Process.StandardInput.Write("c"); + _isPaused = true; + } + catch (Exception ex) + { + _logger.ErrorException("Error pausing transcoding", ex); + } + } + } + + private void UnpauseTranscoding() + { + if (_isPaused) + { + _logger.Debug("Sending unpause command to ffmpeg"); + + try + { + _job.Process.StandardInput.WriteLine(); + _isPaused = false; + } + catch (Exception ex) + { + _logger.ErrorException("Error unpausing transcoding", ex); + } + } + } + + private bool IsThrottleAllowed(TranscodingJob job) + { + var bytesDownloaded = job.BytesDownloaded ?? 0; + var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; + var downloadPositionTicks = job.DownloadPositionTicks ?? 0; + + var path = job.Path; + + if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) + { + // HLS - time-based consideration + + var targetGap = _gapLengthInTicks; + var gap = transcodingPositionTicks - downloadPositionTicks; + + if (gap < targetGap) + { + //_logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); + return false; + } + + //_logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); + return true; + } + + if (bytesDownloaded > 0 && transcodingPositionTicks > 0) + { + // Progressive Streaming - byte-based consideration + + try + { + var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length; + + // Estimate the bytes the transcoder should be ahead + double gapFactor = _gapLengthInTicks; + gapFactor /= transcodingPositionTicks; + var targetGap = bytesTranscoded * gapFactor; + + var gap = bytesTranscoded - bytesDownloaded; + + if (gap < targetGap) + { + //_logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return false; + } + + //_logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return true; + } + catch + { + //_logger.Error("Error getting output size"); + return false; + } + } + + //_logger.Debug("No throttle data for " + path); + return false; + } + + public void Dispose() + { + DisposeTimer(); + } + + private void DisposeTimer() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + } +} diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index f5c8935f8..4bd78f1f5 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -5,6 +5,7 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Registration; using MediaBrowser.Model.Serialization; using ServiceStack; using ServiceStack.Web; @@ -106,6 +107,14 @@ namespace MediaBrowser.Api public string Mb2Equivalent { get; set; } } + [Route("/Registrations/{Name}", "GET", Summary = "Gets registration status for a feature")] + [Authenticated] + public class GetRegistration : IReturn<RegistrationInfo> + { + [ApiMember(Name = "Name", Description = "Feature Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Name { get; set; } + } + /// <summary> /// Class PluginsService /// </summary> @@ -144,13 +153,26 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetRegistrationStatus request) + public async Task<object> Get(GetRegistrationStatus request) { - var result = _securityManager.GetRegistrationStatus(request.Name, request.Mb2Equivalent).Result; + var result = await _securityManager.GetRegistrationStatus(request.Name, request.Mb2Equivalent).ConfigureAwait(false); return ToOptimizedResult(result); } + public async Task<object> Get(GetRegistration request) + { + var result = await _securityManager.GetRegistrationStatus(request.Name).ConfigureAwait(false); + + return ToOptimizedResult(new RegistrationInfo + { + ExpirationDate = result.ExpirationDate, + IsRegistered = result.IsRegistered, + IsTrial = result.TrialVersion, + Name = request.Name + }); + } + /// <summary> /// Gets the specified request. /// </summary> @@ -178,7 +200,7 @@ namespace MediaBrowser.Api } catch { - + } return ToOptimizedSerializedResultUsingCache(result); diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs index e3722b4a7..949dac926 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs @@ -3,7 +3,6 @@ using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Tasks; using ServiceStack; -using ServiceStack.Text.Controller; using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index ee48946d5..2cca72593 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -211,7 +211,7 @@ namespace MediaBrowser.Api result.SongCount = album.Tracks.Count(); result.Artists = album.Artists.ToArray(); - result.AlbumArtist = album.AlbumArtists.FirstOrDefault(); + result.AlbumArtist = album.AlbumArtist; } var song = item as Audio; diff --git a/MediaBrowser.Api/Session/SessionsService.cs b/MediaBrowser.Api/Session/SessionsService.cs index 319b3d28c..52ecb95ec 100644 --- a/MediaBrowser.Api/Session/SessionsService.cs +++ b/MediaBrowser.Api/Session/SessionsService.cs @@ -418,7 +418,7 @@ namespace MediaBrowser.Api.Session SeekPositionTicks = request.SeekPositionTicks }; - var task = _sessionManager.SendPlaystateCommand(GetSession().Id, request.Id, command, CancellationToken.None); + var task = _sessionManager.SendPlaystateCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -436,7 +436,7 @@ namespace MediaBrowser.Api.Session ItemType = request.ItemType }; - var task = _sessionManager.SendBrowseCommand(GetSession().Id, request.Id, command, CancellationToken.None); + var task = _sessionManager.SendBrowseCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -455,7 +455,7 @@ namespace MediaBrowser.Api.Session name = commandType.ToString(); } - var currentSession = GetSession(); + var currentSession = GetSession().Result; var command = new GeneralCommand { @@ -481,7 +481,7 @@ namespace MediaBrowser.Api.Session Text = request.Text }; - var task = _sessionManager.SendMessageCommand(GetSession().Id, request.Id, command, CancellationToken.None); + var task = _sessionManager.SendMessageCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } @@ -500,14 +500,14 @@ namespace MediaBrowser.Api.Session StartPositionTicks = request.StartPositionTicks }; - var task = _sessionManager.SendPlayCommand(GetSession().Id, request.Id, command, CancellationToken.None); + var task = _sessionManager.SendPlayCommand(GetSession().Result.Id, request.Id, command, CancellationToken.None); Task.WaitAll(task); } public void Post(SendGeneralCommand request) { - var currentSession = GetSession(); + var currentSession = GetSession().Result; var command = new GeneralCommand { @@ -522,7 +522,7 @@ namespace MediaBrowser.Api.Session public void Post(SendFullGeneralCommand request) { - var currentSession = GetSession(); + var currentSession = GetSession().Result; request.ControllingUserId = currentSession.UserId.HasValue ? currentSession.UserId.Value.ToString("N") : null; @@ -545,7 +545,7 @@ namespace MediaBrowser.Api.Session { if (string.IsNullOrWhiteSpace(request.Id)) { - request.Id = GetSession().Id; + request.Id = GetSession().Result.Id; } _sessionManager.ReportCapabilities(request.Id, new ClientCapabilities { @@ -569,7 +569,7 @@ namespace MediaBrowser.Api.Session { if (string.IsNullOrWhiteSpace(request.Id)) { - request.Id = GetSession().Id; + request.Id = GetSession().Result.Id; } _sessionManager.ReportCapabilities(request.Id, request); } diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index bf5c04540..4655f2c6f 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -65,6 +65,7 @@ namespace MediaBrowser.Api _config.Configuration.MergeMetadataAndImagesByName = true; _config.Configuration.EnableStandaloneMetadata = true; _config.Configuration.EnableLibraryMetadataSubFolder = true; + _config.Configuration.EnableUserSpecificUserViews = true; _config.SaveConfiguration(); } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 32e6ba076..07eb74e81 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -124,20 +124,23 @@ namespace MediaBrowser.Api.Subtitles private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IProviderManager _providerManager; - public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder) + public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, ISubtitleEncoder subtitleEncoder, IMediaSourceManager mediaSourceManager, IProviderManager providerManager) { _libraryManager = libraryManager; _subtitleManager = subtitleManager; _subtitleEncoder = subtitleEncoder; + _mediaSourceManager = mediaSourceManager; + _providerManager = providerManager; } public object Get(GetSubtitlePlaylist request) { var item = (Video)_libraryManager.GetItemById(new Guid(request.Id)); - var mediaSource = item.GetMediaSources(false) - .First(i => string.Equals(i.Id, request.MediaSourceId ?? request.Id)); + var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false); var builder = new StringBuilder(); @@ -255,7 +258,7 @@ namespace MediaBrowser.Api.Subtitles await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None) .ConfigureAwait(false); - await video.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService()), CancellationToken.None).ConfigureAwait(false); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions()); } catch (Exception ex) { diff --git a/MediaBrowser.Api/Sync/SyncHelper.cs b/MediaBrowser.Api/Sync/SyncHelper.cs new file mode 100644 index 000000000..6a5b6a927 --- /dev/null +++ b/MediaBrowser.Api/Sync/SyncHelper.cs @@ -0,0 +1,78 @@ +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace MediaBrowser.Api.Sync +{ + public static class SyncHelper + { + public static List<SyncJobOption> GetSyncOptions(List<BaseItemDto> items) + { + List<SyncJobOption> options = new List<SyncJobOption>(); + + if (items.Count > 1) + { + options.Add(SyncJobOption.Name); + } + + foreach (BaseItemDto item in items) + { + if (item.SupportsSync ?? false) + { + if (item.IsVideo) + { + options.Add(SyncJobOption.Quality); + options.Add(SyncJobOption.Profile); + if (items.Count > 1) + { + options.Add(SyncJobOption.UnwatchedOnly); + } + break; + } + if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre) + { + options.Add(SyncJobOption.Quality); + options.Add(SyncJobOption.Profile); + options.Add(SyncJobOption.UnwatchedOnly); + break; + } + if (item.IsGenre) + { + options.Add(SyncJobOption.SyncNewContent); + options.Add(SyncJobOption.ItemLimit); + break; + } + } + } + + foreach (BaseItemDto item in items) + { + if (item.SupportsSync ?? false) + { + if (item.IsFolder || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson) + { + options.Add(SyncJobOption.SyncNewContent); + options.Add(SyncJobOption.ItemLimit); + break; + } + } + } + + return options; + } + + public static List<SyncJobOption> GetSyncOptions(SyncCategory category) + { + List<SyncJobOption> options = new List<SyncJobOption>(); + + options.Add(SyncJobOption.Name); + options.Add(SyncJobOption.Quality); + options.Add(SyncJobOption.Profile); + options.Add(SyncJobOption.UnwatchedOnly); + options.Add(SyncJobOption.SyncNewContent); + options.Add(SyncJobOption.ItemLimit); + + return options; + } + } +} diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index 3f57ca2a0..bcadb4c08 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -94,6 +94,9 @@ namespace MediaBrowser.Api.Sync [ApiMember(Name = "ParentId", Description = "ParentId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string ParentId { get; set; } + [ApiMember(Name = "TargetId", Description = "TargetId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string TargetId { get; set; } + [ApiMember(Name = "Category", Description = "Category", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public SyncCategory? Category { get; set; } } @@ -226,6 +229,21 @@ namespace MediaBrowser.Api.Sync result.Targets = _syncManager.GetSyncTargets(request.UserId) .ToList(); + if (!string.IsNullOrWhiteSpace(request.TargetId)) + { + result.Targets = result.Targets + .Where(i => string.Equals(i.Id, request.TargetId, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + result.QualityOptions = _syncManager + .GetQualityOptions(request.TargetId) + .ToList(); + + result.ProfileOptions = _syncManager + .GetProfileOptions(request.TargetId) + .ToList(); + } + if (request.Category.HasValue) { result.Options = SyncHelper.GetSyncOptions(request.Category.Value); diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 9b5ef3a98..a734581f5 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -39,6 +39,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Person { get; set; } + [ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string PersonIds { get; set; } + /// <summary> /// If the Person filter is used, this can also be used to restrict to a specific person type /// </summary> @@ -46,9 +49,6 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string PersonTypes { get; set; } - [ApiMember(Name = "AllGenres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string AllGenres { get; set; } - /// <summary> /// Limit results to items containing specific studios /// </summary> @@ -56,6 +56,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Studios { get; set; } + [ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string StudioIds { get; set; } + /// <summary> /// Gets or sets the studios. /// </summary> @@ -63,6 +66,9 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Artists { get; set; } + [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ArtistIds { get; set; } + [ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Albums { get; set; } @@ -226,14 +232,14 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? CollapseBoxSetItems { get; set; } - public string[] GetAllGenres() + public string[] GetStudios() { - return (AllGenres ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } - public string[] GetStudios() + public string[] GetStudioIds() { - return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + return (StudioIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } public string[] GetPersonTypes() @@ -241,7 +247,12 @@ namespace MediaBrowser.Api.UserLibrary return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } - public IEnumerable<VideoType> GetVideoTypes() + public string[] GetPersonIds() + { + return (PersonIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public VideoType[] GetVideoTypes() { var val = VideoTypes; @@ -250,7 +261,7 @@ namespace MediaBrowser.Api.UserLibrary return new VideoType[] { }; } - return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)); + return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray(); } } @@ -471,9 +482,10 @@ namespace MediaBrowser.Api.UserLibrary Tags = request.GetTags(), OfficialRatings = request.GetOfficialRatings(), Genres = request.GetGenres(), - AllGenres = request.GetAllGenres(), Studios = request.GetStudios(), + StudioIds = request.GetStudioIds(), Person = request.Person, + PersonIds = request.GetPersonIds(), PersonTypes = request.GetPersonTypes(), Years = request.GetYears(), ImageTypes = request.GetImageTypes().ToArray(), @@ -609,6 +621,8 @@ namespace MediaBrowser.Api.UserLibrary private bool ApplyAdditionalFilters(GetItems request, BaseItem i, User user, bool isPreFiltered, ILibraryManager libraryManager) { + var video = i as Video; + if (!isPreFiltered) { var mediaTypes = request.GetMediaTypes(); @@ -656,7 +670,6 @@ namespace MediaBrowser.Api.UserLibrary if (request.Is3D.HasValue) { var val = request.Is3D.Value; - var video = i as Video; if (video == null || val != video.Video3DFormat.HasValue) { @@ -667,7 +680,6 @@ namespace MediaBrowser.Api.UserLibrary if (request.IsHD.HasValue) { var val = request.IsHD.Value; - var video = i as Video; if (video == null || val != video.IsHD) { @@ -809,8 +821,6 @@ namespace MediaBrowser.Api.UserLibrary { var val = request.HasSubtitles.Value; - var video = i as Video; - if (video == null || val != video.HasSubtitles) { return false; @@ -930,23 +940,11 @@ namespace MediaBrowser.Api.UserLibrary return false; } - // Apply genre filter - var allGenres = request.GetAllGenres(); - if (allGenres.Length > 0 && !allGenres.All(v => i.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) - { - return false; - } - // Filter by VideoType - if (!string.IsNullOrEmpty(request.VideoTypes)) + var videoTypes = request.GetVideoTypes(); + if (videoTypes.Length > 0 && (video == null || !videoTypes.Contains(video.VideoType))) { - var types = request.VideoTypes.Split(','); - - var video = i as Video; - if (video == null || !types.Contains(video.VideoType.ToString(), StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } var imageTypes = request.GetImageTypes().ToList(); @@ -965,11 +963,37 @@ namespace MediaBrowser.Api.UserLibrary return false; } + // Apply studio filter + var studioIds = request.GetStudioIds(); + if (studioIds.Length > 0 && !studioIds.Any(id => + { + var studioItem = libraryManager.GetItemById(id); + return studioItem != null && i.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + // Apply year filter var years = request.GetYears(); if (years.Length > 0 && !(i.ProductionYear.HasValue && years.Contains(i.ProductionYear.Value))) { return false; + } + + // Apply person filter + var personIds = request.GetPersonIds(); + if (personIds.Length > 0) + { + var names = personIds + .Select(libraryManager.GetItemById) + .Select(p => p == null ? "-1" : p.Name) + .ToList(); + + if (!(names.Any(v => i.People.Select(p => p.Name).Contains(v, StringComparer.OrdinalIgnoreCase)))) + { + return false; + } } // Apply person filter @@ -1031,13 +1055,30 @@ namespace MediaBrowser.Api.UserLibrary } // Artists + if (!string.IsNullOrEmpty(request.ArtistIds)) + { + var artistIds = request.ArtistIds.Split('|'); + + var audio = i as IHasArtist; + + if (!(audio != null && artistIds.Any(id => + { + var artistItem = libraryManager.GetItemById(id); + return artistItem != null && audio.HasAnyArtist(artistItem.Name); + }))) + { + return false; + } + } + + // Artists if (!string.IsNullOrEmpty(request.Artists)) { var artists = request.Artists.Split('|'); var audio = i as IHasArtist; - if (!(audio != null && artists.Any(audio.HasArtist))) + if (!(audio != null && artists.Any(audio.HasAnyArtist))) { return false; } diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index fae83e369..55e1681e0 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -231,7 +231,7 @@ namespace MediaBrowser.Api.UserLibrary datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); } - var session = GetSession(); + var session = await GetSession().ConfigureAwait(false); var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false); @@ -266,7 +266,7 @@ namespace MediaBrowser.Api.UserLibrary public void Post(ReportPlaybackStart request) { - request.SessionId = GetSession().Id; + request.SessionId = GetSession().Result.Id; var task = _sessionManager.OnPlaybackStart(request); @@ -294,7 +294,7 @@ namespace MediaBrowser.Api.UserLibrary public void Post(ReportPlaybackProgress request) { - request.SessionId = GetSession().Id; + request.SessionId = GetSession().Result.Id; var task = _sessionManager.OnPlaybackProgress(request); @@ -317,7 +317,7 @@ namespace MediaBrowser.Api.UserLibrary public void Post(ReportPlaybackStopped request) { - request.SessionId = GetSession().Id; + request.SessionId = GetSession().Result.Id; var task = _sessionManager.OnPlaybackStopped(request); @@ -339,7 +339,7 @@ namespace MediaBrowser.Api.UserLibrary { var user = _userManager.GetUserById(request.UserId); - var session = GetSession(); + var session = await GetSession().ConfigureAwait(false); var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false); diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index c17f33348..3996a0311 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -253,18 +253,14 @@ namespace MediaBrowser.Api /// The _user manager /// </summary> private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; private readonly ISessionManager _sessionMananger; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; private readonly IDeviceManager _deviceManager; - public IAuthorizationContext AuthorizationContext { get; set; } - - public UserService(IUserManager userManager, IDtoService dtoService, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager) + public UserService(IUserManager userManager, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager) { _userManager = userManager; - _dtoService = dtoService; _sessionMananger = sessionMananger; _config = config; _networkManager = networkManager; @@ -464,7 +460,7 @@ namespace MediaBrowser.Api public async Task PostAsync(UpdateUserPassword request) { - AssertCanUpdateUser(request.Id); + AssertCanUpdateUser(_userManager, request.Id); var user = _userManager.GetUserById(request.Id); @@ -498,7 +494,7 @@ namespace MediaBrowser.Api public async Task PostAsync(UpdateUserEasyPassword request) { - AssertCanUpdateUser(request.Id); + AssertCanUpdateUser(_userManager, request.Id); var user = _userManager.GetUserById(request.Id); @@ -534,7 +530,7 @@ namespace MediaBrowser.Api // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs var id = GetPathValue(1); - AssertCanUpdateUser(id); + AssertCanUpdateUser(_userManager, id); var dtoUser = request; @@ -584,29 +580,13 @@ namespace MediaBrowser.Api public void Post(UpdateUserConfiguration request) { - AssertCanUpdateUser(request.Id); + AssertCanUpdateUser(_userManager, request.Id); var task = _userManager.UpdateConfiguration(request.Id, request); Task.WaitAll(task); } - private void AssertCanUpdateUser(string userId) - { - var auth = AuthorizationContext.GetAuthorizationInfo(Request); - - // If they're going to update the record of another user, they must be an administrator - if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase)) - { - var authenticatedUser = _userManager.GetUserById(auth.UserId); - - if (!authenticatedUser.Policy.IsAdministrator) - { - throw new SecurityException("Unauthorized access."); - } - } - } - public void Post(UpdateUserPolicy request) { var task = UpdateUserPolicy(request); |
