refactor: 重构作品播放页性能、媒体同步与内存管理
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+176
-108
@@ -2,7 +2,13 @@
|
||||
|
||||
import { Play } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { BackgroundCanvas } from "./components/BackgroundCanvas";
|
||||
import { CommentPanel } from "./components/CommentPanel";
|
||||
import { ImageCarousel } from "./components/ImageCarousel";
|
||||
@@ -52,7 +58,7 @@ export default function AwemeDetailClient({
|
||||
data,
|
||||
neighbors,
|
||||
transcript,
|
||||
}: AwemeDetailClientProps) {
|
||||
}: AwemeDetailClientProps): ReactElement {
|
||||
const router = useRouter();
|
||||
const isVideo = data.type === "video";
|
||||
|
||||
@@ -64,24 +70,44 @@ export default function AwemeDetailClient({
|
||||
isVideo ? (data as VideoData).content_tags : [],
|
||||
);
|
||||
|
||||
// 同步跨作品状态重置
|
||||
useEffect(() => {
|
||||
setContentTags(isVideo ? (data as VideoData).content_tags : []);
|
||||
setTranscriptOpen(false);
|
||||
playerState.setProgress(0);
|
||||
playerState.setProgressRestored(false);
|
||||
playerState.setRotation(0);
|
||||
}, [
|
||||
data,
|
||||
isVideo,
|
||||
playerState.setProgress,
|
||||
playerState.setProgressRestored,
|
||||
playerState.setRotation,
|
||||
]);
|
||||
|
||||
// 引用
|
||||
const mediaContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
||||
const backgroundCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const progressRef = useRef<number>(playerState.progress);
|
||||
progressRef.current = playerState.progress;
|
||||
|
||||
const pauseImageMedia = useCallback((reset = false) => {
|
||||
const pauseImageMedia = useCallback((reset = false): void => {
|
||||
const audioEl = audioRef.current;
|
||||
if (audioEl) {
|
||||
audioEl.pause();
|
||||
if (reset) audioEl.currentTime = 0;
|
||||
}
|
||||
|
||||
scrollerRef.current?.querySelectorAll("video").forEach((videoEl) => {
|
||||
videoEl.pause();
|
||||
if (reset) videoEl.currentTime = 0;
|
||||
});
|
||||
const videos = scrollerRef.current?.querySelectorAll("video");
|
||||
if (videos) {
|
||||
for (const videoEl of videos) {
|
||||
videoEl.pause();
|
||||
if (reset) videoEl.currentTime = 0;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 图文轮播状态
|
||||
@@ -115,7 +141,7 @@ export default function AwemeDetailClient({
|
||||
|
||||
// 媒体操作函数
|
||||
const seekTo = useCallback(
|
||||
(ratio: number) => {
|
||||
(ratio: number): void => {
|
||||
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
@@ -129,70 +155,85 @@ export default function AwemeDetailClient({
|
||||
[imageCarouselState.seekTo, imageCount, isVideo],
|
||||
);
|
||||
|
||||
const seekImageByVisualRatio = (ratio: number) => {
|
||||
if (!imageCount) return;
|
||||
const seekImageByVisualRatio = useCallback(
|
||||
(ratio: number): void => {
|
||||
if (!imageCount) return;
|
||||
|
||||
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
||||
const rawSegment = clampedRatio * imageCount;
|
||||
const targetIndex = Math.min(imageCount - 1, Math.floor(rawSegment));
|
||||
const segmentProgress = Math.min(1, Math.max(0, rawSegment - targetIndex));
|
||||
imageCarouselState.goToIndex(targetIndex, segmentProgress);
|
||||
};
|
||||
const clampedRatio = Math.min(1, Math.max(0, ratio));
|
||||
const rawSegment = clampedRatio * imageCount;
|
||||
const targetIndex = Math.min(imageCount - 1, Math.floor(rawSegment));
|
||||
const segmentProgress = Math.min(
|
||||
1,
|
||||
Math.max(0, rawSegment - targetIndex),
|
||||
);
|
||||
imageCarouselState.goToIndex(targetIndex, segmentProgress);
|
||||
},
|
||||
[imageCarouselState.goToIndex, imageCount],
|
||||
);
|
||||
|
||||
const handleControlSeek = (ratio: number) => {
|
||||
if (isVideo) {
|
||||
seekTo(ratio);
|
||||
return;
|
||||
}
|
||||
seekImageByVisualRatio(ratio);
|
||||
};
|
||||
const handleControlSeek = useCallback(
|
||||
(ratio: number): void => {
|
||||
if (isVideo) {
|
||||
seekTo(ratio);
|
||||
return;
|
||||
}
|
||||
seekImageByVisualRatio(ratio);
|
||||
},
|
||||
[isVideo, seekImageByVisualRatio, seekTo],
|
||||
);
|
||||
|
||||
const togglePlay = async () => {
|
||||
const togglePlay = useCallback((): void => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
if (v.paused) await v.play().catch(() => {});
|
||||
else v.pause();
|
||||
if (v.paused) {
|
||||
void v.play().catch(() => {});
|
||||
} else {
|
||||
v.pause();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const el = audioRef.current;
|
||||
if (!playerState.isPlaying) {
|
||||
playerState.setIsPlaying(true);
|
||||
try {
|
||||
await el?.play().catch(() => {});
|
||||
} catch {}
|
||||
void el?.play().catch(() => {});
|
||||
} else {
|
||||
playerState.setIsPlaying(false);
|
||||
pauseImageMedia();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
isVideo,
|
||||
pauseImageMedia,
|
||||
playerState.isPlaying,
|
||||
playerState.setIsPlaying,
|
||||
]);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
const toggleFullscreen = useCallback((): void => {
|
||||
if (!document.fullscreenElement) {
|
||||
if (document.body.requestFullscreen) {
|
||||
document.body.requestFullscreen().catch(() => {});
|
||||
void document.body.requestFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
const vRef = videoRef.current as WebkitVideoElement | null;
|
||||
if (vRef?.requestFullscreen) {
|
||||
vRef.requestFullscreen().catch(() => {});
|
||||
void vRef.requestFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (vRef?.webkitEnterFullscreen) {
|
||||
vRef.webkitEnterFullscreen();
|
||||
}
|
||||
} else {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
void document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const prevImg = useCallback(() => {
|
||||
const prevImg = useCallback((): void => {
|
||||
if (!imageCount) return;
|
||||
const next = Math.max(0, imageCarouselState.idxRef.current - 1);
|
||||
imageCarouselState.goToIndex(next);
|
||||
}, [imageCarouselState.goToIndex, imageCarouselState.idxRef, imageCount]);
|
||||
|
||||
const nextImg = useCallback(() => {
|
||||
const nextImg = useCallback((): void => {
|
||||
if (!imageCount) return;
|
||||
const next = Math.min(
|
||||
imageCount - 1,
|
||||
@@ -201,7 +242,7 @@ export default function AwemeDetailClient({
|
||||
imageCarouselState.goToIndex(next);
|
||||
}, [imageCarouselState.goToIndex, imageCarouselState.idxRef, imageCount]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const handleDownload = useCallback((): void => {
|
||||
if (isVideo) {
|
||||
const videoUrl = (data as VideoData).video_url;
|
||||
const link = document.createElement("a");
|
||||
@@ -212,15 +253,44 @@ export default function AwemeDetailClient({
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
if (!images?.length) return;
|
||||
const currentImage = images[imageCarouselState.idx];
|
||||
const currentImage = images[imageCarouselState.idxRef.current];
|
||||
if (!currentImage) return;
|
||||
const link = document.createElement("a");
|
||||
link.href = currentImage.url;
|
||||
link.download = `image_${data.aweme_id}_${imageCarouselState.idx + 1}.jpg`;
|
||||
link.download = `image_${data.aweme_id}_${imageCarouselState.idxRef.current + 1}.jpg`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
}, [data, imageCarouselState.idxRef, images, isVideo]);
|
||||
|
||||
const handleShowTranscript = useCallback((): void => {
|
||||
setTranscriptOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseTranscript = useCallback((): void => {
|
||||
setTranscriptOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCloseComments = useCallback((): void => {
|
||||
commentState.setOpen(false);
|
||||
}, [commentState.setOpen]);
|
||||
|
||||
const handleToggleComments = useCallback((): void => {
|
||||
commentState.setOpen((v) => !v);
|
||||
}, [commentState.setOpen]);
|
||||
|
||||
const handleNavigatePrev = useCallback((): void => {
|
||||
if (neighbors.prev) {
|
||||
router.push(`/aweme/${neighbors.prev.aweme_id}`);
|
||||
}
|
||||
}, [neighbors.prev, router]);
|
||||
|
||||
const handleNavigateNext = useCallback((): void => {
|
||||
if (neighbors.next) {
|
||||
router.push(`/aweme/${neighbors.next.aweme_id}`);
|
||||
}
|
||||
}, [neighbors.next, router]);
|
||||
|
||||
// 导航相关
|
||||
useNavigation({
|
||||
@@ -233,23 +303,22 @@ export default function AwemeDetailClient({
|
||||
togglePlay,
|
||||
});
|
||||
|
||||
// 背景画布
|
||||
// 背景画布(仅视频且播放时采样绘制)
|
||||
useBackgroundCanvas({
|
||||
isVideo,
|
||||
idx: imageCarouselState.idx,
|
||||
isPlaying: playerState.isPlaying,
|
||||
videoRef,
|
||||
scrollerRef,
|
||||
backgroundCanvasRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isVideo) return;
|
||||
|
||||
const pauseForPageLifecycle = () => {
|
||||
const pauseForPageLifecycle = (): void => {
|
||||
playerState.setIsPlaying(false);
|
||||
pauseImageMedia();
|
||||
};
|
||||
const handleVisibilityChange = () => {
|
||||
const handleVisibilityChange = (): void => {
|
||||
if (document.hidden) pauseForPageLifecycle();
|
||||
};
|
||||
|
||||
@@ -263,27 +332,22 @@ export default function AwemeDetailClient({
|
||||
};
|
||||
}, [isVideo, pauseImageMedia, playerState.setIsPlaying]);
|
||||
|
||||
// Media Session API 集成
|
||||
// Media Session API:元信息仅在作品切换时设置一次,避免高频触发 iOS IPC 卡顿
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
|
||||
const ms = navigator.mediaSession as MediaSessionWithPositionState;
|
||||
|
||||
// 元信息:标题、作者、专辑(使用创建时间),封面图(优先当前图片,其次作者头像)
|
||||
const ms = navigator.mediaSession;
|
||||
const title = data.desc || `作品 ${data.aweme_id}`;
|
||||
const artist = data.author.nickname || "作者";
|
||||
const album = new Date(data.created_at).toLocaleString();
|
||||
// 单一封面图:使用作品 cover_url
|
||||
const coverUrl = (data as AwemeData).cover_url as string | undefined;
|
||||
const coverSize = (data as AwemeData).cover_size as
|
||||
| { w: number; h: number }
|
||||
| undefined;
|
||||
const coverUrl = data.cover_url;
|
||||
const coverSize = data.cover_size;
|
||||
const artwork = coverUrl
|
||||
? [
|
||||
{
|
||||
src: coverUrl,
|
||||
size: `${coverSize?.w || 512}x${coverSize?.h || 512}`,
|
||||
sizes: `${coverSize?.w || 512}x${coverSize?.h || 512}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
@@ -291,15 +355,20 @@ export default function AwemeDetailClient({
|
||||
try {
|
||||
ms.metadata = new MediaMetadata({ title, artist, album, artwork });
|
||||
} catch {}
|
||||
}, [data]);
|
||||
|
||||
// Media Session API:播放状态与低频位置同步(不依赖 progress state,消除 30fps 重绑开销)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
|
||||
const ms = navigator.mediaSession as MediaSessionWithPositionState;
|
||||
|
||||
// 更新播放状态
|
||||
try {
|
||||
ms.playbackState = playerState.isPlaying ? "playing" : "paused";
|
||||
} catch {}
|
||||
|
||||
const getImagesTotalMs = () => imageCarouselState.totalDurationMs;
|
||||
|
||||
const updatePosition = () => {
|
||||
const updatePosition = (): void => {
|
||||
try {
|
||||
if (!ms.setPositionState) return;
|
||||
if (isVideo) {
|
||||
@@ -313,73 +382,67 @@ export default function AwemeDetailClient({
|
||||
playbackRate: playerState.rate ?? 1,
|
||||
});
|
||||
} else if (images?.length) {
|
||||
const totalMs = getImagesTotalMs();
|
||||
const totalMs = imageCarouselState.totalDurationMs;
|
||||
const duration = totalMs / 1000;
|
||||
const position = Math.max(
|
||||
0,
|
||||
Math.min(duration, (playerState.progress * totalMs) / 1000),
|
||||
Math.min(duration, (progressRef.current * totalMs) / 1000),
|
||||
);
|
||||
ms.setPositionState({ duration, position, playbackRate: 1 });
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// 绑定视频事件以同步状态
|
||||
const v = videoRef.current;
|
||||
if (isVideo && v) {
|
||||
const onPlay = () => {
|
||||
const onPlay = (): void => {
|
||||
try {
|
||||
ms.playbackState = "playing";
|
||||
} catch {}
|
||||
updatePosition();
|
||||
};
|
||||
const onPause = () => {
|
||||
const onPause = (): void => {
|
||||
try {
|
||||
ms.playbackState = "paused";
|
||||
} catch {}
|
||||
updatePosition();
|
||||
};
|
||||
const onTimeUpdate = () => updatePosition();
|
||||
const onSeeked = (): void => updatePosition();
|
||||
|
||||
v.addEventListener("play", onPlay);
|
||||
v.addEventListener("pause", onPause);
|
||||
v.addEventListener("timeupdate", onTimeUpdate);
|
||||
// 初始同步
|
||||
v.addEventListener("seeked", onSeeked);
|
||||
const id = window.setInterval(updatePosition, 1000);
|
||||
updatePosition();
|
||||
|
||||
// 清理
|
||||
return () => {
|
||||
window.clearInterval(id);
|
||||
v.removeEventListener("play", onPlay);
|
||||
v.removeEventListener("pause", onPause);
|
||||
v.removeEventListener("timeupdate", onTimeUpdate);
|
||||
v.removeEventListener("seeked", onSeeked);
|
||||
};
|
||||
} else if (!isVideo && images?.length) {
|
||||
// 图文用定时器定期同步位置
|
||||
}
|
||||
|
||||
if (!isVideo && images?.length) {
|
||||
const id = window.setInterval(updatePosition, 1000);
|
||||
// 初始同步
|
||||
updatePosition();
|
||||
return () => window.clearInterval(id);
|
||||
}
|
||||
// 依赖:当这些变化时刷新元信息与位置状态
|
||||
}, [
|
||||
data,
|
||||
imageCarouselState.totalDurationMs,
|
||||
images,
|
||||
images?.length,
|
||||
isVideo,
|
||||
playerState.isPlaying,
|
||||
playerState.progress,
|
||||
playerState.rate,
|
||||
]);
|
||||
|
||||
// 绑定系统媒体控件的操作处理程序
|
||||
// 绑定系统媒体控件的操作处理程序(不依赖 progress state,仅在作品切换时注册一次)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!("mediaSession" in navigator)) return;
|
||||
const ms = navigator.mediaSession;
|
||||
|
||||
const getImagesTotalMs = () => imageCarouselState.totalDurationMs;
|
||||
|
||||
const handlePlay = async () => {
|
||||
const handlePlay = async (): Promise<void> => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
try {
|
||||
@@ -392,7 +455,7 @@ export default function AwemeDetailClient({
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
const handlePause = () => {
|
||||
const handlePause = (): void => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
v?.pause();
|
||||
@@ -401,7 +464,7 @@ export default function AwemeDetailClient({
|
||||
pauseImageMedia();
|
||||
}
|
||||
};
|
||||
const handleStop = () => {
|
||||
const handleStop = (): void => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (v) {
|
||||
@@ -414,30 +477,30 @@ export default function AwemeDetailClient({
|
||||
seekTo(0);
|
||||
}
|
||||
};
|
||||
const handleSeekDelta = (deltaSec: number) => {
|
||||
const handleSeekDelta = (deltaSec: number): void => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (v)
|
||||
if (v) {
|
||||
v.currentTime = Math.max(
|
||||
0,
|
||||
Math.min(v.duration || Infinity, v.currentTime + deltaSec),
|
||||
);
|
||||
}
|
||||
} else if (images?.length) {
|
||||
const totalMs = getImagesTotalMs();
|
||||
const totalMs = imageCarouselState.totalDurationMs;
|
||||
if (totalMs <= 0) return;
|
||||
const deltaRatio = (deltaSec * 1000) / totalMs;
|
||||
seekTo(playerState.progress + deltaRatio);
|
||||
seekTo(progressRef.current + deltaRatio);
|
||||
}
|
||||
};
|
||||
const handleSeekTo = (seekTimeSec: number) => {
|
||||
const handleSeekTo = (seekTimeSec: number): void => {
|
||||
if (isVideo) {
|
||||
const v = videoRef.current;
|
||||
if (v && Number.isFinite(v.duration)) {
|
||||
v.currentTime = Math.max(0, Math.min(v.duration, seekTimeSec));
|
||||
}
|
||||
} else if (images?.length) {
|
||||
const totalMs = getImagesTotalMs();
|
||||
const totalSec = totalMs / 1000;
|
||||
const totalSec = imageCarouselState.totalDurationMs / 1000;
|
||||
if (totalSec > 0) seekTo(seekTimeSec / totalSec);
|
||||
}
|
||||
};
|
||||
@@ -446,10 +509,10 @@ export default function AwemeDetailClient({
|
||||
ms.setActionHandler("pause", handlePause);
|
||||
ms.setActionHandler("stop", handleStop);
|
||||
ms.setActionHandler("seekbackward", (details: MediaSessionActionDetails) =>
|
||||
handleSeekDelta(-((details?.seekOffset as number) || 10)),
|
||||
handleSeekDelta(-(details?.seekOffset ?? 10)),
|
||||
);
|
||||
ms.setActionHandler("seekforward", (details: MediaSessionActionDetails) =>
|
||||
handleSeekDelta((details?.seekOffset as number) || 10),
|
||||
handleSeekDelta(details?.seekOffset ?? 10),
|
||||
);
|
||||
ms.setActionHandler("seekto", (details: MediaSessionActionDetails) => {
|
||||
if (typeof details?.seekTime === "number") handleSeekTo(details.seekTime);
|
||||
@@ -483,11 +546,10 @@ export default function AwemeDetailClient({
|
||||
};
|
||||
}, [
|
||||
isVideo,
|
||||
images,
|
||||
images?.length,
|
||||
neighbors.prev,
|
||||
neighbors.next,
|
||||
router,
|
||||
playerState.progress,
|
||||
playerState.setIsPlaying,
|
||||
pauseImageMedia,
|
||||
imageCarouselState.totalDurationMs,
|
||||
@@ -498,7 +560,7 @@ export default function AwemeDetailClient({
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<BackgroundCanvas ref={backgroundCanvasRef} />
|
||||
{isVideo ? <BackgroundCanvas ref={backgroundCanvasRef} /> : null}
|
||||
|
||||
<div className="relative h-full landscape:flex landscape:flex-row">
|
||||
{/* 主媒体区域 */}
|
||||
@@ -510,6 +572,7 @@ export default function AwemeDetailClient({
|
||||
<div className="absolute inset-0 z-0">
|
||||
{isVideo ? (
|
||||
<VideoPlayer
|
||||
key={data.aweme_id}
|
||||
ref={videoRef}
|
||||
videoUrl={(data as VideoData).video_url}
|
||||
rotation={playerState.rotation}
|
||||
@@ -519,15 +582,24 @@ export default function AwemeDetailClient({
|
||||
/>
|
||||
) : (
|
||||
<ImageCarousel
|
||||
key={data.aweme_id}
|
||||
ref={scrollerRef}
|
||||
images={images}
|
||||
currentIndex={imageCarouselState.idx}
|
||||
isPlaying={playerState.isPlaying}
|
||||
segmentProgress={imageCarouselState.segProgress}
|
||||
segmentProgressRef={imageCarouselState.segProgressRef}
|
||||
mediaSyncToken={imageCarouselState.mediaSyncToken}
|
||||
loopMode={playerState.loopMode}
|
||||
hasNextNeighbor={Boolean(neighbors.next)}
|
||||
rotation={playerState.rotation}
|
||||
objectFit={playerState.objectFit}
|
||||
failedAnimatedMap={imageCarouselState.failedAnimatedMap}
|
||||
onTogglePlay={togglePlay}
|
||||
onAnimatedDuration={imageCarouselState.setSegmentDuration}
|
||||
onMediaReady={imageCarouselState.markMediaReady}
|
||||
onAnimatedFailed={imageCarouselState.markAnimatedFailed}
|
||||
onAnimatedProgress={imageCarouselState.handleAnimatedProgress}
|
||||
onAnimatedEnded={imageCarouselState.handleAnimatedEnded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -535,7 +607,7 @@ export default function AwemeDetailClient({
|
||||
{/* 暂停图标 */}
|
||||
{!playerState.isPlaying && (
|
||||
<div className="pointer-events-none absolute inset-0 z-30 grid place-items-center">
|
||||
<div className="rounded-full bg-black/40 p-6 backdrop-blur-sm">
|
||||
<div className="rounded-full bg-black/50 p-6">
|
||||
<Play size={64} className="text-white" fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,8 +651,8 @@ export default function AwemeDetailClient({
|
||||
segmentProgress={imageCarouselState.segProgress}
|
||||
musicUrl={isVideo ? undefined : (data as ImageData).music_url}
|
||||
audioRef={audioRef}
|
||||
hasTranscript={isVideo && !!transcript}
|
||||
onShowTranscript={() => setTranscriptOpen(true)}
|
||||
hasTranscript={isVideo && Boolean(transcript)}
|
||||
onShowTranscript={handleShowTranscript}
|
||||
onTogglePlay={togglePlay}
|
||||
onSeek={handleControlSeek}
|
||||
onVolumeChange={playerState.setVolume}
|
||||
@@ -598,20 +670,16 @@ export default function AwemeDetailClient({
|
||||
neighbors={neighbors}
|
||||
commentsCount={data.commentsCount}
|
||||
likesCount={data.likesCount}
|
||||
onNavigatePrev={() =>
|
||||
neighbors.prev && router.push(`/aweme/${neighbors.prev.aweme_id}`)
|
||||
}
|
||||
onNavigateNext={() =>
|
||||
neighbors.next && router.push(`/aweme/${neighbors.next.aweme_id}`)
|
||||
}
|
||||
onToggleComments={() => commentState.setOpen((v) => !v)}
|
||||
onNavigatePrev={handleNavigatePrev}
|
||||
onNavigateNext={handleNavigateNext}
|
||||
onToggleComments={handleToggleComments}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* 评论面板 */}
|
||||
<CommentPanel
|
||||
open={commentState.open}
|
||||
onClose={() => commentState.setOpen(false)}
|
||||
onClose={handleCloseComments}
|
||||
author={data.author}
|
||||
createdAt={data.created_at}
|
||||
awemeId={data.aweme_id}
|
||||
@@ -621,7 +689,7 @@ export default function AwemeDetailClient({
|
||||
{/* 转录面板 */}
|
||||
<TranscriptPanel
|
||||
open={transcriptOpen}
|
||||
onClose={() => setTranscriptOpen(false)}
|
||||
onClose={handleCloseTranscript}
|
||||
transcript={transcript}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user