mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
216 lines
6.8 KiB
Go
216 lines
6.8 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"codeberg.org/gruf/go-ffmpreg/wasm"
|
|
"github.com/tetratelabs/wazero"
|
|
)
|
|
|
|
// Generate tiny deterministic media with the exact embedded build under test;
|
|
// no system FFmpeg, network media or third-party fixture licensing is needed.
|
|
func fixture(t *testing.T, audio bool) []byte {
|
|
t.Helper()
|
|
initialize()
|
|
if shared.err != nil {
|
|
t.Fatal(shared.err)
|
|
}
|
|
args := []string{"-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "color=c=black:s=32x32:r=10", "-t", "0.3", "-c:v", "libx264", "-preset", "ultrafast", "-movflags", "frag_keyframe+empty_moov", "-f", "mp4", "pipe:1"}
|
|
if audio {
|
|
args = []string{"-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "anullsrc=r=48000:cl=mono", "-t", "0.3", "-c:a", "aac", "-movflags", "frag_keyframe+empty_moov", "-f", "mp4", "pipe:1"}
|
|
}
|
|
var out, logs bytes.Buffer
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{Name: "ffmpeg", Args: args, Stdout: &out, Stderr: &logs})
|
|
if err != nil || rc != 0 {
|
|
t.Fatalf("fixture: %d %v %s", rc, err, logs.String())
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func TestTailMoovNeedsRange(t *testing.T) {
|
|
initialize()
|
|
if shared.err != nil {
|
|
t.Fatal(shared.err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
dir := t.TempDir()
|
|
var logs bytes.Buffer
|
|
rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{Name: "ffmpeg", Args: []string{
|
|
"-v", "error", "-f", "lavfi", "-i", "color=c=black:s=32x32:r=10", "-t", "0.3", "-c:v", "libx264", "-preset", "ultrafast", "/out/video.mp4",
|
|
}, Stderr: &logs, Config: func(c wazero.ModuleConfig) wazero.ModuleConfig {
|
|
return c.WithFSConfig(wazero.NewFSConfig().WithDirMount(dir, "/out"))
|
|
}})
|
|
if rc != 0 || err != nil {
|
|
t.Fatalf("fixture %d %v %s", rc, err, logs.String())
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, "video.mp4"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var video []byte
|
|
for offset := 0; offset < len(raw); {
|
|
length := int(binary.BigEndian.Uint32(raw[offset : offset+4]))
|
|
if string(raw[offset+4:offset+8]) == "moov" {
|
|
free := make([]byte, 2*httpBlockSize)
|
|
binary.BigEndian.PutUint32(free, uint32(len(free)))
|
|
copy(free[4:], "free")
|
|
video = append(video, free...)
|
|
}
|
|
video = append(video, raw[offset:offset+length]...)
|
|
offset += length
|
|
}
|
|
audio := fixture(t, true)
|
|
for _, ranged := range []bool{true, false} {
|
|
t.Run(fmtBool(ranged), func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
data := video
|
|
if r.URL.Path == "/audio" {
|
|
data = audio
|
|
}
|
|
if ranged {
|
|
http.ServeContent(w, r, "media", time.Time{}, bytes.NewReader(data))
|
|
} else {
|
|
w.Write(data)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
v, err := OpenHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer v.Close()
|
|
a, err := OpenHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL + "/audio"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer a.Close()
|
|
err = Run(ctx, v, a, io.Discard, "mp4", nil)
|
|
if ranged && err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ranged && err == nil {
|
|
t.Fatal("non-seekable tail-moov input unexpectedly succeeded")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func fmtBool(b bool) string {
|
|
if b {
|
|
return "range"
|
|
}
|
|
return "sequential"
|
|
}
|
|
|
|
func TestWebMToMP4(t *testing.T) {
|
|
initialize()
|
|
if shared.err != nil {
|
|
t.Fatal(shared.err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
inputs := make([]*StreamInput, 2)
|
|
for i, name := range []string{"video.webm", "audio.webm"} {
|
|
data, err := os.ReadFile(filepath.Join("..", "testdata", "ffmpeg", name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inputs[i] = NewStreamInput(ctx)
|
|
defer inputs[i].Close()
|
|
if _, err = inputs[i].Write(data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inputs[i].End(nil)
|
|
}
|
|
|
|
var output, probe, logs bytes.Buffer
|
|
if err := Run(ctx, inputs[0], inputs[1], &output, "mp4", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{Name: "ffprobe", Args: []string{"-v", "error", "-show_entries", "stream=codec_name", "-of", "json", "pipe:0"}, Stdin: bytes.NewReader(output.Bytes()), Stdout: &probe, Stderr: &logs})
|
|
if rc != 0 || err != nil {
|
|
t.Fatalf("probe %d %v %s", rc, err, logs.String())
|
|
}
|
|
if !bytes.Contains(probe.Bytes(), []byte(`"vp9"`)) || !bytes.Contains(probe.Bytes(), []byte(`"opus"`)) {
|
|
t.Fatalf("codecs changed: %s", probe.String())
|
|
}
|
|
}
|
|
|
|
func TestWASMMerge(t *testing.T) {
|
|
video, audio := fixture(t, false), fixture(t, true)
|
|
for _, ranged := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "sequential", true: "range"}[ranged], func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
data := video
|
|
if r.URL.Path == "/audio" {
|
|
data = audio
|
|
}
|
|
if ranged {
|
|
http.ServeContent(w, r, "media", time.Time{}, bytes.NewReader(data))
|
|
} else {
|
|
w.Write(data)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
v, err := OpenHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL + "/video"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer v.Close()
|
|
a, err := OpenHTTP(ctx, srv.Client(), HTTPSource{URL: srv.URL + "/audio"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer a.Close()
|
|
var output bytes.Buffer
|
|
if err = Run(ctx, v, a, &output, "", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(output.Bytes(), []byte("moof")) {
|
|
t.Fatal("output is not fragmented MP4")
|
|
}
|
|
// Probe the actual result, rather than just checking its filename/header.
|
|
var probe, logs bytes.Buffer
|
|
rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{Name: "ffprobe", Args: []string{"-v", "error", "-show_streams", "-of", "json", "pipe:0"}, Stdin: bytes.NewReader(output.Bytes()), Stdout: &probe, Stderr: &logs})
|
|
if rc != 0 || err != nil {
|
|
t.Fatalf("probe: %d %v %s", rc, err, logs.String())
|
|
}
|
|
var result struct {
|
|
Streams []struct {
|
|
CodecType string `json:"codec_type"`
|
|
CodecName string `json:"codec_name"`
|
|
}
|
|
}
|
|
if err = json.Unmarshal(probe.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result.Streams) != 2 || result.Streams[0].CodecName != "h264" || result.Streams[1].CodecName != "aac" {
|
|
t.Fatalf("unexpected streams %s", probe.String())
|
|
}
|
|
})
|
|
}
|
|
// A malformed input must produce an error, not successful empty output.
|
|
s := NewStreamInput(context.Background())
|
|
defer s.Close()
|
|
s.Write([]byte("invalid"))
|
|
s.End(nil)
|
|
if err := Run(context.Background(), s, s, io.Discard, "", nil); err == nil {
|
|
t.Fatal("accepted invalid media")
|
|
}
|
|
}
|