Total rewrite mov/mp4 encoder
This commit is contained in:
+25
-14
@@ -1,19 +1,30 @@
|
||||
## Fragmented MP4
|
||||
|
||||
```
|
||||
ffmpeg -i "rtsp://..." -movflags +frag_keyframe+separate_moof+default_base_moof+empty_moov -frag_duration 1 -c copy -t 5 sample.mp4
|
||||
```
|
||||
|
||||
- movflags frag_keyframe
|
||||
Start a new fragment at each video keyframe.
|
||||
- frag_duration duration
|
||||
Create fragments that are duration microseconds long.
|
||||
- movflags separate_moof
|
||||
Write a separate moof (movie fragment) atom for each track.
|
||||
- movflags default_base_moof
|
||||
Similarly to the omit_tfhd_offset, this flag avoids writing the absolute base_data_offset field in tfhd atoms, but does so by using the new default-base-is-moof flag instead.
|
||||
|
||||
https://ffmpeg.org/ffmpeg-formats.html#Options-13
|
||||
|
||||
## 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,
|
||||
```
|
||||
| Browser | avc1 | hvc1 | hev1 |
|
||||
|-------------|------|------|------|
|
||||
| Mac Chrome | + | - | + |
|
||||
| Mac Safari | + | + | - |
|
||||
| iOS 15? | + | + | - |
|
||||
| Mac Firefox | + | - | - |
|
||||
| iOS 12 | + | - | - |
|
||||
| Android 13 | + | - | - |
|
||||
|
||||
## Useful links
|
||||
|
||||
|
||||
+52
-149
@@ -1,22 +1,19 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mov"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"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"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
fragIndex uint32
|
||||
flags []uint32
|
||||
dts []uint64
|
||||
pts []uint32
|
||||
}
|
||||
@@ -45,7 +42,11 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
}
|
||||
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
moov := MOOV()
|
||||
mv := mov.NewMovie(1024)
|
||||
mv.WriteFileType()
|
||||
|
||||
mv.StartAtom(mov.Moov)
|
||||
mv.WriteMovieHeader()
|
||||
|
||||
for i, codec := range codecs {
|
||||
switch codec.Name {
|
||||
@@ -62,35 +63,13 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.ClockRate,
|
||||
uint16(codecData.Width()), uint16(codecData.Height()),
|
||||
codecData.AVCDecoderConfRecordBytes(), true,
|
||||
)
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
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},
|
||||
}
|
||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||
Flags: 0x000001,
|
||||
}
|
||||
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
|
||||
DataRefIdx: 1,
|
||||
HorizontalResolution: 72,
|
||||
VorizontalResolution: 72,
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
FrameCount: 1,
|
||||
Depth: 24,
|
||||
ColorTableId: -1,
|
||||
Conf: &mp4io.AVC1Conf{
|
||||
Data: codecData.AVCDecoderConfRecordBytes(),
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
m.flags = append(m.flags, 0x1010000)
|
||||
|
||||
case streamer.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
@@ -106,35 +85,13 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
mv.WriteVideoTrack(
|
||||
uint32(i+1), codec.ClockRate,
|
||||
uint16(codecData.Width()), uint16(codecData.Height()),
|
||||
codecData.AVCDecoderConfRecordBytes(), false,
|
||||
)
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
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},
|
||||
}
|
||||
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(),
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
m.flags = append(m.flags, 0x1010000)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
@@ -143,44 +100,26 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.AlternateGroup = 1
|
||||
trak.Header.Duration = 0
|
||||
trak.Header.Volume = 1
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
mv.WriteAudioTrack(
|
||||
uint32(i+1), codec.ClockRate, codec.Channels, 16, b,
|
||||
)
|
||||
|
||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||
}
|
||||
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
||||
|
||||
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||
DataRefIdx: 1,
|
||||
NumberOfChannels: int16(codec.Channels),
|
||||
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||
SampleRate: float64(codec.ClockRate),
|
||||
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
m.flags = append(m.flags, 0x2000000)
|
||||
}
|
||||
|
||||
trex := &mp4io.TrackExtend{
|
||||
TrackId: uint32(i + 1),
|
||||
DefaultSampleDescIdx: 1,
|
||||
DefaultSampleDuration: 0,
|
||||
}
|
||||
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
||||
|
||||
m.pts = append(m.pts, 0)
|
||||
m.dts = append(m.dts, 0)
|
||||
}
|
||||
|
||||
data := make([]byte, moov.Len())
|
||||
moov.Marshal(data)
|
||||
mv.StartAtom(mov.MoovMvex)
|
||||
for i := range codecs {
|
||||
mv.WriteTrackExtend(uint32(i + 1))
|
||||
}
|
||||
mv.EndAtom() // MVEX
|
||||
|
||||
return append(FTYP(), data...), nil
|
||||
mv.EndAtom() // MOOV
|
||||
|
||||
return mv.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m *Muxer) Reset() {
|
||||
@@ -192,65 +131,29 @@ func (m *Muxer) Reset() {
|
||||
}
|
||||
|
||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||
run := &mp4fio.TrackFragRun{
|
||||
Flags: 0x000b05,
|
||||
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
||||
DataOffset: 0,
|
||||
Entries: []mp4io.TrackFragRunEntry{},
|
||||
}
|
||||
|
||||
moof := &mp4fio.MovieFrag{
|
||||
Header: &mp4fio.MovieFragHeader{
|
||||
Seqnum: m.fragIndex + 1,
|
||||
},
|
||||
Tracks: []*mp4fio.TrackFrag{
|
||||
{
|
||||
Header: &mp4fio.TrackFragHeader{
|
||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
||||
},
|
||||
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
||||
Version: 1,
|
||||
Flags: 0,
|
||||
Time: m.dts[trackID],
|
||||
},
|
||||
Run: run,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
entry := mp4io.TrackFragRunEntry{
|
||||
Size: uint32(len(packet.Payload)),
|
||||
}
|
||||
|
||||
newTime := packet.Timestamp
|
||||
if m.pts[trackID] > 0 {
|
||||
entry.Duration = newTime - m.pts[trackID]
|
||||
m.dts[trackID] += uint64(entry.Duration)
|
||||
} else {
|
||||
// important, or Safari will fail with first frame
|
||||
entry.Duration = 1
|
||||
}
|
||||
m.pts[trackID] = newTime
|
||||
|
||||
// important before moof.Len()
|
||||
run.Entries = append(run.Entries, entry)
|
||||
|
||||
moofLen := moof.Len()
|
||||
mdatLen := 8 + len(packet.Payload)
|
||||
|
||||
// important after moof.Len()
|
||||
run.DataOffset = uint32(moofLen + 8)
|
||||
|
||||
buf := make([]byte, moofLen+mdatLen)
|
||||
moof.Marshal(buf)
|
||||
|
||||
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
|
||||
copy(buf[moofLen+4:], "mdat")
|
||||
copy(buf[moofLen+8:], packet.Payload)
|
||||
// important before increment
|
||||
time := m.dts[trackID]
|
||||
|
||||
m.fragIndex++
|
||||
|
||||
//m.total += moofLen + mdatLen
|
||||
var duration uint32
|
||||
newTime := packet.Timestamp
|
||||
if m.pts[trackID] > 0 {
|
||||
duration = newTime - m.pts[trackID]
|
||||
m.dts[trackID] += uint64(duration)
|
||||
} else {
|
||||
// important, or Safari will fail with first frame
|
||||
duration = 1
|
||||
}
|
||||
m.pts[trackID] = newTime
|
||||
|
||||
return buf
|
||||
mv := mov.NewMovie(1024 + len(packet.Payload))
|
||||
mv.WriteMovieFragment(
|
||||
m.fragIndex, uint32(trackID+1), duration,
|
||||
uint32(len(packet.Payload)),
|
||||
m.flags[trackID], time,
|
||||
)
|
||||
mv.WriteData(packet.Payload)
|
||||
|
||||
return mv.Bytes()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"github.com/deepch/vdk/codec/aacparser"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/mp4f"
|
||||
"github.com/pion/rtp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
Medias []*streamer.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *mp4f.Muxer
|
||||
streams []av.CodecData
|
||||
mimeType string
|
||||
start bool
|
||||
|
||||
send int
|
||||
}
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264, ClockRate: 90000},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC, ClockRate: 16000},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
codec := track.Codec
|
||||
trackID := int8(len(c.streams))
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.start {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||
if pkt.Time > 0 {
|
||||
pkt.Duration = newTime - pkt.Time
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||
if ready {
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := h264.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
stream, _ := aacparser.NewCodecDataFromMPEG4AudioConfigBytes([]byte{20, 8})
|
||||
|
||||
c.mimeType += ",mp4a.40.2"
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
|
||||
|
||||
ts2time := time.Second / time.Duration(codec.ClockRate)
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if !c.start {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkt.Data = packet.Payload
|
||||
newTime := time.Duration(packet.Timestamp) * ts2time
|
||||
if pkt.Time > 0 {
|
||||
pkt.Duration = newTime - pkt.Time
|
||||
}
|
||||
pkt.Time = newTime
|
||||
|
||||
ready, buf, _ := c.muxer.WritePacket(pkt, false)
|
||||
if ready {
|
||||
c.send += len(buf)
|
||||
c.Fire(buf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeType() string {
|
||||
return `video/mp4; codecs="` + c.mimeType + `"`
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
c.muxer = mp4f.NewMuxer(nil)
|
||||
if err := c.muxer.WriteHeader(c.streams); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, data := c.muxer.GetInit(c.streams)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() {
|
||||
c.start = true
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Consumer struct {
|
||||
streamer.Element
|
||||
|
||||
Medias []*streamer.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
muxer *Muxer
|
||||
codecs []*streamer.Codec
|
||||
wait byte
|
||||
|
||||
send uint32
|
||||
}
|
||||
|
||||
const (
|
||||
waitNone byte = iota
|
||||
waitKeyframe
|
||||
waitInit
|
||||
)
|
||||
|
||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: streamer.KindAudio,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecAAC},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
trackID := byte(len(c.codecs))
|
||||
c.codecs = append(c.codecs, track.Codec)
|
||||
|
||||
codec := track.Codec
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
if codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
c.wait = waitInit
|
||||
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if packet.Version != h264.RTPPacketVersionAVC {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.wait != waitNone {
|
||||
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
c.wait = waitNone
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if c.wait != waitNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := c.muxer.Marshal(trackID, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if codec.IsRTP() {
|
||||
wrapper := aac.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Consumer) MimeType() string {
|
||||
return c.muxer.MimeType(c.codecs)
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
c.muxer = &Muxer{}
|
||||
return c.muxer.GetInit(c.codecs)
|
||||
}
|
||||
|
||||
func (c *Consumer) Start() {
|
||||
if c.wait == waitInit {
|
||||
c.wait = waitKeyframe
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "MP4 client",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Send: atomic.LoadUint32(&c.send),
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/av"
|
||||
"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"
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
fragIndex uint32
|
||||
dts []uint64
|
||||
pts []uint32
|
||||
}
|
||||
|
||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
s := `video/mp4; codecs="`
|
||||
|
||||
for i, codec := range codecs {
|
||||
if i > 0 {
|
||||
s += ","
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
case streamer.CodecH265:
|
||||
// H.265 profile=main level=5.1
|
||||
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
|
||||
s += "hvc1.1.6.L153.B0"
|
||||
case streamer.CodecAAC:
|
||||
s += "mp4a.40.2"
|
||||
}
|
||||
}
|
||||
|
||||
return s + `"`
|
||||
}
|
||||
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
moov := MOOV()
|
||||
|
||||
for i, codec := range codecs {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
sps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
|
||||
pps = []byte{0x68, 0xce, 0x38, 0x80}
|
||||
}
|
||||
|
||||
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
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},
|
||||
}
|
||||
trak.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||
Flags: 0x000001,
|
||||
}
|
||||
trak.Media.Info.Sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
|
||||
DataRefIdx: 1,
|
||||
HorizontalResolution: 72,
|
||||
VorizontalResolution: 72,
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
FrameCount: 1,
|
||||
Depth: 24,
|
||||
ColorTableId: -1,
|
||||
Conf: &mp4io.AVC1Conf{
|
||||
Data: codecData.AVCDecoderConfRecordBytes(),
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecH265:
|
||||
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
// some dummy SPS and PPS not a problem
|
||||
vps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}
|
||||
sps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}
|
||||
pps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}
|
||||
}
|
||||
|
||||
codecData, err := h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
height := codecData.Height()
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.TrackWidth = float64(width)
|
||||
trak.Header.TrackHeight = float64(height)
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
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},
|
||||
}
|
||||
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(),
|
||||
},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
|
||||
case streamer.CodecAAC:
|
||||
s := streamer.Between(codec.FmtpLine, "config=", ";")
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trak := TRAK(i + 1)
|
||||
trak.Header.AlternateGroup = 1
|
||||
trak.Header.Duration = 0
|
||||
trak.Header.Volume = 1
|
||||
trak.Media.Header.TimeScale = int32(codec.ClockRate)
|
||||
|
||||
trak.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||
Name: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'm', 'a', 'i', 'n', 0},
|
||||
}
|
||||
trak.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
||||
|
||||
trak.Media.Info.Sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||
DataRefIdx: 1,
|
||||
NumberOfChannels: int16(codec.Channels),
|
||||
SampleSize: int16(av.FLTP.BytesPerSample() * 4),
|
||||
SampleRate: float64(codec.ClockRate),
|
||||
Unknowns: []mp4io.Atom{ESDS(b)},
|
||||
}
|
||||
|
||||
moov.Tracks = append(moov.Tracks, trak)
|
||||
}
|
||||
|
||||
trex := &mp4io.TrackExtend{
|
||||
TrackId: uint32(i + 1),
|
||||
DefaultSampleDescIdx: 1,
|
||||
DefaultSampleDuration: 0,
|
||||
}
|
||||
moov.MovieExtend.Tracks = append(moov.MovieExtend.Tracks, trex)
|
||||
|
||||
m.pts = append(m.pts, 0)
|
||||
m.dts = append(m.dts, 0)
|
||||
}
|
||||
|
||||
data := make([]byte, moov.Len())
|
||||
moov.Marshal(data)
|
||||
|
||||
return append(FTYP(), data...), nil
|
||||
}
|
||||
|
||||
func (m *Muxer) Reset() {
|
||||
m.fragIndex = 0
|
||||
for i := range m.dts {
|
||||
m.dts[i] = 0
|
||||
m.pts[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||
run := &mp4fio.TrackFragRun{
|
||||
Flags: 0x000b05,
|
||||
FirstSampleFlags: uint32(fmp4io.SampleNoDependencies),
|
||||
DataOffset: 0,
|
||||
Entries: []mp4io.TrackFragRunEntry{},
|
||||
}
|
||||
|
||||
moof := &mp4fio.MovieFrag{
|
||||
Header: &mp4fio.MovieFragHeader{
|
||||
Seqnum: m.fragIndex + 1,
|
||||
},
|
||||
Tracks: []*mp4fio.TrackFrag{
|
||||
{
|
||||
Header: &mp4fio.TrackFragHeader{
|
||||
Data: []byte{0x00, 0x02, 0x00, 0x20, 0x00, 0x00, 0x00, trackID + 1, 0x01, 0x01, 0x00, 0x00},
|
||||
},
|
||||
DecodeTime: &mp4fio.TrackFragDecodeTime{
|
||||
Version: 1,
|
||||
Flags: 0,
|
||||
Time: m.dts[trackID],
|
||||
},
|
||||
Run: run,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
entry := mp4io.TrackFragRunEntry{
|
||||
Size: uint32(len(packet.Payload)),
|
||||
}
|
||||
|
||||
newTime := packet.Timestamp
|
||||
if m.pts[trackID] > 0 {
|
||||
entry.Duration = newTime - m.pts[trackID]
|
||||
m.dts[trackID] += uint64(entry.Duration)
|
||||
} else {
|
||||
// important, or Safari will fail with first frame
|
||||
entry.Duration = 1
|
||||
}
|
||||
m.pts[trackID] = newTime
|
||||
|
||||
// important before moof.Len()
|
||||
run.Entries = append(run.Entries, entry)
|
||||
|
||||
moofLen := moof.Len()
|
||||
mdatLen := 8 + len(packet.Payload)
|
||||
|
||||
// important after moof.Len()
|
||||
run.DataOffset = uint32(moofLen + 8)
|
||||
|
||||
buf := make([]byte, moofLen+mdatLen)
|
||||
moof.Marshal(buf)
|
||||
|
||||
binary.BigEndian.PutUint32(buf[moofLen:], uint32(mdatLen))
|
||||
copy(buf[moofLen+4:], "mdat")
|
||||
copy(buf[moofLen+8:], packet.Payload)
|
||||
|
||||
m.fragIndex++
|
||||
|
||||
//m.total += moofLen + mdatLen
|
||||
|
||||
return buf
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package mp4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtp"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Segment struct {
|
||||
streamer.Element
|
||||
|
||||
Medias []*streamer.Media
|
||||
UserAgent string
|
||||
RemoteAddr string
|
||||
|
||||
MimeType string
|
||||
OnlyKeyframe bool
|
||||
|
||||
send uint32
|
||||
}
|
||||
|
||||
func (c *Segment) GetMedias() []*streamer.Media {
|
||||
if c.Medias != nil {
|
||||
return c.Medias
|
||||
}
|
||||
|
||||
// default medias
|
||||
return []*streamer.Media{
|
||||
{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionRecvonly,
|
||||
Codecs: []*streamer.Codec{
|
||||
{Name: streamer.CodecH264},
|
||||
{Name: streamer.CodecH265},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||
muxer := &Muxer{}
|
||||
|
||||
codecs := []*streamer.Codec{track.Codec}
|
||||
|
||||
init, err := muxer.GetInit(codecs)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.MimeType = muxer.MimeType(codecs)
|
||||
|
||||
switch track.Codec.Name {
|
||||
case streamer.CodecH264:
|
||||
var push streamer.WriterFunc
|
||||
|
||||
if c.OnlyKeyframe {
|
||||
push = func(packet *rtp.Packet) error {
|
||||
if !h264.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
var buf []byte
|
||||
|
||||
push = func(packet *rtp.Packet) error {
|
||||
if h264.IsKeyframe(packet.Payload) {
|
||||
// fist frame - send only IFrame
|
||||
// other frames - send IFrame and all PFrames
|
||||
if buf == nil {
|
||||
buf = append(buf, init...)
|
||||
b := muxer.Marshal(0, packet)
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(buf)
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, init...)
|
||||
muxer.Reset()
|
||||
}
|
||||
|
||||
if buf != nil {
|
||||
b := muxer.Marshal(0, packet)
|
||||
buf = append(buf, b...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var wrapper streamer.WrapperFunc
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper = h264.RTPDepay(track)
|
||||
} else {
|
||||
wrapper = h264.RepairAVC(track)
|
||||
}
|
||||
push = wrapper(push)
|
||||
|
||||
return track.Bind(push)
|
||||
|
||||
case streamer.CodecH265:
|
||||
push := func(packet *rtp.Packet) error {
|
||||
if !h265.IsKeyframe(packet.Payload) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf := muxer.Marshal(0, packet)
|
||||
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||
c.Fire(append(init, buf...))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if track.Codec.IsRTP() {
|
||||
wrapper := h265.RTPDepay(track)
|
||||
push = wrapper(push)
|
||||
}
|
||||
|
||||
return track.Bind(push)
|
||||
}
|
||||
|
||||
panic("unsupported codec")
|
||||
}
|
||||
|
||||
func (c *Segment) MarshalJSON() ([]byte, error) {
|
||||
info := &streamer.Info{
|
||||
Type: "WS/MP4 client",
|
||||
RemoteAddr: c.RemoteAddr,
|
||||
UserAgent: c.UserAgent,
|
||||
Send: atomic.LoadUint32(&c.send),
|
||||
}
|
||||
return json.Marshal(info)
|
||||
}
|
||||
Reference in New Issue
Block a user