mirror of https://github.com/shuang854/Turtle
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
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<SubscriptionFrameProps> = ({ ownerId, userId, roomId, videoUrl }) => {
|
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
const [playerReady, setPlayerReady] = useState(false);
|
|
const [hasExtension, setHasExtension] = useState(false);
|
|
|
|
const getExtensionVersion = () => {
|
|
return new Promise<boolean>((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 ? (
|
|
<iframe
|
|
ref={frameRef}
|
|
src={videoUrl}
|
|
frameBorder="0"
|
|
title="Subscription Service"
|
|
allow="encrypted-media; fullscreen"
|
|
allow-scripts=""
|
|
sandbox="allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-presentation allow-scripts allow-same-origin allow-storage-access-by-user-activation"
|
|
height="100%"
|
|
width="100%"
|
|
></iframe>
|
|
) : (
|
|
<GetExtension></GetExtension>
|
|
);
|
|
};
|
|
|
|
export default SubscriptionFrame;
|