/// <inheritdoc /> public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); if (user.SyncPlayAccess == SyncPlayAccess.None) { _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id); var error = new GroupUpdate <string>() { Type = GroupUpdateType.JoinGroupDenied }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } lock (_groupsLock) { _sessionToGroupMap.TryGetValue(session.Id, out var group); if (group == null) { _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id); var error = new GroupUpdate <string>() { Type = GroupUpdateType.NotInGroup }; _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } group.HandleRequest(session, request, cancellationToken); } }
/// <summary> /// Handles a play action requested by a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The play action.</param> /// <param name="cancellationToken">The cancellation token.</param> private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { if (_group.IsPaused) { // Pick a suitable time that accounts for latency var delay = _group.GetHighestPing() * 2; delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; // 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 ); var command = NewSyncPlayCommand(SendCommandType.Play); SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); } else { // Client got lost, sending current state var command = NewSyncPlayCommand(SendCommandType.Play); SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); } }
/// <summary> /// Handles a buffering action requested by a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The buffering action.</param> /// <param name="cancellationToken">The cancellation token.</param> private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { 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, cancellationToken); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); } else { // Client got lost, sending current state var command = NewSyncPlayCommand(SendCommandType.Pause); SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); } }
/// <inheritdoc /> public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { // 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) { case PlaybackRequestType.Play: HandlePlayRequest(session, request, cancellationToken); break; case PlaybackRequestType.Pause: HandlePauseRequest(session, request, cancellationToken); break; case PlaybackRequestType.Seek: HandleSeekRequest(session, request, cancellationToken); break; case PlaybackRequestType.Buffering: HandleBufferingRequest(session, request, cancellationToken); break; case PlaybackRequestType.BufferingDone: HandleBufferingDoneRequest(session, request, cancellationToken); break; case PlaybackRequestType.UpdatePing: HandlePingUpdateRequest(session, request); break; } }
public async Task ExecuteAsync(QueryResult result) { if (!(result is IPlayableResource playable)) { return; } var req = new PlaybackRequest(); if (string.IsNullOrEmpty(playable.ContextUri)) { req.Uris = playable.ResourceUris; } else { req.ContextUri = playable.ContextUri; if (playable.ResourceUris == null) { req.Offset = new PositionOffset(1); } else { req.Offset = new UriOffset(playable.ResourceUris.First()); } } await _spotifyClient.PlayAsync(req); }
/// <summary> /// Handles a buffering-done action requested by a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The buffering-done action.</param> /// <param name="cancellationToken">The cancellation token.</param> private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { if (_group.IsPaused) { _group.SetBuffering(session, false); var requestTicks = SanitizePositionTicks(request.PositionTicks); var when = request.When ?? DateTime.UtcNow; var currentTime = DateTime.UtcNow; var elapsedTime = currentTime - when; var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; var delay = _group.PositionTicks - clientPosition.Ticks; if (_group.IsBuffering()) { // Others are still buffering, tell this client to pause when ready var command = NewSyncPlayCommand(SendCommandType.Pause); var pauseAtTime = currentTime.AddMilliseconds(delay); command.When = DateToUTCString(pauseAtTime); SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); } else { // Let other clients resume as soon as the buffering client catches up _group.IsPaused = false; 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.AllExceptCurrentSession, command, cancellationToken); } 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, cancellationToken); } } } else { // Group was not waiting, make sure client has latest state var command = NewSyncPlayCommand(SendCommandType.Play); SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); } }
/// <summary> /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> public void Post(SyncPlayPause request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.Pause }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); }
public ActionResult SyncPlayPause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.Pause }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return(NoContent()); }
/// <summary> /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> public void Post(SyncPlayPing request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.UpdatePing, Ping = Convert.ToInt64(request.Ping) }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); }
/// <summary> /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> public void Post(SyncPlaySeek request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.Seek, PositionTicks = request.PositionTicks }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); }
public ActionResult SyncPlayPing([FromQuery] double ping) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.Ping, Ping = Convert.ToInt64(ping) }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return(NoContent()); }
public ActionResult SyncPlaySeek([FromQuery] long positionTicks) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() { Type = PlaybackRequestType.Seek, PositionTicks = positionTicks }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return(NoContent()); }
/// <summary> /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> public void Post(SyncPlayBuffering request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, When = DateTime.Parse(request.When), PositionTicks = request.PositionTicks }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); }
public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() { Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer, When = when, PositionTicks = positionTicks }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return(NoContent()); }
/// <summary> /// Handles a seek action requested by a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The seek action.</param> /// <param name="cancellationToken">The cancellation token.</param> private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { // Sanitize PositionTicks var ticks = SanitizePositionTicks(request.PositionTicks); // Pause and seek _group.IsPaused = true; _group.PositionTicks = ticks; _group.LastActivity = DateTime.UtcNow; var command = NewSyncPlayCommand(SendCommandType.Seek); SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); }
/// <summary> /// Handles a pause action requested by a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The pause action.</param> /// <param name="cancellationToken">The cancellation token.</param> private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { 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, cancellationToken); } else { // Client got lost, sending current state var command = NewSyncPlayCommand(SendCommandType.Pause); SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); } }
public async Task <OperationResult> PlayAsync(PlaybackRequest request, CancellationToken ct = default) { var requestUrl = $"{ApiBaseUrl}/me/player/play{ _queryParameterBuilder.Build(request)}"; var response = await _httpClient.PutAsync(requestUrl, new JsonContent(request, _serializer), ct); switch (response.StatusCode) { case HttpStatusCode.NoContent: return(OperationResult.Success); case HttpStatusCode.Accepted: return(OperationResult.DeviceUnavailable); case HttpStatusCode.NotFound: return(OperationResult.DeviceNotFound); case HttpStatusCode.Forbidden: return(OperationResult.NonPremiumUser); case HttpStatusCode.BadRequest: var error = await DeserializeBodyAsync <UnsuccessfulOperation>(response); throw new SpotifyException("Unable to authenticate", error.Error); default: return(OperationResult.Unknown); } }
/// <summary> /// Updates ping of a session. /// </summary> /// <param name="session">The session.</param> /// <param name="request">The update.</param> 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 string Build(PlaybackRequest request) { return(string.IsNullOrEmpty(request?.DeviceId) ? string.Empty : $"?device_id={request?.DeviceId}"); }
/// <summary> /// Get Playback Start Request /// </summary> /// <param name="playbackStartType">Playlist Start Type</param> /// <param name="id">Spotify Id</param> /// <param name="position">(Optional) Indicates from what position to start playback. Must be a positive number. Passing in a position that is greater than the length of the track will cause the player to start playing the next song.</param> /// <param name="offsetId">(Optional) Only available for PlaybackStartType.Album, PlaybackStartType.Artist and PlaybackStartType.Playlist. Only use either Position or OffsetId. Offset Id indicates from where in the context playback should start.</param> /// <returns>Playback Request</returns> /// <exception cref="ArgumentException">Thrown when Position and OffsetId are Provided</exception> public static PlaybackRequest GetPlaybackStartRequest( PlaybackStartType playbackStartType, string id, int?position = null, string offsetId = null) { PlaybackRequest playbackRequest = new PlaybackRequest(); switch (playbackStartType) { case PlaybackStartType.Track: playbackRequest.Position = position; playbackRequest.Uris = new List <string> { GetTrackUri(id) }; break; case PlaybackStartType.Episode: playbackRequest.Position = position; playbackRequest.Uris = new List <string> { GetEpisodeUri(id) }; break; case PlaybackStartType.Album: if (offsetId != null && position != null) { throw new ArgumentException($"Set {nameof(offsetId)} or {nameof(position)}"); } if (position != null && offsetId == null) { playbackRequest.Offset = new PositionRequest() { Position = position } } ; if (offsetId != null && position == null) { playbackRequest.Offset = new UriRequest { Uri = GetTrackUri(offsetId) } } ; playbackRequest.ContextUri = GetAlbumUri(id); break; case PlaybackStartType.Artist: if (offsetId != null && position != null) { throw new ArgumentException($"Set {nameof(offsetId)} or {nameof(position)}"); } if (position != null && offsetId == null) { playbackRequest.Offset = new PositionRequest() { Position = position } } ; if (offsetId != null && position == null) { playbackRequest.Offset = new UriRequest { Uri = GetTrackUri(offsetId) } } ; playbackRequest.ContextUri = GetArtistUri(id); break; case PlaybackStartType.Playlist: if (offsetId != null && position != null) { throw new ArgumentException($"Set {nameof(offsetId)} or {nameof(position)}"); } if (position != null && offsetId == null) { playbackRequest.Offset = new PositionRequest() { Position = position } } ; if (offsetId != null && position == null) { playbackRequest.Offset = new UriRequest { Uri = GetTrackUri(offsetId) } } ; playbackRequest.ContextUri = GetPlaylistUri(id); break; case PlaybackStartType.Show: if (offsetId != null && position != null) { throw new ArgumentException($"Set {nameof(offsetId)} or {nameof(position)}"); } if (position != null && offsetId == null) { playbackRequest.Offset = new PositionRequest() { Position = position } } ; if (offsetId != null && position == null) { playbackRequest.Offset = new UriRequest { Uri = GetEpisodeUri(offsetId) } } ; playbackRequest.ContextUri = GetShowUri(id); break; } return(playbackRequest); }