diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index eb792ae3b..f4bfb5929 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -91,6 +91,7 @@ type Thumbnail struct { FileTypeImage FileType = 1 // FileTypeImage is for jpegs, pngs, and standard gifs FileTypeAudio FileType = 2 // FileTypeAudio is for audio-only files (no video) FileTypeVideo FileType = 3 // FileTypeVideo is for files with audio + visual + FileTypeGifv FileType = 4 // FileTypeGifv is for short video-only files (20s or less, mp4, no audio). ) // String returns a stringified, frontend API compatible form of FileType. @@ -104,6 +105,8 @@ func (t FileType) String() string { return "audio" case FileTypeVideo: return "video" + case FileTypeGifv: + return "gifv" default: panic("invalid filetype") } diff --git a/internal/media/ffmpeg.go b/internal/media/ffmpeg.go index 72ee1bc33..eb6dd9263 100644 --- a/internal/media/ffmpeg.go +++ b/internal/media/ffmpeg.go @@ -305,7 +305,15 @@ func (res *result) GetFileType() (gtsmodel.FileType, string) { case "mov,mp4,m4a,3gp,3g2,mj2": switch { case len(res.video) > 0: - return gtsmodel.FileTypeVideo, "mp4" + if len(res.audio) == 0 && + res.duration <= 30 { + // Short, soundless + // video file aka gifv. + return gtsmodel.FileTypeGifv, "mp4" + } else { + // Video file (with or without audio). + return gtsmodel.FileTypeVideo, "mp4" + } case len(res.audio) > 0 && res.audio[0].codec == "aac": // m4a only supports [aac] audio. diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go index 504cda11e..1d286bda7 100644 --- a/internal/media/processingmedia.go +++ b/internal/media/processingmedia.go @@ -202,7 +202,8 @@ func (p *ProcessingMedia) store(ctx context.Context) error { switch p.media.Type { case gtsmodel.FileTypeImage, - gtsmodel.FileTypeVideo: + gtsmodel.FileTypeVideo, + gtsmodel.FileTypeGifv: // Attempt to clean as metadata from file as possible. if err := clearMetadata(ctx, temppath); err != nil { return gtserror.Newf("error cleaning metadata: %w", err) diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js index 74cb28460..b88c64680 100644 --- a/web/source/frontend/index.js +++ b/web/source/frontend/index.js @@ -26,6 +26,8 @@ const Prism = require("./prism.js"); Prism.manual = true; Prism.highlightAll(); +const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + let [_, _user, type, id] = window.location.pathname.split("/"); if (type == "statuses") { let firstStatus = document.getElementsByClassName("thread")[0].children[0]; @@ -49,9 +51,12 @@ new PhotoswipeCaptionPlugin(lightbox, { lightbox.addFilter('itemData', (item) => { const el = item.element; - if (el && el.classList.contains("plyr-video")) { + if ( + el && + el.classList.contains("plyr-video") && + el._plyrContainer !== undefined + ) { const parentNode = el._plyrContainer.parentNode; - return { alt: el.getAttribute("alt"), _video: { @@ -118,13 +123,14 @@ dynamicSpoiler("text-spoiler", (spoiler) => { dynamicSpoiler("media-spoiler", (spoiler) => { const eye = spoiler.querySelector(".eye.button"); const video = spoiler.querySelector(".plyr-video"); + const loopingAuto = !reduceMotion.matches && video != null && video.classList.contains("gifv"); return () => { if (spoiler.open) { eye.setAttribute("aria-label", "Hide media"); } else { eye.setAttribute("aria-label", "Show media"); - if (video) { + if (video && !loopingAuto) { video.pause(); } } @@ -132,6 +138,22 @@ dynamicSpoiler("media-spoiler", (spoiler) => { }); Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => { + const loopingAuto = !reduceMotion.matches && video.classList.contains("gifv"); + + if (loopingAuto) { + // If we're able to play this as a + // looping gifv, then do so, else fall + // back to user-controllable video player. + video.draggable = false; + video.autoplay = true; + video.loop = true; + video.classList.remove("photoswipe-slide"); + video.classList.remove("plry-video"); + video.load(); + video.play(); + return; + } + let player = new Plyr(video, { title: video.title, settings: ["loop"], diff --git a/web/template/status_attachments.tmpl b/web/template/status_attachments.tmpl index 8e05aafd8..ee564d934 100644 --- a/web/template/status_attachments.tmpl +++ b/web/template/status_attachments.tmpl @@ -99,7 +99,7 @@ media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ - {{- if eq .Type "video" }} + {{- if or (eq .Type "video") (eq .Type "gifv") }} {{- include "videoPreview" $media | indent 4 }} {{- else if eq .Type "image" }} {{- include "imagePreview" $media | indent 4 }} @@ -107,11 +107,17 @@ media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ {{- include "audioPreview" $media | indent 4 }} {{- end }} - {{- if eq .Type "video" }} + {{- if or (eq .Type "video") (eq .Type "gifv") }}