# 发送和接收媒体流 (/zh-CN/realtime-media/rtc-server-sdk/build/send-receive)

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

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="cpp" platform="cpp" />
## 前提条件 [#前提条件]

本文介绍如何使用 RTC 服务端 SDK 向客户端发送媒体流并从客户端接收媒体流。你已经下载了最新的 RTC 服务端 SDK。详见[跑通 RTC 服务端 SDK 示例项目](/zh-CN/realtime-media/rtc-server-sdk/get-started/run-example)。

## 准备工作 [#准备工作]

在发送和接收媒体流之前，你需要完成以下准备工作。

### 1. 初始化 SDK [#1-初始化-sdk]

首先，你需要在你的应用程序里调用 `createAgoraService` 和 `initialize` 进行全局初始化，创建并初始化 `IAgoraService` 对象。

这个操作只需要进行一次，`IAgoraService` 对象的生命期和应用程序的生命期保持一致，只要应用程序没有退出，`IAgoraService` 可以一直存在。

SDK 支持 `int` 型用户 ID 和 `String` 型用户 ID。本文仅演示 `int` 型用户 ID（即字符只能为数字）；`String` 型用户 ID（字符可以是数字、字母或特殊符号）的使用方法，参考 [使用 String 型用户 ID](/zh-CN/realtime-media/rtc-server-sdk/build/string-uid)。

```cpp
// 创建 IAgoraService 对象
auto service = createAgoraService();
// 初始化 IAgoraService 对象
agora::base::AgoraServiceConfiguration scfg;
// 设置声网 App ID
scfg.appId = appid;
// 是否开启音频处理模块。本文设为开启。
scfg.enableAudioProcessor = enableAudioProcessor;
// 是否开启音频设备模块。此设置不适用于服务端。本文设为关闭。
scfg.enableAudioDevice = enableAudioDevice;
// 是否开启视频。本文设为开启。
scfg.enableVideo = enableVideo;
// 是否允许 String 型用户 ID（字符可以是数字、字母或特殊符号）。
// 本文设为 false，使用 int 型用户 ID（即字符只能为数字）。
scfg.useStringUid = enableuseStringUid;
if (service->initialize(scfg) != agora::ERR_OK) {
    return nullptr;
}
```

### 2. 与 RTC 频道建立连接 [#2-与-rtc-频道建立连接]

初始化后，根据以下步骤与声网 RTC 频道建立连接。

1. 调用 `createRtcConnection` 方法创建 `IRtcConnection` 对象，用于与声网服务器建立连接。

    ```cpp
    // 创建 IRtcConnection 对象
    agora::rtc::RtcConnectionConfiguration ccfg;
    ccfg.autoSubscribeAudio = false;
    ccfg.autoSubscribeVideo = false;
    ccfg.clientRoleType = agora::rtc::CLIENT_ROLE_BROADCASTER;
    agora::agora_refptr<agora::rtc::IRtcConnection> connection = service->createRtcConnection(ccfg);
    ```

2. 调用 `registerObserver` 方法监听连接事件。示例代码中的 `SampleConnectionObserver` 继承了 `IRtcConnectionObserver` 类和 `INetworkObserver` 类。

    ```cpp
    // 调用 registerObserver 方法监听连接事件
    auto connObserver = std::make_shared<SampleConnectionObserver>();
    connection->registerObserver(connObserver.get());
    ```

3. 调用 `connect` 与声网频道建立连接。

    ```cpp
    // 调用 connect 与声网频道建立连接
    if (connection->connect(options.appId.c_str(), options.channelId.c_str(),
                            options.userId.c_str())) {
        AG_LOG(ERROR, "Failed to connect to the channel!");
        return -1;
    }
    ```

## 向客户端发送媒体流 [#向客户端发送媒体流]

参考以下步骤向客户端发送媒体流。

### 1. 创建媒体流发送器 [#1-创建媒体流发送器]

你可以通过媒体节点工厂对象 `IMediaNodeFactory` 创建不同类型的媒体流发送器：

- `IAudioPcmDataSender`：发送 PCM 格式的音频数据。
- `IVideoFrameSender`：发送 YUV 格式的视频数据。
- `IAudioEncodedFrameSender`：发送已编码的音频数据。
- `IVideoEncodedImageSender`：发送已编码的视频数据。

1. 创建 `IMediaNodeFactory` 媒体节点工厂对象。

    ```cpp
    // 创建媒体节点工厂对象
    agora::agora_refptr<agora::rtc::IMediaNodeFactory> factory = service->createMediaNodeFactory();
    if (!factory) {
        AG_LOG(ERROR, "Failed to create media node factory!");
    }
    ```

2. 根据你的需求，创建 `IAudioPcmDataSender` 对象、`IVideoFrameSender`对象、`IAudioEncodedFrameSender` 对象或 `IVideoEncodedImageSender` 对象，分别用于发送 PCM 格式的音频流、YUV 格式的视频流、已编码的音频流及已编码的视频流。

    ```cpp
    // 创建可发送 PCM 格式的音频数据的发送器
    agora::agora_refptr<agora::rtc::IAudioPcmDataSender> audioPcmDataSender =
        factory->createAudioPcmDataSender();
    if (!audioPcmDataSender) {
        AG_LOG(ERROR, "Failed to create audio data sender!");
        return -1;
    }

    // 创建可发送 YUV 格式的视频数据的发送器
    agora::agora_refptr<agora::rtc::IVideoFrameSender> videoFrameSender =
        factory->createVideoFrameSender();
    if (!videoFrameSender) {
        AG_LOG(ERROR, "Failed to create video frame sender!");
        return -1;
    }

    // 创建可发送已编码的音频数据的发送器
    agora::agora_refptr<agora::rtc::IAudioEncodedFrameSender> audioFrameSender =
        factory->createAudioEncodedFrameSender();
    if (!audioFrameSender) {
        AG_LOG(ERROR, "Failed to create audio encoded frame sender!");
        return -1;
    }

    // 创建可发送已编码的视频数据的发送器
    agora::agora_refptr<agora::rtc::IVideoEncodedImageSender> videoEncodedFrameSender = factory->createVideoEncodedImageSender();
    if (!videoEncodedFrameSender) {
        AG_LOG(ERROR, "Failed to create encoded video frame sender!");
        return -1;
    }
    ```

3. 创建 `ILocalAudioTrack` 对象和 `ILocalVideoTrack` 对象，分别对应即将发布至频道内的本地音频轨道和本地视频轨道。对象中可以使用上一步创建的发送器。

    ```cpp
    // 创建自定义音频轨道（使用可发送 PCM 格式的音频数据的发送器）
    agora::agora_refptr<agora::rtc::ILocalAudioTrack> customAudioTrack =
        service->createCustomAudioTrack(audioPcmDataSender);
    if (!customAudioTrack) {
        AG_LOG(ERROR, "Failed to create audio track!");
        return -1;
    }

    // 创建自定义音频轨道（使用可发送已编码的音频数据的发送器）
    agora::agora_refptr<agora::rtc::ILocalAudioTrack> customAudioTrack = service->createCustomAudioTrack(audioFrameSender, agora::base::MIX_DISABLED);
    if (!customAudioTrack) {
        AG_LOG(ERROR, "Failed to create audio track!");
        return -1;
    }

    // 创建自定义视频轨道（可发送 YUV 格式的视频数据的发送器）
    agora::agora_refptr<agora::rtc::ILocalVideoTrack> customVideoTrack =
        service->createCustomVideoTrack(videoFrameSender);
    if (!customVideoTrack) {
        AG_LOG(ERROR, "Failed to create video track!");
        return -1;
    }

    // 创建自定义视频轨道（可发送已编码的视频数据的发送器）
    agora::agora_refptr<agora::rtc::ILocalVideoTrack> customVideoTrack =
        service->createCustomVideoTrack(videoEncodedFrameSender);
    if (!customVideoTrack) {
        AG_LOG(ERROR, "Failed to create video track!");
        return -1;
    }
    ```

### 2. 发送媒体流 [#2-发送媒体流]

1. 通过 `ILocalUser` 对象的 publish 相关方法在声网 RTC 频道中发布上一步创建的本地音频轨道和视频轨道。

    ```cpp
    // 开启并发布音频轨道
    customAudioTrack->setEnabled(true);
    connection->getLocalUser()->publishAudio(customAudioTrack);
    // 开启并发布视频轨道
    customVideoTrack->setEnabled(true);
    connection->getLocalUser()->publishVideo(customVideoTrack);
    ```

2. 开启发送线程，调用发送器的 send 方法发送音视频数据。

    ```cpp
    // 音频发送线程
    std::thread sendAudioThread(SampleSendAudioTask, options, audioFrameSender, std::ref(exitFlag));
    // 视频发送线程
    std::thread sendVideoThread(SampleSendVideoH264Task, options, videoFrameSender,
                                std::ref(exitFlag));

    sendAudioThread.join();
    sendVideoThread.join();
    ```

    本文以发送 PCM 音频数据为例进行介绍，`SampleSendAudioTask` 的实现如下：

    ```cpp
    static void SampleSendAudioTask(
        const SampleOptions& options,
        agora::agora_refptr<agora::rtc::IAudioPcmDataSender> audioFrameSender, bool& exitFlag) {
        // 目前 SDK 仅支持一次发送 10 毫秒的 PCM 数据。因此 PCM 数据的发送间隔设为
        // 10 毫秒。
        PacerInfo pacer = {0, 10, std::chrono::steady_clock::now()};

        while (!exitFlag) {
            sendOnePcmFrame(options, audioFrameSender);
            waitBeforeNextSend(pacer);  // sleep for a while before sending next frame
        }
    }
    ```

    `sendOnePcmFrame` 的实现如下。可以看到调用了发送器 `audioFrameSender` 的 `sendAudioPcmData` 方法发送 PCM 格式的音频数据。

    ```cpp
    static void sendOnePcmFrame(
        const SampleOptions& options,
        agora::agora_refptr<agora::rtc::IAudioPcmDataSender> audioFrameSender) {
        static FILE* file = nullptr;
        const char* fileName = options.audioFile.c_str();

        // 根据一次发送的样本时长计算样本大小
        int sampleSize = sizeof(int16_t) * options.audio.numOfChannels;
        int samplesPer10ms = options.audio.sampleRate / 100;
        int sendBytes = sampleSize * samplesPer10ms;

        if (!file) {
            if (!(file = fopen(fileName, "rb"))) {
                AG_LOG(ERROR, "Failed to open audio file %s", fileName);
                return;
            }
            AG_LOG(INFO, "Open audio file %s successfully", fileName);
        }

        uint8_t frameBuf[sendBytes];

        if (fread(frameBuf, 1, sizeof(frameBuf), file) != sizeof(frameBuf)) {
            if (feof(file)) {
                fclose(file);
                file = nullptr;
                AG_LOG(INFO, "End of audio file");
            } else {
                AG_LOG(ERROR, "Error reading audio data: %s", std::strerror(errno));
            }
            return;
        }
        // 调用发送器 `audioFrameSender` 的 `sendAudioPcmData` 方法发送 PCM
        // 格式的音频数据
        if (audioFrameSender->sendAudioPcmData(
                frameBuf, 0, samplesPer10ms, agora::rtc::TWO_BYTES_PER_SAMPLE,
                options.audio.numOfChannels, options.audio.sampleRate) < 0) {
            AG_LOG(ERROR, "Failed to send audio frame!");
        }
    }
    ```

    以发送 H.264 格式的编码视频为例，`SampleSendVideoH264Task` 的实现如下：

    ```cpp
    static void SampleSendVideoH264Task(
        const SampleOptions& options,
        agora::agora_refptr<agora::rtc::IVideoEncodedImageSender> videoH264FrameSender,
        bool& exitFlag) {
        std::unique_ptr<HelperH264FileParser> h264FileParser(
            new HelperH264FileParser(options.videoFile.c_str()));
        h264FileParser->initialize();

        // Calculate send interval based on frame rate. H264 frames are sent at this
        // interval
        PacerInfo pacer = {0, 1000 / options.video.frameRate, 0, std::chrono::steady_clock::now()};

        while (!exitFlag) {
            if (auto h264Frame = h264FileParser->getH264Frame()) {
                sendOneH264Frame(options.video.frameRate, std::move(h264Frame), videoH264FrameSender);
                waitBeforeNextSend(pacer);  // sleep for a while before sending next frame
            }
        };
    }
    ```

    `sendOneH264Frame` 的实现如下，可以看到调用了发送器 `videoH264FrameSender` 的 `sendEncodedVideoImage` 方法发送 H.264 格式的编码视频数据。

    :::info[信息]
    编码视频数据的格式通过 `sendEncodedVideoImage` 方法的 `videoEncodedFrameInfo` 参数进行设置。
    :::

    ```cpp
    static void sendOneH264Frame(
        int frameRate, std::unique_ptr<HelperH264Frame> h264Frame,
        agora::agora_refptr<agora::rtc::IVideoEncodedImageSender> videoH264FrameSender) {
        agora::rtc::EncodedVideoFrameInfo videoEncodedFrameInfo;
        videoEncodedFrameInfo.rotation = agora::rtc::VIDEO_ORIENTATION_0;
        videoEncodedFrameInfo.codecType = agora::rtc::VIDEO_CODEC_H264;
        videoEncodedFrameInfo.framesPerSecond = frameRate;
        videoEncodedFrameInfo.frameType =
            (h264Frame.get()->isKeyFrame ? agora::rtc::VIDEO_FRAME_TYPE::VIDEO_FRAME_TYPE_KEY_FRAME
                : agora::rtc::VIDEO_FRAME_TYPE::VIDEO_FRAME_TYPE_DELTA_FRAME);

        videoH264FrameSender->sendEncodedVideoImage(
            reinterpret_cast<uint8_t*>(h264Frame.get()->buffer.get()), h264Frame.get()->bufferLen, videoEncodedFrameInfo);
    }
    ```

### 3. 断开连接并释放资源 [#3-断开连接并释放资源]

媒体流发送完成后，你可以通过以下步骤退出频道并断开与声网服务器的连接：

:::warning[注意]
你必须严格按照示例代码中的顺序释放相关资源。
:::

1. 调用 `unpublish` 相关方法取消发布音频流和视频流。

    ```cpp
    connection->getLocalUser()->unpublishAudio(customAudioTrack);
    connection->getLocalUser()->unpublishVideo(customVideoTrack);
    ```

2. 调用 `unregisterObserver` 取消连接 observer。

    ```cpp
    connection->unregisterObserver(connObserver.get());
    ```

3. 调用 `disconnect` 断开与声网服务器的连接。

    ```cpp
    if (connection->disconnect()) {
        AG_LOG(ERROR, "Failed to disconnect from声网channel!");
        return -1;
    }
    AG_LOG(INFO, "Disconnected from声网channel successfully");
    ```

4. 释放已创建对象的资源。

    ```cpp
    connObserver.reset();
    audioPcmDataSender = nullptr;
    videoFrameSender = nullptr;
    customAudioTrack = nullptr;
    customVideoTrack = nullptr;
    factory = nullptr;
    connection = nullptr;
    ```

## 从客户端接收媒体流 [#从客户端接收媒体流]

参考以下步骤从客户端接收媒体流。

### 1. 注册音视频帧 observer [#1-注册音视频帧-observer]

实例化音视频帧 observer 对象，并通过 `ILocalUserObserver` 类的成员方法注册音视频帧 observer。成功注册 observer 后，SDK 会在捕捉到每个音频或视频帧时触发回调。你可以在回调中获得该音频或视频帧。

示例中的 `SampleLocalUserObserver` 类包含  `IVideoEncodedFrameObserver`、`IAudioFrameObserverBase` 和 `IVideoFrameObserver2` 类的音视频帧 observer 对象作为成员，且继承了 `ILocalUserObserver` 类，可以使用其成员方法注册作为成员的音视频帧 observer 对象。

```cpp
// SampleLocalUserObserver 类继承了 ILocalUserObserver 类
auto localUserObserver =
    std::make_shared<SampleLocalUserObserver>(connection->getLocalUser());

// PcmFrameObserver 类继承了 IAudioFrameObserver 类
auto pcmFrameObserver = std::make_shared<PcmFrameObserver>(options.audioFile);

if (connection->getLocalUser()->setPlaybackAudioFrameBeforeMixingParameters(
        options.audio.numOfChannels, options.audio.sampleRate)) {
    AG_LOG(ERROR, "Failed to set audio frame parameters!");
    return -1;
}
// 实例化 IAudioFrameObserver 类
localUserObserver->setAudioFrameObserver(pcmFrameObserver.get());

// H264FrameReceiver 类继承了 IVideoEncodedImageReceiver 类
auto h264FrameReceiver = std::make_shared<H264FrameReceiver>(options.videoFile);
// 实例化 IVideoEncodedImageReceiver 类
localUserObserver->setVideoEncodedImageReceiver(h264FrameReceiver.get());
```

`setAudioFrameObserver` 和 `setVideoEncodedImageReceiver` 是 `SampleLocalUserObserver` 的成员方法，用于实例化 `IAudioFrameObserver` 类和 `IVideoEncodedImageReceiver` 类。

```cpp
void setAudioFrameObserver(agora::media::IAudioFrameObserver* observer) {
    audio_frame_observer_ = observer;
}

void setVideoEncodedImageReceiver(agora::rtc::IVideoEncodedImageReceiver* receiver) {
    video_encoded_receiver_ = receiver;
}
```

### 2. 接收媒体流 [#2-接收媒体流]

下面的代码展示了如何接收已编码的视频、YUV 格式的视频和 PCM 格式的音频。

```cpp
// 通过 OnEncodedVideoImageReceived 回调接收已编码的视频并以文件形式输出
class H264FrameReceiver : public agora::rtc::IVideoEncodedImageReceiver {
   public:
    H264FrameReceiver(const std::string& outputFilePath)
        : outputFilePath_(outputFilePath),
          h264File_(nullptr),
          fileCount(0),
          fileSize_(0) {}

    bool OnEncodedVideoImageReceived(const uint8_t* imageBuffer, size_t length,
                                     const agora::rtc::EncodedVideoFrameInfo& videoEncodedFrameInfo) override;

   private:
    std::string outputFilePath_;
    FILE* h264File_;
    int fileCount;
    int fileSize_;
};

// 通过 onFrame 回调接收 YUV 格式的视频并以文件形式输出
class YuvFrameObserver : public agora::rtc::IVideoFrameObserver2 {
   public:
    YuvFrameObserver(const std::string& outputFilePath)
        : outputFilePath_(outputFilePath),
          yuvFile_(nullptr),
          fileCount(0),
          fileSize_(0) {}

    void onFrame(const char* channelId, agora::user_id_t remoteUid, const agora::media::base::VideoFrame* frame) override;

    virtual ~YuvFrameObserver() = default;

   private:
    std::string outputFilePath_;
    FILE* yuvFile_;
    int fileCount;
    int fileSize_;
};

// 通过 onPlaybackAudioFrameBeforeMixing 回调接收 PCM 格式的音频并以文件形式输出
bool PcmFrameObserver::onPlaybackAudioFrameBeforeMixing(const char* channelId, agora::media::base::user_id_t userId, AudioFrame& audioFrame) {
    // 创建文件保存收到的 PCM 音频数据
    if (!pcmFile_) {
        std::string fileName = (++fileCount > 1)
                                   ? (outputFilePath_ + to_string(fileCount))
                                   : outputFilePath_;
        if (!(pcmFile_ = fopen(fileName.c_str(), "w"))) {
            AG_LOG(ERROR, "Failed to create received audio file %s",
                   fileName.c_str());
            return false;
        }
        AG_LOG(INFO, "Created file %s to save received PCM samples",
               fileName.c_str());
    }

    // 将 PCM 音频数据写入文件
    size_t writeBytes =
        audioFrame.samplesPerChannel * audioFrame.channels * sizeof(int16_t);
    if (fwrite(audioFrame.buffer, 1, writeBytes, pcmFile_) != writeBytes) {
        AG_LOG(ERROR, "Error writing decoded audio data: %s", std::strerror(errno));
        return false;
    }
    fileSize_ += writeBytes;

    if (fileSize_ >= DEFAULT_FILE_LIMIT) {
        fclose(pcmFile_);
        pcmFile_ = nullptr;
        fileSize_ = 0;
    }
    return true;
}
```

### 3. 断开连接并释放资源 [#3-断开连接并释放资源-1]

媒体流接收完成后，你可以通过以下步骤退出频道并断开与声网服务器的连接：

:::warning[注意]
你必须严格按照示例代码中的顺序释放相关资源。
:::

1. 调用 unregister 方法释放视频、音频 observer。

    ```cpp
    // 释放视频、音频 observer
    local_user_->unregisterAudioFrameObserver(audio_frame_observer_);
    local_user_->unregisterVideoFrameObserver(video_frame_observer_);
    ```

2. 调用 `disconnect` 与声网 RTC 频道断开连接。

    ```cpp
    // 与声网 RTC 频道断开连接
    if (connection->disconnect()) {
        AG_LOG(ERROR, "Failed to disconnect from声网channel!");
        return -1;
    }
    AG_LOG(INFO, "Disconnected from声网channel successfully");
    ```

3. 销毁创建的相关对象。

    ```cpp
    // 销毁创建的相关对象
    localUserObserver.reset();
    pcmFrameObserver.reset();
    h264FrameReceiver.reset();
    connection = nullptr;
    ```

## 注销 `IAgoraService` 对象 [#注销-iagoraservice-对象]

当你的服务端程序退出时，可以调用如下逻辑，注销整个 `IAgoraService` 对象。

```cpp
service->release();
service = nullptr;
```

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

你可以在这里查看时序图、开发注意事项等信息。

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

你可以通过 [API 参考](/zh-CN/api-reference/rtc-server-sdk/cpp/overview) 获取详细的接口和参数信息。

### API 时序图 [#api-时序图]

你可以通过 API 时序图了解 API 的调用时序。

:::info[信息]
下文各图中，虚线为对象的生命线，线上的矩形为激活框，表示在交互中该对象何时起作用。
:::

#### 发送 PCM 音频数据和 YUV 视频数据 [#发送-pcm-音频数据和-yuv-视频数据]

![发送 PCM 音频数据和视频数据时序图](https://web-cdn.agora.io/docs-files/1591688188083)

#### 发送编码后音视频数据 [#发送编码后音视频数据]

![发送编码后音视频数据时序图](https://web-cdn.agora.io/docs-files/1591688206240)

#### 接收 PCM 音频数据和 YUV 视频数据 [#接收-pcm-音频数据和-yuv-视频数据]

![接收 PCM 音频数据和 YUV 视频数据时序图](https://web-cdn.agora.io/docs-files/1652417005647)

#### 接收编码后视频数据和 PCM 音频数据 [#接收编码后视频数据和-pcm-音频数据]

![接收编码后视频数据和 PCM 音频数据](https://web-cdn.agora.io/docs-files/1591759192749)

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

- 以上所有接口函数，如无特殊说明，都是异步调用，无需担心阻塞用户线程。
- 在声网 RTC 客户端 SDK 和服务端 SDK 互通的场景中，请确保将客户端 SDK 的频道场景设置为 `LIVE_BROADCASTING`。
- SDK 发送 AAC 格式的音频数据时，不支持 44.1 Kbps 的采样率。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="cpp" platform="java" />
本文介绍如何使用 RTC 服务端 SDK 向客户端发送媒体流并从客户端接收媒体流。

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

你已经下载了最新的 RTC 服务端 SDK。详见[跑通 RTC 服务端 SDK 示例项目](/zh-CN/realtime-media/rtc-server-sdk/get-started/run-example)。

## 准备工作 [#准备工作-1]

在发送和接收媒体流之前，你需要完成以下准备工作。

### 1. 初始化 SDK [#1-初始化-sdk-1]

首先，你需要在应用程序中创建 `AgoraService` 对象，并调用 `initialize` 方法进行全局初始化。

这个操作只需要进行一次，`AgoraService` 对象的生命期和应用程序的生命期保持一致，只要应用程序没有退出，`AgoraService` 可以一直存在。

SDK 支持 `int` 型和 `String` 型用户 ID。本文仅演示 `int` 型用户 ID（即字符只能为数字）；`String` 型用户 ID（字符可以是数字、字母或特殊符号）的使用方法请参考 [使用 String 型用户 ID](/zh-CN/realtime-media/rtc-server-sdk/build/string-uid)。

```java
// Import SDK, AgoraService, and AgoraServiceConfig classes for initialization
import io.agora.rtc.SDK;
import io.agora.rtc.AgoraService;
import io.agora.rtc.AgoraServiceConfig;

// Creates an AgoraService object
AgoraService service = new AgoraService();
// Initializes the AgoraServiceConfig object
AgoraServiceConfig config = new AgoraServiceConfig();
// Enables the audio processing module
config.setEnableAudioProcessor(1);
// Disables the audio device module (Normally we do not directly connect audio capture or playback devices to a server)
config.setEnableAudioDevice(0);
// Enables video
config.setEnableVideo(1);
// Sets声网 App ID
config.setAppId(appid);
// Initializes the SDK
service.initialize(config);
```

### 2. 与 RTC 频道建立连接 [#2-与-rtc-频道建立连接-1]

初始化后，根据以下步骤与声网 RTC 频道建立连接。

1. 调用 `agoraRtcConnCreate` 方法创建 `AgoraRtcConn` 对象，用于与声网服务器建立连接。

    ```java
    AgoraRtcConn conn = service.agoraRtcConnCreate(new RtcConnConfig());
    ```

2. 调用 `registerObserver` 方法监听连接事件。

    ```java
    conn.registerObserver(new DefaultRtcConnObserver());
    ```

3. 调用 `connect` 与声网频道建立连接。

    ```java
    conn.connect(token, "test_channel", "1");
    ```

## 向客户端发送媒体流 [#向客户端发送媒体流-1]

参考以下步骤向客户端发送媒体流。

### 1. 创建媒体流发送器 [#1-创建媒体流发送器-1]

你可以通过媒体节点工厂对象 `AgoraMediaNodeFactory` 创建不同类型的媒体流发送器：

- `AgoraAudioPcmDataSender`：发送 PCM 格式的音频数据。
- `AgoraVideoFrameSender`：发送 YUV 格式的视频数据。
- `AgoraAudioEncodedFrameSender`：发送已编码的音频数据。
- `AgoraVideoEncodedImageSender`：发送已编码的视频数据。

1. 创建 `AgoraMediaNodeFactory` 媒体节点工厂对象。

    ```java
    AgoraMediaNodeFactory factory = service.createMediaNodeFactory();
    ```

2. 根据你的需求，创建 `AgoraAudioPcmDataSender` 对象、`AgoraVideoFrameSender`对象、`AgoraAudioEncodedFrameSender` 对象或 `AgoraVideoEncodedImageSender` 对象，分别用于发送 PCM 格式的音频流、YUV 格式的视频流、已编码的音频流及已编码的视频流。

    ```java
    // Creates a sender for PCM audio
    AgoraAudioPcmDataSender audioFrameSender = factory.createAudioPcmDataSender();

    // Creates a sender for YUV video
    AgoraVideoFrameSender videoFrameSender = factory.createVideoFrameSender();

    // Creates a sender for encoded audio
    AgoraAudioEncodedFrameSender audioFrameSender = factory.createAudioEncodedFrameSender();

    // Creates a sender for encoded video
    AgoraVideoEncodedImageSender imageSender = factory.createVideoEncodedImageSender();
    ```

3. 创建 `AgoraLocalAudioTrack` 对象和 `AgoraLocalVideoTrack` 对象，分别对应即将发布至频道内的本地音频轨道和本地视频轨道。对象中可以使用上一步创建的发送器。

    ```java
    // Creates a custom audio track that uses a PCM audio stream sender
    customAudioTrack = service.createCustomAudioTrackPcm(audioFrameSender);

    // Creates a custom audio track that uses an encoded audio stream sender
    customAudioTrack = service.createCustomAudioTrackEncoded(audioFrameSender,0);

    // Creates a custom video track that uses a YUV video stream sender
    customVideoTrack = service.createCustomVideoTrackFrame(videoFrameSender);

    // Creates a custom video track that uses encoded video stream sender
    customVideoTrack = service.createCustomVideoTrackEncoded(videoFrameSender, option);
    ```

### 2. 发送媒体流 [#2-发送媒体流-1]

1. 通过 `AgoraLocalUser` 对象的 publish 相关方法在声网 RTC 频道中发布上一步创建的本地音频轨道和视频轨道。

    ```java
    // Enables and publishes audio and video track
    customAudioTrack.setEnabled(1);
    customVideoTrack.setEnabled(1);
    conn.getLocalUser().publishAudio(customAudioTrack);
    conn.getLocalUser().publishVideo(customVideoTrack);
    ```

2. 开启发送线程，调用发送器的 send 方法发送音视频数据。

    ```java
    pcmSender = new PcmSender(audioFile,audioFrameSender,numOfChannels,sampleRate);
    h264Sender = new H264Sender(videoFile,1000/fps,0,0,videoFrameSender);

    pcmSender.start();
    h264Sender.start();
    ```

    本文以发送 PCM 音频数据为例进行介绍，`PcmSender` 的实现如下：

    ```java
    // audio thread
    // send audio data every 10 ms;
    class PcmSender extends FileSender {
        private AgoraAudioPcmDataSender audioFrameSender;
        private static final int INTERVAL = 10; //ms
        private int channels;
        private int samplerate;
        private int bufferSize = 0;
        private byte[] buffer;
        public PcmSender(String filepath, AgoraAudioPcmDataSender sender,int channels,int samplerate){
            super(filepath, INTERVAL);
            audioFrameSender = sender;
            this.channels = channels;
            this.samplerate = samplerate;
            this.bufferSize = channels * samplerate * 2 * INTERVAL /1000;
            this.buffer = new byte[this.bufferSize];
        }

        // sendOneFrame calls the send method of audioFrameSender to send PCM data
        @Override
        public void sendOneFrame(byte[] data) {
            if(data == null) return;
            AudioFrame audioFrame = new AudioFrame();
            audioFrame.setBuffer(ByteBuffer.wrap(data));
            audioFrame.setSamplesPerChannel(sampleRate/(1000/INTERVAL));
            audioFrame.setBytesPerSample(2);
            audioFrame.setChannels(1);
            audioFrame.setSamplesPerSec(samplerate);
            int ret = audioFrameSender.sendAudioPcmData(audioFrame);
        }

        @Override
        public byte[] readOneFrame(FileInputStream fos) {
            if(fos != null ){
                try {
                    int size = fos.read(buffer,0,bufferSize);
                    if( size <= 0){
                        reset();
                        return null;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return buffer;
        }
    }
    ```

    以发送 H.264 格式的编码视频为例，`H264Sender` 的实现如下：

    ```java
    class H264Sender extends FileSender {
        private AgoraVideoEncodedImageSender imageSender;
        private H264Reader h264Reader;
        private int lastFrameType = 0;
        private int height;
        private int width;
        private int fps;
        public H264Sender(String path,int interval, int height,int width, AgoraVideoEncodedImageSender videoEncodedImageSender){
            super(path,interval,false);
            this.imageSender = videoEncodedImageSender;
            this.h264Reader = new H264Reader(path);
            this.height = height;
            this.width = width;
            this.fps = 1000/interval;
        }
        
        // sendOneH264Frame calls the send method of imageSender to send H.264 data.
        public void sendOneFrame(byte[] data) {
            if(data == null) return;
            EncodedVideoFrameInfo info = new EncodedVideoFrameInfo();
            info.setFrameType(lastFrameType);
            info.setWidth(width);
            info.setHeight(height);
            info.setCodecType(Constants.VIDEO_CODEC_H264);
            info.setFramesPerSecond(fps);
            info.setRotation(0);
            imageSender.sendEncodedVideoImage(data,info);
        }

        @Override
        public byte[] readOneFrame(FileInputStream fos) {
            int retry = 0;
            H264Reader.H264Frame frame =  h264Reader.readNextFrame();
            while ( frame == null && retry < 4){
                h264Reader.reset();
                frame = h264Reader.readNextFrame();
                retry ++;
            }
            if( frame != null ) {
                lastFrameType = frame.frameType;
                return frame.data;
            } else {
                return null;
            }
        }

        @Override
        public void release() {
            super.release();
            h264Reader.close();
        }
    }
    ```

    编码视频数据的格式通过 `send` 方法的 `info` 参数进行设置。

### 3. 断开连接并释放资源 [#3-断开连接并释放资源-2]

媒体流发送完成后，你可以通过以下步骤退出频道并断开与声网服务器的连接：

1. 调用 `unpublish` 相关方法取消发布音频流和视频流。

    ```java
    if(conn != null) {
        conn.getLocalUser().unpublishAudio(customAudioTrack);
        conn.getLocalUser().unpublishVideo(customVideoTrack);
    }
    ```

2. 调用 `unregisterObserver` 取消连接 observer。

    ```java
    conn.unregisterObserver();
    ```

3. 调用 `disconnect` 断开与声网服务器的连接。

    ```java
    int ret = conn.disconnect();
    ```

4. 释放已创建对象的资源。

    ```java
    conn.destroy();
    ```

## 从客户端接收媒体流 [#从客户端接收媒体流-1]

参考以下步骤从客户端接收媒体流。

### 1. 注册音视频帧 observer [#1-注册音视频帧-observer-1]

实例化音视频帧 observer 对象，并通过 `AgoraLocalUser` 类的成员方法注册音视频帧 observer。成功注册 observer 后，SDK 会在捕捉到每个音频或视频帧时触发回调。你可以在回调中获得该音频或视频帧。

```java
// 监听pcm
int ret = conn.getLocalUser().setPlaybackAudioFrameBeforeMixingParameters(numOfChannels, sampleRate);
conn.getLocalUser().registerAudioFrameObserver(audioFrameObserver, enableVad, new AgoraAudioVadConfigV2());

// 监听编码音频
conn.getLocalUser().registerAudioEncodedFrameObserver(audioEncodedFrameObserver);

// 监听yuv
conn.getLocalUser().registerVideoFrameObserver(videoFrameObserver);

// 监听编码视频
conn.getLocalUser().registerVideoEncodedFrameObserver(videoEncodedFrameObserver);
```

### 2. 接收媒体流 [#2-接收媒体流-1]

下面的代码展示了如何接收已编码的视频、YUV 格式的视频和 PCM 格式的音频。

```java
// Receives PCM audio by using the onPlaybackAudioFrameBeforeMixing callback
public static class PcmFrameObserver extends FileWriter implements IAudioFrameObserver {
    public PcmFrameObserver(String outputFilePath) {
        super(outputFilePath);
    }

    @Override
    public int onPlaybackAudioFrame(AgoraLocalUser agoraLocalUser, String channelId, AudioFrame frame) {
        System.out.println("onPlaybackAudioFrame success");
        return 1;
    }

    @Override
    public int onMixedAudioFrame(AgoraLocalUser agoraLocalUser, String channelId, AudioFrame frame) {
        System.out.println("onMixedAudioFrame success");
        return 1;
    }

    @Override
    public int onPlaybackAudioFrameBeforeMixing(AgoraLocalUser agoraLocalUser, String channelId, String userId,
            AudioFrame frame, VadProcessResult vadResult) {
        System.out.println("onPlaybackAudioFrameBeforeMixing success");
        writeData(frame.getBuffer(), frame.getBuffer().remaining());
        return 1;
    }
}

// Receives encoded audio by using the onEncodedAudioFrameReceived callback
class AudioEncodedFrameObserver extends FileWriter implements IAudioEncodedFrameObserver {

    public AudioEncodedFrameObserver(String path) {
        super(path);
    }

    @Override
    public int onEncodedAudioFrameReceived(String remoteUserId, ByteBuffer buffer,
            EncodedAudioFrameReceiverInfo info) {
        System.out.println("onEncodedAudioFrameReceived success");
        return 1;
    }
}

// Receives YUV video by using the onFrame callback
class YuvFrameObserver extends FileWriter implements IVideoFrameObserver2 {

    public YuvFrameObserver(String path) {
        super(path);
    }

    @Override
    public void onFrame(AgoraVideoFrameObserver2 agoraVideoFrameObserver2, String channelId, String remoteUserId,
            VideoFrame frame) {
        System.out.println("onFrame success  ");
        writeData(frame.getYBuffer(), frame.getYBuffer().remaining());
        writeData(frame.getUBuffer(), frame.getUBuffer().remaining());
        writeData(frame.getVBuffer(), frame.getVBuffer().remaining());
        return;
    }
}

// Receives encoded video by using the onEncodedVideoFrame callback
class H264FrameReceiver extends FileWriter implements IVideoEncodedFrameObserver {

    public H264FrameReceiver(String path) {
        super(path);
    }

    @Override
    public int onEncodedVideoFrame(AgoraVideoEncodedFrameObserver observer, int userId,
            ByteBuffer buffer, EncodedVideoFrameInfo info) {
        System.out.println("onEncodedVideoFrame success  " + info.getFrameType());
        writeData(buffer.array(), buffer.remaining());
        return 1;
    }
}
```

### 3. 断开连接并释放资源 [#3-断开连接并释放资源-3]

媒体流接收完成后，你可以通过以下步骤退出频道并断开与声网服务器的连接：

:::warning[注意]
你必须严格按照示例代码中的顺序释放相关资源。
:::

1. 释放视频、音频相关资源。

    ```java
    conn.getLocalUser().unpublishAudio(customAudioTrack);
    customAudioTrack.destroy();
    
    audioFrameSender.destroy();
    audioEncodedFrameSender.destroy();
    
    conn.getLocalUser().unpublishVideo(customEncodedVideoTrack);
    customEncodedVideoTrack.destroy();
    
    videoFrameSender.destroy();
    customEncodedImageSender.destroy();
    
    conn.getLocalUser().unregisterAudioFrameObserver();
    conn.getLocalUser().unregisterAudioEncodedFrameObserver(audioEncodedFrameObserver);
    conn.getLocalUser().unregisterVideoFrameObserver(videoFrameObserver);
    videoFrameObserver.destroy();
    conn.getLocalUser().unregisterVideoEncodedFrameObserver(videoEncodedFrameObserver);
    videoEncodedFrameObserver.destroy();
    ```

2. 调用 `disconnect` 与声网 RTC 频道断开连接。

    ```java
    // Disconnect from the声网channel
    int ret = conn.disconnect();
    if (ret != 0) {
        System.out.printf("conn.disconnect fail ret=%d\n", ret);
    }
    ```

3. 销毁创建的相关对象。

    ```java
    // Releases the created objects.
    conn.destroy();
    ```

## 注销 AgoraService 对象 [#注销-agoraservice-对象]

当你的服务端程序退出时，可以调用如下逻辑，注销整个 `AgoraService` 对象。

```java
// Destroy声网 Service
service.destroy();
```

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

- 以上所有接口函数，如无特殊说明，都是异步调用，无需担心阻塞用户线程。
- 在声网 RTC 客户端 SDK 和服务端 SDK 互通的场景中，请确保将客户端 SDK 的频道场景设置为 `LIVE_BROADCASTING`。
- SDK 发送 AAC 格式的音频数据时，不支持 44.1 kHz 的采样率。

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

你可以在这里查看时序图、开发注意事项等信息。

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

你可以通过 [API 参考](/zh-CN/api-reference/rtc-server-sdk/java/overview) 获取详细的接口和参数信息。

### 示例项目 [#示例项目]

声网提供了开源的服务端示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title="Agora-Java-Server-SDK" href="https://github.com/AgoraIO-Extensions/Agora-Java-Server-SDK" />
</Cards>
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="cpp" platform="python" />
本文介绍如何集成服务端 Python SDK，通过少量代码从 0 开始实现发送和接收媒体流。

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

在实现功能以前，请按照以下要求准备开发环境：

- 已下载 PyCharm 或 Visual Studio Code
- Python 3.10 或以上版本
- 服务器接入公网，有公网 IP。服务器允许访问 `.agora.io` 以及 `.agoralab.co` 域名。
- 操作系统与硬件环境需满足下列要求：

    | 开发平台 | 操作系统版本 | CPU 架构 | 设备性能 |
    | --- | --- | --- | --- |
    | Linux | [send-receive-python-markdown-0-1-1 details](#table-slot-send-receive-python-markdown-0-1-1) | x86-64 | [send-receive-python-markdown-0-1-3 details](#table-slot-send-receive-python-markdown-0-1-3) |

    <a id="table-slot-send-receive-python-markdown-0-1-1"></a>

    **send-receive-python-markdown-0-1-1 details**

    - Ubuntu 18.04 LTS 或以上
    - CentOS 7.0 或以上

    <a id="table-slot-send-receive-python-markdown-0-1-3"></a>

    **send-receive-python-markdown-0-1-3 details**

    - CPU：8 核，1.8 GHz 主频
    - 内存：2 GB 或以上，推荐 4 GB 或以上

- 一个有效的声网账号以及声网项目。请参考[开通服务](/zh-CN/realtime-media/rtc-server-sdk/build/enable-service)从声网控制台获得以下信息：
  - App ID：声网随机生成的字符串，用于识别你的项目。
  - 临时 Token：Token 也称为动态密钥，在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。

## 集成 SDK [#集成-sdk]

在终端中，进入你的项目根目录，然后运行以下命令集成 SDK：

```bash
pip install agora_python_server_sdk
```

## 实现步骤 [#实现步骤]

本小节介绍如何实现服务端发送和接收媒体流，你可以按照实现步骤了解核心 API 调用。下图展示了基本的实现流程：

<DocsImage alt="服务端发送和接收媒体流的实现流程图" src="https://web-cdn.agora.io/docs-portal/img/rtc-server-sdk/quick-start-sequence.svg" displayWidth="45%" />

### 初始化 SDK [#初始化-sdk]

在调用其他声网 API 前，需要创建并初始化 `AgoraService` 实例：

1. 调用 `AgoraService()` 来创建 `AgoraService` 实例。
2. 调用 `initialize` 进行初始化。

```python
agora_service = AgoraService()

config = AgoraServiceConfig()
config.audio_scenario = AudioScenarioType.AUDIO_SCENARIO_CHORUS
config.appid = sample_options.app_id
agora_service.initialize(config)
```

### 与频道建立连接 [#与频道建立连接]

初始化后，按照以下步骤与声网 RTC 频道建立连接：

1. 调用 `create_rtc_connection` 方法创建 `RTCConnection` 对象，用于与声网服务器建立连接。

    :::warning[注意]
    如需创建多个 Connection，声网推荐使用多进程的方式，以避免内存泄露等问题。
    :::

   ```python
   con_config = RTCConnConfig(
       client_role_type=ClientRoleType.CLIENT_ROLE_BROADCASTER,
       channel_profile=ChannelProfileType.CHANNEL_PROFILE_LIVE_BROADCASTING,
   )
   connection = agora_service.create_rtc_connection(con_config)
   if not connection:
       logger.error("create connection failed")
       return
   ```

2. 调用 `register_observer` 方法注册连接事件观测器。示例代码中的 `DYSConnectionObserver` 继承了 `IRTCConnectionObserver` 类。

   ```python
   # DYSConnectionObserver 继承自 IRTCConnectionObserver 类
   conn_observer = DYSConnectionObserver()
   connection.register_observer(conn_observer)
   ```

3. 调用 `connect` 与声网 RTC 频道建立连接。

   ```python
   ret = connection.connect(sample_options.token, channel_id, uid)
   if ret < 0:
       logger.error(f"connect failed: {ret}")
       return
   ```

### 发送媒体流 [#发送媒体流]

成功建立连接后，参考以下步骤向客户端发送媒体流：

1. 创建 `MediaNodeFactory` 媒体节点工厂对象。

    ```python
    media_node_factory = agora_service.create_media_node_factory()
    if not media_node_factory:
        logger.error("create media node factory failed")
        return
    ```

2. 在 `MediaNodeFactory` 对象中，根据实际需求创建音视频数据发送器。

    - `AudioPcmDataSender`：发送 PCM 格式的音频数据。
    - `VideoFrameSender`：发送 YUV 格式的视频数据。
    - `AudioEncodedFrameSender`：发送已编码的音频数据。
    - `VideoEncodedImageSender`：发送已编码的视频数据。

    ```python
    # 创建可发送 PCM 格式的音频数据的发送器
    pcm_data_sender = media_node_factory.create_audio_pcm_data_sender()
    if not pcm_data_sender:
        logger.error("create pcm data sender failed")
        return

    # 创建可发送已编码的音频数据的发送器
    audio_sender = media_node_factory.create_audio_encoded_frame_sender()
    if not audio_sender:
        logger.error("create audio sender failed")
        return

    # 创建可发送 YUV 格式的视频数据的发送器
    video_sender = media_node_factory.create_video_frame_sender()
    if not video_sender:
        logger.error("create video frame sender failed")
        return

    # 创建可发送已编码的视频数据的发送器
    video_sender = media_node_factory.create_video_encoded_image_sender()
    if not video_sender:
        logger.error("create video sender failed")
        return
    ```

3. 使用步骤 2 中创建好的音视频数据发送器，创建 `LocalAudioTrack` 对象和 `LocalVideoTrack` 对象，分别对应本地音频轨道和本地视频轨道。

    ```python
    # 创建自定义音频轨道（使用可发送 PCM 格式的音频数据的发送器）
    audio_track = agora_service.create_custom_audio_track_pcm(pcm_data_sender)
    if not audio_track:
        logger.error("create audio track failed")
        return

    # 创建自定义音频轨道（使用可发送已编码的音频数据的发送器）
    audio_track = agora_service.create_custom_audio_track_encoded(audio_sender, 1)
    if not audio_track:
        logger.error("create audio track failed")
        return

    # 创建自定义视频轨道（可发送 YUV 格式的视频数据的发送器）
    video_track = agora_service.create_custom_video_track_frame(video_sender)
    if not video_track:
        logger.error("create video track failed")
        return

    # 创建自定义视频轨道（可发送已编码的视频数据的发送器）
    video_track = agora_service.create_custom_video_track_encoded(video_sender, sender_options)
    if not video_track:
        logger.error("create video track failed")
        return
    ```

4. 调用 `set_enabled` 方法分别开启本地音频轨道和视频轨道。

   ```python
   # 开启本地音频轨道
   audio_track.set_enabled(1)

   # 开启本地视频轨道
   video_track.set_enabled(1)
   ```

5. 调用 `publish_audio` 和 `publish_video` 方法在频道中分别发布本地音频轨道和视频轨道。

   ```python
   # 发布本地音频轨道
   local_user.publish_audio(audio_track)

   # 发布本地视频轨道
   local_user.publish_video(video_track)
   ```

6. 开启音视频发送线程。

    ```python
    async def send(self,sample_options:ExampleOptions, pcm_data_sender, yuv_data_sender):
        # 创建并启动音频发送任务
        pcm_task = asyncio.create_task(push_pcm_data_from_file(sample_options.sample_rate, sample_options.num_of_channels, pcm_data_sender, sample_options.audio_file, self._exit))
        # 创建并启动视频发送任务
        yuv_task = asyncio.create_task(push_yuv_data_from_file(sample_options.width, sample_options.height, sample_options.fps, yuv_data_sender, sample_options.video_file, self._exit))
        # 等待音频发送任务完成
        await pcm_task
        # 等待视频发送任务完成
        await yuv_task
        logger.info("send finish")
    ```

7. 发送音视频数据。本节以发送 PCM 音频数据和 YUV 视频数据为例，示例代码如下：

    ```python
    # 发送 PCM 音频数据
    async def push_pcm_data_from_file(sample_rate, num_of_channels , pcm_data_sender:AudioPcmDataSender, audio_file_path, _exit:Event):
        with open(audio_file_path, "rb") as audio_file:
            pcm_sendinterval = 0.1
            pacer_pcm = Pacer(pcm_sendinterval)
            pcm_count = 0
            send_size = int(sample_rate*num_of_channels*pcm_sendinterval*2)
            frame_buf = bytearray(send_size)
            while not _exit.is_set():
                success = audio_file.readinto(frame_buf)
                if not success:
                    audio_file.seek(0)
                    continue
                frame = PcmAudioFrame()
                frame.data = frame_buf
                frame.timestamp = 0
                frame.samples_per_channel = int(sample_rate * pcm_sendinterval)
                frame.bytes_per_sample = 2
                frame.number_of_channels = num_of_channels
                frame.sample_rate = sample_rate
                ret = pcm_data_sender.send_audio_pcm_data(frame)
                pcm_count += 1
                logger.info(f"send pcm: count,ret={pcm_count}, {ret}, {send_size}, {pcm_sendinterval}")
                await pacer_pcm.apace_interval(0.1)
            frame_buf = None
    ```

    ```python
    # 发送 YUV 视频数据
    async def push_yuv_data_from_file(width, height, fps, video_sender:VideoFrameSender, video_file_path, _exit:Event):
        with open(video_file_path, "rb") as video_file:
            yuv_sendinterval = 1.0/fps
            pacer_yuv = Pacer(yuv_sendinterval)
            yuv_count = 0
            yuv_len = int(width*height*3/2)
            frame_buf = bytearray(yuv_len)
            while not _exit.is_set():
                success = video_file.readinto(frame_buf)
                if not success:
                    video_file.seek(0)
                    continue
                frame = ExternalVideoFrame()
                frame.buffer = frame_buf
                frame.type = 1
                frame.format = 1
                frame.stride = width
                frame.height = height
                frame.timestamp = 0
                frame.metadata = "hello metadata"
                ret = video_sender.send_video_frame(frame)
                yuv_count += 1
                logger.info("send yuv: count,ret=%d, %s", yuv_count, ret)
                await pacer_yuv.apace_interval(yuv_sendinterval)
            frame_buf = None
    ```

### 接收媒体流 [#接收媒体流]

成功建立连接后，参考以下步骤从客户端接收媒体流：

1. 根据实际需求，调用 `register_audio_frame_observer` 和 `register_video_frame_observer` 方法注册音视频数据观测器，并监听相关回调。支持以下几种数据观测器，示例代码以注册 `IAudioFrameObserver` 和 `IVideoFrameObserver` 观测器为例。

    - `IAudioFrameObserver`：音频数据观测器。
    - `IVideoFrameObserver`：原始视频数据观测器。
    - `IVideoEncodedImageReceiver`：已编码视频数据观测器。

    ```python
    # 在 register_audio_frame_observer 方法之前调用 set_playback_audio_frame_before_mixing_parameters 方法，设置音频数据的采样率
    local_user.set_playback_audio_frame_before_mixing_parameters(1, 16000)
    # SampleAudioFrameObserver 类继承了 IAudioFrameObserver
    audio_frame_observer = SampleAudioFrameObserver()
    local_user.register_audio_frame_observer(audio_frame_observer)

    # SampleVideoFrameObserver 类继承了 IVideoFrameObserver
    video_frame_observer = SampleVideoFrameObserver()
    local_user.register_video_frame_observer(video_frame_observer)
    ```

2. 当客户端开始发送媒体流时，SDK 会通过相关回调接收媒体流。以接收已编码的视频、YUV 格式的视频和 PCM 格式的音频为例，示例代码如下：

    ```python
    # 通过 on_encoded_video_frame 回调接收已编码的视频并以文件形式输出
    class SampleVideoEncodedFrameObserver(IVideoEncodedFrameObserver):
        def on_encoded_video_frame(self, uid, image_buffer, length, video_encoded_frame_info):
            file_path = os.path.join(log_folder, str(uid) + '.h264')
            with open(file_path, 'ab') as f:
                f.write(image_buffer[:length])
            return 1

    # 通过 on_frame 回调接收 YUV 格式的视频并以文件形式输出
    class SampleVideoFrameObserver(IVideoFrameObserver):
        def on_frame(self, channel_id, remote_uid, frame:VideoFrame):
            file_path = os.path.join(log_folder, channel_id + "_" + remote_uid + '.yuv')
            y_size = frame.y_stride * frame.height
            uv_size = (frame.u_stride * frame.height # 2)
            with open(file_path, 'ab') as f:
                f.write(frame.y_buffer[:y_size])
                f.write(frame.u_buffer[:uv_size])
                f.write(frame.v_buffer[:uv_size])
            return 1

    # 通过 on_playback_audio_frame_before_mixing 回调接收 PCM 格式的音频并以文件形式输出
    class SampleAudioFrameObserver(IAudioFrameObserver):
        def on_playback_audio_frame_before_mixing(self, agora_local_user, channelId, uid, audio_frame:AudioFrame):
            file_path = os.path.join(log_folder, channelId + "_" + uid + '.pcm')
            with open(file_path, "ab") as f:
                f.write(audio_frame.buffer)
            return 1
    ```

### 停止发送和接收媒体流 [#停止发送和接收媒体流]

如果你不再需要发送和接收媒体流，你可以调用以下方法停止发送和接收媒体流。

#### 停止发送媒体流 [#停止发送媒体流]

1. 调用 `unpublish_audio` 和 `unpublish_video` 方法停止发布音视频轨道。

   ```python
   local_user.unpublish_audio(audio_track)
   local_user.unpublish_video(video_track)
   ```

2. 调用 `set_enabled` 方法分别关闭本地音频轨道和视频轨道。

   ```python
   local_user.unpublish_audio(audio_track)
   local_user.unpublish_video(video_track)
   audio_track.set_enabled(0)
   video_track.set_enabled(0)
   ```

3. 调用 `release` 方法依次销毁本地音视频轨道、音视频数据发送器、媒体节点工厂对象。

   ```python
   # 销毁 PCM 音频数据发送器
   pcm_data_sender.release()
   # 销毁本地音频轨道
   audio_track.release()
   # 销毁 YUV 视频数据发送器
   yuv_data_sender.release()
   # 销毁本地视频轨道
   video_track.release()
   # 销毁媒体节点工厂对象
   media_node_factory.release()

   pcm_data_sender = None
   audio_track = None
   yuv_data_sender = None
   video_track = None
   media_node_factory = None
   video_frame_observer = None
   audio_frame_observer = None
   ```

#### 停止接收媒体流 [#停止接收媒体流]

调用 `unregister_audio_frame_observer` 和 `unregister_video_frame_observer` 方法注销音视频数据观测器。

```python
local_user.unregister_audio_frame_observer(audio_frame_observer)
local_user.unregister_video_frame_observer(video_frame_observer)
```

### 与频道断开连接 [#与频道断开连接]

如果你不再需要对频道进行任何操作，或者不再需要接收任何频道的事件，按照以下步骤与频道断开连接：

:::warning[注意]
你必须严格按照步骤释放相关资源。
:::

1. 调用 `disconnect` 方法断开与声网服务器的连接。

   ```python
   ret = connection.disconnect()
   if ret < 0:
       logger.error(f"disconnect failed: {ret}")
       return
   ```

2. 调用 `unregister_observer` 方法注销连接事件观测器。

   ```python
   local_user.unregister_local_user_observer()
   connection.unregister_observer()
   ```

3. 调用 `release` 方法释放 `RTCConnection` 对象。

    ```python
    connection.release()
    ```

### 释放 SDK 资源 [#释放-sdk-资源]

当你退出服务端程序时，为避免资源浪费，调用 `release` 方法释放 `AgoraService` 对象，以释放 SDK 资源。

```python
agora_service.release()
```

## 调试项目 [#调试项目]

按照以下步骤来测试你的项目：

1. 在终端运行你的 Python 文件。你需要填写以下参数。
    - `appId`: 你声网项目的 App ID。
    - `channelId`: 你的频道名，本节以 `test_example` 为例。
    - `userId`: 你的用户 ID，本节以 `6` 为例。
    - `audioFile`: 音频数据文件的存放路径，本节以 `./test_data/demo.pcm` 为例。为方便测试，声网提供了[测试数据](https://download.agora.io/demo/test/test_data_202408221437.zip)，你可以下载并解压至 `Agora-Python-Server-SDK` 路径下使用。
    - `sampleRate`: 采样率，本节以 `16000` 为例。
    - `numOfChannels`: 声道数，本节以 `1` 为例。

   ```bash
   # 假设你的 Python 文件路径为 Desktop/videocall.py
   python Desktop/videocall.py --appId=0a****************************99 --channelId=test_dys --userId=6 --audioFile=./test_data/demo.pcm --sampleRate=16000 --numOfChannels=1
   ```

2. 打开声网实时互动 [Web Demo](https://webdemo-global.agora.io/index.html)，完成**初始化设置**，在浏览器中模拟一个客户端。App ID 需和你在步骤 1 中填入的 App ID 一致。
3. 进入**基础视频通话**页面，在 **Channel** 填入与步骤 2 相同的频道名，依次点击 **Create Client**、**Join Channel**。
4. 成功加入频道后，你可以开始发流。当频道中有发流用户时，Web Demo 会在 **Step4 Subscribe & Play** 下方的输入框中自动填充发流用户的用户 ID，点击 **Subscribe & Play** 按钮订阅发流用户。

:::warning[注意]
由于 Web Demo 的限制，体验服务端发流时，你需要先在 Web 端完成加入频道，然后再在服务端运行项目发流；体验服务端收流时，你需要先在服务端运行项目连接频道，然后再在 Web 端加入频道、发流。
:::

## 示例项目 [#示例项目-1]

声网提供了开源的服务端示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title="Agora-Python-Server-SDK" href="https://github.com/AgoraIO-Extensions/Agora-Python-Server-SDK/tree/release/2.1.0/agora_rtc/examples" />
</Cards>
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="cpp" platform="go" />
本文介绍如何集成服务端 Go SDK，通过少量代码从 0 开始实现发送和接收媒体流。

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

在实现功能以前，请按照以下要求准备开发环境：

- 已下载 Visual Studio Code
- Go 1.21 或以上版本
- 服务器接入公网，有公网 IP。服务器允许访问 `.agora.io` 以及 `.agoralab.co` 域名。
- 操作系统与硬件环境需满足下列要求：

    | 开发平台 | 操作系统版本 | CPU 架构 | 设备性能 |
    | --- | --- | --- | --- |
    | Linux | [send-receive-go-markdown-0-1-1 details](#table-slot-send-receive-go-markdown-0-1-1) | x86-64 | [send-receive-go-markdown-0-1-3 details](#table-slot-send-receive-go-markdown-0-1-3) |

    <a id="table-slot-send-receive-go-markdown-0-1-1"></a>

    **send-receive-go-markdown-0-1-1 details**

    - Ubuntu 18.04 LTS 或以上
    - CentOS 7.0 或以上

    <a id="table-slot-send-receive-go-markdown-0-1-3"></a>

    **send-receive-go-markdown-0-1-3 details**

    - CPU：8 核，1.8 GHz 主频
    - 内存：2 GB 或以上，推荐 4 GB 或以上

- 一个有效的声网账号以及声网项目。请参考[开通服务](/zh-CN/realtime-media/rtc-server-sdk/build/enable-service)从声网控制台获得以下信息：
  - App ID：声网随机生成的字符串，用于识别你的项目。
  - 临时 Token：Token 也称为动态密钥，在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。

## 集成 SDK [#集成-sdk-1]

按照以下步骤集成 SDK：

1. 在终端中，运行以下命令下载 SDK。

   ```bash
   git clone https://github.com/AgoraIO-Extensions/Agora-Golang-Server-SDK.git go_rtc_sdk
   ```

2. 依次运行以下命令编译 SDK。

   :::info[信息]
   如果在运行 `make deps` 命令下载 Go Module 时发生超时，可以尝试先运行 `go env -w GOPROXY=https://goproxy.cn,direct` 命令设置代理，再运行 `make deps` 命令重新下载。
   :::

   ```bash
   # 进入 SDK 目录
   cd go_rtc_sdk
   # 下载 SDK 动态库，该命令会把 SDK 动态库下载到 agora_sdk 目录下
   # 在后续项目调试时，你需要把 agora_sdk 的路径添加进环境变量
   make deps
   # 执行编译
   make build
   ```

3. 在项目的 `go.mod` 文件里面，添加下列行，增加 SDK 所需依赖。

   ```bash
   # 将 /path/to/go_rtc_sdk 修改为你在步骤 1 中下载的 SDK 的实际路径
   replace github.com/AgoraIO-Extensions/Agora-Golang-Server-SDK/v2 => /path/to/go_rtc_sdk
   # 将 version_number 修改为 SDK 实际版本号，你可以在发版说明中获取版本号，例如 v2.1.0
   require github.com/AgoraIO-Extensions/Agora-Golang-Server-SDK/v2 version_number
   ```

## 实现步骤 [#实现步骤-1]

本小节介绍如何实现服务端发送和接收媒体流，你可以按照实现步骤了解核心 API 调用。下图展示了基本的实现流程：

<DocsImage alt="服务端发送和接收媒体流的基本实现流程图" src="https://web-cdn.agora.io/docs-portal/img/rtc-server-sdk/quick-start-sequence.svg" displayWidth="45%" />

### 初始化 SDK [#初始化-sdk-1]

在调用其他声网 API 前，需要先导入 `agoraservice` 包并调用 `initialize` 初始化 `agoraservice` 包：

```go
package main

import (
  agoraservice "github.com/AgoraIO-Extensions/Agora-Golang-Server-SDK/v2/go_sdk/agoraservice"
)

func main() {
    svcCfg := agoraservice.NewAgoraServiceConfig()
    // 如需收发视频，设置 EnableVideo 为 true
    svcCfg.EnableVideo = true
    svcCfg.AppId = appid
    // 全局只初始化一次
    agoraservice.Initialize(svcCfg)
}
```

### 与频道建立连接 [#与频道建立连接-1]

初始化后，按照以下步骤与声网 RTC 频道建立连接：

1. 调用 `NewRtcConnection` 方法创建 `RtcConnection` 对象，用于与声网服务器建立连接。

    :::info[信息]
    你可以在创建连接时设置 `AutoSubscribeAudio: true` 和 `AutoSubscribeVideo: true` 自动订阅音视频数据，也可以在创建连接后，随时调用 `SubscribeAudio` 和 `SubscribeVideo` 方法开始订阅音视频数据。本文以设置 `AutoSubscribeAudio: true` 和 `AutoSubscribeVideo: true` 为例。
    :::

    ```go
    conCfg := agoraservice.RtcConnectionConfig{
        AutoSubscribeAudio: true,
        AutoSubscribeVideo: true,
        ClientRole:         agoraservice.ClientRoleBroadcaster,
        ChannelProfile:     agoraservice.ChannelProfileLiveBroadcasting,
    }
    con := agoraservice.NewRtcConnection(&conCfg)
    ```

2. 调用 `RegisterObserver` 方法注册连接事件观测器，并监听相关回调。

    :::info[信息]
    示例代码中仅处理了连接成功事件，实际开发中，你还需要监听其他事件，并处理连接异常的情况。
    :::

    ```go
    conSignal := make(chan struct{})
    conHandler := &agoraservice.RtcConnectionObserver{
        OnConnected: func(con *agoraservice.RtcConnection, info *agoraservice.RtcConnectionInfo, reason int) {
            fmt.Printf("Connected, reason %d\n", reason)
            conSignal <- struct{}{}
        },
        OnDisconnected: func(con *agoraservice.RtcConnection, info *agoraservice.RtcConnectionInfo, reason int){
            fmt.Printf("Disconnected, reason %d\n", reason)
        },
        OnConnecting: func(con *agoraservice.RtcConnection, conInfo *agoraservice.RtcConnectionInfo, reason int) {
            fmt.Printf("Connecting, reason %d\n", reason)
        },
        OnReconnecting: func(con *agoraservice.RtcConnection, conInfo *agoraservice.RtcConnectionInfo, reason int) {
            fmt.Printf("Reconnecting, reason %d\n", reason)
        },
        OnReconnected: func(con *agoraservice.RtcConnection, conInfo *agoraservice.RtcConnectionInfo, reason int) {
            fmt.Printf("Reconnected, reason %d\n", reason)
        },
        OnConnectionLost: func(con *agoraservice.RtcConnection, conInfo *agoraservice.RtcConnectionInfo) {
            fmt.Printf("Connection lost\n")
        },
        OnConnectionFailure: func(con *agoraservice.RtcConnection, conInfo *agoraservice.RtcConnectionInfo, errCode int) {
            fmt.Printf("Connection failure, error code %d\n", errCode)
        },
        OnUserJoined: func(con *agoraservice.RtcConnection, uid string) {
            fmt.Println("user joined, " + uid)
        },
        OnUserLeft: func(con *agoraservice.RtcConnection, uid string, reason int) {
            fmt.Println("user left, " + uid)
        },
    }
    con.RegisterObserver(conHandler)
    ```

3. 调用 `Connect` 与声网 RTC 频道建立连接，你需要传入以下参数：

   - `token`: RTC Token。如果你的项目开启了 App 证书，你需要传入 RTC Token，详见[获取临时 Token](/zh-CN/realtime-media/rtc-server-sdk/build/enable-service#get-temp-token)；如果你的项目未开启 App 证书，则只需传入空字符串。
   - `channelName`: 你自行定义的频道名。
   - `userId`: 你自行定义的本地用户 ID。

   ```go
   con.Connect(token, channelName, userId)
   // 等待连接成功
   <-conSignal
   ```

### 发送媒体流 [#发送媒体流-1]

成功建立连接后，参考以下步骤向客户端发送媒体流：

1. 调用 `NewMediaNodeFactory` 方法创建 `MediaNodeFactory` 媒体节点工厂对象。

   :::info[信息]
   全局只需创建一个 `MediaNodeFactory` 实例。
   :::

    ```go
    mediaNodeFactory := agoraservice.NewMediaNodeFactory()
    ```

2. 在 `MediaNodeFactory` 对象中，根据实际需求创建音视频数据发送器。支持以下发送器：

    - `AudioPcmDataSender`：发送 PCM 格式的音频数据。
    - `VideoFrameSender`：发送 YUV 格式的视频数据。
    - `AudioEncodedFrameSender`：发送已编码的音频数据。
    - `VideoEncodedImageSender`：发送已编码的视频数据。

    本节以发送 PCM 音频数据和 YUV 视频数据为例，示例代码如下：

    ```go
    // 创建可发送 PCM 格式的音频数据的发送器
    audioSender := mediaNodeFactory.NewAudioPcmDataSender()
    defer audioSender.Release()
    // 创建可发送 YUV 格式的视频数据的发送器
    videoSender := mediaNodeFactory.NewVideoFrameSender()
    defer videoSender.Release()
    ```

3. 使用步骤 2 中创建好的音视频数据发送器，创建 `LocalAudioTrack` 对象和 `LocalVideoTrack` 对象，分别对应本地音频轨道和本地视频轨道。

    ```go
    // 创建自定义音频轨道（使用可发送 PCM 格式的音频数据的发送器）
    audioTrack := agoraservice.NewCustomAudioTrackPcm(audioSender)
    // 如果不需要使用音频轨道，则调用 Release 方法释放资源
    defer audioTrack.Release()
    // 创建自定义视频轨道（可发送 YUV 格式的视频数据的发送器）
    videoTrack := agoraservice.NewCustomVideoTrackFrame(videoSender)
    // 如果不需要使用视频轨道，则调用 Release 方法释放资源
    defer videoTrack.Release()
    ```

4. 调用 `SetEnabled` 方法分别开启本地音频轨道和视频轨道。

   ```go
   // 开启本地音频轨道
   audioTrack.SetEnabled(true)

   // 开启本地视频轨道
   videoTrack.SetEnabled(true)
   ```

5. 调用 `PublishAudio` 和 `PublishVideo` 方法在频道中分别发布本地音频轨道和视频轨道。

   ```go
   // 发布本地音频轨道
   localUser.PublishAudio(audioTrack)

   // 发布本地视频轨道
   localUser.PublishVideo(videoTrack)
   ```

6. 发送音视频数据。
   - 调用 `AudioPcmDataSender` 对象的 `SendAudioPcmData` 方法发送 PCM 音频数据。

    ```go
    frame := agoraservice.AudioFrame{
        Type:              agoraservice.AudioFrameTypePCM16,
        SamplesPerChannel: 160,
        BytesPerSample:    2,
        Channels:          1,
        SamplesPerSec:     16000,
        Buffer:            make([]byte, 320),
        RenderTimeMs:      0,
    }

    file, err := os.Open("./test.pcm")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()

    firstSendTime := time.Now()
    for !(*bStop) {
        shouldSendCount := int(time.Since(firstSendTime).Milliseconds()/10) - (sendCount - 18)
        for i := 0; i < shouldSendCount; i++ {
            dataLen, err := file.Read(frame.Buffer)
            if err != nil || dataLen < 320 {
                fmt.Println("Finished reading file:", err)
                file.Seek(0, 0)
                i--
                continue
            }

            sendCount++
            ret := sender.SendAudioPcmData(&frame)
            fmt.Printf("SendAudioPcmData %d ret: %d\n", sendCount, ret)
        }
        fmt.Printf("Sent %d frames this time\n", shouldSendCount)
        time.Sleep(50 * time.Millisecond)
    }
    ```

   - 调用 `SetVideoEncoderConfiguration` 方法设置视频编码参数，并调用 `SendVideoFrame` 方法发送 YUV 视频数据。

    ```go
    // 设置视频编码分辨率，帧率，码率等参数
    videoTrack.SetVideoEncoderConfiguration(&agoraservice.VideoEncoderConfiguration{
        CodecType:         agoraservice.VideoCodecTypeH264,
        Width:             320,
        Height:            240,
        Framerate:         30,
        Bitrate:           500,
        MinBitrate:        100,
        OrientationMode:   agoraservice.OrientationModeAdaptive,
        DegradePreference: 0,
    })
    w := SendYuvWidth
    h := SendYuvHeight
    dataSize := w * h * 3 / 2
    data := make([]byte, dataSize)
    // 打开 YUV 文件
    file, err := os.Open(SendYuvPath)
    if err != nil {
        fmt.Printf("task %d Error opening file: %s\n", taskCtx.id, err.Error())
        return
    }
    defer file.Close()
    // 循环读取 YUV 文件并发送
    ticker := time.NewTicker(33 * time.Millisecond)
    for {
        select {
        case <-ticker.C:
            dataLen, err := file.Read(data)
            if err != nil || dataLen < dataSize {
                file.Seek(0, 0)
                continue
            }
            yuvSender.SendVideoFrame(&agoraservice.ExternalVideoFrame{
                Type:      agoraservice.VideoBufferRawData,
                Format:    agoraservice.VideoPixelI420,
                Buffer:    data,
                Stride:    w,
                Height:    h,
                Timestamp: 0,
            })
        case <-ctx.Done():
            fmt.Printf("task %d video sender finished\n", taskCtx.id)
            return
        }
    }
    ```

### 接收媒体流 [#接收媒体流-1]

成功建立连接后，根据实际需求，调用 `RegisterAudioFrameObserver` 和 `RegisterVideoFrameObserver` 方法注册以下音视频数据观测器，并监听相关回调。

- `AudioFrameObserver`：音频数据观测器。
- `VideoFrameObserver`：原始视频数据观测器。
- `VideoEncodedFrameObserver`：已编码视频数据观测器。

本节以接收 PCM 音频数据和 YUV 视频数据为例，示例代码如下：

```go
// 当客户端开始发送音频数据时，通过 OnPlaybackAudioFrameBeforeMixing 回调接收 PCM 音频数据
audioObserver := &agoraservice.AudioFrameObserver{
    OnPlaybackAudioFrameBeforeMixing: func(localUser *agoraservice.LocalUser, channelId string, userId string, frame *agoraservice.AudioFrame) bool {
        // 添加处理逻辑
        fmt.Printf("Playback audio frame before mixing, from userId %s\n", userId)
        return true
    },
}
localUser := con.GetLocalUser()
// 在 RegisterAudioFrameObserver 方法之前调用 SetPlaybackAudioFrameBeforeMixingParameters 方法，设置音频数据的采样率
localUser.SetPlaybackAudioFrameBeforeMixingParameters(1, 16000)
// 注册音频数据观测器
localUser.RegisterAudioFrameObserver(audioObserver)
```

```go
// 当客户端开始发送视频数据时，通过 OnFrame 回调接收 YUV 视频数据
videoObserver := &agoraservice.VideoFrameObserver{
    OnFrame: func(channelId string, userId string, frame *agoraservice.VideoFrame) bool {
        // 添加处理逻辑
        fmt.Printf("recv video frame, from channel %s, user %s\n", channelId, userId)
        return true
    },
}
localUser := con.GetLocalUser()
// 注册视频数据观测器
localUser.RegisterVideoFrameObserver(videoObserver)
```

### 停止发送和接收媒体流 [#停止发送和接收媒体流-1]

如果你不再需要发送和接收媒体流，你可以按照以下步骤停止发送和接收媒体流。

#### 停止发送媒体流 [#停止发送媒体流-1]

1. 先调用 `UnpublishAudio` 和 `UnpublishVideo` 方法停止发布音视频轨道

   ```go
    localUser.UnpublishAudio(audioTrack)
    localUser.UnpublishVideo(videoTrack)
   ```

2. 调用 `SetEnabled` 方法分别关闭本地音频轨道和视频轨道。

   ```go
    audioTrack.SetEnabled(false)
    videoTrack.SetEnabled(false)
   ```

3. 依次调用 `Release` 方法释放音视频轨道、音视频发送器、媒体节点工厂的资源。

   ```go
    // 释放音频轨道
    defer audioTrack.Release()
    // 释放音频发送器
    defer audioSender.Release()
    // 释放视频轨道
    defer videoTrack.Release()
    // 释放视频发送器
    defer videoSender.Release()
    // 释放媒体节点工厂
    defer mediaNodeFactory.Release()
   ```

#### 停止接收媒体流 [#停止接收媒体流-1]

调用 `UnregisterAudioFrameObserver` 和 `UnregisterVideoFrameObserver` 方法注销音视频数据观测器。

   ```go
    localUser.UnregisterAudioFrameObserver(audioObserver)
    localUser.UnregisterVideoFrameObserver(videoObserver)
   ```

### 与频道断开连接 [#与频道断开连接-1]

如果你不再需要对频道进行任何操作，或者不再需要接收任何频道的事件，按照以下步骤与频道断开连接：

:::warning[注意]
你必须严格按照步骤释放相关资源。
:::

1. 调用 `Disconnect` 方法断开与声网服务器的连接。

   ```go
   con.Disconnect()
   ```

2. 调用 `UnregisterObserver` 方法注销连接事件观测器。

   ```go
   con.UnregisterObserver()
   ```

3. 调用 `Release` 方法释放 `RTCConnection` 对象。

    ```go
    con.Release()
    ```

### 释放 SDK 资源 [#释放-sdk-资源-1]

当你退出服务端程序时，为避免资源浪费，调用 `Release` 方法释放 `agoraservice` 包，以释放 SDK 的资源。

:::info[信息]
全局只需调用一次 `agoraservice.Release` 释放 SDK 资源。资源释放后，你将无法调用 `agoraservice` 包中的任何方法。
:::

```go
agoraservice.Release()
```

## 调试项目 [#调试项目-1]

按照以下步骤来测试你的项目：

1. 在终端中依次运行下列命令行，编译你的 Go 项目：

    ```bash
    # 进入你的项目目录
    cd /path/to/your/project
    # 清理并更新项目的依赖项
    go mod tidy
    # 编译项目
    go build
    # 设置环境变量，指向 SDK 动态库所在目录
    export LD_LIBRARY_PATH=/path/to/go_rtc_sdk/agora_sdk
    # 运行项目
    ./your_project
    ```

2. 打开声网实时互动 [Web Demo](https://webdemo-global.agora.io/index.html)，完成**初始化设置**，在浏览器中模拟一个客户端。App ID 需和你在项目中配置的 App ID 一致。
3. 进入**基础视频通话**页面，在 **Channel** 填入与步骤 2 相同的频道名，依次点击 **Create Client**、**Join Channel**。
4. 成功加入频道后，你可以开始发流。当频道中有发流用户时，Web Demo 会在 **Step4 Subscribe & Play** 下方的输入框中自动填充发流用户的用户 ID，点击 **Subscribe & Play** 按钮订阅发流用户。

:::warning[注意]
由于 Web Demo 的限制，体验服务端发流时，你需要先在 Web 端完成加入频道，然后再在服务端运行项目发流；体验服务端收流时，你需要先在服务端运行项目连接频道，然后再在 Web 端加入频道、发流。
:::

## 示例项目 [#示例项目-2]

声网提供了开源的服务端示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title="Agora-Golang-Server-SDK" href="https://github.com/AgoraIO-Extensions/Agora-Golang-Server-SDK/tree/release/2.1.0/go_sdk/examples" />
</Cards>
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
