player icon indicating copy to clipboard operation
player copied to clipboard

HLS with ManageMediaSource throws NotFoundError: The object can not be found here.

Open pvm1987 opened this issue 1 year ago • 2 comments

Current Behavior:

I'm using Safari 17.1 and trying to play HLS video. This browser has support of ManagedMediaSource and hls.js uses it. I sometimes receive the following error "NotFoundError: The object can not be found here." and the whole my React component is removed from the DOM.

Expected Behavior:

Player should load and play the HLS video every time as it's woking on all other browsers with the old MediaSource support. For example it works on Chrome.

Steps To Reproduce:

  1. Setup vidstack player with autoPlay={true} and load="visible"
  2. Setup HLS provider library to be "locally" accessed:
onProviderChange={(provider: MediaProviderAdapter | null, nativeEvent: MediaProviderChangeEvent,) => {
    if (isHLSProvider(provider)) {
      // Set hls.js to be used locally (i.e., not over a CDN).
      provider.library = HLS;
    }
  };
  }
  1. Reload the page and observe the console until you catch the moment when the following error occurs.
"NotFoundError: The object can not be found here." 

Environment:

Screenshot 2024-11-28 at 13 24 51 Screenshot 2024-11-28 at 13 46 18

I think this error happens when the <source> is appended to the <video> by hls.js. The lib is manipulating the DOM tree which is previously created by Vidstack MediaOutlet React component. Such actions are not allowed by React's re-render methods and they throw an error which breaks the whole app.

Note that I tested with HLS config param:

preferManagedMediaSource: false 

This turns off the usage of ManagedMediaSource and then everything works as expected. When MediaSource is used hls.js changes only the "src" attribute of the video tag and doesn't add any <source> child tags.

pvm1987 avatar Nov 28 '24 11:11 pvm1987

I am also seeing this error and can confirm that turning off the managed media source fixes this bug on MacOS Safari. However, I am still seeing the same error on iOS Safari (tested 17 and 18) and here the fix does not help.

digilist avatar Jun 23 '25 10:06 digilist

I did some further investigation and created this reproducer: https://stackblitz.com/edit/react-ts-5pftr24v?file=package.json,App.tsx,index.html

Since this is using preferManagedMediaSource: false it works on MacOS, but not on iOS. Removing that config breaks it also on MacOS.

Show code

App.tsx

import React, { useState } from 'react';
import './style.css';
import Lightbox from 'yet-another-react-lightbox';
import {
  isHLSProvider,
  MediaPlayer,
  MediaProvider,
  MediaProviderAdapter,
} from '@vidstack/react';
import {
  PlyrLayout,
  plyrLayoutIcons,
} from '@vidstack/react/player/layouts/plyr';
import HLS from 'hls.js';
import 'yet-another-react-lightbox/styles.css';
import '@vidstack/react/player/styles/base.css';
import '@vidstack/react/player/styles/plyr/theme.css';

export default function App() {
  const slides = [
    {
      type: 'video' as const,
      controls: true,
      sources: [
        {
          src: 'https://vz-502e4c47-4f1.b-cdn.net/1ed7bc92-a22e-48d1-9721-ed1d3b242415/playlist.m3u8',
          type: 'hls',
        },
      ],
    },
  ];

  function onProviderChange(provider: MediaProviderAdapter | null) {
    if (isHLSProvider(provider)) {
      provider.library = HLS;
      provider.config = {
        preferManagedMediaSource: false,
      };
    }
  }

  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle Lightbox</button>

      <Lightbox
        open={open}
        slides={slides}
        close={() => setOpen(false)}
        render={{
          slide: ({ slide, offset }) => {
            if (slide.type === 'video' && slide.sources[0].type === 'hls') {
              return (
                <MediaPlayer
                  src={slide.sources[0].src}
                  onProviderChange={onProviderChange}
                >
                  <MediaProvider></MediaProvider>
                  <PlyrLayout icons={plyrLayoutIcons} />
                </MediaPlayer>
              );
            }

            return undefined; // Render default slide
          },
        }}
        index={0}
      />
    </div>
  );
}

package.json

{
  "name": "react-ts",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "@types/react": "^18.0.8",
    "@types/react-dom": "^18.0.2",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "yet-another-react-lightbox": "^3.21.7",
    "vidstack": "^1.12.13",
    "hls.js": "^1.5.20",
    "@vidstack/react": "^1.11.21"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {
    "react-scripts": "latest"
  }
}

I am using vidstack inside yet-another-react-lightbox and the error appears when the lightbox is opened, closed, and then opened another time.

It looks like the error is related to a repeated mounting and unmount of (or creating and destroying) the component for the different slides and slide effects. However, it only happens after closing and re-opening the lightbox.

As a workaround, I tried memoizing the <MediaPlayer> component (by setting a variable or using useMemoy outside of the slide property, not via React.memo) and this "fixed" the issue, but it's not really a good solution.

Another edit: I managed to get Memoization working by putting the Slide content into a new component with React.memo. This is what it looks like now: https://stackblitz.com/edit/react-ts-mwbvsepe?file=package.json,App.tsx,index.tsx

Show code
function onProviderChange(provider: MediaProviderAdapter | null) {
  if (isHLSProvider(provider)) {
    provider.library = HLS;
    provider.config = {
      preferManagedMediaSource: false,
    };
  }
}

export default function App() {
  const slides = [
    {
      type: 'video' as const,
      controls: true,
      sources: [
        {
          src: 'https://vz-502e4c47-4f1.b-cdn.net/1ed7bc92-a22e-48d1-9721-ed1d3b242415/playlist.m3u8',
          type: 'hls',
        },
      ],
    },
  ];

  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle Lightbox</button>

      <Lightbox
        open={open}
        slides={slides}
        close={() => setOpen(false)}
        render={{
          slide: ({ slide, offset }) => {
            if (slide.type === 'video' && slide.sources[0].type === 'hls') {
              console.log('render slide');
              return <VideoSlide slide={slide} />;
            }

            return undefined; // Render default slide
          },
        }}
        index={0}
      />
    </div>
  );
}

interface VideoSlideProps {
  slide: SlideVideo;
}

const VideoSlide = React.memo(({ slide }: VideoSlideProps) => {
  console.log('render memoized slide');
  return (
    <MediaPlayer src={slide.sources[0].src} onProviderChange={onProviderChange}>
      <MediaProvider></MediaProvider>
      <PlyrLayout icons={plyrLayoutIcons} />
    </MediaPlayer>
  );
});

This seems to work now, even though the memoized component is still rendered quite often (but less often than the slide callback is called).

Image

digilist avatar Jun 23 '25 13:06 digilist