package hds import ( "errors" "sync" ) // HDS message types const ( ProtoDataSend = "dataSend" ProtoControl = "control" TopicOpen = "open" TopicData = "data" TopicClose = "close" TopicAck = "ack" TopicHello = "hello" StatusSuccess = 0 ) // Message represents an HDS application-level message type Message struct { Protocol string Topic string ID int64 IsEvent bool Status int64 Body map[string]any } // Session wraps an HDS encrypted connection with application-level protocol handling. // HDS messages format: [1 byte header_length][opack header dict][opack message dict] type Session struct { conn *Conn mu sync.Mutex id int64 OnDataSendOpen func(streamID int) error OnDataSendClose func(streamID int) error } func NewSession(conn *Conn) *Session { return &Session{conn: conn} } // ReadMessage reads and decodes an HDS application message func (s *Session) ReadMessage() (*Message, error) { buf := make([]byte, 64*1024) n, err := s.conn.Read(buf) if err != nil { return nil, err } data := buf[:n] if len(data) < 2 { return nil, errors.New("hds: message too short") } headerLen := int(data[0]) if len(data) < 1+headerLen { return nil, errors.New("hds: header truncated") } headerData := data[1 : 1+headerLen] bodyData := data[1+headerLen:] headerVal, err := OpackUnmarshal(headerData) if err != nil { return nil, err } header, ok := headerVal.(map[string]any) if !ok { return nil, errors.New("hds: header is not dict") } msg := &Message{ Protocol: opackString(header["protocol"]), } if topic, ok := header["event"]; ok { msg.IsEvent = true msg.Topic = opackString(topic) } else if topic, ok := header["request"]; ok { msg.Topic = opackString(topic) msg.ID = opackInt(header["id"]) } else if topic, ok := header["response"]; ok { msg.Topic = opackString(topic) msg.ID = opackInt(header["id"]) msg.Status = opackInt(header["status"]) } if len(bodyData) > 0 { bodyVal, err := OpackUnmarshal(bodyData) if err != nil { return nil, err } if m, ok := bodyVal.(map[string]any); ok { msg.Body = m } } return msg, nil } // WriteMessage sends an HDS application message func (s *Session) WriteMessage(header, body map[string]any) error { headerBytes := OpackMarshal(header) bodyBytes := OpackMarshal(body) msg := make([]byte, 0, 1+len(headerBytes)+len(bodyBytes)) msg = append(msg, byte(len(headerBytes))) msg = append(msg, headerBytes...) msg = append(msg, bodyBytes...) s.mu.Lock() defer s.mu.Unlock() _, err := s.conn.Write(msg) return err } // WriteResponse sends a response to a request func (s *Session) WriteResponse(protocol, topic string, id int64, status int, body map[string]any) error { header := map[string]any{ "protocol": protocol, "response": topic, "id": id, "status": status, } if body == nil { body = map[string]any{} } return s.WriteMessage(header, body) } // WriteEvent sends an unsolicited event func (s *Session) WriteEvent(protocol, topic string, body map[string]any) error { header := map[string]any{ "protocol": protocol, "event": topic, } if body == nil { body = map[string]any{} } return s.WriteMessage(header, body) } // WriteRequest sends a request func (s *Session) WriteRequest(protocol, topic string, body map[string]any) (int64, error) { s.mu.Lock() s.id++ id := s.id s.mu.Unlock() header := map[string]any{ "protocol": protocol, "request": topic, "id": id, } if body == nil { body = map[string]any{} } return id, s.WriteMessage(header, body) } // maxChunkSize is the maximum data chunk size for HDS media transfer (256 KiB) const maxChunkSize = 0x40000 // SendMediaInit sends the fMP4 initialization segment (ftyp+moov) func (s *Session) SendMediaInit(streamID int, initData []byte) error { return s.sendMediaData(streamID, "mediaInitialization", initData, 1) } // SendMediaFragment sends an fMP4 fragment (moof+mdat), splitting into chunks if needed func (s *Session) SendMediaFragment(streamID int, fragment []byte, sequence int) error { return s.sendMediaData(streamID, "mediaFragment", fragment, sequence) } // sendMediaData sends media data with proper HAP-NodeJS compatible packet structure. // Large data is split into chunks of maxChunkSize bytes. func (s *Session) sendMediaData(streamID int, dataType string, data []byte, sequence int) error { totalSize := len(data) chunkSeq := 1 for offset := 0; offset < totalSize; offset += maxChunkSize { end := offset + maxChunkSize if end > totalSize { end = totalSize } chunk := data[offset:end] isLast := end >= totalSize metadata := map[string]any{ "dataType": dataType, "dataSequenceNumber": sequence, "dataChunkSequenceNumber": chunkSeq, "isLastDataChunk": isLast, } if chunkSeq == 1 { metadata["dataTotalSize"] = totalSize } body := map[string]any{ "streamId": streamID, "packets": []any{ map[string]any{ "data": chunk, "metadata": metadata, }, }, } if err := s.WriteEvent(ProtoDataSend, TopicData, body); err != nil { return err } chunkSeq++ } return nil } // Run processes incoming HDS messages in a loop func (s *Session) Run() error { // Handle control/hello handshake msg, err := s.ReadMessage() if err != nil { return err } if msg.Protocol == ProtoControl && msg.Topic == TopicHello { if err := s.WriteResponse(ProtoControl, TopicHello, msg.ID, StatusSuccess, nil); err != nil { return err } } // Main message loop for { msg, err := s.ReadMessage() if err != nil { return err } if msg.Protocol != ProtoDataSend { continue } switch msg.Topic { case TopicOpen: streamID := int(opackInt(msg.Body["streamId"])) // Acknowledge the open request if err := s.WriteResponse(ProtoDataSend, TopicOpen, msg.ID, StatusSuccess, nil); err != nil { return err } if s.OnDataSendOpen != nil { if err := s.OnDataSendOpen(streamID); err != nil { return err } } case TopicClose: streamID := int(opackInt(msg.Body["streamId"])) // Acknowledge the close request if err := s.WriteResponse(ProtoDataSend, TopicClose, msg.ID, StatusSuccess, nil); err != nil { return err } if s.OnDataSendClose != nil { if err := s.OnDataSendClose(streamID); err != nil { return err } } case TopicAck: // Acknowledgement from controller, nothing to do } } } func (s *Session) Close() error { return s.conn.Close() } func opackString(v any) string { if s, ok := v.(string); ok { return s } return "" } func opackInt(v any) int64 { switch v := v.(type) { case int64: return v case int: return int64(v) case float64: return int64(v) } return 0 }