Media Player

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 refs
  • PlayerContainer - Container for the player with fullscreen support
  • ShakaVideo - The video element component
  • ControlsContainer - Root container for all controls (exported as Controls)
  • ControlsGroup - Groups related controls
  • useVideo, 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:

SimpleCustomLayout.tsx
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:

AdvancedCustomLayout.tsx
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:

CustomControls.tsx
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.