SyncplayController.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using MediaBrowser.Controller.Session;
  7. using MediaBrowser.Controller.Syncplay;
  8. using MediaBrowser.Model.Session;
  9. using MediaBrowser.Model.Syncplay;
  10. using Microsoft.Extensions.Logging;
  11. namespace Emby.Server.Implementations.Syncplay
  12. {
  13. /// <summary>
  14. /// Class SyncplayController.
  15. /// </summary>
  16. public class SyncplayController : ISyncplayController, IDisposable
  17. {
  18. private enum BroadcastType
  19. {
  20. AllGroup = 0,
  21. SingleSession = 1,
  22. AllExceptSession = 2,
  23. AllReady = 3
  24. }
  25. /// <summary>
  26. /// The logger.
  27. /// </summary>
  28. private readonly ILogger _logger;
  29. /// <summary>
  30. /// The session manager.
  31. /// </summary>
  32. private readonly ISessionManager _sessionManager;
  33. /// <summary>
  34. /// The syncplay manager.
  35. /// </summary>
  36. private readonly ISyncplayManager _syncplayManager;
  37. /// <summary>
  38. /// The group to manage.
  39. /// </summary>
  40. private readonly GroupInfo _group = new GroupInfo();
  41. /// <inheritdoc />
  42. public Guid GetGroupId() => _group.GroupId;
  43. /// <inheritdoc />
  44. public Guid GetPlayingItemId() => _group.PlayingItem.Id;
  45. /// <inheritdoc />
  46. public bool IsGroupEmpty() => _group.IsEmpty();
  47. private bool _disposed = false;
  48. public SyncplayController(
  49. ILogger logger,
  50. ISessionManager sessionManager,
  51. ISyncplayManager syncplayManager)
  52. {
  53. _logger = logger;
  54. _sessionManager = sessionManager;
  55. _syncplayManager = syncplayManager;
  56. }
  57. /// <inheritdoc />
  58. public void Dispose()
  59. {
  60. Dispose(true);
  61. GC.SuppressFinalize(this);
  62. }
  63. /// <summary>
  64. /// Releases unmanaged and optionally managed resources.
  65. /// </summary>
  66. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  67. protected virtual void Dispose(bool disposing)
  68. {
  69. if (_disposed)
  70. {
  71. return;
  72. }
  73. _disposed = true;
  74. }
  75. // TODO: use this somewhere
  76. private void CheckDisposed()
  77. {
  78. if (_disposed)
  79. {
  80. throw new ObjectDisposedException(GetType().Name);
  81. }
  82. }
  83. private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
  84. {
  85. if (type == BroadcastType.SingleSession)
  86. {
  87. return new SessionInfo[] { from };
  88. }
  89. else if (type == BroadcastType.AllGroup)
  90. {
  91. return _group.Partecipants.Values.Select(
  92. session => session.Session
  93. ).ToArray();
  94. }
  95. else if (type == BroadcastType.AllExceptSession)
  96. {
  97. return _group.Partecipants.Values.Select(
  98. session => session.Session
  99. ).Where(
  100. session => !session.Id.Equals(from.Id)
  101. ).ToArray();
  102. }
  103. else if (type == BroadcastType.AllReady)
  104. {
  105. return _group.Partecipants.Values.Where(
  106. session => !session.IsBuffering
  107. ).Select(
  108. session => session.Session
  109. ).ToArray();
  110. }
  111. else
  112. {
  113. return new SessionInfo[] {};
  114. }
  115. }
  116. private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message)
  117. {
  118. IEnumerable<Task> GetTasks()
  119. {
  120. SessionInfo[] sessions = FilterSessions(from, type);
  121. foreach (var session in sessions)
  122. {
  123. yield return _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), message, CancellationToken.None);
  124. }
  125. }
  126. return Task.WhenAll(GetTasks());
  127. }
  128. private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message)
  129. {
  130. IEnumerable<Task> GetTasks()
  131. {
  132. SessionInfo[] sessions = FilterSessions(from, type);
  133. foreach (var session in sessions)
  134. {
  135. yield return _sessionManager.SendSyncplayCommand(session.Id.ToString(), message, CancellationToken.None);
  136. }
  137. }
  138. return Task.WhenAll(GetTasks());
  139. }
  140. private SendCommand NewSyncplayCommand(SendCommandType type)
  141. {
  142. var command = new SendCommand();
  143. command.GroupId = _group.GroupId.ToString();
  144. command.Command = type;
  145. command.PositionTicks = _group.PositionTicks;
  146. command.When = _group.LastActivity.ToUniversalTime().ToString("o");
  147. command.EmittedAt = DateTime.UtcNow.ToUniversalTime().ToString("o");
  148. return command;
  149. }
  150. private GroupUpdate<T> NewSyncplayGroupUpdate<T>(GroupUpdateType type, T data)
  151. {
  152. var command = new GroupUpdate<T>();
  153. command.GroupId = _group.GroupId.ToString();
  154. command.Type = type;
  155. command.Data = data;
  156. return command;
  157. }
  158. /// <inheritdoc />
  159. public void InitGroup(SessionInfo session)
  160. {
  161. _group.AddSession(session);
  162. _syncplayManager.MapSessionToGroup(session, this);
  163. _group.PlayingItem = session.FullNowPlayingItem;
  164. _group.IsPaused = true;
  165. _group.PositionTicks = session.PlayState.PositionTicks ??= 0;
  166. _group.LastActivity = DateTime.UtcNow;
  167. var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
  168. SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
  169. var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
  170. SendCommand(session, BroadcastType.SingleSession, pauseCommand);
  171. }
  172. /// <inheritdoc />
  173. public void SessionJoin(SessionInfo session, JoinGroupRequest request)
  174. {
  175. if (session.NowPlayingItem != null &&
  176. session.NowPlayingItem.Id.Equals(_group.PlayingItem.Id) &&
  177. request.PlayingItemId.Equals(_group.PlayingItem.Id))
  178. {
  179. _group.AddSession(session);
  180. _syncplayManager.MapSessionToGroup(session, this);
  181. var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
  182. SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
  183. var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
  184. SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
  185. // Client join and play, syncing will happen client side
  186. if (!_group.IsPaused)
  187. {
  188. var playCommand = NewSyncplayCommand(SendCommandType.Play);
  189. SendCommand(session, BroadcastType.SingleSession, playCommand);
  190. }
  191. else
  192. {
  193. var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
  194. SendCommand(session, BroadcastType.SingleSession, pauseCommand);
  195. }
  196. }
  197. else
  198. {
  199. var playRequest = new PlayRequest();
  200. playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
  201. playRequest.StartPositionTicks = _group.PositionTicks;
  202. var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
  203. SendGroupUpdate(session, BroadcastType.SingleSession, update);
  204. }
  205. }
  206. /// <inheritdoc />
  207. public void SessionLeave(SessionInfo session)
  208. {
  209. _group.RemoveSession(session);
  210. _syncplayManager.UnmapSessionFromGroup(session, this);
  211. var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
  212. SendGroupUpdate(session, BroadcastType.SingleSession, updateSession);
  213. var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
  214. SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
  215. }
  216. /// <inheritdoc />
  217. public void HandleRequest(SessionInfo session, PlaybackRequest request)
  218. {
  219. if (request.Type.Equals(PlaybackRequestType.Play))
  220. {
  221. if (_group.IsPaused)
  222. {
  223. var delay = _group.GetHighestPing() * 2;
  224. delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
  225. _group.IsPaused = false;
  226. _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
  227. delay
  228. );
  229. var command = NewSyncplayCommand(SendCommandType.Play);
  230. SendCommand(session, BroadcastType.AllGroup, command);
  231. }
  232. else
  233. {
  234. // Client got lost
  235. var command = NewSyncplayCommand(SendCommandType.Play);
  236. SendCommand(session, BroadcastType.SingleSession, command);
  237. }
  238. }
  239. else if (request.Type.Equals(PlaybackRequestType.Pause))
  240. {
  241. if (!_group.IsPaused)
  242. {
  243. _group.IsPaused = true;
  244. var currentTime = DateTime.UtcNow;
  245. var elapsedTime = currentTime - _group.LastActivity;
  246. _group.LastActivity = currentTime;
  247. _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
  248. var command = NewSyncplayCommand(SendCommandType.Pause);
  249. SendCommand(session, BroadcastType.AllGroup, command);
  250. }
  251. else
  252. {
  253. var command = NewSyncplayCommand(SendCommandType.Pause);
  254. SendCommand(session, BroadcastType.SingleSession, command);
  255. }
  256. }
  257. else if (request.Type.Equals(PlaybackRequestType.Seek))
  258. {
  259. // Sanitize PositionTicks
  260. var ticks = request.PositionTicks ??= 0;
  261. ticks = ticks >= 0 ? ticks : 0;
  262. if (_group.PlayingItem.RunTimeTicks != null)
  263. {
  264. var runTimeTicks = _group.PlayingItem.RunTimeTicks ??= 0;
  265. ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
  266. }
  267. _group.IsPaused = true;
  268. _group.PositionTicks = ticks;
  269. _group.LastActivity = DateTime.UtcNow;
  270. var command = NewSyncplayCommand(SendCommandType.Seek);
  271. SendCommand(session, BroadcastType.AllGroup, command);
  272. }
  273. // TODO: client does not implement this yet
  274. else if (request.Type.Equals(PlaybackRequestType.Buffering))
  275. {
  276. if (!_group.IsPaused)
  277. {
  278. _group.IsPaused = true;
  279. var currentTime = DateTime.UtcNow;
  280. var elapsedTime = currentTime - _group.LastActivity;
  281. _group.LastActivity = currentTime;
  282. _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
  283. _group.SetBuffering(session, true);
  284. // Send pause command to all non-buffering sessions
  285. var command = NewSyncplayCommand(SendCommandType.Pause);
  286. SendCommand(session, BroadcastType.AllReady, command);
  287. var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
  288. SendGroupUpdate(session, BroadcastType.AllExceptSession, updateOthers);
  289. }
  290. else
  291. {
  292. var command = NewSyncplayCommand(SendCommandType.Pause);
  293. SendCommand(session, BroadcastType.SingleSession, command);
  294. }
  295. }
  296. // TODO: client does not implement this yet
  297. else if (request.Type.Equals(PlaybackRequestType.BufferingComplete))
  298. {
  299. if (_group.IsPaused)
  300. {
  301. _group.SetBuffering(session, false);
  302. if (_group.IsBuffering()) {
  303. // Others are buffering, tell this client to pause when ready
  304. var when = request.When ??= DateTime.UtcNow;
  305. var currentTime = DateTime.UtcNow;
  306. var elapsedTime = currentTime - when;
  307. var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
  308. var delay = _group.PositionTicks - clientPosition.Ticks;
  309. var command = NewSyncplayCommand(SendCommandType.Pause);
  310. command.When = currentTime.AddMilliseconds(
  311. delay
  312. ).ToUniversalTime().ToString("o");
  313. SendCommand(session, BroadcastType.SingleSession, command);
  314. }
  315. else
  316. {
  317. // Let other clients resume as soon as the buffering client catches up
  318. var when = request.When ??= DateTime.UtcNow;
  319. var currentTime = DateTime.UtcNow;
  320. var elapsedTime = currentTime - when;
  321. var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
  322. var delay = _group.PositionTicks - clientPosition.Ticks;
  323. _group.IsPaused = false;
  324. if (delay > _group.GetHighestPing() * 2)
  325. {
  326. // Client that was buffering is recovering, notifying others to resume
  327. _group.LastActivity = currentTime.AddMilliseconds(
  328. delay
  329. );
  330. var command = NewSyncplayCommand(SendCommandType.Play);
  331. SendCommand(session, BroadcastType.AllExceptSession, command);
  332. }
  333. else
  334. {
  335. // Client, that was buffering, resumed playback but did not update others in time
  336. delay = _group.GetHighestPing() * 2;
  337. delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
  338. _group.LastActivity = currentTime.AddMilliseconds(
  339. delay
  340. );
  341. var command = NewSyncplayCommand(SendCommandType.Play);
  342. SendCommand(session, BroadcastType.AllGroup, command);
  343. }
  344. }
  345. }
  346. else
  347. {
  348. // Make sure client has latest group state
  349. var command = NewSyncplayCommand(SendCommandType.Play);
  350. SendCommand(session, BroadcastType.SingleSession, command);
  351. }
  352. }
  353. else if (request.Type.Equals(PlaybackRequestType.UpdatePing))
  354. {
  355. _group.UpdatePing(session, request.Ping ??= _group.DefaulPing);
  356. }
  357. }
  358. /// <inheritdoc />
  359. public GroupInfoView GetInfo()
  360. {
  361. var info = new GroupInfoView();
  362. info.GroupId = GetGroupId().ToString();
  363. info.PlayingItemName = _group.PlayingItem.Name;
  364. info.PlayingItemId = _group.PlayingItem.Id.ToString();
  365. info.PositionTicks = _group.PositionTicks;
  366. info.Partecipants = _group.Partecipants.Values.Select(session => session.Session.UserName).ToArray();
  367. return info;
  368. }
  369. }
  370. }