# 多主播场景下的视频体验优化 (/zh-CN/realtime-media/rtc/build/video/custom-layout)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="android" />
在多主播（4 个或以上）发流的视频场景下，观众端面临着如下挑战和需求：

- 设备性能和下行带宽压力：多路视频流订阅对设备性能和观众端带宽的要求较高，尤其在中低端设备上可能出现卡顿和掉帧问题，容易给观众带来负面体验。
- 灵活布局需求：需要提供自定义视频画面布局的功能，支持观众调整每个主播画面的位置，以满足观众个性化的观看需求。
- 窗口切换与放大需求：在众多主播中，属于每个主播的画面大小有限，灵活切换和调整窗口大小可以实现“千人千面”的观看体验。

该方案可广泛应用于如下场景：

- 团战 PK
- 多人会议
- 在线教育大班课

## 技术原理 [#技术原理]

客户端自定义合图布局需要结合云端转码服务一起使用，技术原理如下所示：

<DocsImage alt="自定义合图布局的云端转码技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech1.png" displayWidth="90%" />

当观众端收到已转码的合图视频流后，在本地实现自定义合图布局的技术原理如下所示：

<DocsImage alt="观众端自定义合图布局技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech2.png" displayWidth="90%" />

## 前提条件 [#前提条件]

- 项目已集成 RTC SDK v4.5.x 以上版本。
- 已经在项目中实现了基本的实时音视频功能。详见[实现音视频互动](/zh-CN/realtime-media/rtc/get-started/quick-start)。
- 已实现[云端转码](/zh-CN/realtime-media/transcoding)，将合图后的视频转发至其他频道，并在调用[创建云端转码 API](/zh-CN/api-reference/api-ref/cloud-transcoding/create) 时设置以下参数：

  ```json
  "outputs": [
    {   
        "seiOption":{
            "source":{
                "agora": {
                    "uidvideoLayout": true
                }
            },
            "sink":{
                "type": 100
            }
        }
    }
  ]
  
  ```

## 实现方法 [#实现方法]

本节介绍如何实现客户端自定义合图布局，API 调用时序如下图所示。

<DocsImage alt="客户端自定义合图布局 API 调用时序" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/custom-layout-sequence.png" displayWidth="80%" />

通过以下步骤，在接收端实现自定义合图布局。

:::info[信息]
本文的 API 和示例代码均以 Android 平台为例。如目标平台为 iOS，请参考 iOS 的 API 文档。
:::

1. 调用 `joinChannel`[2/2] 加入合图流所在的频道。

2. 通过 SDK 触发的 `onTranscodedStreamLayoutInfo` 回调，获取合图视频流的以下信息：
   - 合图视频流的宽、高，以及发布该视频流的用户 ID。
   - 发布合图子视频流的用户 ID。
   - 合图子视频流在合图画布上的 x 和 y 坐标。
   - 合图子视频流的宽度和高度。
   - 合图子视频流在合图画布上的状态：正常渲染、显示为占位符，或显示为黑色图片。

3. 接收端调用 `setupRemoteVideo` 方法设置远端视频流在本地的渲染模式，重点参数设置如下所示：

   - `uid`：将合图视频流转发至当前频道的用户 ID，即在 `onTranscodedStreamLayoutInfo` 回调中获取的 `uid`。
   - `view`：在本地显示的远端视频视图。你可以通过该参数设置每一路子视频流在本地视图中显示的位置。
   - `subviewUid`：合图视频流中某一路子视频流所对应的用户 ID。

   :::info[信息]
   - 当 `subviewUid` 的值不为 0 时，`setupMode` 的默认值为 `VIEW_SETUP_MODE_REPLACE`（增加一个视图）。

         - 如果设置了 `subviewUid`，则对 `rect` 的设置将不会生效。
   :::

   以下示例代码以多频道场景下调用 `setupRemoteVideoEx` 为例进行介绍，在非多频道场景下调用 `setupRemoteVideo` 的方式与之类似。

   ```java
   mRtcEngine.joinChannelEx(null, connection, mediaOptions, new IRtcEngineEventHandler() {
    @Override
    public void onTranscodedStreamLayoutInfo(final int uid, final VideoLayoutInfo info) {
        super.onTranscodedStreamLayoutInfo(uid, info);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int size = info.layoutNumber;
 
                viewGrids.setChildCount(info.layoutNumber);
                for (int i = 0; i < size; i++) {
                    if(mUserViews.containsKey(info.layoutList[i].uid)) {
                        continue;
                    }
                    // 设置 view 动态添加
                    TextureView textureView = new TextureView(VideoChatViewActivity.this);
                    textureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    users.add(info.layoutList[i].uid);
                    views.add(textureView);
                    mUserViews.put(info.layoutList[i].uid, textureView);
                    viewGrids.addView(textureView);
                    // 设置 VideoCanvas，除 uid 和 subviewUid 以外的其他参数可根据实际需要设置
                    VideoCanvas canvas = new VideoCanvas(textureView);
                    // 转发合图视频流到当前频道的用户 ID
                    canvas.uid = uid;
                    // 对应某一路合图子视频流的用户 ID
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.renderMode = Constants.RENDER_MODE_FIT;
                    mRtcEngine.setupRemoteVideoEx(canvas, connection);
                    // 如果要删除某一路子视频流，则添加以下代码
                    VideoCanvas canvas = new VideoCanvas(textureView); // 此处的 textureview 设置为需要删除的 view
                    canvas.uid = uid;
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE;
                    mRtcEngine.setupRemoteVideoEx(canvas);
                }
            }
        });
    }
   });
   ```

4. 当合图视频流的布局信息发生变化时，`onTranscodedStreamLayoutInfo` 回调会再次触发。接收端可根据每一次回调上报的具体信息，重新调用 `setupRemoteVideo` 调整本地的视图渲染布局。

   :::info[信息]
   布局信息变化可能存在以下几种情况：

            - 合图子视频在合图画布上的坐标变化。
            - 合图子视频的宽度或高度变化。
            - 合图子视频流的状态变化。当发布该子视频流的主播退出频道时，该路子视频流的 `videoState` 参数会从 `0`（正常） 变为 `1`（该视频流无视频画面）。你可以在调用 `setupRemoteVideo` 时，将 `subviewUid` 设置为该路子视频流的用户 ID、然后将 `setupMode` 设置为 `VIEW_SETUP_MODE_REMOVE`（删除视图），即可停止渲染该主播的视频画面。
   :::

5. 监听 `onUserOffline` 回调，当转发合图流的用户退出频道时，停止在接收端渲染合图流。

## 注意事项 [#注意事项]

- 目前仅支持在移动端实现自定义合图布局功能。
- 当合图视频流被添加了水印时，水印会随所在的区域被渲染，可能会出现被分割的情况。

## 参考信息 [#参考信息]

- [`onTranscodedStreamLayoutInfo`](/zh-CN/api-reference/rtc/android/video/video-rendering#callback_irtcengineeventhandler_ontranscodedstreamlayoutinfo)
- [`setupRemoteVideo`](/zh-CN/api-reference/rtc/android/video/video-rendering#api_irtcengine_setupremotevideo)
- [`setupRemoteVideoEx`](/zh-CN/api-reference/rtc/android/video/video-rendering#api_irtcengineex_setupremotevideoex)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="ios" />
在多主播（4 个或以上）发流的视频场景下，观众端面临着如下挑战和需求：

- 设备性能和下行带宽压力：多路视频流订阅对设备性能和观众端带宽的要求较高，尤其在中低端设备上可能出现卡顿和掉帧问题，容易给观众带来负面体验。
- 灵活布局需求：需要提供自定义视频画面布局的功能，支持观众调整每个主播画面的位置，以满足观众个性化的观看需求。
- 窗口切换与放大需求：在众多主播中，属于每个主播的画面大小有限，灵活切换和调整窗口大小可以实现“千人千面”的观看体验。

因此，声网推出了多主播视频场景下的视频体验优化方案，通过**客户端自定义合图布局**实现，以观众为中心，创造流畅、个性化的视频观看体验。

该方案可广泛应用于如下场景：

- 团战 PK
- 多人会议
- 在线教育大班课

## 技术原理 [#技术原理-1]

客户端自定义合图布局需要结合云端转码服务一起使用，技术原理如下所示：

<DocsImage alt="自定义合图布局的云端转码技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech1.png" displayWidth="90%" />

当观众端收到已转码的合图视频流后，在本地实现自定义合图布局的技术原理如下所示：

<DocsImage alt="观众端自定义合图布局技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech2.png" displayWidth="90%" />

## 前提条件 [#前提条件-1]

- 项目已集成 RTC SDK v4.5.x 以上版本。
- 已经在项目中实现了基本的实时音视频功能。详见[实现音视频互动](/zh-CN/realtime-media/rtc/get-started/quick-start)。
- 已实现[云端转码](/zh-CN/realtime-media/transcoding)，将合图后的视频转发至其他频道，并在调用[创建云端转码 API](/zh-CN/api-reference/api-ref/cloud-transcoding/create) 时设置以下参数：

  ```json
  "outputs": [
    {   
        "seiOption":{
            "source":{
                "agora": {
                    "uidvideoLayout": true
                }
            },
            "sink":{
                "type": 100
            }
        }
    }
  ]
  
  ```

## 实现方法 [#实现方法-1]

本节介绍如何实现客户端自定义合图布局，API 调用时序如下图所示。

<DocsImage alt="客户端自定义合图布局 API 调用时序" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/custom-layout-sequence.png" displayWidth="80%" />

通过以下步骤，在接收端实现自定义合图布局。

:::info[信息]
本文的 API 和示例代码均以 Android 平台为例。如目标平台为 iOS，请参考 iOS 的 API 文档。
:::

1. 调用 `joinChannel`[2/2] 加入合图流所在的频道。

2. 通过 SDK 触发的 `onTranscodedStreamLayoutInfo` 回调，获取合图视频流的以下信息：
   - 合图视频流的宽、高，以及发布该视频流的用户 ID。
   - 发布合图子视频流的用户 ID。
   - 合图子视频流在合图画布上的 x 和 y 坐标。
   - 合图子视频流的宽度和高度。
   - 合图子视频流在合图画布上的状态：正常渲染、显示为占位符，或显示为黑色图片。

3. 接收端调用 `setupRemoteVideo` 方法设置远端视频流在本地的渲染模式，重点参数设置如下所示：

   - `uid`：将合图视频流转发至当前频道的用户 ID，即在 `onTranscodedStreamLayoutInfo` 回调中获取的 `uid`。
   - `view`：在本地显示的远端视频视图。你可以通过该参数设置每一路子视频流在本地视图中显示的位置。
   - `subviewUid`：合图视频流中某一路子视频流所对应的用户 ID。

   :::info[信息]
   - 当 `subviewUid` 的值不为 0 时，`setupMode` 的默认值为 `VIEW_SETUP_MODE_REPLACE`（增加一个视图）。

         - 如果设置了 `subviewUid`，则对 `cropArea` 的设置将不会生效。
   :::

   以下示例代码以多频道场景下调用 `setupRemoteVideoEx` 为例进行介绍，在非多频道场景下调用 `setupRemoteVideo` 的方式与之类似。

   ```java
   mRtcEngine.joinChannelEx(null, connection, mediaOptions, new IRtcEngineEventHandler() {
    @Override
    public void onTranscodedStreamLayoutInfo(final int uid, final VideoLayoutInfo info) {
        super.onTranscodedStreamLayoutInfo(uid, info);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int size = info.layoutNumber;
 
                viewGrids.setChildCount(info.layoutNumber);
                for (int i = 0; i < size; i++) {
                    if(mUserViews.containsKey(info.layoutList[i].uid)) {
                        continue;
                    }
                    // 设置 view 动态添加
                    TextureView textureView = new TextureView(VideoChatViewActivity.this);
                    textureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    users.add(info.layoutList[i].uid);
                    views.add(textureView);
                    mUserViews.put(info.layoutList[i].uid, textureView);
                    viewGrids.addView(textureView);
                    // 设置 VideoCanvas，除 uid 和 subviewUid 以外的其他参数可根据实际需要设置
                    VideoCanvas canvas = new VideoCanvas(textureView);
                    // 转发合图视频流到当前频道的用户 ID
                    canvas.uid = uid;
                    // 对应某一路合图子视频流的用户 ID
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.renderMode = Constants.RENDER_MODE_FIT;
                    mRtcEngine.setupRemoteVideoEx(canvas, connection);
                    // 如果要删除某一路子视频流，则添加以下代码
                    VideoCanvas canvas = new VideoCanvas(textureView); // 此处的 textureview 设置为需要删除的 view
                    canvas.uid = uid;
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE;
                    mRtcEngine.setupRemoteVideoEx(canvas);
                }
            }
        });
    }
   });
   ```

4. 当合图视频流的布局信息发生变化时，`onTranscodedStreamLayoutInfo` 回调会再次触发。接收端可根据每一次回调上报的具体信息，重新调用 `setupRemoteVideo` 调整本地的视图渲染布局。

   :::info[信息]
   布局信息变化可能存在以下几种情况：

            - 合图子视频在合图画布上的坐标变化。
            - 合图子视频的宽度或高度变化。
            - 合图子视频流的状态变化。当发布该子视频流的主播退出频道时，该路子视频流的 `videoState` 参数会从 `0`（正常） 变为 `1`（该视频流无视频画面）。你可以在调用 `setupRemoteVideo` 时，将 `subviewUid` 设置为该路子视频流的用户 ID、然后将 `setupMode` 设置为 `VIEW_SETUP_MODE_REMOVE`（删除视图），即可停止渲染该主播的视频画面。
   :::

5. 监听 `onUserOffline` 回调，当转发合图流的用户退出频道时，停止在接收端渲染合图流。

## 注意事项 [#注意事项-1]

- 目前仅支持在移动端实现自定义合图布局功能。
- 当合图视频流被添加了水印时，水印会随所在的区域被渲染，可能会出现被分割的情况。

## 参考信息 [#参考信息-1]

- [`onTranscodedStreamLayoutInfo`](/zh-CN/api-reference/rtc/ios/video/video-rendering#callback_irtcengineeventhandler_ontranscodedstreamlayoutinfo)
- [`setupRemoteVideo`](/zh-CN/api-reference/rtc/ios/video/video-rendering#api_irtcengine_setupremotevideo)
- [`setupRemoteVideoEx`](/zh-CN/api-reference/rtc/ios/video/video-rendering#api_irtcengineex_setupremotevideoex)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="unity" />
在多主播（4 个或以上）发流的视频场景下，观众端面临着如下挑战和需求：

- 设备性能和下行带宽压力：多路视频流订阅对设备性能和观众端带宽的要求较高，尤其在中低端设备上可能出现卡顿和掉帧问题，容易给观众带来负面体验。
- 灵活布局需求：需要提供自定义视频画面布局的功能，支持观众调整每个主播画面的位置，以满足观众个性化的观看需求。
- 窗口切换与放大需求：在众多主播中，属于每个主播的画面大小有限，灵活切换和调整窗口大小可以实现“千人千面”的观看体验。

因此，声网推出了多主播视频场景下的视频体验优化方案，通过**客户端自定义合图布局**实现，以观众为中心，创造流畅、个性化的视频观看体验。

该方案可广泛应用于如下场景：

- 团战 PK
- 多人会议
- 在线教育大班课

## 技术原理 [#技术原理-2]

客户端自定义合图布局需要结合云端转码服务一起使用，技术原理如下所示：

<DocsImage alt="自定义合图布局的云端转码技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech1.png" displayWidth="90%" />

当观众端收到已转码的合图视频流后，在本地实现自定义合图布局的技术原理如下所示：

<DocsImage alt="观众端自定义合图布局技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech2.png" displayWidth="90%" />

## 前提条件 [#前提条件-2]

- 项目已集成 RTC SDK v4.5.x 以上版本。
- 已经在项目中实现了基本的实时音视频功能。详见[实现音视频互动](/zh-CN/realtime-media/rtc/get-started/quick-start)。
- 已实现[云端转码](/zh-CN/realtime-media/transcoding)，将合图后的视频转发至其他频道，并在调用[创建云端转码 API](/zh-CN/api-reference/api-ref/cloud-transcoding/create) 时设置以下参数：

  ```json
  "outputs": [
    {   
        "seiOption":{
            "source":{
                "agora": {
                    "uidvideoLayout": true
                }
            },
            "sink":{
                "type": 100
            }
        }
    }
  ]
  
  ```

## 实现方法 [#实现方法-2]

本节介绍如何实现客户端自定义合图布局，API 调用时序如下图所示。

<DocsImage alt="客户端自定义合图布局 API 调用时序" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/custom-layout-sequence.png" displayWidth="80%" />

通过以下步骤，在接收端实现自定义合图布局。

:::info[信息]
本文的 API 和示例代码均以 Android 平台为例。如目标平台为 iOS，请参考 iOS 的 API 文档。
:::

1. 调用 `joinChannel`[2/2] 加入合图流所在的频道。

2. 通过 SDK 触发的 `onTranscodedStreamLayoutInfo` 回调，获取合图视频流的以下信息：
   - 合图视频流的宽、高，以及发布该视频流的用户 ID。
   - 发布合图子视频流的用户 ID。
   - 合图子视频流在合图画布上的 x 和 y 坐标。
   - 合图子视频流的宽度和高度。
   - 合图子视频流在合图画布上的状态：正常渲染、显示为占位符，或显示为黑色图片。

3. 接收端调用 `setupRemoteVideo` 方法设置远端视频流在本地的渲染模式，重点参数设置如下所示：

   - `uid`：将合图视频流转发至当前频道的用户 ID，即在 `onTranscodedStreamLayoutInfo` 回调中获取的 `uid`。
   - `view`：在本地显示的远端视频视图。你可以通过该参数设置每一路子视频流在本地视图中显示的位置。
   - `subviewUid`：合图视频流中某一路子视频流所对应的用户 ID。

   :::info[信息]
   - 当 `subviewUid` 的值不为 0 时，`setupMode` 的默认值为 `VIEW_SETUP_MODE_REPLACE`（增加一个视图）。
   :::

   以下示例代码以多频道场景下调用 `setupRemoteVideoEx` 为例进行介绍，在非多频道场景下调用 `setupRemoteVideo` 的方式与之类似。

   ```java
   mRtcEngine.joinChannelEx(null, connection, mediaOptions, new IRtcEngineEventHandler() {
    @Override
    public void onTranscodedStreamLayoutInfo(final int uid, final VideoLayoutInfo info) {
        super.onTranscodedStreamLayoutInfo(uid, info);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int size = info.layoutNumber;
 
                viewGrids.setChildCount(info.layoutNumber);
                for (int i = 0; i < size; i++) {
                    if(mUserViews.containsKey(info.layoutList[i].uid)) {
                        continue;
                    }
                    // 设置 view 动态添加
                    TextureView textureView = new TextureView(VideoChatViewActivity.this);
                    textureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    users.add(info.layoutList[i].uid);
                    views.add(textureView);
                    mUserViews.put(info.layoutList[i].uid, textureView);
                    viewGrids.addView(textureView);
                    // 设置 VideoCanvas，除 uid 和 subviewUid 以外的其他参数可根据实际需要设置
                    VideoCanvas canvas = new VideoCanvas(textureView);
                    // 转发合图视频流到当前频道的用户 ID
                    canvas.uid = uid;
                    // 对应某一路合图子视频流的用户 ID
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.renderMode = Constants.RENDER_MODE_FIT;
                    mRtcEngine.setupRemoteVideoEx(canvas, connection);
                    // 如果要删除某一路子视频流，则添加以下代码
                    VideoCanvas canvas = new VideoCanvas(textureView); // 此处的 textureview 设置为需要删除的 view
                    canvas.uid = uid;
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE;
                    mRtcEngine.setupRemoteVideoEx(canvas);
                }
            }
        });
    }
   });
   ```

4. 当合图视频流的布局信息发生变化时，`onTranscodedStreamLayoutInfo` 回调会再次触发。接收端可根据每一次回调上报的具体信息，重新调用 `setupRemoteVideo` 调整本地的视图渲染布局。

   :::info[信息]
   布局信息变化可能存在以下几种情况：

            - 合图子视频在合图画布上的坐标变化。
            - 合图子视频的宽度或高度变化。
            - 合图子视频流的状态变化。当发布该子视频流的主播退出频道时，该路子视频流的 `videoState` 参数会从 `0`（正常） 变为 `1`（该视频流无视频画面）。你可以在调用 `setupRemoteVideo` 时，将 `subviewUid` 设置为该路子视频流的用户 ID、然后将 `setupMode` 设置为 `VIEW_SETUP_MODE_REMOVE`（删除视图），即可停止渲染该主播的视频画面。
   :::

5. 监听 `onUserOffline` 回调，当转发合图流的用户退出频道时，停止在接收端渲染合图流。

## 注意事项 [#注意事项-2]

- 目前仅支持在移动端实现自定义合图布局功能。
- 当合图视频流被添加了水印时，水印会随所在的区域被渲染，可能会出现被分割的情况。

## 参考信息 [#参考信息-2]

- [`onTranscodedStreamLayoutInfo`](/zh-CN/api-reference/rtc/unity/video/video-rendering#callback_irtcengineeventhandler_ontranscodedstreamlayoutinfo)
- [`setupRemoteVideo`](/zh-CN/api-reference/rtc/unity/video/video-rendering#api_irtcengine_setupremotevideo)
- [`setupRemoteVideoEx`](/zh-CN/api-reference/rtc/unity/video/video-rendering#api_irtcengineex_setupremotevideoex)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="flutter" />
在多主播（4 个或以上）发流的视频场景下，观众端面临着如下挑战和需求：

- 设备性能和下行带宽压力：多路视频流订阅对设备性能和观众端带宽的要求较高，尤其在中低端设备上可能出现卡顿和掉帧问题，容易给观众带来负面体验。
- 灵活布局需求：需要提供自定义视频画面布局的功能，支持观众调整每个主播画面的位置，以满足观众个性化的观看需求。
- 窗口切换与放大需求：在众多主播中，属于每个主播的画面大小有限，灵活切换和调整窗口大小可以实现“千人千面”的观看体验。

因此，声网推出了多主播视频场景下的视频体验优化方案，通过**客户端自定义合图布局**实现，以观众为中心，创造流畅、个性化的视频观看体验。

该方案可广泛应用于如下场景：

- 团战 PK
- 多人会议
- 在线教育大班课

## 技术原理 [#技术原理-3]

客户端自定义合图布局需要结合云端转码服务一起使用，技术原理如下所示：

<DocsImage alt="自定义合图布局的云端转码技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech1.png" displayWidth="90%" />

当观众端收到已转码的合图视频流后，在本地实现自定义合图布局的技术原理如下所示：

<DocsImage alt="观众端自定义合图布局技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech2.png" displayWidth="90%" />

## 前提条件 [#前提条件-3]

- 项目已集成 RTC SDK v4.5.x 以上版本。
- 已经在项目中实现了基本的实时音视频功能。详见[实现音视频互动](/zh-CN/realtime-media/rtc/get-started/quick-start)。
- 已实现[云端转码](/zh-CN/realtime-media/transcoding)，将合图后的视频转发至其他频道，并在调用[创建云端转码 API](/zh-CN/api-reference/api-ref/cloud-transcoding/create) 时设置以下参数：

  ```json
  "outputs": [
    {   
        "seiOption":{
            "source":{
                "agora": {
                    "uidvideoLayout": true
                }
            },
            "sink":{
                "type": 100
            }
        }
    }
  ]
  
  ```

## 实现方法 [#实现方法-3]

本节介绍如何实现客户端自定义合图布局，API 调用时序如下图所示。

<DocsImage alt="客户端自定义合图布局 API 调用时序" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/custom-layout-sequence.png" displayWidth="80%" />

通过以下步骤，在接收端实现自定义合图布局。

:::info[信息]
本文的 API 和示例代码均以 Android 平台为例。如目标平台为 iOS，请参考 iOS 的 API 文档。
:::

1. 调用 `joinChannel`[2/2] 加入合图流所在的频道。

2. 通过 SDK 触发的 `onTranscodedStreamLayoutInfo` 回调，获取合图视频流的以下信息：
   - 合图视频流的宽、高，以及发布该视频流的用户 ID。
   - 发布合图子视频流的用户 ID。
   - 合图子视频流在合图画布上的 x 和 y 坐标。
   - 合图子视频流的宽度和高度。
   - 合图子视频流在合图画布上的状态：正常渲染、显示为占位符，或显示为黑色图片。

3. 接收端调用 `setupRemoteVideo` 方法设置远端视频流在本地的渲染模式，重点参数设置如下所示：

   - `uid`：将合图视频流转发至当前频道的用户 ID，即在 `onTranscodedStreamLayoutInfo` 回调中获取的 `uid`。
   - `view`：在本地显示的远端视频视图。你可以通过该参数设置每一路子视频流在本地视图中显示的位置。
   - `subviewUid`：合图视频流中某一路子视频流所对应的用户 ID。

   :::info[信息]
   - 当 `subviewUid` 的值不为 0 时，`setupMode` 的默认值为 `VIEW_SETUP_MODE_REPLACE`（增加一个视图）。
   :::

   以下示例代码以多频道场景下调用 `setupRemoteVideoEx` 为例进行介绍，在非多频道场景下调用 `setupRemoteVideo` 的方式与之类似。

   ```java
   mRtcEngine.joinChannelEx(null, connection, mediaOptions, new IRtcEngineEventHandler() {
    @Override
    public void onTranscodedStreamLayoutInfo(final int uid, final VideoLayoutInfo info) {
        super.onTranscodedStreamLayoutInfo(uid, info);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int size = info.layoutNumber;
 
                viewGrids.setChildCount(info.layoutNumber);
                for (int i = 0; i < size; i++) {
                    if(mUserViews.containsKey(info.layoutList[i].uid)) {
                        continue;
                    }
                    // 设置 view 动态添加
                    TextureView textureView = new TextureView(VideoChatViewActivity.this);
                    textureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    users.add(info.layoutList[i].uid);
                    views.add(textureView);
                    mUserViews.put(info.layoutList[i].uid, textureView);
                    viewGrids.addView(textureView);
                    // 设置 VideoCanvas，除 uid 和 subviewUid 以外的其他参数可根据实际需要设置
                    VideoCanvas canvas = new VideoCanvas(textureView);
                    // 转发合图视频流到当前频道的用户 ID
                    canvas.uid = uid;
                    // 对应某一路合图子视频流的用户 ID
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.renderMode = Constants.RENDER_MODE_FIT;
                    mRtcEngine.setupRemoteVideoEx(canvas, connection);
                    // 如果要删除某一路子视频流，则添加以下代码
                    VideoCanvas canvas = new VideoCanvas(textureView); // 此处的 textureview 设置为需要删除的 view
                    canvas.uid = uid;
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE;
                    mRtcEngine.setupRemoteVideoEx(canvas);
                }
            }
        });
    }
   });
   ```

4. 当合图视频流的布局信息发生变化时，`onTranscodedStreamLayoutInfo` 回调会再次触发。接收端可根据每一次回调上报的具体信息，重新调用 `setupRemoteVideo` 调整本地的视图渲染布局。

   :::info[信息]
   布局信息变化可能存在以下几种情况：

            - 合图子视频在合图画布上的坐标变化。
            - 合图子视频的宽度或高度变化。
            - 合图子视频流的状态变化。当发布该子视频流的主播退出频道时，该路子视频流的 `videoState` 参数会从 `0`（正常） 变为 `1`（该视频流无视频画面）。你可以在调用 `setupRemoteVideo` 时，将 `subviewUid` 设置为该路子视频流的用户 ID、然后将 `setupMode` 设置为 `VIEW_SETUP_MODE_REMOVE`（删除视图），即可停止渲染该主播的视频画面。
   :::

5. 监听 `onUserOffline` 回调，当转发合图流的用户退出频道时，停止在接收端渲染合图流。

## 注意事项 [#注意事项-3]

- 目前仅支持在移动端实现自定义合图布局功能。
- 当合图视频流被添加了水印时，水印会随所在的区域被渲染，可能会出现被分割的情况。

## 参考信息 [#参考信息-3]

- [`onTranscodedStreamLayoutInfo`](/zh-CN/api-reference/rtc/flutter/video/video-rendering#callback_irtcengineeventhandler_ontranscodedstreamlayoutinfo)
- [`setupRemoteVideo`](/zh-CN/api-reference/rtc/flutter/video/video-rendering#api_irtcengine_setupremotevideo)
- [`setupRemoteVideoEx`](/zh-CN/api-reference/rtc/flutter/video/video-rendering#api_irtcengineex_setupremotevideoex)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="react-native" />
在多主播（4 个或以上）发流的视频场景下，观众端面临着如下挑战和需求：

- 设备性能和下行带宽压力：多路视频流订阅对设备性能和观众端带宽的要求较高，尤其在中低端设备上可能出现卡顿和掉帧问题，容易给观众带来负面体验。
- 灵活布局需求：需要提供自定义视频画面布局的功能，支持观众调整每个主播画面的位置，以满足观众个性化的观看需求。
- 窗口切换与放大需求：在众多主播中，属于每个主播的画面大小有限，灵活切换和调整窗口大小可以实现“千人千面”的观看体验。

因此，声网推出了多主播视频场景下的视频体验优化方案，通过**客户端自定义合图布局**实现，以观众为中心，创造流畅、个性化的视频观看体验。

该方案可广泛应用于如下场景：

- 团战 PK
- 多人会议
- 在线教育大班课

## 技术原理 [#技术原理-4]

客户端自定义合图布局需要结合云端转码服务一起使用，技术原理如下所示：

<DocsImage alt="自定义合图布局的云端转码技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech1.png" displayWidth="90%" />

当观众端收到已转码的合图视频流后，在本地实现自定义合图布局的技术原理如下所示：

<DocsImage alt="观众端自定义合图布局技术原理" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/tech2.png" displayWidth="90%" />

## 前提条件 [#前提条件-4]

- 项目已集成 RTC SDK v4.5.x 以上版本。
- 已经在项目中实现了基本的实时音视频功能。详见[实现音视频互动](/zh-CN/realtime-media/rtc/get-started/quick-start)。
- 已实现[云端转码](/zh-CN/realtime-media/transcoding)，将合图后的视频转发至其他频道，并在调用[创建云端转码 API](/zh-CN/api-reference/api-ref/cloud-transcoding/create) 时设置以下参数：

  ```json
  "outputs": [
    {   
        "seiOption":{
            "source":{
                "agora": {
                    "uidvideoLayout": true
                }
            },
            "sink":{
                "type": 100
            }
        }
    }
  ]
  
  ```

## 实现方法 [#实现方法-4]

本节介绍如何实现客户端自定义合图布局，API 调用时序如下图所示。

<DocsImage alt="客户端自定义合图布局 API 调用时序" src="https://web-cdn.agora.io/docs-portal/img/rtc/custom-layout/custom-layout-sequence.png" displayWidth="80%" />

通过以下步骤，在接收端实现自定义合图布局。

:::info[信息]
本文的 API 和示例代码均以 Android 平台为例。如目标平台为 iOS，请参考 iOS 的 API 文档。
:::

1. 调用 `joinChannel`[2/2] 加入合图流所在的频道。

2. 通过 SDK 触发的 `onTranscodedStreamLayoutInfo` 回调，获取合图视频流的以下信息：
   - 合图视频流的宽、高，以及发布该视频流的用户 ID。
   - 发布合图子视频流的用户 ID。
   - 合图子视频流在合图画布上的 x 和 y 坐标。
   - 合图子视频流的宽度和高度。
   - 合图子视频流在合图画布上的状态：正常渲染、显示为占位符，或显示为黑色图片。

3. 接收端调用 `setupRemoteVideo` 方法设置远端视频流在本地的渲染模式，重点参数设置如下所示：

   - `uid`：将合图视频流转发至当前频道的用户 ID，即在 `onTranscodedStreamLayoutInfo` 回调中获取的 `uid`。
   - `view`：在本地显示的远端视频视图。你可以通过该参数设置每一路子视频流在本地视图中显示的位置。
   - `subviewUid`：合图视频流中某一路子视频流所对应的用户 ID。

   :::info[信息]
   - 当 `subviewUid` 的值不为 0 时，`setupMode` 的默认值为 `VIEW_SETUP_MODE_REPLACE`（增加一个视图）。
   :::

   以下示例代码以多频道场景下调用 `setupRemoteVideoEx` 为例进行介绍，在非多频道场景下调用 `setupRemoteVideo` 的方式与之类似。

   ```java
   mRtcEngine.joinChannelEx(null, connection, mediaOptions, new IRtcEngineEventHandler() {
    @Override
    public void onTranscodedStreamLayoutInfo(final int uid, final VideoLayoutInfo info) {
        super.onTranscodedStreamLayoutInfo(uid, info);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                int size = info.layoutNumber;
 
                viewGrids.setChildCount(info.layoutNumber);
                for (int i = 0; i < size; i++) {
                    if(mUserViews.containsKey(info.layoutList[i].uid)) {
                        continue;
                    }
                    // 设置 view 动态添加
                    TextureView textureView = new TextureView(VideoChatViewActivity.this);
                    textureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                    users.add(info.layoutList[i].uid);
                    views.add(textureView);
                    mUserViews.put(info.layoutList[i].uid, textureView);
                    viewGrids.addView(textureView);
                    // 设置 VideoCanvas，除 uid 和 subviewUid 以外的其他参数可根据实际需要设置
                    VideoCanvas canvas = new VideoCanvas(textureView);
                    // 转发合图视频流到当前频道的用户 ID
                    canvas.uid = uid;
                    // 对应某一路合图子视频流的用户 ID
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.renderMode = Constants.RENDER_MODE_FIT;
                    mRtcEngine.setupRemoteVideoEx(canvas, connection);
                    // 如果要删除某一路子视频流，则添加以下代码
                    VideoCanvas canvas = new VideoCanvas(textureView); // 此处的 textureview 设置为需要删除的 view
                    canvas.uid = uid;
                    canvas.subviewUid = info.layoutList[i].uid;
                    canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE;
                    mRtcEngine.setupRemoteVideoEx(canvas);
                }
            }
        });
    }
   });
   ```

4. 当合图视频流的布局信息发生变化时，`onTranscodedStreamLayoutInfo` 回调会再次触发。接收端可根据每一次回调上报的具体信息，重新调用 `setupRemoteVideo` 调整本地的视图渲染布局。

   :::info[信息]
   布局信息变化可能存在以下几种情况：

            - 合图子视频在合图画布上的坐标变化。
            - 合图子视频的宽度或高度变化。
            - 合图子视频流的状态变化。当发布该子视频流的主播退出频道时，该路子视频流的 `videoState` 参数会从 `0`（正常） 变为 `1`（该视频流无视频画面）。你可以在调用 `setupRemoteVideo` 时，将 `subviewUid` 设置为该路子视频流的用户 ID、然后将 `setupMode` 设置为 `VIEW_SETUP_MODE_REMOVE`（删除视图），即可停止渲染该主播的视频画面。
   :::

5. 监听 `onUserOffline` 回调，当转发合图流的用户退出频道时，停止在接收端渲染合图流。

## 注意事项 [#注意事项-4]

- 目前仅支持在移动端实现自定义合图布局功能。
- 当合图视频流被添加了水印时，水印会随所在的区域被渲染，可能会出现被分割的情况。

## 参考信息 [#参考信息-4]

- [`onTranscodedStreamLayoutInfo`](/zh-CN/api-reference/rtc/react-native/video/video-rendering#callback_irtcengineeventhandler_ontranscodedstreamlayoutinfo)
- `setupRemoteVideo`
- `setupRemoteVideoEx`
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
