IVF视频文件格式:
https://www.jianshu.com/p/cfbab5c3c8f7
Pion WebRTC
pion webrtc 是一个纯 golang 的 webrtc 实现开源项目,没有使用 cgo,继承了 golang 跨平台能力,基本所有平台都能使用,mips、ppc64 实测也可以使用。 pion webrtc 还有多个衍生项目,如 ion、ion-sfu 等等。
性能测试
性能测试主要使用其一个衍生项目 pion/rtsp-bench。服务端将 rtsp 转封装为 rtp 发布,客户端订阅服务端发布的 rtp 流,周期的增加订阅数量,达到压测的目的。 服务端在转发 rtp 数据的同时,也会周期的记录当前时间、订阅连接数、系统 CPU 使用百分比,这些记录数据会作为测试报告实时输出到 csv 格式文件中。
原始测试
原始测试中的 rtsp 源码率大概250kbps,如果想测其他码率可以通过搜索引擎找找公网上还能访问的 rtsp 源。这里我用 live555 搭建了一个rtsp服务器,通过 live555 提供稳定的 rtsp 源。
编译 live555
wget http://www.live555.com/liveMedia/public/live555-latest.tar.gz
tar xvfz live555-latest.tar.gz
cd live
./genMakefiles linux-64bit
make
启动 rtsp 服务
编译成功后,执行make install安装。也可以不安装,进入 mediaServer 目录运行 live555。
cd mediaServer
./live555MediaServer
运行后出现下面的这样的提示代表启动成功,按照提示把把媒体文件放到 live555 同级目录中,即可通过 rtsp 协议访问了。
LIVE555 Media Server
version 1.09 (LIVE555 Streaming Media library version 2021.08.24).
Play streams from this server using the URL
rtsp://192.168.0.100/<filename>
where <filename> is a file present in the current directory.
Each file's type is inferred from its name suffix:
".264" => a H.264 Video Elementary Stream file
".265" => a H.265 Video Elementary Stream file
".aac" => an AAC Audio (ADTS format) file
".ac3" => an AC-3 Audio file
".amr" => an AMR Audio file
".dv" => a DV Video file
".m4e" => a MPEG-4 Video Elementary Stream file
".mkv" => a Matroska audio+video+(optional)subtitles file
".mp3" => a MPEG-1 or 2 Audio file
".mpg" => a MPEG-1 or 2 Program Stream (audio+video) file
".ogg" or ".ogv" or ".opus" => an Ogg audio and/or video file
".ts" => a MPEG Transport Stream file (a ".tsx" index file - if present - provides server 'trick play' support)
".vob" => a VOB (MPEG-2 video with AC-3 audio) file
".wav" => a WAV Audio file
".webm" => a WebM audio(Vorbis)+video(VP8) file
See http://www.live555.com/mediaServer/ for additional documentation.
(We use port 80 for optional RTSP-over-HTTP tunneling).)
简单改造 live555
默认的 live555 在文件播放结束后便会停止这个会话,为了长时间压测,修改 live555 源码让他循环播放。改造原理就是当没有数据后seek到最开始的地方。 修改./liveMedia/ByteStreamFileSource.cpp文件的ByteStreamFileSource::doGetNextFrame方法后,再次编译运行即可。
void ByteStreamFileSource::doGetNextFrame() {
if (feof(fFid) || ferror(fFid) || (fLimitNumBytesToStream && fNumBytesToStream == 0)) {
//handleClosure();
//return;
fseek(fFid, 0, SEEK_SET);
}
#ifdef READ_FROM_FILES_SYNCHRONOUSLY
doReadFromFile();
#else
if (!fHaveStartedReading) {
envir().taskScheduler().turnOnBackgroundReadHandling(fileno(fFid),
(TaskScheduler::BackgroundHandlerProc*)&fileReadableHandler, this);
fHaveStartedReading = True;
}
#endif
}
编译启动 rtsp-bench server
修改rtsp-bench/server/main.go,把 rtspURL 换成 live555 的 rtsp 地址(不修改,直接使用示例源码中的也可以,码率大概200Kb)。
const rtspURL = “rtsp://192.168.0.100:554/1080P.264”
编译并启动:
export GO111MODULE=on
git clone https://github.com/pion/rtsp-bench.git
cd rtsp-bench/server
go run main.go
编译启动 rtsp-bench client
cd rtsp-bench/client
go run main.go localhost:8080
4. 改造测试
原始测试使用 rtsp 作为视频源,如果不是使用 rtsp-bench 中的示例 rtsp 地址,还需要自己搭建 rtsp 服务,比较麻烦。结合其另一个衍生项目,我对 rtsp-bench 做了一点改动,将 h264 流作为视频源,个人感觉更方便灵活一些,暂且将其称为 file-bench。
改造 file-bench server
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/pion/webrtc/v3/pkg/media/h264reader"
"github.com/shirou/gopsutil/cpu"
)
var (
outboundVideoTrack *webrtc.TrackLocalStaticSample
peerConnectionCount int64
)
var (
videoFilePath string
videoFrameDuration time.Duration
)
func GetCurrentDirectory() string {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
panic(err)
}
return strings.Replace(dir, "\\", "/", -1)
}
// Generate CSV with columns of timestamp, peerConnectionCount, and cpuUsage
func reportBuilder() {
file, err := os.OpenFile("report.csv", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
if _, err := file.WriteString("timestamp, peerConnectionCount, cpuUsage\n"); err != nil {
panic(err)
}
for range time.NewTicker(3 * time.Second).C {
usage, err := cpu.Percent(0, false)
if err != nil {
panic(err)
} else if len(usage) != 1 {
panic(fmt.Sprintf("CPU Usage results should have 1 sample, have %d", len(usage)))
}
if _, err = file.WriteString(fmt.Sprintf("%s, %d, %f\n", time.Now().Format(time.RFC3339), atomic.LoadInt64(&peerConnectionCount), usage[0])); err != nil {
panic(err)
}
}
}
func doSignaling(w http.ResponseWriter, r *http.Request) {
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{})
if err != nil {
panic(err)
}
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
if connectionState == webrtc.ICEConnectionStateDisconnected {
atomic.AddInt64(&peerConnectionCount, -1)
if err := peerConnection.Close(); err != nil {
panic(err)
}
} else if connectionState == webrtc.ICEConnectionStateConnected {
atomic.AddInt64(&peerConnectionCount, 1)
}
})
if rtpSender, err := peerConnection.AddTrack(outboundVideoTrack); err != nil {
panic(err)
} else {
go func() {
rtcpBuf := make([]byte, 1500)
for {
if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
}()
}
var offer webrtc.SessionDescription
if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
panic(err)
}
if err = peerConnection.SetRemoteDescription(offer); err != nil {
panic(err)
}
gatherCompletePromise := webrtc.GatheringCompletePromise(peerConnection)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
panic(err)
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
panic(err)
}
<-gatherCompletePromise
response, err := json.Marshal(*peerConnection.LocalDescription())
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(response); err != nil {
panic(err)
}
}
func main() {
if len(os.Args) < 3 {
panic("missing startup parameters: h264FileName h264FileFps")
}
videoFileName := os.Args[1]
if videoFileName == "" {
panic("invalid video file name")
}
fmt.Println("video file:", videoFileName)
videoFilePath = GetCurrentDirectory() + "/" + videoFileName
fps, fpsParamErr := strconv.Atoi(os.Args[2])
if fpsParamErr != nil {
panic(fpsParamErr)
}
fmt.Println("video fps:", fps)
videoFrameDuration = time.Duration(1000/fps) * time.Millisecond
fmt.Println()
_, openVideoErr := os.Stat(videoFilePath)
if os.IsNotExist(openVideoErr) {
panic("Could not find `" + videoFilePath + "`")
}
var err error
outboundVideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
}, "video", "pion")
if err != nil {
panic(err)
}
go videoFileConsumer()
go reportBuilder()
http.Handle("/", http.FileServer(http.Dir("./static")))
http.HandleFunc("/doSignaling", doSignaling)
fmt.Println("Open http://localhost:8080 to access this demo")
panic(http.ListenAndServe(":8080", nil))
}
func videoFileConsumer() {
for {
file, h264Err := os.Open(videoFilePath)
if h264Err != nil {
panic(h264Err)
}
h264, h264Err := h264reader.NewReader(file)
if h264Err != nil {
panic(h264Err)
}
ticker := time.NewTicker(videoFrameDuration)
for ; true; <-ticker.C {
nal, h264Err := h264.NextNAL()
if h264Err == io.EOF {
fmt.Println("all video frames parsed and sent, loop playback")
break
}
if h264Err != nil {
panic(h264Err)
}
if h264Err = outboundVideoTrack.WriteSample(media.Sample{Data: nal.Data, Duration: time.Second}); h264Err != nil {
panic(h264Err)
}
}
_ = file.Close()
}
}
编译并启动:
export GO111MODULE=on && go build -o server main.go && ./server 1080P.h264 30
使用 ffmpeg 导出 h264
导出h264参考:
ffmpeg -i $INPUT_FILE -an -c:v libx264 -profile:v baseline -level 3.0 -bsf:v h264_mp4toannexb -max_delay 0 -bf 0 $OUTPUT_FILE
导出ogg参考:
ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn $OUTPUT_FILE
5. 测试结果
在某公有云主机实测,分发 3Mbps 1080P 音视频,2核能够支撑300路转发,性能还是比较理想的。
参考:
https://39.99.141.248/archives/pion-webrtc-performance#title-11