Remote participant video player not working

I’m migrating from Twilio to Zoom Video SDK and I’m having a problem rendering the videos for remote participants using the code provided in the documentation.
Here is what I’m doing:

zoomClient.on("peer-video-state-change", (payload) => {
    const mediaStream = zoomClient.getMediaStream();
    if (payload.action === "Start") {
        mediaStream.attachVideo(payload.userId, 3).then((userVideo) => {
            const videoPlayerContainer = document.querySelector("video-player-container") as Element;
            videoPlayerContainer.appendChild(userVideo as VideoPlayer);
        });
    } else if (payload.action === "Stop") {
        mediaStream.detachVideo(payload.userId);
    }
});

And then in the HTML I have the <video-player-container></video-player-container>.
The participants are joining after the local participant and I don’t get any errors but the remote participant video just shows a black screen.
Any ideas on what I might be doing wrong?

Hi Marcia,

Welcome!
Can you provide me some more information as to where and how this event listener is being implemented? Do you know if it is successfully subscribed and if your code block is actually running?

Best,
Will

Hey @m.martins

Thanks for your feedback.

The element created by mediaStream.attachVideo may not have an initial size, which could be causing the black screen. Since it’s an HTML element, you can add a class to it and then set its width and height in CSS.

  video-player-container{
    width: 1280px;
    height:720px;
  }

  .video-cell {
    aspect-ratio: 16/9;
    width: 300px;
  }
mediaStream.attachVideo(payload.userId, 3).then((userVideo) => {
  const videoPlayerContainer = document.querySelector(
    "video-player-container"
  ) as Element;
  userVideo.classList.add("video-cell");
  videoPlayerContainer.appendChild(userVideo as VideoPlayer);
});

Thanks
Vic

Hi Will, sure!

I’m using react and have created a hook to handle all things related to the zoom SDK. The event handler is being added this way:

useEffect(() => {
    if (zoomClient) {
        ...

        // update videos of remote participants
        zoomClient.on("peer-video-state-change", (payload) => {
            console.log("peer-video-state-change", payload);
            ...
        });

        return () => {
            ...
            zoomClient.off("peer-video-state-change")
        };
    }
}, [zoomClient]);

Then on the main component I just have this:

return (
    <div>
        <style>
            {`
                video-player-container {
                    width: 1280px;
                    height:720px;
                }

                .video-cell {
                    aspect-ratio: 16/9;
                    width: 300px;
                }
             `}
            </style>
           <video-player-container></video-player-container>
    </div>
)

I can tell the event handler is working since I added some console logs inside to render the returned video player and I can see it being added when I inspect the page. I’m also rendering the local user video using a similar approach and have no problems with that one:

const toggleVideo = async () => {
        const userId = zoomClient!.getCurrentUserInfo().userId;
        if (mediaStream!.isCapturingVideo()) {
            mediaStream!.stopVideo().then(() => mediaStream!.detachVideo(userId));
        } else {
            const videoPlayerContainer = document.querySelector("video-player-container");
            mediaStream!.startVideo().then(() => {
                mediaStream!.attachVideo(userId, VideoQuality.Video_720P).then((userVideo) => {
                    console.log("local user video player", userVideo);
                    userVideo.classList.add("video-cell");
                    videoPlayerContainer?.appendChild(userVideo as VideoPlayer);
                });
            });
        }
    };

Thanks for the help!

Hi Vic!

Unfortunately this didn’t work, I still get a black video player for the remote participants.

Hey @m.martins

Could you share the sessionId with issues to help us troubleshoot the problem?

Thanks
Vic

Hi @vic.yang
I’m not sure how to get the sessionID. Is it the zoomID I get after doing zoomClient.join()?

Thank you

Hey @m.martins

You can obtain it from the Dashboard on the Video SDK Web portal page, or if your session is still ongoing, you can retrieve it using client.getSessionInfo().

Thanks
Vic

Hi @vic.yang

The session ID is ctAt5w3VSZubj8WPVtN+Eg==. Thank you!

Hey @m.martins

From the logs, we’ve noticed errors like Cannot read properties of null (reading 'appendChild').

Could you please check the console for errors when the issue occurs?

Thanks
Vic

Hi @vic.yang

The only error I’m seeing in the console at the moment is Uncaught (in promise) DOMException: The play() request was interrupted by a new load request.

I’ve created a small test app where the problem is happening so it will be easier to share with you. I’m using Next.js

Thank you!

Hey @m.martins

Thank you for sharing the complete code example with us.

I’ve tested the code you provided and found some areas that need improvement. Below, I will list out some points for modification and explain the reasons.

  • Style in page.tsx file
        video-player {
           width: 400px;
           aspect-ratio:16/9;
         }
    

Since the video player does not have a default width and height, you need to set them manually. You previously set the width; the default height should be 9/16 of the width. Therefore, I added the aspect-ratio property to specify the height.

  • Init option in useZoomVideo.tsx file

     await zoomClient.init("en-US", "Global", { patchJsMedia: true, enforceMultipleVideos:true });
    

We recommend enabling SharedArrayBuffer to improve performance. You can refer to this doc.

Since the test example doesn’t have these configurations, we recommend specifying the enforceMultipleVideos option as true when calling the client.init method.

This allows placing multiple video-player elements within the video-player-container even when SharedArrayBuffer is disabled. Otherwise, you would need one video-player-container per video.

Correspondingly,Video SDK Web provide the stream.isSupportMultipleVideos() method to detect whether this feature is supported.

Please feel free to reach out if you have any further questions or require additional assistance.

Thanks
Vic

1 Like

Hi @vic.yang

The changes you recommended worked, but now I’m trying to attach the video to an existing video player, so I can add some styling to each participant’s video, and I’m having some issues.

I created a new component (ParticipantContainer.tsx) to render the participant video player and I’m running mediaStream.attachVideo(participant.userId, 3, videoPlayerRef.current) when the participant.bVideoOn changes to true.
This works for the local participant but not for the remote participant. I’ve also noticed that, even for the local participant, if I toggle the video on and off a few times, the video stops rendering and the node-id of the video player is changed to 0 instead of the userID.

Do you know what might be the problem? I updated the demo with all the new code.
Thank you!

Hey @m.martins

I made some changes in ParticipantContainer.tsx:

  • On line 24, I removed the backgroundColor style setting. I moved the background setting to the video-player-container in page.tsx. This is because within the video-player-container, we internally create a canvas, and the video rendering occurs on this canvas. If its descendant elements have a background color, it will block the display of the video.

    <div
       style={{
         position: "relative",
         // backgroundColor: "black",
       }}
     >
    
    <style>{`video-player-container { width: 400px; background-color: black; }`}</style>
    
  • On lines 28-29, I removed the setting for the node-id and media-type attributes. These attributes will be handled within the Video SDK.

      <video-player
        // node-id={participant.userId}
        // media-type="video"
        ref={videoPlayerRef}
        style={{ width: "400px", height: "auto", aspectRatio: "16/9", }}
      ></video-player>
    

Please feel free to reach out if you have any further questions or require additional assistance.

Thanks
Vic

2 Likes

Thank you @vic.yang !

That fixed the render of the remote participant’s video but I’m still having problems when I toggle the video on and off a few times. If I toggle the video for the local participant, the video stops rendering and only shows a black video player, and if I toggle the remote participant, the video in the local participant’s page will show the last frame before the remote participant toggled the video off.
I update the demo so you can see the problem.

Hey @m.martins

After making a few code changes, it can now work:

  • In useZoomVideo.tsx, within the toggleVideo method, I removed the call to mediaStream.detachVideo after stopVideo. Since both self-view and remote-view rendering use ParticipantContainer, there’s no need for an additional detach video here.

    const toggleVideo = async () => {
      const userId = zoomClient!.getCurrentUserInfo().userId;
      if (mediaStream!.isCapturingVideo()) {
       // mediaStream!.stopVideo().then(() => mediaStream!.detachVideo(userId));
       mediaStream!.stopVideo();
      } else {
        mediaStream!.startVideo();
      }
    };
    
  • In ParticipantContainer.tsx, within the useEffect, you originally only handled the scenario where bVideoOn is true, but you missed the scenario where it’s false. Therefore, you need to call detachVideo to unbind the user from the video-player.

    useEffect(() => {
      if (participant.bVideoOn) {
        mediaStream.attachVideo(participant.userId, 3, videoPlayerRef.current);
      }else{
        mediaStream.detachVideo(participant.userId,videoPlayerRef.current);
      }
    }, [participant,mediaStream]); 
    

Please feel free to reach out if you have any further questions or require additional assistance.

Thanks
Vic

2 Likes