integrated netflix syncing into app

optimize-reads
Simon Huang 5 years ago
parent 60aa771572
commit 4e57fa3090

@ -26,7 +26,6 @@ chrome.webRequest.onHeadersReceived.addListener(
header.value += '\n';
}
}
//console.log(header.value);
headers[index].value = header.value;
}

@ -4,11 +4,13 @@ const NETFLIX_VID_URL = /https?:\/\/www\.netflix\.com\/watch\/([-a-zA-Z0-9()@:%_
document.addEventListener('click', (e) => {
if (!playerReady && window.location.toString().match(NETFLIX_VID_URL)) {
let s = document.createElement('script');
s.setAttribute('id', 'netflix');
s.src = chrome.runtime.getURL('netflix.js');
(document.head || document.documentElement).appendChild(s);
playerReady = true;
} else if (playerReady && !window.location.toString().match(NETFLIX_VID_URL)) {
playerReady = false;
window.parent.postMessage('video not ready', '*');
document.getElementById('netflix').remove();
}
});

@ -1,19 +1,18 @@
import React, { useEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import { db, arrayUnion } from '../services/firebase';
import { SYNC_MARGIN } from '../services/utilities';
import { arrayUnion, db } from '../../services/firebase';
import { SYNC_MARGIN } from '../../services/utilities';
type VideoPlayerProps = {
type ReactPlayerFrameProps = {
ownerId: string;
userId: string;
roomId: string;
videoUrl: string;
};
const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) => {
const ReactPlayerFrame: React.FC<ReactPlayerFrameProps> = ({ ownerId, userId, roomId, videoUrl }) => {
const player = useRef<ReactPlayer>(null);
const [playing, setPlaying] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [allowUpdate, setAllowUpdate] = useState(true);
// Update database on play (owner only)
const onPlay = () => {
@ -55,6 +54,34 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
}
};
// Listen for 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((docSnapshot) => {
const requests = docSnapshot.data()?.requests;
const req = requests[requests.length - 1];
if (!!req && req.type === 'updateState' && req.senderId !== userId) {
const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) {
stateRef.update({
time: currTime,
isPlaying: player.current?.props.playing,
});
}
}
});
return () => {
roomUnsubscribe();
};
}
}, [ownerId, roomId, userId]);
// Request an update after buffering is finished (member only)
const onBufferEnd = () => {
if (ownerId !== userId) {
@ -66,13 +93,14 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
}
};
// Subscribe member only listener
// Listen for changes in video state from Firebase (member only)
useEffect(() => {
if (ownerId !== userId) {
const stateRef = db.collection('states').doc(roomId);
const roomRef = db.collection('rooms').doc(roomId);
let allowUpdate = true;
// Add a listener to 'states' collection, listening for video state changes
// listen to 'states' collection for video state changes from owner
const stateUnsubscribe = stateRef.onSnapshot((docSnapshot) => {
const docData = docSnapshot.data();
@ -83,11 +111,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
setPlaying(realPlayState);
if (allowUpdate && Math.abs(currTime - realTimeState) > SYNC_MARGIN / 1000 && realPlayState) {
setAllowUpdate(false);
allowUpdate = false;
setTimeout(() => {
// throttle update requests
setAllowUpdate(true);
}, 3000);
// Throttle update requests
allowUpdate = true;
}, 5000);
player.current?.seekTo(realTimeState);
roomRef.update({
@ -101,51 +129,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
stateUnsubscribe();
};
}
}, [ownerId, roomId, userId, allowUpdate]);
// Subscribe owner only listener
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((docSnapshot) => {
const requests = docSnapshot.data()?.requests;
const req = requests[requests.length - 1];
if (!!req && req.type === 'updateState' && req.senderId !== userId) {
const currTime = player.current?.getCurrentTime();
if (currTime !== undefined) {
stateRef.update({
time: currTime,
isPlaying: true,
});
}
}
});
return () => {
roomUnsubscribe();
};
}
}, [ownerId, roomId, userId]);
// 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) {
setVideoUrl(data.url);
}
});
return () => {
playlistUnsubscribe();
};
}, [roomId]);
return (
<ReactPlayer
ref={player}
@ -169,4 +154,4 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ ownerId, userId, roomId }) =>
);
};
export default VideoPlayer;
export default ReactPlayerFrame;

@ -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;

@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { RouteComponentProps, useHistory } from 'react-router';
import Frame from '../components/Frame';
import RoomHeader from '../components/RoomHeader';
import VideoPlayer from '../components/VideoPlayer';
import VideoPlayer from '../components/Player/VideoPlayer';
import { auth, db, decrement, increment, rtdb } from '../services/firebase';
import { generateAnonName } from '../services/utilities';
import './Room.css';
@ -84,10 +84,14 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
});
// Manage user count
rtdb.ref('.info/connected').on('value', (snapshot) => {
rtdb.ref('.info/connected').on('value', async (snapshot) => {
if (snapshot.val() === true) {
roomRef.update({ userCount: increment });
roomRef.onDisconnect().update({ userCount: decrement });
try {
await roomRef.update({ userCount: increment });
await roomRef.onDisconnect().update({ userCount: decrement });
} catch (err) {
console.log(err);
}
}
});
@ -129,7 +133,7 @@ const Room: React.FC<RouteComponentProps<{ roomId: string }>> = ({ match }) => {
<IonGrid class="room-grid">
<IonRow class="room-row">
<IonCol size="12" sizeLg="9" class="player-col">
<VideoPlayer ownerId={ownerId} userId={userId} roomId={roomId}></VideoPlayer>
<VideoPlayer roomId={roomId} ownerId={ownerId} userId={userId}></VideoPlayer>
</IonCol>
<IonCol size="12" sizeLg="3" class="frame-col">
<Frame ownerId={ownerId} roomId={roomId} userId={userId} userList={userList} joinTime={joinTime}></Frame>

@ -88,6 +88,16 @@ const animals = [
// MATCH_URL_VIDYARD: /vidyard.com\/(?:watch\/)?([a-zA-Z0-9-]+)/,
// };
const MATCH_URL_NETFLIX = /https?:\/\/(www\.)?netflix\.com\/watch\/([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
export const matchUrl = (url: string) => {
if (!!url.match(MATCH_URL_NETFLIX)) {
return 'NETFLIX';
} else {
return 'DEFAULT';
}
};
export const generateAnonName = (): string => {
const adj: string = adjectives[Math.floor(Math.random() * 40)];
const animal: string = animals[Math.floor(Math.random() * 30)];

Loading…
Cancel
Save