更新记录
1.0.0(2026-11-06)
首次发布
平台兼容性
uni-app(5.21)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | app-nvue插件版本 | Android | Android插件版本 | iOS | iOS插件版本 | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | √ | 1.0.0 | 8.1 | 1.0.0 | 16 | 1.0.0 | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
uni-app x(5.31)
| Chrome | Safari | Android | Android插件版本 | iOS | iOS插件版本 | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|---|
| × | × | 8.0 | 1.0.0 | 16 | 1.0.0 | × | × |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | × | √ | √ |
lyzml-touping DLNA 投屏插件
简介
lyzml-touping 是一个基于 uni-app x 的 UTS 插件,实现 DLNA/UPnP 协议的设备发现与媒体投屏控制。支持 Android 和 iOS 双端,无需第三方 SDK,纯协议层实现。
核心功能:
- SSDP 多播搜索局域网 DLNA 设备(电视、盒子、音箱等)
- UPnP SOAP 播放控制:播放、暂停、继续、停止、Seek、音量调节
- 播放进度轮询与状态查询
- 播放列表自动连播
平台支持
| 平台 | 支持状态 |
|---|---|
| uni-app x App-Android | ✅ |
| uni-app x App-iOS | ✅ |
| Web/小程序/快应用 | ❌ 不支持 |
安装
在 HBuilderX 中导入本插件到 uni_modules 目录,或从 DCloud 插件市场 安装。
uni_modules/
└── lyzml-touping/
├── utssdk/
│ ├── app-android/index.uts # Android 原生实现
│ ├── app-ios/index.uts # iOS 原生实现
│ └── interface.uts # TypeScript 类型定义
└── package.json
权限配置
Android (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\" />"
]
iOS (manifest.json → app-plus → distribute → ios → privacyDescription)
{
"NSLocalNetworkUsageDescription": "本应用需要访问局域网以搜索 DLNA 投屏设备"
}
快速开始
1. 导入 API
import {
startDiscovery,
stopDiscovery,
getDevices,
play,
pause,
resume,
stop,
seek,
setVolume,
getVolume,
getPositionInfo,
getTransportState,
setMonitor,
getLocalIp,
TpDevice,
TpResult
} from '@/uni_modules/lyzml-touping';
2. 搜索设备
// 注意:回调必须作为顶层参数传递,不能放进 options 对象。
// iOS 端 UTS 桥接解析对象内函数字段时会 dynamicCast 失败直接崩溃。
startDiscovery(
{ timeoutMs: 8000 }, // 搜索超时时间(毫秒)
() => {
console.log('SSDP 搜索已启动');
},
(device: TpDevice) => {
console.log('发现设备:', device.name, device.ip);
},
(devices: TpDevice[]) => {
console.log('设备列表更新:', devices.length, '台');
},
(devices: TpDevice[]) => {
console.log('搜索完成:', devices.length, '台');
},
(res: TpResult) => {
console.error('搜索失败:', res.message);
}
);
// 不需要的回调传 null 占位(注意保持参数顺序)
3. 播放媒体
play({
deviceId: 'uuid:xxx...', // 设备唯一标识(从搜索结果获取)
url: 'https://example.com/video.mp4', // 媒体 URL(需电视可直接访问)
title: '视频标题', // 可选,显示在电视上
mediaType: 'video', // 'video' | 'audio'
creator: '应用名称', // 可选
coverUrl: 'https://...' // 可选,封面图
}, (res: TpResult) => {
if (res.code === 0) {
console.log('播放成功');
} else {
console.error('播放失败:', res.message);
}
});
4. 播放控制
// 暂停
pause(deviceId, (res) => { ... });
// 继续
resume(deviceId, (res) => { ... });
// 停止
stop(deviceId, (res) => { ... });
// Seek 跳转(秒)
seek(deviceId, 30, (res) => { ... });
// 设置音量(0-100)
setVolume(deviceId, 50, (res) => { ... });
// 获取音量
getVolume(deviceId, (res) => { ... });
5. 查询状态
// 获取播放进度(返回 XML 字符串)
getPositionInfo(deviceId, (res) => {
if (res.code === 0) {
console.log('进度信息:', res.data);
// 示例输出:
// <RelTime>00:01:23</RelTime><TrackDuration>00:05:00</TrackDuration>...
}
});
// 获取播放状态
getTransportState(deviceId, (res) => {
if (res.code === 0) {
console.log('播放状态:', res.data); // PLAYING | PAUSED_PLAYBACK | STOPPED
}
});
6. 获取缓存设备列表
getDevices((devices: TpDevice[]) => {
console.log('缓存设备:', devices);
});
7. 停止搜索
stopDiscovery((res: TpResult) => {
console.log('停止搜索:', res.message);
});
8. 获取本机 IP
getLocalIp((ip: string) => {
console.log('本机 IP:', ip);
});
9. 设置监控回调(调试用)
setMonitor((event: UTSJSONObject) => {
console.log('监控事件:', JSON.stringify(event));
});
播放进度解析
getPositionInfo 返回原始 XML 字符串,需自行解析:
function parseXmlTag(xml: string, tag: string): string {
const start = xml.indexOf('<' + tag + '>');
if (start === -1) return '';
const end = xml.indexOf('</' + tag + '>', start);
if (end === -1) return '';
return xml.substring(start + tag.length + 2, end);
}
function timeToSeconds(timeStr: string): number {
const parts = timeStr.split(':');
if (parts.length !== 3) return 0;
return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
}
// 使用示例
getPositionInfo(deviceId, (res) => {
if (res.code === 0 && res.data) {
const xml = String(res.data);
const relTime = parseXmlTag(xml, 'RelTime'); // 当前播放位置
const duration = parseXmlTag(xml, 'TrackDuration'); // 总时长
console.log('进度:', relTime, '/', duration);
}
});
自动连播实现参考
基于进度轮询实现播放结束自动切换下一集:
let pollTimer: number | null = null;
let playlist = [
{ url: '...', title: '视频1', mediaType: 'video' },
{ url: '...', title: '视频2', mediaType: 'video' }
];
let currentIndex = 0;
let autoPlayNext = true;
function startPoll() {
stopPoll();
pollTimer = setInterval(() => {
getPositionInfo(deviceId, (res) => {
if (res.code === 0 && res.data) {
const xml = String(res.data);
const relTime = parseXmlTag(xml, 'RelTime');
const duration = parseXmlTag(xml, 'TrackDuration');
const relSec = timeToSeconds(relTime);
const durSec = timeToSeconds(duration);
// 剩余 5 秒内且开启自动连播,切换下一集
if (autoPlayNext && durSec > 0 && durSec - relSec <= 5) {
playNext();
}
}
});
getTransportState(deviceId, (res) => {
if (res.code === 0) {
console.log('状态:', res.data);
}
});
}, 3000);
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function playNext() {
currentIndex++;
if (currentIndex >= playlist.length) {
console.log('播放列表结束');
stopPoll();
return;
}
const item = playlist[currentIndex];
play({ deviceId, url: item.url, title: item.title, mediaType: item.mediaType });
}
类型定义
interface TpDevice {
id: string; // 设备唯一标识(UDN)
name: string; // 设备显示名称
ip?: string; // IP 地址
location?: string; // SSDP 描述文件 URL
udn?: string; // 设备唯一标识
manufacturer?: string; // 制造商
modelName?: string; // 型号
}
interface TpResult {
code: number; // 0 成功,非 0 失败
message: string; // 结果描述
data?: any; // 附加数据(如 XML 响应)
}
interface TpDiscoveryOptions {
timeoutMs?: number;
onStart?: () => void;
onDeviceFound?: (device: TpDevice) => void;
onDeviceListChanged?: (devices: TpDevice[]) => void;
onComplete?: (devices: TpDevice[]) => void;
onError?: (res: TpResult) => void;
}
interface TpPlayOptions {
deviceId: string;
url: string;
title?: string;
mediaType?: string; // 'video' | 'audio'
creator?: string;
coverUrl?: string;
}
注意事项
- 网络要求:手机和电视必须在同一局域网(Wi-Fi)下
- URL 可访问性:投屏的媒体 URL 必须能被电视直接访问(建议使用公网 URL 或局域网内服务)
- iOS 权限:iOS 14+ 需要
NSLocalNetworkUsageDescription权限说明 - Android 权限:需要
CHANGE_WIFI_MULTICAST_STATE权限以接收 SSDP 多播响应 - 设备兼容性:不同品牌电视对 DLNA 协议实现有差异,部分功能可能不支持
- SSDP 搜索稳定性:UDP 广播无确认机制,建议多发几次 M-SEARCH 或适当延长超时时间
错误码
| 错误码 | 说明 |
|---|---|
| 9020001 | 通用错误 |
| 9020002 | 设备未找到 |
| 9020003 | 网络错误 |
| 9020004 | 协议错误 |
| 9020005 | 操作不支持 |
更新日志
v1.0.0
- 初始版本
- 支持 SSDP 设备发现
- 支持 UPnP 播放控制(播放/暂停/继续/停止/Seek/音量)
- 支持 Android + iOS 双端
许可证
MIT License
作者: lyzml
问题反馈: 请通过 DCloud 插件市场或 GitHub Issues 提交

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 1931
赞赏 1
下载 12748495
赞赏 1959
赞赏
京公网安备:11010802035340号