From b1bd57feaad619106a76d75cbb478cf63e199425 Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sun, 15 Jan 2023 00:32:51 +0800 Subject: [PATCH 01/19] support api-server golang --- trunk/research/api-server/src/chat.go | 123 ++++++++++++++ trunk/research/api-server/src/client.go | 117 +++++++++++++ trunk/research/api-server/src/dvr.go | 80 +++++++++ trunk/research/api-server/src/forward.go | 97 +++++++++++ trunk/research/api-server/src/hls.go | 105 ++++++++++++ trunk/research/api-server/src/main.go | 162 ++++++++++++++++++ trunk/research/api-server/src/proxy.go | 53 ++++++ trunk/research/api-server/src/server.go | 138 +++++++++++++++ trunk/research/api-server/src/session.go | 117 +++++++++++++ trunk/research/api-server/src/snapshot.go | 195 ++++++++++++++++++++++ trunk/research/api-server/src/stream.go | 88 ++++++++++ 11 files changed, 1275 insertions(+) create mode 100644 trunk/research/api-server/src/chat.go create mode 100644 trunk/research/api-server/src/client.go create mode 100644 trunk/research/api-server/src/dvr.go create mode 100644 trunk/research/api-server/src/forward.go create mode 100644 trunk/research/api-server/src/hls.go create mode 100644 trunk/research/api-server/src/main.go create mode 100644 trunk/research/api-server/src/proxy.go create mode 100644 trunk/research/api-server/src/server.go create mode 100644 trunk/research/api-server/src/session.go create mode 100644 trunk/research/api-server/src/snapshot.go create mode 100644 trunk/research/api-server/src/stream.go diff --git a/trunk/research/api-server/src/chat.go b/trunk/research/api-server/src/chat.go new file mode 100644 index 0000000000..597ee8a528 --- /dev/null +++ b/trunk/research/api-server/src/chat.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "sync" + "time" +) + +/* +# object fields: +# id: an int value indicates the id of user. +# username: a str indicates the user name. +# url: a str indicates the url of user stream. +# agent: a str indicates the agent of user. +# join_date: a number indicates the join timestamp in seconds. +# join_date_str: a str specifies the formated friendly time. +# heartbeat: a number indicates the heartbeat timestamp in seconds. +# vcodec: a dict indicates the video codec info. +# acodec: a dict indicates the audio codec info. + +# dead time in seconds, if exceed, remove the chat. +*/ + +type Chat struct { + Id int `json:"id"` + Username string `json:"username"` + Url string `json:"url"` + JoinDate int64 `json:"join_date"` + JoinDateStr string `json:"join_date_str"` + Heartbeat int64 `json:"heartbeat"` +} + +type ChatManager struct { + globalId int + chats *sync.Map + deadTime int +} + +func NewChatManager() *ChatManager { + v := &ChatManager{ + globalId: 100, + // key is globalId, value is chat + chats: new(sync.Map), + deadTime: 15, + } + return v +} + +func (v *ChatManager) List() (chats []*Chat) { + chats = []*Chat{} + v.chats.Range(func(key, value any) bool { + _, chat := key.(int), value.(*Chat) + if (time.Now().Unix() - chat.Heartbeat) > int64(v.deadTime) { + v.chats.Delete(key) + return true + } + chats = append(chats, chat) + return true + }) + return +} + +func (v *ChatManager) Update(id int) (se *SrsError) { + value, ok := v.chats.Load(id) + if !ok { + return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} + } + c := value.(*Chat) + c.Heartbeat = time.Now().Unix() + log.Println(fmt.Sprintf("heartbeat chat success, id=%v", id)) + return nil +} + +func (v *ChatManager) Delete(id int) (se *SrsError) { + if _, ok := v.chats.Load(id); !ok { + return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} + } + v.chats.Delete(id) + log.Println(fmt.Sprintf("delete chat success, id=%v", id)) + return +} + +func (v *ChatManager) Add(c *Chat) { + c.Id = v.globalId + now := time.Now() + c.JoinDate, c.Heartbeat = now.Unix(), now.Unix() + c.JoinDateStr = now.Format("2006-01-02 15:04:05") + v.globalId += 1 + v.chats.Store(c.Id, c) +} + +// the chat streams, public chat room. +func ChatServe(w http.ResponseWriter, r *http.Request) { + log.Println(fmt.Sprintf("got a chat req, uPath=%v", r.URL.Path)) + if r.Method == "GET" { + chats := cm.List() + Response(&SrsError{Code: 0, Data: chats}).ServeHTTP(w, r) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + c := &Chat{} + if err := json.Unmarshal(body, c); err != nil { + Response(&SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse body to chat json failed, err is %v", err)}) + return + } + cm.Add(c) + log.Println(fmt.Sprintf("create chat success, id=%v", c.Id)) + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + } else if r.Method == "PUT" { + // TODO: parse id? + Response(cm.Update(0)).ServeHTTP(w, r) + } else if r.Method == "DELETE" { + // TODO: parse id? + Response(cm.Delete(0)).ServeHTTP(w, r) + } +} diff --git a/trunk/research/api-server/src/client.go b/trunk/research/api-server/src/client.go new file mode 100644 index 0000000000..0a619262f1 --- /dev/null +++ b/trunk/research/api-server/src/client.go @@ -0,0 +1,117 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* +handle the clients requests: connect/disconnect vhost/app. + for SRS hook: on_connect/on_close + on_connect: + when client connect to vhost/app, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_connect", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", + "pageUrl": "http://www.test.com/live.html" + } + on_close: + when client close/disconnect to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_close", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "send_bytes": 10240, + "recv_bytes": 10240 + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ +type ClientMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` +} + +type ClientOnConnectMsg struct { + ClientMsg + TcUrl string `json:"tcUrl"` + PageUrl string `json:"pageUrl"` +} + +func (v *ClientOnConnectMsg) String() string { + return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.PageUrl) +} + +type ClientOnCloseMsg struct { + ClientMsg + SendBytes int64 `json:"send_bytes"` + RecvBytes int64 `json:"recv_bytes"` +} + +func (v *ClientOnCloseMsg) String() string { + return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, send_bytes=%v, recv_bytes=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.SendBytes, v.RecvBytes) +} + +type Client struct {} + +func (v *Client) Parse(body []byte) (se *SrsError) { + data := &struct { + Action string `json:"action"` + }{} + if err := json.Unmarshal(body, data); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client action failed, err is %v", err.Error())} + } + + if data.Action == client_action_on_connect { + msg := &ClientOnConnectMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_connect, err.Error())} + } + log.Println(msg) + } else if data.Action == client_action_on_close { + msg := &ClientOnCloseMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_close, err.Error())} + } + log.Println(msg) + } + return nil +} + +// handle the clients requests: connect/disconnect vhost/app. +func ClientServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) + c := &Client{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} diff --git a/trunk/research/api-server/src/dvr.go b/trunk/research/api-server/src/dvr.go new file mode 100644 index 0000000000..e0b0a15488 --- /dev/null +++ b/trunk/research/api-server/src/dvr.go @@ -0,0 +1,80 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* + for SRS hook: on_dvr + on_dvr: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_dvr", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776.flv" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type DvrMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` + Cwd string `json:"cwd"` + File string `json:"file"` +} + +func (v *DvrMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, cwd=%v, file=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Cwd, v.File) +} + +type Dvr struct {} + +func (v *Dvr) Parse(body []byte) (se *SrsError) { + msg := &DvrMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse dvr msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the dvrs requests: dvr stream. +func DvrServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) + c := &Dvr{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} diff --git a/trunk/research/api-server/src/forward.go b/trunk/research/api-server/src/forward.go new file mode 100644 index 0000000000..8d230a718d --- /dev/null +++ b/trunk/research/api-server/src/forward.go @@ -0,0 +1,97 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* +handle the forward requests: dynamic forward url. +for SRS hook: on_forward +on_forward: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_forward", + "server_id": "server_test", + "client_id": 1985, + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type ForwardMsg struct { + Action string `json:"action"` + ServerId string `json:"server_id"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + TcUrl string `json:"tc_url"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +func (v *ForwardMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) +} + +type Forward struct {} + +/* +backend service config description: + support multiple rtmp urls(custom addresses or third-party cdn service), + url's host is slave service. +For example: + ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] +*/ +func (v *Forward) Parse(body []byte) (se *SrsError) { + msg := &DvrMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse forward msg failed, err is %v", err.Error())} + } + if msg.Action == "on_forward" { + log.Println(msg) + res := &struct { + Urls []string `json:"urls"` + }{ + Urls: []string{"rtmp://127.0.0.1:19350/test/teststream"}, + } + return &SrsError{Code: 0, Data: res} + } else { + return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid action:%v", msg.Action)} + } + return +} + +func ForwardServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) + c := &Forward{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} diff --git a/trunk/research/api-server/src/hls.go b/trunk/research/api-server/src/hls.go new file mode 100644 index 0000000000..f9fea12705 --- /dev/null +++ b/trunk/research/api-server/src/hls.go @@ -0,0 +1,105 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" +) + +/* + for SRS hook: on_hls_notify + on_hls_notify: + when srs reap a ts file of hls, call this hook, + used to push file to cdn network, by get the ts file from cdn network. + so we use HTTP GET and use the variable following: + [app], replace with the app. + [stream], replace with the stream. + [param], replace with the param. + [ts_url], replace with the ts url. + ignore any return data of server. + + for SRS hook: on_hls + on_hls: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_dvr", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "duration": 9.68, // in seconds + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", + "seq_no": 100 + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type HlsMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` + Duration float64 `json:"duration"` + Cwd string `json:"cwd"` + File string `json:"file"` + SeqNo int `json:"seq_no"` +} + +func (v *HlsMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, duration=%v, cwd=%v, file=%v, seq_no=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Duration, v.Cwd, v.File, v.SeqNo) +} + +type Hls struct {} + +func (v *Hls) Parse(body []byte) (se *SrsError) { + msg := &HlsMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse hls msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the hls requests: hls stream. +func HlsServe(w http.ResponseWriter, r *http.Request) { + log.Println(fmt.Sprintf("hls serve, uPath=%v", r.URL.Path)) + if r.Method == "GET" { + subPath := r.URL.Path[len("/api/v1/hls/"):] + res := struct { + Args []string `json:"args"` + KwArgs url.Values `json:"kwargs"` + }{ + Args: strings.Split(subPath, "/"), + KwArgs: r.URL.Query(), + } + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) + c := &Hls{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} diff --git a/trunk/research/api-server/src/main.go b/trunk/research/api-server/src/main.go new file mode 100644 index 0000000000..4f9656047e --- /dev/null +++ b/trunk/research/api-server/src/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + "path" + "path/filepath" +) + +const ( + // ok, success, completed. + success = 0 + // error when read http request + error_system_read_request = 100 + // error when parse json + error_system_parse_json = 101 + // request action invalid + error_request_invalid_action = 200 + // cdn node not exists + error_cdn_node_not_exists = 201 + // http request failed + error_http_request_failed = 202 + + // chat id not exist + error_chat_id_not_exist = 300 + // + + client_action_on_connect = "on_connect" + client_action_on_close = "on_close" + session_action_on_play = "on_play" + session_action_on_stop = "on_stop" +) + +const ( + HttpJson = "application/json" +) + +type SrsError struct { + Code int `json:"code"` + Data interface{} `json:"data"` +} + +func Response(se *SrsError) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := json.Marshal(se) + w.Header().Set("Content-Type", HttpJson) + w.Write(body) + }) +} + +var StaticDir string + +const Example = ` +SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) +Example: + ./api-server -p 8085 -s ./static-dir +See also: https://github.com/ossrs/srs +` + +//func FileServer() http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// upath := r.URL.Path +// if !strings.HasPrefix(upath, "/") { +// upath = "/" + upath +// } +// log.Println(fmt.Sprintf("upath=%v", upath)) +// }) +//} + +var cm *ChatManager +var sm *ServerManager +var sw *SnapshotWorker + +func main() { + var port int + var ffmpegPath string + flag.IntVar(&port, "p", 8085, "use -p to specify listen port, default is 8085") + flag.StringVar(&StaticDir, "s", "./static-dir", "use -s to specify static-dir, default is ./static-dir") + flag.StringVar(&ffmpegPath, "ffmpeg", "./objs/ffmpeg/bin/ffmpeg", "use -ffmpeg to specify ffmpegPath, default is ./objs/ffmpeg/bin/ffmpeg") + flag.Usage = func() { + fmt.Fprintln(flag.CommandLine.Output(), "Usage: apiServer [flags]") + flag.PrintDefaults() + fmt.Fprintln(flag.CommandLine.Output(), Example) + } + flag.Parse() + + if len(os.Args[1:]) == 0 { + flag.Usage() + os.Exit(0) + } + + cm = NewChatManager() + sm = NewServerManager() + sw = NewSnapshotWorker(ffmpegPath) + go sw.Serve() + + if len(StaticDir) == 0 { + curAbsDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) + StaticDir = path.Join(curAbsDir, "./static-dir") + } else { + StaticDir, _ = filepath.Abs(StaticDir) + } + log.Println(fmt.Sprintf("api server listen at port:%v, static_dir:%v", port, StaticDir)) + + http.Handle("/", http.FileServer(http.Dir(StaticDir))) + http.HandleFunc("/api/v1", func(writer http.ResponseWriter, request *http.Request) { + res := &struct { + Code int `json:"code"` + Urls struct{ + Clients string `json:"clients"` + Streams string `json:"streams"` + Sessions string `json:"sessions"` + Dvrs string `json:"dvrs"` + Chats string `json:"chats"` + Servers struct{ + Summary string `json:"summary"` + Get string `json:"GET"` + Post string `json:"POST ip=node_ip&device_id=device_id"` + } + } `json:"urls"` + }{ + Code: 0, + } + res.Urls.Clients = "for srs http callback, to handle the clients requests: connect/disconnect vhost/app." + res.Urls.Streams = "for srs http callback, to handle the streams requests: publish/unpublish stream." + res.Urls.Sessions = "for srs http callback, to handle the sessions requests: client play/stop stream." + res.Urls.Dvrs = "for srs http callback, to handle the dvr requests: dvr stream." + //res.Urls.Chats = "for srs demo meeting, the chat streams, public chat room." + res.Urls.Servers.Summary = "for srs raspberry-pi and meeting demo." + res.Urls.Servers.Get = "get the current raspberry-pi servers info." + res.Urls.Servers.Post = "the new raspberry-pi server info." + // TODO: no snapshots + body, _ := json.Marshal(res) + writer.Write(body) + }) + + http.HandleFunc("/api/v1/clients", ClientServe) + http.HandleFunc("/api/v1/streams", StreamServe) + http.HandleFunc("/api/v1/sessions", SessionServe) + http.HandleFunc("/api/v1/dvrs", DvrServe) + http.HandleFunc("/api/v1/hls", HlsServe) + http.HandleFunc("/api/v1/hls/", HlsServe) + http.HandleFunc("/api/v1/proxy/", ProxyServe) + + // not support yet + http.HandleFunc("/api/v1/chat", ChatServe) + + http.HandleFunc("/api/v1/servers", ServerServe) + http.HandleFunc("/api/v1/servers/", ServerServe) + http.HandleFunc("/api/v1/snapshots", SnapshotServe) + http.HandleFunc("/api/v1/forward", ForwardServe) + + addr := fmt.Sprintf(":%v", port) + log.Println(fmt.Sprintf("start listen on:%v", addr)) + if err := http.ListenAndServe(addr, nil); err != nil { + log.Println(fmt.Sprintf("listen on addr:%v failed, err is %v", addr, err)) + } +} \ No newline at end of file diff --git a/trunk/research/api-server/src/proxy.go b/trunk/research/api-server/src/proxy.go new file mode 100644 index 0000000000..68bfb5633d --- /dev/null +++ b/trunk/research/api-server/src/proxy.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* + for SRS hook: on_hls_notify + on_hls_notify: + when srs reap a ts file of hls, call this hook, + used to push file to cdn network, by get the ts file from cdn network. + so we use HTTP GET and use the variable following: + [app], replace with the app. + [stream], replace with the stream. + [param], replace with the param. + [ts_url], replace with the ts url. + ignore any return data of server. +*/ + +type Proxy struct { + proxyUrl string +} + +func (v *Proxy) Serve(notifyPath string) (se *SrsError) { + v.proxyUrl = fmt.Sprintf("http://%v", notifyPath) + log.Println(fmt.Sprintf("start to proxy url:%v", v.proxyUrl)) + resp, err := http.Get(v.proxyUrl) + if err != nil { + return &SrsError{error_http_request_failed, fmt.Sprintf("get %v failed, err is %v", v.proxyUrl, err)} + } + defer resp.Body.Close() + if _, err = ioutil.ReadAll(resp.Body); err != nil { + return &SrsError{error_system_read_request, fmt.Sprintf("read proxy body failed, err is %v", err)} + } + log.Println(fmt.Sprintf("completed proxy url:%v", v.proxyUrl)) + return nil +} + +// handle the hls proxy requests: hls stream. +func ProxyServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + subPath := r.URL.Path[len("/api/v1/proxy/"):] + c := &Proxy{} + if se := c.Serve(subPath); se != nil { + Response(se).ServeHTTP(w, r) + return + } + w.Write([]byte(c.proxyUrl)) + } +} diff --git a/trunk/research/api-server/src/server.go b/trunk/research/api-server/src/server.go new file mode 100644 index 0000000000..b847c01e62 --- /dev/null +++ b/trunk/research/api-server/src/server.go @@ -0,0 +1,138 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" +) + +/* +the server list +*/ + +type ServerMsg struct { + Ip string `json:"ip"` + DeviceId string `json:"device_id"` + Summaries interface{} `json:"summaries"` + Devices interface{} `json:"devices"` //not used now +} + +type ArmServer struct { + Id string `json:"id"` + ServerMsg + PublicIp string `json:"public_ip"` + Heartbeat int64 `json:"heartbeat"` + HeartbeatH string `json:"heartbeat_h"` + Api string `json:"api"` + Console string `json:"console"` +} + +func (v *ArmServer) Dead() bool { + deadTimeSeconds := int64(20) + if time.Now().Unix() - v.Heartbeat > deadTimeSeconds { + return true + } + return false +} + +type ServerManager struct { + globalArmServerId int + nodes *sync.Map // key is deviceId + lastUpdateAt time.Time +} + +func NewServerManager() *ServerManager { + sm := &ServerManager{ + globalArmServerId: os.Getpid(), + nodes: new(sync.Map), + lastUpdateAt: time.Now(), + } + return sm +} + +func (v *ServerManager) List(id string) (nodes []*ArmServer) { + nodes = []*ArmServer{} + // list nodes, remove dead node + v.nodes.Range(func(key, value any) bool { + node, _ := value.(*ArmServer) + if node.Dead() { + v.nodes.Delete(key) + return true + } + if len(id) == 0 { + nodes = append(nodes, node) + return true + } + if id == node.Id || id == node.DeviceId { + nodes = append(nodes, node) + return true + } + return true + }) + return +} + +func (v *ServerManager) Parse(body []byte, r *http.Request) (se *SrsError) { + msg := &ServerMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse server msg failed, err is %v", err.Error())} + } + + var node *ArmServer + value, ok := v.nodes.Load(msg.DeviceId) + if !ok { + node = &ArmServer{} + node.ServerMsg = *msg + node.Id = strconv.Itoa(v.globalArmServerId) + v.globalArmServerId += 1 + } else { + node = value.(*ArmServer) + if msg.Summaries != nil { + node.Summaries = msg.Summaries + } + if msg.Devices != nil { + node.Devices = msg.Devices + } + } + node.PublicIp = r.RemoteAddr + now := time.Now() + node.Heartbeat = now.Unix() + node.HeartbeatH = now.Format("2006-01-02 15:04:05") + v.nodes.Store(msg.DeviceId, node) + return nil +} + +func ServerServe(w http.ResponseWriter, r *http.Request) { + uPath := r.URL.Path + if r.Method == "GET" { + index := strings.Index(uPath, "/api/v1/servers/") + if index == -1 { + Response(&SrsError{Code: 0, Data: sm.List("")}).ServeHTTP(w, r) + } else { + id := uPath[(index + len("/api/v1/servers/")):] + Response(&SrsError{Code: 0, Data: sm.List(id)}).ServeHTTP(w, r) + } + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to nodes, req=%v", string(body))) + + if se := sm.Parse(body, r); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } + +} \ No newline at end of file diff --git a/trunk/research/api-server/src/session.go b/trunk/research/api-server/src/session.go new file mode 100644 index 0000000000..251901505c --- /dev/null +++ b/trunk/research/api-server/src/session.go @@ -0,0 +1,117 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* + for SRS hook: on_play/on_stop + on_play: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_play", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "pageUrl": "http://www.test.com/live.html" + } + on_stop: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_stop", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type SessionMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +type SessionOnPlayMsg struct { + SessionMsg + PageUrl string `json:"pageUrl"` +} + +func (v *SessionOnPlayMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.PageUrl) +} + +type SessionOnStopMsg struct { + SessionMsg +} + +func (v *SessionOnStopMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) +} + +type Session struct {} + +func (v *Session) Parse(body []byte) (se *SrsError) { + data := &struct { + Action string `json:"action"` + }{} + if err := json.Unmarshal(body, data); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session action failed, err is %v", err.Error())} + } + + if data.Action == session_action_on_play { + msg := &SessionOnPlayMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} + } + log.Println(msg) + } else if data.Action == session_action_on_stop { + msg := &SessionOnStopMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} + } + log.Println(msg) + } + return nil +} + +// handle the sessions requests: client play/stop stream +func SessionServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) + c := &Session{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} diff --git a/trunk/research/api-server/src/snapshot.go b/trunk/research/api-server/src/snapshot.go new file mode 100644 index 0000000000..fc9fe53f73 --- /dev/null +++ b/trunk/research/api-server/src/snapshot.go @@ -0,0 +1,195 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path" + "sync" + "time" +) + +/* +the snapshot api, +to start a snapshot when encoder start publish stream, +stop the snapshot worker when stream finished. + +{"action":"on_publish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} +{"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} +*/ + +type SnapShot struct {} + +func (v *SnapShot) Parse(body []byte) (se *SrsError) { + msg := &StreamMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse snapshot msg failed, err is %v", err.Error())} + } + if msg.Action == "on_publish" { + sw.Create(msg) + return &SrsError{Code: 0, Data: nil} + } else if msg.Action == "on_unpublish" { + sw.Destroy(msg) + return &SrsError{Code: 0, Data: nil} + } else { + return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid req action:%v", msg.Action)} + } +} + +func SnapshotServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to snapshot, req=%v", string(body))) + s := &SnapShot{} + if se := s.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + } +} + +type SnapshotJob struct { + StreamMsg + cmd *exec.Cmd + abort bool + timestamp time.Time + lock *sync.RWMutex +} + +func NewSnapshotJob() *SnapshotJob { + v := &SnapshotJob{ + lock: new(sync.RWMutex), + } + return v +} + +func (v *SnapshotJob) UpdateAbort(status bool) { + v.lock.Lock() + defer v.lock.Unlock() + v.abort = status +} + +func (v *SnapshotJob) IsAbort() bool { + v.lock.RLock() + defer v.lock.RUnlock() + return v.abort +} + +type SnapshotWorker struct { + snapshots *sync.Map // key is stream url + ffmpegPath string +} + +func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { + sw := &SnapshotWorker{ + snapshots: new(sync.Map), + ffmpegPath: ffmpegPath, + } + return sw +} + +/* +./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live...vhost...__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y /Users/mengxiaowei/jdcloud/mt/srs/trunk/research/api-server/static-dir/live/panda-%03d.png +*/ + +func (v *SnapshotWorker) Serve() { + for { + time.Sleep(time.Second) + v.snapshots.Range(func(key, value any) bool { + // range each snapshot job + streamUrl := key.(string) + sj := value.(*SnapshotJob) + streamTag := fmt.Sprintf("%v/%v/%v", sj.Vhost, sj.App, sj.Stream) + if sj.IsAbort() { // delete aborted snapshot job + if sj.cmd != nil && sj.cmd.Process != nil { + if err := sj.cmd.Process.Kill(); err != nil { + log.Println(fmt.Sprintf("snapshot job:%v kill running cmd failed, err is %v", streamTag, err)) + } + } + v.snapshots.Delete(key) + return true + } + + if sj.cmd == nil { // start a ffmpeg snap cmd + outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream) + "-%03d.png") + bestPng := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-best.png", sj.Stream)) + if err := os.MkdirAll(path.Base(outputDir), 0777); err != nil { + log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", path.Base(outputDir), err)) + return true + } + vframes := 5 + param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", v.ffmpegPath, streamUrl, vframes, outputDir) + timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30) * time.Second) + cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) + if err := cmd.Start(); err != nil { + log.Println(fmt.Sprintf("start snapshot %v cmd failed, err is %v", streamTag, err)) + return true + } + sj.cmd = cmd + log.Println(fmt.Sprintf("start snapshot success, cmd param=%v", param)) + go func() { + if err := sj.cmd.Wait(); err != nil { + log.Println(fmt.Sprintf("snapshot %v cmd wait failed, err is %v", streamTag, err)) + } else { // choose the best quality image + bestFileSize := int64(0) + for i := 1; i <= vframes; i ++ { + pic := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-%03d.png", sj.Stream, i)) + fi, err := os.Stat(pic) + if err != nil { + log.Println(fmt.Sprintf("stat pic:%v failed, err is %v", pic, err)) + continue + } + if bestFileSize == 0 { + bestFileSize = fi.Size() + } else if fi.Size() > bestFileSize { + os.Remove(bestPng) + os.Link(pic, bestPng) + bestFileSize = fi.Size() + } + } + log.Println(fmt.Sprintf("%v the best thumbnail is %v", streamTag, bestPng)) + } + sj.cmd = nil + }() + } else { + log.Println(fmt.Sprintf("snapshot %v cmd process is running, status=%v", streamTag, sj.cmd.ProcessState)) + } + return true + }) + } +} + +func (v *SnapshotWorker) Create(sm *StreamMsg) { + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + if _, ok := v.snapshots.Load(streamUrl); ok { + return + } + sj := NewSnapshotJob() + sj.StreamMsg = *sm + sj.timestamp = time.Now() + v.snapshots.Store(streamUrl, sj) +} + +func (v *SnapshotWorker) Destroy(sm *StreamMsg) { + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + value, ok := v.snapshots.Load(streamUrl) + if ok { + sj := value.(*SnapshotJob) + sj.UpdateAbort(true) + v.snapshots.Store(streamUrl, sj) + log.Println(fmt.Sprintf("set stream:%v to destroy, update abort", sm.Stream)) + } else { + log.Println(fmt.Sprintf("cannot find stream:%v in snapshot worker", streamUrl)) + } + return +} diff --git a/trunk/research/api-server/src/stream.go b/trunk/research/api-server/src/stream.go new file mode 100644 index 0000000000..826a01732e --- /dev/null +++ b/trunk/research/api-server/src/stream.go @@ -0,0 +1,88 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" +) + +/* + for SRS hook: on_publish/on_unpublish + on_publish: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_publish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + on_unpublish: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_unpublish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ +type StreamMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +func (v *StreamMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) +} + +type Stream struct {} + +func (v *Stream) Parse(body []byte) (se *SrsError) { + msg := &StreamMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse stream msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the streams requests: publish/unpublish stream. + +func StreamServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) + c := &Stream{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} From 1eec26cf3a42b467aeaf8ff3aa6947fce48737e9 Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sun, 15 Jan 2023 11:54:14 +0800 Subject: [PATCH 02/19] simplify api-server go files to one file, add go.mod --- trunk/research/api-server/go.mod | 3 + trunk/research/api-server/server.go | 1000 +++++++++++++++++++++ trunk/research/api-server/src/chat.go | 123 --- trunk/research/api-server/src/client.go | 117 --- trunk/research/api-server/src/dvr.go | 80 -- trunk/research/api-server/src/forward.go | 97 -- trunk/research/api-server/src/hls.go | 105 --- trunk/research/api-server/src/main.go | 162 ---- trunk/research/api-server/src/proxy.go | 53 -- trunk/research/api-server/src/server.go | 138 --- trunk/research/api-server/src/session.go | 117 --- trunk/research/api-server/src/snapshot.go | 195 ---- trunk/research/api-server/src/stream.go | 88 -- 13 files changed, 1003 insertions(+), 1275 deletions(-) create mode 100644 trunk/research/api-server/go.mod create mode 100644 trunk/research/api-server/server.go delete mode 100644 trunk/research/api-server/src/chat.go delete mode 100644 trunk/research/api-server/src/client.go delete mode 100644 trunk/research/api-server/src/dvr.go delete mode 100644 trunk/research/api-server/src/forward.go delete mode 100644 trunk/research/api-server/src/hls.go delete mode 100644 trunk/research/api-server/src/main.go delete mode 100644 trunk/research/api-server/src/proxy.go delete mode 100644 trunk/research/api-server/src/server.go delete mode 100644 trunk/research/api-server/src/session.go delete mode 100644 trunk/research/api-server/src/snapshot.go delete mode 100644 trunk/research/api-server/src/stream.go diff --git a/trunk/research/api-server/go.mod b/trunk/research/api-server/go.mod new file mode 100644 index 0000000000..486515ed8d --- /dev/null +++ b/trunk/research/api-server/go.mod @@ -0,0 +1,3 @@ +module api-server + +go 1.18 diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go new file mode 100644 index 0000000000..a3f46a6cc1 --- /dev/null +++ b/trunk/research/api-server/server.go @@ -0,0 +1,1000 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + "time" +) + +const ( + // ok, success, completed. + success = 0 + // error when read http request + error_system_read_request = 100 + // error when parse json + error_system_parse_json = 101 + // request action invalid + error_request_invalid_action = 200 + // cdn node not exists + error_cdn_node_not_exists = 201 + // http request failed + error_http_request_failed = 202 + + // chat id not exist + error_chat_id_not_exist = 300 + // + + client_action_on_connect = "on_connect" + client_action_on_close = "on_close" + session_action_on_play = "on_play" + session_action_on_stop = "on_stop" +) + +const ( + HttpJson = "application/json" +) + +type SrsError struct { + Code int `json:"code"` + Data interface{} `json:"data"` +} + +func Response(se *SrsError) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := json.Marshal(se) + w.Header().Set("Content-Type", HttpJson) + w.Write(body) + }) +} + +const Example = ` +SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) +Example: + ./api-server -p 8085 -s ./static-dir +See also: https://github.com/ossrs/srs +` + +var StaticDir string +var cm *ChatManager +var sw *SnapshotWorker + + +/* +handle the clients requests: connect/disconnect vhost/app. + for SRS hook: on_connect/on_close + on_connect: + when client connect to vhost/app, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_connect", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", + "pageUrl": "http://www.test.com/live.html" + } + on_close: + when client close/disconnect to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_close", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "send_bytes": 10240, + "recv_bytes": 10240 + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ +type ClientMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` +} + +type ClientOnConnectMsg struct { + ClientMsg + TcUrl string `json:"tcUrl"` + PageUrl string `json:"pageUrl"` +} + +func (v *ClientOnConnectMsg) String() string { + return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.PageUrl) +} + +type ClientOnCloseMsg struct { + ClientMsg + SendBytes int64 `json:"send_bytes"` + RecvBytes int64 `json:"recv_bytes"` +} + +func (v *ClientOnCloseMsg) String() string { + return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, send_bytes=%v, recv_bytes=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.SendBytes, v.RecvBytes) +} + +type Client struct {} + +func (v *Client) Parse(body []byte) (se *SrsError) { + data := &struct { + Action string `json:"action"` + }{} + if err := json.Unmarshal(body, data); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client action failed, err is %v", err.Error())} + } + + if data.Action == client_action_on_connect { + msg := &ClientOnConnectMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_connect, err.Error())} + } + log.Println(msg) + } else if data.Action == client_action_on_close { + msg := &ClientOnCloseMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_close, err.Error())} + } + log.Println(msg) + } + return nil +} + +// handle the clients requests: connect/disconnect vhost/app. +func ClientServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) + c := &Client{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + + +/* + for SRS hook: on_publish/on_unpublish + on_publish: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_publish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + on_unpublish: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_unpublish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ +type StreamMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +func (v *StreamMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) +} + +type Stream struct {} + +func (v *Stream) Parse(body []byte) (se *SrsError) { + msg := &StreamMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse stream msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the streams requests: publish/unpublish stream. + +func StreamServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) + c := &Stream{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + + +/* + for SRS hook: on_play/on_stop + on_play: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_play", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "pageUrl": "http://www.test.com/live.html" + } + on_stop: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_stop", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type SessionMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +type SessionOnPlayMsg struct { + SessionMsg + PageUrl string `json:"pageUrl"` +} + +func (v *SessionOnPlayMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.PageUrl) +} + +type SessionOnStopMsg struct { + SessionMsg +} + +func (v *SessionOnStopMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) +} + +type Session struct {} + +func (v *Session) Parse(body []byte) (se *SrsError) { + data := &struct { + Action string `json:"action"` + }{} + if err := json.Unmarshal(body, data); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session action failed, err is %v", err.Error())} + } + + if data.Action == session_action_on_play { + msg := &SessionOnPlayMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} + } + log.Println(msg) + } else if data.Action == session_action_on_stop { + msg := &SessionOnStopMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} + } + log.Println(msg) + } + return nil +} + +// handle the sessions requests: client play/stop stream +func SessionServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) + c := &Session{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + + +/* + for SRS hook: on_dvr + on_dvr: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_dvr", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776.flv" + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type DvrMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` + Cwd string `json:"cwd"` + File string `json:"file"` +} + +func (v *DvrMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, cwd=%v, file=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Cwd, v.File) +} + +type Dvr struct {} + +func (v *Dvr) Parse(body []byte) (se *SrsError) { + msg := &DvrMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse dvr msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the dvrs requests: dvr stream. +func DvrServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) + c := &Dvr{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + + +/* + for SRS hook: on_hls_notify + on_hls_notify: + when srs reap a ts file of hls, call this hook, + used to push file to cdn network, by get the ts file from cdn network. + so we use HTTP GET and use the variable following: + [app], replace with the app. + [stream], replace with the stream. + [param], replace with the param. + [ts_url], replace with the ts url. + ignore any return data of server. + + for SRS hook: on_hls + on_hls: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_dvr", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "duration": 9.68, // in seconds + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", + "seq_no": 100 + } + if valid, the hook must return HTTP code 200(Stauts OK) and response + an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type HlsMsg struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` + Duration float64 `json:"duration"` + Cwd string `json:"cwd"` + File string `json:"file"` + SeqNo int `json:"seq_no"` +} + +func (v *HlsMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, duration=%v, cwd=%v, file=%v, seq_no=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Duration, v.Cwd, v.File, v.SeqNo) +} + +type Hls struct {} + +func (v *Hls) Parse(body []byte) (se *SrsError) { + msg := &HlsMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse hls msg failed, err is %v", err.Error())} + } + log.Println(msg) + return nil +} + +// handle the hls requests: hls stream. +func HlsServe(w http.ResponseWriter, r *http.Request) { + log.Println(fmt.Sprintf("hls serve, uPath=%v", r.URL.Path)) + if r.Method == "GET" { + subPath := r.URL.Path[len("/api/v1/hls/"):] + res := struct { + Args []string `json:"args"` + KwArgs url.Values `json:"kwargs"` + }{ + Args: strings.Split(subPath, "/"), + KwArgs: r.URL.Query(), + } + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) + c := &Hls{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + + +/* +# object fields: +# id: an int value indicates the id of user. +# username: a str indicates the user name. +# url: a str indicates the url of user stream. +# agent: a str indicates the agent of user. +# join_date: a number indicates the join timestamp in seconds. +# join_date_str: a str specifies the formated friendly time. +# heartbeat: a number indicates the heartbeat timestamp in seconds. +# vcodec: a dict indicates the video codec info. +# acodec: a dict indicates the audio codec info. + +# dead time in seconds, if exceed, remove the chat. +*/ + +type Chat struct { + Id int `json:"id"` + Username string `json:"username"` + Url string `json:"url"` + JoinDate int64 `json:"join_date"` + JoinDateStr string `json:"join_date_str"` + Heartbeat int64 `json:"heartbeat"` +} + +type ChatManager struct { + globalId int + chats *sync.Map + deadTime int +} + +func NewChatManager() *ChatManager { + v := &ChatManager{ + globalId: 100, + // key is globalId, value is chat + chats: new(sync.Map), + deadTime: 15, + } + return v +} + +func (v *ChatManager) List() (chats []*Chat) { + chats = []*Chat{} + v.chats.Range(func(key, value any) bool { + _, chat := key.(int), value.(*Chat) + if (time.Now().Unix() - chat.Heartbeat) > int64(v.deadTime) { + v.chats.Delete(key) + return true + } + chats = append(chats, chat) + return true + }) + return +} + +func (v *ChatManager) Update(id int) (se *SrsError) { + value, ok := v.chats.Load(id) + if !ok { + return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} + } + c := value.(*Chat) + c.Heartbeat = time.Now().Unix() + log.Println(fmt.Sprintf("heartbeat chat success, id=%v", id)) + return nil +} + +func (v *ChatManager) Delete(id int) (se *SrsError) { + if _, ok := v.chats.Load(id); !ok { + return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} + } + v.chats.Delete(id) + log.Println(fmt.Sprintf("delete chat success, id=%v", id)) + return +} + +func (v *ChatManager) Add(c *Chat) { + c.Id = v.globalId + now := time.Now() + c.JoinDate, c.Heartbeat = now.Unix(), now.Unix() + c.JoinDateStr = now.Format("2006-01-02 15:04:05") + v.globalId += 1 + v.chats.Store(c.Id, c) +} + +// the chat streams, public chat room. +func ChatServe(w http.ResponseWriter, r *http.Request) { + log.Println(fmt.Sprintf("got a chat req, uPath=%v", r.URL.Path)) + if r.Method == "GET" { + chats := cm.List() + Response(&SrsError{Code: 0, Data: chats}).ServeHTTP(w, r) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + c := &Chat{} + if err := json.Unmarshal(body, c); err != nil { + Response(&SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse body to chat json failed, err is %v", err)}) + return + } + cm.Add(c) + log.Println(fmt.Sprintf("create chat success, id=%v", c.Id)) + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + } else if r.Method == "PUT" { + // TODO: parse id? + Response(cm.Update(0)).ServeHTTP(w, r) + } else if r.Method == "DELETE" { + // TODO: parse id? + Response(cm.Delete(0)).ServeHTTP(w, r) + } +} + +/* +the snapshot api, +to start a snapshot when encoder start publish stream, +stop the snapshot worker when stream finished. + +{"action":"on_publish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} +{"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} +*/ + +type SnapShot struct {} + +func (v *SnapShot) Parse(body []byte) (se *SrsError) { + msg := &StreamMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse snapshot msg failed, err is %v", err.Error())} + } + if msg.Action == "on_publish" { + sw.Create(msg) + return &SrsError{Code: 0, Data: nil} + } else if msg.Action == "on_unpublish" { + sw.Destroy(msg) + return &SrsError{Code: 0, Data: nil} + } else { + return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid req action:%v", msg.Action)} + } +} + +func SnapshotServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to snapshot, req=%v", string(body))) + s := &SnapShot{} + if se := s.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + } +} + +type SnapshotJob struct { + StreamMsg + cmd *exec.Cmd + abort bool + timestamp time.Time + lock *sync.RWMutex +} + +func NewSnapshotJob() *SnapshotJob { + v := &SnapshotJob{ + lock: new(sync.RWMutex), + } + return v +} + +func (v *SnapshotJob) UpdateAbort(status bool) { + v.lock.Lock() + defer v.lock.Unlock() + v.abort = status +} + +func (v *SnapshotJob) IsAbort() bool { + v.lock.RLock() + defer v.lock.RUnlock() + return v.abort +} + +type SnapshotWorker struct { + snapshots *sync.Map // key is stream url + ffmpegPath string +} + +func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { + sw := &SnapshotWorker{ + snapshots: new(sync.Map), + ffmpegPath: ffmpegPath, + } + return sw +} + +/* +./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live?vhost=__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png +*/ + +func (v *SnapshotWorker) Serve() { + for { + time.Sleep(time.Second) + v.snapshots.Range(func(key, value any) bool { + // range each snapshot job + streamUrl := key.(string) + sj := value.(*SnapshotJob) + streamTag := fmt.Sprintf("%v/%v/%v", sj.Vhost, sj.App, sj.Stream) + if sj.IsAbort() { // delete aborted snapshot job + if sj.cmd != nil && sj.cmd.Process != nil { + if err := sj.cmd.Process.Kill(); err != nil { + log.Println(fmt.Sprintf("snapshot job:%v kill running cmd failed, err is %v", streamTag, err)) + } + } + v.snapshots.Delete(key) + return true + } + + if sj.cmd == nil { // start a ffmpeg snap cmd + outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream) + "-%03d.png") + bestPng := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-best.png", sj.Stream)) + if err := os.MkdirAll(path.Dir(outputDir), 0777); err != nil { + log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", path.Base(outputDir), err)) + return true + } + vframes := 5 + param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", v.ffmpegPath, streamUrl, vframes, outputDir) + timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30) * time.Second) + cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) + if err := cmd.Start(); err != nil { + log.Println(fmt.Sprintf("start snapshot %v cmd failed, err is %v", streamTag, err)) + return true + } + sj.cmd = cmd + log.Println(fmt.Sprintf("start snapshot success, cmd param=%v", param)) + go func() { + if err := sj.cmd.Wait(); err != nil { + log.Println(fmt.Sprintf("snapshot %v cmd wait failed, err is %v", streamTag, err)) + } else { // choose the best quality image + bestFileSize := int64(0) + for i := 1; i <= vframes; i ++ { + pic := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-%03d.png", sj.Stream, i)) + fi, err := os.Stat(pic) + if err != nil { + log.Println(fmt.Sprintf("stat pic:%v failed, err is %v", pic, err)) + continue + } + if bestFileSize == 0 { + bestFileSize = fi.Size() + } else if fi.Size() > bestFileSize { + os.Remove(bestPng) + os.Link(pic, bestPng) + bestFileSize = fi.Size() + } + } + log.Println(fmt.Sprintf("%v the best thumbnail is %v", streamTag, bestPng)) + } + sj.cmd = nil + }() + } else { + log.Println(fmt.Sprintf("snapshot %v cmd process is running, status=%v", streamTag, sj.cmd.ProcessState)) + } + return true + }) + } +} + +func (v *SnapshotWorker) Create(sm *StreamMsg) { + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + if _, ok := v.snapshots.Load(streamUrl); ok { + return + } + sj := NewSnapshotJob() + sj.StreamMsg = *sm + sj.timestamp = time.Now() + v.snapshots.Store(streamUrl, sj) +} + +func (v *SnapshotWorker) Destroy(sm *StreamMsg) { + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + value, ok := v.snapshots.Load(streamUrl) + if ok { + sj := value.(*SnapshotJob) + sj.UpdateAbort(true) + v.snapshots.Store(streamUrl, sj) + log.Println(fmt.Sprintf("set stream:%v to destroy, update abort", sm.Stream)) + } else { + log.Println(fmt.Sprintf("cannot find stream:%v in snapshot worker", streamUrl)) + } + return +} + +/* +handle the forward requests: dynamic forward url. +for SRS hook: on_forward +on_forward: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_forward", + "server_id": "server_test", + "client_id": 1985, + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 +*/ + +type ForwardMsg struct { + Action string `json:"action"` + ServerId string `json:"server_id"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + TcUrl string `json:"tc_url"` + Stream string `json:"stream"` + Param string `json:"param"` +} + +func (v *ForwardMsg) String() string { + return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) +} + +type Forward struct {} + +/* +backend service config description: + support multiple rtmp urls(custom addresses or third-party cdn service), + url's host is slave service. +For example: + ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] +*/ +func (v *Forward) Parse(body []byte) (se *SrsError) { + msg := &ForwardMsg{} + if err := json.Unmarshal(body, msg); err != nil { + return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse forward msg failed, err is %v", err.Error())} + } + if msg.Action == "on_forward" { + log.Println(msg) + res := &struct { + Urls []string `json:"urls"` + }{ + Urls: []string{"rtmp://127.0.0.1:19350/test/teststream"}, + } + return &SrsError{Code: 0, Data: res} + } else { + return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid action:%v", msg.Action)} + } + return +} + +func ForwardServe(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + res := struct {}{} + body, _ := json.Marshal(res) + w.Write(body) + } else if r.Method == "POST" { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) + return + } + log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) + c := &Forward{} + if se := c.Parse(body); se != nil { + Response(se).ServeHTTP(w, r) + return + } + Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) + return + } +} + +func main() { + var port int + var ffmpegPath string + flag.IntVar(&port, "p", 8085, "use -p to specify listen port, default is 8085") + flag.StringVar(&StaticDir, "s", "./static-dir", "use -s to specify static-dir, default is ./static-dir") + flag.StringVar(&ffmpegPath, "ffmpeg", "./objs/ffmpeg/bin/ffmpeg", "use -ffmpeg to specify ffmpegPath, default is ./objs/ffmpeg/bin/ffmpeg") + flag.Usage = func() { + fmt.Fprintln(flag.CommandLine.Output(), "Usage: apiServer [flags]") + flag.PrintDefaults() + fmt.Fprintln(flag.CommandLine.Output(), Example) + } + flag.Parse() + + if len(os.Args[1:]) == 0 { + flag.Usage() + os.Exit(0) + } + + log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) + cm = NewChatManager() + sw = NewSnapshotWorker(ffmpegPath) + go sw.Serve() + + if len(StaticDir) == 0 { + curAbsDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) + StaticDir = path.Join(curAbsDir, "./static-dir") + } else { + StaticDir, _ = filepath.Abs(StaticDir) + } + log.Println(fmt.Sprintf("api server listen at port:%v, static_dir:%v", port, StaticDir)) + + http.Handle("/", http.FileServer(http.Dir(StaticDir))) + http.HandleFunc("/api/v1", func(writer http.ResponseWriter, request *http.Request) { + res := &struct { + Code int `json:"code"` + Urls struct{ + Clients string `json:"clients"` + Streams string `json:"streams"` + Sessions string `json:"sessions"` + Dvrs string `json:"dvrs"` + Chats string `json:"chats"` + Servers struct{ + Summary string `json:"summary"` + Get string `json:"GET"` + Post string `json:"POST ip=node_ip&device_id=device_id"` + } + } `json:"urls"` + }{ + Code: 0, + } + res.Urls.Clients = "for srs http callback, to handle the clients requests: connect/disconnect vhost/app." + res.Urls.Streams = "for srs http callback, to handle the streams requests: publish/unpublish stream." + res.Urls.Sessions = "for srs http callback, to handle the sessions requests: client play/stop stream." + res.Urls.Dvrs = "for srs http callback, to handle the dvr requests: dvr stream." + //res.Urls.Chats = "for srs demo meeting, the chat streams, public chat room." + res.Urls.Servers.Summary = "for srs raspberry-pi and meeting demo." + res.Urls.Servers.Get = "get the current raspberry-pi servers info." + res.Urls.Servers.Post = "the new raspberry-pi server info." + // TODO: no snapshots + body, _ := json.Marshal(res) + writer.Write(body) + }) + + http.HandleFunc("/api/v1/clients", ClientServe) + http.HandleFunc("/api/v1/streams", StreamServe) + http.HandleFunc("/api/v1/sessions", SessionServe) + http.HandleFunc("/api/v1/dvrs", DvrServe) + http.HandleFunc("/api/v1/hls", HlsServe) + http.HandleFunc("/api/v1/hls/", HlsServe) + + // not support yet + http.HandleFunc("/api/v1/chat", ChatServe) + + http.HandleFunc("/api/v1/snapshots", SnapshotServe) + http.HandleFunc("/api/v1/forward", ForwardServe) + + addr := fmt.Sprintf(":%v", port) + log.Println(fmt.Sprintf("start listen on:%v", addr)) + if err := http.ListenAndServe(addr, nil); err != nil { + log.Println(fmt.Sprintf("listen on addr:%v failed, err is %v", addr, err)) + } +} \ No newline at end of file diff --git a/trunk/research/api-server/src/chat.go b/trunk/research/api-server/src/chat.go deleted file mode 100644 index 597ee8a528..0000000000 --- a/trunk/research/api-server/src/chat.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "sync" - "time" -) - -/* -# object fields: -# id: an int value indicates the id of user. -# username: a str indicates the user name. -# url: a str indicates the url of user stream. -# agent: a str indicates the agent of user. -# join_date: a number indicates the join timestamp in seconds. -# join_date_str: a str specifies the formated friendly time. -# heartbeat: a number indicates the heartbeat timestamp in seconds. -# vcodec: a dict indicates the video codec info. -# acodec: a dict indicates the audio codec info. - -# dead time in seconds, if exceed, remove the chat. -*/ - -type Chat struct { - Id int `json:"id"` - Username string `json:"username"` - Url string `json:"url"` - JoinDate int64 `json:"join_date"` - JoinDateStr string `json:"join_date_str"` - Heartbeat int64 `json:"heartbeat"` -} - -type ChatManager struct { - globalId int - chats *sync.Map - deadTime int -} - -func NewChatManager() *ChatManager { - v := &ChatManager{ - globalId: 100, - // key is globalId, value is chat - chats: new(sync.Map), - deadTime: 15, - } - return v -} - -func (v *ChatManager) List() (chats []*Chat) { - chats = []*Chat{} - v.chats.Range(func(key, value any) bool { - _, chat := key.(int), value.(*Chat) - if (time.Now().Unix() - chat.Heartbeat) > int64(v.deadTime) { - v.chats.Delete(key) - return true - } - chats = append(chats, chat) - return true - }) - return -} - -func (v *ChatManager) Update(id int) (se *SrsError) { - value, ok := v.chats.Load(id) - if !ok { - return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} - } - c := value.(*Chat) - c.Heartbeat = time.Now().Unix() - log.Println(fmt.Sprintf("heartbeat chat success, id=%v", id)) - return nil -} - -func (v *ChatManager) Delete(id int) (se *SrsError) { - if _, ok := v.chats.Load(id); !ok { - return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} - } - v.chats.Delete(id) - log.Println(fmt.Sprintf("delete chat success, id=%v", id)) - return -} - -func (v *ChatManager) Add(c *Chat) { - c.Id = v.globalId - now := time.Now() - c.JoinDate, c.Heartbeat = now.Unix(), now.Unix() - c.JoinDateStr = now.Format("2006-01-02 15:04:05") - v.globalId += 1 - v.chats.Store(c.Id, c) -} - -// the chat streams, public chat room. -func ChatServe(w http.ResponseWriter, r *http.Request) { - log.Println(fmt.Sprintf("got a chat req, uPath=%v", r.URL.Path)) - if r.Method == "GET" { - chats := cm.List() - Response(&SrsError{Code: 0, Data: chats}).ServeHTTP(w, r) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - c := &Chat{} - if err := json.Unmarshal(body, c); err != nil { - Response(&SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse body to chat json failed, err is %v", err)}) - return - } - cm.Add(c) - log.Println(fmt.Sprintf("create chat success, id=%v", c.Id)) - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - } else if r.Method == "PUT" { - // TODO: parse id? - Response(cm.Update(0)).ServeHTTP(w, r) - } else if r.Method == "DELETE" { - // TODO: parse id? - Response(cm.Delete(0)).ServeHTTP(w, r) - } -} diff --git a/trunk/research/api-server/src/client.go b/trunk/research/api-server/src/client.go deleted file mode 100644 index 0a619262f1..0000000000 --- a/trunk/research/api-server/src/client.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* -handle the clients requests: connect/disconnect vhost/app. - for SRS hook: on_connect/on_close - on_connect: - when client connect to vhost/app, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_connect", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", - "pageUrl": "http://www.test.com/live.html" - } - on_close: - when client close/disconnect to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_close", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "send_bytes": 10240, - "recv_bytes": 10240 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 -*/ -type ClientMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` -} - -type ClientOnConnectMsg struct { - ClientMsg - TcUrl string `json:"tcUrl"` - PageUrl string `json:"pageUrl"` -} - -func (v *ClientOnConnectMsg) String() string { - return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.PageUrl) -} - -type ClientOnCloseMsg struct { - ClientMsg - SendBytes int64 `json:"send_bytes"` - RecvBytes int64 `json:"recv_bytes"` -} - -func (v *ClientOnCloseMsg) String() string { - return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, send_bytes=%v, recv_bytes=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.SendBytes, v.RecvBytes) -} - -type Client struct {} - -func (v *Client) Parse(body []byte) (se *SrsError) { - data := &struct { - Action string `json:"action"` - }{} - if err := json.Unmarshal(body, data); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client action failed, err is %v", err.Error())} - } - - if data.Action == client_action_on_connect { - msg := &ClientOnConnectMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_connect, err.Error())} - } - log.Println(msg) - } else if data.Action == client_action_on_close { - msg := &ClientOnCloseMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_close, err.Error())} - } - log.Println(msg) - } - return nil -} - -// handle the clients requests: connect/disconnect vhost/app. -func ClientServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) - c := &Client{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} diff --git a/trunk/research/api-server/src/dvr.go b/trunk/research/api-server/src/dvr.go deleted file mode 100644 index e0b0a15488..0000000000 --- a/trunk/research/api-server/src/dvr.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* - for SRS hook: on_dvr - on_dvr: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_dvr", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776.flv" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 -*/ - -type DvrMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` - Cwd string `json:"cwd"` - File string `json:"file"` -} - -func (v *DvrMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, cwd=%v, file=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Cwd, v.File) -} - -type Dvr struct {} - -func (v *Dvr) Parse(body []byte) (se *SrsError) { - msg := &DvrMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse dvr msg failed, err is %v", err.Error())} - } - log.Println(msg) - return nil -} - -// handle the dvrs requests: dvr stream. -func DvrServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) - c := &Dvr{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} diff --git a/trunk/research/api-server/src/forward.go b/trunk/research/api-server/src/forward.go deleted file mode 100644 index 8d230a718d..0000000000 --- a/trunk/research/api-server/src/forward.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* -handle the forward requests: dynamic forward url. -for SRS hook: on_forward -on_forward: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_forward", - "server_id": "server_test", - "client_id": 1985, - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } -if valid, the hook must return HTTP code 200(Stauts OK) and response -an int value specifies the error code(0 corresponding to success): - 0 -*/ - -type ForwardMsg struct { - Action string `json:"action"` - ServerId string `json:"server_id"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - TcUrl string `json:"tc_url"` - Stream string `json:"stream"` - Param string `json:"param"` -} - -func (v *ForwardMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) -} - -type Forward struct {} - -/* -backend service config description: - support multiple rtmp urls(custom addresses or third-party cdn service), - url's host is slave service. -For example: - ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] -*/ -func (v *Forward) Parse(body []byte) (se *SrsError) { - msg := &DvrMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse forward msg failed, err is %v", err.Error())} - } - if msg.Action == "on_forward" { - log.Println(msg) - res := &struct { - Urls []string `json:"urls"` - }{ - Urls: []string{"rtmp://127.0.0.1:19350/test/teststream"}, - } - return &SrsError{Code: 0, Data: res} - } else { - return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid action:%v", msg.Action)} - } - return -} - -func ForwardServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) - c := &Forward{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} diff --git a/trunk/research/api-server/src/hls.go b/trunk/research/api-server/src/hls.go deleted file mode 100644 index f9fea12705..0000000000 --- a/trunk/research/api-server/src/hls.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "strings" -) - -/* - for SRS hook: on_hls_notify - on_hls_notify: - when srs reap a ts file of hls, call this hook, - used to push file to cdn network, by get the ts file from cdn network. - so we use HTTP GET and use the variable following: - [app], replace with the app. - [stream], replace with the stream. - [param], replace with the param. - [ts_url], replace with the ts url. - ignore any return data of server. - - for SRS hook: on_hls - on_hls: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_dvr", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "duration": 9.68, // in seconds - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", - "seq_no": 100 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 -*/ - -type HlsMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` - Duration float64 `json:"duration"` - Cwd string `json:"cwd"` - File string `json:"file"` - SeqNo int `json:"seq_no"` -} - -func (v *HlsMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, duration=%v, cwd=%v, file=%v, seq_no=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Duration, v.Cwd, v.File, v.SeqNo) -} - -type Hls struct {} - -func (v *Hls) Parse(body []byte) (se *SrsError) { - msg := &HlsMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse hls msg failed, err is %v", err.Error())} - } - log.Println(msg) - return nil -} - -// handle the hls requests: hls stream. -func HlsServe(w http.ResponseWriter, r *http.Request) { - log.Println(fmt.Sprintf("hls serve, uPath=%v", r.URL.Path)) - if r.Method == "GET" { - subPath := r.URL.Path[len("/api/v1/hls/"):] - res := struct { - Args []string `json:"args"` - KwArgs url.Values `json:"kwargs"` - }{ - Args: strings.Split(subPath, "/"), - KwArgs: r.URL.Query(), - } - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) - c := &Hls{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} diff --git a/trunk/research/api-server/src/main.go b/trunk/research/api-server/src/main.go deleted file mode 100644 index 4f9656047e..0000000000 --- a/trunk/research/api-server/src/main.go +++ /dev/null @@ -1,162 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "log" - "net/http" - "os" - "path" - "path/filepath" -) - -const ( - // ok, success, completed. - success = 0 - // error when read http request - error_system_read_request = 100 - // error when parse json - error_system_parse_json = 101 - // request action invalid - error_request_invalid_action = 200 - // cdn node not exists - error_cdn_node_not_exists = 201 - // http request failed - error_http_request_failed = 202 - - // chat id not exist - error_chat_id_not_exist = 300 - // - - client_action_on_connect = "on_connect" - client_action_on_close = "on_close" - session_action_on_play = "on_play" - session_action_on_stop = "on_stop" -) - -const ( - HttpJson = "application/json" -) - -type SrsError struct { - Code int `json:"code"` - Data interface{} `json:"data"` -} - -func Response(se *SrsError) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(se) - w.Header().Set("Content-Type", HttpJson) - w.Write(body) - }) -} - -var StaticDir string - -const Example = ` -SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) -Example: - ./api-server -p 8085 -s ./static-dir -See also: https://github.com/ossrs/srs -` - -//func FileServer() http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// upath := r.URL.Path -// if !strings.HasPrefix(upath, "/") { -// upath = "/" + upath -// } -// log.Println(fmt.Sprintf("upath=%v", upath)) -// }) -//} - -var cm *ChatManager -var sm *ServerManager -var sw *SnapshotWorker - -func main() { - var port int - var ffmpegPath string - flag.IntVar(&port, "p", 8085, "use -p to specify listen port, default is 8085") - flag.StringVar(&StaticDir, "s", "./static-dir", "use -s to specify static-dir, default is ./static-dir") - flag.StringVar(&ffmpegPath, "ffmpeg", "./objs/ffmpeg/bin/ffmpeg", "use -ffmpeg to specify ffmpegPath, default is ./objs/ffmpeg/bin/ffmpeg") - flag.Usage = func() { - fmt.Fprintln(flag.CommandLine.Output(), "Usage: apiServer [flags]") - flag.PrintDefaults() - fmt.Fprintln(flag.CommandLine.Output(), Example) - } - flag.Parse() - - if len(os.Args[1:]) == 0 { - flag.Usage() - os.Exit(0) - } - - cm = NewChatManager() - sm = NewServerManager() - sw = NewSnapshotWorker(ffmpegPath) - go sw.Serve() - - if len(StaticDir) == 0 { - curAbsDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) - StaticDir = path.Join(curAbsDir, "./static-dir") - } else { - StaticDir, _ = filepath.Abs(StaticDir) - } - log.Println(fmt.Sprintf("api server listen at port:%v, static_dir:%v", port, StaticDir)) - - http.Handle("/", http.FileServer(http.Dir(StaticDir))) - http.HandleFunc("/api/v1", func(writer http.ResponseWriter, request *http.Request) { - res := &struct { - Code int `json:"code"` - Urls struct{ - Clients string `json:"clients"` - Streams string `json:"streams"` - Sessions string `json:"sessions"` - Dvrs string `json:"dvrs"` - Chats string `json:"chats"` - Servers struct{ - Summary string `json:"summary"` - Get string `json:"GET"` - Post string `json:"POST ip=node_ip&device_id=device_id"` - } - } `json:"urls"` - }{ - Code: 0, - } - res.Urls.Clients = "for srs http callback, to handle the clients requests: connect/disconnect vhost/app." - res.Urls.Streams = "for srs http callback, to handle the streams requests: publish/unpublish stream." - res.Urls.Sessions = "for srs http callback, to handle the sessions requests: client play/stop stream." - res.Urls.Dvrs = "for srs http callback, to handle the dvr requests: dvr stream." - //res.Urls.Chats = "for srs demo meeting, the chat streams, public chat room." - res.Urls.Servers.Summary = "for srs raspberry-pi and meeting demo." - res.Urls.Servers.Get = "get the current raspberry-pi servers info." - res.Urls.Servers.Post = "the new raspberry-pi server info." - // TODO: no snapshots - body, _ := json.Marshal(res) - writer.Write(body) - }) - - http.HandleFunc("/api/v1/clients", ClientServe) - http.HandleFunc("/api/v1/streams", StreamServe) - http.HandleFunc("/api/v1/sessions", SessionServe) - http.HandleFunc("/api/v1/dvrs", DvrServe) - http.HandleFunc("/api/v1/hls", HlsServe) - http.HandleFunc("/api/v1/hls/", HlsServe) - http.HandleFunc("/api/v1/proxy/", ProxyServe) - - // not support yet - http.HandleFunc("/api/v1/chat", ChatServe) - - http.HandleFunc("/api/v1/servers", ServerServe) - http.HandleFunc("/api/v1/servers/", ServerServe) - http.HandleFunc("/api/v1/snapshots", SnapshotServe) - http.HandleFunc("/api/v1/forward", ForwardServe) - - addr := fmt.Sprintf(":%v", port) - log.Println(fmt.Sprintf("start listen on:%v", addr)) - if err := http.ListenAndServe(addr, nil); err != nil { - log.Println(fmt.Sprintf("listen on addr:%v failed, err is %v", addr, err)) - } -} \ No newline at end of file diff --git a/trunk/research/api-server/src/proxy.go b/trunk/research/api-server/src/proxy.go deleted file mode 100644 index 68bfb5633d..0000000000 --- a/trunk/research/api-server/src/proxy.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* - for SRS hook: on_hls_notify - on_hls_notify: - when srs reap a ts file of hls, call this hook, - used to push file to cdn network, by get the ts file from cdn network. - so we use HTTP GET and use the variable following: - [app], replace with the app. - [stream], replace with the stream. - [param], replace with the param. - [ts_url], replace with the ts url. - ignore any return data of server. -*/ - -type Proxy struct { - proxyUrl string -} - -func (v *Proxy) Serve(notifyPath string) (se *SrsError) { - v.proxyUrl = fmt.Sprintf("http://%v", notifyPath) - log.Println(fmt.Sprintf("start to proxy url:%v", v.proxyUrl)) - resp, err := http.Get(v.proxyUrl) - if err != nil { - return &SrsError{error_http_request_failed, fmt.Sprintf("get %v failed, err is %v", v.proxyUrl, err)} - } - defer resp.Body.Close() - if _, err = ioutil.ReadAll(resp.Body); err != nil { - return &SrsError{error_system_read_request, fmt.Sprintf("read proxy body failed, err is %v", err)} - } - log.Println(fmt.Sprintf("completed proxy url:%v", v.proxyUrl)) - return nil -} - -// handle the hls proxy requests: hls stream. -func ProxyServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - subPath := r.URL.Path[len("/api/v1/proxy/"):] - c := &Proxy{} - if se := c.Serve(subPath); se != nil { - Response(se).ServeHTTP(w, r) - return - } - w.Write([]byte(c.proxyUrl)) - } -} diff --git a/trunk/research/api-server/src/server.go b/trunk/research/api-server/src/server.go deleted file mode 100644 index b847c01e62..0000000000 --- a/trunk/research/api-server/src/server.go +++ /dev/null @@ -1,138 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" - "strings" - "sync" - "time" -) - -/* -the server list -*/ - -type ServerMsg struct { - Ip string `json:"ip"` - DeviceId string `json:"device_id"` - Summaries interface{} `json:"summaries"` - Devices interface{} `json:"devices"` //not used now -} - -type ArmServer struct { - Id string `json:"id"` - ServerMsg - PublicIp string `json:"public_ip"` - Heartbeat int64 `json:"heartbeat"` - HeartbeatH string `json:"heartbeat_h"` - Api string `json:"api"` - Console string `json:"console"` -} - -func (v *ArmServer) Dead() bool { - deadTimeSeconds := int64(20) - if time.Now().Unix() - v.Heartbeat > deadTimeSeconds { - return true - } - return false -} - -type ServerManager struct { - globalArmServerId int - nodes *sync.Map // key is deviceId - lastUpdateAt time.Time -} - -func NewServerManager() *ServerManager { - sm := &ServerManager{ - globalArmServerId: os.Getpid(), - nodes: new(sync.Map), - lastUpdateAt: time.Now(), - } - return sm -} - -func (v *ServerManager) List(id string) (nodes []*ArmServer) { - nodes = []*ArmServer{} - // list nodes, remove dead node - v.nodes.Range(func(key, value any) bool { - node, _ := value.(*ArmServer) - if node.Dead() { - v.nodes.Delete(key) - return true - } - if len(id) == 0 { - nodes = append(nodes, node) - return true - } - if id == node.Id || id == node.DeviceId { - nodes = append(nodes, node) - return true - } - return true - }) - return -} - -func (v *ServerManager) Parse(body []byte, r *http.Request) (se *SrsError) { - msg := &ServerMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse server msg failed, err is %v", err.Error())} - } - - var node *ArmServer - value, ok := v.nodes.Load(msg.DeviceId) - if !ok { - node = &ArmServer{} - node.ServerMsg = *msg - node.Id = strconv.Itoa(v.globalArmServerId) - v.globalArmServerId += 1 - } else { - node = value.(*ArmServer) - if msg.Summaries != nil { - node.Summaries = msg.Summaries - } - if msg.Devices != nil { - node.Devices = msg.Devices - } - } - node.PublicIp = r.RemoteAddr - now := time.Now() - node.Heartbeat = now.Unix() - node.HeartbeatH = now.Format("2006-01-02 15:04:05") - v.nodes.Store(msg.DeviceId, node) - return nil -} - -func ServerServe(w http.ResponseWriter, r *http.Request) { - uPath := r.URL.Path - if r.Method == "GET" { - index := strings.Index(uPath, "/api/v1/servers/") - if index == -1 { - Response(&SrsError{Code: 0, Data: sm.List("")}).ServeHTTP(w, r) - } else { - id := uPath[(index + len("/api/v1/servers/")):] - Response(&SrsError{Code: 0, Data: sm.List(id)}).ServeHTTP(w, r) - } - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to nodes, req=%v", string(body))) - - if se := sm.Parse(body, r); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } - -} \ No newline at end of file diff --git a/trunk/research/api-server/src/session.go b/trunk/research/api-server/src/session.go deleted file mode 100644 index 251901505c..0000000000 --- a/trunk/research/api-server/src/session.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* - for SRS hook: on_play/on_stop - on_play: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_play", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "pageUrl": "http://www.test.com/live.html" - } - on_stop: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_stop", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 -*/ - -type SessionMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` -} - -type SessionOnPlayMsg struct { - SessionMsg - PageUrl string `json:"pageUrl"` -} - -func (v *SessionOnPlayMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.PageUrl) -} - -type SessionOnStopMsg struct { - SessionMsg -} - -func (v *SessionOnStopMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) -} - -type Session struct {} - -func (v *Session) Parse(body []byte) (se *SrsError) { - data := &struct { - Action string `json:"action"` - }{} - if err := json.Unmarshal(body, data); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session action failed, err is %v", err.Error())} - } - - if data.Action == session_action_on_play { - msg := &SessionOnPlayMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} - } - log.Println(msg) - } else if data.Action == session_action_on_stop { - msg := &SessionOnStopMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} - } - log.Println(msg) - } - return nil -} - -// handle the sessions requests: client play/stop stream -func SessionServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) - c := &Session{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} diff --git a/trunk/research/api-server/src/snapshot.go b/trunk/research/api-server/src/snapshot.go deleted file mode 100644 index fc9fe53f73..0000000000 --- a/trunk/research/api-server/src/snapshot.go +++ /dev/null @@ -1,195 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "os/exec" - "path" - "sync" - "time" -) - -/* -the snapshot api, -to start a snapshot when encoder start publish stream, -stop the snapshot worker when stream finished. - -{"action":"on_publish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} -{"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} -*/ - -type SnapShot struct {} - -func (v *SnapShot) Parse(body []byte) (se *SrsError) { - msg := &StreamMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse snapshot msg failed, err is %v", err.Error())} - } - if msg.Action == "on_publish" { - sw.Create(msg) - return &SrsError{Code: 0, Data: nil} - } else if msg.Action == "on_unpublish" { - sw.Destroy(msg) - return &SrsError{Code: 0, Data: nil} - } else { - return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid req action:%v", msg.Action)} - } -} - -func SnapshotServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to snapshot, req=%v", string(body))) - s := &SnapShot{} - if se := s.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - } -} - -type SnapshotJob struct { - StreamMsg - cmd *exec.Cmd - abort bool - timestamp time.Time - lock *sync.RWMutex -} - -func NewSnapshotJob() *SnapshotJob { - v := &SnapshotJob{ - lock: new(sync.RWMutex), - } - return v -} - -func (v *SnapshotJob) UpdateAbort(status bool) { - v.lock.Lock() - defer v.lock.Unlock() - v.abort = status -} - -func (v *SnapshotJob) IsAbort() bool { - v.lock.RLock() - defer v.lock.RUnlock() - return v.abort -} - -type SnapshotWorker struct { - snapshots *sync.Map // key is stream url - ffmpegPath string -} - -func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { - sw := &SnapshotWorker{ - snapshots: new(sync.Map), - ffmpegPath: ffmpegPath, - } - return sw -} - -/* -./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live...vhost...__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y /Users/mengxiaowei/jdcloud/mt/srs/trunk/research/api-server/static-dir/live/panda-%03d.png -*/ - -func (v *SnapshotWorker) Serve() { - for { - time.Sleep(time.Second) - v.snapshots.Range(func(key, value any) bool { - // range each snapshot job - streamUrl := key.(string) - sj := value.(*SnapshotJob) - streamTag := fmt.Sprintf("%v/%v/%v", sj.Vhost, sj.App, sj.Stream) - if sj.IsAbort() { // delete aborted snapshot job - if sj.cmd != nil && sj.cmd.Process != nil { - if err := sj.cmd.Process.Kill(); err != nil { - log.Println(fmt.Sprintf("snapshot job:%v kill running cmd failed, err is %v", streamTag, err)) - } - } - v.snapshots.Delete(key) - return true - } - - if sj.cmd == nil { // start a ffmpeg snap cmd - outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream) + "-%03d.png") - bestPng := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-best.png", sj.Stream)) - if err := os.MkdirAll(path.Base(outputDir), 0777); err != nil { - log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", path.Base(outputDir), err)) - return true - } - vframes := 5 - param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", v.ffmpegPath, streamUrl, vframes, outputDir) - timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30) * time.Second) - cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) - if err := cmd.Start(); err != nil { - log.Println(fmt.Sprintf("start snapshot %v cmd failed, err is %v", streamTag, err)) - return true - } - sj.cmd = cmd - log.Println(fmt.Sprintf("start snapshot success, cmd param=%v", param)) - go func() { - if err := sj.cmd.Wait(); err != nil { - log.Println(fmt.Sprintf("snapshot %v cmd wait failed, err is %v", streamTag, err)) - } else { // choose the best quality image - bestFileSize := int64(0) - for i := 1; i <= vframes; i ++ { - pic := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-%03d.png", sj.Stream, i)) - fi, err := os.Stat(pic) - if err != nil { - log.Println(fmt.Sprintf("stat pic:%v failed, err is %v", pic, err)) - continue - } - if bestFileSize == 0 { - bestFileSize = fi.Size() - } else if fi.Size() > bestFileSize { - os.Remove(bestPng) - os.Link(pic, bestPng) - bestFileSize = fi.Size() - } - } - log.Println(fmt.Sprintf("%v the best thumbnail is %v", streamTag, bestPng)) - } - sj.cmd = nil - }() - } else { - log.Println(fmt.Sprintf("snapshot %v cmd process is running, status=%v", streamTag, sj.cmd.ProcessState)) - } - return true - }) - } -} - -func (v *SnapshotWorker) Create(sm *StreamMsg) { - streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) - if _, ok := v.snapshots.Load(streamUrl); ok { - return - } - sj := NewSnapshotJob() - sj.StreamMsg = *sm - sj.timestamp = time.Now() - v.snapshots.Store(streamUrl, sj) -} - -func (v *SnapshotWorker) Destroy(sm *StreamMsg) { - streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) - value, ok := v.snapshots.Load(streamUrl) - if ok { - sj := value.(*SnapshotJob) - sj.UpdateAbort(true) - v.snapshots.Store(streamUrl, sj) - log.Println(fmt.Sprintf("set stream:%v to destroy, update abort", sm.Stream)) - } else { - log.Println(fmt.Sprintf("cannot find stream:%v in snapshot worker", streamUrl)) - } - return -} diff --git a/trunk/research/api-server/src/stream.go b/trunk/research/api-server/src/stream.go deleted file mode 100644 index 826a01732e..0000000000 --- a/trunk/research/api-server/src/stream.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" -) - -/* - for SRS hook: on_publish/on_unpublish - on_publish: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_publish", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - on_unpublish: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_unpublish", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 -*/ -type StreamMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` -} - -func (v *StreamMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) -} - -type Stream struct {} - -func (v *Stream) Parse(body []byte) (se *SrsError) { - msg := &StreamMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse stream msg failed, err is %v", err.Error())} - } - log.Println(msg) - return nil -} - -// handle the streams requests: publish/unpublish stream. - -func StreamServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) - c := &Stream{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} From eb764b8ad6d9fb83c9a383b8caf404ea4e1c89bf Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 12:46:22 +0800 Subject: [PATCH 03/19] Remove py and fix Go any error. --- trunk/research/api-server/server.go | 4 +- trunk/research/api-server/server.py | 1132 --------------------------- 2 files changed, 2 insertions(+), 1134 deletions(-) delete mode 100755 trunk/research/api-server/server.py diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index a3f46a6cc1..7ba8882462 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -578,7 +578,7 @@ func NewChatManager() *ChatManager { func (v *ChatManager) List() (chats []*Chat) { chats = []*Chat{} - v.chats.Range(func(key, value any) bool { + v.chats.Range(func(key, value interface{}) bool { _, chat := key.(int), value.(*Chat) if (time.Now().Unix() - chat.Heartbeat) > int64(v.deadTime) { v.chats.Delete(key) @@ -739,7 +739,7 @@ func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { func (v *SnapshotWorker) Serve() { for { time.Sleep(time.Second) - v.snapshots.Range(func(key, value any) bool { + v.snapshots.Range(func(key, value interface{}) bool { // range each snapshot job streamUrl := key.(string) sj := value.(*SnapshotJob) diff --git a/trunk/research/api-server/server.py b/trunk/research/api-server/server.py deleted file mode 100755 index 382c90f691..0000000000 --- a/trunk/research/api-server/server.py +++ /dev/null @@ -1,1132 +0,0 @@ -#!/usr/bin/python - -# -# Copyright (c) 2013-2021 Winlin -# -# SPDX-License-Identifier: MIT -# - -""" -the api-server is a default demo server for srs to call -when srs get some event, for example, when client connect -to srs, srs can invoke the http api of the api-server -""" - -import sys -# reload sys model to enable the getdefaultencoding method. -reload(sys) -# set the default encoding to utf-8 -# using exec to set the encoding, to avoid error in IDE. -exec("sys.setdefaultencoding('utf-8')") -assert sys.getdefaultencoding().lower() == "utf-8" - -import os, json, time, datetime, cherrypy, threading, urllib2, shlex, subprocess -import cherrypy.process.plugins - -# simple log functions. -def trace(msg): - date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print "[%s][trace] %s"%(date, msg) - -# enable crossdomain access for js-client -# define the following method: -# def OPTIONS(self, *args, **kwargs) -# enable_crossdomain() -# invoke this method to enable js to request crossdomain. -def enable_crossdomain(): - cherrypy.response.headers["Access-Control-Allow-Origin"] = "*" - cherrypy.response.headers["Access-Control-Allow-Methods"] = "GET, POST, HEAD, PUT, DELETE" - # generate allow headers for crossdomain. - allow_headers = ["Cache-Control", "X-Proxy-Authorization", "X-Requested-With", "Content-Type"] - cherrypy.response.headers["Access-Control-Allow-Headers"] = ",".join(allow_headers) - -# error codes definition -class Error: - # ok, success, completed. - success = 0 - # error when parse json - system_parse_json = 100 - # request action invalid - request_invalid_action = 200 - # cdn node not exists - cdn_node_not_exists = 201 - -''' -handle the clients requests: connect/disconnect vhost/app. -''' -class RESTClients(object): - exposed = True - - def GET(self): - enable_crossdomain() - - clients = {} - return json.dumps(clients) - - ''' - for SRS hook: on_connect/on_close - on_connect: - when client connect to vhost/app, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_connect", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", - "pageUrl": "http://www.test.com/live.html" - } - on_close: - when client close/disconnect to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_close", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "send_bytes": 10240, "recv_bytes": 10240 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to clients, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_connect": - code = self.__on_connect(json_req) - elif action == "on_close": - code = self.__on_close(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_connect(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, tcUrl=%s, pageUrl=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["tcUrl"], req["pageUrl"] - )) - - # TODO: process the on_connect event - - return code - - def __on_close(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, send_bytes=%s, recv_bytes=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["send_bytes"], req["recv_bytes"] - )) - - # TODO: process the on_close event - - return code - -''' -handle the streams requests: publish/unpublish stream. -''' -class RESTStreams(object): - exposed = True - - def GET(self): - enable_crossdomain() - - streams = {} - return json.dumps(streams) - - ''' - for SRS hook: on_publish/on_unpublish - on_publish: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_publish", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy" - } - on_unpublish: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_unpublish", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to streams, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_publish": - code = self.__on_publish(json_req) - elif action == "on_unpublish": - code = self.__on_unpublish(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_publish(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"] - )) - - # TODO: process the on_publish event - - return code - - def __on_unpublish(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"] - )) - - # TODO: process the on_unpublish event - - return code - -''' -handle the dvrs requests: dvr stream. -''' -class RESTDvrs(object): - exposed = True - - def GET(self): - enable_crossdomain() - - dvrs = {} - return json.dumps(dvrs) - - ''' - for SRS hook: on_dvr - on_dvr: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_dvr", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy", - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776.flv" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to dvrs, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_dvr": - code = self.__on_dvr(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_dvr(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s, cwd=%s, file=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"], - req["cwd"], req["file"] - )) - - # TODO: process the on_dvr event - - return code - - -''' -handle the hls proxy requests: hls stream. -''' -class RESTProxy(object): - exposed = True - - ''' - for SRS hook: on_hls_notify - on_hls_notify: - when srs reap a ts file of hls, call this hook, - used to push file to cdn network, by get the ts file from cdn network. - so we use HTTP GET and use the variable following: - [app], replace with the app. - [stream], replace with the stream. - [param], replace with the param. - [ts_url], replace with the ts url. - ignore any return data of server. - ''' - def GET(self, *args, **kwargs): - enable_crossdomain() - - url = "http://" + "/".join(args); - print "start to proxy url: %s"%url - - f = None - try: - f = urllib2.urlopen(url) - f.read() - except: - print "error proxy url: %s"%url - finally: - if f: f.close() - print "completed proxy url: %s"%url - return url - -''' -handle the hls requests: hls stream. -''' -class RESTHls(object): - exposed = True - - ''' - for SRS hook: on_hls_notify - on_hls_notify: - when srs reap a ts file of hls, call this hook, - used to push file to cdn network, by get the ts file from cdn network. - so we use HTTP GET and use the variable following: - [app], replace with the app. - [stream], replace with the stream. - [param], replace with the param. - [ts_url], replace with the ts url. - ignore any return data of server. - ''' - def GET(self, *args, **kwargs): - enable_crossdomain() - - hls = { - "args": args, - "kwargs": kwargs - } - return json.dumps(hls) - - ''' - for SRS hook: on_hls - on_hls: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_dvr", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy", - "duration": 9.68, // in seconds - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", - "seq_no": 100 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to hls, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_hls": - code = self.__on_hls(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_hls(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s, duration=%s, cwd=%s, file=%s, seq_no=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"], req["duration"], - req["cwd"], req["file"], req["seq_no"] - )) - - # TODO: process the on_hls event - - return code - -''' -handle the sessions requests: client play/stop stream -''' -class RESTSessions(object): - exposed = True - - def GET(self): - enable_crossdomain() - - sessions = {} - return json.dumps(sessions) - - ''' - for SRS hook: on_play/on_stop - on_play: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_play", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy", - "pageUrl": "http://www.test.com/live.html" - } - on_stop: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_stop", - "client_id": "9308h583", - "ip": "192.168.1.10", "vhost": "video.test.com", "app": "live", - "stream": "livestream", "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to sessions, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_play": - code = self.__on_play(json_req) - elif action == "on_stop": - code = self.__on_stop(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_play(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s, pageUrl=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"], req["pageUrl"] - )) - - # TODO: process the on_play event - - return code - - def __on_stop(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, stream=%s, param=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["stream"], req["param"] - )) - - # TODO: process the on_stop event - - return code - -global_arm_server_id = os.getpid(); -class ArmServer: - def __init__(self): - global global_arm_server_id - global_arm_server_id += 1 - - self.id = str(global_arm_server_id) - self.ip = None - self.device_id = None - self.summaries = None - self.devices = None - - self.public_ip = cherrypy.request.remote.ip - self.heartbeat = time.time() - - self.clients = 0 - - def dead(self): - dead_time_seconds = 20 - if time.time() - self.heartbeat > dead_time_seconds: - return True - return False - - def json_dump(self): - data = {} - data["id"] = self.id - data["ip"] = self.ip - data["device_id"] = self.device_id - data["summaries"] = self.summaries - data["devices"] = self.devices - data["public_ip"] = self.public_ip - data["heartbeat"] = self.heartbeat - data["heartbeat_h"] = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(self.heartbeat)) - data["api"] = "http://%s:1985/api/v1/summaries"%(self.ip) - data["console"] = "http://ossrs.net/console/ng_index.html#/summaries?host=%s&port=1985"%(self.ip) - return data - -''' -the server list -''' -class RESTServers(object): - exposed = True - - def __init__(self): - self.__nodes = [] - - self.__last_update = datetime.datetime.now(); - - self.__lock = threading.Lock() - - def __get_node(self, device_id): - for node in self.__nodes: - if node.device_id == device_id: - return node - return None - - def __refresh_nodes(self): - while len(self.__nodes) > 0: - has_dead_node = False - for node in self.__nodes: - if node.dead(): - self.__nodes.remove(node) - has_dead_node = True - if not has_dead_node: - break - - ''' - post to update server ip. - request body: the new raspberry-pi server ip. TODO: FIXME: more info. - ''' - def POST(self): - enable_crossdomain() - - try: - self.__lock.acquire() - - req = cherrypy.request.body.read() - trace("post to nodes, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code":code, "data": None}) - - device_id = json_req["device_id"] - node = self.__get_node(device_id) - if node is None: - node = ArmServer() - self.__nodes.append(node) - - node.ip = json_req["ip"] - if "summaries" in json_req: - node.summaries = json_req["summaries"] - if "devices" in json_req: - node.devices = json_req["devices"] - node.device_id = device_id - node.public_ip = cherrypy.request.remote.ip - node.heartbeat = time.time() - - return json.dumps({"code":Error.success, "data": {"id":node.id}}) - finally: - self.__lock.release() - - ''' - get all servers which report to this api-server. - ''' - def GET(self, id=None): - enable_crossdomain() - - try: - self.__lock.acquire() - - self.__refresh_nodes() - - data = [] - for node in self.__nodes: - if id == None or node.id == str(id) or node.device_id == str(id): - data.append(node.json_dump()) - - return json.dumps(data) - finally: - self.__lock.release() - - def DELETE(self, id): - enable_crossdomain() - raise cherrypy.HTTPError(405, "Not allowed.") - - def PUT(self, id): - enable_crossdomain() - raise cherrypy.HTTPError(405, "Not allowed.") - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - -global_chat_id = os.getpid(); -''' -the chat streams, public chat room. -''' -class RESTChats(object): - exposed = True - global_id = 100 - - def __init__(self): - # object fields: - # id: an int value indicates the id of user. - # username: a str indicates the user name. - # url: a str indicates the url of user stream. - # agent: a str indicates the agent of user. - # join_date: a number indicates the join timestamp in seconds. - # join_date_str: a str specifies the formated friendly time. - # heatbeat: a number indicates the heartbeat timestamp in seconds. - # vcodec: a dict indicates the video codec info. - # acodec: a dict indicates the audio codec info. - self.__chats = []; - self.__chat_lock = threading.Lock(); - - # dead time in seconds, if exceed, remove the chat. - self.__dead_time = 15; - - ''' - get the rtmp url of chat object. None if overflow. - ''' - def get_url_by_index(self, index): - index = int(index) - if index is None or index >= len(self.__chats): - return None; - return self.__chats[index]["url"]; - - def GET(self): - enable_crossdomain() - - try: - self.__chat_lock.acquire(); - - chats = []; - copy = self.__chats[:]; - for chat in copy: - if time.time() - chat["heartbeat"] > self.__dead_time: - self.__chats.remove(chat); - continue; - - chats.append({ - "id": chat["id"], - "username": chat["username"], - "url": chat["url"], - "join_date_str": chat["join_date_str"], - "heartbeat": chat["heartbeat"], - }); - finally: - self.__chat_lock.release(); - - return json.dumps({"code":0, "data": {"now": time.time(), "chats": chats}}) - - def POST(self): - enable_crossdomain() - - req = cherrypy.request.body.read() - chat = json.loads(req) - - global global_chat_id; - chat["id"] = global_chat_id - global_chat_id += 1 - - chat["join_date"] = time.time(); - chat["heartbeat"] = time.time(); - chat["join_date_str"] = time.strftime("%Y-%m-%d %H:%M:%S"); - - try: - self.__chat_lock.acquire(); - - self.__chats.append(chat) - finally: - self.__chat_lock.release(); - - trace("create chat success, id=%s"%(chat["id"])) - - return json.dumps({"code":0, "data": chat["id"]}) - - def DELETE(self, id): - enable_crossdomain() - - try: - self.__chat_lock.acquire(); - - for chat in self.__chats: - if str(id) != str(chat["id"]): - continue - - self.__chats.remove(chat) - trace("delete chat success, id=%s"%(id)) - - return json.dumps({"code":0, "data": None}) - finally: - self.__chat_lock.release(); - - raise cherrypy.HTTPError(405, "Not allowed.") - - def PUT(self, id): - enable_crossdomain() - - try: - self.__chat_lock.acquire(); - - for chat in self.__chats: - if str(id) != str(chat["id"]): - continue - - chat["heartbeat"] = time.time(); - trace("heartbeat chat success, id=%s"%(id)) - - return json.dumps({"code":0, "data": None}) - finally: - self.__chat_lock.release(); - - raise cherrypy.HTTPError(405, "Not allowed.") - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - -''' -the snapshot api, -to start a snapshot when encoder start publish stream, -stop the snapshot worker when stream finished. -''' -class RESTSnapshots(object): - exposed = True - - def __init__(self): - pass - - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to streams, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_publish": - code = worker.snapshot_create(json_req) - elif action == "on_unpublish": - code = worker.snapshot_destroy(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - -''' -handle the forward requests: dynamic forward url. -''' -class RESTForward(object): - exposed = True - - def __init__(self): - self.__forwards = [] - - def GET(self): - enable_crossdomain() - - forwards = {} - return json.dumps(forwards) - - ''' - for SRS hook: on_forward - on_forward: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_forward", - "server_id": "server_test", - "client_id": 1985, - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 - ''' - def POST(self): - enable_crossdomain() - - # return the error code in str - code = Error.success - - req = cherrypy.request.body.read() - trace("post to forwards, req=%s"%(req)) - try: - json_req = json.loads(req) - except Exception, ex: - code = Error.system_parse_json - trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code)) - return json.dumps({"code": int(code), "data": None}) - - action = json_req["action"] - if action == "on_forward": - return self.__on_forward(json_req) - else: - trace("invalid request action: %s"%(json_req["action"])) - code = Error.request_invalid_action - - return json.dumps({"code": int(code), "data": None}) - - def OPTIONS(self, *args, **kwargs): - enable_crossdomain() - - def __on_forward(self, req): - code = Error.success - - trace("srs %s: client id=%s, ip=%s, vhost=%s, app=%s, tcUrl=%s, stream=%s, param=%s"%( - req["action"], req["client_id"], req["ip"], req["vhost"], req["app"], req["tcUrl"], req["stream"], req["param"] - )) - - ''' - backend service config description: - support multiple rtmp urls(custom addresses or third-party cdn service), - url's host is slave service. - For example: - ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] - ''' - forwards = ["rtmp://127.0.0.1:19350/test/teststream"] - - return json.dumps({"code": int(code), "data": {"urls": forwards}}) - -# HTTP RESTful path. -class Root(object): - exposed = True - - def __init__(self): - self.api = Api() - def GET(self): - enable_crossdomain(); - return json.dumps({"code":Error.success, "urls":{"api":"the api root"}}) - def OPTIONS(self, *args, **kwargs): - enable_crossdomain(); -# HTTP RESTful path. -class Api(object): - exposed = True - - def __init__(self): - self.v1 = V1() - def GET(self): - enable_crossdomain(); - return json.dumps({"code":Error.success, - "urls": { - "v1": "the api version 1.0" - } - }); - def OPTIONS(self, *args, **kwargs): - enable_crossdomain(); -# HTTP RESTful path. to access as: -# http://127.0.0.1:8085/api/v1/clients -class V1(object): - exposed = True - - def __init__(self): - self.clients = RESTClients() - self.streams = RESTStreams() - self.sessions = RESTSessions() - self.dvrs = RESTDvrs() - self.hls = RESTHls() - self.proxy = RESTProxy() - self.chats = RESTChats() - self.servers = RESTServers() - self.snapshots = RESTSnapshots() - self.forward = RESTForward() - def GET(self): - enable_crossdomain(); - return json.dumps({"code":Error.success, "urls":{ - "clients": "for srs http callback, to handle the clients requests: connect/disconnect vhost/app.", - "streams": "for srs http callback, to handle the streams requests: publish/unpublish stream.", - "sessions": "for srs http callback, to handle the sessions requests: client play/stop stream", - "dvrs": "for srs http callback, to handle the dvr requests: dvr stream.", - "chats": "for srs demo meeting, the chat streams, public chat room.", - "servers": { - "summary": "for srs raspberry-pi and meeting demo", - "GET": "get the current raspberry-pi servers info", - "POST ip=node_ip&device_id=device_id": "the new raspberry-pi server info." - } - }}); - def OPTIONS(self, *args, **kwargs): - enable_crossdomain(); - -''' -main code start. -''' -# donot support use this module as library. -if __name__ != "__main__": - raise Exception("embed not support") - -# check the user options -if len(sys.argv) <= 1: - print "SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs)" - print "Usage: python %s "%(sys.argv[0]) - print " port: the port to listen at." - print "For example:" - print " python %s 8085"%(sys.argv[0]) - print "" - print "See also: https://github.com/ossrs/srs" - sys.exit(1) - -# parse port from user options. -port = int(sys.argv[1]) -static_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "static-dir")) -trace("api server listen at port: %s, static_dir: %s"%(port, static_dir)) - - -discard = open("/dev/null", "rw") -''' -create process by specifies command. -@param command the command str to start the process. -@param stdout_fd an int fd specifies the stdout fd. -@param stderr_fd an int fd specifies the stderr fd. -@param log_file a file object specifies the additional log to write to. ignore if None. -@return a Popen object created by subprocess.Popen(). -''' -def create_process(command, stdout_fd, stderr_fd): - # log the original command - msg = "process start command: %s"%(command); - - # to avoid shell injection, directly use the command, no need to filter. - args = shlex.split(str(command)); - process = subprocess.Popen(args, stdout=stdout_fd, stderr=stderr_fd); - - return process; -''' -isolate thread for srs worker, to do some job in background, -for example, to snapshot thumbnail of RTMP stream. -''' -class SrsWorker(cherrypy.process.plugins.SimplePlugin): - def __init__(self, bus): - cherrypy.process.plugins.SimplePlugin.__init__(self, bus); - self.__snapshots = {} - - def start(self): - print "srs worker thread started" - - def stop(self): - print "srs worker thread stopped" - - def main(self): - for url in self.__snapshots: - snapshot = self.__snapshots[url] - - diff = time.time() - snapshot['timestamp'] - process = snapshot['process'] - - # aborted. - if process is not None and snapshot['abort']: - process.kill() - process.poll() - del self.__snapshots[url] - print 'abort snapshot %s'%snapshot['cmd'] - break - - # how many snapshots to output. - vframes = 5 - # the expire in seconds for ffmpeg to snapshot. - expire = 1 - # the timeout to kill ffmpeg. - kill_ffmpeg_timeout = 30 * expire - # the ffmpeg binary path - ffmpeg = "./objs/ffmpeg/bin/ffmpeg" - # the best url for thumbnail. - besturl = os.path.join(static_dir, "%s/%s-best.png"%(snapshot['app'], snapshot['stream'])) - # the lambda to generate the thumbnail with index. - lgo = lambda dir, app, stream, index: os.path.join(dir, "%s/%s-%03d.png"%(app, stream, index)) - # the output for snapshot command - output = os.path.join(static_dir, "%s/%s-%%03d.png"%(snapshot['app'], snapshot['stream'])) - # the ffmepg command to snapshot - cmd = '%s -i %s -vf fps=1 -vcodec png -f image2 -an -y -vframes %s -y %s'%(ffmpeg, url, vframes, output) - - # already snapshoted and not expired. - if process is not None and diff < expire: - continue - - # terminate the active process - if process is not None: - # the poll will set the process.returncode - process.poll() - - # None incidates the process hasn't terminate yet. - if process.returncode is not None: - # process terminated with error. - if process.returncode != 0: - print 'process terminated with error=%s, cmd=%s'%(process.returncode, snapshot['cmd']) - # process terminated normally. - else: - # guess the best one. - bestsize = 0 - for i in range(0, vframes): - output = lgo(static_dir, snapshot['app'], snapshot['stream'], i + 1) - fsize = os.path.getsize(output) - if bestsize < fsize: - os.system("rm -f '%s'"%besturl) - os.system("ln -sf '%s' '%s'"%(output, besturl)) - bestsize = fsize - print 'the best thumbnail is %s'%besturl - else: - # wait for process to terminate, timeout is N*expire. - if diff < kill_ffmpeg_timeout: - continue - # kill the process when user cancel. - else: - process.kill() - print 'kill the process %s'%snapshot['cmd'] - - # create new process to snapshot. - print 'snapshot by: %s'%cmd - - process = create_process(cmd, discard.fileno(), discard.fileno()) - snapshot['process'] = process - snapshot['cmd'] = cmd - snapshot['timestamp'] = time.time() - pass; - - # {"action":"on_publish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} - # ffmpeg -i rtmp://127.0.0.1:1935/live?vhost=dev/stream -vf fps=1 -vcodec png -f image2 -an -y -vframes 3 -y static-dir/live/livestream-%03d.png - def snapshot_create(self, req): - url = "rtmp://127.0.0.1/%s...vhost...%s/%s"%(req['app'], req['vhost'], req['stream']) - if url in self.__snapshots: - print 'ignore exists %s'%url - return Error.success - - req['process'] = None - req['abort'] = False - req['timestamp'] = time.time() - self.__snapshots[url] = req - return Error.success - - # {"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} - def snapshot_destroy(self, req): - url = "rtmp://127.0.0.1/%s...vhost...%s/%s"%(req['app'], req['vhost'], req['stream']) - if url in self.__snapshots: - snapshot = self.__snapshots[url] - snapshot['abort'] = True - return Error.success - -# subscribe the plugin to cherrypy. -worker = SrsWorker(cherrypy.engine) -worker.subscribe(); - -# disable the autoreloader to make it more simple. -cherrypy.engine.autoreload.unsubscribe(); - -# cherrypy config. -conf = { - 'global': { - 'server.shutdown_timeout': 3, - 'server.socket_host': '0.0.0.0', - 'server.socket_port': port, - 'tools.encode.on': True, - 'tools.staticdir.on': True, - 'tools.encode.encoding': "utf-8", - #'server.thread_pool': 2, # single thread server. - }, - '/': { - 'tools.staticdir.dir': static_dir, - 'tools.staticdir.index': "index.html", - # for cherrypy RESTful api support - 'request.dispatch': cherrypy.dispatch.MethodDispatcher() - } -} - -# start cherrypy web engine -trace("start cherrypy server") -root = Root() -cherrypy.quickstart(root, '/', conf) - From 4e5b6ad250cc64820532c20d0cafe4cc0550df65 Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:23:58 +0800 Subject: [PATCH 04/19] Refine clients api for on_connect and on_close. --- trunk/research/api-server/server.go | 143 ++++++++++++++-------------- 1 file changed, 69 insertions(+), 74 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 7ba8882462..ed1915b0a0 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -19,25 +19,16 @@ import ( ) const ( - // ok, success, completed. - success = 0 // error when read http request error_system_read_request = 100 // error when parse json error_system_parse_json = 101 // request action invalid error_request_invalid_action = 200 - // cdn node not exists - error_cdn_node_not_exists = 201 - // http request failed - error_http_request_failed = 202 // chat id not exist error_chat_id_not_exist = 300 - // - client_action_on_connect = "on_connect" - client_action_on_close = "on_close" session_action_on_play = "on_play" session_action_on_stop = "on_stop" ) @@ -59,6 +50,27 @@ func Response(se *SrsError) http.Handler { }) } +type SrsCommonResponse struct { + Code int `json:"code"` + Data interface{} `json:"data"` +} + +func WriteErrorResponse(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) +} + +func WriteDataResponse(w http.ResponseWriter, data interface{}) { + j, err := json.Marshal(data) + if err != nil { + WriteErrorResponse(w, fmt.Errorf("marshal %v, err %v", err)) + return + } + + w.Header().Set("Content-Type", HttpJson) + w.Write(j) +} + const Example = ` SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) Example: @@ -70,6 +82,13 @@ var StaticDir string var cm *ChatManager var sw *SnapshotWorker +type CommonRequest struct { + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` +} /* handle the clients requests: connect/disconnect vhost/app. @@ -103,83 +122,33 @@ handle the clients requests: connect/disconnect vhost/app. 0 */ type ClientMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` -} - -type ClientOnConnectMsg struct { - ClientMsg + CommonRequest + // For on_connect message TcUrl string `json:"tcUrl"` PageUrl string `json:"pageUrl"` -} - -func (v *ClientOnConnectMsg) String() string { - return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.PageUrl) -} - -type ClientOnCloseMsg struct { - ClientMsg + // For on_close message SendBytes int64 `json:"send_bytes"` RecvBytes int64 `json:"recv_bytes"` } -func (v *ClientOnCloseMsg) String() string { - return fmt.Sprintf("srs:%v, client id=%v, ip=%v, vhost=%v, app=%v, send_bytes=%v, recv_bytes=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.SendBytes, v.RecvBytes) -} - -type Client struct {} - -func (v *Client) Parse(body []byte) (se *SrsError) { - data := &struct { - Action string `json:"action"` - }{} - if err := json.Unmarshal(body, data); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client action failed, err is %v", err.Error())} - } - - if data.Action == client_action_on_connect { - msg := &ClientOnConnectMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_connect, err.Error())} - } - log.Println(msg) - } else if data.Action == client_action_on_close { - msg := &ClientOnCloseMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse client %v msg failed, err is %v", client_action_on_close, err.Error())} - } - log.Println(msg) +func (v *ClientMsg) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse action from %v, err %v", string(b), err) } return nil } -// handle the clients requests: connect/disconnect vhost/app. -func ClientServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct {}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) - c := &Client{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return +func (v *ClientMsg) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("action=%v, client_id=%v, ip=%v, vhost=%v", v.Action, v.ClientId, v.Ip, v.Vhost)) + if v.Action == "on_connect" { + sb.WriteString(fmt.Sprintf(", tcUrl=%v, pageUrl=%v", v.TcUrl, v.PageUrl)) + } else if v.Action == "on_close" { + sb.WriteString(fmt.Sprintf(", send_bytes=%v, recv_bytes=%v", v.SendBytes, v.RecvBytes)) } + return sb.String() } - /* for SRS hook: on_publish/on_unpublish on_publish: @@ -979,7 +948,33 @@ func main() { writer.Write(body) }) - http.HandleFunc("/api/v1/clients", ClientServe) + // handle the clients requests: connect/disconnect vhost/app. + http.HandleFunc("/api/v1/clients", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + WriteDataResponse(w, struct {}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) + + msg := &ClientMsg{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + } (); err != nil { + WriteErrorResponse(w, err) + } + }) + http.HandleFunc("/api/v1/streams", StreamServe) http.HandleFunc("/api/v1/sessions", SessionServe) http.HandleFunc("/api/v1/dvrs", DvrServe) From 4ad2bb17d616f79d3fc6de532830b4284706687a Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:28:59 +0800 Subject: [PATCH 05/19] Refine code by gofmt. --- trunk/research/api-server/server.go | 160 ++++++++++++++-------------- 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index ed1915b0a0..c7beac1dd6 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -34,11 +34,11 @@ const ( ) const ( - HttpJson = "application/json" + HttpJson = "application/json" ) type SrsError struct { - Code int `json:"code"` + Code int `json:"code"` Data interface{} `json:"data"` } @@ -51,7 +51,7 @@ func Response(se *SrsError) http.Handler { } type SrsCommonResponse struct { - Code int `json:"code"` + Code int `json:"code"` Data interface{} `json:"data"` } @@ -124,7 +124,7 @@ handle the clients requests: connect/disconnect vhost/app. type ClientMsg struct { CommonRequest // For on_connect message - TcUrl string `json:"tcUrl"` + TcUrl string `json:"tcUrl"` PageUrl string `json:"pageUrl"` // For on_close message SendBytes int64 `json:"send_bytes"` @@ -180,20 +180,20 @@ func (v *ClientMsg) String() string { 0 */ type StreamMsg struct { - Action string `json:"action"` + Action string `json:"action"` ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` } func (v *StreamMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) } -type Stream struct {} +type Stream struct{} func (v *Stream) Parse(body []byte) (se *SrsError) { msg := &StreamMsg{} @@ -208,7 +208,7 @@ func (v *Stream) Parse(body []byte) (se *SrsError) { func StreamServe(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - res := struct {}{} + res := struct{}{} body, _ := json.Marshal(res) w.Write(body) } else if r.Method == "POST" { @@ -228,7 +228,6 @@ func StreamServe(w http.ResponseWriter, r *http.Request) { } } - /* for SRS hook: on_play/on_stop on_play: @@ -262,13 +261,13 @@ func StreamServe(w http.ResponseWriter, r *http.Request) { */ type SessionMsg struct { - Action string `json:"action"` + Action string `json:"action"` ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` } type SessionOnPlayMsg struct { @@ -288,7 +287,7 @@ func (v *SessionOnStopMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) } -type Session struct {} +type Session struct{} func (v *Session) Parse(body []byte) (se *SrsError) { data := &struct { @@ -317,7 +316,7 @@ func (v *Session) Parse(body []byte) (se *SrsError) { // handle the sessions requests: client play/stop stream func SessionServe(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - res := struct {}{} + res := struct{}{} body, _ := json.Marshal(res) w.Write(body) } else if r.Method == "POST" { @@ -337,7 +336,6 @@ func SessionServe(w http.ResponseWriter, r *http.Request) { } } - /* for SRS hook: on_dvr on_dvr: @@ -360,22 +358,22 @@ func SessionServe(w http.ResponseWriter, r *http.Request) { */ type DvrMsg struct { - Action string `json:"action"` + Action string `json:"action"` ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` - Cwd string `json:"cwd"` - File string `json:"file"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` + Cwd string `json:"cwd"` + File string `json:"file"` } func (v *DvrMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, cwd=%v, file=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Cwd, v.File) } -type Dvr struct {} +type Dvr struct{} func (v *Dvr) Parse(body []byte) (se *SrsError) { msg := &DvrMsg{} @@ -389,7 +387,7 @@ func (v *Dvr) Parse(body []byte) (se *SrsError) { // handle the dvrs requests: dvr stream. func DvrServe(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - res := struct {}{} + res := struct{}{} body, _ := json.Marshal(res) w.Write(body) } else if r.Method == "POST" { @@ -409,7 +407,6 @@ func DvrServe(w http.ResponseWriter, r *http.Request) { } } - /* for SRS hook: on_hls_notify on_hls_notify: @@ -445,24 +442,24 @@ func DvrServe(w http.ResponseWriter, r *http.Request) { */ type HlsMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - Stream string `json:"stream"` - Param string `json:"param"` + Action string `json:"action"` + ClientId string `json:"client_id"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + Stream string `json:"stream"` + Param string `json:"param"` Duration float64 `json:"duration"` - Cwd string `json:"cwd"` - File string `json:"file"` - SeqNo int `json:"seq_no"` + Cwd string `json:"cwd"` + File string `json:"file"` + SeqNo int `json:"seq_no"` } func (v *HlsMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, duration=%v, cwd=%v, file=%v, seq_no=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Duration, v.Cwd, v.File, v.SeqNo) } -type Hls struct {} +type Hls struct{} func (v *Hls) Parse(body []byte) (se *SrsError) { msg := &HlsMsg{} @@ -479,10 +476,10 @@ func HlsServe(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { subPath := r.URL.Path[len("/api/v1/hls/"):] res := struct { - Args []string `json:"args"` + Args []string `json:"args"` KwArgs url.Values `json:"kwargs"` }{ - Args: strings.Split(subPath, "/"), + Args: strings.Split(subPath, "/"), KwArgs: r.URL.Query(), } body, _ := json.Marshal(res) @@ -504,7 +501,6 @@ func HlsServe(w http.ResponseWriter, r *http.Request) { } } - /* # object fields: # id: an int value indicates the id of user. @@ -521,17 +517,17 @@ func HlsServe(w http.ResponseWriter, r *http.Request) { */ type Chat struct { - Id int `json:"id"` - Username string `json:"username"` - Url string `json:"url"` - JoinDate int64 `json:"join_date"` + Id int `json:"id"` + Username string `json:"username"` + Url string `json:"url"` + JoinDate int64 `json:"join_date"` JoinDateStr string `json:"join_date_str"` - Heartbeat int64 `json:"heartbeat"` + Heartbeat int64 `json:"heartbeat"` } type ChatManager struct { globalId int - chats *sync.Map + chats *sync.Map deadTime int } @@ -539,7 +535,7 @@ func NewChatManager() *ChatManager { v := &ChatManager{ globalId: 100, // key is globalId, value is chat - chats: new(sync.Map), + chats: new(sync.Map), deadTime: 15, } return v @@ -626,7 +622,7 @@ stop the snapshot worker when stream finished. {"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} */ -type SnapShot struct {} +type SnapShot struct{} func (v *SnapShot) Parse(body []byte) (se *SrsError) { msg := &StreamMsg{} @@ -689,13 +685,13 @@ func (v *SnapshotJob) IsAbort() bool { } type SnapshotWorker struct { - snapshots *sync.Map // key is stream url + snapshots *sync.Map // key is stream url ffmpegPath string } func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { sw := &SnapshotWorker{ - snapshots: new(sync.Map), + snapshots: new(sync.Map), ffmpegPath: ffmpegPath, } return sw @@ -724,7 +720,7 @@ func (v *SnapshotWorker) Serve() { } if sj.cmd == nil { // start a ffmpeg snap cmd - outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream) + "-%03d.png") + outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream)+"-%03d.png") bestPng := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-best.png", sj.Stream)) if err := os.MkdirAll(path.Dir(outputDir), 0777); err != nil { log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", path.Base(outputDir), err)) @@ -732,7 +728,7 @@ func (v *SnapshotWorker) Serve() { } vframes := 5 param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", v.ffmpegPath, streamUrl, vframes, outputDir) - timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30) * time.Second) + timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30)*time.Second) cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) if err := cmd.Start(); err != nil { log.Println(fmt.Sprintf("start snapshot %v cmd failed, err is %v", streamTag, err)) @@ -745,7 +741,7 @@ func (v *SnapshotWorker) Serve() { log.Println(fmt.Sprintf("snapshot %v cmd wait failed, err is %v", streamTag, err)) } else { // choose the best quality image bestFileSize := int64(0) - for i := 1; i <= vframes; i ++ { + for i := 1; i <= vframes; i++ { pic := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-%03d.png", sj.Stream, i)) fi, err := os.Stat(pic) if err != nil { @@ -820,22 +816,22 @@ an int value specifies the error code(0 corresponding to success): */ type ForwardMsg struct { - Action string `json:"action"` + Action string `json:"action"` ServerId string `json:"server_id"` ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` - TcUrl string `json:"tc_url"` - Stream string `json:"stream"` - Param string `json:"param"` + Ip string `json:"ip"` + Vhost string `json:"vhost"` + App string `json:"app"` + TcUrl string `json:"tc_url"` + Stream string `json:"stream"` + Param string `json:"param"` } func (v *ForwardMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) } -type Forward struct {} +type Forward struct{} /* backend service config description: @@ -863,9 +859,9 @@ func (v *Forward) Parse(body []byte) (se *SrsError) { return } -func ForwardServe(w http.ResponseWriter, r *http.Request) { +func ForwardServe(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - res := struct {}{} + res := struct{}{} body, _ := json.Marshal(res) w.Write(body) } else if r.Method == "POST" { @@ -885,7 +881,7 @@ func ForwardServe(w http.ResponseWriter, r *http.Request) { } } -func main() { +func main() { var port int var ffmpegPath string flag.IntVar(&port, "p", 8085, "use -p to specify listen port, default is 8085") @@ -920,16 +916,16 @@ func main() { http.HandleFunc("/api/v1", func(writer http.ResponseWriter, request *http.Request) { res := &struct { Code int `json:"code"` - Urls struct{ - Clients string `json:"clients"` - Streams string `json:"streams"` + Urls struct { + Clients string `json:"clients"` + Streams string `json:"streams"` Sessions string `json:"sessions"` - Dvrs string `json:"dvrs"` - Chats string `json:"chats"` - Servers struct{ + Dvrs string `json:"dvrs"` + Chats string `json:"chats"` + Servers struct { Summary string `json:"summary"` - Get string `json:"GET"` - Post string `json:"POST ip=node_ip&device_id=device_id"` + Get string `json:"GET"` + Post string `json:"POST ip=node_ip&device_id=device_id"` } } `json:"urls"` }{ @@ -951,7 +947,7 @@ func main() { // handle the clients requests: connect/disconnect vhost/app. http.HandleFunc("/api/v1/clients", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct {}{}) + WriteDataResponse(w, struct{}{}) return } @@ -970,7 +966,7 @@ func main() { WriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil - } (); err != nil { + }(); err != nil { WriteErrorResponse(w, err) } }) @@ -992,4 +988,4 @@ func main() { if err := http.ListenAndServe(addr, nil); err != nil { log.Println(fmt.Sprintf("listen on addr:%v failed, err is %v", addr, err)) } -} \ No newline at end of file +} From 0ca513f662f4330ee842c9cd2ab611a0bab460c5 Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:30:57 +0800 Subject: [PATCH 06/19] Refine structs by common request. --- trunk/research/api-server/server.go | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index c7beac1dd6..5012412b98 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -82,6 +82,7 @@ var StaticDir string var cm *ChatManager var sw *SnapshotWorker +// All type CommonRequest struct { Action string `json:"action"` ClientId string `json:"client_id"` @@ -90,6 +91,10 @@ type CommonRequest struct { App string `json:"app"` } +func (v *CommonRequest) String() string { + return fmt.Sprintf("action=%v, client_id=%v, ip=%v, vhost=%v", v.Action, v.ClientId, v.Ip, v.Vhost) +} + /* handle the clients requests: connect/disconnect vhost/app. for SRS hook: on_connect/on_close @@ -140,7 +145,7 @@ func (v *ClientMsg) Parse(b []byte) error { func (v *ClientMsg) String() string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("action=%v, client_id=%v, ip=%v, vhost=%v", v.Action, v.ClientId, v.Ip, v.Vhost)) + sb.WriteString(v.CommonRequest.String()) if v.Action == "on_connect" { sb.WriteString(fmt.Sprintf(", tcUrl=%v, pageUrl=%v", v.TcUrl, v.PageUrl)) } else if v.Action == "on_close" { @@ -180,11 +185,7 @@ func (v *ClientMsg) String() string { 0 */ type StreamMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` + CommonRequest Stream string `json:"stream"` Param string `json:"param"` } @@ -261,11 +262,7 @@ func StreamServe(w http.ResponseWriter, r *http.Request) { */ type SessionMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` + CommonRequest Stream string `json:"stream"` Param string `json:"param"` } @@ -358,11 +355,7 @@ func SessionServe(w http.ResponseWriter, r *http.Request) { */ type DvrMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` + CommonRequest Stream string `json:"stream"` Param string `json:"param"` Cwd string `json:"cwd"` From 2b6384426678340775c6bb39f206ba49253e8beb Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:44:31 +0800 Subject: [PATCH 07/19] Refine streams, sessions, dvrs and hls API. --- trunk/research/api-server/server.go | 471 ++++++++++------------------ 1 file changed, 163 insertions(+), 308 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 5012412b98..ca8a4d85b7 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -8,7 +8,6 @@ import ( "io/ioutil" "log" "net/http" - "net/url" "os" "os/exec" "path" @@ -25,16 +24,6 @@ const ( error_system_parse_json = 101 // request action invalid error_request_invalid_action = 200 - - // chat id not exist - error_chat_id_not_exist = 300 - - session_action_on_play = "on_play" - session_action_on_stop = "on_stop" -) - -const ( - HttpJson = "application/json" ) type SrsError struct { @@ -45,7 +34,7 @@ type SrsError struct { func Response(se *SrsError) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := json.Marshal(se) - w.Header().Set("Content-Type", HttpJson) + w.Header().Set("Content-Type", "application/json") w.Write(body) }) } @@ -67,7 +56,7 @@ func WriteDataResponse(w http.ResponseWriter, data interface{}) { return } - w.Header().Set("Content-Type", HttpJson) + w.Header().Set("Content-Type", "application/json") w.Write(j) } @@ -79,7 +68,6 @@ See also: https://github.com/ossrs/srs ` var StaticDir string -var cm *ChatManager var sw *SnapshotWorker // All @@ -138,7 +126,7 @@ type ClientMsg struct { func (v *ClientMsg) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse action from %v, err %v", string(b), err) + return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } @@ -186,47 +174,24 @@ func (v *ClientMsg) String() string { */ type StreamMsg struct { CommonRequest - Stream string `json:"stream"` - Param string `json:"param"` + Stream string `json:"stream"` + Param string `json:"param"` } -func (v *StreamMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) -} - -type Stream struct{} - -func (v *Stream) Parse(body []byte) (se *SrsError) { - msg := &StreamMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse stream msg failed, err is %v", err.Error())} +func (v *StreamMsg) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(b), err) } - log.Println(msg) return nil } -// handle the streams requests: publish/unpublish stream. - -func StreamServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct{}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) - c := &Stream{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return +func (v *StreamMsg) String() string { + var sb strings.Builder + sb.WriteString(v.CommonRequest.String()) + if v.Action == "on_publish" || v.Action == "on_unpublish" { + sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } + return sb.String() } /* @@ -263,74 +228,29 @@ func StreamServe(w http.ResponseWriter, r *http.Request) { type SessionMsg struct { CommonRequest - Stream string `json:"stream"` - Param string `json:"param"` -} - -type SessionOnPlayMsg struct { - SessionMsg + Stream string `json:"stream"` + Param string `json:"param"` + // For on_play only. PageUrl string `json:"pageUrl"` } -func (v *SessionOnPlayMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, pageUrl=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.PageUrl) -} - -type SessionOnStopMsg struct { - SessionMsg -} - -func (v *SessionOnStopMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param) -} - -type Session struct{} - -func (v *Session) Parse(body []byte) (se *SrsError) { - data := &struct { - Action string `json:"action"` - }{} - if err := json.Unmarshal(body, data); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session action failed, err is %v", err.Error())} - } - - if data.Action == session_action_on_play { - msg := &SessionOnPlayMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} - } - log.Println(msg) - } else if data.Action == session_action_on_stop { - msg := &SessionOnStopMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse session %v msg failed, err is %v", data.Action, err.Error())} - } - log.Println(msg) +func (v *SessionMsg) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -// handle the sessions requests: client play/stop stream -func SessionServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct{}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) - c := &Session{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return +func (v *SessionMsg) String() string { + var sb strings.Builder + sb.WriteString(v.CommonRequest.String()) + if v.Action == "on_play" || v.Action == "on_stop" { + sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } + if v.Action == "on_play" { + sb.WriteString(fmt.Sprintf(", pageUrl=%v", v.PageUrl)) + } + return sb.String() } /* @@ -356,48 +276,26 @@ func SessionServe(w http.ResponseWriter, r *http.Request) { type DvrMsg struct { CommonRequest - Stream string `json:"stream"` - Param string `json:"param"` - Cwd string `json:"cwd"` - File string `json:"file"` + Stream string `json:"stream"` + Param string `json:"param"` + Cwd string `json:"cwd"` + File string `json:"file"` } -func (v *DvrMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, cwd=%v, file=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Cwd, v.File) -} - -type Dvr struct{} - -func (v *Dvr) Parse(body []byte) (se *SrsError) { - msg := &DvrMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse dvr msg failed, err is %v", err.Error())} +func (v *DvrMsg) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(b), err) } - log.Println(msg) return nil } -// handle the dvrs requests: dvr stream. -func DvrServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct{}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) - c := &Dvr{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return +func (v *DvrMsg) String() string { + var sb strings.Builder + sb.WriteString(v.CommonRequest.String()) + if v.Action == "on_dvr" { + sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v", v.Stream, v.Param, v.Cwd, v.File)) } + return sb.String() } /* @@ -417,7 +315,7 @@ func DvrServe(w http.ResponseWriter, r *http.Request) { when srs reap a dvr file, call the hook, the request in the POST data string is a object encode by json: { - "action": "on_dvr", + "action": "on_hls", "client_id": "9308h583", "ip": "192.168.1.10", "vhost": "video.test.com", @@ -435,11 +333,7 @@ func DvrServe(w http.ResponseWriter, r *http.Request) { */ type HlsMsg struct { - Action string `json:"action"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` + CommonRequest Stream string `json:"stream"` Param string `json:"param"` Duration float64 `json:"duration"` @@ -448,162 +342,20 @@ type HlsMsg struct { SeqNo int `json:"seq_no"` } -func (v *HlsMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, stream=%v, param=%v, duration=%v, cwd=%v, file=%v, seq_no=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.Stream, v.Param, v.Duration, v.Cwd, v.File, v.SeqNo) -} - -type Hls struct{} - -func (v *Hls) Parse(body []byte) (se *SrsError) { - msg := &HlsMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse hls msg failed, err is %v", err.Error())} - } - log.Println(msg) - return nil -} - -// handle the hls requests: hls stream. -func HlsServe(w http.ResponseWriter, r *http.Request) { - log.Println(fmt.Sprintf("hls serve, uPath=%v", r.URL.Path)) - if r.Method == "GET" { - subPath := r.URL.Path[len("/api/v1/hls/"):] - res := struct { - Args []string `json:"args"` - KwArgs url.Values `json:"kwargs"` - }{ - Args: strings.Split(subPath, "/"), - KwArgs: r.URL.Query(), - } - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) - c := &Hls{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return - } -} - -/* -# object fields: -# id: an int value indicates the id of user. -# username: a str indicates the user name. -# url: a str indicates the url of user stream. -# agent: a str indicates the agent of user. -# join_date: a number indicates the join timestamp in seconds. -# join_date_str: a str specifies the formated friendly time. -# heartbeat: a number indicates the heartbeat timestamp in seconds. -# vcodec: a dict indicates the video codec info. -# acodec: a dict indicates the audio codec info. - -# dead time in seconds, if exceed, remove the chat. -*/ - -type Chat struct { - Id int `json:"id"` - Username string `json:"username"` - Url string `json:"url"` - JoinDate int64 `json:"join_date"` - JoinDateStr string `json:"join_date_str"` - Heartbeat int64 `json:"heartbeat"` -} - -type ChatManager struct { - globalId int - chats *sync.Map - deadTime int -} - -func NewChatManager() *ChatManager { - v := &ChatManager{ - globalId: 100, - // key is globalId, value is chat - chats: new(sync.Map), - deadTime: 15, - } - return v -} - -func (v *ChatManager) List() (chats []*Chat) { - chats = []*Chat{} - v.chats.Range(func(key, value interface{}) bool { - _, chat := key.(int), value.(*Chat) - if (time.Now().Unix() - chat.Heartbeat) > int64(v.deadTime) { - v.chats.Delete(key) - return true - } - chats = append(chats, chat) - return true - }) - return -} - -func (v *ChatManager) Update(id int) (se *SrsError) { - value, ok := v.chats.Load(id) - if !ok { - return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} +func (v *HlsMsg) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(b), err) } - c := value.(*Chat) - c.Heartbeat = time.Now().Unix() - log.Println(fmt.Sprintf("heartbeat chat success, id=%v", id)) return nil } -func (v *ChatManager) Delete(id int) (se *SrsError) { - if _, ok := v.chats.Load(id); !ok { - return &SrsError{Code: error_chat_id_not_exist, Data: fmt.Sprintf("cannot find id:%v", id)} - } - v.chats.Delete(id) - log.Println(fmt.Sprintf("delete chat success, id=%v", id)) - return -} - -func (v *ChatManager) Add(c *Chat) { - c.Id = v.globalId - now := time.Now() - c.JoinDate, c.Heartbeat = now.Unix(), now.Unix() - c.JoinDateStr = now.Format("2006-01-02 15:04:05") - v.globalId += 1 - v.chats.Store(c.Id, c) -} - -// the chat streams, public chat room. -func ChatServe(w http.ResponseWriter, r *http.Request) { - log.Println(fmt.Sprintf("got a chat req, uPath=%v", r.URL.Path)) - if r.Method == "GET" { - chats := cm.List() - Response(&SrsError{Code: 0, Data: chats}).ServeHTTP(w, r) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - c := &Chat{} - if err := json.Unmarshal(body, c); err != nil { - Response(&SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse body to chat json failed, err is %v", err)}) - return - } - cm.Add(c) - log.Println(fmt.Sprintf("create chat success, id=%v", c.Id)) - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - } else if r.Method == "PUT" { - // TODO: parse id? - Response(cm.Update(0)).ServeHTTP(w, r) - } else if r.Method == "DELETE" { - // TODO: parse id? - Response(cm.Delete(0)).ServeHTTP(w, r) +func (v *HlsMsg) String() string { + var sb strings.Builder + sb.WriteString(v.CommonRequest.String()) + if v.Action == "on_hls" { + sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v, duration=%v, seq_no=%v", v.Stream, v.Param, v.Cwd, v.File, v.Duration, v.SeqNo)) } + return sb.String() } /* @@ -893,7 +645,6 @@ func main() { } log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) - cm = NewChatManager() sw = NewSnapshotWorker(ffmpegPath) go sw.Serve() @@ -964,14 +715,118 @@ func main() { } }) - http.HandleFunc("/api/v1/streams", StreamServe) - http.HandleFunc("/api/v1/sessions", SessionServe) - http.HandleFunc("/api/v1/dvrs", DvrServe) - http.HandleFunc("/api/v1/hls", HlsServe) - http.HandleFunc("/api/v1/hls/", HlsServe) + // handle the streams requests: publish/unpublish stream. + http.HandleFunc("/api/v1/streams", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + WriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) + + msg := &StreamMsg{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + }(); err != nil { + WriteErrorResponse(w, err) + } + }) + + // handle the sessions requests: client play/stop stream + http.HandleFunc("/api/v1/sessions", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + WriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) + + msg := &SessionMsg{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + }(); err != nil { + WriteErrorResponse(w, err) + } + }) + + // handle the dvrs requests: dvr stream. + http.HandleFunc("/api/v1/dvrs", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + WriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) + + msg := &DvrMsg{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + }(); err != nil { + WriteErrorResponse(w, err) + } + }) + + // handle the dvrs requests: on_hls stream. + http.HandleFunc("/api/v1/hls", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + WriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) + + msg := &HlsMsg{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + }(); err != nil { + WriteErrorResponse(w, err) + } + }) // not support yet - http.HandleFunc("/api/v1/chat", ChatServe) + http.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) { + WriteErrorResponse(w, fmt.Errorf("not implemented")) + }) http.HandleFunc("/api/v1/snapshots", SnapshotServe) http.HandleFunc("/api/v1/forward", ForwardServe) From b7ab4d7b2e307517a9e50f5a57dd53f340d03ec7 Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:51:14 +0800 Subject: [PATCH 08/19] Rename structure names. --- trunk/research/api-server/server.go | 127 +++++++++++++--------------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index ca8a4d85b7..1810245c27 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -44,15 +44,15 @@ type SrsCommonResponse struct { Data interface{} `json:"data"` } -func WriteErrorResponse(w http.ResponseWriter, err error) { +func SrsWriteErrorResponse(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } -func WriteDataResponse(w http.ResponseWriter, data interface{}) { +func SrsWriteDataResponse(w http.ResponseWriter, data interface{}) { j, err := json.Marshal(data) if err != nil { - WriteErrorResponse(w, fmt.Errorf("marshal %v, err %v", err)) + SrsWriteErrorResponse(w, fmt.Errorf("marshal %v, err %v", err)) return } @@ -70,8 +70,8 @@ See also: https://github.com/ossrs/srs var StaticDir string var sw *SnapshotWorker -// All -type CommonRequest struct { +// SrsCommonRequest is the common fields of request messages from SRS HTTP callback. +type SrsCommonRequest struct { Action string `json:"action"` ClientId string `json:"client_id"` Ip string `json:"ip"` @@ -79,7 +79,7 @@ type CommonRequest struct { App string `json:"app"` } -func (v *CommonRequest) String() string { +func (v *SrsCommonRequest) String() string { return fmt.Sprintf("action=%v, client_id=%v, ip=%v, vhost=%v", v.Action, v.ClientId, v.Ip, v.Vhost) } @@ -114,8 +114,8 @@ handle the clients requests: connect/disconnect vhost/app. an int value specifies the error code(0 corresponding to success): 0 */ -type ClientMsg struct { - CommonRequest +type SrsClientRequest struct { + SrsCommonRequest // For on_connect message TcUrl string `json:"tcUrl"` PageUrl string `json:"pageUrl"` @@ -124,16 +124,16 @@ type ClientMsg struct { RecvBytes int64 `json:"recv_bytes"` } -func (v *ClientMsg) Parse(b []byte) error { +func (v *SrsClientRequest) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -func (v *ClientMsg) String() string { +func (v *SrsClientRequest) String() string { var sb strings.Builder - sb.WriteString(v.CommonRequest.String()) + sb.WriteString(v.SrsCommonRequest.String()) if v.Action == "on_connect" { sb.WriteString(fmt.Sprintf(", tcUrl=%v, pageUrl=%v", v.TcUrl, v.PageUrl)) } else if v.Action == "on_close" { @@ -172,22 +172,22 @@ func (v *ClientMsg) String() string { an int value specifies the error code(0 corresponding to success): 0 */ -type StreamMsg struct { - CommonRequest +type SrsStreamRequest struct { + SrsCommonRequest Stream string `json:"stream"` Param string `json:"param"` } -func (v *StreamMsg) Parse(b []byte) error { +func (v *SrsStreamRequest) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -func (v *StreamMsg) String() string { +func (v *SrsStreamRequest) String() string { var sb strings.Builder - sb.WriteString(v.CommonRequest.String()) + sb.WriteString(v.SrsCommonRequest.String()) if v.Action == "on_publish" || v.Action == "on_unpublish" { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } @@ -226,24 +226,24 @@ func (v *StreamMsg) String() string { 0 */ -type SessionMsg struct { - CommonRequest +type SrsSessionRequest struct { + SrsCommonRequest Stream string `json:"stream"` Param string `json:"param"` // For on_play only. PageUrl string `json:"pageUrl"` } -func (v *SessionMsg) Parse(b []byte) error { +func (v *SrsSessionRequest) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -func (v *SessionMsg) String() string { +func (v *SrsSessionRequest) String() string { var sb strings.Builder - sb.WriteString(v.CommonRequest.String()) + sb.WriteString(v.SrsCommonRequest.String()) if v.Action == "on_play" || v.Action == "on_stop" { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } @@ -274,24 +274,24 @@ func (v *SessionMsg) String() string { 0 */ -type DvrMsg struct { - CommonRequest +type SrsDvrRequest struct { + SrsCommonRequest Stream string `json:"stream"` Param string `json:"param"` Cwd string `json:"cwd"` File string `json:"file"` } -func (v *DvrMsg) Parse(b []byte) error { +func (v *SrsDvrRequest) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -func (v *DvrMsg) String() string { +func (v *SrsDvrRequest) String() string { var sb strings.Builder - sb.WriteString(v.CommonRequest.String()) + sb.WriteString(v.SrsCommonRequest.String()) if v.Action == "on_dvr" { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v", v.Stream, v.Param, v.Cwd, v.File)) } @@ -332,8 +332,8 @@ func (v *DvrMsg) String() string { 0 */ -type HlsMsg struct { - CommonRequest +type SrsHlsRequest struct { + SrsCommonRequest Stream string `json:"stream"` Param string `json:"param"` Duration float64 `json:"duration"` @@ -342,16 +342,16 @@ type HlsMsg struct { SeqNo int `json:"seq_no"` } -func (v *HlsMsg) Parse(b []byte) error { +func (v *SrsHlsRequest) Parse(b []byte) error { if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("parse message from %v, err %v", string(b), err) } return nil } -func (v *HlsMsg) String() string { +func (v *SrsHlsRequest) String() string { var sb strings.Builder - sb.WriteString(v.CommonRequest.String()) + sb.WriteString(v.SrsCommonRequest.String()) if v.Action == "on_hls" { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v, duration=%v, seq_no=%v", v.Stream, v.Param, v.Cwd, v.File, v.Duration, v.SeqNo)) } @@ -370,7 +370,7 @@ stop the snapshot worker when stream finished. type SnapShot struct{} func (v *SnapShot) Parse(body []byte) (se *SrsError) { - msg := &StreamMsg{} + msg := &SrsStreamRequest{} if err := json.Unmarshal(body, msg); err != nil { return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse snapshot msg failed, err is %v", err.Error())} } @@ -403,7 +403,7 @@ func SnapshotServe(w http.ResponseWriter, r *http.Request) { } type SnapshotJob struct { - StreamMsg + SrsStreamRequest cmd *exec.Cmd abort bool timestamp time.Time @@ -513,18 +513,18 @@ func (v *SnapshotWorker) Serve() { } } -func (v *SnapshotWorker) Create(sm *StreamMsg) { +func (v *SnapshotWorker) Create(sm *SrsStreamRequest) { streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) if _, ok := v.snapshots.Load(streamUrl); ok { return } sj := NewSnapshotJob() - sj.StreamMsg = *sm + sj.SrsStreamRequest = *sm sj.timestamp = time.Now() v.snapshots.Store(streamUrl, sj) } -func (v *SnapshotWorker) Destroy(sm *StreamMsg) { +func (v *SnapshotWorker) Destroy(sm *SrsStreamRequest) { streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) value, ok := v.snapshots.Load(streamUrl) if ok { @@ -560,19 +560,14 @@ an int value specifies the error code(0 corresponding to success): 0 */ -type ForwardMsg struct { - Action string `json:"action"` - ServerId string `json:"server_id"` - ClientId string `json:"client_id"` - Ip string `json:"ip"` - Vhost string `json:"vhost"` - App string `json:"app"` +type SrsForwardMsg struct { + SrsCommonRequest TcUrl string `json:"tc_url"` Stream string `json:"stream"` Param string `json:"param"` } -func (v *ForwardMsg) String() string { +func (v *SrsForwardMsg) String() string { return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) } @@ -586,7 +581,7 @@ For example: ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] */ func (v *Forward) Parse(body []byte) (se *SrsError) { - msg := &ForwardMsg{} + msg := &SrsForwardMsg{} if err := json.Unmarshal(body, msg); err != nil { return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse forward msg failed, err is %v", err.Error())} } @@ -691,7 +686,7 @@ func main() { // handle the clients requests: connect/disconnect vhost/app. http.HandleFunc("/api/v1/clients", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct{}{}) + SrsWriteDataResponse(w, struct{}{}) return } @@ -702,23 +697,23 @@ func main() { } log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) - msg := &ClientMsg{} + msg := &SrsClientRequest{} if err := msg.Parse(body); err != nil { return err } log.Println(fmt.Sprintf("Got %v", msg.String())) - WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { - WriteErrorResponse(w, err) + SrsWriteErrorResponse(w, err) } }) // handle the streams requests: publish/unpublish stream. http.HandleFunc("/api/v1/streams", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct{}{}) + SrsWriteDataResponse(w, struct{}{}) return } @@ -729,23 +724,23 @@ func main() { } log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) - msg := &StreamMsg{} + msg := &SrsStreamRequest{} if err := msg.Parse(body); err != nil { return err } log.Println(fmt.Sprintf("Got %v", msg.String())) - WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { - WriteErrorResponse(w, err) + SrsWriteErrorResponse(w, err) } }) // handle the sessions requests: client play/stop stream http.HandleFunc("/api/v1/sessions", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct{}{}) + SrsWriteDataResponse(w, struct{}{}) return } @@ -756,23 +751,23 @@ func main() { } log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) - msg := &SessionMsg{} + msg := &SrsSessionRequest{} if err := msg.Parse(body); err != nil { return err } log.Println(fmt.Sprintf("Got %v", msg.String())) - WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { - WriteErrorResponse(w, err) + SrsWriteErrorResponse(w, err) } }) // handle the dvrs requests: dvr stream. http.HandleFunc("/api/v1/dvrs", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct{}{}) + SrsWriteDataResponse(w, struct{}{}) return } @@ -783,23 +778,23 @@ func main() { } log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) - msg := &DvrMsg{} + msg := &SrsDvrRequest{} if err := msg.Parse(body); err != nil { return err } log.Println(fmt.Sprintf("Got %v", msg.String())) - WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { - WriteErrorResponse(w, err) + SrsWriteErrorResponse(w, err) } }) // handle the dvrs requests: on_hls stream. http.HandleFunc("/api/v1/hls", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - WriteDataResponse(w, struct{}{}) + SrsWriteDataResponse(w, struct{}{}) return } @@ -810,22 +805,22 @@ func main() { } log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) - msg := &HlsMsg{} + msg := &SrsHlsRequest{} if err := msg.Parse(body); err != nil { return err } log.Println(fmt.Sprintf("Got %v", msg.String())) - WriteDataResponse(w, &SrsCommonResponse{Code: 0}) + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { - WriteErrorResponse(w, err) + SrsWriteErrorResponse(w, err) } }) // not support yet http.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) { - WriteErrorResponse(w, fmt.Errorf("not implemented")) + SrsWriteErrorResponse(w, fmt.Errorf("not implemented")) }) http.HandleFunc("/api/v1/snapshots", SnapshotServe) From c5e6ca3c35deafe14c810fa93d80bb9ac4681fe9 Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:54:24 +0800 Subject: [PATCH 09/19] Refine on_forward API. --- trunk/research/api-server/server.go | 98 +++++++++++++---------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 1810245c27..1450dbbf4d 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -560,65 +560,27 @@ an int value specifies the error code(0 corresponding to success): 0 */ -type SrsForwardMsg struct { +type SrsForwardRequest struct { SrsCommonRequest - TcUrl string `json:"tc_url"` - Stream string `json:"stream"` - Param string `json:"param"` -} - -func (v *SrsForwardMsg) String() string { - return fmt.Sprintf("srs %v: client id=%v, ip=%v, vhost=%v, app=%v, tcUrl=%v, stream=%v, param=%v", v.Action, v.ClientId, v.Ip, v.Vhost, v.App, v.TcUrl, v.Stream, v.Param) + TcUrl string `json:"tc_url"` + Stream string `json:"stream"` + Param string `json:"param"` } -type Forward struct{} - -/* -backend service config description: - support multiple rtmp urls(custom addresses or third-party cdn service), - url's host is slave service. -For example: - ["rtmp://127.0.0.1:19350/test/teststream", "rtmp://127.0.0.1:19350/test/teststream?token=xxxx"] -*/ -func (v *Forward) Parse(body []byte) (se *SrsError) { - msg := &SrsForwardMsg{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse forward msg failed, err is %v", err.Error())} - } - if msg.Action == "on_forward" { - log.Println(msg) - res := &struct { - Urls []string `json:"urls"` - }{ - Urls: []string{"rtmp://127.0.0.1:19350/test/teststream"}, - } - return &SrsError{Code: 0, Data: res} - } else { - return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid action:%v", msg.Action)} +func (v *SrsForwardRequest) Parse(b []byte) error { + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(b), err) } - return + return nil } -func ForwardServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - res := struct{}{} - body, _ := json.Marshal(res) - w.Write(body) - } else if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) - c := &Forward{} - if se := c.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - return +func (v *SrsForwardRequest) String() string { + var sb strings.Builder + sb.WriteString(v.SrsCommonRequest.String()) + if v.Action == "on_forward" { + sb.WriteString(fmt.Sprintf(", tcUrl=%v, stream=%v, param=%v", v.TcUrl, v.Stream, v.Param)) } + return sb.String() } func main() { @@ -824,7 +786,37 @@ func main() { }) http.HandleFunc("/api/v1/snapshots", SnapshotServe) - http.HandleFunc("/api/v1/forward", ForwardServe) + + // handle the dynamic forward requests: on_forward stream. + http.HandleFunc("/api/v1/forward", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + SrsWriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) + + msg := &SrsForwardRequest{} + if err := msg.Parse(body); err != nil { + return err + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0, Data: &struct { + Urls []string `json:"urls"` + }{ + Urls: []string{"rtmp://127.0.0.1:19350/test/teststream"}, + }}) + return nil + }(); err != nil { + SrsWriteErrorResponse(w, err) + } + }) addr := fmt.Sprintf(":%v", port) log.Println(fmt.Sprintf("start listen on:%v", addr)) From 79315c443035010a9871dcdca15bf4f80da0b25d Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:55:40 +0800 Subject: [PATCH 10/19] Eliminate the parse api. --- trunk/research/api-server/server.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 1450dbbf4d..d25ef51c10 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -124,13 +124,6 @@ type SrsClientRequest struct { RecvBytes int64 `json:"recv_bytes"` } -func (v *SrsClientRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsClientRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) @@ -660,8 +653,8 @@ func main() { log.Println(fmt.Sprintf("post to clients, req=%v", string(body))) msg := &SrsClientRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) From 941e349342e63c4e7bccedb3a91fc00790cc205c Mon Sep 17 00:00:00 2001 From: winlin Date: Sun, 15 Jan 2023 13:58:40 +0800 Subject: [PATCH 11/19] Check request action. --- trunk/research/api-server/server.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index d25ef51c10..7221a86ab7 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -124,12 +124,20 @@ type SrsClientRequest struct { RecvBytes int64 `json:"recv_bytes"` } +func (v *SrsClientRequest) IsOnConnect() bool { + return v.Action == "on_connect" +} + +func (v *SrsClientRequest) IsOnClose() bool { + return v.Action == "on_close" +} + func (v *SrsClientRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_connect" { + if v.IsOnConnect() { sb.WriteString(fmt.Sprintf(", tcUrl=%v, pageUrl=%v", v.TcUrl, v.PageUrl)) - } else if v.Action == "on_close" { + } else if v.IsOnClose() { sb.WriteString(fmt.Sprintf(", send_bytes=%v, recv_bytes=%v", v.SendBytes, v.RecvBytes)) } return sb.String() @@ -658,6 +666,10 @@ func main() { } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnConnect() && !msg.IsOnClose() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { From 43f93d29d9a74e04228e3044c55f23744d45781e Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sun, 15 Jan 2023 17:56:47 +0800 Subject: [PATCH 12/19] drop other request Parse function --- trunk/research/api-server/server.go | 224 +++++++++++++++------------- 1 file changed, 117 insertions(+), 107 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 7221a86ab7..dbe1f56434 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -17,28 +17,6 @@ import ( "time" ) -const ( - // error when read http request - error_system_read_request = 100 - // error when parse json - error_system_parse_json = 101 - // request action invalid - error_request_invalid_action = 200 -) - -type SrsError struct { - Code int `json:"code"` - Data interface{} `json:"data"` -} - -func Response(se *SrsError) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(se) - w.Header().Set("Content-Type", "application/json") - w.Write(body) - }) -} - type SrsCommonResponse struct { Code int `json:"code"` Data interface{} `json:"data"` @@ -179,22 +157,23 @@ type SrsStreamRequest struct { Param string `json:"param"` } -func (v *SrsStreamRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsStreamRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_publish" || v.Action == "on_unpublish" { + if v.IsOnPublish() || v.IsOnUnPublish() { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } return sb.String() } +func (v *SrsStreamRequest) IsOnPublish() bool { + return v.Action == "on_publish" +} + +func (v *SrsStreamRequest) IsOnUnPublish() bool { + return v.Action == "on_unpublish" +} + /* for SRS hook: on_play/on_stop on_play: @@ -235,25 +214,26 @@ type SrsSessionRequest struct { PageUrl string `json:"pageUrl"` } -func (v *SrsSessionRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsSessionRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_play" || v.Action == "on_stop" { + if v.IsOnPlay() || v.IsOnStop() { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v", v.Stream, v.Param)) } - if v.Action == "on_play" { + if v.IsOnPlay() { sb.WriteString(fmt.Sprintf(", pageUrl=%v", v.PageUrl)) } return sb.String() } +func (v *SrsSessionRequest) IsOnPlay() bool { + return v.Action == "on_play" +} + +func (v *SrsSessionRequest) IsOnStop() bool { + return v.Action == "on_stop" +} + /* for SRS hook: on_dvr on_dvr: @@ -283,22 +263,19 @@ type SrsDvrRequest struct { File string `json:"file"` } -func (v *SrsDvrRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsDvrRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_dvr" { + if v.IsOnDvr() { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v", v.Stream, v.Param, v.Cwd, v.File)) } return sb.String() } +func (v *SrsDvrRequest) IsOnDvr() bool { + return v.Action == "on_dvr" +} + /* for SRS hook: on_hls_notify on_hls_notify: @@ -343,22 +320,19 @@ type SrsHlsRequest struct { SeqNo int `json:"seq_no"` } -func (v *SrsHlsRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsHlsRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_hls" { + if v.IsOnHls() { sb.WriteString(fmt.Sprintf(", stream=%v, param=%v, cwd=%v, file=%v, duration=%v, seq_no=%v", v.Stream, v.Param, v.Cwd, v.File, v.Duration, v.SeqNo)) } return sb.String() } +func (v *SrsHlsRequest) IsOnHls() bool { + return v.Action == "on_hls" +} + /* the snapshot api, to start a snapshot when encoder start publish stream, @@ -368,43 +342,30 @@ stop the snapshot worker when stream finished. {"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"} */ -type SnapShot struct{} +type SrsSnapShotRequest struct { + SrsCommonRequest + Stream string `json:"stream"` +} -func (v *SnapShot) Parse(body []byte) (se *SrsError) { - msg := &SrsStreamRequest{} - if err := json.Unmarshal(body, msg); err != nil { - return &SrsError{Code: error_system_parse_json, Data: fmt.Sprintf("parse snapshot msg failed, err is %v", err.Error())} - } - if msg.Action == "on_publish" { - sw.Create(msg) - return &SrsError{Code: 0, Data: nil} - } else if msg.Action == "on_unpublish" { - sw.Destroy(msg) - return &SrsError{Code: 0, Data: nil} - } else { - return &SrsError{Code: error_request_invalid_action, Data: fmt.Sprintf("invalid req action:%v", msg.Action)} +func (v *SrsSnapShotRequest) String() string { + var sb strings.Builder + sb.WriteString(v.SrsCommonRequest.String()) + if v.IsOnPublish() || v.IsOnUnPublish() { + sb.WriteString(fmt.Sprintf(", stream=%v", v.Stream)) } + return sb.String() } -func SnapshotServe(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - Response(&SrsError{Code: error_system_read_request, Data: fmt.Sprintf("read request body failed, err is %v", err)}).ServeHTTP(w, r) - return - } - log.Println(fmt.Sprintf("post to snapshot, req=%v", string(body))) - s := &SnapShot{} - if se := s.Parse(body); se != nil { - Response(se).ServeHTTP(w, r) - return - } - Response(&SrsError{Code: 0, Data: nil}).ServeHTTP(w, r) - } +func (v *SrsSnapShotRequest) IsOnPublish() bool { + return v.Action == "on_publish" +} + +func (v *SrsSnapShotRequest) IsOnUnPublish() bool { + return v.Action == "on_unpublish" } type SnapshotJob struct { - SrsStreamRequest + SrsSnapShotRequest cmd *exec.Cmd abort bool timestamp time.Time @@ -514,18 +475,18 @@ func (v *SnapshotWorker) Serve() { } } -func (v *SnapshotWorker) Create(sm *SrsStreamRequest) { +func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) { streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) if _, ok := v.snapshots.Load(streamUrl); ok { return } sj := NewSnapshotJob() - sj.SrsStreamRequest = *sm + sj.SrsSnapShotRequest = *sm sj.timestamp = time.Now() v.snapshots.Store(streamUrl, sj) } -func (v *SnapshotWorker) Destroy(sm *SrsStreamRequest) { +func (v *SnapshotWorker) Destroy(sm *SrsSnapShotRequest) { streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) value, ok := v.snapshots.Load(streamUrl) if ok { @@ -568,22 +529,19 @@ type SrsForwardRequest struct { Param string `json:"param"` } -func (v *SrsForwardRequest) Parse(b []byte) error { - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("parse message from %v, err %v", string(b), err) - } - return nil -} - func (v *SrsForwardRequest) String() string { var sb strings.Builder sb.WriteString(v.SrsCommonRequest.String()) - if v.Action == "on_forward" { + if v.IsOnForward() { sb.WriteString(fmt.Sprintf(", tcUrl=%v, stream=%v, param=%v", v.TcUrl, v.Stream, v.Param)) } return sb.String() } +func (v *SrsForwardRequest) IsOnForward() bool { + return v.Action == "on_forward" +} + func main() { var port int var ffmpegPath string @@ -692,11 +650,15 @@ func main() { log.Println(fmt.Sprintf("post to streams, req=%v", string(body))) msg := &SrsStreamRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnPublish() && !msg.IsOnUnPublish() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { @@ -719,11 +681,15 @@ func main() { log.Println(fmt.Sprintf("post to sessions, req=%v", string(body))) msg := &SrsSessionRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnPlay() && !msg.IsOnStop() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { @@ -746,11 +712,15 @@ func main() { log.Println(fmt.Sprintf("post to dvrs, req=%v", string(body))) msg := &SrsDvrRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnDvr() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { @@ -773,11 +743,15 @@ func main() { log.Println(fmt.Sprintf("post to hls, req=%v", string(body))) msg := &SrsHlsRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnHls() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) return nil }(); err != nil { @@ -790,7 +764,39 @@ func main() { SrsWriteErrorResponse(w, fmt.Errorf("not implemented")) }) - http.HandleFunc("/api/v1/snapshots", SnapshotServe) + http.HandleFunc("/api/v1/snapshots", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + SrsWriteDataResponse(w, struct{}{}) + return + } + + if err := func() error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("read request body, err %v", err) + } + log.Println(fmt.Sprintf("post to snapshots, req=%v", string(body))) + + msg := &SrsSnapShotRequest{} + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) + } + log.Println(fmt.Sprintf("Got %v", msg.String())) + + if msg.IsOnPublish() { + sw.Create(msg) + } else if msg.IsOnUnPublish() { + sw.Destroy(msg) + } else { + return fmt.Errorf("invalid message %v", msg.String()) + } + + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0}) + return nil + }(); err != nil { + SrsWriteErrorResponse(w, err) + } + }) // handle the dynamic forward requests: on_forward stream. http.HandleFunc("/api/v1/forward", func(w http.ResponseWriter, r *http.Request) { @@ -807,11 +813,15 @@ func main() { log.Println(fmt.Sprintf("post to forward, req=%v", string(body))) msg := &SrsForwardRequest{} - if err := msg.Parse(body); err != nil { - return err + if err := json.Unmarshal(body, msg); err != nil { + return fmt.Errorf("parse message from %v, err %v", string(body), err) } log.Println(fmt.Sprintf("Got %v", msg.String())) + if !msg.IsOnForward() { + return fmt.Errorf("invalid message %v", msg.String()) + } + SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0, Data: &struct { Urls []string `json:"urls"` }{ From 169c144b5ccab5491e2cc16abe758511eb472620 Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sun, 15 Jan 2023 21:52:23 +0800 Subject: [PATCH 13/19] support ./api-server 8085 method --- trunk/research/api-server/server.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index dbe1f56434..af4ae15cc9 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -12,6 +12,7 @@ import ( "os/exec" "path" "path/filepath" + "strconv" "strings" "sync" "time" @@ -41,7 +42,10 @@ func SrsWriteDataResponse(w http.ResponseWriter, data interface{}) { const Example = ` SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) Example: + the suggest start method: ./api-server -p 8085 -s ./static-dir + or the simple start method: + ./api-server 8085 See also: https://github.com/ossrs/srs ` @@ -561,6 +565,18 @@ func main() { } log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) + + // check if only one number arg + if len(os.Args[1:]) == 1 { + portArg := os.Args[1] + var err error + if port, err = strconv.Atoi(portArg); err != nil { + log.Println(fmt.Sprintf("parse port arg:%v to int failed, err %v", portArg, err)) + flag.Usage() + os.Exit(1) + } + } + sw = NewSnapshotWorker(ffmpegPath) go sw.Serve() From 866fc21870e1dc1d0c6554fbcbc199fbae90024a Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Tue, 17 Jan 2023 17:00:04 +0800 Subject: [PATCH 14/19] refine snapshot job --- trunk/research/api-server/server.go | 162 +++++++++++++--------------- 1 file changed, 74 insertions(+), 88 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index af4ae15cc9..0b361a9bbc 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -370,29 +370,86 @@ func (v *SrsSnapShotRequest) IsOnUnPublish() bool { type SnapshotJob struct { SrsSnapShotRequest - cmd *exec.Cmd - abort bool - timestamp time.Time - lock *sync.RWMutex + updatedAt time.Time + cancelCtx context.Context + cancelFunc context.CancelFunc + vframes int + timeout time.Duration } func NewSnapshotJob() *SnapshotJob { v := &SnapshotJob{ - lock: new(sync.RWMutex), + vframes: 5, + timeout: time.Duration(30) * time.Second, } + v.cancelCtx, v.cancelFunc = context.WithCancel(context.Background()) return v } -func (v *SnapshotJob) UpdateAbort(status bool) { - v.lock.Lock() - defer v.lock.Unlock() - v.abort = status +func (v *SnapshotJob) Tag() string { + return fmt.Sprintf("%v/%v/%v", v.Vhost, v.App, v.Stream) } -func (v *SnapshotJob) IsAbort() bool { - v.lock.RLock() - defer v.lock.RUnlock() - return v.abort +func (v *SnapshotJob) Abort() { + v.cancelFunc() + log.Println(fmt.Sprintf("cancel snapshot job %v", v.Tag())) +} + +/* +./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live?vhost=__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png +*/ +func (v *SnapshotJob) do(ffmpegPath, inputUrl string) (err error) { + outputPicDir := path.Join(StaticDir, v.App) + if err = os.MkdirAll(outputPicDir, 0777); err != nil { + log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", outputPicDir, err)) + return + } + + normalPicPath := path.Join(outputPicDir, fmt.Sprintf("%v", v.Stream)+"-%03d.png") + bestPng := path.Join(outputPicDir, fmt.Sprintf("%v-best.png", v.Stream)) + + param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", ffmpegPath, inputUrl, v.vframes, normalPicPath) + log.Println(fmt.Sprintf("start snapshot, cmd param=%v", param)) + timeoutCtx, _ := context.WithTimeout(v.cancelCtx, v.timeout) + cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) + if err = cmd.Run(); err != nil { + log.Println(fmt.Sprintf("run snapshot %v cmd failed, err is %v", v.Tag(), err)) + return + } + + bestFileSize := int64(0) + for i := 1; i <= v.vframes; i++ { + pic := path.Join(outputPicDir, fmt.Sprintf("%v-%03d.png", v.Stream, i)) + fi, err := os.Stat(pic) + if err != nil { + log.Println(fmt.Sprintf("stat pic:%v failed, err is %v", pic, err)) + continue + } + if bestFileSize == 0 { + bestFileSize = fi.Size() + } else if fi.Size() > bestFileSize { + os.Remove(bestPng) + os.Symlink(pic, bestPng) + bestFileSize = fi.Size() + } + } + log.Println(fmt.Sprintf("%v the best thumbnail is %v", v.Tag(), bestPng)) + return +} + +func (v *SnapshotJob) Serve(ffmpegPath, inputUrl string) { + sleep := time.Duration(1) * time.Second + for { + v.do(ffmpegPath, inputUrl) + select { + case <-time.After(sleep): + log.Println(fmt.Sprintf("%v sleep %v to redo snapshot", v.Tag(), sleep)) + break + case <-v.cancelCtx.Done(): + log.Println(fmt.Sprintf("snapshot job %v cancelled", v.Tag())) + return + } + } } type SnapshotWorker struct { @@ -408,77 +465,6 @@ func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { return sw } -/* -./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live?vhost=__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png -*/ - -func (v *SnapshotWorker) Serve() { - for { - time.Sleep(time.Second) - v.snapshots.Range(func(key, value interface{}) bool { - // range each snapshot job - streamUrl := key.(string) - sj := value.(*SnapshotJob) - streamTag := fmt.Sprintf("%v/%v/%v", sj.Vhost, sj.App, sj.Stream) - if sj.IsAbort() { // delete aborted snapshot job - if sj.cmd != nil && sj.cmd.Process != nil { - if err := sj.cmd.Process.Kill(); err != nil { - log.Println(fmt.Sprintf("snapshot job:%v kill running cmd failed, err is %v", streamTag, err)) - } - } - v.snapshots.Delete(key) - return true - } - - if sj.cmd == nil { // start a ffmpeg snap cmd - outputDir := path.Join(StaticDir, sj.App, fmt.Sprintf("%v", sj.Stream)+"-%03d.png") - bestPng := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-best.png", sj.Stream)) - if err := os.MkdirAll(path.Dir(outputDir), 0777); err != nil { - log.Println(fmt.Sprintf("create snapshot image dir:%v failed, err is %v", path.Base(outputDir), err)) - return true - } - vframes := 5 - param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", v.ffmpegPath, streamUrl, vframes, outputDir) - timeoutCtx, _ := context.WithTimeout(context.Background(), time.Duration(30)*time.Second) - cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param) - if err := cmd.Start(); err != nil { - log.Println(fmt.Sprintf("start snapshot %v cmd failed, err is %v", streamTag, err)) - return true - } - sj.cmd = cmd - log.Println(fmt.Sprintf("start snapshot success, cmd param=%v", param)) - go func() { - if err := sj.cmd.Wait(); err != nil { - log.Println(fmt.Sprintf("snapshot %v cmd wait failed, err is %v", streamTag, err)) - } else { // choose the best quality image - bestFileSize := int64(0) - for i := 1; i <= vframes; i++ { - pic := path.Join(StaticDir, sj.App, fmt.Sprintf("%v-%03d.png", sj.Stream, i)) - fi, err := os.Stat(pic) - if err != nil { - log.Println(fmt.Sprintf("stat pic:%v failed, err is %v", pic, err)) - continue - } - if bestFileSize == 0 { - bestFileSize = fi.Size() - } else if fi.Size() > bestFileSize { - os.Remove(bestPng) - os.Link(pic, bestPng) - bestFileSize = fi.Size() - } - } - log.Println(fmt.Sprintf("%v the best thumbnail is %v", streamTag, bestPng)) - } - sj.cmd = nil - }() - } else { - log.Println(fmt.Sprintf("snapshot %v cmd process is running, status=%v", streamTag, sj.cmd.ProcessState)) - } - return true - }) - } -} - func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) { streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) if _, ok := v.snapshots.Load(streamUrl); ok { @@ -486,7 +472,8 @@ func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) { } sj := NewSnapshotJob() sj.SrsSnapShotRequest = *sm - sj.timestamp = time.Now() + sj.updatedAt = time.Now() + go sj.Serve(v.ffmpegPath, streamUrl) v.snapshots.Store(streamUrl, sj) } @@ -495,8 +482,8 @@ func (v *SnapshotWorker) Destroy(sm *SrsSnapShotRequest) { value, ok := v.snapshots.Load(streamUrl) if ok { sj := value.(*SnapshotJob) - sj.UpdateAbort(true) - v.snapshots.Store(streamUrl, sj) + sj.Abort() + v.snapshots.Delete(streamUrl) log.Println(fmt.Sprintf("set stream:%v to destroy, update abort", sm.Stream)) } else { log.Println(fmt.Sprintf("cannot find stream:%v in snapshot worker", streamUrl)) @@ -578,7 +565,6 @@ func main() { } sw = NewSnapshotWorker(ffmpegPath) - go sw.Serve() if len(StaticDir) == 0 { curAbsDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) From 5f21bc5a63eb24566feaf4a0a4c93058c4e9506b Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Tue, 17 Jan 2023 22:27:42 +0800 Subject: [PATCH 15/19] refine comment, update stream url addr --- trunk/research/api-server/server.go | 274 ++++++++++++++-------------- 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 0b361a9bbc..abeddc5714 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -67,34 +67,34 @@ func (v *SrsCommonRequest) String() string { /* handle the clients requests: connect/disconnect vhost/app. - for SRS hook: on_connect/on_close - on_connect: - when client connect to vhost/app, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_connect", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", - "pageUrl": "http://www.test.com/live.html" - } - on_close: - when client close/disconnect to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_close", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "send_bytes": 10240, - "recv_bytes": 10240 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 +for SRS hook: on_connect/on_close +on_connect: + when client connect to vhost/app, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_connect", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "tcUrl": "rtmp://video.test.com/live?key=d2fa801d08e3f90ed1e1670e6e52651a", + "pageUrl": "http://www.test.com/live.html" + } +on_close: + when client close/disconnect to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_close", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "send_bytes": 10240, + "recv_bytes": 10240 + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 */ type SrsClientRequest struct { SrsCommonRequest @@ -126,34 +126,34 @@ func (v *SrsClientRequest) String() string { } /* - for SRS hook: on_publish/on_unpublish - on_publish: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_publish", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - on_unpublish: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_unpublish", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 +for SRS hook: on_publish/on_unpublish +on_publish: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_publish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } +on_unpublish: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_unpublish", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 */ type SrsStreamRequest struct { SrsCommonRequest @@ -179,35 +179,35 @@ func (v *SrsStreamRequest) IsOnUnPublish() bool { } /* - for SRS hook: on_play/on_stop - on_play: - when client(encoder) publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_play", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "pageUrl": "http://www.test.com/live.html" - } - on_stop: - when client(encoder) stop publish to vhost/app/stream, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_stop", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 +for SRS hook: on_play/on_stop +on_play: + when client(encoder) publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_play", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "pageUrl": "http://www.test.com/live.html" + } +on_stop: + when client(encoder) stop publish to vhost/app/stream, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_stop", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy" + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 */ type SrsSessionRequest struct { @@ -239,24 +239,24 @@ func (v *SrsSessionRequest) IsOnStop() bool { } /* - for SRS hook: on_dvr - on_dvr: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_dvr", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776.flv" - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 +for SRS hook: on_dvr +on_dvr: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_dvr", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776.flv" + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 */ type SrsDvrRequest struct { @@ -281,37 +281,37 @@ func (v *SrsDvrRequest) IsOnDvr() bool { } /* - for SRS hook: on_hls_notify - on_hls_notify: - when srs reap a ts file of hls, call this hook, - used to push file to cdn network, by get the ts file from cdn network. - so we use HTTP GET and use the variable following: - [app], replace with the app. - [stream], replace with the stream. - [param], replace with the param. - [ts_url], replace with the ts url. - ignore any return data of server. - - for SRS hook: on_hls - on_hls: - when srs reap a dvr file, call the hook, - the request in the POST data string is a object encode by json: - { - "action": "on_hls", - "client_id": "9308h583", - "ip": "192.168.1.10", - "vhost": "video.test.com", - "app": "live", - "stream": "livestream", - "param":"?token=xxx&salt=yyy", - "duration": 9.68, // in seconds - "cwd": "/usr/local/srs", - "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", - "seq_no": 100 - } - if valid, the hook must return HTTP code 200(Stauts OK) and response - an int value specifies the error code(0 corresponding to success): - 0 +for SRS hook: on_hls_notify +on_hls_notify: + when srs reap a ts file of hls, call this hook, + used to push file to cdn network, by get the ts file from cdn network. + so we use HTTP GET and use the variable following: + [app], replace with the app. + [stream], replace with the stream. + [param], replace with the param. + [ts_url], replace with the ts url. + ignore any return data of server. + +for SRS hook: on_hls +on_hls: + when srs reap a dvr file, call the hook, + the request in the POST data string is a object encode by json: + { + "action": "on_hls", + "client_id": "9308h583", + "ip": "192.168.1.10", + "vhost": "video.test.com", + "app": "live", + "stream": "livestream", + "param":"?token=xxx&salt=yyy", + "duration": 9.68, // in seconds + "cwd": "/usr/local/srs", + "file": "./objs/nginx/html/live/livestream.1420254068776-100.ts", + "seq_no": 100 + } +if valid, the hook must return HTTP code 200(Stauts OK) and response +an int value specifies the error code(0 corresponding to success): + 0 */ type SrsHlsRequest struct { @@ -396,7 +396,7 @@ func (v *SnapshotJob) Abort() { } /* -./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live?vhost=__defaultVhost__/panda -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png +./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live/livestream?vhost=__defaultVhost__ -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png */ func (v *SnapshotJob) do(ffmpegPath, inputUrl string) (err error) { outputPicDir := path.Join(StaticDir, v.App) @@ -466,7 +466,7 @@ func NewSnapshotWorker(ffmpegPath string) *SnapshotWorker { } func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) { - streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v/%v?vhost=%v", sm.App, sm.Stream, sm.Vhost) if _, ok := v.snapshots.Load(streamUrl); ok { return } @@ -478,7 +478,7 @@ func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) { } func (v *SnapshotWorker) Destroy(sm *SrsSnapShotRequest) { - streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v?vhost=%v/%v", sm.App, sm.Vhost, sm.Stream) + streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v/%v?vhost=%v", sm.App, sm.Stream, sm.Vhost) value, ok := v.snapshots.Load(streamUrl) if ok { sj := value.(*SnapshotJob) From d8e918938c7c41f89ae7412851917ca4a64cee58 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 18 Jan 2023 12:46:38 +0800 Subject: [PATCH 16/19] Refine the usage for api server. --- trunk/research/api-server/server.go | 42 +++++++++++------------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index abeddc5714..1b53fa41ed 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -39,16 +39,6 @@ func SrsWriteDataResponse(w http.ResponseWriter, data interface{}) { w.Write(j) } -const Example = ` -SRS api callback server, Copyright (c) 2013-2016 SRS(ossrs) -Example: - the suggest start method: - ./api-server -p 8085 -s ./static-dir - or the simple start method: - ./api-server 8085 -See also: https://github.com/ossrs/srs -` - var StaticDir string var sw *SnapshotWorker @@ -534,23 +524,26 @@ func (v *SrsForwardRequest) IsOnForward() bool { } func main() { + srsBin := os.Args[0] + if strings.HasPrefix(srsBin, "/var") { + srsBin = "go run ." + } + var port int var ffmpegPath string - flag.IntVar(&port, "p", 8085, "use -p to specify listen port, default is 8085") - flag.StringVar(&StaticDir, "s", "./static-dir", "use -s to specify static-dir, default is ./static-dir") - flag.StringVar(&ffmpegPath, "ffmpeg", "./objs/ffmpeg/bin/ffmpeg", "use -ffmpeg to specify ffmpegPath, default is ./objs/ffmpeg/bin/ffmpeg") + flag.IntVar(&port, "p", 8085, "HTTP listen port. Default is 8085") + flag.StringVar(&StaticDir, "s", "./static-dir", "HTML home for snapshot. Default is ./static-dir") + flag.StringVar(&ffmpegPath, "ffmpeg", "/usr/local/bin/ffmpeg", "FFmpeg for snapshot. Default is /usr/local/bin/ffmpeg") flag.Usage = func() { - fmt.Fprintln(flag.CommandLine.Output(), "Usage: apiServer [flags]") + fmt.Println("A demo api-server for SRS\n") + fmt.Println(fmt.Sprintf("Usage: %v [flags]", srsBin)) flag.PrintDefaults() - fmt.Fprintln(flag.CommandLine.Output(), Example) + fmt.Println(fmt.Sprintf("For example:")) + fmt.Println(fmt.Sprintf(" %v -p 8085", srsBin)) + fmt.Println(fmt.Sprintf(" %v 8085", srsBin)) } flag.Parse() - if len(os.Args[1:]) == 0 { - flag.Usage() - os.Exit(0) - } - log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) // check if only one number arg @@ -565,12 +558,9 @@ func main() { } sw = NewSnapshotWorker(ffmpegPath) - - if len(StaticDir) == 0 { - curAbsDir, _ := filepath.Abs(filepath.Dir(os.Args[0])) - StaticDir = path.Join(curAbsDir, "./static-dir") - } else { - StaticDir, _ = filepath.Abs(StaticDir) + StaticDir, err := filepath.Abs(StaticDir) + if err != nil { + panic(err) } log.Println(fmt.Sprintf("api server listen at port:%v, static_dir:%v", port, StaticDir)) From 16085cd0ca60f8a1774a1cdd1726fe78c599a633 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 18 Jan 2023 12:48:20 +0800 Subject: [PATCH 17/19] Update release to v6.0.18 --- trunk/doc/CHANGELOG.md | 1 + trunk/src/core/srs_core_version6.hpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index f94628b63e..5b9dfb50e2 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -8,6 +8,7 @@ The changelog for SRS. ## SRS 6.0 Changelog +* v6.0, 2023-01-18, Merge [#3382](https://github.com/ossrs/srs/pull/3382): Rewrite research/api-server code by Go, remove Python. v6.0.18 (#3382) * v6.0, 2023-01-18, Merge [#3386](https://github.com/ossrs/srs/pull/3386): SRT: fix crash when srt_to_rtmp off. v6.0.17 (#3386) * v5.0, 2023-01-17, Merge [#3385](https://github.com/ossrs/srs/pull/3385): API: Support server/pid/service label for exporter and api. v6.0.16 (#3385) * v6.0, 2023-01-17, Merge [#3379](https://github.com/ossrs/srs/pull/3379): H265: Support demux vps/pps info. v6.0.15 diff --git a/trunk/src/core/srs_core_version6.hpp b/trunk/src/core/srs_core_version6.hpp index 34fc507e3f..2077a0c74a 100644 --- a/trunk/src/core/srs_core_version6.hpp +++ b/trunk/src/core/srs_core_version6.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 6 #define VERSION_MINOR 0 -#define VERSION_REVISION 17 +#define VERSION_REVISION 18 #endif From c36e26357acad24dd4eb3ba348285c99e0fbc769 Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 18 Jan 2023 12:50:45 +0800 Subject: [PATCH 18/19] Refine comments. --- trunk/research/api-server/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trunk/research/api-server/server.go b/trunk/research/api-server/server.go index 1b53fa41ed..8942d8c41e 100644 --- a/trunk/research/api-server/server.go +++ b/trunk/research/api-server/server.go @@ -386,7 +386,9 @@ func (v *SnapshotJob) Abort() { } /* -./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live/livestream?vhost=__defaultVhost__ -vf fps=1 -vcodec png -f image2 -an -y -vframes 5 -y static-dir/live/panda-%03d.png +./objs/ffmpeg/bin/ffmpeg -i rtmp://127.0.0.1/live/livestream \ + -vf fps=1 -vcodec png -f image2 -an -vframes 5 \ + -y static-dir/live/livestream-%03d.png */ func (v *SnapshotJob) do(ffmpegPath, inputUrl string) (err error) { outputPicDir := path.Join(StaticDir, v.App) From 6694b55760d9159ab74c21f09280603471458d5e Mon Sep 17 00:00:00 2001 From: winlin Date: Wed, 18 Jan 2023 12:54:33 +0800 Subject: [PATCH 19/19] Update release v5.0.137 --- trunk/doc/CHANGELOG.md | 1 + trunk/src/core/srs_core_version5.hpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 5b9dfb50e2..38b90fb063 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -32,6 +32,7 @@ The changelog for SRS. ## SRS 5.0 Changelog +* v5.0, 2023-01-18, Merge [#3382](https://github.com/ossrs/srs/pull/3382): Rewrite research/api-server code by Go, remove Python. v5.0.137 (#3382) * v5.0, 2023-01-18, Merge [#3386](https://github.com/ossrs/srs/pull/3386): SRT: fix crash when srt_to_rtmp off. v5.0.136 (#3386) * v5.0, 2023-01-17, Merge [#3385](https://github.com/ossrs/srs/pull/3385): API: Support server/pid/service label for exporter and api. v5.0.135 (#3385) * v5.0, 2023-01-17, Merge [#3383](https://github.com/ossrs/srs/pull/3383): GB: Fix PSM parsing indicator bug. v5.0.134 (#3383) diff --git a/trunk/src/core/srs_core_version5.hpp b/trunk/src/core/srs_core_version5.hpp index 9bb8da5c95..74139f7bf2 100644 --- a/trunk/src/core/srs_core_version5.hpp +++ b/trunk/src/core/srs_core_version5.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 5 #define VERSION_MINOR 0 -#define VERSION_REVISION 136 +#define VERSION_REVISION 137 #endif