diff --git a/examples/gstreamer-receive/gst/gst.go b/examples/gstreamer-receive/gst/gst.go index 2a0f14d5..2d9a906c 100644 --- a/examples/gstreamer-receive/gst/gst.go +++ b/examples/gstreamer-receive/gst/gst.go @@ -32,6 +32,8 @@ func CreatePipeline(codec webrtc.TrackType) *Pipeline { pipelineStr += ", payload=96, encoding-name=OPUS ! rtpopusdepay ! decodebin ! autoaudiosink" case webrtc.VP9: pipelineStr += " ! rtpvp9depay ! decodebin ! autovideosink" + case webrtc.H264: + pipelineStr += " ! rtph264depay ! decodebin ! autovideosink" default: panic("Unhandled codec " + codec.String()) } diff --git a/examples/gstreamer-send/gst/gst.go b/examples/gstreamer-send/gst/gst.go index fa7321c8..11646fcc 100644 --- a/examples/gstreamer-send/gst/gst.go +++ b/examples/gstreamer-send/gst/gst.go @@ -38,6 +38,8 @@ func CreatePipeline(codec webrtc.TrackType, in chan<- webrtc.RTCSample) *Pipelin pipelineStr = "videotestsrc ! vp8enc ! " + pipelineStr case webrtc.VP9: pipelineStr = "videotestsrc ! vp9enc ! " + pipelineStr + case webrtc.H264: + pipelineStr = "videotestsrc ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr case webrtc.Opus: pipelineStr = "audiotestsrc ! opusenc ! " + pipelineStr default: diff --git a/internal/sdp/util.go b/internal/sdp/util.go index bac54e70..298352cc 100644 --- a/internal/sdp/util.go +++ b/internal/sdp/util.go @@ -22,7 +22,8 @@ type SessionBuilder struct { Tracks []*SessionBuilderTrack } -// BaseSessionDescription generates a default SDP response that is ice-lite, initiates the DTLS session and supports VP8, VP9 and Opus +// BaseSessionDescription generates a default SDP response that is ice-lite, initiates the DTLS session and +// supports VP8, VP9, H264 and Opus func BaseSessionDescription(b *SessionBuilder) *SessionDescription { addMediaCandidates := func(m *MediaDescription) *MediaDescription { m.Attributes = append(m.Attributes, b.Candidates...) @@ -49,7 +50,7 @@ func BaseSessionDescription(b *SessionBuilder) *SessionDescription { } videoMediaDescription := &MediaDescription{ - MediaName: "video 9 RTP/SAVPF 96 98", + MediaName: "video 9 RTP/SAVPF 96 98 100", ConnectionData: "IN IP4 127.0.0.1", Attributes: []string{ "setup:active", @@ -63,6 +64,8 @@ func BaseSessionDescription(b *SessionBuilder) *SessionDescription { "rtcp-rsize", "rtpmap:96 VP8/90000", "rtpmap:98 VP9/90000", + "rtpmap:100 H264/90000", + "fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", }, } diff --git a/pkg/rtp/codecs/h264_packet.go b/pkg/rtp/codecs/h264_packet.go new file mode 100644 index 00000000..3343a572 --- /dev/null +++ b/pkg/rtp/codecs/h264_packet.go @@ -0,0 +1,123 @@ +package codecs + +// H264Payloader payloads H264 packets +type H264Payloader struct{} + +const ( + fuaHeaderSize = 2 +) + +func emitNalus(nals []byte, emit func([]byte)) { + nextInd := func(nalu []byte, start int) (indStart int, indLen int) { + zeroCount := 0 + + for i, b := range nalu[start:] { + if b == 0 { + zeroCount++ + continue + } else if b == 1 { + if zeroCount >= 2 { + return start + i - zeroCount, zeroCount + 1 + } + } + zeroCount = 0 + } + return -1, -1 + } + + nextIndStart, nextIndLen := nextInd(nals, 0) + if nextIndStart == -1 { + emit(nals) + } else { + for nextIndStart != -1 { + prevStart := nextIndStart + nextIndLen + nextIndStart, nextIndLen = nextInd(nals, prevStart) + if nextIndStart != -1 { + emit(nals[prevStart:nextIndStart]) + } else { + // Emit until end of stream, no end indicator found + emit(nals[prevStart:]) + } + } + } +} + +// Payload fragments a H264 packet across one or more byte arrays +func (p *H264Payloader) Payload(mtu int, payload []byte) [][]byte { + + var payloads [][]byte + + emitNalus(payload, func(nalu []byte) { + naluType := nalu[0] & 0x1F + naluRefIdc := nalu[0] & 0x60 + + if naluType == 9 || naluType == 12 { + return + } + + // Single NALU + if len(nalu) <= mtu { + out := make([]byte, len(nalu)) + copy(out, nalu) + payloads = append(payloads, out) + return + } + + // FU-A + maxFragmentSize := mtu - fuaHeaderSize + + // The FU payload consists of fragments of the payload of the fragmented + // NAL unit so that if the fragmentation unit payloads of consecutive + // FUs are sequentially concatenated, the payload of the fragmented NAL + // unit can be reconstructed. The NAL unit type octet of the fragmented + // NAL unit is not included as such in the fragmentation unit payload, + // but rather the information of the NAL unit type octet of the + // fragmented NAL unit is conveyed in the F and NRI fields of the FU + // indicator octet of the fragmentation unit and in the type field of + // the FU header. An FU payload MAY have any number of octets and MAY + // be empty. + + naluData := nalu + // According to the RFC, the first octet is skipped due to redundant information + naluDataIndex := 1 + naluDataLength := len(nalu) - naluDataIndex + naluDataRemaining := naluDataLength + + for naluDataRemaining > 0 { + currentFragmentSize := min(maxFragmentSize, naluDataRemaining) + out := make([]byte, fuaHeaderSize+currentFragmentSize) + + // +---------------+ + // |0|1|2|3|4|5|6|7| + // +-+-+-+-+-+-+-+-+ + // |F|NRI| Type | + // +---------------+ + out[0] = 28 + out[0] |= naluRefIdc + + // +---------------+ + //|0|1|2|3|4|5|6|7| + //+-+-+-+-+-+-+-+-+ + //|S|E|R| Type | + //+---------------+ + + out[1] = naluType + if naluDataRemaining == naluDataLength { + // Set start bit + out[1] |= 1 << 7 + } else if naluDataRemaining-currentFragmentSize == 0 { + // Set end bit + out[1] |= 1 << 6 + } + + copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize]) + payloads = append(payloads, out) + + naluDataRemaining -= currentFragmentSize + naluDataIndex += currentFragmentSize + } + + }) + + return payloads +} diff --git a/rtcpeerconnection.go b/rtcpeerconnection.go index 9c8a61b3..fdab6b69 100644 --- a/rtcpeerconnection.go +++ b/rtcpeerconnection.go @@ -34,6 +34,7 @@ type TrackType int const ( VP8 TrackType = iota + 1 VP9 + H264 Opus ) @@ -43,6 +44,8 @@ func (t TrackType) String() string { return "VP8" case VP9: return "VP9" + case H264: + return "H264" case Opus: return "Opus" default: @@ -127,7 +130,7 @@ func (r *RTCPeerConnection) CreateAnswer() error { // This function returns a channel to push buffers on, and an error if the channel can't be added // Closing the channel ends this stream func (r *RTCPeerConnection) AddTrack(mediaType TrackType, clockRate uint32) (samples chan<- RTCSample, err error) { - if mediaType != VP8 && mediaType != Opus { + if mediaType != VP8 && mediaType != H264 && mediaType != Opus { panic("TODO Discarding packet, need media parsing") } @@ -202,6 +205,8 @@ func (r *RTCPeerConnection) generateChannel(ssrc uint32, payloadType uint8) (buf codec = VP9 case "opus": codec = Opus + case "H264": + codec = H264 default: fmt.Printf("Codec %s in not supported by pion-WebRTC \n", codecStr) return nil