diff --git a/api.go b/api.go
index 0680645..94a0cb3 100644
--- a/api.go
+++ b/api.go
@@ -551,32 +551,26 @@ func (s *Server) StopPublish(ctx context.Context, req *pb.StreamSnapRequest) (re
// /api/stream/list
func (s *Server) StreamList(_ context.Context, req *pb.StreamListRequest) (res *pb.StreamListResponse, err error) {
recordingMap := make(map[string][]*pb.RecordingDetail)
- s.Records.Call(func() error {
- for record := range s.Records.Range {
- recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
- FilePath: record.RecConf.FilePath,
- Mode: record.Mode,
- Fragment: durationpb.New(record.RecConf.Fragment),
- Append: record.RecConf.Append,
- PluginName: record.Plugin.Meta.Name,
- Pointer: uint64(record.GetTaskPointer()),
- })
+ for record := range s.Records.SafeRange {
+ recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
+ FilePath: record.RecConf.FilePath,
+ Mode: record.Mode,
+ Fragment: durationpb.New(record.RecConf.Fragment),
+ Append: record.RecConf.Append,
+ PluginName: record.Plugin.Meta.Name,
+ Pointer: uint64(record.GetTaskPointer()),
+ })
+ }
+ var streams []*pb.StreamInfo
+ for publisher := range s.Streams.SafeRange {
+ info, err := s.getStreamInfo(publisher)
+ if err != nil {
+ continue
}
- return nil
- })
- s.Streams.Call(func() error {
- var streams []*pb.StreamInfo
- for publisher := range s.Streams.Range {
- info, err := s.getStreamInfo(publisher)
- if err != nil {
- continue
- }
- info.Data.Recording = recordingMap[info.Data.Path]
- streams = append(streams, info.Data)
- }
- res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
- return nil
- })
+ info.Data.Recording = recordingMap[info.Data.Path]
+ streams = append(streams, info.Data)
+ }
+ res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
return
}
diff --git a/plugin/mp4/box_structure.md b/plugin/mp4/box_structure.md
index 9d67a04..85dee48 100644
--- a/plugin/mp4/box_structure.md
+++ b/plugin/mp4/box_structure.md
@@ -37,9 +37,27 @@
| | | | | | sbgp | | sample-to-group |
| | | | | | sgpd | | sample group description |
| | | | | | subs | | sub-sample information |
+| | | udta | | | | | user-data (track level)
轨道级别的用户数据容器 |
+| | | | cprt | | | | copyright etc.
版权信息 |
+| | | | titl | | | | title
标题 |
+| | | | auth | | | | author
作者 |
| | mvex | | | | | | movie extends box |
| | | mehd | | | | | movie extends header box |
| | | trex | | | | ✓ | track extends defaults |
+| | udta | | | | | | user-data (movie level)
电影级别的用户数据容器 |
+| | | cprt | | | | | copyright etc.
版权信息 |
+| | | titl | | | | | title
标题 |
+| | | auth | | | | | author
作者 |
+| | | albm | | | | | album
专辑 |
+| | | yrrc | | | | | year
年份 |
+| | | rtng | | | | | rating
评级 |
+| | | clsf | | | | | classification
分类 |
+| | | kywd | | | | | keywords
关键词 |
+| | | loci | | | | | location information
位置信息 |
+| | | dscp | | | | | description
描述 |
+| | | perf | | | | | performer
表演者 |
+| | | gnre | | | | | genre
类型 |
+| | | meta | | | | | metadata atom
元数据原子 |
| | ipmc | | | | | | IPMP Control Box |
| moof | | | | | | | movie fragment |
| | mfhd | | | | | ✓ | movie fragment header |
@@ -54,8 +72,10 @@
| mdat | | | | | | | media data container |
| free | | | | | | | free space |
| skip | | | | | | | free space |
-| | udta | | | | | | user-data |
-| | | cprt | | | | | copyright etc. |
+| udta | | | | | | | user-data (file level)
文件级别的用户数据容器 |
+| | cprt | | | | | | copyright etc.
版权信息 |
+| | titl | | | | | | title
标题 |
+| | auth | | | | | | author
作者 |
| meta | | | | | | | metadata |
| | hdlr | | | | | ✓ | handler, declares the metadata (handler) type |
| | dinf | | | | | | data information box, container |
diff --git a/plugin/mp4/pkg/box/box.go b/plugin/mp4/pkg/box/box.go
index 6d23fd6..c91238d 100644
--- a/plugin/mp4/pkg/box/box.go
+++ b/plugin/mp4/pkg/box/box.go
@@ -343,7 +343,25 @@ var (
TypeAUXV = f("auxv")
TypeHINT = f("hint")
TypeUDTA = f("udta")
- TypeM7SP = f("m7sp") // Custom box type for M7S StreamPath
+
+ // Common metadata box types
+ TypeTITL = f("©nam") // Title
+ TypeART = f("©ART") // Artist/Author
+ TypeALB = f("©alb") // Album
+ TypeDAY = f("©day") // Date/Year
+ TypeCMT = f("©cmt") // Comment/Description
+ TypeGEN = f("©gen") // Genre
+ TypeCPRT = f("cprt") // Copyright
+ TypeENCO = f("©too") // Encoder/Tool
+ TypeWRT = f("©wrt") // Writer/Composer
+ TypePRD = f("©prd") // Producer
+ TypePRF = f("©prf") // Performer
+ TypeGRP = f("©grp") // Grouping
+ TypeLYR = f("©lyr") // Lyrics
+ TypeKEYW = f("keyw") // Keywords
+ TypeLOCI = f("loci") // Location Information
+ TypeRTNG = f("rtng") // Rating
+ TypeMETA_CUST = f("----") // Custom metadata (iTunes-style)
)
// aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
diff --git a/plugin/mp4/pkg/box/metadata.go b/plugin/mp4/pkg/box/metadata.go
new file mode 100644
index 0000000..c322a1d
--- /dev/null
+++ b/plugin/mp4/pkg/box/metadata.go
@@ -0,0 +1,334 @@
+package box
+
+import (
+ "encoding/binary"
+ "io"
+ "time"
+)
+
+// Metadata holds various metadata information for MP4
+type Metadata struct {
+ Title string // 标题
+ Artist string // 艺术家/作者
+ Album string // 专辑
+ Date string // 日期
+ Comment string // 注释/描述
+ Genre string // 类型
+ Copyright string // 版权信息
+ Encoder string // 编码器
+ Writer string // 作词者
+ Producer string // 制作人
+ Performer string // 表演者
+ Grouping string // 分组
+ Lyrics string // 歌词
+ Keywords string // 关键词
+ Location string // 位置信息
+ Rating uint8 // 评级 (0-5)
+ Custom map[string]string // 自定义键值对
+}
+
+// Text Data Box - for storing text metadata
+type TextDataBox struct {
+ FullBox
+ Text string
+}
+
+// Metadata Data Box - for storing binary metadata with type indicator
+type MetadataDataBox struct {
+ FullBox
+ DataType uint32 // Data type indicator
+ Country uint32 // Country code
+ Language uint32 // Language code
+ Data []byte // Actual data
+}
+
+// Copyright Box
+type CopyrightBox struct {
+ FullBox
+ Language [3]byte
+ Notice string
+}
+
+// Custom Metadata Box (iTunes-style ---- box)
+type CustomMetadataBox struct {
+ BaseBox
+ Mean string // Mean (namespace)
+ Name string // Name (key)
+ Data []byte // Data
+}
+
+// Create functions
+
+func CreateTextDataBox(boxType BoxType, text string) *TextDataBox {
+ return &TextDataBox{
+ FullBox: FullBox{
+ BaseBox: BaseBox{
+ typ: boxType,
+ size: uint32(FullBoxLen + len(text)),
+ },
+ Version: 0,
+ Flags: [3]byte{0, 0, 0},
+ },
+ Text: text,
+ }
+}
+
+func CreateMetadataDataBox(dataType uint32, data []byte) *MetadataDataBox {
+ return &MetadataDataBox{
+ FullBox: FullBox{
+ BaseBox: BaseBox{
+ typ: f("data"),
+ size: uint32(FullBoxLen + 8 + len(data)), // 8 bytes for type+country+language
+ },
+ Version: 0,
+ Flags: [3]byte{0, 0, 0},
+ },
+ DataType: dataType,
+ Country: 0,
+ Language: 0,
+ Data: data,
+ }
+}
+
+func CreateCopyrightBox(language [3]byte, notice string) *CopyrightBox {
+ return &CopyrightBox{
+ FullBox: FullBox{
+ BaseBox: BaseBox{
+ typ: TypeCPRT,
+ size: uint32(FullBoxLen + 3 + 1 + len(notice)), // 3 for language, 1 for null terminator
+ },
+ Version: 0,
+ Flags: [3]byte{0, 0, 0},
+ },
+ Language: language,
+ Notice: notice,
+ }
+}
+
+func CreateCustomMetadataBox(mean, name string, data []byte) *CustomMetadataBox {
+ size := uint32(BasicBoxLen + 4 + len(mean) + 4 + len(name) + len(data))
+ return &CustomMetadataBox{
+ BaseBox: BaseBox{
+ typ: TypeMETA_CUST,
+ size: size,
+ },
+ Mean: mean,
+ Name: name,
+ Data: data,
+ }
+}
+
+// WriteTo methods
+
+func (box *TextDataBox) WriteTo(w io.Writer) (n int64, err error) {
+ nn, err := w.Write([]byte(box.Text))
+ return int64(nn), err
+}
+
+func (box *MetadataDataBox) WriteTo(w io.Writer) (n int64, err error) {
+ var tmp [8]byte
+ binary.BigEndian.PutUint32(tmp[0:4], box.DataType)
+ binary.BigEndian.PutUint32(tmp[4:8], box.Country)
+ // Language field is implicit zero
+
+ nn, err := w.Write(tmp[:8])
+ if err != nil {
+ return int64(nn), err
+ }
+ n = int64(nn)
+
+ nn, err = w.Write(box.Data)
+ return n + int64(nn), err
+}
+
+func (box *CopyrightBox) WriteTo(w io.Writer) (n int64, err error) {
+ // Write language code
+ nn, err := w.Write(box.Language[:])
+ if err != nil {
+ return int64(nn), err
+ }
+ n = int64(nn)
+
+ // Write notice + null terminator
+ nn, err = w.Write([]byte(box.Notice + "\x00"))
+ return n + int64(nn), err
+}
+
+func (box *CustomMetadataBox) WriteTo(w io.Writer) (n int64, err error) {
+ var tmp [4]byte
+
+ // Write mean length + mean
+ binary.BigEndian.PutUint32(tmp[:], uint32(len(box.Mean)))
+ nn, err := w.Write(tmp[:])
+ if err != nil {
+ return int64(nn), err
+ }
+ n = int64(nn)
+
+ nn, err = w.Write([]byte(box.Mean))
+ if err != nil {
+ return n + int64(nn), err
+ }
+ n += int64(nn)
+
+ // Write name length + name
+ binary.BigEndian.PutUint32(tmp[:], uint32(len(box.Name)))
+ nn, err = w.Write(tmp[:])
+ if err != nil {
+ return n + int64(nn), err
+ }
+ n += int64(nn)
+
+ nn, err = w.Write([]byte(box.Name))
+ if err != nil {
+ return n + int64(nn), err
+ }
+ n += int64(nn)
+
+ // Write data
+ nn, err = w.Write(box.Data)
+ return n + int64(nn), err
+}
+
+// Unmarshal methods
+
+func (box *TextDataBox) Unmarshal(buf []byte) (IBox, error) {
+ box.Text = string(buf)
+ return box, nil
+}
+
+func (box *MetadataDataBox) Unmarshal(buf []byte) (IBox, error) {
+ if len(buf) < 8 {
+ return nil, io.ErrShortBuffer
+ }
+
+ box.DataType = binary.BigEndian.Uint32(buf[0:4])
+ box.Country = binary.BigEndian.Uint32(buf[4:8])
+ box.Data = buf[8:]
+ return box, nil
+}
+
+func (box *CopyrightBox) Unmarshal(buf []byte) (IBox, error) {
+ if len(buf) < 4 {
+ return nil, io.ErrShortBuffer
+ }
+
+ copy(box.Language[:], buf[0:3])
+ // Find null terminator
+ for i := 3; i < len(buf); i++ {
+ if buf[i] == 0 {
+ box.Notice = string(buf[3:i])
+ break
+ }
+ }
+ if box.Notice == "" && len(buf) > 3 {
+ box.Notice = string(buf[3:])
+ }
+ return box, nil
+}
+
+func (box *CustomMetadataBox) Unmarshal(buf []byte) (IBox, error) {
+ if len(buf) < 8 {
+ return nil, io.ErrShortBuffer
+ }
+
+ offset := 0
+
+ // Read mean length + mean
+ meanLen := binary.BigEndian.Uint32(buf[offset:])
+ offset += 4
+ if offset+int(meanLen) > len(buf) {
+ return nil, io.ErrShortBuffer
+ }
+ box.Mean = string(buf[offset : offset+int(meanLen)])
+ offset += int(meanLen)
+
+ // Read name length + name
+ if offset+4 > len(buf) {
+ return nil, io.ErrShortBuffer
+ }
+ nameLen := binary.BigEndian.Uint32(buf[offset:])
+ offset += 4
+ if offset+int(nameLen) > len(buf) {
+ return nil, io.ErrShortBuffer
+ }
+ box.Name = string(buf[offset : offset+int(nameLen)])
+ offset += int(nameLen)
+
+ // Read remaining data
+ box.Data = buf[offset:]
+ return box, nil
+}
+
+// Create metadata entries from Metadata struct
+func CreateMetadataEntries(metadata *Metadata) []IBox {
+ var entries []IBox
+
+ // Standard text metadata
+ if metadata.Title != "" {
+ entries = append(entries, CreateTextDataBox(TypeTITL, metadata.Title))
+ }
+ if metadata.Artist != "" {
+ entries = append(entries, CreateTextDataBox(TypeART, metadata.Artist))
+ }
+ if metadata.Album != "" {
+ entries = append(entries, CreateTextDataBox(TypeALB, metadata.Album))
+ }
+ if metadata.Date != "" {
+ entries = append(entries, CreateTextDataBox(TypeDAY, metadata.Date))
+ }
+ if metadata.Comment != "" {
+ entries = append(entries, CreateTextDataBox(TypeCMT, metadata.Comment))
+ }
+ if metadata.Genre != "" {
+ entries = append(entries, CreateTextDataBox(TypeGEN, metadata.Genre))
+ }
+ if metadata.Encoder != "" {
+ entries = append(entries, CreateTextDataBox(TypeENCO, metadata.Encoder))
+ }
+ if metadata.Writer != "" {
+ entries = append(entries, CreateTextDataBox(TypeWRT, metadata.Writer))
+ }
+ if metadata.Producer != "" {
+ entries = append(entries, CreateTextDataBox(TypePRD, metadata.Producer))
+ }
+ if metadata.Performer != "" {
+ entries = append(entries, CreateTextDataBox(TypePRF, metadata.Performer))
+ }
+ if metadata.Grouping != "" {
+ entries = append(entries, CreateTextDataBox(TypeGRP, metadata.Grouping))
+ }
+ if metadata.Lyrics != "" {
+ entries = append(entries, CreateTextDataBox(TypeLYR, metadata.Lyrics))
+ }
+ if metadata.Keywords != "" {
+ entries = append(entries, CreateTextDataBox(TypeKEYW, metadata.Keywords))
+ }
+ if metadata.Location != "" {
+ entries = append(entries, CreateTextDataBox(TypeLOCI, metadata.Location))
+ }
+
+ // Copyright (special format)
+ if metadata.Copyright != "" {
+ entries = append(entries, CreateCopyrightBox([3]byte{'u', 'n', 'd'}, metadata.Copyright))
+ }
+
+ // Custom metadata
+ for key, value := range metadata.Custom {
+ entries = append(entries, CreateCustomMetadataBox("live.m7s.custom", key, []byte(value)))
+ }
+
+ return entries
+}
+
+// Helper function to create current date string
+func GetCurrentDateString() string {
+ return time.Now().Format("2006-01-02")
+}
+
+func init() {
+ RegisterBox[*TextDataBox](TypeTITL, TypeART, TypeALB, TypeDAY, TypeCMT, TypeGEN, TypeENCO, TypeWRT, TypePRD, TypePRF, TypeGRP, TypeLYR, TypeKEYW, TypeLOCI, TypeRTNG)
+ RegisterBox[*MetadataDataBox](f("data"))
+ RegisterBox[*CopyrightBox](TypeCPRT)
+ RegisterBox[*CustomMetadataBox](TypeMETA_CUST)
+}
diff --git a/plugin/mp4/pkg/box/udta.go b/plugin/mp4/pkg/box/udta.go
index d3349dc..9ecc45e 100644
--- a/plugin/mp4/pkg/box/udta.go
+++ b/plugin/mp4/pkg/box/udta.go
@@ -12,12 +12,6 @@ type UserDataBox struct {
Entries []IBox
}
-// Custom metadata box for storing stream path
-type StreamPathBox struct {
- FullBox
- StreamPath string
-}
-
// Create a new User Data Box
func CreateUserDataBox(entries ...IBox) *UserDataBox {
size := uint32(BasicBoxLen)
@@ -33,21 +27,6 @@ func CreateUserDataBox(entries ...IBox) *UserDataBox {
}
}
-// Create a new StreamPath Box
-func CreateStreamPathBox(streamPath string) *StreamPathBox {
- return &StreamPathBox{
- FullBox: FullBox{
- BaseBox: BaseBox{
- typ: TypeM7SP, // Custom box type for M7S StreamPath
- size: uint32(FullBoxLen + len(streamPath)),
- },
- Version: 0,
- Flags: [3]byte{0, 0, 0},
- },
- StreamPath: streamPath,
- }
-}
-
// WriteTo writes the UserDataBox to the given writer
func (box *UserDataBox) WriteTo(w io.Writer) (n int64, err error) {
return WriteTo(w, box.Entries...)
@@ -69,19 +48,6 @@ func (box *UserDataBox) Unmarshal(buf []byte) (IBox, error) {
return box, nil
}
-// WriteTo writes the StreamPathBox to the given writer
-func (box *StreamPathBox) WriteTo(w io.Writer) (n int64, err error) {
- nn, err := w.Write([]byte(box.StreamPath))
- return int64(nn), err
-}
-
-// Unmarshal parses the given buffer into a StreamPathBox
-func (box *StreamPathBox) Unmarshal(buf []byte) (IBox, error) {
- box.StreamPath = string(buf)
- return box, nil
-}
-
func init() {
RegisterBox[*UserDataBox](TypeUDTA)
- RegisterBox[*StreamPathBox](TypeM7SP)
}
diff --git a/plugin/mp4/pkg/muxer.go b/plugin/mp4/pkg/muxer.go
index b984c2a..cce0281 100644
--- a/plugin/mp4/pkg/muxer.go
+++ b/plugin/mp4/pkg/muxer.go
@@ -29,7 +29,8 @@ type (
moov IBox
mdatOffset uint64
mdatSize uint64
- StreamPath string // Added to store the stream path
+ StreamPath string // Added to store the stream path
+ Metadata *Metadata // 添加元数据支持
}
)
@@ -52,6 +53,7 @@ func NewMuxer(flag Flag) *Muxer {
Tracks: make(map[uint32]*Track),
Flag: flag,
fragDuration: 2000,
+ Metadata: &Metadata{Custom: make(map[string]string)},
}
}
@@ -59,6 +61,8 @@ func NewMuxer(flag Flag) *Muxer {
func NewMuxerWithStreamPath(flag Flag, streamPath string) *Muxer {
muxer := NewMuxer(flag)
muxer.StreamPath = streamPath
+ muxer.Metadata.Producer = "M7S Live"
+ muxer.Metadata.Album = streamPath
return muxer
}
@@ -232,10 +236,10 @@ func (m *Muxer) MakeMoov() IBox {
children = append(children, m.makeMvex())
}
- // Add user data box with stream path if available
- if m.StreamPath != "" {
- streamPathBox := CreateStreamPathBox(m.StreamPath)
- udta := CreateUserDataBox(streamPathBox)
+ // Add user data box with metadata if available
+ metadataEntries := CreateMetadataEntries(m.Metadata)
+ if len(metadataEntries) > 0 {
+ udta := CreateUserDataBox(metadataEntries...)
children = append(children, udta)
}
@@ -365,3 +369,82 @@ func (m *Muxer) WriteTrailer(file *os.File) (err error) {
func (m *Muxer) SetFragmentDuration(duration uint32) {
m.fragDuration = duration
}
+
+// SetMetadata sets the metadata for the MP4 file
+func (m *Muxer) SetMetadata(metadata *Metadata) {
+ m.Metadata = metadata
+ if metadata.Custom == nil {
+ metadata.Custom = make(map[string]string)
+ }
+}
+
+// SetTitle sets the title metadata
+func (m *Muxer) SetTitle(title string) {
+ m.Metadata.Title = title
+}
+
+// SetArtist sets the artist/author metadata
+func (m *Muxer) SetArtist(artist string) {
+ m.Metadata.Artist = artist
+}
+
+// SetAlbum sets the album metadata
+func (m *Muxer) SetAlbum(album string) {
+ m.Metadata.Album = album
+}
+
+// SetComment sets the comment/description metadata
+func (m *Muxer) SetComment(comment string) {
+ m.Metadata.Comment = comment
+}
+
+// SetGenre sets the genre metadata
+func (m *Muxer) SetGenre(genre string) {
+ m.Metadata.Genre = genre
+}
+
+// SetCopyright sets the copyright metadata
+func (m *Muxer) SetCopyright(copyright string) {
+ m.Metadata.Copyright = copyright
+}
+
+// SetEncoder sets the encoder metadata
+func (m *Muxer) SetEncoder(encoder string) {
+ m.Metadata.Encoder = encoder
+}
+
+// SetDate sets the date metadata (format: YYYY-MM-DD)
+func (m *Muxer) SetDate(date string) {
+ m.Metadata.Date = date
+}
+
+// SetCurrentDate sets the date metadata to current date
+func (m *Muxer) SetCurrentDate() {
+ m.Metadata.Date = GetCurrentDateString()
+}
+
+// AddCustomMetadata adds custom key-value metadata
+func (m *Muxer) AddCustomMetadata(key, value string) {
+ if m.Metadata.Custom == nil {
+ m.Metadata.Custom = make(map[string]string)
+ }
+ m.Metadata.Custom[key] = value
+}
+
+// SetKeywords sets the keywords metadata
+func (m *Muxer) SetKeywords(keywords string) {
+ m.Metadata.Keywords = keywords
+}
+
+// SetLocation sets the location metadata
+func (m *Muxer) SetLocation(location string) {
+ m.Metadata.Location = location
+}
+
+// SetRating sets the rating metadata (0-5)
+func (m *Muxer) SetRating(rating uint8) {
+ if rating > 5 {
+ rating = 5
+ }
+ m.Metadata.Rating = rating
+}
diff --git a/plugin/mp4/recovery.go b/plugin/mp4/recovery.go
index 188720b..c7b6bcf 100644
--- a/plugin/mp4/recovery.go
+++ b/plugin/mp4/recovery.go
@@ -220,8 +220,8 @@ func extractStreamPathFromMP4(demuxer *mp4.Demuxer) string {
moov := demuxer.GetMoovBox()
if moov != nil && moov.UDTA != nil {
for _, entry := range moov.UDTA.Entries {
- if streamPathBox, ok := entry.(*box.StreamPathBox); ok {
- return streamPathBox.StreamPath
+ if entry.Type() == box.TypeALB {
+ return entry.(*box.TextDataBox).Text
}
}
}
diff --git a/server.go b/server.go
index 8461d21..99c3ec5 100644
--- a/server.go
+++ b/server.go
@@ -582,12 +582,9 @@ func (s *Server) Dispose() {
func (s *Server) GetPublisher(streamPath string) (publisher *Publisher, err error) {
var ok bool
- s.Streams.Call(func() error {
- publisher, ok = s.Streams.Get(streamPath)
- return nil
- })
+ publisher, ok = s.Streams.SafeGet(streamPath)
if !ok {
- err = fmt.Errorf("src stream not found")
+ err = pkg.ErrNotFound
return
}
return