From cda459ae9ca596f199f75a51f36e495e135e4f08 Mon Sep 17 00:00:00 2001 From: daiao <358551898@qq.com> Date: Tue, 23 Nov 2021 19:50:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=83=E7=89=9B=E4=BA=91rtc=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rtc_controller.go => aliyun_controller.go} | 0 controllers/rtc/qiniu_controller.go | 53 ++++ controllers/rtc/qiniu_util.go | 140 ++++++++++ controllers/rtc/qiu_api.go | 246 ++++++++++++++++++ controllers/user/user_controller.go | 1 + go.mod | 1 + go.sum | 4 + routers/web_routers.go | 4 + 8 files changed, 449 insertions(+) rename controllers/rtc/{rtc_controller.go => aliyun_controller.go} (100%) create mode 100644 controllers/rtc/qiniu_controller.go create mode 100644 controllers/rtc/qiniu_util.go create mode 100644 controllers/rtc/qiu_api.go diff --git a/controllers/rtc/rtc_controller.go b/controllers/rtc/aliyun_controller.go similarity index 100% rename from controllers/rtc/rtc_controller.go rename to controllers/rtc/aliyun_controller.go diff --git a/controllers/rtc/qiniu_controller.go b/controllers/rtc/qiniu_controller.go new file mode 100644 index 0000000..57fd103 --- /dev/null +++ b/controllers/rtc/qiniu_controller.go @@ -0,0 +1,53 @@ +package rtc + +import ( + "fmt" + "gowebsocket/common" + "gowebsocket/controllers" + "time" + + "github.com/gin-gonic/gin" + "github.com/qiniu/api.v7/v7/auth" +) + +var manager *Manager + +func init() { + accessKey := "dzumJl3gfsMSR3fvfABL4e0kDpo6FJmrlcuTu8TF" + secretKey := "aqWegU2o8tTCe0JtIVfMDdOjC3-jvuv2eWFvKQOm" + fmt.Println("accessKey: secretKey: ", accessKey, secretKey) + mac := auth.New(accessKey, secretKey) + manager = NewManager(mac) +} + +// TODO: 获取直播流地址 +func GetRoomToken(c *gin.Context) { + if o := c.Request.Header.Get("Origin"); o != "" { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,HEAD,PUT,DELETE,OPTIONS") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Server,Range,Content-Length,Content-Range") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin,Range,Accept-Encoding,Referer,Cache-Control,X-Proxy-Authorization,X-Requested-With,Content-Type") + } + + q := c.Request.URL.Query() + appID, roomName, userID, Permission := q.Get("app_id"), q.Get("room_name"), q.Get("user_id"), q.Get("permission") + + roomAccess := RoomAccess{ + AppID: appID, + RoomName: roomName, + UserID: userID, + ExpireAt: time.Now().Unix() + 3600, + Permission: Permission, + } + + token, _ := manager.GetRoomToken(roomAccess) + url := fmt.Sprintf("https://rtc.qiniuapi.com/v3/apps/%v/rooms/%v/auth?user=%v&token=%v", roomAccess.AppID, roomAccess.RoomName, roomAccess.UserID, token) + fmt.Println("url: ", url) + + data := gin.H{ + "token": token, + } + controllers.Response(c, common.OK, "", data) + // url := fmt.Sprintf("https://rtc.qiniuapi.com/v3/apps/%v/rooms/%v/auth?user=%v&token=%v", roomAccess.AppID, roomAccess.RoomName, roomAccess.UserID, token) + +} diff --git a/controllers/rtc/qiniu_util.go b/controllers/rtc/qiniu_util.go new file mode 100644 index 0000000..f4da441 --- /dev/null +++ b/controllers/rtc/qiniu_util.go @@ -0,0 +1,140 @@ +package rtc + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/qiniu/api.v7/v7/auth" +) + +// resInfo is httpresponse infomation +type resInfo struct { + Code int + Err error +} + +func newResInfo() resInfo { + info := resInfo{} + return info +} + +func getReqid(src *http.Header) string { + for k, v := range *src { + K := strings.Title(k) + if strings.Contains(K, "Reqid") { + return strings.Join(v, ", ") + } + } + return "" +} + +func buildURL(path string) string { + if strings.Index(path, "/") != 0 { + path = "/" + path + } + return "https://" + RtcHost + path +} + +func postReq(httpClient *http.Client, mac *auth.Credentials, url string, + reqParam interface{}, ret interface{}) *resInfo { + info := newResInfo() + var reqData []byte + var err error + + switch v := reqParam.(type) { + case *string: + reqData = []byte(*v) + case string: + reqData = []byte(v) + case *[]byte: + reqData = *v + case []byte: + reqData = v + default: + reqData, err = json.Marshal(reqParam) + } + + if err != nil { + info.Err = err + return &info + } + req, err := http.NewRequest("POST", url, bytes.NewReader(reqData)) + if err != nil { + info.Err = err + return &info + } + req.Header.Add("Content-Type", "application/json") + return callReq(httpClient, req, mac, &info, ret) +} + +func getReq(httpClient *http.Client, mac *auth.Credentials, url string, ret interface{}) *resInfo { + info := newResInfo() + req, err := http.NewRequest("GET", url, nil) + if err != nil { + info.Err = err + return &info + } + return callReq(httpClient, req, mac, &info, ret) +} + +func delReq(httpClient *http.Client, mac *auth.Credentials, url string, ret interface{}) *resInfo { + info := newResInfo() + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + info.Err = err + return &info + } + return callReq(httpClient, req, mac, &info, ret) +} + +func callReq(httpClient *http.Client, req *http.Request, mac *auth.Credentials, + info *resInfo, ret interface{}) (oinfo *resInfo) { + oinfo = info + accessToken, err := mac.SignRequestV2(req) + if err != nil { + info.Err = err + return + } + req.Header.Add("Authorization", "Qiniu "+accessToken) + client := httpClient + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req) + if err != nil { + info.Err = err + return + } + defer resp.Body.Close() + info.Code = resp.StatusCode + reqid := getReqid(&resp.Header) + rebuildErr := func(msg string) error { + return fmt.Errorf("Code: %v, Reqid: %v, %v", info.Code, reqid, msg) + } + + if resp.ContentLength > 2*1024*1024 { + err = rebuildErr(fmt.Sprintf("response is too long. Content-Length: %v", resp.ContentLength)) + info.Err = err + return + } + resData, err := ioutil.ReadAll(resp.Body) + if err != nil { + info.Err = rebuildErr(err.Error()) + return + } + if info.Code != 200 { + info.Err = rebuildErr(string(resData)) + return + } + if ret != nil { + err = json.Unmarshal(resData, ret) + if err != nil { + info.Err = rebuildErr(fmt.Sprintf("err: %v, res: %v", err, resData)) + } + } + return +} diff --git a/controllers/rtc/qiu_api.go b/controllers/rtc/qiu_api.go new file mode 100644 index 0000000..772f0ce --- /dev/null +++ b/controllers/rtc/qiu_api.go @@ -0,0 +1,246 @@ +package rtc + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/qiniu/api.v7/v7/auth" +) + +var ( + // RtcHost 为 Qiniu RTC Server API服务域名 + RtcHost = "rtc.qiniuapi.com" +) + +// Manager 提供了 Qiniu RTC Server API 相关功能 +type Manager struct { + mac *auth.Credentials + httpClient *http.Client +} + +// MergePublishRtmp 连麦合流转推 RTMP 的配置 +// Enable: 布尔类型,用于开启和关闭所有房间的合流功能。 +// AudioOnly: 布尔类型,可选,指定是否只合成音频。 +// Height, Width: int,可选,指定合流输出的高和宽,默认为 640 x 480。 +// OutputFps: int,可选,指定合流输出的帧率,默认为 25 fps 。 +// OutputKbps: int,可选,指定合流输出的码率,默认为 1000 。 +// URL: 合流后转推旁路直播的地址,可选,支持魔法变量配置按照连麦房间号生成不同的推流地址。如果是转推到七牛直播云,不建议使用该配置 +// StreamTitle: 转推七牛直播云的流名,可选,支持魔法变量配置按照连麦房间号生成不同的流名。例如,配置 Hub 为 qn-zhibo ,配置 StreamTitle 为 $(roomName) ,则房间 meeting-001 的合流将会被转推到 rtmp://pili-publish.qn-zhibo.***.com/qn-zhibo/meeting-001地址。详细配置细则,请咨询七牛技术支持。 +type MergePublishRtmp struct { + Enable bool `json:"enable,omitempty"` + AudioOnly bool `json:"audioOnly,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + OutputFps int `json:"fps,omitempty"` + OutputKbps int `json:"kbps,omitempty"` + URL string `json:"url,omitempty"` + StreamTitle string `json:"streamTitle,omitempty"` +} + +// App 完整信息 +// AppID: app 的唯一标识,创建的时候由系统生成。 +type App struct { + AppID string `json:"appId"` + AppInitConf + MergePublishRtmp MergePublishRtmp `json:"mergePublishRtmp,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// AppInitConf 创建 App 请求参数 +// Title: app 的名称, 可选。 +// Hub: 绑定的直播 hub,可选,用于合流后 rtmp 推流。 +// MaxUsers: int 类型,可选,连麦房间支持的最大在线人数。 +// NoAutoKickUser: bool 类型,可选,禁止自动踢人。 +type AppInitConf struct { + Hub string `json:"hub,omitempty"` + Title string `json:"title,omitempty"` + MaxUsers int `json:"maxUsers,omitempty"` + NoAutoKickUser bool `json:"noAutoKickUser,omitempty"` +} + +// MergePublishRtmpInfo 连麦合流转推 RTMP 的配置更改信息 +type MergePublishRtmpInfo struct { + Enable *bool `json:"enable,omitempty"` + AudioOnly *bool `json:"audioOnly,omitempty"` + Height *int `json:"height,omitempty"` + Width *int `json:"width,omitempty"` + OutputFps *int `json:"fps,omitempty"` + OutputKbps *int `json:"kbps,omitempty"` + URL *string `json:"url,omitempty"` + StreamTitle *string `json:"streamTitle,omitempty"` +} + +// AppUpdateInfo 更改信息 +// MergePublishRtmpInfo 连麦合流转推 RTMP 的配置更改信息 +type AppUpdateInfo struct { + Hub *string `json:"hub,omitempty"` + Title *string `json:"title,omitempty"` + MaxUsers *int `json:"maxUsers,omitempty"` + NoAutoKickUser *bool `json:"noAutoKickUser,omitempty"` + MergePublishRtmp *MergePublishRtmpInfo `json:"mergePublishRtmp,omitempty"` +} + +// User 连麦房间里的用户 +type User struct { + UserID string `json:"userId"` +} + +// NewManager 用来构建一个新的 Manager +func NewManager(mac *auth.Credentials) *Manager { + httpClient := http.DefaultClient + return &Manager{mac: mac, httpClient: httpClient} +} + +// CreateApp 新建实时音视频云 +func (r *Manager) CreateApp(appReq AppInitConf) (App, error) { + url := buildURL("/v3/apps") + ret := App{} + info := postReq(r.httpClient, r.mac, url, &appReq, &ret) + return ret, info.Err +} + +// GetApp 根据 appID 获取 实时音视频云 信息 +func (r *Manager) GetApp(appID string) (App, error) { + url := buildURL("/v3/apps/" + appID) + ret := App{} + info := getReq(r.httpClient, r.mac, url, &ret) + return ret, info.Err +} + +// DeleteApp 根据 appID 删除 实时音视频云 +func (r *Manager) DeleteApp(appID string) error { + url := buildURL("/v3/apps/" + appID) + info := delReq(r.httpClient, r.mac, url, nil) + return info.Err +} + +// UpdateApp 根据 appID, App 更改实时音视频云 信息 +func (r *Manager) UpdateApp(appID string, appInfo AppUpdateInfo) (App, error) { + url := buildURL("/v3/apps/" + appID) + ret := App{} + info := postReq(r.httpClient, r.mac, url, &appInfo, &ret) + return ret, info.Err +} + +// ListUser 根据 appID, roomName 获取连麦房间里在线的用户 +// appID: 连麦房间所属的 app 。 +// roomName: 操作所查询的连麦房间。 +func (r *Manager) ListUser(appID, roomName string) ([]User, error) { + url := buildURL("/v3/apps/" + appID + "/rooms/" + roomName + "/users") + users := struct { + Users []User `json:"users"` + }{} + info := getReq(r.httpClient, r.mac, url, &users) + return users.Users, info.Err +} + +// KickUser 根据 appID, roomName, UserID 剔除在线的用户 +// appID: 连麦房间所属的 app 。 +// roomName: 连麦房间。 +// userID: 操作所剔除的用户。 +func (r *Manager) KickUser(appID, roomName, userID string) error { + url := buildURL("/v3/apps/" + appID + "/rooms/" + roomName + "/users/" + userID) + info := delReq(r.httpClient, r.mac, url, nil) + return info.Err +} + +// RoomQuery 房间查询响应结果 +// IsEnd: bool 类型,分页查询是否已经查完所有房间。 +// Offset: int 类型,下次分页查询使用的位移标记。 +// Rooms: 当前活跃的房间名列表。 +type RoomQuery struct { + IsEnd bool `json:"end"` + Offset int `json:"offset"` + Rooms []RoomName `json:"rooms"` +} + +// RoomName 房间名 +type RoomName string + +// ListActiveRooms 根据 appID, roomNamePrefix, offset, limit 查询当前活跃的房间 +// appID: 连麦房间所属的 app 。 +// roomNamePrefix: 所查询房间名的前缀索引,可以为空。 +// offset: int 类型,分页查询的位移标记。 +// limit: int 类型,此次查询的最大长度。 +func (r *Manager) ListActiveRooms(appID, roomNamePrefix string, offset, limit int) (RoomQuery, error) { + ret, _, err := r.doListActiveRoom(appID, roomNamePrefix, offset, limit) + return ret, err +} + +// ListAllActiveRooms 根据 appID, roomNamePrefix 查询当前活跃的房间 +// appID: 连麦房间所属的 app 。 +// roomNamePrefix: 所查询房间名的前缀索引,可以为空。 +func (r *Manager) ListAllActiveRooms(appID, roomNamePrefix string) ([]RoomName, error) { + ns := []RoomName{} + var outErr error + for offset := 0; ; { + q, info, err := r.doListActiveRoom(appID, roomNamePrefix, offset, 100) + if err != nil && info.Code != 401 { + time.Sleep(500 * time.Millisecond) + q, info, err = r.doListActiveRoom(appID, roomNamePrefix, offset, 100) + } + + if err != nil || len(q.Rooms) == 0 { + outErr = err + break + } + offset = q.Offset + ns = append(ns, q.Rooms...) + if q.IsEnd { + break + } + } + return ns, outErr +} + +func (r *Manager) doListActiveRoom(appID, roomNamePrefix string, offset, limit int) (RoomQuery, resInfo, error) { + query := "" + roomNamePrefix = strings.TrimSpace(roomNamePrefix) + if len(roomNamePrefix) != 0 { + query = "prefix=" + roomNamePrefix + "&" + } + query += fmt.Sprintf("offset=%v&limit=%v", offset, limit) + url := buildURL("/v3/apps/" + appID + "/rooms?" + query) + ret := RoomQuery{} + info := getReq(r.httpClient, r.mac, url, &ret) + return ret, *info, info.Err +} + +// RoomAccess 房间管理凭证 +// AppID: 房间所属帐号的 app 。 +// RoomName: 房间名称,需满足规格 ^[a-zA-Z0-9_-]{3,64}$ +// UserID: 请求加入房间的用户 ID,需满足规格 ^[a-zA-Z0-9_-]{3,50}$ +// ExpireAt: int64 类型,鉴权的有效时间,传入以秒为单位的64位Unix绝对时间,token 将在该时间后失效。 +// Permission: 该用户的房间管理权限,"admin" 或 "user",默认为 "user" 。当权限角色为 "admin" 时,拥有将其他用户移除出房间等特权. +type RoomAccess struct { + AppID string `json:"appId"` + RoomName string `json:"roomName"` + UserID string `json:"userId"` + ExpireAt int64 `json:"expireAt"` + Permission string `json:"permission"` +} + +// GetRoomToken 生成房间管理鉴权,连麦用户终端通过房间管理鉴权获取七牛连麦服务。 +func (r *Manager) GetRoomToken(roomAccess RoomAccess) (token string, err error) { + roomAccessByte, err := json.Marshal(roomAccess) + if err != nil { + return + } + buf := make([]byte, base64.URLEncoding.EncodedLen(len(roomAccessByte))) + base64.URLEncoding.Encode(buf, roomAccessByte) + + hmacsha1 := hmac.New(sha1.New, r.mac.SecretKey) + hmacsha1.Write(buf) + sign := hmacsha1.Sum(nil) + + encodedSign := base64.URLEncoding.EncodeToString(sign) + token = r.mac.AccessKey + ":" + encodedSign + ":" + string(buf) + return +} diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go index ae4be57..0dc3bdb 100644 --- a/controllers/user/user_controller.go +++ b/controllers/user/user_controller.go @@ -65,6 +65,7 @@ func SendMessage(c *gin.Context) { appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32) appId := uint32(appIdUint64) + fmt.Println("http_request 给用户发送消息", appIdStr, userId, msgId, message) // TODO::进行用户权限认证,一般是客户端传入TOKEN,然后检验TOKEN是否合法,通过TOKEN解析出来用户ID diff --git a/go.mod b/go.mod index 53e0f08..bbb1d21 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/myesui/uuid v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/ossrs/go-oryx-lib v0.0.9 + github.com/qiniu/api.v7/v7 v7.8.2 github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07 diff --git a/go.sum b/go.sum index 034ee1a..fa3c2ca 100644 --- a/go.sum +++ b/go.sum @@ -362,6 +362,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gookit/color v1.3.6 h1:Rgbazd4JO5AgSTVGS3o0nvaSdwdrS8bzvIXwtK6OiMk= +github.com/gookit/color v1.3.6/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -569,6 +571,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/qiniu/api.v7/v7 v7.8.2 h1:f08kI0MmsJNzK4sUS8bG3HDH67ktwd/ji23Gkiy2ra4= +github.com/qiniu/api.v7/v7 v7.8.2/go.mod h1:FPsIqxh1Ym3X01sANE5ZwXfLZSWoCUp5+jNI8cLo3l0= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/routers/web_routers.go b/routers/web_routers.go index c70f2b6..0491a6b 100644 --- a/routers/web_routers.go +++ b/routers/web_routers.go @@ -19,6 +19,7 @@ import ( ) func Init(router *gin.Engine) { + router.LoadHTMLGlob("views/**/*") // 静态文件 router.StaticFS("/static", http.Dir("static/")) @@ -47,6 +48,9 @@ func Init(router *gin.Engine) { rtcRouter := router.Group("/rtc") { rtcRouter.GET("/get_token", rtc.GetToken) + //TODO: options请求转发 + rtcRouter.OPTIONS("/get_qiniu_token", rtc.GetRoomToken) + rtcRouter.GET("/get_qiniu_token", rtc.GetRoomToken) } // docker