Created
July 9, 2025 07:58
-
-
Save SijmenHuizenga/cf053d01892694b802687835bc1e8d61 to your computer and use it in GitHub Desktop.
Golang Pion RTP JPEG payloader RFC 2435 Prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/binary" | |
"fmt" | |
) | |
// JPEGPayloader payloads JPEG frames according to RFC 2435 | |
// Tested a little bit on 1 camera, but may very well not be fully compliant. It's like a prototype. | |
type JPEGPayloader struct { | |
Width int // Width in pixels, used to fill the header | |
Height int // Height in pixels, used to fill the header | |
Quality byte // “quality” / quant-table ID (0 – 127). 0 works with almost every decoder | |
} | |
// Payload fragments one JPEG frame into one-or-more RTP-JPEG payloads. | |
// This payloader assumes the packetizer.Packetize() is called with the output bytes of below encodeJPEG func. | |
func (p *JPEGPayloader) Payload(mtu uint16, jpegContent []byte) [][]byte { | |
var out [][]byte | |
if len(jpegContent) == 0 || mtu <= 8 { // 8 = JPEG header size we add | |
return out | |
} | |
w8 := byte(p.Width / 8) // 8-pixel multiples per RFC 2435 | |
h8 := byte(p.Height / 8) | |
const hdrLen = 8 | |
baseHdr := make([]byte, hdrLen) | |
baseHdr[0] = 0 // tspec: progressive (non-interlaced) frame | |
/* bytes 1-3 (offset) are per-packet */ | |
baseHdr[4] = 1 // 4:2:0, no restart markers | |
baseHdr[5] = p.Quality // Q | |
baseHdr[6] = w8 // Width /8 | |
baseHdr[7] = h8 // Height/8 | |
payloadBytes, err := stripHeaders(jpegContent) | |
if err != nil { | |
return out // error, no payload | |
} | |
maxFrag := int(mtu) - hdrLen | |
for off := 0; off < len(payloadBytes); off += maxFrag { | |
end := off + maxFrag | |
if end > len(payloadBytes) { | |
end = len(payloadBytes) | |
} | |
frag := payloadBytes[off:end] | |
// header copy + offset | |
hdr := make([]byte, hdrLen) | |
copy(hdr, baseHdr) | |
binary.BigEndian.PutUint32(hdr[0:4], uint32(off)) // write 32 bits then drop MSB | |
hdr[0] = 0 // restore top byte (tspec) | |
packet := append(hdr, frag...) | |
out = append(out, packet) | |
} | |
return out | |
} | |
// scanSegment returns the entropy-coded scan bytes suitable for RFC 2435: | |
// - Start = just after the SOS marker header | |
// - End = right before the EOI marker | |
func stripHeaders(jpeg []byte) ([]byte, error) { | |
const ( | |
mSOS = 0xDA // Start-Of-Scan marker code (after 0xFF) | |
mEOI = 0xD9 // End-Of-Image | |
) | |
// --- find the SOS marker ----------------------------------------------- | |
pos := bytes.Index(jpeg, []byte{0xFF, mSOS}) | |
if pos < 0 || pos+4 > len(jpeg) { | |
return nil, fmt.Errorf("SOS not found") | |
} | |
// Two bytes right after the marker give the segment length **including** | |
// those two bytes but **excluding** the 0xFFDA itself. | |
segLen := int(jpeg[pos+2])<<8 | int(jpeg[pos+3]) | |
start := pos + 2 + segLen | |
if start > len(jpeg) { | |
return nil, fmt.Errorf("SOS length out of range") | |
} | |
// --- trim a trailing EOI if present ------------------------------------- | |
end := len(jpeg) | |
if end >= 2 && jpeg[end-2] == 0xFF && jpeg[end-1] == mEOI { | |
end -= 2 | |
} | |
return jpeg[start:end], nil | |
} | |
func (ch *SeekCameraPipeline) encodeJPEG(img image.Image) ([]byte, error) { | |
buf := new(bytes.Buffer) | |
opts := &jpeg.Options{Quality: jpegQuality} | |
if err := jpeg.Encode(buf, img, opts); err != nil { | |
return nil, err | |
} | |
return buf.Bytes(), nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment