Files
stream-cinema/CinemaJellyfin/CinemaMediaSourceManager.cs

1166 lines
38 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.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 CinemaLib.API;
using MediaBrowser.Controller;
using MediaBrowser.Controller.MediaEncoding;
using System.Runtime.CompilerServices;
using LinkGenerator = CinemaLib.Webshare.LinkGenerator;
using MediaBrowser.Controller.Configuration;
namespace Jellyfin.Plugin.Cinema;
sealed class CinemaMediaSourceManager : IMediaSourceManager
{
private const double BitrateMargin = 0.1; // 10 %
private const double WebshareFreeBitrate = 300000 * 8;
private const int FirstRelativeVideoIndex = -2;
private const int FirstRelativeAudioIndex = -5;
private const int FirstRelativeSubtitleIndex = -15;
private const int MediaInspectionMinFileSize = 65536 * 2 + 16384;
private const int MediaInspectionBaseSize = 65536 * 2;
private const int MediaInspectionSizeDivisor = 1024 * 1024 / 64;
private static readonly Uri FakeVideoUri = new Uri("https://127.0.0.1");
internal const string ContextItemsMediaSourceIdKey = "mediaSource-9ad56bce";
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(180);
private static readonly TimeSpan LinkValidityTimeout = VersionValidityTimeout;
internal static readonly TimeSpan SubfolderValidityTimeout = VersionValidityTimeout;
private static CinemaMediaSourceManager? _instance;
private readonly IMediaSourceManager _inner;
private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _host;
private readonly IHttpContextAccessor _http;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IUserManager _userManager;
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
public CinemaMediaSourceManager(CinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host,
IServiceProvider svc, IHttpContextAccessor http, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder,
IServerConfigurationManager serverConfigurationManager, IUserManager userManager)
{
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._host = host;
this._http = http;
this._httpClientFactory = httpClientFactory;
this._mediaEncoder = mediaEncoder;
this._serverConfigurationManager = serverConfigurationManager;
this._userManager = userManager;
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
_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);
}
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
|| !CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|| string.IsNullOrEmpty(item.ExternalId))
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
List<MediaSourceInfo> result = GetPlaybackMediaSourcesInternal(item, user, false, enablePathSubstitution, needsPreciseStreamIndicies: false, default).GetAwaiter().GetResult();
// HACK Prevent crash
if (result.Count == 0)
result.Append(new MediaSourceInfo() { MediaStreams = new List<MediaStream>() });
return result;
}
public Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User? user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
return GetPlaybackMediaSourcesInternal(item, user, allowMediaProbe, enablePathSubstitution, needsPreciseStreamIndicies: true, cancellationToken);
}
private async Task<List<MediaSourceInfo>> GetPlaybackMediaSourcesInternal(BaseItem item, User? user, bool allowMediaProbe, bool enablePathSubstitution, bool needsPreciseStreamIndicies, CancellationToken cancellationToken)
{
// Intercept for CinemaItems
if (item == null
|| !CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|| string.IsNullOrEmpty(item.ExternalId))
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
switch (item)
{
case Video video:
VersionSetEntry ver = await GetVersionSet(video, out BaseItem? videoPrimary, out string? csId, default);
if (ver.Versions != null)
{
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancellationToken);
HttpContext? ctx = _http.HttpContext;
IEnumerable<Video> items;
bool sortByPrefs;
switch (video)
{
case CinemaMovie movie: items = GetVideoVersionsEnumerate(csId!, movie, videoPrimary!, ver.Versions); sortByPrefs = false; break;
case CinemaEpisode episode: items = GetVideoVersionsEnumerate(csId!, episode, videoPrimary!, ver.Versions); sortByPrefs = true; break;
case MusicVideo musicVideo: items = GetVideoVersionsEnumerate(csId!, musicVideo, videoPrimary!, ver.Versions); sortByPrefs = false; break;
default: throw new NotSupportedException(string.Format("BaseItem type '{0}' not supported in CinemaMediaSources.", video.GetType().Name));
}
// HACK: Currently Jellyfin behaves extremely stupid and even if we offer alternative
// versions, even sorted versions with best coming first it always comes back asking
// for the primary version. So ignore mediaSourceId and always return single best version
// with faked id.
// HACK: One of our callers (GetMasterHlsVideoPlaylist) does not bother resolving current user
string? userIdS;
if (sortByPrefs && user == null && ctx?.User != null && (userIdS = ctx.User.Claims.Where(x => x.Type == "Jellyfin-UserId").Select(x => x.Value).FirstOrDefault()) != null && Guid.TryParse(userIdS, out Guid userId))
user = _userManager.GetUserById(userId);
int[]? scores;
int bestIdx = -1;
if (sortByPrefs && ver.Versions.Length != 0 && (scores = ScoreVersionsByPreferences(user, ver.Versions, isFreeAccount)) != null)
{
// The output must be stable with score-ties so sort also by _id
int delta;
bestIdx = 0;
for (int i = 0; i < scores.Length; i++)
if ((delta = scores[i] - scores[bestIdx]) > 0 || (delta == 0 && string.CompareOrdinal(ver.Versions[i].Meta._id, ver.Versions[bestIdx].Meta._id) < 0))
bestIdx = i;
}
if (needsPreciseStreamIndicies)
{
// We need to directly inspect the video file and therefore need to know which
// version to specifically return. For free accounts it is not possible to just
// read files at random, for VIP accounts that would slow playback startup also.
Guid mediaSourceId;
if (ctx == null
|| !ctx.Items.TryGetValue(ContextItemsMediaSourceIdKey, out object? mediaSourceIdO)
|| mediaSourceIdO is not string mediaSourceIdS
|| (!Guid.TryParse(mediaSourceIdS, out mediaSourceId)))
throw new InvalidOperationException("For precise stream indexing knowing mediaSourceId is required.");
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
int idxVer = 0;
Video? videoVer = null;
foreach (Video i in items)
if ((!sortByPrefs && i.Id == mediaSourceId)
|| (sortByPrefs && idxVer == bestIdx))
{
videoVer = i;
break;
}
else
idxVer++;
if (videoVer == null)
// Version not found, return empty set
break;
// Get few first kb of the file that is going to be played anyway and
// inspect it for available streams.
VersionEntry metaVer = ver.Versions[idxVer];
Uri link = await GenerateLink(metaVer.Meta.provider, metaVer.Meta.ident, metaVer.Meta.name, cancellationToken);
if (metaVer.AnalyzedInfo == null)
metaVer.AnalyzedInfo = await CinemaMediaAnalyzer.AnalyzeResourceAsync(link, metaVer.Meta.size, metaVer.Meta, _httpClientFactory, _mediaEncoder, _serverConfigurationManager, cancellationToken);
result.Add(GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, isFreeAccount, link));
}
else
{
// Warning: We assume GetVideoVersionsEnumerate keeps the order between its input and output collections
// Note: Also makes sure BaseItems exist for each version
int idx = 0;
foreach (Video i in items)
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, null, isFreeAccount, FakeVideoUri));
if (sortByPrefs && ver.Versions.Length != 0 && bestIdx >= 0)
{
MediaSourceInfo best = result[bestIdx];
result.Clear();
result.Add(best);
}
}
// For episodes we must choose the audio/subtitle automatically as there is no UI for it
// and MediaInfoHelper.SetDeviceSpecificData and deeper StreamBuilder.BuildVideoItem does
// a poor job at selecting the proper streams
if (sortByPrefs)
{
foreach (MediaSourceInfo i in result)
{
ForceByPreferences(user, i, isFreeAccount);
i.Id = videoPrimary!.Id.ToString("N", CultureInfo.InvariantCulture);
}
}
}
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>
/// Sorts media verions by preferences as episodes currently do not support multiple versions
/// and the first one gets played.
/// </summary>
private static int[]? ScoreVersionsByPreferences(User? user, VersionEntry[] items, bool isFreeAccount)
{
if (user == null)
// We have no info about the user so nothing to sort
return null;
double? bitrateLimit = isFreeAccount ? WebshareFreeBitrate : null;
string? audioLang = user.AudioLanguagePreference;
string? subtitleLang = user.SubtitleLanguagePreference;
int[] scores = new int[items.Length];
for (int i = 0; i < items.Length; i++)
{
var m = items[i].Meta;
int score = 0;
StreamVideo? video = m.video?.FirstOrDefault();
if (video != null)
{
score += video.height / 100; // range [1..30]
score += video.hasHdr ? 3 : 0; // HDR is plus but resolution ust rule
}
if (m.subtitles != null && subtitleLang != null)
foreach (StreamSubtitle j in m.subtitles)
if (ISO639_1ToISO639_2(j.language) == subtitleLang)
{
score += 4; // proper subtitles are preferred but much better resulution still rules
break;
}
if (m.audio != null && audioLang != null)
foreach (StreamAudio j in m.audio)
if (ISO639_1ToISO639_2(j.language) == audioLang)
{
score += 25; // proper audio channel overrides resolution and other minor bonuses
break;
}
if (bitrateLimit != null && video != null)
{
double bitRate = (m.size ?? 0.0) * 8 / (video.duration ?? 1.0);
score += bitRate / (1 + BitrateMargin) < bitrateLimit.Value ? 50 : 0; // without proper bitrate it is not playable at all
}
scores[i] = score;
}
return scores;
}
private static void ForceByPreferences(User? user, MediaSourceInfo item, bool isFreeAccount)
{
if (user == null)
// We have no info about the user so nothing to force
return;
string? audioLang = user.AudioLanguagePreference;
// Free account prevents seeking and thus subtitle exctraction
string? subtitleLang = isFreeAccount ? null : user.SubtitleLanguagePreference;
foreach (MediaStream i in item.MediaStreams)
{
// We cannot use DefaultAudioStreamIndex and DefaultSubtitleStreamIndex as we do not
// have absolute stream index values in i.Index. Thankfully MediaSourceInfo.GetDefaultAudioStream
// uses IsDefault as a fallback.
if (i.Type == MediaStreamType.Audio && audioLang != null && audioLang == i.Language)
i.IsDefault = true;
else if (i.Type == MediaStreamType.Subtitle && subtitleLang != null && subtitleLang == i.Language)
i.IsDefault = true;
else
i.IsDefault = false;
}
}
/// <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, out string? csId, cancel);
return ver.Versions == null ? null : GetVideoVersionsEnumerate(csId!, item, primary!, ver.Versions);
}
private IEnumerable<T> GetVideoVersionsEnumerate<T>(string csId, 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 = CinemaQueryExtensions.GetMediaItemById<T>(csId, 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, out string? csId, CancellationToken cancel)
{
if (item == null)
throw new ArgumentNullException();
if (!CinemaQueryExtensions.IsCinemaExternalId(item.ExternalId)
|| string.IsNullOrEmpty(item.ExternalId))
{
// Not a Stream Cinema video
primary = null;
csId = null;
return Task.FromResult<VersionSetEntry>(default);
}
Guid primaryId;
string externalId;
if (!string.IsNullOrEmpty(item.PrimaryVersionId))
{
// Move to the primary item
primaryId = Guid.Parse(item.PrimaryVersionId, CultureInfo.InvariantCulture);
primary = _libraryManager.GetItemById(primaryId);
if (primary == null
|| !CinemaQueryExtensions.IsCinemaExternalId(primary.ExternalId)
|| string.IsNullOrEmpty(primary.ExternalId))
{
// Not a Stream Cinema video
csId = null;
return Task.FromResult<VersionSetEntry>(default);
}
externalId = primary.ExternalId;
}
else
{
primary = item;
primaryId = item.Id;
externalId = item.ExternalId;
}
if (!CinemaQueryExtensions.TryGetCinemaIdFromExternalId(externalId, out csId))
throw new InvalidOperationException("Cannot parse Cinema identifier.");
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, out string? csId, 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 = CinemaQueryExtensions.GetMediaItemId(csId!, 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;
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
return GetVersionInfo(item, ver.Meta, null, isFreeAccount, FakeVideoUri);
}
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, bool isFreeAccount, Uri path)
{
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, analyzedStreams);
double bitRate = (ver.size ?? 0.0) * 8 / (item.RunTimeTicks ?? 1.0) * TimeSpan.TicksPerSecond;
bool showBitrateWarning = isFreeAccount && 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;
result.Path = path.ToString();
result.Container = item.Container;
result.RunTimeTicks = item.RunTimeTicks;
result.Size = item.Size;
// 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 (isFreeAccount)
{
if (result.RequiredHttpHeaders == null)
result.RequiredHttpHeaders = new Dictionary<string, string>();
result.RequiredHttpHeaders["User-Agent"] = "Lavf/60\" -seekable \"0";
}
return result;
}
private static List<MediaStream> VersionToMediaStreams(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams)
{
List<MediaStream> result = new List<MediaStream>();
if (analyzedStreams != null && analyzedStreams.Count != 0)
{
// Use the analyzed streams and just add external subtitles
result.AddRange(analyzedStreams);
if (ver.subtitles != null)
{
foreach (StreamSubtitle j in ver.subtitles)
{
if (!string.IsNullOrEmpty(j.src))
{
MediaStream a = new MediaStream();
a.Index = -1;
a.Type = MediaStreamType.Subtitle;
a.Language = ISO639_1ToISO639_2(j.language);
a.IsForced = j.forced;
a.DeliveryUrl = j.src;
a.IsExternalUrl = true;
result.Add(a);
}
}
}
return result;
}
// 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
// We must assign MediaStream.Index to something at least unique as this
// values is also passed from client (ie. to identify transcoding channel).
// Therefore use clearly insane values. Also -1 seems to be used on many places
// as "no channel" so start with -2.
if (ver.video != null)
{
int uniqueId = FirstRelativeVideoIndex;
foreach (StreamVideo j in ver.video)
{
MediaStream a = new MediaStream();
a.Index = uniqueId--;
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;
result.Add(a);
}
}
if (ver.audio != null)
{
int uniqueId = FirstRelativeAudioIndex;
foreach (StreamAudio j in ver.audio)
{
MediaStream a = new MediaStream();
a.Index = uniqueId--;
a.Type = MediaStreamType.Audio;
a.Language = ISO639_1ToISO639_2(j.language);
a.Codec = j.codec;
a.Channels = j.channels;
result.Add(a);
}
}
if (ver.subtitles != null)
{
int uniqueId = FirstRelativeSubtitleIndex;
foreach (StreamSubtitle j in ver.subtitles)
{
MediaStream a = new MediaStream();
a.Index = uniqueId--;
a.Type = MediaStreamType.Subtitle;
a.Language = ISO639_1ToISO639_2(j.language);
a.IsForced = j.forced;
a.DeliveryUrl = j.src;
a.IsExternalUrl = true;
result.Add(a);
}
}
return result;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
internal static string EncodingHelper_GetMapArgs_New(EncodingHelper @this, EncodingJobInfo state)
{
return "";/*
if ((state.VideoStream == null && state.AudioStream == null)
|| (state.VideoStream != null && state.VideoStream.Index == -1)
|| (state.AudioStream != null && state.AudioStream.Index == -1))
// Use original implementation
// Note: The condition reflects the beginning of the original method
return EncodingHelper_GetMapArgs_Orig(@this, state);
// HACK: Stream indicies below FirstRelativeVideoIndex are misused by VersionToMediaStreams to
// indicate relative indexing for ffmpeg
if ((state.VideoStream?.Index ?? 0) > FirstRelativeVideoIndex
&& (state.AudioStream?.Index ?? 0) > FirstRelativeAudioIndex
&& (state.SubtitleStream?.Index ?? 0) > FirstRelativeSubtitleIndex)
// Use original implementation
return EncodingHelper_GetMapArgs_Orig(@this, state);
// Video stream
string args = "";
if (state.VideoStream == null)
{
// No known video stream
args += "-vn";
}
else if (state.VideoStream.Index <= FirstRelativeVideoIndex)
{
args += string.Format(CultureInfo.InvariantCulture, "-map 0:v:{0}", -(state.VideoStream.Index - FirstRelativeVideoIndex));
}
else
{
int videoStreamIndex = EncodingHelper.FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
args += string.Format(CultureInfo.InvariantCulture, "-map 0:{0}", videoStreamIndex);
}
// Audio stream
if (state.AudioStream == null)
{
args += " -map -0:a";
}
else if (state.AudioStream.Index <= FirstRelativeAudioIndex)
{
args += string.Format(CultureInfo.InvariantCulture, " -map 0:a:{0}", -(state.AudioStream.Index - FirstRelativeAudioIndex));
}
else
{
int audioStreamIndex = EncodingHelper.FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal)
{
throw new InvalidOperationException("Cinema shall not have external audio streams.");
}
else
{
args += string.Format(CultureInfo.InvariantCulture, " -map 0:{0}", audioStreamIndex);
}
}
// Subtitle stream
var subtitleMethod = state.SubtitleDeliveryMethod;
if (state.SubtitleStream == null || subtitleMethod == SubtitleDeliveryMethod.Hls)
{
args += " -map -0:s";
}
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
{
if (state.SubtitleStream.Index <= FirstRelativeSubtitleIndex)
{
args += string.Format(CultureInfo.InvariantCulture, " -map 0:s:{0}", -(state.SubtitleStream.Index - FirstRelativeSubtitleIndex));
}
else
{
int subtitleStreamIndex = EncodingHelper.FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
args += string.Format(CultureInfo.InvariantCulture, " -map 0:{0}", subtitleStreamIndex);
}
}
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
{
int externalSubtitleStreamIndex = EncodingHelper.FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
args += string.Format(CultureInfo.InvariantCulture, " -map 1:{0} -sn", externalSubtitleStreamIndex);
}
// Do not call the original
return args;*/
}
internal static string EncodingHelper_GetMapArgs_Orig(EncodingHelper @this, EncodingJobInfo state)
{
throw new InvalidOperationException();
}
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;
}
public async Task<Uri> GenerateLink(string provider, string ident, string? name, CancellationToken cancel)
{
name = name ?? "";
DateTime now = DateTime.UtcNow;
LinkKey key = new LinkKey(provider, ident, name);
LinkEntry entry;
if (!_links.TryGetValue(key, out entry) || !entry.IsValid(now))
{
Uri link;
switch (provider)
{
case "webshare": link = await LinkGenerator.GenerateDownloadLinkAsync(ident, name, CinemaHost.Webshare, cancel); break;
default: throw new InvalidOperationException();
}
entry = new LinkEntry(now + LinkValidityTimeout, link);
_links[key] = entry;
}
return entry.Link;
}
/// <summary>
/// Converts two-letter 639-1 language code to three letter 639-2.
/// </summary>
private static string? ISO639_1ToISO639_2(string? twoLetter639_1)
{
// Based on https://www.loc.gov/standards/iso639-2/php/code_list-utf8.php
switch (twoLetter639_1)
{
case "aa": return "aar";
case "ab": return "abk";
case "af": return "afr";
case "ak": return "aka";
case "sq": return "alb";
case "am": return "amh";
case "ar": return "ara";
case "an": return "arg";
case "hy": return "arm";
case "as": return "asm";
case "av": return "ava";
case "ae": return "ave";
case "ay": return "aym";
case "az": return "aze";
case "ba": return "bak";
case "bm": return "bam";
case "eu": return "baq";
case "be": return "bel";
case "bn": return "ben";
case "bi": return "bis";
case "bo": return "tib";
case "bs": return "bos";
case "br": return "bre";
case "bg": return "bul";
case "my": return "bur";
case "ca": return "cat";
case "cs": return "cze";
case "ch": return "cha";
case "ce": return "che";
case "zh": return "chi";
case "cu": return "chu";
case "cv": return "chv";
case "kw": return "cor";
case "co": return "cos";
case "cr": return "cre";
case "cy": return "wel";
case "da": return "dan";
case "de": return "ger";
case "dv": return "div";
case "nl": return "dut";
case "dz": return "dzo";
case "el": return "gre";
case "en": return "eng";
case "eo": return "epo";
case "et": return "est";
case "ee": return "ewe";
case "fo": return "fao";
case "fa": return "per";
case "fj": return "fij";
case "fi": return "fin";
case "fr": return "fre";
case "fy": return "fry";
case "ff": return "ful";
case "ka": return "geo";
case "gd": return "gla";
case "ga": return "gle";
case "gl": return "glg";
case "gv": return "glv";
case "gn": return "grn";
case "gu": return "guj";
case "ht": return "hat";
case "ha": return "hau";
case "he": return "heb";
case "hz": return "her";
case "hi": return "hin";
case "ho": return "hmo";
case "hr": return "hrv";
case "hu": return "hun";
case "ig": return "ibo";
case "is": return "ice";
case "io": return "ido";
case "ii": return "iii";
case "iu": return "iku";
case "ie": return "ile";
case "ia": return "ina";
case "id": return "ind";
case "ik": return "ipk";
case "it": return "ita";
case "jv": return "jav";
case "ja": return "jpn";
case "kl": return "kal";
case "kn": return "kan";
case "ks": return "kas";
case "kr": return "kau";
case "kk": return "kaz";
case "km": return "khm";
case "ki": return "kik";
case "rw": return "kin";
case "ky": return "kir";
case "kv": return "kom";
case "kg": return "kon";
case "ko": return "kor";
case "kj": return "kua";
case "ku": return "kur";
case "lo": return "lao";
case "la": return "lat";
case "lv": return "lav";
case "li": return "lim";
case "ln": return "lin";
case "lt": return "lit";
case "lb": return "ltz";
case "lu": return "lub";
case "lg": return "lug";
case "mk": return "mac";
case "mh": return "mah";
case "ml": return "mal";
case "mi": return "mao";
case "mr": return "mar";
case "ms": return "may";
case "mg": return "mlg";
case "mt": return "mlt";
case "mn": return "mon";
case "na": return "nau";
case "nv": return "nav";
case "nr": return "nbl";
case "nd": return "nde";
case "ng": return "ndo";
case "ne": return "nep";
case "nn": return "nno";
case "nb": return "nob";
case "no": return "nor";
case "ny": return "nya";
case "oc": return "oci";
case "oj": return "oji";
case "or": return "ori";
case "om": return "orm";
case "os": return "oss";
case "pa": return "pan";
case "pi": return "pli";
case "pl": return "pol";
case "pt": return "por";
case "ps": return "pus";
case "qu": return "que";
case "rm": return "roh";
case "ro": return "rum";
case "rn": return "run";
case "ru": return "rus";
case "sg": return "sag";
case "sa": return "san";
case "si": return "sin";
case "sk": return "slo";
case "sl": return "slv";
case "se": return "sme";
case "sm": return "smo";
case "sn": return "sna";
case "sd": return "snd";
case "so": return "som";
case "st": return "sot";
case "es": return "spa";
case "sc": return "srd";
case "sr": return "srp";
case "ss": return "ssw";
case "su": return "sun";
case "sw": return "swa";
case "sv": return "swe";
case "ty": return "tah";
case "ta": return "tam";
case "tt": return "tat";
case "te": return "tel";
case "tg": return "tgk";
case "tl": return "tgl";
case "th": return "tha";
case "ti": return "tir";
case "to": return "ton";
case "tn": return "tsn";
case "ts": return "tso";
case "tk": return "tuk";
case "tr": return "tur";
case "tw": return "twi";
case "ug": return "uig";
case "uk": return "ukr";
case "ur": return "urd";
case "uz": return "uzb";
case "ve": return "ven";
case "vi": return "vie";
case "vo": return "vol";
case "wa": return "wln";
case "wo": return "wol";
case "xh": return "xho";
case "yi": return "yid";
case "yo": return "yor";
case "za": return "zha";
case "zu": return "zul";
default: return twoLetter639_1;
}
}
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 MediaSourceInfo? AnalyzedInfo { get; set; }
}
struct LinkKey : IEquatable<LinkKey>
{
private readonly string _provider;
private readonly string _ident;
private readonly string _name;
internal LinkKey(string provider, string ident, string name)
{
this._provider = provider;
this._ident = ident;
this._name = name;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is LinkKey && Equals((LinkKey)obj);
}
public bool Equals(LinkKey other)
{
return _provider == other._provider && _ident == other._ident && _name == other._name;
}
public override int GetHashCode()
{
return _provider.GetHashCode() + 3 * _ident.GetHashCode() + 7 * _name.GetHashCode();
}
}
struct LinkEntry
{
private readonly DateTime _validUntil;
private readonly Uri _link;
internal LinkEntry(DateTime validUntil, Uri link)
{
this._validUntil = validUntil;
this._link = link;
}
public bool IsValid(DateTime now)
{
return _validUntil > now;
}
public Uri Link => _link;
}
}