# 录制与回放 (/zh-CN/realtime-media/whiteboard/fastboard-sdk/build/extend-whiteboard/record-and-replay)

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

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />
声网 Fastboard SDK 提供录制与回放功能，支持将实时房间中的所有互动行为录制下来，并支持高质量回放。相较于传统的录屏模式，白板录制和回放具有以下优势：

- **高效录制**：采用信令记录方式，相比传统录屏模式占用带宽更少、存储空间更小
- **高质量回放**：回放时还原现场效果，画质清晰、动画流畅
- **服务端录制**：录制在服务端自动进行，无需客户端额外处理
- **灵活回放**：支持指定时间范围回放、播放控制、视角跟随等功能
- **完整记录**：录制白板操作、自定义事件、视角变化、多窗口应用等所有互动行为
- **音视频同步**：支持白板回放与音视频播放同步

本文介绍如何开启白板录制功能以及如何使用 Fastboard SDK 实现白板内容回放。

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

### 白板录制 [#白板录制]

声网互动白板录制不是屏幕录制，而是记录信令与数据增量构成的私有二进制数据。录制过程完全在服务端进行，具有以下优势：

- **自动录制**：只需在创建房间时开启录制功能，服务端会自动录制房间内的所有互动行为
- **智能管理**：只有在房间活跃时才会生成录制信息，房间无活动时录制会自动停止
- **数据紧凑**：录制生成的数据相比传统音视频格式更紧凑，占用存储空间更小
- **多窗口支持**：完整记录 Fastboard 多窗口模式下的所有应用和操作

### 白板回放 [#白板回放]

Fastboard Android SDK 对回放功能提供了轻量级封装，通过 `FastReplay` 类简化了配置和使用流程。但核心回放逻辑仍由底层 **Whiteboard SDK** 的 `Player` 类提供。

回放时，SDK 会依照录制的信令与数据增量还原现场，得到的效果和实时互动时一模一样。回放支持：

- 指定时间范围回放
- 播放、暂停、跳转等播放控制
- 播放速度调节
- 视角跟随和自由视角切换
- 自定义事件和全局状态变化的回放
- 多窗口应用的完整回放
- 音视频与白板同步回放

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

开始前，请确保：

1. 已在项目中集成声网 Fastboard SDK。详见[快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)。
2. 已获取有效的 App Identifier 和 Room Token。详见[获取互动白板项目的安全密钥](/zh-CN/realtime-media/whiteboard/fastboard-sdk/build/setup-and-access/enable-service#获取互动白板项目的安全密钥)。
3. 如需在业务服务器上创建房间，请参考[房间管理服务端 API](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)。

## 实现录制功能 [#实现录制功能]

### 开启录制 [#开启录制]

要开启白板录制功能，需要在创建房间时设置 `isRecord: true` 参数。通过服务端 API [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)创建支持录制的房间。

以下示例展示如何通过服务端 API 创建支持录制的房间：

```java
// 注意：建议在业务服务器上执行创建房间操作，不要在客户端执行
String url = "https://api.netless.link/v5/rooms";

OkHttpClient client = new OkHttpClient();

JSONObject requestBody = new JSONObject();
try {
    requestBody.put("name", "我的 Fastboard 录制房间");
    // highlight-start
    requestBody.put("isRecord", true); // 开启录制功能
    // highlight-end
} catch (JSONException e) {
    e.printStackTrace();
}

RequestBody body = RequestBody.create(
    MediaType.parse("application/json"), 
    requestBody.toString()
);

Request request = new Request.Builder()
    .url(url)
    .addHeader("token", "NETLESSSDK_YWs9xxxxxxM2MjRi") // 替换为你的 SDK Token
    .addHeader("region", "cn-hz") // 替换为你的数据中心
    .post(body)
    .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (response.isSuccessful()) {
            String responseBody = response.body().string();
            try {
                JSONObject json = new JSONObject(responseBody);
                String uuid = json.getString("uuid");
                Log.i(TAG, "房间 UUID: " + uuid);
                Log.i(TAG, "房间已开启录制功能");
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onFailure(Call call, IOException e) {
        Log.e(TAG, "创建房间失败: " + e.getMessage());
    }
});
```

:::warning[注意]
- SDK Token 是重要的安全凭证，建议在业务服务器中使用，不要写死在客户端代码中。
- 开启录制功能后，该房间内的所有互动行为都会被自动录制。
:::

### 录制内容 [#录制内容]

开启录制功能后，以下内容会被自动录制：

- 白板绘制操作（画笔、图形、文字等）
- 场景切换和内容变化
- 图片、音视频、课件（PPT、DOC）的展示和操作
- 多窗口应用的所有操作和状态变化
- 用户视角变化
- 自定义事件（通过 `dispatchMagixEvent` 发送的事件）
- 全局状态变化（`GlobalState` 的修改）

## 实现回放功能 [#实现回放功能]

由于 Fastboard Android SDK 不提供回放功能的封装，你需要直接使用底层 Whiteboard SDK 来实现回放功能。

### 创建回放播放器 [#创建回放播放器]

使用 `WhiteSdk` 的 `createPlayer` 方法创建回放播放器。以下示例展示了完整的创建流程：

```java
import com.herewhite.sdk.WhiteSdk;
import com.herewhite.sdk.WhiteSdkConfiguration;
import com.herewhite.sdk.Player;
import com.herewhite.sdk.PlayerConfiguration;
import com.herewhite.sdk.PlayerListener;
import com.herewhite.sdk.domain.PlayerPhase;
import com.herewhite.sdk.domain.PlayerState;
import com.herewhite.sdk.domain.SDKError;
import com.herewhite.sdk.domain.Region;
import com.herewhite.sdk.domain.WindowParams;
import com.herewhite.sdk.domain.Promise;

public class ReplayActivity extends AppCompatActivity implements PlayerListener {
    
    private static final String TAG = "ReplayActivity";
    private WhiteboardView whiteboardView;
    private WhiteSdk whiteSdk;
    private Player player;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_replay);
        
        // 1. 创建白板视图
        whiteboardView = findViewById(R.id.whiteboard_view);
        
        // 2. 配置并初始化 WhiteSdk
        initWhiteSdk();
        
        // 3. 创建回放播放器
        createReplayPlayer();
    }
    
    private void initWhiteSdk() {
        // 配置 SDK
        WhiteSdkConfiguration config = new WhiteSdkConfiguration("RMxxxAQ", true); // 替换为你的 App Identifier
        config.setRegion(Region.cn); // 设置数据中心
        config.setUseMultiViews(true); // 启用多窗口回放
        
        // 初始化 WhiteSdk
        whiteSdk = new WhiteSdk(whiteboardView, this, config);
    }
    
    private void createReplayPlayer() {
        // 配置回放参数
        PlayerConfiguration playerConfig = new PlayerConfiguration(
            "a7xxx69",  // 房间 UUID
            "NETLESSROOM_YWxxxjk"  // Room Token
        );
        
        // 设置回放时间范围
        playerConfig.setBeginTimestamp(1609459200000L); // 回放开始时间（毫秒）
        playerConfig.setDuration(45 * 60 * 1000L);      // 回放持续时间（毫秒）
        
        // 配置多窗口参数（如果录制时使用了多窗口模式）
        WindowParams windowParams = new WindowParams();
        windowParams.setContainerSizeRatio(9f / 16f); // 设置窗口比例
        playerConfig.setWindowParams(windowParams);
        
        // 创建回放播放器
        whiteSdk.createPlayer(playerConfig, this, new Promise<Player>() {
            @Override
            public void then(Player p) {
                player = p;
                Log.i(TAG, "回放播放器创建成功");
                
                // 初始化播放位置并开始播放
                player.seekToScheduleTime(0);
                player.play();
            }
            
            @Override
            public void catchEx(SDKError error) {
                Log.e(TAG, "创建回放播放器失败: " + error.getMessage());
            }
        });
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (player != null) {
            player.stop();
        }
    }
    
    // 实现 PlayerListener 回调方法（见下文）
}
```

### 播放器控制 [#播放器控制]

`Player` 提供了丰富的播放控制功能：

```java
// 播放控制
player.play();     // 开始播放
player.pause();    // 暂停播放
player.stop();     // 停止播放

// 跳转到指定位置（单位：毫秒，相对于回放开始时间）
player.seekToScheduleTime(10 * 60 * 1000); // 跳转到第 10 分钟

// 设置播放倍速
player.setPlaybackSpeed(1.5); // 1.5 倍速播放
player.setPlaybackSpeed(1.0); // 恢复正常速度

// 获取播放状态信息（同步方法）
PlayerPhase phase = player.getPlayerPhase();
PlayerTimeInfo timeInfo = player.getPlayerTimeInfo();
Log.i(TAG, "当前播放进度: " + timeInfo.getScheduleTime());
Log.i(TAG, "回放总时长: " + timeInfo.getTimeDuration());
Log.i(TAG, "播放状态: " + phase);

// 设置观看模式
player.setObserverMode(PlayerObserverMode.directory); // 跟随模式，跟随录制时的视角变化
player.setObserverMode(PlayerObserverMode.freedom);   // 自由模式，可以自由调整视角
```

### 监听播放器状态变化 [#监听播放器状态变化]

通过实现 `PlayerListener` 接口来监听播放器状态的变化：

```java
public class ReplayActivity extends AppCompatActivity implements PlayerListener {
    
    // ... 其他代码 ...
    
    @Override
    public void onPhaseChanged(PlayerPhase phase) {
        // 播放状态变化回调
        Log.i(TAG, "播放状态: " + phase);
        switch (phase) {
            case waitingFirstFrame:
                Log.i(TAG, "等待首帧");
                break;
            case playing:
                Log.i(TAG, "正在播放");
                break;
            case pause:
                Log.i(TAG, "已暂停");
                break;
            case stopped:
                Log.i(TAG, "已停止");
                break;
            case ended:
                Log.i(TAG, "播放结束");
                break;
            case buffering:
                Log.i(TAG, "正在缓冲");
                break;
        }
    }
    
    @Override
    public void onLoadFirstFrame() {
        // 首帧加载完成回调
        Log.i(TAG, "首帧加载完成");
    }
    
    @Override
    public void onPlayerStateChanged(PlayerState modifyState) {
        // 播放器状态变化回调
        // 可以在这里获取场景状态、全局状态等信息
    }
    
    @Override
    public void onStoppedWithError(SDKError error) {
        // 播放出错回调
        Log.e(TAG, "播放出错: " + error.getMessage());
    }
    
    @Override
    public void onScheduleTimeChanged(long time) {
        // 播放进度变化回调（约每 500ms 回调一次）
        Log.d(TAG, "当前播放进度: " + time + "ms");
        
        // 更新 UI 显示播放进度
        runOnUiThread(() -> {
            int minutes = (int) (time / 60000);
            int seconds = (int) ((time % 60000) / 1000);
            // 更新进度显示
        });
    }
    
    @Override
    public void onSliceChanged(String slice) {
        // 切片变化回调（一般不需要实现）
    }
    
    @Override
    public void onCatchErrorWhenAppendFrame(SDKError error) {
        // 追加帧错误回调
        Log.e(TAG, "追加帧错误: " + error.getMessage());
    }
    
    @Override
    public void onCatchErrorWhenRender(SDKError error) {
        // 渲染错误回调
        Log.e(TAG, "渲染错误: " + error.getMessage());
    }
}
```

### 同步 RTC 回放 [#同步-rtc-回放]

在实际应用场景中，你可能需要同时回放 RTC 录制的音视频和白板内容。你可以使用 `PlayerSyncManager` 类同时管理视频播放器和白板回放播放器，保证了播放状态的一致性，从而实现同步回放。

以下示例展示如何使用 `PlayerSyncManager` 实现 RTC 音视频与白板的同步回放：

```java
import com.herewhite.sdk.combinePlayer.PlayerSyncManager;
import com.herewhite.sdk.combinePlayer.NativePlayer;

public class SyncReplayActivity extends AppCompatActivity implements PlayerListener {
    
    private WhiteSdk whiteSdk;
    private Player whitePlayer;
    private PlayerSyncManager syncManager;
    private NativePlayer mediaPlayer;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sync_replay);
        
        // 1. 创建原生媒体播放器（以 ExoPlayer 为例）
        mediaPlayer = new WhiteExoPlayer(this);
        ((WhiteExoPlayer) mediaPlayer).setVideoPath("https://example.com/video.m3u8");
        
        // 2. 创建同步管理器
        syncManager = new PlayerSyncManager(mediaPlayer, new PlayerSyncManager.Callbacks() {
            @Override
            public void startBuffering() {
                Log.d(TAG, "开始缓冲");
            }
            
            @Override
            public void endBuffering() {
                Log.d(TAG, "缓冲结束");
            }
        });
        
        // 3. 初始化 WhiteSdk 并创建白板回放
        WhiteSdkConfiguration config = new WhiteSdkConfiguration("RMxxxAQ", true);
        config.setRegion(Region.cn);
        whiteSdk = new WhiteSdk(whiteboardView, this, config);
        
        PlayerConfiguration playerConfig = new PlayerConfiguration("a7xxx69", "NETLESSROOM_YWxxxjk");
        playerConfig.setBeginTimestamp(1609459200000L);
        playerConfig.setDuration(45 * 60 * 1000L);
        
        whiteSdk.createPlayer(playerConfig, this, new Promise<Player>() {
            @Override
            public void then(Player player) {
                whitePlayer = player;
                
                // 关联白板播放器到同步管理器
                syncManager.setWhitePlayer(player);
                
                // 开始同步播放
                player.seekToScheduleTime(0);
                syncManager.play();
            }
            
            @Override
            public void catchEx(SDKError error) {
                Log.e(TAG, "创建回放失败: " + error.getMessage());
            }
        });
    }
    
    @Override
    public void onPhaseChanged(PlayerPhase phase) {
        // 重要！必须同步白板播放器的阶段变化
        if (syncManager != null) {
            syncManager.updateWhitePlayerPhase(phase);
        }
        
        Log.i(TAG, "播放状态: " + phase);
    }
    
    // 播放控制
    private void play() {
        if (syncManager != null) {
            syncManager.play();
        }
    }
    
    private void pause() {
        if (syncManager != null) {
            syncManager.pause();
        }
    }
    
    private void seekTo(long time) {
        if (syncManager != null) {
            syncManager.seek(time);
        }
    }
    
    private void setPlaybackSpeed(float speed) {
        if (syncManager != null) {
            syncManager.setPlaybackSpeed(speed);
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (whitePlayer != null) {
            whitePlayer.stop();
        }
        if (mediaPlayer != null) {
            mediaPlayer.release();
        }
    }
    
    // 实现其他 PlayerListener 方法...
}
```

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

### 开发注意事项 [#开发注意事项]

在使用 Fastboard 录制与回放功能时，需要注意以下几点：

- **底层实现**：Fastboard Android SDK 不提供回放功能的封装，你需要直接使用底层 Whiteboard SDK 的 `Player` 类实现回放功能。
- **多窗口回放**：如果录制时使用了多窗口模式，回放时需要在 SDK 配置和回放配置中都启用多窗口支持，并设置正确的窗口比例。
- **录制延时**：录制内容不是实时生成的，新创建的房间可能需要等待一段时间才能获得录制内容。
- **时间范围**：建议根据实际使用场景设置合适的回放时间范围，避免回放过长的无效内容。
- **网络缓冲**：回放时可能需要网络缓冲，特别是跳转到较远时间点时。
- **同步与异步方法**：
  - 同步方法（如 `getPlayerPhase()`）返回本地缓存的值，可能不是最新值
  - 异步方法（如 `getPhase(promise)`）返回从 JS 端获取的最新值
  - 建议日常使用同步方法，调试时使用异步方法
- **线程安全**：`PlayerListener` 的回调在子线程执行，UI 操作需要使用 `runOnUiThread()` 切换到主线程。
- **状态同步**：使用 `PlayerSyncManager` 时，必须在 `onPhaseChanged` 回调中调用 `updateWhitePlayerPhase` 方法，否则无法正确同步状态。
- **资源释放**：使用完毕后记得调用 `player.stop()` 释放播放器资源。

### 检查房间是否可回放 [#检查房间是否可回放]

在创建回放前，可以先检查房间是否有可用的回放数据：

```java
whiteSdk.isPlayable(playerConfig, new Promise<Boolean>() {
    @Override
    public void then(Boolean isPlayable) {
        if (isPlayable) {
            // 可以创建回放
            whiteSdk.createPlayer(playerConfig, listener, promise);
        } else {
            Log.w(TAG, "该房间没有可用的回放数据");
        }
    }
    
    @Override
    public void catchEx(SDKError error) {
        Log.e(TAG, "检查回放状态失败: " + error.getMessage());
    }
});
```

### API 参考 [#api-参考]

- [`Player.play`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/android/classcom-1-1herewhite-1-1sdk-1-1-player#play) - 开始播放
- [`Player.pause`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/android/classcom-1-1herewhite-1-1sdk-1-1-player#pause) - 暂停播放
- [`Player.seekToScheduleTime`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/android/classcom-1-1herewhite-1-1sdk-1-1-player#seektoscheduletime) - 跳转播放位置
- [`Player.setPlaybackSpeed`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/android/classcom-1-1herewhite-1-1sdk-1-1-player#setplaybackspeed) - 设置播放速度
- [`Player.setObserverMode`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/android/classcom-1-1herewhite-1-1sdk-1-1-player#setobservermode) - 设置观看模式
- [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)

### 相关文档 [#相关文档]

- [快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />
声网 Fastboard SDK 提供录制与回放功能，支持将实时房间中的所有互动行为录制下来，并支持高质量回放。相较于传统的录屏模式，白板录制和回放具有以下优势：

- **高效录制**：采用信令记录方式，相比传统录屏模式占用带宽更少、存储空间更小
- **高质量回放**：回放时还原现场效果，画质清晰、动画流畅
- **服务端录制**：录制在服务端自动进行，无需客户端额外处理
- **灵活回放**：支持指定时间范围回放、播放控制、视角跟随等功能
- **完整记录**：录制白板操作、自定义事件、视角变化、多窗口应用等所有互动行为
- **音视频同步**：支持白板回放与音视频播放同步

本文介绍如何开启白板录制功能以及如何使用 Fastboard SDK 实现白板内容回放。

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

### 白板录制 [#白板录制-1]

声网互动白板录制不是屏幕录制，而是记录信令与数据增量构成的私有二进制数据。录制过程完全在服务端进行，具有以下优势：

- **自动录制**：只需在创建房间时开启录制功能，服务端会自动录制房间内的所有互动行为
- **智能管理**：只有在房间活跃时才会生成录制信息，房间无活动时录制会自动停止
- **数据紧凑**：录制生成的数据相比传统音视频格式更紧凑，占用存储空间更小
- **多窗口支持**：完整记录 Fastboard 多窗口模式下的所有应用和操作

### 白板回放 [#白板回放-1]

Fastboard iOS SDK 本身专注于实时互动白板房间的快速创建和管理，不提供回放功能的封装。回放功能完全由底层的 **Whiteboard SDK** 提供。

回放时，SDK 会依照录制的信令与数据增量还原现场，得到的效果和实时互动时一模一样。回放支持：

- 指定时间范围回放
- 播放、暂停、跳转等播放控制
- 播放速度调节
- 视角跟随和自由视角切换
- 自定义事件和全局状态变化的回放
- 多窗口应用的完整回放
- 音视频与白板同步回放

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

开始前，请确保：

1. 已在项目中集成声网 Fastboard SDK。详见[快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)。
2. 已获取有效的 App Identifier 和 Room Token。详见[获取互动白板项目的安全密钥](/zh-CN/realtime-media/whiteboard/fastboard-sdk/build/setup-and-access/enable-service#获取互动白板项目的安全密钥)。
3. 如需在业务服务器上创建房间，请参考[房间管理服务端 API](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)。

## 实现录制功能 [#实现录制功能-1]

### 开启录制 [#开启录制-1]

要开启白板录制功能，需要在创建房间时设置 `isRecord: true` 参数。通过服务端 API [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)创建支持录制的房间。

以下示例展示如何通过服务端 API 创建支持录制的房间：

```swift
// 注意：建议在业务服务器上执行创建房间操作，不要在客户端执行
let url = URL(string: "https://api.netless.link/v5/rooms")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("NETLESSSDK_YWs9xxxxxxM2MjRi", forHTTPHeaderField: "token") // 替换为你的 SDK Token
request.setValue("cn-hz", forHTTPHeaderField: "region") // 替换为你的数据中心

let parameters: [String: Any] = [
    "name": "我的 Fastboard 录制房间",
    // highlight-start
    "isRecord": true  // 开启录制功能
    // highlight-end
]

request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    if let error = error {
        print("创建房间失败:", error)
        return
    }
    
    if let data = data,
       let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
        print("房间 UUID:", json["uuid"] ?? "")
        print("房间已开启录制功能")
    }
}
task.resume()
```

:::warning[注意]
- SDK Token 是重要的安全凭证，建议在业务服务器中使用，不要写死在客户端代码中。
- 开启录制功能后，该房间内的所有互动行为都会被自动录制。
:::

### 录制内容 [#录制内容-1]

开启录制功能后，以下内容会被自动录制：

- 白板绘制操作（画笔、图形、文字等）
- 场景切换和内容变化
- 图片、音视频、课件（PPT、DOC）的展示和操作
- 多窗口应用的所有操作和状态变化
- 用户视角变化
- 自定义事件（通过 `dispatchMagixEvent` 发送的事件）
- 全局状态变化（`GlobalState` 的修改）

## 实现回放功能 [#实现回放功能-1]

由于 Fastboard iOS SDK 不提供回放功能的封装，你需要通过 `FastRoom` 对象的 `whiteSDK` 属性访问底层 Whiteboard SDK 来实现回放功能。

### 创建回放播放器 [#创建回放播放器-1]

使用 `WhiteSDK` 的 `createReplayerWithConfig` 方法创建回放播放器：

```swift
import UIKit
import Whiteboard

class ReplayViewController: UIViewController {
    
    var boardView: WhiteBoardView!
    var whiteSDK: WhiteSDK!
    var player: WhitePlayer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 创建白板视图
        boardView = WhiteBoardView()
        boardView.frame = view.bounds
        view.addSubview(boardView)
        
        // 2. 配置并初始化 WhiteSDK
        let config = WhiteSdkConfiguration(app: "RMxxxAQ") // 替换为你的 App Identifier
        config.region = .CN // 替换为你的数据中心
        whiteSDK = WhiteSDK(whiteBoardView: boardView, config: config)
        
        // 3. 配置回放参数
        let playerConfig = WhitePlayerConfig(
            room: "a7xxx69",  // 房间 UUID
            roomToken: "NETLESSROOM_YWxxxjk"  // Room Token
        )
        
        // 设置回放时间范围
        playerConfig.beginTimestamp = NSNumber(value: Date(timeIntervalSince1970: 1609459200).timeIntervalSince1970) // 回放开始时间
        playerConfig.duration = NSNumber(value: 45 * 60) // 回放持续时间（秒）
        
        // 4. 创建回放播放器
        whiteSDK.createReplayer(with: playerConfig, callbacks: self) { [weak self] success, player, error in
            if let error = error {
                print("创建回放播放器失败:", error.localizedDescription)
                return
            }
            
            if success, let player = player {
                self?.player = player
                print("回放播放器创建成功")
                
                // 开始播放
                player.play()
            }
        }
    }
}

// MARK: - WhitePlayerEventDelegate
extension ReplayViewController: WhitePlayerEventDelegate {
    
    func phaseChanged(_ phase: WhitePlayerPhase) {
        switch phase {
        case .waitingFirstFrame:
            print("等待首帧")
        case .playing:
            print("正在播放")
        case .pause:
            print("已暂停")
        case .stopped:
            print("已停止")
        case .ended:
            print("播放结束")
        case .buffering:
            print("正在缓冲")
        @unknown default:
            break
        }
    }
    
    func loadFirstFrame() {
        print("首帧加载完成")
    }
    
    func stoppedWithError(_ error: Error) {
        print("播放出错:", error.localizedDescription)
    }
    
    func scheduleTimeChanged(_ time: TimeInterval) {
        print("当前播放进度:", time, "秒")
    }
}
```

### 播放器控制 [#播放器控制-1]

`WhitePlayer` 提供了丰富的播放控制功能：

```swift
// 播放控制
player.play()     // 开始播放
player.pause()    // 暂停播放
player.stop()     // 停止播放

// 跳转到指定位置（单位：秒，相对于回放开始时间）
player.seekToScheduleTime(10 * 60) // 跳转到第 10 分钟

// 带回调的跳转
player.seekToScheduleTime(10 * 60) { result in
    switch result {
    case .success:
        print("跳转成功")
    case .fail:
        print("跳转失败")
    case .outOfRange:
        print("跳转位置超出范围")
    @unknown default:
        break
    }
}

// 设置播放倍速
player.playbackSpeed = 1.5  // 1.5 倍速播放
player.playbackSpeed = 1.0  // 恢复正常速度

// 获取播放状态信息
print("当前播放状态:", player.phase)
print("播放速度:", player.playbackSpeed)
print("时间信息:", player.timeInfo)
```

### 视角控制 [#视角控制]

回放时可以控制观看视角：

```swift
// 设置观看模式
player.setObserverMode(.directory)  // 跟随模式，跟随录制时的视角变化
player.setObserverMode(.freedom)    // 自由模式，可以自由调整视角

// 禁止用户调整视角
player.disableCameraTransform = true   // 禁止用户通过手势调整视角
player.disableCameraTransform = false  // 允许用户调整视角
```

### 同步 RTC 回放 [#同步-rtc-回放-1]

在实际应用场景中，你可能需要同时回放 RTC 录制的音视频和白板内容。你可以使用 `WhiteCombinePlayer` 类同时管理视频播放器和白板回放播放器，保证了播放状态的一致性，从而实现同步回放。

以下示例展示如何使用 `WhiteCombinePlayer` 实现 RTC 音视频与白板的同步回放：

```swift
import UIKit
import AVKit
import Whiteboard

class CombineReplayViewController: UIViewController {
    
    var boardView: WhiteBoardView!
    var whiteSDK: WhiteSDK!
    var combinePlayer: WhiteCombinePlayer?
    var playerView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建视频播放视图
        playerView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 300))
        view.addSubview(playerView)
        
        // 创建白板视图
        boardView = WhiteBoardView(frame: CGRect(x: 0, y: 300, width: view.bounds.width, height: view.bounds.height - 300))
        view.addSubview(boardView)
        
        // 初始化 WhiteSDK
        let config = WhiteSdkConfiguration(app: "RMxxxAQ")
        config.region = .CN
        whiteSDK = WhiteSDK(whiteBoardView: boardView, config: config)
        
        // 创建混合播放器
        let mediaUrl = URL(string: "https://example.com/video.m3u8")!
        combinePlayer = WhiteCombinePlayer(mediaUrl: mediaUrl)
        combinePlayer?.delegate = self
        
        // 配置回放参数
        let playerConfig = WhitePlayerConfig(room: "a7xxx69", roomToken: "NETLESSROOM_YWxxxjk")
        playerConfig.beginTimestamp = NSNumber(value: Date(timeIntervalSince1970: 1609459200).timeIntervalSince1970)
        playerConfig.duration = NSNumber(value: 45 * 60)
        
        // 创建白板回放播放器
        whiteSDK.createReplayer(with: playerConfig, callbacks: self) { [weak self] success, player, error in
            if let error = error {
                print("创建回放播放器失败:", error.localizedDescription)
                return
            }
            
            if success, let player = player {
                // 配置混合播放器
                self?.combinePlayer?.whitePlayer = player
                
                // 显示视频画面
                if let nativePlayer = self?.combinePlayer?.nativePlayer {
                    let playerLayer = AVPlayerLayer(player: nativePlayer)
                    playerLayer.frame = self?.playerView.bounds ?? .zero
                    self?.playerView.layer.addSublayer(playerLayer)
                }
                
                // WhitePlayer 需要先手动 seek 到 0 才会触发缓冲行为
                player.seekToScheduleTime(0)
                
                print("混合播放器创建成功")
            }
        }
    }
    
    // 播放控制
    func playVideo() {
        combinePlayer?.play()
    }
    
    func pauseVideo() {
        combinePlayer?.pause()
    }
    
    func seekToTime(_ time: TimeInterval) {
        let cmTime = CMTime(seconds: time, preferredTimescale: 1)
        combinePlayer?.seek(to: cmTime) { finished in
            print("跳转完成:", finished)
        }
    }
    
    func setPlaybackSpeed(_ speed: CGFloat) {
        combinePlayer?.playbackSpeed = speed
    }
}

// MARK: - WhitePlayerEventDelegate
extension CombineReplayViewController: WhitePlayerEventDelegate {
    
    func phaseChanged(_ phase: WhitePlayerPhase) {
        // 注意！必须完成该操作，WhiteCombinePlayer 才能正确同步状态
        combinePlayer?.updateWhitePlayerPhase(phase)
        
        switch phase {
        case .playing:
            print("正在播放")
        case .pause:
            print("已暂停")
        case .ended:
            print("播放结束")
        default:
            break
        }
    }
    
    func stoppedWithError(_ error: Error) {
        print("播放出错:", error.localizedDescription)
    }
}

// MARK: - WhiteCombineDelegate
extension CombineReplayViewController: WhiteCombineDelegate {
    
    func combinePlayerStartBuffering() {
        // 任意一端进入缓冲
        print("开始缓冲")
    }
    
    func combinePlayerEndBuffering() {
        // 两端都结束缓冲
        print("缓冲结束")
    }
}
```

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

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

在使用 Fastboard 录制与回放功能时，需要注意以下几点：

- **Fastboard 不提供回放 API**：Fastboard iOS SDK 专注于实时互动白板房间的快速创建和管理，回放功能需要直接使用底层 Whiteboard SDK。
- **访问底层 SDK**：通过 `FastRoom` 对象的 `whiteSDK` 属性访问底层 Whiteboard SDK 实现回放功能。
- **录制延时**：录制内容不是实时生成的，新创建的房间可能需要等待一段时间才能获得录制内容。
- **时间范围**：建议根据实际使用场景设置合适的回放时间范围，避免回放过长的无效内容。
- **网络缓冲**：回放时可能需要网络缓冲，特别是跳转到较远时间点时。
- **m3u8 格式**：m3u8 格式的音视频可能需要经过一次 `combinePlayerEndBuffering` 调用后，才能进行 `seek` 播放。
- **状态同步**：使用 `WhiteCombinePlayer` 时，必须在 `phaseChanged` 回调中调用 `updateWhitePlayerPhase` 方法，否则无法正确同步状态。
- **资源释放**：使用完毕后记得调用 `player.stop()` 释放播放器资源。

### API 参考 [#api-参考-1]

- [`WhiteSDK.createReplayerWithConfig`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/ios/classes/white-sdk#//api/name/createReplayerWithConfig:callbacks:completionHandler:) - 创建回放播放器
- [`WhitePlayer.play`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/ios/classes/white-player#//api/name/play) - 开始播放
- [`WhitePlayer.pause`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/ios/classes/white-player#//api/name/pause) - 暂停播放
- [`WhitePlayer.seekToScheduleTime`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/ios/classes/white-player#//api/name/seekToScheduleTime:) - 跳转播放位置
- [`WhitePlayer.setObserverMode`](/zh-CN/api-reference/whiteboard/whiteboard-sdk/ios/classes/white-player#//api/name/setObserverMode:) - 设置观看模式
- [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)

### 相关文档 [#相关文档-1]

- [快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />
声网 Fastboard SDK 提供录制与回放功能，支持将实时房间中的所有互动行为录制下来，并支持高质量回放。相较于传统的录屏模式，白板录制和回放具有以下优势：

- **高效录制**：采用信令记录方式，相比传统录屏模式占用带宽更少、存储空间更小
- **高质量回放**：回放时还原现场效果，画质清晰、动画流畅
- **服务端录制**：录制在服务端自动进行，无需客户端额外处理
- **灵活回放**：支持指定时间范围回放、播放控制、视角跟随等功能
- **完整记录**：录制白板操作、自定义事件、视角变化、多窗口应用等所有互动行为
- **内置 UI**：提供开箱即用的回放 UI 组件，支持主题和语言自定义

本文介绍如何开启白板录制功能以及如何使用 Fastboard SDK 实现白板内容回放。

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

### 白板录制 [#白板录制-2]

声网互动白板录制不是屏幕录制，而是记录信令与数据增量构成的私有二进制数据。录制过程完全在服务端进行，具有以下优势：

- **自动录制**：只需在创建房间时开启录制功能，服务端会自动录制房间内的所有互动行为
- **智能管理**：只有在房间活跃时才会生成录制信息，房间无活动时录制会自动停止
- **数据紧凑**：录制生成的数据相比传统音视频格式更紧凑，占用存储空间更小
- **多窗口支持**：完整记录 Fastboard 多窗口模式下的所有应用和操作

### 白板回放 [#白板回放-2]

Fastboard SDK 基于底层 Whiteboard SDK 的回放能力，提供了更易用的 API 和内置 UI 组件。回放时，SDK 会依照录制的信令与数据增量还原现场，得到的效果和实时互动时一模一样。回放支持：

- 指定时间范围回放
- 播放、暂停、跳转等播放控制
- 播放速度调节
- 视角跟随和自由视角切换
- 自定义事件和全局状态变化的回放
- 多窗口应用的完整回放

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

开始前，请确保：

1. 已在项目中集成声网 Fastboard SDK。详见[快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)。
2. 已获取有效的 App Identifier 和 Room Token。详见[获取互动白板项目的安全密钥](/zh-CN/realtime-media/whiteboard/fastboard-sdk/build/setup-and-access/enable-service#获取互动白板项目的安全密钥)。
3. 如需在业务服务器上创建房间，请参考[房间管理服务端 API](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)。

## 实现录制功能 [#实现录制功能-2]

### 开启录制 [#开启录制-2]

要开启白板录制功能，需要在创建房间时设置 `isRecord: true` 参数。通过服务端 API [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)创建支持录制的房间：

```javascript
var url = "https://api.netless.link/v5/rooms";
var requestInit = {
    method: "POST",
    headers: {
        "content-type": "application/json",
        "token": "NETLESSSDK_YWs9xxxxxxM2MjRi", // 替换为你的 SDK Token
        "region": "cn-hz", // 替换为你的数据中心
    },
    body: JSON.stringify({
        name: "我的 Fastboard 录制房间",
        // highlight-start
        isRecord: true, // 开启录制功能
        // highlight-end
    }),
};

fetch(url, requestInit).then(function(response) {
    return response.json();
}).then(function(roomJSON) {
    // 创建房间成功，获取房间信息
    console.log("房间 UUID:", roomJSON.uuid);
    console.log("房间已开启录制功能");
}).catch(function(err) {
    console.error("创建房间失败:", err);
});
```

:::warning[注意]
- SDK Token 是重要的安全凭证，建议在业务服务器中使用，不要写死在前端代码中。
- 开启录制功能后，该房间内的所有互动行为都会被自动录制。
:::

### 录制内容 [#录制内容-2]

开启录制功能后，以下内容会被自动录制：

- 白板绘制操作（画笔、图形、文字等）
- 场景切换和内容变化
- 图片、音视频、课件（PPT、DOC）的展示和操作
- 多窗口应用的所有操作和状态变化
- 用户视角变化
- 自定义事件（通过 `dispatchMagixEvent` 发送的事件）
- 全局状态变化（`GlobalState` 的修改）

## 实现回放功能 [#实现回放功能-2]

### 方法一：使用 replayFastboard 和 createReplayUI [#方法一使用-replayfastboard-和-createreplayui]

这是推荐的方法，提供完整的回放 UI 组件：

```javascript
import { replayFastboard, createReplayUI } from "@netless/fastboard";

// 创建回放播放器
const player = await replayFastboard({
    sdkConfig: {
        appIdentifier: "RMxxxAQ", // 替换为你的 App Identifier
        region: "cn-hz", // 替换为你的数据中心
    },
    replayRoom: {
        room: "a7xxx69", // 房间 UUID
        roomToken: "NETLESSROOM_YWxxxjk", // Room Token
        beginTimestamp: new Date("2023-01-01 08:00:00").getTime(), // 回放开始时间
        duration: 45 * 60 * 1000, // 回放持续时间（毫秒）
    },
    managerConfig: {
        cursor: true, // 显示光标
    },
});

// 创建并挂载回放 UI
const ui = createReplayUI(player, document.getElementById("whiteboard"));

// 可选：更新 UI 配置
ui.update({ 
    theme: "dark", 
    language: "zh-CN" 
});

// 使用完毕后销毁
// ui.destroy();
```

### 方法二：使用 React Hook [#方法二使用-react-hook]

如果你使用 React 框架，可以使用 `useReplayFastboard` Hook：

```jsx
import React from "react";
import { useReplayFastboard, ReplayFastboard } from "@netless/fastboard-react";

function ReplayComponent() {
    const player = useReplayFastboard(() => ({
        sdkConfig: {
            appIdentifier: "RMxxxAQ", // 替换为你的 App Identifier
            region: "cn-hz",
        },
        replayRoom: {
            room: "a7xxx69", // 房间 UUID
            roomToken: "NETLESSROOM_YWxxxjk", // Room Token
            beginTimestamp: new Date("2023-01-01 08:00:00").getTime(),
            duration: 45 * 60 * 1000,
        },
        managerConfig: {
            cursor: true,
        },
    }));

    return (
        <ReplayFastboard 
            player={player} 
            theme="light" 
            language="zh-CN" 
        />
    );
}
```

### 插件配置 [#插件配置]

如果在创建实时房间时启用了插件（如 `AppliancePlugin` 或 `AppInMainViewPlugin`），在回放时也需要配置相同的插件：

```javascript
const player = await replayFastboard({
    sdkConfig: {
        appIdentifier: "RMxxxAQ",
        region: "cn-hz",
    },
    replayRoom: {
        room: "a7xxx69",
        roomToken: "NETLESSROOM_YWxxxjk",
        beginTimestamp: new Date("2023-01-01 08:00:00").getTime(),
        duration: 45 * 60 * 1000,
    },
    // 如果实时房间启用了 AppliancePlugin，回放时也需要配置
    enableAppliancePlugin: {
        cdn: {
            fullWorkerUrl: "path/to/fullWorker.js",
            subWorkerUrl: "path/to/subWorker.js",
        },
        strokeWidth: {
            min: 1,
            max: 32
        },
        syncOpt: {
            interval: 200
        },
    },
    // 如果实时房间启用了 AppInMainViewPlugin，回放时也需要配置
    enableAppInMainViewPlugin: true,
});
```

### 播放器控制 [#播放器控制-2]

`FastboardPlayer` 提供了丰富的播放控制功能：

```javascript
// 播放控制
player.play();     // 开始播放
player.pause();    // 暂停播放
player.stop();     // 停止播放

// 跳转到指定位置（单位：毫秒，相对于回放开始时间）
player.seek(10 * 60 * 1000); // 跳转到第 10 分钟

// 设置播放倍速
player.setPlaybackRate(1.5); // 1.5 倍速播放
player.setPlaybackRate(1.0); // 恢复正常速度

// 获取播放状态信息
console.log("当前播放进度:", player.currentTime.value); // 当前播放位置（毫秒）
console.log("总时长:", player.duration.value);          // 回放总时长（毫秒）
console.log("播放状态:", player.phase.value);           // 播放状态
console.log("播放速度:", player.playbackRate.value);    // 当前播放速度
console.log("是否可播放:", player.canplay.value);       // 是否准备好播放
```

### 监听播放器状态变化 [#监听播放器状态变化-1]

可以通过订阅可观察属性来监听播放器状态的变化：

```javascript
// 监听播放进度变化
const unsubscribeProgress = player.currentTime.subscribe(time => {
    console.log("当前播放进度:", time + "ms");
});

// 监听播放状态变化
const unsubscribePhase = player.phase.subscribe(phase => {
    switch(phase) {
        case "playing":
            console.log("正在播放");
            break;
        case "pause":
            console.log("已暂停");
            break;
        case "buffering":
            console.log("正在缓冲");
            break;
        case "ended":
            console.log("播放结束");
            break;
        case "waitingFirstFrame":
            console.log("等待首帧");
            break;
    }
});

// 监听播放速度变化
const unsubscribeRate = player.playbackRate.subscribe(rate => {
    console.log("播放速度:", rate + "x");
});

// 取消订阅
// unsubscribeProgress();
// unsubscribePhase();
// unsubscribeRate();
```

### 自定义渲染 [#自定义渲染]

如果需要自定义渲染，可以使用 `bindContainer` 方法：

```javascript
// 只渲染白板内容，不包含控制 UI
player.bindContainer(document.getElementById("whiteboard"));

// 可选：移动 collector 到指定位置
player.bindCollector(document.getElementById("collector"));
```

### 同步 RTC 回放 [#同步-rtc-回放-2]

在实际应用场景中，你可能需要同时回放 RTC 录制的音视频和白板内容。你可以使用 `@netless/combine-player` 库同时管理视频播放器和白板回放播放器，保证了播放状态的一致性，从而实现同步回放。

:::info[信息]
使用 `@netless/combine-player` 需要 1.1.7 及以上版本才支持以下接入方式。
:::

以下示例展示如何使用 `@netless/combine-player` 实现 RTC 音视频与白板的同步回放：

```javascript
import { replayFastboard } from "@netless/fastboard";
import CombinePlayerFactory from "@netless/combine-player";

// 1. 创建白板回放播放器
const player = await replayFastboard({
    sdkConfig: {
        appIdentifier: "RMxxxAQ", // 替换为你的 App Identifier
        region: "cn-hz",
    },
    replayRoom: {
        room: "a7xxx69", // 房间 UUID
        roomToken: "NETLESSROOM_YWxxxjk", // Room Token
        beginTimestamp: new Date("2023-01-01 08:00:00").getTime(),
        duration: 45 * 60 * 1000,
    },
    managerConfig: {
        cursor: true,
    },
    // 如果实时房间启用了插件，回放时也需要配置
    enableAppliancePlugin: {},
});

// 2. 绑定白板容器
const whiteboard = document.getElementById("whiteboard");
player.bindContainer(whiteboard);

// 3. 配置视频播放器参数
const factoryParams = {
    url: "https://example.com/rtc-recording.mp4", // RTC 录制的视频地址
    videoDOM: document.getElementById("videoDom"), // 用于存放视频播放器的 DOM 节点
};

// 4. 创建同步播放器
const combinePlayer = new CombinePlayerFactory(player.player, factoryParams).create(false);

// 5. 使用同步播放器控制播放
combinePlayer.play();   // 开始播放
combinePlayer.pause();  // 暂停播放
combinePlayer.seek(10 * 1000); // 跳转到 10 秒位置（单位：毫秒）

// 6. 监听播放状态变化
combinePlayer.setOnStatusChange((status, message) => {
    console.log("播放状态:", status, message);
});

// 7. 获取播放信息
console.log("播放速率:", combinePlayer.playbackRate);
console.log("总时长:", combinePlayer.timeDuration);
console.log("当前状态:", combinePlayer.combinedStatus);
```

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

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

在使用 Fastboard 录制与回放功能时，需要注意以下几点：

- **录制延时**：录制内容不是实时生成的，新创建的房间可能需要等待一段时间才能获得录制内容
- **插件一致性**：如果实时房间启用了插件，回放时必须配置相同的插件，否则可能导致回放异常
- **时间范围**：建议根据实际使用场景设置合适的回放时间范围，避免回放过长的无效内容
- **网络缓冲**：回放时可能需要网络缓冲，特别是跳转到较远时间点时
- **存储成本**：长期开启录制功能会产生存储成本，建议根据业务需要合理使用
- **资源释放**：使用完毕后记得调用 `ui.destroy()` 和 `player.destroy()` 释放资源

### API 参考 [#api-参考-2]

- [`replayFastboard`](/zh-CN/api-reference/whiteboard/fastboard/web#replayfastboard) - 创建回放播放器
- [`createReplayUI`](/zh-CN/api-reference/whiteboard/fastboard/web#createreplayui) - 创建回放 UI
- [POST 创建房间](/zh-CN/api-reference/api-ref/whiteboard/restful/create-room)

### 相关文档 [#相关文档-2]

- [快速开始](/zh-CN/realtime-media/whiteboard/fastboard-sdk/get-started/join-room-fb)
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
