Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions osu.Game/Online/Spectator/OnlineSpectatorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;

Expand Down Expand Up @@ -51,16 +52,17 @@ private void load(IAPIProvider api)
}
}

protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
protected override async Task<bool> BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
if (!IsConnected.Value)
return;
return false;

Debug.Assert(connection != null);

try
{
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), scoreToken, state).ConfigureAwait(false);
return true;
}
catch (Exception exception)
{
Expand All @@ -69,11 +71,14 @@ protected override async Task BeginPlayingInternal(long? scoreToken, SpectatorSt
Debug.Assert(connector != null);

await connector.Reconnect().ConfigureAwait(false);
await BeginPlayingInternal(scoreToken, state).ConfigureAwait(false);
return await BeginPlayingInternal(scoreToken, state).ConfigureAwait(false);
}

// Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
// For now, let's ignore these so they don't cause unobserved exceptions to appear to the user (and sentry).
// For now, let's ignore these so they don't cause unobserved exceptions to appear to the user (and sentry),
// but log to disk for diagnostic purposes.
Logger.Log($"{nameof(OnlineSpectatorClient)}.{nameof(BeginPlayingInternal)} failed: {exception.Message}", LoggingTarget.Network);
return false;
}
}

Expand Down
70 changes: 57 additions & 13 deletions osu.Game/Online/Spectator/SpectatorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
Expand Down Expand Up @@ -216,21 +217,34 @@ public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
if (isPlaying)
throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing");

isPlaying = true;

// transfer state at point of beginning play
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID;
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;

currentBeatmap = state.Beatmap;
currentScore = score;
currentScoreToken = scoreToken;
currentScoreProcessor = state.ScoreProcessor;
setStateForScore(scoreToken, state, score);

BeginPlayingInternal(currentScoreToken, currentState).ContinueWith(t =>
{
bool success = t.GetResultSafely();

BeginPlayingInternal(currentScoreToken, currentState);
if (!success)
{
Logger.Log($"Clearing {nameof(SpectatorClient)} state due to failed {nameof(BeginPlayingInternal)} call.");
Schedule(() =>
{
clearScoreState();

currentState.BeatmapID = null;
currentState.RulesetID = null;
currentState.Mods = [];
currentState.State = SpectatedUserState.Idle;
currentState.MaximumStatistics = [];
});
}
});
});
}

Expand Down Expand Up @@ -278,11 +292,7 @@ public void EndPlaying(GameplayState state)
if (pendingFrames.Count > 0)
purgePendingFrames();

isPlaying = false;
currentBeatmap = null;
currentScore = null;
currentScoreProcessor = null;
currentScoreToken = null;
clearScoreState();

if (state.HasPassed)
currentState.State = SpectatedUserState.Passed;
Expand All @@ -295,6 +305,26 @@ public void EndPlaying(GameplayState state)
});
}

private void setStateForScore(long? scoreToken, GameplayState state, Score score)
{
isPlaying = true;

currentBeatmap = state.Beatmap;
currentScore = score;
currentScoreToken = scoreToken;
currentScoreProcessor = state.ScoreProcessor;
}

private void clearScoreState()
{
isPlaying = false;

currentBeatmap = null;
currentScore = null;
currentScoreProcessor = null;
currentScoreToken = null;
}

public virtual void WatchUser(int userId)
{
Debug.Assert(ThreadSafety.IsUpdateThread);
Expand Down Expand Up @@ -326,7 +356,11 @@ public void StopWatchingUser(int userId)
});
}

protected abstract Task BeginPlayingInternal(long? scoreToken, SpectatorState state);
/// <summary>
/// Contains the actual implementation of the "begin play" operation.
/// </summary>
/// <returns>Whether the server-side invocation to start play succeeded.</returns>
protected abstract Task<bool> BeginPlayingInternal(long? scoreToken, SpectatorState state);

protected abstract Task SendFramesInternal(FrameDataBundle bundle);

Expand Down Expand Up @@ -355,6 +389,16 @@ private void purgePendingFrames()
if (pendingFrames.Count == 0)
return;

if (!isPlaying)
{
// it is possible for this to happen if the `BeginPlayingInternal()` call takes a long time,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question I pose is that what if it takes a long time but doesn't fail? We are potentially dropping frames from the start of a recorded replay... unless I'm missing something.

Copy link
Copy Markdown
Collaborator Author

@bdach bdach Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not looked into it. If it's a real shortcoming, it's already in master.

My hope based on what I know about signalr delivery guarantees would be that signalr side invocation ordering would delay / queue the client frame sending calls until the begin play call succeeds, but I've not tested. I can try testing it tomorrow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference on master may be the clearing of frames which was added here. But yeah, let's investigate this one before pushing this out.

Copy link
Copy Markdown
Collaborator Author

@bdach bdach Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been putting it off because I knew it was going to be annoying but I did finally test this today.

What I basically did is I took a full-stack osu + osu-server-spectator + osu-web setup and:

  • added an artificial 15-second sleep in osu-server-spectator, inside BeginPlaySession
  • added some logging to both sides to denote when the frames are sent (and how many times)

The good case (BeginPlaySession succeeds server-side after 15s)

The sequence of events is basically such:

  1. Client calls SpectatorClient.BeginPlaying(). That method via
    setStateForScore(scoreToken, state, score);
    sets and only then invokes the signalr method via
    BeginPlayingInternal(currentScoreToken, currentState).ContinueWith(t =>
    in an essentially fire-and-forget fashion. There is no waiting for the operation to complete here.
  2. Because the above invocation is fire-and-forget, despite the server still not having acknowledged the start, client is still calling HandleFrame() / purgePendingFrames() / SendFramesInternal() / SendFrameData() (outbound to signalr). Of note, all of this is able to fire, because isPlaying has been true all this time even though the server still hasn't responded to the BeginPlaySession() request - but the signalr invocations appear to all be buffered client-side, because the server isn't receiving these invocations yet, either.
  3. Eventually BeginPlaySession() succeeds, the client-side signalr buffer is flushed, and the server receives all of the backlogged SendFrameData() invocations and continues from there.
  4. The end result is that the server eventually receives all of the invocations of SendFrameData().
client-side logs
[runtime] 2026-04-08 09:19:06 [verbose]: [OnlineSpectatorClient] BeginPlayingInternal server invocation START
[runtime] 2026-04-08 09:19:07 [verbose]: GameplayClockContainer started via call to StartGameplayClock
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #1 START
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #1 END
[network] 2026-04-08 09:19:07 [verbose]: Request to https://a.ppy.sh/21 successfully completed!
[network] 2026-04-08 09:19:07 [verbose]: Request to https://a.ppy.sh/64 successfully completed!
[network] 2026-04-08 09:19:07 [verbose]: Request to https://a.ppy.sh/157 successfully completed!
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #2 START
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #2 END
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #3 START
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #3 END
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #4 START
[runtime] 2026-04-08 09:19:07 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #4 END
(...)
[runtime] 2026-04-08 09:19:21 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #73 START
[runtime] 2026-04-08 09:19:21 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #73 END
[runtime] 2026-04-08 09:19:22 [verbose]: [OnlineSpectatorClient] BeginPlayingInternal server invocation END
[runtime] 2026-04-08 09:19:22 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #74 START
[runtime] 2026-04-08 09:19:22 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #74 END
[runtime] 2026-04-08 09:19:22 [debug]: Pressed (LeftButton) handled by DrawableHitCircle+HitReceptor.
[runtime] 2026-04-08 09:19:22 [debug]: KeyDownEvent(Z, False) handled by OsuInputManager+OsuKeyBindingContainer.
[runtime] 2026-04-08 09:19:22 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #75 START
[runtime] 2026-04-08 09:19:22 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #75 END
(...)
[runtime] 2026-04-08 09:19:48 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #205 START
[runtime] 2026-04-08 09:19:48 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #205 END
[runtime] 2026-04-08 09:19:48 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #206 START
[runtime] 2026-04-08 09:19:48 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #206 END
[runtime] 2026-04-08 09:19:48 [verbose]: Beginning score submission (token:344)...
server-side logs
info: Spectator[0]
      [user:157] BeginPlaySession START @ 4/8/2026 9:19:06 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] BeginPlaySession END @ 4/8/2026 9:19:22 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #1 START @ 4/8/2026 9:19:22 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #1 END @ 4/8/2026 9:19:22 AM +00:00
(...)
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #73 START @ 4/8/2026 9:19:22 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #73 END @ 4/8/2026 9:19:22 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #74 START @ 4/8/2026 9:19:22 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #74 END @ 4/8/2026 9:19:22 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #75 START @ 4/8/2026 9:19:22 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #75 END @ 4/8/2026 9:19:22 AM +00:00
(...)
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #205 START @ 4/8/2026 9:19:48 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #205 END @ 4/8/2026 9:19:48 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #206 START @ 4/8/2026 9:19:48 AM +00:00
info: Spectator[0]
      [user:157] SendFrameData #206 END @ 4/8/2026 9:19:48 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "2", Target: "EndPlaySession", Arguments: [ Beatmap:843308 Mods: Ruleset:0 State:Passed ], StreamIds: [  ] }.
info: FileScoreStorage[0]
      Writing replay for score 237 to 237

The bad case (BeginPlaySession() fails server-side after 15s)

  1. Same as the good case, the client fire-and-forgets SpectatorClient.BeginPlaying(), sets isPlaying = true, and continues to call HandleFrame() / purgePendingFrames() / SendFramesInternal() / SendFrameData() (outbound to signalr) until the server is done.
  2. Eventually BeginPlaySession() fails, and the client-side signalr buffer is flushed. At this point two things happen on both ends:
    a. The server receives all of the backlogged SendFrameData() invocations. Because BeginPlaySession() failed and the user's spectator state is basically such that they're not in a play session, all of the backlogged invocations are dropped.
    b. The client receives the exception that failed the invocation of BeginPlaySession(). In response, the user is denoted as no-longer-playing via then Because isPlaying is set to false, all incoming HandleFrame() invocations, which come directly from gameplay and are supposed to enqueue incoming frames as pending for submission later are dropped via
    if (!isPlaying)
    {
    Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored.");
    return;
    }
    and all already-pending frames at this time get dropped on next requested pending frame flush via
    if (!isPlaying)
    {
    // it is possible for this to happen if the `BeginPlayingInternal()` call takes a long time,
    // the client accumulates a purgeable bundle of frames in the meantime,
    // and then `BeginPlayingInternal()` finally fails and `clearScoreState()` is called to abort the streaming session.
    Logger.Log($"{nameof(SpectatorClient)} dropping pending frames as the user is no longer considered to be playing.");
    pendingFrames.Clear();
    return;
    }
client-side logs
[runtime] 2026-04-08 10:40:06 [verbose]: [OnlineSpectatorClient] BeginPlayingInternal server invocation START
[runtime] 2026-04-08 10:40:06 [verbose]: GameplayClockContainer started via call to StartGameplayClock
[runtime] 2026-04-08 10:40:06 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #1 START
[runtime] 2026-04-08 10:40:06 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #1 END
(...)
[runtime] 2026-04-08 10:40:21 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #73 START
[runtime] 2026-04-08 10:40:21 [verbose]: [OnlineSpectatorClient] SendFramesInternal server invocation #73 END
[network] 2026-04-08 10:40:21 [verbose]: OnlineSpectatorClient.BeginPlayingInternal failed: An unexpected error occurred invoking 'BeginPlaySession' on the server. HubException: Nope.
[runtime] 2026-04-08 10:40:21 [verbose]: Clearing SpectatorClient state due to failed BeginPlayingInternal call.
[runtime] 2026-04-08 10:40:21 [verbose]: Frames arrived at SpectatorClient outside of gameplay scope and will be ignored.
[runtime] 2026-04-08 10:40:21 [verbose]: SpectatorClient dropping pending frames as the user is no longer considered to be playing.
server-side logs
info: Spectator[0]
      [user:157] BeginPlaySession START @ 4/8/2026 10:40:06 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
fail: Spectator[0]
      [user:157] Failed to invoke hub method: BeginPlaySession(346, Beatmap:843308 Mods: Ruleset:0 State:Playing)
      Microsoft.AspNetCore.SignalR.HubException: Nope.
         at osu.Server.Spectator.Hubs.Spectator.SpectatorHub.BeginPlaySession(Nullable`1 scoreToken, SpectatorState state) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs:line 62
         at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.ExecuteMethod(ObjectMethodExecutor methodExecutor, Hub hub, Object[] arguments)
         at osu.Server.Spectator.ClientVersionChecker.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/ClientVersionChecker.cs:line 53
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at osu.Server.Spectator.ConcurrentConnectionLimiter.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/ConcurrentConnectionLimiter.cs:line 106
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at osu.Server.Spectator.LoggingHubFilter.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/LoggingHubFilter.cs:line 30
fail: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[8]
      Failed to invoke hub method 'BeginPlaySession'.
      Microsoft.AspNetCore.SignalR.HubException: Nope.
         at osu.Server.Spectator.Hubs.Spectator.SpectatorHub.BeginPlaySession(Nullable`1 scoreToken, SpectatorState state) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs:line 62
         at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.ExecuteMethod(ObjectMethodExecutor methodExecutor, Hub hub, Object[] arguments)
         at osu.Server.Spectator.ClientVersionChecker.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/ClientVersionChecker.cs:line 53
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at osu.Server.Spectator.ConcurrentConnectionLimiter.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/ConcurrentConnectionLimiter.cs:line 106
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at osu.Server.Spectator.LoggingHubFilter.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next) in /Users/bdach/Documents/Work/open-source/osu-server-spectator/osu.Server.Spectator/LoggingHubFilter.cs:line 30
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.HubFilterFactory.InvokeMethodAsync(HubInvocationContext invocationContext, Func`2 next)
         at Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher`1.<Invoke>g__ExecuteInvocation|18_0(DefaultHubDispatcher`1 dispatcher, ObjectMethodExecutor methodExecutor, THub hub, Object[] arguments, AsyncServiceScope scope, IHubActivator`1 hubActivator, HubConnectionContext connection, HubMethodInvocationMessage hubMethodInvocationMessage, Boolean isStreamCall)
info: Spectator[0]
      [user:157] SendFrameData #1 START @ 4/8/2026 10:40:21 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.
info: Spectator[0]
      [user:157] SendFrameData #2 START @ 4/8/2026 10:40:21 AM +00:00
dbug: Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher[1]
      Received hub invocation: InvocationMessage { InvocationId: "", Target: "SendFrameData", Arguments: [ osu.Game.Online.Spectator.FrameDataBundle ], StreamIds: [  ] }.

Hope this wall of text assuages doubts and hasn't just been a complete waste of time.

// the client accumulates a purgeable bundle of frames in the meantime,
// and then `BeginPlayingInternal()` finally fails and `clearScoreState()` is called to abort the streaming session.
Logger.Log($"{nameof(SpectatorClient)} dropping pending frames as the user is no longer considered to be playing.");
pendingFrames.Clear();
return;
}

Debug.Assert(currentScore != null);
Debug.Assert(currentScoreProcessor != null);

Expand Down
5 changes: 3 additions & 2 deletions osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,15 @@ void flush()
}
}

protected override Task BeginPlayingInternal(long? scoreToken, SpectatorState state)
protected override async Task<bool> BeginPlayingInternal(long? scoreToken, SpectatorState state)
{
// Track the local user's playing beatmap ID.
Debug.Assert(state.BeatmapID != null);
userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value;
userModsDictionary[api.LocalUser.Value.Id] = state.Mods.ToArray();

return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state);
await ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state).ConfigureAwait(false);
return true;
}

protected override Task SendFramesInternal(FrameDataBundle bundle)
Expand Down
Loading