Files
monibuca/plugin/hls/pkg/writer.go
langhuihui 8a9fffb987 refactor: frame converter and mp4 track improvements
- Refactor frame converter implementation
- Update mp4 track to use ICodex
- General refactoring and code improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 19:55:37 +08:00

182 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package hls
import (
"container/ring"
"fmt"
"math"
"strconv"
"strings"
"sync"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/format"
mpegts "m7s.live/v5/pkg/format/ts"
"m7s.live/v5/pkg/util"
)
func NewTransform() m7s.ITransformer {
ret := &HLSWriter{
Window: 3,
Fragment: 5 * time.Second,
}
return ret
}
type HLSWriter struct {
m7s.DefaultTransformer
Window int
Fragment time.Duration
M3u8 util.Buffer
ts *TsInMemory
write_time time.Duration
memoryTs sync.Map
hls_segment_count uint32 // hls segment count
playlist Playlist
infoRing *ring.Ring
hls_playlist_count uint32
hls_segment_window uint32
lastReadTime time.Time
}
func (w *HLSWriter) Start() (err error) {
return w.TransformJob.Subscribe()
}
func (w *HLSWriter) GetTs(key string) (any, bool) {
w.lastReadTime = time.Now()
return w.memoryTs.Load(key)
}
func (w *HLSWriter) Run() (err error) {
if conf, ok := w.TransformJob.Config.Input.(string); ok {
ss := strings.Split(conf, "x")
if len(ss) != 2 {
return fmt.Errorf("invalid input config %s", conf)
}
w.Fragment, err = time.ParseDuration(strings.TrimSpace(ss[0]))
if err != nil {
return
}
w.Window, err = strconv.Atoi(strings.TrimSpace(ss[1]))
if err != nil {
return
}
}
subscriber := w.TransformJob.Subscriber
w.hls_segment_window = uint32(w.Window) + 1
w.infoRing = ring.New(w.Window + 1)
w.playlist = Playlist{
Writer: &w.M3u8,
Version: 3,
Sequence: 0,
Targetduration: int(w.Fragment / time.Millisecond / 666), // hlsFragment * 1.5 / 1000
}
MemoryTs.Store(w.TransformJob.StreamPath, w)
var audioCodec, videoCodec codec.FourCC
if subscriber.Publisher.HasAudioTrack() {
audioCodec = subscriber.Publisher.AudioTrack.FourCC()
}
if subscriber.Publisher.HasVideoTrack() {
videoCodec = subscriber.Publisher.VideoTrack.FourCC()
}
w.ts = &TsInMemory{}
pesAudio, pesVideo := mpegts.CreatePESWriters()
w.ts.WritePMTPacket(audioCodec, videoCodec)
return m7s.PlayBlock(subscriber, func(audio *format.Mpeg2Audio) error {
pesAudio.Pts = uint64(subscriber.AudioReader.AbsTime) * 90
return pesAudio.WritePESPacket(audio.Memory, &w.ts.RecyclableMemory)
}, func(video *mpegts.VideoFrame) (err error) {
vr := w.TransformJob.Subscriber.VideoReader
if vr.Value.IDR {
if err = w.checkFragment(video.Timestamp); err != nil {
return
}
}
pesVideo.IsKeyFrame = video.IDR
pesVideo.Pts = uint64(vr.AbsTime+video.GetCTS32()) * 90
pesVideo.Dts = uint64(vr.AbsTime) * 90
return pesVideo.WritePESPacket(video.Memory, &w.ts.RecyclableMemory)
})
}
func (w *HLSWriter) checkFragment(ts time.Duration) (err error) {
// 当前的时间戳减去上一个ts切片的时间戳
if dur := ts - w.write_time; dur >= w.Fragment {
streamPath := w.TransformJob.StreamPath
ss := strings.Split(streamPath, "/")
// fmt.Println("time :", video.Timestamp, tsSegmentTimestamp)
if dur == ts && w.write_time == 0 { //时间戳不对的情况首个默认为2s
dur = time.Duration(2) * time.Second
}
num := w.hls_segment_count
tsFilename := strconv.FormatInt(time.Now().Unix(), 10) + "_" + strconv.FormatUint(uint64(num), 10) + ".ts"
tsFilePath := streamPath + "/" + tsFilename
// println(hls.currentTs.Length)
w.Debug("write ts", "tsFilePath", tsFilePath)
w.memoryTs.Store(tsFilePath, w.ts)
w.ts = &TsInMemory{
PMT: w.ts.PMT,
}
if w.playlist.Targetduration < int(dur.Seconds()) {
w.playlist.Targetduration = int(math.Ceil(dur.Seconds()))
}
if w.M3u8.Len() == 0 {
w.playlist.Init()
}
inf := PlaylistInf{
//浮点计算精度
Duration: dur.Seconds(),
URL: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
FilePath: tsFilePath,
}
if w.hls_segment_count > 0 {
if w.hls_playlist_count >= uint32(w.Window) {
w.M3u8.Reset()
if err = w.playlist.Init(); err != nil {
return
}
//playlist起点是ring.next长度是len(ring)-1
for p := w.infoRing.Next(); p != w.infoRing; p = p.Next() {
w.playlist.WriteInf(p.Value.(PlaylistInf))
}
} else {
if err = w.playlist.WriteInf(w.infoRing.Prev().Value.(PlaylistInf)); err != nil {
return
}
}
MemoryM3u8.Store(w.TransformJob.StreamPath, string(w.M3u8))
w.hls_playlist_count++
}
if w.hls_segment_count >= w.hls_segment_window {
if mts, loaded := w.memoryTs.LoadAndDelete(w.infoRing.Value.(PlaylistInf).FilePath); loaded {
mts.(*TsInMemory).Recycle()
}
w.infoRing.Value = inf
w.infoRing = w.infoRing.Next()
} else {
w.infoRing.Value = inf
w.infoRing = w.infoRing.Next()
}
w.hls_segment_count++
w.write_time = ts
}
return
}
func (w *HLSWriter) Dispose() {
MemoryM3u8.Delete(w.TransformJob.StreamPath)
if ts, loaded := MemoryTs.LoadAndDelete(w.TransformJob.StreamPath); loaded {
ts.(*HLSWriter).memoryTs.Range(func(key, value any) bool {
value.(*TsInMemory).Recycle()
return true
})
}
}