diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 171047e65f..c819c163a7 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -176,10 +176,9 @@ namespace Emby.Server.Implementations.HttpServer
{
SendKeepAliveResponse();
}
-
- if (OnReceive != null)
+ else
{
- OnReceive(info);
+ OnReceive?.Invoke(info);
}
}
catch (Exception ex)
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index d8e02ef395..b0c6d0aa08 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System;
using System.Collections.Concurrent;
using System.Linq;
@@ -25,7 +26,7 @@ namespace Emby.Server.Implementations.Session
public readonly int WebSocketLostTimeout = 60;
///
- /// The timer factor; controls the frequency of the timer.
+ /// The keep-alive timer factor; controls how often the timer will check on the status of the WebSockets.
///
public readonly double TimerFactor = 0.2;
@@ -136,11 +137,10 @@ namespace Emby.Server.Implementations.Session
///
/// The WebSocket.
/// The event arguments.
- private void _webSocket_Closed(object sender, EventArgs e)
+ private void OnWebSocketClosed(object sender, EventArgs e)
{
var webSocket = (IWebSocketConnection) sender;
- webSocket.Closed -= _webSocket_Closed;
- _webSockets.TryRemove(webSocket, out _);
+ RemoveWebSocket(webSocket);
}
///
@@ -149,8 +149,12 @@ namespace Emby.Server.Implementations.Session
/// The WebSocket to monitor.
private async void KeepAliveWebSocket(IWebSocketConnection webSocket)
{
- _webSockets.TryAdd(webSocket, 0);
- webSocket.Closed += _webSocket_Closed;
+ if (!_webSockets.TryAdd(webSocket, 0))
+ {
+ _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
+ return;
+ }
+ webSocket.Closed += OnWebSocketClosed;
webSocket.LastKeepAliveDate = DateTime.UtcNow;
// Notify WebSocket about timeout
@@ -160,12 +164,22 @@ namespace Emby.Server.Implementations.Session
}
catch (WebSocketException exception)
{
- _logger.LogDebug(exception, "Error sending ForceKeepAlive message to WebSocket.");
+ _logger.LogWarning(exception, "Error sending ForceKeepAlive message to WebSocket.");
}
StartKeepAliveTimer();
}
+ ///
+ /// Removes a WebSocket from the KeepAlive watchlist.
+ ///
+ /// The WebSocket to remove.
+ private void RemoveWebSocket(IWebSocketConnection webSocket)
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ _webSockets.TryRemove(webSocket, out _);
+ }
+
///
/// Starts the KeepAlive timer.
///
@@ -195,7 +209,7 @@ namespace Emby.Server.Implementations.Session
foreach (var pair in _webSockets)
{
- pair.Key.Closed -= _webSocket_Closed;
+ pair.Key.Closed -= OnWebSocketClosed;
}
}
@@ -214,7 +228,7 @@ namespace Emby.Server.Implementations.Session
if (inactive.Any())
{
- _logger.LogDebug("Sending ForceKeepAlive message to {0} WebSockets.", inactive.Count());
+ _logger.LogDebug("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count());
}
foreach (var webSocket in inactive)
@@ -225,15 +239,19 @@ namespace Emby.Server.Implementations.Session
}
catch (WebSocketException exception)
{
- _logger.LogDebug(exception, "Error sending ForceKeepAlive message to WebSocket.");
+ _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
lost.Append(webSocket);
}
}
if (lost.Any())
{
- // TODO: handle lost webSockets
- _logger.LogDebug("Lost {0} WebSockets.", lost.Count());
+ _logger.LogInformation("Lost {0} WebSockets.", lost.Count());
+ foreach (var webSocket in lost)
+ {
+ // TODO: handle session relative to the lost webSocket
+ RemoveWebSocket(webSocket);
+ }
}
if (!_webSockets.Any())
diff --git a/Emby.Server.Implementations/Syncplay/SyncplayController.cs b/Emby.Server.Implementations/Syncplay/SyncplayController.cs
index 83b4779447..02cf08cd7c 100644
--- a/Emby.Server.Implementations/Syncplay/SyncplayController.cs
+++ b/Emby.Server.Implementations/Syncplay/SyncplayController.cs
@@ -16,11 +16,26 @@ namespace Emby.Server.Implementations.Syncplay
///
public class SyncplayController : ISyncplayController, IDisposable
{
+ ///
+ /// Used to filter the sessions of a group.
+ ///
private enum BroadcastType
{
+ ///
+ /// All sessions will receive the message.
+ ///
AllGroup = 0,
- SingleSession = 1,
- AllExceptSession = 2,
+ ///
+ /// Only the specified session will receive the message.
+ ///
+ CurrentSession = 1,
+ ///
+ /// All sessions, except the current one, will receive the message.
+ ///
+ AllExceptCurrentSession = 2,
+ ///
+ /// Only sessions that are not buffering will receive the message.
+ ///
AllReady = 3
}
@@ -95,40 +110,46 @@ namespace Emby.Server.Implementations.Syncplay
}
}
+ ///
+ /// Filters sessions of this group.
+ ///
+ /// The current session.
+ /// The filtering type.
+ /// The array of sessions matching the filter.
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
{
- if (type == BroadcastType.SingleSession)
+ switch (type)
{
- return new SessionInfo[] { from };
- }
- else if (type == BroadcastType.AllGroup)
- {
- return _group.Partecipants.Values.Select(
- session => session.Session
- ).ToArray();
- }
- else if (type == BroadcastType.AllExceptSession)
- {
- return _group.Partecipants.Values.Select(
- session => session.Session
- ).Where(
- session => !session.Id.Equals(from.Id)
- ).ToArray();
- }
- else if (type == BroadcastType.AllReady)
- {
- return _group.Partecipants.Values.Where(
- session => !session.IsBuffering
- ).Select(
- session => session.Session
- ).ToArray();
- }
- else
- {
- return new SessionInfo[] {};
+ case BroadcastType.CurrentSession:
+ return new SessionInfo[] { from };
+ case BroadcastType.AllGroup:
+ return _group.Participants.Values.Select(
+ session => session.Session
+ ).ToArray();
+ case BroadcastType.AllExceptCurrentSession:
+ return _group.Participants.Values.Select(
+ session => session.Session
+ ).Where(
+ session => !session.Id.Equals(from.Id)
+ ).ToArray();
+ case BroadcastType.AllReady:
+ return _group.Participants.Values.Where(
+ session => !session.IsBuffering
+ ).Select(
+ session => session.Session
+ ).ToArray();
+ default:
+ return new SessionInfo[] { };
}
}
+ ///
+ /// Sends a GroupUpdate message to the interested sessions.
+ ///
+ /// The current session.
+ /// The filtering type.
+ /// The message to send.
+ /// The task.
private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message)
{
IEnumerable GetTasks()
@@ -143,6 +164,13 @@ namespace Emby.Server.Implementations.Syncplay
return Task.WhenAll(GetTasks());
}
+ ///
+ /// Sends a playback command to the interested sessions.
+ ///
+ /// The current session.
+ /// The filtering type.
+ /// The message to send.
+ /// The task.
private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message)
{
IEnumerable GetTasks()
@@ -157,31 +185,44 @@ namespace Emby.Server.Implementations.Syncplay
return Task.WhenAll(GetTasks());
}
+ ///
+ /// Builds a new playback command with some default values.
+ ///
+ /// The command type.
+ /// The SendCommand.
private SendCommand NewSyncplayCommand(SendCommandType type)
{
- var command = new SendCommand();
- command.GroupId = _group.GroupId.ToString();
- command.Command = type;
- command.PositionTicks = _group.PositionTicks;
- command.When = _group.LastActivity.ToUniversalTime().ToString("o");
- command.EmittedAt = DateTime.UtcNow.ToUniversalTime().ToString("o");
- return command;
+ return new SendCommand()
+ {
+ GroupId = _group.GroupId.ToString(),
+ Command = type,
+ PositionTicks = _group.PositionTicks,
+ When = _group.LastActivity.ToUniversalTime().ToString("o"),
+ EmittedAt = DateTime.UtcNow.ToUniversalTime().ToString("o")
+ };
}
+ ///
+ /// Builds a new group update message.
+ ///
+ /// The update type.
+ /// The data to send.
+ /// The GroupUpdate.
private GroupUpdate NewSyncplayGroupUpdate(GroupUpdateType type, T data)
{
- var command = new GroupUpdate();
- command.GroupId = _group.GroupId.ToString();
- command.Type = type;
- command.Data = data;
- return command;
+ return new GroupUpdate()
+ {
+ GroupId = _group.GroupId.ToString(),
+ Type = type,
+ Data = data
+ };
}
///
public void InitGroup(SessionInfo session)
{
_group.AddSession(session);
- _syncplayManager.MapSessionToGroup(session, this);
+ _syncplayManager.AddSessionToGroup(session, this);
_group.PlayingItem = session.FullNowPlayingItem;
_group.IsPaused = true;
@@ -189,37 +230,35 @@ namespace Emby.Server.Implementations.Syncplay
_group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
- SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.SingleSession, pauseCommand);
+ SendCommand(session, BroadcastType.CurrentSession, pauseCommand);
}
///
public void SessionJoin(SessionInfo session, JoinGroupRequest request)
{
- if (session.NowPlayingItem != null &&
- session.NowPlayingItem.Id.Equals(_group.PlayingItem.Id) &&
- request.PlayingItemId.Equals(_group.PlayingItem.Id))
+ if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
{
_group.AddSession(session);
- _syncplayManager.MapSessionToGroup(session, this);
+ _syncplayManager.AddSessionToGroup(session, this);
var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
- SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
// Client join and play, syncing will happen client side
if (!_group.IsPaused)
{
var playCommand = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.SingleSession, playCommand);
+ SendCommand(session, BroadcastType.CurrentSession, playCommand);
}
else
{
var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.SingleSession, pauseCommand);
+ SendCommand(session, BroadcastType.CurrentSession, pauseCommand);
}
}
else
@@ -228,7 +267,7 @@ namespace Emby.Server.Implementations.Syncplay
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
playRequest.StartPositionTicks = _group.PositionTicks;
var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
- SendGroupUpdate(session, BroadcastType.SingleSession, update);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, update);
}
}
@@ -236,182 +275,250 @@ namespace Emby.Server.Implementations.Syncplay
public void SessionLeave(SessionInfo session)
{
_group.RemoveSession(session);
- _syncplayManager.UnmapSessionFromGroup(session, this);
+ _syncplayManager.RemoveSessionFromGroup(session, this);
var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
- SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
}
///
public void HandleRequest(SessionInfo session, PlaybackRequest request)
{
- if (request.Type.Equals(PlaybackRequestType.Play))
+ // The server's job is to mantain a consistent state to which clients refer to,
+ // as also to notify clients of state changes.
+ // The actual syncing of media playback happens client side.
+ // Clients are aware of the server's time and use it to sync.
+ switch (request.Type)
{
- if (_group.IsPaused)
- {
- var delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
-
- _group.IsPaused = false;
- _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
- delay
- );
-
- var command = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllGroup, command);
- }
- else
- {
- // Client got lost
- var command = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.SingleSession, command);
- }
+ case PlaybackRequestType.Play:
+ HandlePlayRequest(session, request);
+ break;
+ case PlaybackRequestType.Pause:
+ HandlePauseRequest(session, request);
+ break;
+ case PlaybackRequestType.Seek:
+ HandleSeekRequest(session, request);
+ break;
+ case PlaybackRequestType.Buffering:
+ HandleBufferingRequest(session, request);
+ break;
+ case PlaybackRequestType.BufferingDone:
+ HandleBufferingDoneRequest(session, request);
+ break;
+ case PlaybackRequestType.UpdatePing:
+ HandlePingUpdateRequest(session, request);
+ break;
}
- else if (request.Type.Equals(PlaybackRequestType.Pause))
+ }
+
+ ///
+ /// Handles a play action requested by a session.
+ ///
+ /// The session.
+ /// The play action.
+ private void HandlePlayRequest(SessionInfo session, PlaybackRequest request)
+ {
+ if (_group.IsPaused)
{
- if (!_group.IsPaused)
- {
- _group.IsPaused = true;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - _group.LastActivity;
- _group.LastActivity = currentTime;
- _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+ // Pick a suitable time that accounts for latency
+ var delay = _group.GetHighestPing() * 2;
+ delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
- var command = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.AllGroup, command);
- }
- else
- {
- var command = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.SingleSession, command);
- }
- }
- else if (request.Type.Equals(PlaybackRequestType.Seek))
- {
- // Sanitize PositionTicks
- var ticks = request.PositionTicks ??= 0;
- ticks = ticks >= 0 ? ticks : 0;
- if (_group.PlayingItem.RunTimeTicks != null)
- {
- var runTimeTicks = _group.PlayingItem.RunTimeTicks ??= 0;
- ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
- }
+ // Unpause group and set starting point in future
+ // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
+ // The added delay does not guarantee, of course, that the command will be received in time
+ // Playback synchronization will mainly happen client side
+ _group.IsPaused = false;
+ _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
+ delay
+ );
- _group.IsPaused = true;
- _group.PositionTicks = ticks;
- _group.LastActivity = DateTime.UtcNow;
-
- var command = NewSyncplayCommand(SendCommandType.Seek);
+ var command = NewSyncplayCommand(SendCommandType.Play);
SendCommand(session, BroadcastType.AllGroup, command);
}
- // TODO: client does not implement this yet
- else if (request.Type.Equals(PlaybackRequestType.Buffering))
+ else
{
- if (!_group.IsPaused)
+ // Client got lost, sending current state
+ var command = NewSyncplayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.CurrentSession, command);
+ }
+ }
+
+ ///
+ /// Handles a pause action requested by a session.
+ ///
+ /// The session.
+ /// The pause action.
+ private void HandlePauseRequest(SessionInfo session, PlaybackRequest request)
+ {
+ if (!_group.IsPaused)
+ {
+ // Pause group and compute the media playback position
+ _group.IsPaused = true;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - _group.LastActivity;
+ _group.LastActivity = currentTime;
+ // Seek only if playback actually started
+ // (a pause request may be issued during the delay added to account for latency)
+ _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ var command = NewSyncplayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.AllGroup, command);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = NewSyncplayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, command);
+ }
+ }
+
+ ///
+ /// Handles a seek action requested by a session.
+ ///
+ /// The session.
+ /// The seek action.
+ private void HandleSeekRequest(SessionInfo session, PlaybackRequest request)
+ {
+ // Sanitize PositionTicks
+ var ticks = request.PositionTicks ??= 0;
+ ticks = ticks >= 0 ? ticks : 0;
+ if (_group.PlayingItem.RunTimeTicks != null)
+ {
+ var runTimeTicks = _group.PlayingItem.RunTimeTicks ??= 0;
+ ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
+ }
+
+ // Pause and seek
+ _group.IsPaused = true;
+ _group.PositionTicks = ticks;
+ _group.LastActivity = DateTime.UtcNow;
+
+ var command = NewSyncplayCommand(SendCommandType.Seek);
+ SendCommand(session, BroadcastType.AllGroup, command);
+ }
+
+ ///
+ /// Handles a buffering action requested by a session.
+ ///
+ /// The session.
+ /// The buffering action.
+ private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request)
+ {
+ if (!_group.IsPaused)
+ {
+ // Pause group and compute the media playback position
+ _group.IsPaused = true;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - _group.LastActivity;
+ _group.LastActivity = currentTime;
+ _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ _group.SetBuffering(session, true);
+
+ // Send pause command to all non-buffering sessions
+ var command = NewSyncplayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.AllReady, command);
+
+ var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = NewSyncplayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, command);
+ }
+ }
+
+ ///
+ /// Handles a buffering-done action requested by a session.
+ ///
+ /// The session.
+ /// The buffering-done action.
+ private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request)
+ {
+ if (_group.IsPaused)
+ {
+ _group.SetBuffering(session, false);
+
+ var when = request.When ??= DateTime.UtcNow;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - when;
+ var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
+ var delay = _group.PositionTicks - clientPosition.Ticks;
+
+ if (_group.IsBuffering())
{
- _group.IsPaused = true;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - _group.LastActivity;
- _group.LastActivity = currentTime;
- _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
-
- _group.SetBuffering(session, true);
-
- // Send pause command to all non-buffering sessions
+ // Others are buffering, tell this client to pause when ready
var command = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.AllReady, command);
-
- var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
+ command.When = currentTime.AddMilliseconds(
+ delay
+ ).ToUniversalTime().ToString("o");
+ SendCommand(session, BroadcastType.CurrentSession, command);
}
else
{
- var command = NewSyncplayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.SingleSession, command);
- }
- }
- // TODO: client does not implement this yet
- else if (request.Type.Equals(PlaybackRequestType.BufferingComplete))
- {
- if (_group.IsPaused)
- {
- _group.SetBuffering(session, false);
+ // Let other clients resume as soon as the buffering client catches up
+ _group.IsPaused = false;
- if (_group.IsBuffering()) {
- // Others are buffering, tell this client to pause when ready
- var when = request.When ??= DateTime.UtcNow;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - when;
- var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
- var delay = _group.PositionTicks - clientPosition.Ticks;
-
- var command = NewSyncplayCommand(SendCommandType.Pause);
- command.When = currentTime.AddMilliseconds(
+ if (delay > _group.GetHighestPing() * 2)
+ {
+ // Client that was buffering is recovering, notifying others to resume
+ _group.LastActivity = currentTime.AddMilliseconds(
delay
- ).ToUniversalTime().ToString("o");
- SendCommand(session, BroadcastType.SingleSession, command);
+ );
+ var command = NewSyncplayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.AllExceptCurrentSession, command);
}
else
{
- // Let other clients resume as soon as the buffering client catches up
- var when = request.When ??= DateTime.UtcNow;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - when;
- var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
- var delay = _group.PositionTicks - clientPosition.Ticks;
+ // Client, that was buffering, resumed playback but did not update others in time
+ delay = _group.GetHighestPing() * 2;
+ delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
- _group.IsPaused = false;
+ _group.LastActivity = currentTime.AddMilliseconds(
+ delay
+ );
- if (delay > _group.GetHighestPing() * 2)
- {
- // Client that was buffering is recovering, notifying others to resume
- _group.LastActivity = currentTime.AddMilliseconds(
- delay
- );
- var command = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllExceptSession, command);
- }
- else
- {
- // Client, that was buffering, resumed playback but did not update others in time
- delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
-
- _group.LastActivity = currentTime.AddMilliseconds(
- delay
- );
-
- var command = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllGroup, command);
- }
- }
- }
- else
- {
- // Make sure client has latest group state
- var command = NewSyncplayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.SingleSession, command);
+ var command = NewSyncplayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.AllGroup, command);
+ }
}
}
- else if (request.Type.Equals(PlaybackRequestType.UpdatePing))
+ else
{
- _group.UpdatePing(session, request.Ping ??= _group.DefaulPing);
+ // Group was not waiting, make sure client has latest state
+ var command = NewSyncplayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.CurrentSession, command);
}
}
+ ///
+ /// Updates ping of a session.
+ ///
+ /// The session.
+ /// The update.
+ private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
+ {
+ // Collected pings are used to account for network latency when unpausing playback
+ _group.UpdatePing(session, request.Ping ??= _group.DefaulPing);
+ }
+
///
public GroupInfoView GetInfo()
{
- var info = new GroupInfoView();
- info.GroupId = GetGroupId().ToString();
- info.PlayingItemName = _group.PlayingItem.Name;
- info.PlayingItemId = _group.PlayingItem.Id.ToString();
- info.PositionTicks = _group.PositionTicks;
- info.Partecipants = _group.Partecipants.Values.Select(session => session.Session.UserName).ToArray();
- return info;
+ return new GroupInfoView()
+ {
+ GroupId = GetGroupId().ToString(),
+ PlayingItemName = _group.PlayingItem.Name,
+ PlayingItemId = _group.PlayingItem.Id.ToString(),
+ PositionTicks = _group.PositionTicks,
+ Participants = _group.Participants.Values.Select(session => session.Session.UserName).ToArray()
+ };
}
}
}
diff --git a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs
index 60d70e5fde..e7df8925e8 100644
--- a/Emby.Server.Implementations/Syncplay/SyncplayManager.cs
+++ b/Emby.Server.Implementations/Syncplay/SyncplayManager.cs
@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Syncplay
/// The groups.
///
private readonly ConcurrentDictionary _groups =
- new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private bool _disposed = false;
@@ -64,8 +64,8 @@ namespace Emby.Server.Implementations.Syncplay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
- _sessionManager.SessionEnded += _sessionManager_SessionEnded;
- _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
+ _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
}
///
@@ -92,8 +92,8 @@ namespace Emby.Server.Implementations.Syncplay
return;
}
- _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
- _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
_disposed = true;
}
@@ -106,14 +106,14 @@ namespace Emby.Server.Implementations.Syncplay
}
}
- void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (!IsSessionInGroup(session)) return;
LeaveGroup(session);
}
- void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var session = e.Session;
if (!IsSessionInGroup(session)) return;
@@ -130,13 +130,13 @@ namespace Emby.Server.Implementations.Syncplay
var item = _libraryManager.GetItemById(itemId);
var hasParentalRatingAccess = user.Policy.MaxParentalRating.HasValue ? item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating : true;
- if (!user.Policy.EnableAllFolders)
+ if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
{
var collections = _libraryManager.GetCollectionFolders(item).Select(
folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
);
var intersect = collections.Intersect(user.Policy.EnabledFolders);
- return intersect.Count() > 0 && hasParentalRatingAccess;
+ return intersect.Count() > 0;
}
else
{
@@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Syncplay
if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups)
{
- // TODO: shall an error message be sent back to the client?
+ // TODO: report the error to the client
throw new ArgumentException("User does not have permission to create groups");
}
@@ -187,22 +187,16 @@ namespace Emby.Server.Implementations.Syncplay
if (user.Policy.SyncplayAccess == SyncplayAccess.None)
{
- // TODO: shall an error message be sent back to the client?
+ // TODO: report the error to the client
throw new ArgumentException("User does not have access to syncplay");
}
- if (IsSessionInGroup(session))
- {
- if (GetSessionGroup(session).Equals(groupId)) return;
- LeaveGroup(session);
- }
-
ISyncplayController group;
_groups.TryGetValue(groupId, out group);
if (group == null)
{
- _logger.LogError("Syncplaymanager JoinGroup: " + groupId + " does not exist.");
+ _logger.LogWarning("Syncplaymanager JoinGroup: {0} does not exist.", groupId);
var update = new GroupUpdate();
update.Type = GroupUpdateType.NotInGroup;
@@ -215,20 +209,26 @@ namespace Emby.Server.Implementations.Syncplay
throw new ArgumentException("User does not have access to playing item");
}
+ if (IsSessionInGroup(session))
+ {
+ if (GetSessionGroup(session).Equals(groupId)) return;
+ LeaveGroup(session);
+ }
+
group.SessionJoin(session, request);
}
///
public void LeaveGroup(SessionInfo session)
{
- // TODO: what happens to users that are in a group and get their permissions revoked?
+ // TODO: determine what happens to users that are in a group and get their permissions revoked
ISyncplayController group;
_sessionToGroupMap.TryGetValue(session.Id, out group);
if (group == null)
{
- _logger.LogWarning("Syncplaymanager HandleRequest: " + session.Id + " not in group.");
+ _logger.LogWarning("Syncplaymanager LeaveGroup: {0} does not belong to any group.", session.Id);
var update = new GroupUpdate();
update.Type = GroupUpdateType.NotInGroup;
@@ -257,9 +257,7 @@ namespace Emby.Server.Implementations.Syncplay
if (session.NowPlayingItem != null)
{
return _groups.Values.Where(
- group => HasAccessToItem(user, group.GetPlayingItemId())
- ).Where(
- group => group.GetPlayingItemId().Equals(session.FullNowPlayingItem.Id)
+ group => group.GetPlayingItemId().Equals(session.FullNowPlayingItem.Id) && HasAccessToItem(user, group.GetPlayingItemId())
).Select(
group => group.GetInfo()
).ToList();
@@ -291,7 +289,7 @@ namespace Emby.Server.Implementations.Syncplay
if (group == null)
{
- _logger.LogWarning("Syncplaymanager HandleRequest: " + session.Id + " not in group.");
+ _logger.LogWarning("Syncplaymanager HandleRequest: {0} not in a group.", session.Id);
var update = new GroupUpdate();
update.Type = GroupUpdateType.NotInGroup;
@@ -302,7 +300,7 @@ namespace Emby.Server.Implementations.Syncplay
}
///
- public void MapSessionToGroup(SessionInfo session, ISyncplayController group)
+ public void AddSessionToGroup(SessionInfo session, ISyncplayController group)
{
if (IsSessionInGroup(session))
{
@@ -312,7 +310,7 @@ namespace Emby.Server.Implementations.Syncplay
}
///
- public void UnmapSessionFromGroup(SessionInfo session, ISyncplayController group)
+ public void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group)
{
if (!IsSessionInGroup(session))
{
diff --git a/MediaBrowser.Api/Syncplay/SyncplayService.cs b/MediaBrowser.Api/Syncplay/SyncplayService.cs
index af220ed81d..2eaf9ce834 100644
--- a/MediaBrowser.Api/Syncplay/SyncplayService.cs
+++ b/MediaBrowser.Api/Syncplay/SyncplayService.cs
@@ -90,12 +90,20 @@ namespace MediaBrowser.Api.Syncplay
[ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string SessionId { get; set; }
+ ///
+ /// Gets or sets the date used to pin PositionTicks in time.
+ ///
+ /// The date related to PositionTicks.
[ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
public string When { get; set; }
[ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
public long PositionTicks { get; set; }
+ ///
+ /// Gets or sets whether this is a buffering or a buffering-done request.
+ ///
+ /// true if buffering is complete; false otherwise.
[ApiMember(Name = "Resume", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Resume { get; set; }
}
@@ -162,8 +170,10 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplayJoinGroup request)
{
var currentSession = GetSession(_sessionContext);
- var joinRequest = new JoinGroupRequest();
- joinRequest.GroupId = Guid.Parse(request.GroupId);
+ var joinRequest = new JoinGroupRequest()
+ {
+ GroupId = Guid.Parse(request.GroupId)
+ };
try
{
joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId);
@@ -207,8 +217,10 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplayPlayRequest request)
{
var currentSession = GetSession(_sessionContext);
- var syncplayRequest = new PlaybackRequest();
- syncplayRequest.Type = PlaybackRequestType.Play;
+ var syncplayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Play
+ };
_syncplayManager.HandleRequest(currentSession, syncplayRequest);
}
@@ -219,8 +231,10 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplayPauseRequest request)
{
var currentSession = GetSession(_sessionContext);
- var syncplayRequest = new PlaybackRequest();
- syncplayRequest.Type = PlaybackRequestType.Pause;
+ var syncplayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Pause
+ };
_syncplayManager.HandleRequest(currentSession, syncplayRequest);
}
@@ -231,9 +245,11 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplaySeekRequest request)
{
var currentSession = GetSession(_sessionContext);
- var syncplayRequest = new PlaybackRequest();
- syncplayRequest.Type = PlaybackRequestType.Seek;
- syncplayRequest.PositionTicks = request.PositionTicks;
+ var syncplayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.Seek,
+ PositionTicks = request.PositionTicks
+ };
_syncplayManager.HandleRequest(currentSession, syncplayRequest);
}
@@ -244,10 +260,12 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplayBufferingRequest request)
{
var currentSession = GetSession(_sessionContext);
- var syncplayRequest = new PlaybackRequest();
- syncplayRequest.Type = request.Resume ? PlaybackRequestType.BufferingComplete : PlaybackRequestType.Buffering;
- syncplayRequest.When = DateTime.Parse(request.When);
- syncplayRequest.PositionTicks = request.PositionTicks;
+ var syncplayRequest = new PlaybackRequest()
+ {
+ Type = request.Resume ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
+ When = DateTime.Parse(request.When),
+ PositionTicks = request.PositionTicks
+ };
_syncplayManager.HandleRequest(currentSession, syncplayRequest);
}
@@ -258,9 +276,11 @@ namespace MediaBrowser.Api.Syncplay
public void Post(SyncplayUpdatePing request)
{
var currentSession = GetSession(_sessionContext);
- var syncplayRequest = new PlaybackRequest();
- syncplayRequest.Type = PlaybackRequestType.UpdatePing;
- syncplayRequest.Ping = Convert.ToInt64(request.Ping);
+ var syncplayRequest = new PlaybackRequest()
+ {
+ Type = PlaybackRequestType.UpdatePing,
+ Ping = Convert.ToInt64(request.Ping)
+ };
_syncplayManager.HandleRequest(currentSession, syncplayRequest);
}
}
diff --git a/MediaBrowser.Api/Syncplay/TimeSyncService.cs b/MediaBrowser.Api/Syncplay/TimeSyncService.cs
index a69e0e293a..8974130157 100644
--- a/MediaBrowser.Api/Syncplay/TimeSyncService.cs
+++ b/MediaBrowser.Api/Syncplay/TimeSyncService.cs
@@ -54,7 +54,6 @@ namespace MediaBrowser.Api.Syncplay
var response = new UtcTimeResponse();
response.RequestReceptionTime = requestReceptionTime;
- var currentSession = GetSession(_sessionContext);
// Important to keep the following two lines at the end
var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
diff --git a/MediaBrowser.Controller/Syncplay/GroupInfo.cs b/MediaBrowser.Controller/Syncplay/GroupInfo.cs
index 42e85ef864..8e886a2cb2 100644
--- a/MediaBrowser.Controller/Syncplay/GroupInfo.cs
+++ b/MediaBrowser.Controller/Syncplay/GroupInfo.cs
@@ -46,11 +46,11 @@ namespace MediaBrowser.Controller.Syncplay
public DateTime LastActivity { get; set; }
///
- /// Gets the partecipants.
+ /// Gets the participants.
///
- /// The partecipants.
- public readonly ConcurrentDictionary Partecipants =
- new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ /// The participants, or members of the group.
+ public readonly ConcurrentDictionary Participants =
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
///
/// Checks if a session is in this group.
@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Syncplay
/// true if the session is in this group; false otherwise.
public bool ContainsSession(string sessionId)
{
- return Partecipants.ContainsKey(sessionId);
+ return Participants.ContainsKey(sessionId);
}
///
@@ -72,7 +72,7 @@ namespace MediaBrowser.Controller.Syncplay
member.Session = session;
member.Ping = DefaulPing;
member.IsBuffering = false;
- Partecipants[session.Id.ToString()] = member;
+ Participants[session.Id.ToString()] = member;
}
///
@@ -84,7 +84,7 @@ namespace MediaBrowser.Controller.Syncplay
{
if (!ContainsSession(session.Id.ToString())) return;
GroupMember member;
- Partecipants.Remove(session.Id.ToString(), out member);
+ Participants.Remove(session.Id.ToString(), out member);
}
///
@@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.Syncplay
public void UpdatePing(SessionInfo session, long ping)
{
if (!ContainsSession(session.Id.ToString())) return;
- Partecipants[session.Id.ToString()].Ping = ping;
+ Participants[session.Id.ToString()].Ping = ping;
}
///
@@ -105,7 +105,7 @@ namespace MediaBrowser.Controller.Syncplay
public long GetHighestPing()
{
long max = Int64.MinValue;
- foreach (var session in Partecipants.Values)
+ foreach (var session in Participants.Values)
{
max = Math.Max(max, session.Ping);
}
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Syncplay
public void SetBuffering(SessionInfo session, bool isBuffering)
{
if (!ContainsSession(session.Id.ToString())) return;
- Partecipants[session.Id.ToString()].IsBuffering = isBuffering;
+ Participants[session.Id.ToString()].IsBuffering = isBuffering;
}
///
@@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Syncplay
/// true if there is a session buffering in the group; false otherwise.
public bool IsBuffering()
{
- foreach (var session in Partecipants.Values)
+ foreach (var session in Participants.Values)
{
if (session.IsBuffering) return true;
}
@@ -142,7 +142,7 @@ namespace MediaBrowser.Controller.Syncplay
/// true if the group is empty; false otherwise.
public bool IsEmpty()
{
- return Partecipants.Count == 0;
+ return Participants.Count == 0;
}
}
}
diff --git a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs
index d0cf8fa9c8..433d6d8bc1 100644
--- a/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs
+++ b/MediaBrowser.Controller/Syncplay/ISyncplayManager.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.Syncplay
/// The session.
/// The group.
///
- void MapSessionToGroup(SessionInfo session, ISyncplayController group);
+ void AddSessionToGroup(SessionInfo session, ISyncplayController group);
///
/// Unmaps a session from a group.
@@ -58,6 +58,6 @@ namespace MediaBrowser.Controller.Syncplay
/// The session.
/// The group.
///
- void UnmapSessionFromGroup(SessionInfo session, ISyncplayController group);
+ void RemoveSessionFromGroup(SessionInfo session, ISyncplayController group);
}
}
diff --git a/MediaBrowser.Model/Syncplay/GroupInfoModel.cs b/MediaBrowser.Model/Syncplay/GroupInfoView.cs
similarity index 84%
rename from MediaBrowser.Model/Syncplay/GroupInfoModel.cs
rename to MediaBrowser.Model/Syncplay/GroupInfoView.cs
index 599c0dbfc9..50ad70630f 100644
--- a/MediaBrowser.Model/Syncplay/GroupInfoModel.cs
+++ b/MediaBrowser.Model/Syncplay/GroupInfoView.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Model.Syncplay
{
///
- /// Class GroupInfoModel.
+ /// Class GroupInfoView.
///
public class GroupInfoView
{
@@ -30,9 +30,9 @@ namespace MediaBrowser.Model.Syncplay
public long PositionTicks { get; set; }
///
- /// Gets or sets the partecipants.
+ /// Gets or sets the participants.
///
- /// The partecipants.
- public string[] Partecipants { get; set; }
+ /// The participants.
+ public string[] Participants { get; set; }
}
}
diff --git a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs b/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs
index 3d99b2718e..b3d49d09ef 100644
--- a/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs
+++ b/MediaBrowser.Model/Syncplay/PlaybackRequestType.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.Syncplay
///
/// A user is signaling that playback resumed.
///
- BufferingComplete = 4,
+ BufferingDone = 4,
///
/// A user is reporting its ping.
///