diff --git a/package-lock.json b/package-lock.json index 70938af..56eda02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "bootstrap": "^5.3.1", "howler": "^2.2.3", "pino": "^8.16.1", + "rc-slider": "^10.5.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-colorful": "^5.6.1", @@ -14463,6 +14464,41 @@ "node": ">=0.10.0" } }, + "node_modules/rc-slider": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", + "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.27.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.38.2", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.2.tgz", + "integrity": "sha512-yRGRPKyi84H7NkRSP6FzEIYBdUt4ufdsmXUZ7qM2H5qoByPax70NnGPkfo36N+UKUnUBj2f2Q2eUbwYMuAsIOQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react": { "version": "18.2.0", "license": "MIT", diff --git a/package.json b/package.json index 156e53f..91858df 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "bootstrap": "^5.3.1", "howler": "^2.2.3", "pino": "^8.16.1", + "rc-slider": "^10.5.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-colorful": "^5.6.1", diff --git a/src/components/MediaWidget/MediaWidget.css b/src/components/MediaWidget/MediaWidget.css index 23e25b1..4b761a5 100644 --- a/src/components/MediaWidget/MediaWidget.css +++ b/src/components/MediaWidget/MediaWidget.css @@ -11,6 +11,16 @@ .player-container { flex: 0 1 auto; + display: flex; + align-items: center; +} + +.player-container .rc-slider { + width: 100px; +} + +.player-container .rc-slider-track { + background-color: rgb(13,110,253); } .playlist { diff --git a/src/components/MediaWidget/PlaylistController.ts b/src/components/MediaWidget/PlaylistController.ts index b301813..3e4b662 100644 --- a/src/components/MediaWidget/PlaylistController.ts +++ b/src/components/MediaWidget/PlaylistController.ts @@ -15,25 +15,47 @@ export class PlaylistController { recipientId; constructor(recipientId: string, widgetId: string, conf: any) { - this.recipientId = recipientId; const requested = new Playlist(PLAYLIST_TYPE.REQUESTED, conf.topic.player); + const personal = new Playlist(PLAYLIST_TYPE.PERSONAL, conf.topic.player); + const playlistEndListener = { + id: `playlistController_${widgetId}`, + trigger: (playlist: Playlist) => { + log.debug( + `checking playlist end, total song count: ${ + playlist.songs().length + }, current index: ${playlist.index()}`, + ); + if (playlist.index() !== null) { + return; + } + const personalIndex = personal.index(); + if (personalIndex === null) { + return; + } + if (personalIndex < personal.songs().length) { + this.switchTo(PLAYLIST_TYPE.PERSONAL); + } + }, + }; + requested.addListener(playlistEndListener); this.current = requested; + this.recipientId = recipientId; this.playlists.set(PLAYLIST_TYPE.REQUESTED, requested); - this.playlists.set( - PLAYLIST_TYPE.PERSONAL, - new Playlist(PLAYLIST_TYPE.PERSONAL, conf.topic.player), - ); + this.playlists.set(PLAYLIST_TYPE.PERSONAL, personal); log.info(`Loading playlist for ${recipientId}`); axios - .get( - `${process.env.REACT_APP_MEDIA_API_ENDPOINT}/media/video`, - ) + .get(`${process.env.REACT_APP_MEDIA_API_ENDPOINT}/media/video`) .then((response) => { let songs = response.data; songs = songs.map( - (element: { url: string; id: string; title: string; owner: string }) => { + (element: { + url: string; + id: string; + title: string; + owner: string; + }) => { return { src: element.url, type: "video/youtube", diff --git a/src/components/MediaWidget/VideoJSComponent.tsx b/src/components/MediaWidget/VideoJSComponent.tsx index fb82998..3114298 100644 --- a/src/components/MediaWidget/VideoJSComponent.tsx +++ b/src/components/MediaWidget/VideoJSComponent.tsx @@ -10,6 +10,8 @@ import ProgressBar from "./ProgressBar"; import VideoDuration from "./VideoDuration"; import { PlaylistController } from "./PlaylistController"; import { publish, subscribe, unsubscribe } from "../../socket"; +import Slider from 'rc-slider'; +import 'rc-slider/assets/index.css'; let options: VideoJsPlayerOptions = { autoplay: true, @@ -46,6 +48,13 @@ export default function VideoJSComponent({ const pausedByCommand = useRef(false); const [player, setPlayer] = useState(null); const commandHandler = useRef(null); + const [volume, setVolume] = useState(() => { + const vol = localStorage.getItem("volume"); + if (vol) { + return JSON.parse(vol); + } + return 50; + }); function freeze() { log.debug(`freezing player`); @@ -58,10 +67,12 @@ export default function VideoJSComponent({ function unfreeze() { log.debug(`unfreezing player`); if (!player) { + log.debug(`cancel unfreeze because of missing player`); return; } if (pausedByCommand.current) { pausedByCommand.current = false; + log.debug(`calling player.play()`); player.play(); } } @@ -138,7 +149,7 @@ export default function VideoJSComponent({ const player = videojs(videoElement, options); player.src(song); - player.volume(0.5); + player.volume(volume/100); player.on("play", () => { log.debug("start playing"); setPlayerState(PLAYER_STATE.PLAYING); @@ -167,6 +178,13 @@ export default function VideoJSComponent({ }; }, [song, isRemote]); + useEffect(() => { + if (player) { + player.volume(volume/100); + localStorage.setItem("volume", JSON.stringify(volume)); + } + },[volume]); + return ( <> {!isRemote && song && ( @@ -240,6 +258,9 @@ export default function VideoJSComponent({ {!isRemote && } + { + setVolume(value); + }} /> ); diff --git a/src/logic/playlist/Playlist.ts b/src/logic/playlist/Playlist.ts index 60bd516..458bdf0 100644 --- a/src/logic/playlist/Playlist.ts +++ b/src/logic/playlist/Playlist.ts @@ -9,6 +9,10 @@ enum PLAYLIST_TYPE { PERSONAL, } +function nameOf(type: PLAYLIST_TYPE) { + return type === PLAYLIST_TYPE.PERSONAL ? "personal" : "requested"; +} + class Playlist { _songs: Song[] = []; _index: number | null = null; @@ -47,6 +51,9 @@ class Playlist { markListened(originId); } this._songs.splice(index, 1); + if (this._songs.length == 0) { + this._index = null; + } this.triggerListeners(); } @@ -123,22 +130,33 @@ class Playlist { } addListener(listener: IPlaylistChangesListener) { - log.debug(`adding listener, current songs amount: ${this._songs.length}, index: ${this.index}`); this._listeners.push(listener); + log.debug( + `adding listener ${listener.id} to playlist ${nameOf( + this.type(), + )}, current songs amount: ${this._songs.length}, index: ${ + this.index() + }, total listeners count: ${this._listeners.length}`, + ); listener.trigger(this); } removeListener(id: string) { - const index = this._listeners.findIndex((listener) => (listener.id = id)); + log.debug(`listeners before removing ${id}: ${JSON.stringify(this._listeners)}`); + const index = this._listeners.findIndex((listener) => (listener.id === id)); this._listeners.splice(index, 1); + log.debug(`listeners after removing ${id}: ${JSON.stringify(this._listeners)}`); } triggerListeners() { publish(this.topic, { count: this._index === null ? 0 : this._songs.length, - number: this._index === null ? 0 : this._index + number: this._index === null ? 0 : this._index, + }); + this._listeners.forEach((listener) => { + listener.trigger(this); + log.debug(`playlist ${nameOf(this.type())} notifing listener ${listener.id}`); }); - this._listeners.forEach((listener) => listener.trigger(this)); } }