functionally complete

This commit is contained in:
J Taylor 2021-06-24 17:50:44 +01:00
parent fb3f94f36b
commit 2785991fe2
15 changed files with 183 additions and 137 deletions

View File

@ -23,10 +23,12 @@ export const Intro = ({
onJoin, onJoin,
title, title,
fetching = false, fetching = false,
forceFetchToken = false,
forceOwner = false,
}) => { }) => {
const [roomName, setRoomName] = useState(); const [roomName, setRoomName] = useState();
const [owner, setOwner] = useState(false); const [fetchToken, setFetchToken] = useState(forceFetchToken);
const [fetchToken, setFetchToken] = useState(false); const [owner, setOwner] = useState(forceOwner);
useEffect(() => { useEffect(() => {
setRoomName(room); setRoomName(room);
@ -51,10 +53,12 @@ export const Intro = ({
required required
/> />
</Field> </Field>
<Field label="Fetch meeting token"> {!forceFetchToken && (
<BooleanInput onChange={(e) => setFetchToken(e.target.checked)} /> <Field label="Fetch meeting token">
</Field> <BooleanInput onChange={(e) => setFetchToken(e.target.checked)} />
{fetchToken && ( </Field>
)}
{fetchToken && !forceOwner && (
<Field label="Join as owner"> <Field label="Join as owner">
<BooleanInput onChange={(e) => setOwner(e.target.checked)} /> <BooleanInput onChange={(e) => setOwner(e.target.checked)} />
</Field> </Field>
@ -79,6 +83,8 @@ Intro.propTypes = {
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
onJoin: PropTypes.func.isRequired, onJoin: PropTypes.func.isRequired,
fetching: PropTypes.bool, fetching: PropTypes.bool,
forceFetchToken: PropTypes.bool,
forceOwner: PropTypes.bool,
}; };
export default Intro; export default Intro;

View File

@ -1,8 +1,10 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
export const Header = () => { export const Header = () => {
const { participantCount } = useParticipants(); const { participantCount } = useParticipants();
const { customCapsule } = useUIState();
return useMemo( return useMemo(
() => ( () => (
@ -14,6 +16,12 @@ export const Header = () => {
participantCount > 1 ? 'participants' : 'participant' participantCount > 1 ? 'participants' : 'participant'
}`} }`}
</div> </div>
{customCapsule && (
<div className={`capsule ${customCapsule.variant}`}>
{customCapsule.variant === 'recording' && <span />}
{customCapsule.label}
</div>
)}
<style jsx>{` <style jsx>{`
.room-header { .room-header {
@ -31,6 +39,9 @@ export const Header = () => {
} }
.capsule { .capsule {
display: flex;
align-items: center;
gap: var(--spacing-xxxs);
background-color: var(--blue-dark); background-color: var(--blue-dark);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: var(--spacing-xxs) var(--spacing-xs); padding: var(--spacing-xxs) var(--spacing-xs);
@ -39,10 +50,35 @@ export const Header = () => {
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
user-select: none; 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>
), ),
[participantCount] [participantCount, customCapsule]
); );
}; };

View File

@ -21,6 +21,8 @@ export default function Index({
domain, domain,
isConfigured = false, isConfigured = false,
predefinedRoom = false, predefinedRoom = false,
forceFetchToken = false,
forceOwner = false,
asides, asides,
modals, modals,
customTrayComponent, customTrayComponent,
@ -74,6 +76,8 @@ export default function Index({
<NotConfigured /> <NotConfigured />
) : ( ) : (
<Intro <Intro
forceFetchToken
forceOwner
title={process.env.PROJECT_TITLE} title={process.env.PROJECT_TITLE}
room={roomName} room={roomName}
error={tokenError} error={tokenError}
@ -128,6 +132,8 @@ Index.propTypes = {
modals: PropTypes.arrayOf(PropTypes.func), modals: PropTypes.arrayOf(PropTypes.func),
customTrayComponent: PropTypes.node, customTrayComponent: PropTypes.node,
customAppComponent: PropTypes.node, customAppComponent: PropTypes.node,
forceFetchToken: PropTypes.bool,
forceOwner: PropTypes.bool,
}; };
export async function getStaticProps() { export async function getStaticProps() {

View File

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import App from '@dailyjs/basic-call/components/App'; import App from '@dailyjs/basic-call/components/App';
import { ChatProvider } from '../../contexts/ChatProvider'; import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
// Extend our basic call app component with the chat context // Extend our basic call app component with the live streaming context
export const AppWithChat = () => ( export const AppWithLiveStreaming = () => (
<ChatProvider> <LiveStreamingProvider>
<App /> <App />
</ChatProvider> </LiveStreamingProvider>
); );
export default AppWithChat; export default AppWithLiveStreaming;

View File

@ -1 +1 @@
export { AppWithChat as default } from './App'; export { AppWithLiveStreaming as default } from './App';

View File

@ -1,25 +1,49 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button } from '@dailyjs/shared/components/Button'; import { Button } from '@dailyjs/shared/components/Button';
import Field from '@dailyjs/shared/components/Field'; import Field from '@dailyjs/shared/components/Field';
import { TextInput } from '@dailyjs/shared/components/Input'; import { TextInput } from '@dailyjs/shared/components/Input';
import Modal from '@dailyjs/shared/components/Modal'; import Modal from '@dailyjs/shared/components/Modal';
import { Well } from '@dailyjs/shared/components/Well';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
export const LIVE_STREAMING_MODAL = 'live-streaming'; export const LIVE_STREAMING_MODAL = 'live-streaming';
export const LiveStreamingModal = () => { export const LiveStreamingModal = () => {
const { callObject } = useCallState(); const { callObject } = useCallState();
const { currentModals, closeModal } = useUIState(); const { currentModals, closeModal } = useUIState();
const { isStreaming, streamError } = useLiveStreaming();
const [pending, setPending] = useState(false);
const [rtmpUrl, setRtmpUrl] = useState(''); const [rtmpUrl, setRtmpUrl] = useState('');
useEffect(() => {
// Reset pending state whenever stream state changes
setPending(false);
}, [isStreaming]);
function startLiveStream() {
setPending(true);
callObject.startLiveStreaming({ rtmpUrl });
}
function stopLiveStreaming() {
setPending(true);
callObject.stopLiveStreaming();
}
return ( return (
<Modal <Modal
title="Live stream" title="Live stream"
isOpen={currentModals[LIVE_STREAMING_MODAL]} isOpen={currentModals[LIVE_STREAMING_MODAL]}
onClose={() => closeModal(LIVE_STREAMING_MODAL)} onClose={() => closeModal(LIVE_STREAMING_MODAL)}
> >
<Field label="Enter room to join"> {streamError && (
<Well variant="error">
Unable to start stream. Error message: {streamError}
</Well>
)}
<Field label="Enter RTMP endpoint">
<TextInput <TextInput
type="text" type="text"
placeholder="RTMP URL" placeholder="RTMP URL"
@ -27,15 +51,18 @@ export const LiveStreamingModal = () => {
onChange={(e) => setRtmpUrl(e.target.value)} onChange={(e) => setRtmpUrl(e.target.value)}
/> />
</Field> </Field>
<Button {!isStreaming ? (
disabled={!rtmpUrl} <Button
onClick={() => callObject.startLiveStreaming({ rtmpUrl })} disabled={!rtmpUrl || pending}
> onClick={() => startLiveStream()}
Start live streaming >
</Button> {pending ? 'Starting stream...' : 'Start live streaming'}
<Button onClick={() => callObject.stopLiveStreaming()}> </Button>
Stop live streaming ) : (
</Button> <Button variant="warning" onClick={() => stopLiveStreaming()}>
Stop live streaming
</Button>
)}
</Modal> </Modal>
); );
}; };

View File

@ -4,15 +4,18 @@ import { TrayButton } from '@dailyjs/shared/components/Tray';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import { ReactComponent as IconStream } from '@dailyjs/shared/icons/streaming-md.svg'; import { ReactComponent as IconStream } from '@dailyjs/shared/icons/streaming-md.svg';
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
import { LIVE_STREAMING_MODAL } from '../LiveStreamingModal'; import { LIVE_STREAMING_MODAL } from '../LiveStreamingModal';
export const Tray = () => { export const Tray = () => {
const { openModal } = useUIState(); const { openModal } = useUIState();
const { isStreaming } = useLiveStreaming();
return ( return (
<> <>
<TrayButton <TrayButton
label="Stream" label={isStreaming ? 'Live' : 'Stream'}
orange={isStreaming}
onClick={() => openModal(LIVE_STREAMING_MODAL)} onClick={() => openModal(LIVE_STREAMING_MODAL)}
> >
<IconStream /> <IconStream />

View File

@ -1,90 +0,0 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { nanoid } from 'nanoid';
import PropTypes from 'prop-types';
export const ChatContext = createContext();
export const ChatProvider = ({ children }) => {
const { callObject } = useCallState();
const [chatHistory, setChatHistory] = useState([]);
const [hasNewMessages, setHasNewMessages] = useState(false);
const handleNewMessage = useCallback(
(e) => {
const participants = callObject.participants();
const sender = participants[e.fromId].user_name
? participants[e.fromId].user_name
: 'Guest';
setChatHistory((oldState) => [
...oldState,
{ sender, message: e.data.message, id: nanoid() },
]);
setHasNewMessages(true);
},
[callObject]
);
const sendMessage = useCallback(
(message) => {
if (!callObject) {
return false;
}
console.log('💬 Sending app message');
callObject.sendAppMessage({ message }, '*');
// Get the sender (local participant) name
const sender = callObject.participants().local.user_name
? callObject.participants().local.user_name
: 'Guest';
// Update local chat history
return setChatHistory((oldState) => [
...oldState,
{ sender, message, id: nanoid(), isLocal: true },
]);
},
[callObject]
);
useEffect(() => {
if (!callObject) {
return false;
}
console.log(`💬 Chat provider listening for app messages`);
callObject.on('app-message', handleNewMessage);
return () => callObject.off('app-message', handleNewMessage);
}, [callObject, handleNewMessage]);
return (
<ChatContext.Provider
value={{
sendMessage,
chatHistory,
hasNewMessages,
setHasNewMessages,
}}
>
{children}
</ChatContext.Provider>
);
};
ChatProvider.propTypes = {
children: PropTypes.node,
};
export const useChat = () => useContext(ChatContext);

View File

@ -0,0 +1,70 @@
import React, {
useState,
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import PropTypes from 'prop-types';
export const LiveStreamingContext = createContext();
export const LiveStreamingProvider = ({ children }) => {
const [isStreaming, setIsStreaming] = useState(false);
const [streamError, setStreamError] = useState();
const { setCustomCapsule } = useUIState();
const { callObject } = useCallState();
const handleStreamStarted = useCallback(() => {
console.log('📺 Live stream started');
setIsStreaming(true);
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
}, [setCustomCapsule]);
const handleStreamStopped = useCallback(() => {
console.log('📺 Live stream stopped');
setIsStreaming(false);
setCustomCapsule(null);
}, [setCustomCapsule]);
const handleStreamError = useCallback(
(e) => {
setIsStreaming(false);
setCustomCapsule(null);
setStreamError(e.errorMsg);
},
[setCustomCapsule]
);
useEffect(() => {
if (!callObject) {
return false;
}
console.log('📺 Live streaming provider listening for stream events');
callObject.on('live-streaming-started', handleStreamStarted);
callObject.on('live-streaming-stopped', handleStreamStopped);
callObject.on('live-streaming-error', handleStreamError);
return () => {
callObject.off('live-streaming-started', handleStreamStarted);
callObject.off('live-streaming-stopped', handleStreamStopped);
callObject.on('live-streaming-error', handleStreamError);
};
}, [callObject, handleStreamStarted, handleStreamStopped, handleStreamError]);
return (
<LiveStreamingContext.Provider value={{ isStreaming, streamError }}>
{children}
</LiveStreamingContext.Provider>
);
};
LiveStreamingProvider.propTypes = {
children: PropTypes.node,
};
export const useLiveStreaming = () => useContext(LiveStreamingContext);

View File

@ -1,19 +0,0 @@
import { useEffect, useMemo } from 'react';
import { useSound } from '@dailyjs/shared/hooks/useSound';
import { debounce } from 'debounce';
/**
* Convenience hook to play `join.mp3` when participants join the call
*/
export const useMessageSound = () => {
const { load, play } = useSound('message.mp3');
useEffect(() => {
load();
}, [load]);
return useMemo(() => debounce(() => play(), 5000, true), [play]);
};
export default useMessageSound;

View File

@ -1,12 +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 AppWithChat from '../components/App'; import AppWithLiveStreaming from '../components/App';
import { LiveStreamingModal } from '../components/LiveStreamingModal'; import { LiveStreamingModal } from '../components/LiveStreamingModal';
import Tray from '../components/Tray'; import Tray from '../components/Tray';
App.modals = [LiveStreamingModal]; App.modals = [LiveStreamingModal];
App.customAppComponent = <AppWithChat />; App.customAppComponent = <AppWithLiveStreaming />;
App.customTrayComponent = <Tray />; App.customTrayComponent = <Tray />;
export default App; export default App;

View File

@ -10,6 +10,8 @@ export async function getStaticProps() {
props: { props: {
domain: process.env.DAILY_DOMAIN || null, domain: process.env.DAILY_DOMAIN || null,
isConfigured, isConfigured,
forceFetchToken: true,
forceOwner: true,
}, },
}; };
} }

View File

@ -12,6 +12,7 @@ export const GlobalStyle = () => (
--primary-dark: ${theme.primary.dark}; --primary-dark: ${theme.primary.dark};
--secondary-default: ${theme.secondary.default}; --secondary-default: ${theme.secondary.default};
--secondary-dark: ${theme.secondary.dark}; --secondary-dark: ${theme.secondary.dark};
--secondary-light: ${theme.secondary.light};
--blue-light: ${theme.blue.light}; --blue-light: ${theme.blue.light};
--blue-default: ${theme.blue.default}; --blue-default: ${theme.blue.default};
--blue-dark: ${theme.blue.dark}; --blue-dark: ${theme.blue.dark};

View File

@ -12,6 +12,7 @@ export const UIStateProvider = ({
}) => { }) => {
const [showAside, setShowAside] = useState(); const [showAside, setShowAside] = useState();
const [activeModals, setActiveModals] = useState({}); const [activeModals, setActiveModals] = useState({});
const [customCapsule, setCustomCapsule] = useState();
const openModal = useCallback((modalName) => { const openModal = useCallback((modalName) => {
setActiveModals((prevState) => ({ setActiveModals((prevState) => ({
@ -45,6 +46,8 @@ export const UIStateProvider = ({
toggleAside, toggleAside,
showAside, showAside,
setShowAside, setShowAside,
customCapsule,
setCustomCapsule,
}} }}
> >
{children} {children}

View File

@ -10,6 +10,7 @@ export const defaultTheme = {
secondary: { secondary: {
default: '#FF9254', default: '#FF9254',
dark: '#FB651E', dark: '#FB651E',
light: '#FF9254',
}, },
blue: { blue: {