added recording and streaming

This commit is contained in:
Jon 2021-07-20 15:10:48 +01:00
parent 7923cc7856
commit 7723d0c86c
22 changed files with 223 additions and 70 deletions

View File

@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import HeaderCapsule from '@dailyjs/shared/components/HeaderCapsule';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
@ -10,17 +11,18 @@ export const Header = () => {
() => ( () => (
<header className="room-header"> <header className="room-header">
<img src="assets/daily-logo.svg" alt="Daily" className="logo" /> <img src="assets/daily-logo.svg" alt="Daily" className="logo" />
<div className="capsule">Basic call demo</div>
<div className="capsule"> <HeaderCapsule>Basic call demo</HeaderCapsule>
<HeaderCapsule>
{`${participantCount} ${ {`${participantCount} ${
participantCount === 1 ? 'participant' : 'participants' participantCount === 1 ? 'participant' : 'participants'
}`} }`}
</div> </HeaderCapsule>
{customCapsule && ( {customCapsule && (
<div className={`capsule ${customCapsule.variant}`}> <HeaderCapsule variant={customCapsule.variant}>
{customCapsule.variant === 'recording' && <span />} {customCapsule.variant === 'recording' && <span />}
{customCapsule.label} {customCapsule.label}
</div> </HeaderCapsule>
)} )}
<style jsx>{` <style jsx>{`
@ -37,44 +39,6 @@ export const Header = () => {
.logo { .logo {
margin-right: var(--spacing-xs); margin-right: var(--spacing-xs);
} }
.capsule {
display: flex;
align-items: center;
gap: var(--spacing-xxxs);
background-color: var(--blue-dark);
border-radius: var(--radius-sm);
padding: var(--spacing-xxs) var(--spacing-xs);
box-sizing: border-box;
line-height: 1;
font-weight: var(--weight-medium);
user-select: none;
}
.capsule.recording {
background: var(--secondary-default);
}
.capsule.recording span {
display: block;
width: 12px;
height: 12px;
background: white;
border-radius: 12px;
animation: capsulePulse 2s infinite linear;
}
@keyframes capsulePulse {
0% {
opacity: 1;
}
50% {
opacity: 0.25;
}
100% {
opacity: 1;
}
}
`}</style> `}</style>
</header> </header>
), ),

View File

@ -2,7 +2,7 @@ import React from 'react';
import { VideoGrid } from '../VideoGrid'; import { VideoGrid } from '../VideoGrid';
import { Header } from './Header'; import { Header } from './Header';
import RoomContainer from './RoomContainer'; import { RoomContainer } from './RoomContainer';
export const Room = () => ( export const Room = () => (
<RoomContainer> <RoomContainer>

View File

@ -7,8 +7,7 @@ import PropTypes from 'prop-types';
import WaitingRoom from '../WaitingRoom'; import WaitingRoom from '../WaitingRoom';
export const RoomContainer = ({ children }) => { export const RoomContainer = ({ children }) => {
const { localParticipant } = useParticipants(); const { isOwner } = useParticipants();
const isOwner = !!localParticipant?.isOwner;
useJoinSound(); useJoinSound();

View File

@ -16,6 +16,7 @@ export default async function handler(req, res) {
exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes
eject_at_room_exp: true, eject_at_room_exp: true,
enable_knocking: privacy !== 'public', enable_knocking: privacy !== 'public',
enable_recording: 'local',
}, },
}), }),
}; };

View File

@ -94,8 +94,8 @@ export const FlyingEmojisOverlay = () => {
position: fixed; position: fixed;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
left: 24px; left: 0px;
right: 24px; right: 0px;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;

View File

@ -2,6 +2,8 @@ import React from 'react';
import App from '@dailyjs/basic-call/components/App'; import App from '@dailyjs/basic-call/components/App';
import FlyingEmojiOverlay from '@dailyjs/flying-emojis/components/FlyingEmojis'; import FlyingEmojiOverlay from '@dailyjs/flying-emojis/components/FlyingEmojis';
import { LiveStreamingProvider } from '@dailyjs/live-streaming/contexts/LiveStreamingProvider';
import { RecordingProvider } from '@dailyjs/recording/contexts/RecordingProvider';
import { ChatProvider } from '@dailyjs/text-chat/contexts/ChatProvider'; import { ChatProvider } from '@dailyjs/text-chat/contexts/ChatProvider';
import Room from '../Room'; import Room from '../Room';
@ -10,16 +12,18 @@ import Room from '../Room';
* as the layout logic changes considerably for the basic demo * as the layout logic changes considerably for the basic demo
*/ */
export const LiveFitnessApp = () => ( export const LiveFitnessApp = () => (
<> <ChatProvider>
<ChatProvider> <LiveStreamingProvider>
<FlyingEmojiOverlay /> <RecordingProvider>
<App <FlyingEmojiOverlay />
customComponentForState={{ <App
room: () => <Room />, customComponentForState={{
}} room: () => <Room />,
/> }}
</ChatProvider> />
</> </RecordingProvider>
</LiveStreamingProvider>
</ChatProvider>
); );
export default LiveFitnessApp; export default LiveFitnessApp;

View File

@ -0,0 +1,44 @@
import React, { useMemo } from 'react';
import Button from '@dailyjs/shared/components/Button';
import HeaderCapsule from '@dailyjs/shared/components/HeaderCapsule';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
export const Header = () => {
const { participantCount } = useParticipants();
return useMemo(
() => (
<header className="room-header">
<img src="assets/daily-logo.svg" alt="Daily" className="logo" />
<HeaderCapsule variant="button">
{`${participantCount} ${
participantCount === 1 ? 'participant' : 'participants'
}`}
<Button size="tiny" variant="outline-dark">
Invite
</Button>
</HeaderCapsule>
<style jsx>{`
.room-header {
display: flex;
flex: 0 0 auto;
column-gap: var(--spacing-xxs);
box-sizing: border-box;
padding: var(--spacing-sm);
align-items: center;
width: 100%;
}
.logo {
margin-right: var(--spacing-xs);
}
`}</style>
</header>
),
[participantCount]
);
};
export default Header;

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import RoomContainer from '@dailyjs/basic-call/components/Room/RoomContainer'; import { RoomContainer } from '@dailyjs/basic-call/components/Room/RoomContainer';
import { Header } from './Header';
export const Room = () => ( export const Room = () => (
<RoomContainer> <RoomContainer>
<Header />
<main>Hello</main> <main>Hello</main>
</RoomContainer> </RoomContainer>
); );

View File

@ -1,12 +1,30 @@
import React from 'react'; import React from 'react';
import FlyingEmojiTrayButton from '@dailyjs/flying-emojis/components/Tray'; import FlyingEmojiTrayButton from '@dailyjs/flying-emojis/components/Tray';
import LiveStreamingButton from '@dailyjs/live-streaming/components/Tray';
import RecordingButton from '@dailyjs/recording/components/Tray';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import ChatTrayButton from '@dailyjs/text-chat/components/Tray'; import ChatTrayButton from '@dailyjs/text-chat/components/Tray';
export const Tray = () => ( export const Tray = () => {
<> const { isOwner } = useParticipants();
<FlyingEmojiTrayButton />
<ChatTrayButton /> if (isOwner) {
</> return (
); <>
<FlyingEmojiTrayButton />
<ChatTrayButton />
<LiveStreamingButton />
<RecordingButton />
</>
);
}
return (
<>
<FlyingEmojiTrayButton />
<ChatTrayButton />
</>
);
};
export default Tray; export default Tray;

View File

@ -4,6 +4,8 @@ const withTM = require('next-transpile-modules')([
'@dailyjs/basic-call', '@dailyjs/basic-call',
'@dailyjs/flying-emojis', '@dailyjs/flying-emojis',
'@dailyjs/text-chat', '@dailyjs/text-chat',
'@dailyjs/live-streaming',
'@dailyjs/recording',
]); ]);
const packageJson = require('./package.json'); const packageJson = require('./package.json');

View File

@ -14,6 +14,8 @@
"@dailyjs/basic-call": "*", "@dailyjs/basic-call": "*",
"@dailyjs/flying-emojis": "*", "@dailyjs/flying-emojis": "*",
"@dailyjs/text-chat": "*", "@dailyjs/text-chat": "*",
"@dailyjs/live-streaming": "*",
"@dailyjs/recording": "*",
"next": "^11.0.0", "next": "^11.0.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"react": "^17.0.2", "react": "^17.0.2",

View File

@ -20,6 +20,7 @@ export default function Room({
domain, domain,
customTrayComponent, customTrayComponent,
asides, asides,
modals,
}) { }) {
const [token, setToken] = useState(); const [token, setToken] = useState();
const [tokenError, setTokenError] = useState(); const [tokenError, setTokenError] = useState();
@ -78,7 +79,11 @@ export default function Room({
* Main call UI * Main call UI
*/ */
return ( return (
<UIStateProvider customTrayComponent={customTrayComponent} asides={asides}> <UIStateProvider
customTrayComponent={customTrayComponent}
asides={asides}
modals={modals}
>
<CallProvider domain={domain} room={room} token={token}> <CallProvider domain={domain} room={room} token={token}>
<ParticipantsProvider> <ParticipantsProvider>
<TracksProvider> <TracksProvider>
@ -100,6 +105,7 @@ Room.propTypes = {
instructor: PropTypes.bool, instructor: PropTypes.bool,
customTrayComponent: PropTypes.node, customTrayComponent: PropTypes.node,
asides: PropTypes.arrayOf(PropTypes.func), asides: PropTypes.arrayOf(PropTypes.func),
modals: PropTypes.arrayOf(PropTypes.func),
}; };
export async function getServerSideProps(context) { export async function getServerSideProps(context) {

View File

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import App from '@dailyjs/basic-call/pages/_app'; import App from '@dailyjs/basic-call/pages/_app';
import LiveStreamingModal from '@dailyjs/live-streaming/components/LiveStreamingModal';
import RecordingModal from '@dailyjs/recording/components/RecordingModal';
import ChatAside from '@dailyjs/text-chat/components/ChatAside'; import ChatAside from '@dailyjs/text-chat/components/ChatAside';
import Tray from '../components/Tray'; import Tray from '../components/Tray';
App.customTrayComponent = <Tray />; App.customTrayComponent = <Tray />;
App.asides = [ChatAside]; App.asides = [ChatAside];
App.modals = [LiveStreamingModal, RecordingModal];
export default App; export default App;

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile

View File

@ -195,7 +195,7 @@ export const Button = forwardRef(
} }
.button.tiny { .button.tiny {
height: 32px; height: 28px;
font-size: 11px; font-size: 11px;
border-radius: var(--radius-xs); border-radius: var(--radius-xs);
text-transform: uppercase; text-transform: uppercase;
@ -320,6 +320,22 @@ export const Button = forwardRef(
box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.05);
} }
.button.outline-dark {
background: transparent;
border: 1px solid var(--blue-light);
color: var(--text-light);
}
.button.outline-dark:hover,
.button.outline-dark:focus,
.button.outline-dark:active {
border: 1px solid var(--primary-default);
box-shadow: none;
}
.button.outline-dark:focus {
box-shadow: 0 0 0px 3px ${hexa(theme.primary.default, 0.35)};
}
.button.muted { .button.muted {
color: var(--red-default); color: var(--red-default);
} }

View File

@ -11,12 +11,19 @@ export const ExpiryTimer = ({ expiry }) => {
} }
const i = setInterval(() => { const i = setInterval(() => {
const timeLeft = Math.round((expiry - Date.now()) / 1000); const timeLeft = Math.round((expiry - Date.now()) / 1000);
if (timeLeft < 0) {
return setSecs(null);
}
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`); setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
}, 1000); }, 1000);
return () => clearInterval(i); return () => clearInterval(i);
}, [expiry]); }, [expiry]);
if (!secs) {
return null;
}
return ( return (
<div className="countdown"> <div className="countdown">
{secs} {secs}

View File

@ -0,0 +1,65 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
export const HeaderCapsule = ({ children, variant }) => {
const cx = classNames('capsule', variant);
return (
<div className={cx}>
{children}
<style jsx>{`
.capsule {
display: flex;
align-items: center;
gap: var(--spacing-xxs);
background-color: var(--blue-dark);
border-radius: var(--radius-sm);
padding: var(--spacing-xxxxs) var(--spacing-xxs);
min-height: 40px;
box-sizing: border-box;
line-height: 1;
font-weight: var(--weight-medium);
user-select: none;
}
.capsule.button {
padding-right: var(--spacing-xxxxs);
}
.capsule.recording {
background: var(--secondary-default);
}
.capsule.recording span {
display: block;
width: 12px;
height: 12px;
background: white;
border-radius: 12px;
animation: capsulePulse 2s infinite linear;
}
@keyframes capsulePulse {
0% {
opacity: 1;
}
50% {
opacity: 0.25;
}
100% {
opacity: 1;
}
}
`}</style>
</div>
);
};
HeaderCapsule.propTypes = {
children: PropTypes.node,
variant: PropTypes.string,
};
export default HeaderCapsule;

View File

@ -0,0 +1 @@
export { HeaderCapsule as default } from './HeaderCapsule';

View File

@ -75,7 +75,10 @@ export const ParticipantsProvider = ({ children }) => {
[allParticipants] [allParticipants]
); );
const isOwner = useMemo(() => localParticipant?.isOwner, [localParticipant]); const isOwner = useMemo(
() => !!localParticipant?.isOwner,
[localParticipant]
);
/** /**
* The participant who should be rendered prominently right now * The participant who should be rendered prominently right now

View File

@ -22,6 +22,10 @@ export const UIStateProvider = ({
}, []); }, []);
const closeModal = useCallback((modalName) => { const closeModal = useCallback((modalName) => {
if (!modalName) {
setActiveModals({});
}
setActiveModals((prevState) => ({ setActiveModals((prevState) => ({
...prevState, ...prevState,
[modalName]: false, [modalName]: false,
@ -34,6 +38,10 @@ export const UIStateProvider = ({
setShowAside((p) => (p === newAside ? null : newAside)); setShowAside((p) => (p === newAside ? null : newAside));
}, []); }, []);
const closeAside = useCallback(() => {
setShowAside(null);
}, []);
return ( return (
<UIStateContext.Provider <UIStateContext.Provider
value={{ value={{
@ -42,6 +50,7 @@ export const UIStateProvider = ({
customTrayComponent, customTrayComponent,
openModal, openModal,
closeModal, closeModal,
closeAside,
currentModals, currentModals,
toggleAside, toggleAside,
showAside, showAside,
@ -57,7 +66,6 @@ export const UIStateProvider = ({
UIStateProvider.propTypes = { UIStateProvider.propTypes = {
children: PropTypes.node, children: PropTypes.node,
demoMode: PropTypes.bool,
asides: PropTypes.arrayOf(PropTypes.func), asides: PropTypes.arrayOf(PropTypes.func),
modals: PropTypes.arrayOf(PropTypes.func), modals: PropTypes.arrayOf(PropTypes.func),
customTrayComponent: PropTypes.node, customTrayComponent: PropTypes.node,

View File

@ -15,6 +15,7 @@ import {
} from '@dailyjs/shared/contexts/useCallMachine'; } from '@dailyjs/shared/contexts/useCallMachine';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import HairCheck from '../components/HairCheck'; import HairCheck from '../components/HairCheck';
import { useUIState } from '../contexts/UIStateProvider';
export const useCallUI = ({ export const useCallUI = ({
state, state,
@ -25,6 +26,7 @@ export const useCallUI = ({
notFoundRedirect = 'not-found', notFoundRedirect = 'not-found',
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { closeAside, closeModal } = useUIState();
useEffect(() => { useEffect(() => {
console.log(`%c🔀 App state changed: ${state}`, `color: gray;`); console.log(`%c🔀 App state changed: ${state}`, `color: gray;`);
@ -36,6 +38,10 @@ export const useCallUI = ({
return <Loader />; return <Loader />;
} }
// Make sure we hide any active asides or modals when the state changes
closeAside();
closeModal();
// Update the UI based on the state of our call // Update the UI based on the state of our call
switch (state) { switch (state) {
case CALL_STATE_NOT_FOUND: case CALL_STATE_NOT_FOUND:
@ -80,7 +86,7 @@ export const useCallUI = ({
return callEnded ? ( return callEnded ? (
callEnded() callEnded()
) : ( ) : (
<MessageCard onBack={() => window.location.reload()}> <MessageCard>
You have left the call. We hope you had fun! You have left the call. We hope you had fun!
</MessageCard> </MessageCard>
); );