diff --git a/controllers/admin/inboundBroadcasterDetails.go b/controllers/admin/inboundBroadcasterDetails.go new file mode 100644 index 000000000..fa20198f3 --- /dev/null +++ b/controllers/admin/inboundBroadcasterDetails.go @@ -0,0 +1,38 @@ +package admin + +import ( + "encoding/json" + "net/http" + + "github.com/gabek/owncast/controllers" + "github.com/gabek/owncast/core" + "github.com/gabek/owncast/models" + "github.com/gabek/owncast/router/middleware" +) + +// GetInboundBroadasterDetails gets the details of the inbound broadcaster +func GetInboundBroadasterDetails(w http.ResponseWriter, r *http.Request) { + middleware.EnableCors(&w) + + broadcaster := core.GetBroadcaster() + if broadcaster == nil { + controllers.WriteSimpleResponse(w, false, "no broadcaster connected") + return + } + + response := inboundBroadasterDetailsResponse{ + models.BaseAPIResponse{ + true, + "", + }, + broadcaster, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +type inboundBroadasterDetailsResponse struct { + models.BaseAPIResponse + Broadcaster *models.Broadcaster `json:"broadcaster"` +} diff --git a/core/core.go b/core/core.go index 72d220047..b0c304549 100644 --- a/core/core.go +++ b/core/core.go @@ -21,6 +21,7 @@ var ( _storage models.ChunkStorageProvider _cleanupTimer *time.Timer _yp *yp.YP + _broadcaster *models.Broadcaster ) //Start starts up the core processing diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index ddab33e5a..53703b8a6 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -17,6 +17,7 @@ import ( "github.com/gabek/owncast/config" "github.com/gabek/owncast/core" "github.com/gabek/owncast/core/ffmpeg" + "github.com/gabek/owncast/models" "github.com/gabek/owncast/utils" "github.com/nareix/joy5/format/rtmp" ) @@ -62,10 +63,36 @@ func Start() { } } +func setCurrentBroadcasterInfo(t flvio.Tag, remoteAddr string) { + data, err := getInboundDetailsFromMetadata(t.DebugFields()) + if err != nil { + log.Errorln(err) + return + } + + broadcaster := models.Broadcaster{ + RemoteAddr: remoteAddr, + Time: time.Now(), + StreamDetails: models.InboundStreamDetails{ + Width: data.Width, + Height: data.Height, + VideoBitrate: int(data.VideoBitrate), + VideoCodec: getVideoCodec(data.VideoCodec), + VideoFramerate: data.VideoFramerate, + AudioBitrate: int(data.AudioBitrate), + AudioCodec: getAudioCodec(data.AudioCodec), + Encoder: data.Encoder, + }, + } + + core.SetBroadcaster(broadcaster) +} + func HandleConn(c *rtmp.Conn, nc net.Conn) { c.LogTagEvent = func(isRead bool, t flvio.Tag) { if t.Type == flvio.TAG_AMF0 { log.Tracef("%+v\n", t.DebugFields()) + setCurrentBroadcasterInfo(t, nc.RemoteAddr().String()) } } diff --git a/core/rtmp/utils.go b/core/rtmp/utils.go new file mode 100644 index 000000000..00580c01f --- /dev/null +++ b/core/rtmp/utils.go @@ -0,0 +1,64 @@ +package rtmp + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + + "github.com/gabek/owncast/models" + "github.com/nareix/joy5/format/flv/flvio" +) + +func getInboundDetailsFromMetadata(metadata []interface{}) (models.RTMPStreamMetadata, error) { + metadataComponentsString := fmt.Sprintf("%+v", metadata) + re := regexp.MustCompile(`\{(.*?)\}`) + submatchall := re.FindAllString(metadataComponentsString, 1) + + if len(submatchall) == 0 { + return models.RTMPStreamMetadata{}, errors.New("unable to parse inbound metadata") + } + + metadataJSONString := submatchall[0] + var details models.RTMPStreamMetadata + json.Unmarshal([]byte(metadataJSONString), &details) + return details, nil +} + +func getAudioCodec(codec interface{}) string { + var codecID float64 + if assertedCodecID, ok := codec.(float64); ok { + codecID = assertedCodecID + } else { + return codec.(string) + } + + switch codecID { + case flvio.SOUND_MP3: + return "MP3" + case flvio.SOUND_AAC: + return "AAC" + case flvio.SOUND_SPEEX: + return "Speex" + } + + return "Unknown" +} + +func getVideoCodec(codec interface{}) string { + var codecID float64 + if assertedCodecID, ok := codec.(float64); ok { + codecID = assertedCodecID + } else { + return codec.(string) + } + + switch codecID { + case flvio.VIDEO_H264: + return "H.264" + case flvio.VIDEO_H265: + return "H.265" + } + + return "Unknown" +} diff --git a/core/status.go b/core/status.go index 4eef1831b..9a517182e 100644 --- a/core/status.go +++ b/core/status.go @@ -49,6 +49,7 @@ func SetStreamAsConnected() { func SetStreamAsDisconnected() { _stats.StreamConnected = false _stats.LastDisconnectTime = utils.NullTime{time.Now(), true} + _broadcaster = nil if _yp != nil { _yp.Stop() @@ -57,3 +58,12 @@ func SetStreamAsDisconnected() { ffmpeg.ShowStreamOfflineState() startCleanupTimer() } + +// SetBroadcaster will store the current inbound broadcasting details +func SetBroadcaster(broadcaster models.Broadcaster) { + _broadcaster = &broadcaster +} + +func GetBroadcaster() *models.Broadcaster { + return _broadcaster +} diff --git a/models/broadcaster.go b/models/broadcaster.go new file mode 100644 index 000000000..a32db1bdf --- /dev/null +++ b/models/broadcaster.go @@ -0,0 +1,33 @@ +package models + +import "time" + +// Broadcaster represents the details around the inbound broadcasting connection. +type Broadcaster struct { + RemoteAddr string `json:"remoteAddr"` + StreamDetails InboundStreamDetails `json:"streamDetails"` + Time time.Time `json:"time"` +} + +type InboundStreamDetails struct { + Width int `json:"width"` + Height int `json:"height"` + VideoFramerate int `json:"framerate"` + VideoBitrate int `json:"videoBitrate"` + VideoCodec string `json:"videoCodec"` + AudioBitrate int `json:"audioBitrate"` + AudioCodec string `json:"audioCodec"` + Encoder string `json:"encoder"` +} + +// RTMPStreamMetadata is the raw metadata that comes in with a RTMP connection +type RTMPStreamMetadata struct { + Width int `json:"width"` + Height int `json:"height"` + VideoBitrate float32 `json:"videodatarate"` + VideoCodec interface{} `json:"videocodecid"` + VideoFramerate int `json:"framerate"` + AudioBitrate float32 `json:"audiodatarate"` + AudioCodec interface{} `json:"audiocodecid"` + Encoder string `json:"encoder"` +} diff --git a/router/router.go b/router/router.go index cf1ec759d..6f42f5928 100644 --- a/router/router.go +++ b/router/router.go @@ -51,6 +51,9 @@ func Start() error { // Authenticated admin requests + // Current inbound broadcaster + http.HandleFunc("/api/admin/broadcaster", middleware.RequireAdminAuth(admin.GetInboundBroadasterDetails)) + // Disconnect inbound stream http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection))