Initial commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package fake
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
Medias []*streamer.Media
|
||||
Tracks []*streamer.Track
|
||||
|
||||
RecvPackets int
|
||||
SendPackets int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
switch track.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
track = track.Bind(func(packet *rtp.Packet) error {
|
||||
if track.Codec.PayloadType != packet.PayloadType {
|
||||
panic("wrong payload type")
|
||||
}
|
||||
c.RecvPackets++
|
||||
return nil
|
||||
})
|
||||
case streamer.DirectionRecvonly:
|
||||
go func() {
|
||||
for {
|
||||
pkt := &rtp.Packet{}
|
||||
pkt.PayloadType = track.Codec.PayloadType
|
||||
if err := track.WriteRTP(pkt); err != nil {
|
||||
return
|
||||
}
|
||||
c.SendPackets++
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
c.Tracks = append(c.Tracks, track)
|
||||
return track
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fake
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
streamer.Element
|
||||
Medias []*streamer.Media
|
||||
Tracks []*streamer.Track
|
||||
|
||||
RecvPackets int
|
||||
SendPackets int
|
||||
}
|
||||
|
||||
func (p *Producer) GetMedias() []*streamer.Media {
|
||||
return p.Medias
|
||||
}
|
||||
|
||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
if !streamer.Contains(p.Medias, media, codec) {
|
||||
panic("you shall not pass!")
|
||||
}
|
||||
|
||||
track := &streamer.Track{Codec: codec, Direction: media.Direction}
|
||||
|
||||
switch media.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
track2 := track.Bind(func(packet *rtp.Packet) error {
|
||||
p.RecvPackets++
|
||||
return nil
|
||||
})
|
||||
p.Tracks = append(p.Tracks, track2)
|
||||
case streamer.DirectionRecvonly:
|
||||
p.Tracks = append(p.Tracks, track)
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
func (p *Producer) Start() error {
|
||||
for {
|
||||
for _, track := range p.Tracks {
|
||||
if track.Direction != streamer.DirectionSendonly {
|
||||
continue
|
||||
}
|
||||
pkt := &rtp.Packet{}
|
||||
pkt.PayloadType = track.Codec.PayloadType
|
||||
if err := track.WriteRTP(pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
p.SendPackets++
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Producer) Stop() error {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
## WebRTC
|
||||
|
||||
Video codec | Media string | Device
|
||||
----------------|--------------|-------
|
||||
H.264/baseline! | avc1.42E0xx | Chromecast
|
||||
H.264/baseline! | avc1.42E0xx | Chrome/Safari WebRTC
|
||||
H.264/baseline! | avc1.42C0xx | FFmpeg ultrafast
|
||||
H.264/baseline! | avc1.4240xx | Dahua H264B
|
||||
H.264/baseline | avc1.4200xx | Chrome WebRTC
|
||||
H.264/main! | avc1.4D40xx | Chromecast
|
||||
H.264/main! | avc1.4D40xx | FFmpeg superfast main
|
||||
H.264/main! | avc1.4D40xx | Dahua H264
|
||||
H.264/main | avc1.4D00xx | Chrome WebRTC
|
||||
H.264/high! | avc1.640Cxx | Safari WebRTC
|
||||
H.264/high | avc1.6400xx | Chromecast
|
||||
H.264/high | avc1.6400xx | FFmpeg superfast
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)
|
||||
- [The H264 Sequence parameter set](https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set)
|
||||
- [H.264 Video Types (Microsoft)](https://docs.microsoft.com/en-us/windows/win32/directshow/h-264-video-types)
|
||||
- [Automatic Generation of H.264 Parameter Sets to Recover Video File Fragments](https://arxiv.org/pdf/2104.14522.pdf)
|
||||
- [Chromium sources](https://chromium.googlesource.com/external/webrtc/+/HEAD/common_video/h264)
|
||||
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
|
||||
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
|
||||
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)
|
||||
@@ -0,0 +1,87 @@
|
||||
package golomb
|
||||
|
||||
import "bytes"
|
||||
|
||||
type Reader struct {
|
||||
r *bytes.Reader
|
||||
b byte
|
||||
shift byte
|
||||
}
|
||||
|
||||
func NewReader(b []byte) *Reader {
|
||||
return &Reader{
|
||||
r: bytes.NewReader(b),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Reader) ReadBit() (b byte, err error) {
|
||||
if g.shift == 0 {
|
||||
if g.b, err = g.r.ReadByte(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
g.shift = 7
|
||||
} else {
|
||||
g.shift--
|
||||
}
|
||||
b = (g.b >> g.shift) & 0b1
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Reader) ReadBits(n byte) (res uint, err error) {
|
||||
var b byte
|
||||
for i := n - 1; i != 255; i-- {
|
||||
if b, err = g.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
res |= uint(b) << i
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Reader) ReadUEGolomb() (res uint, err error) {
|
||||
var b uint
|
||||
var i byte
|
||||
for i = 0; i < 32; i++ {
|
||||
if b, err = g.ReadBits(1); err != nil {
|
||||
return
|
||||
}
|
||||
if b != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if res, err = g.ReadBits(i); err != nil {
|
||||
return
|
||||
}
|
||||
res += (1 << i) - 1
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Reader) ReadSEGolomb() (res int, err error) {
|
||||
var b uint
|
||||
if b, err = g.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
if b%2 == 0 {
|
||||
res = -int(b >> 1)
|
||||
} else {
|
||||
res = int(b>>1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *Reader) ReadByte() (byte, error) {
|
||||
return g.r.ReadByte()
|
||||
}
|
||||
|
||||
func (g *Reader) End() bool {
|
||||
// if only one bit in next byte left
|
||||
if g.shift == 0 && g.r.Len() == 1 {
|
||||
b, _ := g.r.ReadByte()
|
||||
_ = g.r.UnreadByte()
|
||||
return b == 0x80
|
||||
}
|
||||
if g.r.Len() == 0 {
|
||||
//panic("not implemented")
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package golomb
|
||||
|
||||
import "math/bits"
|
||||
|
||||
type Writer struct {
|
||||
buf []byte
|
||||
b byte // last byte
|
||||
i int // last byte index
|
||||
shift byte
|
||||
}
|
||||
|
||||
func NewWriter() *Writer {
|
||||
return &Writer{i: -1}
|
||||
}
|
||||
|
||||
func (g *Writer) WriteBit(b byte) {
|
||||
if g.shift == 0 {
|
||||
g.buf = append(g.buf, 0)
|
||||
g.b = 0
|
||||
g.i++
|
||||
g.shift = 7
|
||||
} else {
|
||||
g.shift--
|
||||
}
|
||||
g.b |= b << g.shift
|
||||
g.buf[g.i] = g.b
|
||||
}
|
||||
|
||||
func (g *Writer) WriteBits(b, n byte) {
|
||||
for i := n - 1; i != 255; i-- {
|
||||
g.WriteBit((b >> i) & 0b1)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Writer) WriteByte(b byte) {
|
||||
g.buf = append(g.buf, b)
|
||||
g.i++
|
||||
}
|
||||
|
||||
func (g *Writer) WriteUEGolomb(b byte) {
|
||||
b++
|
||||
n := uint8(bits.Len8(b))*2 - 1
|
||||
g.WriteBits(b, n)
|
||||
}
|
||||
|
||||
func (g *Writer) WriteSEGolomb(b int8) {
|
||||
if b > 0 {
|
||||
g.WriteUEGolomb(byte(b)*2 - 1)
|
||||
} else {
|
||||
g.WriteUEGolomb(byte(-b) * 2)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Writer) Bytes() []byte {
|
||||
return g.buf
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package h264
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
NALUTypePFrame = 1
|
||||
NALUTypeIFrame = 5
|
||||
NALUTypeSPS = 7
|
||||
NALUTypePPS = 8
|
||||
|
||||
PayloadTypeAVC = 255
|
||||
)
|
||||
|
||||
func NALUType(b []byte) byte {
|
||||
return b[4] & 0x1F
|
||||
}
|
||||
|
||||
func EncodeAVC(raw []byte) (avc []byte) {
|
||||
avc = make([]byte, len(raw)+4)
|
||||
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
|
||||
copy(avc[4:], raw)
|
||||
return
|
||||
}
|
||||
|
||||
func IsAVC(codec *streamer.Codec) bool {
|
||||
return codec.PayloadType == PayloadTypeAVC
|
||||
}
|
||||
|
||||
func GetParameterSet(fmtp string) (sps, pps []byte) {
|
||||
if fmtp == "" {
|
||||
return
|
||||
}
|
||||
|
||||
s := streamer.Between(fmtp, "sprop-parameter-sets=", ";")
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
i := strings.IndexByte(s, ',')
|
||||
if i < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sps, _ = base64.StdEncoding.DecodeString(s[:i])
|
||||
pps, _ = base64.StdEncoding.DecodeString(s[i+1:])
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package h264
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// Payloader payloads H264 packets
|
||||
type Payloader struct {
|
||||
IsAVC bool
|
||||
spsNalu, ppsNalu []byte
|
||||
}
|
||||
|
||||
const (
|
||||
stapaNALUType = 24
|
||||
fuaNALUType = 28
|
||||
fubNALUType = 29
|
||||
spsNALUType = 7
|
||||
ppsNALUType = 8
|
||||
audNALUType = 9
|
||||
fillerNALUType = 12
|
||||
|
||||
fuaHeaderSize = 2
|
||||
//stapaHeaderSize = 1
|
||||
//stapaNALULengthSize = 2
|
||||
|
||||
naluTypeBitmask = 0x1F
|
||||
naluRefIdcBitmask = 0x60
|
||||
//fuStartBitmask = 0x80
|
||||
//fuEndBitmask = 0x40
|
||||
|
||||
outputStapAHeader = 0x78
|
||||
)
|
||||
|
||||
//func annexbNALUStartCode() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
|
||||
|
||||
func emitNalus(nals []byte, isAVC bool, emit func([]byte)) {
|
||||
if !isAVC {
|
||||
nextInd := func(nalu []byte, start int) (indStart int, indLen int) {
|
||||
zeroCount := 0
|
||||
|
||||
for i, b := range nalu[start:] {
|
||||
if b == 0 {
|
||||
zeroCount++
|
||||
continue
|
||||
} else if b == 1 {
|
||||
if zeroCount >= 2 {
|
||||
return start + i - zeroCount, zeroCount + 1
|
||||
}
|
||||
}
|
||||
zeroCount = 0
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
nextIndStart, nextIndLen := nextInd(nals, 0)
|
||||
if nextIndStart == -1 {
|
||||
emit(nals)
|
||||
} else {
|
||||
for nextIndStart != -1 {
|
||||
prevStart := nextIndStart + nextIndLen
|
||||
nextIndStart, nextIndLen = nextInd(nals, prevStart)
|
||||
if nextIndStart != -1 {
|
||||
emit(nals[prevStart:nextIndStart])
|
||||
} else {
|
||||
// Emit until end of stream, no end indicator found
|
||||
emit(nals[prevStart:])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
end := 4 + binary.BigEndian.Uint32(nals)
|
||||
emit(nals[4:end])
|
||||
if int(end) >= len(nals) {
|
||||
break
|
||||
}
|
||||
nals = nals[end:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Payload fragments a H264 packet across one or more byte arrays
|
||||
func (p *Payloader) Payload(mtu uint16, payload []byte) [][]byte {
|
||||
var payloads [][]byte
|
||||
if len(payload) == 0 {
|
||||
return payloads
|
||||
}
|
||||
|
||||
emitNalus(payload, p.IsAVC, func(nalu []byte) {
|
||||
if len(nalu) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
naluType := nalu[0] & naluTypeBitmask
|
||||
naluRefIdc := nalu[0] & naluRefIdcBitmask
|
||||
|
||||
switch {
|
||||
case naluType == audNALUType || naluType == fillerNALUType:
|
||||
return
|
||||
case naluType == spsNALUType:
|
||||
p.spsNalu = nalu
|
||||
return
|
||||
case naluType == ppsNALUType:
|
||||
p.ppsNalu = nalu
|
||||
return
|
||||
case p.spsNalu != nil && p.ppsNalu != nil:
|
||||
// Pack current NALU with SPS and PPS as STAP-A
|
||||
spsLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(spsLen, uint16(len(p.spsNalu)))
|
||||
|
||||
ppsLen := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(ppsLen, uint16(len(p.ppsNalu)))
|
||||
|
||||
stapANalu := []byte{outputStapAHeader}
|
||||
stapANalu = append(stapANalu, spsLen...)
|
||||
stapANalu = append(stapANalu, p.spsNalu...)
|
||||
stapANalu = append(stapANalu, ppsLen...)
|
||||
stapANalu = append(stapANalu, p.ppsNalu...)
|
||||
if len(stapANalu) <= int(mtu) {
|
||||
out := make([]byte, len(stapANalu))
|
||||
copy(out, stapANalu)
|
||||
payloads = append(payloads, out)
|
||||
}
|
||||
|
||||
p.spsNalu = nil
|
||||
p.ppsNalu = nil
|
||||
}
|
||||
|
||||
// Single NALU
|
||||
if len(nalu) <= int(mtu) {
|
||||
out := make([]byte, len(nalu))
|
||||
copy(out, nalu)
|
||||
payloads = append(payloads, out)
|
||||
return
|
||||
}
|
||||
|
||||
// FU-A
|
||||
maxFragmentSize := int(mtu) - fuaHeaderSize
|
||||
|
||||
// The FU payload consists of fragments of the payload of the fragmented
|
||||
// NAL unit so that if the fragmentation unit payloads of consecutive
|
||||
// FUs are sequentially concatenated, the payload of the fragmented NAL
|
||||
// unit can be reconstructed. The NAL unit type octet of the fragmented
|
||||
// NAL unit is not included as such in the fragmentation unit payload,
|
||||
// but rather the information of the NAL unit type octet of the
|
||||
// fragmented NAL unit is conveyed in the F and NRI fields of the FU
|
||||
// indicator octet of the fragmentation unit and in the type field of
|
||||
// the FU header. An FU payload MAY have any number of octets and MAY
|
||||
// be empty.
|
||||
|
||||
naluData := nalu
|
||||
// According to the RFC, the first octet is skipped due to redundant information
|
||||
naluDataIndex := 1
|
||||
naluDataLength := len(nalu) - naluDataIndex
|
||||
naluDataRemaining := naluDataLength
|
||||
|
||||
if min(maxFragmentSize, naluDataRemaining) <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for naluDataRemaining > 0 {
|
||||
currentFragmentSize := min(maxFragmentSize, naluDataRemaining)
|
||||
out := make([]byte, fuaHeaderSize+currentFragmentSize)
|
||||
|
||||
// +---------------+
|
||||
// |0|1|2|3|4|5|6|7|
|
||||
// +-+-+-+-+-+-+-+-+
|
||||
// |F|NRI| Type |
|
||||
// +---------------+
|
||||
out[0] = fuaNALUType
|
||||
out[0] |= naluRefIdc
|
||||
|
||||
// +---------------+
|
||||
// |0|1|2|3|4|5|6|7|
|
||||
// +-+-+-+-+-+-+-+-+
|
||||
// |S|E|R| Type |
|
||||
// +---------------+
|
||||
|
||||
out[1] = naluType
|
||||
if naluDataRemaining == naluDataLength {
|
||||
// Set start bit
|
||||
out[1] |= 1 << 7
|
||||
} else if naluDataRemaining-currentFragmentSize == 0 {
|
||||
// Set end bit
|
||||
out[1] |= 1 << 6
|
||||
}
|
||||
|
||||
copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize])
|
||||
payloads = append(payloads, out)
|
||||
|
||||
naluDataRemaining -= currentFragmentSize
|
||||
naluDataIndex += currentFragmentSize
|
||||
}
|
||||
})
|
||||
|
||||
return payloads
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
|
||||
)
|
||||
|
||||
const PPSHeader = 0x68
|
||||
|
||||
// https://www.itu.int/rec/T-REC-H.264
|
||||
// 7.3.2.2 Picture parameter set RBSP syntax
|
||||
|
||||
type PPS struct{}
|
||||
|
||||
func (p *PPS) Marshal() []byte {
|
||||
w := golomb.NewWriter()
|
||||
|
||||
// this is typical PPS for most H264 cameras
|
||||
w.WriteByte(PPSHeader)
|
||||
w.WriteUEGolomb(0) // pic_parameter_set_id
|
||||
w.WriteUEGolomb(0) // seq_parameter_set_id
|
||||
w.WriteBit(1) // entropy_coding_mode_flag
|
||||
w.WriteBit(0) // bottom_field_pic_order_in_frame_present_flag
|
||||
w.WriteUEGolomb(0) // num_slice_groups_minus1
|
||||
w.WriteUEGolomb(0) // num_ref_idx_l0_default_active_minus1
|
||||
w.WriteUEGolomb(0) // num_ref_idx_l1_default_active_minus1
|
||||
w.WriteBit(0) // weighted_pred_flag
|
||||
w.WriteBits(0, 2) // weighted_bipred_idc
|
||||
w.WriteSEGolomb(0) // pic_init_qp_minus26
|
||||
w.WriteSEGolomb(0) // pic_init_qs_minus26
|
||||
w.WriteSEGolomb(0) // chroma_qp_index_offset
|
||||
w.WriteBit(1) // deblocking_filter_control_present_flag
|
||||
w.WriteBit(0) // constrained_intra_pred_flag
|
||||
w.WriteBit(0) // redundant_pic_cnt_present_flag
|
||||
|
||||
w.WriteBit(1) // rbsp_trailing_bits()
|
||||
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
func (p *PPS) Unmarshal(data []byte) (err error) {
|
||||
r := golomb.NewReader(data)
|
||||
|
||||
var b byte
|
||||
var u uint
|
||||
|
||||
if b, err = r.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if b&0x1F != 8 {
|
||||
err = errors.New("not PPS data")
|
||||
return
|
||||
}
|
||||
|
||||
// pic_parameter_set_id
|
||||
if u, err = r.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// seq_parameter_set_id
|
||||
if u, err = r.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// entropy_coding_mode_flag
|
||||
if b, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
// bottom_field_pic_order_in_frame_present_flag
|
||||
if b, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// num_slice_groups_minus1
|
||||
if u, err = r.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
if u > 0 {
|
||||
//panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
// num_ref_idx_l0_default_active_minus1
|
||||
if _, err = r.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// num_ref_idx_l1_default_active_minus1
|
||||
if _, err = r.ReadUEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// weighted_pred_flag
|
||||
if _, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
// weighted_bipred_idc
|
||||
if _, err = r.ReadBits(2); err != nil {
|
||||
return
|
||||
}
|
||||
// pic_init_qp_minus26
|
||||
if _, err = r.ReadSEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// pic_init_qs_minus26
|
||||
if _, err = r.ReadSEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// chroma_qp_index_offset
|
||||
if _, err = r.ReadSEGolomb(); err != nil {
|
||||
return
|
||||
}
|
||||
// deblocking_filter_control_present_flag
|
||||
if _, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
// constrained_intra_pred_flag
|
||||
if _, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
// redundant_pic_cnt_present_flag
|
||||
if _, err = r.ReadBit(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !r.End() {
|
||||
//panic("not implemented")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
|
||||
)
|
||||
|
||||
const firstByte = 0x67
|
||||
|
||||
// Google to "h264 specification pdf"
|
||||
// https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201602-S!!PDF-E&type=items
|
||||
|
||||
type SPS struct {
|
||||
Profile string
|
||||
ProfileIDC uint8
|
||||
ProfileIOP uint8
|
||||
LevelIDC uint8
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
func NewSPS(profile string, level uint8, width uint16, height uint16) *SPS {
|
||||
s := &SPS{
|
||||
Profile: profile, LevelIDC: level, Width: width, Height: height,
|
||||
}
|
||||
s.ProfileIDC, s.ProfileIOP = DecodeProfile(profile)
|
||||
return s
|
||||
}
|
||||
|
||||
// https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set
|
||||
|
||||
func (s *SPS) Marshal() []byte {
|
||||
w := golomb.NewWriter()
|
||||
|
||||
// this is typical SPS for most H264 cameras
|
||||
w.WriteByte(firstByte)
|
||||
w.WriteByte(s.ProfileIDC)
|
||||
w.WriteByte(s.ProfileIOP)
|
||||
w.WriteByte(s.LevelIDC)
|
||||
|
||||
w.WriteUEGolomb(0) // seq_parameter_set_id (0)
|
||||
w.WriteUEGolomb(0) // log2_max_frame_num_minus4 (depends)
|
||||
w.WriteUEGolomb(0) // pic_order_cnt_type (0 or 2)
|
||||
w.WriteUEGolomb(0) // log2_max_pic_order_cnt_lsb_minus4 (depends)
|
||||
w.WriteUEGolomb(1) // num_ref_frames (1)
|
||||
w.WriteBit(0) // gaps_in_frame_num_value_allowed_flag (0)
|
||||
|
||||
w.WriteUEGolomb(uint8(s.Width>>4) - 1) // pic_width_in_mbs_minus_1
|
||||
w.WriteUEGolomb(uint8(s.Height>>4) - 1) // pic_height_in_map_units_minus_1
|
||||
|
||||
w.WriteBit(1) // frame_mbs_only_flag (1)
|
||||
w.WriteBit(1) // direct_8x8_inference_flag (1)
|
||||
w.WriteBit(0) // frame_cropping_flag (0 is OK)
|
||||
w.WriteBit(0) // vui_prameters_present_flag (0 is OK)
|
||||
w.WriteBit(1) // rbsp_stop_one_bit
|
||||
|
||||
return w.Bytes()
|
||||
}
|
||||
|
||||
func (s *SPS) Unmarshal(data []byte) (err error) {
|
||||
r := golomb.NewReader(data)
|
||||
|
||||
var b byte
|
||||
var u uint
|
||||
|
||||
if b, err = r.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if b&0x1F != 7 {
|
||||
err = errors.New("not SPS data")
|
||||
return
|
||||
}
|
||||
|
||||
if s.ProfileIDC, err = r.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if s.ProfileIOP, err = r.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if s.LevelIDC, err = r.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.Profile = EncodeProfile(s.ProfileIDC, s.ProfileIOP)
|
||||
|
||||
u, err = r.ReadUEGolomb() // seq_parameter_set_id
|
||||
|
||||
if s.ProfileIDC == 100 || s.ProfileIDC == 110 || s.ProfileIDC == 122 ||
|
||||
s.ProfileIDC == 244 || s.ProfileIDC == 44 || s.ProfileIDC == 83 ||
|
||||
s.ProfileIDC == 86 || s.ProfileIDC == 118 || s.ProfileIDC == 128 ||
|
||||
s.ProfileIDC == 138 || s.ProfileIDC == 139 || s.ProfileIDC == 134 ||
|
||||
s.ProfileIDC == 135 {
|
||||
var n byte
|
||||
|
||||
u, err = r.ReadUEGolomb() // chroma_format_idc
|
||||
if u == 3 {
|
||||
b, err = r.ReadBit() // separate_colour_plane_flag
|
||||
n = 12
|
||||
} else {
|
||||
n = 8
|
||||
}
|
||||
|
||||
u, err = r.ReadUEGolomb() // bit_depth_luma_minus8
|
||||
u, err = r.ReadUEGolomb() // bit_depth_chroma_minus8
|
||||
b, err = r.ReadBit() // qpprime_y_zero_transform_bypass_flag
|
||||
|
||||
b, err = r.ReadBit() // seq_scaling_matrix_present_flag
|
||||
if b > 0 {
|
||||
for i := byte(0); i < n; i++ {
|
||||
b, err = r.ReadBit() // seq_scaling_list_present_flag[i]
|
||||
if b > 0 {
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u, err = r.ReadUEGolomb() // log2_max_frame_num_minus4
|
||||
|
||||
u, err = r.ReadUEGolomb() // pic_order_cnt_type
|
||||
switch u {
|
||||
case 0:
|
||||
u, err = r.ReadUEGolomb() // log2_max_pic_order_cnt_lsb_minus4
|
||||
case 1:
|
||||
b, err = r.ReadBit() // delta_pic_order_always_zero_flag
|
||||
_, err = r.ReadSEGolomb() // offset_for_non_ref_pic
|
||||
_, err = r.ReadSEGolomb() // offset_for_top_to_bottom_field
|
||||
u, err = r.ReadUEGolomb() // num_ref_frames_in_pic_order_cnt_cycle
|
||||
for i := byte(0); i < b; i++ {
|
||||
_, err = r.ReadSEGolomb() // offset_for_ref_frame[i]
|
||||
}
|
||||
}
|
||||
|
||||
u, err = r.ReadUEGolomb() // num_ref_frames
|
||||
b, err = r.ReadBit() // gaps_in_frame_num_value_allowed_flag
|
||||
|
||||
u, err = r.ReadUEGolomb() // pic_width_in_mbs_minus_1
|
||||
s.Width = uint16(u+1) << 4
|
||||
u, err = r.ReadUEGolomb() // pic_height_in_map_units_minus_1
|
||||
s.Height = uint16(u+1) << 4
|
||||
|
||||
b, err = r.ReadBit() // frame_mbs_only_flag
|
||||
if b == 0 {
|
||||
_, err = r.ReadBit()
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // direct_8x8_inference_flag
|
||||
|
||||
b, err = r.ReadBit() // frame_cropping_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadUEGolomb() // frame_crop_left_offset
|
||||
s.Width -= uint16(u) << 1
|
||||
u, err = r.ReadUEGolomb() // frame_crop_right_offset
|
||||
s.Width -= uint16(u) << 1
|
||||
u, err = r.ReadUEGolomb() // frame_crop_top_offset
|
||||
s.Height -= uint16(u) << 1
|
||||
u, err = r.ReadUEGolomb() // frame_crop_bottom_offset
|
||||
s.Height -= uint16(u) << 1
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // vui_prameters_present_flag
|
||||
if b > 0 {
|
||||
b, err = r.ReadBit() // vui_prameters_present_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadBits(8) // aspect_ratio_idc
|
||||
if b == 255 {
|
||||
u, err = r.ReadBits(16) // sar_width
|
||||
u, err = r.ReadBits(16) // sar_height
|
||||
}
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // overscan_info_present_flag
|
||||
if b > 0 {
|
||||
b, err = r.ReadBit() // overscan_appropriate_flag
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // video_signal_type_present_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadBits(3) // video_format
|
||||
b, err = r.ReadBit() // video_full_range_flag
|
||||
|
||||
b, err = r.ReadBit() // colour_description_present_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadBits(8) // colour_primaries
|
||||
u, err = r.ReadBits(8) // transfer_characteristics
|
||||
u, err = r.ReadBits(8) // matrix_coefficients
|
||||
}
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // chroma_loc_info_present_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_top_field
|
||||
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_bottom_field
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // timing_info_present_flag
|
||||
if b > 0 {
|
||||
u, err = r.ReadBits(32) // num_units_in_tick
|
||||
u, err = r.ReadBits(32) // time_scale
|
||||
b, err = r.ReadBit() // fixed_frame_rate_flag
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // nal_hrd_parameters_present_flag
|
||||
if b > 0 {
|
||||
//panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // vcl_hrd_parameters_present_flag
|
||||
if b > 0 {
|
||||
//panic("not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
// if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag)
|
||||
// b, err = r.ReadBit() // low_delay_hrd_flag
|
||||
|
||||
b, err = r.ReadBit() // pic_struct_present_flag
|
||||
|
||||
b, err = r.ReadBit() // bitstream_restriction_flag
|
||||
if b > 0 {
|
||||
b, err = r.ReadBit() // motion_vectors_over_pic_boundaries_flag
|
||||
u, err = r.ReadUEGolomb() // max_bytes_per_pic_denom
|
||||
u, err = r.ReadUEGolomb() // max_bits_per_mb_denom
|
||||
u, err = r.ReadUEGolomb() // log2_max_mv_length_horizontal
|
||||
u, err = r.ReadUEGolomb() // log2_max_mv_length_vertical
|
||||
u, err = r.ReadUEGolomb() // max_num_reorder_frames
|
||||
u, err = r.ReadUEGolomb() // max_dec_frame_buffering
|
||||
}
|
||||
}
|
||||
|
||||
b, err = r.ReadBit() // rbsp_stop_one_bit
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func EncodeProfile(idc, iop byte) string {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6184#page-41
|
||||
switch {
|
||||
// 4240xx 42C0xx 42E0xx
|
||||
case idc == 0x42 && iop&0b01001111 == 0b01000000:
|
||||
return "CB"
|
||||
case idc == 0x4D && iop&0b10001111 == 0b10000000:
|
||||
return "CB"
|
||||
case idc == 0x58 && iop&0b11001111 == 0b11000000:
|
||||
return "CB"
|
||||
// 4200xx
|
||||
case idc == 0x42 && iop&0b01001111 == 0:
|
||||
return "B"
|
||||
case idc == 0x58 && iop&0b11001111 == 0b10000000:
|
||||
return "B"
|
||||
// 4d40xx
|
||||
case idc == 0x4D && iop&0b10101111 == 0:
|
||||
return "M"
|
||||
case idc == 0x58 && iop&0b11001111 == 0:
|
||||
return "E"
|
||||
case idc == 0x64 && iop == 0:
|
||||
return "H"
|
||||
case idc == 0x6E && iop == 0:
|
||||
return "H10"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func DecodeProfile(profile string) (idc, iop byte) {
|
||||
switch profile {
|
||||
case "CB":
|
||||
return 0x42, 0b01000000
|
||||
case "B":
|
||||
return 0x42, 0 // 66
|
||||
case "M":
|
||||
return 0x4D, 0 // 77
|
||||
case "E":
|
||||
return 0x58, 0 // 88
|
||||
case "H":
|
||||
return 0x64, 0
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshalSPS(t *testing.T) {
|
||||
raw := []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
s := SPS{}
|
||||
if err := s.Unmarshal(raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw2 := s.Marshal()
|
||||
if bytes.Compare(raw, raw2) != 0 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalPPS(t *testing.T) {
|
||||
raw := []byte{0x68, 0xce, 0x38, 0x80}
|
||||
p := PPS{}
|
||||
if err := p.Unmarshal(raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw2 := p.Marshal()
|
||||
if bytes.Compare(raw, raw2) != 0 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalPPS2(t *testing.T) {
|
||||
raw := []byte{72, 238, 60, 128}
|
||||
p := PPS{}
|
||||
if err := p.Unmarshal(raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw2 := p.Marshal()
|
||||
if bytes.Compare(raw, raw2) != 0 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafari(t *testing.T) {
|
||||
// CB66, L3.1: chrome, edge, safari, android chrome
|
||||
s := EncodeProfile(0x42, 0xE0)
|
||||
t.Logf("Profile: %s, Level: %d", s, 0x1F)
|
||||
|
||||
// B66, L3.1: chrome, edge
|
||||
s = EncodeProfile(0x42, 0x00)
|
||||
t.Logf("Profile: %s, Level: %d", s, 0x1F)
|
||||
|
||||
// M77, L3.1: chrome, edge
|
||||
s = EncodeProfile(0x4D, 0x00)
|
||||
t.Logf("Profile: %s, Level: %d", s, 0x1F)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package h264
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
)
|
||||
|
||||
const RTPPacketVersionAVC = 0
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
depack := &codecs.H264Packet{IsAVC: true}
|
||||
|
||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||
sps = EncodeAVC(sps)
|
||||
pps = EncodeAVC(pps)
|
||||
|
||||
var buffer []byte
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//println(packet.SequenceNumber, packet.Payload[0]&0x1F, packet.Payload[0], packet.Payload[1], packet.Marker, packet.Timestamp)
|
||||
|
||||
data, err := depack.Unmarshal(packet.Payload)
|
||||
if len(data) == 0 || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
naluType := NALUType(data)
|
||||
//println(naluType, len(data))
|
||||
|
||||
switch naluType {
|
||||
case NALUTypeSPS:
|
||||
//println("new SPS")
|
||||
sps = data
|
||||
return nil
|
||||
case NALUTypePPS:
|
||||
//println("new PPS")
|
||||
pps = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
|
||||
// and every NALU will be sliced to multiple NALUs
|
||||
if !packet.Marker {
|
||||
buffer = append(buffer, data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
if buffer != nil {
|
||||
buffer = append(buffer, data...)
|
||||
data = buffer
|
||||
buffer = nil
|
||||
}
|
||||
|
||||
var clone rtp.Packet
|
||||
|
||||
if naluType == NALUTypeIFrame {
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = sps
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = pps
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = data
|
||||
return push(&clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
||||
payloader := &Payloader{IsAVC: true}
|
||||
sequencer := rtp.NewRandomSequencer()
|
||||
mtu -= 12 // rtp.Header size
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
if packet.Version == RTPPacketVersionAVC {
|
||||
payloads := payloader.Payload(mtu, packet.Payload)
|
||||
for i, payload := range payloads {
|
||||
clone := rtp.Packet{
|
||||
Header: rtp.Header{
|
||||
Version: 2,
|
||||
Marker: i == len(payloads)-1,
|
||||
//PayloadType: packet.PayloadType,
|
||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||
Timestamp: packet.Timestamp,
|
||||
//SSRC: packet.SSRC,
|
||||
},
|
||||
Payload: payload,
|
||||
}
|
||||
if err := push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return push(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package mse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/mp4f"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MsgTypeMSE = "mse"
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *mp4f.Muxer
|
||||
streams []av.CodecData
|
||||
start bool
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||
},
|
||||
}, {
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
codec := track.Codec
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
idx := int8(len(c.streams))
|
||||
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: idx, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch h264.NALUType(packet.Payload) {
|
||||
case h264.NALUTypeIFrame:
|
||||
c.start = true
|
||||
pkt.IsKeyFrame = true
|
||||
case h264.NALUTypePFrame:
|
||||
if !c.start {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||
if pkt.Time > 0 {
|
||||
pkt.Duration = newTime - pkt.Time
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
for _, buf := range c.muxer.WritePacketV5(pkt) {
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h264.IsAVC(codec) {
|
||||
wrapper := h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() {
|
||||
c.muxer = mp4f.NewMuxer(nil)
|
||||
if err := c.muxer.WriteHeader(c.streams); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
codecs, buf := c.muxer.GetInit(c.streams)
|
||||
c.Fire(&streamer.Message{Type: MsgTypeMSE, Value: codecs})
|
||||
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
"type": "MSE server consumer",
|
||||
"send": c.send,
|
||||
"remote_addr": c.RemoteAddr,
|
||||
"user_agent": c.UserAgent,
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package ngrok
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Ngrok struct {
|
||||
streamer.Element
|
||||
|
||||
Tunnels map[string]string
|
||||
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Msg string `json:"msg"`
|
||||
Addr string `json:"addr"`
|
||||
URL string `json:"url"`
|
||||
Line string
|
||||
}
|
||||
|
||||
func NewNgrok(command interface{}) (*Ngrok, error) {
|
||||
var arg []string
|
||||
switch command.(type) {
|
||||
case string:
|
||||
arg = strings.Split(command.(string), " ")
|
||||
case []string:
|
||||
arg = command.([]string)
|
||||
}
|
||||
|
||||
arg = append(arg, "--log", "stdout", "--log-format", "json")
|
||||
|
||||
cmd := exec.Command(arg[0], arg[1:]...)
|
||||
|
||||
r, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
|
||||
n := &Ngrok{
|
||||
Tunnels: map[string]string{},
|
||||
reader: bufio.NewReader(r),
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *Ngrok) Serve() error {
|
||||
for {
|
||||
line, _, err := n.reader.ReadLine()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := new(Message)
|
||||
_ = json.Unmarshal(line, msg)
|
||||
|
||||
if msg.Msg == "started tunnel" {
|
||||
n.Tunnels[msg.Addr] = msg.URL
|
||||
}
|
||||
|
||||
msg.Line = string(line)
|
||||
|
||||
n.Fire(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/rtmp"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
URI string
|
||||
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
conn *rtmp.Conn
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewClient(uri string) *Client {
|
||||
return &Client{URI: uri}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
c.conn, err = rtmp.Dial(c.URI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// important to get SPS/PPS
|
||||
streams, err := c.conn.Streams()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, stream := range streams {
|
||||
switch stream.Type() {
|
||||
case av.H264:
|
||||
cd := stream.(h264parser.CodecData)
|
||||
fmtp := "sprop-parameter-sets=" +
|
||||
base64.StdEncoding.EncodeToString(cd.RecordInfo.SPS[0]) + "," +
|
||||
base64.StdEncoding.EncodeToString(cd.RecordInfo.PPS[0])
|
||||
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecH264,
|
||||
ClockRate: 90000,
|
||||
FmtpLine: fmtp,
|
||||
PayloadType: h264.PayloadTypeAVC,
|
||||
}
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := &streamer.Track{
|
||||
Codec: codec, Direction: media.Direction,
|
||||
}
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
case av.AAC:
|
||||
panic("not implemented")
|
||||
default:
|
||||
panic("unsupported codec")
|
||||
}
|
||||
}
|
||||
|
||||
c.Fire(streamer.StateReady)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Handle() (err error) {
|
||||
defer c.Fire(streamer.StateNull)
|
||||
|
||||
c.Fire(streamer.StatePlaying)
|
||||
|
||||
for {
|
||||
var pkt av.Packet
|
||||
pkt, err = c.conn.ReadPacket()
|
||||
if err != nil {
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
track := c.tracks[int(pkt.Idx)]
|
||||
|
||||
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate))
|
||||
|
||||
var payloads [][]byte
|
||||
if track.Codec.Name == streamer.CodecH264 {
|
||||
payloads = splitAVC(pkt.Data)
|
||||
} else {
|
||||
payloads = [][]byte{pkt.Data}
|
||||
}
|
||||
|
||||
for _, payload := range payloads {
|
||||
packet := &rtp.Packet{
|
||||
Header: rtp.Header{Timestamp: timestamp},
|
||||
Payload: payload,
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func splitAVC(data []byte) [][]byte {
|
||||
var nals [][]byte
|
||||
for {
|
||||
// get AVC length
|
||||
size := int(binary.BigEndian.Uint32(data))
|
||||
|
||||
// check if multiple items in one packet
|
||||
if size+4 < len(data) {
|
||||
nals = append(nals, data[:size+4])
|
||||
data = data[size+4:]
|
||||
} else {
|
||||
nals = append(nals, data)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nals
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package rtmp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
panic("wrong codec")
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
return c.Handle()
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
return c.Close()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Useful links
|
||||
|
||||
- https://www.kurento.org/blog/rtp-i-intro-rtp-and-sdp
|
||||
@@ -0,0 +1,696 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ProtoRTSP = "RTSP/1.0"
|
||||
MethodOptions = "OPTIONS"
|
||||
MethodSetup = "SETUP"
|
||||
MethodTeardown = "TEARDOWN"
|
||||
MethodDescribe = "DESCRIBE"
|
||||
MethodPlay = "PLAY"
|
||||
MethodPause = "PAUSE"
|
||||
MethodAnnounce = "ANNOUNCE"
|
||||
MethodRecord = "RECORD"
|
||||
)
|
||||
|
||||
type Mode byte
|
||||
|
||||
const (
|
||||
ModeUnknown Mode = iota
|
||||
ModeClientProducer
|
||||
ModeServerUnknown
|
||||
ModeServerProducer
|
||||
ModeServerConsumer
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
// public
|
||||
|
||||
Medias []*streamer.Media
|
||||
Session string
|
||||
UserAgent string
|
||||
URL *url.URL
|
||||
|
||||
// internal
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
|
||||
mode Mode
|
||||
|
||||
tracks []*streamer.Track
|
||||
channels map[byte]*streamer.Track
|
||||
|
||||
// stats
|
||||
|
||||
receive int
|
||||
send int
|
||||
}
|
||||
|
||||
func NewClient(uri string) (*Conn, error) {
|
||||
var err error
|
||||
|
||||
c := new(Conn)
|
||||
c.URL, err = url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.URL.Host, ':') < 0 {
|
||||
c.URL.Host += ":554"
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.mode = ModeClientProducer
|
||||
c.URL.User = nil
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewServer(conn net.Conn) *Conn {
|
||||
c := new(Conn)
|
||||
c.conn = conn
|
||||
c.mode = ModeServerUnknown
|
||||
c.reader = bufio.NewReader(conn)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() (err error) {
|
||||
//if c.state != StateClientInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
|
||||
c.conn, err = net.DialTimeout(
|
||||
"tcp", c.URL.Host, 10*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var tlsConf *tls.Config
|
||||
switch c.URL.Scheme {
|
||||
case "rtsps":
|
||||
tlsConf = &tls.Config{ServerName: c.URL.Hostname()}
|
||||
case "rtspx":
|
||||
c.URL.Scheme = "rtsps"
|
||||
tlsConf = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
if tlsConf != nil {
|
||||
tlsConn := tls.Client(c.conn, tlsConf)
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = tlsConn
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request sends only Request
|
||||
func (c *Conn) Request(req *tcp.Request) error {
|
||||
if req.Proto == "" {
|
||||
req.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if req.Header == nil {
|
||||
req.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
c.sequence++
|
||||
req.Header.Set("CSeq", strconv.Itoa(c.sequence))
|
||||
|
||||
c.auth.Write(req)
|
||||
|
||||
if c.Session != "" {
|
||||
req.Header.Set("Session", c.Session)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
val := strconv.Itoa(len(req.Body))
|
||||
req.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
return req.Write(c.conn)
|
||||
}
|
||||
|
||||
// Do send Request and receive and process Response
|
||||
func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
|
||||
if err := c.Request(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := tcp.ReadResponse(c.reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
switch c.auth.Method {
|
||||
case tcp.AuthNone:
|
||||
return nil, errors.New("user/pass not provided")
|
||||
case tcp.AuthUnknown:
|
||||
if c.auth.Read(res) {
|
||||
return c.Do(req)
|
||||
}
|
||||
case tcp.AuthBasic, tcp.AuthDigest:
|
||||
return nil, errors.New("wrong user/pass")
|
||||
}
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("wrong response on %s", req.Method)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Response(res *tcp.Response) error {
|
||||
if res.Proto == "" {
|
||||
res.Proto = ProtoRTSP
|
||||
}
|
||||
|
||||
if res.Status == "" {
|
||||
res.Status = "200 OK"
|
||||
}
|
||||
|
||||
if res.Header == nil {
|
||||
res.Header = make(map[string][]string)
|
||||
}
|
||||
|
||||
if res.Request != nil && res.Request.Header != nil {
|
||||
seq := res.Request.Header.Get("CSeq")
|
||||
if seq != "" {
|
||||
res.Header.Set("CSeq", seq)
|
||||
}
|
||||
}
|
||||
|
||||
if c.Session != "" {
|
||||
res.Header.Set("Session", c.Session)
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
val := strconv.Itoa(len(res.Body))
|
||||
res.Header.Set("Content-Length", val)
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
|
||||
return res.Write(c.conn)
|
||||
}
|
||||
|
||||
func (c *Conn) Options() error {
|
||||
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Base"); val != "" {
|
||||
c.URL, err = url.Parse(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Describe() error {
|
||||
// 5.3 Back channel connection
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf
|
||||
req := &tcp.Request{
|
||||
Method: MethodDescribe,
|
||||
URL: c.URL,
|
||||
Header: map[string][]string{
|
||||
"Accept": {"application/sdp"},
|
||||
"Require": {"www.onvif.org/ver20/backchannel"},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
|
||||
// TODO: make some universal fix
|
||||
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
|
||||
res.Body[i+3] = '_'
|
||||
}
|
||||
|
||||
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mode = ModeClientProducer
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (c *Conn) Announce() (err error) {
|
||||
// req := &tcp.Request{
|
||||
// Method: MethodAnnounce,
|
||||
// URL: c.URL,
|
||||
// Header: map[string][]string{
|
||||
// "Content-Type": {"application/sdp"},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// //req.Body, err = c.sdp.Marshal()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// _, err = c.Do(req)
|
||||
//
|
||||
// return
|
||||
//}
|
||||
|
||||
func (c *Conn) Setup() error {
|
||||
for _, media := range c.Medias {
|
||||
_, err := c.SetupMedia(media, media.Codecs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetupMedia(
|
||||
media *streamer.Media, codec *streamer.Codec,
|
||||
) (*streamer.Track, error) {
|
||||
ch := c.GetChannel(media)
|
||||
if ch < 0 {
|
||||
return nil, fmt.Errorf("wrong media: %v", media)
|
||||
}
|
||||
|
||||
trackURL, err := url.Parse(media.Control)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trackURL = c.URL.ResolveReference(trackURL)
|
||||
|
||||
req := &tcp.Request{
|
||||
Method: MethodSetup,
|
||||
URL: trackURL,
|
||||
Header: map[string][]string{
|
||||
"Transport": {fmt.Sprintf(
|
||||
// i - RTP (data channel)
|
||||
// i+1 - RTCP (control channel)
|
||||
"RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1,
|
||||
)},
|
||||
},
|
||||
}
|
||||
|
||||
var res *tcp.Response
|
||||
res, err = c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.Session == "" {
|
||||
// Session: 216525287999;timeout=60
|
||||
if s := res.Header.Get("Session"); s != "" {
|
||||
if j := strings.IndexByte(s, ';'); j > 0 {
|
||||
s = s[:j]
|
||||
}
|
||||
c.Session = s
|
||||
}
|
||||
}
|
||||
|
||||
// we send our `interleaved`, but camera can answer with another
|
||||
|
||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
||||
s := res.Header.Get("Transport")
|
||||
s, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
|
||||
if !ok1 || !ok2 {
|
||||
panic("wrong response")
|
||||
}
|
||||
|
||||
ch, err = strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
track := &streamer.Track{
|
||||
Codec: codec, Direction: media.Direction,
|
||||
}
|
||||
|
||||
switch track.Direction {
|
||||
case streamer.DirectionSendonly:
|
||||
if c.channels == nil {
|
||||
c.channels = make(map[byte]*streamer.Track)
|
||||
}
|
||||
c.channels[byte(ch)] = track
|
||||
|
||||
case streamer.DirectionRecvonly:
|
||||
track = c.bindTrack(track, byte(ch), codec.PayloadType)
|
||||
}
|
||||
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Play() (err error) {
|
||||
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
|
||||
return c.Request(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Teardown() (err error) {
|
||||
//if c.state != StateClientPlay {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
|
||||
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
|
||||
return c.Request(req)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.Teardown(); err != nil {
|
||||
return err
|
||||
}
|
||||
conn := c.conn
|
||||
c.conn = nil
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
const transport = "RTP/AVP/TCP;unicast;interleaved="
|
||||
|
||||
func (c *Conn) Accept() error {
|
||||
//if c.state != StateServerInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
|
||||
for {
|
||||
req, err := tcp.ReadRequest(c.reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
|
||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||
switch req.Method {
|
||||
case MethodOptions:
|
||||
c.URL = req.URL
|
||||
c.UserAgent = req.Header.Get("User-Agent")
|
||||
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{
|
||||
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
|
||||
},
|
||||
Request: req,
|
||||
}
|
||||
if err = c.Response(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodAnnounce:
|
||||
if req.Header.Get("Content-Type") != "application/sdp" {
|
||||
return errors.New("wrong content type")
|
||||
}
|
||||
|
||||
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: fix someday...
|
||||
c.channels = map[byte]*streamer.Track{}
|
||||
for i, media := range c.Medias {
|
||||
track := &streamer.Track{
|
||||
Codec: media.Codecs[0], Direction: media.Direction,
|
||||
}
|
||||
c.tracks = append(c.tracks, track)
|
||||
c.channels[byte(i<<1)] = track
|
||||
}
|
||||
|
||||
c.mode = ModeServerProducer
|
||||
c.Fire(MethodAnnounce)
|
||||
|
||||
res := &tcp.Response{Request: req}
|
||||
if err = c.Response(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodDescribe:
|
||||
c.mode = ModeServerConsumer
|
||||
c.Fire(MethodDescribe)
|
||||
|
||||
if c.tracks == nil {
|
||||
res := &tcp.Response{
|
||||
Status: "404 Not Found",
|
||||
Request: req,
|
||||
}
|
||||
return c.Response(res)
|
||||
}
|
||||
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/sdp"},
|
||||
},
|
||||
Request: req,
|
||||
}
|
||||
|
||||
// convert tracks to real output medias medias
|
||||
var medias []*streamer.Media
|
||||
for _, track := range c.tracks {
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.GetKind(track.Codec.Name),
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{track.Codec},
|
||||
}
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
res.Body, err = streamer.MarshalSDP(medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.Response(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodSetup:
|
||||
tr := req.Header.Get("Transport")
|
||||
|
||||
res := &tcp.Response{
|
||||
Header: map[string][]string{},
|
||||
Request: req,
|
||||
}
|
||||
|
||||
if tr[:len(transport)] == transport {
|
||||
c.Session = "1" // TODO: fixme
|
||||
res.Header.Set("Transport", tr[:len(transport)+3])
|
||||
} else {
|
||||
res.Status = "461 Unsupported transport"
|
||||
}
|
||||
|
||||
if err = c.Response(res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case MethodRecord, MethodPlay:
|
||||
res := &tcp.Response{Request: req}
|
||||
return c.Response(res)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported method: %s", req.Method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Handle() (err error) {
|
||||
defer func() {
|
||||
if c.conn == nil {
|
||||
err = nil
|
||||
}
|
||||
//c.Fire(streamer.StateNull)
|
||||
}()
|
||||
|
||||
//c.Fire(streamer.StatePlaying)
|
||||
|
||||
for {
|
||||
// we can read:
|
||||
// 1. RTP interleaved: `$` + 1B channel number + 2B size
|
||||
// 2. RTSP response: RTSP/1.0 200 OK
|
||||
// 3. RTSP request: OPTIONS ...
|
||||
var buf4 []byte // `$` + 1B channel number + 2B size
|
||||
buf4, err = c.reader.Peek(4)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if buf4[0] != '$' {
|
||||
if string(buf4) == "RTSP" {
|
||||
var res *tcp.Response
|
||||
res, err = tcp.ReadResponse(c.reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Fire(res)
|
||||
} else {
|
||||
var req *tcp.Request
|
||||
req, err = tcp.ReadRequest(c.reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Fire(req)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// hope that the odd channels are always RTCP
|
||||
channelID := buf4[1]
|
||||
|
||||
// get data size
|
||||
size := int(binary.BigEndian.Uint16(buf4[2:]))
|
||||
|
||||
if _, err = c.reader.Discard(4); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// init memory for data
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(c.reader, buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.receive += size
|
||||
|
||||
if channelID&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
if err = packet.Unmarshal(buf); err != nil {
|
||||
return errors.New("wrong RTP data")
|
||||
}
|
||||
|
||||
track := c.channels[channelID]
|
||||
if track != nil {
|
||||
_ = track.WriteRTP(packet)
|
||||
//return fmt.Errorf("wrong channelID: %d", channelID)
|
||||
} else {
|
||||
panic("wrong channelID")
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
|
||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||
return errors.New("wrong RTCP data")
|
||||
}
|
||||
|
||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||
if err != nil {
|
||||
return errors.New("wrong RTCP data")
|
||||
}
|
||||
|
||||
c.Fire(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) GetChannel(media *streamer.Media) int {
|
||||
for i, m := range c.Medias {
|
||||
if m == media {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (c *Conn) bindTrack(
|
||||
track *streamer.Track, channel uint8, payloadType uint8,
|
||||
) *streamer.Track {
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
packet.Header.PayloadType = payloadType
|
||||
//packet.Header.PayloadType = 100
|
||||
//packet.Header.PayloadType = 8
|
||||
//packet.Header.PayloadType = 106
|
||||
|
||||
size := packet.MarshalSize()
|
||||
|
||||
data := make([]byte, 4+size)
|
||||
data[0] = '$'
|
||||
data[1] = channel
|
||||
//data[1] = 10
|
||||
binary.BigEndian.PutUint16(data[2:], uint16(size))
|
||||
|
||||
if _, err := packet.MarshalTo(data[4:]); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.send += size
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
type RTCP struct {
|
||||
Channel byte
|
||||
Header rtcp.Header
|
||||
Packets []rtcp.Packet
|
||||
}
|
||||
|
||||
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
|
||||
i := strings.Index(s, sub1)
|
||||
if i >= 0 {
|
||||
ok1 = true
|
||||
s = s[i+len(sub1):]
|
||||
}
|
||||
|
||||
i = strings.Index(s, sub2)
|
||||
if i >= 0 {
|
||||
return s[:i], ok1, true
|
||||
}
|
||||
|
||||
return s, ok1, false
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Element Producer
|
||||
|
||||
func (c *Conn) GetMedias() []*streamer.Media {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
for _, track := range c.tracks {
|
||||
if track.Codec == codec {
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
track, err := c.SetupMedia(media, codec)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
func (c *Conn) Start() error {
|
||||
if c.mode == ModeServerProducer {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.Play(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Handle()
|
||||
}
|
||||
|
||||
func (c *Conn) Stop() error {
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
// Consumer
|
||||
|
||||
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
switch track.Direction {
|
||||
// send our track to RTSP consumer (ex. FFmpeg)
|
||||
case streamer.DirectionSendonly:
|
||||
i := len(c.tracks)
|
||||
channelID := byte(i << 1)
|
||||
|
||||
codec := track.Codec.Clone()
|
||||
codec.PayloadType = uint8(96 + i)
|
||||
|
||||
for i, m := range c.Medias {
|
||||
if m == media {
|
||||
media.Codecs = []*streamer.Codec{codec}
|
||||
c.Medias[i] = media
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
track = c.bindTrack(track, channelID, codec.PayloadType)
|
||||
track.Codec = codec
|
||||
c.tracks = append(c.tracks, track)
|
||||
|
||||
return track
|
||||
|
||||
case streamer.DirectionRecvonly:
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
panic("wrong direction")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
streamer.JSONReceive: c.receive,
|
||||
streamer.JSONSend: c.send,
|
||||
}
|
||||
switch c.mode {
|
||||
case ModeUnknown:
|
||||
v[streamer.JSONType] = "RTSP unknown"
|
||||
case ModeClientProducer:
|
||||
v[streamer.JSONType] = "RTSP client producer"
|
||||
case ModeServerProducer:
|
||||
v[streamer.JSONType] = "RTSP server producer"
|
||||
case ModeServerConsumer:
|
||||
v[streamer.JSONType] = "RTSP server consumer"
|
||||
}
|
||||
//if c.URI != "" {
|
||||
// v["uri"] = c.URI
|
||||
//}
|
||||
if c.URL != nil {
|
||||
v["url"] = c.URL.String()
|
||||
}
|
||||
if c.conn != nil {
|
||||
v[streamer.JSONRemoteAddr] = c.conn.RemoteAddr().String()
|
||||
}
|
||||
if c.UserAgent != "" {
|
||||
v[streamer.JSONUserAgent] = c.UserAgent
|
||||
}
|
||||
for i, media := range c.Medias {
|
||||
k := "media:" + strconv.Itoa(i)
|
||||
v[k] = media.String()
|
||||
}
|
||||
for i, track := range c.tracks {
|
||||
k := "track:" + strconv.Itoa(int(i>>1))
|
||||
v[k] = track.String()
|
||||
}
|
||||
//for i, track := range c.tracks {
|
||||
// k := "track:" + strconv.Itoa(i+1)
|
||||
// if track.MimeType() == streamer.MimeTypeH264 {
|
||||
// v[k] = h264.Describe(track.Caps())
|
||||
// } else {
|
||||
// v[k] = track.MimeType()
|
||||
// }
|
||||
//}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONType = "type"
|
||||
JSONRemoteAddr = "remote_addr"
|
||||
JSONUserAgent = "user_agent"
|
||||
JSONReceive = "receive"
|
||||
JSONSend = "send"
|
||||
)
|
||||
|
||||
// Message - struct for data exchange in Web API
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
|
||||
if len(sub2) == 1 {
|
||||
i = strings.IndexByte(s, sub2[0])
|
||||
} else {
|
||||
i = strings.Index(s, sub2)
|
||||
}
|
||||
if i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func Contains(medias []*Media, media *Media, codec *Codec) bool {
|
||||
var ok1, ok2 bool
|
||||
for _, m := range medias {
|
||||
if m == media {
|
||||
ok1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, c := range media.Codecs {
|
||||
if c == codec {
|
||||
ok2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return ok1 && ok2
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DirectionRecvonly = "recvonly"
|
||||
DirectionSendonly = "sendonly"
|
||||
DirectionSendRecv = "sendrecv"
|
||||
)
|
||||
|
||||
const (
|
||||
KindVideo = "video"
|
||||
KindAudio = "audio"
|
||||
)
|
||||
|
||||
const (
|
||||
CodecH264 = "H264" // payloadType: 96
|
||||
CodecH265 = "H265"
|
||||
CodecVP8 = "VP8"
|
||||
CodecVP9 = "VP9"
|
||||
CodecAV1 = "AV1"
|
||||
|
||||
CodecPCMU = "PCMU" // payloadType: 0
|
||||
CodecPCMA = "PCMA" // payloadType: 8
|
||||
CodecAAC = "MPEG4-GENERIC"
|
||||
CodecOpus = "OPUS" // payloadType: 111
|
||||
CodecG722 = "G722"
|
||||
)
|
||||
|
||||
func GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722:
|
||||
return KindAudio
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Media take best from:
|
||||
// - deepch/vdk/format/rtsp/sdp.Media
|
||||
// - pion/sdp.MediaDescription
|
||||
type Media struct {
|
||||
Kind string // video, audio
|
||||
Direction string
|
||||
Codecs []*Codec
|
||||
|
||||
MID string // TODO: fixme?
|
||||
Control string // TODO: fixme?
|
||||
}
|
||||
|
||||
func (m *Media) String() string {
|
||||
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
|
||||
for _, codec := range m.Codecs {
|
||||
s += ", " + codec.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *Media) Clone() *Media {
|
||||
clone := *m
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (m *Media) AV() bool {
|
||||
return m.Kind == KindVideo || m.Kind == KindAudio
|
||||
}
|
||||
|
||||
func (m *Media) MatchCodec(codec *Codec) bool {
|
||||
for _, c := range m.Codecs {
|
||||
if c.Match(codec) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Media) MatchMedia(media *Media) *Codec {
|
||||
if m.Kind != media.Kind {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch m.Direction {
|
||||
case DirectionSendonly:
|
||||
if media.Direction != DirectionRecvonly {
|
||||
return nil
|
||||
}
|
||||
case DirectionRecvonly:
|
||||
if media.Direction != DirectionSendonly {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
panic("wrong direction")
|
||||
}
|
||||
|
||||
for _, localCodec := range m.Codecs {
|
||||
if media.Codecs == nil {
|
||||
return localCodec
|
||||
}
|
||||
|
||||
for _, remoteCodec := range media.Codecs {
|
||||
if localCodec.Match(remoteCodec) {
|
||||
return localCodec
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Codec take best from:
|
||||
// - deepch/vdk/av.CodecData
|
||||
// - pion/webrtc.RTPCodecCapability
|
||||
type Codec struct {
|
||||
Name string // H264, PCMU, PCMA, opus...
|
||||
ClockRate uint32 // 90000, 8000, 16000...
|
||||
Channels uint16 // 0, 1, 2
|
||||
FmtpLine string
|
||||
PayloadType uint8
|
||||
}
|
||||
|
||||
func NewCodec(name string) *Codec {
|
||||
name = strings.ToUpper(name)
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
|
||||
return &Codec{Name: name, ClockRate: 90000}
|
||||
case CodecPCMU, CodecPCMA:
|
||||
return &Codec{Name: name, ClockRate: 8000}
|
||||
case CodecOpus:
|
||||
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported codec: %s", name))
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
|
||||
if c.Channels > 0 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) Clone() *Codec {
|
||||
clone := *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *Codec) Match(codec *Codec) bool {
|
||||
return c.Name == codec.Name &&
|
||||
c.ClockRate == codec.ClockRate &&
|
||||
c.Channels == codec.Channels
|
||||
}
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var medias []*Media
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
media := UnmarshalMedia(md)
|
||||
|
||||
if media.Direction == DirectionSendRecv {
|
||||
media.Direction = DirectionRecvonly
|
||||
medias = append(medias, media)
|
||||
|
||||
media = media.Clone()
|
||||
media.Direction = DirectionSendonly
|
||||
}
|
||||
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
|
||||
medias, err := UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
for _, media := range medias {
|
||||
switch media.Direction {
|
||||
case DirectionRecvonly, "":
|
||||
media.Direction = DirectionSendonly
|
||||
case DirectionSendonly:
|
||||
media.Direction = DirectionRecvonly
|
||||
}
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
}
|
||||
|
||||
func MarshalSDP(medias []*Media) ([]byte, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
|
||||
payloadType := uint8(96)
|
||||
|
||||
for _, media := range medias {
|
||||
if media.Codecs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := media.Codecs[0]
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: media.Kind,
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
|
||||
|
||||
payloadType++
|
||||
}
|
||||
|
||||
return sd.Marshal()
|
||||
}
|
||||
|
||||
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
|
||||
m := &Media{
|
||||
Kind: md.MediaName.Media,
|
||||
}
|
||||
|
||||
for _, attr := range md.Attributes {
|
||||
switch attr.Key {
|
||||
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
|
||||
m.Direction = attr.Key
|
||||
case "control":
|
||||
m.Control = attr.Value
|
||||
case "mid":
|
||||
m.MID = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
for _, format := range md.MediaName.Formats {
|
||||
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
|
||||
c := &Codec{PayloadType: byte(atoi(payloadType))}
|
||||
|
||||
for _, attr := range md.Attributes {
|
||||
switch {
|
||||
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
|
||||
i := strings.IndexByte(attr.Value, ' ')
|
||||
ss := strings.Split(attr.Value[i+1:], "/")
|
||||
|
||||
c.Name = strings.ToUpper(ss[0])
|
||||
c.ClockRate = uint32(atoi(ss[1]))
|
||||
|
||||
if len(ss) == 3 && ss[2] == "2" {
|
||||
c.Channels = 2
|
||||
}
|
||||
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
|
||||
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
|
||||
c.FmtpLine = attr.Value[i+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
switch payloadType {
|
||||
case "0":
|
||||
c.Name = "PCMU"
|
||||
c.ClockRate = 8000
|
||||
case "8":
|
||||
c.Name = "PCMA"
|
||||
c.ClockRate = 8000
|
||||
default:
|
||||
panic("unknown codec")
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package streamer
|
||||
|
||||
// States, Queries and Events
|
||||
|
||||
type EventType byte
|
||||
|
||||
const (
|
||||
StateNull EventType = iota
|
||||
StateReady
|
||||
StatePaused
|
||||
StatePlaying
|
||||
)
|
||||
|
||||
// Element base struct for all classes with support feedback
|
||||
type Element struct {
|
||||
events []EventFunc
|
||||
}
|
||||
|
||||
type EventFunc func(msg interface{})
|
||||
|
||||
func (e *Element) Listen(f EventFunc) {
|
||||
e.events = append(e.events, f)
|
||||
}
|
||||
|
||||
func (e *Element) Fire(msg interface{}) {
|
||||
for _, f := range e.events {
|
||||
f(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Element) Push(msg interface{}) {
|
||||
}
|
||||
|
||||
// Producer and Consumer interfaces
|
||||
|
||||
type Producer interface {
|
||||
Listen(f EventFunc)
|
||||
GetMedias() []*Media
|
||||
GetTrack(media *Media, codec *Codec) *Track
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type Consumer interface {
|
||||
Listen(f EventFunc)
|
||||
GetMedias() []*Media
|
||||
AddTrack(media *Media, track *Track) *Track
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package streamer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type WriterFunc func(packet *rtp.Packet) error
|
||||
type WrapperFunc func(push WriterFunc) WriterFunc
|
||||
|
||||
type Track struct {
|
||||
Codec *Codec
|
||||
Direction string
|
||||
Sink map[*Track]WriterFunc
|
||||
}
|
||||
|
||||
func (t *Track) String() string {
|
||||
s := t.Codec.String()
|
||||
s += fmt.Sprintf(", sinks=%d", len(t.Sink))
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||
for _, f := range t.Sink {
|
||||
_ = f(p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Track) Bind(w WriterFunc) *Track {
|
||||
if t.Sink == nil {
|
||||
t.Sink = map[*Track]WriterFunc{}
|
||||
}
|
||||
|
||||
clone := &Track{
|
||||
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
|
||||
}
|
||||
t.Sink[clone] = w
|
||||
return clone
|
||||
}
|
||||
|
||||
func (t *Track) Unbind() {
|
||||
delete(t.Sink, t)
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
Method byte
|
||||
user string
|
||||
pass string
|
||||
header string
|
||||
h1nonce string
|
||||
}
|
||||
|
||||
const (
|
||||
AuthNone byte = iota
|
||||
AuthUnknown
|
||||
AuthBasic
|
||||
AuthDigest
|
||||
)
|
||||
|
||||
func NewAuth(user *url.Userinfo) *Auth {
|
||||
a := new(Auth)
|
||||
a.user = user.Username()
|
||||
a.pass, _ = user.Password()
|
||||
if a.user != "" {
|
||||
a.Method = AuthUnknown
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Auth) Read(res *Response) bool {
|
||||
auth := res.Header.Get("WWW-Authenticate")
|
||||
if len(auth) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch auth[:6] {
|
||||
case "Basic ":
|
||||
a.header = "Basic " + B64(a.user, a.pass)
|
||||
a.Method = AuthBasic
|
||||
return true
|
||||
case "Digest":
|
||||
realm := Between(auth, `realm="`, `"`)
|
||||
nonce := Between(auth, `nonce="`, `"`)
|
||||
|
||||
a.h1nonce = HexMD5(a.user, realm, a.pass) + ":" + nonce
|
||||
a.header = fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s"`,
|
||||
a.user, realm, nonce,
|
||||
)
|
||||
a.Method = AuthDigest
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Write(req *Request) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch a.Method {
|
||||
case AuthBasic:
|
||||
req.Header.Set("Authorization", a.header)
|
||||
case AuthDigest:
|
||||
uri := req.URL.RequestURI()
|
||||
h2 := HexMD5(req.Method, uri)
|
||||
response := HexMD5(a.h1nonce, h2)
|
||||
header := a.header + fmt.Sprintf(
|
||||
`, uri="%s", response="%s"`, uri, response,
|
||||
)
|
||||
req.Header.Set("Authorization", header)
|
||||
}
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
i = strings.Index(s, sub2)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
func HexMD5(s ...string) string {
|
||||
b := md5.Sum([]byte(strings.Join(s, ":")))
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func B64(s ...string) string {
|
||||
b := []byte(strings.Join(s, ":"))
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
streamer.Element
|
||||
|
||||
listener net.Listener
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewServer(address string) (srv *Server, err error) {
|
||||
srv = &Server{}
|
||||
srv.listener, err = net.Listen("tcp", address)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) Serve() {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
s.Fire(conn)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
return s.listener.Close()
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const EndLine = "\r\n"
|
||||
|
||||
// Response like http.Response, but with any proto
|
||||
type Response struct {
|
||||
Status string
|
||||
StatusCode int
|
||||
Proto string
|
||||
Header textproto.MIMEHeader
|
||||
Body []byte
|
||||
Request *Request
|
||||
}
|
||||
|
||||
func (r Response) String() string {
|
||||
s := r.Proto + " " + r.Status + EndLine
|
||||
for k, v := range r.Header {
|
||||
s += k + ": " + v[0] + EndLine
|
||||
}
|
||||
s += EndLine
|
||||
if r.Body != nil {
|
||||
s += string(r.Body)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *Response) Write(w io.Writer) (err error) {
|
||||
_, err = w.Write([]byte(r.String()))
|
||||
return
|
||||
}
|
||||
|
||||
func ReadResponse(r *bufio.Reader) (*Response, error) {
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss := strings.SplitN(line, " ", 3)
|
||||
if len(ss) != 3 {
|
||||
return nil, errors.New("malformed response")
|
||||
}
|
||||
|
||||
res := &Response{
|
||||
Status: ss[1] + " " + ss[2],
|
||||
Proto: ss[0],
|
||||
}
|
||||
|
||||
res.StatusCode, err = strconv.Atoi(ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Header, err = tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := res.Header.Get("Content-Length"); val != "" {
|
||||
var i int
|
||||
i, err = strconv.Atoi(val)
|
||||
res.Body = make([]byte, i)
|
||||
if _, err = io.ReadAtLeast(r, res.Body, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Request like http.Request, but with any proto
|
||||
type Request struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string
|
||||
Header textproto.MIMEHeader
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (r *Request) String() string {
|
||||
s := r.Method + " " + r.URL.String() + " " + r.Proto + EndLine
|
||||
for k, v := range r.Header {
|
||||
s += k + ": " + v[0] + EndLine
|
||||
}
|
||||
s += EndLine
|
||||
if r.Body != nil {
|
||||
s += string(r.Body)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *Request) Write(w io.Writer) (err error) {
|
||||
_, err = w.Write([]byte(r.String()))
|
||||
return
|
||||
}
|
||||
|
||||
func ReadRequest(r *bufio.Reader) (*Request, error) {
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss := strings.SplitN(line, " ", 3)
|
||||
if len(ss) != 3 {
|
||||
return nil, fmt.Errorf("wrong request: %s", line)
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
Method: ss[0],
|
||||
Proto: ss[2],
|
||||
}
|
||||
|
||||
req.URL, err = url.Parse(ss[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header, err = tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := req.Header.Get("Content-Length"); val != "" {
|
||||
var i int
|
||||
i, err = strconv.Atoi(val)
|
||||
req.Body = make([]byte, i)
|
||||
if _, err = io.ReadAtLeast(r, req.Body, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, one, two interface{}) {
|
||||
if one != two {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
data := []byte(`RTSP/1.0 401 Unauthorized
|
||||
WWW-Authenticate: Digest realm="testrealm@host.com",
|
||||
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
|
||||
|
||||
`)
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
r := bufio.NewReader(buf)
|
||||
|
||||
res, err := ReadResponse(r)
|
||||
assert(t, err, nil)
|
||||
|
||||
assert(t, res.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
)
|
||||
|
||||
func NewAPI(address string) (*webrtc.API, error) {
|
||||
// for debug logs add to env: `PION_LOG_DEBUG=all`
|
||||
m := &webrtc.MediaEngine{}
|
||||
//if err := m.RegisterDefaultCodecs(); err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
if err := RegisterDefaultCodecs(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := &interceptor.Registry{}
|
||||
if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), nil
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
), err
|
||||
}
|
||||
|
||||
s := webrtc.SettingEngine{
|
||||
//LoggerFactory: customLoggerFactory{},
|
||||
}
|
||||
s.SetNetworkTypes([]webrtc.NetworkType{
|
||||
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
|
||||
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
|
||||
})
|
||||
|
||||
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
|
||||
s.SetICETCPMux(tcpMux)
|
||||
|
||||
return webrtc.NewAPI(
|
||||
webrtc.WithMediaEngine(m),
|
||||
webrtc.WithInterceptorRegistry(i),
|
||||
webrtc.WithSettingEngine(s),
|
||||
), nil
|
||||
}
|
||||
|
||||
func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
||||
PayloadType: 101, //111,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMU, 8000, 0, "", nil},
|
||||
PayloadType: 0,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMA, 8000, 0, "", nil},
|
||||
PayloadType: 8,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
|
||||
for _, codec := range []webrtc.RTPCodecParameters{
|
||||
// macOS Google Chrome 103.0.5060.134
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback},
|
||||
PayloadType: 96, //102,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
|
||||
PayloadType: 97, //125,
|
||||
}, {
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback},
|
||||
PayloadType: 98, //123,
|
||||
},
|
||||
// macOS Safari 15.1
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
|
||||
PayloadType: 99,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
|
||||
PayloadType: 100,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeOffer = "webrtc/offer"
|
||||
MsgTypeOfferComplete = "webrtc/offer-complete"
|
||||
MsgTypeAnswer = "webrtc/answer"
|
||||
MsgTypeCandidate = "webrtc/candidate"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
|
||||
Conn *webrtc.PeerConnection
|
||||
|
||||
medias []*streamer.Media
|
||||
tracks []*streamer.Track
|
||||
|
||||
receive int
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Conn) Init() {
|
||||
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate != nil {
|
||||
c.Fire(&streamer.Message{
|
||||
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
for _, track := range c.tracks {
|
||||
if track.Direction != streamer.DirectionRecvonly {
|
||||
continue
|
||||
}
|
||||
if track.Codec.PayloadType != uint8(remote.PayloadType()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for {
|
||||
packet, _, err := remote.ReadRTP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(packet.Payload) == 0 {
|
||||
continue
|
||||
}
|
||||
c.receive += len(packet.Payload)
|
||||
_ = track.WriteRTP(packet)
|
||||
}
|
||||
}
|
||||
|
||||
panic("something wrong")
|
||||
})
|
||||
|
||||
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
c.Fire(state)
|
||||
|
||||
// TODO: remove
|
||||
switch state {
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
c.Fire(streamer.StatePlaying)
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
c.Fire(streamer.StateNull)
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
_ = c.Conn.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
}
|
||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//for _, tr := range c.Conn.GetTransceivers() {
|
||||
// switch tr.Direction() {
|
||||
// case webrtc.RTPTransceiverDirectionSendonly:
|
||||
// // disable transceivers if we don't have track
|
||||
// // make direction=inactive
|
||||
// // don't really necessary, but anyway
|
||||
// if tr.Sender() == nil {
|
||||
// if err = tr.Stop(); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// case webrtc.RTPTransceiverDirectionRecvonly:
|
||||
// // TODO: change codecs list
|
||||
// caps := webrtc.RTPCodecCapability{
|
||||
// MimeType: webrtc.MimeTypePCMU,
|
||||
// ClockRate: 8000,
|
||||
// }
|
||||
// codecs := []webrtc.RTPCodecParameters{
|
||||
// {RTPCodecCapability: caps},
|
||||
// }
|
||||
// if err = tr.SetCodecPreferences(codecs); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
var sdAnswer webrtc.SessionDescription
|
||||
sdAnswer, err = c.Conn.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//var sd *sdp.SessionDescription
|
||||
//sd, err = sdAnswer.Unmarshal()
|
||||
//for _, media := range sd.MediaDescriptions {
|
||||
// if media.MediaName.Media != "audio" {
|
||||
// continue
|
||||
// }
|
||||
// for i, attr := range media.Attributes {
|
||||
// if attr.Key == "sendonly" {
|
||||
// attr.Key = "inactive"
|
||||
// media.Attributes[i] = attr
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//var b []byte
|
||||
//b, err = sd.Marshal()
|
||||
//sdAnswer.SDP = string(b)
|
||||
|
||||
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if complete {
|
||||
<-webrtc.GatheringCompletePromise(c.Conn)
|
||||
return c.Conn.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
return sdAnswer.SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetOffer(offer string) (err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
}
|
||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||
return
|
||||
}
|
||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||
c.medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) GetAnswer() (answer string, err error) {
|
||||
for _, tr := range c.Conn.GetTransceivers() {
|
||||
if tr.Direction() != webrtc.RTPTransceiverDirectionSendonly {
|
||||
continue
|
||||
}
|
||||
|
||||
// disable transceivers if we don't have track
|
||||
// make direction=inactive
|
||||
// don't really necessary, but anyway
|
||||
if tr.Sender() == nil {
|
||||
if err = tr.Stop(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sdAnswer webrtc.SessionDescription
|
||||
sdAnswer, err = c.Conn.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return sdAnswer.SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) remote() string {
|
||||
for _, trans := range c.Conn.GetTransceivers() {
|
||||
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
|
||||
return pair.Remote.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/stun"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func NewCandidate(address string) (string, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
||||
Network: "tcp",
|
||||
Address: host,
|
||||
Port: i,
|
||||
Component: ice.ComponentRTP,
|
||||
TCPType: ice.TCPTypePassive,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "candidate:" + cand.Marshal(), nil
|
||||
}
|
||||
|
||||
// GetPublicIP example from https://github.com/pion/stun
|
||||
func GetPublicIP() (net.IP, error) {
|
||||
c, err := stun.Dial("udp", "stun.l.google.com:19302")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res stun.Event
|
||||
|
||||
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
}
|
||||
|
||||
var xorAddr stun.XORMappedAddress
|
||||
if err = xorAddr.GetFrom(res.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xorAddr.IP, nil
|
||||
}
|
||||
|
||||
func MimeType(codec *streamer.Codec) string {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
return webrtc.MimeTypeH264
|
||||
case streamer.CodecH265:
|
||||
return webrtc.MimeTypeH265
|
||||
case streamer.CodecVP8:
|
||||
return webrtc.MimeTypeVP8
|
||||
case streamer.CodecVP9:
|
||||
return webrtc.MimeTypeVP9
|
||||
case streamer.CodecAV1:
|
||||
return webrtc.MimeTypeAV1
|
||||
case streamer.CodecPCMU:
|
||||
return webrtc.MimeTypePCMU
|
||||
case streamer.CodecPCMA:
|
||||
return webrtc.MimeTypePCMA
|
||||
case streamer.CodecOpus:
|
||||
return webrtc.MimeTypeOpus
|
||||
case streamer.CodecG722:
|
||||
return webrtc.MimeTypeG722
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v3"
|
||||
)
|
||||
|
||||
// Consumer
|
||||
|
||||
func (c *Conn) GetMedias() []*streamer.Media {
|
||||
return c.medias
|
||||
}
|
||||
|
||||
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
switch track.Direction {
|
||||
// send our track to WebRTC consumer
|
||||
case streamer.DirectionSendonly:
|
||||
codec := track.Codec
|
||||
|
||||
// webrtc.codecParametersFuzzySearch
|
||||
caps := webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
Channels: codec.Channels,
|
||||
ClockRate: codec.ClockRate,
|
||||
}
|
||||
|
||||
if codec.Name == streamer.CodecH264 {
|
||||
// don't know if this really neccessary
|
||||
// I have tested multiple browsers and H264 profile has no effect on anything
|
||||
caps.SDPFmtpLine = "packetization-mode=1;profile-level-id=42e01f"
|
||||
}
|
||||
|
||||
// important to use same streamID so JS will automatically
|
||||
// join two tracks as one source/stream
|
||||
trackLocal, err := webrtc.NewTrackLocalStaticRTP(
|
||||
caps, caps.MimeType[:5], "go2rtc",
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = c.Conn.AddTrack(trackLocal); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
c.send += packet.MarshalSize()
|
||||
return trackLocal.WriteRTP(packet)
|
||||
}
|
||||
|
||||
if codec.Name == streamer.CodecH264 {
|
||||
wrapper := h264.RTPPay(1200)
|
||||
push = wrapper(push)
|
||||
|
||||
if codec.PayloadType != 255 {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
}
|
||||
|
||||
track = track.Bind(push)
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
|
||||
// receive track from WebRTC consumer (microphone, backchannel, two way audio)
|
||||
case streamer.DirectionRecvonly:
|
||||
for _, tr := range c.Conn.GetTransceivers() {
|
||||
if tr.Mid() != media.MID {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := track.Codec
|
||||
caps := webrtc.RTPCodecCapability{
|
||||
MimeType: MimeType(codec),
|
||||
ClockRate: codec.ClockRate,
|
||||
Channels: codec.Channels,
|
||||
}
|
||||
codecs := []webrtc.RTPCodecParameters{
|
||||
{RTPCodecCapability: caps},
|
||||
}
|
||||
if err := tr.SetCodecPreferences(codecs); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.tracks = append(c.tracks, track)
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
panic("wrong direction")
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Conn) Push(msg interface{}) {
|
||||
if msg := msg.(*streamer.Message); msg != nil {
|
||||
if msg.Type == MsgTypeCandidate {
|
||||
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
|
||||
Candidate: msg.Value.(string),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||
v := map[string]interface{}{
|
||||
streamer.JSONType: "WebRTC server consumer",
|
||||
streamer.JSONRemoteAddr: c.remote(),
|
||||
}
|
||||
|
||||
if c.receive > 0 {
|
||||
v[streamer.JSONReceive] = c.receive
|
||||
}
|
||||
if c.send > 0 {
|
||||
v[streamer.JSONSend] = c.send
|
||||
}
|
||||
if c.UserAgent != "" {
|
||||
v[streamer.JSONUserAgent] = c.UserAgent
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/ice/v2"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
i, _ := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
||||
Network: "tcp",
|
||||
Address: "192.168.1.123",
|
||||
Port: 8555,
|
||||
Component: ice.ComponentRTP,
|
||||
TCPType: ice.TCPTypePassive,
|
||||
})
|
||||
a := i.Marshal()
|
||||
println(a)
|
||||
}
|
||||
|
||||
func TestPublicIP(t *testing.T) {
|
||||
ip, err := GetPublicIP()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ip)
|
||||
t.Logf("your public IP: %s", ip.String())
|
||||
}
|
||||
|
||||
func TestMedia(t *testing.T) {
|
||||
codec := webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
|
||||
},
|
||||
PayloadType: 96,
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: "video", Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(
|
||||
uint8(codec.PayloadType), codec.MimeType[6:], codec.ClockRate,
|
||||
codec.Channels, codec.SDPFmtpLine,
|
||||
)
|
||||
|
||||
sd := &sdp.SessionDescription{
|
||||
MediaDescriptions: []*sdp.MediaDescription{md},
|
||||
}
|
||||
data, err := sd.Marshal()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, data)
|
||||
}
|
||||
Reference in New Issue
Block a user