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>
130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
package media
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// inspectPiece answers when a piece's first video packet is shown, whether it is a keyframe,
|
|
// how many keyframes the piece has, and when its last packet is.
|
|
func inspectPiece(t *testing.T, path string) (first float64, key bool, keys int, last float64) {
|
|
t.Helper()
|
|
b, err := exec.Command("ffprobe", "-v", "error", "-show_entries", "packet=pts_time,flags,codec_type", "-of", "json", path).Output()
|
|
if err != nil {
|
|
t.Fatalf("ffprobe %s: %v", path, err)
|
|
}
|
|
var p struct {
|
|
Packets []struct {
|
|
PTS string `json:"pts_time"`
|
|
Flags string `json:"flags"`
|
|
Type string `json:"codec_type"`
|
|
} `json:"packets"`
|
|
}
|
|
_ = json.Unmarshal(b, &p)
|
|
first = -1
|
|
for _, k := range p.Packets {
|
|
if k.Type != "video" {
|
|
continue
|
|
}
|
|
x, _ := strconv.ParseFloat(k.PTS, 64)
|
|
if first < 0 {
|
|
first, key = x, strings.HasPrefix(k.Flags, "K")
|
|
}
|
|
first = math.Min(first, x)
|
|
last = math.Max(last, x)
|
|
if strings.HasPrefix(k.Flags, "K") {
|
|
keys++
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// TestPieces makes pieces of a made-up video (or SRC, on DEVICE, to try the real thing on a
|
|
// machine with a GPU) in order and after a seek, at two qualities, and checks they line up:
|
|
// each starts on a keyframe at its own place in the film, with no other keyframe inside it.
|
|
func TestPieces(t *testing.T) {
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
t.Skip("no ffmpeg")
|
|
}
|
|
dir := t.TempDir()
|
|
in := os.Getenv("SRC")
|
|
if in == "" {
|
|
in = filepath.Join(dir, "in.mkv")
|
|
if out, err := exec.Command("ffmpeg", "-v", "error", "-f", "lavfi", "-i", "testsrc2=size=1920x1080:rate=50:duration=40",
|
|
"-f", "lavfi", "-i", "sine=frequency=440:duration=40", "-c:v", "libx264", "-preset", "ultrafast", "-g", "500",
|
|
"-c:a", "libopus", "-shortest", in).CombinedOutput(); err != nil {
|
|
t.Fatalf("making the test video: %v: %s", err, out)
|
|
}
|
|
}
|
|
caps := Probe(context.Background())
|
|
if caps.Error != "" {
|
|
t.Skip(caps.Error)
|
|
}
|
|
info, err := Inspect(context.Background(), caps, in, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := NewPlays(dir, "", 2)
|
|
device := os.Getenv("DEVICE")
|
|
if device == "" {
|
|
device = CPU
|
|
}
|
|
p, err := m.Open(caps, false, in, "in.mkv", info, device)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer m.Close(p.ID)
|
|
if !strings.Contains(string(p.Master()), "RESOLUTION=") {
|
|
t.Fatal(string(p.Master()))
|
|
}
|
|
low := p.Rungs[len(p.Rungs)-1].Name
|
|
var offset float64
|
|
far := min(p.segs-2, 60)
|
|
for i, n := range []int{0, 1, 2, far, far + 1, 3} {
|
|
t0 := time.Now()
|
|
path, err := p.Segment(nil, low, n)
|
|
if err != nil {
|
|
t.Fatalf("piece %d: %v", n, err)
|
|
}
|
|
first, key, keys, last := inspectPiece(t, path)
|
|
if i == 0 {
|
|
offset = first
|
|
}
|
|
at := first - offset
|
|
t.Logf("piece %d: %d ms on %s, starts %.3f, %d keyframe(s), %.2f s long", n, time.Since(t0).Milliseconds(), p.Status().Engine, at, keys, last-first)
|
|
if !key || keys != 1 {
|
|
t.Errorf("piece %d: first packet keyframe %v, %d keyframes", n, key, keys)
|
|
}
|
|
if math.Abs(at-float64(n)*Seg) > 0.05 {
|
|
t.Errorf("piece %d starts at %.3f, want %.3f", n, at, float64(n)*Seg)
|
|
}
|
|
if n < p.segs-1 && math.Abs(last-first-Seg) > 0.1 {
|
|
t.Errorf("piece %d is %.3f s long", n, last-first)
|
|
}
|
|
}
|
|
// another quality, at a piece already made at the first: its own run, lined up too
|
|
if len(p.Rungs) > 1 {
|
|
path, err := p.Segment(nil, p.Rungs[0].Name, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first, key, _, _ := inspectPiece(t, path); !key || math.Abs(first-offset-2*Seg) > 0.05 {
|
|
t.Errorf("%s piece 2 starts %.3f (keyframe %v)", p.Rungs[0].Name, first-offset, key)
|
|
}
|
|
}
|
|
if _, err := p.Segment(nil, low, p.segs); err != ErrNotFound {
|
|
t.Errorf("past the end: %v", err)
|
|
}
|
|
if _, err := p.Segment(nil, "999", 0); err != ErrNotFound {
|
|
t.Errorf("no such quality: %v", err)
|
|
}
|
|
}
|