1. Pion WebRTC

pion webrtc 是一个纯 golang 的 webrtc 实现开源项目,没有使用 cgo,继承了 golang 跨平台能力,基本所有平台都能使用,mips、ppc64 实测也可以使用。 pion webrtc 还有多个衍生项目,如 ion、ion-sfu 等等。

2. 性能测试

性能测试主要使用其一个衍生项目 pion/rtsp-bench。服务端将 rtsp 转封装为 rtp 发布,客户端订阅服务端发布的 rtp 流,周期的增加订阅数量,达到压测的目的。 服务端在转发 rtp 数据的同时,也会周期的记录当前时间、订阅连接数、系统 CPU 使用百分比,这些记录数据会作为测试报告实时输出到 csv 格式文件中。

3. 原始测试

原始测试中的 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路转发,性能还是比较理想的。