| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
- // SPDX-License-Identifier: MIT
- //go:build e2e
- // +build e2e
- package main
- import (
- "context"
- "encoding/json"
- "fmt"
- "os"
- "strconv"
- "strings"
- "testing"
- "time"
- "github.com/pion/webrtc/v3"
- "github.com/pion/webrtc/v3/pkg/media"
- "github.com/sclevine/agouti"
- )
- var silentOpusFrame = []byte{0xf8, 0xff, 0xfe} // 20ms, 8kHz, mono
- var drivers = map[string]func() *agouti.WebDriver{
- "Chrome": func() *agouti.WebDriver {
- return agouti.ChromeDriver(
- agouti.ChromeOptions("args", []string{
- "--headless",
- "--disable-gpu",
- "--no-sandbox",
- }),
- agouti.Desired(agouti.Capabilities{
- "loggingPrefs": map[string]string{
- "browser": "INFO",
- },
- }),
- )
- },
- }
- func TestE2E_Audio(t *testing.T) {
- for name, d := range drivers {
- driver := d()
- t.Run(name, func(t *testing.T) {
- if err := driver.Start(); err != nil {
- t.Fatalf("Failed to start WebDriver: %v", err)
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer func() {
- cancel()
- time.Sleep(50 * time.Millisecond)
- _ = driver.Stop()
- }()
- page, errPage := driver.NewPage()
- if errPage != nil {
- t.Fatalf("Failed to open page: %v", errPage)
- }
- if err := page.SetPageLoad(1000); err != nil {
- t.Fatalf("Failed to load page: %v", err)
- }
- if err := page.SetImplicitWait(1000); err != nil {
- t.Fatalf("Failed to set wait: %v", err)
- }
- chStarted := make(chan struct{})
- chSDP := make(chan *webrtc.SessionDescription)
- chStats := make(chan stats)
- go logParseLoop(ctx, t, page, chStarted, chSDP, chStats)
- pwd, errPwd := os.Getwd()
- if errPwd != nil {
- t.Fatalf("Failed to get working directory: %v", errPwd)
- }
- if err := page.Navigate(
- fmt.Sprintf("file://%s/test.html", pwd),
- ); err != nil {
- t.Fatalf("Failed to navigate: %v", err)
- }
- sdp := <-chSDP
- pc, answer, track, errTrack := createTrack(*sdp)
- if errTrack != nil {
- t.Fatalf("Failed to create track: %v", errTrack)
- }
- defer func() {
- _ = pc.Close()
- }()
- answerBytes, errAnsSDP := json.Marshal(answer)
- if errAnsSDP != nil {
- t.Fatalf("Failed to marshal SDP: %v", errAnsSDP)
- }
- var result string
- if err := page.RunScript(
- "pc.setRemoteDescription(JSON.parse(answer))",
- map[string]interface{}{"answer": string(answerBytes)},
- &result,
- ); err != nil {
- t.Fatalf("Failed to run script to set SDP: %v", err)
- }
- go func() {
- for {
- if err := track.WriteSample(
- media.Sample{Data: silentOpusFrame, Duration: time.Millisecond * 20},
- ); err != nil {
- t.Errorf("Failed to WriteSample: %v", err)
- return
- }
- select {
- case <-time.After(20 * time.Millisecond):
- case <-ctx.Done():
- return
- }
- }
- }()
- select {
- case <-chStarted:
- case <-time.After(5 * time.Second):
- t.Fatal("Timeout")
- }
- <-chStats
- var packetReceived [2]int
- for i := 0; i < 2; i++ {
- select {
- case stat := <-chStats:
- for _, s := range stat {
- if s.Type != "inbound-rtp" {
- continue
- }
- if s.Kind != "audio" {
- t.Errorf("Unused track stat received: %+v", s)
- continue
- }
- packetReceived[i] = s.PacketsReceived
- }
- case <-time.After(5 * time.Second):
- t.Fatal("Timeout")
- }
- }
- packetsPerSecond := packetReceived[1] - packetReceived[0]
- if packetsPerSecond < 45 || 55 < packetsPerSecond {
- t.Errorf("Number of OPUS packets is expected to be: 50/second, got: %d/second", packetsPerSecond)
- }
- })
- }
- }
- func TestE2E_DataChannel(t *testing.T) {
- for name, d := range drivers {
- driver := d()
- t.Run(name, func(t *testing.T) {
- if err := driver.Start(); err != nil {
- t.Fatalf("Failed to start WebDriver: %v", err)
- }
- ctx, cancel := context.WithCancel(context.Background())
- defer func() {
- cancel()
- time.Sleep(50 * time.Millisecond)
- _ = driver.Stop()
- }()
- page, errPage := driver.NewPage()
- if errPage != nil {
- t.Fatalf("Failed to open page: %v", errPage)
- }
- if err := page.SetPageLoad(1000); err != nil {
- t.Fatalf("Failed to load page: %v", err)
- }
- if err := page.SetImplicitWait(1000); err != nil {
- t.Fatalf("Failed to set wait: %v", err)
- }
- chStarted := make(chan struct{})
- chSDP := make(chan *webrtc.SessionDescription)
- go logParseLoop(ctx, t, page, chStarted, chSDP, nil)
- pwd, errPwd := os.Getwd()
- if errPwd != nil {
- t.Fatalf("Failed to get working directory: %v", errPwd)
- }
- if err := page.Navigate(
- fmt.Sprintf("file://%s/test.html", pwd),
- ); err != nil {
- t.Fatalf("Failed to navigate: %v", err)
- }
- sdp := <-chSDP
- pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{})
- if errPc != nil {
- t.Fatalf("Failed to create peer connection: %v", errPc)
- }
- defer func() {
- _ = pc.Close()
- }()
- chValid := make(chan struct{})
- pc.OnDataChannel(func(dc *webrtc.DataChannel) {
- dc.OnOpen(func() {
- // Ping
- if err := dc.SendText("hello world"); err != nil {
- t.Errorf("Failed to send data: %v", err)
- }
- })
- dc.OnMessage(func(msg webrtc.DataChannelMessage) {
- // Pong
- if string(msg.Data) != "HELLO WORLD" {
- t.Errorf("expected message from browser: HELLO WORLD, got: %s", string(msg.Data))
- } else {
- chValid <- struct{}{}
- }
- })
- })
- if err := pc.SetRemoteDescription(*sdp); err != nil {
- t.Fatalf("Failed to set remote description: %v", err)
- }
- answer, errAns := pc.CreateAnswer(nil)
- if errAns != nil {
- t.Fatalf("Failed to create answer: %v", errAns)
- }
- if err := pc.SetLocalDescription(answer); err != nil {
- t.Fatalf("Failed to set local description: %v", err)
- }
- answerBytes, errAnsSDP := json.Marshal(answer)
- if errAnsSDP != nil {
- t.Fatalf("Failed to marshal SDP: %v", errAnsSDP)
- }
- var result string
- if err := page.RunScript(
- "pc.setRemoteDescription(JSON.parse(answer))",
- map[string]interface{}{"answer": string(answerBytes)},
- &result,
- ); err != nil {
- t.Fatalf("Failed to run script to set SDP: %v", err)
- }
- select {
- case <-chStarted:
- case <-time.After(5 * time.Second):
- t.Fatal("Timeout")
- }
- select {
- case <-chValid:
- case <-time.After(5 * time.Second):
- t.Fatal("Timeout")
- }
- })
- }
- }
- type stats []struct {
- Kind string `json:"kind"`
- Type string `json:"type"`
- PacketsReceived int `json:"packetsReceived"`
- }
- func logParseLoop(ctx context.Context, t *testing.T, page *agouti.Page, chStarted chan struct{}, chSDP chan *webrtc.SessionDescription, chStats chan stats) {
- for {
- select {
- case <-time.After(time.Second):
- case <-ctx.Done():
- return
- }
- logs, errLog := page.ReadNewLogs("browser")
- if errLog != nil {
- t.Errorf("Failed to read log: %v", errLog)
- return
- }
- for _, log := range logs {
- k, v, ok := parseLog(log)
- if !ok {
- t.Log(log.Message)
- continue
- }
- switch k {
- case "connection":
- switch v {
- case "connected":
- close(chStarted)
- case "failed":
- t.Error("Browser reported connection failed")
- return
- }
- case "sdp":
- sdp := &webrtc.SessionDescription{}
- if err := json.Unmarshal([]byte(v), sdp); err != nil {
- t.Errorf("Failed to unmarshal SDP: %v", err)
- return
- }
- chSDP <- sdp
- case "stats":
- if chStats == nil {
- break
- }
- s := &stats{}
- if err := json.Unmarshal([]byte(v), &s); err != nil {
- t.Errorf("Failed to parse log: %v", err)
- break
- }
- select {
- case chStats <- *s:
- case <-time.After(10 * time.Millisecond):
- }
- default:
- t.Log(log.Message)
- }
- }
- }
- }
- func parseLog(log agouti.Log) (string, string, bool) {
- l := strings.SplitN(log.Message, " ", 4)
- if len(l) != 4 {
- return "", "", false
- }
- k, err1 := strconv.Unquote(l[2])
- if err1 != nil {
- return "", "", false
- }
- v, err2 := strconv.Unquote(l[3])
- if err2 != nil {
- return "", "", false
- }
- return k, v, true
- }
- func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webrtc.SessionDescription, *webrtc.TrackLocalStaticSample, error) {
- pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{})
- if errPc != nil {
- return nil, nil, nil, errPc
- }
- track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
- if errTrack != nil {
- return nil, nil, nil, errTrack
- }
- if _, err := pc.AddTrack(track); err != nil {
- return nil, nil, nil, err
- }
- if err := pc.SetRemoteDescription(offer); err != nil {
- return nil, nil, nil, err
- }
- answer, errAns := pc.CreateAnswer(nil)
- if errAns != nil {
- return nil, nil, nil, errAns
- }
- if err := pc.SetLocalDescription(answer); err != nil {
- return nil, nil, nil, err
- }
- return pc, &answer, track, nil
- }
|