snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
This commit is contained in:
16
e2e/Dockerfile
Normal file
16
e2e/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
FROM golang:1.27-alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
chromium-chromedriver \
|
||||
git
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
COPY . /go/src/github.com/pion/webrtc
|
||||
WORKDIR /go/src/github.com/pion/webrtc/e2e
|
||||
|
||||
CMD ["go", "test", "-tags=e2e", "-v", "."]
|
||||
358
e2e/e2e_test.go
Normal file
358
e2e/e2e_test.go
Normal file
@@ -0,0 +1,358 @@
|
||||
// SPDX-FileCopyrightText: 2026 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/v4"
|
||||
"github.com/pion/webrtc/v4/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]any{"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]any{"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
|
||||
}
|
||||
45
e2e/test.html
Normal file
45
e2e/test.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<div id="media"></div>
|
||||
|
||||
<script>
|
||||
const pc = new RTCPeerConnection()
|
||||
pc.ontrack = event => {
|
||||
if (event.track.kind === 'audio') {
|
||||
var el = document.createElement(event.track.kind)
|
||||
el.srcObject = new MediaStream(event.streams[0].getAudioTracks())
|
||||
document.getElementById('media').appendChild(el)
|
||||
}
|
||||
}
|
||||
pc.oniceconnectionstatechange = event => {
|
||||
console.log("connection", pc.iceConnectionState)
|
||||
if (pc.iceConnectionState == 'connected') {
|
||||
setInterval(statsReport, 1000)
|
||||
}
|
||||
}
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
console.log("sdp", JSON.stringify(pc.localDescription))
|
||||
}
|
||||
}
|
||||
pc.addTransceiver('audio', {'direction': 'recvonly'})
|
||||
|
||||
const dc = pc.createDataChannel("upper")
|
||||
dc.onmessage = event => {
|
||||
dc.send(event.data.toUpperCase())
|
||||
}
|
||||
|
||||
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(console.log)
|
||||
|
||||
const statsReport = async () => {
|
||||
const stats = await pc.getStats()
|
||||
var data = []
|
||||
await stats.forEach(item => {
|
||||
data.push(item)
|
||||
})
|
||||
console.log("stats", JSON.stringify(data))
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user