mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
236 lines
6.2 KiB
Go
236 lines
6.2 KiB
Go
package ffmpeg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHTTPRange(t *testing.T) {
|
|
data := bytes.Repeat([]byte("0123456789"), 300000)
|
|
var requests atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
count := requests.Add(1)
|
|
if r.Header.Get("X-Test") != "secret" {
|
|
t.Error("missing headers")
|
|
}
|
|
if r.Header.Get("Accept-Encoding") != "identity" {
|
|
t.Error("missing identity encoding")
|
|
}
|
|
if count > 1 && r.Header.Get("If-Range") != `"v1"` {
|
|
t.Error("missing If-Range")
|
|
}
|
|
w.Header().Set("ETag", `"v1"`)
|
|
http.ServeContent(w, r, "media", time.Time{}, bytes.NewReader(data))
|
|
}))
|
|
defer srv.Close()
|
|
in, err := OpenHTTP(context.Background(), srv.Client(), HTTPSource{URL: srv.URL, Headers: map[string]string{"X-Test": "secret"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
if !in.Seekable() || in.Size() != int64(len(data)) {
|
|
t.Fatal("range not detected")
|
|
}
|
|
for _, off := range []int64{0, int64(len(data) - 16), 12, int64(httpBlockSize + 35)} {
|
|
if _, err = in.Seek(off, io.SeekStart); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := make([]byte, 16)
|
|
if _, err = io.ReadFull(in, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(b, data[off:off+16]) {
|
|
t.Fatalf("wrong bytes at %d", off)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHTTPSequentialAndValidation(t *testing.T) {
|
|
for _, kind := range []string{"sequential", "bad-range", "changed", "truncated", "unauthorized"} {
|
|
t.Run(kind, func(t *testing.T) {
|
|
var requests atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
count := requests.Add(1)
|
|
switch kind {
|
|
case "sequential":
|
|
w.Write([]byte("media"))
|
|
case "unauthorized":
|
|
w.WriteHeader(403)
|
|
case "bad-range":
|
|
w.Header().Set("Content-Range", "bytes 2-6/10")
|
|
w.WriteHeader(206)
|
|
w.Write([]byte("media"))
|
|
case "truncated":
|
|
w.Header().Set("Content-Range", "bytes 0-9/10")
|
|
w.WriteHeader(206)
|
|
w.Write([]byte("media"))
|
|
case "changed":
|
|
if count == 1 {
|
|
w.Header().Set("Content-Range", "bytes 0-4/10")
|
|
w.Header().Set("ETag", `"v1"`)
|
|
w.WriteHeader(206)
|
|
}
|
|
w.Write([]byte("media"))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
in, err := OpenHTTP(context.Background(), srv.Client(), HTTPSource{URL: srv.URL})
|
|
if kind == "sequential" {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
if in.Seekable() {
|
|
t.Fatal("unexpected seeking")
|
|
}
|
|
if _, err = in.Seek(0, 0); err != ErrSeek {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := io.ReadAll(in)
|
|
if err != nil || string(b) != "media" || requests.Load() != 1 {
|
|
t.Fatalf("%q %v count %d", b, err, requests.Load())
|
|
}
|
|
} else if kind == "changed" {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
in.Seek(5, 0)
|
|
if _, err = in.Read(make([]byte, 2)); err == nil {
|
|
t.Fatal("accepted 200 after range")
|
|
}
|
|
} else if err == nil {
|
|
in.Close()
|
|
t.Fatal("accepted invalid response")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStreamRing(t *testing.T) {
|
|
s := NewStreamInput(context.Background(), "")
|
|
defer s.Close()
|
|
// Start near the boundary to exercise wrap-around without a huge fixture.
|
|
s.read = StreamBufferLimit - 3
|
|
s.written = s.read
|
|
if _, err := s.Write([]byte("abcdefgh")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.End(nil)
|
|
b, err := io.ReadAll(s)
|
|
if err != nil || string(b) != "abcdefgh" {
|
|
t.Fatalf("%q %v", b, err)
|
|
}
|
|
name := s.file.Name()
|
|
if err = s.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = os.Stat(name); err == nil {
|
|
t.Fatal("temporary file leaked")
|
|
}
|
|
}
|
|
|
|
func TestOutputArguments(t *testing.T) {
|
|
a, err := Arguments("", []string{"-shortest", "-metadata", "title=test"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(a, " "), "-f mp4 pipe:1") {
|
|
t.Fatal(a)
|
|
}
|
|
for _, args := range [][]string{{"-i", "/etc/passwd"}, {"-report"}, {"-c:v", "libx264"}, {"-metadata"}, {"other.mp4"}} {
|
|
if _, err := Arguments("mp4", args); err == nil {
|
|
t.Fatal(fmt.Sprint(args))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStreamBackpressureAndCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
s := NewStreamInput(ctx, "")
|
|
defer s.Close()
|
|
f, err := os.CreateTemp(t.TempDir(), "ring")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.file = f
|
|
if err = f.Truncate(StreamBufferLimit); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.written = StreamBufferLimit
|
|
finished := make(chan error, 1)
|
|
go func() { _, err := s.Write([]byte{42}); finished <- err }()
|
|
select {
|
|
case err := <-finished:
|
|
t.Fatalf("did not apply backpressure: %v", err)
|
|
case <-time.After(10 * time.Millisecond):
|
|
}
|
|
if _, err = s.Read(make([]byte, 1)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case err := <-finished:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("writer did not resume")
|
|
}
|
|
go func() { _, err := s.Write([]byte{43}); finished <- err }()
|
|
cancel()
|
|
select {
|
|
case err := <-finished:
|
|
if err == nil {
|
|
t.Fatal("cancelled write succeeded")
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("writer did not cancel")
|
|
}
|
|
}
|
|
|
|
// A mobile app can write to its storage directory even when the OS default
|
|
// temporary directory is unavailable. The buffer must also be removed on close.
|
|
func TestStreamInputAppTempDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
unavailable := filepath.Join(root, "not-a-directory")
|
|
if err := os.WriteFile(unavailable, []byte("blocked"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TMPDIR", unavailable)
|
|
t.Setenv("TMP", unavailable)
|
|
t.Setenv("TEMP", unavailable)
|
|
dir := filepath.Join(root, "app", "temp", "ffmpeg")
|
|
s := NewStreamInput(context.Background(), dir)
|
|
t.Cleanup(func() { s.Close() })
|
|
data := []byte("media stream")
|
|
if _, err := s.Write(data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if filepath.Dir(s.file.Name()) != dir {
|
|
t.Fatalf("unexpected buffer: %s", s.file.Name())
|
|
}
|
|
s.End(nil)
|
|
got, err := io.ReadAll(s)
|
|
if err != nil || !bytes.Equal(got, data) {
|
|
t.Fatalf("read %q, error %v", got, err)
|
|
}
|
|
if err := s.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil || len(entries) != 0 {
|
|
t.Fatalf("buffer cleanup: %v, %v", entries, err)
|
|
}
|
|
}
|