You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

247 lines
8.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}