Obtain mediaSourceId from ASP.NET. Analyze video file before play to get proper stream indicies. For TV Series report only single version based on user preferences.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -20,6 +20,12 @@ using CinemaLib.API;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using LinkGenerator = Cinema.Webshare.LinkGenerator;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
|
||||
@@ -32,6 +38,10 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
private const int FirstRelativeAudioIndex = -5;
|
||||
private const int FirstRelativeSubtitleIndex = -15;
|
||||
|
||||
private const int MediaInspectionFileSize = 16384;
|
||||
|
||||
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;
|
||||
@@ -42,10 +52,16 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
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)
|
||||
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();
|
||||
@@ -56,8 +72,12 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
this._inner = inner;
|
||||
|
||||
this._libraryManager = libraryManager;
|
||||
this._http = http;
|
||||
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>();
|
||||
|
||||
@@ -157,7 +177,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|| string.IsNullOrEmpty(item.ExternalId))
|
||||
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
||||
List<MediaSourceInfo> result = GetPlaybackMediaSources(item, user, false, enablePathSubstitution, default).GetAwaiter().GetResult();
|
||||
List<MediaSourceInfo> result = GetPlaybackMediaSourcesInternal(item, user, false, enablePathSubstitution, needsPreciseStreamIndicies: false, default).GetAwaiter().GetResult();
|
||||
|
||||
// HACK Prevent crash
|
||||
if (result.Count == 0)
|
||||
@@ -166,7 +186,12 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||
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
|
||||
HttpContext? ctx = _http.HttpContext;
|
||||
@@ -187,25 +212,102 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancellationToken);
|
||||
|
||||
IEnumerable<Video> items;
|
||||
bool sortByPrefs;
|
||||
switch (video)
|
||||
{
|
||||
case CinemaMovie movie: items = GetVideoVersionsEnumerate<CinemaMovie>(csId!, movie, videoPrimary!, ver.Versions); break;
|
||||
case CinemaEpisode episode: items = GetVideoVersionsEnumerate<CinemaEpisode>(csId!, episode, videoPrimary!, SortVersionsByPreferences(user, ver.Versions, isFreeAccount)); break;
|
||||
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;
|
||||
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, thisServerBaseUri, isFreeAccount));
|
||||
// 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.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];
|
||||
if (metaVer.AnalyzedInfo == null)
|
||||
{
|
||||
Uri link = await GenerateLink(metaVer.Meta.provider, metaVer.Meta.ident, metaVer.Meta.name, cancellationToken);
|
||||
byte[] dataVer = new byte[MediaInspectionFileSize];
|
||||
int dataLen = await GetPartialDataResourceAsync(link, dataVer, cancellationToken);
|
||||
metaVer.AnalyzedInfo = await AnalyzeVideoStreamDataAsync(dataVer, dataLen, cancellationToken);
|
||||
}
|
||||
result.Add(GetVersionInfo(videoVer, metaVer.Meta, metaVer.AnalyzedInfo.MediaStreams, thisServerBaseUri, isFreeAccount));
|
||||
|
||||
}
|
||||
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, thisServerBaseUri, isFreeAccount));
|
||||
|
||||
if (sortByPrefs && ver.Versions.Length != 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 (video is CinemaEpisode)
|
||||
if (sortByPrefs)
|
||||
{
|
||||
foreach (MediaSourceInfo i in result)
|
||||
{
|
||||
ForceByPreferences(user, i, isFreeAccount);
|
||||
i.Id = videoPrimary!.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -239,11 +341,11 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
/// Sorts media verions by preferences as episodes currently do not support multiple versions
|
||||
/// and the first one gets played.
|
||||
/// </summary>
|
||||
private static VersionEntry[] SortVersionsByPreferences(User user, VersionEntry[] items, bool isFreeAccount)
|
||||
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 items;
|
||||
return null;
|
||||
|
||||
double? bitrateLimit = isFreeAccount ? WebshareFreeBitrate : null;
|
||||
string? audioLang = user.AudioLanguagePreference;
|
||||
@@ -286,12 +388,10 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
scores[i] = score;
|
||||
}
|
||||
|
||||
Array.Sort(scores, items);
|
||||
Array.Reverse(items);
|
||||
return items;
|
||||
return scores;
|
||||
}
|
||||
|
||||
private static void ForceByPreferences(User user, MediaSourceInfo item, bool isFreeAccount)
|
||||
private static void ForceByPreferences(User? user, MediaSourceInfo item, bool isFreeAccount)
|
||||
{
|
||||
if (user == null)
|
||||
// We have no info about the user so nothing to force
|
||||
@@ -472,17 +572,17 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
bool isFreeAccount = await CinemaHost.IsWebshareFreeAccount(cancel);
|
||||
|
||||
return GetVersionInfo(item, ver.Meta, thisServerBaseUri, isFreeAccount);
|
||||
return GetVersionInfo(item, ver.Meta, null, thisServerBaseUri, isFreeAccount);
|
||||
}
|
||||
|
||||
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, Uri thisServerBaseUri, bool isFreeAccount)
|
||||
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, IReadOnlyList<MediaStream>? analyzedStreams, Uri thisServerBaseUri, bool isFreeAccount)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
@@ -545,9 +645,34 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<MediaStream> VersionToMediaStreams(Video item, CinemaLib.API.Stream ver)
|
||||
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;
|
||||
@@ -611,7 +736,8 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
a.Type = MediaStreamType.Subtitle;
|
||||
a.Language = ISO639_1ToISO639_2(j.language);
|
||||
a.IsForced = j.forced;
|
||||
a.Path = j.src;
|
||||
a.DeliveryUrl = j.src;
|
||||
a.IsExternalUrl = true;
|
||||
|
||||
result.Add(a);
|
||||
}
|
||||
@@ -620,8 +746,10 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
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))
|
||||
@@ -703,7 +831,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
}
|
||||
|
||||
// Do not call the original
|
||||
return args;
|
||||
return args;*/
|
||||
}
|
||||
|
||||
internal static string EncodingHelper_GetMapArgs_Orig(EncodingHelper @this, EncodingJobInfo state)
|
||||
@@ -981,6 +1109,49 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> GetPartialDataResourceAsync(Uri uri, byte[] buffer, CancellationToken cancel)
|
||||
{
|
||||
using (HttpResponseMessage res = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancel))
|
||||
using (System.IO.Stream resBody = await res.Content.ReadAsStreamAsync(cancel))
|
||||
{
|
||||
int read, offset = 0;
|
||||
while ((read = await resBody.ReadAsync(buffer, offset, buffer.Length - offset)) != 0)
|
||||
offset += read;
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> AnalyzeVideoStreamDataAsync(byte[] partialVideoData, int partialDataSize, CancellationToken cancel)
|
||||
{
|
||||
MediaSourceInfo result;
|
||||
string path = Path.Combine(_serverConfigurationManager.GetTranscodePath(), "cnm-" + Guid.NewGuid().ToString("N"));
|
||||
FileStream partialVideoDataS = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
|
||||
try
|
||||
{
|
||||
await partialVideoDataS.WriteAsync(partialVideoData, 0, partialDataSize, cancel);
|
||||
await partialVideoDataS.FlushAsync(cancel);
|
||||
|
||||
result = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest()
|
||||
{
|
||||
MediaType = DlnaProfileType.Video,
|
||||
ExtractChapters = false,
|
||||
MediaSource = new MediaSourceInfo()
|
||||
{
|
||||
Path = path,
|
||||
Protocol = MediaProtocol.File,
|
||||
VideoType = VideoType.VideoFile,
|
||||
}
|
||||
}, cancel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
partialVideoDataS.Close();
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
struct VersionSetEntry
|
||||
{
|
||||
internal DateTime ValidUntil;
|
||||
@@ -998,7 +1169,7 @@ sealed class CinemaMediaSourceManager : IMediaSourceManager
|
||||
|
||||
internal CinemaLib.API.Stream Meta => _meta;
|
||||
|
||||
internal Uri? DownloadLink { get; set; }
|
||||
internal MediaSourceInfo? AnalyzedInfo { get; set; }
|
||||
}
|
||||
|
||||
struct LinkKey : IEquatable<LinkKey>
|
||||
|
||||
@@ -25,14 +25,6 @@ public class CinemaPlugin : BasePlugin<CinemaPluginConfiguration>, IHasWebPages
|
||||
: base(applicationPaths, xmlSerializer)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
// Perform patching
|
||||
MethodInfo encodingGetMapArgs_orig = ((Func<EncodingJobInfo, string>)new EncodingHelper(null, null, null, null, null).GetMapArgs).Method;
|
||||
MethodInfo encodingGetMapArgs_new = ((Func<EncodingHelper, EncodingJobInfo, string>)CinemaMediaSourceManager.EncodingHelper_GetMapArgs_New).Method;
|
||||
MethodInfo encodingGetMapArgs_proxy = ((Func<EncodingHelper, EncodingJobInfo, string>)CinemaMediaSourceManager.EncodingHelper_GetMapArgs_Orig).Method;
|
||||
|
||||
RedirectTo(encodingGetMapArgs_proxy, encodingGetMapArgs_orig);
|
||||
RedirectTo(encodingGetMapArgs_orig, encodingGetMapArgs_new);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -61,64 +53,5 @@ public class CinemaPlugin : BasePlugin<CinemaPluginConfiguration>, IHasWebPages
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static void RedirectTo(MethodInfo origin, MethodInfo target)
|
||||
{
|
||||
IntPtr ori = GetMethodAddress(origin);
|
||||
IntPtr tar = GetMethodAddress(target);
|
||||
|
||||
Marshal.Copy(new IntPtr[] { Marshal.ReadIntPtr(tar) }, 0, ori, 1);
|
||||
}
|
||||
|
||||
private static IntPtr GetMethodAddress(MethodInfo mi)
|
||||
{
|
||||
const ushort SLOT_NUMBER_MASK = 0xffff; // 2 bytes mask
|
||||
const int MT_OFFSET_32BIT = 0x28; // 40 bytes offset
|
||||
const int MT_OFFSET_64BIT = 0x40; // 64 bytes offset
|
||||
|
||||
IntPtr address;
|
||||
|
||||
// JIT compilation of the method
|
||||
RuntimeHelpers.PrepareMethod(mi.MethodHandle);
|
||||
|
||||
IntPtr md = mi.MethodHandle.Value; // MethodDescriptor address
|
||||
IntPtr mt = mi.DeclaringType!.TypeHandle.Value; // MethodTable address
|
||||
|
||||
bool isNetFramework = RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework");
|
||||
|
||||
if (mi.IsVirtual)
|
||||
{
|
||||
// The fixed-size portion of the MethodTable structure depends on the process type
|
||||
int offset = IntPtr.Size == 4 ? MT_OFFSET_32BIT : MT_OFFSET_64BIT;
|
||||
|
||||
// First method slot = MethodTable address + fixed-size offset
|
||||
// This is the address of the first method of any type (i.e. ToString)
|
||||
IntPtr ms = Marshal.ReadIntPtr(mt + offset);
|
||||
|
||||
// Get the slot number of the virtual method entry from the MethodDesc data structure
|
||||
long shift = Marshal.ReadInt64(md) >> 32;
|
||||
int slot = (int)(shift & SLOT_NUMBER_MASK);
|
||||
|
||||
// Get the virtual method address relative to the first method slot
|
||||
address = ms + (slot * IntPtr.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bypass default MethodDescriptor padding (8 bytes)
|
||||
// Reach the CodeOrIL field which contains the address of the JIT-compiled code
|
||||
if (isNetFramework)
|
||||
address = md + 8;
|
||||
else
|
||||
address = md + 16;
|
||||
}
|
||||
|
||||
nint a = mi.MethodHandle.GetFunctionPointer();
|
||||
nint b = Marshal.ReadIntPtr(address);
|
||||
if (b != a)
|
||||
throw new InvalidOperationException("Method patching primitives are broken.");
|
||||
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jellyfin.Plugin.Cinema;
|
||||
@@ -60,5 +62,73 @@ public class CinemaServiceRegistrator : IPluginServiceRegistrator
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Extract values for some API calls that are otherwise inaccessible
|
||||
serviceCollection.AddMvc(options =>
|
||||
{
|
||||
options.Conventions.Add(new MediaSourceIdExtractConvention());
|
||||
});
|
||||
}
|
||||
|
||||
sealed class MediaSourceIdExtractConvention : IApplicationModelConvention
|
||||
{
|
||||
public void Apply(ApplicationModel application)
|
||||
{
|
||||
ControllerModel? mediaInfo = application.Controllers.Where(x => x.ControllerName == "MediaInfo").FirstOrDefault();
|
||||
ControllerModel? dynamicHls = application.Controllers.Where(x => x.ControllerName == "DynamicHls").FirstOrDefault();
|
||||
ActionModel? getPostedPlaybackInfo, getMasterHlsVideoPlaylist, getVariantHlsVideoPlaylist, getHlsVideoSegment;
|
||||
if (mediaInfo == null
|
||||
|| (getPostedPlaybackInfo = mediaInfo.Actions.Where(x => x.ActionName == "GetPostedPlaybackInfo").FirstOrDefault()) == null
|
||||
|| dynamicHls == null
|
||||
|| (getMasterHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetMasterHlsVideoPlaylist").FirstOrDefault()) == null
|
||||
|| (getVariantHlsVideoPlaylist = dynamicHls.Actions.Where(x => x.ActionName == "GetVariantHlsVideoPlaylist").FirstOrDefault()) == null
|
||||
|| (getHlsVideoSegment = dynamicHls.Actions.Where(x => x.ActionName == "GetHlsVideoSegment").FirstOrDefault()) == null)
|
||||
throw new InvalidOperationException("Failed to register for MediaSourceId extraction from the Jellyfin's MediaInfo controller.");
|
||||
|
||||
getPostedPlaybackInfo.Filters.Add(new GetPostedPlaybackInfoMediaSourceIdFilter());
|
||||
// Note: All DynamicHls controller actions share the same parameter name so we can
|
||||
// re-use the same filter for all
|
||||
getMasterHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||
getVariantHlsVideoPlaylist.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||
getHlsVideoSegment.Filters.Add(new DynamicHlsMediaSourceIdFilter());
|
||||
}
|
||||
}
|
||||
|
||||
sealed class GetPostedPlaybackInfoMediaSourceIdFilter : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
string? mediaSourceId;
|
||||
if (!context.ActionArguments.TryGetValue("playbackInfoDto", out object? playbackInfoO)
|
||||
|| (mediaSourceId = playbackInfoO!.GetType().GetProperty("MediaSourceId")?.GetValue(playbackInfoO) as string) == null)
|
||||
{
|
||||
// Fallback to the obsolete query argument
|
||||
if (!context.ActionArguments.TryGetValue("mediaSourceId", out object? mediaSourceIdO)
|
||||
|| (mediaSourceId = mediaSourceIdO as string) == null)
|
||||
throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's action MediaInfo.GetPostedPlaybackInfo.");
|
||||
}
|
||||
|
||||
context.HttpContext.Items.Add(CinemaMediaSourceManager.ContextItemsMediaSourceIdKey, mediaSourceId);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DynamicHlsMediaSourceIdFilter : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
string? mediaSourceId;
|
||||
if (!context.ActionArguments.TryGetValue("mediaSourceId", out object? mediaSourceIdO)
|
||||
|| (mediaSourceId = mediaSourceIdO as string) == null)
|
||||
throw new InvalidOperationException("Cannot extract MediaSourceId from the Jellyfin's controller DynamicHls.");
|
||||
|
||||
context.HttpContext.Items.Add(CinemaMediaSourceManager.ContextItemsMediaSourceIdKey, mediaSourceId);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user