1.3.0 โ€ข Published 5 months ago

sanity-plugin-media-video v1.3.0

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

Sanity Plugin Media Video

A Sanity plugin for adding a media object (Image/Video) to your sanity studio schemas and displaying the media with built-in functionalities such as auto-play, custom PiP on scroll, etc.

This is a Sanity Studio v3 plugin.

๐Ÿ”Œ Installation

Install peer dependencies such as zod and sanity-plugin-mux-input as well since this uses Mux as part of the video encoding service.

npm install sanity-plugin-media-video sanity-plugin-mux-input zod

๐Ÿง‘โ€๐Ÿ’ป Usage

Add it as a plugin in sanity.config.ts (or .js):

Basic configuration

// sanity.config.ts

import { defineConfig } from 'sanity';
import { mediaVideoPlugin } from 'sanity-plugin-media-video';

export default defineConfig({
  //...
  plugins: [
    // Add the muxInput from `sanity-plugin-mux-input` to make the mux 3rd party integration work
    muxInput({
      // your optional mux config here. Refer to this link for more info: https://github.com/sanity-io/sanity-plugin-mux-input?tab=readme-ov-file#configuring-mux-video-uploads
    }),

    // Add the mediaVideo plugin
    mediaVideoPlugin({
      // your optional configuration here
    }),
  ],
});

The plugin adds a new object type called media. Simply add it as a type to one of your defined fields like so

export default defineType({
  name: 'my-section',
  title: 'My Example Section',
  type: 'object',
  fields: [
    // ...your-other-fields
    defineField({
      name: 'my-custom-media-field',
      title: 'My Custom Media Field',
      type: 'media',
    }),
    // ...your-other-fields
  ],
});

Structure would look something like this:

โš™๏ธ Plugin Configuration

This is the main configuration of the plugin. The available options are:

{
  // Optional boolean to enable/disable required validation on the image field
  isImageRequired?: boolean
}

๐ŸŒ Localization

This plugin uses the Studio UI Localization resource bundle, it is now possible to localize the fields to fit your needs.

Here is the default English bundle:

{
  'image.title': 'Image',
  'image.description': 'Serves as the image preview of the video',
  'image.required.title': 'Image is required',
  'image.altText.title': 'Alt Text',
  'image.altText.description':
    'Set an alternative text for accessibility purposes',
  'enableVideo.title': 'Enable Video',
  'enableVideo.description': 'Toggle to enable video',
  'videoType.title': 'Video Type',
  'videoType.link.title': 'Link',
  'videoType.mux.title': 'Mux',
  'videoType.required.title': 'Video Type is required',
  'isAutoPlay.title': 'Auto Play',
  'isAutoPlay.description': 'Automatically play the video when loaded',
  'isPipAutomatic.title': 'Enable Automatic PiP for Autoplay',
  'isPipAutomatic.description':
    'This automatically creates a small floating video player when you scroll past the main video',
  'videoUrl.title': 'Video Link',
  'videoUrl.required.title': 'Video Link is required',
  'muxVideo.required.title': 'Mux Video is required',
}

Available locales to be overriden

  • en-US
  • de-DE

If you want to override or add a new language, you will need to create a custom bundle with your desired translations. In order to override/add you must use mediaVideo as the namespace and add it to the i18n object in your sanity plugin configuration. Here is an example:

const myEnglishOverride = defineLocaleResourceBundle({
  // make sure the `locale` language code corresponds to the one you want to override
  locale: 'en-US',
  namespace: 'mediaVideo',
  resources: {
    'image.title': 'This is my override title',
  },
});

// sanity.config.ts
export default defineConfig({
  // ...
  i18n: {
    bundles: [myEnglishOverride],
  },
});

๐ŸŽฌ How to render the media video on your website

This plugin gives out a ready out of the box made component for rendering the media video on your website with built-in functionalities such as auto-play, custom PiP on scroll, etc. One way to render it is to use the provided renderer component or you can implement your own renderer component with the data that you get from the media object.

NOTE

  • This plugin uses the react-player library for rendering the media video.
  • The renderer also needs to be wrapped within the provided custom Provider component in order to function properly.

1. Import the Provider component and wrap the renderer component with it. Suggested to be placed in the root of your application.

import { MediaVideoProvider } from 'sanity-plugin-media-video/contexts';

// other code

<body>
  <MediaVideoProvider>
    {/* Your renderer component here or other components */}
  </MediaVideoProvider>
</body>;

2. Use the renderer component like so. Example usage below

You will need to require the CSS file from this package (or provide your own). The example below shows how to include the CSS from this package if your build system supports requiring CSS files

import { MediaVideo } from 'sanity-plugin-media-video/renderer';

// IMPORTANT: require the CSS file from this package
// Recommended to us the provided CSS as well, but you can override it if you want.
// Refer to the table above for the CSS selectors.
import 'sanity-plugin-media-video/dist/sanity-plugin-media-video.css';

const MyComponent = (props) => {
  return (
    <MediaVideo
      className='my-custom-class-name'
      classNames={{
        rootCn: 'my-custom-root-container-class-name',
        containerCn: 'my-custom-container-class-name',
        imageContainerCn: 'my-custom-image-container-class-name',
        imageCn: 'my-custom-image-class-name',
        videoBackgroundCn: 'my-custom-video-background-class-name',
        inlineVideoBackgroundCn: 'my-custom-inline-video-background-class-name',
        videoCn: 'my-custom-video-class-name',
        dialogTriggerCn: 'my-custom-dialog-trigger-class-name',
        dialogContentCn: 'my-custom-dialog-content-class-name',
        dialogOverlayCn: 'my-custom-dialog-overlay-class-name',
        dialogCloseCn: 'my-custom-dialog-close-class-name',
        playBtnContainerCn: 'my-custom-play-button-container-class-name',
        playBtnCn: 'my-custom-play-button-class-name',
      }}
      videoUrl={videoUrl}
      imagePreview={imagePreview}
      isAutoPlay={isAutoPlay}
      isPipAutomatic={isPipAutomatic}
      videoType={videoType ?? 'link'}
      muxData={muxData}
    />
  );
};

Props

PropDescriptionDefaultRequired
videoUrlThe URL of the video to be displayed.undefinedYes
muxDataData object associated with Mux video assets for advanced integrations.undefinedNo
videoTypeType of video (default is 'link'), could be values like 'link', 'mux', etc.'link'No
imagePreviewThe image displayed for the video thumbnail.nullYes
customImageComponentCustom Image Preview component that will be rendered on the image preview.undefinedNo
isAutoPlayDetermines if the video should play automatically.falseNo
isPipAutomaticDetermines if picture-in-picture mode should be enabled automatically.falseNo
customPipIdCustom ID for the PIP mode, used for managing multiple instances or special configurations.undefinedNo
playInPopoutWhether to play the video in a popout dialog by default.undefinedNo
playButtonCustom play button component or element to be displayed as the play trigger. Can pass a React component or a JSX element.undefinedNo
autoPlayVideoPlayerPropsProps to be passed to the Auto Play Video Background ReactPlayer instance for advanced video player customization.undefinedNo
videoPlayerPropsProps to be passed to the ReactPlayer instance for advanced video player customization.undefinedNo
isDesktopScreenWhether the current screen is in a desktop size. Useful for determining if to play in the popout mode or not.undefinedNo
videoHookCallbacksObject of callback functions for handling video playback events (onPlay, onPause, etc.) within the custom hook.undefinedNo
classNamesCustom class names for various UI elements to facilitate styling and theming.undefinedNo
refForwarded ref to the root div element of the component.undefinedNo

videoHookCallbacks object

PropDescription
onPlayCustom callback that triggers whenever the video playback starts, useful for handling side effects.
onPauseCustom callback that triggers when the video is paused, allowing for additional logic like UI updates or tracking.
onReadyCustom callback that is executed once the video is ready to be played, useful for preloading or initialization tasks.
onPipEnableCustom callback triggered when Picture-in-Picture (PiP) mode is activated, allowing custom behavior when PiP is enabled.
onPipDisableCustom callback triggered when Picture-in-Picture (PiP) mode is deactivated, useful for handling any teardown logic or UI updates.

Custom class names and its corresponding CSS selectors

PropCSS SelectorDescription
classNames.rootCn.media-video-rootRoot container.
classNames.containerCn.media-video-containerMain container.
classNames.imageContainerCn.media-video-image-containerImage container.
classNames.imageCn.media-video-imagePreview image.
classNames.videoBackgroundCn.media-video-auto-play-wrapperBackground of the video.
classNames.inlineVideoBackgroundCn.media-video-inline-player-wrapperSpecifically for the inline video background.
classNames.videoCn.media-video-auto-play-player or .media-video-inline-playerVideo element.
classNames.dialogTriggerCn.media-video-dialog-triggerDialog trigger that opens the popout.
classNames.dialogContentCn.media-video-dialog-contentContent of the dialog.
classNames.dialogOverlayCn.media-video-dialog-overlayOverlay of the dialog.
classNames.dialogCloseCn.media-video-dialog-closeClose button of the dialog.
classNames.playBtnContainerCn.media-video-play-button-containerContainer of the play button.
classNames.playBtnCn.media-video-play-buttonPlay button itself.

CSS Selectors for Media Video Components

ComponentPrimary CSS SelectorDescription
MediaVideoRoot.comp-media-video-rootRoot element for the entire MediaVideo component.
MediaVideoContainer.comp-media-video-containerWrapper for the main content of MediaVideo.
MediaVideoPlayerWrapper.comp-media-video-player-wrapperWrapper specifically for the video player instance, used for styling controls.
MediaVideoImageContainer.comp-media-video-image-containerWrapper for the preview image.
MediaVideoImage.comp-media-video-image, .comp-media-video-image__imgContains the preview image (img variant is used within the Sanity image component).
MediaVideoPlayButtonContainer.comp-media-video-play-button-containerWrapper for the play button.
MediaVideoPlayButton.comp-media-video-play-button, .comp-media-video-play-button-innerStyles for the play button, including an optional inner wrapper for icon alignment.
MediaVideoPlayer.comp-media-video-playerMain video player element; adjusts player layout and dimensions within the component.
MediaVideoPopout.comp-media-video-popout-container, .comp-media-video-popout-playerPopout container and player styles, used for PiP or popout mode adjustments.
MediaVideoAutoPlayVideoLink.comp-media-video-auto-play-video-linkStyling for autoplay or looping video backgrounds; suppresses controls and adjusts object-fit.

Use your own implementation

You can use your own implementation if you want since the the Media Video renderer is only an optional added feature in this plugin. Suggested way to do this is by leveraging react-player library to make it easier to render the video.

Optionally the plugin also provides some block components to help you build your own implementation if you want to. You can access them through the MediaVideoComponents object like so:

import { MediaVideoComponents } from 'sanity-plugin-media-video/renderer';

// Recommended to us the provided CSS as well, but you can override it if you want.
// Refer to the table above for the CSS selectors.
import 'sanity-plugin-media-video/dist/sanity-plugin-media-video.css';

const MyCustomComponent = (props) => {
  return (
    <MediaVideoComponents.MediaVideoRoot
      ref={ref}
      className={className}
      {...props}
    >
      <MediaVideoComponents.MediaVideoContainer>
        {/* Your own implementation */}
      </MediaVideoComponents.MediaVideoContainer>
    </MediaVideoComponents.MediaVideoRoot>
  );
};

Using the provided Hooks

You can also use the hooks provided by the plugin to help you build your own implementation:

import { MediaVideoComponents } from 'sanity-plugin-media-video/renderer';

import 'sanity-plugin-media-video/dist/sanity-plugin-media-video.css';

const MyCustomComponent = (props) => {
  const isDesktop = useMediaQuery('(min-width: 1024px)'); // Used to know if the screen is desktop or not
  const pipUniqueId = uuidv4();

  // Setup Intersection Observer to determine when the component is in view
  const intersectionObserver = useInView({
    rootMargin: '0px',
    threshold: [0, 0.5, 1],
  });
  const { ref: topLevelRef } = intersectionObserver;

  // Custom hook to manage video playback state and related events
  const {
    desktopPopoutPlay,
    inlinePlay,
    inlinePause,
    showImage,
    playedByAutoPlay,
    isPopoutOpen,
    isFloatingPip,
    setInlinePause,
    handleOnVideoProgress,
    handleClickPlay,
    handleVideoOnReady,
    handleInlineOnPlay,
    handleOnPipForceClose,
    setActivePip,
  } = useMediaVideoPlayback({
    /**
     * The following props are required for the hook to work
     * can refer to the implementation in the MediaVideo component
     * for more details
     */
    isAutoPlay,
    isPipAutomatic,
    intersectionObserver,
    pipUniqueId,
    isDesktop,
    playInPopout,
  });

  return {
    /* Your own implementation */
  };
};

๐Ÿ—ƒ๏ธ Data model

{
  _type: 'media';
  enableVideo: boolean;
  isAutoPlay: boolean;
  videoType: 'link' | 'mux';
  isPipAutomatic: boolean;
  videoUrl: string;
  muxVideo: {
    _type: 'mux.video';
    asset: {
      _ref: string;
      _type: 'reference';
      _weak: boolean;
    }
  }
  image: {
    _type: 'image';
    asset: {
      _type: 'reference';
      _ref: string;
    }
    altText: string;
    crop: {
      _type: 'sanity.imageCrop';
      bottom: number;
      left: number;
      right: number;
      top: number;
    }
    hotspot: {
      _type: 'sanity.imageHotspot';
      height: number;
      width: number;
      x: number;
      y: number;
    }
  }
}

๐Ÿ›ข GROQ Query

You can query the data model with this sample groq query

// Example for fetching data above
*[ _type == "exampleSchemaWithMedia" ] {
  myMediaField {
    image {
      asset->{
        _id,
        url,
        metadata {
          lqip
        }
      },
      crop,
      hotspot,
      altText
    },
    enableVideo,
    videoType,
    isAutoPlay,
    isPipAutomatic,
    videoUrl,
    muxVideo {
      _type,
      asset->{
        playbackId,
        assetId,
        filename,
        ...
      },
    },
  }
}

โ“ FAQs

  • How to add a new language?

  • Why the need to add an image field to the media object?

    • The image field is used to display a custom preview image of the video. This is useful for SSR performance and SEO purposes as it does not load the video itself directly when the page is rendered.
    • This also makes it possible to just be a Media Picture instead if you do not want to use the video feature. With that you can then render it however you want with the image data

๐Ÿ“ License

MIT ยฉ Evelan

๐Ÿงช Develop & test

This plugin uses @sanity/plugin-kit with default configuration for build & watch scripts.

See Testing a plugin in Sanity Studio on how to run this plugin with hotreload in the studio.

Release new version

Run "CI & Release" workflow. Make sure to select the main branch and check "Release new version".

Semantic release will only release on configured branches, so it is safe to run release on any branch.

1.2.2

8 months ago

1.3.0

5 months ago

1.2.1

8 months ago

1.2.0

9 months ago

1.1.2

9 months ago

1.1.1

9 months ago

1.1.0

9 months ago

1.0.4

10 months ago

1.0.3

11 months ago

1.0.2

11 months ago

1.0.1

11 months ago

1.0.0

11 months ago