289 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package mp4
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"time"
 | |
| 
 | |
| 	"git.r-2.top/kunmeng/vdk/av"
 | |
| 	"git.r-2.top/kunmeng/vdk/codec/aacparser"
 | |
| 	"git.r-2.top/kunmeng/vdk/codec/h264parser"
 | |
| 	"git.r-2.top/kunmeng/vdk/format/mp4/mp4io"
 | |
| 	"git.r-2.top/kunmeng/vdk/utils/bits/pio"
 | |
| )
 | |
| 
 | |
| type Muxer struct {
 | |
| 	w                  io.WriteSeeker
 | |
| 	bufw               *bufio.Writer
 | |
| 	wpos               int64
 | |
| 	streams            []*Stream
 | |
| 	NegativeTsMakeZero bool
 | |
| }
 | |
| 
 | |
| 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 {
 | |
| 		if self.muxer.NegativeTsMakeZero {
 | |
| 			rawdur = 0
 | |
| 		} else {
 | |
| 			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
 | |
| }
 | 
