uni-app直播推流踩坑指南:live-pusher从黑屏到上线的完整实践
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新手遇到最多的问题,而且成因五花八门。我按照从高频到低频的顺序,整理了一份排查清单,你按顺序检查,大概率能锁定问题:
- 检查权限 :先确认App是否弹出了相机/麦克风权限申请,如果没弹,去看manifest配置。iOS上如果Info.plist缺少权限描述,直接闪退;Android上如果没声明权限,组件不会黑屏但也不会有画面。
-
检查推流地址
:
url如果是空字符串,组件预览都不会有画面。先别急着填真实地址,随便填一个格式正确的rtmp://127.0.0.1/live/123,看摄像头预览是否出现。如果预览出现了,说明是推流地址的问题。 -
检查autopush和start调用
:如果你设置了
autopush=false,但没有调用start,画面会停留在摄像头预览状态,画面不会黑,但如果你把url设成无效地址,再看autopush=false且没调start,预览其实应该还是有的。真正黑屏更多是组件尺寸问题。 -
检查组件尺寸
:live-pusher组件必须有实际的宽高。如果在flex容器里没有给高度,或者父容器是
position: absolute但没设置top和left,组件渲染可能异常。我有个项目就是因为父容器高度设成了0,黑屏了一整天。 -
检查摄像头占用
:部分安卓机型如果后台有微信或其他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秒上报一次,当用户投诉卡顿时,你翻报表看有没有断崖式下跌,很快就能判断是上行网络问题还是服务端转码问题。这比任何用户截图都管用。
更多推荐
所有评论(0)