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) } }