Skip to content

Commit

Permalink
feat: add use signed video hook
Browse files Browse the repository at this point in the history
  • Loading branch information
JBR90 committed Dec 15, 2022
1 parent 5e22ca0 commit 0ac460a
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 1 deletion.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"react-stately": "^3.18.0",
"react-transition-group": "^4.4.5",
"styled-components": "^5.3.6",
"swr": "^2.0.0",
"uuid": "^9.0.0",
"zod": "^3.19.1"
},
Expand Down
1 change: 1 addition & 0 deletions src/components/CMSVideo/CMSVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type CMSVideoProps = OtherVideoPlayerProps & {
const CMSVideo: FC<CMSVideoProps> = ({ video, ...rest }) => {
return (
<VideoPlayer
playbackPolicy="public"
thumbnailTime={video.video.asset.thumbTime}
playbackId={video.video.asset.playbackId}
title={video.title}
Expand Down
24 changes: 23 additions & 1 deletion src/components/VideoPlayer/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import OakError from "../../errors/OakError";
import theme, { OakColorName } from "../../styles/theme";
import errorReporter from "../../common-lib/error-reporter";
import { VideoLocationValueType } from "../../browser-lib/avo/Avo";
import LoadingSpinner from "../LoadingSpinner";

import useVideoTracking, { VideoTrackingGetState } from "./useVideoTracking";
import getTimeElapsed from "./getTimeElapsed";
import getSubtitleTrack from "./getSubtitleTrack";
import getDuration from "./getDuration";
import getPercentageElapsed from "./getPercentageElapsed";
import useSignedVideoToken, { PlaybackPolicy } from "./useSignedVideoToken";

const INITIAL_DEBUG = false;
const INITIAL_ENV_KEY = process.env.MUX_ENVIRONMENT_KEY;
Expand All @@ -27,17 +29,26 @@ export type VideoStyleConfig = {

export type VideoPlayerProps = {
playbackId: string;
playbackPolicy: PlaybackPolicy;
thumbnailTime?: number | null;
title: string;
location: VideoLocationValueType;
};

const VideoPlayer: FC<VideoPlayerProps> = (props) => {
const { playbackId, thumbnailTime: thumbTime, title, location } = props;
const {
thumbnailTime: thumbTime,
title,
location,
playbackId,
playbackPolicy,
} = props;
const mediaElRef = useRef<MuxPlayerElement>(null);
const hasTrackedEndRef = useRef(false);
const [envKey] = useState(INITIAL_ENV_KEY);
const [debug] = useState(INITIAL_DEBUG);
// const playbackId = "iVr600df01p002CIj5723gjLVh97iXs3me1bfJ8NlID02Q00";
// const playbackPolicy = "signed";

const getState: VideoTrackingGetState = () => {
const captioned = Boolean(getSubtitleTrack(mediaElRef));
Expand All @@ -57,6 +68,10 @@ const VideoPlayer: FC<VideoPlayerProps> = (props) => {
};

const videoTracking = useVideoTracking({ getState });
const tokens = useSignedVideoToken({
playbackId: playbackId,
playbackPolicy: playbackPolicy,
});

const metadata = {
"metadata-video-id": playbackId,
Expand Down Expand Up @@ -97,6 +112,10 @@ const VideoPlayer: FC<VideoPlayerProps> = (props) => {
*/
return null;
}
if (tokens.loading) {
return <LoadingSpinner />;
}

return (
<Flex $flexDirection={"column"} $width={"100%"}>
<MuxPlayer
Expand All @@ -106,6 +125,9 @@ const VideoPlayer: FC<VideoPlayerProps> = (props) => {
envKey={envKey}
metadata={metadata}
playbackId={playbackId}
tokens={
tokens.playbackToken ? { playback: tokens.playbackToken } : undefined
}
thumbnailTime={thumbTime || undefined}
customDomain={"video.thenational.academy"}
beaconCollectionDomain={"mux-litix.thenational.academy"}
Expand Down
71 changes: 71 additions & 0 deletions src/components/VideoPlayer/useSignedVideoToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { renderHook } from "@testing-library/react";

import useSignedVideoToken from "./useSignedVideoToken";

const mockUseSWR = jest.fn<{ data: unknown; error: unknown }, []>(() => ({
data: null,
error: null,
}));

jest.mock("swr", () => ({
__esModule: true,
default: (...args: []) => mockUseSWR(...args),
}));

describe("useSignedVideoToken", () => {
test("'loading' should default to false on public video", () => {
const { result } = renderHook(() =>
useSignedVideoToken({
playbackId: "123",
playbackPolicy: "public",
})
);
const { loading } = result.current;

expect(loading).toBe(false);
});
test("'loading' should default to true on signed video", () => {
const { result } = renderHook(() =>
useSignedVideoToken({
playbackId: "123",
playbackPolicy: "signed",
})
);
const { loading } = result.current;

expect(loading).toBe(true);
});
test("should return correct state on error ", () => {
mockUseSWR.mockImplementationOnce(() => ({ data: null, error: "error" }));
const { result } = renderHook(() =>
useSignedVideoToken({
playbackId: "123",
playbackPolicy: "signed",
})
);

expect(result.current).toEqual({
loading: false,
playbackToken: null,
error: "error",
});
});
test("should return correct signed playback token ", () => {
mockUseSWR.mockImplementationOnce(() => ({
data: { token: "1234" },
error: null,
}));
const { result } = renderHook(() =>
useSignedVideoToken({
playbackId: "123",
playbackPolicy: "signed",
})
);

expect(result.current).toEqual({
loading: false,
playbackToken: "1234",
error: null,
});
});
});
107 changes: 107 additions & 0 deletions src/components/VideoPlayer/useSignedVideoToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useEffect } from "react";
import useSWR from "swr";

import errorReporter from "../../common-lib/error-reporter";
import OakError from "../../errors/OakError";

const reportError = errorReporter("useSignedPlaybackId");
export const apiEndpoint =
"https://api.thenational.academy/api/signed-video-token";

export const options = {
revalidateOnFocus: false,
dedupingInterval: 6 * 60 * 60 * 1000, // Don't generate a new signedUrl unless the old one has expired
};

export type PlaybackPolicy = "public" | "signed";

type UseSignedPlaybackIdProps = {
playbackId: string;
playbackPolicy: PlaybackPolicy;
};

type UseSignedPlaybackIdReturnProps = {
loading: boolean;
error: Error | null;
playbackToken: string | null | undefined;
};

const useSignedVideoToken = ({
playbackId,
playbackPolicy,
}: UseSignedPlaybackIdProps): UseSignedPlaybackIdReturnProps => {
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const json = await res.json();
const error = new OakError({
code: "video/fetch-signed-token",
meta: {
json,
status: res.status,
statusText: res.statusText,
playbackId,
playbackPolicy,
},
});
reportError(error);
throw error;
}
return res.json();
};

const { data, error } = useSWR(
playbackPolicy === "signed"
? `${apiEndpoint}?id=${playbackId}&type=video`
: null,
fetcher,
options
);
const token = data?.token;

useEffect(() => {
if (data && !data.token) {
const error = new OakError({
code: "video/fetch-signed-token",
meta: {
data,
playbackId,
playbackPolicy,
},
});
reportError(error);
}
}, [data, playbackId, playbackPolicy]);

if (playbackPolicy === "public") {
return {
loading: false,
error: null,
playbackToken: null,
};
}

if (error) {
return {
loading: false,
error: error,
playbackToken: null,
};
}

if (token) {
return {
loading: false,
error: null,
playbackToken: playbackPolicy === "signed" ? token : undefined,
};
}

return {
loading: true,
error: null,
playbackToken: null,
};
};

export default useSignedVideoToken;
5 changes: 5 additions & 0 deletions src/errors/OakError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ERROR_CODES = [
"hubspot/invalid-email",
"hubspot/unknown",
"video/unknown",
"video/fetch-signed-token",
"hubspot/not-loaded",
"hubspot/lost-information",
"hubspot/identify-no-email",
Expand Down Expand Up @@ -105,6 +106,10 @@ const errorConfigs: Record<ErrorCode, ErrorConfig> = {
message: "Sorry this video couldn't play, please try again",
shouldNotify: true,
},
"video/fetch-signed-token": {
message: "Failed to fetch signed video token",
shouldNotify: true,
},
"preview/invalid-token": {
message: "Invalid CMS preview token provided",
responseStatusCode: 401,
Expand Down

0 comments on commit 0ac460a

Please sign in to comment.