2020-10-22 18:41:10 +08: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 18:05:20 +08:00
using System ;
using System.Collections.Generic ;
2020-10-22 17:37:19 +08:00
using System.Diagnostics ;
2020-10-21 18:05:20 +08:00
using System.Linq ;
using System.Threading.Tasks ;
2020-10-27 07:05:03 +08:00
using JetBrains.Annotations ;
2020-10-21 18:05:20 +08:00
using Microsoft.AspNetCore.SignalR.Client ;
2020-10-22 12:41:54 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2021-02-09 12:46:00 +08:00
using osu.Framework.Graphics ;
2020-10-22 13:54:27 +08:00
using osu.Game.Beatmaps ;
2020-10-22 12:41:54 +08:00
using osu.Game.Online.API ;
2020-10-22 18:17:19 +08:00
using osu.Game.Replays.Legacy ;
2020-10-23 16:24:19 +08:00
using osu.Game.Rulesets ;
2020-10-22 16:29:38 +08:00
using osu.Game.Rulesets.Mods ;
2020-10-22 13:54:27 +08:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
2020-12-14 16:33:23 +08:00
using osu.Game.Scoring ;
2020-10-27 07:05:03 +08:00
using osu.Game.Screens.Play ;
2020-10-21 18:05:20 +08:00
namespace osu.Game.Online.Spectator
{
2021-02-09 12:46:00 +08:00
public class SpectatorStreamingClient : Component , ISpectatorClient
2020-10-21 18:05:20 +08:00
{
2020-10-26 15:31:39 +08:00
/// <summary>
/// The maximum milliseconds between frame bundle sends.
/// </summary>
public const double TIME_BETWEEN_SENDS = 200 ;
2021-02-09 13:02:32 +08:00
private readonly string endpoint ;
2021-02-09 07:15:51 +08:00
2021-02-11 17:32:54 +08:00
[CanBeNull]
2021-02-15 15:31:00 +08:00
private IHubClientConnector connector ;
2021-02-09 13:02:32 +08:00
private readonly IBindable < bool > isConnected = new BindableBool ( ) ;
2021-02-09 07:15:51 +08:00
2021-02-11 17:32:54 +08:00
private HubConnection connection = > connector ? . CurrentConnection ;
2020-10-21 18:05:20 +08:00
2020-10-22 17:37:19 +08:00
private readonly List < int > watchingUsers = new List < int > ( ) ;
2020-12-02 18:02:49 +08:00
private readonly object userLock = new object ( ) ;
2020-10-22 17:37:19 +08:00
public IBindableList < int > PlayingUsers = > playingUsers ;
private readonly BindableList < int > playingUsers = new BindableList < int > ( ) ;
2020-10-21 18:05:20 +08:00
2021-04-19 15:07:00 +08:00
private readonly Dictionary < int , SpectatorState > playingUserStates = new Dictionary < int , SpectatorState > ( ) ;
2021-04-19 15:06:40 +08:00
2020-10-27 07:05:03 +08:00
[CanBeNull]
private IBeatmap currentBeatmap ;
2020-10-22 13:54:27 +08:00
2020-12-14 16:33:23 +08:00
[CanBeNull]
private Score currentScore ;
2020-10-23 16:24:19 +08:00
[Resolved]
2020-10-27 09:59:24 +08:00
private IBindable < RulesetInfo > currentRuleset { get ; set ; }
2020-10-23 16:24:19 +08:00
2020-10-22 16:29:38 +08:00
[Resolved]
2020-10-27 09:59:24 +08:00
private IBindable < IReadOnlyList < Mod > > currentMods { get ; set ; }
2020-10-22 16:29:38 +08:00
private readonly SpectatorState currentState = new SpectatorState ( ) ;
2020-10-22 17:37:19 +08:00
private bool isPlaying ;
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
public event Action < int , FrameDataBundle > OnNewFrames ;
2020-10-22 17:10:27 +08:00
2020-10-26 19:05:11 +08:00
/// <summary>
2021-04-16 13:11:55 +08: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 19:05:11 +08:00
/// </summary>
2021-04-16 16:29:42 +08:00
public event Action < int , SpectatorState > OnUserBeganPlaying ;
2020-10-26 19:05:11 +08:00
/// <summary>
2020-11-01 21:39:10 +08:00
/// Called whenever a user finishes a play session.
2020-10-26 19:05:11 +08:00
/// </summary>
public event Action < int , SpectatorState > OnUserFinishedPlaying ;
2020-12-24 16:58:38 +08:00
public SpectatorStreamingClient ( EndpointConfiguration endpoints )
{
2021-02-09 13:02:32 +08:00
endpoint = endpoints . SpectatorEndpointUrl ;
2020-12-24 16:58:38 +08:00
}
2020-10-22 12:41:54 +08:00
[BackgroundDependencyLoader]
2021-02-09 13:02:32 +08:00
private void load ( IAPIProvider api )
2020-10-21 18:05:20 +08:00
{
2021-02-15 15:31:00 +08:00
connector = api . GetHubConnector ( nameof ( SpectatorStreamingClient ) , endpoint ) ;
2021-02-11 17:32:54 +08:00
if ( connector ! = null )
2020-10-22 12:41:54 +08:00
{
2021-02-11 17:32:54 +08:00
connector . ConfigureConnection = connection = >
2020-11-02 16:32:10 +08:00
{
2021-02-11 17:32:54 +08: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 14:27:04 +08:00
{
2021-02-11 17:32:54 +08: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-09 07:15:51 +08:00
}
2021-02-11 17:32:54 +08:00
else
{
2021-04-19 15:06:40 +08:00
lock ( userLock )
{
playingUsers . Clear ( ) ;
2021-04-19 15:07:00 +08:00
playingUserStates . Clear ( ) ;
2021-04-19 15:06:40 +08:00
}
2021-02-11 17:32:54 +08:00
}
} , true ) ;
}
2020-10-21 18:05:20 +08:00
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserBeganPlaying ( int userId , SpectatorState state )
2020-10-21 18:05:20 +08:00
{
2021-04-16 13:11:55 +08:00
lock ( userLock )
2021-04-19 15:06:40 +08:00
{
if ( ! playingUsers . Contains ( userId ) )
playingUsers . Add ( userId ) ;
2021-04-19 15:07:00 +08:00
playingUserStates [ userId ] = state ;
2021-04-19 15:06:40 +08:00
}
2021-04-16 13:11:55 +08:00
2021-04-16 16:29:42 +08:00
OnUserBeganPlaying ? . Invoke ( userId , state ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserFinishedPlaying ( int userId , SpectatorState state )
2020-10-21 18:05:20 +08:00
{
2021-04-16 13:11:55 +08:00
lock ( userLock )
2021-04-19 15:06:40 +08:00
{
playingUsers . Remove ( userId ) ;
2021-04-19 15:07:00 +08:00
playingUserStates . Remove ( userId ) ;
2021-04-19 15:06:40 +08:00
}
2021-04-16 13:11:55 +08:00
2020-10-26 19:05:11 +08:00
OnUserFinishedPlaying ? . Invoke ( userId , state ) ;
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-10-22 17:37:19 +08:00
Task ISpectatorClient . UserSentFrames ( int userId , FrameDataBundle data )
2020-10-21 18:05:20 +08:00
{
2020-10-22 17:37:19 +08:00
OnNewFrames ? . Invoke ( userId , data ) ;
2020-10-26 19:05:11 +08:00
2020-10-21 18:05:20 +08:00
return Task . CompletedTask ;
}
2020-12-14 16:33:23 +08:00
public void BeginPlaying ( GameplayBeatmap beatmap , Score score )
2020-10-22 14:27:04 +08:00
{
2020-10-22 17:37:19 +08:00
if ( isPlaying )
throw new InvalidOperationException ( $"Cannot invoke {nameof(BeginPlaying)} when already playing" ) ;
isPlaying = true ;
2020-10-22 16:29:43 +08:00
// transfer state at point of beginning play
2020-10-27 07:05:03 +08:00
currentState . BeatmapID = beatmap . BeatmapInfo . OnlineBeatmapID ;
2020-10-27 09:59:24 +08:00
currentState . RulesetID = currentRuleset . Value . ID ;
currentState . Mods = currentMods . Value . Select ( m = > new APIMod ( m ) ) ;
2020-10-22 16:29:43 +08:00
2020-10-27 07:05:03 +08:00
currentBeatmap = beatmap . PlayableBeatmap ;
2020-12-14 16:33:23 +08:00
currentScore = score ;
2020-10-22 17:37:19 +08:00
beginPlaying ( ) ;
}
private void beginPlaying ( )
{
Debug . Assert ( isPlaying ) ;
2021-02-09 13:02:32 +08:00
if ( ! isConnected . Value ) return ;
2020-10-22 18:30:07 +08:00
2020-10-22 16:29:43 +08:00
connection . SendAsync ( nameof ( ISpectatorServer . BeginPlaySession ) , currentState ) ;
2020-10-22 14:27:04 +08:00
}
2020-10-21 18:05:20 +08:00
2020-10-22 14:27:04 +08:00
public void SendFrames ( FrameDataBundle data )
{
2021-02-09 13:02:32 +08:00
if ( ! isConnected . Value ) return ;
2020-10-22 14:27:04 +08:00
2020-10-22 18:17:19 +08:00
lastSend = connection . SendAsync ( nameof ( ISpectatorServer . SendFrameData ) , data ) ;
2020-10-22 14:27:04 +08:00
}
2020-10-22 16:29:38 +08:00
public void EndPlaying ( )
2020-10-22 14:27:04 +08:00
{
2020-10-22 17:37:19 +08:00
isPlaying = false ;
2020-10-27 07:05:03 +08:00
currentBeatmap = null ;
2020-10-22 21:56:23 +08:00
2021-02-09 13:02:32 +08:00
if ( ! isConnected . Value ) return ;
2020-10-22 21:56:23 +08:00
2020-10-22 16:29:38 +08:00
connection . SendAsync ( nameof ( ISpectatorServer . EndPlaySession ) , currentState ) ;
2020-10-22 14:27:04 +08:00
}
2020-10-27 13:57:23 +08:00
public virtual void WatchUser ( int userId )
2020-10-22 14:27:04 +08:00
{
2020-12-02 18:02:49 +08:00
lock ( userLock )
{
if ( watchingUsers . Contains ( userId ) )
return ;
2020-10-22 17:37:19 +08:00
2020-12-02 18:02:49 +08:00
watchingUsers . Add ( userId ) ;
2020-10-22 18:17:19 +08:00
2021-02-09 13:02:32 +08:00
if ( ! isConnected . Value )
2020-12-02 18:02:49 +08:00
return ;
}
2020-10-22 18:17:19 +08:00
2020-10-22 14:27:04 +08:00
connection . SendAsync ( nameof ( ISpectatorServer . StartWatchingUser ) , userId ) ;
}
2020-10-22 13:54:27 +08:00
2020-10-22 18:17:19 +08:00
public void StopWatchingUser ( int userId )
{
2020-12-02 18:02:49 +08:00
lock ( userLock )
{
watchingUsers . Remove ( userId ) ;
2020-10-22 18:17:19 +08:00
2021-02-09 13:02:32 +08:00
if ( ! isConnected . Value )
2020-12-02 18:02:49 +08:00
return ;
}
2020-10-22 18:17:19 +08: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 15:31:39 +08:00
if ( pendingFrames . Count > 0 & & Time . Current - lastSendTime > TIME_BETWEEN_SENDS )
2020-10-22 18:17:19 +08:00
purgePendingFrames ( ) ;
}
2020-10-22 13:54:27 +08:00
public void HandleFrame ( ReplayFrame frame )
{
if ( frame is IConvertibleReplayFrame convertible )
2020-10-27 07:05:03 +08:00
pendingFrames . Enqueue ( convertible . ToLegacy ( currentBeatmap ) ) ;
2020-10-22 18:17:19 +08: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 16:33:23 +08:00
Debug . Assert ( currentScore ! = null ) ;
SendFrames ( new FrameDataBundle ( currentScore . ScoreInfo , frames ) ) ;
2020-10-22 18:17:19 +08:00
lastSendTime = Time . Current ;
2020-10-22 13:54:27 +08:00
}
2021-04-16 16:29:42 +08:00
2021-04-19 15:48:55 +08:00
/// <summary>
/// Attempts to retrieve the <see cref="SpectatorState"/> for a currently-playing user.
/// </summary>
/// <param name="userId">The user.</param>
/// <param name="state">The current <see cref="SpectatorState"/> for the user, if they're playing. <c>null</c> if the user is not playing.</param>
/// <returns><c>true</c> if successful (the user is playing), <c>false</c> otherwise.</returns>
public bool TryGetPlayingUserState ( int userId , out SpectatorState state )
{
lock ( userLock )
return playingUserStates . TryGetValue ( userId , out state ) ;
}
2021-04-16 16:29:42 +08: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 )
{
2021-04-20 20:20:08 +08:00
// The lock is taken before the event is subscribed to to prevent doubling of events.
2021-04-16 16:29:42 +08:00
lock ( userLock )
{
2021-04-20 20:20:08 +08:00
OnUserBeganPlaying + = callback ;
if ( ! runOnceImmediately )
return ;
2021-04-19 15:07:00 +08:00
foreach ( var ( userId , state ) in playingUserStates )
2021-04-16 16:29:42 +08:00
callback ( userId , state ) ;
}
}
2020-10-21 18:05:20 +08:00
}
}