This commit is contained in:
@@ -15,128 +15,128 @@ namespace Jellyfin.Plugin.Cinema;
|
||||
/// </summary>
|
||||
sealed class CinemaMediaAnalyzer
|
||||
{
|
||||
private const int MaxAnalyzeSizeBase = 1 * 1024 * 1024;
|
||||
private const int MaxAnalyzeSizeDivisor = 1024 * 1024 / 4;
|
||||
private const int MaxAnalyzeSizeLimit = 32 * 1024 * 1024;
|
||||
private const int MaxAnalyzeSizeBase = 1 * 1024 * 1024;
|
||||
private const int MaxAnalyzeSizeDivisor = 1024 * 1024 / 64;
|
||||
private const int MaxAnalyzeSizeLimit = 32 * 1024 * 1024;
|
||||
|
||||
private const int WaitIfNoProgressMs = 50; // ms
|
||||
private const int WaitIfNoProgressMs = 50; // ms
|
||||
|
||||
private static readonly Task DownloadFinishedTask = Task.Delay(0);
|
||||
private static readonly Task DownloadFinishedTask = Task.Delay(0);
|
||||
|
||||
private Task _currentReadTask;
|
||||
private Task _currentReadTask;
|
||||
|
||||
private CinemaMediaAnalyzer()
|
||||
{
|
||||
this._currentReadTask = Task.CompletedTask;
|
||||
}
|
||||
private CinemaMediaAnalyzer()
|
||||
{
|
||||
this._currentReadTask = Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task VolatileCurrentReadTask
|
||||
{
|
||||
get
|
||||
{
|
||||
// Note: The comparison always fails. We need just a volatile read.
|
||||
return Interlocked.CompareExchange(ref _currentReadTask, null!, null);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException();
|
||||
Interlocked.Exchange(ref _currentReadTask, value);
|
||||
}
|
||||
}
|
||||
private Task VolatileCurrentReadTask
|
||||
{
|
||||
get
|
||||
{
|
||||
// Note: The comparison always fails. We need just a volatile read.
|
||||
return Interlocked.CompareExchange(ref _currentReadTask, null!, null);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException();
|
||||
Interlocked.Exchange(ref _currentReadTask, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<MediaSourceInfo> AnalyzeResourceAsync(Uri resource, long? fileSize, CinemaLib.API.Stream meta, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder, IServerConfigurationManager serverConfigurationManager, CancellationToken cancel)
|
||||
{
|
||||
if (resource == null || meta == null || httpClientFactory == null || mediaEncoder == null || serverConfigurationManager == null)
|
||||
throw new ArgumentNullException();
|
||||
public static Task<MediaSourceInfo> AnalyzeResourceAsync(Uri resource, long? fileSize, CinemaLib.API.Stream meta, IHttpClientFactory httpClientFactory, IMediaEncoder mediaEncoder, IServerConfigurationManager serverConfigurationManager, CancellationToken cancel)
|
||||
{
|
||||
if (resource == null || meta == null || httpClientFactory == null || mediaEncoder == null || serverConfigurationManager == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
string tmpFilePath = Path.Combine(serverConfigurationManager.GetTranscodePath(), "cnm-" + Guid.NewGuid().ToString("N"));
|
||||
HttpClient http = httpClientFactory.CreateClient(NamedClient.Default);
|
||||
string tmpFilePath = Path.Combine(serverConfigurationManager.GetTranscodePath(), "cnm-" + Guid.NewGuid().ToString("N"));
|
||||
HttpClient http = httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
||||
return new CinemaMediaAnalyzer().AnalyzeAsyncInternal(resource, http, tmpFilePath, fileSize, meta, mediaEncoder, cancel);
|
||||
}
|
||||
return new CinemaMediaAnalyzer().AnalyzeAsyncInternal(resource, http, tmpFilePath, fileSize, meta, mediaEncoder, cancel);
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> AnalyzeAsyncInternal(Uri resource, HttpClient http, string tmpFilePath, long? fileSize, CinemaLib.API.Stream meta, IMediaEncoder mediaEncoder, CancellationToken cancel)
|
||||
{
|
||||
int? maxAnalyzeSize = MaxAnalyzeSizeBase + (int?)(fileSize / MaxAnalyzeSizeDivisor);
|
||||
if (maxAnalyzeSize == null || maxAnalyzeSize > MaxAnalyzeSizeLimit)
|
||||
maxAnalyzeSize = MaxAnalyzeSizeLimit;
|
||||
public async Task<MediaSourceInfo> AnalyzeAsyncInternal(Uri resource, HttpClient http, string tmpFilePath, long? fileSize, CinemaLib.API.Stream meta, IMediaEncoder mediaEncoder, CancellationToken cancel)
|
||||
{
|
||||
int? maxAnalyzeSize = MaxAnalyzeSizeBase + (int?)(fileSize / MaxAnalyzeSizeDivisor);
|
||||
if (maxAnalyzeSize == null || maxAnalyzeSize > MaxAnalyzeSizeLimit)
|
||||
maxAnalyzeSize = MaxAnalyzeSizeLimit;
|
||||
|
||||
// Run two tasks cooperatively in parallel so we can stop
|
||||
// downloading (possibly at slow speed) as soon as we analyze
|
||||
// sufficient data. We try analyzing repeatedly after each chunk
|
||||
// of data is received as we consider it cheap in contrast to
|
||||
// downloading the data over network.
|
||||
MediaSourceInfo? result = null;
|
||||
CancellationTokenSource innerCancel = new CancellationTokenSource();
|
||||
using (CancellationTokenRegistration cancelReg = cancel.Register(() => innerCancel.Cancel()))
|
||||
{
|
||||
Stream tmpFile = new FileStream(tmpFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
|
||||
Task? tDownload = null;
|
||||
Task<MediaSourceInfo?>? tProbe = null;
|
||||
try
|
||||
{
|
||||
tDownload = GetPartialDataAsync(resource, http, tmpFile, maxAnalyzeSize.Value, innerCancel.Token);
|
||||
tProbe = ProbeDataAsync(meta, tmpFilePath, mediaEncoder, innerCancel.Token);
|
||||
// Run two tasks cooperatively in parallel so we can stop
|
||||
// downloading (possibly at slow speed) as soon as we analyze
|
||||
// sufficient data. We try analyzing repeatedly after each chunk
|
||||
// of data is received as we consider it cheap in contrast to
|
||||
// downloading the data over network.
|
||||
MediaSourceInfo? result = null;
|
||||
CancellationTokenSource innerCancel = new CancellationTokenSource();
|
||||
using (CancellationTokenRegistration cancelReg = cancel.Register(() => innerCancel.Cancel()))
|
||||
{
|
||||
Stream tmpFile = new FileStream(tmpFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
|
||||
Task? tDownload = null;
|
||||
Task<MediaSourceInfo?>? tProbe = null;
|
||||
try
|
||||
{
|
||||
tDownload = GetPartialDataAsync(resource, http, tmpFile, maxAnalyzeSize.Value, innerCancel.Token);
|
||||
tProbe = ProbeDataAsync(meta, tmpFilePath, mediaEncoder, innerCancel.Token);
|
||||
|
||||
// Get the data we want
|
||||
result = await tProbe;
|
||||
// Get the data we want
|
||||
result = await tProbe;
|
||||
|
||||
// Cancel downloading
|
||||
innerCancel.Cancel();
|
||||
await tDownload;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Cancel the other task
|
||||
innerCancel.Cancel();
|
||||
// Cancel downloading
|
||||
innerCancel.Cancel();
|
||||
await tDownload;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Cancel the other task
|
||||
innerCancel.Cancel();
|
||||
|
||||
// Wait for completition of the other one so
|
||||
// we can delete the temporary file
|
||||
try
|
||||
{
|
||||
Task? t = (tDownload?.IsCompleted ?? true) ? tProbe : tDownload;
|
||||
if (t != null)
|
||||
await t;
|
||||
}
|
||||
catch (Exception) { }
|
||||
// Wait for completition of the other one so
|
||||
// we can delete the temporary file
|
||||
try
|
||||
{
|
||||
Task? t = (tDownload?.IsCompleted ?? true) ? tProbe : tDownload;
|
||||
if (t != null)
|
||||
await t;
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
// Only throw if we did not get result from ProbeDataAsync as GetPartialDataAsync
|
||||
// most probably threw due to task cancellation.
|
||||
if (result == null)
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
tmpFile.Close();
|
||||
File.Delete(tmpFilePath);
|
||||
}
|
||||
}
|
||||
// Only throw if we did not get result from ProbeDataAsync as GetPartialDataAsync
|
||||
// most probably threw due to task cancellation.
|
||||
if (result == null)
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
tmpFile.Close();
|
||||
File.Delete(tmpFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
if (result == null)
|
||||
// Shall be null only if cancellation has been triggered
|
||||
throw new InvalidOperationException();
|
||||
if (result == null)
|
||||
// Shall be null only if cancellation has been triggered
|
||||
throw new InvalidOperationException();
|
||||
|
||||
// Fix the bitrates as some may have been calculated from the tiny file size
|
||||
int? fileSizeBasedBitrate = (int?)(meta.size * 8 / meta.video?.FirstOrDefault()?.duration);
|
||||
if (fileSizeBasedBitrate == null)
|
||||
fileSizeBasedBitrate = (int?)(meta.size * 8 / (90 * 60)); // assume 90 minute movie
|
||||
// Fix the bitrates as some may have been calculated from the tiny file size
|
||||
int? fileSizeBasedBitrate = (int?)(meta.size * 8 / meta.video?.FirstOrDefault()?.duration);
|
||||
if (fileSizeBasedBitrate == null)
|
||||
fileSizeBasedBitrate = (int?)(meta.size * 8 / (90 * 60)); // assume 90 minute movie
|
||||
|
||||
foreach (MediaStream i in result.MediaStreams)
|
||||
switch (i.Type)
|
||||
{
|
||||
case MediaStreamType.Video:
|
||||
if (i.BitRate == null || i.BitRate.Value < fileSizeBasedBitrate / 2)
|
||||
i.BitRate = fileSizeBasedBitrate;
|
||||
break;
|
||||
foreach (MediaStream i in result.MediaStreams)
|
||||
switch (i.Type)
|
||||
{
|
||||
case MediaStreamType.Video:
|
||||
if (i.BitRate == null || i.BitRate.Value < fileSizeBasedBitrate / 2)
|
||||
i.BitRate = fileSizeBasedBitrate;
|
||||
break;
|
||||
|
||||
case MediaStreamType.Audio:
|
||||
if (i.BitRate == null || i.BitRate < 16 * 1024)
|
||||
i.BitRate = 16 * 1024;
|
||||
break;
|
||||
}
|
||||
case MediaStreamType.Audio:
|
||||
if (i.BitRate == null || i.BitRate < 16 * 1024)
|
||||
i.BitRate = 16 * 1024;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add unrecognized bogus streams so stream Index values are continous and sorted in the
|
||||
// ascending order as implementation of EncodingHelper.FindIndex is extremely stupid.
|
||||
@@ -163,103 +163,114 @@ sealed class CinemaMediaAnalyzer
|
||||
result.MediaStreams = streams;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task GetPartialDataAsync(Uri src, HttpClient http, Stream dst, int maxLength, CancellationToken cancel)
|
||||
{
|
||||
byte[] buffer = new byte[65536];
|
||||
private async Task GetPartialDataAsync(Uri src, HttpClient http, Stream dst, int maxLength, CancellationToken cancel)
|
||||
{
|
||||
byte[] buffer = new byte[65536];
|
||||
|
||||
// Let the other task have something to await for
|
||||
var tReq = http.GetAsync(src, HttpCompletionOption.ResponseHeadersRead, cancel);
|
||||
VolatileCurrentReadTask = tReq;
|
||||
int readTotal = 0;
|
||||
// Let the other task have something to await for
|
||||
var tReq = http.GetAsync(src, HttpCompletionOption.ResponseHeadersRead, cancel);
|
||||
VolatileCurrentReadTask = tReq;
|
||||
int readTotal = 0;
|
||||
|
||||
using (HttpResponseMessage res = await tReq)
|
||||
{
|
||||
var tStream = res.Content.ReadAsStreamAsync(cancel);
|
||||
VolatileCurrentReadTask = tStream;
|
||||
using (Stream resBody = await tStream)
|
||||
{
|
||||
while (readTotal < maxLength && !cancel.IsCancellationRequested)
|
||||
{
|
||||
var tRead = resBody.ReadAsync(buffer, 0, Math.Min(buffer.Length, maxLength - readTotal), cancel);
|
||||
VolatileCurrentReadTask = tRead;
|
||||
using (HttpResponseMessage res = await tReq)
|
||||
{
|
||||
var tStream = res.Content.ReadAsStreamAsync(cancel);
|
||||
VolatileCurrentReadTask = tStream;
|
||||
using (Stream resBody = await tStream)
|
||||
{
|
||||
while (readTotal < maxLength && !cancel.IsCancellationRequested)
|
||||
{
|
||||
var tRead = resBody.ReadAsync(buffer, 0, Math.Min(buffer.Length, maxLength - readTotal), cancel);
|
||||
VolatileCurrentReadTask = tRead;
|
||||
|
||||
int read = await tRead;
|
||||
int read = await tRead;
|
||||
|
||||
if (read == 0)
|
||||
break;
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
await dst.WriteAsync(buffer, 0, read, cancel);
|
||||
await dst.WriteAsync(buffer, 0, read, cancel);
|
||||
|
||||
readTotal += read;
|
||||
readTotal += read;
|
||||
|
||||
await dst.FlushAsync(cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
await dst.FlushAsync(cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate no more data will arrive
|
||||
VolatileCurrentReadTask = DownloadFinishedTask;
|
||||
}
|
||||
// Indicate no more data will arrive
|
||||
VolatileCurrentReadTask = DownloadFinishedTask;
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo?> ProbeDataAsync(CinemaLib.API.Stream meta, string dataPath, IMediaEncoder mediaEncoder, CancellationToken cancel)
|
||||
{
|
||||
// The hardest thing to get is the video PixelFormat so
|
||||
// continue cycling until we resolve it.
|
||||
Task lastReadTask = _currentReadTask;
|
||||
Task toWait = lastReadTask;
|
||||
while (!cancel.IsCancellationRequested)
|
||||
{
|
||||
await toWait;
|
||||
if (cancel.IsCancellationRequested)
|
||||
break;
|
||||
private async Task<MediaSourceInfo?> ProbeDataAsync(CinemaLib.API.Stream meta, string dataPath, IMediaEncoder mediaEncoder, CancellationToken cancel)
|
||||
{
|
||||
// The hardest thing to get is the video PixelFormat so
|
||||
// continue cycling until we resolve it.
|
||||
Task lastReadTask = _currentReadTask;
|
||||
Task toWait = lastReadTask;
|
||||
while (!cancel.IsCancellationRequested)
|
||||
{
|
||||
await toWait;
|
||||
if (cancel.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (toWait == lastReadTask)
|
||||
{
|
||||
// Do the probe
|
||||
MediaSourceInfo result = await mediaEncoder.GetMediaInfo(new MediaInfoRequest()
|
||||
{
|
||||
MediaType = DlnaProfileType.Video,
|
||||
ExtractChapters = false,
|
||||
MediaSource = new MediaSourceInfo()
|
||||
{
|
||||
Path = dataPath,
|
||||
Protocol = MediaProtocol.File,
|
||||
VideoType = VideoType.VideoFile,
|
||||
}
|
||||
}, cancel);
|
||||
if (toWait == lastReadTask)
|
||||
{
|
||||
// Do the probe
|
||||
MediaSourceInfo? result;
|
||||
try
|
||||
{
|
||||
result = await mediaEncoder.GetMediaInfo(new MediaInfoRequest()
|
||||
{
|
||||
MediaType = DlnaProfileType.Video,
|
||||
ExtractChapters = false,
|
||||
MediaSource = new MediaSourceInfo()
|
||||
{
|
||||
Path = dataPath,
|
||||
Protocol = MediaProtocol.File,
|
||||
VideoType = VideoType.VideoFile,
|
||||
}
|
||||
}, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = null;
|
||||
}
|
||||
|
||||
// Evaluate the quality of the probe
|
||||
bool allVideoStreamsHavePixelFormat = true;
|
||||
int videoCount = 0;
|
||||
foreach (MediaStream i in result.MediaStreams)
|
||||
if (i.Type == MediaStreamType.Video)
|
||||
{
|
||||
allVideoStreamsHavePixelFormat &= !string.IsNullOrEmpty(i.PixelFormat);
|
||||
videoCount++;
|
||||
}
|
||||
// Evaluate the quality of the probe
|
||||
if (result != null)
|
||||
{
|
||||
bool allVideoStreamsHavePixelFormat = true;
|
||||
int videoCount = 0;
|
||||
foreach (MediaStream i in result.MediaStreams)
|
||||
if (i.Type == MediaStreamType.Video)
|
||||
{
|
||||
allVideoStreamsHavePixelFormat &= !string.IsNullOrEmpty(i.PixelFormat);
|
||||
videoCount++;
|
||||
}
|
||||
|
||||
if (allVideoStreamsHavePixelFormat && videoCount != 0)
|
||||
return result;
|
||||
}
|
||||
if (allVideoStreamsHavePixelFormat && videoCount != 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (cancel.IsCancellationRequested)
|
||||
break;
|
||||
if (cancel.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Get something to wait for
|
||||
// This has been just a synthetic wait
|
||||
toWait = VolatileCurrentReadTask;
|
||||
if (toWait == lastReadTask)
|
||||
// Still nothing waitable from the downloader
|
||||
toWait = Task.Delay(WaitIfNoProgressMs);
|
||||
else if (lastReadTask == DownloadFinishedTask)
|
||||
throw new IOException("Video file ended before stream metadata could be fully exctracted");
|
||||
else
|
||||
lastReadTask = toWait;
|
||||
}
|
||||
// Get something to wait for
|
||||
// This has been just a synthetic wait
|
||||
toWait = VolatileCurrentReadTask;
|
||||
if (lastReadTask == DownloadFinishedTask)
|
||||
throw new IOException("Video file ended before stream metadata could be fully extracted");
|
||||
else if (toWait == lastReadTask)
|
||||
// Still nothing waitable from the downloader
|
||||
toWait = Task.Delay(WaitIfNoProgressMs);
|
||||
else
|
||||
lastReadTask = toWait;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user