Playback of correct version stream works
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-11-30 03:21:13 +01:00
parent 0bcb6d571f
commit c28537e25c
4 changed files with 172 additions and 107 deletions

View File

@@ -330,6 +330,7 @@ public abstract class CinemaFilterFolder : Folder
item.ProviderIds = new Dictionary<string, string>();
item.ProviderIds[CinemaPlugin.CinemaProviderName] = "";
// Indicate just HTTP CinemaMediaSourceManager and CinemaMediaSourceController will handle the rest
item.Path = "https://a/b";
if (item is IHasArtist hasArtists)

View File

@@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.Cinema;
/// <summary>
/// The artists controller.
/// </summary>
[Route("Cinema")]
[ApiController]
public class CinemaMediaSourceController : ControllerBase
{
private readonly CinemaMediaSourceManager _mediaManager;
public CinemaMediaSourceController(CinemaMediaSourceManager mediaManager)
{
_mediaManager = mediaManager;
}
/// <summary>
/// Generates a download link using the given provider and its identifier.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <response code="302">External resource redirect.</response>
/// <response code="404">Link not found.</response>
[HttpGet("{provider}/{ident}/{name}/link")]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetExternalIdInfos(
[FromRoute, Required] string provider,
[FromRoute, Required] string ident,
[FromRoute, Optional] string? name,
CancellationToken cancel)
{
try
{
return Redirect((await _mediaManager.GenerateLink(provider, ident, name, cancel)).ToString());
}
catch (Exception)
{
return NotFound();
}
}
}

View File

@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Cinema.Webshare;
using CinemaLib.API;
using MediaBrowser.Controller;
namespace Jellyfin.Plugin.Cinema;
@@ -28,15 +29,18 @@ public class CinemaMediaSourceManager : IMediaSourceManager
private const double WebshareFreeBitrate = 300000 * 8;
private static readonly TimeSpan VersionValidityTimeout = TimeSpan.FromMinutes(180);
private static readonly TimeSpan LinkValidityTimeout = VersionValidityTimeout;
private static CinemaMediaSourceManager? _instance;
private readonly IMediaSourceManager _inner;
private readonly ILibraryManager _libraryManager;
private readonly IServerApplicationHost _host;
private readonly IHttpContextAccessor _http;
private readonly ConcurrentDictionary<Guid, VersionSetEntry> _videoVersions;
private readonly ConcurrentDictionary<LinkKey, LinkEntry> _links;
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServiceProvider svc, IHttpContextAccessor http)
public CinemaMediaSourceManager(ICinemaInnerMediaSourceManager innerMediaSourceManager, ILibraryManager libraryManager, IServerApplicationHost host, IServiceProvider svc, IHttpContextAccessor http)
{
if (innerMediaSourceManager == null || svc == null)
throw new ArgumentNullException();
@@ -48,7 +52,9 @@ public class CinemaMediaSourceManager : IMediaSourceManager
this._libraryManager = libraryManager;
this._http = http;
this._host = host;
this._videoVersions = new ConcurrentDictionary<Guid, VersionSetEntry>();
this._links = new ConcurrentDictionary<LinkKey, LinkEntry>();
_instance = this;
}
@@ -152,17 +158,40 @@ public class CinemaMediaSourceManager : IMediaSourceManager
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User? user = null)
{
// Intercept for CinemaItems
if (item == null
HttpContext? ctx = _http.HttpContext;
if (ctx == null
|| item == null
|| item.ProviderIds == null
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|| string.IsNullOrEmpty(item.ExternalId))
return _inner.GetStaticMediaSources(item, enablePathSubstitution, user);
List<MediaSourceInfo> result = GetPlaybackMediaSources(item, user, false, enablePathSubstitution, default).GetAwaiter().GetResult();
// 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
HttpContext? ctx = _http.HttpContext;
if (ctx == null
|| item == null
|| item.ProviderIds == null
|| !item.ProviderIds.ContainsKey(CinemaPlugin.CinemaProviderName)
|| string.IsNullOrEmpty(item.ExternalId))
return await _inner.GetPlaybackMediaSources(item, user, allowMediaProbe, enablePathSubstitution, cancellationToken);
List<MediaSourceInfo> result = new List<MediaSourceInfo>();
Uri thisServerBaseUri = new Uri(_host.GetSmartApiUrl(ctx.Request));
switch (item)
{
case Video video:
VersionSetEntry ver = GetVersionSet(video, out BaseItem? videoPrimary, default).GetAwaiter().GetResult();
VersionSetEntry ver = await GetVersionSet(video, out BaseItem? videoPrimary, default);
if (ver.Versions != null)
{
IEnumerable<Video> items;
@@ -176,73 +205,7 @@ public class CinemaMediaSourceManager : IMediaSourceManager
// 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);
result.Add(GetVersionInfo(i, ver.Versions[idx++].Meta, thisServerBaseUri));
}
break;
}
@@ -370,11 +333,13 @@ public class CinemaMediaSourceManager : IMediaSourceManager
if (!_videoVersions.TryGetValue(primaryId, out result) || result.ValidUntil < now)
{
var a = await Metadata.StreamsAsync(csId, cancel);
if (a != null) {
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
}
else
result.Versions = null;
result.ValidUntil = now + VersionValidityTimeout;
_videoVersions[primaryId] = result;
@@ -412,14 +377,19 @@ public class CinemaMediaSourceManager : IMediaSourceManager
if (item == null)
throw new ArgumentNullException();
HttpContext? ctx = _http.HttpContext;
if (ctx == null)
throw new InvalidOperationException("HttpContext required in GetVersionInfo");
Uri thisServerBaseUri = new Uri(_host.GetSmartApiUrl(ctx.Request));
VersionEntry? ver = await GetVersion(item, cancel);
if (ver == null)
return null;
return GetVersionInfo(item, ver.Meta);
return GetVersionInfo(item, ver.Meta, thisServerBaseUri);
}
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver)
private MediaSourceInfo GetVersionInfo(Video item, CinemaLib.API.Stream ver, Uri thisServerBaseUri)
{
MediaSourceInfo result = new MediaSourceInfo();
result.VideoType = VideoType.VideoFile;
@@ -469,12 +439,21 @@ public class CinemaMediaSourceManager : IMediaSourceManager
}
}
result.Name = name;
//Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
result.Path = $"{thisServerBaseUri.ToString()}Cinema/{Uri.EscapeDataString(ver.provider)}/{Uri.EscapeDataString(ver.ident)}/{Uri.EscapeDataString(ver.name)}/link";
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 (IsWebshareFreeAccount)
{
if (result.RequiredHttpHeaders == null)
result.RequiredHttpHeaders = new Dictionary<string, string>();
result.RequiredHttpHeaders["User-Agent"] = "Lavf/60\" -seekable \"0";
}
return result;
}
@@ -593,6 +572,28 @@ public class CinemaMediaSourceManager : IMediaSourceManager
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, cancel); break;
default: throw new InvalidOperationException();
}
entry = new LinkEntry(now + LinkValidityTimeout, link);
_links[key] = entry;
}
return entry.Link;
}
struct VersionSetEntry
{
internal DateTime ValidUntil;
@@ -612,4 +613,52 @@ public class CinemaMediaSourceManager : IMediaSourceManager
internal Uri? DownloadLink { 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;
}
}

View File

@@ -1,31 +0,0 @@
/*
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using CinemaLib.API;
namespace Jellyfin.Plugin.Cinema;
/// <summary>
/// Media source provider for for Cinema media items.
/// </summary>
public class CinemaMediaSourceProvider : IMediaSourceProvider
{
/// <inheritdoc />
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
return GetMediaSourcesInternal(item, cancellationToken);
}
/// <inheritdoc />
public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
}
*/