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"));
|
playlistService.commandTrackEnded(broadcaster, playlistId, body.get("trackId"));
|
||||||
return ResponseEntity.ok().build();
|
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() {
|
connect() {
|
||||||
const socket = new SockJS("/ws");
|
const socket = new SockJS("/ws");
|
||||||
const stompClient = Stomp.over(socket);
|
this.stompClient = Stomp.over(socket);
|
||||||
stompClient.connect({}, () => {
|
this.stompClient.connect({}, () => {
|
||||||
stompClient.subscribe(`/topic/channel/${this.broadcaster}`, (payload) => {
|
this.stompClient.subscribe(`/topic/channel/${this.broadcaster}`, (payload) => {
|
||||||
const body = JSON.parse(payload.body);
|
const body = JSON.parse(payload.body);
|
||||||
this.handleEvent(body);
|
this.handleEvent(body);
|
||||||
});
|
});
|
||||||
@@ -119,25 +119,18 @@ export class BroadcastRenderer {
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
// Periodically persist playback position so reconnects can resume accurately
|
// Periodically persist playback position over STOMP so reconnects can resume accurately
|
||||||
this._positionReporterInterval = setInterval(() => {
|
this._positionReporterInterval = setInterval(() => {
|
||||||
if (!this.playlistCurrentElement || this.playlistCurrentElement.paused) return;
|
if (!this.playlistCurrentElement || this.playlistCurrentElement.paused) return;
|
||||||
if (!this.playlistState.playlistId || !this.playlistState.trackId) return;
|
if (!this.playlistState.playlistId || !this.playlistState.trackId) return;
|
||||||
const xsrf = this._xsrfToken();
|
this.stompClient.send(
|
||||||
fetch(
|
`/app/channel/${encodeURIComponent(this.broadcaster)}/playlists/${encodeURIComponent(this.playlistState.playlistId)}/position`,
|
||||||
`/api/channels/${encodeURIComponent(this.broadcaster)}/playlists/${encodeURIComponent(this.playlistState.playlistId)}/position`,
|
{},
|
||||||
{
|
JSON.stringify({
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(xsrf ? { "X-XSRF-TOKEN": xsrf } : {}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
trackId: this.playlistState.trackId,
|
trackId: this.playlistState.trackId,
|
||||||
position: this.playlistCurrentElement.currentTime,
|
position: this.playlistCurrentElement.currentTime,
|
||||||
}),
|
})
|
||||||
}
|
);
|
||||||
).catch(() => {});
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user