fix(ymb): refresh shorts playback sources
This commit is contained in:
1 parent
535bb57c83
commit
ebf7886485
5 files changed
+71
-9
No files matched your search
@@ -0,0 +1,42 @@
|
||||
# YMB 站点运行态与 Shorts 体验优化计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 让 Shorts 与观看页统一使用 `/app/episodes/{id}/play` 的最新签名播放地址。
|
||||
- 优化播放等待、播放失败、空结果和移动端手势反馈,减少“像演示数据”的体验。
|
||||
- 保持现有深色品牌、YouTube 式信息架构、多语言、匿名浏览和无底部导航。
|
||||
|
||||
## 范围
|
||||
|
||||
- `src/features/shorts/**`
|
||||
- `src/shared/components/player/**`
|
||||
- `src/features/search/**`
|
||||
- 必要的共享样式与本计划文件
|
||||
|
||||
## 约束
|
||||
|
||||
- 不修改 FruitServer 接口契约,不自行拼接签名播放地址。
|
||||
- 播放地址只使用接口返回值;预览流只作为接口失败时的兼容回退。
|
||||
- 不引入新的状态管理或重量级 UI 依赖。
|
||||
- 保持当前分支 `dev`,只提交 YMB 子项目本轮文件。
|
||||
|
||||
## 验证
|
||||
|
||||
- `pnpm lint`
|
||||
- `pnpm build`
|
||||
- `git diff --check`
|
||||
- 本地验证 `/app/feed`、`/app/episodes/{id}/play`、HLS 清单响应和前端首页可访问。
|
||||
|
||||
## 风险 / 待确认
|
||||
|
||||
- Shorts 每次激活视频会请求一次播放票据;使用现有请求取消机制,避免切换页面后写入失效状态。
|
||||
- 当前浏览器自动播放策略仍可能要求静音启动,保留现有声音提示和手动开启逻辑。
|
||||
|
||||
## 实施回执
|
||||
|
||||
- 已完成:Shorts 当前激活视频改为调用 `/app/episodes/{id}/play` 获取最新签名地址,切换激活项时复用 `useRequest` 的取消机制;接口失败时仅回退到推荐流已有播放地址。
|
||||
- 媒体解析:图片 `resolveMedia` 与播放 `resolvePlaybackUrl` 分离,缺失播放地址返回空字符串,不再把 `hero.jpg` 送入 HLS;锁定集和无地址分集保持占位状态。
|
||||
- 交互:Shorts 当前视频播放结束后自动进入下一条,手势、键盘方向键和桌面箭头控制保持不变;等待和播放失败状态在卡片内可见。
|
||||
- 真实链路:`/app/feed?size=6` 返回 6 条并带游标;首条 `/app/episodes/{id}/play` 返回 `accessible=true`、签名 `playUrl` 和多清晰度;HLS 清单响应 `application/vnd.apple.mpegurl`;前端 `http://127.0.0.1:4179/` 返回 200。
|
||||
- 门禁:`pnpm lint` 通过;`pnpm build` 通过(仅有既有大 chunk 提示);`git diff --check` 通过。
|
||||
- 当前状态:待提交本轮源码与计划文件;未修改 FruitServer 契约,未增加底部导航。
|
||||
@@ -1,5 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
import { getImage, resolveMedia } from '../../shared/lib/media';
|
||||
import { getImage, resolveMedia, resolvePlaybackUrl } from '../../shared/lib/media';
|
||||
import { getText } from '../../shared/lib/text';
|
||||
|
||||
export function normalizeDrama(item = {}, lang = 'id') {
|
||||
@@ -18,8 +18,8 @@ export function normalizeDrama(item = {}, lang = 'id') {
|
||||
favoriteCount: Number(item.favoriteCount) || 0,
|
||||
commentCount: Number(item.commentCount) || 0,
|
||||
description: getText(item.synopsisLocales, lang) || '',
|
||||
episodes: episodes.map((episode) => ({ ...episode, playUrl: resolveMedia(episode.playUrl) })),
|
||||
preview: preview ? { ...preview, playUrl: resolveMedia(preview.playUrl) } : null,
|
||||
episodes: episodes.map((episode) => ({ ...episode, playUrl: resolvePlaybackUrl(episode.playUrl) })),
|
||||
preview: preview ? { ...preview, playUrl: resolvePlaybackUrl(preview.playUrl) } : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ export function ShortsFeedPage() {
|
||||
onToggleFollow={() => toggleFollow(video.channel)}
|
||||
onOpenComments={() => setCommentsVideo(video)}
|
||||
onShare={() => shareVideo(video)}
|
||||
onEnded={() => move(1)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,28 @@ import { Bookmark, MessageCircle, Share2, ThumbsUp } from 'lucide-react';
|
||||
import { useLocale } from '../../../app/providers/AppProviders';
|
||||
import { VideoPlayer } from '../../../shared/components/player/VideoPlayer';
|
||||
import { useEngagement } from '../../../shared/hooks/useEngagement';
|
||||
import { useRequest } from '../../../shared/hooks/useRequest';
|
||||
import { formatCount } from '../../../shared/lib/format';
|
||||
import { selectPlaybackUrl } from '../../../shared/lib/media';
|
||||
import { reportWatchProgress } from '../../../shared/lib/watchProgress';
|
||||
import { dramaApi } from '../../../entities/drama/api';
|
||||
|
||||
export function ShortSlide({ video, active, followed, onToggleFollow, onOpenComments, onShare }) {
|
||||
export function ShortSlide({ video, active, followed, onToggleFollow, onOpenComments, onShare, onEnded }) {
|
||||
const { locale } = useLocale();
|
||||
const src = video.preview?.playUrl;
|
||||
const previewId = video.preview?.id ? String(video.preview.id) : '';
|
||||
const fallbackSrc = video.preview?.playUrl || '';
|
||||
const playback = useRequest(
|
||||
({ signal }) => dramaApi.playback(previewId, { signal }),
|
||||
[active, previewId],
|
||||
{ enabled: active && Boolean(previewId) },
|
||||
);
|
||||
const src = active
|
||||
? playback.data
|
||||
? selectPlaybackUrl(playback.data, fallbackSrc)
|
||||
: playback.error
|
||||
? fallbackSrc
|
||||
: ''
|
||||
: '';
|
||||
const likeTargetId = video.preview?.id || video.id;
|
||||
const { liked, likeCount, saved, saveCount, toggleLike, toggleSave } = useEngagement({
|
||||
likeTargetType: video.preview?.id ? 'EPISODE' : 'DRAMA',
|
||||
@@ -33,8 +49,8 @@ export function ShortSlide({ video, active, followed, onToggleFollow, onOpenComm
|
||||
<div className="short-player-wrap">
|
||||
<div className="short-slide-player">
|
||||
{src
|
||||
? <VideoPlayer src={src} poster={video.image} autoPlay={active} soundHint={locale.soundHint} onProgress={(position, duration, flush) => reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
|
||||
: <div className="short-slide-placeholder"><img src={video.image} alt={video.title} /><span>{video.title}</span></div>}
|
||||
? <VideoPlayer src={src} poster={video.image} autoPlay={active} soundHint={locale.soundHint} onEnded={onEnded} onProgress={(position, duration, flush) => reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
|
||||
: <div className="short-slide-placeholder"><img src={video.image} alt={video.title} /><span>{active && playback.loading ? locale.loading : active && playback.error ? locale.playbackUnavailable : video.title}</span></div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,15 @@ export const resolveMedia = (value) => {
|
||||
return /^https?:\/\//.test(value) ? value : `${MEDIA_BASE_URL}${value}`;
|
||||
};
|
||||
|
||||
export const resolvePlaybackUrl = resolveMedia;
|
||||
export function resolvePlaybackUrl(value) {
|
||||
if (!value) return '';
|
||||
return /^https?:\/\//.test(value) ? value : `${MEDIA_BASE_URL}${value}`;
|
||||
}
|
||||
|
||||
export const getImage = (item = {}) => resolveMedia(item.coverLandscape || item.coverUrl || item.cover || item.posterUrl || item.imageUrl);
|
||||
|
||||
export function selectPlaybackUrl(playback, fallback = '') {
|
||||
const tracks = playback?.tracks || [];
|
||||
const preferred = tracks.find((track) => track.quality === '360p') || tracks.find((track) => track.quality === 'AUTO') || tracks[0];
|
||||
return resolveMedia(preferred?.playUrl || playback?.playUrl || fallback);
|
||||
return resolvePlaybackUrl(preferred?.playUrl || playback?.playUrl || fallback);
|
||||
}
|
||||
Reference in new issue
Block a user