resolve conflicts
This commit is contained in:
commit
137afb1831
|
|
@ -10,7 +10,15 @@ Send messages to other participants using sendAppMessage
|
|||
|
||||
### [📺 Live streaming](./live-streaming)
|
||||
|
||||
Broadcast call to a custom RTMP endpoint using a variety of difference layout modes
|
||||
Broadcast call to a custom RTMP endpoint using a variety of different layout modes
|
||||
|
||||
### [⏺️ Recording](./recording)
|
||||
|
||||
Record a call video and audio using both cloud and local modes
|
||||
|
||||
### [🔥 Flying emojis](./flying-emojis)
|
||||
|
||||
Send emoji reactions to all clients using sendAppMessage
|
||||
|
||||
### [📃 Pagination](./pagination)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useCallUI } from '@dailyjs/shared/hooks/useCallUI';
|
||||
|
||||
|
|
@ -8,7 +8,20 @@ import { Asides } from './Asides';
|
|||
import { Modals } from './Modals';
|
||||
|
||||
export const App = ({ customComponentForState }) => {
|
||||
const { state } = useCallState();
|
||||
const { roomExp, state } = useCallState();
|
||||
const [secs, setSecs] = useState();
|
||||
|
||||
// If room has an expiry time, we'll calculate how many seconds until expiry
|
||||
useEffect(() => {
|
||||
if (!roomExp) {
|
||||
return false;
|
||||
}
|
||||
const i = setInterval(() => {
|
||||
const timeLeft = Math.round((roomExp - Date.now()) / 1000);
|
||||
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
|
||||
}, 1000);
|
||||
return () => clearInterval(i);
|
||||
}, [roomExp]);
|
||||
|
||||
const componentForState = useCallUI({
|
||||
state,
|
||||
|
|
@ -17,7 +30,7 @@ export const App = ({ customComponentForState }) => {
|
|||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
return useMemo(
|
||||
const memoizedApp = useMemo(
|
||||
() => (
|
||||
<div className="app">
|
||||
{componentForState()}
|
||||
|
|
@ -38,11 +51,32 @@ export const App = ({ customComponentForState }) => {
|
|||
),
|
||||
[componentForState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{roomExp && <div className="countdown">{secs}</div>} {memoizedApp}
|
||||
<style jsx>{`
|
||||
.countdown {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--weight-medium);
|
||||
border-radius: 0 0 0 var(--radius-sm);
|
||||
background: var(--blue-dark);
|
||||
color: white;
|
||||
z-index: 999;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
asides: PropTypes.arrayOf(PropTypes.func),
|
||||
customComponentsForState: PropTypes.any,
|
||||
customComponentForState: PropTypes.any,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardBody } from '@dailyjs/shared/components/Card';
|
||||
import Loader from '@dailyjs/shared/components/Loader';
|
||||
import { Well } from '@dailyjs/shared/components/Well';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const CreatingRoom = ({ onCreated }) => {
|
||||
const [room, setRoom] = useState();
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (room) return;
|
||||
|
||||
async function createRoom() {
|
||||
setError(false);
|
||||
setFetching(true);
|
||||
|
||||
console.log(`🚪 Creating new demo room...`);
|
||||
|
||||
// Create a room server side (using Next JS serverless)
|
||||
const res = await fetch('/api/createRoom', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const resJson = await res.json();
|
||||
|
||||
if (resJson.name) {
|
||||
setFetching(false);
|
||||
setRoom(resJson.name);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(resJson.error || 'An unknown error occured');
|
||||
setFetching(false);
|
||||
}
|
||||
|
||||
createRoom();
|
||||
}, [room]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!room || !onCreated) return;
|
||||
|
||||
console.log(`🚪 Room created: ${room}, joining now`);
|
||||
|
||||
onCreated(room, true);
|
||||
}, [room, onCreated]);
|
||||
|
||||
return (
|
||||
<div className="creating-room">
|
||||
{fetching && (
|
||||
<div className="creating">
|
||||
<Loader /> Creating new demo room...
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<Card error>
|
||||
<CardHeader>An error occured</CardHeader>
|
||||
<CardBody>
|
||||
<Well variant="error">{error}</Well>
|
||||
An error occured when trying to create a demo room. Please check
|
||||
that your environmental variables are correct and try again.
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.creating-room {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.creating-room .creating {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.creating-room :global(.loader) {
|
||||
margin-right: var(--spacing-xxxs);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CreatingRoom.propTypes = {
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CreatingRoom;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { CreatingRoom as default } from './CreatingRoom';
|
||||
export { CreatingRoom } from './CreatingRoom';
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
console.log(`Creating room on domain ${process.env.DAILY_DOMAIN}`);
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
properties: {
|
||||
exp: Math.round(Date.now() / 1000) + 5 * 60, // expire in 5 minutes
|
||||
eject_at_room_exp: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const dailyRes = await fetch(
|
||||
`${process.env.DAILY_REST_DOMAIN || 'https://api.daily.co/v1'}/rooms`,
|
||||
options
|
||||
);
|
||||
|
||||
const { name, url, error } = await dailyRes.json();
|
||||
|
||||
if (error) {
|
||||
return res.status(500).json({ error });
|
||||
}
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ name, url, domain: process.env.DAILY_DOMAIN });
|
||||
}
|
||||
|
||||
return res.status(500);
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import { TracksProvider } from '@dailyjs/shared/contexts/TracksProvider';
|
|||
import { UIStateProvider } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import { WaitingRoomProvider } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
||||
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import App from '../components/App';
|
||||
import { CreatingRoom } from '../components/CreatingRoom';
|
||||
import { Intro, NotConfigured } from '../components/Intro';
|
||||
|
||||
/**
|
||||
|
|
@ -26,6 +26,7 @@ export default function Index({
|
|||
forceFetchToken = false,
|
||||
forceOwner = false,
|
||||
subscribeToTracksAutomatically = true,
|
||||
demoMode = false,
|
||||
asides,
|
||||
modals,
|
||||
customTrayComponent,
|
||||
|
|
@ -75,22 +76,24 @@ export default function Index({
|
|||
if (!isReady) {
|
||||
return (
|
||||
<main>
|
||||
{!isConfigured ? (
|
||||
<NotConfigured />
|
||||
) : (
|
||||
<Intro
|
||||
forceFetchToken={forceFetchToken}
|
||||
forceOwner={forceOwner}
|
||||
title={process.env.PROJECT_TITLE}
|
||||
room={roomName}
|
||||
error={tokenError}
|
||||
fetching={fetchingToken}
|
||||
domain={domain}
|
||||
onJoin={(room, isOwner, fetchToken) =>
|
||||
fetchToken ? getMeetingToken(room, isOwner) : setRoomName(room)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
if (!isConfigured) return <NotConfigured />;
|
||||
if (demoMode) return <CreatingRoom onCreated={getMeetingToken} />;
|
||||
return (
|
||||
<Intro
|
||||
forceFetchToken={forceFetchToken}
|
||||
forceOwner={forceOwner}
|
||||
title={process.env.PROJECT_TITLE}
|
||||
room={roomName}
|
||||
error={tokenError}
|
||||
fetching={fetchingToken}
|
||||
domain={domain}
|
||||
onJoin={(room, isOwner, fetchToken) =>
|
||||
fetchToken ? getMeetingToken(room, isOwner) : setRoomName(room)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
<style jsx>{`
|
||||
height: 100vh;
|
||||
|
|
@ -143,6 +146,7 @@ Index.propTypes = {
|
|||
forceFetchToken: PropTypes.bool,
|
||||
forceOwner: PropTypes.bool,
|
||||
subscribeToTracksAutomatically: PropTypes.bool,
|
||||
demoMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
export async function getStaticProps() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Flying Emojis
|
||||
|
||||

|
||||
|
||||
### Live example
|
||||
|
||||
**[See it in action here ➡️](https://dailyjs-flying-emojis.vercel.app)**
|
||||
|
||||
---
|
||||
|
||||
## What does this demo do?
|
||||
|
||||
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send flying emojis to all clients
|
||||
- Implements a custom `<App />` that adds `<FlyingEmojisOverlay />` component that listens for incoming emoji events and appends a new node to the DOM
|
||||
- Todo: pool emoji DOM nodes to optimise on DOM mutations
|
||||
|
||||
Please note: this demo is not currently mobile optimised
|
||||
|
||||
### Getting started
|
||||
|
||||
```
|
||||
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||
mv env.example .env.local
|
||||
|
||||
yarn
|
||||
yarn workspace @dailyjs/flying-emojis dev
|
||||
```
|
||||
|
||||
## Deploy your own on Vercel
|
||||
|
||||
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import App from '@dailyjs/basic-call/components/App';
|
||||
import FlyingEmojiOverlay from '../FlyingEmojis/FlyingEmojisOverlay';
|
||||
|
||||
export const AppWithEmojis = () => (
|
||||
<>
|
||||
<FlyingEmojiOverlay />
|
||||
<App />
|
||||
</>
|
||||
);
|
||||
|
||||
export default AppWithEmojis;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithEmojis as default } from './App';
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
|
||||
const EMOJI_MAP = {
|
||||
fire: '🔥',
|
||||
squid: '🦑',
|
||||
laugh: '🤣',
|
||||
};
|
||||
|
||||
export const FlyingEmojisOverlay = () => {
|
||||
const { callObject } = useCallState();
|
||||
const overlayRef = useRef();
|
||||
|
||||
// -- Handlers
|
||||
|
||||
const handleRemoveFlyingEmoji = useCallback((node) => {
|
||||
if (!overlayRef.current) return;
|
||||
overlayRef.current.removeChild(node);
|
||||
}, []);
|
||||
|
||||
const handleNewFlyingEmoji = useCallback(
|
||||
(emoji) => {
|
||||
if (!overlayRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`⭐ Displaying flying emoji: ${emoji}`);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.appendChild(document.createTextNode(EMOJI_MAP[emoji]));
|
||||
node.className =
|
||||
Math.random() * 1 > 0.5 ? 'emoji wiggle-1' : 'emoji wiggle-2';
|
||||
node.style.transform = `rotate(${-30 + Math.random() * 60}deg)`;
|
||||
node.style.left = `${Math.random() * 100}%`;
|
||||
node.src = '';
|
||||
overlayRef.current.appendChild(node);
|
||||
|
||||
node.addEventListener('animationend', (e) =>
|
||||
handleRemoveFlyingEmoji(e.target)
|
||||
);
|
||||
},
|
||||
[handleRemoveFlyingEmoji]
|
||||
);
|
||||
|
||||
// -- Effects
|
||||
|
||||
// Listen for new app messages and show new flying emojis
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`⭐ Listening for flying emojis...`);
|
||||
|
||||
callObject.on('app-message', handleNewFlyingEmoji);
|
||||
|
||||
return () => callObject.off('app-message', handleNewFlyingEmoji);
|
||||
}, [callObject, handleNewFlyingEmoji]);
|
||||
|
||||
// Listen to window events to show local user emojis
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleIncomingEmoji(e) {
|
||||
const { emoji } = e.detail;
|
||||
console.log(`⭐ Sending flying emoji: ${emoji}`);
|
||||
|
||||
if (emoji) {
|
||||
callObject.sendAppMessage({ emoji }, '*');
|
||||
handleNewFlyingEmoji(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('reaction_added', handleIncomingEmoji);
|
||||
return () =>
|
||||
window.removeEventListener('reaction_added', handleIncomingEmoji);
|
||||
}, [callObject, handleNewFlyingEmoji]);
|
||||
|
||||
// Remove all event listeners on unmount to prevent console warnings
|
||||
useEffect(
|
||||
() => () =>
|
||||
overlayRef.current.childNodes.forEach((n) =>
|
||||
n.removeEventListener('animationend', handleRemoveFlyingEmoji)
|
||||
),
|
||||
[handleRemoveFlyingEmoji]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flying-emojis" ref={overlayRef}>
|
||||
<style jsx>{`
|
||||
.flying-emojis {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji) {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 50%;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji.wiggle-1) {
|
||||
animation: emerge 3s forwards,
|
||||
wiggle-1 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji.wiggle-2) {
|
||||
animation: emerge 3s forwards,
|
||||
wiggle-2 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes emerge {
|
||||
to {
|
||||
bottom: 85%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle-1 {
|
||||
from {
|
||||
margin-left: -50px;
|
||||
}
|
||||
to {
|
||||
margin-left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle-2 {
|
||||
from {
|
||||
margin-left: 50px;
|
||||
}
|
||||
to {
|
||||
margin-left: -50px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlyingEmojisOverlay;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { FlyingEmojisOverlay } from './FlyingEmojisOverlay';
|
||||
export { FlyingEmojisOverlay as default } from './FlyingEmojisOverlay';
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Button from '@dailyjs/shared/components/Button';
|
||||
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||
import { ReactComponent as IconStar } from '@dailyjs/shared/icons/star-md.svg';
|
||||
|
||||
const COOLDOWN = 1500;
|
||||
|
||||
export const Tray = () => {
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
const [isThrottled, setIsThrottled] = useState(false);
|
||||
|
||||
function sendEmoji(emoji) {
|
||||
// Dispatch custom event here so the local user can see their own emoji
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('reaction_added', { detail: { emoji } })
|
||||
);
|
||||
setShowEmojis(false);
|
||||
setIsThrottled(true);
|
||||
}
|
||||
|
||||
// Pseudo-throttling (should ideally be done serverside)
|
||||
useEffect(() => {
|
||||
if (!isThrottled) {
|
||||
return false;
|
||||
}
|
||||
const t = setTimeout(() => setIsThrottled(false), COOLDOWN);
|
||||
return () => clearTimeout(t);
|
||||
}, [isThrottled]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showEmojis && (
|
||||
<div className="emojis">
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('fire')}
|
||||
>
|
||||
🔥
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('squid')}
|
||||
>
|
||||
🦑
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('laugh')}
|
||||
>
|
||||
🤣
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TrayButton
|
||||
label="Emoji"
|
||||
onClick={() => setShowEmojis(!showEmojis)}
|
||||
disabled={isThrottled}
|
||||
>
|
||||
<IconStar />
|
||||
</TrayButton>
|
||||
<style jsx>{`
|
||||
position: relative;
|
||||
|
||||
.emojis {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: calc(-100% + var(--spacing-xs));
|
||||
left: 0px;
|
||||
transform: translateX(calc(-50% + 26px));
|
||||
z-index: 99;
|
||||
background: white;
|
||||
padding: var(--spacing-xxxs);
|
||||
column-gap: 5px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-depth-2);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Tray as default } from './Tray';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'@dailyjs/shared',
|
||||
'@dailyjs/basic-call',
|
||||
]);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = withPlugins([withTM], {
|
||||
env: {
|
||||
PROJECT_TITLE: packageJson.description,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@dailyjs/flying-emojis",
|
||||
"description": "Basic Call + Flying Emojis",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dailyjs/basic-call": "*",
|
||||
"@dailyjs/shared": "*",
|
||||
"next": "^11.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import App from '@dailyjs/basic-call/pages/_app';
|
||||
import AppWithEmojis from '../components/App';
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
App.customAppComponent = <AppWithEmojis />;
|
||||
App.customTrayComponent = <Tray />;
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Index from '@dailyjs/basic-call/pages';
|
||||
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
// Pass through domain as prop
|
||||
return {
|
||||
props: defaultProps,
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../basic-call/public
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Recording
|
||||
|
||||

|
||||
|
||||
### Live example
|
||||
|
||||
**[See it in action here ➡️](https://dailyjs-recording.vercel.app)**
|
||||
|
||||
---
|
||||
|
||||
## What does this demo do?
|
||||
|
||||
- Use [startRecording](https://docs.daily.co/reference#%EF%B8%8F-startrecording) to create a video and audio recording of your call. You can read more about Daily call recording (and the different modes and types) [here](https://docs.daily.co/reference#recordings)
|
||||
- Supports both `cloud` and `local` recording modes (specified when creating the room or managed using the Daily dashboard)
|
||||
- Coming soon: support different recording layouts / composites
|
||||
- Coming soon: use the Daily REST API to retrieve a list of cloud recordings for the currently active room
|
||||
|
||||
**To turn on recording, you need to be on the Scale plan. There is also a per minute recording fee for cloud recording.**
|
||||
|
||||
Please note: this demo is not currently mobile optimised
|
||||
|
||||
### Getting started
|
||||
|
||||
```
|
||||
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||
mv env.example .env.local
|
||||
|
||||
yarn
|
||||
yarn workspace @dailyjs/recording dev
|
||||
```
|
||||
|
||||
### How does this demo work?
|
||||
|
||||
This example introduces a new [RecordingProvider](./contexts/RecordingProvider.js) context that listens for the various [recording events](https://docs.daily.co/reference#recording-started), counts down to begin a recording and stops a currently active recording. We also introduce a new recording modal and tray button.
|
||||
|
||||
Remember to follow the best practises detailed in [the documentation](https://docs.daily.co/reference#recordings) to avoid lengthy or stuck recordings.
|
||||
|
||||
## Deploy your own on Vercel
|
||||
|
||||
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@dailyjs/basic-call/components/App';
|
||||
import { RecordingProvider } from '../../contexts/RecordingProvider';
|
||||
|
||||
// Extend our basic call app component with the recording context
|
||||
export const AppWithRecording = () => (
|
||||
<RecordingProvider>
|
||||
<App />
|
||||
</RecordingProvider>
|
||||
);
|
||||
|
||||
export default AppWithRecording;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithRecording as default } from './App';
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button } from '@dailyjs/shared/components/Button';
|
||||
import { CardBody } from '@dailyjs/shared/components/Card';
|
||||
import Modal from '@dailyjs/shared/components/Modal';
|
||||
import Well from '@dailyjs/shared/components/Well';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import { enable } from 'debug';
|
||||
import {
|
||||
RECORDING_COUNTDOWN_1,
|
||||
RECORDING_COUNTDOWN_2,
|
||||
RECORDING_COUNTDOWN_3,
|
||||
RECORDING_ERROR,
|
||||
RECORDING_IDLE,
|
||||
RECORDING_RECORDING,
|
||||
RECORDING_SAVED,
|
||||
RECORDING_TYPE_CLOUD,
|
||||
RECORDING_UPLOADING,
|
||||
useRecording,
|
||||
} from '../../contexts/RecordingProvider';
|
||||
|
||||
export const RECORDING_MODAL = 'recording';
|
||||
|
||||
export const RecordingModal = () => {
|
||||
const { currentModals, closeModal } = useUIState();
|
||||
const { enableRecording } = useCallState();
|
||||
const {
|
||||
recordingStartedDate,
|
||||
recordingState,
|
||||
startRecordingWithCountdown,
|
||||
stopRecording,
|
||||
} = useRecording();
|
||||
|
||||
useEffect(() => {
|
||||
if (recordingState === RECORDING_RECORDING) {
|
||||
closeModal(RECORDING_MODAL);
|
||||
}
|
||||
}, [recordingState, closeModal]);
|
||||
|
||||
const disabled =
|
||||
enableRecording &&
|
||||
[RECORDING_IDLE, RECORDING_RECORDING].includes(recordingState);
|
||||
|
||||
function renderButtonLabel() {
|
||||
if (!enableRecording) {
|
||||
return 'Recording disabled';
|
||||
}
|
||||
|
||||
switch (recordingState) {
|
||||
case RECORDING_COUNTDOWN_3:
|
||||
return '3...';
|
||||
case RECORDING_COUNTDOWN_2:
|
||||
return '2...';
|
||||
case RECORDING_COUNTDOWN_1:
|
||||
return '1...';
|
||||
case RECORDING_RECORDING:
|
||||
return 'Stop recording';
|
||||
case RECORDING_UPLOADING:
|
||||
case RECORDING_SAVED:
|
||||
return 'Stopping recording...';
|
||||
default:
|
||||
return 'Start recording';
|
||||
}
|
||||
}
|
||||
|
||||
function handleRecordingClick() {
|
||||
if (recordingState === RECORDING_IDLE) {
|
||||
startRecordingWithCountdown();
|
||||
} else {
|
||||
stopRecording();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Recording"
|
||||
isOpen={currentModals[RECORDING_MODAL]}
|
||||
onClose={() => closeModal(RECORDING_MODAL)}
|
||||
actions={[
|
||||
<Button fullWidth variant="outline">
|
||||
Close
|
||||
</Button>,
|
||||
<Button
|
||||
fullWidth
|
||||
disabled={!disabled}
|
||||
onClick={() => handleRecordingClick()}
|
||||
>
|
||||
{renderButtonLabel()}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<CardBody>
|
||||
{!enableRecording ? (
|
||||
<Well variant="error">
|
||||
Recording is not enabled for this room (or your browser does not
|
||||
support it.) Please enabled recording when creating the room or via
|
||||
the Daily dashboard.
|
||||
</Well>
|
||||
) : (
|
||||
<p>
|
||||
Recording type enabled: <strong>{enableRecording}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{recordingStartedDate && (
|
||||
<p>Recording started: {recordingStartedDate.toString()}</p>
|
||||
)}
|
||||
|
||||
{enableRecording === RECORDING_TYPE_CLOUD && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
Cloud recordings can be accessed via the Daily dashboard under the
|
||||
"Recordings" section.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingModal;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { RecordingModal as default } from './RecordingModal';
|
||||
export { RecordingModal } from './RecordingModal';
|
||||
export { RECORDING_MODAL } from './RecordingModal';
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconRecord } from '@dailyjs/shared/icons/record-md.svg';
|
||||
|
||||
import {
|
||||
RECORDING_ERROR,
|
||||
RECORDING_RECORDING,
|
||||
RECORDING_SAVED,
|
||||
RECORDING_UPLOADING,
|
||||
useRecording,
|
||||
} from '../../contexts/RecordingProvider';
|
||||
import { RECORDING_MODAL } from '../RecordingModal';
|
||||
|
||||
export const Tray = () => {
|
||||
const { openModal } = useUIState();
|
||||
const { recordingState } = useRecording();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`⏺️ Recording state: ${recordingState}`);
|
||||
|
||||
if (recordingState === RECORDING_ERROR) {
|
||||
// show error modal here
|
||||
}
|
||||
}, [recordingState]);
|
||||
|
||||
const isRecording = [
|
||||
RECORDING_RECORDING,
|
||||
RECORDING_UPLOADING,
|
||||
RECORDING_SAVED,
|
||||
].includes(recordingState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrayButton
|
||||
label={isRecording ? 'Recording' : 'Record'}
|
||||
orange={isRecording}
|
||||
onClick={() => openModal(RECORDING_MODAL)}
|
||||
>
|
||||
<IconRecord />
|
||||
</TrayButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Tray as default } from './Tray';
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import {
|
||||
CALL_STATE_REDIRECTING,
|
||||
CALL_STATE_JOINED,
|
||||
} from '@dailyjs/shared/contexts/useCallMachine';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDeepCompareEffect } from 'use-deep-compare';
|
||||
|
||||
export const RECORDING_ERROR = 'error';
|
||||
export const RECORDING_SAVED = 'saved';
|
||||
export const RECORDING_RECORDING = 'recording';
|
||||
export const RECORDING_UPLOADING = 'uploading';
|
||||
export const RECORDING_COUNTDOWN_1 = 'starting1';
|
||||
export const RECORDING_COUNTDOWN_2 = 'starting2';
|
||||
export const RECORDING_COUNTDOWN_3 = 'starting3';
|
||||
export const RECORDING_IDLE = 'idle';
|
||||
|
||||
export const RECORDING_TYPE_CLOUD = 'cloud';
|
||||
export const RECORDING_TYPE_LOCAL = 'local';
|
||||
|
||||
const RecordingContext = createContext({
|
||||
isRecordingLocally: false,
|
||||
recordingStartedDate: null,
|
||||
recordingState: RECORDING_IDLE,
|
||||
startRecording: null,
|
||||
stopRecording: null,
|
||||
});
|
||||
|
||||
export const RecordingProvider = ({ children }) => {
|
||||
const { callObject, enableRecording, startCloudRecording, state } =
|
||||
useCallState();
|
||||
const { participants } = useParticipants();
|
||||
const [recordingStartedDate, setRecordingStartedDate] = useState(null);
|
||||
const [recordingState, setRecordingState] = useState(RECORDING_IDLE);
|
||||
const [isRecordingLocally, setIsRecordingLocally] = useState(false);
|
||||
const [hasRecordingStarted, setHasRecordingStarted] = useState(false);
|
||||
const { setCustomCapsule } = useUIState();
|
||||
|
||||
const handleOnUnload = useCallback(
|
||||
() => 'Unsaved recording in progress. Do you really want to leave?',
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!enableRecording ||
|
||||
!isRecordingLocally ||
|
||||
recordingState !== RECORDING_RECORDING ||
|
||||
state === CALL_STATE_REDIRECTING
|
||||
)
|
||||
return false;
|
||||
const prev = window.onbeforeunload;
|
||||
window.onbeforeunload = handleOnUnload;
|
||||
return () => {
|
||||
window.onbeforeunload = prev;
|
||||
};
|
||||
}, [
|
||||
enableRecording,
|
||||
handleOnUnload,
|
||||
recordingState,
|
||||
isRecordingLocally,
|
||||
state,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject || !enableRecording) return false;
|
||||
|
||||
const handleAppMessage = (ev) => {
|
||||
switch (ev?.data?.event) {
|
||||
case 'recording-starting':
|
||||
setRecordingState(RECORDING_COUNTDOWN_3);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecordingUploadCompleted = () => {
|
||||
setRecordingState(RECORDING_SAVED);
|
||||
};
|
||||
|
||||
callObject.on('app-message', handleAppMessage);
|
||||
callObject.on('recording-upload-completed', handleRecordingUploadCompleted);
|
||||
|
||||
return () => {
|
||||
callObject.off('app-message', handleAppMessage);
|
||||
callObject.off(
|
||||
'recording-upload-completed',
|
||||
handleRecordingUploadCompleted
|
||||
);
|
||||
};
|
||||
}, [callObject, enableRecording]);
|
||||
|
||||
/**
|
||||
* Automatically start cloud recording, if startCloudRecording is set.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
hasRecordingStarted ||
|
||||
!callObject ||
|
||||
!startCloudRecording ||
|
||||
enableRecording !== 'cloud' ||
|
||||
state !== CALL_STATE_JOINED
|
||||
)
|
||||
return false;
|
||||
|
||||
// Small timeout, in case other participants are already in-call.
|
||||
const timeout = setTimeout(() => {
|
||||
const isSomebodyRecording = participants.some((p) => p.isRecording);
|
||||
if (!isSomebodyRecording) {
|
||||
callObject.startRecording();
|
||||
setIsRecordingLocally(true);
|
||||
setHasRecordingStarted(true);
|
||||
} else {
|
||||
setHasRecordingStarted(true);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [
|
||||
callObject,
|
||||
enableRecording,
|
||||
hasRecordingStarted,
|
||||
participants,
|
||||
startCloudRecording,
|
||||
state,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle participant updates to sync recording state.
|
||||
*/
|
||||
useDeepCompareEffect(() => {
|
||||
if (isRecordingLocally || recordingState === RECORDING_SAVED) return;
|
||||
if (participants.some(({ isRecording }) => isRecording)) {
|
||||
setRecordingState(RECORDING_RECORDING);
|
||||
} else {
|
||||
setRecordingState(RECORDING_IDLE);
|
||||
}
|
||||
}, [isRecordingLocally, participants, recordingState]);
|
||||
|
||||
/**
|
||||
* Handle recording started.
|
||||
*/
|
||||
const handleRecordingStarted = useCallback(
|
||||
(event) => {
|
||||
if (recordingState === RECORDING_RECORDING) return;
|
||||
if (event.local) {
|
||||
// Recording started locally, either through UI or programmatically
|
||||
setIsRecordingLocally(true);
|
||||
if (!recordingStartedDate) setRecordingStartedDate(new Date());
|
||||
}
|
||||
setRecordingState(RECORDING_RECORDING);
|
||||
},
|
||||
[recordingState, recordingStartedDate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject || !enableRecording) return false;
|
||||
|
||||
callObject.on('recording-started', handleRecordingStarted);
|
||||
return () => callObject.off('recording-started', handleRecordingStarted);
|
||||
}, [callObject, enableRecording, handleRecordingStarted]);
|
||||
|
||||
/**
|
||||
* Handle recording stopped.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!callObject || !enableRecording) return false;
|
||||
|
||||
const handleRecordingStopped = () => {
|
||||
if (isRecordingLocally) return;
|
||||
setRecordingState(RECORDING_IDLE);
|
||||
setRecordingStartedDate(null);
|
||||
};
|
||||
|
||||
callObject.on('recording-stopped', handleRecordingStopped);
|
||||
return () => callObject.off('recording-stopped', handleRecordingStopped);
|
||||
}, [callObject, enableRecording, isRecordingLocally]);
|
||||
|
||||
/**
|
||||
* Handle recording error.
|
||||
*/
|
||||
const handleRecordingError = useCallback(() => {
|
||||
if (isRecordingLocally) setRecordingState(RECORDING_ERROR);
|
||||
setIsRecordingLocally(false);
|
||||
}, [isRecordingLocally]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject || !enableRecording) return false;
|
||||
|
||||
callObject.on('recording-error', handleRecordingError);
|
||||
return () => callObject.off('recording-error', handleRecordingError);
|
||||
}, [callObject, enableRecording, handleRecordingError]);
|
||||
|
||||
const startRecording = useCallback(() => {
|
||||
if (!callObject || !isRecordingLocally) return;
|
||||
callObject.startRecording();
|
||||
}, [callObject, isRecordingLocally]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout;
|
||||
switch (recordingState) {
|
||||
case RECORDING_COUNTDOWN_3:
|
||||
timeout = setTimeout(() => {
|
||||
setRecordingState(RECORDING_COUNTDOWN_2);
|
||||
}, 1000);
|
||||
break;
|
||||
case RECORDING_COUNTDOWN_2:
|
||||
timeout = setTimeout(() => {
|
||||
setRecordingState(RECORDING_COUNTDOWN_1);
|
||||
}, 1000);
|
||||
break;
|
||||
case RECORDING_COUNTDOWN_1:
|
||||
startRecording();
|
||||
break;
|
||||
case RECORDING_ERROR:
|
||||
case RECORDING_SAVED:
|
||||
timeout = setTimeout(() => {
|
||||
setRecordingState(RECORDING_IDLE);
|
||||
setIsRecordingLocally(false);
|
||||
}, 5000);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [recordingState, startRecording]);
|
||||
|
||||
// Show a custom capsule when recording in progress
|
||||
useEffect(() => {
|
||||
if (recordingState !== RECORDING_RECORDING) {
|
||||
setCustomCapsule(null);
|
||||
} else {
|
||||
setCustomCapsule({ variant: 'recording', label: 'Recording' });
|
||||
}
|
||||
}, [recordingState, setCustomCapsule]);
|
||||
|
||||
const startRecordingWithCountdown = useCallback(() => {
|
||||
if (!callObject || !enableRecording) return;
|
||||
setIsRecordingLocally(true);
|
||||
setRecordingState(RECORDING_COUNTDOWN_3);
|
||||
callObject?.sendAppMessage({
|
||||
event: 'recording-starting',
|
||||
});
|
||||
}, [callObject, enableRecording]);
|
||||
|
||||
const stopRecording = useCallback(() => {
|
||||
if (!callObject || !enableRecording || !isRecordingLocally) return;
|
||||
if (recordingState === RECORDING_RECORDING) {
|
||||
switch (enableRecording) {
|
||||
case RECORDING_TYPE_LOCAL:
|
||||
setRecordingState(RECORDING_SAVED);
|
||||
setIsRecordingLocally(false);
|
||||
break;
|
||||
case RECORDING_TYPE_CLOUD:
|
||||
setRecordingState(RECORDING_UPLOADING);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (recordingState === RECORDING_IDLE) {
|
||||
return;
|
||||
} else {
|
||||
setIsRecordingLocally(false);
|
||||
setRecordingState(RECORDING_IDLE);
|
||||
}
|
||||
setRecordingStartedDate(null);
|
||||
callObject.stopRecording();
|
||||
}, [callObject, enableRecording, isRecordingLocally, recordingState]);
|
||||
|
||||
return (
|
||||
<RecordingContext.Provider
|
||||
value={{
|
||||
isRecordingLocally,
|
||||
recordingStartedDate,
|
||||
recordingState,
|
||||
startRecordingWithCountdown,
|
||||
stopRecording,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecordingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
RecordingProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export const useRecording = () => useContext(RecordingContext);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'@dailyjs/shared',
|
||||
'@dailyjs/basic-call',
|
||||
]);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = withPlugins([withTM], {
|
||||
env: {
|
||||
PROJECT_TITLE: packageJson.description,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@dailyjs/recording",
|
||||
"description": "Basic Call + Recording",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dailyjs/shared": "*",
|
||||
"@dailyjs/basic-call": "*",
|
||||
"next": "^11.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import App from '@dailyjs/basic-call/pages/_app';
|
||||
import AppWithRecording from '../components/App';
|
||||
|
||||
import { RecordingModal } from '../components/RecordingModal';
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
App.modals = [RecordingModal];
|
||||
App.customAppComponent = <AppWithRecording />;
|
||||
App.customTrayComponent = <Tray />;
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import Index from '@dailyjs/basic-call/pages';
|
||||
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...defaultProps,
|
||||
forceFetchToken: true,
|
||||
forceOwner: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../basic-call/public
|
||||
|
|
@ -286,6 +286,9 @@ export const Button = forwardRef(
|
|||
.button.dark:focus {
|
||||
box-shadow: 0 0 0px 3px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.button.dark:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.outline {
|
||||
background: transparent;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,17 @@ export const TrayButton = ({
|
|||
onClick,
|
||||
bubble = false,
|
||||
orange = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const cx = classNames('tray-button', { orange, bubble });
|
||||
return (
|
||||
<div className={cx}>
|
||||
<Button onClick={() => onClick()} variant="dark" size="large-square">
|
||||
<Button
|
||||
onClick={() => onClick()}
|
||||
variant="dark"
|
||||
size="large-square"
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
<span>{label}</span>
|
||||
|
|
@ -57,6 +63,7 @@ TrayButton.propTypes = {
|
|||
orange: PropTypes.bool,
|
||||
bubble: PropTypes.bool,
|
||||
label: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const Tray = ({ children }) => (
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import React, {
|
|||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Bowser from 'bowser';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ACCESS_STATE_LOBBY,
|
||||
|
|
@ -31,6 +32,9 @@ export const CallProvider = ({
|
|||
}) => {
|
||||
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
|
||||
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
|
||||
const [enableRecording, setEnableRecording] = useState(null);
|
||||
const [startCloudRecording, setStartCloudRecording] = useState(false);
|
||||
const [roomExp, setRoomExp] = useState(null);
|
||||
|
||||
// Daily CallMachine hook (primarily handles status of the call)
|
||||
const { daily, leave, state, setRedirectOnLeave } = useCallMachine({
|
||||
|
|
@ -40,6 +44,37 @@ export const CallProvider = ({
|
|||
subscribeToTracksAutomatically,
|
||||
});
|
||||
|
||||
// Feature detection taken from daily room object and client browser support
|
||||
useEffect(() => {
|
||||
if (!daily) return;
|
||||
const updateRoomConfigState = async () => {
|
||||
const roomConfig = await daily.room();
|
||||
if (!('config' in roomConfig)) return;
|
||||
|
||||
if (roomConfig?.config?.exp) {
|
||||
setRoomExp(
|
||||
roomConfig?.config?.exp * 1000 || Date.now() + 1 * 60 * 1000
|
||||
);
|
||||
}
|
||||
const browser = Bowser.parse(window.navigator.userAgent);
|
||||
const supportsRecording =
|
||||
browser.platform.type === 'desktop' && browser.engine.name === 'Blink';
|
||||
// recording and screen sharing is hidden in owner_only_broadcast for non-owners
|
||||
if (supportsRecording) {
|
||||
const recordingType =
|
||||
roomConfig?.tokenConfig?.enable_recording ??
|
||||
roomConfig?.config?.enable_recording;
|
||||
if (['local', 'cloud'].includes(recordingType)) {
|
||||
setEnableRecording(recordingType);
|
||||
setStartCloudRecording(
|
||||
roomConfig?.tokenConfig?.start_cloud_recording ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
updateRoomConfigState();
|
||||
}, [state, daily]);
|
||||
|
||||
// Convience wrapper for adding a fake participant to the call
|
||||
const addFakeParticipant = useCallback(() => {
|
||||
daily.addFakeParticipant();
|
||||
|
|
@ -71,10 +106,13 @@ export const CallProvider = ({
|
|||
addFakeParticipant,
|
||||
preJoinNonAuthorized,
|
||||
leave,
|
||||
roomExp,
|
||||
videoQuality,
|
||||
enableRecording,
|
||||
setVideoQuality,
|
||||
setBandwidth,
|
||||
setRedirectOnLeave,
|
||||
startCloudRecording,
|
||||
subscribeToTracksAutomatically,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export const UIStateProvider = ({
|
|||
|
||||
UIStateProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
demoMode: PropTypes.bool,
|
||||
asides: PropTypes.arrayOf(PropTypes.func),
|
||||
modals: PropTypes.arrayOf(PropTypes.func),
|
||||
customTrayComponent: PropTypes.node,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="4" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" stroke="currentColor"><polygon points="12,2.49 15.09,8.75 22,9.754 17,14.628 18.18,21.51 12,18.262 5.82,21.51 7,14.628 2,9.754 8.91,8.75 "></polygon></g></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
|
|
@ -7,5 +7,7 @@ export default function getDemoProps() {
|
|||
predefinedRoom: process.env.DAILY_ROOM || '',
|
||||
// Manual or automatic track subscriptions
|
||||
subscribeToTracksAutomatically: !process.env.MANUAL_TRACK_SUBS,
|
||||
// Are we running in demo mode? (automatically creates a short-expiry room)
|
||||
demoMode: !!process.env.DAILY_DEMO_MODE,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"private": true,
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.14.0",
|
||||
"@daily-co/daily-js": "^0.15.0",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.3.1",
|
||||
"debounce": "^1.2.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
|
|
@ -160,10 +160,17 @@
|
|||
"@babel/helper-validator-identifier" "^7.12.11"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
<<<<<<< HEAD
|
||||
"@daily-co/daily-js@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.14.0.tgz#29308c77e00886514df7d932d771980d5cdb7618"
|
||||
integrity sha512-OD2epVohYraTfOH/ZuO5rP9Ej4Rfu/ufGXX0XJQG+mAu1hJ1610JWunnszTmfhk+uUH4aA9i7+5/PQ2meOXUtQ==
|
||||
=======
|
||||
"@daily-co/daily-js@^0.15.0":
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.15.0.tgz#9dfd5c3ed8855df31c370d5b21a3b5098cce3c4f"
|
||||
integrity sha512-rnivho7yx/yEOtqL81L4daPy9C/FDXf06k06df8vmyUXsE8y+cxSTD7ZvYIJDGJHN6IZRhVxxfbCyPI8CHfwCg==
|
||||
>>>>>>> 1068a9f75322d380546319034f8c236029567085
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
bowser "^2.8.1"
|
||||
|
|
@ -584,7 +591,7 @@ boolbase@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
bowser@^2.8.1:
|
||||
bowser@^2.11.0, bowser@^2.8.1:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
||||
|
|
|
|||
Loading…
Reference in New Issue