You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
7.2 KiB
Go
284 lines
7.2 KiB
Go
package mp4
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/Danile71/joy4/av"
|
|
"github.com/Danile71/joy4/codec/aacparser"
|
|
"github.com/Danile71/joy4/codec/h264parser"
|
|
"github.com/Danile71/joy4/format/mp4/mp4io"
|
|
"github.com/Danile71/joy4/utils/bits/pio"
|
|
)
|
|
|
|
type Muxer struct {
|
|
w io.WriteSeeker
|
|
bufw *bufio.Writer
|
|
wpos int64
|
|
streams []*Stream
|
|
}
|
|
|
|
func NewMuxer(w io.WriteSeeker) *Muxer {
|
|
return &Muxer{
|
|
w: w,
|
|
bufw: bufio.NewWriterSize(w, pio.RecommendBufioSize),
|
|
}
|
|
}
|
|
|
|
func (self *Muxer) newStream(codec av.CodecData) (err error) {
|
|
switch codec.Type() {
|
|
case av.H264, av.AAC:
|
|
|
|
default:
|
|
err = fmt.Errorf("mp4: codec type=%v is not supported", codec.Type())
|
|
return
|
|
}
|
|
stream := &Stream{CodecData: codec}
|
|
|
|
stream.sample = &mp4io.SampleTable{
|
|
SampleDesc: &mp4io.SampleDesc{},
|
|
TimeToSample: &mp4io.TimeToSample{},
|
|
SampleToChunk: &mp4io.SampleToChunk{
|
|
Entries: []mp4io.SampleToChunkEntry{
|
|
{
|
|
FirstChunk: 1,
|
|
SampleDescId: 1,
|
|
SamplesPerChunk: 1,
|
|
},
|
|
},
|
|
},
|
|
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.Type() {
|
|
case av.H264:
|
|
stream.sample.SyncSample = &mp4io.SyncSample{}
|
|
}
|
|
|
|
stream.timeScale = 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.Type() == av.H264 {
|
|
codec := self.CodecData.(h264parser.CodecData)
|
|
width, height := codec.Width(), codec.Height()
|
|
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: codec.AVCDecoderConfRecordBytes()},
|
|
}
|
|
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.Type() == av.AAC {
|
|
codec := self.CodecData.(aacparser.CodecData)
|
|
self.sample.SampleDesc.MP4ADesc = &mp4io.MP4ADesc{
|
|
DataRefIdx: 1,
|
|
NumberOfChannels: int16(codec.ChannelLayout().Count()),
|
|
SampleSize: int16(codec.SampleFormat().BytesPerSample()),
|
|
SampleRate: float64(codec.SampleRate()),
|
|
Conf: &mp4io.ElemStreamDesc{
|
|
DecConfig: codec.MPEG4AudioConfigBytes(),
|
|
},
|
|
}
|
|
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("Sound Handler"),
|
|
}
|
|
self.trackAtom.Media.Info.Sound = &mp4io.SoundMediaInfo{}
|
|
|
|
} else {
|
|
err = fmt.Errorf("mp4: codec type=%d invalid", self.Type())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (self *Muxer) WriteHeader(streams []av.CodecData) (err error) {
|
|
self.streams = []*Stream{}
|
|
for _, stream := range streams {
|
|
if err = self.newStream(stream); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
taghdr := make([]byte, 8)
|
|
pio.PutU32BE(taghdr[4:], uint32(mp4io.MDAT))
|
|
if _, err = self.w.Write(taghdr); err != nil {
|
|
return
|
|
}
|
|
self.wpos += 8
|
|
|
|
for _, stream := range self.streams {
|
|
if stream.Type().IsVideo() {
|
|
stream.sample.CompositionOffset = &mp4io.CompositionOffset{}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (self *Muxer) WritePacket(pkt av.Packet) (err error) {
|
|
stream := self.streams[pkt.Idx]
|
|
if stream.lastpkt != nil {
|
|
if err = stream.writePacket(*stream.lastpkt, pkt.Time-stream.lastpkt.Time); err != nil {
|
|
return
|
|
}
|
|
}
|
|
stream.lastpkt = &pkt
|
|
return
|
|
}
|
|
|
|
func (self *Stream) writePacket(pkt av.Packet, rawdur time.Duration) (err error) {
|
|
if rawdur < 0 {
|
|
err = fmt.Errorf("mp4: stream#%d time=%v < lasttime=%v", pkt.Idx, pkt.Time, self.lastpkt.Time)
|
|
return
|
|
}
|
|
|
|
if _, err = self.muxer.bufw.Write(pkt.Data); err != nil {
|
|
return
|
|
}
|
|
|
|
if pkt.IsKeyFrame && self.sample.SyncSample != nil {
|
|
self.sample.SyncSample.Entries = append(self.sample.SyncSample.Entries, uint32(self.sampleIndex+1))
|
|
}
|
|
|
|
duration := uint32(self.timeToTs(rawdur))
|
|
if self.sttsEntry == nil || duration != self.sttsEntry.Duration {
|
|
self.sample.TimeToSample.Entries = append(self.sample.TimeToSample.Entries, mp4io.TimeToSampleEntry{Duration: duration})
|
|
self.sttsEntry = &self.sample.TimeToSample.Entries[len(self.sample.TimeToSample.Entries)-1]
|
|
}
|
|
self.sttsEntry.Count++
|
|
|
|
if self.sample.CompositionOffset != nil {
|
|
offset := uint32(self.timeToTs(pkt.CompositionTime))
|
|
if self.cttsEntry == nil || offset != self.cttsEntry.Offset {
|
|
table := self.sample.CompositionOffset
|
|
table.Entries = append(table.Entries, mp4io.CompositionOffsetEntry{Offset: offset})
|
|
self.cttsEntry = &table.Entries[len(table.Entries)-1]
|
|
}
|
|
self.cttsEntry.Count++
|
|
}
|
|
|
|
self.duration += int64(duration)
|
|
self.sampleIndex++
|
|
self.sample.ChunkOffset.Entries = append(self.sample.ChunkOffset.Entries, uint32(self.muxer.wpos))
|
|
self.sample.SampleSize.Entries = append(self.sample.SampleSize.Entries, uint32(len(pkt.Data)))
|
|
|
|
self.muxer.wpos += int64(len(pkt.Data))
|
|
return
|
|
}
|
|
|
|
func (self *Muxer) WriteTrailer() (err error) {
|
|
for _, stream := range self.streams {
|
|
if stream.lastpkt != nil {
|
|
if err = stream.writePacket(*stream.lastpkt, 0); err != nil {
|
|
return
|
|
}
|
|
stream.lastpkt = nil
|
|
}
|
|
}
|
|
|
|
moov := &mp4io.Movie{}
|
|
moov.Header = &mp4io.MovieHeader{
|
|
PreferredRate: 1,
|
|
PreferredVolume: 1,
|
|
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
|
NextTrackId: 2,
|
|
}
|
|
|
|
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))
|
|
|
|
if err = self.bufw.Flush(); err != nil {
|
|
return
|
|
}
|
|
|
|
var mdatsize int64
|
|
if mdatsize, err = self.w.Seek(0, 1); err != nil {
|
|
return
|
|
}
|
|
if _, err = self.w.Seek(0, 0); err != nil {
|
|
return
|
|
}
|
|
taghdr := make([]byte, 4)
|
|
pio.PutU32BE(taghdr, uint32(mdatsize))
|
|
if _, err = self.w.Write(taghdr); err != nil {
|
|
return
|
|
}
|
|
|
|
if _, err = self.w.Seek(0, 2); err != nil {
|
|
return
|
|
}
|
|
b := make([]byte, moov.Len())
|
|
moov.Marshal(b)
|
|
if _, err = self.w.Write(b); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|