mirror of
https://github.com/pion/webrtc.git
synced 2025-09-27 03:25:58 +08:00
359 lines
8.6 KiB
Go
359 lines
8.6 KiB
Go
// 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
|
|
}
|