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..6ad9e2b --- /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..e357840 --- /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..62eaf89 --- /dev/null +++ b/dailyjs/active-speaker/env.example @@ -0,0 +1,12 @@ +# 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 + + +# Enable manual track subscriptions +MANUAL_TRACK_SUBS=1 \ No newline at end of file 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/active-speaker/public/assets/daily-logo.svg b/dailyjs/active-speaker/public/assets/daily-logo.svg new file mode 100644 index 0000000..534a18a --- /dev/null +++ b/dailyjs/active-speaker/public/assets/daily-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dailyjs/active-speaker/public/assets/join.mp3 b/dailyjs/active-speaker/public/assets/join.mp3 new file mode 100644 index 0000000..7657915 Binary files /dev/null and b/dailyjs/active-speaker/public/assets/join.mp3 differ diff --git a/dailyjs/active-speaker/public/assets/message.mp3 b/dailyjs/active-speaker/public/assets/message.mp3 new file mode 100644 index 0000000..a067315 Binary files /dev/null and b/dailyjs/active-speaker/public/assets/message.mp3 differ diff --git a/dailyjs/active-speaker/public/assets/pattern-bg.png b/dailyjs/active-speaker/public/assets/pattern-bg.png new file mode 100644 index 0000000..01e0d0d Binary files /dev/null and b/dailyjs/active-speaker/public/assets/pattern-bg.png differ