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