概要
ニコニコ動画の埋め込みプレイヤーってのはこういう記事中とかで動画を流せるやつです。
埋め込みコードは各動画ページからコピーでき、以下のような形式になっています。
<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を置く今回の埋め込みではプレイヤーがページの最下部に配置されてしまい使えませんでした。