从零搭建 Android 语音聊天室:基于 ZEGO SDK 的 8 麦位实时语音方案

2026/06/22

本文是面向 Android 开发者的实战教程,手把手教你用 ZEGO Express Audio SDK + ZIM SDK 构建一个支持 8 麦位管理、多人实时语音的语音聊天室应用。全部核心逻辑仅用两个 Java 文件实现,代码清晰易读,适合快速上手。

1. 项目介绍

语音聊天室是当下社交、游戏、音乐等场景中非常热门的功能。本项目实现了一个完整的 Android 语音聊天室 Demo,核心特性包括:

  • 8 麦位管理:2×4 网格布局,支持上麦、下麦操作
  • 多人实时语音:基于 ZEGO Express Audio SDK,实现低延迟语音通话
  • 房主特权:踢人、全员静音等管理功能
  • 麦位状态实时同步:通过 ZIM 房间属性实现多端麦位状态同步
  • Token 鉴权:基于 Node.js 服务端生成 Token04,确保通信安全

项目采用 单文件实现 原则,所有业务逻辑集中在两个 Activity 中,没有额外的 Manager/Service 层,代码直白易懂:

MainActivity.java    → 登录界面 + 房间入口
RoomActivity.java    → 语音聊天室(麦位 + 语音 + 管理)

2. 技术架构

┌──────────────────┐        ┌──────────────────┐        ┌──────────────────┐
│  Android Client   │◄──────►│   Node.js Server  │        │   ZEGO Cloud     │
│  (Express Audio   │  HTTP  │  (Express.js)     │◄──────►│   - RTC Engine   │
│   + ZIM SDK)      │────────│  - Token Gen      │        │   - ZIM Service  │
└──────────────────┘  ZIM   └──────────────────┘        └──────────────────┘

关键设计决策

决策说明
Express Audio SDK仅传输语音,不涉及视频,降低性能消耗
ZIM SDK负责房间信令和麦位状态同步,通过房间属性实现
房间属性(Room Attributes)存储麦位状态:seat_0 ~ seat_7,值为 JSON 格式的用户信息
Token 服务端生成使用 Token04 鉴权机制,保障 ZIM 和 RTC 通信安全

数据流

用户操作 → ZIM 房间属性变更 → 其他端收到 onRoomAttributesUpdated 回调 → UI 更新
用户上麦 → ZIM setRoomAttributes + RTC startPublishingStream → 远端 startPlayingStream
用户下麦 → ZIM deleteRoomAttributes + RTC stopPublishingStream → 远端 stopPlayingStream

3. 环境准备

前置条件

  • Android Studio Hedgehog 或更高版本
  • Android 5.0(API 24)及以上设备或模拟器
  • Node.js 8+ (用于 Token 服务端)
  • 注册ZEGO 控制台账号,获取 AppID 和 ServerSecret

SDK 依赖

项目使用以下 SDK 版本:

// build.gradle.kts
implementation("im.zego:zim:2.29.0")           // ZIM 信令 SDK
implementation("com.zegocloud:express-audio:3.24.0")  // Express Audio 语音 SDK

服务端配置

cd examples/server
cp .env.example .env
# 编辑 .env 文件,填入你的 ZEGO AppID 和 ServerSecret
# APP_ID=你的AppID
# SERVER_SECRET=你的ServerSecret
npm install
npm start
# 服务运行在 http://localhost:3000

Android 客户端配置

cd examples/android
cp local.properties.example local.properties
# 编辑 local.properties:
# ZEGO_APP_ID=你的AppID
# ZEGO_API_BASE_URL=http://10.0.2.2:3000  (模拟器)
# 或 http://你的电脑IP:3000  (真机)
./gradlew assembleDebug

4. 核心代码解析

4.1 SDK 初始化与用户登录

登录流程分为三步:获取 Token → 初始化 ZIM → 创建 Express Audio 引擎。

第一步:从服务端获取 Token

// MainActivity.java - getTokenFromServer()
private void getTokenFromServer(String userId, Runnable onSuccess) {
    String url = com.zego.voicechatroom.BuildConfig.ZEGO_API_BASE_URL + "/api/token";

    JSONObject body = new JSONObject();
    body.put("userId", userId);

    JsonObjectRequest request = new JsonObjectRequest(
            Request.Method.POST, url, body,
            response -> {
                String token = response.optString("token", "");
                if (!TextUtils.isEmpty(token)) {
                    currentToken = token;
                    onSuccess.run();
                }
            },
            error -> {
                Toast.makeText(this, "Failed to get token: " + error.getMessage(), Toast.LENGTH_LONG).show();
            }
    );
    requestQueue.add(request);
}

第二步:初始化 ZIM SDK 并登录

// MainActivity.java - initZIMAndLogin()
private void initZIMAndLogin() {
    // 创建 ZIM 实例(全局单例)
    ZIM.create(BuildConfig.ZEGO_APP_ID, getApplicationContext());

    // 配置登录信息
    ZIMLoginConfig config = new ZIMLoginConfig();
    config.userName = currentUserName;
    config.token = currentToken;

    // 异步登录
    ZIM.getInstance().login(currentUserId, config, new ZIMLoggedInCallback() {
        @Override
        public void onLoggedIn(ZIMError error) {
            runOnUiThread(() -> {
                if (error.code == ZIMErrorCode.SUCCESS) {
                    createExpressEngine();  // 登录成功后创建 RTC 引擎
                    // 切换到房间入口界面
                    loginSection.setVisibility(View.GONE);
                    roomEntrySection.setVisibility(View.VISIBLE);
                } else {
                    Toast.makeText(MainActivity.this,
                        "ZIM login failed: " + error.message, Toast.LENGTH_LONG).show();
                }
            });
        }
    });
}

第三步:创建 Express Audio 引擎

// MainActivity.java - createExpressEngine()
private void createExpressEngine() {
    ZegoEngineProfile profile = new ZegoEngineProfile();
    profile.appID = BuildConfig.ZEGO_APP_ID;
    profile.scenario = ZegoScenario.DEFAULT;
    profile.application = getApplication();
    ZegoExpressEngine.createEngine(profile, null);
}

4.2 房间创建与加入

房主创建房间、普通成员加入房间使用不同的 ZIM API:

房主创建房间

// RoomActivity.java - createRoom()
private void createRoom() {
    ZIMRoomInfo roomInfo = new ZIMRoomInfo();
    roomInfo.roomID = roomId;
    roomInfo.roomName = "VoiceChat_" + roomId;

    ZIM.getInstance().createRoom(roomInfo, new ZIMRoomCreatedCallback() {
        @Override
        public void onRoomCreated(ZIMRoomFullInfo zimRoomFullInfo, ZIMError error) {
            runOnUiThread(() -> {
                if (error.code == ZIMErrorCode.SUCCESS) {
                    addMessage("Room created: " + roomId);
                    loginRTCRoom();   // 登录 RTC 房间
                    takeSeat(0);      // 房主自动占据 0 号麦位
                } else {
                    Toast.makeText(RoomActivity.this,
                        "Create room failed: " + error.message, Toast.LENGTH_LONG).show();
                    finish();
                }
            });
        }
    });
}

普通成员加入房间

// RoomActivity.java - joinRoom()
private void joinRoom() {
    ZIM.getInstance().joinRoom(roomId, new ZIMRoomJoinedCallback() {
        @Override
        public void onRoomJoined(ZIMRoomFullInfo zimRoomFullInfo, ZIMError error) {
            runOnUiThread(() -> {
                if (error.code == ZIMErrorCode.SUCCESS) {
                    addMessage("Joined room: " + roomId);
                    loginRTCRoom();   // 登录 RTC 房间
                    querySeats();     // 查询已有麦位状态
                } else {
                    Toast.makeText(RoomActivity.this,
                        "Join room failed: " + error.message, Toast.LENGTH_LONG).show();
                    finish();
                }
            });
        }
    });
}

RTC 房间登录

// RoomActivity.java - loginRTCRoom()
private void loginRTCRoom() {
    ZegoExpressEngine engine = ZegoExpressEngine.getEngine();
    if (engine == null) return;

    ZegoUser user = new ZegoUser(userId, userName);
    ZegoRoomConfig roomConfig = new ZegoRoomConfig();
    roomConfig.token = token;
    roomConfig.isUserStatusNotify = true;

    engine.loginRoom(roomId, user, roomConfig, (int error, JSONObject extendedData) -> {
        runOnUiThread(() -> {
            if (error == 0) {
                addMessage("RTC login success");
            } else {
                addMessage("RTC login failed: " + error);
            }
        });
    });
}

4.3 麦位管理(上麦 / 下麦)

麦位状态通过 ZIM 房间属性存储,每个麦位对应一个 key-value 对:

KeyValue(已占用)Value(空闲)
seat_0{"userID":"host1","userName":"Host","isMuted":false}""
seat_1{"userID":"user2","userName":"Alice","isMuted":false}""
""

上麦操作

// RoomActivity.java - takeSeat()
private void takeSeat(int seatIndex) {
    if (mySeatIndex >= 0) {
        Toast.makeText(this, "You are already on seat " + mySeatIndex, Toast.LENGTH_SHORT).show();
        return;
    }
    if (seats[seatIndex] != null) {
        Toast.makeText(this, "Seat " + seatIndex + " is occupied", Toast.LENGTH_SHORT).show();
        return;
    }

    SeatInfo seatInfo = new SeatInfo(userId, userName, false);
    String jsonValue = gson.toJson(seatInfo);

    HashMap attrs = new HashMap<>();
    attrs.put(SEAT_KEY_PREFIX + seatIndex, jsonValue);

    ZIMRoomAttributesSetConfig config = new ZIMRoomAttributesSetConfig();
    config.isForce = false;
    config.isDeleteAfterOwnerLeft = true;

    ZIM.getInstance().setRoomAttributes(attrs, roomId, config, new ZIMRoomAttributesOperatedCallback() {
        @Override
        public void onRoomAttributesOperated(String roomID, ArrayList errorKeys, ZIMError errorInfo) {
            runOnUiThread(() -> {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    mySeatIndex = seatIndex;
                    seats[seatIndex] = seatInfo;
                    updateSeatUI(seatIndex);
                    updateControlButtons();
                    startPublishing();  // 开始推音频流
                    addMessage("You took seat " + seatIndex);
                }
            });
        }
    });
}

下麦操作

// RoomActivity.java - removeSeat()
private void removeSeat(int seatIndex) {
    ArrayList keys = new ArrayList<>();
    keys.add(SEAT_KEY_PREFIX + seatIndex);

    ZIMRoomAttributesDeleteConfig config = new ZIMRoomAttributesDeleteConfig();
    config.isForce = true;

    ZIM.getInstance().deleteRoomAttributes(keys, roomId, config, new ZIMRoomAttributesOperatedCallback() {
        @Override
        public void onRoomAttributesOperated(String roomID, ArrayList errorKeys, ZIMError errorInfo) {
            runOnUiThread(() -> {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    if (seatIndex == mySeatIndex) {
                        stopPublishing();  // 停止推流
                        mySeatIndex = -1;
                    }
                    seats[seatIndex] = null;
                    updateSeatUI(seatIndex);
                    updateControlButtons();
                    addMessage("Seat " + seatIndex + " is now vacant");
                }
            });
        }
    });
}

4.4 实时语音推拉流

上麦时开始推送音频流,远端自动订阅并播放:

推流(Publishing)

// RoomActivity.java - startPublishing()
private void startPublishing() {
    ZegoExpressEngine engine = ZegoExpressEngine.getEngine();
    if (engine != null) {
        String streamID = "stream_" + userId;
        engine.startPublishingStream(streamID);
        addMessage("Publishing stream: " + streamID);
    }
}

自动订阅远端流

// RoomActivity.java - setupRTCEventHandler()
engine.setEventHandler(new IZegoEventHandler() {
    @Override
    public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType,
                                   ArrayList streamList, JSONObject extendedData) {
        runOnUiThread(() -> {
            if (updateType == ZegoUpdateType.ADD) {
                for (ZegoStream stream : streamList) {
                    addMessage("New stream: " + stream.streamID);
                    engine.startPlayingStream(stream.streamID);  // 自动拉流
                }
            } else if (updateType == ZegoUpdateType.DELETE) {
                for (ZegoStream stream : streamList) {
                    addMessage("Stream removed: " + stream.streamID);
                    engine.stopPlayingStream(stream.streamID);
                }
            }
        });
    }
});

4.5 麦克风开关控制

麦克风控制同时更新本地 RTC 状态和 ZIM 房间属性,确保多端 UI 同步:

// RoomActivity.java - toggleMic()
private void toggleMic() {
    isMicMuted = !isMicMuted;

    // 1. 更新 RTC 引擎的麦克风状态
    ZegoExpressEngine engine = ZegoExpressEngine.getEngine();
    if (engine != null) {
        engine.muteMicrophone(isMicMuted);
    }

    // 2. 更新按钮文本
    btnMute.setText(isMicMuted ? "Unmute" : "Mute");

    // 3. 同步麦位属性(通知其他端)
    if (mySeatIndex >= 0) {
        updateMySeatMicStatus();
    }
}

// 更新麦位属性中的静音状态
private void updateMySeatMicStatus() {
    if (mySeatIndex < 0 || seats[mySeatIndex] == null) return;

    seats[mySeatIndex].isMuted = isMicMuted;
    String jsonValue = gson.toJson(seats[mySeatIndex]);

    HashMap attrs = new HashMap<>();
    attrs.put(SEAT_KEY_PREFIX + mySeatIndex, jsonValue);

    ZIMRoomAttributesSetConfig config = new ZIMRoomAttributesSetConfig();
    config.isForce = false;

    ZIM.getInstance().setRoomAttributes(attrs, roomId, config, null);
}

4.6 房主管理(踢人 / 全员静音)

踢人:点击麦位即可踢出用户

// RoomActivity.java - kickUserFromSeat()
private void kickUserFromSeat(int seatIndex) {
    if (!isHost) return;
    if (seats[seatIndex] == null) return;

    String kickedUser = seats[seatIndex].userName;
    removeSeat(seatIndex);  // 复用下麦逻辑
    addMessage("Host kicked " + kickedUser + " from seat " + seatIndex);
}

全员静音:使用批量操作 API

// RoomActivity.java - muteAllSeats()
private void muteAllSeats() {
    if (!isHost) return;

    // 开启批量操作(所有 setRoomAttributes 会在 endBatch 后一次性回调)
    ZIMRoomAttributesBatchOperationConfig batchConfig = new ZIMRoomAttributesBatchOperationConfig();
    batchConfig.isForce = true;
    ZIM.getInstance().beginRoomAttributesBatchOperation(roomId, batchConfig);

    // 批量更新所有已占用的麦位
    for (int i = 0; i < SEAT_COUNT; i++) {
        if (seats[i] != null) {
            seats[i].isMuted = true;
            String jsonValue = gson.toJson(seats[i]);
            HashMap attrs = new HashMap<>();
            attrs.put(SEAT_KEY_PREFIX + i, jsonValue);
            ZIMRoomAttributesSetConfig config = new ZIMRoomAttributesSetConfig();
            config.isForce = true;
            ZIM.getInstance().setRoomAttributes(attrs, roomId, config, null);
        }
    }

    // 结束批量操作
    ZIM.getInstance().endRoomAttributesBatchOperation(roomId, new ZIMRoomAttributesBatchOperatedCallback() {
        @Override
        public void onRoomAttributesBatchOperated(String roomID, ZIMError errorInfo) {
            runOnUiThread(() -> {
                if (errorInfo.code == ZIMErrorCode.SUCCESS) {
                    isMicMuted = true;
                    ZegoExpressEngine engine = ZegoExpressEngine.getEngine();
                    if (engine != null) {
                        engine.muteMicrophone(true);
                    }
                    btnMute.setText("Unmute");
                    for (int i = 0; i < SEAT_COUNT; i++) {
                        updateSeatUI(i);
                    }
                    addMessage("Host muted all microphones");
                }
            });
        }
    });
}

5. 功能实现详解

5.1 房间生命周期

完整的房间生命周期如下:

创建/加入房间 → 登录 RTC 房间 → 查询/设置麦位 → 推拉流 → 离开房间

退出房间时需要清理所有资源:

// RoomActivity.java - leaveRoom()
private void leaveRoom() {
    // 1. 释放麦位
    if (mySeatIndex >= 0) {
        ArrayList keys = new ArrayList<>();
        keys.add(SEAT_KEY_PREFIX + mySeatIndex);
        ZIMRoomAttributesDeleteConfig config = new ZIMRoomAttributesDeleteConfig();
        config.isForce = true;
        ZIM.getInstance().deleteRoomAttributes(keys, roomId, config, null);
        stopPublishing();
    }

    // 2. 登出 RTC 房间
    ZegoExpressEngine engine = ZegoExpressEngine.getEngine();
    if (engine != null) {
        engine.logoutRoom(roomId);
    }

    // 3. 离开 ZIM 房间
    ZIM.getInstance().leaveRoom(roomId, new ZIMRoomLeftCallback() {
        @Override
        public void onRoomLeft(String roomID, ZIMError errorInfo) {
            runOnUiThread(() -> finish());
        }
    });
}

5.2 麦位状态同步机制

麦位状态通过 ZIM 房间属性实现实时多端同步:

  1. 写入方:调用 setRoomAttributes()deleteRoomAttributes() 修改麦位
  2. 接收方:通过 onRoomAttributesUpdated() 回调获取变更
  3. 新加入者:调用 queryRoomAllAttributes() 获取完整快照
// RoomActivity.java - 麦位属性变更监听
ZIM.getInstance().setEventHandler(new ZIMEventHandler() {
    @Override
    public void onRoomAttributesUpdated(ZIM zim, ZIMRoomAttributesUpdateInfo info, String roomID) {
        runOnUiThread(() -> {
            HashMap roomAttributes = info.roomAttributes;
            for (Map.Entry entry : roomAttributes.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();

                if (key != null && key.startsWith(SEAT_KEY_PREFIX)) {
                    int seatIndex = Integer.parseInt(key.substring(SEAT_KEY_PREFIX.length()));
                    if (value == null || value.isEmpty()) {
                        seats[seatIndex] = null;      // 麦位变为空闲
                    } else {
                        seats[seatIndex] = gson.fromJson(value, SeatInfo.class);  // 有人上麦
                    }
                    updateSeatUI(seatIndex);
                }
            }
        });
    }
});

5.3 语音流自动订阅

当有新的远端流加入时,自动开始拉流播放;当流被移除时,自动停止拉流:

// RTC 流变更回调
@Override
public void onRoomStreamUpdate(String roomID, ZegoUpdateType updateType,
                               ArrayList streamList, JSONObject extendedData) {
    runOnUiThread(() -> {
        if (updateType == ZegoUpdateType.ADD) {
            for (ZegoStream stream : streamList) {
                engine.startPlayingStream(stream.streamID);  // 新流 → 开始拉流
            }
        } else if (updateType == ZegoUpdateType.DELETE) {
            for (ZegoStream stream : streamList) {
                engine.stopPlayingStream(stream.streamID);   // 流消失 → 停止拉流
            }
        }
    });
}

6. 运行说明

启动 Token 服务端

cd examples/server
npm install
npm start
# 服务运行在 http://localhost:3000
# 健康检查:curl http://localhost:3000/api/health

构建并安装 Android 客户端

cd examples/android
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk

测试流程

  1. 设备 A(房主):输入 User ID 和 User Name → 点击 Login → 点击 Create Room
  2. 设备 B(成员):输入不同的 User ID → 点击 Login → 输入相同的 Room ID → 点击 Join Room
  3. 设备 B 点击 Take Seat 加入麦位
  4. 双方即可进行实时语音通话

模拟器测试时,服务端地址使用 http://10.0.2.2:3000(模拟器访问宿主机的特殊地址)。

运行后效果示例图:

7. 总结与扩展

项目核心要点

要点实现方式
实时语音ZEGO Express Audio SDK 的推拉流机制
信令同步ZIM SDK 的房间属性(Room Attributes)
麦位管理seat_0 ~ seat_7 的 JSON 属性存储
鉴权安全Node.js 服务端生成 Token04
UI 布局GridLayout 实现 2×4 麦位网格

ZIM SDK API 速查

API用途
ZIM.create(appID, application)创建 ZIM 实例
zim.login(userID, config)使用 Token 登录
zim.createRoom(roomInfo)创建房间
zim.joinRoom(roomID)加入房间
zim.leaveRoom(roomID)离开房间
zim.setRoomAttributes(attrs, roomID, config)设置麦位属性
zim.deleteRoomAttributes(keys, roomID, config)清除麦位
zim.queryRoomAllAttributes(roomID)查询所有麦位
zim.beginRoomAttributesBatchOperation(roomID, config)开始批量操作
zim.endRoomAttributesBatchOperation(roomID)结束批量操作

Express Audio SDK API 速查

API用途
ZegoExpressEngine.createEngine(profile)创建 RTC 引擎
engine.loginRoom(roomID, user, config)登录 RTC 房间
engine.logoutRoom(roomID)登出 RTC 房间
engine.startPublishingStream(streamID)发布音频流
engine.stopPublishingStream()停止发布
engine.startPlayingStream(streamID)播放远端流
engine.stopPlayingStream(streamID)停止播放
engine.muteMicrophone(mute)静音/取消静音麦克风

扩展方向

  • 麦位申请/审批机制:使用 ZIM 自定义信令实现举手排队
  • 背景音乐混音:利用 Express SDK 的混音 API
  • 音效/变声:利用 Express SDK 的音频处理能力
  • 聊天消息:利用 ZIM 的房间消息功能
  • 房间列表:利用 ZIM 的房间查询 API
最新文章
从零搭建 Android 语音聊天室:基于 ZEGO SDK 的 8 麦位实时语音方案
2026/06/22
中国国际金融展|即构助力金融行业构建新一代实时音视频互动能力
2026/06/18
如何凭借卓越的 RTC 丢包和抖动处理能力实现全球规模扩展
2026/06/17
如何构建一个真正具备可扩展性的直播平台
2026/06/15
2026世界杯倒计时!超低延迟+4K高清,ZEGO「赛事直播方案」让球迷不错过绝杀瞬间
2026/06/11
扫一扫,获取更多服务与支持
关注我们
获得更多服务与支持了解价格与优惠 扫码关注我们
关注我们
获得更多服务与支持了解价格与优惠 扫码关注我们