Custom Layout
Learn how to build custom player layouts using primitive components
Overview
In this guide, we will walk you through building a custom player layout using the primitive components provided by the Shelby Media Player.
This guide assumes you have completed the Basic Usage Guide and understand the fundamentals of the player.
Getting Started
Understanding the Component Structure
The Shelby Media Player uses a composition-based architecture. The main components are:
MediaProvider- Provides media state management (from media-chrome)PlayerProvider- Provides player context and refsPlayerContainer- Container for the player with fullscreen supportShakaVideo- The video element componentControlsContainer- Root container for all controls (exported asControls)ControlsGroup- Groups related controlsuseVideo,useShaka- Hooks to access player refs and API
Basic Custom Layout
Let's start with a simple custom layout that includes only play/pause and time display:
import {
MediaProvider,
PlayerProvider,
PlayerContainer,
ShakaVideo,
ControlsContainer,
ControlsGroup,
PlayButton,
CurrentTimeDisplay,
} from "@shelby-protocol/player";
function SimpleCustomLayout() {
return (
<ControlsContainer>
<ControlsGroup className="p-4">
<PlayButton />
<CurrentTimeDisplay />
</ControlsGroup>
</ControlsContainer>
);
}
export function SimpleVideoPlayer() {
return (
<MediaProvider>
<PlayerProvider>
<PlayerContainer>
<ShakaVideo
src="https://example.com/video.m3u8"
poster="https://example.com/poster.jpg"
className="w-full"
/>
<SimpleCustomLayout />
</PlayerContainer>
</PlayerProvider>
</MediaProvider>
);
}Adding More Controls
Now let's add more controls including volume, time slider, and fullscreen:
import {
MediaProvider,
PlayerProvider,
PlayerContainer,
ShakaVideo,
ControlsContainer,
ControlsGroup,
PlayButton,
MuteButton,
FullscreenButton,
Seekbar,
CurrentTimeDisplay,
MediaTitle,
} from "@shelby-protocol/player";
function AdvancedCustomLayout({ title }: { title?: string }) {
return (
<ControlsContainer>
<div className="flex-1" />
<ControlsGroup className="px-4 w-full">
<Seekbar />
</ControlsGroup>
<ControlsGroup className="p-4">
<PlayButton />
<MuteButton />
<CurrentTimeDisplay />
<MediaTitle title={title} />
<div className="flex-1" />
<FullscreenButton />
</ControlsGroup>
</ControlsContainer>
);
}
export function AdvancedVideoPlayer() {
return (
<MediaProvider>
<PlayerProvider>
<PlayerContainer>
<ShakaVideo
src="https://example.com/video.m3u8"
poster="https://example.com/poster.jpg"
className="w-full"
/>
<AdvancedCustomLayout title="My Video" />
</PlayerContainer>
</PlayerProvider>
</MediaProvider>
);
}Using Player Hooks
You can access player state and create custom controls using the useVideo hook and media-chrome's useMediaSelector:
import {
ControlsContainer,
ControlsGroup,
useVideo,
useMediaSelector,
} from "@shelby-protocol/player";
import { MediaActionTypes, useMediaDispatch } from "media-chrome/react/media-store";
function CustomControls() {
const video = useVideo();
const dispatch = useMediaDispatch();
const mediaPaused = useMediaSelector(
(state) => typeof state.mediaPaused !== "boolean" || state.mediaPaused
);
const mediaCurrentTime = useMediaSelector((state) => state.mediaCurrentTime ?? 0);
const mediaDuration = useMediaSelector((state) => state.mediaDuration ?? 0);
const mediaVolumeLevel = useMediaSelector((state) => state.mediaVolumeLevel);
const progress = mediaDuration > 0 ? (mediaCurrentTime / mediaDuration) * 100 : 0;
const isMuted = mediaVolumeLevel === "off";
const togglePlay = () => {
const type = mediaPaused
? MediaActionTypes.MEDIA_PLAY_REQUEST
: MediaActionTypes.MEDIA_PAUSE_REQUEST;
dispatch({ type });
};
return (
<ControlsContainer>
<ControlsGroup className="p-4">
<button onClick={togglePlay}>
{mediaPaused ? "Play" : "Pause"}
</button>
<div className="flex-1 mx-4">
<div className="w-full bg-gray-300 h-2 rounded">
<div
className="bg-blue-500 h-2 rounded"
style={{ width: `${progress}%` }}
/>
</div>
</div>
<span>{Math.floor(mediaCurrentTime)}s / {Math.floor(mediaDuration)}s</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={isMuted ? 0 : (video?.volume ?? 1)}
onChange={(e) => {
if (video) {
video.volume = parseFloat(e.target.value);
}
}}
/>
</ControlsGroup>
</ControlsContainer>
);
}Conclusion
You now know how to build custom player layouts! The composition-based architecture gives you full control over the player interface while maintaining all the functionality of the underlying Shaka Player.