aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorBrian Howe <howe.m.brian@gmail.com>2024-02-27 21:07:30 -0600
committerBrian Howe <howe.m.brian@gmail.com>2024-02-27 21:07:30 -0600
commit54eb81395ef8d3d4cb064b56361ce94fc72b38b5 (patch)
tree73240b556055557b0ae034ef5d5ba60cb5cb051e /MediaBrowser.Controller
parent7f1fec688cc1a6f7f69fa5b059af01cf9c456d3f (diff)
parent4786901bb796c3e912f13b686571fde8d16f49c5 (diff)
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs7
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs3
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs20
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs9
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs11
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs5
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs26
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs70
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs3
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs9
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs9
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs17
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs13
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs9
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHostManager.cs46
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs54
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs77
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs210
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs30
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs104
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs280
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs218
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs3
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs28
-rw-r--r--MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs182
-rw-r--r--MediaBrowser.Controller/Streaming/StreamState.cs183
-rw-r--r--MediaBrowser.Controller/Streaming/StreamingRequestDto.cs49
-rw-r--r--MediaBrowser.Controller/Streaming/VideoRequestDto.cs23
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&lt;DeviceInfo&gt;.</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&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</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; }
+}