diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1f07e73..21a1744 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,6 +10,68 @@ on: branches: [ "main" ] jobs: + govulncheck: + runs-on: ubuntu-latest + name: govulncheck + steps: + - id: govulncheck + uses: golang/govulncheck-action@v1 + + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.18' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + # Require: The version of golangci-lint to use. + # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. + # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. + version: v1.56.2 + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + args: --config=.golangci.yml + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true, then all caching functionality will be completely disabled, + # takes precedence over all other caching options. + # skip-cache: true + + # Optional: if set to true, then the action won't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. + # install-mode: "goinstall" + + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.18' + + - name: Test + run: go test -v ./... build: runs-on: ubuntu-latest @@ -17,12 +79,9 @@ jobs: - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.18' - - name: Test - run: go test -v ./... - - name: Build run: go build -v ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ba04a48 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..79c86f1 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +BASE_REGISTRY ?= docker.io/ + +.PHONY: lint test build + +lint: + @echo ">>running lint" + @docker run --rm -t \ + -v $(PWD):/app -w /app \ + $(BASE_REGISTRY)golangci/golangci-lint:v1.56.2 golangci-lint -v run --config=./.golangci.yml + +test: + @go test -v ./... + +build: + @go build -v ./... \ No newline at end of file diff --git a/README.md b/README.md index 947aa94..35cb948 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Agora REST Client for Go +[![Go](https://github.com/AgoraIO-Community/agora-rest-client-go/actions/workflows/go.yml/badge.svg)](https://github.com/seymourtang/agora-rest-client-go/actions/workflows/go.yml) `agora-rest-client-go`是用Go语言编写的一个开源项目,专门为 Agora REST API设计。它包含了 Agora 官方提供的REST API接口的包装和内部实现,可以帮助开发者更加方便的集成服务端Agora REST API。 ## 特性 diff --git a/core/client.go b/core/client.go index 6013e2d..c41fb21 100644 --- a/core/client.go +++ b/core/client.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "net/http" "time" @@ -12,13 +11,14 @@ import ( type Client interface { GetAppID() string + GetLogger() Logger DoREST(ctx context.Context, path string, method string, requestBody interface{}) (*BaseResponse, error) } type Config struct { - AppID string - Timeout time.Duration - Credential Credential + AppID string + HttpTimeout time.Duration + Credential Credential RegionCode RegionArea Logger Logger @@ -35,16 +35,20 @@ type ClientImpl struct { domainPool *DomainPool } +func (c *ClientImpl) GetLogger() Logger { + return c.logger +} + var _ Client = (*ClientImpl)(nil) -const defaultTimeout = 10 * time.Second +const defaultHttpTimeout = 10 * time.Second func NewClient(config *Config) *ClientImpl { - if config.Timeout == 0 { - config.Timeout = defaultTimeout + if config.HttpTimeout == 0 { + config.HttpTimeout = defaultHttpTimeout } cc := &http.Client{ - Timeout: config.Timeout, + Timeout: config.HttpTimeout, } if config.Logger == nil { config.Logger = defaultLogger @@ -53,7 +57,7 @@ func NewClient(config *Config) *ClientImpl { appID: config.AppID, credential: config.Credential, httpClient: cc, - timeout: config.Timeout, + timeout: config.HttpTimeout, logger: config.Logger, module: "http client", domainPool: NewDomainPool(config.RegionCode, config.Logger), @@ -72,75 +76,25 @@ func (c *ClientImpl) marshalBody(body interface{}) (io.Reader, error) { 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) { +const doRESTTimeout = 10 * time.Second +func (c *ClientImpl) DoREST(ctx context.Context, path string, + method string, requestBody interface{}, +) (*BaseResponse, error) { var ( err error resp *http.Response req *http.Request ) + timeoutCtx, cancel := context.WithTimeout(ctx, doRESTTimeout) + defer cancel() - if err = c.domainPool.SelectBestDomain(ctx); err != nil { + if err = c.domainPool.SelectBestDomain(timeoutCtx); err != nil { return nil, err } doHttpRequest := func() error { - req, err = c.createRequest(ctx, path, method, requestBody) + req, err = c.createRequest(timeoutCtx, path, method, requestBody) if err != nil { return err } @@ -149,12 +103,12 @@ func (c *ClientImpl) doREST(ctx context.Context, path string, } err = RetryDo( func(retryCount int) error { - c.logger.Debugf(ctx, c.module, "http retry attempt:%d", retryCount) + c.logger.Debugf(timeoutCtx, c.module, "http retry attempt:%d", retryCount) return doHttpRequest() }, func() bool { select { - case <-ctx.Done(): + case <-timeoutCtx.Done(): return true default: } @@ -167,7 +121,7 @@ func (c *ClientImpl) doREST(ctx context.Context, path string, return 500 * time.Millisecond }, func(err error) { - c.logger.Debugf(ctx, c.module, "http request err:%s", err) + c.logger.Debugf(timeoutCtx, c.module, "http request err:%s", err) c.domainPool.NextRegion() }, ) @@ -182,6 +136,7 @@ func (c *ClientImpl) doREST(ctx context.Context, path string, if err != nil { return nil, err } + c.logger.Debugf(ctx, c.module, "http response:%s", body) return &BaseResponse{ RawBody: body, @@ -200,7 +155,8 @@ func (c *ClientImpl) GetAppID() string { } func (c *ClientImpl) createRequest(ctx context.Context, path string, - method string, requestBody interface{}) (*http.Request, error) { + method string, requestBody interface{}, +) (*http.Request, error) { url := c.domainPool.GetCurrentUrl() + path c.logger.Debugf(ctx, "create request url:%s", url) diff --git a/core/domain.go b/core/domain.go index 015911b..59fb256 100644 --- a/core/domain.go +++ b/core/domain.go @@ -99,11 +99,10 @@ type DomainPool struct { currentRegionPrefixes []string locker sync.Mutex - resolver DomainResolver - lastUpdate time.Time - majorDomainHasErr bool - logger Logger - module string + resolver DomainResolver + lastUpdate time.Time + logger Logger + module string } func NewDomainPool(domainArea RegionArea, logger Logger) *DomainPool { diff --git a/core/log.go b/core/log.go index 96223a7..be8a92a 100644 --- a/core/log.go +++ b/core/log.go @@ -89,50 +89,49 @@ func NewDefaultLogger(level LogLevel) *sampleLogger { func (d *sampleLogger) Debug(ctx context.Context, module string, v ...interface{}) { if d.level <= LogDebug { - d.DEBUG.Output(2, fmt.Sprintln(v...)) + _ = d.DEBUG.Output(2, fmt.Sprintln(v...)) } } func (d *sampleLogger) Debugf(ctx context.Context, module string, format string, v ...interface{}) { if d.level <= LogDebug { - d.DEBUG.Output(2, fmt.Sprintf(format, v...)) + _ = d.DEBUG.Output(2, fmt.Sprintf(format, v...)) } } func (d *sampleLogger) Error(ctx context.Context, module string, v ...interface{}) { if d.level <= LogErr { - d.ERROR.Output(2, fmt.Sprintln(v...)) + _ = d.ERROR.Output(2, fmt.Sprintln(v...)) } - } func (d *sampleLogger) Errorf(ctx context.Context, module string, format string, v ...interface{}) { if d.level <= LogErr { - d.ERROR.Output(2, fmt.Sprintf(format, v...)) + _ = d.ERROR.Output(2, fmt.Sprintf(format, v...)) } } func (d *sampleLogger) Info(ctx context.Context, module string, v ...interface{}) { if d.level >= LogInfo { - d.INFO.Output(2, fmt.Sprintln(v...)) + _ = d.INFO.Output(2, fmt.Sprintln(v...)) } } func (d *sampleLogger) Infof(ctx context.Context, module string, format string, v ...interface{}) { if d.level >= LogInfo { - d.WARN.Output(2, fmt.Sprintln(v...)) + _ = d.WARN.Output(2, fmt.Sprintln(v...)) } } func (d *sampleLogger) Warn(ctx context.Context, module string, v ...interface{}) { if d.level <= LogWarning { - d.WARN.Output(2, fmt.Sprintln(v...)) + _ = d.WARN.Output(2, fmt.Sprintln(v...)) } } func (d *sampleLogger) Warnf(ctx context.Context, module string, format string, v ...interface{}) { if d.level <= LogWarning { - d.WARN.Output(2, fmt.Sprintf(format, v...)) + _ = d.WARN.Output(2, fmt.Sprintf(format, v...)) } } diff --git a/examples/cloudrecording/main.go b/examples/cloudrecording/main.go index 1170f1f..f4ef659 100644 --- a/examples/cloudrecording/main.go +++ b/examples/cloudrecording/main.go @@ -30,24 +30,22 @@ var ( region core.RegionArea = core.CN ) -var ( - // 选择你的存储配置 第三方云存储地区说明详情见 https://doc.shengwang.cn/api-ref/cloud-recording/restful/region-vendor - // 配置存储需要的参数 - storageConfig = &v1.StorageConfig{ - Vendor: 0, - Region: 0, - Bucket: "", - AccessKey: "", - SecretKey: "", - FileNamePrefix: []string{ - "", - }, - } -) +// 选择你的存储配置 第三方云存储地区说明详情见 https://doc.shengwang.cn/api-ref/cloud-recording/restful/region-vendor +// 配置存储需要的参数 +var storageConfig = &v1.StorageConfig{ + Vendor: 0, + Region: 0, + Bucket: "", + AccessKey: "", + SecretKey: "", + FileNamePrefix: []string{ + "", + }, +} func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) - err := godotenv.Load("/Users/admin/go/src/agora-rest-client-go/examples/cloudrecording/.env") + err := godotenv.Load(".env") if err != nil { log.Fatal(err) } @@ -110,8 +108,6 @@ func main() { // MixRecording hls&mp4 func MixRecording() { - mode := "mix" - ctx := context.Background() c := core.NewClient(&core.Config{ AppID: appId, @@ -120,15 +116,9 @@ func MixRecording() { Logger: core.NewDefaultLogger(core.LogDebug), }) - cloudRecordingAPI := cloudrecording.NewAPI(c) - - resp, err := cloudRecordingAPI.V1().Acquire().Do(ctx, &v1.AcquirerReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.AcquirerClientRequest{ - Scene: 0, - ResourceExpiredHour: 24, - }, + mixRecordingV1 := cloudrecording.NewAPI(c).V1().MixRecording() + resp, err := mixRecordingV1.Acquire().Do(ctx, cname, uid, &v1.AcquirerMixRecodingClientRequest{ + ResourceExpiredHour: 24, }) if err != nil { log.Fatal(err) @@ -139,38 +129,35 @@ func MixRecording() { log.Printf("start resp:%+v", resp.ErrResponse) } - starterResp, err := cloudRecordingAPI.V1().Start().Do(ctx, resp.SuccessRes.ResourceId, mode, &v1.StartReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.StartClientRequest{ - Token: token, - RecordingConfig: &v1.RecordingConfig{ - ChannelType: 1, - StreamTypes: 2, - AudioProfile: 2, - MaxIdleTime: 30, - TranscodingConfig: &v1.TranscodingConfig{ - Width: 640, - Height: 260, - FPS: 15, - BitRate: 500, - MixedVideoLayout: 0, - BackgroundColor: "#000000", - }, - SubscribeAudioUIDs: []string{ - "#allstream#", - }, - SubscribeVideoUIDs: []string{ - "#allstream#", - }, + starterResp, err := mixRecordingV1.Start().Do(ctx, resp.SuccessRes.ResourceId, cname, uid, &v1.StartMixRecordingClientRequest{ + Token: token, + RecordingConfig: &v1.RecordingConfig{ + ChannelType: 1, + StreamTypes: 2, + AudioProfile: 2, + MaxIdleTime: 30, + TranscodingConfig: &v1.TranscodingConfig{ + Width: 640, + Height: 260, + FPS: 15, + BitRate: 500, + MixedVideoLayout: 0, + BackgroundColor: "#000000", }, - RecordingFileConfig: &v1.RecordingFileConfig{ - AvFileType: []string{ - "hls", - }, + SubscribeAudioUIDs: []string{ + "#allstream#", + }, + SubscribeVideoUIDs: []string{ + "#allstream#", }, - StorageConfig: storageConfig, }, + RecordingFileConfig: &v1.RecordingFileConfig{ + AvFileType: []string{ + "hls", + "mp4", + }, + }, + StorageConfig: storageConfig, }) if err != nil { log.Fatalln(err) @@ -183,11 +170,11 @@ func MixRecording() { startSuccessResp := starterResp.SuccessResp defer func() { - stopResp, err := cloudRecordingAPI.V1().Stop().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode, &v1.StopReqBody{ + stopResp, err := mixRecordingV1.Stop().DoHLSAndMP4(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, &v1.StopReqBody{ Cname: cname, Uid: uid, ClientRequest: &v1.StopClientRequest{ - AsyncStop: true, + AsyncStop: false, }, }) if err != nil { @@ -198,32 +185,10 @@ func MixRecording() { } else { log.Fatalf("stop failed:%+v", &stopResp.ErrResponse) } - stopSuccess := stopResp.SuccessResp - var stopServerResponse interface{} - switch stopSuccess.GetServerResponseMode() { - case v1.StopServerResponseUnknownMode: - log.Fatalln("unknown mode") - case v1.StopIndividualRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopIndividualRecordingServerResponseMode) - stopServerResponse = stopSuccess.GetIndividualRecordingServerResponse() - case v1.StopIndividualVideoScreenshotServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopIndividualVideoScreenshotServerResponseMode) - stopServerResponse = stopSuccess.GetIndividualVideoScreenshotServerResponse() - case v1.StopMixRecordingHlsServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopMixRecordingHlsServerResponseMode) - stopServerResponse = stopSuccess.GetMixRecordingHLSServerResponse() - case v1.StopMixRecordingHlsAndMp4ServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopMixRecordingHlsAndMp4ServerResponseMode) - stopServerResponse = stopSuccess.GetMixRecordingHLSAndMP4ServerResponse() - case v1.StopWebRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopWebRecordingServerResponseMode) - stopServerResponse = stopSuccess.GetWebRecordingServerResponse() - } - log.Printf("stopServerResponse:%+v", stopServerResponse) - + log.Printf("stopServerResponse:%+v", stopResp.SuccessResp.ServerResponse) }() - queryResp, err := cloudRecordingAPI.V1().Query().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode) + queryResp, err := mixRecordingV1.Query().DoHLSAndMP4(ctx, startSuccessResp.ResourceId, startSuccessResp.SID) if err != nil { log.Fatalln(err) } @@ -234,61 +199,35 @@ func MixRecording() { return } - var queryServerResponse interface{} - - querySuccess := queryResp.SuccessResp - switch querySuccess.GetServerResponseMode() { - case v1.QueryServerResponseUnknownMode: - log.Fatalln("unknown mode") - case v1.QueryIndividualRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryIndividualRecordingServerResponseMode) - queryServerResponse = querySuccess.GetIndividualRecordingServerResponse() - case v1.QueryIndividualVideoScreenshotServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryIndividualVideoScreenshotServerResponseMode) - queryServerResponse = querySuccess.GetIndividualVideoScreenshotServerResponse() - case v1.QueryMixRecordingHlsServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryMixRecordingHlsServerResponseMode) - queryServerResponse = querySuccess.GetMixRecordingHLSServerResponse() - case v1.QueryMixRecordingHlsAndMp4ServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryMixRecordingHlsAndMp4ServerResponseMode) - queryServerResponse = querySuccess.GetMixRecordingHLSAndMP4ServerResponse() - case v1.QueryWebRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryWebRecordingServerResponseMode) - queryServerResponse = querySuccess.GetWebRecording2CDNServerResponse() - } - - log.Printf("queryServerResponse:%+v", queryServerResponse) + log.Printf("queryServerResponse:%+v", queryResp.SuccessResp.ServerResponse) time.Sleep(3 * time.Second) - updateLayoutResp, err := cloudRecordingAPI.V1().UpdateLayout().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode, &v1.UpdateLayoutReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.UpdateLayoutClientRequest{ - MixedVideoLayout: 3, - BackgroundColor: "#FF0000", - LayoutConfig: []v1.UpdateLayoutConfig{ - { - UID: "22", - XAxis: 0.1, - YAxis: 0.1, - Width: 0.1, - Height: 0.1, - Alpha: 1, - RenderMode: 1, - }, - { - UID: "2", - XAxis: 0.2, - YAxis: 0.2, - Width: 0.1, - Height: 0.1, - Alpha: 1, - RenderMode: 1, - }, + updateLayoutResp, err := mixRecordingV1.UpdateLayout().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, cname, uid, &v1.UpdateLayoutUpdateMixRecordingClientRequest{ + MixedVideoLayout: 3, + BackgroundColor: "#FF0000", + LayoutConfig: []v1.UpdateLayoutConfig{ + { + UID: "22", + XAxis: 0.1, + YAxis: 0.1, + Width: 0.1, + Height: 0.1, + Alpha: 1, + RenderMode: 1, + }, + { + UID: "2", + XAxis: 0.2, + YAxis: 0.2, + Width: 0.1, + Height: 0.1, + Alpha: 1, + RenderMode: 1, }, }, - }) + }, + ) if err != nil { log.Fatalln(err) } @@ -298,26 +237,22 @@ func MixRecording() { log.Printf("updateLayout failed:%+v", updateLayoutResp.ErrResponse) return } + time.Sleep(3 * time.Second) - updateResp, err := cloudRecordingAPI.V1().Update().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode, &v1.UpdateReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.UpdateClientRequest{ - StreamSubscribe: &v1.UpdateStreamSubscribe{ - AudioUidList: &v1.UpdateAudioUIDList{ - SubscribeAudioUIDs: []string{ - "#allstream#", - }, + updateResp, err := mixRecordingV1.Update().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, cname, uid, &v1.UpdateMixRecordingClientRequest{ + StreamSubscribe: &v1.UpdateStreamSubscribe{ + AudioUidList: &v1.UpdateAudioUIDList{ + SubscribeAudioUIDs: []string{ + "#allstream#", }, - VideoUidList: &v1.UpdateVideoUIDList{ - SubscribeVideoUIDs: []string{ - "#allstream#", - }, + }, + VideoUidList: &v1.UpdateVideoUIDList{ + SubscribeVideoUIDs: []string{ + "#allstream#", }, }, }, }) - if err != nil { log.Fatalln(err) } @@ -328,13 +263,10 @@ func MixRecording() { return } time.Sleep(2 * time.Second) - } // IndividualRecording hls func IndividualRecording() { - mode := "individual" - ctx := context.Background() c := core.NewClient(&core.Config{ AppID: appId, @@ -343,15 +275,10 @@ func IndividualRecording() { Logger: core.NewDefaultLogger(core.LogDebug), }) - cloudRecordingAPI := cloudrecording.NewAPI(c) + individualRecordingV1 := cloudrecording.NewAPI(c).V1().IndividualRecording() - resp, err := cloudRecordingAPI.V1().Acquire().Do(ctx, &v1.AcquirerReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.AcquirerClientRequest{ - Scene: 0, - ResourceExpiredHour: 24, - }, + resp, err := individualRecordingV1.Acquire().Do(ctx, cname, uid, false, &v1.AcquirerIndividualRecodingClientRequest{ + ResourceExpiredHour: 24, }) if err != nil { log.Fatal(err) @@ -362,27 +289,23 @@ func IndividualRecording() { log.Fatalf("acquire failed:%+v", resp) } - starterResp, err := cloudRecordingAPI.V1().Start().Do(ctx, resp.SuccessRes.ResourceId, mode, &v1.StartReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.StartClientRequest{ - Token: token, - RecordingConfig: &v1.RecordingConfig{ - ChannelType: 1, - StreamTypes: 2, - SubscribeAudioUIDs: []string{ - "22", - "456", - }, - SubscribeUidGroup: 0, + starterResp, err := individualRecordingV1.Start().Do(ctx, resp.SuccessRes.ResourceId, cname, uid, &v1.StartIndividualRecordingClientRequest{ + Token: token, + RecordingConfig: &v1.RecordingConfig{ + ChannelType: 1, + StreamTypes: 2, + SubscribeAudioUIDs: []string{ + "22", + "456", }, - RecordingFileConfig: &v1.RecordingFileConfig{ - AvFileType: []string{ - "hls", - }, + SubscribeUidGroup: 0, + }, + RecordingFileConfig: &v1.RecordingFileConfig{ + AvFileType: []string{ + "hls", }, - StorageConfig: storageConfig, }, + StorageConfig: storageConfig, }) if err != nil { log.Fatal(err) @@ -395,11 +318,11 @@ func IndividualRecording() { startSuccessResp := starterResp.SuccessResp defer func() { - stopResp, err := cloudRecordingAPI.V1().Stop().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode, &v1.StopReqBody{ + stopResp, err := individualRecordingV1.Stop().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, &v1.StopReqBody{ Cname: cname, Uid: uid, ClientRequest: &v1.StopClientRequest{ - AsyncStop: true, + AsyncStop: false, }, }) if err != nil { @@ -410,30 +333,10 @@ func IndividualRecording() { } else { log.Fatalf("stopResp failed:%+v", &stopResp.ErrResponse) } - stopSuccess := stopResp.SuccessResp - var stopServerResponse interface{} - switch stopSuccess.GetServerResponseMode() { - case v1.StopServerResponseUnknownMode: - log.Fatalln("unknown mode") - case v1.StopIndividualRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopIndividualRecordingServerResponseMode) - stopServerResponse = stopSuccess.GetIndividualRecordingServerResponse() - case v1.StopIndividualVideoScreenshotServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopIndividualVideoScreenshotServerResponseMode) - stopServerResponse = stopSuccess.GetIndividualVideoScreenshotServerResponse() - case v1.StopMixRecordingHlsServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopMixRecordingHlsServerResponseMode) - stopServerResponse = stopSuccess.GetMixRecordingHLSServerResponse() - case v1.StopMixRecordingHlsAndMp4ServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopMixRecordingHlsAndMp4ServerResponseMode) - stopServerResponse = stopSuccess.GetMixRecordingHLSAndMP4ServerResponse() - case v1.StopWebRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.StopWebRecordingServerResponseMode) - stopServerResponse = stopSuccess.GetWebRecordingServerResponse() - } - log.Printf("stopServerResponse:%+v", stopServerResponse) + + log.Printf("stopServerResponse:%+v", stopResp.SuccessResp.ServerResponse) }() - queryResp, err := cloudRecordingAPI.V1().Query().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode) + queryResp, err := individualRecordingV1.Query().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID) if err != nil { log.Fatalln(err) } @@ -443,51 +346,23 @@ func IndividualRecording() { log.Fatalf("queryResp failed:%+v", queryResp.ErrResponse) } - var queryServerResponse interface{} - - querySuccess := queryResp.SuccessResp - switch querySuccess.GetServerResponseMode() { - case v1.QueryServerResponseUnknownMode: - log.Fatalln("unknown mode") - case v1.QueryIndividualRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryIndividualRecordingServerResponseMode) - queryServerResponse = querySuccess.GetIndividualRecordingServerResponse() - case v1.QueryIndividualVideoScreenshotServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryIndividualVideoScreenshotServerResponseMode) - queryServerResponse = querySuccess.GetIndividualVideoScreenshotServerResponse() - case v1.QueryMixRecordingHlsServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryMixRecordingHlsServerResponseMode) - queryServerResponse = querySuccess.GetMixRecordingHLSServerResponse() - case v1.QueryMixRecordingHlsAndMp4ServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryMixRecordingHlsAndMp4ServerResponseMode) - queryServerResponse = querySuccess.GetMixRecordingHLSAndMP4ServerResponse() - case v1.QueryWebRecordingServerResponseMode: - log.Printf("serverResponseMode:%d", v1.QueryWebRecordingServerResponseMode) - queryServerResponse = querySuccess.GetWebRecording2CDNServerResponse() - } - - log.Printf("queryServerResponse:%+v", queryServerResponse) + log.Printf("queryServerResponse:%+v", queryResp.SuccessResp.ServerResponse) time.Sleep(3 * time.Second) - updateResp, err := cloudRecordingAPI.V1().Update().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, mode, &v1.UpdateReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &v1.UpdateClientRequest{ - StreamSubscribe: &v1.UpdateStreamSubscribe{ - AudioUidList: &v1.UpdateAudioUIDList{ - SubscribeAudioUIDs: []string{ - "999", - }, + updateResp, err := individualRecordingV1.Update().Do(ctx, startSuccessResp.ResourceId, startSuccessResp.SID, cname, uid, &v1.UpdateIndividualRecordingClientRequest{ + StreamSubscribe: &v1.UpdateStreamSubscribe{ + AudioUidList: &v1.UpdateAudioUIDList{ + SubscribeAudioUIDs: []string{ + "999", }, - VideoUidList: &v1.UpdateVideoUIDList{ - SubscribeVideoUIDs: []string{ - "999", - }, + }, + VideoUidList: &v1.UpdateVideoUIDList{ + SubscribeVideoUIDs: []string{ + "999", }, }, }, }) - if err != nil { log.Fatalln(err) } diff --git a/services/cloudrecording/cloudrecording.go b/services/cloudrecording/cloudrecording.go index 392bf03..b9600b7 100644 --- a/services/cloudrecording/cloudrecording.go +++ b/services/cloudrecording/cloudrecording.go @@ -3,6 +3,8 @@ package cloudrecording import ( "github.com/AgoraIO-Community/agora-rest-client-go/core" v1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" + "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1/individualrecording" + "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1/mixrecording" "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1/webrecording" ) @@ -21,5 +23,5 @@ func (a *API) buildPrefixPath() string { } func (a *API) V1() *v1.BaseCollection { - return v1.NewCollection(a.buildPrefixPath(), a.client, webrecording.NewWebRecording()) + return v1.NewCollection(a.buildPrefixPath(), a.client, webrecording.NewWebRecording(), mixrecording.NewMixRecording(), individualrecording.NewIndividualRecording()) } diff --git a/services/cloudrecording/v1/individualrecording.go b/services/cloudrecording/v1/individualrecording.go new file mode 100644 index 0000000..d0c4763 --- /dev/null +++ b/services/cloudrecording/v1/individualrecording.go @@ -0,0 +1,146 @@ +package v1 + +import ( + "context" +) + +type AcquirerIndividualRecodingClientRequest struct { + // 云端录制 RESTful API 的调用时效。从成功开启云端录制并获得 sid (录制 ID)后开始计算。单位为小时。 + ResourceExpiredHour int + + // 另一路或几路录制任务的 resourceId。该字段用于排除指定的录制资源,以便新发起的录制任务可以使用新区域的资源,实现跨区域多路录制。 + ExcludeResourceIds []string + + // 指定使用某个区域的资源进行录制。支持取值如下: + // + // 0: 根据发起请求的区域就近调用资源。 + // + // 1: 中国。 + // + // 2: 东南亚。 + // + // 3: 欧洲。 + // + // 4: 北美。 + RegionAffinity int +} + +type AcquireIndividualRecording interface { + // Do Acquire a resource for individual recording. + // + // cname: Channel name. + // + // uid:RTC User ID. + // + // enablePostponeTranscodingMix: Whether to enable the postpone transcoding mix. + // + // clientRequest: AcquirerIndividualRecodingClientRequest + Do(ctx context.Context, cname string, uid string, enablePostponeTranscodingMix bool, clientRequest *AcquirerIndividualRecodingClientRequest) (*AcquirerResp, error) +} + +type StartIndividualRecordingClientRequest struct { + // Token 用于鉴权的动态密钥(Token)。如果你的项目已启用 App 证书,则务必在该字段中传入你项目的动态密钥 + Token string + + // StorageConfig 第三方云存储的配置项 + StorageConfig *StorageConfig + + // RecordingConfig 录制的音视频流配置项 + RecordingConfig *RecordingConfig + + // RecordingFileConfig 录制文件的配置项 + RecordingFileConfig *RecordingFileConfig + + // SnapshotConfig 视频截图的配置项 + SnapshotConfig *SnapshotConfig + + // AppsCollection 应用配置项 + AppsCollection *AppsCollection + + // TranscodeOptions 延时转码或延时混音下,生成的录制文件的配置项 + TranscodeOptions *TranscodeOptions +} + +type StartIndividualRecording interface { + // Do Start individual recording. + // + // resourceID: Resource ID. + // + // cname: Channel name. + // + // uid:RTC User ID. + // + // clientRequest: StartIndividualRecordingClientRequest + Do(ctx context.Context, resourceID string, cname string, uid string, clientRequest *StartIndividualRecordingClientRequest) (*StarterResp, error) +} + +type QueryIndividualRecordingSuccessResp struct { + ResourceId string + SID string + ServerResponse QueryIndividualRecordingServerResponse +} + +type QueryIndividualRecordingResp struct { + Response + SuccessResp QueryIndividualRecordingSuccessResp +} + +type QueryIndividualRecordingVideoScreenshotSuccessResp struct { + ResourceId string + SID string + ServerResponse QueryIndividualVideoScreenshotServerResponse +} + +type QueryIndividualRecordingVideoScreenshotResp struct { + Response + SuccessResp QueryIndividualRecordingVideoScreenshotSuccessResp +} + +type QueryIndividualRecording interface { + Do(ctx context.Context, resourceID string, sid string) (*QueryIndividualRecordingResp, error) + DoVideoScreenshot(ctx context.Context, resourceID string, sid string) (*QueryIndividualRecordingVideoScreenshotResp, error) +} + +type UpdateIndividualRecordingClientRequest struct { + StreamSubscribe *UpdateStreamSubscribe +} + +type UpdateIndividualRecording interface { + Do(ctx context.Context, resourceID string, sid string, cname string, uid string, clientRequest *UpdateIndividualRecordingClientRequest) (*UpdateResp, error) +} + +type StopIndividualRecordingSuccessResp struct { + ResourceId string + SID string + ServerResponse StopIndividualRecordingServerResponse +} + +type StopIndividualRecordingVideoScreenshotSuccessResp struct { + ResourceId string + SID string + ServerResponse StopIndividualVideoScreenshotServerResponse +} + +type StopIndividualRecordingResp struct { + Response + SuccessResp StopIndividualRecordingSuccessResp +} + +type StopIndividualRecordingVideoScreenshotResp struct { + Response + SuccessResp StopIndividualRecordingVideoScreenshotSuccessResp +} + +type StopIndividualRecording interface { + Do(ctx context.Context, resourceID string, sid string, payload *StopReqBody) (*StopIndividualRecordingResp, error) + DoVideoScreenshot(ctx context.Context, resourceID string, sid string, payload *StopReqBody) (*StopIndividualRecordingVideoScreenshotResp, error) +} + +type IndividualRecording interface { + SetBase(base *BaseCollection) + Acquire() AcquireIndividualRecording + Start() StartIndividualRecording + Query() QueryIndividualRecording + Update() UpdateIndividualRecording + Stop() StopIndividualRecording +} diff --git a/services/cloudrecording/v1/individualrecording/acquire.go b/services/cloudrecording/v1/individualrecording/acquire.go new file mode 100644 index 0000000..efdc15c --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/acquire.go @@ -0,0 +1,30 @@ +package individualrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Acquire struct { + Base *baseV1.Acquire +} + +var _ baseV1.AcquireIndividualRecording = (*Acquire)(nil) + +func (a *Acquire) Do(ctx context.Context, cname string, uid string, enablePostponeTranscodingMix bool, clientRequest *baseV1.AcquirerIndividualRecodingClientRequest) (*baseV1.AcquirerResp, error) { + scene := 0 + if enablePostponeTranscodingMix { + scene = 2 + } + return a.Base.Do(ctx, &baseV1.AcquirerReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.AcquirerClientRequest{ + Scene: scene, + ResourceExpiredHour: clientRequest.ResourceExpiredHour, + ExcludeResourceIds: clientRequest.ExcludeResourceIds, + RegionAffinity: clientRequest.RegionAffinity, + }, + }) +} diff --git a/services/cloudrecording/v1/individualrecording/individualrecording.go b/services/cloudrecording/v1/individualrecording/individualrecording.go new file mode 100644 index 0000000..09fa6c3 --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/individualrecording.go @@ -0,0 +1,39 @@ +package individualrecording + +import ( + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Impl struct { + Base *baseV1.BaseCollection +} + +var _ baseV1.IndividualRecording = (*Impl)(nil) + +func NewIndividualRecording() *Impl { + return &Impl{} +} + +func (i *Impl) SetBase(base *baseV1.BaseCollection) { + i.Base = base +} + +func (i *Impl) Acquire() baseV1.AcquireIndividualRecording { + return &Acquire{Base: i.Base.Acquire()} +} + +func (i *Impl) Start() baseV1.StartIndividualRecording { + return &Starter{Base: i.Base.Start()} +} + +func (i *Impl) Query() baseV1.QueryIndividualRecording { + return &Query{Base: i.Base.Query()} +} + +func (i *Impl) Update() baseV1.UpdateIndividualRecording { + return &Update{Base: i.Base.Update()} +} + +func (i *Impl) Stop() baseV1.StopIndividualRecording { + return &Stop{BaseStop: i.Base.Stop()} +} diff --git a/services/cloudrecording/v1/individualrecording/query.go b/services/cloudrecording/v1/individualrecording/query.go new file mode 100644 index 0000000..2160ba8 --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/query.go @@ -0,0 +1,55 @@ +package individualrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Query struct { + Base *baseV1.Query +} + +var _ baseV1.QueryIndividualRecording = (*Query)(nil) + +func (q *Query) Do(ctx context.Context, resourceID string, sid string) (*baseV1.QueryIndividualRecordingResp, error) { + resp, err := q.Base.Do(ctx, resourceID, sid, baseV1.IndividualMode) + if err != nil { + return nil, err + } + + var individualResp baseV1.QueryIndividualRecordingResp + + individualResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + individualResp.SuccessResp = baseV1.QueryIndividualRecordingSuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetIndividualRecordingServerResponse(), + } + } + + return &individualResp, nil +} + +func (q *Query) DoVideoScreenshot(ctx context.Context, resourceID string, sid string) (*baseV1.QueryIndividualRecordingVideoScreenshotResp, error) { + resp, err := q.Base.Do(ctx, resourceID, sid, baseV1.IndividualMode) + if err != nil { + return nil, err + } + + var individualResp baseV1.QueryIndividualRecordingVideoScreenshotResp + + individualResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + individualResp.SuccessResp = baseV1.QueryIndividualRecordingVideoScreenshotSuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetIndividualVideoScreenshotServerResponse(), + } + } + + return &individualResp, nil +} diff --git a/services/cloudrecording/v1/individualrecording/start.go b/services/cloudrecording/v1/individualrecording/start.go new file mode 100644 index 0000000..fc4abcd --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/start.go @@ -0,0 +1,29 @@ +package individualrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Starter struct { + Base *baseV1.Starter +} + +var _ baseV1.StartIndividualRecording = (*Starter)(nil) + +func (s *Starter) Do(ctx context.Context, resourceID string, cname string, uid string, clientRequest *baseV1.StartIndividualRecordingClientRequest) (*baseV1.StarterResp, error) { + return s.Base.Do(ctx, resourceID, baseV1.IndividualMode, &baseV1.StartReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.StartClientRequest{ + Token: clientRequest.Token, + AppsCollection: clientRequest.AppsCollection, + RecordingConfig: clientRequest.RecordingConfig, + RecordingFileConfig: clientRequest.RecordingFileConfig, + TranscodeOptions: clientRequest.TranscodeOptions, + SnapshotConfig: clientRequest.SnapshotConfig, + StorageConfig: clientRequest.StorageConfig, + }, + }) +} diff --git a/services/cloudrecording/v1/individualrecording/stop.go b/services/cloudrecording/v1/individualrecording/stop.go new file mode 100644 index 0000000..17877f5 --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/stop.go @@ -0,0 +1,55 @@ +package individualrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Stop struct { + BaseStop *baseV1.Stop +} + +var _ baseV1.StopIndividualRecording = (*Stop)(nil) + +func (s *Stop) Do(ctx context.Context, resourceID string, sid string, payload *baseV1.StopReqBody) (*baseV1.StopIndividualRecordingResp, error) { + resp, err := s.BaseStop.Do(ctx, resourceID, sid, baseV1.IndividualMode, payload) + if err != nil { + return nil, err + } + + var individualResp baseV1.StopIndividualRecordingResp + + individualResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + individualResp.SuccessResp = baseV1.StopIndividualRecordingSuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetIndividualRecordingServerResponse(), + } + } + + return &individualResp, nil +} + +func (s *Stop) DoVideoScreenshot(ctx context.Context, resourceID string, sid string, payload *baseV1.StopReqBody) (*baseV1.StopIndividualRecordingVideoScreenshotResp, error) { + resp, err := s.BaseStop.Do(ctx, resourceID, sid, baseV1.IndividualMode, payload) + if err != nil { + return nil, err + } + + var individualResp baseV1.StopIndividualRecordingVideoScreenshotResp + + individualResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + individualResp.SuccessResp = baseV1.StopIndividualRecordingVideoScreenshotSuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetIndividualVideoScreenshotServerResponse(), + } + } + + return &individualResp, nil +} diff --git a/services/cloudrecording/v1/individualrecording/update.go b/services/cloudrecording/v1/individualrecording/update.go new file mode 100644 index 0000000..254a7b4 --- /dev/null +++ b/services/cloudrecording/v1/individualrecording/update.go @@ -0,0 +1,23 @@ +package individualrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Update struct { + Base *baseV1.Update +} + +var _ baseV1.UpdateIndividualRecording = (*Update)(nil) + +func (u *Update) Do(ctx context.Context, resourceID string, sid string, cname string, uid string, clientRequest *baseV1.UpdateIndividualRecordingClientRequest) (*baseV1.UpdateResp, error) { + return u.Base.Do(ctx, resourceID, sid, baseV1.IndividualMode, &baseV1.UpdateReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.UpdateClientRequest{ + StreamSubscribe: clientRequest.StreamSubscribe, + }, + }) +} diff --git a/services/cloudrecording/v1/mixrecording.go b/services/cloudrecording/v1/mixrecording.go new file mode 100644 index 0000000..b20c319 --- /dev/null +++ b/services/cloudrecording/v1/mixrecording.go @@ -0,0 +1,110 @@ +package v1 + +import ( + "context" +) + +type AcquirerMixRecodingClientRequest struct { + ResourceExpiredHour int `json:"resourceExpiredHour"` + ExcludeResourceIds []string `json:"excludeResourceIds,omitempty"` + RegionAffinity int `json:"regionAffinity,omitempty"` +} + +type AcquireMixRecording interface { + Do(ctx context.Context, cname string, uid string, clientRequest *AcquirerMixRecodingClientRequest) (*AcquirerResp, error) +} + +type QueryMixRecordingHLSSuccessResp struct { + ResourceId string + SID string + ServerResponse QueryMixRecordingHLSServerResponse +} + +type QueryMixRecordingHLSResp struct { + Response + SuccessResp QueryMixRecordingHLSSuccessResp +} + +type QueryMixRecordingHLSAndMP4SuccessResp struct { + ResourceId string + SID string + ServerResponse QueryMixRecordingHLSAndMP4ServerResponse +} +type QueryMixRecordingHLSAndMP4Resp struct { + Response + SuccessResp QueryMixRecordingHLSAndMP4SuccessResp +} + +type QueryMixRecording interface { + DoHLS(ctx context.Context, resourceID string, sid string) (*QueryMixRecordingHLSResp, error) + DoHLSAndMP4(ctx context.Context, resourceID string, sid string) (*QueryMixRecordingHLSAndMP4Resp, error) +} + +type StartMixRecordingClientRequest struct { + Token string + RecordingConfig *RecordingConfig + RecordingFileConfig *RecordingFileConfig + StorageConfig *StorageConfig +} + +type StartMixRecording interface { + Do(ctx context.Context, resourceID string, cname string, uid string, clientRequest *StartMixRecordingClientRequest) (*StarterResp, error) +} + +type StopMixRecordingHLSResp struct { + ResourceId string + SID string + ServerResponse StopMixRecordingHLSServerResponse +} + +type StopMixRecordingHLSSuccessResponse struct { + Response + SuccessResp StopMixRecordingHLSResp +} + +type StopMixRecordingHLSAndMP4Resp struct { + ResourceId string + SID string + ServerResponse StopMixRecordingHLSAndMP4ServerResponse +} + +type StopMixRecordingHLSAndMP4SuccessResponse struct { + Response + SuccessResp StopMixRecordingHLSAndMP4Resp +} + +type StopMixRecording interface { + DoHLS(ctx context.Context, resourceID string, sid string, payload *StopReqBody) (*StopMixRecordingHLSSuccessResponse, error) + DoHLSAndMP4(ctx context.Context, resourceID string, sid string, payload *StopReqBody) (*StopMixRecordingHLSAndMP4SuccessResponse, error) +} + +type UpdateMixRecordingClientRequest struct { + StreamSubscribe *UpdateStreamSubscribe +} +type UpdateMixRecording interface { + Do(ctx context.Context, resourceID string, sid string, cname string, uid string, clientRequest *UpdateMixRecordingClientRequest) (*UpdateResp, error) +} + +type UpdateLayoutUpdateMixRecordingClientRequest struct { + MaxResolutionUID string + MixedVideoLayout int + BackgroundColor string + BackgroundImage string + DefaultUserBackgroundImage string + LayoutConfig []UpdateLayoutConfig + BackgroundConfig []BackgroundConfig +} + +type UpdateLayoutMixRecording interface { + Do(ctx context.Context, resourceID string, sid string, cname string, uid string, clientRequest *UpdateLayoutUpdateMixRecordingClientRequest) (*UpdateLayoutResp, error) +} + +type MixRecording interface { + SetBase(base *BaseCollection) + Acquire() AcquireMixRecording + Query() QueryMixRecording + Start() StartMixRecording + Stop() StopMixRecording + Update() UpdateMixRecording + UpdateLayout() UpdateLayoutMixRecording +} diff --git a/services/cloudrecording/v1/mixrecording/acquire.go b/services/cloudrecording/v1/mixrecording/acquire.go new file mode 100644 index 0000000..cb016d4 --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/acquire.go @@ -0,0 +1,26 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Acquire struct { + Base *baseV1.Acquire +} + +var _ baseV1.AcquireMixRecording = (*Acquire)(nil) + +func (a *Acquire) Do(ctx context.Context, cname string, uid string, clientRequest *baseV1.AcquirerMixRecodingClientRequest) (*baseV1.AcquirerResp, error) { + return a.Base.Do(ctx, &baseV1.AcquirerReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.AcquirerClientRequest{ + Scene: 0, + ResourceExpiredHour: clientRequest.ResourceExpiredHour, + ExcludeResourceIds: clientRequest.ExcludeResourceIds, + RegionAffinity: clientRequest.RegionAffinity, + }, + }) +} diff --git a/services/cloudrecording/v1/mixrecording/mixrecording.go b/services/cloudrecording/v1/mixrecording/mixrecording.go new file mode 100644 index 0000000..87638ac --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/mixrecording.go @@ -0,0 +1,43 @@ +package mixrecording + +import ( + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Impl struct { + Base *baseV1.BaseCollection +} + +func NewMixRecording() *Impl { + return &Impl{} +} + +var _ baseV1.MixRecording = (*Impl)(nil) + +func (i *Impl) SetBase(base *baseV1.BaseCollection) { + i.Base = base +} + +func (i *Impl) Acquire() baseV1.AcquireMixRecording { + return &Acquire{Base: i.Base.Acquire()} +} + +func (i *Impl) Query() baseV1.QueryMixRecording { + return &Query{Base: i.Base.Query()} +} + +func (i *Impl) Start() baseV1.StartMixRecording { + return &Starter{Base: i.Base.Start()} +} + +func (i *Impl) Stop() baseV1.StopMixRecording { + return &Stop{Base: i.Base.Stop()} +} + +func (i *Impl) Update() baseV1.UpdateMixRecording { + return &Update{Base: i.Base.Update()} +} + +func (i *Impl) UpdateLayout() baseV1.UpdateLayoutMixRecording { + return &UpdateLayout{Base: i.Base.UpdateLayout()} +} diff --git a/services/cloudrecording/v1/mixrecording/query.go b/services/cloudrecording/v1/mixrecording/query.go new file mode 100644 index 0000000..b23800f --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/query.go @@ -0,0 +1,55 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Query struct { + Base *baseV1.Query +} + +var _ baseV1.QueryMixRecording = (*Query)(nil) + +func (q Query) DoHLS(ctx context.Context, resourceID string, sid string) (*baseV1.QueryMixRecordingHLSResp, error) { + resp, err := q.Base.Do(ctx, resourceID, sid, baseV1.MixMode) + if err != nil { + return nil, err + } + + var mixResp baseV1.QueryMixRecordingHLSResp + + mixResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + mixResp.SuccessResp = baseV1.QueryMixRecordingHLSSuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetMixRecordingHLSServerResponse(), + } + } + + return &mixResp, nil +} + +func (q Query) DoHLSAndMP4(ctx context.Context, resourceID string, sid string) (*baseV1.QueryMixRecordingHLSAndMP4Resp, error) { + resp, err := q.Base.Do(ctx, resourceID, sid, baseV1.MixMode) + if err != nil { + return nil, err + } + + var mixResp baseV1.QueryMixRecordingHLSAndMP4Resp + + mixResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + mixResp.SuccessResp = baseV1.QueryMixRecordingHLSAndMP4SuccessResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetMixRecordingHLSAndMP4ServerResponse(), + } + } + + return &mixResp, nil +} diff --git a/services/cloudrecording/v1/mixrecording/start.go b/services/cloudrecording/v1/mixrecording/start.go new file mode 100644 index 0000000..c0a5728 --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/start.go @@ -0,0 +1,26 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Starter struct { + Base *baseV1.Starter +} + +var _ baseV1.StartMixRecording = (*Starter)(nil) + +func (s *Starter) Do(ctx context.Context, resourceID string, cname string, uid string, clientRequest *baseV1.StartMixRecordingClientRequest) (*baseV1.StarterResp, error) { + return s.Base.Do(ctx, resourceID, baseV1.MixMode, &baseV1.StartReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.StartClientRequest{ + Token: clientRequest.Token, + RecordingFileConfig: clientRequest.RecordingFileConfig, + RecordingConfig: clientRequest.RecordingConfig, + StorageConfig: clientRequest.StorageConfig, + }, + }) +} diff --git a/services/cloudrecording/v1/mixrecording/stop.go b/services/cloudrecording/v1/mixrecording/stop.go new file mode 100644 index 0000000..70d62eb --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/stop.go @@ -0,0 +1,55 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Stop struct { + Base *baseV1.Stop +} + +var _ baseV1.StopMixRecording = (*Stop)(nil) + +func (s *Stop) DoHLS(ctx context.Context, resourceID string, sid string, payload *baseV1.StopReqBody) (*baseV1.StopMixRecordingHLSSuccessResponse, error) { + resp, err := s.Base.Do(ctx, resourceID, sid, baseV1.MixMode, payload) + if err != nil { + return nil, err + } + + var mixResp baseV1.StopMixRecordingHLSSuccessResponse + + mixResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + mixResp.SuccessResp = baseV1.StopMixRecordingHLSResp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetMixRecordingHLSServerResponse(), + } + } + + return &mixResp, nil +} + +func (s *Stop) DoHLSAndMP4(ctx context.Context, resourceID string, sid string, payload *baseV1.StopReqBody) (*baseV1.StopMixRecordingHLSAndMP4SuccessResponse, error) { + resp, err := s.Base.Do(ctx, resourceID, sid, baseV1.MixMode, payload) + if err != nil { + return nil, err + } + + var mixResp baseV1.StopMixRecordingHLSAndMP4SuccessResponse + + mixResp.Response = resp.Response + if resp.IsSuccess() { + successResp := resp.SuccessResp + mixResp.SuccessResp = baseV1.StopMixRecordingHLSAndMP4Resp{ + ResourceId: successResp.ResourceId, + SID: successResp.SID, + ServerResponse: *successResp.GetMixRecordingHLSAndMP4ServerResponse(), + } + } + + return &mixResp, nil +} diff --git a/services/cloudrecording/v1/mixrecording/update.go b/services/cloudrecording/v1/mixrecording/update.go new file mode 100644 index 0000000..bd67556 --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/update.go @@ -0,0 +1,25 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type Update struct { + Base *baseV1.Update +} + +var _ baseV1.UpdateMixRecording = (*Update)(nil) + +func (i *Update) Do(ctx context.Context, resourceID string, sid string, cname string, uid string, + clientRequest *baseV1.UpdateMixRecordingClientRequest, +) (*baseV1.UpdateResp, error) { + return i.Base.Do(ctx, resourceID, sid, baseV1.MixMode, &baseV1.UpdateReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.UpdateClientRequest{ + StreamSubscribe: clientRequest.StreamSubscribe, + }, + }) +} diff --git a/services/cloudrecording/v1/mixrecording/updatelayout.go b/services/cloudrecording/v1/mixrecording/updatelayout.go new file mode 100644 index 0000000..3b29279 --- /dev/null +++ b/services/cloudrecording/v1/mixrecording/updatelayout.go @@ -0,0 +1,31 @@ +package mixrecording + +import ( + "context" + + baseV1 "github.com/AgoraIO-Community/agora-rest-client-go/services/cloudrecording/v1" +) + +type UpdateLayout struct { + Base *baseV1.UpdateLayout +} + +var _ baseV1.UpdateLayoutMixRecording = (*UpdateLayout)(nil) + +func (u *UpdateLayout) Do(ctx context.Context, resourceID string, sid string, cname string, uid string, + clientRequest *baseV1.UpdateLayoutUpdateMixRecordingClientRequest, +) (*baseV1.UpdateLayoutResp, error) { + return u.Base.Do(ctx, resourceID, sid, baseV1.MixMode, &baseV1.UpdateLayoutReqBody{ + Cname: cname, + Uid: uid, + ClientRequest: &baseV1.UpdateLayoutClientRequest{ + MaxResolutionUID: clientRequest.MaxResolutionUID, + MixedVideoLayout: clientRequest.MixedVideoLayout, + BackgroundColor: clientRequest.BackgroundColor, + BackgroundImage: clientRequest.BackgroundImage, + DefaultUserBackgroundImage: clientRequest.DefaultUserBackgroundImage, + LayoutConfig: clientRequest.LayoutConfig, + BackgroundConfig: clientRequest.BackgroundConfig, + }, + }) +} diff --git a/services/cloudrecording/v1/query.go b/services/cloudrecording/v1/query.go index cb5dd16..5ebf1e1 100644 --- a/services/cloudrecording/v1/query.go +++ b/services/cloudrecording/v1/query.go @@ -51,60 +51,283 @@ type QueryResp struct { } type QueryIndividualRecordingServerResponse struct { - Status int `json:"status"` + // Status 当前云服务的状态: + // + // 0:没有开始云服务。 + // + // 1:云服务初始化完成。 + // + // 2:云服务组件开始启动。 + // + // 3:云服务部分组件启动完成。 + // + // 4:云服务所有组件启动完成。 + // + // 5:云服务正在进行中。 + // + // 6:云服务收到停止请求。 + // + // 7:云服务所有组件均停止。 + // + // 8:云服务已退出。 + // + // 20:云服务异常退出。 + Status int `json:"status"` + + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 FileListMode string `json:"fileListMode"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` + + // FileList 录制文件列表。 + FileList []struct { + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + FileName string `json:"fileName"` + + // TrackType 录制文件的类型: + // + // "audio":纯音频文件。 + // + // "video":纯视频文件。 + // + // "audio_and_video":音视频文件。 + TrackType string `json:"trackType"` + + // Uid 用户 UID,表示录制的是哪个用户的音频流或视频流。 + // + // 合流录制模式下,uid 为 "0"。 + Uid string `json:"uid"` + + // MixedAllUser 用户是否是分开录制 + // + // true:所有用户合并在一个录制文件中。 + // + // false:每个用户分开录制。 + MixedAllUser bool `json:"mixedAllUser"` + + // IsPlayable 是否可以在线播放。 + // + // true:可以在线播放。 + // + // false:无法在线播放。 + IsPlayable bool `json:"isPlayable"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 SliceStartTime int64 `json:"sliceStartTime"` } type QueryIndividualVideoScreenshotServerResponse struct { - Status int `json:"status"` + // Status 当前云服务的状态: + // + // 0:没有开始云服务。 + // + // 1:云服务初始化完成。 + // + // 2:云服务组件开始启动。 + // + // 3:云服务部分组件启动完成。 + // + // 4:云服务所有组件启动完成。 + // + // 5:云服务正在进行中。 + // + // 6:云服务收到停止请求。 + // + // 7:云服务所有组件均停止。 + // + // 8:云服务已退出。 + // + // 20:云服务异常退出。 + Status int `json:"status"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 SliceStartTime int64 `json:"sliceStartTime"` } type QueryMixRecordingHLSServerResponse struct { - Status int `json:"status"` - FileListMode string `json:"fileListMode"` - FileList string `json:"fileList"` - SliceStartTime int64 `json:"sliceStartTime"` + // Status 当前云服务的状态: + // + // 0:没有开始云服务。 + // + // 1:云服务初始化完成。 + // + // 2:云服务组件开始启动。 + // + // 3:云服务部分组件启动完成。 + // + // 4:云服务所有组件启动完成。 + // + // 5:云服务正在进行中。 + // + // 6:云服务收到停止请求。 + // + // 7:云服务所有组件均停止。 + // + // 8:云服务已退出。 + // + // 20:云服务异常退出。 + Status int `json:"status"` + + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 + FileListMode string `json:"fileListMode"` + + // FileList 录制产生的 M3U8 文件的文件名。 + FileList string `json:"fileList"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } type QueryMixRecordingHLSAndMP4ServerResponse struct { - Status int `json:"status"` + // Status 当前云服务的状态: + // + // 0:没有开始云服务。 + // + // 1:云服务初始化完成。 + // + // 2:云服务组件开始启动。 + // + // 3:云服务部分组件启动完成。 + // + // 4:云服务所有组件启动完成。 + // + // 5:云服务正在进行中。 + // + // 6:云服务收到停止请求。 + // + // 7:云服务所有组件均停止。 + // + // 8:云服务已退出。 + // + // 20:云服务异常退出。 + Status int `json:"status"` + + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 FileListMode string `json:"fileListMode"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` + + // FileList 录制文件列表。 + FileList []struct { + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + FileName string `json:"fileName"` + + // TrackType 录制文件的类型: + // + // "audio":纯音频文件。 + // + // "video":纯视频文件。 + // + // "audio_and_video":音视频文件。 + TrackType string `json:"trackType"` + + // Uid 用户 UID,表示录制的是哪个用户的音频流或视频流。 + // + // 合流录制模式下,uid 为 "0"。 + Uid string `json:"uid"` + + // MixedAllUser 用户是否是分开录制 + // + // true:所有用户合并在一个录制文件中。 + // + // false:每个用户分开录制。 + MixedAllUser bool `json:"mixedAllUser"` + + // IsPlayable 是否可以在线播放。 + // + // true:可以在线播放。 + // + // false:无法在线播放。 + IsPlayable bool `json:"isPlayable"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` + + // SliceStartTime 录制开始的时间,Unix 时间戳,单位为毫秒。 SliceStartTime int64 `json:"sliceStartTime"` } type QueryWebRecordingServerResponse struct { + // Status 当前云服务的状态: + // + // 0:没有开始云服务。 + // + // 1:云服务初始化完成。 + // + // 2:云服务组件开始启动。 + // + // 3:云服务部分组件启动完成。 + // + // 4:云服务所有组件启动完成。 + // + // 5:云服务正在进行中。 + // + // 6:云服务收到停止请求。 + // + // 7:云服务所有组件均停止。 + // + // 8:云服务已退出。 + // + // 20:云服务异常退出。 Status int `json:"status"` ExtensionServiceState []struct { Payload struct { + // FileListMode 文件列表 FileList []struct { - Filename string `json:"filename"` - SliceStartTime int64 `json:"sliceStartTime"` + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + Filename string `json:"filename"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` - Onhold bool `json:"onhold"` - State string `json:"state"` + + // Onhold 页面录制是否处于暂停状态: + // + // true:处于暂停状态。 + // + // false:处于运行状态。 + Onhold bool `json:"onhold"` + + // State 将订阅内容上传至扩展服务的状态: + // + // "init":服务正在初始化。 + // + // "inProgress":服务启动完成,正在进行中。 + // + // "exit":服务退出。 + State string `json:"state"` + Outputs []struct { + // RtmpUrl CDN 推流地址。 RtmpUrl string `json:"rtmpUrl"` - Status string `json:"status"` + + // Status 页面录制当前的推流状态: + // + // "connecting":正在连接 CDN 服务器。 + // + // "publishing":正在推流。 + // + // "onhold":设置是否暂停推流。 + // + // "disconnected":连接 CDN 服务器失败,声网建议你更换 CDN 推流地址。 + Status string `json:"status"` } `json:"outputs"` } `json:"payload"` + // ServiceName 扩展服务的名称: + // + // web_recorder_service:代表扩展服务为页面录制。 + // + // rtmp_publish_service:代表扩展服务为转推页面录制到 CDN。 ServiceName string `json:"serviceName"` } `json:"extensionServiceState"` } diff --git a/services/cloudrecording/v1/start.go b/services/cloudrecording/v1/start.go index b6b2ef0..8543ca6 100644 --- a/services/cloudrecording/v1/start.go +++ b/services/cloudrecording/v1/start.go @@ -3,7 +3,9 @@ package v1 import ( "context" "errors" + "fmt" "net/http" + "time" "github.com/tidwall/gjson" @@ -11,25 +13,37 @@ import ( ) type Starter struct { + module string + logger core.Logger client core.Client prefixPath string // /v1/apps/{appid}/cloud_recording } const ( + // IndividualMode 云端录制模式:单流录制 IndividualMode = "individual" - MixMode = "mix" - WebMode = "web" + + // MixMode 云端录制模式:合流录制 + MixMode = "mix" + + // WebMode 云端录制模式:页面录制 + WebMode = "web" ) // BuildPath returns the request path. // /v1/apps/{appid}/cloud_recording/resourceid/{resourceid}/mode/{mode}/start -func (a *Starter) BuildPath(resourceID string, mode string) string { - return a.prefixPath + "/resourceid/" + resourceID + "/mode/" + mode + "/start" +func (s *Starter) BuildPath(resourceID string, mode string) string { + return s.prefixPath + "/resourceid/" + resourceID + "/mode/" + mode + "/start" } type StartReqBody struct { - Cname string `json:"cname"` - Uid string `json:"uid"` + // Cname 录制的频道名 + Cname string `json:"cname"` + + // Uid 字符串内容为云端录制服务在 RTC 频道内使用的 UID,用于标识频道内的录制服务。 + Uid string `json:"uid"` + + // ClientRequest 客户端请求 ClientRequest *StartClientRequest `json:"clientRequest"` } @@ -49,54 +63,89 @@ const ( PostPhoneTranscodingCombinationPolicy = "postphone_transcoding" ) +// AppsCollection 应用配置项 type AppsCollection struct { - CombinationPolicy string `json:"combinationPolicy"` + // CombinationPolicy 各云端录制应用的组合方式 + // + // postpone_transcoding:如需延时转码或延时混音,则选用此种方式。 + // + // default:除延时转码和延时混音外,均选用此种方式。 + // + // 默认值:default + CombinationPolicy string `json:"combinationPolicy,omitempty"` } -type ChannelType int - -const ( - CommunicationChannelType ChannelType = 0 - LiveBroadcastingChannelType ChannelType = 1 -) - -type StreamType int - -const ( - AudioStreamType StreamType = 0 - VideoStreamType StreamType = 1 - AudioVideoStreamType StreamType = 2 -) +type RecordingConfig struct { + // ChannelType 频道场景。 + // + // 目前支持以下几种频道场景: + // + // 0: 通信场景 + // + // 1: 直播场景 + // + // 默认值:0 + ChannelType int `json:"channelType"` -type StreamMode string + // StreamTypes 订阅的媒体流类型 + // + // 目前支持以下几种媒体流类型: + // + // 0:仅订阅音频。适用于智能语音审核场景 + // + // 1:仅订阅视频 + // + // 2:同时订阅音频和视频 + // + // 默认值:2 + StreamTypes int `json:"streamTypes"` -const ( - DefaultStreamMode StreamMode = "default" - StandardStreamMode StreamMode = "standard" - OriginalStreamMode StreamMode = "original" -) + // StreamMode 媒体流的输出模式 + // + // 目前支持以下几种输出模式: + // + // default:默认模式。录制过程中音频转码,分别生成 M3U8 音频索引文件和视频索引文件。 + // + // standard:标准模式。声网推荐使用该模式。录制过程中音频转码, + // 分别生成 M3U8 音频索引文件、视频索引文件和合并的音视频索引文件。如果在 Web 端使用 VP8 编码,则生成一个合并的 MPD 音视频索引文件。 + // + // original:原始编码模式。 + //适用于单流音频不转码录制。仅订阅音频时(streamTypes 为 0)时该字段生效,录制过程中音频不转码,生成 M3U8 音频索引文件。 + // + // 默认值:default + StreamMode string `json:"streamMode,omitempty"` -type DecryptionMode int + // DecryptionMode 音频流的解密模式 + // + // 下面是支持的解密模式: + // 0:不加密。 + // + // 1:AES_128_XTS 加密模式。128 位 AES 加密,XTS 模式。 + // + // 2:AES_128_ECB 加密模式。128 位 AES 加密,ECB 模式。 + // + // 3:AES_256_XTS 加密模式。256 位 AES 加密,XTS 模式。 + // + // 4:SM4_128_ECB 加密模式。128 位 SM4 加密,ECB 模式。 + // + // 5:AES_128_GCM 加密模式。128 位 AES 加密,GCM 模式。 + // + // 6:AES_256_GCM 加密模式。256 位 AES 加密,GCM 模式。 + // + // 7:AES_128_GCM2 加密模式。128 位 AES 加密,GCM 模式。相比于 AES_128_GCM 加密模式,AES_128_GCM2 加密模式安全性更高且需要设置密钥和盐。 + // + // 8:AES_256_GCM2 加密模式。256 位 AES 加密,GCM 模式。相比于 AES_256_GCM 加密模式,AES_256_GCM2 加密模式安全性更高且需要设置密钥和盐。 + DecryptionMode int `json:"decryptionMode,omitempty"` -const ( - DefaultDecryptionMode DecryptionMode = 0 - AES128XTSDecryptionMode DecryptionMode = 1 - AES128ECBDecryptionMode DecryptionMode = 2 - AES256XTSDecryptionMode DecryptionMode = 3 - SM4128ECBDecryptionMode DecryptionMode = 4 - AES128GCMDecryptionMode DecryptionMode = 5 - AES256GCMDecryptionMode DecryptionMode = 6 - AES128GCM2DecryptionMod DecryptionMode = 7 - AES256GCM2DecryptionMod DecryptionMode = 8 -) + // Secret 与加解密相关的密钥 + // + // 仅需在 decryptionMode 非 0 时设置。 + Secret string `json:"secret,omitempty"` -type RecordingConfig struct { - ChannelType int `json:"channelType"` - StreamTypes int `json:"streamTypes"` - StreamMode string `json:"streamMode,omitempty"` - DecryptionMode int `json:"decryptionMode,omitempty"` - Secret string `json:"secret,omitempty"` - Salt string `json:"salt,omitempty"` + // Salt 与加解密相关的盐 + // + // Base64 编码、32 位字节。仅需在 decryptionMode 为 7 或 8 时设置。 + Salt string `json:"salt,omitempty"` // AudioProfile 设置输出音频的采样率、码率、编码模式和声道数。目前仅适用于合流录制 // @@ -115,29 +164,91 @@ type RecordingConfig struct { // 1:视频小流,即低分辨率低码率的视频流 VideoStreamType int `json:"videoStreamType,omitempty"` - // MaxIdleTime 最长空闲频道时间,单位为秒。默认值为 30。该值需大于等于 5, - // 且小于等于 2,592,000,即 30 天。如果频道内无用户的状态持续超过该时间, - // 录制程序会自动退出。退出后,再次调用 start 请求,会产生新的录制文件。 + // MaxIdleTime 最大频道空闲时间。 + // 单位为秒。最大值不超过 30 天。超出最大频道空闲时间后,录制服务会自动退出。录制服务退出后,如果你再次发起 start 请求,会产生新的录制文件。 // - // * 通信场景下,如果频道内有用户,但用户没有发流,不算作无用户状态。 + // 频道空闲:直播频道内无任何主播,或通信频道内无任何用户。 // - // * 直播场景下,如果频道内有观众但无主播,一旦无主播的状态超过 maxIdleTime,录制程序会自动退出。 + // 5 <= MaxIdleTime <= 2592000 + // + // 默认值:30 MaxIdleTime int `json:"maxIdleTime,omitempty"` + // TranscodingConfig 转码输出的视频配置项 + // + // 配置参考 https://doc.shengwang.cn/doc/cloud-recording/restful/user-guide/mix-mode/set-output-video-profile TranscodingConfig *TranscodingConfig `json:"transcodingConfig,omitempty"` - SubscribeAudioUIDs []string `json:"subscribeAudioUids,omitempty"` + // SubscribeUIDs 指定订阅哪几个 UID 的音频流。 + // + // 如需订阅全部 UID 的音频流,则无需设置该字段。数组长度不得超过 32,不推荐使用空数组。该字段和 UnsubscribeAudioUIDs 只能设一个。 + // + // 注意: + // + // 1.该字段仅适用于 streamTypes 设为音频,或音频和视频的情况。 + // + // 2.如果你设置了音频的订阅名单,但没有设置视频的订阅名单,云端录制服务不会订阅任何视频流。反之亦然。 + // + // 3.设为 ["#allstream#"] 可订阅频道内所有 UID 的音频流。 + SubscribeAudioUIDs []string `json:"subscribeAudioUids,omitempty"` + + // UnsubscribeAudioUIDs 指定不订阅哪几个 UID 的音频流。 + // + // 云端录制会订阅频道内除指定 UID 外所有 UID 的音频流。数组长度不得超过 32,不推荐使用空数组。该字段和 SubscribeAudioUIDs 只能设一个。 UnsubscribeAudioUIDs []string `json:"unsubscribeAudioUids,omitempty"` - SubscribeVideoUIDs []string `json:"subscribeVideoUids,omitempty"` + + // SubscribeVideoUIDs 指定订阅哪几个 UID 的视频流。 + // + // 如需订阅全部 UID 的视频流,则无需设置该字段。数组长度不得超过 32,不推荐使用空数组。该字段和 UnsubscribeVideoUIDs 只能设一个。 + // + // 注意: + // + // 1.该字段仅适用于 streamTypes 设为视频,或音频和视频的情况。 + // + // 2.如果你设置了视频的订阅名单,但没有设置音频的订阅名单,云端录制服务不会订阅任何音频流。反之亦然。 + // + // 3.设为 ["#allstream#"] 可订阅频道内所有 UID 的视频流。 + SubscribeVideoUIDs []string `json:"subscribeVideoUids,omitempty"` + + // UnsubscribeVideoUIDs 指定不订阅哪几个 UID 的视频流。 + // + // 云端录制会订阅频道内除指定 UID 外所有 UID 的视频流。数组长度不得超过 32,不推荐使用空数组。该字段和 subscribeVideoUids 只能设一个。 UnsubscribeVideoUIDs []string `json:"unsubscribeVideoUids,omitempty"` + // SubscribeUidGroup 预估的订阅人数峰值。 + // + // 枚举值: + // + // 0:1 到 2 个 UID。 + // + // 1:3 到 7 个 UID。 + // + // 2:8 到 12 个 UID。 + // + // 3:13 到 17 个 UID。 + // + // 4:18 到 32 个 UID。 + // + // 5:33 到 49 个 UID。 SubscribeUidGroup int `json:"subscribeUidGroup"` } type Container struct { + // Format 文件的容器格式,支持如下取值: + // + // "mp4":延时转码时的默认格式。MP4 格式。 + // + // "mp3":延时混音时的默认格式。MP3 格式。 + // + // "m4a":M4A 格式。 + // + // "aac":AAC 格式。 + // + // 注意:延时转码暂时只能设为 MP4 格式。 Format string `json:"format"` } +// TranscodeOptions 延时转码或延时混音下,生成的录制文件的配置项。 type TranscodeOptions struct { Container *Container `json:"container"` TransConfig *TransConfig `json:"transConfig"` @@ -145,90 +256,390 @@ type TranscodeOptions struct { } type TransConfig struct { + // TransMode 模式 + // + // "postponeTranscoding":延时转码。 + // + // "audioMix":延时混音。 TransMode string `json:"transMode"` } +// Audio 文件的音频属性 type Audio struct { + // SampleRate 音频的采样率,单位为 Hz,支持如下取值: + // + // "48000":48 kHz。 + // + // "32000":32 kHz。 + // + // "16000":16 kHz。 SampleRate string `json:"sampleRate"` - BitRate string `json:"bitrate"` - Channels string `json:"channels"` + + // BitRate 音频的码率,单位为 Kbps + // + // 默认值:4800 + BitRate string `json:"bitrate"` + + // Channels 音频声道数,支持如下取值: + // + // "1":单声道。 + // + // "2":双声道。 + // + // 默认值:2 + Channels string `json:"channels"` } +// TranscodingConfig 转码输出的视频配置项 type TranscodingConfig struct { - Width int `json:"width"` - Height int `json:"height"` - FPS int `json:"fps"` - BitRate int `json:"bitrate"` - MaxResolutionUid string `json:"maxResolutionUid,omitempty"` - MixedVideoLayout int `json:"mixedVideoLayout"` - BackgroundColor string `json:"backgroundColor,omitempty"` - BackgroundImage string `json:"backgroundImage,omitempty"` - DefaultUserBackgroundImage string `json:"defaultUserBackgroundImage,omitempty"` - LayoutConfig []LayoutConfig `json:"layoutConfig,omitempty"` - BackgroundConfig []BackgroundConfig `json:"backgroundConfig,omitempty"` + // Width 视频的宽度,单位为像素。 + // + // width 和 height 的乘积不能超过 1920 × 1080。 + // + // 默认值:360 + Width int `json:"width,omitempty"` + + // Height 视频的高度,单位为像素。 + // + // width 和 height 的乘积不能超过 1920 × 1080。 + // + // 默认值:640 + Height int `json:"height,omitempty"` + + // Fps 视频的帧率,单位 fps。 + // + // 默认值:15 + FPS int `json:"fps,omitempty"` + + // BitRate 视频的码率,单位为 Kbps。 + // + // 默认值:500 + BitRate int `json:"bitrate,omitempty"` + + // MaxResolutionUid 仅需在垂直布局下设置。 + // + // 指定显示大视窗画面的用户 UID。字符串内容的整型取值范围 1 到 (232-1),且不可设置为 0。 + MaxResolutionUid string `json:"maxResolutionUid,omitempty"` + + // MixedVideoLayout 视频合流布局 + // + // 0:悬浮布局。第一个加入频道的用户在屏幕上会显示为大视窗,铺满整个画布,其他用户的视频画面会显示为小视窗,从下到上水平排列,最多 4 行,每行 4 个画面,最多支持共 17 个画面。 + // + // 1:自适应布局。根据用户的数量自动调整每个画面的大小,每个用户的画面大小一致,最多支持 17 个画面。 + // + // 2:垂直布局。指定 maxResolutionUid 在屏幕左侧显示大视窗画面,其他用户的小视窗画面在右侧垂直排列,最多两列,一列 8 个画面,最多支持共 17 个画面。 + // + // 3:自定义布局。由你在 layoutConfig 字段中自定义合流布局。 + // + // 默认值:0 + MixedVideoLayout int `json:"mixedVideoLayout,omitempty"` + + // BackgroundColor 视频画布的背景颜色。 + // + // 支持 RGB 颜色表,字符串格式为 # 号和 6 个十六进制数。 + // + // 默认值 "#000000",代表黑色。 + BackgroundColor string `json:"backgroundColor,omitempty"` + + // BackgroundImage 视频画布的背景图的 URL,背景图的显示模式为裁剪模式。 + // + // 裁剪模式:优先保证画面被填满。背景图尺寸等比缩放,直至整个画面被背景图填满。如果背景图长宽与显示窗口不同,则背景图会按照画面设置的比例进行周边裁剪后填满画面。 + BackgroundImage string `json:"backgroundImage,omitempty"` + + // DefaultUserBackgroundImage 默认的用户画面背景图的 URL + DefaultUserBackgroundImage string `json:"defaultUserBackgroundImage,omitempty"` + + // LayoutConfig 用户的合流画面布局。 + // + // 由每个用户对应的布局画面设置组成的数组,支持最多 17 个用户。 + LayoutConfig []LayoutConfig `json:"layoutConfig,omitempty"` + + // BackgroundConfig 用户的背景图设置 + BackgroundConfig []BackgroundConfig `json:"backgroundConfig,omitempty"` } type LayoutConfig struct { - UID string `json:"uid"` - XAxis float32 `json:"x_axis"` - YAxis float32 `json:"y_axis"` - Width float32 `json:"width"` - Height float32 `json:"height"` - Alpha float32 `json:"alpha"` - RenderMode int `json:"render_mode"` + // UID 字符串内容为待显示在该区域的用户的 UID,32 位无符号整数。 + // + // 如果不指定 UID,会按照用户加入频道的顺序自动匹配 layoutConfig 中的画面设置。 + UID string `json:"uid"` + + // XAxis 屏幕里该画面左上角的横坐标的相对值,精确到小数点后六位。 + // + // 从左到右布局,0.0 在最左端,1.0 在最右端。该字段也可以设置为整数 0 或 1。 + // + // 0<=XAxis<=1 + XAxis float32 `json:"x_axis"` + + // YAxis 屏幕里该画面左上角的纵坐标的相对值,精确到小数点后六位。 + // + // 屏幕里该画面左上角的纵坐标的相对值,精确到小数点后六位。 + // + // 从上到下布局,0.0 在最上端,1.0 在最下端。该字段也可以设置为整数 0 或 1。 + // + // 0<=YAxis<=1 + YAxis float32 `json:"y_axis"` + + // Width 该画面宽度的相对值,精确到小数点后六位。该字段也可以设置为整数 0 或 1。 + // + // 0<=Width<=1 + Width float32 `json:"width"` + + // Height 该画面高度的相对值,精确到小数点后六位。该字段也可以设置为整数 0 或 1。 + // + // 0<=Height<=1 + Height float32 `json:"height"` + + // Alpha 图像的透明度。精确到小数点后六位。0.0 表示图像为透明的,1.0 表示图像为完全不透明的。 + // + // 0<=Alpha<=1 + // + // 默认值:1 + Alpha float32 `json:"alpha,omitempty"` + + // RenderMode 画面的渲染模式。 + // + // 0:裁剪模式。优先保证画面被填满。视频尺寸等比缩放,直至整个画面被视频填满。如果视频长宽与显示窗口不同,则视频流会按照画面设置的比例进行周边裁剪后填满画面。 + // + // 1:缩放模式。优先保证视频内容全部显示。视频尺寸等比缩放,直至视频窗口的一边与画面边框对齐。如果视频尺寸与画面尺寸不一致,在保持长宽比的前提下,将视频进行缩放后填满画面,缩放后的视频四周会有一圈黑边。 + // + // 默认值:0 + RenderMode int `json:"render_mode"` } type RecordingFileConfig struct { + // AvFileType 录制生成视频的文件类型 + // + // "hls":默认值。M3U8 和 TS 文件。 + // + // "mp4":MP4 文件。 + // + // 注意: + // + // 单流录制模式下,且非仅截图情况,使用默认值即可。 + // + // 合流录制和页面录制模式下,你需设为 ["hls","mp4"]或者["hls"]。仅设为 ["mp4"] 会收到报错。设置后,录制文件行为如下: + // + // 合流录制模式:录制服务会在当前 MP4 文件时长超过约 2 小时或文件大小超过约 2 GB 左右时,创建一个新的 MP4 文件。 + // + // 页面录制模式:录制服务会在当前 MP4 文件时长超过 maxVideoDuration 时,创建一个新的 MP4 文件。 AvFileType []string `json:"avFileType"` } type SnapshotConfig struct { - CaptureInterval int `json:"captureInterval"` - FileType []string `json:"fileType"` + // CaptureInterval 云端录制定期截图的截图周期。单位为秒。 + // + // 5<=CaptureInterval<=300 + // 默认值:10 + CaptureInterval int `json:"captureInterval,omitempty"` + + // FileType 截图的文件格式 + // + // 目前只支持 ["jpg"],即生成 JPG 格式的截图文件。 + FileType []string `json:"fileType"` } type StorageConfig struct { - Vendor int `json:"vendor"` - Region int `json:"region"` - Bucket string `json:"bucket"` - AccessKey string `json:"accessKey"` - SecretKey string `json:"secretKey"` - FileNamePrefix []string `json:"fileNamePrefix,omitempty"` + // Vendor 第三方云存储平台。目前支持的云存储服务商有: + // + // 1:AWS S3 + // + // 2:阿里云 + // + // 3:腾讯云 + // + // 5:Microsoft Azure + // + // 6:谷歌云 + // + // 7:华为云 + // + // 8:百度智能云 + Vendor int `json:"vendor"` + + // Region 第三方云存储指定的地区信息,详情见 https://doc.shengwang.cn/api-ref/cloud-recording/restful/region-vendor + // + // 注意:为确保录制文件上传的成功率和实时性,第三方云存储的 region 与你发起请求的应用服务器必须在同一个区域中。例如:你发起请求的 App 服务器在中国大陆地区,则第三方云存储需要设置为中国大陆区域内。 + Region int `json:"region"` + + // Bucket 第三方云存储的 bucket + // + // Bucket 名称需要符合对应第三方云存储服务的命名规则。 + Bucket string `json:"bucket"` + + // AccessKey 第三方云存储的 Access Key + // + // 如需延时转码,则访问密钥必须具备读写权限;否则建议只需提供写权限。 + AccessKey string `json:"accessKey"` + + // SecretKey 第三方云存储的 Secret Key + SecretKey string `json:"secretKey"` + + // FileNamePrefix 录制文件的文件名前缀 + // + // 录制文件在第三方云存储中的存储位置,与录制文件名前缀有关。 + // 如果设为 ["directory1","directory2"],那么录制文件名前缀为 "directory1/directory2/", + // 即录制文件名为 directory1/directory2/xxx.m3u8。前缀长度(包括斜杠)不得超过 128 个字符。字符串中不得出现斜杠、下划线、括号等符号字符。 + // + // 以下为支持的字符集范围: + // + // 26 个小写英文字母 a~z + // + // 26 个大写英文字母 A~Z + // + // 10 个数字 0-9 + FileNamePrefix []string `json:"fileNamePrefix,omitempty"` + + // ExtensionParams 第三方云存储服务会按照该字段设置对已上传的录制文件进行加密和打标签 ExtensionParams *ExtensionParams `json:"extensionParams,omitempty"` } type ExtensionParams struct { + // SSE 加密模式 + // 设置该字段后,第三方云存储服务会按照该加密模式将已上传的录制文件进行加密。该字段仅适用于 Amazon S3 + // + // kms:KMS 加密。 + // + // aes256:AES256 加密 SSE string `json:"sse"` + + // Tag 标签内容 + // 设置该字段后,第三方云存储服务会按照该标签内容将已上传的录制文件进行打标签操作。该字段仅适用于阿里云和 Amazon S3 Tag string `json:"tag"` } +// ExtensionServiceConfig 扩展服务配置项 type ExtensionServiceConfig struct { - ErrorHandlePolicy string `json:"errorHandlePolicy,omitempty"` + // ErrorHandlePolicy 错误处理策略。 + // + // 默认且仅可设为 "error_abort",表示当扩展服务发生错误后,订阅和云端录制的其他非扩展服务都停止。 + // + // 默认值:error_abort + ErrorHandlePolicy string `json:"errorHandlePolicy,omitempty"` + ExtensionServices []ExtensionService `json:"extensionServices"` } +// ExtensionService 扩展服务 type ExtensionService struct { - ServiceName string `json:"serviceName"` - ErrorHandlePolicy string `json:"errorHandlePolicy"` - ServiceParam *ServiceParam `json:"serviceParam"` + // ServiceName 扩展服务的名称 + // + // web_recorder_service:代表扩展服务为页面录制。 + // + // rtmp_publish_service:代表扩展服务为转推页面录制到 CDN。 + ServiceName string `json:"serviceName"` + + // ErrorHandlePolicy 扩展服务内的错误处理策略 + // + // "error_abort":页面录制时默认且只能为该值。表示当前扩展服务出错时,停止其他扩展服务。 + // + // "error_ignore":转推页面录制到 CDN 时默认且只能为该值。表示当前扩展服务出错时,其他扩展服务不受影响。 + // + // 如果页面录制服务或录制上传服务异常,那么推流到 CDN 失败,因此页面录制服务出错会影响转推页面录制到 CDN 服务。 + // + // 转推到 CDN 的过程发生异常时,页面录制不受影响。 + ErrorHandlePolicy string `json:"errorHandlePolicy"` + + // ServiceParam 扩展服务的参数 + ServiceParam *ServiceParam `json:"serviceParam"` } type Outputs struct { + // RtmpURL CDN 推流地址 RtmpURL string `json:"rtmpUrl"` } type ServiceParam struct { - Outputs []Outputs `json:"outputs,omitempty"` - URL string `json:"url"` - VideoBitRate int `json:"VideoBitrate,omitempty"` - VideoFPS int `json:"videoFps,omitempty"` - AudioProfile int `json:"audioProfile"` - Mobile bool `json:"mobile,omitempty"` - VideoWidth int `json:"videoWidth"` - VideoHeight int `json:"videoHeight"` - MaxRecordingHour int `json:"maxRecordingHour"` - MaxVideoDuration int `json:"maxVideoDuration,omitempty"` - Onhold bool `json:"onhold,omitempty"` - ReadyTimeout int `json:"readyTimeout,omitempty"` + // Outputs 转推页面录制到 CDN 时需设置如下字段 + Outputs []Outputs `json:"outputs,omitempty"` + + // URL 待录制页面的地址 + URL string `json:"url"` + + // VideoBitRate 输出视频的码率,单位为 Kbps + // + // 针对不同的输出视频分辨率,videoBitrate 的默认值不同: + // + // 输出视频分辨率大于或等于 1280 × 720:默认值为 2000。 + // + // 输出视频分辨率小于 1280 × 720:默认值为 1500。 + VideoBitRate int `json:"VideoBitrate,omitempty"` + + // VideoFPS 输出视频的帧率,单位为 fps + // + // 5 <= VideoFPS <=60 + // + // 默认值:15 + VideoFPS int `json:"videoFps,omitempty"` + + // AudioProfile 输出音频的采样率、码率、编码模式和声道数。 + // + // 0:48 kHz 采样率,音乐编码,单声道,编码码率约 48 Kbps。 + // + // 1:48 kHz 采样率,音乐编码,单声道,编码码率约 128 Kbps。 + // + // 2:48 kHz 采样率,音乐编码,双声道,编码码率约 192 Kbps。 + AudioProfile int `json:"audioProfile"` + + // Mobile 是否开启移动端网页模式 + // + // true:开启。开启后,录制服务使用移动端网页渲染模式录制当前页面。 + // + // false:(默认)不开启。 + Mobile bool `json:"mobile,omitempty"` + + // VideoWidth 视频的宽度,单位为像素。 + // + // videoWidth 和 videoHeight 的乘积需小于等于 1920 × 1080 + VideoWidth int `json:"videoWidth"` + + // VideoHeight 视频的高度,单位为像素。 + // + // videoWidth 和 videoHeight 的乘积需小于等于 1920 × 1080 + VideoHeight int `json:"videoHeight"` + + // MaxRecordingHour 页面录制的最大时长,单位为小时 + // + // 超出该值后页面录制会自动停止 + // + // 计费相关:页面录制停止前会持续计费,因此请根据实际业务情况设置合理的值或主动停止页面录制。 + // + // 1 <= MaxRecordingHour <= 720 + MaxRecordingHour int `json:"maxRecordingHour"` + + // MaxVideoDuration 页面录制生成的 MP4 切片文件的最大时长,单位为分钟 + // + // 页面录制过程中,录制服务会在当前 MP4 文件时长超过约 maxVideoDuration 左右时创建一个新的 MP4 切片文件 + // + // 30 <= MaxVideoDuration <= 240 + // + // 默认值:120 + MaxVideoDuration int `json:"maxVideoDuration,omitempty"` + + // Onhold 是否在启动页面录制任务时暂停页面录制 + // + // true:在启动页面录制任务时暂停页面录制。开启页面录制任务后立即暂停录制,录制服务会打开并渲染待录制页面,但不生成切片文件。 + // + // false:启动页面录制任务并进行页面录制。 + // + // 注意:建议你按照如下流程使用 onhold 字段: + // + // 调用 start 方法时将 onhold 设为 true,开启并暂停页面录制,自行判断页面录制开始的合适时机。 + // + // 调用 update 并将 onhold 设为 false,继续进行页面录制。如果需要连续调用 update 方法暂停或继续页面录制,请在收到上一次 update 响应后再进行调用,否则可能导致请求结果与预期不一致。 + // + // 默认值:false + Onhold bool `json:"onhold"` + + // ReadyTimeout 设置页面加载超时时间,单位为秒 + // + // 0 或不设置,表示不检测页面加载状态。 + // + // [1,60] 之间的整数,表示页面加载超时时间。 + // + // 0 <= ReadyTimeout <= 60 + // + // 默认值:0 + ReadyTimeout int `json:"readyTimeout"` } type StarterResp struct { @@ -236,17 +647,27 @@ type StarterResp struct { SuccessResp StartSuccessResp } +// StartSuccessResp 云端录制服务成功开始云端录制后返回的响应 type StartSuccessResp struct { - Cname string `json:"cname"` - UID string `json:"uid"` + // Cname 录制的频道名 + Cname string `json:"cname"` + + // UID 字符串内容为云端录制服务在 RTC 频道内使用的 UID,用于标识频道内的录制服务。 + UID string `json:"uid"` + + // ResourceId 云端录制资源 Resource ID。 + // + // 使用这个 Resource ID 可以开始一段云端录制。这个 Resource ID 的有效期为 5 分钟,超时需要重新请求。 ResourceId string `json:"resourceId"` - SID string `json:"sid"` + + // SID 录制 ID。成功开始云端录制后,你会得到一个 Sid (录制 ID)。该 ID 是一次录制周期的唯一标识。 + SID string `json:"sid"` } -func (a *Starter) Do(ctx context.Context, resourceID string, mode string, payload *StartReqBody) (*StarterResp, error) { - path := a.BuildPath(resourceID, mode) +func (s *Starter) Do(ctx context.Context, resourceID string, mode string, payload *StartReqBody) (*StarterResp, error) { + path := s.BuildPath(resourceID, mode) - responseData, err := a.client.DoREST(ctx, path, http.MethodPost, payload) + responseData, err := s.doRESTWithRetry(ctx, path, http.MethodPost, payload) if err != nil { var internalErr *core.InternalErr if !errors.As(err, &internalErr) { @@ -277,15 +698,50 @@ func (a *Starter) Do(ctx context.Context, resourceID string, mode string, payloa return &resp, nil } -func (a *Starter) DoWebRecording(ctx context.Context, resourceID string, cname string, uid string, clientRequest *StartWebRecordingClientRequest) (*StarterResp, error) { - mode := WebMode - return a.Do(ctx, resourceID, mode, &StartReqBody{ - Cname: cname, - Uid: uid, - ClientRequest: &StartClientRequest{ - RecordingFileConfig: clientRequest.RecordingFileConfig, - StorageConfig: clientRequest.StorageConfig, - ExtensionServiceConfig: clientRequest.ExtensionServiceConfig, - }, +const retryCount = 3 + +func (s *Starter) doRESTWithRetry(ctx context.Context, path string, method string, requestBody interface{}) (*core.BaseResponse, error) { + var ( + resp *core.BaseResponse + err error + retry int + ) + + err = core.RetryDo(func(retryCount int) error { + var doErr error + + resp, doErr = s.client.DoREST(ctx, path, method, requestBody) + if doErr != nil { + return core.NewRetryErr(false, doErr) + } + + statusCode := resp.HttpStatusCode + switch { + case statusCode == 200 || statusCode == 201: + return nil + case statusCode >= 400 && statusCode < 410: + s.logger.Debugf(ctx, s.module, "http status code is %d, no retry,http response:%s", statusCode, resp.RawBody) + return core.NewRetryErr( + false, + core.NewInternalErr(fmt.Sprintf("http status code is %d, no retry,http response:%s", statusCode, resp.RawBody)), + ) + default: + s.logger.Debugf(ctx, s.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 >= retryCount + }, func(i int) time.Duration { + return time.Second * time.Duration(i+1) + }, func(err error) { + s.logger.Debugf(ctx, s.module, "http request err:%s", err) + retry++ }) + + return resp, err } diff --git a/services/cloudrecording/v1/stop.go b/services/cloudrecording/v1/stop.go index 7661d19..c98101c 100644 --- a/services/cloudrecording/v1/stop.go +++ b/services/cloudrecording/v1/stop.go @@ -63,58 +63,187 @@ type StopSuccessResp struct { } type StopIndividualRecordingServerResponse struct { + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 FileListMode string `json:"fileListMode"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` + + FileList []struct { + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + FileName string `json:"fileName"` + + // TrackType 录制文件的类型: + // + // "audio":纯音频文件。 + // + // "video":纯视频文件。 + // + // "audio_and_video":音视频文件。 + TrackType string `json:"trackType"` + + // Uid 用户 UID,表示录制的是哪个用户的音频流或视频流。 + // + // 合流录制模式下,uid 为 "0"。 + Uid string `json:"uid"` + + // MixedAllUser 用户是否是分开录制 + // + // true:所有用户合并在一个录制文件中。 + // + // false:每个用户分开录制。 + MixedAllUser bool `json:"mixedAllUser"` + + // IsPlayable 是否可以在线播放。 + // + // true:可以在线播放。 + // + // false:无法在线播放。 + IsPlayable bool `json:"isPlayable"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` + + // UploadingStatus 当前录制上传的状态: + // + // "uploaded":本次录制的文件已经全部上传至指定的第三方云存储。 + // + // "backuped":本次录制的文件已经全部上传完成,但是至少有一个 TS 文件上传到了声网备份云。声网服务器会自动将这部分文件继续上传至指定的第三方云存储。 + // + // "unknown":未知状态。 UploadingStatus string `json:"uploadingStatus"` } type StopIndividualVideoScreenshotServerResponse struct { + // UploadingStatus 当前录制上传的状态: + // + // "uploaded":本次录制的文件已经全部上传至指定的第三方云存储。 + // + // "backuped":本次录制的文件已经全部上传完成,但是至少有一个 TS 文件上传到了声网备份云。声网服务器会自动将这部分文件继续上传至指定的第三方云存储。 + // + // "unknown":未知状态。 UploadingStatus string `json:"uploadingStatus"` } type StopMixRecordingHLSServerResponse struct { - FileListMode string `json:"fileListMode"` - FileList string `json:"fileList"` + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 + FileListMode string `json:"fileListMode"` + + // FileList 录制产生的 M3U8 文件的文件名。 + FileList string `json:"fileList"` + + // UploadingStatus 当前录制上传的状态: + // + // "uploaded":本次录制的文件已经全部上传至指定的第三方云存储。 + // + // "backuped":本次录制的文件已经全部上传完成,但是至少有一个 TS 文件上传到了声网备份云。声网服务器会自动将这部分文件继续上传至指定的第三方云存储。 + // + // "unknown":未知状态。 UploadingStatus string `json:"uploadingStatus"` } type StopMixRecordingHLSAndMP4ServerResponse struct { + // FileListMode fileList 字段的数据格式: + // "string":fileList 为 String 类型。合流录制模式下,如果 avFileType 设置为 ["hls"],fileListMode 为 "string"。 + // + // "json":fileList 为 JSON Array 类型。单流或合流录制模式下 avFileType 设置为 ["hls","mp4"] 时,fileListMode 为 "json"。 FileListMode string `json:"fileListMode"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` + + // FileList 录制文件列表。 + FileList []struct { + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + FileName string `json:"fileName"` + + // TrackType 录制文件的类型: + // + // "audio":纯音频文件。 + // + // "video":纯视频文件。 + // + // "audio_and_video":音视频文件。 + TrackType string `json:"trackType"` + + // Uid 用户 UID,表示录制的是哪个用户的音频流或视频流。 + // + // 合流录制模式下,uid 为 "0"。 + Uid string `json:"uid"` + + // MixedAllUser 用户是否是分开录制 + // + // true:所有用户合并在一个录制文件中。 + // + // false:每个用户分开录制。 + MixedAllUser bool `json:"mixedAllUser"` + + // IsPlayable 是否可以在线播放。 + // + // true:可以在线播放。 + // + // false:无法在线播放。 + IsPlayable bool `json:"isPlayable"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` + + // UploadingStatus 当前录制上传的状态: + // + // "uploaded":本次录制的文件已经全部上传至指定的第三方云存储。 + // + // "backuped":本次录制的文件已经全部上传完成,但是至少有一个 TS 文件上传到了声网备份云。声网服务器会自动将这部分文件继续上传至指定的第三方云存储。 + // + // "unknown":未知状态。 UploadingStatus string `json:"uploadingStatus"` } type StopWebRecordingServerResponse struct { ExtensionServiceState []struct { Payload struct { + // UploadingStatus 当前录制上传的状态: + // + // "uploaded":本次录制的文件已经全部上传至指定的第三方云存储。 + // + // "backuped":本次录制的文件已经全部上传完成,但是至少有一个 TS 文件上传到了声网备份云。声网服务器会自动将这部分文件继续上传至指定的第三方云存储。 + // + // "unknown":未知状态。 UploadingStatus string `json:"uploadingStatus"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` + + // FileListMode 文件列表 + FileList []struct { + // FileName 录制产生的 M3U8 文件和 MP4 文件的文件名。 + FileName string `json:"fileName"` + + // SliceStartTime 该文件的录制开始时间,Unix 时间戳,单位为毫秒。 + SliceStartTime int64 `json:"sliceStartTime"` } `json:"fileList"` - Onhold bool `json:"onhold"` - State string `json:"state"` + + // Onhold 页面录制是否处于暂停状态: + // + // true:处于暂停状态。 + // + // false:处于运行状态。 + Onhold bool `json:"onhold"` + + // State 将订阅内容上传至扩展服务的状态: + // + // "init":服务正在初始化。 + // + // "inProgress":服务启动完成,正在进行中。 + // + // "exit":服务退出。 + State string `json:"state"` } `json:"payload"` + + // ServiceName 服务类型 + // 服务类型: + // "upload_service":上传服务。 + // + // "web_recorder_service":页面录制服务。 ServiceName string `json:"serviceName"` - ExitReason string `json:"exit_reason"` } `json:"extensionServiceState"` } @@ -152,6 +281,7 @@ func (s *StopSuccessResp) GetWebRecordingServerResponse() *StopWebRecordingServe } return s.webRecordingServerResponse } + func (s *StopSuccessResp) GetServerResponseMode() StopRespServerResponseMode { return s.serverResponseMode } diff --git a/services/cloudrecording/v1/update.go b/services/cloudrecording/v1/update.go index e1e3aff..45db87a 100644 --- a/services/cloudrecording/v1/update.go +++ b/services/cloudrecording/v1/update.go @@ -33,30 +33,89 @@ type UpdateClientRequest struct { RtmpPublishConfig *UpdateRtmpPublishConfig `json:"rtmpPublishConfig,omitempty"` } +// UpdateStreamSubscribe 更新订阅名单 type UpdateStreamSubscribe struct { + // AudioUidList 音频订阅名单 AudioUidList *UpdateAudioUIDList `json:"audioUidList,omitempty"` + + // VideoUidList 视频订阅名单 VideoUidList *UpdateVideoUIDList `json:"videoUidList,omitempty"` } +// UpdateAudioUIDList 音频订阅名单 type UpdateAudioUIDList struct { - SubscribeAudioUIDs []string `json:"subscribeAudioUids,omitempty"` + // SubscribeAudioUIDs 指定订阅哪几个 UID 的音频流 + // + // 如需订阅全部 UID 的音频流,则无需设置该字段。数组长度不得超过 32,不推荐使用空数组。该字段和 unsubscribeAudioUids 只能设一个。 + // + // 注意: + // + // 该字段仅适用于 streamTypes 设为音频,或音频和视频的情况。 + // + // 如果你设置了音频的订阅名单,但没有设置视频的订阅名单,云端录制服务不会订阅任何视频流。反之亦然。 + // + // 设为 ["#allstream#"] 可订阅频道内所有 UID 的音频流。 + SubscribeAudioUIDs []string `json:"subscribeAudioUids,omitempty"` + + // UnsubscribeAudioUIDs 指定取消订阅哪几个 UID 的音频流 + // + // 云端录制会订阅频道内除指定 UID 外所有 UID 的音频流。数组长度不得超过 32,不推荐使用空数组。该字段和 subscribeAudioUids 只能设一个。 + // UnsubscribeAudioUIDs []string `json:"unsubscribeAudioUids,omitempty"` } +// UpdateVideoUIDList 视频订阅名单 type UpdateVideoUIDList struct { - SubscribeVideoUIDs []string `json:"subscribeVideoUids,omitempty"` + // SubscribeVideoUIDs 指定订阅哪几个 UID 的视频流 + // + // 如需订阅全部 UID 的视频流,则无需设置该字段。数组长度不得超过 32,不推荐使用空数组。该字段和 unsubscribeVideoUids 只能设一个。 + // + // 注意: + // + // 该字段仅适用于 streamTypes 设为视频,或音频和视频的情况。 + // + // 如果你设置了视频的订阅名单,但没有设置音频的订阅名单,云端录制服务不会订阅任何音频流。反之亦然。 + // + // 设为 ["#allstream#"] 可订阅频道内所有 UID 的视频流。 + SubscribeVideoUIDs []string `json:"subscribeVideoUids,omitempty"` + + // UnsubscribeVideoUIDs 指定取消订阅哪几个 UID 的视频流 + // + // 云端录制会订阅频道内除指定 UID 外所有 UID 的视频流。数组长度不得超过 32,不推荐使用空数组。该字段和 subscribeVideoUids 只能设一个。 UnsubscribeVideoUIDs []string `json:"unsubscribeVideoUids,omitempty"` } +// UpdateWebRecordingConfig 用于更新页面录制配置项。 type UpdateWebRecordingConfig struct { + // Onhold 是否在启动页面录制任务时暂停页面录制。 + // + // true:在启动页面录制任务时暂停页面录制。开启页面录制任务后立即暂停录制,录制服务会打开并渲染待录制页面,但不生成切片文件。 + // + // false:启动页面录制任务并进行页面录制。 + // + // 建议你按照如下流程使用 onhold 字段: + // + // 调用 start 方法时将 onhold 设为 true,开启并暂停页面录制,自行判断页面录制开始的合适时机。 + // + // 调用 update 并将 onhold 设为 false,继续进行页面录制。如果需要连续调用 update 方法暂停或继续页面录制,请在收到上一次 update 响应后再进行调用,否则可能导致请求结果与预期不一致。 + // + // 默认值:false Onhold bool `json:"onhold"` } +// UpdateRtmpPublishConfig 用于更新转推页面录制到 CDN 的配置项。 type UpdateRtmpPublishConfig struct { Outputs []UpdateOutput `json:"outputs"` } type UpdateOutput struct { + // RtmpURL CDN 推流 URL。 + // + // 注意: + // + // URL 仅支持 RTMP 和 RTMPS 协议。 + // + // 支持的最大转推 CDN 路数为 1。 RtmpURL string `json:"rtmpUrl"` } @@ -72,19 +131,6 @@ type UpdateSuccessResp struct { Cname string `json:"cname"` } -type UpdateServerResponse struct { - FileListMode string `json:"fileListMode"` - FileList []struct { - FileName string `json:"fileName"` - TrackType string `json:"trackType"` - Uid string `json:"uid"` - MixedAllUser bool `json:"mixedAllUser"` - IsPlayable bool `json:"isPlayable"` - SliceStartTime int64 `json:"sliceStartTime"` - } `json:"fileList"` - UploadingStatus string `json:"uploadingStatus"` -} - func (s *Update) Do(ctx context.Context, resourceID string, sid string, mode string, payload *UpdateReqBody) (*UpdateResp, error) { path := s.buildPath(resourceID, sid, mode) diff --git a/services/cloudrecording/v1/update_layout.go b/services/cloudrecording/v1/update_layout.go deleted file mode 100644 index c219e5a..0000000 --- a/services/cloudrecording/v1/update_layout.go +++ /dev/null @@ -1,98 +0,0 @@ -package v1 - -import ( - "context" - "errors" - "net/http" - - "github.com/tidwall/gjson" - - "github.com/AgoraIO-Community/agora-rest-client-go/core" -) - -type UpdateLayout struct { - client core.Client - prefixPath string // /apps/{appid}/cloud_recording -} - -// buildPath returns the request path. -// /v1/apps/{appid}/cloud_recording/resourceid/{resourceid}/sid/{sid}/mode/{mode}/updateLayout -func (s *UpdateLayout) buildPath(resourceID string, sid string, mode string) string { - return s.prefixPath + "/resourceid/" + resourceID + "/sid/" + sid + "/mode/" + mode + "/updateLayout" -} - -type UpdateLayoutReqBody struct { - Cname string `json:"cname"` - Uid string `json:"uid"` - ClientRequest *UpdateLayoutClientRequest `json:"clientRequest"` -} - -type UpdateLayoutClientRequest struct { - MaxResolutionUID string `json:"maxResolutionUid,omitempty"` - MixedVideoLayout int `json:"mixedVideoLayout"` - BackgroundColor string `json:"backgroundColor,omitempty"` - BackgroundImage string `json:"backgroundImage,omitempty"` - DefaultUserBackgroundImage string `json:"defaultUserBackgroundImage,omitempty"` - LayoutConfig []UpdateLayoutConfig `json:"layoutConfig,omitempty"` - BackgroundConfig []BackgroundConfig `json:"backgroundConfig,omitempty"` -} - -type UpdateLayoutConfig struct { - UID string `json:"uid"` - XAxis float32 `json:"x_axis"` - YAxis float32 `json:"y_axis"` - Width float32 `json:"width"` - Height float32 `json:"height"` - Alpha float32 `json:"alpha"` - RenderMode int `json:"render_mode"` -} - -type BackgroundConfig struct { - UID string `json:"uid"` - ImageURL string `json:"image_url"` - RenderMode int `json:"render_mode"` -} - -type UpdateLayoutSuccessResp struct { - ResourceId string `json:"resourceId"` - SID string `json:"sid"` -} - -type UpdateLayoutResp struct { - Response - SuccessResp UpdateLayoutSuccessResp -} - -func (s *UpdateLayout) Do(ctx context.Context, resourceID string, sid string, mode string, payload *UpdateLayoutReqBody) (*UpdateLayoutResp, error) { - path := s.buildPath(resourceID, sid, mode) - - responseData, err := s.client.DoREST(ctx, path, http.MethodPost, payload) - if err != nil { - var internalErr *core.InternalErr - if !errors.As(err, &internalErr) { - return nil, err - } - } - - var resp UpdateLayoutResp - if responseData.HttpStatusCode == http.StatusOK { - var successResponse UpdateLayoutSuccessResp - if err = responseData.UnmarshallToTarget(&successResponse); err != nil { - return nil, err - } - resp.SuccessResp = successResponse - } else { - codeResult := gjson.GetBytes(responseData.RawBody, "code") - if !codeResult.Exists() { - return nil, core.NewGatewayErr(responseData.HttpStatusCode, string(responseData.RawBody)) - } - var errResponse ErrResponse - if err = responseData.UnmarshallToTarget(&errResponse); err != nil { - return nil, err - } - resp.ErrResponse = errResponse - } - - resp.BaseResponse = responseData - return &resp, nil -} diff --git a/services/cloudrecording/v1/updatelayout.go b/services/cloudrecording/v1/updatelayout.go new file mode 100644 index 0000000..80be654 --- /dev/null +++ b/services/cloudrecording/v1/updatelayout.go @@ -0,0 +1,180 @@ +package v1 + +import ( + "context" + "errors" + "net/http" + + "github.com/tidwall/gjson" + + "github.com/AgoraIO-Community/agora-rest-client-go/core" +) + +type UpdateLayout struct { + client core.Client + prefixPath string // /apps/{appid}/cloud_recording +} + +// buildPath returns the request path. +// /v1/apps/{appid}/cloud_recording/resourceid/{resourceid}/sid/{sid}/mode/{mode}/updateLayout +func (s *UpdateLayout) buildPath(resourceID string, sid string, mode string) string { + return s.prefixPath + "/resourceid/" + resourceID + "/sid/" + sid + "/mode/" + mode + "/updateLayout" +} + +type UpdateLayoutReqBody struct { + Cname string `json:"cname"` + Uid string `json:"uid"` + ClientRequest *UpdateLayoutClientRequest `json:"clientRequest"` +} + +type UpdateLayoutClientRequest struct { + // MaxResolutionUID 仅需在垂直布局下设置。指定显示大视窗画面的用户 UID。 + // + // 字符串内容的整型取值范围 1 到 (2^32-1),且不可设置为 0。 + MaxResolutionUID string `json:"maxResolutionUid,omitempty"` + + // MixedVideoLayout 视频合流布局: + // + // 0:悬浮布局。第一个加入频道的用户在屏幕上会显示为大视窗,铺满整个画布,其他用户的视频画面会显示为小视窗,从下到上水平排列,最多 4 行,每行 4 个画面,最多支持共 17 个画面。 + // + // 1:自适应布局。根据用户的数量自动调整每个画面的大小,每个用户的画面大小一致,最多支持 17 个画面。 + // + // 2:垂直布局。指定 maxResolutionUid 在屏幕左侧显示大视窗画面,其他用户的小视窗画面在右侧垂直排列,最多两列,一列 8 个画面,最多支持共 17 个画面。 + // + // 3:自定义布局。由你在 layoutConfig 字段中自定义合流布局。 + MixedVideoLayout int `json:"mixedVideoLayout"` + + // BackgroundColor 视频画布的背景颜色。 + // + // 支持 RGB 颜色表,字符串格式为 # 号和 6 个十六进制数。 + // + // 默认值: #000000 + BackgroundColor string `json:"backgroundColor,omitempty"` + + // BackgroundImage 视频画布的背景图的 URL。背景图的显示模式为裁剪模式。 + // + // 裁剪模式:优先保证画面被填满。背景图尺寸等比缩放,直至整个画面被背景图填满。如果背景图长宽与显示窗口不同,则背景图会按照画面设置的比例进行周边裁剪后填满画面。 + BackgroundImage string `json:"backgroundImage,omitempty"` + + // DefaultUserBackgroundImage 默认的用户画面背景图的 URL。 + // + //配置该字段后,当任一⽤户停止发送视频流超过 3.5 秒,画⾯将切换为该背景图;如果针对某 UID 单独设置了背景图,则该设置会被覆盖。 + DefaultUserBackgroundImage string `json:"defaultUserBackgroundImage,omitempty"` + + LayoutConfig []UpdateLayoutConfig `json:"layoutConfig,omitempty"` + BackgroundConfig []BackgroundConfig `json:"backgroundConfig,omitempty"` +} + +// UpdateLayoutConfig 用户的合流画面布局。由每个用户对应的布局画面设置组成的数组,支持最多 17 个用户。 +type UpdateLayoutConfig struct { + // UID 字符串内容为待显示在该区域的用户的 UID,32 位无符号整数。 + // + // 如果不指定 UID,会按照用户加入频道的顺序自动匹配 layoutConfig 中的画面设置。 + UID string `json:"uid"` + + // XAxis 屏幕里该画面左上角的横坐标的相对值,精确到小数点后六位。从左到右布局,0.0 在最左端,1.0 在最右端。 + // + // 该字段也可以设置为整数 0 或 1。 + // + // 0 <= XAxis <= 1 + XAxis float32 `json:"x_axis"` + + // YAxis 屏幕里该画面左上角的纵坐标的相对值,精确到小数点后六位。从上到下布局,0.0 在最上端,1.0 在最下端。 + // + // 该字段也可以设置为整数 0 或 1。 + // + // 0 <= YAxis <= 1 + YAxis float32 `json:"y_axis"` + + // Width 该画面宽度的相对值,精确到小数点后六位。该字段也可以设置为整数 0 或 1。 + // + // 0 <= Width <= 1 + Width float32 `json:"width"` + + // Height 该画面高度的相对值,精确到小数点后六位。该字段也可以设置为整数 0 或 1。 + // + // 0 <= Height <= 1 + Height float32 `json:"height"` + + // Alpha 图像的透明度。精确到小数点后六位。 + // + // 0.0 表示图像为透明的,1.0 表示图像为完全不透明的。 + // + // 0 <= Alpha <= 1 + // 默认值:1 + Alpha float32 `json:"alpha"` + + // RenderMode 画面显示模式 + // + // 0:裁剪模式。优先保证画面被填满。视频尺寸等比缩放,直至整个画面被视频填满。如果视频长宽与显示窗口不同,则视频流会按照画面设置的比例进行周边裁剪后填满画面。 + // + // 1:缩放模式。优先保证视频内容全部显示。视频尺寸等比缩放,直至视频窗口的一边与画面边框对齐。如果视频尺寸与画面尺寸不一致,在保持长宽比的前提下,将视频进行缩放后填满画面,缩放后的视频四周会有一圈黑边。 + // + // 默认值:0 + RenderMode int `json:"render_mode"` +} + +type BackgroundConfig struct { + // UID 字符串内容为用户 UID + UID string `json:"uid"` + + // ImageURL 字符串内容该用户背景图片的 URL 地址 + // + // 配置背景图后,当该⽤户停止发送视频流超过 3.5 秒,画⾯将切换为该背景图。 + // + //URL 支持 HTTP 和 HTTPS 协议,图片格式支持 JPG 和 BMP。图片大小不得超过 6 MB。录制服务成功下载图片后,设置才会生效;如果下载失败,则设置不⽣效。不同字段设置可能会互相覆盖,具体规则详见设置背景色或背景图。 + ImageURL string `json:"image_url"` + + // RenderMode 画面显示模式 + // + //0:裁剪模式。优先保证画面被填满。视频尺寸等比缩放,直至整个画面被视频填满。如果视频长宽与显示窗口不同,则视频流会按照画面设置的比例进行周边裁剪后填满画面。 + // + //1:缩放模式。优先保证视频内容全部显示。视频尺寸等比缩放,直至视频窗口的一边与画面边框对齐。如果视频尺寸与画面尺寸不一致,在保持长宽比的前提下,将视频进行缩放后填满画面,缩放后的视频四周会有一圈黑边。 + // + // 默认值:0 + RenderMode int `json:"render_mode"` +} + +type UpdateLayoutSuccessResp struct { + ResourceId string `json:"resourceId"` + SID string `json:"sid"` +} + +type UpdateLayoutResp struct { + Response + SuccessResp UpdateLayoutSuccessResp +} + +func (s *UpdateLayout) Do(ctx context.Context, resourceID string, sid string, mode string, payload *UpdateLayoutReqBody) (*UpdateLayoutResp, error) { + path := s.buildPath(resourceID, sid, mode) + + responseData, err := s.client.DoREST(ctx, path, http.MethodPost, payload) + if err != nil { + var internalErr *core.InternalErr + if !errors.As(err, &internalErr) { + return nil, err + } + } + + var resp UpdateLayoutResp + if responseData.HttpStatusCode == http.StatusOK { + var successResponse UpdateLayoutSuccessResp + if err = responseData.UnmarshallToTarget(&successResponse); err != nil { + return nil, err + } + resp.SuccessResp = successResponse + } else { + codeResult := gjson.GetBytes(responseData.RawBody, "code") + if !codeResult.Exists() { + return nil, core.NewGatewayErr(responseData.HttpStatusCode, string(responseData.RawBody)) + } + var errResponse ErrResponse + if err = responseData.UnmarshallToTarget(&errResponse); err != nil { + return nil, err + } + resp.ErrResponse = errResponse + } + + resp.BaseResponse = responseData + return &resp, nil +} diff --git a/services/cloudrecording/v1/v1.go b/services/cloudrecording/v1/v1.go index 2f17884..3709570 100644 --- a/services/cloudrecording/v1/v1.go +++ b/services/cloudrecording/v1/v1.go @@ -5,18 +5,25 @@ import ( ) type BaseCollection struct { - prefixPath string - client core.Client - webRecording WebRecording + prefixPath string + client core.Client + webRecording WebRecording + mixRecording MixRecording + individualRecording IndividualRecording } -func NewCollection(prefixPath string, client core.Client, webRecording WebRecording) *BaseCollection { +func NewCollection(prefixPath string, client core.Client, webRecording WebRecording, mixRecording MixRecording, individualRecording IndividualRecording) *BaseCollection { b := &BaseCollection{ - prefixPath: "/v1" + prefixPath, - client: client, - webRecording: webRecording, + prefixPath: "/v1" + prefixPath, + client: client, + webRecording: webRecording, + mixRecording: mixRecording, + individualRecording: individualRecording, } b.webRecording.SetBase(b) + b.mixRecording.SetBase(b) + b.individualRecording.SetBase(b) + return b } @@ -29,6 +36,8 @@ func (c *BaseCollection) Acquire() *Acquire { func (c *BaseCollection) Start() *Starter { return &Starter{ + module: "cloudRecording:start", + logger: c.client.GetLogger(), client: c.client, prefixPath: c.prefixPath, } @@ -65,3 +74,11 @@ func (c *BaseCollection) UpdateLayout() *UpdateLayout { func (c *BaseCollection) WebRecording() WebRecording { return c.webRecording } + +func (c *BaseCollection) MixRecording() MixRecording { + return c.mixRecording +} + +func (c *BaseCollection) IndividualRecording() IndividualRecording { + return c.individualRecording +} diff --git a/services/cloudrecording/v1/webrecording/acquire.go b/services/cloudrecording/v1/webrecording/acquire.go index 0bf369b..29c31fa 100644 --- a/services/cloudrecording/v1/webrecording/acquire.go +++ b/services/cloudrecording/v1/webrecording/acquire.go @@ -7,13 +7,13 @@ import ( ) type Acquire struct { - BaseAcquire *baseV1.Acquire + Base *baseV1.Acquire } var _ baseV1.AcquireWebRecording = (*Acquire)(nil) func (a *Acquire) Do(ctx context.Context, cname string, uid string, clientRequest *baseV1.AcquirerWebRecodingClientRequest) (*baseV1.AcquirerResp, error) { - return a.BaseAcquire.Do(ctx, &baseV1.AcquirerReqBody{ + return a.Base.Do(ctx, &baseV1.AcquirerReqBody{ Cname: cname, Uid: uid, ClientRequest: &baseV1.AcquirerClientRequest{ diff --git a/services/cloudrecording/v1/webrecording/query.go b/services/cloudrecording/v1/webrecording/query.go index 7482bf6..a003f2c 100644 --- a/services/cloudrecording/v1/webrecording/query.go +++ b/services/cloudrecording/v1/webrecording/query.go @@ -7,13 +7,13 @@ import ( ) type Query struct { - BaseQuery *baseV1.Query + Base *baseV1.Query } var _ baseV1.QueryWebRecording = (*Query)(nil) func (q *Query) Do(ctx context.Context, resourceID string, sid string) (*baseV1.QueryWebRecordingResp, error) { - resp, err := q.BaseQuery.Do(ctx, resourceID, sid, baseV1.WebMode) + resp, err := q.Base.Do(ctx, resourceID, sid, baseV1.WebMode) if err != nil { return nil, err } diff --git a/services/cloudrecording/v1/webrecording/start.go b/services/cloudrecording/v1/webrecording/start.go index 2d0f763..bc66d38 100644 --- a/services/cloudrecording/v1/webrecording/start.go +++ b/services/cloudrecording/v1/webrecording/start.go @@ -7,14 +7,14 @@ import ( ) type Starter struct { - BaseStarter *baseV1.Starter + Base *baseV1.Starter } var _ baseV1.StartWebRecording = (*Starter)(nil) func (s *Starter) Do(ctx context.Context, resourceID string, cname string, uid string, clientRequest *baseV1.StartWebRecordingClientRequest) (*baseV1.StarterResp, error) { mode := baseV1.WebMode - return s.BaseStarter.Do(ctx, resourceID, mode, &baseV1.StartReqBody{ + return s.Base.Do(ctx, resourceID, mode, &baseV1.StartReqBody{ Cname: cname, Uid: uid, ClientRequest: &baseV1.StartClientRequest{ diff --git a/services/cloudrecording/v1/webrecording/webrecording.go b/services/cloudrecording/v1/webrecording/webrecording.go index d785766..e491fc6 100644 --- a/services/cloudrecording/v1/webrecording/webrecording.go +++ b/services/cloudrecording/v1/webrecording/webrecording.go @@ -19,15 +19,15 @@ func (w *Impl) SetBase(base *baseV1.BaseCollection) { } func (w *Impl) Acquire() baseV1.AcquireWebRecording { - return &Acquire{BaseAcquire: w.Base.Acquire()} + return &Acquire{Base: w.Base.Acquire()} } func (w *Impl) Query() baseV1.QueryWebRecording { - return &Query{BaseQuery: w.Base.Query()} + return &Query{Base: w.Base.Query()} } func (w *Impl) Start() baseV1.StartWebRecording { - return &Starter{BaseStarter: w.Base.Start()} + return &Starter{Base: w.Base.Start()} } func (w *Impl) Stop() baseV1.StopWebRecording {