All checks were successful
continuous-integration/drone/push Build is passing
616 lines
23 KiB
C#
616 lines
23 KiB
C#
#pragma warning disable CA1002
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using Jellyfin.Data.Entities;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Entities.Movies;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.LiveTv;
|
|
using MediaBrowser.Controller.Persistence;
|
|
using MediaBrowser.Model.Dto;
|
|
using MediaBrowser.Model.Entities;
|
|
using MediaBrowser.Model.MediaInfo;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Cinema.Webshare;
|
|
using CinemaLib.API;
|
|
|
|
namespace Jellyfin.Plugin.Cinema;
|
|
|
|
public class CinemaMediaSourceManager : IMediaSourceManager
|
|
{
|
|
private const bool IsWebshareFreeAccount = true;
|
|
private const double BitrateMargin = 0.1; // 10 %
|
|
private const double WebshareFreeBitrate = 300000 * 8;
|
|
|
|
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(180);
|
|
|
|
private static CinemaMediaSourceManager? _instance;
|
|
|
|
private readonly IMediaSourceManager _inner;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly IHttpContextAccessor _http;
|
|
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
|
|
|
|
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServiceProvider svc, IHttpContextAccessor http)
|
|
{
|
|
if (innerMediaSourceManager == null || svc == null)
|
|
throw new ArgumentNullException();
|
|
|
|
IMediaSourceManager? inner = svc.GetService(innerMediaSourceManager.InnerType) as IMediaSourceManager;
|
|
if (inner == null)
|
|
throw new InvalidOperationException("Original MediaSourceManager service not found.");
|
|
this._inner = inner;
|
|
|
|
this._libraryManager = libraryManager;
|
|
this._http = http;
|
|
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
|
|
|
|
_instance = this;
|
|
}
|
|
|
|
internal static bool TryGetInstance([NotNullWhen(true)] out CinemaMediaSourceManager? instance)
|
|
{
|
|
instance = _instance;
|
|
return instance != null;
|
|
}
|
|
|
|
#region IMediaSourceManager Members
|
|
|
|
public Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, addProbeDelay, isLiveStream, cancellationToken);
|
|
}
|
|
|
|
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
|
|
{
|
|
_inner.AddParts(providers);
|
|
}
|
|
|
|
public Task CloseLiveStream(string id)
|
|
{
|
|
return _inner.CloseLiveStream(id);
|
|
}
|
|
|
|
public Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.GetLiveStream(id, cancellationToken);
|
|
}
|
|
|
|
public ILiveStream GetLiveStreamInfo(string id)
|
|
{
|
|
return _inner.GetLiveStreamInfo(id);
|
|
}
|
|
|
|
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
|
|
{
|
|
return _inner.GetLiveStreamInfoByUniqueId(uniqueId);
|
|
}
|
|
|
|
public Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.GetLiveStreamMediaInfo(id, cancellationToken);
|
|
}
|
|
|
|
public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.GetLiveStreamWithDirectStreamProvider(id, cancellationToken);
|
|
}
|
|
|
|
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
|
|
{
|
|
return _inner.GetMediaAttachments(itemId);
|
|
}
|
|
|
|
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
|
|
{
|
|
return _inner.GetMediaAttachments(query);
|
|
}
|
|
|
|
public Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.GetMediaSource(item, mediaSourceId, liveStreamId, enablePathSubstitution, cancellationToken);
|
|
}
|
|
|
|
public List<MediaStream> GetMediaStreams(Guid itemId)
|
|
{
|
|
return _inner.GetMediaStreams(itemId);
|
|
/*
|
|
// Intercept for CinemaItems
|
|
Video? item = _libraryManager.GetItemById<Video>(itemId);
|
|
if (item == null
|
|
|| item.ProviderIds == null
|
|
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
|
|| string.IsNullOrEmpty(item.ExternalId))
|
|
return _inner.GetMediaStreams(itemId);
|
|
|
|
CinemaLib.API.Stream? ver = GetVersion(item, default).GetAwaiter().GetResult();
|
|
|
|
return ver != null ? VersionToMediaStreams(item, ver) : new List<MediaStream>();
|
|
*/
|
|
}
|
|
|
|
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
|
|
{
|
|
return _inner.GetMediaStreams(query);
|
|
}
|
|
|
|
public MediaProtocol GetPathProtocol(string path)
|
|
{
|
|
return _inner.GetPathProtocol(path);
|
|
}
|
|
|
|
public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.GetRecordingStreamMediaSources(info, cancellationToken);
|
|
}
|
|
|
|
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User? user = null)
|
|
{
|
|
// Intercept for CinemaItems
|
|
if (item == null
|
|
|| item.ProviderIds == null
|
|
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
|
|| string.IsNullOrEmpty(item.ExternalId))
|
|
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
|
|
|
|
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
|
|
switch (item)
|
|
{
|
|
case Video video:
|
|
VersionSetEntry ver = GetVersionSet(video, out BaseItem? videoPrimary, default).GetAwaiter().GetResult();
|
|
if (ver.Versions != null)
|
|
{
|
|
IEnumerable<Video> items;
|
|
switch (video)
|
|
{
|
|
case CinemaMovie movie: items = GetVideoVersionsEnumerate<CinemaMovie>(movie, videoPrimary!, ver.Versions); break;
|
|
default: throw new NotSupportedException(string.Format("BaseItem type '{0}' not supported in CinemaMediaSources.", video.GetType().Name));
|
|
}
|
|
|
|
int idx = 0;
|
|
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
|
|
// Note: Also makes sure BaseItems exist for each version
|
|
foreach (var i in items)
|
|
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// HACK Prevent crash
|
|
if (result.Count == 0)
|
|
result.Append(new MediaSourceInfo() { MediaStreams = new List<MediaStream>() });
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
|
{
|
|
// Intercept for CinemaItems
|
|
if (item == null
|
|
|| item.ProviderIds == null
|
|
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
|
|| string.IsNullOrEmpty(item.ExternalId))
|
|
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
|
|
|
|
// HACK: We do not want to generate links to all media sources - just the one going
|
|
// to be played. Unfortunately our caller MediaInfoHelper.GetPlaybackInfo and indirectly
|
|
// from MediaInfoController.GetPostedPlaybackInfo does not pass this info to us
|
|
// so we will grab it directly from the HttpRequest
|
|
HttpContext? ctx = _http.HttpContext;
|
|
StringValues mediaSourceIdS;
|
|
string? mediaSourceId;
|
|
if (ctx == null
|
|
|| !ctx.Request.Path.HasValue
|
|
|| !ctx.Request.Path.Value.EndsWith("/PlaybackInfo")
|
|
|| (mediaSourceIdS = ctx.Request.Query["mediaSourceId"]).Count != 1
|
|
|| string.IsNullOrEmpty(mediaSourceId = mediaSourceIdS[0]))
|
|
throw new NotSupportedException(string.Format("Unsupported caller '{0}' of CinemaMediaSourceManager", ctx != null && ctx.Request.Path.HasValue ? ctx.Request.Path.Value : "?"));
|
|
|
|
BaseItem? verItem = _libraryManager.GetItemById(Guid.Parse(mediaSourceId));
|
|
|
|
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
|
|
switch (verItem)
|
|
{
|
|
case Video video:
|
|
VersionEntry? ver = await GetVersion(video, cancellationToken);
|
|
if (ver != null)
|
|
{
|
|
Uri? path = ver.DownloadLink;
|
|
if (path == null)
|
|
{
|
|
switch (ver.Meta.provider)
|
|
{
|
|
case "webshare": path = await LinkGenerator.GenerateDownloadLinkAsync(ver.Meta.ident, ver.Meta.name, cancellationToken); break;
|
|
default: path = null; break;
|
|
}
|
|
ver.DownloadLink = path;
|
|
}
|
|
MediaSourceInfo a = GetVersionInfo(video, ver.Meta);
|
|
if (path != null)
|
|
a.Path = path.ToString();
|
|
// Propagate "-seekable 0" to ffmpeg as free accounts on Webshare cannot use "Range: bytes" HTTP header
|
|
// HACK: We misuse the user-agent as GetExtraArguments in MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
|
|
// does not properly escape its value for command line
|
|
if (IsWebshareFreeAccount)
|
|
{
|
|
if (a.RequiredHttpHeaders == null)
|
|
a.RequiredHttpHeaders = new Dictionary<string, string>();
|
|
a.RequiredHttpHeaders["User-Agent"] = "Lavf/60\" -seekable \"0";
|
|
}
|
|
result.Add(a);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.OpenLiveStream(request, cancellationToken);
|
|
}
|
|
|
|
public Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
|
{
|
|
return _inner.OpenLiveStreamInternal(request, cancellationToken);
|
|
}
|
|
|
|
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
|
|
{
|
|
_inner.SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
|
|
}
|
|
|
|
public bool SupportsDirectStream(string path, MediaProtocol protocol)
|
|
{
|
|
return _inner.SupportsDirectStream(path, protocol);
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Gets stream metadata for a video version (ie. for Movie or Episode).
|
|
/// </summary>
|
|
internal async Task<IEnumerable<T>?> GetVideoVersions<T>(T item, CancellationToken cancel)
|
|
where T : Video, new()
|
|
{
|
|
if (item == null)
|
|
throw new ArgumentNullException();
|
|
|
|
VersionSetEntry ver = await GetVersionSet(item, out BaseItem? primary, cancel);
|
|
|
|
return ver.Versions == null ? null : GetVideoVersionsEnumerate(item, primary!, ver.Versions);
|
|
}
|
|
|
|
private IEnumerable<T> GetVideoVersionsEnumerate<T>(T item, BaseItem primary, VersionEntry[] versions)
|
|
where T : Video, new()
|
|
{
|
|
bool isFirst = true;
|
|
foreach (var i in versions)
|
|
{
|
|
T a;
|
|
if (isFirst)
|
|
{
|
|
// The primary item represents the first version
|
|
a = (T)primary;
|
|
isFirst = false;
|
|
}
|
|
else
|
|
{
|
|
a = CinemaFilterFolder.GetMediaItemById<T>(item.ExternalId, i.Meta._id, out bool isNew);
|
|
if (isNew)
|
|
{
|
|
// Copy properties from the parent version
|
|
// Note: non-primary versions are never persisted so a new volatile instance got created
|
|
Guid aId = a.Id;
|
|
item.DeepCopy(a);
|
|
a.Id = aId;
|
|
a.SetPrimaryVersionId(item.Id.ToString("N", CultureInfo.InvariantCulture));
|
|
|
|
// Just a volatile item
|
|
_libraryManager.RegisterItem(a);
|
|
}
|
|
}
|
|
yield return a;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a version set for a video.
|
|
/// </summary>
|
|
private Task<VersionSetEntry> GetVersionSet(Video item, out BaseItem? primary, CancellationToken cancel)
|
|
{
|
|
if (item == null)
|
|
throw new ArgumentNullException();
|
|
|
|
if (item.ProviderIds == null
|
|
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
|
|| string.IsNullOrEmpty(item.ExternalId))
|
|
{
|
|
// Not a Stream Cinema video
|
|
primary = null;
|
|
return Task.FromResult<VersionSetEntry>(default);
|
|
}
|
|
|
|
Guid primaryId;
|
|
string csId;
|
|
if (!string.IsNullOrEmpty(item.PrimaryVersionId))
|
|
{
|
|
// Move to the primary item
|
|
primaryId = Guid.Parse(item.PrimaryVersionId, CultureInfo.InvariantCulture);
|
|
primary = _libraryManager.GetItemById(primaryId);
|
|
if (primary == null
|
|
|| primary.ProviderIds == null
|
|
|| !primary.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|
|
|| string.IsNullOrEmpty(primary.ExternalId))
|
|
// Not a Stream Cinema video
|
|
return Task.FromResult<VersionSetEntry>(default);
|
|
|
|
csId = primary.ExternalId;
|
|
|
|
}
|
|
else
|
|
{
|
|
primary = item;
|
|
primaryId = item.Id;
|
|
csId = item.ExternalId;
|
|
}
|
|
|
|
return GetVersionSet(primaryId, csId, cancel);
|
|
}
|
|
|
|
private async Task<VersionSetEntry> GetVersionSet(Guid primaryId, string csId, CancellationToken cancel)
|
|
{
|
|
DateTime now = DateTime.UtcNow;
|
|
VersionSetEntry result;
|
|
if (!_videoVersions.TryGetValue(primaryId, out result) || result.ValidUntil < now)
|
|
{
|
|
var a = await Metadata.StreamsAsync(csId, cancel);
|
|
if (a != null) {
|
|
result.Versions = new VersionEntry[a.Length];
|
|
for (int i = 0; i < a.Length; i++)
|
|
result.Versions[i] = new VersionEntry(a[i]);
|
|
} else
|
|
result.Versions = null;
|
|
result.ValidUntil = now + VersionValidityTimeout;
|
|
_videoVersions[primaryId] = result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async Task<VersionEntry?> GetVersion(Video item, CancellationToken cancel)
|
|
{
|
|
VersionSetEntry vers = await GetVersionSet(item, out BaseItem? primary, cancel);
|
|
if (vers.Versions == null)
|
|
return null;
|
|
|
|
bool isFirst = true;
|
|
foreach (var i in vers.Versions)
|
|
{
|
|
Guid id;
|
|
if (isFirst)
|
|
{
|
|
// The primary item represents the first version
|
|
isFirst = false;
|
|
id = primary!.Id;
|
|
}
|
|
else
|
|
id = CinemaFilterFolder.GetMediaItemId(primary!.ExternalId, i.Meta._id);
|
|
|
|
if (id == item.Id)
|
|
return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<MediaSourceInfo?> GetVersionInfo(Video item, CancellationToken cancel)
|
|
{
|
|
if (item == null)
|
|
throw new ArgumentNullException();
|
|
|
|
VersionEntry? ver = await GetVersion(item, cancel);
|
|
if (ver == null)
|
|
return null;
|
|
|
|
return GetVersionInfo(item, ver.Meta);
|
|
}
|
|
|
|
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver)
|
|
{
|
|
MediaSourceInfo result = new MediaSourceInfo();
|
|
result.VideoType = VideoType.VideoFile;
|
|
result.Id = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
|
result.Protocol = MediaProtocol.Http;
|
|
// Warning: We set some properties on item that get used below
|
|
result.MediaStreams = VersionToMediaStreams(item, ver);
|
|
|
|
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
|
|
bool showBitrateWarning = IsWebshareFreeAccount && bitRate / (1 + BitrateMargin) > WebshareFreeBitrate;
|
|
|
|
string name = showBitrateWarning ? "LOGIN Webshare! " : "";
|
|
name += $"{Math.Round((double)(ver.size ?? 0) / (1024 * 1024 * 1024), 2)} GB ({ver.date_added?.ToShortDateString()})"
|
|
+ $" {Math.Round((item.RunTimeTicks ?? 0.0) / TimeSpan.TicksPerMinute)} min";
|
|
MediaStream? firstVid = result.MediaStreams.Where(x => x.Type == MediaStreamType.Video).FirstOrDefault();
|
|
if (firstVid != null)
|
|
{
|
|
var hdr = firstVid.VideoRangeType;
|
|
var hdr2 = firstVid.VideoDoViTitle;
|
|
string hdrS = hdr == Data.Enums.VideoRangeType.Unknown || hdr == Data.Enums.VideoRangeType.SDR
|
|
? " SDR"
|
|
: " HDR" + (string.IsNullOrEmpty(hdr2) ? "" : "[" + hdr2 + "]");
|
|
string is3DS = ver!.video!.First().is3d ? " (3D)" : "";
|
|
name += " " + firstVid.Width.ToString() + "x" + firstVid.Height.ToString() + hdrS + is3DS;
|
|
}
|
|
StringBuilder audioAndSub = new StringBuilder();
|
|
if (ver.audio != null)
|
|
{
|
|
foreach (StreamAudio i in ver.audio)
|
|
audioAndSub.Append($"{i.language} {i.codec} {i.channels}").Append(",");
|
|
if (audioAndSub.Length != 0)
|
|
{
|
|
audioAndSub.Remove(audioAndSub.Length - 1, 1);
|
|
name += " {" + audioAndSub + "}";
|
|
audioAndSub.Clear();
|
|
}
|
|
}
|
|
if (ver.subtitles != null)
|
|
{
|
|
foreach (StreamSubtitle i in ver.subtitles)
|
|
audioAndSub.Append($"{i.language}").Append(",");
|
|
if (audioAndSub.Length != 0)
|
|
{
|
|
audioAndSub.Remove(audioAndSub.Length - 1, 1);
|
|
name += " [" + audioAndSub + "]";
|
|
audioAndSub.Clear();
|
|
}
|
|
}
|
|
result.Name = name;
|
|
|
|
//Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
|
|
result.Container = item.Container;
|
|
result.RunTimeTicks = item.RunTimeTicks;
|
|
result.Size = item.Size;
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<MediaStream> VersionToMediaStreams(Video item, CinemaLib.API.Stream ver)
|
|
{
|
|
List<MediaStream> result = new List<MediaStream>();
|
|
// HACK: Propagate some values to the item also
|
|
item.Size = ver.size;
|
|
//stream_id;
|
|
//stream.name;
|
|
//stream.provider
|
|
//stream.ident;
|
|
//stream.media
|
|
//stream.date_added
|
|
|
|
if (ver.video != null)
|
|
{
|
|
foreach (StreamVideo j in ver.video)
|
|
{
|
|
MediaStream a = new MediaStream();
|
|
result.Add(a);
|
|
a.Type = MediaStreamType.Video;
|
|
a.Width = j.width;
|
|
a.Height = j.height;
|
|
a.Codec = j.codec;
|
|
a.AspectRatio = j.aspect.ToString();
|
|
ConvertHdr(j.hasHdr, j.hdrQuality, a);
|
|
|
|
// HACK: Those values are read after we return (not before)
|
|
item.RunTimeTicks = (long?)(j.duration * TimeSpan.TicksPerSecond);
|
|
if (j.is3d)
|
|
// Just a probability guess
|
|
item.Video3DFormat = Video3DFormat.HalfSideBySide;
|
|
}
|
|
}
|
|
|
|
if (ver.audio != null)
|
|
{
|
|
foreach (StreamAudio j in ver.audio)
|
|
{
|
|
MediaStream a = new MediaStream();
|
|
result.Add(a);
|
|
a.Type = MediaStreamType.Audio;
|
|
a.Language = j.language;
|
|
a.Codec = j.codec;
|
|
a.Channels = j.channels;
|
|
}
|
|
}
|
|
|
|
if (ver.subtitles != null)
|
|
{
|
|
foreach (StreamSubtitle j in ver.subtitles)
|
|
{
|
|
MediaStream a = new MediaStream();
|
|
result.Add(a);
|
|
a.Type = MediaStreamType.Subtitle;
|
|
a.Language = j.language;
|
|
a.IsForced = j.forced;
|
|
a.Path = j.src;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void ConvertHdr(bool isHdr, string? hdr, MediaStream dst)
|
|
{
|
|
if (!isHdr)
|
|
return;
|
|
|
|
if (hdr != null)
|
|
{
|
|
// Examples:
|
|
// Dolby Vision
|
|
// Dolby Vision / SMPTE ST 2094 App 4
|
|
// Dolby Vision / SMPTE ST 2086
|
|
// SMPTE ST 2086
|
|
string[] sections = hdr.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
if (sections.Length != 0)
|
|
{
|
|
string lastValue = sections[sections.Length - 1];
|
|
if (lastValue != "Dolby Vision")
|
|
{
|
|
dst.ColorTransfer = lastValue;
|
|
switch (lastValue)
|
|
{
|
|
case "SMPTE ST 2094 App 4":
|
|
// DoVi Profile 5
|
|
dst.DvProfile = 5;
|
|
dst.RpuPresentFlag = 1;
|
|
dst.BlPresentFlag = 1;
|
|
dst.DvBlSignalCompatibilityId = 0;
|
|
dst.CodecTag = "dovi";
|
|
break;
|
|
|
|
case "SMPTE ST 2084":
|
|
// HDR10
|
|
dst.ColorTransfer = "smpte2084";
|
|
break;
|
|
|
|
case "SMPTE ST 2086":
|
|
// fall through for basic HDR level
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Indicate the "lowest" HDR level
|
|
// See function GetVideoColorRange in MediaBrowser.Model/Entities/MediaStream.cs in Jellyfin for reasons
|
|
dst.DvProfile = 10;
|
|
dst.RpuPresentFlag = 1;
|
|
dst.BlPresentFlag = 1;
|
|
dst.DvBlSignalCompatibilityId = 0;
|
|
dst.CodecTag = "dovi";
|
|
return;
|
|
}
|
|
|
|
struct VersionSetEntry
|
|
{
|
|
internal DateTime ValidUntil;
|
|
internal VersionEntry[]? Versions;
|
|
}
|
|
|
|
class VersionEntry
|
|
{
|
|
private readonly CinemaLib.API.Stream _meta;
|
|
|
|
internal VersionEntry(CinemaLib.API.Stream meta)
|
|
{
|
|
this._meta = meta;
|
|
}
|
|
|
|
internal CinemaLib.API.Stream Meta => _meta;
|
|
|
|
internal Uri? DownloadLink { get; set; }
|
|
}
|
|
}
|