简介:这是一套完整的Android端一对一视频直播社交平台源码,面向移动音视频开发工程师与Android进阶学习者,解决实时音视频通信、社交互动与商业化功能集成等核心问题。资源包含1155个文件,以494个flat资源文件(含UI布局与媒体素材)、138个dex与136个class字节码(构成运行逻辑)、118个json配置(支撑动态模块与接口映射)、68个jar依赖库(涵盖IM、音视频编解码及第三方SDK)及34个xml(定义界面与权限)为主干,整体包体79.14MB。已有450人学习下载,适合开展音视频App二次开发、研究直播业务闭环(如预约、守护、打赏、美颜、轮盘抽奖)及理解社交动态、小视频、私信语音/视频等多模态交互实现。源码结构清晰,含完整_app、_src、_res、_libs模块及_arm64-v8a/_armeabi-v7a原生支持,便于快速构建可商用的一对一视频交友产品原型。

1. 布谷一对一视频直播系统不是“开箱即用”的APP,而是需要深度理解信令流、媒体路径与Android生命周期的可定制直播底座

你下载的 布谷一对一视频直播系统手机视频直播平台android源码.zip ,本质是一套基于WebRTC核心能力封装的Android端点工程,不是成品App安装包。它不依赖任何第三方云服务SDK(如腾讯云TRTC、声网Agora),所有信令协商、ICE候选者交换、音视频编解码控制、SurfaceView/TextureView渲染链路都由Java/Kotlin层与JNI层协同实现。这意味着: 你能完全掌控从摄像头采集到远端画面显示的每一帧延迟、每一路带宽分配、每一次重连策略 ——但也意味着,直接在Android Studio里点击Run,大概率会卡在“连接信令服务器失败”或“本地预览黑屏”。这套源码真正价值在于:为教育陪练、远程医疗问诊、私密社交等强实时、低延迟、高隐私要求的一对一场景,提供可审计、可裁剪、可与自有用户体系深度集成的客户端基座。适合已有信令服务(WebSocket或HTTP长轮询)、熟悉Android音视频开发流程、且明确拒绝将用户音视频流经由第三方公有云中转的团队。新手若仅想快速上线直播功能,应优先评估成熟SDK;而本源码面向的是需要把“一对一”这个业务语义,从协议栈底层就固化进客户端行为逻辑的工程师。

2. 解析布谷源码结构:从AndroidManifest.xml定位入口,用Gradle依赖锁定WebRTC版本

2.1 源码目录层级与关键模块映射关系

解压后典型结构如下(非标准Android Studio项目结构,需手动识别):

├── app/                    # 主模块,含Activity、Fragment、核心Service
│   ├── src/main/
│   │   ├── AndroidManifest.xml     ← 关键!声明Camera、AudioRecord、Internet权限及主Activity
│   │   ├── java/com/bugu/         ← 核心包名,注意命名空间一致性
│   │   │   ├── activity/          ← MainActivity、CallActivity(主呼叫界面)
│   │   │   ├── service/           ← CallService(后台音视频通道管理)
│   │   │   ├── webrtc/            ← WebRTC封装层(PeerConnectionFactory、MediaStreamHandler)
│   │   │   └── util/              ← 自定义工具类(LogUtil、NetworkUtil)
│   │   └── res/                   ← 布局文件集中在layout/call_activity.xml等
├── libs/                     # 静态库目录,重点看libwebrtc.a或webrtc.jar
├── jniLibs/                  # ARM64-v8a、armeabi-v7a等ABI目录,含.so文件
└── build.gradle              # 重点关注dependencies和android.ndk配置

提示 :该工程大概率使用预编译WebRTC Native库( .so ),而非从源码编译。 jniLibs/ 下的 libjingle_peerconnection_so.so 是核心动态库,其ABI必须与目标设备匹配。若运行时报 java.lang.UnsatisfiedLinkError ,首要检查设备架构( adb shell getprop ro.product.cpu.abi )与 jniLibs 中是否存在对应目录。

2.2 Gradle配置中的WebRTC版本与NDK约束

打开 app/build.gradle ,关键配置段落如下:

android {
    compileSdkVersion 33
    defaultConfig {
        applicationId "com.bugu.live"
        minSdkVersion 21      // 注意:WebRTC官方要求minSdkVersion >= 21
        targetSdkVersion 33
        versionCode 1
        versionName "1.0"
        // NDK配置决定.so加载路径
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'  // 必须与jniLibs目录严格一致
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])  // 加载libs/webrtc.jar
    // 若使用AAR方式,此处应为implementation(name: 'webrtc', ext: 'aar')
}
WebRTC版本识别方法(无源码时):
  1. 查看 libs/webrtc.jar 的 META-INF/MANIFEST.MF ,搜索 Implementation-Version 字段;
  2. 或反编译 webrtc.jar ,检查 org/webrtc/BuildConfig.class 中的 VERSION 常量;
  3. 常见版本: 110.x (2023年主流)、 109.x (2022年末)。 版本差异直接影响API调用方式 ,例如 PeerConnection.IceServer 构造在110+需传入 Arrays.asList() ,而旧版支持单个对象。

2.3 AndroidManifest.xml中的权限与组件声明

必须确保以下权限已声明(缺失将导致采集失败):

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Android 12+需额外声明 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

关键Activity声明示例:

<activity
    android:name=".activity.CallActivity"
    android:exported="true"
    android:configChanges="orientation|screenSize|keyboardHidden"
    android:screenOrientation="portrait"  <!-- 一对一场景通常锁定竖屏 -->
    android:theme="@style/Theme.AppCompat.NoActionBar" />

注意 : android:exported="true" 是Android 12+强制要求,若未设置, startActivity() 将抛出 SecurityException 。同时 configChanges 属性必须包含 orientation ,否则屏幕旋转会销毁Activity并重建,导致WebRTC连接中断。

3. 信令层对接:用OkHttp实现WebSocket握手,解析布谷自定义JSON协议

3.1 信令协议逆向分析:从CallActivity切入网络请求

在 CallActivity.java 中搜索 WebSocket 或 OkHttpClient ,典型初始化代码如下:

private void initSignaling() {
    String wsUrl = "wss://signaling.bugu.com/ws?room_id=" + roomId + "&user_id=" + userId;
    OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();
    WebSocket webSocket = client.newWebSocket(
            new Request.Builder().url(wsUrl).build(),
            new CustomWebSocketListener()); // 自定义监听器
}
布谷信令JSON消息格式(实测常见字段):
方向 类型 示例JSON 说明
Client → Server offer {"type":"offer","sdp":"v=0\r\no=- ...","roomId":"abc123","from":"u1001","to":"u1002"} SDP Offer字符串需Base64编码?需验证源码中 JsonUtils.toJson() 是否启用编码
Server → Client answer {"type":"answer","sdp":"v=0\r\no=- ...","roomId":"abc123","from":"u1002","to":"u1001"} 收到answer后必须调用 peerConnection.setRemoteDescription()
Client → Server candidate {"type":"candidate","candidate":"candidate:...","sdpMid":"0","sdpMLineIndex":0,"roomId":"abc123"} ICE候选者需逐条发送,不可合并

提示 : sdpMid 和 sdpMLineIndex 必须与Offer/Answer中的媒体轨道索引严格对应,否则 addIceCandidate() 失败。布谷源码中 MediaStreamHandler.java 通常负责解析这些字段。

3.2 WebSocket连接状态机与重连策略

CustomWebSocketListener 需覆盖关键方法:

@Override
public void onOpen(WebSocket webSocket, Response response) {
    Log.d("Signaling", "WebSocket connected");
    // 发送登录认证消息(如有token)
    sendMessage("{\"type\":\"login\",\"token\":\"xxx\"}");
}

@Override
public void onMessage(WebSocket webSocket, String text) {
    try {
        JSONObject json = new JSONObject(text);
        String type = json.optString("type");
        switch (type) {
            case "offer":
                handleOffer(json); // 触发createAnswer()
                break;
            case "answer":
                handleAnswer(json); // 调用setRemoteDescription()
                break;
            case "candidate":
                handleCandidate(json); // 调用addIceCandidate()
                break;
            case "hangup":
                endCall(); // 主动关闭PeerConnection
                break;
        }
    } catch (JSONException e) {
        Log.e("Signaling", "Parse error", e);
    }
}

@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
    Log.e("Signaling", "WebSocket failed", t);
    // 实现指数退避重连:第一次1s,第二次2s,第三次4s...
    scheduleReconnect(++reconnectCount);
}
重连参数建议表:
参数 推荐值 说明
初始延迟 1000ms 避免瞬时网络抖动触发重连
最大重试次数 5次 超过则提示“网络异常,请检查Wi-Fi/移动数据”
退避因子 2.0 第n次重连延迟 = 初始延迟 × (2^(n-1))
连接超时 ≤10s 长于10s的握手视为失败,避免阻塞UI线程

4. WebRTC媒体管道调试:用logcat过滤关键标签,定位采集/渲染失败点

4.1 启用WebRTC原生日志并定向捕获

在 CallActivity.onCreate() 中插入:

// 必须在创建PeerConnectionFactory前调用
PeerConnectionFactory.InitializationOptions options =
        PeerConnectionFactory.InitializationOptions.builder(this)
                .setEnableInternalTracer(true)  // 启用内部追踪
                .createInitializationOptions();
PeerConnectionFactory.initialize(options);

// 设置日志级别(DEBUG级输出最详细SDP/ICE信息)
Log.e("WebRTC", "WebRTC logging enabled at DEBUG level");
// 注意:实际日志需通过logcat -v threadtime -s org.webrtc:V 过滤
关键logcat过滤命令(终端执行):
# 捕获所有WebRTC相关日志(含Native层)
adb logcat -v threadtime -s org.webrtc:V RTCEventLog:V

# 仅查看ICE连接状态变化
adb logcat -v threadtime | grep -E "(ICE|ice|candidate|connect|disconnected)"

# 监控摄像头采集帧率
adb logcat -v threadtime | grep -E "(CameraCapturer|VideoCapturer)"

4.2 常见失败场景与精准定位指令

场景1:本地预览黑屏,但远端能听到声音

排查路径 :

  1. 执行 adb shell dumpsys media.camera 确认摄像头硬件可用;
  2. 在logcat中搜索 CameraCapturer ,确认是否输出 onFrameCaptured 日志;
  3. 若无此日志,检查 CameraHelper.java 中 openCamera() 是否成功,重点看 cameraManager.openCamera() 回调;
  4. 若有 onFrameCaptured 但SurfaceView无画面,检查 SurfaceView.getHolder().getSurface() 是否为空,或 videoTrack.addSink(videoSink) 是否被调用。
场景2:远端收不到视频,仅音频通

关键日志线索 :

  • 搜索 VideoEncoder :若无 encode 日志,说明编码器未启动;
  • 搜索 RTP :若无 send RTP packet ,说明网络层未发送;
  • 检查 PeerConnection 创建时 MediaConstraints 是否禁用了视频:
    MediaConstraints pcConstraints = new MediaConstraints();
    pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")); // 必须为true
    
场景3:ICE连接长时间停留在 checking 状态

诊断步骤 :

  1. 在logcat中提取完整ICE candidate列表:
    adb logcat | grep -A 5 -B 5 "candidate:"
    
  2. 检查候选者类型:若全为 host (本地IP),说明STUN/TURN未生效;
  3. 验证STUN服务器地址:在 PeerConnectionFactory.Options 中确认 stunServers 数组非空;
  4. 使用 curl 测试STUN连通性:
    curl -v "stun:stun.l.google.com:19302"  # 应返回400 Bad Request(正常)
    

5. 性能优化与生产环境适配:动态码率控制、后台保活与Android 12+适配

5.1 基于CPU负载的动态视频码率调节

布谷源码默认使用固定码率(如1200kbps),但在弱网下易卡顿。需在 MediaStreamHandler.java 中注入动态调节逻辑:

// 在PeerConnection创建后,获取VideoSender
RtpSender videoSender = peerConnection.getSenders().get(0);
VideoTrack videoTrack = (VideoTrack) videoSender.track();

// 创建自定义BitrateAdjuster
BitrateAdjuster adjuster = new CpuLoadBitrateAdjuster();
videoTrack.setBitrateAdjuster(adjuster);

// CpuLoadBitrateAdjuster核心逻辑
public class CpuLoadBitrateAdjuster implements BitrateAdjuster {
    private static final int MIN_BITRATE_KBPS = 400;
    private static final int MAX_BITRATE_KBPS = 2000;
    
    @Override
    public int getSuggestedBitrateBps() {
        float cpuLoad = getCpuLoad(); // 通过/proc/stat计算
        if (cpuLoad > 0.8f) {
            return MIN_BITRATE_KBPS * 1000; // 降为最低码率
        } else if (cpuLoad < 0.3f) {
            return MAX_BITRATE_KBPS * 1000; // 提升至最高码率
        }
        return (int) ((MAX_BITRATE_KBPS - MIN_BITRATE_KBPS) * cpuLoad + MIN_BITRATE_KBPS) * 1000;
    }
}

注意 : getCpuLoad() 需读取 /proc/stat 中 cpu 行,计算1秒内idle时间占比。此方案比单纯依赖网络质量反馈(如RTCP REMB)更及时,尤其适用于低端Android设备。

5.2 Android 12+后台限制下的音视频保活方案

当用户切换到其他App时, CallService 可能被系统杀死。解决方案:

方案A:前台Service + Notification(推荐)
// 在CallService.onStartCommand()中
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    startForeground(1, createNotification()); // 必须提供Notification
} else {
    startForeground(1, null); // 低版本兼容
}

private Notification createNotification() {
    Intent intent = new Intent(this, CallActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(
            this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
    
    return new NotificationCompat.Builder(this, "call_channel")
            .setContentTitle("布谷直播进行中")
            .setContentText("点击返回通话界面")
            .setSmallIcon(R.drawable.ic_call)
            .setContentIntent(pendingIntent)
            .build();
}
方案B:WorkManager兜底重连(网络恢复后)
// 当检测到网络断开时触发
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(ReconnectWorker.class)
        .setInputData(new Data.Builder()
                .putString("room_id", roomId)
                .putString("user_id", userId)
                .build())
        .setConstraints(new Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build())
        .build();
WorkManager.getInstance(this).enqueue(work);

5.3 屏幕共享与摄像头切换的线程安全处理

在 CallActivity 中切换摄像头时,必须确保在 VideoCapturer 线程执行:

// 错误:直接在主线程调用
cameraCapturer.switchCamera(); // 可能崩溃

// 正确:投递到Capturer线程
cameraCapturer.invokeMethod(new Runnable() {
    @Override
    public void run() {
        cameraCapturer.switchCamera();
    }
});

同时,屏幕共享需申请 MediaProjection 权限,并在 onActivityResult() 中获取 virtualDisplay :

// 创建VirtualDisplay时指定尺寸与密度
virtualDisplay = mediaProjection.createVirtualDisplay(
        "screen-capture",
        screenWidth, screenHeight, density,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        virtualSurface, null, null);

关键参数说明 : density 必须与当前DisplayMetrics.density匹配,否则画面拉伸; VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 确保横竖屏自动适配,避免手动旋转Surface导致性能下降。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐