Improvevideo file analysis
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2024-12-23 23:12:46 +01:00
parent a3af3defab
commit 70c1e3627d

View File

@@ -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;
}
}