140 lines
2.9 KiB
Go
140 lines
2.9 KiB
Go
package ingest
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
)
|
|
|
|
type liveChunk struct {
|
|
FrameType byte
|
|
Payload []byte
|
|
}
|
|
|
|
type fmp4Assembler struct {
|
|
pending []byte
|
|
|
|
initReady bool
|
|
initBuf []byte
|
|
|
|
mediaPrefix []byte
|
|
inFragment bool
|
|
fragmentBuf []byte
|
|
}
|
|
|
|
func newFMP4Assembler() *fmp4Assembler {
|
|
return &fmp4Assembler{}
|
|
}
|
|
|
|
func (a *fmp4Assembler) Push(data []byte) ([]liveChunk, error) {
|
|
if len(data) == 0 {
|
|
return nil, nil
|
|
}
|
|
a.pending = append(a.pending, data...)
|
|
|
|
result := make([]liveChunk, 0, 2)
|
|
for {
|
|
box, boxType, ok, err := a.nextBox()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
chunks, err := a.consumeBox(boxType, box)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, chunks...)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *fmp4Assembler) nextBox() ([]byte, string, bool, error) {
|
|
if len(a.pending) < 8 {
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
headerLen := 8
|
|
size := uint64(binary.BigEndian.Uint32(a.pending[:4]))
|
|
if size == 1 {
|
|
if len(a.pending) < 16 {
|
|
return nil, "", false, nil
|
|
}
|
|
headerLen = 16
|
|
size = binary.BigEndian.Uint64(a.pending[8:16])
|
|
}
|
|
if size == 0 {
|
|
// 0 means box runs to stream end; wait for more data.
|
|
return nil, "", false, nil
|
|
}
|
|
if size < uint64(headerLen) {
|
|
return nil, "", false, fmt.Errorf("invalid MP4 box size %d", size)
|
|
}
|
|
if size > uint64(len(a.pending)) {
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
boxLen := int(size)
|
|
boxType := string(a.pending[4:8])
|
|
box := make([]byte, boxLen)
|
|
copy(box, a.pending[:boxLen])
|
|
a.pending = a.pending[boxLen:]
|
|
return box, boxType, true, nil
|
|
}
|
|
|
|
func (a *fmp4Assembler) consumeBox(boxType string, box []byte) ([]liveChunk, error) {
|
|
if !a.initReady {
|
|
a.initBuf = append(a.initBuf, box...)
|
|
if boxType != "moov" {
|
|
return nil, nil
|
|
}
|
|
|
|
a.initReady = true
|
|
initPayload := cloneBytes(a.initBuf)
|
|
a.initBuf = nil
|
|
return []liveChunk{{FrameType: liveFrameTypeInit, Payload: initPayload}}, nil
|
|
}
|
|
|
|
switch boxType {
|
|
case "styp", "sidx", "prft":
|
|
if a.inFragment {
|
|
a.fragmentBuf = append(a.fragmentBuf, box...)
|
|
} else {
|
|
a.mediaPrefix = append(a.mediaPrefix, box...)
|
|
}
|
|
return nil, nil
|
|
case "moof":
|
|
a.fragmentBuf = a.fragmentBuf[:0]
|
|
if len(a.mediaPrefix) > 0 {
|
|
a.fragmentBuf = append(a.fragmentBuf, a.mediaPrefix...)
|
|
a.mediaPrefix = a.mediaPrefix[:0]
|
|
}
|
|
a.fragmentBuf = append(a.fragmentBuf, box...)
|
|
a.inFragment = true
|
|
return nil, nil
|
|
case "mdat":
|
|
if !a.inFragment {
|
|
return nil, nil
|
|
}
|
|
a.fragmentBuf = append(a.fragmentBuf, box...)
|
|
mediaPayload := cloneBytes(a.fragmentBuf)
|
|
a.fragmentBuf = a.fragmentBuf[:0]
|
|
a.inFragment = false
|
|
return []liveChunk{{FrameType: liveFrameTypeMedia, Payload: mediaPayload}}, nil
|
|
default:
|
|
if a.inFragment {
|
|
a.fragmentBuf = append(a.fragmentBuf, box...)
|
|
}
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func cloneBytes(data []byte) []byte {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]byte, len(data))
|
|
copy(out, data)
|
|
return out
|
|
}
|