import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react' import type { CSSProperties, ForwardedRef } from 'react' import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle } from '@/types' type NiconicoPlayerMessage = | { eventName: 'enterProgrammaticFullScreen' } | { eventName: 'exitProgrammaticFullScreen' } | { eventName: 'loadComplete'; playerId?: string; data: { videoInfo: NiconicoVideoInfo } } | { eventName: 'playerMetadataChange'; playerId?: string; data: NiconicoMetadata } | { eventName: 'playerStatusChange' | 'statusChange'; playerId?: string; data?: unknown } | { eventName: 'error'; playerId?: string; data?: unknown; code?: string; message?: string } type NiconicoCommand = | { eventName: 'play'; sourceConnectorType: 1; playerId: string } | { eventName: 'pause'; sourceConnectorType: 1; playerId: string } | { eventName: 'seek'; sourceConnectorType: 1; playerId: string; data: { time: number } } | { eventName: 'mute'; sourceConnectorType: 1; playerId: string; data: { mute: boolean } } | { eventName: 'volumeChange'; sourceConnectorType: 1; playerId: string; data: { volume: number } } | { eventName: 'commentVisibilityChange'; sourceConnectorType: 1; playerId: string; data: { commentVisibility: boolean } } const EMBED_ORIGIN = 'https://embed.nicovideo.jp' type Props = { id: string width: number height: number style?: CSSProperties onLoadComplete?: (info: NiconicoVideoInfo) => void onMetadataChange?: (meta: NiconicoMetadata) => void } export default forwardRef ((props: Props, ref: ForwardedRef) => { const { id, width, height, style = { }, onLoadComplete, onMetadataChange } = props const iframeRef = useRef (null) const playerId = useMemo (() => `nico-${ id }-${ Math.random ().toString (36).slice (2) }`, [id]) const [screenWidth, setScreenWidth] = useState () const [screenHeight, setScreenHeight] = useState () const [landscape, setLandscape] = useState (false) const [fullScreen, setFullScreen] = useState (false) const src = `${ EMBED_ORIGIN }/watch/${ id }` + '?jsapi=1' + `&playerId=${ encodeURIComponent (playerId) }` + '&persistence=1' + '&oldScript=1' + '&referer=' + '&from=0' + '&allowProgrammaticFullScreen=1' const styleFullScreen: CSSProperties = fullScreen ? { top: 0, left: landscape ? 0 : '100%', position: 'fixed', width: screenWidth, height: screenHeight, zIndex: 2_147_483_647, maxWidth: 'none', transformOrigin: '0% 0%', transform: landscape ? 'none' : 'rotate(90deg)', WebkitTransformOrigin: '0% 0%', WebkitTransform: landscape ? 'none' : 'rotate(90deg)' } : { } const margedStyle: CSSProperties = { border: 'none', maxWidth: '100%', ...style, ...styleFullScreen } const postToPlayer = useCallback ((message: NiconicoCommand) => { const win = iframeRef.current?.contentWindow if (!(win)) return win.postMessage (message, EMBED_ORIGIN) }, []) const play = useCallback (() => { postToPlayer ({ eventName: 'play', sourceConnectorType: 1, playerId }) }, [playerId, postToPlayer]) const pause = useCallback (() => { postToPlayer ({ eventName: 'pause', sourceConnectorType: 1, playerId }) }, [playerId, postToPlayer]) const seek = useCallback ((time: number) => { postToPlayer ({ eventName: 'seek', sourceConnectorType: 1, playerId, data: { time } }) }, [playerId, postToPlayer]) const mute = useCallback (() => { postToPlayer ({ eventName: 'mute', sourceConnectorType: 1, playerId, data: { mute: true } }) }, [playerId, postToPlayer]) const unmute = useCallback (() => { postToPlayer ({ eventName: 'mute', sourceConnectorType: 1, playerId, data: { mute: false } }) }, [playerId, postToPlayer]) const setVolume = useCallback ((volume: number) => { postToPlayer ( { eventName: 'volumeChange', sourceConnectorType: 1, playerId, data: { volume } }) }, [playerId, postToPlayer]) const showComments = useCallback (() => { postToPlayer ( { eventName: 'commentVisibilityChange', sourceConnectorType: 1, playerId, data: { commentVisibility: true } }) }, [playerId, postToPlayer]) const hideComments = useCallback (() => { postToPlayer ( { eventName: 'commentVisibilityChange', sourceConnectorType: 1, playerId, data: { commentVisibility: false } }) }, [playerId, postToPlayer]) useImperativeHandle ( ref, () => ({ play, pause, seek, mute, unmute, setVolume, showComments, hideComments }), [play, pause, seek, mute, unmute, setVolume, showComments, hideComments]) useEffect (() => { const onMessage = (event: MessageEvent) => { if (!(iframeRef.current) || (event.source !== iframeRef.current.contentWindow) || (event.origin !== EMBED_ORIGIN)) return const data = event.data if (!(data) || typeof data !== 'object' || !('eventName' in data)) return if (('playerId' in data) && data.playerId && data.playerId !== playerId) return if (data.eventName === 'enterProgrammaticFullScreen') { setFullScreen (true) return } if (data.eventName === 'exitProgrammaticFullScreen') { setFullScreen (false) return } if (data.eventName === 'loadComplete') { onLoadComplete?.(data.data.videoInfo) return } if (data.eventName === 'playerMetadataChange') { onMetadataChange?.(data.data) return } if (data.eventName === 'error') console.error ('niconico player error:', data) } addEventListener ('message', onMessage) return () => removeEventListener ('message', onMessage) }, [onLoadComplete, onMetadataChange, playerId]) useLayoutEffect (() => { if (!(fullScreen)) return const initialScrollX = scrollX const initialScrollY = scrollY let timer: ReturnType let ended = false const pollingResize = () => { if (ended) return const isLandscape = innerWidth >= innerHeight const windowWidth = `${ isLandscape ? innerWidth : innerHeight }px` const windowHeight = `${ isLandscape ? innerHeight : innerWidth }px` setLandscape (isLandscape) setScreenWidth (windowWidth) setScreenHeight (windowHeight) timer = setTimeout (startPollingResize, 200) } const startPollingResize = () => { if (requestAnimationFrame) requestAnimationFrame (pollingResize) else pollingResize () } startPollingResize () return () => { clearTimeout (timer) ended = true scrollTo (initialScrollX, initialScrollY) } }, [fullScreen]) useEffect (() => { if (!(fullScreen)) return scrollTo (0, 0) }, [screenWidth, screenHeight, fullScreen]) return (