1. 从一块黑屏开始:为什么uniapp的摄像头推流总让人想摔键盘

接到一个App项目,要求用手机摄像头把画面实时推到服务器,给用户做直播预览。第一反应是这有什么难的?开发工具打开live-pusher,填个推流地址,不就能跑了吗?结果真落地的时候,黑屏、闪退、音画不同步、权限弹窗不出现、安卓苹果行为不一致……我在这个组件上踩的坑,比在业务代码里加班一个月还多。

先说结论: live-pusher 确实是uni-app官方提供的直播推流组件,在App端和微信小程序端都可用,但它在App端的表现和开发习惯跟普通view组件完全不一样。它的底层是原生摄像头采集、编码和RTMP推流,不是H5的getUserMedia。也就是说,这套能力不是所有端都支持:H5端无法使用live-pusher,只能老老实实走WebRTC或者服务端转推方案;而App端要使用它,必须处理好权限、原生组件层级、生命周期和推流地址这些配套环节。

这篇文章我打算按一条从零到上线的完整链路来讲:先搭一个能跑的推流页面,再把manifest权限配置讲透,然后说清楚推流地址和参数调优,最后把我真机实测遇到过的坑全部摊开,连排查思路一起给你。如果你现在也正在被live-pusher折磨,或者正准备跳进这个坑,这篇内容应该能帮你省下大半周的时间。

2. 先把最基础的样子跑通:live-pusher组件的页面结构

2.1 页面模板:一个最小的直播推流页

live-pusher在uni-app里就是一个原生组件,用法跟普通标签类似,但有几个属性是决定成败的关键: url 推流地址、 autopush 是否自动推流、 aspect 画面比例、 device-position 前后摄像头、 beauty 和 whiteness 美颜美白。下面是一个最小可用的页面模板,我用Vue2写法,因为很多uniapp项目还是Vue2,但思路在Vue3里完全一样。

<template>
  <view class="container">
    <live-pusher
      id="livePusher"
      class="pusher"
      :url="pushUrl"
      :mode="mode"
      aspect="9:16"
      :muted="muted"
      :autopush="false"
      device-position="front"
      :beauty="beautyLevel"
      :whiteness="whitenessLevel"
      @statechange="onStateChange"
      @netstatus="onNetStatus"
      @error="onError"
    />
    
    <cover-view class="control-panel">
      <cover-view class="btn" @tap="startPush">开始推流</cover-view>
      <cover-view class="btn" @tap="stopPush">停止推流</cover-view>
      <cover-view class="btn" @tap="switchCamera">切换镜头</cover-view>
    </cover-view>
  </view>
</template>

注意控制面板我用的是 cover-view 而不是普通 view 。因为live-pusher在App端是原生组件,它的层级天然高于普通前端元素,如果你用普通view覆盖在上面,很可能会被原生画面盖住,点了没反应。 cover-view 是专门用来覆盖原生组件的标签,App端和微信小程序端都支持。你在自己写的时候,也要保证 .container 和 .pusher 都有明确的高度和宽度,比如在样式中设置 width: 100%; height: 50vh ,否则组件区域可能是一个0x0的盒子,看起来就是“一片黑”。

2.2 推流控制:用context把画面“送”出去

模板里设置了 autopush=false ,意思是组件先完成摄像头预览,但不会自动开始推流。我们需要在合适的时机调用context的 start 方法。获取context必须在组件渲染完成之后,一般在页面的 onReady 生命周期里执行:

export default {
  data() {
    return {
      pushUrl: 'rtmp://your-server.com/live/your-stream-key',
      mode: 'HD',
      muted: false,
      beautyLevel: 5,
      whitenessLevel: 3,
      pusherContext: null
    };
  },
  onReady() {
    // 等页面和原生组件都渲染完成,再获取上下文
    this.pusherContext = uni.createLivePusherContext('livePusher');
  },
  methods: {
    startPush() {
      if (!this.pusherContext) {
        this.pusherContext = uni.createLivePusherContext('livePusher');
      }
      this.pusherContext.start({
        success: () => {
          console.log('推流启动');
        },
        fail: (err) => {
          console.error('推流启动失败', err);
        }
      });
    },
    stopPush() {
      this.pusherContext && this.pusherContext.stop({
        success: () => {
          console.log('推流已停止');
        }
      });
    },
    switchCamera() {
      this.pusherContext && this.pusherContext.switchCamera();
    }
  }
};

start 方法的调用时机很关键。我习惯在用户点击按钮之后再调,而不是在 onReady 里直接start。原因是部分安卓机型摄像头初始化比较慢,过早调用start可能直接报打开摄像头失败(状态码-1301)。如果你确实需要进入页面就自动推流,建议在 onReady 里用 setTimeout 延时300~500毫秒再启动,给原生组件留足初始化时间。另外, uni.createLivePusherContext 的第一个参数必须是 live-pusher 标签上的 id ,这个id不是选择器,别写成 .livePusher 或者 #livePusher ,小程序的API习惯在这里容易踩雷。

2.3 事件回调:状态机、码率监控与错误处理

live-pusher会吐出三个重要的原生事件: statechange 、 netstatus 和 error 。这三兄弟分别负责推流状态迁移、网络质量上报和异常信息收集。我在真实项目中几乎把它们当作“黑匣子”来用,所有现场问题都靠这些回调判断。

onStateChange(e) {
  // e.detail.code 对应推流状态码
  const code = e.detail.code;
  const stateMap = {
    1001: '已连接推流服务器',
    1002: '已进入直播',
    1003: '打开摄像头成功',
    1004: '推流成功',
    -1301: '打开摄像头失败',
    -1302: '打开麦克风失败',
    -1303: '视频编码失败',
    -1304: '音频编码失败',
    -1307: '网络断连且重连无效',
    -1308: '网络断连,正在重连'
  };
  if (stateMap[code]) {
    console.log('推流状态:', stateMap[code]);
  }
},
onNetStatus(e) {
  const status = e.detail.info || {};
  // 可拿到的字段包括 videoBitrate、audioBitrate、netSpeed、videoFPS 等
  console.log('网络状态:', status);
},
onError(e) {
  console.error('推流出错:', e.detail);
}

状态码一定要熟记几个要命的: -1307 代表网络彻底断开且重连无效,这时候弹提示让用户检查网络是最合适的; -1308 是正在重连,说明网络有波动但还能抢救。如果看到 -1303 视频编码失败,99%是你设置的 mode 跟设备支持的编码能力不匹配,比如在低端安卓上硬解不支持超清,需要降级到SD或HD。 netstatus 里的 videoBitrate 和 netSpeed 可以做实时码率显示,用户看直播卡顿时,你只需要把这两个值打出来,就能判断是推流端采集不够还是上行带宽不足。

3. 没那么简单:manifest.json权限与App端打包配置

3.1 AndroidManifest与权限配置

很多新手把页面写好,运行到手机上,发现摄像头画面一片黑,控制台也没有报错。第一个想到的是组件用法,但真相往往是权限没配。Android端使用摄像头和麦克风必须要声明相关权限。在HBuilderX的 manifest.json 源码视图里,找到 app-plus 节点,在 distribute 下配置 android.permissions :

{
  "app-plus": {
    "modules": {
      "Camera": {},
      "Record": {}
    },
    "distribute": {
      "android": {
        "permissions": [
          "android.permission.CAMERA",
          "android.permission.RECORD_AUDIO",
          "android.permission.MODIFY_AUDIO_SETTINGS",
          "android.permission.INTERNET",
          "android.permission.ACCESS_NETWORK_STATE",
          "android.permission.ACCESS_WIFI_STATE"
        ]
      }
    }
  }
}

Camera 和 Record 模块是告诉uniapp你要用到原生摄像和录音能力,如果不勾选,运行时可能会提示“对应的模块没有配置”。而 permissions 里的 CAMERA 和 RECORD_AUDIO 是Android系统的危险权限,不声明的话系统直接不给你摄像头和麦克风的使用权。 MODIFY_AUDIO_SETTINGS 也建议加上,它影响音频采集时的参数调整,少了它部分机型录音会出现杂音。 INTERNET 和网络状态权限则是推流时的基础,总不能让直播app没有联网权限吧。

提示:Android 6.0以上系统除了在manifest中声明权限,还必须在运行时动态请求权限。uniapp的 uni.authorize 可以用,但live-pusher组件本身在开始推流时会自动触发系统权限弹窗。如果组件一直没有弹出权限请求,优先检查你的权限声明是否完整。

3.2 iOS的Info.plist权限描述

iOS端的权限配置跟Android完全不是一个路子。iOS不需要在manifest里声明类似Android的permissions,而是在 Info.plist 里写“用途说明”。如果你不写 NSCameraUsageDescription ,App一调用摄像头就闪退,连错误日志都不给一个。在 manifest.json 的 ios 节点下:

{
  "app-plus": {
    "distribute": {
      "ios": {
        "privacyDescription": {
          "NSCameraUsageDescription": "需要使用相机进行直播推流",
          "NSMicrophoneUsageDescription": "需要使用麦克风采集直播声音"
        }
      }
    }
  }
}

这两个key的文案会原样展示在系统弹窗上,苹果审核时会检查你的文案是否和功能一致。如果你只写了相机权限说明,但实际代码里同时采集了音频,审核可能会被拒。反过来,如果你的直播场景不需要声音,把 muted 设成 true ,那iOS上可以不声明 NSMicrophoneUsageDescription 。但实际项目中,哪个直播没声音呢?所以两个都写上,准没错。

3.3 自定义调试基座与真机调试的坑

这是另一个让无数人崩溃的地方。很多人在HBuilderX里直接点“运行到手机”,然后发现live-pusher不生效,报“Cannot read property 'start' of null”或者“api is not defined”。原因往往是:标准基座可能没有打包原生推流模块,或者你的项目中使用了自己集成的原生插件但基座版本不匹配。

解决办法是打自定义调试基座:在HBuilderX菜单栏选择“运行 -> 运行到手机或模拟器 -> 制作自定义调试基座”,然后选择打包,过程中确保你勾选了需要的模块和权限配置。基座装到手机上后,再用“运行到手机 -> 使用自定义基座”的方式启动项目。这一步看起来很麻烦,但它能让你在开发阶段就验证真机权限、摄像头和推流地址的正确性。不要跳过,直接踩坑的代价更大。

4. 推流地址才是决定成败的一半:RTMP、鉴权与延时参数

4.1 推流地址(RTMP URL)的结构

live-pusher组件本身不负责生成流地址,它只负责把采集到的音视频数据推到你给的RTMP地址上。很多第一次接触直播开发的兄弟会问:推流地址是什么?怎么来的?

一个典型的RTMP推流地址长这样:

rtmp://push.example.com/live/stream_abc123?sign=2d3f8a9c&expire=1712499999

拆开来看, rtmp:// 是协议; push.example.com 是推流服务器域名; /live/ 是应用名(application),这个可以自定义; stream_abc123 是流名(stream key),也是用户拉流时的标识;后面的 sign 和 expire 是鉴权参数,防止别人盗推。

如果你用的是腾讯云直播,控制台会让你创建推流域名和播放域名,然后生成一个带鉴权串的推流地址。阿里云、又拍云也是类似的套路。如果只是自己玩,可以用nginx-rtmp模块或者SRS服务器在本地建一个推流服务。但必须提醒:生产环境一定要用云直播或者自建专业流媒体服务,nginx-rtmp玩玩可以,抗不住并发。

4.2 延时、码率与清晰度的平衡

live-pusher的 mode 属性有SD、HD、FHD、RTC几种,对应标清、高清、超清和实时通话模式。我实测下来, mode 影响的是编码分辨率和码率的基准档位,如果你不满足默认参数,还得配合 video-bitrate 等属性调。很多人的项目没有调优,直接拿默认SD模式推流,画面糊得像马赛克;也有人一上来就上FHD,结果上行带宽不够,拖垮了整个推流。

这里给一套我常用的配置思路:

  • 直播类(非实时互动): mode="HD" ,分辨率1280x720,码率控制在1.5~2Mbps,关键帧间隔2秒。
  • 秀场/互动直播: mode="HD" 或 FHD ,码率2Mbps以上,关键帧间隔1秒。
  • 低延迟场景(连麦、远程指导): mode="RTC" ,但这需要服务端支持实时音视频协议,普通RTMP服务器不适用。

音频方面,直播场景建议开启双声道并保持采样率44100Hz,如果你用的服务器对音频编码有特殊要求(比如只支持AAC),要提前确认live-pusher的音频编码格式是否兼容。我遇到过服务器接收不到音频流的情况,排查到最后发现是音频编码参数不匹配,换成AAC后一切正常。

4.3 直播服务器选型:自建还是用云

这不是一个纯技术问题,而是一个成本问题。自建SRS服务器,一台2核4G的云服务器大概能支撑几百人同时观看,但要做码率转码、录制回放、防盗链,工作量大到你怀疑人生。云直播的优势不仅在于稳定,还在于它自带鉴权、转码、截图、监控这些开箱即用的功能。

我的建议是:你的项目如果只做单点直播测试,自建一个SRS完全够用,还能帮你更深入理解整个推拉流协议;如果是上线商业产品,直接选腾讯云或阿里云直播,别在服务器上省这几块钱。你省下的运维时间,够你优化十个页面。

5. 踩坑实录:黑屏、闪退、后台断流的排查链路

5.1 黑屏:最常见的翻车现场

黑屏是live-pusher新手遇到最多的问题,而且成因五花八门。我按照从高频到低频的顺序,整理了一份排查清单,你按顺序检查,大概率能锁定问题:

  1. 检查权限 :先确认App是否弹出了相机/麦克风权限申请,如果没弹,去看manifest配置。iOS上如果Info.plist缺少权限描述,直接闪退;Android上如果没声明权限,组件不会黑屏但也不会有画面。
  2. 检查推流地址 : url 如果是空字符串,组件预览都不会有画面。先别急着填真实地址,随便填一个格式正确的 rtmp://127.0.0.1/live/123 ,看摄像头预览是否出现。如果预览出现了,说明是推流地址的问题。
  3. 检查autopush和start调用 :如果你设置了 autopush=false ,但没有调用 start ,画面会停留在摄像头预览状态,画面不会黑,但如果你把 url 设成无效地址,再看 autopush=false 且没调start,预览其实应该还是有的。真正黑屏更多是组件尺寸问题。
  4. 检查组件尺寸 :live-pusher组件必须有实际的宽高。如果在flex容器里没有给高度,或者父容器是 position: absolute 但没设置top和left,组件渲染可能异常。我有个项目就是因为父容器高度设成了0,黑屏了一整天。
  5. 检查摄像头占用 :部分安卓机型如果后台有微信或其他App占用着摄像头,live-pusher初始化会失败,报 -1301 。这种问题没有太好的解法,只能提示用户关闭可能占用摄像头的应用。

5.2 首帧延迟与Android碎片化

安卓手机一堆厂商各自魔改系统,摄像头初始化速度完全不同。小米、OPPO、vivo表现有差异,老机型经常要等1到2秒摄像头才出画面。如果用户点击“开始推流”后马上判断推流成功,界面会卡在“推流中”但实际上流还没上去。我的做法是:在 statechange 里监听 1003 (打开摄像头成功)之后,才认为推流已就绪,用这个事件触发UI更新。

另外, switchCamera 在部分安卓机型上调用后画面会卡在上一帧,需要等几百毫秒才刷新。如果你连续点击切换摄像头,还有可能直接把App闪退。处理方案是加防抖:摄像头切换完成前,禁止再次点击。

5.3 退出页面声音还在:生命周期管理

这是很多App上线后收到的负面反馈:明明已经退出直播间了,手机还在嗡嗡作响。原因是live-pusher在页面销毁时没有自动停止推流和释放摄像头麦克风。务必在页面的 onUnload 生命周期里调用 stop :

onUnload() {
  if (this.pusherContext) {
    this.pusherContext.stop({
      complete: () => {
        console.log('推流已关闭');
      }
    });
  }
}

不只是退出页面,App切换到后台时也要处理。Android机切后台后,摄像头资源可能被系统回收,但你再次回到页面时live-pusher却停在那里,画面是黑的。建议在 onHide 里调用 pause 暂停推流,在 onShow 里再 start 恢复。这样既能避免后台继续消耗流量和电量,也能防止声音串场。

5.4 明文HTTP和安卓9限制

推流地址本身是RTMP,不走HTTP,但有些项目会通过一个HTTP接口去获取带鉴权的推流地址。Android 9.0开始默认禁止明文HTTP流量,如果你用的接口是 http:// 而不是 https:// ,请求会被拦截,导致拿不到推流地址,组件自然黑屏。解决方法是在 manifest.json 的Android节点下加 android:usesCleartextTraffic="true" ,或者在网络安全配置中放行特定域名。uniapp里没有直接在manifest.json配这个属性的入口,但你可以通过 plus 原生能力或者在项目的 AndroidManifest.xml 里修改。更稳妥的办法是把业务接口统一升级为HTTPS,一劳永逸。

6. 进阶玩法:自定义控制层、美颜、横竖屏与前后摄像头切换

6.1 自定义控制层与覆盖物

前面提到控制面板用 cover-view 。在实际项目中,你需要放很多按钮:开始/停止、切换摄像头、闪光灯、美颜滑块、关闭直播。这些都要覆盖在live-pusher上方,全部用 cover-view 实现。注意 cover-view 里不能放普通的 view 、 button 、 text ,只有 cover-view 和 cover-image 能在其中使用。你做样式时也要注意, cover-view 支持的CSS属性有限,比如 position: fixed 在某些安卓机上会失效,建议一律使用 position: absolute 相对父容器定位。

如果想做更复杂的美颜面板或者需要频繁更新的UI,可以考虑把整个控制层做成 subNVue 原生子窗体,但代价是需要原生调试。绝大多数直播场景,用 cover-view 就够了。

6.2 美颜与滤镜参数调节

live-pusher内置了 beauty 和 whiteness 两个参数,范围都是0到9。 beauty 是美颜磨皮程度, whiteness 是美白程度,越高效果越明显。但很遗憾,这两个属性在Android和iOS的实现效果不一样,甚至同品牌的手机不同型号效果都有差异。我建议在真机上多试试,找到你们目标用户主流机型上效果最好的数值区间。另外,这两个属性在推流过程中是可以动态修改的,即用即生效,适合做滑块调节。

如果内置美颜满足不了需求,比如要加瘦脸、大眼这类特效,就得集成原生美颜SDK(腾讯特效、相芯等)。这种SDK一般要在原生层把处理后的视频帧传给live-pusher,前端基本不可能直接实现,你会需要写原生插件。这块水很深,一般的项目不建议自己造轮子。

6.3 前后摄像头切换与焦距

切换镜头用 switchCamera 方法,方法内部会自动拍照切换。要注意的是,切换后摄像头的 device-position 属性并不会自动变化,你在UI上的摄像头图标状态需要自己维护。至于焦距调节,live-pusher没有直接的API支持。部分安卓机可以通过 plus.camera 获取原生对象进行变焦操作,但兼容性一般。如果你的直播场景有变焦需求,建议在原生层处理,或者直接去掉变焦功能,因为大多数用户根本不会在直播时捏合变焦。

6.4 横竖屏与全屏体验

直播App最常见的场景是竖屏,但游戏直播是横屏。live-pusher的 aspect 属性可以设置为 9:16 或 16:9 ,但要配合整个页面的屏幕方向。如果你让页面支持横竖屏自动旋转,需要小心组件尺寸的适配。比如用户旋转屏幕后,组件宽高需要动态计算,不然画面会被拉扁。比较好的实践是:直播页面固定竖屏,不随系统旋转。确实需要横屏时,建议单独做一个横屏直播页面,并在manifest中设置该页面的 orientation 为 landscape-primary 。这样代码更清晰,也不会出现奇奇怪怪的转场问题。

6.5 低延迟模式、后台推流的取舍

有些场景需要极低延迟,比如在线答题、远程指导,观众受不了3秒以上的延迟。普通的RTMP直播延迟通常在2到5秒,如果网络波动甚至更高。把 mode 设为 RTC 可以有效降低延迟,但前提是你的推流服务器必须支持WebRTC或者低延迟直播协议。如果你用的是云直播平台,需要确认它是否开启低延迟服务。

后台推流是一个更复杂的命题。让App退到后台仍然持续推流,涉及保活、前台服务、后台摄像头权限等一堆原生问题。单纯靠uniapp前端代码无法做到完全后台推流。我在实际项目中的折中方案是:App退到后台后自动停止摄像头采集,只保留音频推流;回到前台再恢复视频推流。但 live-pusher 组件本身没有“后台仅音频”的选项,这在原生层实现比较简单,前端还是要写插件。如果产品经理跟你要求“退后台还能继续直播”,你最好提前评估技术成本,这可能不是一首歌的时间能解决的问题。

7. 一点体会:别把期望全押在组件上

用live-pusher做了几个直播功能之后,我最深的感受是:它只是一个“视频采集+推流”的引擎,它替你封装好了摄像头、编码和RTMP协议,但直播链路里还有太多环节需要你自己把控。权限配置、推流地址、服务器能力、设备兼容性、生命周期管理,每一个都能成为线上故障的引爆点。

我建议你在真正的业务开发之前,先花一天时间把这篇文章里的最小示例完整跑通,包括自定义调试基座、真机权限、推流地址验证。然后把你目标用户的主要机型都测一遍,安卓的低端机尤其要测。我曾经觉得“一个摄像头推流能有啥问题”,结果被现场观众反馈“画面卡顿”打脸,最后发现是低端机硬编码能力不足,只能限制推流分辨率。这种问题,不真机测试永远发现不了。

如果你正在用live-pusher做项目,建议把 statechange 、 netstatus 的错误日志全部接入你的日志监控系统,线上用户出问题时,日志往往比用户描述的信息量大得多。一个小技巧:在 onNetStatus 里把 videoBitrate 每5秒上报一次,当用户投诉卡顿时,你翻报表看有没有断崖式下跌,很快就能判断是上行网络问题还是服务端转码问题。这比任何用户截图都管用。

Logo

邀请您加入社区

更多推荐