Files: re-encode a video the browser can't play, on the machine's own GPU, and play that
The viewer tries the file as it is first. When the browser gives up — Safari and an MKV, HEVC without a hardware decoder — or on a tap, a picker: codec (H.264, HEVC, AV1, or the original codec repackaged) and size (original, 1080p, 720p, 480p), each codec labelled with the engine this machine would use. POST /api/files/play runs ffmpeg into an HLS event playlist of fragmented-MP4 segments under <data dir>/transcode/<id>/; Safari plays HLS itself and everything else goes through hls.js, loaded only then. The recipes are tried best first: a card on the bus before the integrated GPU, decoding on the same card before decoding on the CPU and uploading, the software encoders last; a recipe that produces no first segment is dropped for the next. The seekable range grows as ffmpeg gets ahead; status says how far. Streams are reused for the same request, closed when the player goes, swept after ten idle minutes, and the folder is emptied at start. Root mode runs ffmpeg as root through a helper that ends when the daemon lets go of it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
aa4ae57c22
commit
f3c547adfa
@@ -35,6 +35,7 @@ import (
|
||||
"hamlab/internal/files"
|
||||
"hamlab/internal/hardware"
|
||||
"hamlab/internal/incus"
|
||||
"hamlab/internal/media"
|
||||
"hamlab/internal/metrics"
|
||||
"hamlab/internal/network"
|
||||
"hamlab/internal/nodes"
|
||||
@@ -80,6 +81,7 @@ type Server struct {
|
||||
fsLocal files.FS
|
||||
fsSudo files.FS // nil without passwordless sudo
|
||||
thumbs *thumbSvc
|
||||
plays *media.Streamer
|
||||
self string // this binary, for helpers run as tasks
|
||||
nodes *nodes.Manager
|
||||
fingerprint string
|
||||
@@ -103,6 +105,7 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
s.self = self
|
||||
s.space = space.New(self, filepath.Join(cfg.DataDir(), "space"))
|
||||
s.thumbs = newThumbSvc(cfg.DataDir(), self)
|
||||
s.plays = media.NewStreamer(cfg.DataDir(), self, playLimit)
|
||||
s.hostName() // read it once now so the first request doesn't have to
|
||||
if u, err := user.Current(); err == nil && u.Username != "" {
|
||||
s.user = u.Username
|
||||
@@ -310,6 +313,11 @@ func (s *Server) routes() *http.ServeMux {
|
||||
m.HandleFunc("GET /api/files/thumbs", s.auth(s.thumbsStatus))
|
||||
m.HandleFunc("POST /api/files/thumbs/install", s.auth(s.thumbsInstall))
|
||||
m.HandleFunc("POST /api/files/thumbs/gpu-access", s.auth(s.thumbsGPUAccess))
|
||||
m.HandleFunc("GET /api/files/play-caps", s.auth(s.playCapsHandler))
|
||||
m.HandleFunc("POST /api/files/play", s.auth(s.playStart))
|
||||
m.HandleFunc("GET /api/files/play/{id}", s.auth(s.playStatus))
|
||||
m.HandleFunc("DELETE /api/files/play/{id}", s.auth(s.playStop))
|
||||
m.HandleFunc("GET /api/files/play/{id}/{file}", s.auth(s.playFile))
|
||||
m.HandleFunc("GET /api/hardware", s.auth(s.hardwareReport))
|
||||
m.HandleFunc("GET /api/network", s.auth(s.networkOverview))
|
||||
m.HandleFunc("GET /api/vpn", s.auth(s.vpnReport))
|
||||
|
||||
Reference in New Issue
Block a user