mirror of
https://github.com/imgfloat/server.git
synced 2026-06-22 21:01:23 +00:00
refactor: send playlist position heartbeat over STOMP instead of HTTP POST
- Add PlaylistWsController with @MessageMapping for position reports;
authentication via Principal, same authorizationService guard
- renderer.js: store stompClient on this so the interval can reach it;
replace fetch() position POST with stompClient.send() to /app/channel/...
- Remove POST /{playlistId}/position REST endpoint from PlaylistApiController
This commit is contained in:
@@ -232,19 +232,4 @@ public class PlaylistApiController {
|
||||
playlistService.commandTrackEnded(broadcaster, playlistId, body.get("trackId"));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{playlistId}/position")
|
||||
public ResponseEntity<Void> reportPosition(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("playlistId") String playlistId,
|
||||
@RequestBody java.util.Map<String, Object> body,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(broadcaster, sessionUsername);
|
||||
String trackId = (String) body.get("trackId");
|
||||
double position = body.get("position") instanceof Number n ? n.doubleValue() : 0.0;
|
||||
playlistService.reportPosition(broadcaster, playlistId, trackId, position);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.kruhlmann.imgfloat.controller;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||
import dev.kruhlmann.imgfloat.service.PlaylistService;
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class PlaylistWsController {
|
||||
|
||||
private final PlaylistService playlistService;
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
public PlaylistWsController(PlaylistService playlistService, AuthorizationService authorizationService) {
|
||||
this.playlistService = playlistService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic position heartbeat sent by the broadcast renderer over STOMP.
|
||||
* Payload: { "trackId": "...", "position": 42.5 }
|
||||
*/
|
||||
@MessageMapping("/channel/{broadcaster}/playlists/{playlistId}/position")
|
||||
public void reportPosition(
|
||||
@DestinationVariable String broadcaster,
|
||||
@DestinationVariable String playlistId,
|
||||
@Payload Map<String, Object> payload,
|
||||
Principal principal
|
||||
) {
|
||||
String sessionUsername = sessionUsername(principal);
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(broadcaster, sessionUsername);
|
||||
String trackId = (String) payload.get("trackId");
|
||||
double position = payload.get("position") instanceof Number n ? n.doubleValue() : 0.0;
|
||||
playlistService.reportPosition(broadcaster, playlistId, trackId, position);
|
||||
}
|
||||
|
||||
private String sessionUsername(Principal principal) {
|
||||
if (principal instanceof OAuth2AuthenticationToken token) {
|
||||
OauthSessionUser user = OauthSessionUser.from(token);
|
||||
return user == null ? null : user.login();
|
||||
}
|
||||
return principal == null ? null : principal.getName();
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,9 @@ export class BroadcastRenderer {
|
||||
|
||||
connect() {
|
||||
const socket = new SockJS("/ws");
|
||||
const stompClient = Stomp.over(socket);
|
||||
stompClient.connect({}, () => {
|
||||
stompClient.subscribe(`/topic/channel/${this.broadcaster}`, (payload) => {
|
||||
this.stompClient = Stomp.over(socket);
|
||||
this.stompClient.connect({}, () => {
|
||||
this.stompClient.subscribe(`/topic/channel/${this.broadcaster}`, (payload) => {
|
||||
const body = JSON.parse(payload.body);
|
||||
this.handleEvent(body);
|
||||
});
|
||||
@@ -119,25 +119,18 @@ export class BroadcastRenderer {
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Periodically persist playback position so reconnects can resume accurately
|
||||
// Periodically persist playback position over STOMP so reconnects can resume accurately
|
||||
this._positionReporterInterval = setInterval(() => {
|
||||
if (!this.playlistCurrentElement || this.playlistCurrentElement.paused) return;
|
||||
if (!this.playlistState.playlistId || !this.playlistState.trackId) return;
|
||||
const xsrf = this._xsrfToken();
|
||||
fetch(
|
||||
`/api/channels/${encodeURIComponent(this.broadcaster)}/playlists/${encodeURIComponent(this.playlistState.playlistId)}/position`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(xsrf ? { "X-XSRF-TOKEN": xsrf } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
trackId: this.playlistState.trackId,
|
||||
position: this.playlistCurrentElement.currentTime,
|
||||
}),
|
||||
}
|
||||
).catch(() => {});
|
||||
this.stompClient.send(
|
||||
`/app/channel/${encodeURIComponent(this.broadcaster)}/playlists/${encodeURIComponent(this.playlistState.playlistId)}/position`,
|
||||
{},
|
||||
JSON.stringify({
|
||||
trackId: this.playlistState.trackId,
|
||||
position: this.playlistCurrentElement.currentTime,
|
||||
})
|
||||
);
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user