videojs / video.js

Video.js - open source HTML5 video player
https://videojs.com
Other
38.08k stars 7.45k forks source link

Misplaced widgets because of new docs #8176

Open andreifilip123 opened 1 year ago

andreifilip123 commented 1 year ago

Hello, Using the latest docs I'm running into an issue that could be really specific so I'm asking for guidelines if you have any.

In an old app, I had something like this:

<div data-vjs-player>
    <video ref={videoRef} />
    <div>Some custom added widget</div>
    <div>Another custom added widget</div>
</div>

and then:

  useEffect(() => {
    if (!playerRef.current) {
      const videoElement = videoRef.current;
      if (!videoElement) return;
      playerRef.current = videojs(videoElement, options, () => {
        if (onReady) onReady(playerRef.current);
      });
    }
  }, [options]);

and I used the old docs to create the video. This worked fine and the widgets showed up when going full screen as well.

However, using the new docs, I have something like this:

<div data-vjs-player>
    <div ref={videoRef} />
    <div>Some custom added widget</div>
    <div>Another custom added widget</div>
</div>

and then:

  useEffect(() => {
    if (!playerRef.current) {
      // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode. 
      const videoElement = document.createElement("video-js");

      videoElement.className = 'videojs-big-play-centered';
      videoRef.current.appendChild(videoElement);
      playerRef.current = videojs(videoElement, options, () => {
        if (onReady) onReady(playerRef.current);
      });
    }
  }, [options]);

The issue I'm encountering is that now, the widgets I added are not showing up anymore when going full screen. After hours of debugging, I figured the issue is that with the new setup (.appendChild), the widgets and the video are not children of the same element (they're not siblings but rather there's one more nesting level between them). The final thing looks something like this:

<div data-vjs-player>
    <div>
        <video-js>{/* all video js content */}</video-js>
    </div>
    <div>Some custom added widget</div>
    <div>Another custom added widget</div>
</div>

while in the old one it looked like this:

<div data-vjs-player {/* a bunch of videojs classes and things */}>
    <video />
    <div>Some custom added widget</div>
    <div>Another custom added widget</div>
    {/* other videojs required things like vjs-poster, vjs-text-track-display, etc */}
</div>

Any tips on how can I move forward with this? (and sorry if this is not the right place to ask).

Here's a codesandbox with the issue: https://codesandbox.io/s/react-videojs-strictmode-fullscreen-kn45xj

Thank you very much and I hope someone will get to this issue soon! 🙏

Originally posted by @andreifilip123 in https://github.com/videojs/videojs.com/issues/163#issuecomment-1441896417

welcome[bot] commented 1 year ago

👋 Thanks for opening your first issue here! 👋

If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can. To help make it easier for us to investigate your issue, please follow the contributing guidelines.

mister-ben commented 1 year ago

React 18's strict mode breaks using an element managed by react as the player el with div ingest, so the new recommendation inserts a new el for the player instead. That leaves your adidition components as siblings rather than a child of the player el. There's been occasional discussion of adding a Video.js option to make the standard fullscreen button make a different element fullscreen; this would be a use case for that.

Quickest options would be to insert items into the player DOM after the player has initialised, or replcae the fullscreen button with custom button that makes a different element fulscreen.

andreifilip123 commented 1 year ago

Inserting them into the player DOM was my first thought but how would I do that with functional react components ? From the documentation, it seems like it only works with class components or normal html elements (not JSX)

andreifilip123 commented 1 year ago

I figured out how to display widgets in the latest version of videojs.

  1. you create your component as usual except that the props will be included in an options object:
    
    import { FC } from 'react';

interface Options { title: string; description: string; }

const TitleWidget: FC<{ options: Options }> = ({ options: { title, description } }) => { return (

{title}

{description}

); };

export default TitleWidget;

2. Next to the component, you create a bridge component
```jsx
import { createRoot } from 'react-dom/client';
import videojs from 'video.js';
import TitleWidget from './TitleWidget';

const VjsComponent = videojs.getComponent('Component');

// @ts-expect-error Videojs types are not working correctly
class TitleWidgetBridge extends VjsComponent {
  // @ts-expect-error Videojs types are not working correctly
  constructor(player, options) {
    super(player, options);

    // @ts-expect-error Videojs types are not working correctly
    const container = this.el();
    const root = createRoot(container);

    // When player is ready, mount the React component
    player.ready(() => root.render(<TitleWidget options={options} />));

    // Unmount the React root when this component is destroyed
    // @ts-expect-error Videojs types are not working correctly
    this.on('dispose', () => root.unmount());
  }
}

export default TitleWidgetBridge;

Important note: Props aren't updated at any later point. So you shouldn't pass state from outside the widget inside and then expect it to change. In case you need to change outside state, it's better to have a function that updates that state outside (while maintaining a copy of it inside the component).

  1. In the main video player component, register the bridge component and add it as a child to the player:
    
    import { FC, useCallback, useEffect, useRef } from 'react';
    import videojs from 'video.js';
    import Player from 'video.js/dist/types/player';
    import 'video.js/dist/video-js.min.css';
    import TitleWidgetBridge from './TitleWidget/TitleWidgetBridge';

import './VideoPlayer.scss';

export const CONCEPT_LEARNT_POINTS = 20; export const WATCH_VIDEO_BONUS = 20;

// @ts-expect-error Videojs types are not working correctly videojs.registerComponent('TitleWidget', TitleWidgetBridge);

const playerOptions = { autoplay: true, controls: true, responsive: true, fluid: true, controlBar: { skipButtons: { forward: 10, backward: 10, }, }, bigPlayButton: true, playbackRates: [0.5, 0.8, 1, 1.2, 1.5, 2], sources: [ { src: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', type: 'video/mp4', }, ], };

const VideoPlayer: FC = () => { const videoRef = useRef(null); const playerRef = useRef<Player | null>(null);

const handlePlayerReady = useCallback((player: Player) => { player.addChild('TitleWidget', { title: 'Big Buck Bunny', description: 'Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself.', }); }, []);

useEffect(() => { // Make sure Video.js player is only initialized once if (!playerRef.current) { const videoElement = document.createElement('video-js');

  videoElement.classList.add('vjs-custom-skin', 'vjs-big-play-centered', 'vjs-show-big-play-button-on-pause');
  videoRef.current?.appendChild(videoElement);
  const player = (playerRef.current = videojs(videoElement, playerOptions, () => {
    videojs.log('player ready');
    // focus on the player so that the hotkeys work
    player.focus();

    handlePlayerReady(player);
  }));
} else {
  // You could update an existing player in the `else` block here
  // on prop change, for example:
  const player = playerRef.current;

  if (playerOptions.autoplay) player.autoplay(playerOptions.autoplay);
  if (playerOptions.sources) player.src(playerOptions.sources);
}

}, [videoRef]);

// Dispose the Video.js player when the functional component unmounts useEffect(() => { const player = playerRef.current;

return () => {
  if (player && !player.isDisposed()) {
    player.dispose();
    playerRef.current = null;
  }
};

}, [playerRef]);

return (

); };

export default VideoPlayer;



I wrote this because when I was building our video player I couldn't find a good guide in the docs on how to integrate videojs with custom made widgets.
video-archivist-bot commented 1 year ago

Hey! We've detected some video files in a comment on this issue. If you'd like to permanently archive these videos and tie them to this project, a maintainer of the project can reply to this issue with the following commands: