01. Support GB config as StreamCaster. 02. Support disable GB by --gb28181=off. 03. Add utests for SIP examples. 04. Wireshark plugin to decode TCP/9000 as rtp.rfc4571 05. Support MPEGPS program stream codec. 06. Add utest for PS stream codec. 07. Decode MPEGPS packet stream. 08. Carry RTP and PS packet as helper in PS message. 09. Support recover from error mode. 10. Support process by a pack of PS/TS messages. 11. Add statistic for recovered and msgs dropped. 12. Recover from err position fastly. 13. Define state machine for GB session. 14. Bind context to GB session. 15. Re-invite when media disconnected. 16. Update GitHub actions with GB28181. 17. Support parse CANDIDATE by env or pip. 18. Support mux GB28181 to RTMP. 19. Support regression test by srs-bench.
1699 lines
42 KiB
Go
1699 lines
42 KiB
Go
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2021 Winlin
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
// this software and associated documentation files (the "Software"), to deal in
|
|
// the Software without restriction, including without limitation the rights to
|
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
|
// subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
package srs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/ossrs/go-oryx-lib/amf0"
|
|
"github.com/ossrs/go-oryx-lib/avc"
|
|
"github.com/ossrs/go-oryx-lib/flv"
|
|
"github.com/ossrs/go-oryx-lib/rtmp"
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/rtp/codecs"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ossrs/go-oryx-lib/errors"
|
|
"github.com/ossrs/go-oryx-lib/logger"
|
|
vnet_proxy "github.com/ossrs/srs-bench/vnet"
|
|
"github.com/pion/interceptor"
|
|
"github.com/pion/logging"
|
|
"github.com/pion/rtcp"
|
|
"github.com/pion/transport/vnet"
|
|
"github.com/pion/webrtc/v3"
|
|
"github.com/pion/webrtc/v3/pkg/media/h264reader"
|
|
)
|
|
|
|
var srsHttps *bool
|
|
var srsLog *bool
|
|
|
|
var srsTimeout *int
|
|
var srsPlayPLI *int
|
|
var srsPlayOKPackets *int
|
|
var srsPublishOKPackets *int
|
|
var srsPublishVideoFps *int
|
|
var srsDTLSDropPackets *int
|
|
|
|
var srsSchema string
|
|
var srsServer *string
|
|
var srsStream *string
|
|
var srsLiveStream *string
|
|
var srsPublishAudio *string
|
|
var srsPublishVideo *string
|
|
var srsPublishAvatar *string
|
|
var srsPublishBBB *string
|
|
var srsVnetClientIP *string
|
|
|
|
func prepareTest() (err error) {
|
|
srsHttps = flag.Bool("srs-https", false, "Whther connect to HTTPS-API")
|
|
srsServer = flag.String("srs-server", "127.0.0.1", "The RTC server to connect to")
|
|
srsStream = flag.String("srs-stream", "/rtc/regression", "The RTC app/stream to play")
|
|
srsLiveStream = flag.String("srs-live-stream", "/live/livestream", "The LIVE app/stream to play")
|
|
srsLog = flag.Bool("srs-log", false, "Whether enable the detail log")
|
|
srsTimeout = flag.Int("srs-timeout", 5000, "For each case, the timeout in ms")
|
|
srsPlayPLI = flag.Int("srs-play-pli", 5000, "The PLI interval in seconds for player.")
|
|
srsPlayOKPackets = flag.Int("srs-play-ok-packets", 10, "If recv N RTP packets, it's ok, or fail")
|
|
srsPublishOKPackets = flag.Int("srs-publish-ok-packets", 3, "If send N RTP, recv N RTCP packets, it's ok, or fail")
|
|
srsPublishAudio = flag.String("srs-publish-audio", "avatar.ogg", "The audio file for publisher.")
|
|
srsPublishVideo = flag.String("srs-publish-video", "avatar.h264", "The video file for publisher.")
|
|
srsPublishAvatar = flag.String("srs-publish-avatar", "avatar.flv", "The avatar file for publisher.")
|
|
srsPublishBBB = flag.String("srs-publish-bbb", "bbb.flv", "The bbb file for publisher.")
|
|
srsPublishVideoFps = flag.Int("srs-publish-video-fps", 25, "The video fps for publisher.")
|
|
srsVnetClientIP = flag.String("srs-vnet-client-ip", "192.168.168.168", "The client ip in pion/vnet.")
|
|
srsDTLSDropPackets = flag.Int("srs-dtls-drop-packets", 5, "If dropped N packets, it's ok, or fail")
|
|
|
|
// Should parse it first.
|
|
flag.Parse()
|
|
|
|
// The stream should starts with /, for example, /rtc/regression
|
|
if !strings.HasPrefix(*srsStream, "/") {
|
|
*srsStream = "/" + *srsStream
|
|
}
|
|
|
|
// Generate srs protocol from whether use HTTPS.
|
|
srsSchema = "http"
|
|
if *srsHttps {
|
|
srsSchema = "https"
|
|
}
|
|
|
|
// Check file.
|
|
tryOpenFile := func(filename string) (string, error) {
|
|
if filename == "" {
|
|
return filename, nil
|
|
}
|
|
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
nfilename := path.Join("../", filename)
|
|
f2, err := os.Open(nfilename)
|
|
if err != nil {
|
|
return filename, errors.Wrapf(err, "No video file at %v or %v", filename, nfilename)
|
|
}
|
|
defer f2.Close()
|
|
|
|
return nfilename, nil
|
|
}
|
|
defer f.Close()
|
|
|
|
return filename, nil
|
|
}
|
|
|
|
if *srsPublishVideo, err = tryOpenFile(*srsPublishVideo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *srsPublishAvatar, err = tryOpenFile(*srsPublishAvatar); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *srsPublishBBB, err = tryOpenFile(*srsPublishBBB); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *srsPublishAudio, err = tryOpenFile(*srsPublishAudio); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Request SRS RTC API, the apiPath like "/rtc/v1/play", the r is WebRTC url like
|
|
// "webrtc://localhost/live/livestream", and the offer is SDP in string.
|
|
//
|
|
// Return the response of answer SDP in string.
|
|
func apiRtcRequest(ctx context.Context, apiPath, r, offer string) (string, error) {
|
|
u, err := url.Parse(r)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "Parse url %v", r)
|
|
}
|
|
|
|
// Build api url.
|
|
host := u.Host
|
|
if !strings.Contains(host, ":") {
|
|
host += ":1985"
|
|
}
|
|
|
|
api := fmt.Sprintf("http://%v", host)
|
|
if !strings.HasPrefix(apiPath, "/") {
|
|
api += "/"
|
|
}
|
|
api += apiPath
|
|
|
|
if !strings.HasSuffix(apiPath, "/") {
|
|
api += "/"
|
|
}
|
|
if u.RawQuery != "" {
|
|
api += "?" + u.RawQuery
|
|
}
|
|
|
|
// Build JSON body.
|
|
reqBody := struct {
|
|
Api string `json:"api"`
|
|
ClientIP string `json:"clientip"`
|
|
SDP string `json:"sdp"`
|
|
StreamURL string `json:"streamurl"`
|
|
}{
|
|
api, "", offer, r,
|
|
}
|
|
|
|
resBody := struct {
|
|
Code int `json:"code"`
|
|
Session string `json:"sessionid"`
|
|
SDP string `json:"sdp"`
|
|
}{}
|
|
|
|
if err := apiRequest(ctx, api, reqBody, &resBody); err != nil {
|
|
return "", errors.Wrapf(err, "request api=%v", api)
|
|
}
|
|
|
|
if resBody.Code != 0 {
|
|
return "", errors.Errorf("Server fail code=%v", resBody.Code)
|
|
}
|
|
logger.If(ctx, "Parse response to code=%v, session=%v, sdp=%v",
|
|
resBody.Code, resBody.Session, escapeSDP(resBody.SDP))
|
|
logger.Tf(ctx, "Parse response to code=%v, session=%v, sdp=%v bytes",
|
|
resBody.Code, resBody.Session, len(resBody.SDP))
|
|
|
|
return resBody.SDP, nil
|
|
}
|
|
|
|
func escapeSDP(sdp string) string {
|
|
return strings.ReplaceAll(strings.ReplaceAll(sdp, "\r", "\\r"), "\n", "\\n")
|
|
}
|
|
|
|
func packageAsSTAPA(frames ...*h264reader.NAL) *h264reader.NAL {
|
|
first := frames[0]
|
|
|
|
buf := bytes.Buffer{}
|
|
buf.WriteByte(
|
|
first.RefIdc<<5&0x60 | byte(24), // STAP-A
|
|
)
|
|
|
|
for _, frame := range frames {
|
|
buf.WriteByte(byte(len(frame.Data) >> 8))
|
|
buf.WriteByte(byte(len(frame.Data)))
|
|
buf.Write(frame.Data)
|
|
}
|
|
|
|
return &h264reader.NAL{
|
|
PictureOrderCount: first.PictureOrderCount,
|
|
ForbiddenZeroBit: false,
|
|
RefIdc: first.RefIdc,
|
|
UnitType: h264reader.NalUnitType(24), // STAP-A
|
|
Data: buf.Bytes(),
|
|
}
|
|
}
|
|
|
|
type wallClock struct {
|
|
start time.Time
|
|
duration time.Duration
|
|
}
|
|
|
|
func newWallClock() *wallClock {
|
|
return &wallClock{start: time.Now()}
|
|
}
|
|
|
|
func (v *wallClock) Tick(d time.Duration) time.Duration {
|
|
v.duration += d
|
|
|
|
wc := time.Now().Sub(v.start)
|
|
re := v.duration - wc
|
|
if re > 30*time.Millisecond {
|
|
return re
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Do nothing for SDP.
|
|
func testUtilPassBy(s *webrtc.SessionDescription) error {
|
|
return nil
|
|
}
|
|
|
|
// Set to active, as DTLS client, to start ClientHello.
|
|
func testUtilSetupActive(s *webrtc.SessionDescription) error {
|
|
if strings.Contains(s.SDP, "setup:passive") {
|
|
return errors.New("set to active")
|
|
}
|
|
|
|
s.SDP = strings.ReplaceAll(s.SDP, "setup:actpass", "setup:active")
|
|
return nil
|
|
}
|
|
|
|
// Set to passive, as DTLS client, to start ClientHello.
|
|
func testUtilSetupPassive(s *webrtc.SessionDescription) error {
|
|
if strings.Contains(s.SDP, "setup:active") {
|
|
return errors.New("set to passive")
|
|
}
|
|
|
|
s.SDP = strings.ReplaceAll(s.SDP, "setup:actpass", "setup:passive")
|
|
return nil
|
|
}
|
|
|
|
// Parse address from SDP.
|
|
// candidate:0 1 udp 2130706431 192.168.3.8 8000 typ host generation 0
|
|
func parseAddressOfCandidate(answerSDP string) (*net.UDPAddr, error) {
|
|
answer := webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: answerSDP}
|
|
answerObject, err := answer.Unmarshal()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unmarshal answer %v", answerSDP)
|
|
}
|
|
|
|
if len(answerObject.MediaDescriptions) == 0 {
|
|
return nil, errors.New("no media")
|
|
}
|
|
|
|
candidate, ok := answerObject.MediaDescriptions[0].Attribute("candidate")
|
|
if !ok {
|
|
return nil, errors.New("no candidate")
|
|
}
|
|
|
|
// candidate:0 1 udp 2130706431 192.168.3.8 8000 typ host generation 0
|
|
attrs := strings.Split(candidate, " ")
|
|
if len(attrs) <= 6 {
|
|
return nil, errors.Errorf("no address in %v", candidate)
|
|
}
|
|
|
|
// Parse ip and port from answer.
|
|
ip := attrs[4]
|
|
port, err := strconv.Atoi(attrs[5])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "invalid port %v", candidate)
|
|
}
|
|
|
|
address := fmt.Sprintf("%v:%v", ip, port)
|
|
addr, err := net.ResolveUDPAddr("udp4", address)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "parse %v", address)
|
|
}
|
|
|
|
return addr, nil
|
|
}
|
|
|
|
// Filter the test error, ignore context.Canceled
|
|
func filterTestError(errs ...error) error {
|
|
var filteredErrors []error
|
|
|
|
for _, err := range errs {
|
|
if err == nil || errors.Cause(err) == context.Canceled {
|
|
continue
|
|
}
|
|
|
|
// If url error, server maybe error, do not print the detail log.
|
|
if r0 := errors.Cause(err); r0 != nil {
|
|
if r1, ok := r0.(*url.Error); ok {
|
|
err = r1
|
|
}
|
|
}
|
|
|
|
filteredErrors = append(filteredErrors, err)
|
|
}
|
|
|
|
if len(filteredErrors) == 0 {
|
|
return nil
|
|
}
|
|
if len(filteredErrors) == 1 {
|
|
return filteredErrors[0]
|
|
}
|
|
|
|
var descs []string
|
|
for i, err := range filteredErrors[1:] {
|
|
descs = append(descs, fmt.Sprintf("err #%d, %+v", i, err))
|
|
}
|
|
return errors.Wrapf(filteredErrors[0], "with %v", strings.Join(descs, ","))
|
|
}
|
|
|
|
// For STUN packet, 0x00 is binding request, 0x01 is binding success response.
|
|
// @see srs_is_stun of https://github.com/ossrs/srs
|
|
func srsIsStun(b []byte) bool {
|
|
return len(b) > 0 && (b[0] == 0 || b[0] == 1)
|
|
}
|
|
|
|
// change_cipher_spec(20), alert(21), handshake(22), application_data(23)
|
|
// @see https://tools.ietf.org/html/rfc2246#section-6.2.1
|
|
// @see srs_is_dtls of https://github.com/ossrs/srs
|
|
func srsIsDTLS(b []byte) bool {
|
|
return len(b) >= 13 && (b[0] > 19 && b[0] < 64)
|
|
}
|
|
|
|
// For RTP or RTCP, the V=2 which is in the high 2bits, 0xC0 (1100 0000)
|
|
// @see srs_is_rtp_or_rtcp of https://github.com/ossrs/srs
|
|
func srsIsRTPOrRTCP(b []byte) bool {
|
|
return len(b) >= 12 && (b[0]&0xC0) == 0x80
|
|
}
|
|
|
|
// For RTCP, PT is [128, 223] (or without marker [0, 95]).
|
|
// Literally, RTCP starts from 64 not 0, so PT is [192, 223] (or without marker [64, 95]).
|
|
// @note For RTP, the PT is [96, 127], or [224, 255] with marker.
|
|
// @see srs_is_rtcp of https://github.com/ossrs/srs
|
|
func srsIsRTCP(b []byte) bool {
|
|
return (len(b) >= 12) && (b[0]&0x80) != 0 && (b[1] >= 192 && b[1] <= 223)
|
|
}
|
|
|
|
type chunkType int
|
|
|
|
const (
|
|
chunkTypeICE chunkType = iota + 1
|
|
chunkTypeDTLS
|
|
chunkTypeRTP
|
|
chunkTypeRTCP
|
|
)
|
|
|
|
func (v chunkType) String() string {
|
|
switch v {
|
|
case chunkTypeICE:
|
|
return "ICE"
|
|
case chunkTypeDTLS:
|
|
return "DTLS"
|
|
case chunkTypeRTP:
|
|
return "RTP"
|
|
case chunkTypeRTCP:
|
|
return "RTCP"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
type dtlsContentType int
|
|
|
|
const (
|
|
dtlsContentTypeHandshake dtlsContentType = 22
|
|
dtlsContentTypeChangeCipherSpec dtlsContentType = 20
|
|
dtlsContentTypeAlert dtlsContentType = 21
|
|
)
|
|
|
|
func (v dtlsContentType) String() string {
|
|
switch v {
|
|
case dtlsContentTypeHandshake:
|
|
return "Handshake"
|
|
case dtlsContentTypeChangeCipherSpec:
|
|
return "ChangeCipherSpec"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
type dtlsHandshakeType int
|
|
|
|
const (
|
|
dtlsHandshakeTypeClientHello dtlsHandshakeType = 1
|
|
dtlsHandshakeTypeServerHello dtlsHandshakeType = 2
|
|
dtlsHandshakeTypeCertificate dtlsHandshakeType = 11
|
|
dtlsHandshakeTypeServerKeyExchange dtlsHandshakeType = 12
|
|
dtlsHandshakeTypeCertificateRequest dtlsHandshakeType = 13
|
|
dtlsHandshakeTypeServerDone dtlsHandshakeType = 14
|
|
dtlsHandshakeTypeCertificateVerify dtlsHandshakeType = 15
|
|
dtlsHandshakeTypeClientKeyExchange dtlsHandshakeType = 16
|
|
dtlsHandshakeTypeFinished dtlsHandshakeType = 20
|
|
)
|
|
|
|
func (v dtlsHandshakeType) String() string {
|
|
switch v {
|
|
case dtlsHandshakeTypeClientHello:
|
|
return "ClientHello"
|
|
case dtlsHandshakeTypeServerHello:
|
|
return "ServerHello"
|
|
case dtlsHandshakeTypeCertificate:
|
|
return "Certificate"
|
|
case dtlsHandshakeTypeServerKeyExchange:
|
|
return "ServerKeyExchange"
|
|
case dtlsHandshakeTypeCertificateRequest:
|
|
return "CertificateRequest"
|
|
case dtlsHandshakeTypeServerDone:
|
|
return "ServerDone"
|
|
case dtlsHandshakeTypeCertificateVerify:
|
|
return "CertificateVerify"
|
|
case dtlsHandshakeTypeClientKeyExchange:
|
|
return "ClientKeyExchange"
|
|
case dtlsHandshakeTypeFinished:
|
|
return "Finished"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
type chunkMessageType struct {
|
|
chunk chunkType
|
|
content dtlsContentType
|
|
handshake dtlsHandshakeType
|
|
}
|
|
|
|
func (v *chunkMessageType) String() string {
|
|
if v.chunk == chunkTypeDTLS {
|
|
if v.content == dtlsContentTypeHandshake {
|
|
return fmt.Sprintf("%v-%v-%v", v.chunk, v.content, v.handshake)
|
|
} else {
|
|
return fmt.Sprintf("%v-%v", v.chunk, v.content)
|
|
}
|
|
}
|
|
return fmt.Sprintf("%v", v.chunk)
|
|
}
|
|
|
|
func newChunkMessageType(c vnet.Chunk) (*chunkMessageType, bool) {
|
|
b := c.UserData()
|
|
|
|
if len(b) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
v := &chunkMessageType{}
|
|
|
|
if srsIsRTPOrRTCP(b) {
|
|
if srsIsRTCP(b) {
|
|
v.chunk = chunkTypeRTCP
|
|
} else {
|
|
v.chunk = chunkTypeRTP
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
if srsIsStun(b) {
|
|
v.chunk = chunkTypeICE
|
|
return v, true
|
|
}
|
|
|
|
if !srsIsDTLS(b) {
|
|
return nil, false
|
|
}
|
|
|
|
v.chunk, v.content = chunkTypeDTLS, dtlsContentType(b[0])
|
|
if v.content != dtlsContentTypeHandshake {
|
|
return v, true
|
|
}
|
|
|
|
if len(b) < 14 {
|
|
return v, false
|
|
}
|
|
v.handshake = dtlsHandshakeType(b[13])
|
|
return v, true
|
|
}
|
|
|
|
func (v *chunkMessageType) IsHandshake() bool {
|
|
return v.chunk == chunkTypeDTLS && v.content == dtlsContentTypeHandshake
|
|
}
|
|
|
|
func (v *chunkMessageType) IsClientHello() bool {
|
|
return v.chunk == chunkTypeDTLS && v.content == dtlsContentTypeHandshake && v.handshake == dtlsHandshakeTypeClientHello
|
|
}
|
|
|
|
func (v *chunkMessageType) IsServerHello() bool {
|
|
return v.chunk == chunkTypeDTLS && v.content == dtlsContentTypeHandshake && v.handshake == dtlsHandshakeTypeServerHello
|
|
}
|
|
|
|
func (v *chunkMessageType) IsCertificate() bool {
|
|
return v.chunk == chunkTypeDTLS && v.content == dtlsContentTypeHandshake && v.handshake == dtlsHandshakeTypeCertificate
|
|
}
|
|
|
|
func (v *chunkMessageType) IsChangeCipherSpec() bool {
|
|
return v.chunk == chunkTypeDTLS && v.content == dtlsContentTypeChangeCipherSpec
|
|
}
|
|
|
|
type dtlsRecord struct {
|
|
ContentType dtlsContentType
|
|
Version uint16
|
|
Epoch uint16
|
|
SequenceNumber uint64
|
|
Length uint16
|
|
Data []byte
|
|
}
|
|
|
|
func newDTLSRecord(b []byte) (*dtlsRecord, error) {
|
|
v := &dtlsRecord{}
|
|
return v, v.Unmarshal(b)
|
|
}
|
|
|
|
func (v *dtlsRecord) String() string {
|
|
return fmt.Sprintf("epoch=%v, sequence=%v", v.Epoch, v.SequenceNumber)
|
|
}
|
|
|
|
func (v *dtlsRecord) Equals(p *dtlsRecord) bool {
|
|
return v.Epoch == p.Epoch && v.SequenceNumber == p.SequenceNumber
|
|
}
|
|
|
|
func (v *dtlsRecord) Unmarshal(b []byte) error {
|
|
if len(b) < 13 {
|
|
return errors.Errorf("requires 13B only %v", len(b))
|
|
}
|
|
|
|
v.ContentType = dtlsContentType(b[0])
|
|
v.Version = uint16(b[1])<<8 | uint16(b[2])
|
|
v.Epoch = uint16(b[3])<<8 | uint16(b[4])
|
|
v.SequenceNumber = uint64(b[5])<<40 | uint64(b[6])<<32 | uint64(b[7])<<24 | uint64(b[8])<<16 | uint64(b[9])<<8 | uint64(b[10])
|
|
v.Length = uint16(b[11])<<8 | uint16(b[12])
|
|
v.Data = b[13:]
|
|
return nil
|
|
}
|
|
|
|
// The func to setup testWebRTCAPI
|
|
type testWebRTCAPIOptionFunc func(api *testWebRTCAPI)
|
|
|
|
type testWebRTCAPI struct {
|
|
// The options to setup the api.
|
|
options []testWebRTCAPIOptionFunc
|
|
// The api and settings.
|
|
api *webrtc.API
|
|
mediaEngine *webrtc.MediaEngine
|
|
registry *interceptor.Registry
|
|
settingEngine *webrtc.SettingEngine
|
|
// The vnet router, can be shared by different apis, but we do not share it.
|
|
router *vnet.Router
|
|
// The network for api.
|
|
network *vnet.Net
|
|
// The vnet UDP proxy bind to the router.
|
|
proxy *vnet_proxy.UDPProxy
|
|
}
|
|
|
|
// The func to initialize testWebRTCAPI
|
|
type testWebRTCAPIInitFunc func(api *testWebRTCAPI) error
|
|
|
|
// Implements interface testWebRTCAPIInitFunc to init testWebRTCAPI
|
|
func registerDefaultCodecs(api *testWebRTCAPI) error {
|
|
v := api
|
|
|
|
if err := v.mediaEngine.RegisterDefaultCodecs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := webrtc.RegisterDefaultInterceptors(v.mediaEngine, v.registry); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Implements interface testWebRTCAPIInitFunc to init testWebRTCAPI
|
|
func registerMiniCodecs(api *testWebRTCAPI) error {
|
|
v := api
|
|
|
|
if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
|
PayloadType: 111,
|
|
}, webrtc.RTPCodecTypeAudio); err != nil {
|
|
return err
|
|
}
|
|
|
|
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
|
|
if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
|
|
PayloadType: 108,
|
|
}, webrtc.RTPCodecTypeVideo); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Interceptors for NACK??? @see webrtc.ConfigureNack(v.mediaEngine, v.registry)
|
|
return nil
|
|
}
|
|
|
|
// Implements interface testWebRTCAPIInitFunc to init testWebRTCAPI
|
|
func registerMiniCodecsWithoutNack(api *testWebRTCAPI) error {
|
|
v := api
|
|
|
|
if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
|
PayloadType: 111,
|
|
}, webrtc.RTPCodecTypeAudio); err != nil {
|
|
return err
|
|
}
|
|
|
|
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}}
|
|
if err := v.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", videoRTCPFeedback},
|
|
PayloadType: 108,
|
|
}, webrtc.RTPCodecTypeVideo); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Interceptors for NACK??? @see webrtc.ConfigureNack(v.mediaEngine, v.registry)
|
|
return nil
|
|
}
|
|
|
|
func newTestWebRTCAPI(inits ...testWebRTCAPIInitFunc) (*testWebRTCAPI, error) {
|
|
v := &testWebRTCAPI{}
|
|
|
|
v.mediaEngine = &webrtc.MediaEngine{}
|
|
v.registry = &interceptor.Registry{}
|
|
v.settingEngine = &webrtc.SettingEngine{}
|
|
|
|
// Apply initialize filter, for example, register default codecs when create publisher/player.
|
|
for _, setup := range inits {
|
|
if setup == nil {
|
|
continue
|
|
}
|
|
|
|
if err := setup(v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func (v *testWebRTCAPI) Close() error {
|
|
if v.proxy != nil {
|
|
_ = v.proxy.Close()
|
|
}
|
|
|
|
if v.router != nil {
|
|
_ = v.router.Stop()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *testWebRTCAPI) Setup(vnetClientIP string, options ...testWebRTCAPIOptionFunc) error {
|
|
// Setting engine for https://github.com/pion/transport/tree/master/vnet
|
|
setupVnet := func(vnetClientIP string) (err error) {
|
|
// We create a private router for a api, however, it's possible to share the
|
|
// same router between apis.
|
|
if v.router, err = vnet.NewRouter(&vnet.RouterConfig{
|
|
CIDR: "0.0.0.0/0", // Accept all ip, no sub router.
|
|
LoggerFactory: logging.NewDefaultLoggerFactory(),
|
|
}); err != nil {
|
|
return errors.Wrapf(err, "create router for api")
|
|
}
|
|
|
|
// Each api should bind to a network, however, it's possible to share it
|
|
// for different apis.
|
|
v.network = vnet.NewNet(&vnet.NetConfig{
|
|
StaticIP: vnetClientIP,
|
|
})
|
|
|
|
if err = v.router.AddNet(v.network); err != nil {
|
|
return errors.Wrapf(err, "create network for api")
|
|
}
|
|
|
|
v.settingEngine.SetVNet(v.network)
|
|
|
|
// Create a proxy bind to the router.
|
|
if v.proxy, err = vnet_proxy.NewProxy(v.router); err != nil {
|
|
return errors.Wrapf(err, "create proxy for router")
|
|
}
|
|
|
|
return v.router.Start()
|
|
}
|
|
if err := setupVnet(vnetClientIP); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Apply options from params, for example, tester to register vnet filter.
|
|
for _, setup := range options {
|
|
setup(v)
|
|
}
|
|
|
|
// Apply options in api, for example, publisher register audio-level interceptor.
|
|
for _, setup := range v.options {
|
|
setup(v)
|
|
}
|
|
|
|
v.api = webrtc.NewAPI(
|
|
webrtc.WithMediaEngine(v.mediaEngine),
|
|
webrtc.WithInterceptorRegistry(v.registry),
|
|
webrtc.WithSettingEngine(*v.settingEngine),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *testWebRTCAPI) NewPeerConnection(configuration webrtc.Configuration) (*webrtc.PeerConnection, error) {
|
|
return v.api.NewPeerConnection(configuration)
|
|
}
|
|
|
|
type testPlayerOptionFunc func(p *testPlayer) error
|
|
|
|
type testPlayer struct {
|
|
onOffer func(s *webrtc.SessionDescription) error
|
|
onAnswer func(s *webrtc.SessionDescription) error
|
|
iceReadyCancel context.CancelFunc
|
|
pc *webrtc.PeerConnection
|
|
receivers []*webrtc.RTPReceiver
|
|
// We should dispose it.
|
|
api *testWebRTCAPI
|
|
// Optional suffix for stream url.
|
|
streamSuffix string
|
|
// Optional app/stream to play, use srsStream by default.
|
|
defaultStream string
|
|
}
|
|
|
|
// Create test player, the init is used to initialize api which maybe nil,
|
|
// and the options is used to setup the player itself.
|
|
func newTestPlayer(init testWebRTCAPIInitFunc, options ...testPlayerOptionFunc) (*testPlayer, error) {
|
|
v := &testPlayer{}
|
|
|
|
api, err := newTestWebRTCAPI(init)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v.api = api
|
|
|
|
for _, opt := range options {
|
|
if err := opt(v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func (v *testPlayer) Setup(vnetClientIP string, options ...testWebRTCAPIOptionFunc) error {
|
|
return v.api.Setup(vnetClientIP, options...)
|
|
}
|
|
|
|
func (v *testPlayer) Close() error {
|
|
if v.pc != nil {
|
|
_ = v.pc.Close()
|
|
}
|
|
|
|
for _, receiver := range v.receivers {
|
|
_ = receiver.Stop()
|
|
}
|
|
|
|
if v.api != nil {
|
|
_ = v.api.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *testPlayer) Run(ctx context.Context, cancel context.CancelFunc) error {
|
|
r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream)
|
|
if v.defaultStream != "" {
|
|
r = fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, v.defaultStream)
|
|
}
|
|
if v.streamSuffix != "" {
|
|
r = fmt.Sprintf("%v-%v", r, v.streamSuffix)
|
|
}
|
|
pli := time.Duration(*srsPlayPLI) * time.Millisecond
|
|
logger.Tf(ctx, "Run play url=%v", r)
|
|
|
|
pc, err := v.api.NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Create PC")
|
|
}
|
|
v.pc = pc
|
|
|
|
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{
|
|
Direction: webrtc.RTPTransceiverDirectionRecvonly,
|
|
}); err != nil {
|
|
return errors.Wrapf(err, "add track")
|
|
}
|
|
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{
|
|
Direction: webrtc.RTPTransceiverDirectionRecvonly,
|
|
}); err != nil {
|
|
return errors.Wrapf(err, "add track")
|
|
}
|
|
|
|
offer, err := pc.CreateOffer(nil)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Create Offer")
|
|
}
|
|
|
|
if err := pc.SetLocalDescription(offer); err != nil {
|
|
return errors.Wrapf(err, "Set offer %v", offer)
|
|
}
|
|
|
|
if v.onOffer != nil {
|
|
if err := v.onOffer(&offer); err != nil {
|
|
return errors.Wrapf(err, "sdp %v %v", offer.Type, offer.SDP)
|
|
}
|
|
}
|
|
|
|
answerSDP, err := apiRtcRequest(ctx, "/rtc/v1/play", r, offer.SDP)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Api request offer=%v", offer.SDP)
|
|
}
|
|
|
|
// Run a proxy for real server and vnet.
|
|
if address, err := parseAddressOfCandidate(answerSDP); err != nil {
|
|
return errors.Wrapf(err, "parse address of %v", answerSDP)
|
|
} else if err := v.api.proxy.Proxy(v.api.network, address); err != nil {
|
|
return errors.Wrapf(err, "proxy %v to %v", v.api.network, address)
|
|
}
|
|
|
|
answer := &webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer, SDP: answerSDP,
|
|
}
|
|
if v.onAnswer != nil {
|
|
if err := v.onAnswer(answer); err != nil {
|
|
return errors.Wrapf(err, "on answerSDP")
|
|
}
|
|
}
|
|
|
|
if err := pc.SetRemoteDescription(*answer); err != nil {
|
|
return errors.Wrapf(err, "Set answerSDP %v", answerSDP)
|
|
}
|
|
|
|
handleTrack := func(ctx context.Context, track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) error {
|
|
// Send a PLI on an interval so that the publisher is pushing a keyframe
|
|
go func() {
|
|
if track.Kind() == webrtc.RTPCodecTypeAudio {
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(pli):
|
|
_ = pc.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{
|
|
MediaSSRC: uint32(track.SSRC()),
|
|
}})
|
|
}
|
|
}
|
|
}()
|
|
|
|
v.receivers = append(v.receivers, receiver)
|
|
|
|
for ctx.Err() == nil {
|
|
_, _, err := track.ReadRTP()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Read RTP")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
|
err = handleTrack(ctx, track, receiver)
|
|
if err != nil {
|
|
codec := track.Codec()
|
|
err = errors.Wrapf(err, "Handle track %v, pt=%v", codec.MimeType, codec.PayloadType)
|
|
cancel()
|
|
}
|
|
})
|
|
|
|
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
|
logger.Tf(ctx, "ICE state %v", state)
|
|
})
|
|
|
|
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
logger.Tf(ctx, "PC state %v", state)
|
|
|
|
if state == webrtc.PeerConnectionStateConnected {
|
|
if v.iceReadyCancel != nil {
|
|
v.iceReadyCancel()
|
|
}
|
|
}
|
|
|
|
if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed {
|
|
err = errors.Errorf("Close for PC state %v", state)
|
|
cancel()
|
|
}
|
|
})
|
|
|
|
<-ctx.Done()
|
|
return err
|
|
}
|
|
|
|
type testPublisherOptionFunc func(p *testPublisher) error
|
|
|
|
type testPublisher struct {
|
|
onOffer func(s *webrtc.SessionDescription) error
|
|
onAnswer func(s *webrtc.SessionDescription) error
|
|
iceReadyCancel context.CancelFunc
|
|
// internal objects
|
|
aIngester *audioIngester
|
|
vIngester *videoIngester
|
|
pc *webrtc.PeerConnection
|
|
// We should dispose it.
|
|
api *testWebRTCAPI
|
|
// Optional suffix for stream url.
|
|
streamSuffix string
|
|
// To cancel the publisher, pass by Run.
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// Create test publisher, the init is used to initialize api which maybe nil,
|
|
// and the options is used to setup the publisher itself.
|
|
func newTestPublisher(init testWebRTCAPIInitFunc, options ...testPublisherOptionFunc) (*testPublisher, error) {
|
|
sourceVideo, sourceAudio := *srsPublishVideo, *srsPublishAudio
|
|
|
|
v := &testPublisher{}
|
|
|
|
api, err := newTestWebRTCAPI(init)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v.api = api
|
|
|
|
for _, opt := range options {
|
|
if err := opt(v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Create ingesters.
|
|
if sourceAudio != "" {
|
|
v.aIngester = newAudioIngester(sourceAudio)
|
|
}
|
|
if sourceVideo != "" {
|
|
v.vIngester = newVideoIngester(sourceVideo)
|
|
}
|
|
|
|
// Setup the interceptors for packets.
|
|
api.options = append(api.options, func(api *testWebRTCAPI) {
|
|
// Filter for RTCP packets.
|
|
rtcpInterceptor := &rtcpInterceptor{}
|
|
rtcpInterceptor.rtcpReader = func(buf []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) {
|
|
return rtcpInterceptor.nextRTCPReader.Read(buf, attributes)
|
|
}
|
|
rtcpInterceptor.rtcpWriter = func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) {
|
|
return rtcpInterceptor.nextRTCPWriter.Write(pkts, attributes)
|
|
}
|
|
api.registry.Add(rtcpInterceptor)
|
|
|
|
// Filter for ingesters.
|
|
if sourceAudio != "" {
|
|
api.registry.Add(v.aIngester.audioLevelInterceptor)
|
|
}
|
|
if sourceVideo != "" {
|
|
api.registry.Add(v.vIngester.markerInterceptor)
|
|
}
|
|
})
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func (v *testPublisher) Setup(vnetClientIP string, options ...testWebRTCAPIOptionFunc) error {
|
|
return v.api.Setup(vnetClientIP, options...)
|
|
}
|
|
|
|
func (v *testPublisher) Close() error {
|
|
if v.vIngester != nil {
|
|
_ = v.vIngester.Close()
|
|
}
|
|
|
|
if v.aIngester != nil {
|
|
_ = v.aIngester.Close()
|
|
}
|
|
|
|
if v.pc != nil {
|
|
_ = v.pc.Close()
|
|
}
|
|
|
|
if v.api != nil {
|
|
_ = v.api.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *testPublisher) SetStreamSuffix(suffix string) *testPublisher {
|
|
v.streamSuffix = suffix
|
|
return v
|
|
}
|
|
|
|
func (v *testPublisher) Run(ctx context.Context, cancel context.CancelFunc) error {
|
|
// Save the cancel.
|
|
v.cancel = cancel
|
|
|
|
r := fmt.Sprintf("%v://%v%v", srsSchema, *srsServer, *srsStream)
|
|
if v.streamSuffix != "" {
|
|
r = fmt.Sprintf("%v-%v", r, v.streamSuffix)
|
|
}
|
|
sourceVideo, sourceAudio, fps := *srsPublishVideo, *srsPublishAudio, *srsPublishVideoFps
|
|
|
|
logger.Tf(ctx, "Run publish url=%v, audio=%v, video=%v, fps=%v",
|
|
r, sourceAudio, sourceVideo, fps)
|
|
|
|
pc, err := v.api.NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Create PC")
|
|
}
|
|
v.pc = pc
|
|
|
|
if v.vIngester != nil {
|
|
if err := v.vIngester.AddTrack(pc, fps); err != nil {
|
|
return errors.Wrapf(err, "Add track")
|
|
}
|
|
}
|
|
|
|
if v.aIngester != nil {
|
|
if err := v.aIngester.AddTrack(pc); err != nil {
|
|
return errors.Wrapf(err, "Add track")
|
|
}
|
|
}
|
|
|
|
offer, err := pc.CreateOffer(nil)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Create Offer")
|
|
}
|
|
|
|
if err := pc.SetLocalDescription(offer); err != nil {
|
|
return errors.Wrapf(err, "Set offer %v", offer)
|
|
}
|
|
|
|
if v.onOffer != nil {
|
|
if err := v.onOffer(&offer); err != nil {
|
|
return errors.Wrapf(err, "sdp %v %v", offer.Type, offer.SDP)
|
|
}
|
|
}
|
|
|
|
answerSDP, err := apiRtcRequest(ctx, "/rtc/v1/publish", r, offer.SDP)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Api request offer=%v", offer.SDP)
|
|
}
|
|
|
|
// Run a proxy for real server and vnet.
|
|
if address, err := parseAddressOfCandidate(answerSDP); err != nil {
|
|
return errors.Wrapf(err, "parse address of %v", answerSDP)
|
|
} else if err := v.api.proxy.Proxy(v.api.network, address); err != nil {
|
|
return errors.Wrapf(err, "proxy %v to %v", v.api.network, address)
|
|
}
|
|
|
|
answer := &webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer, SDP: answerSDP,
|
|
}
|
|
if v.onAnswer != nil {
|
|
if err := v.onAnswer(answer); err != nil {
|
|
return errors.Wrapf(err, "on answerSDP")
|
|
}
|
|
}
|
|
|
|
if err := pc.SetRemoteDescription(*answer); err != nil {
|
|
return errors.Wrapf(err, "Set answerSDP %v", answerSDP)
|
|
}
|
|
|
|
logger.Tf(ctx, "State signaling=%v, ice=%v, conn=%v", pc.SignalingState(), pc.ICEConnectionState(), pc.ConnectionState())
|
|
|
|
// ICE state management.
|
|
pc.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) {
|
|
logger.Tf(ctx, "ICE gather state %v", state)
|
|
})
|
|
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
|
logger.Tf(ctx, "ICE candidate %v %v:%v", candidate.Protocol, candidate.Address, candidate.Port)
|
|
|
|
})
|
|
pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
|
logger.Tf(ctx, "ICE state %v", state)
|
|
})
|
|
|
|
pc.OnSignalingStateChange(func(state webrtc.SignalingState) {
|
|
logger.Tf(ctx, "Signaling state %v", state)
|
|
})
|
|
|
|
if v.aIngester != nil {
|
|
v.aIngester.sAudioSender.Transport().OnStateChange(func(state webrtc.DTLSTransportState) {
|
|
logger.Tf(ctx, "DTLS state %v", state)
|
|
})
|
|
}
|
|
|
|
pcDone, pcDoneCancel := context.WithCancel(context.Background())
|
|
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
|
logger.Tf(ctx, "PC state %v", state)
|
|
|
|
if state == webrtc.PeerConnectionStateConnected {
|
|
pcDoneCancel()
|
|
if v.iceReadyCancel != nil {
|
|
v.iceReadyCancel()
|
|
}
|
|
}
|
|
|
|
if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed {
|
|
err = errors.Errorf("Close for PC state %v", state)
|
|
cancel()
|
|
}
|
|
})
|
|
|
|
// Wait for event from context or tracks.
|
|
var wg sync.WaitGroup
|
|
var finalErr error
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer logger.Tf(ctx, "ingest notify done")
|
|
|
|
<-ctx.Done()
|
|
|
|
if v.aIngester != nil && v.aIngester.sAudioSender != nil {
|
|
// We MUST wait for the ingester ready(or closed), because it might crash if sender is disposed.
|
|
<-v.aIngester.ready.Done()
|
|
|
|
_ = v.aIngester.Close()
|
|
}
|
|
|
|
if v.vIngester != nil && v.vIngester.sVideoSender != nil {
|
|
// We MUST wait for the ingester ready(or closed), because it might crash if sender is disposed.
|
|
<-v.vIngester.ready.Done()
|
|
|
|
_ = v.vIngester.Close()
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer cancel()
|
|
|
|
if v.aIngester == nil {
|
|
return
|
|
}
|
|
defer v.aIngester.readyCancel()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-pcDone.Done():
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer logger.Tf(ctx, "aingester sender read done")
|
|
|
|
buf := make([]byte, 1500)
|
|
for ctx.Err() == nil {
|
|
if _, _, err := v.aIngester.sAudioSender.Read(buf); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
if err := v.aIngester.Ingest(ctx); err != nil {
|
|
if err == io.EOF {
|
|
logger.Tf(ctx, "aingester retry for %v", err)
|
|
continue
|
|
}
|
|
if err != context.Canceled {
|
|
finalErr = errors.Wrapf(err, "audio")
|
|
}
|
|
|
|
logger.Tf(ctx, "aingester err=%v, final=%v", err, finalErr)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer cancel()
|
|
|
|
if v.vIngester == nil {
|
|
return
|
|
}
|
|
defer v.vIngester.readyCancel()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-pcDone.Done():
|
|
logger.Tf(ctx, "PC(ICE+DTLS+SRTP) done, start ingest video %v", sourceVideo)
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer logger.Tf(ctx, "vingester sender read done")
|
|
|
|
buf := make([]byte, 1500)
|
|
for ctx.Err() == nil {
|
|
// The Read() might block in r.rtcpInterceptor.Read(b, a),
|
|
// so that the Stop() can not stop it.
|
|
if _, _, err := v.vIngester.sVideoSender.Read(buf); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
for {
|
|
if err := v.vIngester.Ingest(ctx); err != nil {
|
|
if err == io.EOF {
|
|
logger.Tf(ctx, "vingester retry for %v", err)
|
|
continue
|
|
}
|
|
if err != context.Canceled {
|
|
finalErr = errors.Wrapf(err, "video")
|
|
}
|
|
|
|
logger.Tf(ctx, "vingester err=%v, final=%v", err, finalErr)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
logger.Tf(ctx, "ingester done ctx=%v, final=%v", ctx.Err(), finalErr)
|
|
if finalErr != nil {
|
|
return finalErr
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
type RTMPClient struct {
|
|
rtmpUrl string
|
|
|
|
rtmpTcUrl string
|
|
rtmpStream string
|
|
rtmpUrlObject *url.URL
|
|
|
|
streamID int
|
|
|
|
conn *net.TCPConn
|
|
proto *rtmp.Protocol
|
|
}
|
|
|
|
func (v *RTMPClient) Close() error {
|
|
if v.conn != nil {
|
|
v.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *RTMPClient) connect(rtmpUrl string) error {
|
|
v.rtmpUrl = rtmpUrl
|
|
|
|
if index := strings.LastIndex(rtmpUrl, "/"); index <= 0 {
|
|
return fmt.Errorf("invalid url %v, index=%v", rtmpUrl, index)
|
|
} else {
|
|
v.rtmpTcUrl = rtmpUrl[0:index]
|
|
v.rtmpStream = rtmpUrl[index+1:]
|
|
}
|
|
|
|
// Parse RTMP url.
|
|
rtmpUrlObject, err := url.Parse(rtmpUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.rtmpUrlObject = rtmpUrlObject
|
|
|
|
port := rtmpUrlObject.Port()
|
|
if port == "" {
|
|
port = "1935"
|
|
}
|
|
|
|
// Connect to TCP server.
|
|
rtmpAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%v:%v", rtmpUrlObject.Hostname(), port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := net.DialTCP("tcp4", nil, rtmpAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.conn = c
|
|
|
|
// RTMP Handshake with server.
|
|
hs := rtmp.NewHandshake(rand.New(rand.NewSource(time.Now().UnixNano())))
|
|
if err := hs.WriteC0S0(c); err != nil {
|
|
return err
|
|
}
|
|
if err := hs.WriteC1S1(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := hs.ReadC0S0(c); err != nil {
|
|
return err
|
|
}
|
|
s1, err := hs.ReadC1S1(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := hs.ReadC2S2(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := hs.WriteC2S2(c, s1); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Connect to RTMP tcUrl.
|
|
p := rtmp.NewProtocol(v.conn)
|
|
|
|
pkt := rtmp.NewConnectAppPacket()
|
|
pkt.CommandObject.Set("tcUrl", amf0.NewString(v.rtmpTcUrl))
|
|
if err = p.WritePacket(pkt, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
res := rtmp.NewConnectAppResPacket(pkt.TransactionID)
|
|
if _, err := p.ExpectPacket(&res); err != nil {
|
|
return err
|
|
}
|
|
v.proto = p
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *RTMPClient) Publish(ctx context.Context, rtmpUrl string) error {
|
|
if err := v.connect(rtmpUrl); err != nil {
|
|
return err
|
|
}
|
|
p := v.proto
|
|
|
|
// Create RTMP stream.
|
|
if true {
|
|
pkt := rtmp.NewCreateStreamPacket()
|
|
if err := p.WritePacket(pkt, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
res := rtmp.NewCreateStreamResPacket(pkt.TransactionID)
|
|
if _, err := p.ExpectPacket(&res); err != nil {
|
|
return err
|
|
}
|
|
v.streamID = int(res.StreamID)
|
|
}
|
|
|
|
// Publish RTMP stream.
|
|
if true {
|
|
pkt := rtmp.NewPublishPacket()
|
|
pkt.StreamName = *amf0.NewString(v.rtmpStream)
|
|
if err := p.WritePacket(pkt, v.streamID); err != nil {
|
|
return err
|
|
}
|
|
|
|
res := rtmp.NewCallPacket()
|
|
if _, err := p.ExpectPacket(&res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *RTMPClient) Play(ctx context.Context, rtmpUrl string) error {
|
|
if err := v.connect(rtmpUrl); err != nil {
|
|
return err
|
|
}
|
|
p := v.proto
|
|
|
|
// Create RTMP stream.
|
|
if true {
|
|
pkt := rtmp.NewCreateStreamPacket()
|
|
if err := p.WritePacket(pkt, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
res := rtmp.NewCreateStreamResPacket(pkt.TransactionID)
|
|
if _, err := p.ExpectPacket(&res); err != nil {
|
|
return err
|
|
}
|
|
v.streamID = int(res.StreamID)
|
|
}
|
|
|
|
// Play RTMP stream.
|
|
if true {
|
|
pkt := rtmp.NewPlayPacket()
|
|
pkt.StreamName = *amf0.NewString(v.rtmpStream)
|
|
if err := p.WritePacket(pkt, v.streamID); err != nil {
|
|
return err
|
|
}
|
|
|
|
res := rtmp.NewCallPacket()
|
|
if _, err := p.ExpectPacket(&res); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type RTMPPublisher struct {
|
|
client *RTMPClient
|
|
// Whether auto close transport when ingest done.
|
|
closeTransportWhenIngestDone bool
|
|
|
|
onSendPacket func(m *rtmp.Message) error
|
|
}
|
|
|
|
func NewRTMPPublisher() *RTMPPublisher {
|
|
v := &RTMPPublisher{
|
|
client: &RTMPClient{},
|
|
}
|
|
|
|
// By default, set to on.
|
|
v.closeTransportWhenIngestDone = true
|
|
|
|
return v
|
|
}
|
|
|
|
func (v *RTMPPublisher) Close() error {
|
|
return v.client.Close()
|
|
}
|
|
|
|
func (v *RTMPPublisher) Publish(ctx context.Context, rtmpUrl string) error {
|
|
return v.client.Publish(ctx, rtmpUrl)
|
|
}
|
|
|
|
func (v *RTMPPublisher) Ingest(ctx context.Context, flvInput string) error {
|
|
// If ctx is cancelled, close the RTMP transport.
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-ctx.Done()
|
|
if v.closeTransportWhenIngestDone {
|
|
v.Close()
|
|
}
|
|
}()
|
|
|
|
// Consume all packets.
|
|
err := v.ingest(flvInput)
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if ctx.Err() == context.Canceled {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (v *RTMPPublisher) ingest(flvInput string) error {
|
|
p := v.client
|
|
|
|
fs, err := os.Open(flvInput)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.Close()
|
|
|
|
demuxer, err := flv.NewDemuxer(fs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, _, _, err = demuxer.ReadHeader(); err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
tagType, tagSize, timestamp, err := demuxer.ReadTagHeader()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tag, err := demuxer.ReadTag(tagSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if tagType != flv.TagTypeVideo && tagType != flv.TagTypeAudio {
|
|
continue
|
|
}
|
|
|
|
m := rtmp.NewStreamMessage(p.streamID)
|
|
m.MessageType = rtmp.MessageType(tagType)
|
|
m.Timestamp = uint64(timestamp)
|
|
m.Payload = tag
|
|
if err = p.proto.WriteMessage(m); err != nil {
|
|
return err
|
|
}
|
|
|
|
if v.onSendPacket != nil {
|
|
if err = v.onSendPacket(m); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type RTMPPlayer struct {
|
|
// Transport.
|
|
client *RTMPClient
|
|
// FLV packager.
|
|
videoPackager flv.VideoPackager
|
|
|
|
onRecvPacket func(m *rtmp.Message, a *flv.AudioFrame, v *flv.VideoFrame) error
|
|
}
|
|
|
|
func NewRTMPPlayer() *RTMPPlayer {
|
|
return &RTMPPlayer{
|
|
client: &RTMPClient{},
|
|
}
|
|
}
|
|
|
|
func (v *RTMPPlayer) Close() error {
|
|
return v.client.Close()
|
|
}
|
|
|
|
func (v *RTMPPlayer) Play(ctx context.Context, rtmpUrl string) error {
|
|
var err error
|
|
if v.videoPackager, err = flv.NewVideoPackager(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return v.client.Play(ctx, rtmpUrl)
|
|
}
|
|
|
|
func (v *RTMPPlayer) Consume(ctx context.Context) error {
|
|
// If ctx is cancelled, close the RTMP transport.
|
|
var wg sync.WaitGroup
|
|
defer wg.Wait()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-ctx.Done()
|
|
v.Close()
|
|
}()
|
|
|
|
// Consume all packets.
|
|
err := v.consume()
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if ctx.Err() == context.Canceled {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (v *RTMPPlayer) consume() error {
|
|
for {
|
|
res, err := v.client.proto.ExpectMessage(rtmp.MessageTypeVideo, rtmp.MessageTypeAudio)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if v.onRecvPacket != nil {
|
|
var audioFrame *flv.AudioFrame
|
|
var videoFrame *flv.VideoFrame
|
|
if res.MessageType == rtmp.MessageTypeVideo {
|
|
if videoFrame, err = v.videoPackager.Decode(res.Payload); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := v.onRecvPacket(res, audioFrame, videoFrame); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func IsAvccrEquals(a, b *avc.AVCDecoderConfigurationRecord) bool {
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
|
|
if a.AVCLevelIndication != b.AVCLevelIndication ||
|
|
a.AVCProfileIndication != b.AVCProfileIndication ||
|
|
a.LengthSizeMinusOne != b.LengthSizeMinusOne ||
|
|
len(a.SequenceParameterSetNALUnits) != len(b.SequenceParameterSetNALUnits) ||
|
|
len(a.PictureParameterSetNALUnits) != len(b.PictureParameterSetNALUnits) {
|
|
return false
|
|
}
|
|
|
|
for i := 0; i < len(a.SequenceParameterSetNALUnits); i++ {
|
|
if !IsNALUEquals(a.SequenceParameterSetNALUnits[i], b.SequenceParameterSetNALUnits[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(a.PictureParameterSetNALUnits); i++ {
|
|
if !IsNALUEquals(a.PictureParameterSetNALUnits[i], b.PictureParameterSetNALUnits[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsNALUEquals(a, b *avc.NALU) bool {
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
|
|
if a.NALUType != b.NALUType || a.NALRefIDC != b.NALRefIDC {
|
|
return false
|
|
}
|
|
|
|
return bytes.Equal(a.Data, b.Data)
|
|
}
|
|
|
|
func DemuxRtpSpsPps(payload []byte) ([]byte, []*avc.NALU, error) {
|
|
// Parse RTP packet.
|
|
pkt := rtp.Packet{}
|
|
if err := pkt.Unmarshal(payload); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Decode H264 packet.
|
|
h264Packet := codecs.H264Packet{}
|
|
annexb, err := h264Packet.Unmarshal(pkt.Payload)
|
|
if err != nil {
|
|
return annexb, nil, err
|
|
}
|
|
|
|
// Ignore if not STAP-A
|
|
if !bytes.HasPrefix(annexb, []byte{0x00, 0x00, 0x00, 0x01}) {
|
|
return annexb, nil, err
|
|
}
|
|
|
|
// Parse to NALUs
|
|
rawNalus := bytes.Split(annexb, []byte{0x00, 0x00, 0x00, 0x01})
|
|
|
|
nalus := []*avc.NALU{}
|
|
for _, rawNalu := range rawNalus {
|
|
if len(rawNalu) == 0 {
|
|
continue
|
|
}
|
|
|
|
nalu := avc.NewNALU()
|
|
if err := nalu.UnmarshalBinary(rawNalu); err != nil {
|
|
return annexb, nil, err
|
|
}
|
|
|
|
nalus = append(nalus, nalu)
|
|
}
|
|
|
|
return annexb, nalus, nil
|
|
}
|