Skip to content

Commit 54b0ed9

Browse files
committed
Add video_file_reader and test
1 parent f696e23 commit 54b0ed9

19 files changed

+3006
-137
lines changed

.github/.ci.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
2+
# SPDX-License-Identifier: MIT
3+
4+
PRE_TEST_HOOK=_install_dependencies_hook
5+
PRE_LINT_HOOK=_install_dependencies_hook
6+
GO_MOD_VERSION_EXPECTED=1.24
7+
SKIP_i386_TESTS=true
8+
SKIP_WINDOWS_TESTS=true
9+
SKIP_API_DIFF=true
10+
11+
function _install_dependencies_hook(){
12+
set -e
13+
14+
sudo apt-get update
15+
sudo apt-get install -y libvpx-dev pkg-config
16+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
bin/
1515
vendor/
1616
node_modules/
17+
data/
1718

1819
### Files ###
1920
#############
2021
*.ivf
2122
*.ogg
23+
*.y4m
2224
tags
2325
cover.out
2426
*.sw[poe]
2527
*.wasm
2628
examples/sfu-ws/cert.pem
2729
examples/sfu-ws/key.pem
2830
wasm_exec.js
31+
bwe-test

.reuse/dep5

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
22
Upstream-Name: Pion
33
Source: https://github.com/pion/
44

5-
Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock
6-
Copyright: 2023 The Pion community <https://pion.ly>
5+
Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock vnet/phases/*.json
6+
Copyright: 2025 The Pion community <https://pion.ly>
77
License: MIT
88

99
Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@ real-time media described in [RFC8867](https://www.rfc-editor.org/rfc/rfc8867.ht
2222
### Implemented/Planned Test Cases and Applications
2323
The current implementation uses [`vnet.Net` from
2424
pion/transport](https://github.com/pion/transport) to simulate network
25-
constraints. There are two test applications, one using a simple simulcast-like
26-
setup and the other one using adaptive bitrate streaming with a synthetic
27-
encoder.
25+
constraints. There are three test applications:
26+
27+
1. **Simulcast-like setup** - Uses a simple simulcast configuration
28+
2. **Adaptive bitrate streaming** - Uses synthetic encoder for adaptive bitrate
29+
3. **Video file reader** - Reads numbered JPG image files and sends them as video frames
30+
31+
#### Video File Reader
32+
The video file reader (`VideoFileReader`) reads series of numbered JPG image files
33+
(e.g., `frame_000.jpg`, `frame_001.jpg`, etc.) from a directory and sends them as
34+
individual video frames using an `RTCSender`. This allows testing with real video
35+
content while maintaining precise control over frame timing and network conditions.
36+
37+
To use the video file reader test:
38+
- Create directories with numbered JPG files (e.g., `../sample_videos_0/`, `../sample_videos_1/`)
39+
- Files should be named with sequential numbers (frame_000.jpg, frame_001.jpg, etc.)
40+
- The reader automatically discovers, sorts, and cycles through the frames
2841

2942
To run the simulcast test, you must create three input video files as described
3043
in the [bandwidth-esimation-from-disk
@@ -33,6 +46,7 @@ and place them in the `vnet` directory.
3346

3447
- [ ] **Variable Available Capacity with a Single Flow**
3548
- [ ] **Variable Available Capacity with Multiple Flows**
49+
- [x] **Dual Video Tracks with Variable Available Capacity** - Uses video file reader with multiple video tracks
3650
- [ ] **Congested Feedback Link with Bi-directional Media Flows**
3751
- [ ] **Competing Media Flows with the Same Congestion Control Algorithm**
3852
- [ ] **Round Trip Time Fairness**
@@ -53,6 +67,17 @@ interface. In future, we might automate the evaluation.
5367
### Running
5468
To run the tests, run `go test -v ./vnet/`.
5569

70+
To run the main test application with all test cases (including the video file reader test):
71+
```bash
72+
cd vnet
73+
go run .
74+
```
75+
76+
The application will run multiple test scenarios including:
77+
- ABR (Adaptive Bitrate) tests with single and multiple flows
78+
- Simulcast tests with single and multiple flows
79+
- Video file reader test with dual video tracks (requires sample video directories)
80+
5681
### Roadmap
5782
The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
5883

go.mod

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.21
55
toolchain go1.25.1
66

77
require (
8+
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b
89
github.com/pion/interceptor v0.1.41
910
github.com/pion/logging v0.2.4
1011
github.com/pion/mediadevices v0.7.2
@@ -16,24 +17,21 @@ require (
1617
golang.org/x/sync v0.11.0
1718
)
1819

19-
require (
20-
github.com/pion/dtls/v3 v3.0.7 // indirect
21-
github.com/pion/ice/v4 v4.0.10 // indirect
22-
github.com/pion/mdns/v2 v2.0.7 // indirect
23-
github.com/pion/srtp/v3 v3.0.7 // indirect
24-
github.com/pion/stun/v3 v3.0.0 // indirect
25-
github.com/pion/turn/v4 v4.1.1 // indirect
26-
github.com/wlynxg/anet v0.0.5 // indirect
27-
)
28-
2920
require (
3021
github.com/davecgh/go-spew v1.1.1 // indirect
3122
github.com/google/uuid v1.6.0 // indirect
3223
github.com/pion/datachannel v1.5.10 // indirect
24+
github.com/pion/dtls/v3 v3.0.7 // indirect
25+
github.com/pion/ice/v4 v4.0.10 // indirect
26+
github.com/pion/mdns/v2 v2.0.7 // indirect
3327
github.com/pion/randutil v0.1.0 // indirect
3428
github.com/pion/sctp v1.8.39 // indirect
3529
github.com/pion/sdp/v3 v3.0.15 // indirect
30+
github.com/pion/srtp/v3 v3.0.7 // indirect
31+
github.com/pion/stun/v3 v3.0.0 // indirect
32+
github.com/pion/turn/v4 v4.1.1 // indirect
3633
github.com/pmezard/go-difflib v1.0.0 // indirect
34+
github.com/wlynxg/anet v0.0.5 // indirect
3735
golang.org/x/crypto v0.33.0 // indirect
3836
golang.org/x/image v0.23.0 // indirect
3937
golang.org/x/net v0.35.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b h1:5Ci5wpOL75rYF6RQGRoqhEAU6xLJ6n/D4SckXX1yB74=
2+
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b/go.mod h1:obBQGGIFbbv9KWg92Qu9UHeD94JXmHD1jovY/z6I3O8=
13
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
24
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

vnet/flow.go

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14+
"os"
15+
"path/filepath"
16+
"strings"
1417

1518
"github.com/pion/bwe-test/logging"
1619
"github.com/pion/bwe-test/receiver"
1720
"github.com/pion/bwe-test/sender"
1821
plogging "github.com/pion/logging"
22+
"github.com/pion/transport/v3/vnet"
1923
)
2024

2125
// Flow represents a WebRTC connection between a sender and receiver over a virtual network.
@@ -31,8 +35,9 @@ func NewSimpleFlow(
3135
id int,
3236
senderMode senderMode,
3337
dataDir string,
38+
videoFiles ...VideoFileInfo,
3439
) (Flow, error) {
35-
snd, err := newSender(loggerFactory, nm, id, senderMode, dataDir)
40+
snd, err := newSender(loggerFactory, nm, id, senderMode, dataDir, videoFiles...)
3641
if err != nil {
3742
return Flow{}, fmt.Errorf("new sender: %w", err)
3843
}
@@ -47,7 +52,21 @@ func NewSimpleFlow(
4752
return Flow{}, fmt.Errorf("sender create offer: %w", err)
4853
}
4954

50-
rc, err := newReceiver(nm, id, dataDir)
55+
// Create output file path for received video
56+
outputVideoPath := ""
57+
if senderMode == videoFileEncoderMode {
58+
// Create a subdirectory for received videos
59+
receivedDir := filepath.Join(dataDir, "received_video")
60+
if mkdirErr := os.MkdirAll(receivedDir, 0o750); mkdirErr != nil {
61+
return Flow{}, fmt.Errorf("mkdir received_video: %w", mkdirErr)
62+
}
63+
64+
// For multiple tracks, we'll create output files for all tracks
65+
// The receiver will handle multiple tracks automatically
66+
outputVideoPath = filepath.Join(receivedDir, fmt.Sprintf("received_%d.ivf", id))
67+
}
68+
69+
rc, err := newReceiver(nm, id, dataDir, outputVideoPath, loggerFactory)
5170
if err != nil {
5271
return Flow{}, fmt.Errorf("new sender: %w", err)
5372
}
@@ -88,10 +107,13 @@ func (f Flow) Close() error {
88107
return errors.Join(errs...)
89108
}
90109

91-
var errUnknownSenderMode = errors.New("unknown sender mode")
110+
var (
111+
errUnknownSenderMode = errors.New("unknown sender mode")
112+
errVideoFileRequired = errors.New("videoFileEncoderMode requires at least one video file")
113+
)
92114

93115
type sndr struct {
94-
sender *sender.Sender
116+
sender sender.WebRTCSender
95117
ccLogger io.WriteCloser
96118
senderRTPLogger io.WriteCloser
97119
senderRTCPLogger io.WriteCloser
@@ -124,69 +146,102 @@ func newSender(
124146
id int,
125147
senderMode senderMode,
126148
dataDir string,
149+
videoFiles ...VideoFileInfo,
127150
) (sndr, error) {
128151
leftVnet, publicIPLeft, err := nm.GetLeftNet()
129152
if err != nil {
130153
return sndr{}, fmt.Errorf("get left net: %w", err)
131154
}
132155

156+
loggers, err := createSenderLoggers(dataDir, id)
157+
if err != nil {
158+
return sndr{}, err
159+
}
160+
161+
snd, err := createWebRTCSender(senderMode, leftVnet, publicIPLeft, loggers, loggerFactory, videoFiles...)
162+
if err != nil {
163+
return sndr{}, err
164+
}
165+
166+
return sndr{
167+
sender: snd,
168+
ccLogger: loggers.ccLogger,
169+
senderRTPLogger: loggers.rtpLogger,
170+
senderRTCPLogger: loggers.rtcpLogger,
171+
}, nil
172+
}
173+
174+
type senderLoggers struct {
175+
ccLogger io.WriteCloser
176+
rtpLogger io.WriteCloser
177+
rtcpLogger io.WriteCloser
178+
}
179+
180+
func createSenderLoggers(dataDir string, id int) (senderLoggers, error) {
133181
ccLogger, err := logging.GetLogFile(fmt.Sprintf("%v/%v_cc.log", dataDir, id))
134182
if err != nil {
135-
return sndr{}, fmt.Errorf("get cc log file: %w", err)
183+
return senderLoggers{}, fmt.Errorf("get cc log file: %w", err)
136184
}
137185

138186
senderRTPLogger, err := logging.GetLogFile(fmt.Sprintf("%v/%v_sender_rtp.log", dataDir, id))
139187
if err != nil {
140-
return sndr{}, fmt.Errorf("get sender rtp log file: %w", err)
188+
return senderLoggers{}, fmt.Errorf("get sender rtp log file: %w", err)
141189
}
142190

143191
senderRTCPLogger, err := logging.GetLogFile(fmt.Sprintf("%v/%v_sender_rtcp.log", dataDir, id))
144192
if err != nil {
145-
return sndr{}, fmt.Errorf("get sender rtcp log file: %w", err)
193+
return senderLoggers{}, fmt.Errorf("get sender rtcp log file: %w", err)
194+
}
195+
196+
return senderLoggers{
197+
ccLogger: ccLogger,
198+
rtpLogger: senderRTPLogger,
199+
rtcpLogger: senderRTCPLogger,
200+
}, nil
201+
}
202+
203+
func createWebRTCSender(
204+
senderMode senderMode,
205+
leftVnet *vnet.Net,
206+
publicIPLeft string,
207+
loggers senderLoggers,
208+
loggerFactory plogging.LoggerFactory,
209+
videoFiles ...VideoFileInfo,
210+
) (sender.WebRTCSender, error) {
211+
commonOpts := []sender.Option{
212+
sender.SetVnet(leftVnet, []string{publicIPLeft}),
213+
sender.PacketLogWriter(loggers.rtpLogger, loggers.rtcpLogger),
214+
sender.GCC(100_000),
215+
sender.CCLogWriter(loggers.ccLogger),
216+
sender.SetLoggerFactory(loggerFactory),
146217
}
147218

148-
var snd *sender.Sender
149219
switch senderMode {
150220
case abrSenderMode:
151-
snd, err = sender.NewSender(
221+
return sender.NewSender(
152222
sender.NewStatisticalEncoderSource(),
153-
sender.SetVnet(leftVnet, []string{publicIPLeft}),
154-
sender.PacketLogWriter(senderRTPLogger, senderRTCPLogger),
155-
sender.GCC(100_000),
156-
sender.CCLogWriter(ccLogger),
157-
sender.SetLoggerFactory(loggerFactory),
223+
commonOpts...,
158224
)
159-
if err != nil {
160-
return sndr{}, fmt.Errorf("new abr sender: %w", err)
161-
}
162225
case simulcastSenderMode:
163-
snd, err = sender.NewSender(
226+
return sender.NewSender(
164227
sender.NewSimulcastFilesSource(),
165-
sender.SetVnet(leftVnet, []string{publicIPLeft}),
166-
sender.PacketLogWriter(senderRTPLogger, senderRTCPLogger),
167-
sender.GCC(100_000),
168-
sender.CCLogWriter(ccLogger),
169-
sender.SetLoggerFactory(loggerFactory),
228+
commonOpts...,
170229
)
171-
if err != nil {
172-
return sndr{}, fmt.Errorf("new simulcast sender: %w", err)
230+
case videoFileEncoderMode:
231+
if len(videoFiles) == 0 {
232+
return nil, errVideoFileRequired
173233
}
234+
return NewVideoFileSender(videoFiles, commonOpts...)
174235
default:
175-
return sndr{}, fmt.Errorf("%w: %v", errUnknownSenderMode, senderMode)
236+
return nil, fmt.Errorf("%w: %v", errUnknownSenderMode, senderMode)
176237
}
177-
178-
return sndr{
179-
sender: snd,
180-
ccLogger: ccLogger,
181-
senderRTPLogger: senderRTPLogger,
182-
senderRTCPLogger: senderRTCPLogger,
183-
}, nil
184238
}
185239

186240
type recv struct {
187241
receiver *receiver.Receiver
188242
receiverRTPLogger io.WriteCloser
189243
receiverRTCPLogger io.WriteCloser
244+
videoOutputFile *os.File
190245
}
191246

192247
func (s recv) Close() error {
@@ -207,13 +262,17 @@ func (s recv) Close() error {
207262
errs = append(errs, err)
208263
}
209264

265+
// videoOutputFile is no longer used - receiver handles file closing internally
266+
210267
return errors.Join(errs...)
211268
}
212269

213270
func newReceiver(
214271
nm *NetworkManager,
215272
id int,
216273
dataDir string,
274+
outputVideoPath string,
275+
loggerFactory plogging.LoggerFactory,
217276
) (recv, error) {
218277
rightVnet, publicIPRight, err := nm.GetRightNet()
219278
if err != nil {
@@ -230,11 +289,22 @@ func newReceiver(
230289
return recv{}, fmt.Errorf("get receiver rtcp log file: %w", err)
231290
}
232291

233-
rc, err := receiver.NewReceiver(
292+
// Create options for the receiver
293+
opts := []receiver.Option{
234294
receiver.SetVnet(rightVnet, []string{publicIPRight}),
235295
receiver.PacketLogWriter(receiverRTPLogger, receiverRTCPLogger),
236296
receiver.DefaultInterceptors(),
237-
)
297+
receiver.SetLoggerFactory(loggerFactory),
298+
}
299+
300+
// Add video saving option if outputVideoPath is set
301+
if outputVideoPath != "" {
302+
// Use SaveVideo for video saving - remove .ivf extension from base path
303+
baseOutputPath := strings.TrimSuffix(outputVideoPath, ".ivf")
304+
opts = append(opts, receiver.SaveVideo(baseOutputPath))
305+
}
306+
307+
rc, err := receiver.NewReceiver(opts...)
238308
if err != nil {
239309
return recv{}, fmt.Errorf("new receiver: %w", err)
240310
}
@@ -243,5 +313,6 @@ func newReceiver(
243313
receiver: rc,
244314
receiverRTPLogger: receiverRTPLogger,
245315
receiverRTCPLogger: receiverRTCPLogger,
316+
videoOutputFile: nil, // No longer used with multiple track approach
246317
}, nil
247318
}

0 commit comments

Comments
 (0)