このコミットが含まれているのは:
@@ -114,6 +114,10 @@ npm run preview
|
|||||||
requires a break.
|
requires a break.
|
||||||
- Ruby blocks use separate `{ ... }` rules from hashes, with 2-space body
|
- Ruby blocks use separate `{ ... }` rules from hashes, with 2-space body
|
||||||
indentation.
|
indentation.
|
||||||
|
- For arrays, never put whitespace or a line break immediately before `]`.
|
||||||
|
- Keep the first element on the same line as `[` by default.
|
||||||
|
- If an array would exceed the line limit, break after `[` and indent
|
||||||
|
elements by 4 spaces.
|
||||||
- TypeScript and Python: use GNU-style spacing before parentheses where
|
- TypeScript and Python: use GNU-style spacing before parentheses where
|
||||||
syntactically valid.
|
syntactically valid.
|
||||||
- Never write Ruby, TypeScript, or TSX lines longer than 99 characters.
|
- Never write Ruby, TypeScript, or TSX lines longer than 99 characters.
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ service, representation, and spec.
|
|||||||
by 4 spaces.
|
by 4 spaces.
|
||||||
- Put one logical pair per line when the expression would otherwise become
|
- Put one logical pair per line when the expression would otherwise become
|
||||||
dense.
|
dense.
|
||||||
|
- For Ruby arrays, never put whitespace or a line break immediately before `]`.
|
||||||
|
- Keep the first element on the same line as `[` by default.
|
||||||
|
- If an array would exceed the line limit, break after `[` and indent
|
||||||
|
elements by 4 spaces.
|
||||||
- For Ruby blocks, use 2-space indentation for the block body.
|
- For Ruby blocks, use 2-space indentation for the block body.
|
||||||
- Keep comments short and useful; avoid narrating obvious code.
|
- Keep comments short and useful; avoid narrating obvious code.
|
||||||
- Do not add production dependencies without approval.
|
- Do not add production dependencies without approval.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
class TheatrePostSelector
|
class TheatrePostSelector
|
||||||
Candidate = Struct.new(:post, :weight, :penalty, :tags, keyword_init: true)
|
Candidate = Struct.new(:post, :weight, :penalty, :tags, keyword_init: true)
|
||||||
|
ELIGIBLE_POST_URL_CONDITION =
|
||||||
|
["url LIKE '%nicovideo.jp%'",
|
||||||
|
"url LIKE '%youtube.com/watch%'",
|
||||||
|
"url LIKE '%youtu.be/%'"]
|
||||||
|
.join(' OR ')
|
||||||
|
|
||||||
def initialize theatre:
|
def initialize theatre:
|
||||||
@theatre = theatre
|
@theatre = theatre
|
||||||
@@ -52,7 +57,7 @@ class TheatrePostSelector
|
|||||||
end
|
end
|
||||||
|
|
||||||
def eligible_posts
|
def eligible_posts
|
||||||
posts = Post.where("url LIKE '%nicovideo.jp%'")
|
posts = Post.where(ELIGIBLE_POST_URL_CONDITION)
|
||||||
posts = posts.where.not(id: theatre.current_post_id) if theatre.current_post_id
|
posts = posts.where.not(id: theatre.current_post_id) if theatre.current_post_id
|
||||||
posts
|
posts
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ RSpec.describe 'Theatres API', type: :request do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let!(:youtube_post) do
|
||||||
|
Post.create!(
|
||||||
|
title: 'youtube post',
|
||||||
|
url: 'https://www.youtube.com/watch?v=yt123'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
let!(:other_post) do
|
let!(:other_post) do
|
||||||
Post.create!(
|
Post.create!(
|
||||||
title: 'other post',
|
title: 'other post',
|
||||||
@@ -286,7 +293,7 @@ RSpec.describe 'Theatres API', type: :request do
|
|||||||
.to change { theatre.reload.current_post_id }
|
.to change { theatre.reload.current_post_id }
|
||||||
|
|
||||||
expect(response).to have_http_status(:no_content)
|
expect(response).to have_http_status(:no_content)
|
||||||
expect([niconico_post.id, second_niconico_post.id])
|
expect([niconico_post.id, second_niconico_post.id, youtube_post.id])
|
||||||
.to include(theatre.reload.current_post_id)
|
.to include(theatre.reload.current_post_id)
|
||||||
expect(theatre.reload.current_post_started_at)
|
expect(theatre.reload.current_post_started_at)
|
||||||
.to be_within(1.second).of(Time.current)
|
.to be_within(1.second).of(Time.current)
|
||||||
@@ -294,10 +301,27 @@ RSpec.describe 'Theatres API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when only a YouTube post is eligible' do
|
||||||
|
before do
|
||||||
|
niconico_post.destroy!
|
||||||
|
second_niconico_post.destroy!
|
||||||
|
theatre.update!(host_user: member)
|
||||||
|
sign_in_as(member)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets current_post to the YouTube post' do
|
||||||
|
do_request
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
expect(theatre.reload.current_post_id).to eq(youtube_post.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when current user is host and no eligible post exists' do
|
context 'when current user is host and no eligible post exists' do
|
||||||
before do
|
before do
|
||||||
niconico_post.destroy!
|
niconico_post.destroy!
|
||||||
second_niconico_post.destroy!
|
second_niconico_post.destroy!
|
||||||
|
youtube_post.destroy!
|
||||||
theatre.update!(
|
theatre.update!(
|
||||||
host_user: member,
|
host_user: member,
|
||||||
current_post: other_post,
|
current_post: other_post,
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ If either command cannot be run or fails, report the exact command and failure.
|
|||||||
- Never write a TypeScript or TSX line longer than 99 characters.
|
- Never write a TypeScript or TSX line longer than 99 characters.
|
||||||
- Aim to keep TypeScript and TSX lines within 79 characters where practical.
|
- Aim to keep TypeScript and TSX lines within 79 characters where practical.
|
||||||
- Use 4-space logical indentation in TypeScript and TSX.
|
- Use 4-space logical indentation in TypeScript and TSX.
|
||||||
|
- For arrays, never put whitespace or a line break immediately before `]`.
|
||||||
|
- Keep the first element on the same line as `[` by default.
|
||||||
|
- If an array would exceed the line limit, break after `[` and indent
|
||||||
|
elements by 4 spaces.
|
||||||
- In TypeScript and TSX only, replace every leading run of 8 spaces with a tab
|
- In TypeScript and TSX only, replace every leading run of 8 spaces with a tab
|
||||||
to reduce bytes.
|
to reduce bytes.
|
||||||
- Treat one leading tab as exactly equivalent to 8 leading spaces.
|
- Treat one leading tab as exactly equivalent to 8 leading spaces.
|
||||||
|
|||||||
@@ -14,10 +14,20 @@ import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle } from '
|
|||||||
type NiconicoPlayerMessage =
|
type NiconicoPlayerMessage =
|
||||||
| { eventName: 'enterProgrammaticFullScreen' }
|
| { eventName: 'enterProgrammaticFullScreen' }
|
||||||
| { eventName: 'exitProgrammaticFullScreen' }
|
| { eventName: 'exitProgrammaticFullScreen' }
|
||||||
| { eventName: 'loadComplete'; playerId?: string; data: { videoInfo: NiconicoVideoInfo } }
|
| { eventName: 'loadComplete'
|
||||||
| { eventName: 'playerMetadataChange'; playerId?: string; data: NiconicoMetadata }
|
playerId?: string
|
||||||
| { eventName: 'playerStatusChange' | 'statusChange'; playerId?: string; data?: unknown }
|
data: { videoInfo: NiconicoVideoInfo } }
|
||||||
| { eventName: 'error'; playerId?: string; data?: unknown; code?: string; message?: string }
|
| { 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 =
|
type NiconicoCommand =
|
||||||
| { eventName: 'play'; sourceConnectorType: 1; playerId: string }
|
| { eventName: 'play'; sourceConnectorType: 1; playerId: string }
|
||||||
@@ -30,6 +40,7 @@ type NiconicoCommand =
|
|||||||
data: { commentVisibility: boolean } }
|
data: { commentVisibility: boolean } }
|
||||||
|
|
||||||
const EMBED_ORIGIN = 'https://embed.nicovideo.jp'
|
const EMBED_ORIGIN = 'https://embed.nicovideo.jp'
|
||||||
|
const LOAD_COMPLETE_TIMEOUT_MS = 8_000
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
@@ -45,7 +56,10 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
const { id, width, height, style = { }, onLoadComplete, onMetadataChange, onError } = props
|
const { id, width, height, style = { }, onLoadComplete, onMetadataChange, onError } = props
|
||||||
|
|
||||||
const iframeRef = useRef<HTMLIFrameElement> (null)
|
const iframeRef = useRef<HTMLIFrameElement> (null)
|
||||||
const playerId = useMemo (() => `nico-${ id }-${ Math.random ().toString (36).slice (2) }`, [id])
|
const loadCompleteTimerRef = useRef<ReturnType<typeof setTimeout> | null> (null)
|
||||||
|
const playerId = useMemo (
|
||||||
|
() => `nico-${ id }-${ Math.random ().toString (36).slice (2) }`,
|
||||||
|
[id])
|
||||||
|
|
||||||
const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
|
const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
|
||||||
const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
|
const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
|
||||||
@@ -80,6 +94,24 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
const margedStyle: CSSProperties =
|
const margedStyle: CSSProperties =
|
||||||
{ border: 'none', maxWidth: '100%', ...style, ...styleFullScreen }
|
{ border: 'none', maxWidth: '100%', ...style, ...styleFullScreen }
|
||||||
|
|
||||||
|
const clearLoadCompleteTimer = useCallback (() => {
|
||||||
|
if (!(loadCompleteTimerRef.current))
|
||||||
|
return
|
||||||
|
|
||||||
|
clearTimeout (loadCompleteTimerRef.current)
|
||||||
|
loadCompleteTimerRef.current = null
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const startLoadCompleteTimer = useCallback (() => {
|
||||||
|
clearLoadCompleteTimer ()
|
||||||
|
loadCompleteTimerRef.current = setTimeout (() => {
|
||||||
|
onError?.({
|
||||||
|
eventName: 'loadCompleteTimeout',
|
||||||
|
reason: 'niconico video length was not reported by embed',
|
||||||
|
})
|
||||||
|
}, LOAD_COMPLETE_TIMEOUT_MS)
|
||||||
|
}, [clearLoadCompleteTimer, onError])
|
||||||
|
|
||||||
const postToPlayer = useCallback ((message: NiconicoCommand) => {
|
const postToPlayer = useCallback ((message: NiconicoCommand) => {
|
||||||
const win = iframeRef.current?.contentWindow
|
const win = iframeRef.current?.contentWindow
|
||||||
if (!(win))
|
if (!(win))
|
||||||
@@ -163,6 +195,7 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
|
|
||||||
if (data.eventName === 'loadComplete')
|
if (data.eventName === 'loadComplete')
|
||||||
{
|
{
|
||||||
|
clearLoadCompleteTimer ()
|
||||||
onLoadComplete?.(data.data.videoInfo)
|
onLoadComplete?.(data.data.videoInfo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -175,6 +208,7 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
|
|
||||||
if (data.eventName === 'error')
|
if (data.eventName === 'error')
|
||||||
{
|
{
|
||||||
|
clearLoadCompleteTimer ()
|
||||||
console.error ('niconico player error:', data)
|
console.error ('niconico player error:', data)
|
||||||
onError?.(data)
|
onError?.(data)
|
||||||
}
|
}
|
||||||
@@ -183,7 +217,9 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
addEventListener ('message', onMessage)
|
addEventListener ('message', onMessage)
|
||||||
|
|
||||||
return () => removeEventListener ('message', onMessage)
|
return () => removeEventListener ('message', onMessage)
|
||||||
}, [onError, onLoadComplete, onMetadataChange, playerId])
|
}, [clearLoadCompleteTimer, onError, onLoadComplete, onMetadataChange, playerId])
|
||||||
|
|
||||||
|
useEffect (() => clearLoadCompleteTimer, [clearLoadCompleteTimer])
|
||||||
|
|
||||||
useLayoutEffect (() => {
|
useLayoutEffect (() => {
|
||||||
if (!(fullScreen))
|
if (!(fullScreen))
|
||||||
@@ -238,6 +274,7 @@ export default forwardRef ((props: Props, ref: ForwardedRef<NiconicoViewerHandle
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={margedStyle}
|
style={margedStyle}
|
||||||
|
onLoad={startLoadCompleteTimer}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="autoplay"/>)
|
allow="autoplay"/>)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import YoutubeEmbed from 'react-youtube'
|
import YoutubeEmbed from 'react-youtube'
|
||||||
|
|
||||||
import NicoViewer from '@/components/NicoViewer'
|
import NicoViewer from '@/components/NicoViewer'
|
||||||
@@ -8,18 +8,97 @@ import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
|||||||
import type { FC, RefObject } from 'react'
|
import type { FC, RefObject } from 'react'
|
||||||
|
|
||||||
import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle, Post } from '@/types'
|
import type { NiconicoMetadata, NiconicoVideoInfo, NiconicoViewerHandle, Post } from '@/types'
|
||||||
|
import type { YouTubePlayer } from 'react-youtube'
|
||||||
|
|
||||||
|
type YouTubeEvent<T = unknown> = {
|
||||||
|
data: T
|
||||||
|
target: YouTubePlayer }
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ref?: RefObject<NiconicoViewerHandle | null>
|
ref?: RefObject<NiconicoViewerHandle | null>
|
||||||
post: Post
|
post: Post
|
||||||
onLoadComplete?: (info: NiconicoVideoInfo) => void
|
onLoadComplete?: (info: NiconicoVideoInfo) => void
|
||||||
onMetadataChange?: (meta: NiconicoMetadata) => void
|
onMetadataChange?: (meta: NiconicoMetadata) => void
|
||||||
|
onVideoReady?: (durationMs: number) => void
|
||||||
|
onPlaybackChange?: (currentTimeMs: number) => number | void
|
||||||
onError?: (data: unknown) => void }
|
onError?: (data: unknown) => void }
|
||||||
|
|
||||||
|
|
||||||
const PostEmbed: FC<Props> = ({ ref, post, onLoadComplete, onMetadataChange, onError }) => {
|
const PostEmbed: FC<Props> = ({
|
||||||
|
ref,
|
||||||
|
post,
|
||||||
|
onLoadComplete,
|
||||||
|
onMetadataChange,
|
||||||
|
onVideoReady,
|
||||||
|
onPlaybackChange,
|
||||||
|
onError,
|
||||||
|
}) => {
|
||||||
const dialogue = useDialogue ()
|
const dialogue = useDialogue ()
|
||||||
const [framed, setFramed] = useState (false)
|
const [framed, setFramed] = useState (false)
|
||||||
|
const [youtubePlayer, setYoutubePlayer] = useState<YouTubePlayer | null> (null)
|
||||||
|
|
||||||
|
const reportYoutubePlayback = useCallback (async (player: YouTubePlayer) => {
|
||||||
|
const currentTime = await player.getCurrentTime ()
|
||||||
|
const currentTimeMs = currentTime * 1_000
|
||||||
|
const targetTimeMs = onPlaybackChange?.(currentTimeMs)
|
||||||
|
|
||||||
|
if (typeof targetTimeMs !== 'number')
|
||||||
|
return
|
||||||
|
|
||||||
|
if (Math.abs (currentTimeMs - targetTimeMs) > 5_000)
|
||||||
|
await player.seekTo (targetTimeMs / 1_000, true)
|
||||||
|
}, [onPlaybackChange])
|
||||||
|
|
||||||
|
const handleYoutubeReady = async (event: YouTubeEvent) => {
|
||||||
|
setYoutubePlayer (event.target)
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await event.target.playVideo ()
|
||||||
|
|
||||||
|
const duration = await event.target.getDuration ()
|
||||||
|
const durationMs = duration * 1_000
|
||||||
|
onVideoReady?.(durationMs)
|
||||||
|
|
||||||
|
if (!(Number.isFinite (durationMs)) || durationMs <= 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
await reportYoutubePlayback (event.target)
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
onError?.({ platform: 'youtube', error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleYoutubeStateChange = (event: YouTubeEvent<number>) => {
|
||||||
|
void reportYoutubePlayback (event.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleYoutubeError = (event: YouTubeEvent<number>) => {
|
||||||
|
onError?.({ platform: 'youtube', code: event.data })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNiconicoLoadComplete = (info: NiconicoVideoInfo) => {
|
||||||
|
onVideoReady?.(info.lengthInSeconds * 1_000)
|
||||||
|
onLoadComplete?.(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNiconicoMetadataChange = (meta: NiconicoMetadata) => {
|
||||||
|
onPlaybackChange?.(meta.currentTime)
|
||||||
|
onMetadataChange?.(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
if (!(youtubePlayer) || !(onPlaybackChange))
|
||||||
|
return
|
||||||
|
|
||||||
|
const timer = setInterval (
|
||||||
|
() => void reportYoutubePlayback (youtubePlayer),
|
||||||
|
1_000)
|
||||||
|
|
||||||
|
return () => clearInterval (timer)
|
||||||
|
}, [onPlaybackChange, reportYoutubePlayback, youtubePlayer])
|
||||||
|
|
||||||
const url = new URL (post.url)
|
const url = new URL (post.url)
|
||||||
|
|
||||||
@@ -39,8 +118,8 @@ const PostEmbed: FC<Props> = ({ ref, post, onLoadComplete, onMetadataChange, onE
|
|||||||
id={videoId}
|
id={videoId}
|
||||||
width={640}
|
width={640}
|
||||||
height={360}
|
height={360}
|
||||||
onLoadComplete={onLoadComplete}
|
onLoadComplete={handleNiconicoLoadComplete}
|
||||||
onMetadataChange={onMetadataChange}
|
onMetadataChange={handleNiconicoMetadataChange}
|
||||||
onError={onError}/>)
|
onError={onError}/>)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +150,10 @@ const PostEmbed: FC<Props> = ({ ref, post, onLoadComplete, onMetadataChange, onE
|
|||||||
mute: 0,
|
mute: 0,
|
||||||
loop: 1,
|
loop: 1,
|
||||||
width: '640',
|
width: '640',
|
||||||
height: '360' } }}/>)
|
height: '360' } }}
|
||||||
|
onReady={handleYoutubeReady}
|
||||||
|
onStateChange={handleYoutubeStateChange}
|
||||||
|
onError={handleYoutubeError}/>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ import { useValidationErrors } from '@/lib/useValidationErrors'
|
|||||||
|
|
||||||
import type { FC, FormEvent, ReactNode } from 'react'
|
import type { FC, FormEvent, ReactNode } from 'react'
|
||||||
|
|
||||||
import type { NiconicoMetadata,
|
import type { NiconicoViewerHandle,
|
||||||
NiconicoVideoInfo,
|
|
||||||
NiconicoViewerHandle,
|
|
||||||
Post,
|
Post,
|
||||||
Category,
|
Category,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -88,7 +86,9 @@ const commentBox = (
|
|||||||
{dateString (comment.createdAt)}
|
{dateString (comment.createdAt)}
|
||||||
</div>),
|
</div>),
|
||||||
(
|
(
|
||||||
<div key={`${ comment.no }-post`} className="mt-1 w-full text-xs text-zinc-500 dark:text-zinc-400">
|
<div
|
||||||
|
key={`${ comment.no }-post`}
|
||||||
|
className="mt-1 w-full text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
{programme && (
|
{programme && (
|
||||||
<>
|
<>
|
||||||
<PrefetchLink to={`/posts/${ programme.post.id }`} className="font-bold hover:underline">
|
<PrefetchLink to={`/posts/${ programme.post.id }`} className="font-bold hover:underline">
|
||||||
@@ -439,7 +439,7 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
void refreshProgrammes ()
|
void refreshProgrammes ()
|
||||||
}, [refreshProgrammes, theatreInfo.postId])
|
}, [refreshProgrammes, theatreInfo.postId])
|
||||||
|
|
||||||
const syncPlayback = (meta: NiconicoMetadata) => {
|
const syncPlaybackTime = (currentTimeMs: number): number | void => {
|
||||||
if (!(theatreInfo.postStartedAt))
|
if (!(theatreInfo.postStartedAt))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -447,24 +447,38 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
currentPostElapsedMs (theatreInfo),
|
currentPostElapsedMs (theatreInfo),
|
||||||
videoLength)
|
videoLength)
|
||||||
|
|
||||||
const drift = Math.abs (meta.currentTime - targetTime)
|
const drift = Math.abs (currentTimeMs - targetTime)
|
||||||
|
|
||||||
if (drift > 5_000)
|
if (drift > 5_000)
|
||||||
embedRef.current?.seek (targetTime)
|
embedRef.current?.seek (targetTime)
|
||||||
|
|
||||||
|
return targetTime
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePlaybackError = async () => {
|
const handlePlaybackError = async () => {
|
||||||
if (!(theatreInfoRef.current.hostFlg) || loadingRef.current)
|
if (!(theatreInfoRef.current.hostFlg) || loadingRef.current)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
loadingRef.current = true
|
||||||
|
try
|
||||||
|
{
|
||||||
await advancePost ()
|
await advancePost ()
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
loadingRef.current = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleNiconicoLoadComplete = (info: NiconicoVideoInfo) => {
|
const handleVideoReady = (durationMs: number) => {
|
||||||
const lengthMs = info.lengthInSeconds * 1_000
|
const playableDurationMs =
|
||||||
setVideoLength (lengthMs)
|
Number.isFinite (durationMs)
|
||||||
|
? durationMs
|
||||||
|
: 0
|
||||||
|
|
||||||
if (lengthMs <= 0)
|
setVideoLength (playableDurationMs)
|
||||||
|
|
||||||
|
if (playableDurationMs <= 0)
|
||||||
{
|
{
|
||||||
void handlePlaybackError ()
|
void handlePlaybackError ()
|
||||||
return
|
return
|
||||||
@@ -738,7 +752,8 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="position"
|
layout="position"
|
||||||
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
||||||
className="min-h-0 flex-1 overflow-y-auto bg-zinc-50 text-zinc-950 md:overflow-hidden dark:bg-zinc-950 dark:text-zinc-50">
|
className="min-h-0 flex-1 overflow-y-auto bg-zinc-50 text-zinc-950
|
||||||
|
md:overflow-hidden dark:bg-zinc-950 dark:text-zinc-50">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta name="robots" content="noindex"/>
|
<meta name="robots" content="noindex"/>
|
||||||
{theatre && <title>{`${ theatreTitle } | ${ SITE_TITLE }`}</title>}
|
{theatre && <title>{`${ theatreTitle } | ${ SITE_TITLE }`}</title>}
|
||||||
@@ -811,8 +826,8 @@ const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
|
|||||||
key={post.id}
|
key={post.id}
|
||||||
ref={embedRef}
|
ref={embedRef}
|
||||||
post={post}
|
post={post}
|
||||||
onLoadComplete={handleNiconicoLoadComplete}
|
onVideoReady={handleVideoReady}
|
||||||
onMetadataChange={syncPlayback}
|
onPlaybackChange={syncPlaybackTime}
|
||||||
onError={handlePlaybackError}/>) : (
|
onError={handlePlaybackError}/>) : (
|
||||||
<div className="grid min-h-72 place-items-center text-zinc-400">
|
<div className="grid min-h-72 place-items-center text-zinc-400">
|
||||||
{loading ? '次の投稿を選んでゐます……' : '上映待機中'}
|
{loading ? '次の投稿を選んでゐます……' : '上映待機中'}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする