usmusic-server / codec.go
Marvelousshivam's picture
Upload 17 files
8930690 verified
Raw
History Blame Contribute Delete
19.7 kB
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
pb "github.com/EchoMusicApp/Echo-Music-Server/proto"
"google.golang.org/protobuf/proto"
)
// MessageCodec handles encoding/decoding of messages using Protocol Buffers
type MessageCodec struct {
compressionEnabled bool
}
// NewMessageCodec creates a new codec with compression settings
func NewMessageCodec(compression bool) *MessageCodec {
return &MessageCodec{
compressionEnabled: compression,
}
}
// Encode encodes a message using Protocol Buffers
func (c *MessageCodec) Encode(msgType string, payload interface{}) ([]byte, error) {
return c.encodeProtobuf(msgType, payload)
}
// Decode decodes a protobuf message
func (c *MessageCodec) Decode(data []byte) (string, []byte, error) {
return c.decodeProtobuf(data)
}
// encodeProtobuf encodes a message using Protocol Buffers
func (c *MessageCodec) encodeProtobuf(msgType string, payload interface{}) ([]byte, error) {
var payloadBytes []byte
if payload != nil {
// Convert payload to protobuf message
protoMsg, err := toProtoMessage(payload)
if err != nil {
return nil, fmt.Errorf("convert to proto: %w", err)
}
payloadBytes, err = proto.Marshal(protoMsg)
if err != nil {
return nil, fmt.Errorf("marshal proto payload: %w", err)
}
}
// Log uncompressed payload size
uncompressedSize := len(payloadBytes)
// Compress payload if enabled
compressed := false
if c.compressionEnabled && len(payloadBytes) > 100 {
compressedBytes, err := compressData(payloadBytes)
if err == nil && len(compressedBytes) < len(payloadBytes) {
payloadBytes = compressedBytes
compressed = true
}
}
envelope := &pb.Envelope{
Type: msgType,
Payload: payloadBytes,
Compressed: compressed,
}
envelopeBytes, err := proto.Marshal(envelope)
if err != nil {
return nil, fmt.Errorf("marshal envelope: %w", err)
}
// Log final size information
_ = uncompressedSize // Use the variable to avoid unused warning
return envelopeBytes, nil
}
// decodeProtobuf decodes a protobuf message
func (c *MessageCodec) decodeProtobuf(data []byte) (string, []byte, error) {
if len(data) == 0 {
return "", nil, fmt.Errorf("empty data received")
}
envelope := &pb.Envelope{}
if err := proto.Unmarshal(data, envelope); err != nil {
return "", nil, fmt.Errorf("unmarshal envelope (received %d bytes): %w", len(data), err)
}
payloadBytes := envelope.Payload
if envelope.Compressed {
decompressed, err := decompressData(payloadBytes)
if err != nil {
return "", nil, fmt.Errorf("decompress payload: %w", err)
}
payloadBytes = decompressed
}
return envelope.Type, payloadBytes, nil
}
// compressData compresses data using gzip
func compressData(data []byte) ([]byte, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
if _, err := writer.Write(data); err != nil {
writer.Close()
return nil, err
}
if err := writer.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// decompressData decompresses gzip data
func decompressData(data []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
}
// toProtoMessage converts Go structs to protobuf messages
func toProtoMessage(payload interface{}) (proto.Message, error) {
switch p := payload.(type) {
// Pointer types (from pending actions)
case *CreateRoomPayload:
return &pb.CreateRoomPayload{Username: p.Username}, nil
case *JoinRoomPayload:
return &pb.JoinRoomPayload{RoomCode: p.RoomCode, Username: p.Username}, nil
case *ApproveJoinPayload:
return &pb.ApproveJoinPayload{UserId: p.UserID}, nil
case *RejectJoinPayload:
return &pb.RejectJoinPayload{UserId: p.UserID, Reason: p.Reason}, nil
case *PlaybackActionPayload:
pbPayload := &pb.PlaybackActionPayload{
Action: p.Action,
TrackId: p.TrackID,
Position: p.Position,
InsertNext: p.InsertNext,
QueueTitle: p.QueueTitle,
Volume: float32(p.Volume),
ServerTime: p.ServerTime,
}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
if p.Queue != nil {
pbPayload.Queue = make([]*pb.TrackInfo, len(p.Queue))
for i, track := range p.Queue {
pbPayload.Queue[i] = trackInfoToProto(&track)
}
}
return pbPayload, nil
case *BufferReadyPayload:
return &pb.BufferReadyPayload{TrackId: p.TrackID}, nil
case *KickUserPayload:
return &pb.KickUserPayload{UserId: p.UserID, Reason: p.Reason}, nil
case *TransferHostPayload:
return &pb.TransferHostPayload{NewHostId: p.NewHostID}, nil
case *SuggestTrackPayload:
pbPayload := &pb.SuggestTrackPayload{}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
return pbPayload, nil
case *ApproveSuggestionPayload:
return &pb.ApproveSuggestionPayload{SuggestionId: p.SuggestionID}, nil
case *RejectSuggestionPayload:
return &pb.RejectSuggestionPayload{SuggestionId: p.SuggestionID, Reason: p.Reason}, nil
case *ReconnectPayload:
return &pb.ReconnectPayload{SessionToken: p.SessionToken}, nil
case *RoomCreatedPayload:
return &pb.RoomCreatedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
SessionToken: p.SessionToken,
}, nil
case *JoinRequestPayload:
return &pb.JoinRequestPayload{UserId: p.UserID, Username: p.Username}, nil
case *JoinApprovedPayload:
pbPayload := &pb.JoinApprovedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
SessionToken: p.SessionToken,
}
if p.State != nil {
pbPayload.State = roomStateToProto(p.State)
}
return pbPayload, nil
case *JoinRejectedPayload:
return &pb.JoinRejectedPayload{Reason: p.Reason}, nil
case *UserJoinedPayload:
return &pb.UserJoinedPayload{UserId: p.UserID, Username: p.Username}, nil
case *UserLeftPayload:
return &pb.UserLeftPayload{UserId: p.UserID, Username: p.Username}, nil
case *BufferWaitPayload:
return &pb.BufferWaitPayload{TrackId: p.TrackID, WaitingFor: p.WaitingFor}, nil
case *BufferCompletePayload:
return &pb.BufferCompletePayload{TrackId: p.TrackID}, nil
case *ErrorPayload:
return &pb.ErrorPayload{Code: p.Code, Message: p.Message}, nil
case *HostChangedPayload:
return &pb.HostChangedPayload{NewHostId: p.NewHostID, NewHostName: p.NewHostName}, nil
case *KickedPayload:
return &pb.KickedPayload{Reason: p.Reason}, nil
case *SyncStatePayload:
pbPayload := &pb.SyncStatePayload{
IsPlaying: p.IsPlaying,
Position: p.Position,
LastUpdate: p.LastUpdate,
Volume: float32(p.Volume),
}
if p.CurrentTrack != nil {
pbPayload.CurrentTrack = trackInfoToProto(p.CurrentTrack)
}
if p.Queue != nil {
pbPayload.Queue = make([]*pb.TrackInfo, len(p.Queue))
for i, track := range p.Queue {
pbPayload.Queue[i] = trackInfoToProto(&track)
}
}
return pbPayload, nil
case *ReconnectedPayload:
pbPayload := &pb.ReconnectedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
IsHost: p.IsHost,
}
if p.State != nil {
pbPayload.State = roomStateToProto(p.State)
}
return pbPayload, nil
case *UserReconnectedPayload:
return &pb.UserReconnectedPayload{UserId: p.UserID, Username: p.Username}, nil
case *UserDisconnectedPayload:
return &pb.UserDisconnectedPayload{UserId: p.UserID, Username: p.Username}, nil
case *SuggestionReceivedPayload:
pbPayload := &pb.SuggestionReceivedPayload{
SuggestionId: p.SuggestionID,
FromUserId: p.FromUserID,
FromUsername: p.FromUsername,
}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
return pbPayload, nil
case *SuggestionApprovedPayload:
pbPayload := &pb.SuggestionApprovedPayload{SuggestionId: p.SuggestionID}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
return pbPayload, nil
case *SuggestionRejectedPayload:
return &pb.SuggestionRejectedPayload{SuggestionId: p.SuggestionID, Reason: p.Reason}, nil
// Value types (from sendMessage)
case RoomCreatedPayload:
return &pb.RoomCreatedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
SessionToken: p.SessionToken,
}, nil
case JoinRequestPayload:
return &pb.JoinRequestPayload{UserId: p.UserID, Username: p.Username}, nil
case JoinApprovedPayload:
pbPayload := &pb.JoinApprovedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
SessionToken: p.SessionToken,
}
if p.State != nil {
pbPayload.State = roomStateToProto(p.State)
}
return pbPayload, nil
case JoinRejectedPayload:
return &pb.JoinRejectedPayload{Reason: p.Reason}, nil
case UserJoinedPayload:
return &pb.UserJoinedPayload{UserId: p.UserID, Username: p.Username}, nil
case UserLeftPayload:
return &pb.UserLeftPayload{UserId: p.UserID, Username: p.Username}, nil
case BufferWaitPayload:
return &pb.BufferWaitPayload{TrackId: p.TrackID, WaitingFor: p.WaitingFor}, nil
case BufferCompletePayload:
return &pb.BufferCompletePayload{TrackId: p.TrackID}, nil
case ErrorPayload:
return &pb.ErrorPayload{Code: p.Code, Message: p.Message}, nil
case HostChangedPayload:
return &pb.HostChangedPayload{NewHostId: p.NewHostID, NewHostName: p.NewHostName}, nil
case KickedPayload:
return &pb.KickedPayload{Reason: p.Reason}, nil
case SyncStatePayload:
pbPayload := &pb.SyncStatePayload{
IsPlaying: p.IsPlaying,
Position: p.Position,
LastUpdate: p.LastUpdate,
Volume: float32(p.Volume),
}
if p.CurrentTrack != nil {
pbPayload.CurrentTrack = trackInfoToProto(p.CurrentTrack)
}
if p.Queue != nil {
pbPayload.Queue = make([]*pb.TrackInfo, len(p.Queue))
for i, track := range p.Queue {
pbPayload.Queue[i] = trackInfoToProto(&track)
}
}
return pbPayload, nil
case ReconnectedPayload:
pbPayload := &pb.ReconnectedPayload{
RoomCode: p.RoomCode,
UserId: p.UserID,
IsHost: p.IsHost,
}
if p.State != nil {
pbPayload.State = roomStateToProto(p.State)
}
return pbPayload, nil
case UserReconnectedPayload:
return &pb.UserReconnectedPayload{UserId: p.UserID, Username: p.Username}, nil
case UserDisconnectedPayload:
return &pb.UserDisconnectedPayload{UserId: p.UserID, Username: p.Username}, nil
case SuggestionReceivedPayload:
pbPayload := &pb.SuggestionReceivedPayload{
SuggestionId: p.SuggestionID,
FromUserId: p.FromUserID,
FromUsername: p.FromUsername,
}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
return pbPayload, nil
case SuggestionApprovedPayload:
pbPayload := &pb.SuggestionApprovedPayload{SuggestionId: p.SuggestionID}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
return pbPayload, nil
case SuggestionRejectedPayload:
return &pb.SuggestionRejectedPayload{SuggestionId: p.SuggestionID, Reason: p.Reason}, nil
case PlaybackActionPayload:
pbPayload := &pb.PlaybackActionPayload{
Action: p.Action,
TrackId: p.TrackID,
Position: p.Position,
InsertNext: p.InsertNext,
QueueTitle: p.QueueTitle,
Volume: float32(p.Volume),
ServerTime: p.ServerTime,
}
if p.TrackInfo != nil {
pbPayload.TrackInfo = trackInfoToProto(p.TrackInfo)
}
if p.Queue != nil {
pbPayload.Queue = make([]*pb.TrackInfo, len(p.Queue))
for i, track := range p.Queue {
pbPayload.Queue[i] = trackInfoToProto(&track)
}
}
return pbPayload, nil
default:
return nil, fmt.Errorf("unsupported payload type: %T", payload)
}
}
// fromProtoMessage converts protobuf messages to Go structs
func fromProtoMessage(msgType string, data []byte) (interface{}, error) {
switch msgType {
case MsgTypeCreateRoom:
var pbb pb.CreateRoomPayload
if err := proto.Unmarshal(data, &pbb); err != nil {
return nil, err
}
return &CreateRoomPayload{Username: pbb.Username}, nil
case MsgTypeJoinRoom:
var pbb pb.JoinRoomPayload
if err := proto.Unmarshal(data, &pbb); err != nil {
return nil, err
}
return &JoinRoomPayload{RoomCode: pbb.RoomCode, Username: pbb.Username}, nil
case MsgTypeApproveJoin:
var pbb pb.ApproveJoinPayload
if err := proto.Unmarshal(data, &pbb); err != nil {
return nil, err
}
return &ApproveJoinPayload{UserID: pbb.UserId}, nil
case MsgTypeRejectJoin:
var pbb pb.RejectJoinPayload
if err := proto.Unmarshal(data, &pbb); err != nil {
return nil, err
}
return &RejectJoinPayload{UserID: pbb.UserId, Reason: pbb.Reason}, nil
case MsgTypePlaybackAction:
var pbMsg pb.PlaybackActionPayload
if err := proto.Unmarshal(data, &pbMsg); err != nil {
return nil, err
}
payload := &PlaybackActionPayload{
Action: pbMsg.Action,
TrackID: pbMsg.TrackId,
Position: pbMsg.Position,
InsertNext: pbMsg.InsertNext,
QueueTitle: pbMsg.QueueTitle,
Volume: float64(pbMsg.Volume),
ServerTime: pbMsg.ServerTime,
}
if pbMsg.TrackInfo != nil {
payload.TrackInfo = protoToTrackInfo(pbMsg.TrackInfo)
}
if pbMsg.Queue != nil {
payload.Queue = make([]TrackInfo, len(pbMsg.Queue))
for i, track := range pbMsg.Queue {
payload.Queue[i] = *protoToTrackInfo(track)
}
}
return payload, nil
case MsgTypeBufferReady:
var pb pb.BufferReadyPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &BufferReadyPayload{TrackID: pb.TrackId}, nil
case MsgTypeKickUser:
var pb pb.KickUserPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &KickUserPayload{UserID: pb.UserId, Reason: pb.Reason}, nil
case MsgTypeTransferHost:
var pb pb.TransferHostPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &TransferHostPayload{NewHostID: pb.NewHostId}, nil
case MsgTypeSuggestTrack:
var pbMsg pb.SuggestTrackPayload
if err := proto.Unmarshal(data, &pbMsg); err != nil {
return nil, err
}
payload := &SuggestTrackPayload{}
if pbMsg.TrackInfo != nil {
payload.TrackInfo = protoToTrackInfo(pbMsg.TrackInfo)
}
return payload, nil
case MsgTypeApproveSuggestion:
var pb pb.ApproveSuggestionPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &ApproveSuggestionPayload{SuggestionID: pb.SuggestionId}, nil
case MsgTypeRejectSuggestion:
var pb pb.RejectSuggestionPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &RejectSuggestionPayload{SuggestionID: pb.SuggestionId, Reason: pb.Reason}, nil
case MsgTypeReconnect:
var pb pb.ReconnectPayload
if err := proto.Unmarshal(data, &pb); err != nil {
return nil, err
}
return &ReconnectPayload{SessionToken: pb.SessionToken}, nil
default:
return nil, fmt.Errorf("unsupported message type: %s", msgType)
}
}
// Helper functions for converting between Go and Proto types
func trackInfoToProto(t *TrackInfo) *pb.TrackInfo {
return &pb.TrackInfo{
Id: t.ID,
Title: t.Title,
Artist: t.Artist,
Album: t.Album,
Duration: t.Duration,
Thumbnail: t.Thumbnail,
SuggestedBy: t.SuggestedBy,
}
}
func protoToTrackInfo(p *pb.TrackInfo) *TrackInfo {
return &TrackInfo{
ID: p.Id,
Title: p.Title,
Artist: p.Artist,
Album: p.Album,
Duration: p.Duration,
Thumbnail: p.Thumbnail,
SuggestedBy: p.SuggestedBy,
}
}
func userInfoToProto(u *UserInfo) *pb.UserInfo {
return &pb.UserInfo{
UserId: u.UserID,
Username: u.Username,
IsHost: u.IsHost,
IsConnected: u.IsConnected,
}
}
func roomStateToProto(r *RoomState) *pb.RoomState {
pbState := &pb.RoomState{
RoomCode: r.RoomCode,
HostId: r.HostID,
IsPlaying: r.IsPlaying,
Position: r.Position,
LastUpdate: r.LastUpdate,
Volume: float32(r.Volume),
}
if r.CurrentTrack != nil {
pbState.CurrentTrack = trackInfoToProto(r.CurrentTrack)
}
if r.Users != nil {
pbState.Users = make([]*pb.UserInfo, len(r.Users))
for i, user := range r.Users {
pbState.Users[i] = userInfoToProto(&user)
}
}
// Always initialize queue as non-nil (proto3 repeated fields should not be nil)
if r.Queue != nil && len(r.Queue) > 0 {
pbState.Queue = make([]*pb.TrackInfo, len(r.Queue))
for i, track := range r.Queue {
pbState.Queue[i] = trackInfoToProto(&track)
}
} else {
// Explicitly set to empty slice, not nil
pbState.Queue = []*pb.TrackInfo{}
}
return pbState
}
// decodePayload decodes a protobuf payload into the target interface
func decodePayload(payloadBytes []byte, msgType string, target interface{}) error {
// Use fromProtoMessage to convert protobuf to Go struct
payload, err := fromProtoMessage(msgType, payloadBytes)
if err != nil {
return err
}
// Copy the decoded payload to target using safe type assertion
targetVal := target
switch t := targetVal.(type) {
case *CreateRoomPayload:
p, ok := payload.(*CreateRoomPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected CreateRoomPayload, got %T", payload)
}
*t = *p
case *JoinRoomPayload:
p, ok := payload.(*JoinRoomPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected JoinRoomPayload, got %T", payload)
}
*t = *p
case *ApproveJoinPayload:
p, ok := payload.(*ApproveJoinPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected ApproveJoinPayload, got %T", payload)
}
*t = *p
case *RejectJoinPayload:
p, ok := payload.(*RejectJoinPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected RejectJoinPayload, got %T", payload)
}
*t = *p
case *PlaybackActionPayload:
p, ok := payload.(*PlaybackActionPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected PlaybackActionPayload, got %T", payload)
}
*t = *p
case *BufferReadyPayload:
p, ok := payload.(*BufferReadyPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected BufferReadyPayload, got %T", payload)
}
*t = *p
case *KickUserPayload:
p, ok := payload.(*KickUserPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected KickUserPayload, got %T", payload)
}
*t = *p
case *SuggestTrackPayload:
p, ok := payload.(*SuggestTrackPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected SuggestTrackPayload, got %T", payload)
}
*t = *p
case *ApproveSuggestionPayload:
p, ok := payload.(*ApproveSuggestionPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected ApproveSuggestionPayload, got %T", payload)
}
*t = *p
case *RejectSuggestionPayload:
p, ok := payload.(*RejectSuggestionPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected RejectSuggestionPayload, got %T", payload)
}
*t = *p
case *ReconnectPayload:
p, ok := payload.(*ReconnectPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected ReconnectPayload, got %T", payload)
}
*t = *p
case *TransferHostPayload:
p, ok := payload.(*TransferHostPayload)
if !ok {
return fmt.Errorf("payload type mismatch: expected TransferHostPayload, got %T", payload)
}
*t = *p
default:
return fmt.Errorf("unsupported target type: %T", target)
}
return nil
}