diff options
| -rw-r--r-- | internal/api/handlers.go | 9 | ||||
| -rw-r--r-- | internal/service/browse.go | 1 | ||||
| -rw-r--r-- | internal/service/service.go | 1 | ||||
| -rw-r--r-- | internal/service/share.go | 1 | ||||
| -rw-r--r-- | web/js/player.js | 41 |
5 files changed, 35 insertions, 18 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 5a289a1..411607d 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -127,7 +127,7 @@ func (s *Server) serveFileResult(w http.ResponseWriter, r *http.Request, res *se } if !attachment && probe.LooksLikeMPEGTS(res.Path) { - s.serveRemuxedMP4(w, r, res, stat.Size()) + s.serveRemuxedMP4(w, r, res) return } @@ -143,10 +143,13 @@ func (s *Server) serveFileResult(w http.ResponseWriter, r *http.Request, res *se http.ServeContent(w, r, res.FileName, stat.ModTime(), f) } -func (s *Server) serveRemuxedMP4(w http.ResponseWriter, r *http.Request, res *service.FileResult, size int64) { +func (s *Server) serveRemuxedMP4(w http.ResponseWriter, r *http.Request, res *service.FileResult) { w.Header().Set("Content-Type", "video/mp4") w.Header().Set("Cache-Control", "no-store") - s.logger.Info("api remux stream file", "file", res.FileName, "size", size, "range", r.Header.Get("Range")) + if res.Duration > 0 { + w.Header().Set("X-Duration", fmt.Sprintf("%f", res.Duration)) + } + s.logger.Info("api remux stream file", "file", res.FileName, "size", res.FileSize, "range", r.Header.Get("Range")) if err := s.remuxer.Remux(r.Context(), res.Path, w); err != nil { s.logger.Error("remux media", "file", res.FileName, "err", err) } diff --git a/internal/service/browse.go b/internal/service/browse.go index d1f32cc..b420d70 100644 --- a/internal/service/browse.go +++ b/internal/service/browse.go @@ -166,6 +166,7 @@ func (s *browseService) StreamMedia(ctx context.Context, mediaID, userID int64) Path: media.AbsPath, FileName: media.FileName, FileSize: media.FileSizeBytes, + Duration: media.Duration, }, nil } diff --git a/internal/service/service.go b/internal/service/service.go index 818ae3f..6a5f6e6 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -233,6 +233,7 @@ type FileResult struct { Path string FileName string FileSize int64 + Duration float64 // DB-stored duration (seconds), used for remuxed streams. } // MediaDetail combines media with related data. diff --git a/internal/service/share.go b/internal/service/share.go index c87d8f6..2b917f7 100644 --- a/internal/service/share.go +++ b/internal/service/share.go @@ -129,6 +129,7 @@ func (s *shareService) StreamSharedMedia(ctx context.Context, token string) (*Fi Path: media.AbsPath, FileName: media.FileName, FileSize: media.FileSizeBytes, + Duration: media.Duration, }, nil } diff --git a/web/js/player.js b/web/js/player.js index fea4a19..e179856 100644 --- a/web/js/player.js +++ b/web/js/player.js @@ -23,6 +23,20 @@ let slideshowTimer = null; let slideshowPausedUntil = 0; let cropMode = false; +function currentMediaElement() { + const e = els(); + if (currentMedia?.type === 'audio') return e.audio; + if (currentMedia?.type === 'video') return e.video; + return null; +} + +function effectiveDuration() { + const m = currentMediaElement(); + if (!m) return 0; + if (isFinite(m.duration) && m.duration > 0) return m.duration; + return currentMedia?.duration || 0; +} + const els = () => ({ video: document.getElementById('media-video'), audio: document.getElementById('media-audio'), @@ -36,7 +50,7 @@ const els = () => ({ btnSlideshow: document.getElementById('btn-slideshow'), btnMute: document.getElementById('btn-mute'), btnFs: document.getElementById('btn-fullscreen'), - btnMinimize: document.getElementById('btn-minimize'), + btnMinimize: document.getElementById('btn-minimize-player'), btnRestore: document.getElementById('btn-restore-player'), restoreTitle: document.getElementById('player-restore-title'), bigPlay: document.getElementById('big-play'), @@ -110,15 +124,17 @@ export function initPlayer(options = {}) { [e.video, e.audio].forEach((m) => { m.addEventListener('timeupdate', () => { - if (!m.duration || isNaN(m.duration)) return; - const pct = (m.currentTime / m.duration) * 100; + const dur = effectiveDuration(); + if (!dur) return; + const pct = (m.currentTime / dur) * 100; e.fill.style.width = pct.toFixed(2) + '%'; e.thumb.style.left = pct.toFixed(2) + '%'; e.timeElapsed.textContent = fmt(m.currentTime); updateBufferedRanges(m); }); m.addEventListener('loadedmetadata', () => { - e.timeTotal.textContent = fmt(isFinite(m.duration) ? m.duration : 0); + const dur = effectiveDuration(); + e.timeTotal.textContent = fmt(dur); updateBufferedRanges(m); }); ['progress', 'durationchange', 'loadeddata', 'canplay', 'seeked'].forEach((event) => { @@ -166,8 +182,9 @@ export function initPlayer(options = {}) { let seeking = false; const seekToFraction = (frac) => { const m = currentMediaElement(); - if (!m || !m.duration) return; - m.currentTime = Math.max(0, Math.min(1, frac)) * m.duration; + const dur = effectiveDuration(); + if (!m || !dur) return; + m.currentTime = Math.max(0, Math.min(1, frac)) * dur; }; const handlePointer = (ev) => { const r = e.track.getBoundingClientRect(); @@ -188,9 +205,10 @@ export function initPlayer(options = {}) { e.track?.addEventListener('touchend', () => { seeking = false; }); e.track?.addEventListener('keydown', (ev) => { const m = currentMediaElement(); - if (!m || !m.duration) return; + const dur = effectiveDuration(); + if (!m || !dur) return; if (ev.key === 'ArrowLeft') { ev.preventDefault(); m.currentTime = Math.max(0, m.currentTime - 5); } - if (ev.key === 'ArrowRight') { ev.preventDefault(); m.currentTime = Math.min(m.duration, m.currentTime + 5); } + if (ev.key === 'ArrowRight') { ev.preventDefault(); m.currentTime = Math.min(dur, m.currentTime + 5); } }); } @@ -454,13 +472,6 @@ function loadMedia(media, resumeFrom = 0) { updateFloatingSize(); } -function currentMediaElement() { - const e = els(); - if (currentMedia?.type === 'audio') return e.audio; - if (currentMedia?.type === 'video') return e.video; - return null; -} - function seekWhenMetadataReady(m, seconds) { const target = Number(seconds || 0); if (!m || !isFinite(target) || target <= 0) return; |
