From 262a571a841312ad49cc806f19b60e8490e1985b Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 17 Mar 2026 02:30:42 +0100 Subject: [PATCH] add webcodecs support --- internal/webcodecs/webcodecs.go | 45 ++++++ main.go | 2 + pkg/webcodecs/consumer.go | 257 ++++++++++++++++++++++++++++++++ www/links.html | 4 + www/video-rtc.js | 241 +++++++++++++++++++++++++++++- www/video-stream.js | 1 + 6 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 internal/webcodecs/webcodecs.go create mode 100644 pkg/webcodecs/consumer.go diff --git a/internal/webcodecs/webcodecs.go b/internal/webcodecs/webcodecs.go new file mode 100644 index 00000000..57eecbb7 --- /dev/null +++ b/internal/webcodecs/webcodecs.go @@ -0,0 +1,45 @@ +package webcodecs + +import ( + "errors" + + "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/api/ws" + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/webcodecs" + "github.com/rs/zerolog" +) + +func Init() { + log = app.GetLogger("webcodecs") + + ws.HandleFunc("webcodecs", handlerWS) +} + +var log zerolog.Logger + +func handlerWS(tr *ws.Transport, msg *ws.Message) error { + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) + if stream == nil { + return errors.New(api.StreamNotFound) + } + + cons := webcodecs.NewConsumer(nil) + cons.WithRequest(tr.Request) + + if err := stream.AddConsumer(cons); err != nil { + log.Debug().Err(err).Msg("[webcodecs] add consumer") + return err + } + + tr.Write(&ws.Message{Type: "webcodecs", Value: cons.GetInitInfo()}) + + go cons.WriteTo(tr.Writer()) + + tr.OnClose(func() { + stream.RemoveConsumer(cons) + }) + + return nil +} diff --git a/main.go b/main.go index 0ccc231a..a3724983 100644 --- a/main.go +++ b/main.go @@ -43,6 +43,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/tapo" "github.com/AlexxIT/go2rtc/internal/tuya" "github.com/AlexxIT/go2rtc/internal/v4l2" + "github.com/AlexxIT/go2rtc/internal/webcodecs" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" "github.com/AlexxIT/go2rtc/internal/wyoming" @@ -72,6 +73,7 @@ func main() { {"webrtc", webrtc.Init}, // webrtc source, WebRTC server // Main API {"mp4", mp4.Init}, // MP4 API + {"webcodecs", webcodecs.Init}, // WebCodecs API {"hls", hls.Init}, // HLS API {"mjpeg", mjpeg.Init}, // MJPEG API {"webp", webp.Init}, // WebP API diff --git a/pkg/webcodecs/consumer.go b/pkg/webcodecs/consumer.go new file mode 100644 index 00000000..168351cf --- /dev/null +++ b/pkg/webcodecs/consumer.go @@ -0,0 +1,257 @@ +package webcodecs + +import ( + "encoding/binary" + "errors" + "io" + "sync" + + "github.com/AlexxIT/go2rtc/pkg/aac" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h264/annexb" + "github.com/AlexxIT/go2rtc/pkg/h265" + "github.com/pion/rtp" +) + +// Binary frame header (9 bytes): +// Byte 0: flags (bit7=video, bit6=keyframe, bits0-5=trackID) +// Byte 1-4: timestamp (uint32 BE) +// Byte 5-8: payload length (uint32 BE) +// Byte 9+: payload + +const headerSize = 9 + +type Consumer struct { + core.Connection + wr *core.WriteBuffer + mu sync.Mutex + start bool +} + +type InitInfo struct { + Video *VideoInfo `json:"video,omitempty"` + Audio *AudioInfo `json:"audio,omitempty"` +} + +type VideoInfo struct { + Codec string `json:"codec"` +} + +type AudioInfo struct { + Codec string `json:"codec"` + SampleRate int `json:"sampleRate"` + Channels int `json:"channels"` +} + +func NewConsumer(medias []*core.Media) *Consumer { + if medias == nil { + medias = []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + {Name: core.CodecH265}, + }, + }, + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecAAC}, + {Name: core.CodecOpus}, + {Name: core.CodecPCMA}, + {Name: core.CodecPCMU}, + }, + }, + } + } + + wr := core.NewWriteBuffer(nil) + return &Consumer{ + Connection: core.Connection{ + ID: core.NewID(), + FormatName: "webcodecs", + Medias: medias, + Transport: wr, + }, + wr: wr, + } +} + +func buildFrame(flags byte, timestamp uint32, payload []byte) []byte { + msg := make([]byte, headerSize+len(payload)) + msg[0] = flags + binary.BigEndian.PutUint32(msg[1:5], timestamp) + binary.BigEndian.PutUint32(msg[5:9], uint32(len(payload))) + copy(msg[headerSize:], payload) + return msg +} + +func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error { + trackID := byte(len(c.Senders)) + + codec := track.Codec.Clone() + handler := core.NewSender(media, codec) + + switch track.Codec.Name { + case core.CodecH264: + handler.Handler = func(packet *rtp.Packet) { + keyframe := h264.IsKeyframe(packet.Payload) + if !c.start { + if !keyframe { + return + } + c.start = true + } + + payload := annexb.DecodeAVCC(packet.Payload, true) + flags := byte(0x80) | trackID // video flag + if keyframe { + flags |= 0x40 // keyframe flag + } + + c.mu.Lock() + msg := buildFrame(flags, packet.Timestamp, payload) + if n, err := c.wr.Write(msg); err == nil { + c.Send += n + } + c.mu.Unlock() + } + + if track.Codec.IsRTP() { + handler.Handler = h264.RTPDepay(track.Codec, handler.Handler) + } else { + handler.Handler = h264.RepairAVCC(track.Codec, handler.Handler) + } + + case core.CodecH265: + handler.Handler = func(packet *rtp.Packet) { + keyframe := h265.IsKeyframe(packet.Payload) + if !c.start { + if !keyframe { + return + } + c.start = true + } + + payload := annexb.DecodeAVCC(packet.Payload, true) + flags := byte(0x80) | trackID // video flag + if keyframe { + flags |= 0x40 // keyframe flag + } + + c.mu.Lock() + msg := buildFrame(flags, packet.Timestamp, payload) + if n, err := c.wr.Write(msg); err == nil { + c.Send += n + } + c.mu.Unlock() + } + + if track.Codec.IsRTP() { + handler.Handler = h265.RTPDepay(track.Codec, handler.Handler) + } else { + handler.Handler = h265.RepairAVCC(track.Codec, handler.Handler) + } + + default: + handler.Handler = func(packet *rtp.Packet) { + if !c.start { + return + } + + flags := trackID // audio flag (bit7=0) + + c.mu.Lock() + msg := buildFrame(flags, packet.Timestamp, packet.Payload) + if n, err := c.wr.Write(msg); err == nil { + c.Send += n + } + c.mu.Unlock() + } + + switch track.Codec.Name { + case core.CodecAAC: + if track.Codec.IsRTP() { + handler.Handler = aac.RTPDepay(handler.Handler) + } + case core.CodecOpus, core.CodecPCMA, core.CodecPCMU: + // pass through directly — WebCodecs decodes these natively + default: + handler.Handler = nil + } + } + + if handler.Handler == nil { + s := "webcodecs: unsupported codec: " + track.Codec.String() + println(s) + return errors.New(s) + } + + handler.HandleRTP(track) + c.Senders = append(c.Senders, handler) + + return nil +} + +func (c *Consumer) GetInitInfo() *InitInfo { + info := &InitInfo{} + + for _, sender := range c.Senders { + codec := sender.Codec + switch codec.Name { + case core.CodecH264: + info.Video = &VideoInfo{ + Codec: "avc1." + h264.GetProfileLevelID(codec.FmtpLine), + } + case core.CodecH265: + info.Video = &VideoInfo{ + Codec: "hvc1.1.6.L153.B0", + } + case core.CodecAAC: + channels := int(codec.Channels) + if channels == 0 { + channels = 1 + } + info.Audio = &AudioInfo{ + Codec: "mp4a.40.2", + SampleRate: int(codec.ClockRate), + Channels: channels, + } + case core.CodecOpus: + channels := int(codec.Channels) + if channels == 0 { + channels = 2 + } + info.Audio = &AudioInfo{ + Codec: "opus", + SampleRate: int(codec.ClockRate), + Channels: channels, + } + case core.CodecPCMA: + info.Audio = &AudioInfo{ + Codec: "alaw", + SampleRate: int(codec.ClockRate), + Channels: 1, + } + case core.CodecPCMU: + info.Audio = &AudioInfo{ + Codec: "ulaw", + SampleRate: int(codec.ClockRate), + Channels: 1, + } + } + } + + return info +} + +func (c *Consumer) WriteTo(wr io.Writer) (int64, error) { + if len(c.Senders) == 1 && c.Senders[0].Codec.IsAudio() { + c.start = true + } + + return c.wr.WriteTo(wr) +} diff --git a/www/links.html b/www/links.html index 8d774645..336cf85d 100644 --- a/www/links.html +++ b/www/links.html @@ -340,6 +340,10 @@ Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx MSE stream Browsers: Chrome, Firefox, Safari Mac/iPad / Codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome/Firefox +