diff --git a/resources/[tools]/nordi_dj/client/main.lua b/resources/[tools]/nordi_dj/client/main.lua index f94ec0982..55069d83a 100644 --- a/resources/[tools]/nordi_dj/client/main.lua +++ b/resources/[tools]/nordi_dj/client/main.lua @@ -622,7 +622,7 @@ local function extractVideoId(url) return nil end --- Aktualisiere die PlayMusic Funktion +-- Aktualisierte PlayMusic Funktion function PlayMusic(title, url, volume) if not title or not url then lib.notify({ @@ -633,29 +633,31 @@ function PlayMusic(title, url, volume) return end - -- Bereinige YouTube URL - local cleanedUrl = cleanYouTubeUrl(url) - local videoId = extractVideoId(cleanedUrl) - - if videoId then + -- Bereinige URL von Playlist-Parametern + local cleanUrl = url + if string.find(url, "youtube") then + cleanUrl = string.gsub(url, "&list=.-$", "") + cleanUrl = string.gsub(cleanUrl, "&start_radio=.-$", "") + cleanUrl = string.gsub(cleanUrl, "&index=.-$", "") + lib.notify({ title = 'DJ System', - description = 'YouTube Video wird geladen: ' .. title, + description = 'YouTube Video wird gestreamt: ' .. title, type = 'info' }) - - print('[DJ System] YouTube Video ID: ' .. videoId) - print('[DJ System] Bereinigte URL: ' .. cleanedUrl) end + print('[DJ System] Streaming: ' .. title .. ' | Clean URL: ' .. cleanUrl) + -- Sende an Server - TriggerServerEvent('dj:playMusic', title, cleanedUrl, volume or 50) + TriggerServerEvent('dj:playMusic', title, cleanUrl, volume or 50) -- Update lokale Variablen isPlaying = true currentSong = { title = title, - url = cleanedUrl, + url = cleanUrl, volume = volume or 50 } end + diff --git a/resources/[tools]/nordi_dj/html/index.html b/resources/[tools]/nordi_dj/html/index.html index f7aef6b10..7f3fa6d63 100644 --- a/resources/[tools]/nordi_dj/html/index.html +++ b/resources/[tools]/nordi_dj/html/index.html @@ -3,30 +3,21 @@ - DJ System + DJ System - YouTube Streaming
+ - - - - - + +
diff --git a/resources/[tools]/nordi_dj/html/script.js b/resources/[tools]/nordi_dj/html/script.js index dd7ca2526..9bc2164f9 100644 --- a/resources/[tools]/nordi_dj/html/script.js +++ b/resources/[tools]/nordi_dj/html/script.js @@ -4,78 +4,152 @@ let isPlaying = false; let currentSong = null; let fadeInterval = null; +// YouTube Player API laden +let youtubeAPIReady = false; +let youtubePlayer = null; + // Initialize when page loads document.addEventListener('DOMContentLoaded', function() { - audioPlayer = document.getElementById('audio-player'); + loadYouTubeAPI(); setupAudioPlayer(); }); -// Setup audio player with event listeners -function setupAudioPlayer() { - if (!audioPlayer) return; +// YouTube API laden +function loadYouTubeAPI() { + // YouTube IFrame API Script laden + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; + const firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); +} + +// YouTube API Ready Callback +window.onYouTubeIframeAPIReady = function() { + console.log('DJ System: YouTube API ready'); + youtubeAPIReady = true; - // Audio Events - audioPlayer.addEventListener('loadstart', function() { - console.log('DJ System: Loading started'); - }); + // Erstelle versteckten YouTube Player + createYouTubePlayer(); +}; + +// YouTube Player erstellen +function createYouTubePlayer() { + // Container für YouTube Player + const playerContainer = document.createElement('div'); + playerContainer.id = 'youtube-player-container'; + playerContainer.style.position = 'absolute'; + playerContainer.style.top = '-9999px'; + playerContainer.style.left = '-9999px'; + playerContainer.style.width = '1px'; + playerContainer.style.height = '1px'; + playerContainer.style.opacity = '0'; + document.body.appendChild(playerContainer); - audioPlayer.addEventListener('canplay', function() { - console.log('DJ System: Can start playing'); - }); - - audioPlayer.addEventListener('play', function() { - console.log('DJ System: Playback started'); - isPlaying = true; - }); - - audioPlayer.addEventListener('pause', function() { - console.log('DJ System: Playback paused'); - isPlaying = false; - }); - - audioPlayer.addEventListener('ended', function() { - console.log('DJ System: Song ended'); - isPlaying = false; - // Notify FiveM that song ended (for playlist functionality) - fetch(`https://${GetParentResourceName()}/songEnded`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: JSON.stringify({}) - }); - }); - - audioPlayer.addEventListener('error', function(e) { - console.error('DJ System: Audio error', e); - isPlaying = false; - // Notify FiveM about the error - fetch(`https://${GetParentResourceName()}/audioError`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - }, - body: JSON.stringify({ - error: 'Audio playback error', - code: audioPlayer.error ? audioPlayer.error.code : 'unknown' - }) - }); - }); - - audioPlayer.addEventListener('loadedmetadata', function() { - console.log('DJ System: Metadata loaded, duration:', audioPlayer.duration); - }); - - audioPlayer.addEventListener('timeupdate', function() { - // Optional: Send progress updates - if (isPlaying && audioPlayer.duration) { - const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100; - // You can use this for progress bars if needed + youtubePlayer = new YT.Player('youtube-player-container', { + height: '1', + width: '1', + playerVars: { + 'autoplay': 0, + 'controls': 0, + 'disablekb': 1, + 'fs': 0, + 'modestbranding': 1, + 'playsinline': 1, + 'rel': 0, + 'showinfo': 0, + 'iv_load_policy': 3, + 'cc_load_policy': 0, + 'start': 0, + 'origin': window.location.origin + }, + events: { + 'onReady': onYouTubePlayerReady, + 'onStateChange': onYouTubePlayerStateChange, + 'onError': onYouTubePlayerError } }); } -// Main message handler from FiveM +function onYouTubePlayerReady(event) { + console.log('DJ System: YouTube Player ready'); +} + +function onYouTubePlayerStateChange(event) { + switch(event.data) { + case YT.PlayerState.PLAYING: + console.log('DJ System: YouTube playing'); + isPlaying = true; + break; + case YT.PlayerState.PAUSED: + console.log('DJ System: YouTube paused'); + isPlaying = false; + break; + case YT.PlayerState.ENDED: + console.log('DJ System: YouTube ended'); + isPlaying = false; + notifyFiveM('songEnded', {}); + break; + case YT.PlayerState.BUFFERING: + console.log('DJ System: YouTube buffering'); + break; + } +} + +function onYouTubePlayerError(event) { + console.error('DJ System: YouTube error:', event.data); + let errorMessage = 'YouTube Fehler'; + + switch(event.data) { + case 2: + errorMessage = 'Ungültige Video ID'; + break; + case 5: + errorMessage = 'HTML5 Player Fehler'; + break; + case 100: + errorMessage = 'Video nicht gefunden'; + break; + case 101: + case 150: + errorMessage = 'Video nicht verfügbar (Einbettung deaktiviert)'; + break; + } + + notifyFiveM('audioError', { error: errorMessage }); +} + +// Setup normaler Audio Player für direkte URLs +function setupAudioPlayer() { + audioPlayer = document.getElementById('audio-player'); + if (!audioPlayer) { + audioPlayer = document.createElement('audio'); + audioPlayer.id = 'audio-player'; + audioPlayer.style.display = 'none'; + document.body.appendChild(audioPlayer); + } + + audioPlayer.addEventListener('play', () => { + isPlaying = true; + console.log('DJ System: Audio playing'); + }); + + audioPlayer.addEventListener('pause', () => { + isPlaying = false; + console.log('DJ System: Audio paused'); + }); + + audioPlayer.addEventListener('ended', () => { + isPlaying = false; + notifyFiveM('songEnded', {}); + }); + + audioPlayer.addEventListener('error', (e) => { + console.error('DJ System: Audio error', e); + notifyFiveM('audioError', { error: 'Audio playback error' }); + }); +} + +// Main message handler window.addEventListener('message', function(event) { const data = event.data; @@ -89,12 +163,6 @@ window.addEventListener('message', function(event) { case 'setVolume': setVolume(data.volume); break; - case 'fadeOut': - fadeOut(); - break; - case 'fadeIn': - fadeIn(); - break; case 'pauseMusic': pauseMusic(); break; @@ -104,90 +172,158 @@ window.addEventListener('message', function(event) { } }); -// Play music function with YouTube support +// Musik abspielen - YouTube oder direkte URL async function playMusic(url, volume, title = 'Unknown') { - if (!audioPlayer) { - console.error('DJ System: Audio player not initialized'); - return; - } - try { - // Stop current music first + console.log('DJ System: Playing:', title, url); + + // Stoppe aktuelle Musik stopMusic(); - console.log('DJ System: Attempting to play:', title, url); - - // Set volume + // Setze Lautstärke currentVolume = volume || 50; - audioPlayer.volume = currentVolume / 100; - // Handle different URL types - let playableUrl = await processUrl(url); - - if (!playableUrl) { - console.error('DJ System: Could not process URL:', url); - notifyError('Could not process audio URL'); - return; + // Prüfe ob YouTube URL + if (isYouTubeUrl(url)) { + await playYouTubeMusic(url, volume, title); + } else { + await playDirectMusic(url, volume, title); } - // Set source and play - audioPlayer.src = playableUrl; - audioPlayer.load(); - - // Store current song info + // Speichere aktuelle Song Info currentSong = { title: title, url: url, - playableUrl: playableUrl + volume: volume }; - // Attempt to play - const playPromise = audioPlayer.play(); - - if (playPromise !== undefined) { - playPromise - .then(() => { - console.log('DJ System: Successfully started playing:', title); - isPlaying = true; - }) - .catch(error => { - console.error('DJ System: Play failed:', error); - notifyError('Playback failed: ' + error.message); - }); - } - } catch (error) { - console.error('DJ System: Error in playMusic:', error); - notifyError('Error playing music: ' + error.message); + console.error('DJ System: Error playing music:', error); + notifyFiveM('audioError', { error: error.message }); } } -// Process different URL types -async function processUrl(url) { +// YouTube Musik abspielen +async function playYouTubeMusic(url, volume, title) { + if (!youtubeAPIReady || !youtubePlayer) { + throw new Error('YouTube Player nicht bereit'); + } + + const videoId = extractYouTubeVideoId(url); + if (!videoId) { + throw new Error('Ungültige YouTube URL'); + } + + console.log('DJ System: Playing YouTube video:', videoId); + + // Lade und spiele YouTube Video + youtubePlayer.loadVideoById({ + videoId: videoId, + startSeconds: 0 + }); + + // Setze Lautstärke + youtubePlayer.setVolume(volume || 50); + + // Warte kurz und starte + setTimeout(() => { + youtubePlayer.playVideo(); + }, 1000); +} + +// Direkte Audio URL abspielen +async function playDirectMusic(url, volume, title) { + if (!audioPlayer) { + throw new Error('Audio Player nicht verfügbar'); + } + + console.log('DJ System: Playing direct audio:', url); + + audioPlayer.src = url; + audioPlayer.volume = (volume || 50) / 100; + audioPlayer.load(); + + const playPromise = audioPlayer.play(); + if (playPromise !== undefined) { + await playPromise; + } +} + +// Musik stoppen +function stopMusic() { try { - // Check if it's a YouTube URL - if (isYouTubeUrl(url)) { - console.log('DJ System: Processing YouTube URL'); - return await convertYouTubeUrl(url); + // Stoppe YouTube Player + if (youtubePlayer && youtubeAPIReady) { + youtubePlayer.stopVideo(); } - // Check if it's a direct audio URL - if (isDirectAudioUrl(url)) { - console.log('DJ System: Direct audio URL detected'); - return url; + // Stoppe Audio Player + if (audioPlayer) { + audioPlayer.pause(); + audioPlayer.currentTime = 0; + audioPlayer.src = ''; } - // Try to use URL as-is (might be pre-converted) - console.log('DJ System: Using URL as-is'); - return url; + isPlaying = false; + currentSong = null; + + console.log('DJ System: Music stopped'); } catch (error) { - console.error('DJ System: Error processing URL:', error); - return null; + console.error('DJ System: Error stopping music:', error); } } -// Check if URL is YouTube +// Musik pausieren +function pauseMusic() { + try { + if (currentSong && isYouTubeUrl(currentSong.url)) { + if (youtubePlayer && youtubeAPIReady) { + youtubePlayer.pauseVideo(); + } + } else if (audioPlayer) { + audioPlayer.pause(); + } + } catch (error) { + console.error('DJ System: Error pausing music:', error); + } +} + +// Musik fortsetzen +function resumeMusic() { + try { + if (currentSong && isYouTubeUrl(currentSong.url)) { + if (youtubePlayer && youtubeAPIReady) { + youtubePlayer.playVideo(); + } + } else if (audioPlayer) { + audioPlayer.play(); + } + } catch (error) { + console.error('DJ System: Error resuming music:', error); + } +} + +// Lautstärke setzen +function setVolume(volume) { + try { + currentVolume = Math.max(0, Math.min(100, volume)); + + if (currentSong && isYouTubeUrl(currentSong.url)) { + if (youtubePlayer && youtubeAPIReady) { + youtubePlayer.setVolume(currentVolume); + } + } else if (audioPlayer) { + audioPlayer.volume = currentVolume / 100; + } + + console.log('DJ System: Volume set to', currentVolume + '%'); + } catch (error) { + console.error('DJ System: Error setting volume:', error); + } +} + +// YouTube URL prüfen function isYouTubeUrl(url) { const youtubePatterns = [ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, @@ -197,38 +333,7 @@ function isYouTubeUrl(url) { return youtubePatterns.some(pattern => pattern.test(url)); } -// Check if URL is direct audio -function isDirectAudioUrl(url) { - const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']; - const lowerUrl = url.toLowerCase(); - - return audioExtensions.some(ext => lowerUrl.includes(ext)) || - lowerUrl.includes('audio/') || - lowerUrl.includes('stream'); -} - -// Convert YouTube URL (this would be handled server-side in real implementation) -async function convertYouTubeUrl(url) { - try { - // Extract video ID - const videoId = extractYouTubeVideoId(url); - if (!videoId) { - throw new Error('Could not extract YouTube video ID'); - } - - console.log('DJ System: YouTube Video ID:', videoId); - - // In a real implementation, this would call your server-side converter - // For now, we'll return the original URL and let the server handle it - return url; - - } catch (error) { - console.error('DJ System: YouTube conversion error:', error); - return null; - } -} - -// Extract YouTube video ID +// YouTube Video ID extrahieren function extractYouTubeVideoId(url) { const patterns = [ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, @@ -245,157 +350,30 @@ function extractYouTubeVideoId(url) { return null; } -// Stop music -function stopMusic() { - if (!audioPlayer) return; - - try { - audioPlayer.pause(); - audioPlayer.currentTime = 0; - audioPlayer.src = ''; - isPlaying = false; - currentSong = null; - - // Clear any fade effects - if (fadeInterval) { - clearInterval(fadeInterval); - fadeInterval = null; - } - - console.log('DJ System: Music stopped'); - - } catch (error) { - console.error('DJ System: Error stopping music:', error); - } -} - -// Pause music -function pauseMusic() { - if (!audioPlayer || !isPlaying) return; - - try { - audioPlayer.pause(); - isPlaying = false; - console.log('DJ System: Music paused'); - } catch (error) { - console.error('DJ System: Error pausing music:', error); - } -} - -// Resume music -function resumeMusic() { - if (!audioPlayer || isPlaying) return; - - try { - const playPromise = audioPlayer.play(); - if (playPromise !== undefined) { - playPromise - .then(() => { - isPlaying = true; - console.log('DJ System: Music resumed'); - }) - .catch(error => { - console.error('DJ System: Resume failed:', error); - }); - } - } catch (error) { - console.error('DJ System: Error resuming music:', error); - } -} - -// Set volume -function setVolume(volume) { - if (!audioPlayer) return; - - try { - currentVolume = Math.max(0, Math.min(100, volume)); - audioPlayer.volume = currentVolume / 100; - console.log('DJ System: Volume set to', currentVolume + '%'); - } catch (error) { - console.error('DJ System: Error setting volume:', error); - } -} - -// Fade out effect (when player moves away) -function fadeOut() { - if (!audioPlayer || !isPlaying) return; - - if (fadeInterval) { - clearInterval(fadeInterval); - } - - let currentVol = audioPlayer.volume; - const targetVol = 0; - const fadeStep = 0.05; - - fadeInterval = setInterval(() => { - currentVol -= fadeStep; - if (currentVol <= targetVol) { - currentVol = targetVol; - audioPlayer.volume = currentVol; - clearInterval(fadeInterval); - fadeInterval = null; - } else { - audioPlayer.volume = currentVol; - } - }, 50); -} - -// Fade in effect (when player moves closer) -function fadeIn() { - if (!audioPlayer || !isPlaying) return; - - if (fadeInterval) { - clearInterval(fadeInterval); - } - - let currentVol = audioPlayer.volume; - const targetVol = currentVolume / 100; - const fadeStep = 0.05; - - fadeInterval = setInterval(() => { - currentVol += fadeStep; - if (currentVol >= targetVol) { - currentVol = targetVol; - audioPlayer.volume = currentVol; - clearInterval(fadeInterval); - fadeInterval = null; - } else { - audioPlayer.volume = currentVol; - } - }, 50); -} - -// Notify FiveM about errors -function notifyError(message) { - fetch(`https://${GetParentResourceName()}/audioError`, { +// FiveM benachrichtigen +function notifyFiveM(event, data) { + fetch(`https://${GetParentResourceName()}/` + event, { method: 'POST', headers: { - 'Content-Type': 'application/json; charset=UTF-8', + 'Content-Type': 'application/json' }, - body: JSON.stringify({ - error: message - }) + body: JSON.stringify(data) }).catch(err => { - console.error('DJ System: Failed to notify error:', err); + console.error('DJ System: Failed to notify FiveM:', err); }); } -// Get current resource name function GetParentResourceName() { return window.location.hostname; } -// Debug functions (can be called from browser console) +// Debug window.djDebug = { getCurrentSong: () => currentSong, getVolume: () => currentVolume, isPlaying: () => isPlaying, - getAudioPlayer: () => audioPlayer, - testPlay: (url) => playMusic(url, 50, 'Test Song'), - testStop: () => stopMusic(), - testVolume: (vol) => setVolume(vol) + youtubeReady: () => youtubeAPIReady, + testYouTube: (videoId) => playYouTubeMusic('https://www.youtube.com/watch?v=' + videoId, 50, 'Test') }; -// Log when script is loaded -console.log('DJ System: Script loaded and ready'); +console.log('DJ System: YouTube streaming system loaded'); diff --git a/resources/[tools]/nordi_dj/server/main.lua b/resources/[tools]/nordi_dj/server/main.lua index 68c565d4d..96f9cb0de 100644 --- a/resources/[tools]/nordi_dj/server/main.lua +++ b/resources/[tools]/nordi_dj/server/main.lua @@ -360,54 +360,61 @@ local function useAlternativeConversion(originalUrl, videoId, callback) end) end --- Aktualisiere die PlayMusic Funktion +-- Aktualisierte PlayMusic Funktion RegisterServerEvent('dj:playMusic') AddEventHandler('dj:playMusic', function(title, url, volume) local src = source local Player = QBCore.Functions.GetPlayer(src) if not Player then return end - print('[DJ System] Spiele Musik ab: ' .. title .. ' | URL: ' .. url) - - -- Prüfe ob es eine YouTube URL ist - if string.match(url, "youtube%.com") or string.match(url, "youtu%.be") then - convertYouTubeUrl(url, function(convertedUrl, error) - if error then - TriggerClientEvent('QBCore:Notify', src, 'Fehler beim Konvertieren: ' .. error, 'error') - return - end - - if convertedUrl then - -- Sende konvertierte URL an alle Clients - TriggerClientEvent('dj:playMusicClient', -1, title, convertedUrl, volume) - - -- Speichere in Session History - MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', { - Player.PlayerData.citizenid, - Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname, - 'Unknown', -- Du kannst hier den Booth-Namen hinzufügen - title, - url, - 'youtube', - volume - }) - else - TriggerClientEvent('QBCore:Notify', src, 'Konvertierung fehlgeschlagen', 'error') - end - end) - else - -- Direkte Audio URL - TriggerClientEvent('dj:playMusicClient', -1, title, url, volume) + -- Bereinige YouTube URL (entferne Playlist-Parameter) + local cleanUrl = url + if string.find(url, "youtube%.com") or string.find(url, "youtu%.be") then + -- Entferne &list= und andere Parameter + cleanUrl = string.gsub(url, "&list=.-$", "") + cleanUrl = string.gsub(cleanUrl, "&start_radio=.-$", "") + cleanUrl = string.gsub(cleanUrl, "&index=.-$", "") - -- Speichere in Session History - MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', { - Player.PlayerData.citizenid, - Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname, - 'Unknown', - title, - url, - 'direct', - volume - }) + print('[DJ System] Bereinigte YouTube URL: ' .. cleanUrl) end + + print('[DJ System] Streaming: ' .. title .. ' | URL: ' .. cleanUrl) + + -- Sende direkt an alle Clients zum Streamen + TriggerClientEvent('dj:playMusicClient', -1, title, cleanUrl, volume) + + -- Speichere in Datenbank + local songType = 'direct' + if string.find(cleanUrl, "youtube") then + songType = 'youtube' + end + + MySQL.Async.execute('INSERT INTO dj_session_history (dj_citizenid, dj_name, booth_name, song_title, song_url, song_type, volume, session_start) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', { + Player.PlayerData.citizenid, + Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname, + 'DJ Booth', + title, + cleanUrl, + songType, + volume + }) + + TriggerClientEvent('QBCore:Notify', src, 'Streaming: ' .. title, 'success') end) + +-- Audio Error Handler +RegisterNUICallback('audioError', function(data, cb) + local src = source + print('[DJ System] Audio Error: ' .. (data.error or 'Unknown')) + + TriggerClientEvent('QBCore:Notify', src, 'Audio Fehler: ' .. (data.error or 'Unbekannt'), 'error') + cb('ok') +end) + +-- Song Ended Handler +RegisterNUICallback('songEnded', function(data, cb) + print('[DJ System] Song ended') + -- Hier könntest du Playlist-Logik hinzufügen + cb('ok') +end) +