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",