ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] PIP 직접 구현해보기
    Dev 2023. 6. 21. 14:44

    1. Picture-in-Picture API 시도해보기

    프로젝트 진행 중 PIP 기능을 구현해야하는 상황이 생겼다.

    그래서 처음에는 WebApi로 제공되는 PIP API를 이용하여 간단하게 구현할 생각이었다.

    하지만 PIP API를 사용하기 위해서는 영상 데이터를 보여주는 방식이 video 태그여야했다.

    그러나 프로젝트에서는 iframe을 이용한 유튜브 embed 방식을 사용하고 있었기 때문에 PIP API를 사용하여 구현하는 방법은 포기해야했다.

     

    2. PIP 직접 구현해보기

    직접 PIP 기능을 구현하기 위해서는 현재 프로젝트에서의 PIP 기능에 충족되어야하는 조건들을 간단히 정리할 필요가 있었다.

    대략 정리하고 보니 7개 정도의 조건이 나왔다.

     

    1. 페이지 이탈 시 PIP 활성화

    2. PIP 활성화 시 기존에 시청하던 시점을 이어서 재생

    3. PIP 활성화 시 영상 자동 재생

    4. PIP 끄기 기능

    5. 기존 페이지로 돌아가기 기능
    6. 기존 페이지로 돌아왔을 경우 PIP에서 시청하던 시점 이어서 재생
    7. 기존 페이지에서는 PIP 비활성화

     

    이제 해당 조건들을 코드로 구현하면 된다.

     

    2. 코드로  구현하기

    // PIP 컴포넌트
    
    export type VideoPlayDataType = {
        sessionId: number;
        sec: number;
        url: string;
    };
    
    export default function PIP() {
        let { eventId } = useParams();
        const videoRef = useRef(null);
        const [videoData, setVideoData] = useState<VideoPlayDataType>();
    
        const data = window.localStorage.getItem('sessionVideoData') || '';
    
        useEffect(() => {
        // "6. 기존 페이지로 돌아왔을 경우 PIP에서 시청하던 시점 이어서 재생" 조건을 위한 로컬스토리지에 저장하는 작업
            return () => {
                if (videoRef.current && videoRef.current.getCurrentTime && videoData) {
                    const sec = videoRef.current.getCurrentTime(); // 페이지 이탈 시 재생되고 있던 시점을 가져온다.
                    const { sessionId, url } = videoData;
    
                    window.localStorage.setItem('globalVideoData', JSON.stringify({ url, sec, sessionId }));
                }
            };
        }, [videoData]);
    
        useEffect(() => {
            data && setVideoData(JSON.parse(data));
        }, [data]);
    
    
        const onPlayerReady: YouTubeProps['onReady'] = (event) => {
            event.target.seekTo(videoData?.sec); // "2. PIP 활성화 시 기존에 시청하던 시점을 이어서 재생" 조건을 구현하기 위해 seekTo 함수를 이용해 시작 시점 조절
            event.target.playVideo(); // "3. PIP 활성화 시 영상 자동 재생"
        };
    
    
    
       const onClickClose = () => {
    	   // "4. PIP 끄기 기능"         
            window.localStorage.removeItem('sessionVideoData');
            setVideoData(undefined);
        };
        
        if (!videoData?.sec) return <></>;
    
        return (
            <div
                css={css`
                    background: #000;
                    position: absolute;
                    bottom: 0;
                    right: 0;
                    padding: 18px;
                    display: flex;
                    flex-direction: column;
                    gap: 8px;
    
                    button {
                        padding: 0;
                    }
                `}
            >
            
    			
                <div className="ta-e">
    				<button type="button" onClick={onClickClose}>
                        <Icons icon="cancel" width={20} height={20} fill="#fff" />
                    </button>
                </div>
    			
                // 영상이 재생되던 시점을 가져오는 부분과 재생 시점을 이어서 재생하는 부분을 구현하기 하기 위해 react-youtube라는 라이브러리를 사용하였다.
                <YouTube
                    videoId={getYoutubeId(videoData.url ?? '')}
                    opts={{
                        frameBorder: 0,
                        fullscreen: 1,
                        pictureInPicture: 1,
                        width: '500',
                        height: '300',
                        playerVars: {
                            mute: 1,
                            autoplay: 1,
                        },
                    }}
                    // 영상 재생 준비가 완료되면 실행
                    onReady={(e) => {
                        videoRef.current = e.target; // 페이지 이탈 시 작업을 위해 ref에 저장
                        onPlayerReady(e);
                    }}
                />
    
                <div className="ta-c">
                // "5. 기존 페이지로 돌아가기 기능"
                    <Link to={`/${eventId}/session/${videoData.sessionId}`}>
                        <Space size={10}>
                            <p className="white fs-16">탭으로 돌아가기</p>
                            <LinkOutlined style={{ color: '#fff', fontSize: 20 }} />
                        </Space>
                    </Link>
                </div>
            </div>
        );
    }

     

    // 동영상 재생 컴포넌트
    
    export default function Video() {
    const [videoData, setVideoData] = useState<VideoPlayDataType>();
    const globalVideoData = window.localStorage.getItem('globalVideoData') || '';
    
        useEffect(() => {
            return () => {
                if (videoRef.current && videoRef.current.getCurrentTime) {
                    const sec = videoRef.current.getCurrentTime();
    
                    window.localStorage.setItem( // "1. 페이지 이탈 시 PIP 활성화"를 위한 영상 데이터 로컬스토리지에 저장
                        'sessionVideoData',
                        JSON.stringify({ url: data?.liveChannelUrl, sec, sessionId: typeId }),
                    );
                }
            };
        }, [data?.liveChannelUrl, typeId]);
    
        useEffect(() => {
            globalVideoData && setVideoData(JSON.parse(globalVideoData));
        }, [globalVideoData]);
        
        const onPlayerReady: YouTubeProps['onReady'] = (event) => {
            event.target.seekTo(videoData?.sec); // "6. 기존 페이지로 돌아왔을 경우 PIP에서 시청하던 시점 이어서 재생" 로컬 스토리지에 저장된 마지막 재생 시점을 이용해 재생
            event.target.playVideo();
        };
        
      return (
        <YouTube
            videoId=''
            opts={{
                frameBorder: 0,
                fullscreen: 1,
                pictureInPicture: 1,
                    playerVars: {
                        mute: 1,
                        autoplay: 1,
                        start: videoData?.sec,
                    },
            }}
            onReady={(e) => {
                videoRef.current = e.target;
                onPlayerReady(e);
            }}
        />
      )
    }

    1. 페이지 이탈 시 PIP 활성화, 7. 기존 페이지에서는 PIP 비활성화 이 두개의 조건은 PIP 컴포넌트를 랜더될 때마다 실행되는 컴포넌트 즉 페이지들의 상위 컴포넌트에 작성하여 1번 조건을 해결하였으며, 7번 조건의 경우 라우트 경로가 영상 재생 라우트인 경우 PIP 컴포넌트를 랜더하지 않도록 하여 해결하였다.

     

    PIP API를 이용하여 구현해보지 못한건 아쉽지만 PIP를 직접 만드는 것도 재미있는 경험이었다.

    댓글

Designed by Tistory.