지난 프로젝트에서 실시간 방송에 PIP 기능을 추가하고 싶었습니다. 브라우저에서 제공하는 web API인 Picture-in-Picture API를 이용하면 PIP는 아주 쉽게 구현할 수 있습니다. 동영상이 표시되는 부분에서 마우스를 움직이면 3초 동안 동영상의 위에 소리크기 조절, PIP 시작, 전체화면을 설정할 수 있는 하단바가 표시되도록 하는 것까지는 모두 쉽게 진행되었습니다. 그런데 PIP를 켜놓고 페이지 이동을 하면(편의상 페이지 이동이라고 하겠지만, 정확히 하자면 리액트로 진행되는 프로젝트였기에 동영상을 재생하는 컴포넌트가 언마운트되는 경우였습니다.) PIP에서 나오는 화면이 검은 색으로만 표시되었습니다.

 

페이지 이동을 하면 PIP 화면이 정지되는 문제

앞서 언급했듯이 원인은 동영상을 재생하고 있는 컴포넌트가 언마운트되었기 때문에 동영상 소스를 불러오지 못한다고 생각하였습니다. 해당 컴포넌트가 사라졌으니 당연한 결과이기도 하였습니다. 그렇다면 제 프로젝트에서 PIP API를 이용하는 것으로는 원하는 기능을 구현할 수 없겠다는 생각을 하였습니다. 그래서 모달창을 만드는 것처럼 비디오 화면을 띄울 컴포넌트를 만들고 비디오 소스는 Redux을 이용해 모든 페이지에서 비디오 소스를 알 수 있게 하려고 하였습니다. 그러나 이 모든 기능을 만들기엔 기한 마감이 코앞에 닥쳐있었고 추가해야 할 기능이 더 있었기에 기존의 PIP API를 그대로 사용할 방법을 조금 더 고민해 보았습니다.

 

다행히 실시간 영상 송출 및 시청이라는 프로젝트의 특수성 덕분에 쉽게 해결할 방법이 떠올랐습니다. 실시간 방송을 시청하기 위해서는 웹 소켓으로 연결됭어 있어야 하고 비디오의 소스는 이 소켓 연결만 유지되면 다른 화면에서도 확인할 수 있습니다. 그래서 간단한 수정으로 원하는 기능을 만들 수 있었습니다.

 

기존의 코드는 다음과 같았습니다.

useEffect(() => {
    window.addEventListener("beforeunload", leaveSession);
    return () => {
      ...
      leaveSession();
      window.removeEventListener("beforeunload", leaveSession);
    };
  }, [sessionId]);

sessionId에는 웹 소켓으로 연결되는 정보가 담겨있습니다. 그래서 이 정보가 바뀌는 경우는 방송에 새로 들어 갈 때 혹은 방송 시청을 멈추었을 때 입니다. 따라서 해당 코드는 페이지를 떠날 때와 브라우저를 종료하는 경우(beforeunload), 그리고 sessionId가 바뀌는 경우(방송 시청을 종료하는 경우), 해당 컴포넌트가 언마운트되는 경우 leaveSssion함수를 실행하여 방송과의 연결을 종료합니다. 기존에는 PIP를 생각하지 않았기에, 페이지를 떠나는 모든 행동에 대해 ssession을 종료하는 것이 적절했습니다. 그러나 PIP 기능은 다른 페이지에서도 방송을 시청할 수 있도록 하는 것이 목적이기 때문에 해당 부분의 로직을 바꾸어야 하였습니다.

 

아주 간단한 수정으로 PIP 화면에서 방송을 계속 시청할 수 있도록 할 수 있습니다.

useEffect(() => {
    const handlePIP = () => {
      if (!document.pictureInPictureElement) {
        leaveSession();
      }
    };
    window.addEventListener("beforeunload", leaveSession);
    return () => {
      handlePIP();
      window.removeEventListener("beforeunload", leaveSession);
    };
  }, [sessionId]);

만약 PIP가 켜져있다면 leaveSession을 호출하지 않습니다. 다시 말해, 방송 시청을 종료하지 않습니다. 세션이 계속 연결되어 있고 비디오 컴포넌트는 세션을 통해 재생할 비디오를 받아오므로 PIP를 켜놓고 다른 페이지로 이동하여도 여전히 PIP로 방송을 시청할 수 있게 되었습니다. 그러나 다른 문제가 있었습니다.

 

PIP 자동으로 활성화 하면 영상 재생이 안되는 문제

특정 버튼을 클릭하여 다른 페이지로 이동할 때, PIP를 활성화하면서 페이지를 이동하는 기능을 추가하고 싶었습니다. 마치 유튜브 처럼요. 그런데 잘 작동하는 PIP 기능이 아래와 같이 함수를 통해서는 영상이 재생되지 않았습니다.

 

togglePictureInPicture는 PIP가 꺼져있으면 켜고 켜져있으면 끄는 함수입니다. 그리고 hadnleClick이 호출되면 PIP화면을 켜고난 뒤 페이지를 이동하죠.

  const togglePictureInPicture = () => {
    console.log(videoRef.current);
    if (document.pictureInPictureElement) {
      document.exitPictureInPicture();
    } else {
      videoRef.current
        ?.requestPictureInPicture()
        .then(() => {})
        .catch((error) => {
          console.error("Error entering PiP mode:", error);
        });
    }
  };

// 특정 버튼을 클릭하면 PIP 활성화를 요청하고 페이지 이동
  const handleClick = () => {
    if (!document.pictureInPictureElement) {
      togglePictureInPicture();
    }
    navigate(`/${someID}`);
  };

그런데 페이지 이동 후 활성화된 PIP에서는 영상이 재생되고 있지 않았습니다. PIP API는 비동기 함수이기 때문에 해당 API는 자바스크립트가 실행하지 않습니다. 브라우저가 실행하도록 요청만 할 뿐이죠.  그래서 부탁한 PIP가 언제 완료되는 지는 모릅니다. 비동기 함수인 것이죠 (그렇다고 모든 Web API가 비동기 함수인 것은 아닙니다.) . 그래서 PIP를 확실하게 활성화한 후 다른 함수를 실행하려면 async, await 같은 방법으로 자바스크립트 외부에서 실행되는 함수가 실행 완료되기를 기다려야합니다. 그렇지 않으면 PIP가 실행되는 시점에 PIP를 요청한 video 태그는 이미 사라져 있을 것입니다.(아마도 우리가 요청했던 PIP는 undefined와 같은 것에 대해 실행되겠죠.) 함수를 다음 처럼 바꾸어 비동기 함수를 동기적인 흐름으로 바꾸어 보겠습니다.

  const togglePictureInPicture = async () => {
   if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      try {
        await videoRef.current?.requestPictureInPicture();
    } catch (error) {
      console.error("Error entering PiP mode:", error);
    }
  };

// 특정 버튼을 클릭하면 PIP 활성화 한 후 페이지 이동
  const handleClick = async () => {
    // ...
      await togglePictureInPicture();
    // ...

두 개의 함수를 비동기로 처리하여 PIP가 활성화 된 후 navigate을 실행하도록 하였습니다. 이제는 PIP가 켜진 뒤 화면을 이동하기 때문에 PIP가 시작되면 PIP를 요청한 video를 참조할 수 있습니다.

 

비동기 함수를 동기적으로 사용하는 다른 방법

혹은 togglePicicturInPicture를 다음과 같이 PIP를 호출하는 함수를 반환하도록 할 수도 있습니다. requestPictureInPicture를 직접 호출하므로 비동기 작업을 수행한 후에 즉시 프로미스를 반환할 수 있어 코드가 직관적입니다. 대신 오류 처리가 포함되지 않으므로 해당 함수를 호출할 때마다 .then().catch() 혹은 try-catch를 사용해서 오류처리도 함께 해주어야 하죠. 물론 이 함수에서 then과 catch를 사용하면 오류처리를 추가할 수도 있습니다.

  const togglePictureInPicture = () => {
    console.log(videoRef.current);
    if (document.pictureInPictureElement) {
      return document.exitPictureInPicture();
    } else {
      return videoRef.current!.requestPictureInPicture()  //NOSONAR
    }
  };

const handleClick = async () => {
  // ...
  try {
    await togglePictureInPicture();
    } catch (error) {
      console.error("Error entering PiP mode:", error);
    }
  // ...

 

참고 PIP API를 사용 가능한 브라우저

출처: Can I use,  Picture-in-Picture

썸네일  출처: Microsoft copilot으로 제작

+ Recent posts