1package stream23import (4 "context"5 "io"6 "net/http"7 "strings"8 "testing"910 "airwaves/internal/tuner"11)1213// TestDemoStream runs ffmpeg end to end: a demo channel should produce a14// playable HLS playlist and segments over loopback HTTP.15func TestDemoStream(t *testing.T) {16 if testing.Short() {17 t.Skip("starts ffmpeg")18 }19 s, err := NewServer(true)20 if err != nil {21 t.Skip(err)22 }23 defer s.Close()2425 in := tuner.TestPattern("9.1")26 pb, err := s.Start(context.Background(), "test", in)27 if err != nil {28 t.Fatal(err)29 }3031 master := get(t, pb.URL)32 for _, want := range []string{`NAME="English"`, `NAME="Spanish"`, `AUDIO="aud"`, "CLOSED-CAPTIONS=NONE", "stream_0.m3u8"} {33 if !strings.Contains(master, want) {34 t.Errorf("master playlist lacks %s:\n%s", want, master)35 }36 }37 base := pb.URL[:strings.LastIndex(pb.URL, "/")+1]38 for _, variant := range []string{"stream_0.m3u8", "stream_1.m3u8", "stream_2.m3u8"} {39 body := get(t, base+variant)40 var seg string41 for _, line := range strings.Split(body, "\n") {42 if strings.HasSuffix(line, ".ts") {43 seg = line44 break45 }46 }47 if data := get(t, base+seg); len(data) < 1000 || data[0] != 0x47 {48 t.Errorf("%s segment %s is not MPEG-TS (%d bytes)", variant, seg, len(data))49 }50 }5152 s.Stop("test")53 resp, err := http.Get(pb.URL)54 if err != nil {55 t.Fatal(err)56 }57 resp.Body.Close()58 if resp.StatusCode != http.StatusNotFound {59 t.Errorf("stopped stream still served: HTTP %d", resp.StatusCode)60 }61}6263func get(t *testing.T, url string) string {64 t.Helper()65 resp, err := http.Get(url)66 if err != nil {67 t.Fatal(err)68 }69 defer resp.Body.Close()70 if resp.StatusCode != http.StatusOK {71 t.Fatalf("GET %s: HTTP %d", url, resp.StatusCode)72 }73 b, _ := io.ReadAll(resp.Body)74 return string(b)75}7677func TestMasterPlaylistForBroadcast(t *testing.T) {78 in := tuner.Input{79 Video: "0:v:0",80 Audio: []tuner.AudioTrack{{Map: "0:a:0", Lang: "eng"}, {Map: "0:a:1", Lang: "eng", Described: true}},81 Broadcast: true,82 }83 m := masterPlaylist(in)84 for _, want := range []string{85 `NAME="English",LANGUAGE="en",DEFAULT=YES`,86 `NAME="English (Described)"`, "describes-video",87 `TYPE=CLOSED-CAPTIONS`, `CLOSED-CAPTIONS="cc"`,88 } {89 if !strings.Contains(m, want) {90 t.Errorf("master lacks %s:\n%s", want, m)91 }92 }93 args := strings.Join(ffmpegArgs(in, "/tmp/x"), " ")94 if !strings.Contains(args, "-var_stream_map v:0 a:0 a:1") || !strings.Contains(args, "-hls_list_size 900") {95 t.Errorf("ffmpeg args: %s", args)96 }97 single := strings.Join(ffmpegArgs(tuner.Input{Video: "0:v:0", Audio: in.Audio[:1]}, "/tmp/x"), " ")98 if !strings.Contains(single, "-var_stream_map v:0,a:0") {99 t.Errorf("single-track args: %s", single)100 }101}102103// Custom channels' videos carry closed captions, without being broadcasts.104func TestMasterPlaylistForCaptions(t *testing.T) {105 in := tuner.Input{Video: "0:v:0", Audio: []tuner.AudioTrack{{Map: "0:a:0"}}, Captions: true}106 m := masterPlaylist(in)107 for _, want := range []string{`TYPE=CLOSED-CAPTIONS,GROUP-ID="cc"`, `INSTREAM-ID="CC1",DEFAULT=NO,AUTOSELECT=NO`, `CLOSED-CAPTIONS="cc"`} {108 if !strings.Contains(m, want) {109 t.Errorf("master lacks %s:\n%s", want, m)110 }111 }112 if args := strings.Join(ffmpegArgs(in, "/tmp/x"), " "); strings.Contains(args, "bwdif") || !strings.Contains(args, "-a53cc 1") {113 t.Errorf("ffmpeg args: %s", args)114 }115}116117// A picture that's H.264 already is copied, its segments kept to seek in;118// the sound is still made AAC.119func TestCopyArgs(t *testing.T) {120 in := tuner.Input{Args: []string{"-i", "v.mp4", "-i", "a.m4a"}, Video: "0:v:0", Audio: []tuner.AudioTrack{{Map: "1:a:0"}}, VOD: true, Copy: true}121 args := strings.Join(ffmpegArgs(in, "/tmp/x"), " ")122 for _, want := range []string{"-map 0:v:0 -map 1:a:0", "-c:v copy", "-c:a aac", "-hls_list_size 0"} {123 if !strings.Contains(args, want) {124 t.Errorf("no %q in %s", want, args)125 }126 }127 if strings.Contains(args, "libx264") || strings.Contains(args, "-force_key_frames") {128 t.Errorf("transcodes: %s", args)129 }130}