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

@@ -499,17 +501,20 @@
- X • Follow + X Twitter • Follow - Telegram • Channel + Telegram 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 Ko-fi • Support - GitHub • Source + GitHub 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 @@

🔮 播放性能

@@ -424,8 +424,10 @@

🧩 使用技巧

    -
  • 配置建议:每个媒体库 < 1000 个视频
  • -
  • PWA应用:📲 添加到主屏幕/作为应用安装
  • +
  • 入库更快:媒体库类型选「家庭视频」
  • +
  • 加载更顺:每个媒体库 < 1000 个视频
  • +
  • 管理更爽:使用多个媒体库和播放列表
  • +
  • PWA 应用:添加到主屏幕/作为应用安装 📲
@@ -506,16 +508,20 @@
- B 站·教程 + Bilibili B 站·教程 - 小红书·攻略 + Xiaohongshu 小红书·攻略 - QQ 群·催更 + QQ QQ 群·催更 - GitHub · 开源 + GitHub GitHub · 开源

☕ 打赏鼓励,支持我开发更多有趣应用~

@@ -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) {