更新记录
1.1.0(2026-10-23)
-
平台兼容性
uni-app(5.01)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | √ | √ | √ | √ | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.01)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | √ | √ | √ | - |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | √ | × | √ |
wb-dlna-cast · 局域网投屏与电视遥控器
把手机上的视频 / 音乐 / 图片投到同一 Wi-Fi 下的电视、投影仪、电视盒子,并像遥控器一样控制播放、进度和音量。
Android 用 Kotlin、iOS 用 Swift 原生实现,不依赖 Cling / CocoaPods 等任何三方库,云打包可直接通过。

特性
- 设备发现:SSDP 组播搜索 + NOTIFY 通告,自动过滤掉不支持投放的设备,只留真正能接流的渲染器
- 投放媒体:视频 / 音乐 / 图片,自动按扩展名生成正确的 MIME 与 DIDL-Lite 元数据
- 播放控制:播放、暂停、继续、停止、拖动进度、音量、静音
- 状态同步:每 2 秒回读播放进度与传输状态,暂停 / 播放由电视实际状态决定,不是本地猜的
- 总时长四级兜底:设备不给也能显示(详见下方说明)
- 事件驱动:设备上下线、状态变化、错误、诊断日志全部可监听
- 零三方依赖:Android 只用
java.net+org.json,iOS 只用 Foundation,不引入任何 jar / pod - 原生混编:
DlnaCastNative.kt/DlnaCastNative.swift直接参与编译,改原生逻辑不用重新打基座
界面示例
| 浅色主题 | 设备发现 | 运行日志 |
|---|---|---|
![]() |
![]() |
见 demo pages/index/index.vue 底部折叠面板 |
截图来自配套 demo(
pages/index/index.vue),深色 / 浅色两套主题,可直接拿来改。
支持的生态环境
| 平台 | 支持 | 说明 |
|---|---|---|
| uni-app(Vue3) | ✅ | HBuilderX 4.25+,<script setup> 可直接用 |
| uni-app(Vue2) | ✅ | 同上,需 HBuilderX 4.25+ |
| uni-app x | ✅ | 同上,接口完全一致 |
| Android App | ✅ | Android 5.0(API 21)及以上真机 |
| iOS App | ✅ | iOS 12.0 及以上真机 |
| H5 / 小程序 / 鸿蒙 / 快应用 | ❌ | 依赖原生 UDP 组播与 socket,端上无法实现 |
必须用真机:iOS 模拟器、Android 模拟器都收不到局域网组播,只能看到空设备列表。
运行环境要求
- HBuilderX 4.25 及以上(插件用了 UTS 原生混编)
- 手机与电视在同一局域网(同一 Wi-Fi,或同网段的有线 / 无线互通)
- 电视已开启「投屏 / 多屏互动 / DLNA」功能
快速开始
1. 导入插件
把 uni_modules/wb-dlna-cast 整个目录放进项目根目录的 uni_modules 下(HBuilderX 插件市场一键导入即可)。
2. 配置权限
Android / iOS 的权限说明见下方权限说明,插件已自带清单,绝大多数情况不需要手改。
3. 最少可用代码
import * as DlnaCast from '@/uni_modules/wb-dlna-cast'
// 搜索(不传 timeout 默认 5000ms)
const devices = await DlnaCast.startScan(6000)
if (!devices.length) {
console.log('没有发现设备')
return
}
// 选一台连接
await DlnaCast.connect(devices[0])
// 投放(type: video | audio | image)
await DlnaCast.play('https://example.com/movie.mp4', 'video', '片名', '')
// 监听状态(进度、播放中、音量…)
DlnaCast.onStateChange((state) => {
console.log(state.position, state.duration, state.playing)
})
API
设备发现
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
startScan(timeout?) |
timeout: 毫秒,默认 5000 |
Promise<DlnaDevice[]> |
搜索并返回这段时间内发现的设备 |
stopScan() |
— | void |
提前结束本次搜索窗口(不会断开已连接设备) |
onDeviceFound(cb) |
cb: (device) => void |
void |
搜索过程中陆续发现设备时回调 |
onDeviceLost(cb) |
cb: (udn) => void |
void |
设备下线 / 30 秒无通告 |
连接与投放
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
connect(device) |
device: DlnaDevice |
Promise<void> |
选中设备,内部会同步一次电视当前音量 |
disconnect() |
— | Promise<void> |
停止播放并断开 |
play(url, type, title?, cover?) |
url: 直链;type: video/audio/image |
Promise<void> |
先 SetAVTransportURI 再 Play,两步都成功才 resolve |
播放控制
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
pause() |
— | Promise<void> |
暂停 |
resume() |
— | Promise<void> |
继续 |
stop() |
— | Promise<void> |
停止播放(不断开设备) |
seek(seconds) |
目标秒数 | Promise<void> |
按 REL_TIME 拖动,超出范围会被设备拒绝 |
setVolume(volume) |
0–100 | Promise<void> |
自动收敛到 0–100 |
setMute(muted) |
boolean |
Promise<void> |
静音开关 |
状态与事件
| 方法 | 返回 | 说明 |
|---|---|---|
getState() |
Promise<CastState> |
取当前状态快照 |
getStateSync() |
CastState |
同步取,适合在渲染前读一次 |
onStateChange(cb) |
void |
每 2 秒随进度轮询推送 |
onError(cb) |
void |
渲染器返回 UPnP 错误码时触发,参数是错误描述 |
onLog(cb) |
void |
诊断日志([SOAP] xxx),排查问题首选 |
offStateChange() 等 |
void |
清空该事件的全部监听 |
release() |
void |
页面卸载时释放 socket 与定时器 |
offXxx()传不传 callback 效果一致:都会清空该事件下的全部监听。
数据结构
type DlnaDevice = {
udn: string // 设备唯一标识
name: string // 电视上显示的名称,如「客厅的小米电视」
address: string // 设备描述 XML 的主机地址
port: number
type: string // renderer
location: string // 描述 XML 完整地址
avtControl: string // AVTransport 控制端点(播放控制)
rcControl: string // RenderingControl 控制端点(音量/静音)
}
type CastState = {
connected: boolean
device: DlnaDevice | null
url: string
type: string // video / audio / image / ''
position: number // 当前进度(秒)
duration: number // 总时长(秒),拿不到时为 0
playing: boolean
volume: number // 0–100
muted: boolean
}
权限说明
插件已自带清单文件,正常情况无需手动配置。如果宿主工程做过权限裁剪、或云打包版本较老没合并插件清单,按下面手动补一次。
Android
插件路径:utssdk/app-android/AndroidManifest.xml
| 权限 | 是否必需 | 用途 |
|---|---|---|
android.permission.INTERNET |
✅ 必需 | SSDP 收发、拉取设备描述 XML、下发 SOAP 控制 |
android.permission.ACCESS_NETWORK_STATE |
✅ 必需 | 判断网络 / Wi-Fi 可用性 |
android.permission.ACCESS_WIFI_STATE |
✅ 必需 | 获取 WifiManager 以申请 MulticastLock |
android.permission.CHANGE_WIFI_MULTICAST_STATE |
✅ 必需 | 没有它 Wi-Fi 休眠后收不到组播,表现为「第一次能搜到,之后再也搜不到」 |
android.permission.NEARBY_WIFI_DEVICES |
⭕ 建议 | Android 13+ 部分机型发现局域网设备需要,已标注 neverForLocation(不索取位置) |
手动补到 manifest.json:
{
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_MULTICAST_STATE\"/>",
"<uses-permission android:name=\"android.permission.NEARBY_WIFI_DEVICES\"/>"
]
}
}
}
}
以上都是普通权限,不会弹授权框,用户无感。
iOS
插件路径:utssdk/app-ios/info.plist(会自动合并进宿主 Info.plist)
| Key | 是否必需 | 用途 |
|---|---|---|
NSLocalNetworkUsageDescription |
✅ 必需 | iOS 14+ 访问局域网(SSDP / HTTP)必须声明,否则搜不到任何设备 |
NSAppTransportSecurity → NSAllowsLocalNetworking |
⭕ 建议 | 允许读取局域网内的 http 媒体(探测总时长用),不影响公网 ATS 策略 |
<key>NSLocalNetworkUsageDescription</key>
<string>用于发现同一 Wi-Fi 下的电视、投影仪等投屏设备</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
首次搜索时会弹「允许查找并连接本地网络设备」,必须点允许,否则系统直接丢弃 SSDP 报文。
不需要苹果审批的 com.apple.developer.networking.multicast entitlement,本地网络权限就够了。
隐私
插件不采集、不上传任何数据:设备信息、播放地址、日志全部只在本机内存中处理,出网请求只有两类——局域网内设备描述 XML、以及你传入的媒体直链。
常见问题
Q:搜不到设备?
- 确认手机和电视在同一 Wi-Fi / 同一网段(手机用 4G、电视走有线但路由器隔离,都搜不到)
- 电视端要开启「投屏 / 多屏互动 / DLNA」
- iOS 检查「设置 → 隐私 → 本地网络」里本 App 是否被关闭
- 路由器开了 AP 隔离 / 访客网络会阻断组播,关掉再试
- 打开
onLog,正常应该看到M-SEARCH 已发送和发现设备: xxx
Q:能搜到,但点播放报错(如 701 / 714 / 716)?
701 是「传输状态不允许该动作」(设备还没准备好),714 是「跳转目标非法」(seek 超长),716 是「资源找不到」——多数是媒体地址电视访问不到,检查直链是否可公网访问、是否需要 Referer。
Q:投屏后电视黑屏 / 一直在转圈?
DLNA 是「电视自己去拉流」,手机只负责发地址。所以:
file://、blob:、手机本地相册地址投不过去- 需要能被电视访问的地址:公网 http/https,或局域网里另一个 http 服务
- 部分电视只支持 H.264 + AAC 的 mp4,mkv / hevc 会黑屏
Q:为什么没有显示总时长?
插件按四级顺序取:TrackDuration → DIDL 的 <res duration> → GetMediaInfo 的 MediaDuration → 发送端本地探测(MediaMetadataRetriever / AVURLAsset)。青蜂鸟这类廉价投屏接收器前三级全不给,靠第四级兜底;四级都失败时 UI 显示「未知时长」,不显示假进度。日志里会打印 探测到总时长 Ns。HLS(m3u8)属于分片流,系统探测 API 拿不到总时长,属已知限制。
Q:遥控器的方向键为什么是调音量?
通用 DLNA 协议只定义了播放、进度、音量,没有标准的方向键注入。所以方向键做了语义映射:↑↓ 音量、←→ 快退快进 10 秒、OK 播放暂停。如果电视厂商有私有按键服务(部分是 X_ 扩展动作),需要单独适配。
Q:切到后台 / 息屏还能继续投吗?
可以。播放是电视在执行,App 只负责发控制指令。但 Android 上如果系统把 App 进程回收,就收不到状态推送了,需要自己加前台服务保活。
Q:能投本地视频文件吗?
需要先把文件暴露成一个局域网 http 地址(内置 http server 或上传到服务器),再把该地址传给 play()。插件只接受 url 字符串。
已知限制
- 只支持 DLNA / UPnP 的
MediaRenderer设备;AirPlay、Miracast、Chromecast 不走这套协议 - 组播依赖同一广播域,跨网段 / VLAN 需要路由器配置组播转发
- 部分设备的
GetPositionInfo返回NOT_IMPLEMENTED,此时进度条退化为「未知时长」,播放控制仍正常 - 单设备实例:同一时间只维护一个连接,切换设备请先
disconnect()
更新日志
1.1.0(2026-10-23)
- 新增:总时长四级兜底(TrackDuration → DIDL res@duration → GetMediaInfo → 发送端本地探测),兼容青蜂鸟等不返回时长的投屏接收器
- 新增:连接设备时自动同步电视当前音量,音量条不再从 0 起步
- 新增:配套 demo 改为深色 / 浅色双主题,内置拟物电视遥控器
- 优化:时间解析兼容带毫秒的格式(如
00:03:20.000) - 优化:同时支持 uni-app 与 uni-app x,Android Kotlin / iOS Swift 原生混编,零三方依赖
- 优化:完善 Android / iOS 权限声明与本地网络访问说明
- 修复:部分设备播放进度、总时长解析异常
- 修复:多轮云打包编译问题(Swift 闭包默认参数、
NSNumber/Number数值参数、JSON.parse可选值)
1.0.0(2026-10-22)
- 首个版本:SSDP 局域网设备发现
- 支持视频 / 音乐 / 图片投放
- 支持播放、暂停、继续、停止、拖动进度、音量、静音
- 支持播放状态轮询与设备上下线事件
完整日志见插件根目录
changelog.md。
协议
MIT

收藏人数:
购买普通授权版(
试用
使用 HBuilder 导入示例项目
赞赏(0)


下载 15
赞赏 0
下载 12709037
赞赏 1957
赞赏
京公网安备:11010802035340号