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