Files
hamlab/internal/media/encode_test.go
T
Mitchell-JohnsonandClaude Opus 5.5 fc4126a0ed files: Ham Share's player — Auto quality, any by hand, or the file; 360 view; Transcode device setting
Every video plays YouTube's way: hls.js reads the connection and picks the
quality, or one is picked by hand, or Original plays the file as it is. The
qualities (a ladder by size, H.264) are made on the fly in 3 s pieces from
wherever the viewer seeks, one ffmpeg per play, paused when far enough ahead.
360 videos open in a look-around view (drag, pinch or scroll to zoom from
8 to 300 degrees, phone motion).

Settings -> Transcode device picks the machine and device that makes them,
for every machine's files: a machine transcoding another's file reads it
through the hub by a token for that one file. Replaces the codec/size picker
and the single-quality re-encoder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:52:35 -07:00

138 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package media
import (
"strings"
"testing"
)
func src(codec string, w, h int, fps float64) Info {
return Info{Duration: 100, Video: &Track{Codec: codec, Width: w, Height: h, FPS: fps}, Audio: &Track{Codec: "opus", Channels: 2}}
}
func TestLadder(t *testing.T) {
vr := src("hevc", 7680, 3840, 50)
vr.Video.Spherical = true
r := Ladder(vr)
if len(r) != 4 || r[0].Name != "3840" || r[0].Height != 1920 || r[0].level != "5.2" || r[3].FPS != 25 || r[0].Label != "2160s" {
t.Fatalf("8K 360: %+v", r)
}
if r[0].codecs != "avc1.640034,mp4a.40.2" {
t.Fatalf("codecs %q", r[0].codecs)
}
r = Ladder(src("h264", 2292, 960, 60)) // a capture: its own size on top, then the steps under it
if len(r) != 3 || r[0].Label != "960p" || r[0].Width != 2292 || r[0].FPS != 60 || r[0].Bitrate != 5_000_000 || r[1].Label != "720p" || r[1].Width != 1720 || r[1].FPS != 30 {
t.Fatalf("2292×960: %+v", r)
}
r = Ladder(src("h264", 1920, 1080, 59.94))
if len(r) != 3 || r[0].Label != "1080p" || r[0].FPS != 59.94 || r[1].FPS != 29.97 || r[2].Width != 854 {
t.Fatalf("1080p: %+v", r)
}
if r = Ladder(src("h264", 1080, 1920, 30)); r[0].Width != 1080 || r[0].Height != 1920 { // standing up
t.Fatalf("portrait: %+v", r)
}
if r = Ladder(src("h264", 320, 240, 30)); len(r) != 1 || r[0].Width != 320 || r[0].Label != "240p" {
t.Fatalf("tiny: %+v", r)
}
silent := src("h264", 1280, 720, 30)
silent.Audio = nil
if r = Ladder(silent); strings.Contains(r[0].codecs, "mp4a") {
t.Fatalf("no sound, no AAC: %q", r[0].codecs)
}
if Ladder(Info{Duration: 10}) != nil {
t.Fatal("no picture, no ladder")
}
if fpsArg(29.97) != "30000/1001" || fpsArg(25) != "25" {
t.Fatal(fpsArg(29.97), fpsArg(25))
}
}
func gridCaps() Caps {
return Caps{FFmpeg: "ffmpeg", FFprobe: "ffprobe", Encoders: []string{"aac", "libsvtav1", "libx264", "libx265"},
GPUs: []GPU{
{API: "vaapi", Device: "/dev/dri/renderD128", Name: "Intel UHD Graphics 750", Integrated: true, Codecs: []string{"h264", "hevc", "vp9"}, Encodes: []string{"h264", "hevc"}},
{API: "vaapi", Device: "/dev/dri/renderD129", Name: "Intel Arc A380", Codecs: []string{"h264", "hevc", "vp9"}, Encodes: []string{"av1", "h264", "hevc"}},
}}
}
func engineNames(r []Recipe) []string {
out := make([]string, len(r))
for i := range r {
out[i] = r[i].Engine
}
return out
}
func TestRecipes(t *testing.T) {
c := gridCaps()
in := src("hevc", 3840, 2160, 60)
r1080 := Ladder(in)[2]
// any device: the card first, decoding on it; then the iGPU the same way; then the CPU
got := strings.Join(engineNames(recipes(c, "", in, r1080)), "|")
if got != "Intel Arc A380|Intel Arc A380, decoded on the CPU|Intel UHD Graphics 750|Intel UHD Graphics 750, decoded on the CPU|CPU (libx264)" {
t.Fatalf("any: %s", got)
}
// the iGPU asked for: only it, then the CPU as the fallback
if got = strings.Join(engineNames(recipes(c, "/dev/dri/renderD128", in, r1080)), "|"); got != "Intel UHD Graphics 750|Intel UHD Graphics 750, decoded on the CPU|CPU (libx264)" {
t.Fatalf("iGPU: %s", got)
}
if got = strings.Join(engineNames(recipes(c, CPU, in, r1080)), "|"); got != "CPU (libx264)" {
t.Fatalf("cpu: %s", got)
}
// an AV1 source: neither card decodes it, so no same-card recipe
if got = strings.Join(engineNames(recipes(c, "/dev/dri/renderD129", src("av1", 1920, 1080, 30), r1080)), "|"); got != "Intel Arc A380, decoded on the CPU|CPU (libx264)" {
t.Fatalf("av1: %s", got)
}
// a machine with no GPU and no x264: nothing
bare := Caps{FFmpeg: "ffmpeg", FFprobe: "ffprobe", Encoders: []string{"aac"}}
if len(recipes(bare, "", in, r1080)) != 0 {
t.Fatal("no x264 means nothing")
}
// 30 fps from 60 for the lower rungs; HDR tone-mapped on both paths
r720 := Ladder(in)[3]
hdr := src("hevc", 3840, 2160, 60)
hdr.Video.ColorTransfer = "smpte2084"
rs := recipes(c, "", hdr, r720)
if !strings.HasPrefix(rs[0].VF, "fps=30,tonemap_vaapi") || !strings.Contains(rs[len(rs)-1].VF, "tonemap=hable") {
t.Fatalf("hdr: %q … %q", rs[0].VF, rs[len(rs)-1].VF)
}
if rs := recipes(c, "", in, r1080); strings.Contains(rs[0].VF, "fps=") {
t.Fatalf("same rate needs no fps filter: %q", rs[0].VF)
}
}
func TestDevices(t *testing.T) {
c := gridCaps()
c.Unusable = []GPU{{Device: "/dev/dri/renderD130", Name: "Something", Reason: "permission denied"}}
d := Devices(c)
if len(d) != 4 || !d[0].OK || !d[1].OK || d[2].OK || d[2].Reason == "" || d[3].ID != CPU || !d[3].OK {
t.Fatalf("%+v", d)
}
if d := Devices(Caps{Error: "ffmpeg is not installed"}); len(d) != 1 || d[0].OK {
t.Fatalf("no ffmpeg: %+v", d)
}
}
func TestPieceArgs(t *testing.T) {
in := src("h264", 1920, 1080, 60)
r := Ladder(in)[0]
rc := recipes(gridCaps(), "/dev/dri/renderD129", in, r)[0]
a := strings.Join(pieceArgs("/d", "/v.mkv", in, rc, r, 12), " ")
for _, want := range []string{"-hwaccel vaapi -hwaccel_device /dev/dri/renderD129", "-ss 36.000 -i /v.mkv", "-map 0:v:0 -map 0:a:0?",
"-vf scale_vaapi=w=1920:h=1080:format=nv12", "-c:v h264_vaapi -b:v 5000k -maxrate 7500k", "-level 4.2", "-c:a aac -b:a 160k -ac 2",
"-force_key_frames expr:gte(t,n_forced*3)", "-output_ts_offset 46.000", "-hls_time 2.50", "-start_number 12", "-hls_segment_filename /d/%d.ts"} {
if !strings.Contains(a, want) {
t.Errorf("missing %q in\n%s", want, a)
}
}
if strings.Contains(a, "-reconnect") {
t.Error("a path needs no reconnect")
}
if a := strings.Join(pieceArgs("/d", "http://10.0.0.234:8100/api/play-src/x", in, rc, r, 0), " "); !strings.Contains(a, "-reconnect 1") {
t.Error("a URL reconnects")
}
in.Audio = nil
if a := strings.Join(pieceArgs("/d", "/v", in, rc, r, 0), " "); !strings.Contains(a, "-an") {
t.Error("no sound means -an")
}
}