更新记录

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;
}

注意事项

  1. 网络要求:手机和电视必须在同一局域网(Wi-Fi)下
  2. URL 可访问性:投屏的媒体 URL 必须能被电视直接访问(建议使用公网 URL 或局域网内服务)
  3. iOS 权限:iOS 14+ 需要 NSLocalNetworkUsageDescription 权限说明
  4. Android 权限:需要 CHANGE_WIFI_MULTICAST_STATE 权限以接收 SSDP 多播响应
  5. 设备兼容性:不同品牌电视对 DLNA 协议实现有差异,部分功能可能不支持
  6. SSDP 搜索稳定性:UDP 广播无确认机制,建议多发几次 M-SEARCH 或适当延长超时时间

错误码

错误码 说明
9020001 通用错误
9020002 设备未找到
9020003 网络错误
9020004 协议错误
9020005 操作不支持

更新日志

v1.0.0

  • 初始版本
  • 支持 SSDP 设备发现
  • 支持 UPnP 播放控制(播放/暂停/继续/停止/Seek/音量)
  • 支持 Android + iOS 双端

许可证

MIT License


作者: lyzml
问题反馈: 请通过 DCloud 插件市场或 GitHub Issues 提交

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android需要INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATE、CHANGE_WIFI_MULTICAST_STATE;iOS需要Local Network权限说明

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

局域网DLNA设备信息、投屏播放URL

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。