ProgressiveFileCopier.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using System;
  2. using System.Buffers;
  3. using System.IO;
  4. using System.Runtime.InteropServices;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Api.Models.PlaybackDtos;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Model.IO;
  10. namespace Jellyfin.Api.Helpers
  11. {
  12. /// <summary>
  13. /// Progressive file copier.
  14. /// </summary>
  15. public class ProgressiveFileCopier
  16. {
  17. private readonly TranscodingJobDto? _job;
  18. private readonly string? _path;
  19. private readonly CancellationToken _cancellationToken;
  20. private readonly IDirectStreamProvider? _directStreamProvider;
  21. private readonly TranscodingJobHelper _transcodingJobHelper;
  22. private long _bytesWritten;
  23. /// <summary>
  24. /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
  25. /// </summary>
  26. /// <param name="path">The path to copy from.</param>
  27. /// <param name="job">The transcoding job.</param>
  28. /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
  29. /// <param name="cancellationToken">The cancellation token.</param>
  30. public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
  31. {
  32. _path = path;
  33. _job = job;
  34. _cancellationToken = cancellationToken;
  35. _transcodingJobHelper = transcodingJobHelper;
  36. }
  37. /// <summary>
  38. /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
  39. /// </summary>
  40. /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
  41. /// <param name="job">The transcoding job.</param>
  42. /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
  43. /// <param name="cancellationToken">The cancellation token.</param>
  44. public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
  45. {
  46. _directStreamProvider = directStreamProvider;
  47. _job = job;
  48. _cancellationToken = cancellationToken;
  49. _transcodingJobHelper = transcodingJobHelper;
  50. }
  51. /// <summary>
  52. /// Gets or sets a value indicating whether allow read end of file.
  53. /// </summary>
  54. public bool AllowEndOfFile { get; set; } = true;
  55. /// <summary>
  56. /// Gets or sets copy start position.
  57. /// </summary>
  58. public long StartPosition { get; set; }
  59. /// <summary>
  60. /// Write source stream to output.
  61. /// </summary>
  62. /// <param name="outputStream">Output stream.</param>
  63. /// <param name="cancellationToken">Cancellation token.</param>
  64. /// <returns>A <see cref="Task"/>.</returns>
  65. public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
  66. {
  67. cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
  68. try
  69. {
  70. if (_directStreamProvider != null)
  71. {
  72. await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
  73. return;
  74. }
  75. var fileOptions = FileOptions.SequentialScan;
  76. var allowAsyncFileRead = false;
  77. // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
  78. if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  79. {
  80. fileOptions |= FileOptions.Asynchronous;
  81. allowAsyncFileRead = true;
  82. }
  83. await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
  84. var eofCount = 0;
  85. const int EmptyReadLimit = 20;
  86. if (StartPosition > 0)
  87. {
  88. inputStream.Position = StartPosition;
  89. }
  90. while (eofCount < EmptyReadLimit || !AllowEndOfFile)
  91. {
  92. var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
  93. if (bytesRead == 0)
  94. {
  95. if (_job == null || _job.HasExited)
  96. {
  97. eofCount++;
  98. }
  99. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  100. }
  101. else
  102. {
  103. eofCount = 0;
  104. }
  105. }
  106. }
  107. finally
  108. {
  109. if (_job != null)
  110. {
  111. _transcodingJobHelper.OnTranscodeEndRequest(_job);
  112. }
  113. }
  114. }
  115. private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
  116. {
  117. var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
  118. try
  119. {
  120. int bytesRead;
  121. int totalBytesRead = 0;
  122. if (readAsync)
  123. {
  124. bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
  125. }
  126. else
  127. {
  128. bytesRead = source.Read(array, 0, array.Length);
  129. }
  130. while (bytesRead != 0)
  131. {
  132. var bytesToWrite = bytesRead;
  133. if (bytesToWrite > 0)
  134. {
  135. await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
  136. _bytesWritten += bytesRead;
  137. totalBytesRead += bytesRead;
  138. if (_job != null)
  139. {
  140. _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
  141. }
  142. }
  143. if (readAsync)
  144. {
  145. bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
  146. }
  147. else
  148. {
  149. bytesRead = source.Read(array, 0, array.Length);
  150. }
  151. }
  152. return totalBytesRead;
  153. }
  154. finally
  155. {
  156. ArrayPool<byte>.Shared.Return(array);
  157. }
  158. }
  159. }
  160. }