ProgressiveFileCopier.cs 7.1 KB

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