七牛云rtc服务

master
daiao 3 years ago
parent e438abdba1
commit cda459ae9c

@ -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)
}

@ -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
}

@ -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
}

@ -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

@ -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

@ -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=

@ -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

Loading…
Cancel
Save