Skip to content

Commit 95588db

Browse files
committed
Add danmaku (with gravity)
1 parent 8eb30d8 commit 95588db

File tree

9 files changed

+653
-1
lines changed

9 files changed

+653
-1
lines changed

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,5 @@ dist
158158
Caddyfile
159159
dockerdata/
160160
packages/api-types/dist
161+
162+
apps/web/public/static/vendor/

apps/web/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"live.player.autoplay-blocked": "Click/tap to play",
113113
"live.player.seek-latest": "Seek to the latest",
114114
"live.player.reload": "Reload the stream",
115+
"live.player.danmaku": "弾幕の表示/非表示切り替え",
115116
"live.player.wide": "Wide in window",
116117
"live.player.maximize": "Maximize",
117118
"page.account-settings.title": "Account settings",

apps/web/locales/ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"live.player.autoplay-blocked": "クリック/タップして再生",
113113
"live.player.seek-latest": "最新までシーク",
114114
"live.player.reload": "配信を開き直す",
115+
"live.player.danmaku": "弾幕の表示/非表示切り替え",
115116
"live.player.wide": "ウインドウ内ワイド",
116117
"live.player.maximize": "最大化",
117118
"page.account-settings.title": "アカウント設定",

apps/web/organisms/live/live-app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const LiveApp: FC<Props> = ({ live, streamer }) => {
102102
liveId={live.id}
103103
liveTitle={live.title}
104104
userId={user?.id}
105+
comments={comments}
105106
/>
106107
) : (
107108
<VideoMessageBox

apps/web/organisms/live/video/controller.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { FC, Fragment, RefObject, useEffect, useState } from 'react';
2323
import {
2424
FiChevronsUp,
2525
FiMaximize,
26+
FiMessageCircle,
2627
FiPause,
2728
FiPlay,
2829
FiRefreshCw,
@@ -52,6 +53,7 @@ type LiveProps = {
5253
latency: number;
5354
playType: PlayType<'live'> | undefined;
5455
onChangePlayType: (type: PlayType<'live'>) => void;
56+
onChangeDanmaku: () => void;
5557
};
5658

5759
type VideoProps = {
@@ -204,6 +206,14 @@ export const Controller: FC<Props> = props => {
204206

205207
<Spacer />
206208

209+
{isLive && (
210+
<Tooltip label={intl.formatMessage({ id: 'live.player.danmaku' })}>
211+
<Button variant="ghost" onClick={props.onChangeDanmaku} size="sm">
212+
<FiMessageCircle />
213+
</Button>
214+
</Tooltip>
215+
)}
216+
207217
{props.playType && (
208218
<Menu>
209219
<MenuButton as={Button} variant="ghost" size="sm">

apps/web/organisms/live/video/live-player.tsx

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Controller } from './controller';
88
import { LiveUrls } from 'api-types/api/v1/lives/_liveId@number/url';
99
import { useVideoStream } from '~/utils/hooks/use-video-stream';
1010
import { useMuxData } from '~/utils/hooks/use-mux-data';
11+
import { CommentPublic } from 'api-types/common/types';
12+
import { Gravity } from '~/utils/danmaku/gravity/jgravity-fork';
1113

1214
type Props = {
1315
thumbnailUrl: string;
@@ -19,6 +21,7 @@ type Props = {
1921
liveId?: number;
2022
liveTitle?: string;
2123
userId?: number;
24+
comments: CommentPublic[];
2225
};
2326

2427
export const LivePlayer: FC<Props> = ({
@@ -29,17 +32,22 @@ export const LivePlayer: FC<Props> = ({
2932
isStreamer,
3033
liveId,
3134
liveTitle,
32-
userId
35+
userId,
36+
comments
3337
}) => {
3438
const videoRef = useRef<HTMLVideoElement>(null);
3539
const containerRef = useRef<HTMLDivElement>(null);
40+
const danmakuRef = useRef<HTMLDivElement>(null);
41+
const gravityRef = useRef<Gravity | null>(null);
42+
const commentAddedRef = useRef<Set<number> | null>(null);
3643
const lastPlayingRef = useRef(0);
3744
const [latency, setLatency] = useState<number>(-1);
3845
const [error, setError] = useState<unknown>();
3946
useAPIError(error);
4047
const { show, events } = usePlayerTouch();
4148
const [canPlay, setCanPlay] = useState(false);
4249
const [maybeBlocked, setMaybeBlocked] = useState(false);
50+
const [danmaku, setDanmaku] = useState(true);
4351
const { playType, setPlayType, play } = useVideoStream('live', videoRef, url);
4452
useMuxData(videoRef, liveId, liveTitle, userId, playType);
4553

@@ -161,6 +169,61 @@ export const LivePlayer: FC<Props> = ({
161169
}
162170
}, [canPlay, autoSeek]);
163171

172+
useEffect(() => {
173+
const container = danmakuRef.current;
174+
if (!container || !danmaku) {
175+
return;
176+
}
177+
178+
void (async () => {
179+
const gravity = new Gravity({}, container);
180+
await gravity.init();
181+
gravityRef.current = gravity;
182+
})();
183+
184+
return () => {
185+
const gravity = gravityRef.current;
186+
if (gravity) {
187+
gravity.destroy();
188+
gravityRef.current = null;
189+
}
190+
};
191+
}, [danmaku]);
192+
193+
useEffect(() => {
194+
if (!danmaku) {
195+
commentAddedRef.current = null;
196+
return;
197+
}
198+
if (!commentAddedRef.current) {
199+
const set = new Set<number>();
200+
commentAddedRef.current = set;
201+
202+
comments.forEach(comment => {
203+
set.add(comment.id);
204+
});
205+
return;
206+
}
207+
208+
const gravity = gravityRef.current;
209+
if (!gravity) {
210+
return;
211+
}
212+
213+
for (const comment of comments) {
214+
if (commentAddedRef.current?.has(comment.id)) {
215+
break;
216+
}
217+
218+
commentAddedRef.current.add(comment.id);
219+
const dom = gravity.add(comment.content, false);
220+
221+
setTimeout(() => {
222+
gravity.remove(dom);
223+
}, 10000);
224+
}
225+
}, [comments, danmaku]);
226+
164227
return (
165228
<Box
166229
{...events}
@@ -184,6 +247,8 @@ export const LivePlayer: FC<Props> = ({
184247
<video ref={videoRef} autoPlay playsInline controls={false} />
185248
</VideoContainer>
186249

250+
<DanmakuContainer ref={danmakuRef} />
251+
187252
{!canPlay && (
188253
<LoadingContainer>
189254
<Spinner size="xl" />
@@ -204,6 +269,7 @@ export const LivePlayer: FC<Props> = ({
204269
latency={latency}
205270
playType={playType}
206271
onChangePlayType={setPlayType}
272+
onChangeDanmaku={() => setDanmaku(prev => !prev)}
207273
/>
208274
</Box>
209275
);
@@ -216,6 +282,56 @@ const VideoContainer = styled(AspectRatio)`
216282
background-repeat: no-repeat;
217283
`;
218284

285+
const DanmakuContainer = styled(Box)`
286+
position: absolute;
287+
width: 100%;
288+
height: 100%;
289+
top: 0;
290+
left: 0;
291+
right: 0;
292+
bottom: 0;
293+
pointer-events: none;
294+
user-select: none;
295+
296+
span {
297+
/* credit: https://qiita.com/NoxGit/items/eb0904822c0f0fe97650 */
298+
text-shadow:
299+
black 2px 0,
300+
black -2px 0,
301+
black 0 -2px,
302+
black 0 2px,
303+
black 2px 2px,
304+
black -2px 2px,
305+
black 2px -2px,
306+
black -2px -2px,
307+
black 1px 2px,
308+
black -1px 2px,
309+
black 1px -2px,
310+
black -1px -2px,
311+
black 2px 1px,
312+
black -2px 1px,
313+
black 2px -1px,
314+
black -2px -1px;
315+
316+
font-size: 1.5rem;
317+
line-height: 1 !important;
318+
color: #fff !important;
319+
opacity: 0.7;
320+
white-space: nowrap;
321+
max-width: 100%;
322+
overflow-x: hidden;
323+
overflow-y: visible;
324+
325+
::-webkit-scrollbar {
326+
display: none;
327+
}
328+
329+
@media (min-width: 768px) {
330+
font-size: 2rem;
331+
}
332+
}
333+
`;
334+
219335
const LoadingContainer = styled(Center)`
220336
position: absolute;
221337
width: 100%;

apps/web/public/static/vendor/box2d.min.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/public/static/vendor/inheritance.min.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)