This repository has been archived on 2022-09-16. You can view files and clone it, but cannot push or open issues/pull-requests.
tlaternet-templates/src/music/features/musicplayer/musicPlayerSlice.ts

151 lines
3.9 KiB
TypeScript

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { RootState, AppDispatch } from "../../store";
//************************
// Interface definitions *
//************************
interface MusicPlayerState {
muted: boolean;
playing: PlayState;
title: MusicPlayerTitle;
}
interface MusicPlayerTitle {
source: string;
artist: string;
name: string;
album: string;
length: number;
}
enum PlayState {
Playing = "Playing",
Paused = "Paused",
Loading = "Loading",
}
//*********************
// Music player logic *
//*********************
class MusicPlayer {
private context: AudioContext;
private source: HTMLAudioElement;
private sourceNode: MediaElementAudioSourceNode;
private volume: GainNode;
constructor() {
this.context = new AudioContext();
this.source = new Audio();
this.sourceNode = this.context.createMediaElementSource(this.source);
this.volume = this.context.createGain();
this.sourceNode.connect(this.volume);
this.volume.connect(this.context.destination);
}
get audioContext() {
return this.context;
}
get audioNode() {
return this.sourceNode;
}
set src(source: string) {
this.source.src = source;
}
togglePlay = async (
_: null,
{ getState }: { getState: () => RootState }
): Promise<PlayState> => {
const playing = getState().musicPlayer.playing;
switch (playing) {
case PlayState.Playing:
this.source.pause();
return PlayState.Paused;
case PlayState.Paused:
case PlayState.Loading:
// Chrome's extra cookie, it refuses to play if we
// don't resume after the first user interaction.
await this.context.resume();
return this.source.play().then(() => PlayState.Playing);
}
};
}
const player = new MusicPlayer();
//*************************
// Redux state management *
//*************************
const initialState: MusicPlayerState = {
muted: false,
playing: PlayState.Paused,
title: {
source: "",
artist: "",
name: "",
album: "",
length: 0,
},
};
export const musicPlayerSlice = createSlice({
name: "musicPlayer",
initialState,
reducers: {
setSource: (state, action: PayloadAction<MusicPlayerTitle>) => {
state.title = action.payload;
player.src = state.title.source;
},
},
extraReducers: (builder) => {
builder
.addCase(togglePlay.pending, (state) => {
// If we are currently paused or loading, then this is
// actually an async call, otherwise we just
// synchronously pause the music.
if (state.playing !== PlayState.Playing) {
state.playing = PlayState.Loading;
}
})
.addCase(togglePlay.fulfilled, (state, { payload }) => {
state.playing = payload;
})
.addCase(togglePlay.rejected, (state, { error }) => {
if (error.message !== undefined) {
console.error(`Could not play music: ${error.message}`);
}
state.playing = PlayState.Paused;
});
},
});
export const togglePlay = createAsyncThunk<
PlayState,
null,
{ dispatch: AppDispatch; state: RootState }
>("musicPlayer/togglePlay", player.togglePlay, {
condition: (_, { getState }) => {
const playing = getState().musicPlayer.playing;
if (playing == PlayState.Loading) {
// Block updates when we're loading
return false;
}
},
});
export const { setSource } = musicPlayerSlice.actions;
export { PlayState, player as musicPlayer };
export type { MusicPlayerState };
export default musicPlayerSlice.reducer;