diff options
| author | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
|---|---|---|
| committer | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
| commit | 54eb81395ef8d3d4cb064b56361ce94fc72b38b5 (patch) | |
| tree | 73240b556055557b0ae034ef5d5ba60cb5cb051e /MediaBrowser.Controller | |
| parent | 7f1fec688cc1a6f7f69fa5b059af01cf9c456d3f (diff) | |
| parent | 4786901bb796c3e912f13b686571fde8d16f49c5 (diff) | |
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
Diffstat (limited to 'MediaBrowser.Controller')
32 files changed, 1208 insertions, 513 deletions
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index 8eb27888a..c8b432ecb 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -95,12 +95,5 @@ namespace MediaBrowser.Controller.Channels /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The item media sources.</returns> IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken); - - /// <summary> - /// Whether the item supports media probe. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>Whether media probe should be enabled.</returns> - bool EnableMediaProbe(BaseItem item); } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 8362db1a7..eb181dcc4 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -59,9 +59,8 @@ namespace MediaBrowser.Controller.Devices /// Gets the devices. /// </summary> /// <param name="userId">The user's id, or <c>null</c>.</param> - /// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param> /// <returns>IEnumerable<DeviceInfo>.</returns> - Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync); + Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId); Task DeleteDevice(Device device); diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index d789033f1..b225f22df 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -184,7 +185,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 18d948a62..11cdf8444 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.Audio public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { [JsonIgnore] - public bool IsAccessedByName => ParentId.Equals(default); + public bool IsAccessedByName => ParentId.IsEmpty(); [JsonIgnore] public override bool IsFolder => !IsAccessedByName; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 98485f9a8..ddcc994a0 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -240,7 +240,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (!ChannelId.Equals(default)) + if (!ChannelId.IsEmpty()) { return SourceType.Channel; } @@ -530,7 +530,7 @@ namespace MediaBrowser.Controller.Entities get { var id = DisplayParentId; - if (id.Equals(default)) + if (id.IsEmpty()) { return null; } @@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities if (this is IHasCollectionType view) { - if (view.CollectionType == CollectionType.LiveTv) + if (view.CollectionType == CollectionType.livetv) { return true; } @@ -746,7 +746,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool StopRefreshIfLocalMetadataFound => true; [JsonIgnore] - protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol; + protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol; [JsonIgnore] public virtual bool SupportsPeople => false; @@ -773,8 +773,6 @@ namespace MediaBrowser.Controller.Entities /// <value>The remote trailers.</value> public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } - public virtual bool SupportsExternalTransfer => false; - public virtual double GetDefaultPrimaryImageAspectRatio() { return 0; @@ -825,7 +823,7 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetOwner() { var ownerId = OwnerId; - return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId); + return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId); } public bool CanDelete(User user, List<Folder> allCollectionFolders) @@ -970,7 +968,7 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetParent() { var parentId = ParentId; - if (parentId.Equals(default)) + if (parentId.IsEmpty()) { return null; } @@ -1363,7 +1361,7 @@ namespace MediaBrowser.Controller.Entities var tasks = extras.Select(i => { var subOptions = new MetadataRefreshOptions(options); - if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default)) + if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty()) { i.OwnerId = ownerId; i.ParentId = Guid.Empty; @@ -1675,7 +1673,7 @@ namespace MediaBrowser.Controller.Entities // First get using the cached Id if (info.ItemId.HasValue) { - if (info.ItemId.Value.Equals(default)) + if (info.ItemId.Value.IsEmpty()) { return null; } @@ -2441,7 +2439,7 @@ namespace MediaBrowser.Controller.Entities return Task.FromResult(true); } - if (video.OwnerId.Equals(default)) + if (video.OwnerId.IsEmpty()) { video.OwnerId = this.Id; } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e707eedbf..74eb089de 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -198,7 +199,7 @@ namespace MediaBrowser.Controller.Entities { item.SetParent(this); - if (item.Id.Equals(default)) + if (item.Id.IsEmpty()) { item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); } @@ -697,7 +698,7 @@ namespace MediaBrowser.Controller.Entities if (this is not UserRootFolder && this is not AggregateFolder - && query.ParentId.Equals(default)) + && query.ParentId.IsEmpty()) { query.Parent = this; } @@ -840,7 +841,7 @@ namespace MediaBrowser.Controller.Entities return true; } - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { Logger.LogDebug("Query requires post-filtering due to AdjacentTo"); return true; @@ -987,7 +988,7 @@ namespace MediaBrowser.Controller.Entities #pragma warning restore CA1309 // This must be the last filter - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index bf31508c1..37e241414 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -74,12 +75,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId.Equals(default)) + if (seriesId.IsEmpty()) { seriesId = FindSeriesId(); } - return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); + return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series); } } @@ -89,12 +90,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seasonId = SeasonId; - if (seasonId.Equals(default)) + if (seasonId.IsEmpty()) { seasonId = FindSeasonId(); } - return seasonId.Equals(default) ? null : (LibraryManager.GetItemById(seasonId) as Season); + return seasonId.IsEmpty() ? null : (LibraryManager.GetItemById(seasonId) as Season); } } @@ -271,7 +272,7 @@ namespace MediaBrowser.Controller.Entities.TV var seasonId = SeasonId; - if (!seasonId.Equals(default) && !list.Contains(seasonId)) + if (!seasonId.IsEmpty() && !list.Contains(seasonId)) { list.Add(seasonId); } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 0a040a3c2..c29cefc15 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -48,12 +49,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId.Equals(default)) + if (seriesId.IsEmpty()) { seriesId = FindSeriesId(); } - return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); + return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series); } } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 1f94cf767..c93488a85 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -19,19 +19,19 @@ namespace MediaBrowser.Controller.Entities { private static readonly CollectionType?[] _viewTypesEligibleForGrouping = { - Jellyfin.Data.Enums.CollectionType.Movies, - Jellyfin.Data.Enums.CollectionType.TvShows, + Jellyfin.Data.Enums.CollectionType.movies, + Jellyfin.Data.Enums.CollectionType.tvshows, null }; private static readonly CollectionType?[] _originalFolderViewTypes = { - Jellyfin.Data.Enums.CollectionType.Books, - Jellyfin.Data.Enums.CollectionType.MusicVideos, - Jellyfin.Data.Enums.CollectionType.HomeVideos, - Jellyfin.Data.Enums.CollectionType.Photos, - Jellyfin.Data.Enums.CollectionType.Music, - Jellyfin.Data.Enums.CollectionType.BoxSets + Jellyfin.Data.Enums.CollectionType.books, + Jellyfin.Data.Enums.CollectionType.musicvideos, + Jellyfin.Data.Enums.CollectionType.homevideos, + Jellyfin.Data.Enums.CollectionType.photos, + Jellyfin.Data.Enums.CollectionType.music, + Jellyfin.Data.Enums.CollectionType.boxsets }; public static ITVSeriesManager TVSeriesManager { get; set; } @@ -70,11 +70,11 @@ namespace MediaBrowser.Controller.Entities /// <inheritdoc /> public override IEnumerable<Guid> GetIdsForAncestorQuery() { - if (!DisplayParentId.Equals(default)) + if (!DisplayParentId.IsEmpty()) { yield return DisplayParentId; } - else if (!ParentId.Equals(default)) + else if (!ParentId.IsEmpty()) { yield return ParentId; } @@ -95,11 +95,11 @@ namespace MediaBrowser.Controller.Entities { var parent = this as Folder; - if (!DisplayParentId.Equals(default)) + if (!DisplayParentId.IsEmpty()) { parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent; } - else if (!ParentId.Equals(default)) + else if (!ParentId.IsEmpty()) { parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } @@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Entities return true; } - return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists; + return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists; } public static bool IsEligibleForGrouping(Folder folder) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 42431c832..4af000557 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -58,58 +58,58 @@ namespace MediaBrowser.Controller.Entities switch (viewType) { - case CollectionType.Folders: + case CollectionType.folders: return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query); - case CollectionType.TvShows: + case CollectionType.tvshows: return GetTvView(queryParent, user, query); - case CollectionType.Movies: + case CollectionType.movies: return GetMovieFolders(queryParent, user, query); - case CollectionType.TvShowSeries: + case CollectionType.tvshowseries: return GetTvSeries(queryParent, user, query); - case CollectionType.TvGenres: + case CollectionType.tvgenres: return GetTvGenres(queryParent, user, query); - case CollectionType.TvGenre: + case CollectionType.tvgenre: return GetTvGenreItems(queryParent, displayParent, user, query); - case CollectionType.TvResume: + case CollectionType.tvresume: return GetTvResume(queryParent, user, query); - case CollectionType.TvNextUp: + case CollectionType.tvnextup: return GetTvNextUp(queryParent, query); - case CollectionType.TvLatest: + case CollectionType.tvlatest: return GetTvLatest(queryParent, user, query); - case CollectionType.MovieFavorites: + case CollectionType.moviefavorites: return GetFavoriteMovies(queryParent, user, query); - case CollectionType.MovieLatest: + case CollectionType.movielatest: return GetMovieLatest(queryParent, user, query); - case CollectionType.MovieGenres: + case CollectionType.moviegenres: return GetMovieGenres(queryParent, user, query); - case CollectionType.MovieGenre: + case CollectionType.moviegenre: return GetMovieGenreItems(queryParent, displayParent, user, query); - case CollectionType.MovieResume: + case CollectionType.movieresume: return GetMovieResume(queryParent, user, query); - case CollectionType.MovieMovies: + case CollectionType.moviemovies: return GetMovieMovies(queryParent, user, query); - case CollectionType.MovieCollections: + case CollectionType.moviecollection: return GetMovieCollections(user, query); - case CollectionType.TvFavoriteEpisodes: + case CollectionType.tvfavoriteepisodes: return GetFavoriteEpisodes(queryParent, user, query); - case CollectionType.TvFavoriteSeries: + case CollectionType.tvfavoriteseries: return GetFavoriteSeries(queryParent, user, query); default: @@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent), - GetUserView(CollectionType.MovieLatest, "Latest", "1", parent), - GetUserView(CollectionType.MovieMovies, "Movies", "2", parent), - GetUserView(CollectionType.MovieCollections, "Collections", "3", parent), - GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent), - GetUserView(CollectionType.MovieGenres, "Genres", "5", parent) + GetUserView(CollectionType.movieresume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.movielatest, "Latest", "1", parent), + GetUserView(CollectionType.moviemovies, "Movies", "2", parent), + GetUserView(CollectionType.moviecollection, "Collections", "3", parent), + GetUserView(CollectionType.moviefavorites, "Favorites", "4", parent), + GetUserView(CollectionType.moviegenres, "Genres", "5", parent) }; return GetResult(list, query); @@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(CollectionType.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.moviegenre, i.SortName, parent)); return GetResult(genres, query); } @@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent), - GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent), - GetUserView(CollectionType.TvLatest, "Latest", "2", parent), - GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent), - GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), - GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), - GetUserView(CollectionType.TvGenres, "Genres", "6", parent) + GetUserView(CollectionType.tvresume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.tvnextup, "HeaderNextUp", "1", parent), + GetUserView(CollectionType.tvlatest, "Latest", "2", parent), + GetUserView(CollectionType.tvshowseries, "Shows", "3", parent), + GetUserView(CollectionType.tvfavoriteseries, "HeaderFavoriteShows", "4", parent), + GetUserView(CollectionType.tvfavoriteepisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(CollectionType.tvgenres, "Genres", "6", parent) }; return GetResult(list, query); @@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) { - var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows }); + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.tvshows }); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(CollectionType.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.tvgenre, i.SortName, parent)); return GetResult(genres, query); } @@ -433,7 +433,7 @@ namespace MediaBrowser.Controller.Entities var user = query.User; // This must be the last filter - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index be2eb4d28..5adadec39 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -456,7 +456,7 @@ namespace MediaBrowser.Controller.Entities foreach (var child in LinkedAlternateVersions) { // Reset the cached value - if (child.ItemId.HasValue && child.ItemId.Value.Equals(default)) + if (child.ItemId.IsNullOrEmpty()) { child.ItemId = null; } diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs index 4c44a17fd..bf64aca0f 100644 --- a/MediaBrowser.Controller/Library/ILiveStream.cs +++ b/MediaBrowser.Controller/Library/ILiveStream.cs @@ -2,6 +2,7 @@ #pragma warning disable CA1711, CS1591 +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Library { - public interface ILiveStream + public interface ILiveStream : IDisposable { int ConsumerCount { get; set; } diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index f1758a9d8..bace703ad 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -117,6 +118,14 @@ namespace MediaBrowser.Controller.Library public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId); /// <summary> + /// Gets the media sources for an active recording. + /// </summary> + /// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns> + Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); + + /// <summary> /// Closes the media source. /// </summary> /// <param name="id">The live stream identifier.</param> diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 034c40591..43cccfc65 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + /// <summary> + /// Save the provided user data for the given user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="item">The item.</param> + /// <param name="userDataDto">The reason for updating the user data.</param> + /// <param name="reason">The reason.</param> + void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason); + UserItemData GetUserData(User user, BaseItem item); UserItemData GetUserData(Guid userId, BaseItem item); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 3b6a16dee..26f9fe42d 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.LiveTv /// <value>The services.</value> IReadOnlyList<ILiveTvService> Services { get; } - IListingsProvider[] ListingProviders { get; } + IReadOnlyList<IListingsProvider> ListingProviders { get; } /// <summary> /// Gets the new timer defaults asynchronous. @@ -71,9 +71,8 @@ namespace MediaBrowser.Controller.LiveTv /// Adds the parts. /// </summary> /// <param name="services">The services.</param> - /// <param name="tunerHosts">The tuner hosts.</param> /// <param name="listingProviders">The listing providers.</param> - void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders); + void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders); /// <summary> /// Gets the timer. @@ -254,14 +253,6 @@ namespace MediaBrowser.Controller.LiveTv Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null); /// <summary> - /// Saves the tuner host. - /// </summary> - /// <param name="info">Turner host to save.</param> - /// <param name="dataSourceChanged">Option to specify that data source has changed.</param> - /// <returns>Tuner host information wrapped in a task.</returns> - Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); - - /// <summary> /// Saves the listing provider. /// </summary> /// <param name="info">The information.</param> @@ -298,10 +289,6 @@ namespace MediaBrowser.Controller.LiveTv Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); - List<NameIdPair> GetTunerHostTypes(); - - Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); - string GetEmbyTvActiveRecordingPath(string id); ActiveRecordingInfo GetActiveRecordingInfo(string path); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index ce34954e3..52fb15648 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -141,14 +141,6 @@ namespace MediaBrowser.Controller.LiveTv Task CloseLiveStream(string id, CancellationToken cancellationToken); /// <summary> - /// Records the live stream. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task RecordLiveStream(string id, CancellationToken cancellationToken); - - /// <summary> /// Resets the tuner. /// </summary> /// <param name="id">The identifier.</param> @@ -180,9 +172,4 @@ namespace MediaBrowser.Controller.LiveTv { Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); } - - public interface ISupportsUpdatingDefaults - { - Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken); - } } diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 24820abb9..3689a2adf 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -36,13 +36,6 @@ namespace MediaBrowser.Controller.LiveTv Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken); /// <summary> - /// Gets the tuner infos. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task<List<LiveTvTunerInfo>>.</returns> - Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken); - - /// <summary> /// Gets the channel stream. /// </summary> /// <param name="channelId">The channel identifier.</param> @@ -50,7 +43,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="currentLiveStreams">The current live streams.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>Live stream wrapped in a task.</returns> - Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); /// <summary> /// Gets the channel stream media sources. diff --git a/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs new file mode 100644 index 000000000..3df6066f6 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv; + +/// <summary> +/// Service responsible for managing the <see cref="ITunerHost"/>s. +/// </summary> +public interface ITunerHostManager +{ + /// <summary> + /// Gets the available <see cref="ITunerHost"/>s. + /// </summary> + IReadOnlyList<ITunerHost> TunerHosts { get; } + + /// <summary> + /// Gets the <see cref="NameIdPair"/>s for the available <see cref="ITunerHost"/>s. + /// </summary> + /// <returns>The <see cref="NameIdPair"/>s.</returns> + IEnumerable<NameIdPair> GetTunerHostTypes(); + + /// <summary> + /// Saves the tuner host. + /// </summary> + /// <param name="info">Turner host to save.</param> + /// <param name="dataSourceChanged">Option to specify that data source has changed.</param> + /// <returns>Tuner host information wrapped in a task.</returns> + Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); + + /// <summary> + /// Discovers the available tuners. + /// </summary> + /// <param name="newDevicesOnly">A value indicating whether to only return new devices.</param> + /// <returns>The <see cref="TunerHostInfo"/>s.</returns> + IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly); + + /// <summary> + /// Scans for tuner devices that have changed URLs. + /// </summary> + /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param> + /// <returns>A task that represents the scanning operation.</returns> + Task ScanForTunerDeviceChanges(CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs deleted file mode 100644 index eb3babc18..000000000 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ /dev/null @@ -1,54 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class LiveTvServiceStatusInfo - { - public LiveTvServiceStatusInfo() - { - Tuners = new List<LiveTvTunerInfo>(); - IsVisible = true; - } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public LiveTvServiceStatus Status { get; set; } - - /// <summary> - /// Gets or sets the status message. - /// </summary> - /// <value>The status message.</value> - public string StatusMessage { get; set; } - - /// <summary> - /// Gets or sets the version. - /// </summary> - /// <value>The version.</value> - public string Version { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has update available. - /// </summary> - /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> - public bool HasUpdateAvailable { get; set; } - - /// <summary> - /// Gets or sets the tuners. - /// </summary> - /// <value>The tuners.</value> - public List<LiveTvTunerInfo> Tuners { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is visible. - /// </summary> - /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value> - public bool IsVisible { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs deleted file mode 100644 index aa5eb59d1..000000000 --- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class LiveTvTunerInfo - { - public LiveTvTunerInfo() - { - Clients = new List<string>(); - } - - /// <summary> - /// Gets or sets the type of the source. - /// </summary> - /// <value>The type of the source.</value> - public string SourceType { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public LiveTvTunerStatus Status { get; set; } - - /// <summary> - /// Gets or sets the channel identifier. - /// </summary> - /// <value>The channel identifier.</value> - public string ChannelId { get; set; } - - /// <summary> - /// Gets or sets the recording identifier. - /// </summary> - /// <value>The recording identifier.</value> - public string RecordingId { get; set; } - - /// <summary> - /// Gets or sets the name of the program. - /// </summary> - /// <value>The name of the program.</value> - public string ProgramName { get; set; } - - /// <summary> - /// Gets or sets the clients. - /// </summary> - /// <value>The clients.</value> - public List<string> Clients { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance can reset. - /// </summary> - /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value> - public bool CanReset { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs deleted file mode 100644 index 1dcf7a58f..000000000 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ /dev/null @@ -1,210 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class RecordingInfo - { - public RecordingInfo() - { - Genres = new List<string>(); - } - - /// <summary> - /// Gets or sets the id of the recording. - /// </summary> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the series timer identifier. - /// </summary> - /// <value>The series timer identifier.</value> - public string SeriesTimerId { get; set; } - - /// <summary> - /// Gets or sets the timer identifier. - /// </summary> - /// <value>The timer identifier.</value> - public string TimerId { get; set; } - - /// <summary> - /// Gets or sets the channelId of the recording. - /// </summary> - public string ChannelId { get; set; } - - /// <summary> - /// Gets or sets the type of the channel. - /// </summary> - /// <value>The type of the channel.</value> - public ChannelType ChannelType { get; set; } - - /// <summary> - /// Gets or sets the name of the recording. - /// </summary> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - public string Path { get; set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - /// <summary> - /// Gets or sets the overview. - /// </summary> - /// <value>The overview.</value> - public string Overview { get; set; } - - /// <summary> - /// Gets or sets the start date of the recording, in UTC. - /// </summary> - public DateTime StartDate { get; set; } - - /// <summary> - /// Gets or sets the end date of the recording, in UTC. - /// </summary> - public DateTime EndDate { get; set; } - - /// <summary> - /// Gets or sets the program identifier. - /// </summary> - /// <value>The program identifier.</value> - public string ProgramId { get; set; } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public RecordingStatus Status { get; set; } - - /// <summary> - /// Gets or sets the genre of the program. - /// </summary> - public List<string> Genres { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is repeat. - /// </summary> - /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> - public bool IsRepeat { get; set; } - - /// <summary> - /// Gets or sets the episode title. - /// </summary> - /// <value>The episode title.</value> - public string EpisodeTitle { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is hd. - /// </summary> - /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> - public bool? IsHD { get; set; } - - /// <summary> - /// Gets or sets the audio. - /// </summary> - /// <value>The audio.</value> - public ProgramAudio? Audio { get; set; } - - /// <summary> - /// Gets or sets the original air date. - /// </summary> - /// <value>The original air date.</value> - public DateTime? OriginalAirDate { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is movie. - /// </summary> - /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> - public bool IsMovie { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is sports. - /// </summary> - /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - public bool IsSports { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is series. - /// </summary> - /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - public bool IsSeries { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is live. - /// </summary> - /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - public bool IsLive { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is news. - /// </summary> - /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - public bool IsNews { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is kids. - /// </summary> - /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - public bool IsKids { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is premiere. - /// </summary> - /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> - public bool IsPremiere { get; set; } - - /// <summary> - /// Gets or sets the official rating. - /// </summary> - /// <value>The official rating.</value> - public string OfficialRating { get; set; } - - /// <summary> - /// Gets or sets the community rating. - /// </summary> - /// <value>The community rating.</value> - public float? CommunityRating { get; set; } - - /// <summary> - /// Gets or sets the image path if it can be accessed directly from the file system. - /// </summary> - /// <value>The image path.</value> - public string ImagePath { get; set; } - - /// <summary> - /// Gets or sets the image url if it can be downloaded. - /// </summary> - /// <value>The image URL.</value> - public string ImageUrl { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has image. - /// </summary> - /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> - public bool? HasImage { get; set; } - - /// <summary> - /// Gets or sets the show identifier. - /// </summary> - /// <value>The show identifier.</value> - public string ShowId { get; set; } - - /// <summary> - /// Gets or sets the date last updated. - /// </summary> - /// <value>The date last updated.</value> - public DateTime DateLastUpdated { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs deleted file mode 100644 index 0b943c939..000000000 --- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class RecordingStatusChangedEventArgs : EventArgs - { - public string RecordingId { get; set; } - - public RecordingStatus NewStatus { get; set; } - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 46fd1ae47..400e7f40f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1068,7 +1068,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // hw transpose filters should be added manually. - args.Append(" -autorotate 0"); + args.Append(" -noautorotate"); return args.ToString().Trim(); } @@ -1159,7 +1159,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options)); if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4)) { - arg.Append(" -autoscale 0"); + arg.Append(" -noautoscale"); } return arg.ToString(); @@ -3343,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding // [0:s]scale=s=1280x720 var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } return (mainFilters, subFilters, overlayFilters); @@ -3520,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=cuda"); - overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0"); } } else @@ -3529,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3718,7 +3718,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=opencl"); - overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); overlayFilters.Add("format=d3d11"); } @@ -3729,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3964,7 +3964,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -3975,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4180,7 +4180,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4445,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayVaapiFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_vaapi=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayVaapiFilter); } @@ -4456,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { @@ -4616,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=vulkan"); subFilters.Add("format=vulkan"); - overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_vulkan=eof_action=pass:repeatlast=0"); if (isSwEncoder) { @@ -4817,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { diff --git a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs new file mode 100644 index 000000000..c19a12ae7 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Streaming; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// <summary> +/// A service for managing media transcoding. +/// </summary> +public interface ITranscodeManager +{ + /// <summary> + /// Get transcoding job. + /// </summary> + /// <param name="playSessionId">Playback session id.</param> + /// <returns>The transcoding job.</returns> + public TranscodingJob? GetTranscodingJob(string playSessionId); + + /// <summary> + /// Get transcoding job. + /// </summary> + /// <param name="path">Path to the transcoding file.</param> + /// <param name="type">The <see cref="TranscodingJobType"/>.</param> + /// <returns>The transcoding job.</returns> + public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type); + + /// <summary> + /// Ping transcoding job. + /// </summary> + /// <param name="playSessionId">Play session id.</param> + /// <param name="isUserPaused">Is user paused.</param> + /// <exception cref="ArgumentNullException">Play session id is null.</exception> + public void PingTranscodingJob(string playSessionId, bool? isUserPaused); + + /// <summary> + /// Kills the single transcoding job. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <param name="playSessionId">The play session identifier.</param> + /// <param name="deleteFiles">The delete files.</param> + /// <returns>Task.</returns> + public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles); + + /// <summary> + /// Report the transcoding progress to the session manager. + /// </summary> + /// <param name="job">The <see cref="TranscodingJob"/> of which the progress will be reported.</param> + /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param> + /// <param name="transcodingPosition">The current transcoding position.</param> + /// <param name="framerate">The framerate of the transcoding job.</param> + /// <param name="percentComplete">The completion percentage of the transcode.</param> + /// <param name="bytesTranscoded">The number of bytes transcoded.</param> + /// <param name="bitRate">The bitrate of the transcoding job.</param> + public void ReportTranscodingProgress( + TranscodingJob job, + StreamState state, + TimeSpan? transcodingPosition, + float? framerate, + double? percentComplete, + long? bytesTranscoded, + int? bitRate); + + /// <summary> + /// Starts FFMpeg. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="outputPath">The output path.</param> + /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param> + /// <param name="userId">The user id.</param> + /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> + /// <param name="cancellationTokenSource">The cancellation token source.</param> + /// <param name="workingDirectory">The working directory.</param> + /// <returns>Task.</returns> + public Task<TranscodingJob> StartFfMpeg( + StreamState state, + string outputPath, + string commandLineArguments, + Guid userId, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource, + string? workingDirectory = null); + + /// <summary> + /// Called when [transcode begin request]. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="type">The type.</param> + /// <returns>The <see cref="TranscodingJob"/>.</returns> + public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type); + + /// <summary> + /// Called when [transcode end]. + /// </summary> + /// <param name="job">The transcode job.</param> + public void OnTranscodeEndRequest(TranscodingJob job); + + /// <summary> + /// Gets the transcoding lock. + /// </summary> + /// <param name="outputPath">The output path of the transcoded file.</param> + /// <returns>A <see cref="SemaphoreSlim"/>.</returns> + public SemaphoreSlim GetTranscodingLock(string outputPath); +} diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs new file mode 100644 index 000000000..1e6d5933c --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -0,0 +1,280 @@ +using System; +using System.Diagnostics; +using System.Threading; +using MediaBrowser.Model.Dto; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// <summary> +/// Class TranscodingJob. +/// </summary> +public sealed class TranscodingJob : IDisposable +{ + private readonly ILogger<TranscodingJob> _logger; + private readonly object _processLock = new(); + private readonly object _timerLock = new(); + + private Timer? _killTimer; + + /// <summary> + /// Initializes a new instance of the <see cref="TranscodingJob"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param> + public TranscodingJob(ILogger<TranscodingJob> logger) + { + _logger = logger; + } + + /// <summary> + /// Gets or sets the play session identifier. + /// </summary> + public string? PlaySessionId { get; set; } + + /// <summary> + /// Gets or sets the live stream identifier. + /// </summary> + public string? LiveStreamId { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether is live output. + /// </summary> + public bool IsLiveOutput { get; set; } + + /// <summary> + /// Gets or sets the path. + /// </summary> + public MediaSourceInfo? MediaSource { get; set; } + + /// <summary> + /// Gets or sets path. + /// </summary> + public string? Path { get; set; } + + /// <summary> + /// Gets or sets the type. + /// </summary> + public TranscodingJobType Type { get; set; } + + /// <summary> + /// Gets or sets the process. + /// </summary> + public Process? Process { get; set; } + + /// <summary> + /// Gets or sets the active request count. + /// </summary> + public int ActiveRequestCount { get; set; } + + /// <summary> + /// Gets or sets device id. + /// </summary> + public string? DeviceId { get; set; } + + /// <summary> + /// Gets or sets cancellation token source. + /// </summary> + public CancellationTokenSource? CancellationTokenSource { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether has exited. + /// </summary> + public bool HasExited { get; set; } + + /// <summary> + /// Gets or sets exit code. + /// </summary> + public int ExitCode { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether is user paused. + /// </summary> + public bool IsUserPaused { get; set; } + + /// <summary> + /// Gets or sets id. + /// </summary> + public string? Id { get; set; } + + /// <summary> + /// Gets or sets framerate. + /// </summary> + public float? Framerate { get; set; } + + /// <summary> + /// Gets or sets completion percentage. + /// </summary> + public double? CompletionPercentage { get; set; } + + /// <summary> + /// Gets or sets bytes downloaded. + /// </summary> + public long BytesDownloaded { get; set; } + + /// <summary> + /// Gets or sets bytes transcoded. + /// </summary> + public long? BytesTranscoded { get; set; } + + /// <summary> + /// Gets or sets bit rate. + /// </summary> + public int? BitRate { get; set; } + + /// <summary> + /// Gets or sets transcoding position ticks. + /// </summary> + public long? TranscodingPositionTicks { get; set; } + + /// <summary> + /// Gets or sets download position ticks. + /// </summary> + public long? DownloadPositionTicks { get; set; } + + /// <summary> + /// Gets or sets transcoding throttler. + /// </summary> + public TranscodingThrottler? TranscodingThrottler { get; set; } + + /// <summary> + /// Gets or sets last ping date. + /// </summary> + public DateTime LastPingDate { get; set; } + + /// <summary> + /// Gets or sets ping timeout. + /// </summary> + public int PingTimeout { get; set; } + + /// <summary> + /// Stop kill timer. + /// </summary> + public void StopKillTimer() + { + lock (_timerLock) + { + _killTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + /// <summary> + /// Dispose kill timer. + /// </summary> + public void DisposeKillTimer() + { + lock (_timerLock) + { + if (_killTimer is not null) + { + _killTimer.Dispose(); + _killTimer = null; + } + } + } + + /// <summary> + /// Start kill timer. + /// </summary> + /// <param name="callback">Callback action.</param> + public void StartKillTimer(Action<object?> callback) + { + StartKillTimer(callback, PingTimeout); + } + + /// <summary> + /// Start kill timer. + /// </summary> + /// <param name="callback">Callback action.</param> + /// <param name="intervalMs">Callback interval.</param> + public void StartKillTimer(Action<object?> callback, int intervalMs) + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + if (_killTimer is null) + { + _logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); + } + else + { + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + /// <summary> + /// Change kill timer if started. + /// </summary> + public void ChangeKillTimerIfStarted() + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + if (_killTimer is not null) + { + var intervalMs = PingTimeout; + + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + /// <summary> + /// Stops the transcoding job. + /// </summary> + public void Stop() + { + lock (_processLock) + { +#pragma warning disable CA1849 // Can't await in lock block + TranscodingThrottler?.Stop().GetAwaiter().GetResult(); + + var process = Process; + + if (!HasExited) + { + try + { + _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", Path); + + process!.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous. + if (!process.WaitForExit(5000)) + { + _logger.LogInformation("Killing FFmpeg process for {Path}", Path); + process.Kill(); + } + } + catch (InvalidOperationException) + { + } + } +#pragma warning restore CA1849 + } + } + + /// <inheritdoc /> + public void Dispose() + { + Process?.Dispose(); + Process = null; + _killTimer?.Dispose(); + _killTimer = null; + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + TranscodingThrottler?.Dispose(); + TranscodingThrottler = null; + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs new file mode 100644 index 000000000..813f13eae --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs @@ -0,0 +1,218 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// <summary> +/// Transcoding throttler. +/// </summary> +public class TranscodingThrottler : IDisposable +{ + private readonly TranscodingJob _job; + private readonly ILogger<TranscodingThrottler> _logger; + private readonly IConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private Timer? _timer; + private bool _isPaused; + + /// <summary> + /// Initializes a new instance of the <see cref="TranscodingThrottler"/> class. + /// </summary> + /// <param name="job">Transcoding job dto.</param> + /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param> + /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> + public TranscodingThrottler(TranscodingJob job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + { + _job = job; + _logger = logger; + _config = config; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + } + + /// <summary> + /// Start timer. + /// </summary> + public void Start() + { + _timer = new Timer(TimerCallback, null, 5000, 5000); + } + + /// <summary> + /// Unpause transcoding. + /// </summary> + /// <returns>A <see cref="Task"/>.</returns> + public async Task UnpauseTranscoding() + { + if (_isPaused) + { + _logger.LogDebug("Sending resume command to ffmpeg"); + + try + { + var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine; + await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false); + _isPaused = false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resuming transcoding"); + } + } + } + + /// <summary> + /// Stop throttler. + /// </summary> + /// <returns>A <see cref="Task"/>.</returns> + public async Task Stop() + { + DisposeTimer(); + await UnpauseTranscoding().ConfigureAwait(false); + } + + /// <summary> + /// Dispose throttler. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Dispose throttler. + /// </summary> + /// <param name="disposing">Disposing.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposeTimer(); + } + } + + private EncodingOptions GetOptions() + { + return _config.GetEncodingOptions(); + } + + private async void TimerCallback(object? state) + { + if (_job.HasExited) + { + DisposeTimer(); + return; + } + + var options = GetOptions(); + + if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds)) + { + await PauseTranscoding().ConfigureAwait(false); + } + else + { + await UnpauseTranscoding().ConfigureAwait(false); + } + } + + private async Task PauseTranscoding() + { + if (!_isPaused) + { + var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c"; + + _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey); + + try + { + await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false); + _isPaused = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error pausing transcoding"); + } + } + } + + private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds) + { + var bytesDownloaded = job.BytesDownloaded; + var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; + var downloadPositionTicks = job.DownloadPositionTicks ?? 0; + + var path = job.Path ?? throw new ArgumentException("Path can't be null."); + + var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks; + + if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) + { + // HLS - time-based consideration + + var targetGap = gapLengthInTicks; + var gap = transcodingPositionTicks - downloadPositionTicks; + + if (gap < targetGap) + { + _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); + return false; + } + + _logger.LogDebug("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 ?? _fileSystem.GetFileInfo(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.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return false; + } + + _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting output size"); + return false; + } + } + + _logger.LogDebug("No throttle data for {Path}", path); + return false; + } + + private void DisposeTimer() + { + if (_timer is not null) + { + _timer.Dispose(); + _timer = null; + } + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 53df7133b..5a47236f9 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -111,7 +111,8 @@ namespace MediaBrowser.Controller.Session /// Reports the session ended. /// </summary> /// <param name="sessionId">The session identifier.</param> - void ReportSessionEnded(string sessionId); + /// <returns>Task.</returns> + ValueTask ReportSessionEnded(string sessionId); /// <summary> /// Sends the general command. diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 3e30c8dc4..3a12a56f1 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Class SessionInfo. /// </summary> - public sealed class SessionInfo : IAsyncDisposable, IDisposable + public sealed class SessionInfo : IAsyncDisposable { // 1 second private const long ProgressIncrement = 10000000; @@ -374,26 +374,6 @@ namespace MediaBrowser.Controller.Session } } - /// <inheritdoc /> - public void Dispose() - { - _disposed = true; - - StopAutomaticProgress(); - - var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty<ISessionController>(); - - foreach (var controller in controllers) - { - if (controller is IDisposable disposable) - { - _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name); - disposable.Dispose(); - } - } - } - public async ValueTask DisposeAsync() { _disposed = true; @@ -401,6 +381,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); + SessionControllers = Array.Empty<ISessionController>(); foreach (var controller in controllers) { @@ -409,6 +390,11 @@ namespace MediaBrowser.Controller.Session _logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name); await disposableAsync.DisposeAsync().ConfigureAwait(false); } + else if (controller is IDisposable disposable) + { + _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name); + disposable.Dispose(); + } } } } diff --git a/MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs b/MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs new file mode 100644 index 000000000..f44dc92d7 --- /dev/null +++ b/MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs @@ -0,0 +1,182 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.IO; + +namespace MediaBrowser.Controller.Streaming; + +/// <summary> +/// A progressive file stream for transferring transcoded files as they are written to. +/// </summary> +public class ProgressiveFileStream : Stream +{ + private readonly Stream _stream; + private readonly TranscodingJob? _job; + private readonly ITranscodeManager? _transcodeManager; + private readonly int _timeoutMs; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. + /// </summary> + /// <param name="filePath">The path to the transcoded file.</param> + /// <param name="job">The transcoding job information.</param> + /// <param name="transcodeManager">The transcode manager.</param> + /// <param name="timeoutMs">The timeout duration in milliseconds.</param> + public ProgressiveFileStream(string filePath, TranscodingJob? job, ITranscodeManager transcodeManager, int timeoutMs = 30000) + { + _job = job; + _transcodeManager = transcodeManager; + _timeoutMs = timeoutMs; + + _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); + } + + /// <summary> + /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. + /// </summary> + /// <param name="stream">The stream to progressively copy.</param> + /// <param name="timeoutMs">The timeout duration in milliseconds.</param> + public ProgressiveFileStream(Stream stream, int timeoutMs = 30000) + { + _job = null; + _transcodeManager = null; + _timeoutMs = timeoutMs; + _stream = stream; + } + + /// <inheritdoc /> + public override bool CanRead => _stream.CanRead; + + /// <inheritdoc /> + public override bool CanSeek => false; + + /// <inheritdoc /> + public override bool CanWrite => false; + + /// <inheritdoc /> + public override long Length => throw new NotSupportedException(); + + /// <inheritdoc /> + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// <inheritdoc /> + public override void Flush() + { + // Not supported + } + + /// <inheritdoc /> + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + /// <inheritdoc /> + public override int Read(Span<byte> buffer) + { + int totalBytesRead = 0; + var stopwatch = Stopwatch.StartNew(); + + while (true) + { + totalBytesRead += _stream.Read(buffer); + if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) + { + break; + } + + Thread.Sleep(50); + } + + UpdateBytesWritten(totalBytesRead); + + return totalBytesRead; + } + + /// <inheritdoc /> + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + + /// <inheritdoc /> + public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + int totalBytesRead = 0; + var stopwatch = Stopwatch.StartNew(); + + while (true) + { + totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) + { + break; + } + + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + + UpdateBytesWritten(totalBytesRead); + + return totalBytesRead; + } + + /// <inheritdoc /> + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException(); + + /// <inheritdoc /> + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// <inheritdoc /> + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// <inheritdoc /> + protected override void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + try + { + if (disposing) + { + _stream.Dispose(); + + if (_job is not null) + { + _transcodeManager?.OnTranscodeEndRequest(_job); + } + } + } + finally + { + _disposed = true; + base.Dispose(disposing); + } + } + + private void UpdateBytesWritten(int totalBytesRead) + { + if (_job is not null) + { + _job.BytesDownloaded += totalBytesRead; + } + } + + private bool StopReading(int bytesRead, long elapsed) + { + // It should stop reading when anything has been successfully read or if the job has exited + // If the job is null, however, it's a live stream and will require user action to close, + // but don't keep it open indefinitely if it isn't reading anything + return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs); + } +} diff --git a/MediaBrowser.Controller/Streaming/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs new file mode 100644 index 000000000..b5dbe29ec --- /dev/null +++ b/MediaBrowser.Controller/Streaming/StreamState.cs @@ -0,0 +1,183 @@ +using System; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Controller.Streaming; + +/// <summary> +/// The stream state dto. +/// </summary> +public class StreamState : EncodingJobInfo, IDisposable +{ + private readonly IMediaSourceManager _mediaSourceManager; + private readonly ITranscodeManager _transcodeManager; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="StreamState" /> class. + /// </summary> + /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param> + /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param> + /// <param name="transcodeManager">The <see cref="ITranscodeManager" /> singleton.</param> + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, ITranscodeManager transcodeManager) + : base(transcodingType) + { + _mediaSourceManager = mediaSourceManager; + _transcodeManager = transcodeManager; + } + + /// <summary> + /// Gets or sets the requested url. + /// </summary> + public string? RequestedUrl { get; set; } + + /// <summary> + /// Gets or sets the request. + /// </summary> + public StreamingRequestDto Request + { + get => (StreamingRequestDto)BaseRequest; + set + { + BaseRequest = value; + IsVideoRequest = VideoRequest is not null; + } + } + + /// <summary> + /// Gets the video request. + /// </summary> + public VideoRequestDto? VideoRequest => Request as VideoRequestDto; + + /// <summary> + /// Gets or sets the direct stream provicer. + /// </summary> + /// <remarks> + /// Deprecated. + /// </remarks> + public IDirectStreamProvider? DirectStreamProvider { get; set; } + + /// <summary> + /// Gets or sets the path to wait for. + /// </summary> + public string? WaitForPath { get; set; } + + /// <summary> + /// Gets a value indicating whether the request outputs video. + /// </summary> + public bool IsOutputVideo => Request is VideoRequestDto; + + /// <summary> + /// Gets the segment length. + /// </summary> + public int SegmentLength + { + get + { + if (Request.SegmentLength.HasValue) + { + return Request.SegmentLength.Value; + } + + if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) + { + var userAgent = UserAgent ?? string.Empty; + + if (userAgent.Contains("AppleTV", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("cfnetwork", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipad", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("iphone", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("ipod", StringComparison.OrdinalIgnoreCase)) + { + return 6; + } + + if (IsSegmentedLiveStream) + { + return 3; + } + + return 6; + } + + return 3; + } + } + + /// <summary> + /// Gets the minimum number of segments. + /// </summary> + public int MinSegments + { + get + { + if (Request.MinSegments.HasValue) + { + return Request.MinSegments.Value; + } + + return SegmentLength >= 10 ? 2 : 3; + } + } + + /// <summary> + /// Gets or sets the user agent. + /// </summary> + public string? UserAgent { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to estimate the content length. + /// </summary> + public bool EstimateContentLength { get; set; } + + /// <summary> + /// Gets or sets the transcode seek info. + /// </summary> + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// <summary> + /// Gets or sets the transcoding job. + /// </summary> + public TranscodingJob? TranscodingJob { get; set; } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <inheritdoc /> + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) + { + _transcodeManager.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + } + + /// <summary> + /// Disposes the stream state. + /// </summary> + /// <param name="disposing">Whether the object is currently being disposed.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // REVIEW: Is this the right place for this? + if (MediaSource.RequiresClosing + && string.IsNullOrWhiteSpace(Request.LiveStreamId) + && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId)) + { + _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); + } + } + + TranscodingJob = null; + + _disposed = true; + } +} diff --git a/MediaBrowser.Controller/Streaming/StreamingRequestDto.cs b/MediaBrowser.Controller/Streaming/StreamingRequestDto.cs new file mode 100644 index 000000000..e47ef65f0 --- /dev/null +++ b/MediaBrowser.Controller/Streaming/StreamingRequestDto.cs @@ -0,0 +1,49 @@ +using MediaBrowser.Controller.MediaEncoding; + +namespace MediaBrowser.Controller.Streaming; + +/// <summary> +/// The audio streaming request dto. +/// </summary> +public class StreamingRequestDto : BaseEncodingJobOptions +{ + /// <summary> + /// Gets or sets the params. + /// </summary> + public string? Params { get; set; } + + /// <summary> + /// Gets or sets the play session id. + /// </summary> + public string? PlaySessionId { get; set; } + + /// <summary> + /// Gets or sets the tag. + /// </summary> + public string? Tag { get; set; } + + /// <summary> + /// Gets or sets the segment container. + /// </summary> + public string? SegmentContainer { get; set; } + + /// <summary> + /// Gets or sets the segment length. + /// </summary> + public int? SegmentLength { get; set; } + + /// <summary> + /// Gets or sets the min segments. + /// </summary> + public int? MinSegments { get; set; } + + /// <summary> + /// Gets or sets the position of the requested segment in ticks. + /// </summary> + public long CurrentRuntimeTicks { get; set; } + + /// <summary> + /// Gets or sets the actual segment length in ticks. + /// </summary> + public long ActualSegmentLengthTicks { get; set; } +} diff --git a/MediaBrowser.Controller/Streaming/VideoRequestDto.cs b/MediaBrowser.Controller/Streaming/VideoRequestDto.cs new file mode 100644 index 000000000..44dc831fd --- /dev/null +++ b/MediaBrowser.Controller/Streaming/VideoRequestDto.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Controller.Streaming; + +/// <summary> +/// The video request dto. +/// </summary> +public class VideoRequestDto : StreamingRequestDto +{ + /// <summary> + /// Gets a value indicating whether this instance has fixed resolution. + /// </summary> + /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value> + public bool HasFixedResolution => Width.HasValue || Height.HasValue; + + /// <summary> + /// Gets or sets a value indicating whether to enable subtitles in the manifest. + /// </summary> + public bool EnableSubtitlesInManifest { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to enable trickplay images. + /// </summary> + public bool EnableTrickplay { get; set; } +} |
