Skip to content

Commit

Permalink
[core] Add prebuffering and CancellationTokens to StreamProvider
Browse files Browse the repository at this point in the history
Ensure we download the first and last pieces before giving the user
the Stream. This means a streaming player can (probably) play
without buffering/stuttering.

Prebuffering can be turned off if needed by passing 'false' to
the correct CreateStream overload.
  • Loading branch information
alanmcgovern committed Mar 16, 2020
1 parent 4e1b5a4 commit d47d734
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 9 deletions.
7 changes: 4 additions & 3 deletions src/MonoTorrent.Tests/Streaming/StreamProviderTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.CodeDom;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MonoTorrent.Client;
using MonoTorrent.Client.PiecePicking;
Expand Down Expand Up @@ -88,7 +89,7 @@ public async Task CreateStream ()
{
var provider = new StreamProvider (Engine, "testDir", Torrent);
await provider.StartAsync ();
using var stream = provider.CreateStreamAsync (Torrent.Files[0]);
using var stream = await provider.CreateStreamAsync (Torrent.Files[0], false, CancellationToken.None);
Assert.IsNotNull (stream);
}

Expand All @@ -104,8 +105,8 @@ public async Task CreateStreamTwice ()
{
var provider = new StreamProvider (Engine, "testDir", Torrent);
await provider.StartAsync ();
using var stream = provider.CreateStreamAsync (Torrent.Files[0]);
Assert.ThrowsAsync<InvalidOperationException> (() => provider.CreateStreamAsync (Torrent.Files[0]));
using var stream = await provider.CreateStreamAsync (Torrent.Files[0], false, CancellationToken.None);
Assert.ThrowsAsync<InvalidOperationException> (() => provider.CreateStreamAsync (Torrent.Files[0], false, CancellationToken.None));
}

[Test]
Expand Down
71 changes: 65 additions & 6 deletions src/MonoTorrent/MonoTorrent.Streaming/StreamProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,38 @@ public async Task StopAsync ()

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. This stream must be disposed
/// while it is downloading. This stream is seekable and readable. The first and last pieces of
/// this file will be buffered before the stream is created. Finally, this stream must be disposed
/// before another stream can be created.
/// </summary>
/// <param name="file"></param>
/// <param name="file">The file to open</param>
/// <returns></returns>
public Task<Stream> CreateStreamAsync (TorrentFile file)
=> CreateStreamAsync (file, prebuffer: true, CancellationToken.None);

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. The first and last pieces of
/// this file will be buffered before the stream is created. Finally, this stream must be disposed
/// before another stream can be created.
/// </summary>
/// <param name="file">The file to open</param>
/// <param name="token">The cancellation token</param>
/// <returns></returns>
public Task<Stream> CreateStreamAsync (TorrentFile file, CancellationToken token)
=> CreateStreamAsync (file, prebuffer: true, token);

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. The first and last pieces of
/// this file will be buffered before the stream is created if <paramref name="prebuffer"/> is
/// set to true. Finally, this stream must be disposed before another stream can be created.
/// </summary>
/// <param name="file">The file to open</param>
/// <param name="prebuffer">True if the first and last piece should be downloaded before the Stream is created.</param>
/// <param name="token">The cancellation token.</param>
/// <returns></returns>
public async Task<Stream> CreateStreamAsync (TorrentFile file, bool prebuffer, CancellationToken token)
{
if (file == null)
throw new ArgumentNullException (nameof (file));
Expand All @@ -216,21 +242,54 @@ public Task<Stream> CreateStreamAsync (TorrentFile file)
throw new InvalidOperationException ("You must call StartAsync before creating a stream.");
if (ActiveStream != null && !ActiveStream.Disposed)
throw new InvalidOperationException ("You must Dispose the previous stream before creating a new one.");

Picker.SeekToPosition (file, 0);
ActiveStream = new LocalStream (Manager, file, Picker);
return Task.FromResult<Stream> (ActiveStream);

var tcs = CancellationTokenSource.CreateLinkedTokenSource (Cancellation.Token, token);
if (prebuffer) {
ActiveStream.Seek (ActiveStream.Length - 1, SeekOrigin.Begin);
await ActiveStream.ReadAsync (new byte[1], 0, 1, tcs.Token);

ActiveStream.Seek (0, SeekOrigin.Begin);
await ActiveStream.ReadAsync (new byte[1], 0, 1, tcs.Token);
}
return ActiveStream;
}

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. This stream must be disposed
/// before another stream can be created.
/// </summary>
/// <param name="file"></param>
/// <param name="file">The file to open</param>
/// <returns></returns>
public Task<IUriStream> CreateHttpStreamAsync (TorrentFile file)
=> CreateHttpStreamAsync (file, prebuffer: true, CancellationToken.None);

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. This stream must be disposed
/// before another stream can be created.
/// </summary>
/// <param name="file">The file to open</param>
/// <param name="token">The cancellation token</param>
/// <returns></returns>
public Task<IUriStream> CreateHttpStreamAsync (TorrentFile file, CancellationToken token)
=> CreateHttpStreamAsync (file, prebuffer: true, token);

/// <summary>
/// Creates a <see cref="Stream"/> which can be used to access the given <see cref="TorrentFile"/>
/// while it is downloading. This stream is seekable and readable. This stream must be disposed
/// before another stream can be created.
/// </summary>
/// <param name="file">The file to open</param>
/// <param name="prebuffer">True if the first and last piece should be downloaded before the Stream is created.</param>
/// <param name="token">The cancellation token</param>
/// <returns></returns>
public async Task<IUriStream> CreateHttpStreamAsync (TorrentFile file)
public async Task<IUriStream> CreateHttpStreamAsync (TorrentFile file, bool prebuffer, CancellationToken token)
{
var stream = await CreateStreamAsync (file);
var stream = await CreateStreamAsync (file, prebuffer, token);
var httpStreamer = new HttpStream (stream);
return httpStreamer;
}
Expand Down

0 comments on commit d47d734

Please sign in to comment.