Webhooks offer a versatile and dynamic approach to access control for both assets and livestreams. By setting up a webhook, you can validate access requests on the fly, allowing for real-time, context-sensitive decisions.

When a user requests access to content, the webhook you’ve specified will be called. This webhook can then decide whether to grant or deny access based on custom logic, such as user authentication, subscription status, or any other criteria.

This guide is written for developers using @livepeer/react in a React application.

The example below uses webhook to create and watch a gated content.

Create an Access Control Webhook Handler

Set up an endpoint (e.g., https://yourdomain.com/api/check-access) to handle the logic for granting or denying access to your assets. This endpoint should accept a POST request with a JSON payload containing the access key and webhook context.

This is an example of a payload this endpoint would receive:

POST /api/check-access
{
  "accessKey": "your-access-key",
  "context": {
    "assetId": "abcd1234",
    "userId": "user5678"
  },
  "timestamp": 1680530722502
}

The payload in context is defined by your application.

Register an Access Control Webhook

Use the Livepeer Studio dashboard to create a new webhook with the type playback.accessControl and specify the URL of your access control endpoint.

You can then use the ID of the webhook in the next step.

Create a content with a playback policy webhook

For assets

Create an asset with a playback policy webhook, passing the ID of the webhook you created in the previous step.

accessControl.tsx
import { useCreateAsset } from "@livepeer/react";

import { useCallback, useState, useMemo } from "react";
import { useDropzone } from "react-dropzone";

export const CreateAndViewAsset = () => {
  const [video, setVideo] = useState<File | undefined>();
  const {
    mutate: createAsset,
    data: asset,
    status: createStatus,
    progress,
    error: createError,
  } = useCreateAsset(
    video
      ? {
          sources: [
            {
              name: video.name,
              file: video,
              playbackPolicy: {
                type: "webhook",
                // This is the id of the webhook you created in step 2
                webhookId: "<webhook_id>",
                webhookContext: {
                  // This is the context you want to pass to your webhook
                  // It can be anything you want, and it will be passed back to your webhook
                },
              },
            },
          ] as const,
        }
      : null
  );

  const onDrop = useCallback(async (acceptedFiles: File[]) => {
    if (acceptedFiles && acceptedFiles.length > 0 && acceptedFiles?.[0]) {
      setVideo(acceptedFiles[0]);
    }
  }, []);

  const { getRootProps, getInputProps } = useDropzone({
    accept: {
      "video/*": ["*.mp4"],
    },
    maxFiles: 1,
    onDrop,
  });

  const progressFormatted = useMemo(
    () =>
      progress?.[0].phase === "failed"
        ? "Failed to process video."
        : progress?.[0].phase === "waiting"
        ? "Waiting"
        : progress?.[0].phase === "uploading"
        ? `Uploading: ${Math.round(progress?.[0]?.progress * 100)}%`
        : progress?.[0].phase === "processing"
        ? `Processing: ${Math.round(progress?.[0].progress * 100)}%`
        : null,
    [progress]
  );

  return (
    <>
      <div {...getRootProps()}>
        <input {...getInputProps()} />
        <p>Drag and drop or browse files</p>
      </div>

      {createError?.message && <p>{createError.message}</p>}

      {video ? <p>{video.name}</p> : <p>Select a video file to upload.</p>}
      {progressFormatted && <p>{progressFormatted}</p>}

      <button
        onClick={() => {
          createAsset?.();
        }}
        disabled={!createAsset || createStatus === "loading"}
      >
        Upload
      </button>
    </>
  );
};

For livestreams

Create a stream with a playback policy webhook, passing the ID of the webhook you created in the previous step.

accessControl.tsx
import { useCreateStream, useStream } from "@livepeer/react";

import { useMemo, useState } from "react";

export const AccessControl = () => {
  const [streamName, setStreamName] = useState<string>("");
  const {
    mutate: createStream,
    data: createdStream,
    status,
  } = useCreateStream(
    streamName
      ? {
          name: streamName,
          playbackPolicy: {
            type: "webhook",
            // This is the id of the webhook you created in step 2
            webhookId: "<webhook_id>",
            webhookContext: {
              // This is the context you want to pass to your webhook
              // It can be anything you want, and it will be passed back to your webhook
            },
          },
        }
      : null
  );

  const { data: stream } = useStream({
    streamId: createdStream?.id,
    refetchInterval: (stream) => (!stream?.isActive ? 5000 : false),
  });

  const isLoading = useMemo(() => status === "loading", [status]);

  return (
    <div>
      <input
        placeholder="Stream Name"
        onChange={(e) => setStreamName(e.target.value)}
      />

      <button
        onClick={() => {
          createStream?.();
        }}
        disabled={isLoading || !createStream || Boolean(stream)}
      >
        Create Gated Stream
      </button>
    </div>
  );
};

Configure the Player with an Access Key

If you are using the Livepeer React Player, you can use the accessKey prop to provide your custom access key, which is then passed to the webhook you created to verify that it is valid.

The React Player passes the access key with a header, Livepeer-Access-Key, to the backend, for WebRTC and HLS playback. For MP4 playback, it uses a query parameter, accessKey.

import { Player } from "@livepeer/react";

export const CreateAndViewAsset = () => {
  const accessKey = getAccessKeyForYourApplication();

  return <Player playbackId={playbackId} accessKey={accessKey} />;
};

Using a custom player

If you are not using the player, you will need to pass a header, Livepeer-Access-Key, when you perform WebRTC SDP negotiation, or when you play back from a m3u8 URL.

For WebRTC SDP negotation, here is an example of the header being passed:

curl -X POST \
     -H "Content-Type: application/sdp" \
     -H "Livepeer-Access-Key: your-access-key" \
     --data-binary "@sdpfile.sdp" \
     "https://livepeercdn.studio/webrtc/abcd1234"

You can also append the access key to the WebRTC URL as a query parameter, similar to:

curl -X POST \
     -H "Content-Type: application/sdp" \
     --data-binary "@sdpfile.sdp" \
     "https://livepeercdn.studio/webrtc/abcd1234?accessKey=your-access-key"

Similarly, for HLS playback, you can pass the access key in a header:

curl -X GET \
     -H "Livepeer-Access-Key: your-access-key" \
     "https://playback.livepeer.studio/asset/hls/abcd1234/index.m3u8"

If you are using HLS.js for your own custom player, you can set the access key header like this:

const hlsConfig = {
  xhrSetup: function (xhr, url) {
    xhr.setRequestHeader("Livepeer-Access-Key", "your-access-key");
  },
};

Finally, you can append the access key to the m3u8 URL as a query parameter:

curl -X GET \
     "https://playback.livepeer.studio/asset/hls/abcd1234/index.m3u8?accessKey=your-access-key"

Receive the Webhook from Livepeer

When a user attempts to access the content, Livepeer will call your access control endpoint with the access key and webhook context. Your endpoint should process the request and return a response indicating whether the stream access should be allowed or disallowed.

Here is an example request sent to your access control endpoint:

{
  "context": {
    // Same value from the `asset.playbackPolicy.webhookContext` field
  },
  "accessKey": "<access_key>" // this is exact value of the prop passed to the Player, or the query parameter
}

In your access control endpoint, implement the logic to verify the access key and decide whether to grant access to the content. If the access is allowed, return a 2XX response. Otherwise, the playback will be disallowed.