import React, { useEffect, useRef, useState } from 'react'; import { arrayUnion, db } from '../../services/firebase'; import { SYNC_MARGIN } from '../../services/utilities'; import GetExtension from './GetExtension'; type SubscriptionFrameProps = { roomId: string; ownerId: string; userId: string; videoUrl: string; }; const SubscriptionFrame: React.FC = ({ ownerId, userId, roomId, videoUrl }) => { const frameRef = useRef(null); const [playerReady, setPlayerReady] = useState(false); const [hasExtension, setHasExtension] = useState(false); const getExtensionVersion = () => { return new Promise((resolve, reject) => { const channel = new MessageChannel(); channel.port1.onmessage = ({ data }) => { channel.port1.close(); if (data.error) { reject(false); } else { resolve(true); } }; window.postMessage('get extension version', '*', [channel.port2]); }); }; // Validate browser extension version useEffect(() => { const checkExtension = async () => { const val = await getExtensionVersion(); setHasExtension(val); }; checkExtension(); }, []); // Listen for events from browser extension (owner only) useEffect(() => { if (ownerId === userId) { const handleMessage = (e: MessageEvent) => { const type = e.data.type; // Send play/pause events to Firebase if (type === 'play' || type === 'pause') { db.collection('states') .doc(roomId) .update({ isPlaying: type === 'play', time: e.data.time, }); db.collection('rooms') .doc(roomId) .update({ requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: e.data.time, type: type }), }); } }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; } }, [roomId, ownerId, userId]); // Request current state of video from browser extension const getCurrentStatus = () => { return new Promise<{ isPlaying: boolean; time: number }>((resolve, reject) => { const channel = new MessageChannel(); channel.port1.onmessage = ({ data }) => { channel.port1.close(); if (data.error) { reject(data.error); } else { resolve({ isPlaying: data.isPlaying, time: data.time }); } }; frameRef.current?.contentWindow?.postMessage('fetch current status', '*', [channel.port2]); }); }; // Listen for updateState requests from Firebase (owner only) useEffect(() => { if (ownerId === userId) { const roomRef = db.collection('rooms').doc(roomId); const stateRef = db.collection('states').doc(roomId); // Add a listener to 'rooms' collection, listening for updateState requests const roomUnsubscribe = roomRef.onSnapshot(async (docSnapshot) => { const requests = docSnapshot.data()?.requests; const req = requests[requests.length - 1]; if (!!req && req.type === 'updateState' && req.senderId !== userId) { const status = await getCurrentStatus(); stateRef.update({ time: status.time, isPlaying: status.isPlaying, }); } }); return () => { roomUnsubscribe(); }; } }, [ownerId, roomId, userId]); // Listen for browser extension events (member only) useEffect(() => { if (ownerId !== userId) { const handleMessage = (e: MessageEvent) => { const event = e.data.toString(); if (event === 'video ready') { setPlayerReady(true); } if (event === 'video not ready') { setPlayerReady(false); } }; // Listen for current video state updates from browser extension window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; } }, [ownerId, userId, videoUrl]); // Listen for changes in video state from Firebase (member only) useEffect(() => { if (ownerId !== userId && playerReady) { const stateRef = db.collection('states').doc(roomId); const roomRef = db.collection('rooms').doc(roomId); let allowUpdate = true; // Send player seeking event to browser extension const seekTo = (time: number) => { frameRef.current?.contentWindow?.postMessage({ type: 'seek', currentTime: time }, '*'); }; // Send player set event to browser extension const setPlaying = (isPlaying: boolean) => { frameRef.current?.contentWindow?.postMessage({ type: 'playing', playing: isPlaying }, '*'); }; // Listen to 'states' collection for video state changes from owner const stateUnsubscribe = stateRef.onSnapshot(async (docSnapshot) => { const actual = docSnapshot.data(); if (allowUpdate) { const status = await getCurrentStatus(); console.log('status:', status); setPlaying(actual?.isPlaying); if (Math.abs(status.time - actual?.time) > SYNC_MARGIN / 1000 && actual?.isPlaying) { allowUpdate = false; setTimeout(() => { // Throttle update requests allowUpdate = true; }, 5000); seekTo(actual?.time); roomRef.update({ requests: arrayUnion({ createdAt: Date.now(), senderId: userId, data: 0, type: 'updateState' }), }); } } }); return () => { stateUnsubscribe(); }; } }, [playerReady, roomId, ownerId, userId]); return hasExtension ? ( ) : ( ); }; export default SubscriptionFrame;