Watch Together: Embed-Fehlerbehandlung, klickbare Queue, Video-Titel

- YouTube onError Handler: Erkennt Error 101/150 (Embedding deaktiviert),
  zeigt Fehlermeldung + "Auf YouTube oeffnen" Link, auto-skip nach 3s
- Queue-Items klickbar fuer Host (play_video mit Index)
- Video-Titel werden via noembed.com oEmbed API geholt
- Server-Endpoint: GET /api/watch-together/video-info?url=...
- "Hinzufuegen" Button zeigt Ladezustand waehrend Titel-Fetch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-07 11:19:59 +01:00
parent e4895a792c
commit 09813b626f
7 changed files with 4967 additions and 4838 deletions

View file

@ -74,6 +74,8 @@ export default function WatchTogetherTab({ data }: { data: any }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [playerError, setPlayerError] = useState<string | null>(null);
const [addingToQueue, setAddingToQueue] = useState(false);
// ── Refs ──
const wsRef = useRef<WebSocket | null>(null);
@ -183,6 +185,7 @@ export default function WatchTogetherTab({ data }: { data: any }) {
// ── Load video ──
const loadVideo = useCallback((url: string) => {
destroyPlayer();
setPlayerError(null);
const parsed = parseVideoUrl(url);
if (!parsed) return;
@ -220,6 +223,26 @@ export default function WatchTogetherTab({ data }: { data: any }) {
}
}
},
onError: (ev: any) => {
const code = ev.data;
// 101 or 150 = embedding disabled by video owner
// 100 = video not found
// 5 = HTML5 player error
if (code === 101 || code === 150) {
setPlayerError('Embedding deaktiviert \u2014 Video kann nur auf YouTube angesehen werden.');
} else if (code === 100) {
setPlayerError('Video nicht gefunden oder entfernt.');
} else {
setPlayerError('Wiedergabefehler \u2014 Video wird \u00FCbersprungen.');
}
// Auto-skip after 3 seconds if host
setTimeout(() => {
const room = currentRoomRef.current;
if (room && clientIdRef.current === room.hostId) {
wsSend({ type: 'skip' });
}
}, 3000);
},
},
});
} else {
@ -456,10 +479,23 @@ export default function WatchTogetherTab({ data }: { data: any }) {
}, [wsSend, destroyPlayer]);
// ── Add to queue ──
const addToQueue = useCallback(() => {
if (!queueUrl.trim()) return;
wsSend({ type: 'add_to_queue', url: queueUrl.trim() });
const addToQueue = useCallback(async () => {
const url = queueUrl.trim();
if (!url) return;
setQueueUrl('');
setAddingToQueue(true);
let title = '';
try {
const resp = await fetch(`/api/watch-together/video-info?url=${encodeURIComponent(url)}`);
if (resp.ok) {
const data = await resp.json();
title = data.title || '';
}
} catch { /* use URL as fallback */ }
wsSend({ type: 'add_to_queue', url, title: title || undefined });
setAddingToQueue(false);
}, [queueUrl, wsSend]);
// ── Remove from queue ──
@ -592,6 +628,23 @@ export default function WatchTogetherTab({ data }: { data: any }) {
style={currentVideoTypeRef.current === 'direct' ? {} : { display: 'none' }}
playsInline
/>
{playerError && (
<div className="wt-player-error">
<div className="wt-error-icon">{'\u26A0\uFE0F'}</div>
<p>{playerError}</p>
{currentRoom.currentVideo?.url && (
<a
className="wt-yt-link"
href={currentRoom.currentVideo.url}
target="_blank"
rel="noopener noreferrer"
>
Auf YouTube &ouml;ffnen &#8599;
</a>
)}
<p className="wt-skip-info">Wird in 3 Sekunden &uuml;bersprungen...</p>
</div>
)}
{!currentRoom.currentVideo && (
<div className="wt-player-placeholder">
<div className="wt-placeholder-icon">{'\uD83C\uDFAC'}</div>
@ -653,7 +706,9 @@ export default function WatchTogetherTab({ data }: { data: any }) {
currentRoom.queue.map((item, i) => (
<div
key={i}
className={`wt-queue-item${currentRoom.currentVideo?.url === item.url ? ' playing' : ''}`}
className={`wt-queue-item${currentRoom.currentVideo?.url === item.url ? ' playing' : ''}${isHost ? ' clickable' : ''}`}
onClick={() => isHost && wsSend({ type: 'play_video', index: i })}
title={isHost ? 'Klicken zum Abspielen' : undefined}
>
<div className="wt-queue-item-info">
<div className="wt-queue-item-title">{item.title || item.url}</div>
@ -676,7 +731,9 @@ export default function WatchTogetherTab({ data }: { data: any }) {
onChange={e => setQueueUrl(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') addToQueue(); }}
/>
<button className="wt-btn wt-queue-add-btn" onClick={addToQueue}>Hinzufuegen</button>
<button className="wt-btn wt-queue-add-btn" onClick={addToQueue} disabled={addingToQueue}>
{addingToQueue ? 'Laden...' : 'Hinzufuegen'}
</button>
</div>
</div>
</div>