mirror of
https://github.com/imgfloat/server.git
synced 2026-06-22 21:01:23 +00:00
feat: persist and restore playlist playback state across refreshes
- V19 migration: add playlist_current_track_id, playlist_is_playing,
playlist_is_paused, playlist_track_position to channels table
- Channel.java: 4 new fields + getters/setters
- ActivePlaylistState.java: new response DTO with full playback state
- PlaylistService: all command methods now @Transactional and persist state;
new reportPosition() method; getActivePlaylistState() returns the enriched DTO
- PlaylistApiController: GET /active returns ActivePlaylistState;
new POST /{playlistId}/position endpoint for position heartbeats
- renderer.js: on reconnect reads isPlaying/isPaused/currentTrackId/position
and calls _resumeTrack() to seek and optionally pause; 5-second interval
reports currentTime to /position while playing; _xsrfToken() helper
extracted and reused across all playlist POSTs
- playlist.js: loadActivePlaylist seeds playbackState from ActivePlaylistState
so the admin panel shows the correct playing/paused indicator on load
This commit is contained in:
@@ -5,6 +5,7 @@ import dev.kruhlmann.imgfloat.model.api.request.ActivePlaylistRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.PlaylistRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.PlaylistTrackOrderRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.request.PlaylistTrackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.ActivePlaylistState;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.PlaylistView;
|
||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||
import dev.kruhlmann.imgfloat.service.PlaylistService;
|
||||
@@ -139,13 +140,13 @@ public class PlaylistApiController {
|
||||
// ── Active playlist ───────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/active")
|
||||
public ResponseEntity<PlaylistView> getActive(
|
||||
public ResponseEntity<ActivePlaylistState> getActive(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(broadcaster, sessionUsername);
|
||||
return playlistService.getActivePlaylist(broadcaster)
|
||||
return playlistService.getActivePlaylistState(broadcaster)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
@@ -231,4 +232,19 @@ 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,20 @@
|
||||
package dev.kruhlmann.imgfloat.model.api.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Returned by GET /api/channels/{broadcaster}/playlists/active.
|
||||
* Extends the basic PlaylistView with persisted playback state so that
|
||||
* reconnecting clients (broadcast view, admin view) can resume correctly.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ActivePlaylistState(
|
||||
String id,
|
||||
String name,
|
||||
List<PlaylistTrackView> tracks,
|
||||
String currentTrackId,
|
||||
boolean isPlaying,
|
||||
boolean isPaused,
|
||||
double trackPosition
|
||||
) {}
|
||||
@@ -50,6 +50,18 @@ public class Channel {
|
||||
@Column(name = "active_playlist_id")
|
||||
private String activePlaylistId;
|
||||
|
||||
@Column(name = "playlist_current_track_id")
|
||||
private String playlistCurrentTrackId;
|
||||
|
||||
@Column(name = "playlist_is_playing", nullable = false)
|
||||
private boolean playlistIsPlaying = false;
|
||||
|
||||
@Column(name = "playlist_is_paused", nullable = false)
|
||||
private boolean playlistIsPaused = false;
|
||||
|
||||
@Column(name = "playlist_track_position", nullable = false)
|
||||
private double playlistTrackPosition = 0.0;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -142,6 +154,38 @@ public class Channel {
|
||||
this.activePlaylistId = activePlaylistId;
|
||||
}
|
||||
|
||||
public String getPlaylistCurrentTrackId() {
|
||||
return playlistCurrentTrackId;
|
||||
}
|
||||
|
||||
public void setPlaylistCurrentTrackId(String playlistCurrentTrackId) {
|
||||
this.playlistCurrentTrackId = playlistCurrentTrackId;
|
||||
}
|
||||
|
||||
public boolean isPlaylistIsPlaying() {
|
||||
return playlistIsPlaying;
|
||||
}
|
||||
|
||||
public void setPlaylistIsPlaying(boolean playlistIsPlaying) {
|
||||
this.playlistIsPlaying = playlistIsPlaying;
|
||||
}
|
||||
|
||||
public boolean isPlaylistIsPaused() {
|
||||
return playlistIsPaused;
|
||||
}
|
||||
|
||||
public void setPlaylistIsPaused(boolean playlistIsPaused) {
|
||||
this.playlistIsPaused = playlistIsPaused;
|
||||
}
|
||||
|
||||
public double getPlaylistTrackPosition() {
|
||||
return playlistTrackPosition;
|
||||
}
|
||||
|
||||
public void setPlaylistTrackPosition(double playlistTrackPosition) {
|
||||
this.playlistTrackPosition = playlistTrackPosition;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package dev.kruhlmann.imgfloat.service;
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.api.response.ActivePlaylistState;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.PlaylistEvent;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.PlaylistTrackView;
|
||||
import dev.kruhlmann.imgfloat.model.api.response.PlaylistView;
|
||||
@@ -14,7 +15,6 @@ import dev.kruhlmann.imgfloat.repository.AudioAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.PlaylistRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.PlaylistTrackRepository;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -83,12 +83,17 @@ public class PlaylistService {
|
||||
@Transactional
|
||||
public void deletePlaylist(String broadcaster, String playlistId) {
|
||||
Playlist playlist = requirePlaylist(broadcaster, playlistId);
|
||||
// Clear active_playlist_id on channel if it pointed to this playlist
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
if (playlistId.equals(channel.getActivePlaylistId())) {
|
||||
channel.setActivePlaylistId(null);
|
||||
channelRepository.save(channel);
|
||||
}
|
||||
// Clear playback state if this playlist was playing
|
||||
if (playlistId.equals(channel.getActivePlaylistId())
|
||||
|| (channel.getPlaylistCurrentTrackId() != null
|
||||
&& channel.isPlaylistIsPlaying())) {
|
||||
clearPlaybackState(channel);
|
||||
}
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
playlistTrackRepository.deleteAllByPlaylistId(playlistId);
|
||||
playlistRepository.delete(playlist);
|
||||
@@ -100,7 +105,7 @@ public class PlaylistService {
|
||||
@Transactional
|
||||
public PlaylistView addTrack(String broadcaster, String playlistId, String audioAssetId) {
|
||||
requirePlaylist(broadcaster, playlistId);
|
||||
AudioAsset audio = audioAssetRepository.findById(audioAssetId)
|
||||
audioAssetRepository.findById(audioAssetId)
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Audio asset not found"));
|
||||
int nextOrder = playlistTrackRepository.countByPlaylistId(playlistId);
|
||||
PlaylistTrack track = new PlaylistTrack(playlistId, audioAssetId, nextOrder);
|
||||
@@ -121,6 +126,13 @@ public class PlaylistService {
|
||||
remaining.get(i).setTrackOrder(i);
|
||||
}
|
||||
playlistTrackRepository.saveAll(remaining);
|
||||
// Clear playback state if the removed track was the current one
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
if (trackId.equals(channel.getPlaylistCurrentTrackId())) {
|
||||
clearPlaybackState(channel);
|
||||
channelRepository.save(channel);
|
||||
}
|
||||
});
|
||||
return refreshAndPublish(broadcaster, playlistId);
|
||||
}
|
||||
|
||||
@@ -142,13 +154,24 @@ public class PlaylistService {
|
||||
// ── Active playlist ───────────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<PlaylistView> getActivePlaylist(String broadcaster) {
|
||||
public Optional<ActivePlaylistState> getActivePlaylistState(String broadcaster) {
|
||||
Channel channel = channelRepository.findById(normalize(broadcaster)).orElse(null);
|
||||
if (channel == null || channel.getActivePlaylistId() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return playlistRepository.findByIdAndBroadcaster(channel.getActivePlaylistId(), normalize(broadcaster))
|
||||
.map(p -> toView(p, loadTracks(p.getId())));
|
||||
.map(p -> {
|
||||
List<PlaylistTrackView> tracks = loadTracks(p.getId());
|
||||
return new ActivePlaylistState(
|
||||
p.getId(),
|
||||
p.getName(),
|
||||
tracks,
|
||||
channel.getPlaylistCurrentTrackId(),
|
||||
channel.isPlaylistIsPlaying(),
|
||||
channel.isPlaylistIsPaused(),
|
||||
channel.getPlaylistTrackPosition()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -162,6 +185,7 @@ public class PlaylistService {
|
||||
view = toView(playlist, loadTracks(playlistId));
|
||||
}
|
||||
channel.setActivePlaylistId(playlistId);
|
||||
clearPlaybackState(channel);
|
||||
channelRepository.save(channel);
|
||||
publish(broadcaster, PlaylistEvent.selected(normalize(broadcaster), view));
|
||||
return Optional.ofNullable(view);
|
||||
@@ -169,19 +193,36 @@ public class PlaylistService {
|
||||
|
||||
// ── Playback commands ─────────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public void commandPlay(String broadcaster, String playlistId, String trackId) {
|
||||
requirePlaylist(broadcaster, playlistId);
|
||||
publish(broadcaster, PlaylistEvent.play(normalize(broadcaster), playlistId, trackId));
|
||||
// If no trackId specified, resolve the first track
|
||||
String resolvedTrackId = trackId;
|
||||
if (resolvedTrackId == null) {
|
||||
List<PlaylistTrack> tracks = playlistTrackRepository.findAllByPlaylistIdOrderByTrackOrderAsc(playlistId);
|
||||
if (!tracks.isEmpty()) resolvedTrackId = tracks.get(0).getId();
|
||||
}
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
channel.setPlaylistCurrentTrackId(resolvedTrackId);
|
||||
channel.setPlaylistIsPlaying(true);
|
||||
channel.setPlaylistIsPaused(false);
|
||||
channel.setPlaylistTrackPosition(0.0);
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
publish(broadcaster, PlaylistEvent.play(normalize(broadcaster), playlistId, resolvedTrackId));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public void commandPause(String broadcaster, String playlistId) {
|
||||
requirePlaylist(broadcaster, playlistId);
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
channel.setPlaylistIsPaused(true);
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
publish(broadcaster, PlaylistEvent.pause(normalize(broadcaster), playlistId));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public void commandNext(String broadcaster, String playlistId, String currentTrackId) {
|
||||
requirePlaylist(broadcaster, playlistId);
|
||||
List<PlaylistTrack> tracks = playlistTrackRepository.findAllByPlaylistIdOrderByTrackOrderAsc(playlistId);
|
||||
@@ -194,14 +235,25 @@ public class PlaylistService {
|
||||
}
|
||||
}
|
||||
if (nextTrackId != null) {
|
||||
final String resolvedNext = nextTrackId;
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
channel.setPlaylistCurrentTrackId(resolvedNext);
|
||||
channel.setPlaylistIsPlaying(true);
|
||||
channel.setPlaylistIsPaused(false);
|
||||
channel.setPlaylistTrackPosition(0.0);
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
publish(broadcaster, PlaylistEvent.next(normalize(broadcaster), playlistId, nextTrackId));
|
||||
} else {
|
||||
// End of playlist
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
clearPlaybackState(channel);
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
publish(broadcaster, PlaylistEvent.ended(normalize(broadcaster), playlistId));
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional
|
||||
public void commandPrev(String broadcaster, String playlistId, String currentTrackId) {
|
||||
requirePlaylist(broadcaster, playlistId);
|
||||
List<PlaylistTrack> tracks = playlistTrackRepository.findAllByPlaylistIdOrderByTrackOrderAsc(playlistId);
|
||||
@@ -213,16 +265,48 @@ public class PlaylistService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If null, there is no previous track — the client restarts the current track
|
||||
// null means restart current — persist the same track, reset position
|
||||
final String resolvedTrackId = prevTrackId != null ? prevTrackId : currentTrackId;
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
channel.setPlaylistCurrentTrackId(resolvedTrackId);
|
||||
channel.setPlaylistIsPlaying(true);
|
||||
channel.setPlaylistIsPaused(false);
|
||||
channel.setPlaylistTrackPosition(0.0);
|
||||
channelRepository.save(channel);
|
||||
});
|
||||
publish(broadcaster, PlaylistEvent.prev(normalize(broadcaster), playlistId, prevTrackId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void commandTrackEnded(String broadcaster, String playlistId, String finishedTrackId) {
|
||||
commandNext(broadcaster, playlistId, finishedTrackId);
|
||||
}
|
||||
|
||||
// ── Position reporting ────────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void reportPosition(String broadcaster, String playlistId, String trackId, double position) {
|
||||
channelRepository.findById(normalize(broadcaster)).ifPresent(channel -> {
|
||||
// Only persist if this is still the active playlist and we're playing
|
||||
if (playlistId.equals(channel.getActivePlaylistId())
|
||||
&& channel.isPlaylistIsPlaying()
|
||||
&& !channel.isPlaylistIsPaused()
|
||||
&& trackId.equals(channel.getPlaylistCurrentTrackId())) {
|
||||
channel.setPlaylistTrackPosition(position);
|
||||
channelRepository.save(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private void clearPlaybackState(Channel channel) {
|
||||
channel.setPlaylistCurrentTrackId(null);
|
||||
channel.setPlaylistIsPlaying(false);
|
||||
channel.setPlaylistIsPaused(false);
|
||||
channel.setPlaylistTrackPosition(0.0);
|
||||
}
|
||||
|
||||
private Playlist requirePlaylist(String broadcaster, String playlistId) {
|
||||
return playlistRepository.findByIdAndBroadcaster(playlistId, normalize(broadcaster))
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Playlist not found"));
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE channels ADD COLUMN playlist_current_track_id TEXT;
|
||||
ALTER TABLE channels ADD COLUMN playlist_is_playing INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE channels ADD COLUMN playlist_is_paused INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE channels ADD COLUMN playlist_track_position REAL NOT NULL DEFAULT 0;
|
||||
@@ -104,16 +104,41 @@ export class BroadcastRenderer {
|
||||
.catch(() => this.showToast("Unable to load overlay assets. Retrying may help.", "error"));
|
||||
fetch(`/api/channels/${this.broadcaster}/playlists/active`)
|
||||
.then((r) => r.ok ? r.json() : null)
|
||||
.then((playlist) => {
|
||||
if (playlist) {
|
||||
this.playlistState.playlistId = playlist.id;
|
||||
this.playlistState.playlistName = playlist.name;
|
||||
this.playlistState.tracks = Array.isArray(playlist.tracks) ? playlist.tracks : [];
|
||||
this.playlistState.trackCount = this.playlistState.tracks.length;
|
||||
this.updateScriptWorkerPlaylist();
|
||||
.then((state) => {
|
||||
if (!state) return;
|
||||
this.playlistState.playlistId = state.id;
|
||||
this.playlistState.playlistName = state.name;
|
||||
this.playlistState.tracks = Array.isArray(state.tracks) ? state.tracks : [];
|
||||
this.playlistState.trackCount = this.playlistState.tracks.length;
|
||||
if (state.isPlaying && state.currentTrackId) {
|
||||
this.playlistState.active = true;
|
||||
this.playlistState.paused = state.isPaused;
|
||||
this._resumeTrack(state.currentTrackId, state.trackPosition, state.isPaused);
|
||||
}
|
||||
this.updateScriptWorkerPlaylist();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Periodically persist playback position 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(() => {});
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -862,9 +887,7 @@ export class BroadcastRenderer {
|
||||
el.preload = "auto";
|
||||
el.controls = false;
|
||||
this.audioManager.releaseMediaElement && this.audioManager.releaseMediaElement(el);
|
||||
// Wire through the channel limiter
|
||||
this.audioManager.ensureAudioController(asset); // ensures limiter is set up
|
||||
// Use a fresh Audio element independent of the per-asset loop controller
|
||||
this.audioManager.ensureAudioController(asset);
|
||||
el.volume = Math.min(1, Math.max(0, asset.audioVolume ?? 1));
|
||||
el.playbackRate = Math.max(0.25, (asset.audioSpeed ?? 1) * (asset.audioPitch ?? 1));
|
||||
el.onended = () => {
|
||||
@@ -877,6 +900,61 @@ export class BroadcastRenderer {
|
||||
el.play().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a track at a specific position, optionally paused.
|
||||
* Used on reconnect to restore state from the server.
|
||||
*/
|
||||
_resumeTrack(trackId, position, startPaused) {
|
||||
this._stopPlaylistAudio();
|
||||
if (!trackId) return;
|
||||
|
||||
const track = this.playlistState.tracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
|
||||
const asset = this.state.assets.get(track.audioAssetId);
|
||||
if (!asset) return;
|
||||
|
||||
const idx = this.playlistState.tracks.indexOf(track);
|
||||
this.playlistState.trackId = trackId;
|
||||
this.playlistState.trackName = track.assetName;
|
||||
this.playlistState.trackIndex = idx;
|
||||
|
||||
const el = new Audio(asset.url);
|
||||
el.preload = "auto";
|
||||
el.controls = false;
|
||||
this.audioManager.releaseMediaElement && this.audioManager.releaseMediaElement(el);
|
||||
this.audioManager.ensureAudioController(asset);
|
||||
el.volume = Math.min(1, Math.max(0, asset.audioVolume ?? 1));
|
||||
el.playbackRate = Math.max(0.25, (asset.audioSpeed ?? 1) * (asset.audioPitch ?? 1));
|
||||
el.onended = () => {
|
||||
if (this.playlistCurrentElement === el) {
|
||||
this.playlistCurrentElement = null;
|
||||
this._onPlaylistTrackEnded();
|
||||
}
|
||||
};
|
||||
this.playlistCurrentElement = el;
|
||||
|
||||
if (position > 0 || startPaused) {
|
||||
el.addEventListener("canplay", () => {
|
||||
if (position > 0) el.currentTime = position;
|
||||
if (startPaused) {
|
||||
el.pause();
|
||||
} else {
|
||||
el.play().catch(() => {});
|
||||
}
|
||||
}, { once: true });
|
||||
// Load without playing so canplay fires
|
||||
el.load();
|
||||
} else {
|
||||
el.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
_xsrfToken() {
|
||||
const cookie = document.cookie.split("; ").find(c => c.startsWith("XSRF-TOKEN="));
|
||||
return cookie ? decodeURIComponent(cookie.split("=")[1]) : null;
|
||||
}
|
||||
|
||||
_stopPlaylistAudio() {
|
||||
if (this.playlistCurrentElement) {
|
||||
this.playlistCurrentElement.onended = null;
|
||||
@@ -888,15 +966,14 @@ export class BroadcastRenderer {
|
||||
|
||||
_onPlaylistTrackEnded() {
|
||||
if (!this.playlistState.playlistId || !this.playlistState.trackId) return;
|
||||
// POST to server so it emits the appropriate NEXT or ENDED event
|
||||
const xsrfToken = document.cookie.split("; ").find(c => c.startsWith("XSRF-TOKEN="))?.split("=")[1];
|
||||
const xsrf = this._xsrfToken();
|
||||
fetch(
|
||||
`/api/channels/${encodeURIComponent(this.broadcaster)}/playlists/${encodeURIComponent(this.playlistState.playlistId)}/track-ended`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(xsrfToken ? { "X-XSRF-TOKEN": decodeURIComponent(xsrfToken) } : {}),
|
||||
...(xsrf ? { "X-XSRF-TOKEN": xsrf } : {}),
|
||||
},
|
||||
body: JSON.stringify({ trackId: this.playlistState.trackId }),
|
||||
}
|
||||
|
||||
@@ -84,8 +84,16 @@
|
||||
|
||||
async function loadActivePlaylist() {
|
||||
try {
|
||||
const active = await fetch(`${apiBase()}/active`).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
activePlaylistId = active?.id ?? null;
|
||||
const state = await fetch(`${apiBase()}/active`).then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
if (!state) return;
|
||||
activePlaylistId = state.id ?? null;
|
||||
if (state.isPlaying && state.currentTrackId) {
|
||||
playbackState = {
|
||||
playing: true,
|
||||
paused: state.isPaused ?? false,
|
||||
trackId: state.currentTrackId,
|
||||
};
|
||||
}
|
||||
renderPlaylistList();
|
||||
} catch { /* silently ignore */ }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user