github.com/pion/webrtc/v4@v4.0.1/e2e/e2e_test.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  //go:build e2e
     5  // +build e2e
     6  
     7  package main
     8  
     9  import (
    10  	"context"
    11  	"encoding/json"
    12  	"fmt"
    13  	"os"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/pion/webrtc/v4"
    20  	"github.com/pion/webrtc/v4/pkg/media"
    21  	"github.com/sclevine/agouti"
    22  )
    23  
    24  var silentOpusFrame = []byte{0xf8, 0xff, 0xfe} // 20ms, 8kHz, mono
    25  
    26  var drivers = map[string]func() *agouti.WebDriver{
    27  	"Chrome": func() *agouti.WebDriver {
    28  		return agouti.ChromeDriver(
    29  			agouti.ChromeOptions("args", []string{
    30  				"--headless",
    31  				"--disable-gpu",
    32  				"--no-sandbox",
    33  			}),
    34  			agouti.Desired(agouti.Capabilities{
    35  				"loggingPrefs": map[string]string{
    36  					"browser": "INFO",
    37  				},
    38  			}),
    39  		)
    40  	},
    41  }
    42  
    43  func TestE2E_Audio(t *testing.T) {
    44  	for name, d := range drivers {
    45  		driver := d()
    46  		t.Run(name, func(t *testing.T) {
    47  			if err := driver.Start(); err != nil {
    48  				t.Fatalf("Failed to start WebDriver: %v", err)
    49  			}
    50  			ctx, cancel := context.WithCancel(context.Background())
    51  			defer func() {
    52  				cancel()
    53  				time.Sleep(50 * time.Millisecond)
    54  				_ = driver.Stop()
    55  			}()
    56  			page, errPage := driver.NewPage()
    57  			if errPage != nil {
    58  				t.Fatalf("Failed to open page: %v", errPage)
    59  			}
    60  			if err := page.SetPageLoad(1000); err != nil {
    61  				t.Fatalf("Failed to load page: %v", err)
    62  			}
    63  			if err := page.SetImplicitWait(1000); err != nil {
    64  				t.Fatalf("Failed to set wait: %v", err)
    65  			}
    66  
    67  			chStarted := make(chan struct{})
    68  			chSDP := make(chan *webrtc.SessionDescription)
    69  			chStats := make(chan stats)
    70  			go logParseLoop(ctx, t, page, chStarted, chSDP, chStats)
    71  
    72  			pwd, errPwd := os.Getwd()
    73  			if errPwd != nil {
    74  				t.Fatalf("Failed to get working directory: %v", errPwd)
    75  			}
    76  			if err := page.Navigate(
    77  				fmt.Sprintf("file://%s/test.html", pwd),
    78  			); err != nil {
    79  				t.Fatalf("Failed to navigate: %v", err)
    80  			}
    81  
    82  			sdp := <-chSDP
    83  			pc, answer, track, errTrack := createTrack(*sdp)
    84  			if errTrack != nil {
    85  				t.Fatalf("Failed to create track: %v", errTrack)
    86  			}
    87  			defer func() {
    88  				_ = pc.Close()
    89  			}()
    90  
    91  			answerBytes, errAnsSDP := json.Marshal(answer)
    92  			if errAnsSDP != nil {
    93  				t.Fatalf("Failed to marshal SDP: %v", errAnsSDP)
    94  			}
    95  			var result string
    96  			if err := page.RunScript(
    97  				"pc.setRemoteDescription(JSON.parse(answer))",
    98  				map[string]interface{}{"answer": string(answerBytes)},
    99  				&result,
   100  			); err != nil {
   101  				t.Fatalf("Failed to run script to set SDP: %v", err)
   102  			}
   103  
   104  			go func() {
   105  				for {
   106  					if err := track.WriteSample(
   107  						media.Sample{Data: silentOpusFrame, Duration: time.Millisecond * 20},
   108  					); err != nil {
   109  						t.Errorf("Failed to WriteSample: %v", err)
   110  						return
   111  					}
   112  					select {
   113  					case <-time.After(20 * time.Millisecond):
   114  					case <-ctx.Done():
   115  						return
   116  					}
   117  				}
   118  			}()
   119  
   120  			select {
   121  			case <-chStarted:
   122  			case <-time.After(5 * time.Second):
   123  				t.Fatal("Timeout")
   124  			}
   125  
   126  			<-chStats
   127  			var packetReceived [2]int
   128  			for i := 0; i < 2; i++ {
   129  				select {
   130  				case stat := <-chStats:
   131  					for _, s := range stat {
   132  						if s.Type != "inbound-rtp" {
   133  							continue
   134  						}
   135  						if s.Kind != "audio" {
   136  							t.Errorf("Unused track stat received: %+v", s)
   137  							continue
   138  						}
   139  						packetReceived[i] = s.PacketsReceived
   140  					}
   141  				case <-time.After(5 * time.Second):
   142  					t.Fatal("Timeout")
   143  				}
   144  			}
   145  
   146  			packetsPerSecond := packetReceived[1] - packetReceived[0]
   147  			if packetsPerSecond < 45 || 55 < packetsPerSecond {
   148  				t.Errorf("Number of OPUS packets is expected to be: 50/second, got: %d/second", packetsPerSecond)
   149  			}
   150  		})
   151  	}
   152  }
   153  
   154  func TestE2E_DataChannel(t *testing.T) {
   155  	for name, d := range drivers {
   156  		driver := d()
   157  		t.Run(name, func(t *testing.T) {
   158  			if err := driver.Start(); err != nil {
   159  				t.Fatalf("Failed to start WebDriver: %v", err)
   160  			}
   161  			ctx, cancel := context.WithCancel(context.Background())
   162  			defer func() {
   163  				cancel()
   164  				time.Sleep(50 * time.Millisecond)
   165  				_ = driver.Stop()
   166  			}()
   167  
   168  			page, errPage := driver.NewPage()
   169  			if errPage != nil {
   170  				t.Fatalf("Failed to open page: %v", errPage)
   171  			}
   172  			if err := page.SetPageLoad(1000); err != nil {
   173  				t.Fatalf("Failed to load page: %v", err)
   174  			}
   175  			if err := page.SetImplicitWait(1000); err != nil {
   176  				t.Fatalf("Failed to set wait: %v", err)
   177  			}
   178  
   179  			chStarted := make(chan struct{})
   180  			chSDP := make(chan *webrtc.SessionDescription)
   181  			go logParseLoop(ctx, t, page, chStarted, chSDP, nil)
   182  
   183  			pwd, errPwd := os.Getwd()
   184  			if errPwd != nil {
   185  				t.Fatalf("Failed to get working directory: %v", errPwd)
   186  			}
   187  			if err := page.Navigate(
   188  				fmt.Sprintf("file://%s/test.html", pwd),
   189  			); err != nil {
   190  				t.Fatalf("Failed to navigate: %v", err)
   191  			}
   192  
   193  			sdp := <-chSDP
   194  			pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{})
   195  			if errPc != nil {
   196  				t.Fatalf("Failed to create peer connection: %v", errPc)
   197  			}
   198  			defer func() {
   199  				_ = pc.Close()
   200  			}()
   201  
   202  			chValid := make(chan struct{})
   203  			pc.OnDataChannel(func(dc *webrtc.DataChannel) {
   204  				dc.OnOpen(func() {
   205  					// Ping
   206  					if err := dc.SendText("hello world"); err != nil {
   207  						t.Errorf("Failed to send data: %v", err)
   208  					}
   209  				})
   210  				dc.OnMessage(func(msg webrtc.DataChannelMessage) {
   211  					// Pong
   212  					if string(msg.Data) != "HELLO WORLD" {
   213  						t.Errorf("expected message from browser: HELLO WORLD, got: %s", string(msg.Data))
   214  					} else {
   215  						chValid <- struct{}{}
   216  					}
   217  				})
   218  			})
   219  
   220  			if err := pc.SetRemoteDescription(*sdp); err != nil {
   221  				t.Fatalf("Failed to set remote description: %v", err)
   222  			}
   223  			answer, errAns := pc.CreateAnswer(nil)
   224  			if errAns != nil {
   225  				t.Fatalf("Failed to create answer: %v", errAns)
   226  			}
   227  			if err := pc.SetLocalDescription(answer); err != nil {
   228  				t.Fatalf("Failed to set local description: %v", err)
   229  			}
   230  
   231  			answerBytes, errAnsSDP := json.Marshal(answer)
   232  			if errAnsSDP != nil {
   233  				t.Fatalf("Failed to marshal SDP: %v", errAnsSDP)
   234  			}
   235  			var result string
   236  			if err := page.RunScript(
   237  				"pc.setRemoteDescription(JSON.parse(answer))",
   238  				map[string]interface{}{"answer": string(answerBytes)},
   239  				&result,
   240  			); err != nil {
   241  				t.Fatalf("Failed to run script to set SDP: %v", err)
   242  			}
   243  
   244  			select {
   245  			case <-chStarted:
   246  			case <-time.After(5 * time.Second):
   247  				t.Fatal("Timeout")
   248  			}
   249  			select {
   250  			case <-chValid:
   251  			case <-time.After(5 * time.Second):
   252  				t.Fatal("Timeout")
   253  			}
   254  		})
   255  	}
   256  }
   257  
   258  type stats []struct {
   259  	Kind            string `json:"kind"`
   260  	Type            string `json:"type"`
   261  	PacketsReceived int    `json:"packetsReceived"`
   262  }
   263  
   264  func logParseLoop(ctx context.Context, t *testing.T, page *agouti.Page, chStarted chan struct{}, chSDP chan *webrtc.SessionDescription, chStats chan stats) {
   265  	for {
   266  		select {
   267  		case <-time.After(time.Second):
   268  		case <-ctx.Done():
   269  			return
   270  		}
   271  		logs, errLog := page.ReadNewLogs("browser")
   272  		if errLog != nil {
   273  			t.Errorf("Failed to read log: %v", errLog)
   274  			return
   275  		}
   276  		for _, log := range logs {
   277  			k, v, ok := parseLog(log)
   278  			if !ok {
   279  				t.Log(log.Message)
   280  				continue
   281  			}
   282  			switch k {
   283  			case "connection":
   284  				switch v {
   285  				case "connected":
   286  					close(chStarted)
   287  				case "failed":
   288  					t.Error("Browser reported connection failed")
   289  					return
   290  				}
   291  			case "sdp":
   292  				sdp := &webrtc.SessionDescription{}
   293  				if err := json.Unmarshal([]byte(v), sdp); err != nil {
   294  					t.Errorf("Failed to unmarshal SDP: %v", err)
   295  					return
   296  				}
   297  				chSDP <- sdp
   298  			case "stats":
   299  				if chStats == nil {
   300  					break
   301  				}
   302  				s := &stats{}
   303  				if err := json.Unmarshal([]byte(v), &s); err != nil {
   304  					t.Errorf("Failed to parse log: %v", err)
   305  					break
   306  				}
   307  				select {
   308  				case chStats <- *s:
   309  				case <-time.After(10 * time.Millisecond):
   310  				}
   311  			default:
   312  				t.Log(log.Message)
   313  			}
   314  		}
   315  	}
   316  }
   317  
   318  func parseLog(log agouti.Log) (string, string, bool) {
   319  	l := strings.SplitN(log.Message, " ", 4)
   320  	if len(l) != 4 {
   321  		return "", "", false
   322  	}
   323  	k, err1 := strconv.Unquote(l[2])
   324  	if err1 != nil {
   325  		return "", "", false
   326  	}
   327  	v, err2 := strconv.Unquote(l[3])
   328  	if err2 != nil {
   329  		return "", "", false
   330  	}
   331  	return k, v, true
   332  }
   333  
   334  func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webrtc.SessionDescription, *webrtc.TrackLocalStaticSample, error) {
   335  	pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{})
   336  	if errPc != nil {
   337  		return nil, nil, nil, errPc
   338  	}
   339  
   340  	track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
   341  	if errTrack != nil {
   342  		return nil, nil, nil, errTrack
   343  	}
   344  	if _, err := pc.AddTrack(track); err != nil {
   345  		return nil, nil, nil, err
   346  	}
   347  	if err := pc.SetRemoteDescription(offer); err != nil {
   348  		return nil, nil, nil, err
   349  	}
   350  	answer, errAns := pc.CreateAnswer(nil)
   351  	if errAns != nil {
   352  		return nil, nil, nil, errAns
   353  	}
   354  	if err := pc.SetLocalDescription(answer); err != nil {
   355  		return nil, nil, nil, err
   356  	}
   357  	return pc, &answer, track, nil
   358  }