mirror of https://github.com/shuang854/Turtle
integrated netflix syncing into app
parent
60aa771572
commit
4e57fa3090
@ -0,0 +1,170 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { arrayUnion, db } from '../../services/firebase';
|
||||
import { SYNC_MARGIN } from '../../services/utilities';
|
||||
|
||||
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);
|
||||
|
||||
// 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, time: 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]);
|
||||
});
|
||||
};
|
||||
|
||||
// Subscribe listener 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, time: 0, type: 'updateState' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
stateUnsubscribe();
|
||||
};
|
||||
}
|
||||
}, [playerReady, roomId, ownerId, userId]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionFrame;
|
||||
@ -0,0 +1,45 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { db } from '../../services/firebase';
|
||||
import { matchUrl } from '../../services/utilities';
|
||||
import ReactPlayerFrame from './ReactPlayerFrame';
|
||||
import SubscriptionFrame from './SubscriptionFrame';
|
||||
|
||||
type VideoPlayerProps = {
|
||||
ownerId: string;
|
||||
userId: string;
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({ roomId, ownerId, userId }) => {
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
|
||||
// Listen for video URL changes
|
||||
useEffect(() => {
|
||||
const playlistRef = db.collection('playlists').doc(roomId);
|
||||
const playlistUnsubscribe = playlistRef.onSnapshot((docSnapshot) => {
|
||||
const data = docSnapshot.data();
|
||||
if (data !== undefined) {
|
||||
let url: string = data.url;
|
||||
if (matchUrl(url) === 'NETFLIX') {
|
||||
// Remove query from url
|
||||
if (url.indexOf('?') > 0) {
|
||||
url = url.substring(0, url.indexOf('?'));
|
||||
}
|
||||
}
|
||||
setVideoUrl(url);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
playlistUnsubscribe();
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
return matchUrl(videoUrl) === 'NETFLIX' ? (
|
||||
<SubscriptionFrame roomId={roomId} ownerId={ownerId} userId={userId} videoUrl={videoUrl}></SubscriptionFrame>
|
||||
) : (
|
||||
<ReactPlayerFrame roomId={roomId} ownerId={ownerId} userId={userId} videoUrl={videoUrl}></ReactPlayerFrame>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayer;
|
||||
Loading…
Reference in New Issue