install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,5 @@
## Useful links
- 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,59 @@
package mjpeg
import (
"io"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
type Consumer struct {
core.Connection
wr *core.WriteBuffer
}
func NewConsumer() *Consumer {
medias := []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecJPEG},
{Name: core.CodecRAW},
},
},
}
wr := core.NewWriteBuffer(nil)
return &Consumer{
Connection: core.Connection{
ID: core.NewID(),
FormatName: "mjpeg",
Medias: medias,
Transport: wr,
},
wr: wr,
}
}
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
sender := core.NewSender(media, track.Codec)
sender.Handler = func(packet *rtp.Packet) {
if n, err := c.wr.Write(packet.Payload); err == nil {
c.Send += n
}
}
if track.Codec.IsRTP() {
sender.Handler = RTPDepay(sender.Handler)
} else if track.Codec.Name == core.CodecRAW {
sender.Handler = Encoder(track.Codec, 0, sender.Handler)
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
func (c *Consumer) WriteTo(wr io.Writer) (int64, error) {
return c.wr.WriteTo(wr)
}
@@ -0,0 +1,100 @@
package mjpeg
import (
"bytes"
"image/jpeg"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/y4m"
"github.com/pion/rtp"
)
func FixJPEG(b []byte) []byte {
// skip non-JPEG
if len(b) < 10 || b[0] != 0xFF || b[1] != markerSOI {
return b
}
// skip JPEG without app marker
if b[2] == 0xFF && b[3] == markerDQT {
return b
}
switch string(b[6:10]) {
case "JFIF", "Exif":
// skip if header OK for imghdr library
// - https://docs.python.org/3/library/imghdr.html
return b
case "AVI1":
// adds DHT tables to JPEG file before SOS marker
// useful when you want to save a JPEG frame from an MJPEG stream
// - https://github.com/image-rs/jpeg-decoder/issues/76
// - https://github.com/pion/mediadevices/pull/493
// - https://bugzilla.mozilla.org/show_bug.cgi?id=963907#c18
return InjectDHT(b)
}
// reencode JPEG if it has wrong header
//
// for example, this app produce "bad" images:
// https://github.com/jacksonliam/mjpg-streamer
//
// and they can't be uploaded to the Telegram servers:
// {"ok":false,"error_code":400,"description":"Bad Request: IMAGE_PROCESS_FAILED"}
img, err := jpeg.Decode(bytes.NewReader(b))
if err != nil {
return b
}
buf := bytes.NewBuffer(nil)
if err = jpeg.Encode(buf, img, nil); err != nil {
return b
}
return buf.Bytes()
}
// Encoder convert YUV frame to Img.
// Support skipping empty frames, for example if USB cam needs time to start.
func Encoder(codec *core.Codec, skipEmpty int, handler core.HandlerFunc) core.HandlerFunc {
newImage := y4m.NewImage(codec.FmtpLine)
return func(packet *rtp.Packet) {
img := newImage(packet.Payload)
if skipEmpty != 0 && y4m.HasSameColor(img) {
skipEmpty--
return
}
buf := bytes.NewBuffer(nil)
if err := jpeg.Encode(buf, img, nil); err != nil {
return
}
clone := *packet
clone.Payload = buf.Bytes()
handler(&clone)
}
}
const dhtSize = 432 // known size for 4 default tables
func InjectDHT(b []byte) []byte {
if bytes.Index(b, []byte{0xFF, markerDHT}) > 0 {
return b // already exist
}
i := bytes.Index(b, []byte{0xFF, markerSOS})
if i < 0 {
return b
}
dht := make([]byte, 0, dhtSize)
dht = MakeHuffmanHeaders(dht)
tmp := make([]byte, len(b)+dhtSize)
copy(tmp, b[:i])
copy(tmp[i:], dht)
copy(tmp[i+dhtSize:], b[i:])
return tmp
}
+10
View File
@@ -0,0 +1,10 @@
package mjpeg
const (
markerSOF = 0xC0 // Start Of Frame (Baseline Sequential)
markerSOI = 0xD8 // Start Of Image
markerEOI = 0xD9 // End Of Image
markerSOS = 0xDA // Start Of Scan
markerDQT = 0xDB // Define Quantization Table
markerDHT = 0xC4 // Define Huffman Table
)
@@ -0,0 +1,13 @@
package mjpeg
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRFC2435(t *testing.T) {
lqt, cqt := MakeTables(71)
require.Equal(t, byte(9), lqt[0])
require.Equal(t, byte(10), cqt[0])
}
@@ -0,0 +1,211 @@
package mjpeg
// RFC 2435. Appendix A
// don't know why two tables are not respect RFC
// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpdec_jpeg.c
var jpeg_luma_quantizer = [64]byte{
16, 11, 12, 14, 12, 10, 16, 14,
13, 14, 18, 17, 16, 19, 24, 40,
26, 24, 22, 22, 24, 49, 35, 37,
29, 40, 58, 51, 61, 60, 57, 51,
56, 55, 64, 72, 92, 78, 64, 68,
87, 69, 55, 56, 80, 109, 81, 87,
95, 98, 103, 104, 103, 62, 77, 113,
121, 112, 100, 120, 92, 101, 103, 99,
}
var jpeg_chroma_quantizer = [64]byte{
17, 18, 18, 24, 21, 24, 47, 26,
26, 47, 99, 66, 56, 66, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
}
func MakeTables(q byte) (lqt, cqt []byte) {
var factor int
switch {
case q < 1:
factor = 1
case q > 99:
factor = 99
default:
factor = int(q)
}
if q < 50 {
factor = 5000 / factor
} else {
factor = 200 - factor*2
}
lqt = make([]byte, 64)
cqt = make([]byte, 64)
for i := 0; i < 64; i++ {
lq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100
cq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100
/* Limit the quantizers to 1 <= q <= 255 */
switch {
case lq < 1:
lqt[i] = 1
case lq > 255:
lqt[i] = 255
default:
lqt[i] = byte(lq)
}
switch {
case cq < 1:
cqt[i] = 1
case cq > 255:
cqt[i] = 255
default:
cqt[i] = byte(cq)
}
}
return
}
// RFC 2435. Appendix B
var lum_dc_codelens = []byte{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
}
var lum_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var lum_ac_codelens = []byte{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
}
var lum_ac_symbols = []byte{
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
var chm_dc_codelens = []byte{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
}
var chm_dc_symbols = []byte{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
}
var chm_ac_codelens = []byte{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
}
var chm_ac_symbols = []byte{
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,
0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,
0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,
0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,
0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,
0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa,
}
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
p = append(p, 0xFF, markerSOI)
p = MakeQuantHeader(p, lqt, 0)
p = MakeQuantHeader(p, cqt, 1)
if t == 0 {
t = 0x21 // hsamp = 2, vsamp = 1
} else {
t = 0x22 // hsamp = 2, vsamp = 2
}
p = append(p, 0xFF, markerSOF,
0, 17, // size
8, // bits per component
byte(h>>8), byte(h&0xFF),
byte(w>>8), byte(w&0xFF),
3, // number of components
0, // comp 0
t,
0, // quant table 0
1, // comp 1
0x11, // hsamp = 1, vsamp = 1
1, // quant table 1
2, // comp 2
0x11, // hsamp = 1, vsamp = 1
1, // quant table 1
)
p = MakeHuffmanHeaders(p)
return append(p, 0xFF, markerSOS,
0, 12, // size
3, // 3 components
0, // comp 0
0, // huffman table 0
1, // comp 1
0x11, // huffman table 1
2, // comp 2
0x11, // huffman table 1
0, // first DCT coeff
63, // last DCT coeff
0, // sucessive approx
)
}
func MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {
p = append(p, 0xFF, markerDQT, 0, 67, tableNo)
return append(p, qt...)
}
func MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {
p = append(p, 0xFF, markerDHT,
0, byte(3+len(codelens)+len(symbols)), // size
(tableClass<<4)|tableNo,
)
p = append(p, codelens...)
return append(p, symbols...)
}
func MakeHuffmanHeaders(p []byte) []byte {
p = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)
p = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)
p = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)
p = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)
return p
}
+220
View File
@@ -0,0 +1,220 @@
package mjpeg
import (
"bytes"
"encoding/binary"
"image"
"image/jpeg"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
buf := make([]byte, 0, 512*1024) // 512K
return func(packet *rtp.Packet) {
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", 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 len(buf) == 0 {
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 sizes more than 2040
switch {
// 512x1920 512x1440
case w == cutSize(2560) && (h == 1920 || h == 1440):
w = 2560
// 1792x112
case w == cutSize(3840) && h == cutSize(2160):
w = 3840
h = 2160
// 256x1296
case w == cutSize(2304) && h == 1296:
w = 2304
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
buf = append(buf, b...)
if !packet.Marker {
return
}
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
buf = append(buf, 0xFF, 0xD9)
}
clone := *packet
clone.Payload = buf
buf = buf[:0] // clear buffer
handlerFunc(&clone)
}
}
func cutSize(size uint16) uint16 {
return ((size >> 3) & 0xFF) << 3
}
func RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc {
const packetSize = 1436
sequencer := rtp.NewRandomSequencer()
return func(packet *rtp.Packet) {
// reincode image to more common form
p, err := Transcode(packet.Payload)
if err != nil {
return
}
h1 := make([]byte, 8)
h1[4] = 1 // Type
h1[5] = 255 // Q
// MBZ=0, Precision=0, Length=128
h2 := make([]byte, 4, 132)
h2[3] = 128
var jpgData []byte
for jpgData == nil {
// 2 bytes h1
if p[0] != 0xFF {
return
}
size := binary.BigEndian.Uint16(p[2:]) + 2
// 2 bytes payload size (include 2 bytes)
switch p[1] {
case 0xD8: // 0. Start Of Image (size=0)
p = p[2:]
continue
case 0xDB: // 1. Define Quantization Table (size=130)
for i := uint16(4 + 1); i < size; i += 1 + 64 {
h2 = append(h2, p[i:i+64]...)
}
case 0xC0: // 2. Start Of Frame (size=15)
if p[4] != 8 {
return
}
h := binary.BigEndian.Uint16(p[5:])
w := binary.BigEndian.Uint16(p[7:])
h1[6] = uint8(w >> 3)
h1[7] = uint8(h >> 3)
case 0xC4: // 3. Define Huffman Table (size=416)
case 0xDA: // 4. Start Of Scan (size=10)
jpgData = p[size:]
}
p = p[size:]
}
offset := 0
p = make([]byte, 0)
for jpgData != nil {
p = p[:0]
if offset > 0 {
h1[1] = byte(offset >> 16)
h1[2] = byte(offset >> 8)
h1[3] = byte(offset)
p = append(p, h1...)
} else {
p = append(p, h1...)
p = append(p, h2...)
}
dataLen := packetSize - len(p)
if dataLen < len(jpgData) {
p = append(p, jpgData[:dataLen]...)
jpgData = jpgData[dataLen:]
offset += dataLen
} else {
p = append(p, jpgData...)
jpgData = nil
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: jpgData == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: p,
}
handlerFunc(&clone)
}
}
}
func Transcode(b []byte) ([]byte, error) {
img, err := jpeg.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
wh := img.Bounds().Size()
w := wh.X
h := wh.Y
if w > 2040 {
w = 2040
} else if w&3 > 0 {
w &= 3
}
if h > 2040 {
h = 2040
} else if h&3 > 0 {
h &= 3
}
if w != wh.X || h != wh.Y {
x0 := (wh.X - w) / 2
y0 := (wh.Y - h) / 2
rect := image.Rect(x0, y0, x0+w, y0+h)
img = img.(*image.YCbCr).SubImage(rect)
}
buf := bytes.NewBuffer(nil)
if err = jpeg.Encode(buf, img, nil); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
@@ -0,0 +1,38 @@
package mjpeg
import (
"io"
"net/http"
"strconv"
)
func NewWriter(w io.Writer) io.Writer {
h := w.(http.ResponseWriter).Header()
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
return &writer{wr: w, buf: []byte(header)}
}
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
type writer struct {
wr io.Writer
buf []byte
}
func (w *writer) Write(p []byte) (n int, err error) {
w.buf = w.buf[:len(header)]
w.buf = append(w.buf, strconv.Itoa(len(p))...)
w.buf = append(w.buf, "\r\n\r\n"...)
w.buf = append(w.buf, p...)
w.buf = append(w.buf, "\r\n"...)
// Chrome bug: mjpeg image always shows the second to last image
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
if _, err = w.wr.Write(w.buf); err != nil {
return 0, err
}
w.wr.(http.Flusher).Flush()
return len(p), nil
}