Skip to content
This repository has been archived by the owner on Feb 13, 2025. It is now read-only.

Commit

Permalink
complete Live Video component (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-maker authored Dec 27, 2023
2 parents 0c276d4 + bf04c52 commit a69540e
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ yarn-error.log*

# vercel
.vercel
.turbo

# typescript
*.tsbuildinfo
Expand Down
31 changes: 31 additions & 0 deletions components/stream-player/fullscreen-control.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center justify-center gap-4">
<Hint label={label} asChild>
<button
onClick={onToggle}
className="text-white p-1.5 hover:bg-white/10 rounded-lg"
>
<Icon className="h-5 w-5" />
</button>
</Hint>
</div>
);
};
72 changes: 70 additions & 2 deletions components/stream-player/live-video.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,9 +13,73 @@ export const LiveVideo = ({ participant }: LiveVideoProps) => {
const videoRef = useRef<HTMLVideoElement>(null);
const wrapperRef = useRef<HTMLDivElement>(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 (
<div ref={wrapperRef} className="relative h-full flex">
<video ref={videoRef} width="100%" />
<div className="absolute top-0 h-full w-full opacity-0 hover:opacity-100 hover:transition-all">
<div className="absolute bottom-0 flex h-14 w-full items-center justify-between bg-gradient-to-r from-neutral-900 px-4">
<VolumeControl
onChange={onVolumeChange}
value={volume}
onToggle={toggleMute}
/>
<FullscreenControl
isFullscreen={isFullscreen}
onToggle={toggleFullscreen}
/>
</div>
</div>
</div>
);
};
55 changes: 55 additions & 0 deletions components/stream-player/volume-control.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2">
<Hint label={label} asChild>
<button
onClick={onToggle}
className="text-white hover:bg-white/10 p-1.5 rounded-lg"
>
<Icon className="h-6 w-6" />
</button>
</Hint>
<Slider
className="w-[8rem] cursor-pointer"
onValueChange={handleChange}
value={[value]}
max={100}
step={1}
/>
</div>
);
};
28 changes: 28 additions & 0 deletions components/ui/slider.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-0.5 w-full grow overflow-hidden rounded-full bg-white/10">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-3.5 w-3.5 rounded-full border-2 border-primary bg-white ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit a69540e

Please # to comment.