Add support HTTP JPEG and MJPEG sources
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
|
||||
- https://www.rfc-editor.org/rfc/rfc2435
|
||||
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
|
||||
- https://mjpeg.sanford.io/
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
closed bool
|
||||
res *http.Response
|
||||
|
||||
track *streamer.Track
|
||||
}
|
||||
|
||||
func NewClient(res *http.Response) *Client {
|
||||
codec := &streamer.Codec{
|
||||
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
|
||||
}
|
||||
return &Client{
|
||||
res: res,
|
||||
track: streamer.NewTrack(codec, streamer.DirectionSendonly),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetMedias() []*streamer.Media {
|
||||
return []*streamer.Media{{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{c.track.Codec},
|
||||
}}
|
||||
}
|
||||
|
||||
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||
return c.track
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
ct := c.res.Header.Get("Content-Type")
|
||||
|
||||
if ct == "image/jpeg" {
|
||||
return c.startJPEG()
|
||||
}
|
||||
|
||||
// added in go1.18
|
||||
if _, s, ok := strings.Cut(ct, "boundary="); ok {
|
||||
return c.startMJPEG(s)
|
||||
}
|
||||
|
||||
return errors.New("wrong Content-Type: " + ct)
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startJPEG() error {
|
||||
buf, err := io.ReadAll(c.res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
|
||||
req := c.res.Request
|
||||
|
||||
for !c.closed {
|
||||
res, err := tcp.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New("wrong status: " + res.Status)
|
||||
}
|
||||
|
||||
buf, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet = &rtp.Packet{Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) startMJPEG(boundary string) error {
|
||||
boundary = "--" + boundary
|
||||
|
||||
r := bufio.NewReader(c.res.Body)
|
||||
tp := textproto.NewReader(r)
|
||||
|
||||
for !c.closed {
|
||||
s, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s != boundary {
|
||||
return errors.New("wrong boundary: " + s)
|
||||
}
|
||||
|
||||
header, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s = header.Get("Content-Length")
|
||||
if s == "" {
|
||||
return errors.New("no content length")
|
||||
}
|
||||
|
||||
size, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
if _, err = io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet := &rtp.Packet{Payload: buf}
|
||||
_ = c.track.WriteRTP(packet)
|
||||
|
||||
if _, err = r.Discard(2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+7
-62
@@ -26,70 +26,15 @@ func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
var header, payload []byte
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
//fmt.Printf(
|
||||
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
|
||||
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
|
||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||
//)
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||
b := packet.Payload
|
||||
|
||||
// 3.1. JPEG header
|
||||
t := b[4]
|
||||
|
||||
// 3.1.7. Restart Marker header
|
||||
if 64 <= t && t <= 127 {
|
||||
b = b[12:] // skip it
|
||||
} else {
|
||||
b = b[8:]
|
||||
}
|
||||
|
||||
if header == nil {
|
||||
var lqt, cqt []byte
|
||||
|
||||
// 3.1.8. Quantization Table header
|
||||
q := packet.Payload[5]
|
||||
if q >= 128 {
|
||||
lqt = b[4:68]
|
||||
cqt = b[68:132]
|
||||
b = b[132:]
|
||||
} else {
|
||||
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
|
||||
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)
|
||||
header = MakeHeaders(t, w, h, lqt, cqt)
|
||||
}
|
||||
|
||||
// 3.1.9. JPEG Payload
|
||||
payload = append(payload, b...)
|
||||
|
||||
if packet.Marker {
|
||||
b = append(header, payload...)
|
||||
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||
b = append(b, 0xFF, 0xD9)
|
||||
}
|
||||
c.Fire(b)
|
||||
|
||||
header = nil
|
||||
payload = nil
|
||||
}
|
||||
|
||||
c.Fire(packet.Payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package mjpeg
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
var header, payload []byte
|
||||
|
||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||
return func(packet *rtp.Packet) error {
|
||||
//fmt.Printf(
|
||||
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
|
||||
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
|
||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
||||
//)
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||
b := packet.Payload
|
||||
|
||||
// 3.1. JPEG header
|
||||
t := b[4]
|
||||
|
||||
// 3.1.7. Restart Marker header
|
||||
if 64 <= t && t <= 127 {
|
||||
b = b[12:] // skip it
|
||||
} else {
|
||||
b = b[8:]
|
||||
}
|
||||
|
||||
if header == nil {
|
||||
var lqt, cqt []byte
|
||||
|
||||
// 3.1.8. Quantization Table header
|
||||
q := packet.Payload[5]
|
||||
if q >= 128 {
|
||||
lqt = b[4:68]
|
||||
cqt = b[68:132]
|
||||
b = b[132:]
|
||||
} else {
|
||||
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
|
||||
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)
|
||||
header = MakeHeaders(t, w, h, lqt, cqt)
|
||||
}
|
||||
|
||||
// 3.1.9. JPEG Payload
|
||||
payload = append(payload, b...)
|
||||
|
||||
if !packet.Marker {
|
||||
return nil
|
||||
}
|
||||
|
||||
b = append(header, payload...)
|
||||
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||
b = append(b, 0xFF, 0xD9)
|
||||
}
|
||||
|
||||
header = nil
|
||||
payload = nil
|
||||
|
||||
packet.Payload = b
|
||||
return push(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user