Implement proper youtube support
This commit is contained in:
117
client/src/components/YouTubePlayer/YouTubePlayer.jsx
Normal file
117
client/src/components/YouTubePlayer/YouTubePlayer.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import './styles.sass';
|
||||
|
||||
const YouTubePlayer = ({
|
||||
videoId,
|
||||
autoplay = false,
|
||||
volume = 50,
|
||||
startTime = 45,
|
||||
onReady = () => {},
|
||||
onError = () => {},
|
||||
className = ''
|
||||
}) => {
|
||||
const iframeRef = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
console.log("YouTubePlayer rendering with videoId:", videoId, "volume:", volume, "startTime:", startTime);
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.YT) {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = 'https://www.youtube.com/iframe_api';
|
||||
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
console.log("YouTube API ready");
|
||||
};
|
||||
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoId) return;
|
||||
|
||||
const createPlayer = () => {
|
||||
if (!window.YT || !window.YT.Player) {
|
||||
console.log("YouTube API not ready, waiting...");
|
||||
setTimeout(createPlayer, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerRef.current) {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
|
||||
console.log("Creating YouTube player for video:", videoId);
|
||||
|
||||
try {
|
||||
playerRef.current = new window.YT.Player(iframeRef.current, {
|
||||
height: '200',
|
||||
width: '300',
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
'playsinline': 1,
|
||||
'controls': 1,
|
||||
'showinfo': 0,
|
||||
'rel': 0,
|
||||
'autoplay': autoplay ? 1 : 0,
|
||||
'start': startTime
|
||||
},
|
||||
events: {
|
||||
'onReady': (event) => {
|
||||
console.log("YouTube player ready");
|
||||
event.target.setVolume(volume);
|
||||
if (autoplay) {
|
||||
event.target.seekTo(startTime);
|
||||
event.target.playVideo();
|
||||
}
|
||||
setIsLoaded(true);
|
||||
onReady();
|
||||
},
|
||||
'onError': (event) => {
|
||||
console.error("YouTube player error:", event);
|
||||
onError(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error creating YouTube player:", error);
|
||||
}
|
||||
};
|
||||
|
||||
createPlayer();
|
||||
|
||||
return () => {
|
||||
if (playerRef.current) {
|
||||
try {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
} catch (e) {
|
||||
console.error("Error destroying player:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [videoId, autoplay, onReady, onError, startTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRef.current && isLoaded) {
|
||||
console.log("Setting YouTube volume to:", volume);
|
||||
try {
|
||||
playerRef.current.setVolume(volume);
|
||||
} catch (error) {
|
||||
console.error("Error setting volume:", error);
|
||||
}
|
||||
}
|
||||
}, [volume, isLoaded]);
|
||||
|
||||
return (
|
||||
<div className={`youtube-player-container ${className}`}>
|
||||
<div ref={iframeRef} className="youtube-embed" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default YouTubePlayer;
|
32
client/src/components/YouTubePlayer/styles.sass
Normal file
32
client/src/components/YouTubePlayer/styles.sass
Normal file
@@ -0,0 +1,32 @@
|
||||
.youtube-player-container
|
||||
position: fixed
|
||||
right: 20px
|
||||
bottom: 120px
|
||||
width: 300px
|
||||
height: 200px
|
||||
z-index: 100
|
||||
|
||||
&.hidden-player
|
||||
width: 1px
|
||||
height: 1px
|
||||
opacity: 0
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
left: -9999px
|
||||
top: -9999px
|
||||
|
||||
.youtube-embed
|
||||
width: 100%
|
||||
height: 100%
|
||||
border: 0
|
||||
border-radius: 8px
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5)
|
||||
|
||||
.audio-player
|
||||
width: 1px
|
||||
height: 1px
|
||||
position: absolute
|
||||
top: -9999px
|
||||
left: -9999px
|
||||
opacity: 0
|
||||
visibility: hidden
|
@@ -3,7 +3,17 @@ import {SocketContext} from "@/common/contexts/SocketContext";
|
||||
import {useContext, useState, useEffect, useRef, useCallback} from "react";
|
||||
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faMessage, faMusic, faHeadphones, faClock, faCrown, faPaperPlane} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faMessage,
|
||||
faMusic,
|
||||
faHeadphones,
|
||||
faClock,
|
||||
faCrown,
|
||||
faPaperPlane,
|
||||
faCheckCircle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
||||
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
||||
|
||||
export const Game = () => {
|
||||
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
||||
@@ -34,6 +44,11 @@ export const Game = () => {
|
||||
const messageEndRef = useRef(null);
|
||||
const timerIntervalRef = useRef(null);
|
||||
|
||||
const [allSongs, setAllSongs] = useState([]);
|
||||
const [playerVolume, setPlayerVolume] = useState(30);
|
||||
const [playbackError, setPlaybackError] = useState(null);
|
||||
const [songsLoading, setSongsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) return;
|
||||
|
||||
@@ -144,6 +159,50 @@ export const Game = () => {
|
||||
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !socket) return;
|
||||
|
||||
const loadSongs = async () => {
|
||||
try {
|
||||
setSongsLoading(true);
|
||||
console.log("Fetching songs with active socket:", socket?.id);
|
||||
const songs = await fetchPlaylistSongs(socket);
|
||||
console.log(`Successfully loaded ${songs.length} songs`);
|
||||
setAllSongs(songs);
|
||||
} catch (error) {
|
||||
console.error("Error loading songs:", error);
|
||||
} finally {
|
||||
setSongsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSongs();
|
||||
}, [socket, connected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allSongs.length || !currentSong || !currentSong.id) return;
|
||||
|
||||
const enhancedSong = allSongs.find(song => song.id === currentSong.id);
|
||||
if (enhancedSong && enhancedSong !== currentSong) {
|
||||
setCurrentSong(enhancedSong);
|
||||
}
|
||||
}, [allSongs, currentSong]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allSongs.length || !songOptions.length) return;
|
||||
|
||||
const enhancedOptions = songOptions.map(option => {
|
||||
if (option.id && !option.title) {
|
||||
return allSongs.find(song => song.id === option.id) || option;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
|
||||
if (JSON.stringify(enhancedOptions) !== JSON.stringify(songOptions)) {
|
||||
setSongOptions(enhancedOptions);
|
||||
}
|
||||
}, [allSongs, songOptions]);
|
||||
|
||||
const handleFrequencyChange = useCallback((newFrequency) => {
|
||||
setFrequency(newFrequency);
|
||||
if (role === "composer") {
|
||||
@@ -185,7 +244,27 @@ export const Game = () => {
|
||||
setTimeLeft(0);
|
||||
}, [send]);
|
||||
|
||||
// Phase-specific content rendering
|
||||
const handlePlayerReady = useCallback(() => {
|
||||
setPlaybackError(null);
|
||||
}, []);
|
||||
|
||||
const handlePlayerError = useCallback((error) => {
|
||||
console.error("YouTube playback error:", error);
|
||||
setPlaybackError("Error playing song - trying to continue...");
|
||||
}, []);
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
const audioElement = document.querySelector('audio');
|
||||
|
||||
if (audioElement) {
|
||||
if (audioElement.paused) {
|
||||
audioElement.play().catch(err => console.error("Play error:", err));
|
||||
} else {
|
||||
audioElement.pause();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderPhaseContent = () => {
|
||||
switch (phase) {
|
||||
case "waiting":
|
||||
@@ -264,7 +343,7 @@ export const Game = () => {
|
||||
<img src={song.coverUrl} alt={song.title} />
|
||||
{selectedSong?.id === song.id && (
|
||||
<div className="selection-indicator">
|
||||
<FontAwesomeIcon icon="fa-solid fa-check-circle" />
|
||||
<FontAwesomeIcon icon={faCheckCircle} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -285,7 +364,7 @@ export const Game = () => {
|
||||
onClick={handleSubmitGuess}
|
||||
disabled={!selectedSong}
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-paper-plane" />
|
||||
<FontAwesomeIcon icon={faPaperPlane} />
|
||||
Antwort einreichen
|
||||
</button>
|
||||
)}
|
||||
@@ -429,6 +508,46 @@ export const Game = () => {
|
||||
frequency={frequency}
|
||||
/>
|
||||
)}
|
||||
|
||||
{role === "composer" && currentSong && currentSong.youtubeId && (
|
||||
<YouTubePlayer
|
||||
videoId={currentSong.youtubeId}
|
||||
autoplay={phase === "composing"}
|
||||
volume={playerVolume}
|
||||
startTime={currentSong.refrainTime || 45}
|
||||
onReady={handlePlayerReady}
|
||||
onError={handlePlayerError}
|
||||
className={phase === "composing" ? "" : "hidden-player"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{playbackError && (
|
||||
<div className="playback-error">
|
||||
{playbackError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "composing" && role === "composer" && currentSong && (
|
||||
<div className="player-controls">
|
||||
<div className="volume-controls">
|
||||
<span>Music Volume:</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={playerVolume}
|
||||
onChange={(e) => setPlayerVolume(parseInt(e.target.value))}
|
||||
className="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{songsLoading && (
|
||||
<div className="songs-loading-indicator">
|
||||
Loading songs...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -961,4 +961,68 @@
|
||||
transform: scale(1)
|
||||
50%
|
||||
opacity: 1
|
||||
transform: scale(1.05)
|
||||
transform: scale(1.05)
|
||||
|
||||
.volume-controls
|
||||
position: fixed
|
||||
top: 70px
|
||||
right: 20px
|
||||
background: rgba(0, 0, 0, 0.5)
|
||||
padding: 10px 15px
|
||||
border-radius: 10px
|
||||
display: flex
|
||||
align-items: center
|
||||
z-index: 10
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||
|
||||
span
|
||||
color: white
|
||||
margin-right: 10px
|
||||
font-size: 14px
|
||||
|
||||
.volume-slider
|
||||
width: 100px
|
||||
height: 6px
|
||||
-webkit-appearance: none
|
||||
appearance: none
|
||||
background: rgba(255, 255, 255, 0.2)
|
||||
border-radius: 3px
|
||||
outline: none
|
||||
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance: none
|
||||
appearance: none
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 50%
|
||||
background: $yellow
|
||||
cursor: pointer
|
||||
box-shadow: 0 0 10px rgba(255, 255, 0, 0.5)
|
||||
|
||||
&::-moz-range-thumb
|
||||
width: 16px
|
||||
height: 16px
|
||||
border-radius: 50%
|
||||
background: $yellow
|
||||
cursor: pointer
|
||||
box-shadow: 0 0 10px rgba(255, 255, 0, 0.5)
|
||||
|
||||
.playback-error
|
||||
position: fixed
|
||||
bottom: 10px
|
||||
left: 50%
|
||||
transform: translateX(-50%)
|
||||
background: rgba(255, 0, 0, 0.7)
|
||||
color: white
|
||||
padding: 8px 20px
|
||||
border-radius: 20px
|
||||
font-size: 14px
|
||||
z-index: 1000
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
||||
animation: fade-out 4s forwards
|
||||
|
||||
@keyframes fade-out
|
||||
0%, 80%
|
||||
opacity: 1
|
||||
100%
|
||||
opacity: 0
|
112
client/src/services/youtubeService.js
Normal file
112
client/src/services/youtubeService.js
Normal file
@@ -0,0 +1,112 @@
|
||||
let cachedSongs = null;
|
||||
let fetchPromise = null;
|
||||
|
||||
|
||||
export const fetchPlaylistSongs = async (socketInstance) => {
|
||||
if (cachedSongs && cachedSongs.length > 0) {
|
||||
console.log("Returning cached songs:", cachedSongs.length);
|
||||
return cachedSongs;
|
||||
}
|
||||
|
||||
if (fetchPromise) {
|
||||
console.log("Request already in progress, waiting for response...");
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
console.log("Starting new fetch request for playlist songs");
|
||||
|
||||
fetchPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let socket = socketInstance;
|
||||
|
||||
if (!socket) {
|
||||
try {
|
||||
const socketModule = await import('@/common/contexts/SocketContext');
|
||||
socket = socketModule.socket;
|
||||
} catch (error) {
|
||||
console.error("Failed to import socket:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!socket) {
|
||||
throw new Error("Socket not available");
|
||||
}
|
||||
|
||||
if (!socket.connected) {
|
||||
console.warn("Socket not connected - attempting to connect");
|
||||
socket.connect();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
if (!socket.connected) {
|
||||
throw new Error("Could not connect to server");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Socket connected, requesting songs...");
|
||||
|
||||
const responsePromise = new Promise((resolve, reject) => {
|
||||
const handleResponse = (data) => {
|
||||
console.log("Received playlist-songs response");
|
||||
|
||||
if (data.error) {
|
||||
console.error("Server returned error:", data.error);
|
||||
reject(new Error(data.error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && data.songs && Array.isArray(data.songs) && data.songs.length > 0) {
|
||||
cachedSongs = data.songs;
|
||||
resolve(data.songs);
|
||||
} else {
|
||||
console.error("Invalid song data in response", data);
|
||||
reject(new Error("Invalid song data"));
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("playlist-songs", handleResponse);
|
||||
|
||||
setTimeout(() => {
|
||||
socket.off("playlist-songs", handleResponse);
|
||||
reject(new Error("Request timed out"));
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
socket.emit("get-playlist-songs");
|
||||
|
||||
const songs = await responsePromise;
|
||||
fetchPromise = null;
|
||||
resolve(songs);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching playlist songs:", error);
|
||||
fetchPromise = null;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return fetchPromise.catch(error => {
|
||||
console.error("Failed to fetch playlist songs:", error);
|
||||
|
||||
return [{
|
||||
id: 1,
|
||||
title: "Connection Error",
|
||||
artist: "Please refresh the page",
|
||||
coverUrl: "https://place-hold.it/500x500/f00/fff?text=Error",
|
||||
youtubeId: "dQw4w9WgXcQ"
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a single song by ID
|
||||
*/
|
||||
export const getSongById = async (id, socketInstance) => {
|
||||
try {
|
||||
const songs = await fetchPlaylistSongs(socketInstance);
|
||||
return songs.find(song => song.id === id) || null;
|
||||
} catch (error) {
|
||||
console.error("Error getting song by ID:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user