IFrame Player API 적용기(1)

jiseong·2022년 3월 30일
0

T I Learned

목록 보기
208/291

단순히 IFrame에 src로 넣어서 Youtube 영상을 볼 수도 있지만 IFrame Player API를 사용하면 볼 수 있을 뿐만 아니라 웹에서 Youtube 동영상 플레이어를 쉽게 제어할 수 있다.

script load 유틸함수 작성

우선, IFrame API를 사용하기 위해서 로드를 해야하기 때문에 script를 생성하고 로드하는 유틸함수를 작성해주었다.

여기서 onYouTubeIframeAPIReady함수는 IFrame API 로드가 완료되었을 때 호출되며 이 함수내에서 표시할 플레이어 개체를 만드는 과정이 필요하다.

this.onload를 null로 하는 이유는 IE9에서 작동을 하지 않기 때문에 많이들 저렇게 작성한다.

function load(src: string, cb: (err: Error) => void) {
  const firstScript = document.getElementsByTagName('script')[0];
  const script = document.createElement('script');
  script.src = src;
  script.async = true;

  script.onerror = function () {
    this.onload = null;
    this.onerror = null;
    cb(new Error(`Failed to load ${this.src}`));
  };

  if (firstScript) firstScript.parentNode?.insertBefore(script, firstScript);
  else {
    document.head.appendChild(script);
  }
}

function loadIFrameApi() {
  return new Promise((resolve, reject) => {
    if (typeof window.YT === 'object') {
      resolve(window.YT);
      return;
    }

    const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';

    load(`${protocol}///www.youtube.com/iframe_api`, (err) => {
      if (err) reject(err);
    });

    window.onYouTubeIframeAPIReady = function () {
      resolve(window.YT);
    };
  });
}

export default loadIFrameApi;

플레이어 개체를 생성하는 컴포넌트 작성

이제 위에서 작성한 유틸함수를 불러오는 컴포넌트가 필요하며 컴포넌트가 mount되는 시점에 호출하고 호출한 함수에서 플레이어 개체를 만들어주면 된다.

플레이어 개체가 생성될 때 참조하고 있는 div tag가<iframe>으로 대체되고 플레이어를 제어할 수 있는 매개변수들을 사용할 수 있고 플레이어 개체를 player라는 변수에 담아두면 이 컴포넌트를 호출하는 부모 컴포넌트에서 직접 접근하여 플레이어 개체를 제어 할 수 있게 된다.

this binding을 해주지 않으면 플레이어 개체의 콜백함수들이 this를 찾지 못하기 때문에 콜백으로 호출되는 함수들에 대해서 this를 binding 해주어야 한다.

class Youtube extends Component<Props, State> {
  public player: YT.Player | undefined = undefined;

  constructor(props: Props) {
    super(props);
    this.onPlayerReady = this.onPlayerReady.bind(this);
    this.onPlayerStateChange = this.onPlayerStateChange.bind(this);
  }

  componentDidMount() {
    this.createYoutubePlayer();
  }

  componentDidUpdate(prevProps: Props) {
    // TODO: update 관련 핸들링
  }

  componentWillUnmount() {
    this.player?.destroy();
  }

  onPlayerReady() {
    const {
      customProps: { volume, muted, paused },
    } = this.props;

    if (typeof volume !== 'undefined') this.setVolume(volume);
    if (typeof muted !== 'undefined') this.setMute(muted);
    if (typeof paused !== 'undefined') this.setPaused(paused);
  }

  onPlayerStateChange(event: YT.OnStateChangeEvent) {
    const { stateChangeHandler } = this.props;
    if (stateChangeHandler) stateChangeHandler(event);
  }

  setVolume(volume: number) {
    this.player?.setVolume(volume * 100);
  }

  setMute(isMute: boolean) {
    if (isMute) {
      this.player?.mute();
    } else {
      this.player?.unMute();
    }
  }

  setPaused(isPaused: boolean) {
    if (isPaused) {
      this.player?.pauseVideo();
    } else {
      this.player?.playVideo();
    }
  }

  async createYoutubePlayer() {
    const {
      width = '100',
      height = '100',
      autoplay,
      customProps: { videoId },
    } = this.props;

    try {
      const YT = await loadIFrameApi();

      this.player = new YT.Player('DJ-playlist-player', {
        videoId,
        height,
        width,
        playerVars: {
          autoplay: autoplay ? 1 : 0,
          ...PLAYER_DEFAULT_OPTS,
        },
        events: {
          onReady: this.onPlayerReady,
          onStateChange: this.onPlayerStateChange,
        },
      });
    } catch (err) {
      console.log('Player를 생성할 수 없습니다.');
    }
  }

  render() {
    return <div id="DJ-playlist-player" />;
  }
}

export default Youtube;

렌더링될 때마다 함수를 다시 생성하고 해결하기 위해 useCallback을 사용하는 등의 추가적인 작업이 필요해 보다 편리하게 작성할 수 있는 클래스 컴포넌트로 작성하게 되었다.

여기까지 작성하고 나면 해당 컴포넌트를 호출했을 때 아래와 같이 자동으로 재생되는 유튜브 영상을 볼 수 있게된다.

하지만 기본적으로 제공하는 플레이어를 사용하지 않고 커스텀 플레이어를 사용하려고 하기 때문에 변화되는 상태에 대해 조작을 해주어야 한다.

그래서 커스텀 플레이어를 조작하게 될 때 플레이어 개체를 동일하게 해줘야하는 작업이 필요하다.

다음에...

TODO:
1. 커스텀 플레이어 상태 통일화
2. any type 수정


Reference

0개의 댓글