Merge pull request #3541 from jellyfin/pause

Keep playstate during syncplay group creation
This commit is contained in:
dkanada 2020-07-14 09:25:46 +09:00 committed by GitHub
commit 203825f772
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 41 additions and 64 deletions

View file

@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay
} }
/// <inheritdoc /> /// <inheritdoc />
public void InitGroup(SessionInfo session, CancellationToken cancellationToken) public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
{ {
_group.AddSession(session); _group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this); _syncPlayManager.AddSessionToGroup(session, this);
_group.PlayingItem = session.FullNowPlayingItem; _group.PlayingItem = session.FullNowPlayingItem;
_group.IsPaused = true; _group.IsPaused = session.PlayState.IsPaused;
_group.PositionTicks = session.PlayState.PositionTicks ?? 0; _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
_group.LastActivity = DateTime.UtcNow; _group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
{ {
if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
{ {
_group.AddSession(session); _group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this); _syncPlayManager.AddSessionToGroup(session, this);
@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
// Client join and play, syncing will happen client side // Syncing will happen client-side
if (!_group.IsPaused) if (!_group.IsPaused)
{ {
var playCommand = NewSyncPlayCommand(SendCommandType.Play); var playCommand = NewSyncPlayCommand(SendCommandType.Play);
@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay
/// <inheritdoc /> /// <inheritdoc />
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
{ {
// The server's job is to mantain a consistent state to which clients refer to, // The server's job is to maintain a consistent state for clients to reference
// as also to notify clients of state changes. // and notify clients of state changes. The actual syncing of media playback
// The actual syncing of media playback happens client side. // happens client side. Clients are aware of the server's time and use it to sync.
// Clients are aware of the server's time and use it to sync.
switch (request.Type) switch (request.Type)
{ {
case PlaybackRequestType.Play: case PlaybackRequestType.Play:
@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay
case PlaybackRequestType.Seek: case PlaybackRequestType.Seek:
HandleSeekRequest(session, request, cancellationToken); HandleSeekRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.Buffering: case PlaybackRequestType.Buffer:
HandleBufferingRequest(session, request, cancellationToken); HandleBufferingRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.BufferingDone: case PlaybackRequestType.Ready:
HandleBufferingDoneRequest(session, request, cancellationToken); HandleBufferingDoneRequest(session, request, cancellationToken);
break; break;
case PlaybackRequestType.UpdatePing: case PlaybackRequestType.Ping:
HandlePingUpdateRequest(session, request); HandlePingUpdateRequest(session, request);
break; break;
} }
@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
// Pick a suitable time that accounts for latency // Pick a suitable time that accounts for latency
var delay = _group.GetHighestPing() * 2; var delay = _group.GetHighestPing() * 2;
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
// Unpause group and set starting point in future // Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@ -337,8 +334,9 @@ namespace Emby.Server.Implementations.SyncPlay
var currentTime = DateTime.UtcNow; var currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - _group.LastActivity; var elapsedTime = currentTime - _group.LastActivity;
_group.LastActivity = currentTime; _group.LastActivity = currentTime;
// Seek only if playback actually started // Seek only if playback actually started
// (a pause request may be issued during the delay added to account for latency) // Pause request may be issued during the delay added to account for latency
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
var command = NewSyncPlayCommand(SendCommandType.Pause); var command = NewSyncPlayCommand(SendCommandType.Pause);
@ -451,7 +449,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
// Client, that was buffering, resumed playback but did not update others in time // Client, that was buffering, resumed playback but did not update others in time
delay = _group.GetHighestPing() * 2; delay = _group.GetHighestPing() * 2;
delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
_group.LastActivity = currentTime.AddMilliseconds( _group.LastActivity = currentTime.AddMilliseconds(
delay); delay);
@ -495,7 +493,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{ {
// Collected pings are used to account for network latency when unpausing playback // Collected pings are used to account for network latency when unpausing playback
_group.UpdatePing(session, request.Ping ?? _group.DefaulPing); _group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
var error = new GroupUpdate<string>() var error = new GroupUpdate<string>
{ {
Type = GroupUpdateType.CreateGroupDenied Type = GroupUpdateType.CreateGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
var group = new SyncPlayController(_sessionManager, this); var group = new SyncPlayController(_sessionManager, this);
_groups[group.GetGroupId()] = group; _groups[group.GetGroupId()] = group;
group.InitGroup(session, cancellationToken); group.CreateGroup(session, cancellationToken);
} }
} }
@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
Type = GroupUpdateType.JoinGroupDenied Type = GroupUpdateType.JoinGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList(); group => group.GetInfo()).ToList();
} }
// Otherwise show all available groups
else else
{ {
// Otherwise show all available groups
return _groups.Values.Where( return _groups.Values.Where(
group => HasAccessToItem(user, group.GetPlayingItemId())).Select( group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
group => group.GetInfo()).ToList(); group => group.GetInfo()).ToList();
@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
{ {
Type = GroupUpdateType.JoinGroupDenied Type = GroupUpdateType.JoinGroupDenied
}; };
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return; return;
} }
@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
} }
_sessionToGroupMap.Remove(session.Id, out var tempGroup); _sessionToGroupMap.Remove(session.Id, out var tempGroup);
if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{ {
throw new InvalidOperationException("Session was in wrong group!"); throw new InvalidOperationException("Session was in wrong group!");

View file

@ -27,13 +27,6 @@ namespace MediaBrowser.Api.SyncPlay
/// <value>The Group id to join.</value> /// <value>The Group id to join.</value>
[ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string GroupId { get; set; } public string GroupId { get; set; }
/// <summary>
/// Gets or sets the playing item id.
/// </summary>
/// <value>The client's currently playing item id.</value>
[ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string PlayingItemId { get; set; }
} }
[Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")] [Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")]
@ -89,7 +82,7 @@ namespace MediaBrowser.Api.SyncPlay
public long PositionTicks { get; set; } public long PositionTicks { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether this is a buffering or a buffering-done request. /// Gets or sets whether this is a buffering or a ready request.
/// </summary> /// </summary>
/// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value> /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
[ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
@ -150,25 +143,15 @@ namespace MediaBrowser.Api.SyncPlay
var currentSession = GetSession(_sessionContext); var currentSession = GetSession(_sessionContext);
Guid groupId; Guid groupId;
Guid playingItemId = Guid.Empty;
if (!Guid.TryParse(request.GroupId, out groupId)) if (!Guid.TryParse(request.GroupId, out groupId))
{ {
Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId); Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
return; return;
} }
// Both null and empty strings mean that client isn't playing anything
if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId))
{
Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
return;
}
var joinRequest = new JoinGroupRequest() var joinRequest = new JoinGroupRequest()
{ {
GroupId = groupId, GroupId = groupId
PlayingItemId = playingItemId
}; };
_syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
@ -254,7 +237,7 @@ namespace MediaBrowser.Api.SyncPlay
var currentSession = GetSession(_sessionContext); var currentSession = GetSession(_sessionContext);
var syncPlayRequest = new PlaybackRequest() var syncPlayRequest = new PlaybackRequest()
{ {
Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
When = DateTime.Parse(request.When), When = DateTime.Parse(request.When),
PositionTicks = request.PositionTicks PositionTicks = request.PositionTicks
}; };
@ -270,7 +253,7 @@ namespace MediaBrowser.Api.SyncPlay
var currentSession = GetSession(_sessionContext); var currentSession = GetSession(_sessionContext);
var syncPlayRequest = new PlaybackRequest() var syncPlayRequest = new PlaybackRequest()
{ {
Type = PlaybackRequestType.UpdatePing, Type = PlaybackRequestType.Ping,
Ping = Convert.ToInt64(request.Ping) Ping = Convert.ToInt64(request.Ping)
}; };
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);

View file

@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary> /// <summary>
/// Gets the default ping value used for sessions. /// Gets the default ping value used for sessions.
/// </summary> /// </summary>
public long DefaulPing { get; } = 500; public long DefaultPing { get; } = 500;
/// <summary> /// <summary>
/// Gets or sets the group identifier. /// Gets or sets the group identifier.
@ -70,16 +70,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param> /// <param name="session">The session.</param>
public void AddSession(SessionInfo session) public void AddSession(SessionInfo session)
{ {
if (ContainsSession(session.Id.ToString())) if (ContainsSession(session.Id))
{ {
return; return;
} }
var member = new GroupMember(); var member = new GroupMember();
member.Session = session; member.Session = session;
member.Ping = DefaulPing; member.Ping = DefaultPing;
member.IsBuffering = false; member.IsBuffering = false;
Participants[session.Id.ToString()] = member; Participants[session.Id] = member;
} }
/// <summary> /// <summary>
@ -88,12 +88,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param> /// <param name="session">The session.</param>
public void RemoveSession(SessionInfo session) public void RemoveSession(SessionInfo session)
{ {
if (!ContainsSession(session.Id.ToString())) if (!ContainsSession(session.Id))
{ {
return; return;
} }
Participants.Remove(session.Id.ToString(), out _); Participants.Remove(session.Id, out _);
} }
/// <summary> /// <summary>
@ -103,12 +103,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="ping">The ping.</param> /// <param name="ping">The ping.</param>
public void UpdatePing(SessionInfo session, long ping) public void UpdatePing(SessionInfo session, long ping)
{ {
if (!ContainsSession(session.Id.ToString())) if (!ContainsSession(session.Id))
{ {
return; return;
} }
Participants[session.Id.ToString()].Ping = ping; Participants[session.Id].Ping = ping;
} }
/// <summary> /// <summary>
@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <value name="session">The highest ping in the group.</value> /// <value name="session">The highest ping in the group.</value>
public long GetHighestPing() public long GetHighestPing()
{ {
long max = Int64.MinValue; long max = long.MinValue;
foreach (var session in Participants.Values) foreach (var session in Participants.Values)
{ {
max = Math.Max(max, session.Ping); max = Math.Max(max, session.Ping);
@ -133,12 +133,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="isBuffering">The state.</param> /// <param name="isBuffering">The state.</param>
public void SetBuffering(SessionInfo session, bool isBuffering) public void SetBuffering(SessionInfo session, bool isBuffering)
{ {
if (!ContainsSession(session.Id.ToString())) if (!ContainsSession(session.Id))
{ {
return; return;
} }
Participants[session.Id.ToString()].IsBuffering = isBuffering; Participants[session.Id].IsBuffering = isBuffering;
} }
/// <summary> /// <summary>

View file

@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupMember public class GroupMember
{ {
/// <summary> /// <summary>
/// Gets or sets whether this member is buffering. /// Gets or sets a value indicating whether this member is buffering.
/// </summary> /// </summary>
/// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value> /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
public bool IsBuffering { get; set; } public bool IsBuffering { get; set; }

View file

@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// </summary> /// </summary>
/// <param name="session">The session.</param> /// <param name="session">The session.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
void InitGroup(SessionInfo session, CancellationToken cancellationToken); void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Adds the session to the group. /// Adds the session to the group.

View file

@ -12,11 +12,5 @@ namespace MediaBrowser.Model.SyncPlay
/// </summary> /// </summary>
/// <value>The Group id to join.</value> /// <value>The Group id to join.</value>
public Guid GroupId { get; set; } public Guid GroupId { get; set; }
/// <summary>
/// Gets or sets the playing item id.
/// </summary>
/// <value>The client's currently playing item id.</value>
public Guid PlayingItemId { get; set; }
} }
} }

View file

@ -23,16 +23,16 @@ namespace MediaBrowser.Model.SyncPlay
/// <summary> /// <summary>
/// A user is signaling that playback is buffering. /// A user is signaling that playback is buffering.
/// </summary> /// </summary>
Buffering = 3, Buffer = 3,
/// <summary> /// <summary>
/// A user is signaling that playback resumed. /// A user is signaling that playback resumed.
/// </summary> /// </summary>
BufferingDone = 4, Ready = 4,
/// <summary> /// <summary>
/// A user is reporting its ping. /// A user is reporting its ping.
/// </summary> /// </summary>
UpdatePing = 5 Ping = 5
} }
} }