diff --git a/en/index.html b/en/index.html
index a584390..c476608 100644
--- a/en/index.html
+++ b/en/index.html
@@ -55,7 +55,7 @@
}
.slide-item {
- @apply absolute top-0 left-0 w-full h-full z-10;
+ @apply absolute top-0 left-0 w-full h-full z-10 overflow-hidden;
}
.play-pause-btn {
@@ -411,8 +411,10 @@
🧩 Tips & Tricks
- - Best Practice: < 1,000 items per library
- - PWA: 📲 Add to Home Screen or Install as App
+ - Faster Importing: Use "Home Video" library type.
+ - Smoother Loading: Limit libraries to < 1,000 videos.
+ - Easier Managing: Use multiple libraries and playlists.
+ - PWA Support: Add to Home Screen or Install as App. 📲
@@ -499,17 +501,20 @@
- X • Follow
+

Twitter • Follow
- Telegram • Channel
+

Telegram • Channel
- Ko-fi • Support
+ class="flex items-center justify-center text-[#FF6433] bg-[#FF5E5B]/10 hover:bg-[#FF5E5B]/20 px-3 py-1.5 rounded-full transition-colors border border-[#FF5E5B]/20 text-[10px] whitespace-nowrap">
+

Ko-fi • Support
- GitHub • Source
+

GitHub • Source
@@ -562,6 +567,8 @@
currentLibraryId: null,
isScaleFill: true,
isMuted: false,
+ lastReportTime: 0,
+ playSessionId: null,
originalVideos: []
};
@@ -1183,6 +1190,7 @@
loadVideo(index) {
if (this.state.viewMode !== 'stream') return;
+ this.state.playSessionId = 'ex_' + Date.now().toString(16);
this.renderBuffer();
const videoData = this.state.videos[this.state.currentIndex];
@@ -1214,6 +1222,7 @@
clearTimeout(this._waitingTimer);
this.toggleLoading(false);
videoEl.style.opacity = '1';
+ this.reportPlayback('playing', videoEl);
};
videoEl.oncanplay = () => {
@@ -1234,10 +1243,13 @@
this.dom.progressLine.style.width = `${pct}%`;
this.dom.currentTime.textContent = this.formatTime(videoEl.currentTime);
this.dom.totalTime.textContent = this.formatTime(videoEl.duration);
+ this.reportPlayback('progress', videoEl);
}
};
+ videoEl.onpause = () => this.reportPlayback('progress', videoEl);
videoEl.onended = () => {
+ this.reportPlayback('stopped', videoEl);
if (this.state.viewMode === 'stream' && this.state.isAutoplay) {
this.nextVideo();
}
@@ -1376,7 +1388,10 @@
switchSlide(newIndex, direction) {
const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
const curV = document.getElementById(`video-p${slideIdx}`);
- if (curV) curV.pause();
+ if (curV) {
+ curV.pause();
+ this.reportPlayback('stopped', curV);
+ }
if (this.dom.slides) {
const destSlideIdx = (newIndex % 3 + 3) % 3;
@@ -1399,6 +1414,71 @@
});
}
+ reportPlayback(event, videoEl) {
+ if (!this.config.server || !this.config.token || this.state.videos.length === 0) return;
+
+ const now = Date.now();
+ if (event === 'progress' && now - (this.state.lastReportTime || 0) < 5000) return;
+ if (event === 'progress') this.state.lastReportTime = now;
+
+ const videoData = this.state.videos[this.state.currentIndex];
+ if (!videoData || !videoEl) return;
+
+ // Emby Fix: 1. Map standard names 2. Add MediaSourceId 3. Report Capabilities
+ const eventMap = { 'playing': 'TimeUpdate', 'progress': 'TimeUpdate', 'stopped': 'Stopped' };
+ const eventName = videoEl.paused ? 'Pause' : (eventMap[event] || 'TimeUpdate');
+ const mediaSourceId = videoData.MediaSources?.[0]?.Id || '';
+
+ if (event === 'playing') {
+ this.reportCapabilities();
+ }
+
+ const ticks = Math.floor(videoEl.currentTime * 10000000);
+
+ let endpoint = '/Sessions/Playing';
+ if (event === 'progress') endpoint = '/Sessions/Playing/Progress';
+ if (event === 'stopped') endpoint = '/Sessions/Playing/Stopped';
+
+ const payload = {
+ ItemId: videoData.Id,
+ PositionTicks: ticks,
+ IsPaused: videoEl.paused || event === 'stopped',
+ IsMuted: videoEl.muted,
+ VolumeLevel: Math.floor(videoEl.volume * 100),
+ PlayMethod: this.config.useStatic ? 'DirectPlay' : 'Transcode',
+ EventName: eventName,
+ CanSeek: true,
+ PlaySessionId: this.state.playSessionId,
+ QueueableMediaTypes: ["Video"]
+ };
+ if (mediaSourceId) payload.MediaSourceId = mediaSourceId;
+
+ fetch(`${this.config.server}/emby${endpoint}?api_key=${this.config.token}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="EmbyX-Device", Version="1.1"`
+ },
+ body: JSON.stringify(payload)
+ }).catch(() => { });
+ }
+
+ reportCapabilities() {
+ const { server, token } = this.config;
+ if (!server || !token) return;
+ fetch(`${server}/emby/Sessions/Capabilities/Full?api_key=${token}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="EmbyX-Device", Version="1.1"`
+ },
+ body: JSON.stringify({
+ PlayableMediaTypes: ["Video"],
+ SupportsMediaControl: true,
+ SupportsPersistentConnections: false
+ })
+ }).catch(() => { });
+ }
getVideoSrc(id, mediaItem) {
const { server, token, useStatic } = this.config;
diff --git a/zh/index.html b/zh/index.html
index 7a28724..f95ec8e 100644
--- a/zh/index.html
+++ b/zh/index.html
@@ -59,7 +59,7 @@
/* 单个视频卡片 */
.slide-item {
- @apply absolute top-0 left-0 w-full h-full z-10;
+ @apply absolute top-0 left-0 w-full h-full z-10 overflow-hidden;
}
/* 播放/暂停按钮动画 - 修复:提高 z-index 确保在视频之上 */
@@ -299,7 +299,7 @@
-
@@ -385,7 +385,7 @@
🔮 播放性能
- 原生 HTML5 播放器,新设备体验最佳
- - 老设备需服务器转码,
不如放到**上回收(没打钱😂
+ - 老设备需服务器转码,
不如放到**上回收(没打钱 😂
@@ -424,8 +424,10 @@
🧩 使用技巧
- - 配置建议:每个媒体库 < 1000 个视频
- - PWA应用:📲 添加到主屏幕/作为应用安装
+ - 入库更快:媒体库类型选「家庭视频」
+ - 加载更顺:每个媒体库 < 1000 个视频
+ - 管理更爽:使用多个媒体库和播放列表
+ - PWA 应用:添加到主屏幕/作为应用安装 📲
@@ -506,16 +508,20 @@
☕ 打赏鼓励,支持我开发更多有趣应用~
@@ -589,6 +595,8 @@
currentLibraryId: null,
isScaleFill: true,
isMuted: false,
+ lastReportTime: 0,
+ playSessionId: null,
originalVideos: [] // 随机模式下保存原始顺序列表
};
@@ -1274,6 +1282,7 @@
loadVideo(index) {
if (this.state.viewMode !== 'stream') return;
+ this.state.playSessionId = 'ex_' + Date.now().toString(16); // 每次加载新视频生成唯一会话 ID
this.renderBuffer(); // 确保 DOM 就位
const videoData = this.state.videos[this.state.currentIndex];
@@ -1309,6 +1318,7 @@
this.toggleLoading(false);
// 确保播放时图层可见,应对部分系统/浏览器事件次序差异
videoEl.style.opacity = '1';
+ this.reportPlayback('playing', videoEl);
};
// 《修复点①》 canplay 时视频淡入覆盖封面
@@ -1331,10 +1341,13 @@
this.dom.progressLine.style.width = `${pct}%`;
this.dom.currentTime.textContent = this.formatTime(videoEl.currentTime);
this.dom.totalTime.textContent = this.formatTime(videoEl.duration);
+ this.reportPlayback('progress', videoEl);
}
};
+ videoEl.onpause = () => this.reportPlayback('progress', videoEl);
videoEl.onended = () => {
+ this.reportPlayback('stopped', videoEl);
if (this.state.viewMode === 'stream' && this.state.isAutoplay) {
this.nextVideo();
}
@@ -1481,7 +1494,10 @@
switchSlide(newIndex, direction) {
const slideIdx = (this.state.currentIndex % 3 + 3) % 3;
const curV = document.getElementById(`video-p${slideIdx}`);
- if (curV) curV.pause();
+ if (curV) {
+ curV.pause();
+ this.reportPlayback('stopped', curV);
+ }
// 【动画修复】先同步预就位目标 slide,再同步触发 container 位移动画
// 这样浏览器能在同一帧内确认 slide 位置,动画由 GPU 合成层完成,不会闪现
@@ -1509,6 +1525,74 @@
});
}
+ reportPlayback(event, videoEl) {
+ if (!this.config.server || !this.config.token || this.state.videos.length === 0) return;
+
+ const now = Date.now();
+ // 进度汇报增加 5s 节流,避免频繁请求,playing 和 stopped 立即执行
+ if (event === 'progress' && now - (this.state.lastReportTime || 0) < 5000) return;
+ if (event === 'progress') this.state.lastReportTime = now;
+
+ const videoData = this.state.videos[this.state.currentIndex];
+ if (!videoData || !videoEl) return;
+
+ // Emby 识别修正:1. 映射标准事件名 2. 补全 MediaSourceId 3. 首次播放进行能力上报
+ const eventMap = { 'playing': 'TimeUpdate', 'progress': 'TimeUpdate', 'stopped': 'Stopped' };
+ const eventName = videoEl.paused ? 'Pause' : (eventMap[event] || 'TimeUpdate');
+ const mediaSourceId = videoData.MediaSources?.[0]?.Id || '';
+
+ if (event === 'playing') {
+ this.reportCapabilities();
+ }
+
+ const ticks = Math.floor(videoEl.currentTime * 10000000);
+ const totalTicks = Math.floor(videoEl.duration * 10000000) || 0;
+
+ let endpoint = '/Sessions/Playing';
+ if (event === 'progress') endpoint = '/Sessions/Playing/Progress';
+ if (event === 'stopped') endpoint = '/Sessions/Playing/Stopped';
+
+ const payload = {
+ ItemId: videoData.Id,
+ PositionTicks: ticks,
+ IsPaused: videoEl.paused || event === 'stopped',
+ IsMuted: videoEl.muted,
+ VolumeLevel: Math.floor(videoEl.volume * 100),
+ PlayMethod: this.config.useStatic ? 'DirectPlay' : 'Transcode',
+ EventName: eventName,
+ CanSeek: true,
+ PlaySessionId: this.state.playSessionId,
+ QueueableMediaTypes: ["Video"]
+ };
+ if (mediaSourceId) payload.MediaSourceId = mediaSourceId;
+
+ fetch(`${this.config.server}/emby${endpoint}?api_key=${this.config.token}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="EmbyX-Device", Version="1.1"`
+ },
+ body: JSON.stringify(payload)
+ }).catch(() => { });
+ }
+
+ reportCapabilities() {
+ const { server, token } = this.config;
+ if (!server || !token) return;
+ fetch(`${server}/emby/Sessions/Capabilities/Full?api_key=${token}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Emby-Authorization': `Emby Client="EmbyX", Device="Web", DeviceId="EmbyX-Device", Version="1.1"`
+ },
+ body: JSON.stringify({
+ PlayableMediaTypes: ["Video"],
+ SupportsMediaControl: true,
+ SupportsPersistentConnections: false
+ })
+ }).catch(() => { });
+ }
+
// --- 辅助功能 ---
getVideoSrc(id, mediaItem) {