Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/fix-review-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@sweet-player/core': patch
'@sweet-player/react': patch
'@sweet-player/vue': patch
---

修复外部 review 发现的 6 个问题:

- **core: Safari 原生 HLS fallback**:`import('hls.js')` 失败时不再阻断原生 HLS 检测,先检测 `canPlayType` 再尝试 import
- **core: 插件销毁跳过一半**:`destroy()` 中 `forEach` + `splice` 导致每隔一个插件被跳过,改为复制数组遍历
- **core: 换源后菜单残留**:`load()` 现在清空 engine-managed 画质/音轨菜单,避免旧数据点击无效
- **core: 新增 `setId(id)`**:切换视频 ID 时保存旧进度、解绑旧事件、重新绑定,修复换集写入旧 ID
- **react: ref 始终 null**:`useImperativeHandle` 的 deps 从 `[]` 改为无 deps(每次 render 刷新),修复 ref 不可用
- **react: watch `id` prop**:`id` 变化时调用 `player.setId(id)`
- **vue: 补全缺失 props**:新增 `loop`、`shareUrl`、`onSavePrefs`、`onSaveProgress`
- **vue: watch `id` prop**:`id` 变化时调用 `player.setId(id)`
29 changes: 24 additions & 5 deletions packages/core/src/core/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ export class MediaController {
return;
}
void (async () => {
// 先检测原生 HLS 支持(Safari),这样即使 hls.js 未安装也能播放
if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
try {
const [{ createHlsEngine }, { default: Hls }] = await Promise.all([
import('./engines/hlsEngine'),
import('hls.js'),
]);
if (seq !== this.loadSeq) return;
if (Hls.isSupported()) {
this.engine = createHlsEngine(this.video, src, engineCallbacks, this.hlsConfig);
return;
}
} catch {
// hls.js 未安装或加载失败——但浏览器原生支持 HLS,降级使用
if (seq !== this.loadSeq) return;
log('hls事件', 'hls.js 不可用,降级原生 HLS');
}
// 走原生 HLS
if (seq !== this.loadSeq) return;
this.nativeHlsOnly = true;
this.video.src = src;
return;
}
// 浏览器不支持原生 HLS,必须依赖 hls.js
try {
const [{ createHlsEngine }, { default: Hls }] = await Promise.all([
import('./engines/hlsEngine'),
Expand All @@ -82,11 +106,6 @@ export class MediaController {
this.engine = createHlsEngine(this.video, src, engineCallbacks, this.hlsConfig);
return;
}
if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
this.nativeHlsOnly = true;
this.video.src = src;
return;
}
this.emitter.emit('error', { type: 'hls-unsupported' });
} catch (err) {
if (seq !== this.loadSeq) return;
Expand Down
50 changes: 44 additions & 6 deletions packages/core/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class SweetPlayer {
/** 偏好 / 进度的节流写入队列;未传对应回调时为 null(走 localStorage 或完全不存) */
private prefsQueue: ReturnType<typeof createSaveQueue<PlayerPrefs>> | null = null;
private progressQueue: ReturnType<typeof createSaveQueue<number | null>> | null = null;
/** 当前进度记忆的视频 ID;setId 时切换 */
private progressId: string | null = null;
/** 进度记忆的清理函数;setId / destroy 时调用 */
private progressCleanup: (() => void) | null = null;
private disposers: Array<() => void> = [];
private pluginCleanups: Array<() => void> = [];
private destroyed = false;
Expand Down Expand Up @@ -277,7 +281,10 @@ export class SweetPlayer {
this.controls.updateRate(this.video.playbackRate);

if (persist || remotePrefs) this.bindPersistence();
if (options.id) this.bindProgressMemory(options.id);
if (options.id) {
this.progressId = options.id;
this.bindProgressMemory(options.id);
}

options.plugins?.forEach((p) => this.use(p));
}
Expand Down Expand Up @@ -507,9 +514,34 @@ export class SweetPlayer {
load(src: string): void {
this.state.hideEnded();
this.state.hideError();
// 换源后旧引擎的画质/音轨菜单不再适用,清空避免残留
if (this.engineManagedQuality) {
this.engineManagedQuality = false;
this.controls.qualityMenu.setItems([]);
}
if (this.engineManagedAudio) {
this.engineManagedAudio = false;
this.controls.audioMenu.setItems([]);
}
this.media.load(src);
}

/**
* 切换视频 ID(断点续播的 key)。
* 先保存旧视频的进度,再解绑旧的事件/定时器,最后用新 ID 重新绑定。
*/
setId(id: string): void {
if (this.progressId === id) return;
this.saveProgressNow();
this.progressQueue?.flush();
this.progressCleanup?.();
this.progressCleanup = null;
this.progressQueue = null;
this.progressId = id;
this.options.id = id;
this.bindProgressMemory(id);
}

setTitle(title: string): void {
const el = this.controls.topEl.querySelector('.sp-title');
if (el) el.textContent = title;
Expand Down Expand Up @@ -556,7 +588,8 @@ export class SweetPlayer {
this.saveProgressNow();
this.progressQueue?.flush(); // 队列是异步的,销毁前立即写掉
this.emitter.emit('destroy', undefined);
this.pluginCleanups.forEach((d) => d());
// 复制后遍历:dispose 内部会 splice 原数组,直接 forEach 会跳过一半
[...this.pluginCleanups].forEach((d) => d());
// 销毁前保证网页全屏被解除,避免残留 body class 与 escape listener
if (this.webFullscreen) this.exitWebFullscreen();
this.keyboard.destroy();
Expand All @@ -569,10 +602,10 @@ export class SweetPlayer {
this.tapFlash.destroy();
// 清理可能悬挂的 restore listener(load 失败 / metadata 未到时)
this.restoreAbort?.abort();
this.progressCleanup?.();
this.disposers.forEach((d) => d());
if (this.hideTimer) clearTimeout(this.hideTimer);
if (this.clickTimer) clearTimeout(this.clickTimer);
if (this.progressTimer) clearInterval(this.progressTimer);
this.media.destroy();
this.emitter.removeAll();
this.container.remove();
Expand Down Expand Up @@ -671,14 +704,19 @@ export class SweetPlayer {
};
window.addEventListener('pagehide', onPageHide);

this.disposers.push(() => {
this.progressCleanup = () => {
this.video.removeEventListener('seeked', onSeeked);
window.removeEventListener('pagehide', onPageHide);
});
if (this.progressTimer) {
clearInterval(this.progressTimer);
this.progressTimer = null;
}
this.progressQueue = null;
};
}

private saveProgressNow(): void {
if (!this.options.id || !this.video.duration) return;
if (!this.progressId || !this.video.duration) return;
const ended = this.video.currentTime >= this.video.duration - PROGRESS_END_GUARD;
if (!ended && this.video.currentTime <= 3) return; // 刚开头,没必要记
this.progressQueue?.push(ended ? null : this.video.currentTime);
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/SweetPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const SweetPlayer = forwardRef<CorePlayer | null, SweetPlayerProps>(funct
const optionsRef = useRef({ ...options, src, onReady });
optionsRef.current = { ...options, src, onReady };

useImperativeHandle(ref, () => playerRef.current as CorePlayer, []);
// useImperativeHandle 的 factory 在 commit 阶段执行,早于创建 player 的 useEffect。
// 因此直接返回 ref.current——它会在 effect 中被赋值,父组件通过 ref.current 拿到实例。
useImperativeHandle(ref, () => playerRef.current as CorePlayer);

useEffect(() => {
if (!containerRef.current) return;
Expand All @@ -43,5 +45,9 @@ export const SweetPlayer = forwardRef<CorePlayer | null, SweetPlayerProps>(funct
playerRef.current?.setTitle(options.title ?? '');
}, [options.title]);

useEffect(() => {
if (options.id) playerRef.current?.setId(options.id);
}, [options.id]);

return <div ref={containerRef} className={className} style={{ width: '100%', height: '100%', ...style }} />;
});
16 changes: 16 additions & 0 deletions packages/vue/src/SweetPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type ControlName,
type HeatmapPoint,
type LongSeekOptions,
type PlayerPrefs,
type QualityLevel,
type SweetPlayerPlugin,
} from '@sweet-player/core';
Expand All @@ -37,6 +38,8 @@ export const SweetPlayer = defineComponent({
heatmap: Array as PropType<HeatmapPoint[]>,
poster: String,
thumbnails: String,
shareUrl: String,
loop: Boolean,
autoQuality: { type: Boolean, default: true },
persist: { type: Boolean, default: true },
autoNext: [Boolean, Number] as PropType<boolean | number>,
Expand All @@ -46,6 +49,8 @@ export const SweetPlayer = defineComponent({
plugins: Array as PropType<SweetPlayerPlugin[]>,
hlsConfig: Object as PropType<Record<string, unknown>>,
dashConfig: Object as PropType<Record<string, unknown>>,
onSavePrefs: Function as PropType<(prefs: PlayerPrefs) => void | Promise<void>>,
onSaveProgress: Function as PropType<(id: string, seconds: number | null) => void | Promise<void>>,
},
emits: [
'ready',
Expand Down Expand Up @@ -84,6 +89,8 @@ export const SweetPlayer = defineComponent({
heatmap: props.heatmap,
poster: props.poster,
thumbnails: props.thumbnails,
shareUrl: props.shareUrl,
loop: props.loop,
autoQuality: props.autoQuality,
persist: props.persist,
autoNext: props.autoNext,
Expand All @@ -93,6 +100,8 @@ export const SweetPlayer = defineComponent({
plugins: props.plugins,
hlsConfig: props.hlsConfig,
dashConfig: props.dashConfig,
onSavePrefs: props.onSavePrefs,
onSaveProgress: props.onSaveProgress,
onPrev: () => emit('prev'),
onNext: () => emit('next'),
onQualityChange: (q) => emit('quality-change', q),
Expand Down Expand Up @@ -122,6 +131,13 @@ export const SweetPlayer = defineComponent({
(title) => player.value?.setTitle(title ?? ''),
);

watch(
() => props.id,
(id) => {
if (id) player.value?.setId(id);
},
);

watch(
() => props.qualities,
(qualities) => {
Expand Down
Loading