Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

410 lines
8.1 KiB

package playback
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"time"
"github.com/abema/go-mp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
)
const (
sampleFlagIsNonSyncSample = 1 << 16
)
func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
timeScale64 := uint64(timeScale)
secs := v / time.Second
dec := v % time.Second
return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
}
func durationMp4ToGo(v uint64, timeScale uint32) time.Duration {
timeScale64 := uint64(timeScale)
secs := v / timeScale64
dec := v % timeScale64
return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)
}
var errTerminated = errors.New("terminated")
func fmp4ReadInit(r io.ReadSeeker) ([]byte, error) {
buf := make([]byte, 8)
_, err := io.ReadFull(r, buf)
if err != nil {
return nil, err
}
if !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {
return nil, fmt.Errorf("ftyp box not found")
}
ftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
_, err = r.Seek(int64(ftypSize), io.SeekStart)
if err != nil {
return nil, err
}
_, err = io.ReadFull(r, buf)
if err != nil {
return nil, err
}
if !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {
return nil, fmt.Errorf("moov box not found")
}
moovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
buf = make([]byte, ftypSize+moovSize)
_, err = io.ReadFull(r, buf)
if err != nil {
return nil, err
}
return buf, nil
}
func seekAndMuxParts(
r io.ReadSeeker,
init []byte,
minTime time.Duration,
maxTime time.Duration,
w io.Writer,
) (time.Duration, error) {
minTimeMP4 := durationGoToMp4(minTime, 90000)
maxTimeMP4 := durationGoToMp4(maxTime, 90000)
moofOffset := uint64(0)
var tfhd *mp4.Tfhd
var tfdt *mp4.Tfdt
var outPart *fmp4.Part
var outTrack *fmp4.PartTrack
var outBuf seekablebuffer.Buffer
elapsed := uint64(0)
initWritten := false
firstSampleWritten := make(map[uint32]struct{})
gop := make(map[uint32][]*fmp4.PartSample)
_, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "moof":
moofOffset = h.BoxInfo.Offset
outPart = &fmp4.Part{}
return h.Expand()
case "traf":
return h.Expand()
case "tfhd":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfhd = box.(*mp4.Tfhd)
case "tfdt":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfdt = box.(*mp4.Tfdt)
if tfdt.BaseMediaDecodeTimeV1 >= maxTimeMP4 {
return nil, errTerminated
}
outTrack = &fmp4.PartTrack{ID: int(tfhd.TrackID)}
case "trun":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
trun := box.(*mp4.Trun)
dataOffset := moofOffset + uint64(trun.DataOffset)
_, err = r.Seek(int64(dataOffset), io.SeekStart)
if err != nil {
return nil, err
}
elapsed = tfdt.BaseMediaDecodeTimeV1
baseTimeSet := false
for _, e := range trun.Entries {
payload := make([]byte, e.SampleSize)
_, err := io.ReadFull(r, payload)
if err != nil {
return nil, err
}
if elapsed >= maxTimeMP4 {
break
}
isRandom := (e.SampleFlags & sampleFlagIsNonSyncSample) == 0
_, fsw := firstSampleWritten[tfhd.TrackID]
sa := &fmp4.PartSample{
Duration: e.SampleDuration,
PTSOffset: e.SampleCompositionTimeOffsetV1,
IsNonSyncSample: !isRandom,
Payload: payload,
}
if !fsw {
if isRandom {
gop[tfhd.TrackID] = []*fmp4.PartSample{sa}
} else {
gop[tfhd.TrackID] = append(gop[tfhd.TrackID], sa)
}
}
if elapsed >= minTimeMP4 {
if !baseTimeSet {
outTrack.BaseTime = elapsed - minTimeMP4
if !fsw {
if !isRandom {
for _, sa2 := range gop[tfhd.TrackID][:len(gop[tfhd.TrackID])-1] {
sa2.Duration = 0
sa2.PTSOffset = 0
outTrack.Samples = append(outTrack.Samples, sa2)
}
}
delete(gop, tfhd.TrackID)
firstSampleWritten[tfhd.TrackID] = struct{}{}
}
}
outTrack.Samples = append(outTrack.Samples, sa)
}
elapsed += uint64(e.SampleDuration)
}
if outTrack.Samples != nil {
outPart.Tracks = append(outPart.Tracks, outTrack)
}
outTrack = nil
case "mdat":
if outPart.Tracks != nil {
if !initWritten {
initWritten = true
_, err := w.Write(init)
if err != nil {
return nil, err
}
}
err := outPart.Marshal(&outBuf)
if err != nil {
return nil, err
}
_, err = w.Write(outBuf.Bytes())
if err != nil {
return nil, err
}
outBuf.Reset()
}
outPart = nil
}
return nil, nil
})
if err != nil && !errors.Is(err, errTerminated) {
return 0, err
}
if !initWritten {
return 0, errNoSegmentsFound
}
elapsed -= minTimeMP4
return durationMp4ToGo(elapsed, 90000), nil
}
func muxParts(
r io.ReadSeeker,
startTime time.Duration,
maxTime time.Duration,
w io.Writer,
) (time.Duration, error) {
maxTimeMP4 := durationGoToMp4(maxTime, 90000)
moofOffset := uint64(0)
var tfhd *mp4.Tfhd
var tfdt *mp4.Tfdt
var outPart *fmp4.Part
var outTrack *fmp4.PartTrack
var outBuf seekablebuffer.Buffer
elapsed := uint64(0)
_, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "moof":
moofOffset = h.BoxInfo.Offset
outPart = &fmp4.Part{}
return h.Expand()
case "traf":
return h.Expand()
case "tfhd":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfhd = box.(*mp4.Tfhd)
case "tfdt":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfdt = box.(*mp4.Tfdt)
if tfdt.BaseMediaDecodeTimeV1 >= maxTimeMP4 {
return nil, errTerminated
}
outTrack = &fmp4.PartTrack{
ID: int(tfhd.TrackID),
BaseTime: tfdt.BaseMediaDecodeTimeV1 + durationGoToMp4(startTime, 90000),
}
case "trun":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
trun := box.(*mp4.Trun)
dataOffset := moofOffset + uint64(trun.DataOffset)
_, err = r.Seek(int64(dataOffset), io.SeekStart)
if err != nil {
return nil, err
}
elapsed = tfdt.BaseMediaDecodeTimeV1
for _, e := range trun.Entries {
payload := make([]byte, e.SampleSize)
_, err := io.ReadFull(r, payload)
if err != nil {
return nil, err
}
if elapsed >= maxTimeMP4 {
break
}
isRandom := (e.SampleFlags & sampleFlagIsNonSyncSample) == 0
sa := &fmp4.PartSample{
Duration: e.SampleDuration,
PTSOffset: e.SampleCompositionTimeOffsetV1,
IsNonSyncSample: !isRandom,
Payload: payload,
}
outTrack.Samples = append(outTrack.Samples, sa)
elapsed += uint64(e.SampleDuration)
}
if outTrack.Samples != nil {
outPart.Tracks = append(outPart.Tracks, outTrack)
}
outTrack = nil
case "mdat":
if outPart.Tracks != nil {
err := outPart.Marshal(&outBuf)
if err != nil {
return nil, err
}
_, err = w.Write(outBuf.Bytes())
if err != nil {
return nil, err
}
outBuf.Reset()
}
outPart = nil
}
return nil, nil
})
if err != nil && !errors.Is(err, errTerminated) {
return 0, err
}
return durationMp4ToGo(elapsed, 90000), nil
}
func fmp4SeekAndMux(
fpath string,
minTime time.Duration,
maxTime time.Duration,
w io.Writer,
) (time.Duration, error) {
f, err := os.Open(fpath)
if err != nil {
return 0, err
}
defer f.Close()
init, err := fmp4ReadInit(f)
if err != nil {
return 0, err
}
elapsed, err := seekAndMuxParts(f, init, minTime, maxTime, w)
if err != nil {
return 0, err
}
return elapsed, nil
}
func fmp4Mux(
fpath string,
startTime time.Duration,
maxTime time.Duration,
w io.Writer,
) (time.Duration, error) {
f, err := os.Open(fpath)
if err != nil {
return 0, err
}
defer f.Close()
elapsed, err := muxParts(f, startTime, maxTime, w)
if err != nil {
return 0, err
}
return elapsed, nil
}