-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloudRecording):implement cloudRecording rest api
- Loading branch information
1 parent
535fa14
commit a756fc6
Showing
24 changed files
with
2,862 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# Agora REST Client for Go | ||
|
||
`agora-rest-client-go`是用Go语言编写的一个开源项目,专门为 Agora REST API设计。它包含了 Agora 官方提供的REST API接口的包装和内部实现,可以帮助开发者更加方便的集成服务端Agora REST API。 | ||
## 特性 | ||
* 封装了Agora REST API的请求和响应处理,简化与Agora REST API 的通信流程 | ||
* 当遇到 DNS 解析失败、网络错误或者请求超时等问题的时候,提供了自动切换最佳域名的能力,以保障请求 REST API 服务的可用性 | ||
* 提供了易于使用的API,可轻松地实现调用 Agora REST API 的常见功能,如开启云录制、停止云录制等 | ||
* 基于Go语言,具有高效性、并发性和可扩展性 | ||
|
||
## 支持的服务 | ||
* [云端录制 Cloud Recording ](./services/cloudrecording/README.md) | ||
|
||
## 环境准备 | ||
* [Go 1.18 或以上版本](https://go.dev/) | ||
* 在声网 [Console 平台](https://console.shengwang.cn/)申请的 App ID 和 App Certificate | ||
* 在声网 [Console 平台](https://console.shengwang.cn/)的 Basic Auth 认证信息 | ||
* 在声网 [Console 平台](https://console.shengwang.cn/)开启相关的服务能力 | ||
|
||
## 安装 | ||
使用以下命令从 GitHub 安装依赖: | ||
```shell | ||
go get -u github.com/AgoraIO/agora-rest-client-go | ||
``` | ||
## 使用示例 | ||
以调用云录制服务为例: | ||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"log" | ||
|
||
"github.com/AgoraIO/agora-rest-client-go/core" | ||
"github.com/AgoraIO/agora-rest-client-go/services/cloudrecording" | ||
v1 "github.com/AgoraIO/agora-rest-client-go/services/cloudrecording/v1" | ||
) | ||
|
||
const ( | ||
appId = "<your appId>" | ||
username = "<the username of basic auth credential>" | ||
password = "<the password of basic auth credential>" | ||
cname = "<your cname>" | ||
uid = "<your uid>" | ||
) | ||
|
||
func main() { | ||
// 初始化Agora REST API客户端 | ||
client := core.NewClient(&core.Config{ | ||
AppID: appId, | ||
Credential: core.NewBasicAuthCredential(username, password), | ||
// 指定服务器所在的区域,可选值有CN, NA, EU, AP,client 将会根据配置的区域自动切换使用最佳的域名 | ||
RegionCode: core.CN, | ||
// 指定日志输出的级别,可选值有LogDebug, LogInfo, LogWarn, LogError | ||
// 如果要关闭日志输出,可将 logger 设置为 DiscardLogger | ||
Logger: core.NewDefaultLogger(core.LogDebug), | ||
}) | ||
// 初始化云端录制服务 API | ||
cloudRecordingAPI := cloudrecording.NewAPI(client) | ||
|
||
// 调用云端录制服务 API 的Acquire接口 | ||
resp, err := cloudRecordingAPI.V1().Acquire().Do(context.TODO(), &v1.AcquirerReqBody{ | ||
Cname: cname, | ||
Uid: uid, | ||
ClientRequest: &v1.AcquirerClientRequest{ | ||
Scene: 0, | ||
ResourceExpiredHour: 24, | ||
}, | ||
}) | ||
// 处理非业务错误 | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
if resp.IsSuccess() { | ||
// 处理业务上成功响应 | ||
log.Printf("resourceId:%s", resp.SuccessRes.ResourceId) | ||
} else { | ||
// 处理业务上失败响应 | ||
log.Printf("resp:%+v", resp) | ||
} | ||
} | ||
``` | ||
更多的示例可在[Example](./examples) 查看 | ||
|
||
## 贡献 | ||
本项目欢迎并接受贡献。如果您在使用中遇到问题或有改进建议,请提出issue或向我们提交Pull Request。 | ||
|
||
# SemVer 版本规范 | ||
本项目使用语义化版本号规范 (SemVer) 来管理版本。格式为 MAJOR.MINOR.PATCH。 | ||
|
||
* MAJOR 版本号表示不向后兼容的重大更改。 | ||
* MINOR 版本号表示向后兼容的新功能或增强。 | ||
* PATCH 版本号表示向后兼容的错误修复和维护。 | ||
有关详细信息,请参阅 [语义化版本](https://semver.org/lang/zh-CN/) 规范。 | ||
|
||
## 参考 | ||
* [Agora API 文档](https://doc.shengwang.cn/) | ||
|
||
## 许可证 | ||
该项目使用MIT许可证,详细信息请参阅LICENSE文件。 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package core | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
type Client interface { | ||
GetAppID() string | ||
DoREST(ctx context.Context, path string, method string, requestBody interface{}) (*BaseResponse, error) | ||
} | ||
|
||
type Config struct { | ||
AppID string | ||
Timeout time.Duration | ||
Credential Credential | ||
|
||
RegionCode RegionArea | ||
Logger Logger | ||
} | ||
|
||
type ClientImpl struct { | ||
appID string | ||
httpClient *http.Client | ||
timeout time.Duration | ||
logger Logger | ||
credential Credential | ||
|
||
module string | ||
domainPool *DomainPool | ||
} | ||
|
||
var _ Client = (*ClientImpl)(nil) | ||
|
||
const defaultTimeout = 10 * time.Second | ||
|
||
func NewClient(config *Config) *ClientImpl { | ||
if config.Timeout == 0 { | ||
config.Timeout = defaultTimeout | ||
} | ||
cc := &http.Client{ | ||
Timeout: config.Timeout, | ||
} | ||
if config.Logger == nil { | ||
config.Logger = defaultLogger | ||
} | ||
return &ClientImpl{ | ||
appID: config.AppID, | ||
credential: config.Credential, | ||
httpClient: cc, | ||
timeout: config.Timeout, | ||
logger: config.Logger, | ||
module: "http client", | ||
domainPool: NewDomainPool(config.RegionCode, config.Logger), | ||
} | ||
} | ||
|
||
func (c *ClientImpl) marshalBody(body interface{}) (io.Reader, error) { | ||
if body == nil { | ||
return nil, nil | ||
} | ||
jsonBody, err := json.Marshal(body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
c.logger.Debugf(context.Background(), c.module, "http request body:%s", jsonBody) | ||
return bytes.NewReader(jsonBody), nil | ||
} | ||
|
||
func (c *ClientImpl) DoREST(ctx context.Context, path string, | ||
method string, requestBody interface{}) (*BaseResponse, error) { | ||
timeoutCtx, cancelFunc := context.WithTimeout(ctx, c.timeout) | ||
defer func() { | ||
_ = cancelFunc | ||
}() | ||
|
||
var ( | ||
resp *BaseResponse | ||
err error | ||
retry int | ||
) | ||
|
||
err = RetryDo(func(retryCount int) error { | ||
var doErr error | ||
|
||
resp, doErr = c.doREST(timeoutCtx, path, method, requestBody) | ||
if doErr != nil { | ||
return NewRetryErr(false, doErr) | ||
} | ||
|
||
statusCode := resp.HttpStatusCode | ||
switch { | ||
case statusCode >= 200 && statusCode <= 300: | ||
return nil | ||
case statusCode >= 400 && statusCode < 410: | ||
c.logger.Debugf(ctx, c.module, "http status code is %d, no retry,http response:%s", statusCode, resp.RawBody) | ||
return &RetryErr{ | ||
false, | ||
NewInternalErr(fmt.Sprintf("http status code is %d, no retry,http response:%s", statusCode, resp.RawBody)), | ||
} | ||
default: | ||
c.logger.Debugf(ctx, c.module, "http status code is %d, retry,http response:%s", statusCode, resp.RawBody) | ||
return fmt.Errorf("http status code is %d, retry", resp.RawBody) | ||
} | ||
}, func() bool { | ||
select { | ||
case <-ctx.Done(): | ||
return true | ||
default: | ||
} | ||
return retry >= 3 | ||
}, func(i int) time.Duration { | ||
return time.Second * time.Duration(i+1) | ||
}, func(err error) { | ||
c.logger.Debugf(ctx, c.module, "http request err:%s", err) | ||
retry++ | ||
}) | ||
if resp != nil { | ||
c.logger.Debugf(ctx, c.module, "http response:%s", resp.RawBody) | ||
} | ||
return resp, err | ||
} | ||
|
||
func (c *ClientImpl) doREST(ctx context.Context, path string, | ||
method string, requestBody interface{}) (*BaseResponse, error) { | ||
|
||
var ( | ||
err error | ||
resp *http.Response | ||
req *http.Request | ||
) | ||
|
||
if err = c.domainPool.SelectBestDomain(ctx); err != nil { | ||
return nil, err | ||
} | ||
|
||
doHttpRequest := func() error { | ||
req, err = c.createRequest(ctx, path, method, requestBody) | ||
if err != nil { | ||
return err | ||
} | ||
resp, err = c.httpClient.Do(req) | ||
return err | ||
} | ||
err = RetryDo( | ||
func(retryCount int) error { | ||
c.logger.Debugf(ctx, c.module, "http retry attempt:%d", retryCount) | ||
return doHttpRequest() | ||
}, | ||
func() bool { | ||
select { | ||
case <-ctx.Done(): | ||
return true | ||
default: | ||
} | ||
return false | ||
}, | ||
func(i int) time.Duration { | ||
if i == 0 { | ||
return 0 * time.Second | ||
} | ||
return 500 * time.Millisecond | ||
}, | ||
func(err error) { | ||
c.logger.Debugf(ctx, c.module, "http request err:%s", err) | ||
c.domainPool.NextRegion() | ||
}, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer func() { | ||
_ = resp.Body.Close() | ||
}() | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &BaseResponse{ | ||
RawBody: body, | ||
HttpStatusCode: resp.StatusCode, | ||
}, nil | ||
} | ||
|
||
func (c *ClientImpl) addCredential(req *http.Request) { | ||
if c.credential != nil { | ||
c.credential.Visit(req) | ||
} | ||
} | ||
|
||
func (c *ClientImpl) GetAppID() string { | ||
return c.appID | ||
} | ||
|
||
func (c *ClientImpl) createRequest(ctx context.Context, path string, | ||
method string, requestBody interface{}) (*http.Request, error) { | ||
url := c.domainPool.GetCurrentUrl() + path | ||
|
||
c.logger.Debugf(ctx, "create request url:%s", url) | ||
jsonBody, err := c.marshalBody(requestBody) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req, err := http.NewRequestWithContext(ctx, method, url, jsonBody) | ||
if err != nil { | ||
return nil, err | ||
} | ||
c.addCredential(req) | ||
req.Header.Set("Content-Type", "application/json;charset=utf-8") | ||
|
||
return req, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package core | ||
|
||
import ( | ||
"net/http" | ||
) | ||
|
||
type CredentialType int | ||
|
||
const ( | ||
BasicAuth CredentialType = iota | ||
) | ||
|
||
type Credential interface { | ||
Type() CredentialType | ||
Visit(r *http.Request) | ||
} | ||
|
||
type BasicAuthCredential struct { | ||
Username string | ||
Password string | ||
} | ||
|
||
func NewBasicAuthCredential(username string, password string) *BasicAuthCredential { | ||
return &BasicAuthCredential{ | ||
Username: username, | ||
Password: password, | ||
} | ||
} | ||
|
||
func (b *BasicAuthCredential) Type() CredentialType { | ||
return BasicAuth | ||
} | ||
|
||
func (b *BasicAuthCredential) Visit(r *http.Request) { | ||
r.SetBasicAuth(b.Username, b.Password) | ||
} |
Oops, something went wrong.