From 3d3df64d7c90f4f1fe07b30318f7a628f69622ac Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 4 Jun 2026 12:06:16 -0400 Subject: [PATCH] fix: missing mic toggle in Live2D view + YouTube autoplay KiraAvatar: Added Talk mic button to Live2D view (was only in AnimatedAvatar fallback). Includes listening-pulse animation. MusicPlayer: Replaced hidden YouTube iframe with proper IFrame Player API. Now starts on explicit user click (Start Lo-Fi button), complying with browser autoplay policies. Supports station switching and volume control after playback starts. --- frontend/src/components/KiraAvatar.tsx | 61 +++++++---- frontend/src/components/MusicPlayer.tsx | 136 +++++++++++++++++------- 2 files changed, 143 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/KiraAvatar.tsx b/frontend/src/components/KiraAvatar.tsx index be2e9e3..5a814ea 100644 --- a/frontend/src/components/KiraAvatar.tsx +++ b/frontend/src/components/KiraAvatar.tsx @@ -223,25 +223,40 @@ export default function KiraAvatar(props: Props) { )} - {/* Expression indicator */} + {/* Expression indicator + Talk button */} {live2dReady && ( -
- {EXPRESSIONS.map((expr) => ( - - ))} -
+ <> +
+ {EXPRESSIONS.map((expr) => ( + + ))} +
+ + {/* Talk button — mic toggle */} + + )} {/* Status bar */} @@ -251,6 +266,16 @@ export default function KiraAvatar(props: Props) { {props.isSpeaking ? 'speaking...' : props.isListening ? 'listening...' : live2dReady ? 'here with you' : 'loading...'} + + ); } diff --git a/frontend/src/components/MusicPlayer.tsx b/frontend/src/components/MusicPlayer.tsx index bba899f..df1ad32 100644 --- a/frontend/src/components/MusicPlayer.tsx +++ b/frontend/src/components/MusicPlayer.tsx @@ -1,27 +1,87 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; -const LOFI_PLAYLISTS = [ - { id: 'lofi-girl', name: 'lofi hip hop radio', url: 'https://www.youtube.com/embed/jfKfPfyJRdk', icon: '🎧' }, - { id: 'lofi-chill', name: 'Chill lofi', url: 'https://www.youtube.com/embed/5qap5aO4i9A', icon: 'đŸŽĩ' }, - { id: 'lofi-synth', name: 'Synthwave lofi', url: 'https://www.youtube.com/embed/MVPTmgNG4x0', icon: '🌃' }, +interface Playlist { + id: string; + name: string; + videoId: string; + icon: string; +} + +const LOFI_PLAYLISTS: Playlist[] = [ + { id: 'lofi-girl', name: 'lofi hip hop radio', videoId: 'jfKfPfyJRdk', icon: '🎧' }, + { id: 'lofi-chill', name: 'Chill lofi', videoId: '5qap5aO4i9A', icon: 'đŸŽĩ' }, + { id: 'lofi-synth', name: 'Synthwave lofi', videoId: 'MVPTmgNG4x0', icon: '🌃' }, ]; export default function MusicPlayer() { - const [active, setActive] = useState('lofi-girl'); + const [activeId, setActiveId] = useState('lofi-girl'); const [volume, setVolume] = useState(0.3); - const iframeRef = useRef(null); + const [started, setStarted] = useState(false); + const wrapperRef = useRef(null); + const playerRef = useRef(null); + const apiReady = useRef(false); - useEffect(() => { - // Post volume to YouTube iframe when it changes - const timer = setTimeout(() => { - if (iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - JSON.stringify({ event: 'command', func: 'setVolume', args: [volume * 100] }), - '*' - ); + // Load YouTube IFrame API on first user interaction + const startPlayer = useCallback(() => { + if (apiReady.current) return; + apiReady.current = true; + + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; + const first = document.getElementsByTagName('script')[0]; + first.parentNode?.insertBefore(tag, first); + + (window as any).onYouTubeIframeAPIReady = () => { + const active = LOFI_PLAYLISTS.find((p) => p.id === activeId); + if (!active) return; + + playerRef.current = new (window as any).YT.Player('kira-youtube-player', { + height: '0', + width: '0', + videoId: active.videoId, + playerVars: { + autoplay: 1, + controls: 0, + loop: 1, + playlist: active.videoId, + enablejsapi: 1, + }, + events: { + onReady: (e: any) => { + e.target.setVolume(volume * 100); + e.target.playVideo(); + }, + }, + }); + }; + }, [activeId, volume]); + + // Handle play button click (first user gesture) + const handlePlay = () => { + if (!started) { + setStarted(true); + startPlayer(); + } + }; + + // Change station + const changeStation = (id: string) => { + setActiveId(id); + if (playerRef.current && (window as any).YT) { + const active = LOFI_PLAYLISTS.find((p) => p.id === id); + if (active) { + playerRef.current.loadVideoById(active.videoId); + playerRef.current.setVolume(volume * 100); + playerRef.current.playVideo(); } - }, 500); - return () => clearTimeout(timer); + } + }; + + // Volume + useEffect(() => { + if (playerRef.current) { + playerRef.current.setVolume(volume * 100); + } }, [volume]); return ( @@ -44,18 +104,16 @@ export default function MusicPlayer() { -
+
{LOFI_PLAYLISTS.map((p) => (
-
- {active && ( -