8cf7107445
#295 #295 #295 #295 #295 #295 #295 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #296
240 lines
7.2 KiB
TypeScript
240 lines
7.2 KiB
TypeScript
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"/>)
|
|
})
|