feat(video): add video upload with remux/transcode playback
Add video as a media kind in photos: streaming upload with per-kind limits, ffprobe metadata, poster frame reusing the image variant pipeline, and a playback object (remux when web-compatible, else h264/yuv420p/aac transcode). Video jobs run on a separate pool with a long lease. Frontend gains a video viewer, grid badge/duration and video upload.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
"galo/internal/sse"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/store"
|
||||
"galo/internal/video"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@@ -42,6 +43,8 @@ type Server struct {
|
||||
photos *photos.Service
|
||||
worker *photos.Worker
|
||||
|
||||
videoTools *video.Toolchain
|
||||
|
||||
social *social.Service
|
||||
|
||||
notifications *notifications.Service
|
||||
@@ -62,6 +65,12 @@ func New(cfg *config.Config, repos store.Repos, ping func(ctx context.Context) e
|
||||
photosSvc := photos.NewService(repos.Photos, albumsSvc, st, hub)
|
||||
photosWorker := photos.NewWorker(photosSvc, cfg.Worker.Interval, cfg.Worker.Batch, cfg.Worker.PendingTTL, cfg.Worker.Concurrency)
|
||||
|
||||
videoTools := video.Detect(cfg.Video)
|
||||
photosSvc.SetUploadLimits(cfg.Upload.ImageMaxBytes, cfg.Upload.VideoMaxBytes)
|
||||
photosSvc.SetVideoEnabled(videoTools.Available())
|
||||
photosSvc.SetTempDir(cfg.Video.TempDir)
|
||||
photosWorker.SetVideo(videoTools, cfg.Video.TranscodeConcurrency)
|
||||
|
||||
socialSvc := social.NewService(repos.Social, albumsSvc, repos.Photos, hub, notifSvc)
|
||||
|
||||
shareSvc := sharelinks.NewService(repos.ShareLinks, albumsSvc)
|
||||
@@ -77,6 +86,7 @@ func New(cfg *config.Config, repos store.Repos, ping func(ctx context.Context) e
|
||||
membersSvc: membersSvc,
|
||||
photos: photosSvc,
|
||||
worker: photosWorker,
|
||||
videoTools: videoTools,
|
||||
social: socialSvc,
|
||||
notifications: notifSvc,
|
||||
share: shareSvc,
|
||||
@@ -188,13 +198,19 @@ func (s *Server) health(c *gin.Context) {
|
||||
if err := s.storage.Ping(ctx); err != nil {
|
||||
storeStatus = "error"
|
||||
}
|
||||
// Video tooling is optional: its absence does not degrade the service, but
|
||||
// it is reported so operators can see why video upload is disabled.
|
||||
videoStatus := "ok"
|
||||
if !s.videoTools.Available() {
|
||||
videoStatus = "unavailable"
|
||||
}
|
||||
status := http.StatusOK
|
||||
overall := "ok"
|
||||
if dbStatus != "ok" || storeStatus != "ok" {
|
||||
status = http.StatusServiceUnavailable
|
||||
overall = "degraded"
|
||||
}
|
||||
c.JSON(status, gin.H{"status": overall, "db": dbStatus, storageLabel: storeStatus})
|
||||
c.JSON(status, gin.H{"status": overall, "db": dbStatus, storageLabel: storeStatus, "video": videoStatus})
|
||||
}
|
||||
|
||||
func (s *Server) serveObject(c *gin.Context) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
"galo/internal/config"
|
||||
"galo/internal/storage"
|
||||
"galo/internal/video"
|
||||
)
|
||||
|
||||
func mustSigner(t *testing.T) *storage.Signer {
|
||||
@@ -58,4 +60,23 @@ func TestHealthStorageLabel(t *testing.T) {
|
||||
if body := call(); body["s3"] != "ok" {
|
||||
t.Fatalf("expected default s3 label, got %v", body)
|
||||
}
|
||||
|
||||
// A missing toolchain reports video unavailable without degrading the service.
|
||||
if body := call(); body["video"] != "unavailable" {
|
||||
t.Fatalf("expected video unavailable, got %v", body)
|
||||
}
|
||||
if body := call(); body["status"] != "ok" {
|
||||
t.Fatalf("video unavailability must not degrade status, got %v", body)
|
||||
}
|
||||
|
||||
// A resolvable toolchain reports video ok. The test binary is a known
|
||||
// executable on every platform.
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("executable: %v", err)
|
||||
}
|
||||
srv.videoTools = video.Detect(config.Video{FFmpegPath: self, FFprobePath: self})
|
||||
if body := call(); body["video"] != "ok" {
|
||||
t.Fatalf("expected video ok, got %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user