Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added further ui for clipping #388

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/old-grapes-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@livepeer/core': patch
'@livepeer/core-react': patch
'livepeer': patch
'@livepeer/react': patch
'@livepeer/react-native': patch
---

**Feature:** added `onClipStarted` and ensured overridden `liveSyncDurationCount` in HLS config does not throw errors in HLS.js.
53 changes: 39 additions & 14 deletions examples/next-13/app/PlayerPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Button } from '@livepeer/design-system';
import { Asset, Player, PlayerProps } from '@livepeer/react';
import { Asset, Player, PlayerProps, useAsset } from '@livepeer/react';
import * as Popover from '@radix-ui/react-popover';

import mux from 'mux-embed';
Expand Down Expand Up @@ -37,7 +37,8 @@ const controls = {

export default (props: PlayerProps<object, any>) => {
const [open, setOpen] = useState(false);
const [clipPlaybackId, setClipPlaybackId] = useState<string | null>(null);
const [clipAssetId, setClipAssetId] = useState<string | null>(null);
const [clipDownloadUrl, setClipDownloadUrl] = useState<string | null>(null);
const timerRef = useRef(0);

useEffect(() => {
Expand All @@ -50,11 +51,30 @@ export default (props: PlayerProps<object, any>) => {
}
}, []);

const { data: clippedAsset } = useAsset({
assetId: clipAssetId ?? undefined,
refetchInterval: (asset) => (!asset?.downloadUrl ? 2000 : false),
});

useEffect(() => {
if (clippedAsset?.downloadUrl) {
setOpen(false);
window?.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setClipDownloadUrl(clippedAsset.downloadUrl ?? null);
setOpen(true);
}, 100);
}
}, [clippedAsset]);

const onClipCreated = useCallback((asset: Asset) => {
setClipAssetId(asset.id ?? null);
}, []);

const onClipStarted = useCallback(() => {
setOpen(false);
window?.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setClipPlaybackId(asset.playbackId ?? null);
setOpen(true);
}, 100);
}, []);
Expand Down Expand Up @@ -99,6 +119,7 @@ export default (props: PlayerProps<object, any>) => {
showPipButton
priority
theme={theme}
onClipStarted={onClipStarted}
onClipCreated={onClipCreated}
controls={controls}
mediaElementRef={mediaElementRef}
Expand Down Expand Up @@ -255,17 +276,21 @@ export default (props: PlayerProps<object, any>) => {
</div>
)}
<ToastRoot open={open} onOpenChange={setOpen}>
<ToastTitle>Livestream clipped</ToastTitle>
<ToastDescription>Your clip has been created.</ToastDescription>
<ToastAction asChild altText="Open clip in new tab">
<a
target="_blank"
rel="noopener noreferrer"
href={`/?v=${clipPlaybackId}`}
>
<Button size="1">Open in new tab</Button>
</a>
</ToastAction>
<ToastTitle>
{!clipDownloadUrl ? 'Clip loading' : 'Livestream clipped'}
</ToastTitle>
<ToastDescription>
{!clipDownloadUrl
? 'Your clip is being processed in the background...'
: 'Your clip has been created.'}
</ToastDescription>
{clipDownloadUrl && (
<ToastAction asChild altText="Download clip">
<a target="_blank" rel="noopener noreferrer" href={clipDownloadUrl}>
<Button size="1">Download clip</Button>
</a>
</ToastAction>
)}
</ToastRoot>
<ToastViewport />
</ToastProvider>
Expand Down
64 changes: 48 additions & 16 deletions examples/next-13/app/clipping/[key]/ClippingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button, Label, TextField } from '@livepeer/design-system';
import {
MediaControllerCallbackState,
Player,
useAsset,
useCreateClip,
} from '@livepeer/react';

Expand All @@ -24,10 +25,18 @@ export type ClippingPageProps = {
playbackId: string;
};

const hlsConfig = {
liveSyncDurationCount: Number.MAX_VALUE - 10,
};

export default (props: ClippingPageProps) => {
const [open, setOpen] = useState(false);
const [clipDownloadUrl, setClipDownloadUrl] = useState<string | null>(null);
const timerRef = useRef(0);
const [clipPlaybackId, setClipPlaybackId] = useState<string | null>(null);

useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);

const [playbackStatus, setPlaybackStatus] = useState<{
duration: number;
Expand All @@ -47,6 +56,8 @@ export default (props: ClippingPageProps) => {
[],
);

const onError = useCallback((error: Error) => console.log(error), []);

const [startTime, setStartTime] = useState<string | null>(null);
const [endTime, setEndTime] = useState<string | null>(null);

Expand All @@ -61,17 +72,32 @@ export default (props: ClippingPageProps) => {
});

useEffect(() => {
if (clipAsset) {
if (isLoading) {
setStartTime(null);
setEndTime(null);
setOpen(false);
window?.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setClipPlaybackId(clipAsset?.playbackId ?? null);
setOpen(true);
}, 100);
}
}, [isLoading]);

const { data: clippedAsset } = useAsset({
assetId: clipAsset?.id ?? undefined,
refetchInterval: (asset) => (!asset?.downloadUrl ? 2000 : false),
});

return () => window?.clearTimeout(timerRef.current);
useEffect(() => {
if (clippedAsset?.downloadUrl) {
setOpen(false);
window?.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setClipDownloadUrl(clippedAsset.downloadUrl ?? null);
setOpen(true);
}, 100);
}
}, [clipAsset]);
}, [clippedAsset]);

return (
<ToastProvider>
Expand All @@ -93,6 +119,8 @@ export default (props: ClippingPageProps) => {
playbackId={props.playbackId}
playbackStatusSelector={playbackStatusSelector}
onPlaybackStatusUpdate={onPlaybackStatusUpdate}
onError={onError}
hlsConfig={hlsConfig}
/>
<div style={{ position: 'absolute', top: 20, right: 20, zIndex: 10 }}>
<Button
Expand Down Expand Up @@ -168,17 +196,21 @@ export default (props: ClippingPageProps) => {
</form>
</div>
<ToastRoot open={open} onOpenChange={setOpen}>
<ToastTitle>Livestream clipped</ToastTitle>
<ToastDescription>Your clip has been created.</ToastDescription>
<ToastAction asChild altText="Open clip in new tab">
<a
target="_blank"
rel="noopener noreferrer"
href={`/?v=${clipPlaybackId ?? ''}`}
>
<Button size="1">Open in new tab</Button>
</a>
</ToastAction>
<ToastTitle>
{!clipDownloadUrl ? 'Clip loading' : 'Livestream clipped'}
</ToastTitle>
<ToastDescription>
{!clipDownloadUrl
? 'Your clip is being processed in the background...'
: 'Your clip has been created.'}
</ToastDescription>
{clipDownloadUrl && (
<ToastAction asChild altText="Download clip">
<a target="_blank" rel="noopener noreferrer" href={clipDownloadUrl}>
<Button size="1">Download clip</Button>
</a>
</ToastAction>
)}
</ToastRoot>
<ToastViewport />
</ToastProvider>
Expand Down
18 changes: 10 additions & 8 deletions examples/next-13/app/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { keyframes, styled } from '@livepeer/design-system';
import * as Toast from '@radix-ui/react-toast';

const VIEWPORT_PADDING = 25;
const VIEWPORT_Y_PADDING = 50;
const VIEWPORT_X_PADDING = 15;

export const ToastProvider = Toast.Provider;

Expand All @@ -11,9 +12,10 @@ export const ToastViewport = styled(Toast.Viewport, {
right: 0,
display: 'flex',
flexDirection: 'column',
padding: VIEWPORT_PADDING,
paddingBottom: VIEWPORT_Y_PADDING,
paddingRight: VIEWPORT_X_PADDING,
gap: 10,
width: 390,
width: 320,
maxWidth: '100vw',
margin: 0,
listStyle: 'none',
Expand All @@ -27,17 +29,17 @@ const hide = keyframes({
});

const slideIn = keyframes({
from: { transform: `translateX(calc(100% + ${VIEWPORT_PADDING}px))` },
from: { transform: `translateX(calc(100% + ${VIEWPORT_X_PADDING}px))` },
to: { transform: 'translateX(0)' },
});

const swipeOut = keyframes({
from: { transform: 'translateX(var(--radix-toast-swipe-end-x))' },
to: { transform: `translateX(calc(100% + ${VIEWPORT_PADDING}px))` },
to: { transform: `translateX(calc(100% + ${VIEWPORT_X_PADDING}px))` },
});

export const ToastRoot = styled(Toast.Root, {
backgroundColor: '$slate12',
backgroundColor: '$gray12',
borderRadius: 6,
boxShadow:
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
Expand Down Expand Up @@ -70,14 +72,14 @@ export const ToastTitle = styled(Toast.Title, {
gridArea: 'title',
marginBottom: 5,
fontWeight: 500,
color: '$slate1',
color: '$gray1',
fontSize: 15,
});

export const ToastDescription = styled(Toast.Description, {
gridArea: 'description',
margin: 0,
color: '$slate1',
color: '$gray1',
fontSize: 13,
lineHeight: 1.3,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export type PlayerProps<
/** The length of a clip in seconds to generate. Set to a number to enable (web only). */
clipLength?: ClipLength;

/** Callback when the clip button is triggered. */
onClipStarted?: () => Promise<any> | any;
/** Callback when a clip is created from the clip button. */
onClipCreated?: (asset: Asset) => Promise<any> | any;
/** Callback when a clip fails to be created from the clip button. */
Expand Down Expand Up @@ -199,6 +201,7 @@ export const usePlayer = <
clipLength,
onClipCreated,
onClipError,
onClipStarted,

viewerId,

Expand Down Expand Up @@ -394,6 +397,7 @@ export const usePlayer = <
clipLength,
onClipCreated,
onClipError,
onClipStarted,
}),
[
autoPlay,
Expand All @@ -404,6 +408,7 @@ export const usePlayer = <
clipLength,
onClipCreated,
onClipError,
onClipStarted,
],
);

Expand Down
2 changes: 2 additions & 0 deletions packages/core-web/src/media/browser/controls/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ const addEffectsToStore = <
const startTime = estimatedServerClipTime - clipLength * 1000;
const endTime = estimatedServerClipTime;

current?.onClipStarted?.();

previousPromise = createClip({
playbackId,
startTime,
Expand Down
10 changes: 8 additions & 2 deletions packages/core-web/src/media/browser/hls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@
const hls = new Hls({
maxBufferLength: 15,
maxMaxBufferLength: 60,
liveMaxLatencyDurationCount: 7,
liveSyncDurationCount: 3,
...config,
...(config?.liveSyncDurationCount
? {
liveSyncDurationCount: config.liveSyncDurationCount,
}
: {
liveMaxLatencyDurationCount: 7,
liveSyncDurationCount: 3,
}),
});

const onDestroy = () => {
Expand All @@ -70,28 +76,28 @@
hls.attachMedia(element);
}

hls.on(Hls.Events.LEVEL_LOADED, async (_e, data) => {

Check warning on line 79 in packages/core-web/src/media/browser/hls/index.ts

View workflow job for this annotation

GitHub Actions / Lint (16, 7)

Caution: `Hls` also has a named export `Events`. Check if you meant to write `import {Events} from 'hls.js'` instead
const { live, totalduration: duration } = data.details;

callbacks?.onLive?.(Boolean(live));
callbacks?.onDuration?.(duration ?? 0);
});

hls.on(Hls.Events.MEDIA_ATTACHED, async () => {

Check warning on line 86 in packages/core-web/src/media/browser/hls/index.ts

View workflow job for this annotation

GitHub Actions / Lint (16, 7)

Caution: `Hls` also has a named export `Events`. Check if you meant to write `import {Events} from 'hls.js'` instead
hls.loadSource(source);

hls.on(Hls.Events.MANIFEST_PARSED, (_event, _data) => {

Check warning on line 89 in packages/core-web/src/media/browser/hls/index.ts

View workflow job for this annotation

GitHub Actions / Lint (16, 7)

Caution: `Hls` also has a named export `Events`. Check if you meant to write `import {Events} from 'hls.js'` instead
callbacks?.onCanPlay?.();
});
});

hls.on(Hls.Events.ERROR, async (_event, data) => {

Check warning on line 94 in packages/core-web/src/media/browser/hls/index.ts

View workflow job for this annotation

GitHub Actions / Lint (16, 7)

Caution: `Hls` also has a named export `Events`. Check if you meant to write `import {Events} from 'hls.js'` instead
const { details, fatal } = data;

hls.detachMedia();

const isManifestParsingError =
Hls.ErrorTypes.NETWORK_ERROR && details === 'manifestParsingError';

Check warning on line 100 in packages/core-web/src/media/browser/hls/index.ts

View workflow job for this annotation

GitHub Actions / Lint (16, 7)

Caution: `Hls` also has a named export `ErrorTypes`. Check if you meant to write `import {ErrorTypes} from 'hls.js'` instead

if (!fatal && !isManifestParsingError) return;
callbacks?.onError?.(data);
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/media/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const omittedKeys = [
'onPause',
'onClipCreated',
'onClipError',
'onClipStarted',
'_updatePlaybackOffsetMs',
] as const;

Expand Down Expand Up @@ -139,6 +140,8 @@ export type MediaControllerState<TElement = void, TMediaStream = void> = {
/** The length (in seconds) of the clip to create from instant clipping. */
clipLength?: ClipLength;
/** Callback when a clip is created from the clip button. */
onClipStarted?: () => Promise<any> | any;
/** Callback when a clip is created from the clip button. */
onClipCreated?: (asset: Asset) => Promise<any> | any;
/** Callback when a clip fails to be created from the clip button. */
onClipError?: (error: any) => Promise<any> | any;
Expand Down Expand Up @@ -352,6 +355,7 @@ export const createControllerStore = <TElement, TMediaStream>({
clipLength: mediaProps.clipLength,
onClipCreated: mediaProps.onClipCreated,
onClipError: mediaProps.onClipError,
onClipStarted: mediaProps.onClipStarted,

playbackOffsetMs: 0,

Expand Down Expand Up @@ -568,6 +572,7 @@ export type MediaPropsOptions = {
viewerId?: string;
clipLength?: ClipLength;

onClipStarted?: () => Promise<any> | any;
onClipCreated?: (asset: Asset) => Promise<any> | any;
onClipError?: (error: any) => Promise<any> | any;

Expand Down
Loading