mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 20:52:08 +08:00
Compare commits
12 Commits
v0.1-beta.
...
v0.1-beta.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
04f1aa2900 | ||
![]() |
0dacdea1c3 | ||
![]() |
24082b1616 | ||
![]() |
7964b1743b | ||
![]() |
49773a1ece | ||
![]() |
c97a48a73f | ||
![]() |
e03231ebb4 | ||
![]() |
649525a842 | ||
![]() |
d411c1a25c | ||
![]() |
2f0bcf4ae0 | ||
![]() |
831c504cab | ||
![]() |
12925a6bc5 |
@@ -85,10 +85,15 @@ var wsHandlers = make(map[string]WSHandler)
|
|||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
|
name := r.URL.Query().Get("name")
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = src
|
||||||
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "PUT":
|
case "PUT":
|
||||||
streams.New(src, src)
|
streams.New(name, src)
|
||||||
return
|
return
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
streams.Delete(src)
|
streams.Delete(src)
|
||||||
|
@@ -8,6 +8,9 @@ import (
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("rtmp", handle)
|
streams.HandleFunc("rtmp", handle)
|
||||||
|
// RTMPT (flv over HTTP)
|
||||||
|
streams.HandleFunc("http", handle)
|
||||||
|
streams.HandleFunc("https", handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (streamer.Producer, error) {
|
func handle(url string) (streamer.Producer, error) {
|
||||||
|
@@ -84,10 +84,10 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// second try without backchannel, we need to reconnect
|
// second try without backchannel, we need to reconnect
|
||||||
|
conn.Backchannel = false
|
||||||
if err = conn.Dial(); err != nil {
|
if err = conn.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
conn.Backchannel = false
|
|
||||||
if err = conn.Describe(); err != nil {
|
if err = conn.Describe(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -64,6 +64,9 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
track := p.element.GetTrack(media, codec)
|
track := p.element.GetTrack(media, codec)
|
||||||
|
if track == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, t := range p.tracks {
|
for _, t := range p.tracks {
|
||||||
if track == t {
|
if track == t {
|
||||||
|
@@ -8,11 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NALUTypePFrame = 1
|
NALUTypePFrame = 1 // Coded slice of a non-IDR picture
|
||||||
NALUTypeIFrame = 5
|
NALUTypeIFrame = 5 // Coded slice of an IDR picture
|
||||||
NALUTypeSEI = 6
|
NALUTypeSEI = 6 // Supplemental enhancement information (SEI)
|
||||||
NALUTypeSPS = 7
|
NALUTypeSPS = 7 // Sequence parameter set
|
||||||
NALUTypePPS = 8
|
NALUTypePPS = 8 // Picture parameter set
|
||||||
|
NALUTypeAUD = 9 // Access unit delimiter
|
||||||
)
|
)
|
||||||
|
|
||||||
func NALUType(b []byte) byte {
|
func NALUType(b []byte) byte {
|
||||||
|
@@ -15,15 +15,14 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
sps, pps := GetParameterSet(track.Codec.FmtpLine)
|
||||||
ps := EncodeAVC(sps, pps)
|
ps := EncodeAVC(sps, pps)
|
||||||
|
|
||||||
var buffer []byte
|
buf := make([]byte, 0, 512*1024) // 512K
|
||||||
|
|
||||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
return func(packet *rtp.Packet) error {
|
return func(packet *rtp.Packet) error {
|
||||||
//nalUnitType := packet.Payload[0] & 0x1F
|
|
||||||
//fmt.Printf(
|
//fmt.Printf(
|
||||||
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n",
|
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v\n",
|
||||||
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
// track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp,
|
||||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber,
|
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||||
//)
|
//)
|
||||||
|
|
||||||
payload, err := depack.Unmarshal(packet.Payload)
|
payload, err := depack.Unmarshal(packet.Payload)
|
||||||
@@ -31,31 +30,54 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
|
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||||
// and every NALU will be sliced to multiple NALUs
|
if packet.Marker {
|
||||||
|
switch NALUType(payload) {
|
||||||
|
case NALUTypeSPS, NALUTypePPS:
|
||||||
|
buf = append(buf, payload...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
||||||
|
// Amcrest IP4M-1051: 9, 6, 1
|
||||||
|
switch NALUType(payload) {
|
||||||
|
case NALUTypeIFrame:
|
||||||
|
// fix IFrame without SPS,PPS
|
||||||
|
buf = append(buf, ps...)
|
||||||
|
case NALUTypeSEI, NALUTypeAUD:
|
||||||
|
// fix ffmpeg with transcoding first frame
|
||||||
|
i := int(4 + binary.BigEndian.Uint32(payload))
|
||||||
|
|
||||||
|
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
||||||
|
if i == len(payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = payload[i:]
|
||||||
|
|
||||||
|
if NALUType(payload) == NALUTypeIFrame {
|
||||||
|
buf = append(buf, ps...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect all NALs for Access Unit
|
||||||
if !packet.Marker {
|
if !packet.Marker {
|
||||||
buffer = append(buffer, payload...)
|
buf = append(buf, payload...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if buffer != nil {
|
if len(buf) > 0 {
|
||||||
payload = append(buffer, payload...)
|
payload = append(buf, payload...)
|
||||||
buffer = nil
|
buf = buf[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Printf("[AVC] %v, len: %d\n", Types(payload), len(payload))
|
//fmt.Printf(
|
||||||
|
// "[AVC] %v, len: %d, %v\n", Types(payload), len(payload),
|
||||||
switch NALUType(payload) {
|
// reflect.ValueOf(buf).Pointer() == reflect.ValueOf(payload).Pointer(),
|
||||||
case NALUTypeIFrame:
|
//)
|
||||||
payload = Join(ps, payload)
|
|
||||||
case NALUTypeSEI:
|
|
||||||
// ffmpeg with transcoding
|
|
||||||
i := 4 + binary.BigEndian.Uint32(payload)
|
|
||||||
payload = payload[i:]
|
|
||||||
if NALUType(payload) == NALUTypeIFrame {
|
|
||||||
payload = Join(ps, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clone := *packet
|
clone := *packet
|
||||||
clone.Version = RTPPacketVersionAVC
|
clone.Version = RTPPacketVersionAVC
|
||||||
|
@@ -61,8 +61,16 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
lqt, cqt = MakeTables(q)
|
lqt, cqt = MakeTables(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
||||||
|
// The maximum width is 2040 pixels.
|
||||||
w := uint16(packet.Payload[6]) << 3
|
w := uint16(packet.Payload[6]) << 3
|
||||||
h := uint16(packet.Payload[7]) << 3
|
h := uint16(packet.Payload[7]) << 3
|
||||||
|
|
||||||
|
// fix 2560x1920 and 2560x1440
|
||||||
|
if w == 512 && (h == 1920 || h == 1440) {
|
||||||
|
w = 2560
|
||||||
|
}
|
||||||
|
|
||||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||||
header = MakeHeaders(t, w, h, lqt, cqt)
|
header = MakeHeaders(t, w, h, lqt, cqt)
|
||||||
}
|
}
|
||||||
|
@@ -5,15 +5,24 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtmpt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
"github.com/deepch/vdk/codec/aacparser"
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Conn for RTMP and RTMPT (flv over HTTP)
|
||||||
|
type Conn interface {
|
||||||
|
Streams() (streams []av.CodecData, err error)
|
||||||
|
ReadPacket() (pkt av.Packet, err error)
|
||||||
|
Close() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
@@ -22,7 +31,7 @@ type Client struct {
|
|||||||
medias []*streamer.Media
|
medias []*streamer.Media
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
conn *rtmp.Conn
|
conn Conn
|
||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
receive int
|
receive int
|
||||||
@@ -33,7 +42,12 @@ func NewClient(uri string) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
func (c *Client) Dial() (err error) {
|
||||||
c.conn, err = rtmp.Dial(c.URI)
|
if strings.HasPrefix(c.URI, "http") {
|
||||||
|
c.conn, err = rtmpt.Dial(c.URI)
|
||||||
|
} else {
|
||||||
|
c.conn, err = rtmp.Dial(c.URI)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ func (c *Client) MarshalJSON() ([]byte, error) {
|
|||||||
v := map[string]interface{}{
|
v := map[string]interface{}{
|
||||||
streamer.JSONReceive: c.receive,
|
streamer.JSONReceive: c.receive,
|
||||||
streamer.JSONType: "RTMP client producer",
|
streamer.JSONType: "RTMP client producer",
|
||||||
streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
||||||
"url": c.URI,
|
"url": c.URI,
|
||||||
}
|
}
|
||||||
for i, media := range c.medias {
|
for i, media := range c.medias {
|
||||||
|
3
pkg/rtmpt/README.md
Normal file
3
pkg/rtmpt/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
100
pkg/rtmpt/rtmpt.go
Normal file
100
pkg/rtmpt/rtmpt.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package rtmpt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/format/flv/flvio"
|
||||||
|
"github.com/deepch/vdk/utils/bits/pio"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial(uri string) (*Conn, error) {
|
||||||
|
req, err := http.NewRequest("GET", uri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Conn{
|
||||||
|
conn: res.Body,
|
||||||
|
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
||||||
|
buf: make([]byte, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flags, n, err := flvio.ParseFileHeader(c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&flvio.FILE_HAS_VIDEO == 0 {
|
||||||
|
return nil, errors.New("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = c.reader.Discard(n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
conn io.ReadCloser
|
||||||
|
reader *bufio.Reader
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||||
|
for {
|
||||||
|
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []av.CodecData{stream}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||||
|
for {
|
||||||
|
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
|
if err != nil {
|
||||||
|
return av.Packet{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return av.Packet{
|
||||||
|
Idx: 0,
|
||||||
|
Data: tag.Data,
|
||||||
|
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
||||||
|
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
||||||
|
Time: flvio.TsToTime(ts),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() (err error) {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
@@ -362,21 +362,25 @@ func (c *Conn) SetupMedia(
|
|||||||
var res *tcp.Response
|
var res *tcp.Response
|
||||||
res, err = c.Do(req)
|
res, err = c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Dahua VTO2111D fail on this step because of backchannel
|
// some Dahua/Amcrest cameras fail here because two simultaneous
|
||||||
|
// backchannel connections
|
||||||
if c.Backchannel {
|
if c.Backchannel {
|
||||||
if err = c.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Backchannel = false
|
c.Backchannel = false
|
||||||
if err = c.Describe(); err != nil {
|
if err := c.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res, err = c.Do(req)
|
if err := c.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newMedia := range c.Medias {
|
||||||
|
if newMedia.Control == media.Control {
|
||||||
|
return c.SetupMedia(newMedia, newMedia.Codecs[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Session == "" {
|
if c.Session == "" {
|
||||||
|
@@ -70,6 +70,7 @@
|
|||||||
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
||||||
'<a href="api/frame.mp4?src={name}">frame</a>',
|
'<a href="api/frame.mp4?src={name}">frame</a>',
|
||||||
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
|
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
|
||||||
|
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
|
||||||
'<a href="api/streams?src={name}">info</a>',
|
'<a href="api/streams?src={name}">info</a>',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -51,11 +51,6 @@
|
|||||||
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
|
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
|
||||||
} else if (msg.type === 'webrtc/answer') {
|
} else if (msg.type === 'webrtc/answer') {
|
||||||
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
||||||
pc.getTransceivers().forEach(t => {
|
|
||||||
if (t.receiver.track.kind === 'audio') {
|
|
||||||
t.currentDirection
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user