mirror of
https://github.com/kerberos-io/agent.git
synced 2026-10-19 17:38:25 +00:00
adding fragmented mp4 (not working) trying to fix black screen on quicktime player mp4
This commit is contained in:
@@ -202,6 +202,7 @@ func (g *Golibrtsp) Start(ctx context.Context, queue *packets.Queue, communicati
|
||||
|
||||
// called when a video RTP packet arrives
|
||||
if g.VideoH264Media != nil {
|
||||
|
||||
g.Client.OnPacketRTP(g.VideoH264Media, g.VideoH264Forma, func(rtppkt *rtp.Packet) {
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
|
||||
@@ -372,6 +372,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
start = true
|
||||
}
|
||||
if start {
|
||||
|
||||
if err := myMuxer.WritePacket(pkt); err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,438 @@
|
||||
package main
|
||||
|
||||
import "github.com/kerberos-io/agent/machinery/src/utils/bits/pio"
|
||||
|
||||
func moov_Movie() {
|
||||
atom(Header, MovieHeader)
|
||||
atom(MovieExtend, MovieExtend)
|
||||
atoms(Tracks, Track)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func mvhd_MovieHeader() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
time32(CreateTime)
|
||||
time32(ModifyTime)
|
||||
int32(TimeScale)
|
||||
int32(Duration)
|
||||
fixed32(PreferredRate)
|
||||
fixed16(PreferredVolume)
|
||||
_skip(10)
|
||||
array(Matrix, int32, 9)
|
||||
time32(PreviewTime)
|
||||
time32(PreviewDuration)
|
||||
time32(PosterTime)
|
||||
time32(SelectionTime)
|
||||
time32(SelectionDuration)
|
||||
time32(CurrentTime)
|
||||
int32(NextTrackId)
|
||||
}
|
||||
|
||||
func trak_Track() {
|
||||
atom(Header, TrackHeader)
|
||||
atom(Media, Media)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func tkhd_TrackHeader() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
time32(CreateTime)
|
||||
time32(ModifyTime)
|
||||
int32(TrackId)
|
||||
_skip(4)
|
||||
int32(Duration)
|
||||
_skip(8)
|
||||
int16(Layer)
|
||||
int16(AlternateGroup)
|
||||
fixed16(Volume)
|
||||
_skip(2)
|
||||
array(Matrix, int32, 9)
|
||||
fixed32(TrackWidth)
|
||||
fixed32(TrackHeight)
|
||||
}
|
||||
|
||||
func hdlr_HandlerRefer() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
bytes(Type, 4)
|
||||
bytes(SubType, 4)
|
||||
bytesleft(Name)
|
||||
}
|
||||
|
||||
func mdia_Media() {
|
||||
atom(Header, MediaHeader)
|
||||
atom(Handler, HandlerRefer)
|
||||
atom(Info, MediaInfo)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func mdhd_MediaHeader() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
time32(CreateTime)
|
||||
time32(ModifyTime)
|
||||
int32(TimeScale)
|
||||
int32(Duration)
|
||||
int16(Language)
|
||||
int16(Quality)
|
||||
}
|
||||
|
||||
func minf_MediaInfo() {
|
||||
atom(Sound, SoundMediaInfo)
|
||||
atom(Video, VideoMediaInfo)
|
||||
atom(Data, DataInfo)
|
||||
atom(Sample, SampleTable)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func dinf_DataInfo() {
|
||||
atom(Refer, DataRefer)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func dref_DataRefer() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
int32(_childrenNR)
|
||||
atom(Url, DataReferUrl)
|
||||
}
|
||||
|
||||
func url__DataReferUrl() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
}
|
||||
|
||||
func smhd_SoundMediaInfo() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
int16(Balance)
|
||||
_skip(2)
|
||||
}
|
||||
|
||||
func vmhd_VideoMediaInfo() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
int16(GraphicsMode)
|
||||
array(Opcolor, int16, 3)
|
||||
}
|
||||
|
||||
func stbl_SampleTable() {
|
||||
atom(SampleDesc, SampleDesc)
|
||||
atom(TimeToSample, TimeToSample)
|
||||
atom(CompositionOffset, CompositionOffset)
|
||||
atom(SampleToChunk, SampleToChunk)
|
||||
atom(SyncSample, SyncSample)
|
||||
atom(ChunkOffset, ChunkOffset)
|
||||
atom(SampleSize, SampleSize)
|
||||
}
|
||||
|
||||
func stsd_SampleDesc() {
|
||||
uint8(Version)
|
||||
_skip(3)
|
||||
int32(_childrenNR)
|
||||
atom(AVC1Desc, AVC1Desc)
|
||||
atom(MP4ADesc, MP4ADesc)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func mp4a_MP4ADesc() {
|
||||
_skip(6)
|
||||
int16(DataRefIdx)
|
||||
int16(Version)
|
||||
int16(RevisionLevel)
|
||||
int32(Vendor)
|
||||
int16(NumberOfChannels)
|
||||
int16(SampleSize)
|
||||
int16(CompressionId)
|
||||
_skip(2)
|
||||
fixed32(SampleRate)
|
||||
atom(Conf, ElemStreamDesc)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func avc1_AVC1Desc() {
|
||||
_skip(6)
|
||||
int16(DataRefIdx)
|
||||
int16(Version)
|
||||
int16(Revision)
|
||||
int32(Vendor)
|
||||
int32(TemporalQuality)
|
||||
int32(SpatialQuality)
|
||||
int16(Width)
|
||||
int16(Height)
|
||||
fixed32(HorizontalResolution)
|
||||
fixed32(VorizontalResolution)
|
||||
_skip(4)
|
||||
int16(FrameCount)
|
||||
bytes(CompressorName, 32)
|
||||
int16(Depth)
|
||||
int16(ColorTableId)
|
||||
atom(Conf, AVC1Conf)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func avcC_AVC1Conf() {
|
||||
bytesleft(Data)
|
||||
}
|
||||
|
||||
func stts_TimeToSample() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, TimeToSampleEntry)
|
||||
}
|
||||
|
||||
func TimeToSampleEntry() {
|
||||
uint32(Count)
|
||||
uint32(Duration)
|
||||
}
|
||||
|
||||
func stsc_SampleToChunk() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, SampleToChunkEntry)
|
||||
}
|
||||
|
||||
func SampleToChunkEntry() {
|
||||
uint32(FirstChunk)
|
||||
uint32(SamplesPerChunk)
|
||||
uint32(SampleDescId)
|
||||
}
|
||||
|
||||
func ctts_CompositionOffset() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, CompositionOffsetEntry)
|
||||
}
|
||||
|
||||
func CompositionOffsetEntry() {
|
||||
uint32(Count)
|
||||
uint32(Offset)
|
||||
}
|
||||
|
||||
func stss_SyncSample() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, uint32)
|
||||
}
|
||||
|
||||
func stco_ChunkOffset() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, uint32)
|
||||
}
|
||||
|
||||
func moof_MovieFrag() {
|
||||
atom(Header, MovieFragHeader)
|
||||
atoms(Tracks, TrackFrag)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func mfhd_MovieFragHeader() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(Seqnum)
|
||||
}
|
||||
|
||||
func traf_TrackFrag() {
|
||||
atom(Header, TrackFragHeader)
|
||||
atom(DecodeTime, TrackFragDecodeTime)
|
||||
atom(Run, TrackFragRun)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func mvex_MovieExtend() {
|
||||
atoms(Tracks, TrackExtend)
|
||||
_unknowns()
|
||||
}
|
||||
|
||||
func trex_TrackExtend() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(TrackId)
|
||||
uint32(DefaultSampleDescIdx)
|
||||
uint32(DefaultSampleDuration)
|
||||
uint32(DefaultSampleSize)
|
||||
uint32(DefaultSampleFlags)
|
||||
}
|
||||
|
||||
func stsz_SampleSize() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(SampleSize)
|
||||
_code(func() {
|
||||
if self.SampleSize != 0 {
|
||||
return
|
||||
}
|
||||
})
|
||||
uint32(_len_Entries)
|
||||
slice(Entries, uint32)
|
||||
}
|
||||
|
||||
func trun_TrackFragRun() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
uint32(_len_Entries)
|
||||
|
||||
uint32(DataOffset, _code(func() {
|
||||
if self.Flags&TRUN_DATA_OFFSET != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
uint32(FirstSampleFlags, _code(func() {
|
||||
if self.Flags&TRUN_FIRST_SAMPLE_FLAGS != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
slice(Entries, TrackFragRunEntry, _code(func() {
|
||||
for i, entry := range self.Entries {
|
||||
var flags uint32
|
||||
if i > 0 {
|
||||
flags = self.Flags
|
||||
} else {
|
||||
flags = self.FirstSampleFlags
|
||||
}
|
||||
if flags&TRUN_SAMPLE_DURATION != 0 {
|
||||
pio.PutU32BE(b[n:], entry.Duration)
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_SIZE != 0 {
|
||||
pio.PutU32BE(b[n:], entry.Size)
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_FLAGS != 0 {
|
||||
pio.PutU32BE(b[n:], entry.Flags)
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_CTS != 0 {
|
||||
pio.PutU32BE(b[n:], entry.Cts)
|
||||
n += 4
|
||||
}
|
||||
}
|
||||
}, func() {
|
||||
for i := range self.Entries {
|
||||
var flags uint32
|
||||
if i > 0 {
|
||||
flags = self.Flags
|
||||
} else {
|
||||
flags = self.FirstSampleFlags
|
||||
}
|
||||
if flags&TRUN_SAMPLE_DURATION != 0 {
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_SIZE != 0 {
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_FLAGS != 0 {
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_CTS != 0 {
|
||||
n += 4
|
||||
}
|
||||
}
|
||||
}, func() {
|
||||
for i := 0; i < int(_len_Entries); i++ {
|
||||
var flags uint32
|
||||
if i > 0 {
|
||||
flags = self.Flags
|
||||
} else {
|
||||
flags = self.FirstSampleFlags
|
||||
}
|
||||
entry := &self.Entries[i]
|
||||
if flags&TRUN_SAMPLE_DURATION != 0 {
|
||||
entry.Duration = pio.U32BE(b[n:])
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_SIZE != 0 {
|
||||
entry.Size = pio.U32BE(b[n:])
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_FLAGS != 0 {
|
||||
entry.Flags = pio.U32BE(b[n:])
|
||||
n += 4
|
||||
}
|
||||
if flags&TRUN_SAMPLE_CTS != 0 {
|
||||
entry.Cts = pio.U32BE(b[n:])
|
||||
n += 4
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TrackFragRunEntry() {
|
||||
uint32(Duration)
|
||||
uint32(Size)
|
||||
uint32(Flags)
|
||||
uint32(Cts)
|
||||
}
|
||||
|
||||
func tfhd_TrackFragHeader() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
|
||||
uint64(BaseDataOffset, _code(func() {
|
||||
if self.Flags&TFHD_BASE_DATA_OFFSET != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
uint32(StsdId, _code(func() {
|
||||
if self.Flags&TFHD_STSD_ID != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
uint32(DefaultDuration, _code(func() {
|
||||
if self.Flags&TFHD_DEFAULT_DURATION != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
uint32(DefaultSize, _code(func() {
|
||||
if self.Flags&TFHD_DEFAULT_SIZE != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
|
||||
uint32(DefaultFlags, _code(func() {
|
||||
if self.Flags&TFHD_DEFAULT_FLAGS != 0 {
|
||||
doit()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func tfdt_TrackFragDecodeTime() {
|
||||
uint8(Version)
|
||||
uint24(Flags)
|
||||
time64(Time, _code(func() {
|
||||
if self.Version != 0 {
|
||||
PutTime64(b[n:], self.Time)
|
||||
n += 8
|
||||
} else {
|
||||
PutTime32(b[n:], self.Time)
|
||||
n += 4
|
||||
}
|
||||
}, func() {
|
||||
if self.Version != 0 {
|
||||
n += 8
|
||||
} else {
|
||||
n += 4
|
||||
}
|
||||
}, func() {
|
||||
if self.Version != 0 {
|
||||
self.Time = GetTime64(b[n:])
|
||||
n += 8
|
||||
} else {
|
||||
self.Time = GetTime32(b[n:])
|
||||
n += 4
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
package mp4io
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/joy4/utils/bits/pio"
|
||||
)
|
||||
|
||||
type ParseError struct {
|
||||
Debug string
|
||||
Offset int
|
||||
prev *ParseError
|
||||
}
|
||||
|
||||
func (self *ParseError) Error() string {
|
||||
s := []string{}
|
||||
for p := self; p != nil; p = p.prev {
|
||||
s = append(s, fmt.Sprintf("%s:%d", p.Debug, p.Offset))
|
||||
}
|
||||
return "mp4io: parse error: " + strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func parseErr(debug string, offset int, prev error) (err error) {
|
||||
_prev, _ := prev.(*ParseError)
|
||||
return &ParseError{Debug: debug, Offset: offset, prev: _prev}
|
||||
}
|
||||
|
||||
func GetTime32(b []byte) (t time.Time) {
|
||||
sec := pio.U32BE(b)
|
||||
t = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
t = t.Add(time.Second * time.Duration(sec))
|
||||
return
|
||||
}
|
||||
|
||||
func PutTime32(b []byte, t time.Time) {
|
||||
dur := t.Sub(time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
sec := uint32(dur / time.Second)
|
||||
pio.PutU32BE(b, sec)
|
||||
}
|
||||
|
||||
func GetTime64(b []byte) (t time.Time) {
|
||||
sec := pio.U64BE(b)
|
||||
t = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
t = t.Add(time.Second * time.Duration(sec))
|
||||
return
|
||||
}
|
||||
|
||||
func PutTime64(b []byte, t time.Time) {
|
||||
dur := t.Sub(time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
sec := uint64(dur / time.Millisecond)
|
||||
pio.PutU64BE(b, sec)
|
||||
}
|
||||
|
||||
func PutFixed16(b []byte, f float64) {
|
||||
intpart, fracpart := math.Modf(f)
|
||||
b[0] = uint8(intpart)
|
||||
b[1] = uint8(fracpart * 256.0)
|
||||
}
|
||||
|
||||
func GetFixed16(b []byte) float64 {
|
||||
return float64(b[0]) + float64(b[1])/256.0
|
||||
}
|
||||
|
||||
func PutFixed32(b []byte, f float64) {
|
||||
intpart, fracpart := math.Modf(f)
|
||||
pio.PutU16BE(b[0:2], uint16(intpart))
|
||||
pio.PutU16BE(b[2:4], uint16(fracpart*65536.0))
|
||||
}
|
||||
|
||||
func GetFixed32(b []byte) float64 {
|
||||
return float64(pio.U16BE(b[0:2])) + float64(pio.U16BE(b[2:4]))/65536.0
|
||||
}
|
||||
|
||||
type Tag uint32
|
||||
|
||||
func (self Tag) String() string {
|
||||
var b [4]byte
|
||||
pio.PutU32BE(b[:], uint32(self))
|
||||
for i := 0; i < 4; i++ {
|
||||
if b[i] == 0 {
|
||||
b[i] = ' '
|
||||
}
|
||||
}
|
||||
return string(b[:])
|
||||
}
|
||||
|
||||
type Atom interface {
|
||||
Pos() (int, int)
|
||||
Tag() Tag
|
||||
Marshal([]byte) int
|
||||
Unmarshal([]byte, int) (int, error)
|
||||
Len() int
|
||||
Children() []Atom
|
||||
}
|
||||
|
||||
type AtomPos struct {
|
||||
Offset int
|
||||
Size int
|
||||
}
|
||||
|
||||
func (self AtomPos) Pos() (int, int) {
|
||||
return self.Offset, self.Size
|
||||
}
|
||||
|
||||
func (self *AtomPos) setPos(offset int, size int) {
|
||||
self.Offset, self.Size = offset, size
|
||||
}
|
||||
|
||||
type Dummy struct {
|
||||
Data []byte
|
||||
Tag_ Tag
|
||||
AtomPos
|
||||
}
|
||||
|
||||
func (self Dummy) Children() []Atom {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self Dummy) Tag() Tag {
|
||||
return self.Tag_
|
||||
}
|
||||
|
||||
func (self Dummy) Len() int {
|
||||
return len(self.Data)
|
||||
}
|
||||
|
||||
func (self Dummy) Marshal(b []byte) int {
|
||||
copy(b, self.Data)
|
||||
return len(self.Data)
|
||||
}
|
||||
|
||||
func (self *Dummy) Unmarshal(b []byte, offset int) (n int, err error) {
|
||||
(&self.AtomPos).setPos(offset, len(b))
|
||||
self.Data = b
|
||||
n = len(b)
|
||||
return
|
||||
}
|
||||
|
||||
func StringToTag(tag string) Tag {
|
||||
var b [4]byte
|
||||
copy(b[:], []byte(tag))
|
||||
return Tag(pio.U32BE(b[:]))
|
||||
}
|
||||
|
||||
func FindChildrenByName(root Atom, tag string) Atom {
|
||||
return FindChildren(root, StringToTag(tag))
|
||||
}
|
||||
|
||||
func FindChildren(root Atom, tag Tag) Atom {
|
||||
if root.Tag() == tag {
|
||||
return root
|
||||
}
|
||||
for _, child := range root.Children() {
|
||||
if r := FindChildren(child, tag); r != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
TFHD_BASE_DATA_OFFSET = 0x01
|
||||
TFHD_STSD_ID = 0x02
|
||||
TFHD_DEFAULT_DURATION = 0x08
|
||||
TFHD_DEFAULT_SIZE = 0x10
|
||||
TFHD_DEFAULT_FLAGS = 0x20
|
||||
TFHD_DURATION_IS_EMPTY = 0x010000
|
||||
TFHD_DEFAULT_BASE_IS_MOOF = 0x020000
|
||||
)
|
||||
|
||||
const (
|
||||
TRUN_DATA_OFFSET = 0x01
|
||||
TRUN_FIRST_SAMPLE_FLAGS = 0x04
|
||||
TRUN_SAMPLE_DURATION = 0x100
|
||||
TRUN_SAMPLE_SIZE = 0x200
|
||||
TRUN_SAMPLE_FLAGS = 0x400
|
||||
TRUN_SAMPLE_CTS = 0x800
|
||||
)
|
||||
|
||||
const (
|
||||
MP4ESDescrTag = 3
|
||||
MP4DecConfigDescrTag = 4
|
||||
MP4DecSpecificDescrTag = 5
|
||||
MP4DescrTag = 6
|
||||
)
|
||||
|
||||
const (
|
||||
MP4DecConfigDataSize = 2 + 3 + 4 + 4
|
||||
)
|
||||
|
||||
type ElemStreamDesc struct {
|
||||
DecConfig []byte
|
||||
TrackId uint16
|
||||
AtomPos
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) Children() []Atom {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) fillLength(b []byte, length int) (n int) {
|
||||
for i := 3; i > 0; i-- {
|
||||
b[n] = uint8(length>>uint(7*i))&0x7f | 0x80
|
||||
n++
|
||||
}
|
||||
b[n] = uint8(length & 0x7f)
|
||||
n++
|
||||
return
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) lenDescHdr() (n int) {
|
||||
return 5
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) fillDescHdr(b []byte, tag uint8, datalen int) (n int) {
|
||||
b[n] = tag
|
||||
n++
|
||||
n += self.fillLength(b[n:], datalen)
|
||||
return
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) lenESDescHdr() (n int) {
|
||||
return self.lenDescHdr()
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) lenESDescData() (n int) {
|
||||
return 3
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) fillESDescHdr(b []byte, datalen int) (n int) {
|
||||
n += self.fillDescHdr(b[n:], MP4ESDescrTag, datalen)
|
||||
pio.PutU16BE(b[n:], self.TrackId)
|
||||
n += 2
|
||||
b[n] = 0 // flags
|
||||
n++
|
||||
return
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) lenDecConfigDescHdr() (n int) {
|
||||
return self.lenDescHdr() + MP4DecConfigDataSize + self.lenDescHdr()
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) fillDecConfigDescHdr(b []byte, datalen int) (n int) {
|
||||
n += self.fillDescHdr(b[n:], MP4DecConfigDescrTag, datalen)
|
||||
b[n] = 0x40 // objectid
|
||||
n++
|
||||
b[n] = 0x15 // streamtype
|
||||
n++
|
||||
// buffer size db
|
||||
pio.PutU24BE(b[n:], 0)
|
||||
n += 3
|
||||
// max bitrage
|
||||
pio.PutU32BE(b[n:], uint32(200000))
|
||||
n += 4
|
||||
// avg bitrage
|
||||
pio.PutU32BE(b[n:], uint32(0))
|
||||
n += 4
|
||||
n += self.fillDescHdr(b[n:], MP4DecSpecificDescrTag, datalen-n)
|
||||
return
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) Len() (n int) {
|
||||
n += 8
|
||||
n += 4
|
||||
// 0x03 MP4ESDescHeader
|
||||
// 5
|
||||
n += self.lenESDescHdr()
|
||||
|
||||
// + ESID + ESFlags
|
||||
// + 2 + 1
|
||||
n += self.lenESDescData()
|
||||
|
||||
// 0x04 MP4DecConfigDescrTag + MP4DecConfigDataSize + 0x05 MP4DecSpecificDescrHeader
|
||||
// 5 + 13 + 5
|
||||
n += self.lenDecConfigDescHdr()
|
||||
|
||||
// Variable size configuration
|
||||
n += len(self.DecConfig)
|
||||
|
||||
// 0x06 MP4DescrHeader + 1
|
||||
// 5 + 1
|
||||
n += self.lenDescHdr() + 1
|
||||
|
||||
return // 8 + 4 + self.lenESDescHdr() + self.lenDecConfigDescHdr() + len(self.DecConfig) + self.lenDescHdr() + 1
|
||||
}
|
||||
|
||||
// Version(4)
|
||||
// ESDesc(
|
||||
// MP4ESDescrTag
|
||||
// ESID(2)
|
||||
// ESFlags(1)
|
||||
// DecConfigDesc(
|
||||
// MP4DecConfigDescrTag
|
||||
// objectId streamType bufSize avgBitrate
|
||||
// DecSpecificDesc(
|
||||
// MP4DecSpecificDescrTag
|
||||
// decConfig
|
||||
// )
|
||||
// )
|
||||
// ?Desc(lenDescHdr+1)
|
||||
// )
|
||||
|
||||
func (self ElemStreamDesc) Marshal(b []byte) (n int) {
|
||||
pio.PutU32BE(b[4:], uint32(ESDS))
|
||||
n += 8
|
||||
pio.PutU32BE(b[n:], 0) // Version
|
||||
n += 4
|
||||
datalen := self.Len()
|
||||
n += self.fillESDescHdr(b[n:], datalen-n-self.lenESDescHdr())
|
||||
|
||||
n += self.fillDecConfigDescHdr(b[n:], datalen-n-self.lenDescHdr()-self.lenDescHdr()-1)
|
||||
copy(b[n:], self.DecConfig)
|
||||
n += len(self.DecConfig)
|
||||
|
||||
n += self.fillDescHdr(b[n:], 0x06, datalen-n-self.lenDescHdr())
|
||||
b[n] = 0x02
|
||||
n++
|
||||
pio.PutU32BE(b[0:], uint32(n))
|
||||
return
|
||||
}
|
||||
|
||||
func (self *ElemStreamDesc) Unmarshal(b []byte, offset int) (n int, err error) {
|
||||
if len(b) < n+12 {
|
||||
err = parseErr("hdr", offset+n, err)
|
||||
return
|
||||
}
|
||||
(&self.AtomPos).setPos(offset, len(b))
|
||||
n += 8
|
||||
n += 4
|
||||
return self.parseDesc(b[n:], offset+n)
|
||||
}
|
||||
|
||||
func (self *ElemStreamDesc) parseDesc(b []byte, offset int) (n int, err error) {
|
||||
var hdrlen int
|
||||
var datalen int
|
||||
var tag uint8
|
||||
if hdrlen, tag, datalen, err = self.parseDescHdr(b, offset); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip over the header lenth (tag size 1 byte + lenlen)
|
||||
n += hdrlen
|
||||
|
||||
if len(b) < n+datalen {
|
||||
err = parseErr("datalen", offset+n, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case MP4ESDescrTag:
|
||||
if len(b) < n+3 {
|
||||
err = parseErr("MP4ESDescrTag", offset+n, err)
|
||||
return
|
||||
}
|
||||
if _, err = self.parseDesc(b[n+3:], offset+n+3); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case MP4DecConfigDescrTag:
|
||||
if len(b) < n+MP4DecConfigDataSize {
|
||||
err = parseErr("MP4DecSpecificDescrTag", offset+n, err)
|
||||
return
|
||||
}
|
||||
if _, err = self.parseDesc(b[n+MP4DecConfigDataSize:], offset+n+MP4DecConfigDataSize); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case MP4DecSpecificDescrTag:
|
||||
self.DecConfig = b[n : n+datalen]
|
||||
}
|
||||
|
||||
n += datalen
|
||||
return
|
||||
}
|
||||
|
||||
func (self *ElemStreamDesc) parseLength(b []byte, offset int) (n int, length int, err error) {
|
||||
for n < 4 {
|
||||
if len(b) < n+1 {
|
||||
err = parseErr("len", offset+n, err)
|
||||
return
|
||||
}
|
||||
c := b[n]
|
||||
n++
|
||||
length = (length << 7) | (int(c) & 0x7f)
|
||||
if c&0x80 == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self *ElemStreamDesc) parseDescHdr(b []byte, offset int) (n int, tag uint8, datalen int, err error) {
|
||||
var lenlen int
|
||||
if len(b) < n+1 {
|
||||
err = parseErr("tag", offset+n, err)
|
||||
return
|
||||
}
|
||||
tag = b[n]
|
||||
n++
|
||||
if lenlen, datalen, err = self.parseLength(b[n:], offset+n); err != nil {
|
||||
return
|
||||
}
|
||||
n += lenlen
|
||||
return
|
||||
}
|
||||
|
||||
func ReadFileAtoms(r io.ReadSeeker) (atoms []Atom, err error) {
|
||||
for {
|
||||
offset, _ := r.Seek(0, 1)
|
||||
taghdr := make([]byte, 8)
|
||||
if _, err = io.ReadFull(r, taghdr); err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
size := pio.U32BE(taghdr[0:])
|
||||
tag := Tag(pio.U32BE(taghdr[4:]))
|
||||
|
||||
var atom Atom
|
||||
switch tag {
|
||||
case MOOV:
|
||||
atom = &Movie{}
|
||||
case MOOF:
|
||||
atom = &MovieFrag{}
|
||||
}
|
||||
|
||||
if atom != nil {
|
||||
b := make([]byte, int(size))
|
||||
if _, err = io.ReadFull(r, b[8:]); err != nil {
|
||||
return
|
||||
}
|
||||
copy(b, taghdr)
|
||||
if _, err = atom.Unmarshal(b, int(offset)); err != nil {
|
||||
return
|
||||
}
|
||||
atoms = append(atoms, atom)
|
||||
} else {
|
||||
dummy := &Dummy{Tag_: tag}
|
||||
dummy.setPos(int(offset), int(size))
|
||||
if _, err = r.Seek(int64(size)-8, 1); err != nil {
|
||||
return
|
||||
}
|
||||
atoms = append(atoms, dummy)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func printatom(out io.Writer, root Atom, depth int) {
|
||||
offset, size := root.Pos()
|
||||
|
||||
type stringintf interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
fmt.Fprintf(out,
|
||||
"%s%s offset=%d size=%d",
|
||||
strings.Repeat(" ", depth*2), root.Tag(), offset, size,
|
||||
)
|
||||
if str, ok := root.(stringintf); ok {
|
||||
fmt.Fprint(out, " ", str.String())
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
|
||||
children := root.Children()
|
||||
for _, child := range children {
|
||||
printatom(out, child, depth+1)
|
||||
}
|
||||
}
|
||||
|
||||
func FprintAtom(out io.Writer, root Atom) {
|
||||
printatom(out, root, 0)
|
||||
}
|
||||
|
||||
func PrintAtom(root Atom) {
|
||||
FprintAtom(os.Stdout, root)
|
||||
}
|
||||
|
||||
func (self MovieHeader) String() string {
|
||||
return fmt.Sprintf("dur=%d", self.Duration)
|
||||
}
|
||||
|
||||
func (self TimeToSample) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self SampleToChunk) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self SampleSize) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self SyncSample) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self CompositionOffset) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self ChunkOffset) String() string {
|
||||
return fmt.Sprintf("entries=%d", len(self.Entries))
|
||||
}
|
||||
|
||||
func (self TrackFragRun) String() string {
|
||||
return fmt.Sprintf("dataoffset=%d", self.DataOffset)
|
||||
}
|
||||
|
||||
func (self TrackFragHeader) String() string {
|
||||
return fmt.Sprintf("basedataoffset=%d", self.BaseDataOffset)
|
||||
}
|
||||
|
||||
func (self ElemStreamDesc) String() string {
|
||||
return fmt.Sprintf("configlen=%d", len(self.DecConfig))
|
||||
}
|
||||
|
||||
func (self *Track) GetAVC1Conf() (conf *AVC1Conf) {
|
||||
atom := FindChildren(self, AVCC)
|
||||
conf, _ = atom.(*AVC1Conf)
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Track) GetElemStreamDesc() (esds *ElemStreamDesc) {
|
||||
atom := FindChildren(self, ESDS)
|
||||
esds, _ = atom.(*ElemStreamDesc)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/fmp4/mp4io"
|
||||
"github.com/kerberos-io/agent/machinery/src/packets"
|
||||
"github.com/kerberos-io/agent/machinery/src/utils/bits/pio"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
w io.WriteSeeker
|
||||
bufw *bufio.Writer
|
||||
wpos int64
|
||||
streams []*Stream
|
||||
videoCodecIndex int
|
||||
AudioCodecIndex int
|
||||
|
||||
moof_seqnum uint32
|
||||
// Streams must start with Keyframes / IDRs
|
||||
// A keyframe is a complete sample that contains all information to produce a single image.
|
||||
// All other samples are deltas w.r.t to the last keyframe that's why MP4's must
|
||||
// always start with a keyframe because any other type of frame will have not point of reference.
|
||||
// It does mean we lose some data but it was useless anyways.
|
||||
// This is on the muxer & not on an individual stream to prevent audio (pre first keyframe) of making
|
||||
// it into our MP4 essentially delaying the audio by a few seconds perhaps (depends on keyframe interval).
|
||||
gotFirstKeyframe bool
|
||||
}
|
||||
|
||||
func NewMuxer(w io.WriteSeeker) *Muxer {
|
||||
return &Muxer{
|
||||
w: w,
|
||||
bufw: bufio.NewWriterSize(w, pio.RecommendBufioSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Muxer) newStream(codec packets.Stream, index int, withoutAudio bool) (err error) {
|
||||
|
||||
switch codec.Name {
|
||||
case "H264":
|
||||
self.videoCodecIndex = index
|
||||
case "AAC":
|
||||
default:
|
||||
self.AudioCodecIndex = index
|
||||
if withoutAudio {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stream := &Stream{
|
||||
CodecData: codec,
|
||||
Idx: index,
|
||||
}
|
||||
|
||||
stream.sample = &mp4io.SampleTable{
|
||||
SampleDesc: &mp4io.SampleDesc{},
|
||||
TimeToSample: &mp4io.TimeToSample{},
|
||||
SampleToChunk: &mp4io.SampleToChunk{},
|
||||
SampleSize: &mp4io.SampleSize{},
|
||||
ChunkOffset: &mp4io.ChunkOffset{},
|
||||
}
|
||||
|
||||
stream.trackAtom = &mp4io.Track{
|
||||
Header: &mp4io.TrackHeader{
|
||||
TrackId: int32(len(self.streams) + 1),
|
||||
Flags: 0x0003, // Track enabled | Track in movie
|
||||
Duration: 0, // fill later
|
||||
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
||||
},
|
||||
Media: &mp4io.Media{
|
||||
Header: &mp4io.MediaHeader{
|
||||
TimeScale: 0, // fill later
|
||||
Duration: 0, // fill later
|
||||
Language: 21956,
|
||||
},
|
||||
Info: &mp4io.MediaInfo{
|
||||
Sample: stream.sample,
|
||||
Data: &mp4io.DataInfo{
|
||||
Refer: &mp4io.DataRefer{
|
||||
Url: &mp4io.DataReferUrl{
|
||||
Flags: 0x000001, // Self reference
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
switch codec.Name {
|
||||
case "H264":
|
||||
stream.sample.SyncSample = &mp4io.SyncSample{}
|
||||
}
|
||||
|
||||
stream.timeScale = 12288 // 90000 //
|
||||
stream.muxer = self
|
||||
self.streams = append(self.streams, stream)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Stream) fillTrackAtom() (err error) {
|
||||
self.trackAtom.Media.Header.TimeScale = int32(self.timeScale)
|
||||
self.trackAtom.Media.Header.Duration = int32(self.duration)
|
||||
|
||||
if self.CodecData.Name == "H264" {
|
||||
|
||||
codec := self.CodecData
|
||||
width, height := codec.Width, codec.Height
|
||||
decoderData := []byte{}
|
||||
|
||||
recordinfo := AVCDecoderConfRecord{}
|
||||
if len(self.CodecData.SPS) > 0 {
|
||||
recordinfo.AVCProfileIndication = self.CodecData.SPS[1]
|
||||
recordinfo.ProfileCompatibility = self.CodecData.SPS[2]
|
||||
recordinfo.AVCLevelIndication = self.CodecData.SPS[3]
|
||||
recordinfo.SPS = [][]byte{self.CodecData.SPS}
|
||||
|
||||
}
|
||||
if len(self.CodecData.PPS) > 0 {
|
||||
recordinfo.PPS = [][]byte{self.CodecData.PPS}
|
||||
}
|
||||
recordinfo.LengthSizeMinusOne = 3 // check...
|
||||
|
||||
buf := make([]byte, recordinfo.Len())
|
||||
recordinfo.Marshal(buf)
|
||||
|
||||
self.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: decoderData},
|
||||
}
|
||||
self.trackAtom.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||
Name: []byte("Video Media Handler"),
|
||||
}
|
||||
self.trackAtom.Media.Info.Video = &mp4io.VideoMediaInfo{
|
||||
Flags: 0x000001,
|
||||
}
|
||||
self.trackAtom.Header.TrackWidth = float64(width)
|
||||
self.trackAtom.Header.TrackHeight = float64(height)
|
||||
|
||||
} else if self.CodecData.Name == "AAC" {
|
||||
/*codec := self.CodecData.(aacparser.CodecData)
|
||||
audioConfig := codec.MPEG4AudioConfigBytes()
|
||||
self.sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
||||
DataRefIdx: 2,
|
||||
NumberOfChannels: int16(codec.ChannelLayout().Count()),
|
||||
SampleSize: int16(codec.SampleFormat().BytesPerSample()),
|
||||
SampleRate: float64(codec.SampleRate()),
|
||||
Conf: &mp4io.ElemStreamDesc{
|
||||
DecConfig: audioConfig,
|
||||
},
|
||||
}
|
||||
self.trackAtom.Header.Volume = 1
|
||||
self.trackAtom.Header.AlternateGroup = 1
|
||||
self.trackAtom.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'s', 'o', 'u', 'n'},
|
||||
Name: []byte{'S', 'o', 'u', 'n', 'd', 'H', 'a', 'n', 'd', 'l', 'e', 'r', 0},
|
||||
}
|
||||
self.trackAtom.Media.Info.Sound = &mp4io.SoundMediaInfo{}*/
|
||||
|
||||
} else {
|
||||
err = fmt.Errorf("mp4: codec type=%d invalid", self.CodecData.Name)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) WriteHeader(streams []packets.Stream) (err error) {
|
||||
self.streams = []*Stream{}
|
||||
for i, stream := range streams {
|
||||
if err = self.newStream(stream, i, false); err != nil {
|
||||
// no need to stop the recording if a codec doesnt match, still try to...
|
||||
}
|
||||
}
|
||||
/*
|
||||
https://www.w3.org/2013/12/byte-stream-format-registry/isobmff-byte-stream-format.html#h2_iso-init-segments
|
||||
The user agent must run the end of stream algorithm with the error parameter set to "decode" if any of the following conditions are met:
|
||||
- A File Type Box contains a major_brand or compatible_brand that the user agent does not support.
|
||||
- A box or field in the Movie Header Box is encountered that violates the requirements mandated by the major_brand or one of the compatible_brands in the File Type Box.
|
||||
- The tracks in the Movie Header Box contain samples (i.e. the entry_count in the stts, stsc or stco boxes are not set to zero).
|
||||
- A Movie Extends (mvex) box is not contained in the Movie (moov) box to indicate that Movie Fragments are to be expected.
|
||||
*/
|
||||
|
||||
moov := &mp4io.Movie{}
|
||||
moov.Header = &mp4io.MovieHeader{
|
||||
PreferredRate: 1,
|
||||
PreferredVolume: 1,
|
||||
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
||||
NextTrackId: int32(len(self.streams)), // ffmpeg uses the last track id as the next track id, makes no sense
|
||||
PreviewTime: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
PreviewDuration: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
PosterTime: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
SelectionTime: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
SelectionDuration: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
CurrentTime: time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
// Movie Extend MVEX is required for fragmented MP4s
|
||||
trackExtends := make([]*mp4io.TrackExtend, 0)
|
||||
for _, stream := range self.streams {
|
||||
// Add an extension for every available track along with their track ids.
|
||||
ext := &mp4io.TrackExtend{
|
||||
TrackId: uint32(stream.Idx),
|
||||
DefaultSampleDescIdx: uint32(1),
|
||||
}
|
||||
trackExtends = append(trackExtends, ext)
|
||||
}
|
||||
moov.MovieExtend = &mp4io.MovieExtend{
|
||||
Tracks: trackExtends,
|
||||
}
|
||||
|
||||
// TODO(atom): write a parser of the User Data Box (udta)
|
||||
|
||||
maxDur := time.Duration(0)
|
||||
timeScale := int64(10000)
|
||||
for _, stream := range self.streams {
|
||||
if err = stream.fillTrackAtom(); err != nil {
|
||||
return
|
||||
}
|
||||
dur := stream.tsToTime(stream.duration)
|
||||
stream.trackAtom.Header.Duration = int32(timeToTs(dur, timeScale))
|
||||
if dur > maxDur {
|
||||
maxDur = dur
|
||||
}
|
||||
moov.Tracks = append(moov.Tracks, stream.trackAtom)
|
||||
}
|
||||
moov.Header.TimeScale = int32(timeScale)
|
||||
moov.Header.Duration = int32(timeToTs(maxDur, timeScale))
|
||||
|
||||
b := make([]byte, moov.Len())
|
||||
moov.Marshal(b)
|
||||
if _, err = self.w.Write(b); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Stream) BuildTrackFragmentWithoutOffset() (trackFragment *mp4io.TrackFrag, err error) {
|
||||
// new duration
|
||||
newDts := self.dts
|
||||
|
||||
// Create TrackFragRunEntries
|
||||
trackfragentries := make([]mp4io.TrackFragRunEntry, 0) // https://ffmpeg.org/pipermail/ffmpeg-devel/2014-November/164898.html
|
||||
// Loop over all the samples and build the Track Fragment Entries.
|
||||
// Each sample gets its own entry which essentially captures the duration of the sample
|
||||
// and the location within the MDAT relative by the size of the sample.
|
||||
for _, pkt := range self.pkts {
|
||||
|
||||
// Calculate the duration of the frame, if no previous frames were recorded then
|
||||
// invent a timestamp (1ms) for it to make sure it's not 0.
|
||||
var duration time.Duration
|
||||
if self.lastpkt != nil {
|
||||
duration = pkt.Time - self.lastpkt.Time
|
||||
}
|
||||
|
||||
// Increment the decode timestamp for the next Track Fragment Decode Time (TFDT)
|
||||
// Essentially it is the combination of the durations of the samples.
|
||||
newDts += self.timeToTs(duration)
|
||||
|
||||
/*
|
||||
if duration == 0 {
|
||||
duration = 40 * time.Millisecond
|
||||
}*/
|
||||
|
||||
// Audio tends to build very predictable packets due to its sampling rate
|
||||
// A possible optimization would be to rely on the default flags instead.
|
||||
// This requires looping over all packets and verifying the default size & duration.const
|
||||
// Saves a few bytes for each trun entry and could be looked into.const
|
||||
// Current behavior is to explicitly write all of the entries their size & duration.
|
||||
entry := mp4io.TrackFragRunEntry{
|
||||
Duration: uint32(self.timeToTs(duration)), // The timescaled duration e.g 2999
|
||||
Size: uint32(len(pkt.Data)), // The length of the sample in bytes e.g 51677
|
||||
Flags: uint32(33554432),
|
||||
Cts: uint32(self.timeToTs(pkt.CompositionTime)), // Composition timestamp is typically for B-frames, which are not used in RTSP
|
||||
}
|
||||
trackfragentries = append(trackfragentries, entry)
|
||||
self.lastpkt = pkt
|
||||
}
|
||||
|
||||
// Build the Track Fragment
|
||||
DefaultSampleFlags := uint32(0)
|
||||
if self.CodecData.Name == "H264" {
|
||||
DefaultSampleFlags = 16842752
|
||||
} else {
|
||||
// audio
|
||||
DefaultSampleFlags = 33554432
|
||||
}
|
||||
|
||||
// If no fragment entries are available, then just set the durations to 512
|
||||
// TODO: demuxer bug for B-frames has the same dts
|
||||
DefaultDuration := uint32(512)
|
||||
|
||||
// Set the track frag flags such that they include the flag for CTS
|
||||
trackFragRunFlags := uint32(mp4io.TRUN_DATA_OFFSET | mp4io.TRUN_FIRST_SAMPLE_FLAGS | mp4io.TRUN_SAMPLE_SIZE | mp4io.TRUN_SAMPLE_DURATION)
|
||||
if self.hasBFrames { // TODO: add check if video track
|
||||
//trackFragRunFlags = trackFragRunFlags | mp4io.TRUN_SAMPLE_CTS
|
||||
trackFragRunFlags = uint32(mp4io.TRUN_DATA_OFFSET | mp4io.TRUN_FIRST_SAMPLE_FLAGS | mp4io.TRUN_SAMPLE_SIZE | mp4io.TRUN_SAMPLE_CTS)
|
||||
// TODO: in ffmpeg this is 33554432 for video track & none if audio
|
||||
}
|
||||
|
||||
FirstSampleFlags := uint32(mp4io.TRUN_SAMPLE_SIZE | mp4io.TRUN_SAMPLE_DURATION) // mp4io.TRUN_DATA_OFFSET | mp4io.TRUN_FIRST_SAMPLE_FLAGS |
|
||||
// The first packet is a b-frame so set the first sample flags to have a CTS.
|
||||
if len(self.pkts) > 0 && self.pkts[0].CompositionTime > 0 {
|
||||
FirstSampleFlags = uint32(mp4io.TRUN_SAMPLE_SIZE | mp4io.TRUN_SAMPLE_CTS)
|
||||
}
|
||||
|
||||
trackFragment = &mp4io.TrackFrag{
|
||||
Header: &mp4io.TrackFragHeader{
|
||||
Version: uint8(0),
|
||||
Flags: uint32(mp4io.TFHD_DEFAULT_FLAGS | mp4io.TFHD_DEFAULT_DURATION | mp4io.TFHD_DEFAULT_BASE_IS_MOOF), // uint32(131128),
|
||||
TrackId: uint32(self.Idx),
|
||||
DefaultDuration: DefaultDuration,
|
||||
DefaultFlags: DefaultSampleFlags, // TODO: fix to real flags
|
||||
},
|
||||
DecodeTime: &mp4io.TrackFragDecodeTime{
|
||||
Version: uint8(1), // Decides whether 1 = 64bit, 0 = 32bit timestamp
|
||||
Flags: uint32(0),
|
||||
DecodeTime: uint64(self.dts), // Decode timestamp timescaled
|
||||
},
|
||||
Run: &mp4io.TrackFragRun{
|
||||
Version: uint8(0),
|
||||
Flags: trackFragRunFlags, // The flags if 0 then no DataOffset & no FirstSampleFlags
|
||||
DataOffset: uint32(368), // NOTE: this is rewritten later
|
||||
FirstSampleFlags: FirstSampleFlags,
|
||||
Entries: trackfragentries,
|
||||
},
|
||||
}
|
||||
|
||||
// Set the next dts
|
||||
newDts += self.timeToTs(1 * time.Millisecond)
|
||||
self.dts = newDts
|
||||
|
||||
// Reset hasBFrames
|
||||
self.hasBFrames = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) flushMoof() (err error) {
|
||||
|
||||
// Build the Track Frags
|
||||
trackFragments := make([]*mp4io.TrackFrag, 0)
|
||||
for _, stream := range self.streams {
|
||||
// Build the Track Frag for this stream
|
||||
var trackFragment *mp4io.TrackFrag
|
||||
trackFragment, err = stream.BuildTrackFragmentWithoutOffset()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
trackFragments = append(trackFragments, trackFragment)
|
||||
}
|
||||
|
||||
// Defer the clearing of the packets, we'll need them later in this function to
|
||||
// write the MDAT contents & calculate its size.
|
||||
defer func() {
|
||||
for _, stream := range self.streams {
|
||||
stream.pkts = make([]*packets.Packet, 0)
|
||||
}
|
||||
}()
|
||||
|
||||
moof := &mp4io.MovieFrag{
|
||||
Header: &mp4io.MovieFragHeader{
|
||||
Version: uint8(0),
|
||||
Flags: uint32(0),
|
||||
Seqnum: self.moof_seqnum,
|
||||
},
|
||||
Tracks: trackFragments,
|
||||
}
|
||||
|
||||
// Fix the dataoffsets of the track run
|
||||
nextDataOffset := uint32(moof.Len() + 8)
|
||||
for _, track := range moof.Tracks {
|
||||
track.Run.DataOffset = nextDataOffset
|
||||
for _, entry := range track.Run.Entries {
|
||||
nextDataOffset += entry.Size
|
||||
}
|
||||
}
|
||||
|
||||
// Write the MOOF
|
||||
b := make([]byte, moof.Len())
|
||||
moof.Marshal(b)
|
||||
if _, err = self.w.Write(b); err != nil {
|
||||
return
|
||||
}
|
||||
b = nil
|
||||
|
||||
// Write the MDAT size
|
||||
mdatsize := uint32(8) // skip itself
|
||||
for _, fragment := range trackFragments {
|
||||
for _, entry := range fragment.Run.Entries {
|
||||
mdatsize += entry.Size
|
||||
}
|
||||
}
|
||||
|
||||
taghdr := make([]byte, 4)
|
||||
pio.PutU32BE(taghdr, mdatsize)
|
||||
if _, err = self.w.Write(taghdr); err != nil {
|
||||
return
|
||||
}
|
||||
taghdr = nil
|
||||
|
||||
// Write the MDAT header
|
||||
taghdr = make([]byte, 4)
|
||||
pio.PutU32BE(taghdr, uint32(mp4io.MDAT))
|
||||
if _, err = self.w.Write(taghdr); err != nil {
|
||||
return
|
||||
}
|
||||
taghdr = nil
|
||||
|
||||
// Write the MDAT contents
|
||||
for _, stream := range self.streams {
|
||||
for _, pkt := range stream.pkts {
|
||||
if _, err = self.w.Write(pkt.Data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the SeqNum
|
||||
self.moof_seqnum++
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) Write(buffer []byte, channel int, time uint32) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Muxer) WritePacket(pkt packets.Packet) (err error) {
|
||||
// Check if pkt.Idx is a valid stream
|
||||
if len(self.streams) < int(pkt.Idx+1) {
|
||||
return
|
||||
}
|
||||
stream := self.streams[pkt.Idx]
|
||||
|
||||
// Wait until we have a video packet & it's a keyframe
|
||||
if pkt.IsKeyFrame && !self.gotFirstKeyframe && stream.CodecData.IsVideo {
|
||||
// First keyframe found, we can start processing
|
||||
self.gotFirstKeyframe = true
|
||||
} else if !self.gotFirstKeyframe {
|
||||
// Skip all packets until keyframe first
|
||||
return
|
||||
} else if pkt.IsKeyFrame {
|
||||
// At this point, we have a keyframe and had one before.
|
||||
self.flushMoof()
|
||||
}
|
||||
|
||||
if err = stream.writePacket(pkt); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Stream) writePacket(pkt packets.Packet /*, rawdur time.Duration*/) (err error) {
|
||||
self.pkts = append(self.pkts, &pkt)
|
||||
// Optimization: set the has B Frames boolean to indicate that there are B-Frames
|
||||
// that require the TrackFragRun will require the CTS flags.
|
||||
self.hasBFrames = self.hasBFrames || pkt.CompositionTime > 0
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) WriteTrailer() (err error) {
|
||||
self.bufw = nil
|
||||
self.streams = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) WriteTrailerWithPacket(pkt packets.Packet) (err error) {
|
||||
// Check if pkt.Idx is a valid stream
|
||||
if len(self.streams) < int(pkt.Idx+1) {
|
||||
return
|
||||
}
|
||||
stream := self.streams[pkt.Idx]
|
||||
|
||||
// Wait until we have a video packet & it's a keyframe
|
||||
if pkt.IsKeyFrame && !self.gotFirstKeyframe && stream.CodecData.IsVideo {
|
||||
// First keyframe found, we can start processing
|
||||
self.gotFirstKeyframe = true
|
||||
} else if !self.gotFirstKeyframe {
|
||||
// Skip all packets until keyframe first
|
||||
return
|
||||
} else if pkt.IsKeyFrame {
|
||||
// At this point, we have a keyframe and had one before.
|
||||
self.flushMoof()
|
||||
}
|
||||
|
||||
if err = stream.writePacket(pkt); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.bufw = nil
|
||||
self.streams = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Muxer) Close() (err error) {
|
||||
for _, stream := range self.streams {
|
||||
stream.muxer = nil
|
||||
stream.trackAtom = nil
|
||||
stream.sample = nil
|
||||
stream.lastpkt = nil
|
||||
stream = nil
|
||||
}
|
||||
self.streams = nil
|
||||
return
|
||||
}
|
||||
|
||||
type AVCDecoderConfRecord struct {
|
||||
AVCProfileIndication uint8
|
||||
ProfileCompatibility uint8
|
||||
AVCLevelIndication uint8
|
||||
LengthSizeMinusOne uint8
|
||||
SPS [][]byte
|
||||
PPS [][]byte
|
||||
}
|
||||
|
||||
func (self AVCDecoderConfRecord) Len() (n int) {
|
||||
n = 7
|
||||
for _, sps := range self.SPS {
|
||||
n += 2 + len(sps)
|
||||
}
|
||||
for _, pps := range self.PPS {
|
||||
n += 2 + len(pps)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self AVCDecoderConfRecord) Marshal(b []byte) (n int) {
|
||||
b[0] = 1
|
||||
b[1] = self.AVCProfileIndication
|
||||
b[2] = self.ProfileCompatibility
|
||||
b[3] = self.AVCLevelIndication
|
||||
b[4] = self.LengthSizeMinusOne | 0xfc
|
||||
b[5] = uint8(len(self.SPS)) | 0xe0
|
||||
n += 6
|
||||
|
||||
for _, sps := range self.SPS {
|
||||
pio.PutU16BE(b[n:], uint16(len(sps)))
|
||||
n += 2
|
||||
copy(b[n:], sps)
|
||||
n += len(sps)
|
||||
}
|
||||
|
||||
b[n] = uint8(len(self.PPS))
|
||||
n++
|
||||
|
||||
for _, pps := range self.PPS {
|
||||
pio.PutU16BE(b[n:], uint16(len(pps)))
|
||||
n += 2
|
||||
copy(b[n:], pps)
|
||||
n += len(pps)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package fmp4
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/fmp4/mp4io"
|
||||
"github.com/kerberos-io/agent/machinery/src/packets"
|
||||
)
|
||||
|
||||
type Stream struct {
|
||||
CodecData packets.Stream
|
||||
|
||||
trackAtom *mp4io.Track
|
||||
Idx int
|
||||
|
||||
// pkts to be used in MDAT and MOOF > TRAF > TRUN
|
||||
lastpkt *packets.Packet
|
||||
pkts []*packets.Packet
|
||||
hasBFrames bool
|
||||
|
||||
timeScale int64
|
||||
duration int64
|
||||
|
||||
muxer *Muxer
|
||||
//demuxer *Demuxer
|
||||
|
||||
sample *mp4io.SampleTable
|
||||
dts int64
|
||||
}
|
||||
|
||||
func timeToTs(tm time.Duration, timeScale int64) int64 {
|
||||
return int64(tm * time.Duration(timeScale) / time.Second)
|
||||
}
|
||||
|
||||
func tsToTime(ts int64, timeScale int64) time.Duration {
|
||||
return time.Duration(ts) * time.Second / time.Duration(timeScale)
|
||||
}
|
||||
|
||||
func (self *Stream) timeToTs(tm time.Duration) int64 {
|
||||
return int64(tm * time.Duration(self.timeScale) / time.Second)
|
||||
}
|
||||
|
||||
func (self *Stream) tsToTime(ts int64) time.Duration {
|
||||
return time.Duration(ts) * time.Second / time.Duration(self.timeScale)
|
||||
}
|
||||
@@ -91,7 +91,6 @@ func (self *Muxer) newStream(codec packets.Stream, index int, withoutAudio bool)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Stream) fillTrackAtom() (err error) {
|
||||
self.trackAtom.Media.Header.TimeScale = int32(self.timeScale)
|
||||
self.trackAtom.Media.Header.Duration = int32(self.duration)
|
||||
@@ -100,6 +99,24 @@ func (self *Stream) fillTrackAtom() (err error) {
|
||||
|
||||
codec := self.CodecData
|
||||
width, height := codec.Width, codec.Height
|
||||
decoderData := []byte{}
|
||||
|
||||
recordinfo := AVCDecoderConfRecord{}
|
||||
if len(self.CodecData.SPS) > 0 {
|
||||
recordinfo.AVCProfileIndication = self.CodecData.SPS[1]
|
||||
recordinfo.ProfileCompatibility = self.CodecData.SPS[2]
|
||||
recordinfo.AVCLevelIndication = self.CodecData.SPS[3]
|
||||
recordinfo.SPS = [][]byte{self.CodecData.SPS}
|
||||
|
||||
}
|
||||
if len(self.CodecData.PPS) > 0 {
|
||||
recordinfo.PPS = [][]byte{self.CodecData.PPS}
|
||||
}
|
||||
recordinfo.LengthSizeMinusOne = 3 // check...
|
||||
|
||||
buf := make([]byte, recordinfo.Len())
|
||||
recordinfo.Marshal(buf)
|
||||
|
||||
self.sample.SampleDesc.AVC1Desc = &mp4io.AVC1Desc{
|
||||
DataRefIdx: 1,
|
||||
HorizontalResolution: 72,
|
||||
@@ -109,7 +126,7 @@ func (self *Stream) fillTrackAtom() (err error) {
|
||||
FrameCount: 1,
|
||||
Depth: 24,
|
||||
ColorTableId: -1,
|
||||
//Conf: &mp4io.AVC1Conf{Data: codec.AVCDecoderConfRecordBytes()},
|
||||
Conf: &mp4io.AVC1Conf{Data: decoderData},
|
||||
}
|
||||
self.trackAtom.Media.Handler = &mp4io.HandlerRefer{
|
||||
SubType: [4]byte{'v', 'i', 'd', 'e'},
|
||||
@@ -379,3 +396,52 @@ func (self *Muxer) Close() (err error) {
|
||||
self.streams = nil
|
||||
return
|
||||
}
|
||||
|
||||
type AVCDecoderConfRecord struct {
|
||||
AVCProfileIndication uint8
|
||||
ProfileCompatibility uint8
|
||||
AVCLevelIndication uint8
|
||||
LengthSizeMinusOne uint8
|
||||
SPS [][]byte
|
||||
PPS [][]byte
|
||||
}
|
||||
|
||||
func (self AVCDecoderConfRecord) Len() (n int) {
|
||||
n = 7
|
||||
for _, sps := range self.SPS {
|
||||
n += 2 + len(sps)
|
||||
}
|
||||
for _, pps := range self.PPS {
|
||||
n += 2 + len(pps)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (self AVCDecoderConfRecord) Marshal(b []byte) (n int) {
|
||||
b[0] = 1
|
||||
b[1] = self.AVCProfileIndication
|
||||
b[2] = self.ProfileCompatibility
|
||||
b[3] = self.AVCLevelIndication
|
||||
b[4] = self.LengthSizeMinusOne | 0xfc
|
||||
b[5] = uint8(len(self.SPS)) | 0xe0
|
||||
n += 6
|
||||
|
||||
for _, sps := range self.SPS {
|
||||
pio.PutU16BE(b[n:], uint16(len(sps)))
|
||||
n += 2
|
||||
copy(b[n:], sps)
|
||||
n += len(sps)
|
||||
}
|
||||
|
||||
b[n] = uint8(len(self.PPS))
|
||||
n++
|
||||
|
||||
for _, pps := range self.PPS {
|
||||
pio.PutU16BE(b[n:], uint16(len(pps)))
|
||||
n += 2
|
||||
copy(b[n:], pps)
|
||||
n += len(pps)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user