diff --git a/.gitignore b/.gitignore index 10e85c1..8df7dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn-error.log* # vercel .vercel +.turbo # typescript *.tsbuildinfo diff --git a/components/stream-player/fullscreen-control.tsx b/components/stream-player/fullscreen-control.tsx new file mode 100644 index 0000000..fe220b8 --- /dev/null +++ b/components/stream-player/fullscreen-control.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { Maximize, Minimize } from "lucide-react"; +import { Hint } from "../hint"; + +interface FullscreenControlProps { + isFullscreen: boolean; + onToggle: () => void; +} + +export const FullscreenControl = ({ + isFullscreen, + onToggle, +}: FullscreenControlProps) => { + const Icon = isFullscreen ? Minimize : Maximize; + + const label = isFullscreen ? "Exit fullscreen" : "Enter fullscreen"; + + return ( +
+ + + +
+ ); +}; diff --git a/components/stream-player/live-video.tsx b/components/stream-player/live-video.tsx index fcb149f..3707cc6 100644 --- a/components/stream-player/live-video.tsx +++ b/components/stream-player/live-video.tsx @@ -1,5 +1,9 @@ -import { Participant } from "livekit-client"; -import React, { useRef } from "react"; +import { useTracks } from "@livekit/components-react"; +import { Participant, Track } from "livekit-client"; +import React, { useEffect, useRef, useState } from "react"; +import { FullscreenControl } from "./fullscreen-control"; +import { useEventListener } from "usehooks-ts"; +import { VolumeControl } from "./volume-control"; interface LiveVideoProps { participant: Participant; @@ -9,9 +13,73 @@ export const LiveVideo = ({ participant }: LiveVideoProps) => { const videoRef = useRef(null); const wrapperRef = useRef(null); + const [isFullscreen, setIsFullscreen] = useState(false); + const [volume, setVolume] = useState(0); + + const onVolumeChange = (value: number) => { + setVolume(+value); + + if (videoRef?.current) { + videoRef.current.muted = value === 0; + videoRef.current.volume = +value * 0.01; + } + }; + + const toggleMute = () => { + const isMuted = volume === 0; + + setVolume(isMuted ? 50 : 0); + + if (videoRef?.current) { + videoRef.current.muted = !isMuted; + videoRef.current.volume = isMuted ? 0.5 : 0; + } + }; + + useEffect(() => { + onVolumeChange(0); + }, []); + + const toggleFullscreen = () => { + if (isFullscreen) { + document.exitFullscreen(); + } else if (wrapperRef?.current) { + wrapperRef.current.requestFullscreen(); + } + }; + + const handleFullscreenChange = () => { + const isCurrentlyFullscreen = document.fullscreenElement !== null; + + setIsFullscreen(isCurrentlyFullscreen); + }; + + useEventListener("fullscreenchange", handleFullscreenChange, wrapperRef); + + useTracks([Track.Source.Camera, Track.Source.Microphone]) + .filter((track) => track.participant.identity === participant.identity) + .forEach((track) => { + if (videoRef.current) { + track.publication.track?.attach(videoRef.current); + } + }); + return (
); }; diff --git a/components/stream-player/volume-control.tsx b/components/stream-player/volume-control.tsx new file mode 100644 index 0000000..91db1a5 --- /dev/null +++ b/components/stream-player/volume-control.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Volume1, Volume2, VolumeX } from "lucide-react"; + +import { Hint } from "@/components/hint"; +import { Slider } from "@/components/ui/slider"; + +interface VolumeControlProps { + onToggle: () => void; + onChange: (value: number) => void; + value: number; +} + +export const VolumeControl = ({ + onToggle, + onChange, + value, +}: VolumeControlProps) => { + const isMuted = value === 0; + const isAboveHalf = value > 50; + + let Icon = Volume1; + + if (isMuted) { + Icon = VolumeX; + } else if (isAboveHalf) { + Icon = Volume2; + } + + const label = isMuted ? "Unmute" : "Mute"; + + const handleChange = (value: number[]) => { + onChange(value[0]); + }; + + return ( +
+ + + + +
+ ); +}; diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx new file mode 100644 index 0000000..fdc4365 --- /dev/null +++ b/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@/lib/utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/package-lock.json b/package-lock.json index be5cd2c..22b0936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", @@ -1192,6 +1193,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", + "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", diff --git a/package.json b/package.json index d74c2db..9be3913 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7",