2020-10-22 10:41:10 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2020-10-21 10:05:20 +00:00
using System ;
using System.Collections.Generic ;
2020-10-22 09:37:19 +00:00
using System.Diagnostics ;
2020-10-21 10:05:20 +00:00
using System.Linq ;
using System.Threading.Tasks ;
2020-10-26 23:05:03 +00:00
using JetBrains.Annotations ;
2020-10-21 10:05:20 +00:00
using Microsoft.AspNetCore.SignalR.Client ;
2020-10-22 04:41:54 +00:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2021-02-09 04:46:00 +00:00
using osu.Framework.Graphics ;
2020-10-22 05:54:27 +00:00
using osu.Game.Beatmaps ;
2020-10-22 04:41:54 +00:00
using osu.Game.Online.API ;
2020-10-22 10:17:19 +00:00
using osu.Game.Replays.Legacy ;
2020-10-23 08:24:19 +00:00
using osu.Game.Rulesets ;
2020-10-22 08:29:38 +00:00
using osu.Game.Rulesets.Mods ;
2020-10-22 05:54:27 +00:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
2020-12-14 08:33:23 +00:00
using osu.Game.Scoring ;
2020-10-26 23:05:03 +00:00
using osu.Game.Screens.Play ;
2020-10-21 10:05:20 +00:00
namespace osu.Game.Online.Spectator
{
2021-02-09 04:46:00 +00:00
public class SpectatorStreamingClient : Component , ISpectatorClient
2020-10-21 10:05:20 +00:00
{
2020-10-26 07:31:39 +00:00
/// <summary>
/// The maximum milliseconds between frame bundle sends.
/// </summary>
public const double TIME_BETWEEN_SENDS = 200 ;
2021-02-09 05:02:32 +00:00
private readonly string endpoint ;
2021-02-08 23:15:51 +00:00
2021-02-11 09:32:54 +00:00
[CanBeNull]
2021-02-15 07:31:00 +00:00
private IHubClientConnector connector ;
2021-02-09 05:02:32 +00:00
private readonly IBindable < bool > isConnected = new BindableBool ( ) ;
2021-02-08 23:15:51 +00:00
2021-02-11 09:32:54 +00:00
private HubConnection connection = > connector ? . CurrentConnection ;
2020-10-21 10:05:20 +00:00
2020-10-22 09:37:19 +00:00
private readonly List < int > watchingUsers = new List < int > ( ) ;
2020-12-02 10:02:49 +00:00
private readonly object userLock = new object ( ) ;
2020-10-22 09:37:19 +00:00
public IBindableList < int > PlayingUsers = > playingUsers ;
private readonly BindableList < int > playingUsers = new BindableList < int > ( ) ;
2020-10-21 10:05:20 +00:00
2021-04-19 07:07:00 +00:00
private readonly Dictionary < int , SpectatorState > playingUserStates = new Dictionary < int , SpectatorState > ( ) ;
2021-04-19 07:06:40 +00:00
2020-10-26 23:05:03 +00:00
[CanBeNull]
private IBeatmap currentBeatmap ;
2020-10-22 05:54:27 +00:00
2020-12-14 08:33:23 +00:00
[CanBeNull]
private Score currentScore ;
2020-10-23 08:24:19 +00:00
[Resolved]
2020-10-27 01:59:24 +00:00
private IBindable < RulesetInfo > currentRuleset { get ; set ; }
2020-10-23 08:24:19 +00:00
2020-10-22 08:29:38 +00:00
[Resolved]
2020-10-27 01:59:24 +00:00
private IBindable < IReadOnlyList < Mod > > currentMods { get ; set ; }
2020-10-22 08:29:38 +00:00
private readonly SpectatorState currentState = new SpectatorState ( ) ;
2020-10-22 09:37:19 +00:00
private bool isPlaying ;
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
public event Action < int , FrameDataBundle > OnNewFrames ;
2020-10-22 09:10:27 +00:00
2020-10-26 11:05:11 +00:00
/// <summary>
2021-04-16 05:11:55 +00:00
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
2020-10-26 11:05:11 +00:00
/// </summary>
2021-04-16 08:29:42 +00:00
public event Action < int , SpectatorState > OnUserBeganPlaying ;
2020-10-26 11:05:11 +00:00
/// <summary>
2020-11-01 13:39:10 +00:00
/// Called whenever a user finishes a play session.
2020-10-26 11:05:11 +00:00
/// </summary>
public event Action < int , SpectatorState > OnUserFinishedPlaying ;
2020-12-24 08:58:38 +00:00
public SpectatorStreamingClient ( EndpointConfiguration endpoints )
{
2021-02-09 05:02:32 +00:00
endpoint = endpoints . SpectatorEndpointUrl ;
2020-12-24 08:58:38 +00:00
}
2020-10-22 04:41:54 +00:00
[BackgroundDependencyLoader]
2021-02-09 05:02:32 +00:00
private void load ( IAPIProvider api )
2020-10-21 10:05:20 +00:00
{
2021-02-15 07:31:00 +00:00
connector = api . GetHubConnector ( nameof ( SpectatorStreamingClient ) , endpoint ) ;
2021-02-11 09:32:54 +00:00
if ( connector ! = null )
2020-10-22 04:41:54 +00:00
{
2021-02-11 09:32:54 +00:00
connector . ConfigureConnection = connection = >
2020-11-02 08:32:10 +00:00
{
2021-02-11 09:32:54 +00:00
// until strong typed client support is added, each method must be manually bound
// (see https://github.com/dotnet/aspnetcore/issues/15198)
connection . On < int , SpectatorState > ( nameof ( ISpectatorClient . UserBeganPlaying ) , ( ( ISpectatorClient ) this ) . UserBeganPlaying ) ;
connection . On < int , FrameDataBundle > ( nameof ( ISpectatorClient . UserSentFrames ) , ( ( ISpectatorClient ) this ) . UserSentFrames ) ;
connection . On < int , SpectatorState > ( nameof ( ISpectatorClient . UserFinishedPlaying ) , ( ( ISpectatorClient ) this ) . UserFinishedPlaying ) ;
} ;
isConnected . BindTo ( connector . IsConnected ) ;
isConnected . BindValueChanged ( connected = >
{
if ( connected . NewValue )
2020-10-22 06:27:04 +00:00
{
2021-02-11 09:32:54 +00:00
// get all the users that were previously being watched
int [ ] users ;
lock ( userLock )
{
users = watchingUsers . ToArray ( ) ;
watchingUsers . Clear ( ) ;
}
// resubscribe to watched users.
foreach ( var userId in users )
WatchUser ( userId ) ;
// re-send state in case it wasn't received
if ( isPlaying )
beginPlaying ( ) ;
2021-02-08 23:15:51 +00:00
}
2021-02-11 09:32:54 +00:00
else
{
2021-04-19 07:06:40 +00:00
lock ( userLock )
{
playingUsers . Clear ( ) ;
2021-04-19 07:07:00 +00:00
playingUserStates . Clear ( ) ;
2021-04-19 07:06:40 +00:00
}
2021-02-11 09:32:54 +00:00
}
} , true ) ;
}
2020-10-21 10:05:20 +00:00
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserBeganPlaying ( int userId , SpectatorState state )
2020-10-21 10:05:20 +00:00
{
2021-04-16 05:11:55 +00:00
lock ( userLock )
2021-04-19 07:06:40 +00:00
{
if ( ! playingUsers . Contains ( userId ) )
playingUsers . Add ( userId ) ;
2021-04-19 07:07:00 +00:00
playingUserStates [ userId ] = state ;
2021-04-19 07:06:40 +00:00
}
2021-04-16 05:11:55 +00:00
2021-04-16 08:29:42 +00:00
OnUserBeganPlaying ? . Invoke ( userId , state ) ;
2020-10-26 11:05:11 +00:00
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserFinishedPlaying ( int userId , SpectatorState state )
2020-10-21 10:05:20 +00:00
{
2021-04-16 05:11:55 +00:00
lock ( userLock )
2021-04-19 07:06:40 +00:00
{
playingUsers . Remove ( userId ) ;
2021-04-19 07:07:00 +00:00
playingUserStates . Remove ( userId ) ;
2021-04-19 07:06:40 +00:00
}
2021-04-16 05:11:55 +00:00
2020-10-26 11:05:11 +00:00
OnUserFinishedPlaying ? . Invoke ( userId , state ) ;
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2020-10-22 09:37:19 +00:00
Task ISpectatorClient . UserSentFrames ( int userId , FrameDataBundle data )
2020-10-21 10:05:20 +00:00
{
2020-10-22 09:37:19 +00:00
OnNewFrames ? . Invoke ( userId , data ) ;
2020-10-26 11:05:11 +00:00
2020-10-21 10:05:20 +00:00
return Task . CompletedTask ;
}
2020-12-14 08:33:23 +00:00
public void BeginPlaying ( GameplayBeatmap beatmap , Score score )
2020-10-22 06:27:04 +00:00
{
2020-10-22 09:37:19 +00:00
if ( isPlaying )
throw new InvalidOperationException ( $"Cannot invoke {nameof(BeginPlaying)} when already playing" ) ;
isPlaying = true ;
2020-10-22 08:29:43 +00:00
// transfer state at point of beginning play
2020-10-26 23:05:03 +00:00
currentState . BeatmapID = beatmap . BeatmapInfo . OnlineBeatmapID ;
2020-10-27 01:59:24 +00:00
currentState . RulesetID = currentRuleset . Value . ID ;
currentState . Mods = currentMods . Value . Select ( m = > new APIMod ( m ) ) ;
2020-10-22 08:29:43 +00:00
2020-10-26 23:05:03 +00:00
currentBeatmap = beatmap . PlayableBeatmap ;
2020-12-14 08:33:23 +00:00
currentScore = score ;
2020-10-22 09:37:19 +00:00
beginPlaying ( ) ;
}
private void beginPlaying ( )
{
Debug . Assert ( isPlaying ) ;
2021-02-09 05:02:32 +00:00
if ( ! isConnected . Value ) return ;
2020-10-22 10:30:07 +00:00
2020-10-22 08:29:43 +00:00
connection . SendAsync ( nameof ( ISpectatorServer . BeginPlaySession ) , currentState ) ;
2020-10-22 06:27:04 +00:00
}
2020-10-21 10:05:20 +00:00
2020-10-22 06:27:04 +00:00
public void SendFrames ( FrameDataBundle data )
{
2021-02-09 05:02:32 +00:00
if ( ! isConnected . Value ) return ;
2020-10-22 06:27:04 +00:00
2020-10-22 10:17:19 +00:00
lastSend = connection . SendAsync ( nameof ( ISpectatorServer . SendFrameData ) , data ) ;
2020-10-22 06:27:04 +00:00
}
2020-10-22 08:29:38 +00:00
public void EndPlaying ( )
2020-10-22 06:27:04 +00:00
{
2020-10-22 09:37:19 +00:00
isPlaying = false ;
2020-10-26 23:05:03 +00:00
currentBeatmap = null ;
2020-10-22 13:56:23 +00:00
2021-02-09 05:02:32 +00:00
if ( ! isConnected . Value ) return ;
2020-10-22 13:56:23 +00:00
2020-10-22 08:29:38 +00:00
connection . SendAsync ( nameof ( ISpectatorServer . EndPlaySession ) , currentState ) ;
2020-10-22 06:27:04 +00:00
}
2020-10-27 05:57:23 +00:00
public virtual void WatchUser ( int userId )
2020-10-22 06:27:04 +00:00
{
2020-12-02 10:02:49 +00:00
lock ( userLock )
{
if ( watchingUsers . Contains ( userId ) )
return ;
2020-10-22 09:37:19 +00:00
2020-12-02 10:02:49 +00:00
watchingUsers . Add ( userId ) ;
2020-10-22 10:17:19 +00:00
2021-02-09 05:02:32 +00:00
if ( ! isConnected . Value )
2020-12-02 10:02:49 +00:00
return ;
}
2020-10-22 10:17:19 +00:00
2020-10-22 06:27:04 +00:00
connection . SendAsync ( nameof ( ISpectatorServer . StartWatchingUser ) , userId ) ;
}
2020-10-22 05:54:27 +00:00
2020-10-22 10:17:19 +00:00
public void StopWatchingUser ( int userId )
{
2020-12-02 10:02:49 +00:00
lock ( userLock )
{
watchingUsers . Remove ( userId ) ;
2020-10-22 10:17:19 +00:00
2021-02-09 05:02:32 +00:00
if ( ! isConnected . Value )
2020-12-02 10:02:49 +00:00
return ;
}
2020-10-22 10:17:19 +00:00
connection . SendAsync ( nameof ( ISpectatorServer . EndWatchingUser ) , userId ) ;
}
private readonly Queue < LegacyReplayFrame > pendingFrames = new Queue < LegacyReplayFrame > ( ) ;
private double lastSendTime ;
private Task lastSend ;
private const int max_pending_frames = 30 ;
protected override void Update ( )
{
base . Update ( ) ;
2020-10-26 07:31:39 +00:00
if ( pendingFrames . Count > 0 & & Time . Current - lastSendTime > TIME_BETWEEN_SENDS )
2020-10-22 10:17:19 +00:00
purgePendingFrames ( ) ;
}
2020-10-22 05:54:27 +00:00
public void HandleFrame ( ReplayFrame frame )
{
if ( frame is IConvertibleReplayFrame convertible )
2020-10-26 23:05:03 +00:00
pendingFrames . Enqueue ( convertible . ToLegacy ( currentBeatmap ) ) ;
2020-10-22 10:17:19 +00:00
if ( pendingFrames . Count > max_pending_frames )
purgePendingFrames ( ) ;
}
private void purgePendingFrames ( )
{
if ( lastSend ? . IsCompleted = = false )
return ;
var frames = pendingFrames . ToArray ( ) ;
pendingFrames . Clear ( ) ;
2020-12-14 08:33:23 +00:00
Debug . Assert ( currentScore ! = null ) ;
SendFrames ( new FrameDataBundle ( currentScore . ScoreInfo , frames ) ) ;
2020-10-22 10:17:19 +00:00
lastSendTime = Time . Current ;
2020-10-22 05:54:27 +00:00
}
2021-04-16 08:29:42 +00:00
/// <summary>
/// Bind an action to <see cref="OnUserBeganPlaying"/> with the option of running the bound action once immediately.
/// </summary>
/// <param name="callback">The action to perform when a user begins playing.</param>
/// <param name="runOnceImmediately">Whether the action provided in <paramref name="callback"/> should be run once immediately for all users currently playing.</param>
public void BindUserBeganPlaying ( Action < int , SpectatorState > callback , bool runOnceImmediately = false )
{
OnUserBeganPlaying + = callback ;
if ( ! runOnceImmediately )
return ;
lock ( userLock )
{
2021-04-19 07:07:00 +00:00
foreach ( var ( userId , state ) in playingUserStates )
2021-04-16 08:29:42 +00:00
callback ( userId , state ) ;
}
}
2020-10-21 10:05:20 +00:00
}
}