Skip to content

Commit

Permalink
feat: created a custom video player
Browse files Browse the repository at this point in the history
  • Loading branch information
dager-mohamed committed Aug 9, 2024
1 parent 8321b89 commit 0ce6b43
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 521 deletions.
26 changes: 11 additions & 15 deletions apps/web/src/app/live/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import '@vidstack/react/player/styles/default/theme.css';
import '@vidstack/react/player/styles/default/layouts/audio.css';
import '@vidstack/react/player/styles/default/layouts/video.css';

import Chat from '../../../components/Chat';
import LiveVideoPlayer from '../../../components/LiveVideoPlayer';

export default function LiveStreamPage() {
return (
<>
<div className="flex w-full h-screen">
<LiveVideoPlayer
src={
'https://stream.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM.m3u8'
}
poster={
'https://image.mux.com/v69RSHhFelSm4701snP22dYz2jICy4E4FUyk02rW4gxRM/thumbnail.webp?time=30'
}
/>
<Chat />
<div className="flex-1">
<LiveVideoPlayer
src={
'http://localhost:8080/hls/test.m3u8' // just for testing
}

/>
</div>
<div className="">
<Chat />
</div>
</div>
</>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SendIcon } from 'lucide-react';

export default function Chat() {
return(
<div className="bg-background border-l flex flex-col w-[400px]">
<div className="bg-background h-full border-l flex flex-col w-[400px]">
<div className="border-b px-4 py-3 text-lg font-medium">Live Chat</div>
<div className="flex-1 overflow-auto p-4 space-y-4">
<div className="flex items-start gap-3">
Expand Down
168 changes: 146 additions & 22 deletions apps/web/src/components/LiveVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,157 @@
import { MediaPlayer, MediaProvider, PlayerSrc, Poster } from '@vidstack/react';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
'use client';
import { useRef, useState, useEffect } from 'react';
import Hls from 'hls.js';
import { Badge, Button } from '@org/shared';
import {
DefaultVideoLayout,
defaultLayoutIcons,
} from '@vidstack/react/player/layouts/default';
MaximizeIcon,
PlayIcon,
PauseIcon,
VolumeXIcon,
Volume2Icon,
MinimizeIcon,
} from 'lucide-react';

interface VideoPlayerProps {
src: PlayerSrc;
poster: string;
src: string;
}

export default function LiveVideoPlayer({ src, poster }: VideoPlayerProps) {
export default function LiveVideoPlayer({ src }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const hlsRef = useRef<Hls | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);

useEffect(() => {
const hls = new Hls();
if (Hls.isSupported()) {
hls.loadSource(src);
hls.attachMedia(videoRef.current!);
hlsRef.current = hls;
hls.on(Hls.Events.MANIFEST_PARSED, () => {
videoRef.current!.muted = true;
setIsMuted(true);

videoRef.current?.play();
setIsPlaying(true);

videoRef.current!.muted = false;
setIsMuted(false);
});
} else if (videoRef.current?.canPlayType('application/vnd.apple.mpegurl')) {
// For Safari, where HLS is natively supported
videoRef.current.src = src;
}

return () => {
if (Hls.isSupported()) {
hls.destroy();
}
};
}, [src]);

const handlePlayPause = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
// When resuming playback, check if the video should jump to the live edge
if (hlsRef.current) {
const liveEdge = videoRef.current.duration - 10; // 10 seconds before the live edge
const currentTime = videoRef.current.currentTime;

// Check if the current time is far from the live edge
if (currentTime < liveEdge) {
videoRef.current.currentTime = liveEdge;
}
}
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};

const handleMuteUnmute = () => {
if (videoRef.current) {
videoRef.current.muted = !isMuted;
setIsMuted(!isMuted);
}
};

const handleFullscreen = () => {
if (containerRef.current) {
if (!isFullscreen) {
if (containerRef.current.requestFullscreen) {
containerRef.current.requestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
setIsFullscreen(!isFullscreen);
}
};

return (
<div className="w-full h-full" style={{ maxWidth: '80%' }}>
<MediaPlayer
src={src}
poster={poster}
viewType="video"
streamType="live"
className="h-full"
logLevel="warn"
crossOrigin
liveEdgeTolerance={6}
<div ref={containerRef} className="relative w-full h-full">
<video
ref={videoRef}
className="w-full h-full object-cover"
controls={false}
controlsList="nodownload"
playsInline
>
<MediaProvider>
<Poster className="vds-poster" />
</MediaProvider>
<DefaultVideoLayout icons={defaultLayoutIcons} />
</MediaPlayer>
<source src={src} type="application/x-mpegURL" />
</video>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/50 to-transparent flex items-center justify-between p-4 space-x-3 text-white">
<div className="flex items-center space-x-3">
<Button
size="icon"
variant="ghost"
className="w-9 h-9 hover:bg-white/10"
onClick={handlePlayPause}
>
{isPlaying ? (
<PauseIcon className="w-6 h-6 fill-white" />
) : (
<PlayIcon className="w-6 h-6 fill-white" />
)}
</Button>
<Button
size="icon"
variant="ghost"
className="w-9 h-9 hover:bg-white/10"
onClick={handleMuteUnmute}
>
{isMuted ? (
<VolumeXIcon className="w-6 h-6 fill-white" />
) : (
<Volume2Icon className="w-6 h-6 fill-white" />
)}
</Button>
<Badge
variant="outline"
className="bg-red-500 text-white px-2 py-1 rounded-sm border-none text-xs"
>
Live
</Badge>
</div>
<Button
size="icon"
variant="ghost"
className="w-9 h-9 hover:bg-white/10"
onClick={handleFullscreen}
>
{isFullscreen ? (
<MinimizeIcon className="w-6 h-6 fill-white" />
) : (
<MaximizeIcon className="w-6 h-6 fill-white" />
)}
</Button>
</div>
</div>
);
}
Loading

0 comments on commit 0ce6b43

Please sign in to comment.