3
2

More than 1 year has passed since last update.

Reactでニコニコ動画の埋め込みプレイヤーを使う

Last updated at Posted at 2023-09-06

概要

ニコニコ動画の埋め込みプレイヤーってのはこういう記事中とかで動画を流せるやつです。
スクリーンショット 2023-09-06 21.48.12.png

埋め込みコードは各動画ページからコピーでき、以下のような形式になっています。

<script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm41951611/script?w=640&h=360"></script>
<noscript>
    <a href="https://www.nicovideo.jp/watch/sm41951611">一千光年</a>
</noscript>

普通のHTMLであればこれをコピペするだけで良いのですが、Reactはscriptタグを使えないため、そのままコピペしても動かないのでなんとかします。

ついでにreact-youtubeみたいにidから表示できるように切り出しておきましょう。

方法

思いついたのは2つです。

1. appendChildでscriptタグを足す

scriptタグを直で書けないだけなので、useEffectでワンクッションおいてscriptタグを作り、appendChildで追加します。

import { useEffect, useRef } from "react";

type Props = {
  id: string,
  width: number,
  height: number,
};

export default function NicovideoPlayer (props: Props) {
  const { id, width, height } = props;

  const divRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const divElm = divRef.current;
    if (divElm === null) return;

    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = `https://embed.nicovideo.jp/watch/${id}/script?w=${width}&h=${height}`;

    divElm.appendChild(script);

    return () => {
      while (divElm.firstChild) {
        divElm.removeChild(divElm.firstChild);
      }
    };
  }, [height, id, width]);

  return (
    <div ref={divRef} />
  );
}

scriptタグが複数追加されないよう、クリーンアップ関数でdivの中身を空にしましょう。

2. scriptの中身を直で書く

https://embed.nicovideo.jp/watch/sm41951611/script?w=640&h=360
埋め込みにはこの形式のURLを使うようですが、URL先のJavaScriptを見てみるとそこまで長くない上にクエリとして渡したid, height, width以外に変数が使われていなさそうなことがわかります。これならこっちで実装できそうです。

読み込み時にscriptタグの中にiframeを設置しているようなので、iframeを返すコンポーネントを作ります。

import { CSSProperties, useEffect, useLayoutEffect, useRef, useState } from "react";

type Props = {
  id: string,
  width: number,
  height: number,
  style?: CSSProperties,
};

export default function NicovideoPlayer (props: Props) {
  const { id, width, height, style = {} } = props;

  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [screenWidth, setScreenWidth] = useState<CSSProperties['width']>();
  const [screenHeight, setScreenHeight] = useState<CSSProperties['height']>();
  const [isLandscape, setIsLandScape] = useState<boolean>(false);
  const [isFullScreen, setIsFullScreen] = useState<boolean>(false);

  const src = `https://embed.nicovideo.jp/watch/${id}?persistence=1&oldScript=1&referer=&from=0&allowProgrammaticFullScreen=1`;

  const styleFullScreen: CSSProperties = isFullScreen ? {
    top: 0,
    left: isLandscape ? 0 : '100%',
    position: 'fixed',
    width: screenWidth,
    height: screenHeight,
    zIndex: 2147483647,
    maxWidth: 'none',
    transformOrigin: '0% 0%',
    transform: isLandscape ? 'none' : 'rotate(90deg)',
    WebkitTransformOrigin: '0% 0%',
    WebkitTransform: isLandscape ? 'none' : 'rotate(90deg)',
  } : {};

  const margedStyle = {
    border: 'none',
    maxWidth: '100%',
    ...style,
    ...styleFullScreen,
  };

  useEffect(() => {
    const onMessage = (event: MessageEvent<any>) => {
      if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) return;
      if (event.data.eventName === 'enterProgrammaticFullScreen') {
        setIsFullScreen(true);
      } else if (event.data.eventName === 'exitProgrammaticFullScreen') {
        setIsFullScreen(false);
      }
    };

    window.addEventListener('message', onMessage);

    return () => {
      window.removeEventListener('message', onMessage);
    };
  }, []);

  useLayoutEffect(() => {
    if (!isFullScreen) return;

    const initialScrollX = window.scrollX;
    const initialScrollY = window.scrollY;
    let timer: NodeJS.Timeout;
    let ended = false;

    const pollingResize = () => {
      if (ended) return;

      const isLandscape = window.innerWidth >= window.innerHeight;
      const windowWidth = `${isLandscape ? window.innerWidth : window.innerHeight}px`;
      const windowHeight = `${isLandscape ? window.innerHeight : window.innerWidth}px`;

      setIsLandScape(isLandscape);
      setScreenWidth(windowWidth);
      setScreenHeight(windowHeight);
      timer = setTimeout(startPollingResize, 200);
    }

    const startPollingResize = () => {
      if (window.requestAnimationFrame) {
        window.requestAnimationFrame(pollingResize);
      } else {
        pollingResize();
      }
    }

    startPollingResize();

    return () => {
      clearTimeout(timer);
      ended = true;
      window.scrollTo(initialScrollX, initialScrollY);
    };
  }, [isFullScreen]);

  useEffect(() => {
    if (!isFullScreen) return;
    window.scrollTo(0, 0);
  }, [screenWidth, screenHeight, isFullScreen]);

  return (
    <iframe
      ref={iframeRef}
      src={src}
      width={width}
      height={height}
      style={margedStyle}
      allowFullScreen
      allow="autoplay"
    />
  );
}

元のコードにはiframeの追加以外にスマホでのフルスクリーン表示に関する処理があり、せっかくなのでそこも含めてReactらしく書き直してみました。中身の処理内容はそのままなはずです。

1つ目の方法に比べてiframeもReactで書けるのでstyleを楽に当てれたり、細かいところを自分でいじれるので小回りは効きますが、手前で実装する以上当然ニコニコ側の仕様変更には弱くなります。

お好みの方をお使いください。

おまけ

next/scriptを使う(使えない)

実はNext.jsにはscriptタグを擬似的に使える機能があります。

import Script from 'next/script'

<Script type="application/javascript" src="https://embed.nicovideo.jp/watch/sm41951611/script?w=640&h=360"></Script>

あまりにそのまんまなのでコードは端折りますが、importして差し替えるだけです。

わぁ簡単、と見せかけてブラウザ上では実際のscriptタグがbodyの下に配置されてしまうため、scriptタグの中にiframeを置く今回の埋め込みではプレイヤーがページの最下部に配置されてしまい使えませんでした。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2