vidstack / player

UI components and hooks for building video/audio players on the web. Robust, customizable, and accessible. Modern alternative to JW Player and Video.js.
https://vidstack.io
MIT License
2.35k stars 137 forks source link

Youtube endless loading when embedding forbidden #1456

Open Benny739 opened 1 month ago

Benny739 commented 1 month ago

Current Behavior:

When embedding is forbidden video shows an endless loading indicator with the poster CleanShot 2024-10-05 at 20 47 52@2x

Expected Behavior:

The video can not be embedded image should be shown CleanShot 2024-10-05 at 20 48 13@2x

Steps To Reproduce:

  1. Use a video where embedding is forbidden: https://www.youtube.com/watch?v=lpdfSB0iKAk

Reproduction Link: Demo Link

yordandev commented 1 month ago

I experienced the same and fixed it the following way:

    const handleProviderChange = (e: MediaProviderChangeEvent) => {
      const provider = e.detail;

      if (isYouTubeProvider(provider)) {
        provider.referrerPolicy = 'no-referrer-when-downgrade';
        provider.iframe.referrerPolicy = 'no-referrer-when-downgrade';
      }
    };
Benny739 commented 1 month ago

Doesn't solve it for me, it still shows the poster with an endless loading indicator.

brendanahart commented 1 month ago

Agreed, I still have the endless loading indicator on mine too. Is there any event that vidstack fires to show that a video cannot be played from YouTube. If so, then I'd like to just show a thumbnail

brendanahart commented 1 month ago

I'm going to share my workaround and although it's not a solution, I think the reason why this is happening is because of the YouTube API. The full solution would have to be you render an iframe from a different URL (similar to how reddit plays youtube videos).

Essentially here is my MediaPlayer component:

        <MediaPlayer ...
           ref={playerRef}
        >
          <MediaProvider>
          ....
          </MediaProvider>
          <YoutubePlayerControl src={props.src} thumbnail={props.thumbnail} isExternal={props.isExternal} />
          <DefaultVideoLayout
          />
        </MediaPlayer>

Notice how I have a YoutubePlayerControl component there. I have a context hook that contains a reference to my video player in that component and use the bufferedEnd to see if the video has loaded. bufferedEnd is true when the video loads 1 second, however is false initially (0). If this video has not buffered 1 second that likely means it's blocked, which I then open the video up in the browser. This is my full code for my component:

const YoutubePlayerControl: React.FC<YoutubePlayerControlProps> = ({ src, isExternal, thumbnail }) => {
    const { playerRef } = useVideoContext();
    const bufferedEnd = useMediaState('bufferedEnd', playerRef);

    const openInBrowser = async () => {
        await Browser.open({ url: src, presentationStyle: 'popover' });
    };

    return (
        !bufferedEnd && isExternal && (
            <div
                className="absolute inset-0 w-full h-full cursor-pointer flex items-center justify-center"
                onClick={openInBrowser}
                aria-label="Youtube video"
            >
                {/* Icon container with higher z-index, using flex centering */}
                <div className="absolute z-30 flex items-center justify-center">
                    <IonImg
                        src="/assets/icon/logos--youtube-icon.svg"
                        alt="Play"
                        className="w-[100px] h-[100px]"
                    />
                </div>

                {/* Poster with lower z-index */}
                <Poster
                    className="absolute inset-0 block h-full w-full bg-black rounded-md opacity-0 transition-opacity data-[visible]:opacity-100 [&>img]:h-full [&>img]:w-full [&>img]:object-cover z-10"
                    src={thumbnail}
                />
            </div>
        )
    );
};

Hope this helps as I've been spending about 2 weeks thinking on how I can fix this and it works well enough for a good user experience!

wplit commented 1 month ago

Hi @brendanahart,

Have you been able to get the player to be reliable for when using with YouTube? I'm still hitting lots of issues (reported but issues still exist), even with the default setup just following the docs exactly.

Especially when trying to view on iPhone. Finding it too unreliable for production. Lots of situations where the player still gets stuck. Some I've reported for where it reliably fails..

Setting playsinline=false prevents video from being playable on iPhone - https://github.com/vidstack/player/issues/1431

The 'play' loading strategy will crash the player on iPhone (never ending loading, can't click to play ) - https://github.com/vidstack/player/issues/1395

After using chapters to navigate, using the time slider no longer works and causes the player to get stuck - https://komododecks.com/recordings/92YAhFQn7wBvNDFaMKC0

Are these things you're using workarounds for? Are you seeing the same issues?

When I'm testing, these issues happen every time.

brendanahart commented 1 month ago

Hey @wplit

I've never seen the player crash the app, however, I've seen the loading "Video is unavailable" whenever I try to load the video natively on an iPhone. I believe this is from the YouTube API blocking anything that is hosted on localhost (which by default is what an app is hosted on). It seems you must need an https connection to the video (according to this issue: https://stackoverflow.com/questions/51969269/embedded-youtube-video-doesnt-work-on-local-server). For some reason, randomly, videos are able to load sometimes and then not load natively on iOS. That's why I went with the custom thumbnail solution and then redirects them to a browser popup in app to load the youtube video. FYI I am using vidstack in a native iOS/Android app

I haven't gotten to chapters yet and the loading strategy I use is custom.

Here is my code. When the video loads, I am able to navigate with the timesliders.

I would make sure that on iPhone, you're audio is always muted unless you are native. That could be an issue. The user must make sure the audio is pressed for it to play according to recent authoritarian restrictions from Chrome and Safari

        <MediaPlayer
          ref={playerRef}
          src={props.src}
          poster={props.thumbnail}
          aspectRatio={aspectRatioFraction}
          crossOrigin
          playsInline
          loop
          paused={!shouldBePlaying}
          onCanPlay={handleCanPlay}
          onError={handleError}
          load="custom"
          posterLoad="eager"
          style={{ width: playerWidth }}
          onProviderChange={onProviderChange}
          className={loadingClass}
          onClick={handleMediaClick}
          muted={!isNative ? true : muted}
          controlsDelay={hasInteracted ? 1250 : 0}
        >
  useEffect(() => {
    // console.log("Scrolling:", props.isScrolling, "Speed:", props.scrollSpeed, "Should Load:", shouldLoad, "In View:", inView, "ID:", props.src);
    if (shouldLoad) {
      playerRef.current?.startLoading();
    }
  }, [inView, props.isScrolling, props.scrollSpeed]);

const shouldLoad = inView && (!props.isScrolling || props.scrollSpeed < playingScrollSpeedMax);

wplit commented 1 month ago

I've never seen the player crash the app

Try setting the playsInline to false, and then view on iPhone. The player will crash the moment play is clicked (stuck in loading with no way to play the video).

Reg. mute being needed. Yes, this is a shame. It makes it less useful than a regular Youtube embed, which can play on one click. Obviously can't be helped from vidstack side of things, but is a huge UX issue imo.