diff --git a/dailyjs/.gitignore b/dailyjs/.gitignore new file mode 100644 index 0000000..058f0ec --- /dev/null +++ b/dailyjs/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +/coverage + +# next.js +.next +out + +# production +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel \ No newline at end of file diff --git a/dailyjs/README.md b/dailyjs/README.md index 4f3c0de..81f366e 100644 --- a/dailyjs/README.md +++ b/dailyjs/README.md @@ -20,6 +20,10 @@ Record a call video and audio using both cloud and local modes Send emoji reactions to all clients using sendAppMessage +### [📃 Pagination](./pagination) + +Demonstrates using manual track management to support larger call sizes + --- ## Getting started diff --git a/dailyjs/active-speaker/.babelrc b/dailyjs/active-speaker/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/active-speaker/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/active-speaker/README.md b/dailyjs/active-speaker/README.md new file mode 100644 index 0000000..4656997 --- /dev/null +++ b/dailyjs/active-speaker/README.md @@ -0,0 +1,31 @@ +# Active Speaker + +![Active speaker](./image.png) + +### Live example + +**[See it in action here ➡️](https://dailyjs-active-speaker.vercel.app)** + +--- + +## What does this demo do? + +- Uses an active speaker view mode that shows the currently talking participant (or active screen share) in a larger tile +- Introduces the `ParticipantBar` column that virtually scrolls through all call participants +- Uses manual subscriptions to paginate between tiles that are currently in view. For more information about how this works, please refer to the [pagination demo](../pagination) + +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/active-speaker dev +``` + +## Deploy your own on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](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) diff --git a/dailyjs/active-speaker/components/App/App.js b/dailyjs/active-speaker/components/App/App.js new file mode 100644 index 0000000..5689315 --- /dev/null +++ b/dailyjs/active-speaker/components/App/App.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import App from '@dailyjs/basic-call/components/App'; +import Room from '../Room'; + +// Extend our basic call app component with our custom Room componenet +export const AppWithSpeakerViewRoom = () => ( + , + }} + /> +); + +export default AppWithSpeakerViewRoom; diff --git a/dailyjs/active-speaker/components/App/index.js b/dailyjs/active-speaker/components/App/index.js new file mode 100644 index 0000000..19427e3 --- /dev/null +++ b/dailyjs/active-speaker/components/App/index.js @@ -0,0 +1 @@ +export { AppWithSpeakerViewRoom as default } from './App'; diff --git a/dailyjs/active-speaker/components/Room/Room.js b/dailyjs/active-speaker/components/Room/Room.js new file mode 100644 index 0000000..f088154 --- /dev/null +++ b/dailyjs/active-speaker/components/Room/Room.js @@ -0,0 +1,7 @@ +import React from 'react'; + +import { SpeakerView } from '../SpeakerView'; + +export const Room = () => ; + +export default Room; diff --git a/dailyjs/active-speaker/components/Room/index.js b/dailyjs/active-speaker/components/Room/index.js new file mode 100644 index 0000000..d26c647 --- /dev/null +++ b/dailyjs/active-speaker/components/Room/index.js @@ -0,0 +1,2 @@ +export { Room as default } from './Room'; +export { Room } from './Room'; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js new file mode 100644 index 0000000..f75163e --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Tile } from '@dailyjs/shared/components/Tile'; +import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants'; +import { useResize } from '@dailyjs/shared/hooks/useResize'; +import PropTypes from 'prop-types'; + +const MAX_RATIO = DEFAULT_ASPECT_RATIO; +const MIN_RATIO = 4 / 3; + +export const SpeakerTile = ({ participant, screenRef }) => { + const [ratio, setRatio] = useState(MAX_RATIO); + const [nativeAspectRatio, setNativeAspectRatio] = useState(null); + const [screenHeight, setScreenHeight] = useState(1); + + const updateRatio = useCallback(() => { + const rect = screenRef.current?.getBoundingClientRect(); + setRatio(rect.width / rect.height); + setScreenHeight(rect.height); + }, [screenRef]); + + useResize(() => { + updateRatio(); + }, [updateRatio]); + + useEffect(() => updateRatio(), [updateRatio]); + + /** + * Only use the video's native aspect ratio if it's in portrait mode + * (e.g. mobile) to update how we crop videos. Otherwise, use landscape + * defaults. + */ + const handleNativeAspectRatio = (r) => { + const isPortrait = r < 1; + setNativeAspectRatio(isPortrait ? r : null); + }; + + const { height, finalRatio, videoFit } = useMemo( + () => + // Avoid cropping mobile videos, which have the nativeAspectRatio set + ({ + height: (nativeAspectRatio ?? ratio) >= MIN_RATIO ? '100%' : null, + finalRatio: + nativeAspectRatio || (ratio <= MIN_RATIO ? MIN_RATIO : MAX_RATIO), + videoFit: ratio >= MAX_RATIO || nativeAspectRatio ? 'contain' : 'cover', + }), + [nativeAspectRatio, ratio] + ); + + const style = { + height, + maxWidth: screenHeight * finalRatio, + overflow: 'hidden', + }; + + return ( + + ); +}; + +SpeakerTile.propTypes = { + participant: PropTypes.object, + screenRef: PropTypes.object, +}; + +export default SpeakerTile; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js new file mode 100644 index 0000000..84639df --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js @@ -0,0 +1,2 @@ +export { SpeakerTile as default } from './SpeakerTile'; +export { SpeakerTile } from './SpeakerTile'; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js new file mode 100644 index 0000000..877b64d --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js @@ -0,0 +1,116 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import RoomContainer from '@dailyjs/basic-call/components/Room/RoomContainer'; +import ParticipantBar from '@dailyjs/shared/components/ParticipantBar/ParticipantBar'; +import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { useTracks } from '@dailyjs/shared/contexts/TracksProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { isScreenId } from '@dailyjs/shared/contexts/participantsState'; +import SpeakerTile from './SpeakerTile'; + +const SIDEBAR_WIDTH = 186; + +export const SpeakerView = () => { + const { currentSpeaker, localParticipant, participants, screens } = + useParticipants(); + const { updateCamSubscriptions } = useTracks(); + const { showLocalVideo } = useCallState(); + const { pinnedId } = useUIState(); + const activeRef = useRef(); + + const screensAndPinned = useMemo( + () => [...screens, ...participants.filter(({ id }) => id === pinnedId)], + [participants, pinnedId, screens] + ); + + const otherParticipants = useMemo( + () => participants.filter(({ isLocal }) => !isLocal), + [participants] + ); + + const showSidebar = useMemo(() => { + const hasScreenshares = screens.length > 0; + + if (isScreenId(pinnedId)) { + return false; + } + + return participants.length > 1 || hasScreenshares; + }, [participants, pinnedId, screens]); + + /* const screenShareTiles = useMemo( + () => , + [screensAndPinned] + ); */ + + const hasScreenshares = useMemo(() => screens.length > 0, [screens]); + + const fixedItems = useMemo(() => { + const items = []; + if (showLocalVideo) { + items.push(localParticipant); + } + if (hasScreenshares && otherParticipants.length > 0) { + items.push(otherParticipants[0]); + } + return items; + }, [hasScreenshares, localParticipant, otherParticipants, showLocalVideo]); + + const otherItems = useMemo(() => { + if (otherParticipants.length > 1) { + return otherParticipants.slice(hasScreenshares ? 1 : 0); + } + return []; + }, [hasScreenshares, otherParticipants]); + + /** + * Update cam subscriptions, in case ParticipantBar is not shown. + */ + useEffect(() => { + // Sidebar takes care of cam subscriptions for all displayed participants. + if (showSidebar) return; + updateCamSubscriptions([ + currentSpeaker?.id, + ...screensAndPinned.map((p) => p.id), + ]); + }, [currentSpeaker, screensAndPinned, showSidebar, updateCamSubscriptions]); + + return ( +
+ + +
+ +
+
+
+ {showSidebar && ( + + )} + + +
+ ); +}; + +export default SpeakerView; diff --git a/dailyjs/active-speaker/components/SpeakerView/index.js b/dailyjs/active-speaker/components/SpeakerView/index.js new file mode 100644 index 0000000..aa06afe --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/index.js @@ -0,0 +1,2 @@ +export { SpeakerView as default } from './SpeakerView'; +export { SpeakerView } from './SpeakerView'; diff --git a/dailyjs/active-speaker/env.example b/dailyjs/active-speaker/env.example new file mode 100644 index 0000000..5ab7e03 --- /dev/null +++ b/dailyjs/active-speaker/env.example @@ -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 diff --git a/dailyjs/active-speaker/image.png b/dailyjs/active-speaker/image.png new file mode 100644 index 0000000..592ea98 Binary files /dev/null and b/dailyjs/active-speaker/image.png differ diff --git a/dailyjs/active-speaker/index.js b/dailyjs/active-speaker/index.js new file mode 100644 index 0000000..9044efc --- /dev/null +++ b/dailyjs/active-speaker/index.js @@ -0,0 +1 @@ +// Note: I am here because next-transpile-modules requires a mainfile diff --git a/dailyjs/active-speaker/next.config.js b/dailyjs/active-speaker/next.config.js new file mode 100644 index 0000000..9a0a6ee --- /dev/null +++ b/dailyjs/active-speaker/next.config.js @@ -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, + }, +}); diff --git a/dailyjs/active-speaker/package.json b/dailyjs/active-speaker/package.json new file mode 100644 index 0000000..959883e --- /dev/null +++ b/dailyjs/active-speaker/package.json @@ -0,0 +1,25 @@ +{ + "name": "@dailyjs/active-speaker", + "description": "Basic Call + Active Speaker", + "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" + } +} diff --git a/dailyjs/active-speaker/pages/_app.js b/dailyjs/active-speaker/pages/_app.js new file mode 100644 index 0000000..4fcae14 --- /dev/null +++ b/dailyjs/active-speaker/pages/_app.js @@ -0,0 +1,7 @@ +import React from 'react'; +import App from '@dailyjs/basic-call/pages/_app'; +import AppWithSpeakerViewRoom from '../components/App'; + +App.customAppComponent = ; + +export default App; diff --git a/dailyjs/active-speaker/pages/api b/dailyjs/active-speaker/pages/api new file mode 120000 index 0000000..999f604 --- /dev/null +++ b/dailyjs/active-speaker/pages/api @@ -0,0 +1 @@ +../../basic-call/pages/api \ No newline at end of file diff --git a/dailyjs/active-speaker/pages/index.js b/dailyjs/active-speaker/pages/index.js new file mode 100644 index 0000000..d25e77e --- /dev/null +++ b/dailyjs/active-speaker/pages/index.js @@ -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; diff --git a/dailyjs/active-speaker/public/assets/daily-logo-dark.svg b/dailyjs/active-speaker/public/assets/daily-logo-dark.svg new file mode 100644 index 0000000..ef3a565 --- /dev/null +++ b/dailyjs/active-speaker/public/assets/daily-logo-dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dailyjs/basic-call/public/images/daily-logo.svg b/dailyjs/active-speaker/public/assets/daily-logo.svg similarity index 100% rename from dailyjs/basic-call/public/images/daily-logo.svg rename to dailyjs/active-speaker/public/assets/daily-logo.svg diff --git a/dailyjs/basic-call/public/join.mp3 b/dailyjs/active-speaker/public/assets/join.mp3 similarity index 100% rename from dailyjs/basic-call/public/join.mp3 rename to dailyjs/active-speaker/public/assets/join.mp3 diff --git a/dailyjs/basic-call/public/message.mp3 b/dailyjs/active-speaker/public/assets/message.mp3 similarity index 100% rename from dailyjs/basic-call/public/message.mp3 rename to dailyjs/active-speaker/public/assets/message.mp3 diff --git a/dailyjs/basic-call/public/images/pattern-bg.png b/dailyjs/active-speaker/public/assets/pattern-bg.png similarity index 100% rename from dailyjs/basic-call/public/images/pattern-bg.png rename to dailyjs/active-speaker/public/assets/pattern-bg.png diff --git a/dailyjs/basic-call/components/App/App.js b/dailyjs/basic-call/components/App/App.js index 51d3524..fa7c678 100644 --- a/dailyjs/basic-call/components/App/App.js +++ b/dailyjs/basic-call/components/App/App.js @@ -1,76 +1,51 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; +import ExpiryTimer from '@dailyjs/shared/components/ExpiryTimer'; import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; import { useCallUI } from '@dailyjs/shared/hooks/useCallUI'; +import PropTypes from 'prop-types'; import Room from '../Room'; import { Asides } from './Asides'; import { Modals } from './Modals'; -export const App = () => { +export const App = ({ customComponentForState }) => { 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, room: () => , + ...customComponentForState, }); // Memoize children to avoid unnecassary renders from HOC - const memoizedApp = useMemo( + return useMemo( () => ( -
- {componentForState()} - - - -
+ .loader { + margin: 0 auto; + } + `} + + ), - [componentForState] - ); - - return ( - <> - {roomExp &&
{secs}
} {memoizedApp} - - + [componentForState, roomExp] ); }; +App.propTypes = { + customComponentForState: PropTypes.any, +}; + export default App; diff --git a/dailyjs/basic-call/components/Room/Header.js b/dailyjs/basic-call/components/Room/Header.js index a18e5d5..7491b1e 100644 --- a/dailyjs/basic-call/components/Room/Header.js +++ b/dailyjs/basic-call/components/Room/Header.js @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import HeaderCapsule from '@dailyjs/shared/components/HeaderCapsule'; import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; @@ -9,18 +10,19 @@ export const Header = () => { return useMemo( () => (
- Daily -
Basic call demo
-
+ Daily + + Basic call demo + {`${participantCount} ${ - participantCount > 1 ? 'participants' : 'participant' + participantCount === 1 ? 'participant' : 'participants' }`} -
+ {customCapsule && ( -
+ {customCapsule.variant === 'recording' && } {customCapsule.label} -
+ )}
), diff --git a/dailyjs/basic-call/components/Room/Room.js b/dailyjs/basic-call/components/Room/Room.js index 33d8cbd..8efc0bb 100644 --- a/dailyjs/basic-call/components/Room/Room.js +++ b/dailyjs/basic-call/components/Room/Room.js @@ -1,64 +1,17 @@ import React from 'react'; -import { Audio } from '@dailyjs/shared/components/Audio'; -import { BasicTray } from '@dailyjs/shared/components/Tray'; -import { - WaitingRoomModal, - WaitingRoomNotification, -} from '@dailyjs/shared/components/WaitingRoom'; -import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; -import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider'; -import useJoinSound from '@dailyjs/shared/hooks/useJoinSound'; +import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer'; import { VideoGrid } from '../VideoGrid'; import { Header } from './Header'; +import { RoomContainer } from './RoomContainer'; -export const Room = () => { - const { setShowModal, showModal } = useWaitingRoom(); - const { localParticipant } = useParticipants(); - - useJoinSound(); - - return ( -
-
- -
- -
- - {/* Show waiting room notification & modal if call owner */} - {localParticipant?.isOwner && ( - <> - - {showModal && ( - setShowModal(false)} /> - )} - - )} - - -
- ); -}; +export const Room = () => ( + +
+ + + + +); export default Room; diff --git a/dailyjs/basic-call/components/Room/RoomContainer.js b/dailyjs/basic-call/components/Room/RoomContainer.js new file mode 100644 index 0000000..b3fed79 --- /dev/null +++ b/dailyjs/basic-call/components/Room/RoomContainer.js @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { Audio } from '@dailyjs/shared/components/Audio'; +import { BasicTray } from '@dailyjs/shared/components/Tray'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import useJoinSound from '@dailyjs/shared/hooks/useJoinSound'; +import PropTypes from 'prop-types'; +import WaitingRoom from '../WaitingRoom'; + +export const RoomContainer = ({ children }) => { + const { isOwner } = useParticipants(); + + useJoinSound(); + + const roomComponents = useMemo( + () => ( + <> + {/* Show waiting room notification & modal if call owner */} + {isOwner && } + {/* Tray buttons */} + + {/* Audio tags */} +