ProgressiveFileCopier.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. int bytesRead;
  93. if (allowAsyncFileRead)
  94. {
  95. bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
  96. }
  97. else
  98. {
  99. bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
  100. }
  101. if (bytesRead == 0)
  102. {
  103. if (_job == null || _job.HasExited)
  104. {
  105. eofCount++;
  106. }
  107. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  108. }
  109. else
  110. {
  111. eofCount = 0;
  112. }
  113. }
  114. }
  115. finally
  116. {
  117. if (_job != null)
  118. {
  119. _transcodingJobHelper.OnTranscodeEndRequest(_job);
  120. }
  121. }
  122. }
  123. private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
  124. {
  125. var array = new byte[IODefaults.CopyToBufferSize];
  126. int bytesRead;
  127. int totalBytesRead = 0;
  128. while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
  129. {
  130. var bytesToWrite = bytesRead;
  131. if (bytesToWrite > 0)
  132. {
  133. await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
  134. _bytesWritten += bytesRead;
  135. totalBytesRead += bytesRead;
  136. if (_job != null)
  137. {
  138. _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
  139. }
  140. }
  141. }
  142. return totalBytesRead;
  143. }
  144. private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
  145. {
  146. var array = new byte[IODefaults.CopyToBufferSize];
  147. int bytesRead;
  148. int totalBytesRead = 0;
  149. while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
  150. {
  151. var bytesToWrite = bytesRead;
  152. if (bytesToWrite > 0)
  153. {
  154. await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
  155. _bytesWritten += bytesRead;
  156. totalBytesRead += bytesRead;
  157. if (_job != null)
  158. {
  159. _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
  160. }
  161. }
  162. }
  163. return totalBytesRead;
  164. }
  165. }
  166. }