golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
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
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 |
|
}
|
|
|