Pulsar-Player-Controller
Provides a standardized React API for efficiently reading, updating, and subscribing to video player state for Pulsar backends.
Usage
Pulsar also provides a number of custom hooks for controlling the video player and listening for player state updates.
To use these features, you'll need to render PlayerControllerRoot
above
PulsarCore
in your application:
import type { FC } from 'react';
import { PulsarCore } from 'pulsar';
import { PlayerControllerRoot } from 'pulsar-player-controller';
const PlayerConsumer: FC = () => (
<PlayerControllerRoot>
<PulsarCore {/* ...props */} />
</PlayerControllerRoot>
);
usePlayerController
This hook allows the consumer to control player state imperatively (play / pause, volume, muted, etc).
import type { FC } from 'react';
import { usePlayerController } from 'pulsar-player-controller';
const CustomControls: FC = () => {
const controller = usePlayerController();
const pauseHandler = () => {
controller.pause();
};
return (
<button disabled={!controller} onClick={pauseHandler}>
Pause
</button>
);
};
Mocking usePlayerController For Testing
import {
PlayerController,
mockPlayerController,
} from 'pulsar-player-controller';
let mockedPlayerController: PlayerController;
jest.mock('pulsar-player-controller', () => ({
...jest.requireActual('pulsar-player-controller'),
usePlayerController: () => mockedPlayerController,
}));
beforeEach(() => {
mockedPlayerController = mockPlayerController({ pause: jest.fn() });
});
it('tells the player to pause', () => {
// some interaction that uses the player controller
expect(mockedPlayerController.pause).toHaveBeenCalledTimes(1);
});
usePlaybackState
Allows you to listen for player state changes: idle, ready, buffering, playing, ended.
import type { FC } from 'react';
import { PlayerState, usePLaybackState } from 'pulsar-player-controller';
import { Spinner } from './Spinner';
const PlayerOverlay: FC = () => {
const state = usePLaybackState();
const currentTime = usePlaybackTime();
switch (state) {
case PlayerState.BUFFERING:
return <Spinner />;
case PlayerState.ENDED:
return <EndOfContentRecommendations />;
case PlayerState.IDLE:
case PlayerState.PLAYING:
case PlayerState.READY:
null;
default:
return state as never;
}
};
usePlaybackTime
Allows you to efficiently listen for player time updates. The value returned is
an integer
number and will only be updated once per second. Due to the nature
of the underlying timeupdate
event, this will not be emitted exactly every
second.
Consumers should use this hook as close to the consumer site as possible to limit the effects of every-second component updates. Use this hook in multiple components if necessary.
import type { FC } from 'react';
import { usePlaybackTime } from 'pulsar-player-controller';
const Seekbar: FC = () => {
const controller = usePlayerController();
const currentTime = usePlaybackTime();
return <div>{`Progress ${currentTime} / ${controller.getDuration()}`}</div>;
};
useVolume / useMuted
Allows the consumer to efficiently handle Player volume / muted state changes as well as imperatively setting new values for both.
import type { FC } from 'react';
import { useMuted, useVolume } from 'pulsar-player-controller';
const VolumeControls: FC = () => {
const { setVolume, volume } = useVolume();
const { muted, setMuted } = useMuted();
return (
<>
<button
onClick={() => {
setMuted(!muted);
}}
>
{muted ? 'Unmute' : 'Mute'}
</button>
<VolumeSlider volume={muted ? 0.0 : volume} onChange={setVolume} />
</>
);
};
Captions (ClosedCaptionsRoot / useClosedCaptions)
Allows the consumer to efficiently detect, toggle, and render a custom Closed Captions UI.
import { FC, useEffect } from 'react';
import { TextCue, useClosedCaptions } from 'pulsar-player-controller';
const Captions: FC = () => {
const { available, enabled, toggle, subscribe } = useClosedCaptions();
const [captions, setCaptions] = useState<TextCue[]>([]);
useEffect(() => {
if (!enabled) {
return;
}
return subscribe((newCaptions) => {
setCaptions(newCaptions);
});
}, [enabled, subscribe]);
return (
<>
<button disabled={!available} onClick={toggle}>
{enabled ? 'Disable CC' : 'Enable CC'}
</button>
{captions.map((caption) => (
<p>{caption.line}</p>
))}
</>
);
};
const PlayerUI: FC = () => (
<PlayerControllerRoot>
<ClosedCaptionsRoot contentKey={'<playing-content-id>'}>
<Captions />
<PulsarCore />
</ClosedCaptionsRoot>
</PlayerControllerRoot>
);
usePlayerError
import { usePlayerError } from 'pulsar-player-controller';
const Player: FC = () => {
const playerError = usePlayerError();
if (playerError) {
return <div>{`Oh no, video error! Code: ${error.code}`}</div>;
}
return <PulsarCore />;
};
usePlaybackAd
import type { FC } from 'react';
import { usePlaybackAd } from 'pulsar-player-controller';
export const AdStatus: FC = () => {
const ad = usePlaybackAd();
if (!ad) {
return null;
}
return (
<p>{`Ad ${ad.cue.podPosition + 1} of ${ad.cue.podCount}. Remaining: ${
ad.remainingTimeSeconds
}`}</p>
);
};
usePlaybackQuality
import type { FC } from 'react';
import { usePlaybackAd } from 'pulsar-player-controller';
export const QualityPicker: FC = () => {
const qualityInfo = usePlaybackQuality();
if (!qualityInfo) {
return null;
}
const { options, quality, setQuality } = qualityInfo;
return (
<>
<p>{`Current Quality: ${quality.name}`}</p>
{options.map((o) => (
<div
onClick={() => {
setQuality(o);
}}
>
{o.name}
</div>
))}
</>
);
};
usePlaybackAutoplayStatus
import type { FC } from 'react';
import { usePlaybackAutoplayStatus } from 'pulsar-player-controller';
export const TapToPlay: FC = () => {
const controller = usePlayerController();
const blockedStatus = usePlaybackAutoplayStatus();
if (!blockedStatus) {
return;
}
const playHandler = () => {
controller.play();
};
const unmuteHandler = () => {
controller.setMuted(false);
};
if (blockedStatus === 'autoplay-blocked') {
return <button onClick={playHandler}>Tap To Play</button>;
}
return <button onClick={unmuteHandler}>Tap To Unmute</button>;
};