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

Commit

Permalink
feat: Viewer token & Video Player (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-maker authored Dec 25, 2023
2 parents 18d8ee1 + 397d90e commit 0c276d4
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 4 deletions.
51 changes: 51 additions & 0 deletions actions/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use server";

import { getSelf } from "@/lib/auth-service";
import { isBlockedByUser } from "@/lib/block-service";
import { getUserById } from "@/lib/user-service";
import { AccessToken } from "livekit-server-sdk";
import { v4 } from "uuid";

export const createViewerToken = async (hostIdentity: string) => {
let self;

try {
self = await getSelf();
} catch {
const id = v4();
const username = `guest#${Math.floor(Math.random() * 1000)}`;
self = { id, username };
}

const host = await getUserById(hostIdentity);

if (!host) {
throw new Error("User not Found");
}

const isBlocked = await isBlockedByUser(host.id);

if (isBlocked) {
throw new Error("User is blocked");
}

const isHost = self.id === host.id;

const token = new AccessToken(
process.env.LIVEKIT_API_KEY!,
process.env.LIVEKIT_API_SECRET!,
{
identity: isHost ? `host-${self.id}` : self.id,
name: self.username,
}
);

token.addGrant({
room: host.id,
roomJoin: true,
canPublish: false,
canPublishData: true,
});

return await Promise.resolve(token.toJwt());
};
9 changes: 7 additions & 2 deletions app/(dashboard)/u/[username]/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StreamPlayer } from "@/components/stream-player";
import { getUserByUsername } from "@/lib/user-service";
import { currentUser } from "@clerk/nextjs";

Expand All @@ -11,11 +12,15 @@ const CreatorPage = async ({ params }: CreatorPageProps) => {
const externalUser = await currentUser();
const user = await getUserByUsername(params.username);

if (!user || user.externalUserId !== externalUser?.id) {
if (!user || user.externalUserId !== externalUser?.id || !user.stream) {
throw new Error("Unauthorized");
}

return <div className="h-full">CreatorPage</div>;
return (
<div className="h-full">
<StreamPlayer user={user} stream={user.stream} isFollowing />
</div>
);
};

export default CreatorPage;
51 changes: 51 additions & 0 deletions components/stream-player/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { useViewerToken } from "@/hooks/use-viewer-token";
import { Stream, User } from "@prisma/client";
import { LiveKitRoom } from "@livekit/components-react";
import { cn } from "@/lib/utils";
import { Video, VideoSkeleton } from "./video";

interface StreamPlayerProps {
user: User & { stream: Stream | null };
stream: Stream;
isFollowing: boolean;
}

export const StreamPlayer = ({
user,
stream,
isFollowing,
}: StreamPlayerProps) => {
const { token, name, identity } = useViewerToken(user.id);

if (!token || !name || !identity) {
return <StreamPlayerSkeleton />;
}

return (
<>
<LiveKitRoom
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_WS_URL}
token={token}
className="grid grid-cols-1 lg:gap-y-0 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-6 h-full"
>
<div className="space-y-4 col-span-1 lg:col-span-2 xl:col-span-2 2xl:col-span-5 lg:overflow-y-auto hidden-scrollbar pb-10">
<Video hostName={user.username} hostIdentity={user.id} />
</div>
</LiveKitRoom>
</>
);
};

export const StreamPlayerSkeleton = () => {
return (
<div className="grid grid-cols-1 lg:gap-y-0 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-6 h-full">
<div className="space-y-4 col-span-1 lg:col-span-2 xl:col-span-2 2xl:col-span-5 lg:overflow-y-auto hidden-scrollbar pb-10">
<VideoSkeleton />
{/*<HeaderSkeleton />*/}
</div>
<div className="col-span-1 bg-background">{/*<ChatSkeleton />*/}</div>
</div>
);
};
17 changes: 17 additions & 0 deletions components/stream-player/live-video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Participant } from "livekit-client";
import React, { useRef } from "react";

interface LiveVideoProps {
participant: Participant;
}

export const LiveVideo = ({ participant }: LiveVideoProps) => {
const videoRef = useRef<HTMLVideoElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);

return (
<div ref={wrapperRef} className="relative h-full flex">
<video ref={videoRef} width="100%" />
</div>
);
};
15 changes: 15 additions & 0 deletions components/stream-player/loading-video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Loader } from "lucide-react";
import React from "react";

interface LoadingVideoProps {
label: string;
}

export const LoadingVideo = ({ label }: LoadingVideoProps) => {
return (
<div className="h-full flex flex-col space-y-4 justify-center items-center">
<Loader className="h-10 w-10 text-muted-foreground animate-spin" />
<p className="text-muted-foreground capitalize">{label}</p>
</div>
);
};
14 changes: 14 additions & 0 deletions components/stream-player/offline-video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { WifiOff } from "lucide-react";

interface OfflineVideoProps {
username: string;
}

export const OfflineVideo = ({ username }: OfflineVideoProps) => {
return (
<div className="h-full flex flex-col space-y-4 justify-center items-center">
<WifiOff className="h-10 w-10 text-muted-foreground" />
<p className="text-muted-foreground">{username} is offline</p>
</div>
);
};
46 changes: 46 additions & 0 deletions components/stream-player/video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { ConnectionState, Track } from "livekit-client";
import {
useConnectionState,
useRemoteParticipant,
useTracks,
} from "@livekit/components-react";
import { OfflineVideo } from "./offline-video";
import { LoadingVideo } from "./loading-video";
import { LiveVideo } from "./live-video";
import { Skeleton } from "../ui/skeleton";

interface VideoProps {
hostName: string;
hostIdentity: string;
}

export const Video = ({ hostName, hostIdentity }: VideoProps) => {
const connectionState = useConnectionState();
const participant = useRemoteParticipant(hostIdentity);
const tracks = useTracks([
Track.Source.Camera,
Track.Source.Microphone,
]).filter((track) => track.participant.identity === hostIdentity);

let content;

if (!participant && connectionState === ConnectionState.Connected) {
content = <OfflineVideo username={hostName} />;
} else if (!participant || tracks.length === 0) {
content = <LoadingVideo label={connectionState} />;
} else {
content = <LiveVideo participant={participant} />;
}

return <div className="aspect-video border-b group relative">{content}</div>;
};

export const VideoSkeleton = () => {
return (
<div className="aspect-video border-x border-background">
<Skeleton className="h-full w-full rounded-none" />
</div>
);
};
39 changes: 39 additions & 0 deletions hooks/use-viewer-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createViewerToken } from "@/actions/token";
import { JwtPayload, jwtDecode } from "jwt-decode";
import { useEffect, useState } from "react";
import { toast } from "sonner";

export const useViewerToken = (hostIdentity: string) => {
const [token, setToken] = useState("");
const [name, setName] = useState("");
const [identity, setIdentity] = useState("");

useEffect(() => {
const createToken = async () => {
try {
const viewerToken = await createViewerToken(hostIdentity);
setToken(viewerToken);

const decodedToken = jwtDecode(viewerToken) as JwtPayload & {
name?: string;
};
const name = decodedToken?.name;
const identity = decodedToken.jti;

if (identity) {
setIdentity(identity);
}

if (name) {
setName(name);
}
} catch (error) {
toast.error("Something went wrong");
}
};

createToken();
}, [hostIdentity]);

return { token, name, identity };
};
16 changes: 16 additions & 0 deletions lib/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ export const getUserByUsername = async (username: string) => {
where: {
username,
},
include: {
stream: true,
},
});

return user;
};

export const getUserById = async (id: string) => {
const user = await db.user.findUnique({
where: {
id,
},
include: {
stream: true,
},
});

return user;
Expand Down
17 changes: 15 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
images: {
domains: ["utfs.io"],
},
webpack: (config) => {
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: "javascript/auto",
});

module.exports = nextConfig
return config;
},
};

module.exports = nextConfig;
29 changes: 29 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jwt-decode": "^4.0.0",
"livekit-client": "^1.15.4",
"livekit-server-sdk": "^1.2.7",
"lucide-react": "^0.294.0",
Expand All @@ -34,12 +35,14 @@
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.9.1",
"uuid": "^9.0.1",
"zustand": "^4.4.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^9.0.7",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.4",
Expand Down

0 comments on commit 0c276d4

Please # to comment.