Adds support H265 for MP4
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://datatracker.ietf.org/doc/html/rfc7798
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
@@ -28,6 +29,7 @@ func (c *Consumer) GetMedias() []*streamer.Media {
|
|||||||
Direction: streamer.DirectionRecvonly,
|
Direction: streamer.DirectionRecvonly,
|
||||||
Codecs: []*streamer.Codec{
|
Codecs: []*streamer.Codec{
|
||||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
{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)
|
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)
|
return track.Bind(push)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
|
"github.com/deepch/vdk/codec/h265parser"
|
||||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||||
"github.com/deepch/vdk/format/mp4/mp4io"
|
"github.com/deepch/vdk/format/mp4/mp4io"
|
||||||
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
"github.com/deepch/vdk/format/mp4f/mp4fio"
|
||||||
@@ -27,6 +29,9 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
|||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
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},
|
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)
|
moov.Tracks = append(moov.Tracks, trak)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user