diff --git a/pkg/h265/README.md b/pkg/h265/README.md new file mode 100644 index 00000000..c6d78911 --- /dev/null +++ b/pkg/h265/README.md @@ -0,0 +1,3 @@ +## Useful links + +- https://datatracker.ietf.org/doc/html/rfc7798 diff --git a/pkg/h265/helper.go b/pkg/h265/helper.go new file mode 100644 index 00000000..756d8a73 --- /dev/null +++ b/pkg/h265/helper.go @@ -0,0 +1,35 @@ +package h265 + +import ( + "encoding/base64" + "github.com/AlexxIT/go2rtc/pkg/streamer" +) + +const ( + NALUnitTypeIFrame = 19 +) + +func NALUnitType(b []byte) byte { + return b[4] >> 1 +} + +func IsKeyframe(b []byte) bool { + return NALUnitType(b) == NALUnitTypeIFrame +} + +func GetParameterSet(fmtp string) (vps, sps, pps []byte) { + if fmtp == "" { + return + } + + s := streamer.Between(fmtp, "sprop-vps=", ";") + vps, _ = base64.StdEncoding.DecodeString(s) + + s = streamer.Between(fmtp, "sprop-sps=", ";") + sps, _ = base64.StdEncoding.DecodeString(s) + + s = streamer.Between(fmtp, "sprop-pps=", ";") + pps, _ = base64.StdEncoding.DecodeString(s) + + return +} diff --git a/pkg/h265/rtp.go b/pkg/h265/rtp.go new file mode 100644 index 00000000..b30ee303 --- /dev/null +++ b/pkg/h265/rtp.go @@ -0,0 +1,57 @@ +package h265 + +import ( + "encoding/binary" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/deepch/vdk/codec/h265parser" + "github.com/pion/rtp" +) + +func RTPDepay(track *streamer.Track) streamer.WrapperFunc { + var buffer []byte + + return func(push streamer.WriterFunc) streamer.WriterFunc { + return func(packet *rtp.Packet) error { + naluType := (packet.Payload[0] >> 1) & 0x3f + //fmt.Printf( + // "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d\n", + // track.Codec.Name, naluType, len(packet.Payload), packet.Timestamp, + // packet.PayloadType, packet.SSRC, packet.SequenceNumber, + //) + + switch naluType { + case h265parser.NAL_UNIT_CODED_SLICE_TRAIL_R: + case h265parser.NAL_UNIT_VPS: + case h265parser.NAL_UNIT_SPS: + case h265parser.NAL_UNIT_PPS: + case h265parser.NAL_UNIT_UNSPECIFIED_49: + data := packet.Payload + switch data[2] >> 6 { + case 2: // begin + buffer = []byte{ + (data[0] & 0x81) | (data[2] & 0x3f << 1), data[1], + } + buffer = append(buffer, data[3:]...) + return nil + case 0: // continue + buffer = append(buffer, data[3:]...) + return nil + case 1: // end + packet.Payload = append(buffer, data[3:]...) + } + default: + //panic("not implemented") + } + + size := make([]byte, 4) + binary.BigEndian.PutUint32(size, uint32(len(packet.Payload))) + + clone := *packet + clone.Version = h264.RTPPacketVersionAVC + clone.Payload = append(size, packet.Payload...) + + return push(&clone) + } + } +} diff --git a/pkg/mp4/README.md b/pkg/mp4/README.md new file mode 100644 index 00000000..29ac63d8 --- /dev/null +++ b/pkg/mp4/README.md @@ -0,0 +1,23 @@ +## HEVC + +Browser | avc1 | hvc1 | hev1 +------------|------|------|--- +Mac Chrome | + | - | + +Mac Safari | + | + | - +iOS 15? | + | + | - +Mac Firefox | + | - | - +iOS 12 | + | - | - +Android 13 | + | - | - + +``` +ffmpeg -i input-hev1.mp4 -c:v copy -tag:v hvc1 -c:a copy output-hvc1.mp4 +Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps, +Stream #0:0(eng): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 720x404, 164 kb/s, 29.97 fps, +``` + +## Useful links + +- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1 +- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec +- https://jellyfin.org/docs/general/clients/codec-support.html +- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding diff --git a/pkg/mp4/consumer.go b/pkg/mp4/consumer.go index 61a3d961..26658450 100644 --- a/pkg/mp4/consumer.go +++ b/pkg/mp4/consumer.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/pion/rtp" ) @@ -28,6 +29,7 @@ func (c *Consumer) GetMedias() []*streamer.Media { Direction: streamer.DirectionRecvonly, Codecs: []*streamer.Codec{ {Name: streamer.CodecH264, ClockRate: 90000}, + {Name: streamer.CodecH265, ClockRate: 90000}, }, }, //{ @@ -74,6 +76,36 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea push = wrapper(push) } + return track.Bind(push) + + case streamer.CodecH265: + c.codecs = append(c.codecs, track.Codec) + + push := func(packet *rtp.Packet) error { + if packet.Version != h264.RTPPacketVersionAVC { + return nil + } + + if !c.start { + if h265.IsKeyframe(packet.Payload) { + c.start = true + } else { + return nil + } + } + + buf := c.muxer.Marshal(packet) + c.send += len(buf) + c.Fire(buf) + + return nil + } + + if !h264.IsAVC(codec) { + wrapper := h265.RTPDepay(track) + push = wrapper(push) + } + return track.Bind(push) } diff --git a/pkg/mp4/muxer.go b/pkg/mp4/muxer.go index 0d5ae3be..96a040d6 100644 --- a/pkg/mp4/muxer.go +++ b/pkg/mp4/muxer.go @@ -4,8 +4,10 @@ import ( "encoding/binary" "fmt" "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/deepch/vdk/codec/h264parser" + "github.com/deepch/vdk/codec/h265parser" "github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/deepch/vdk/format/mp4/mp4io" "github.com/deepch/vdk/format/mp4f/mp4fio" @@ -27,6 +29,9 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string { switch codec.Name { case streamer.CodecH264: s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) + case streamer.CodecH265: + // +Safari +Chrome +Edge -iOS15 -Android13 + s += "hvc1.1.6.L93.B0" // hev1.1.6.L93.B0 } } @@ -80,6 +85,49 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) { Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0}, } + moov.Tracks = append(moov.Tracks, trak) + + case streamer.CodecH265: + vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) + if sps == nil { + return nil, fmt.Errorf("empty SPS: %#v", codec) + } + + codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps) + if err != nil { + return nil, err + } + + width := codecData.Width() + height := codecData.Height() + + trak := TRAK() + trak.Media.Header.TimeScale = int32(codec.ClockRate) + trak.Header.TrackWidth = float64(width) + trak.Header.TrackHeight = float64(height) + + trak.Media.Info.Video = &mp4io.VideoMediaInfo{ + Flags: 0x000001, + } + trak.Media.Info.Sample.SampleDesc.HV1Desc = &mp4io.HV1Desc{ + DataRefIdx: 1, + HorizontalResolution: 72, + VorizontalResolution: 72, + Width: int16(width), + Height: int16(height), + FrameCount: 1, + Depth: 24, + ColorTableId: -1, + Conf: &mp4io.HV1Conf{ + Data: codecData.AVCDecoderConfRecordBytes(), + }, + } + + trak.Media.Handler = &mp4io.HandlerRefer{ + SubType: [4]byte{'v', 'i', 'd', 'e'}, + Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0}, + } + moov.Tracks = append(moov.Tracks, trak) } } @@ -126,7 +174,7 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte { entry := mp4io.TrackFragRunEntry{ //Duration: 90000, - Size: uint32(len(packet.Payload)), + Size: uint32(len(packet.Payload)), } newTime := packet.Timestamp