|
- 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<NiconicoViewerHandle>) => {
- const { id, width, height, style = { }, onLoadComplete, onMetadataChange } = props
-
- const iframeRef = useRef<HTMLIFrameElement> (null)
- const playerId = useMemo (() => `nico-${ id }-${ Math.random ().toString (36).slice (2) }`, [id])
-
- const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
- const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
- const [landscape, setLandscape] = useState<boolean> (false)
- const [fullScreen, setFullScreen] = useState<boolean> (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<NiconicoPlayerMessage>) => {
- 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<typeof setTimeout>
- 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 (
- <iframe
- ref={iframeRef}
- src={src}
- width={width}
- height={height}
- style={margedStyle}
- allowFullScreen
- allow="autoplay"/>)
- })
|