Skip to content

Instantly share code, notes, and snippets.

@SijmenHuizenga
Created July 9, 2025 07:58
Show Gist options
  • Save SijmenHuizenga/cf053d01892694b802687835bc1e8d61 to your computer and use it in GitHub Desktop.
Save SijmenHuizenga/cf053d01892694b802687835bc1e8d61 to your computer and use it in GitHub Desktop.
Golang Pion RTP JPEG payloader RFC 2435 Prototype
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