更新记录

1.2.3(2026-03-15)

  • 修复 iOS 端对 DLNA 设备描述 XML 的服务解析,避免部分设备遗漏 AVTransport / RenderingControl 控制服务。
  • 修复 iOS 端特殊 controlURL 的绝对地址拼接,兼容 Redmi 电视等返回 _urn:... 路径的设备。
  • 补充 iOS 侧设备描述与服务解析日志,便于排查局域网投屏兼容性问题。

1.2.2(2026-03-13)

  • dlnaSeek 支持非负小数秒,Android / iOS 会向接收端发送带毫秒的 HH:MM:SS.xxx 时间格式。
  • 接收端进度/时长解析同步支持小数秒,playground 的 seek 输入与进度展示一并适配。
  • 修复 iOS UTS 桥接的 number -> Double 参数转换问题,避免 seekAsync 编译失败。

1.2.1(2026-03-13)

  • iOS 插件补充 UTS.entitlements,声明 com.apple.developer.networking.multicast 以支持 SSDP 组播发现。
  • README 补充 iOS multicast capability 说明与排查提示。
查看更多

平台兼容性

uni-app(4.87)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(4.87)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - - -

hans-dlna

hans-dlna 是一个用于局域网内发现 DLNA 设备并执行媒体投放控制的 UTS 插件,当前实现覆盖 Android / iOS,提供设备发现、设备选择、播放控制、接收端状态监听、事件回调和日志开关。

文档中的 API 与类型以 utssdk/interface.uts 为准。

平台支持

运行时 Android iOS Harmony
uni-app -
uni-app x -

说明:

  • Android / iOS 为当前完整实现。
  • Harmony 提供了可编译的 stub。
  • Harmony 上功能型 API 会以 90010008 失败返回;监听注册函数返回占位 listenerIdoffDlnaListener 为 no-op。

能力概览

  • 生命周期:dlnaInitdlnaDestroy
  • 设备发现:dlnaStartDiscoverydlnaStopDiscoverydlnaGetDevices
  • 设备选择:dlnaSelectDevice
  • 播放控制:dlnaPlaydlnaPausedlnaStopdlnaSeek
  • 接收端监听:dlnaStartReceiverMonitordlnaStopReceiverMonitordlnaGetReceiverStatus
  • 事件回调:onDlnaDeviceFoundonDlnaErroronDlnaReceiverEventoffDlnaListener
  • 调试能力:setDlnaLogEnabledisDlnaLogEnabled

使用前提

  1. 手机与目标 DLNA 设备处于同一局域网。
  2. 传入的媒体 url 必须能被目标设备直接访问。
  3. 建议先注册监听并调用 dlnaInit,再开始扫描设备。
  4. 若使用接收端监听,建议先调用 dlnaSelectDevice 选择目标设备。

引入方式

按需从插件入口导入:

import {
  dlnaInit,
  dlnaDestroy,
  dlnaStartDiscovery,
  dlnaStopDiscovery,
  dlnaGetDevices,
  dlnaSelectDevice,
  dlnaPlay,
  dlnaPause,
  dlnaStop,
  dlnaSeek,
  dlnaStartReceiverMonitor,
  dlnaStopReceiverMonitor,
  dlnaGetReceiverStatus,
  onDlnaDeviceFound,
  onDlnaError,
  onDlnaReceiverEvent,
  offDlnaListener,
  setDlnaLogEnabled,
  isDlnaLogEnabled,
  DlnaDevice,
  DlnaMedia,
  DlnaFail,
  DlnaReceiverStatus,
  DlnaReceiverEvent,
  DlnaListenerId,
  DlnaInitOptions,
  DlnaStartDiscoveryOptions,
  DlnaSelectDeviceOptions,
  DlnaPlayOptions,
  DlnaSeekOptions,
  DlnaStartReceiverMonitorOptions
} from '@/uni_modules/hans-dlna'

所有 options / result / listener 类型都可以直接从插件入口导入。

快速开始

import {
  dlnaInit,
  dlnaDestroy,
  dlnaStartDiscovery,
  dlnaStopDiscovery,
  dlnaSelectDevice,
  dlnaPlay,
  onDlnaDeviceFound,
  onDlnaError,
  offDlnaListener,
  DlnaDevice,
  DlnaMedia,
  DlnaFail,
  DlnaListenerId,
  DlnaInitOptions,
  DlnaDestroyOptions,
  DlnaStartDiscoveryOptions,
  DlnaStopDiscoveryOptions,
  DlnaSelectDeviceOptions,
  DlnaPlayOptions
} from '@/uni_modules/hans-dlna'

var deviceFoundListenerId : DlnaListenerId | null = null
var errorListenerId : DlnaListenerId | null = null
const devices = ref<Array<DlnaDevice>>([])
const selectedDeviceId = ref('')

onLoad(() => {
  deviceFoundListenerId = onDlnaDeviceFound((res : DlnaDevice) => {
    const idx = devices.value.findIndex((item : DlnaDevice) : boolean => item.id == res.id)
    if (idx >= 0) {
      devices.value[idx] = res
    } else {
      devices.value.push(res)
    }
  })

  errorListenerId = onDlnaError((err : DlnaFail) => {
    console.error('dlna error', err.errCode, err.errMsg)
  })

  const initOptions : DlnaInitOptions = {
    success: (_res : UTSJSONObject) => {
      dlnaStartDiscovery({ timeoutMs: 8000 } as DlnaStartDiscoveryOptions)
    }
  }
  dlnaInit(initOptions)
})

function chooseDevice(deviceId : string) {
  const options : DlnaSelectDeviceOptions = {
    deviceId: deviceId,
    success: (res : DlnaDevice) => {
      selectedDeviceId.value = res.id
    },
    fail: (err : DlnaFail) => {
      console.error('select device failed:', err)
    }
  }
  dlnaSelectDevice(options)
}

function play(url : string) {
  if (selectedDeviceId.value.length == 0) {
    return
  }
  const media : DlnaMedia = {
    url: url,
    title: 'demo',
    mimeType: 'video/mp4'
  }
  const options : DlnaPlayOptions = {
    media: media,
    success: (res : UTSJSONObject) => {
      console.log('play success:', res)
    },
    fail: (err : DlnaFail) => {
      console.error('play failed:', err)
    }
  }
  dlnaPlay(options)
}

onUnload(() => {
  dlnaStopDiscovery({} as DlnaStopDiscoveryOptions)

  if (deviceFoundListenerId != null) {
    offDlnaListener(deviceFoundListenerId!)
    deviceFoundListenerId = null
  }
  if (errorListenerId != null) {
    offDlnaListener(errorListenerId!)
    errorListenerId = null
  }

  dlnaDestroy({} as DlnaDestroyOptions)
})

API 说明

所有 API 都是回调风格:

  • success(res):成功回调
  • fail(err):失败回调,错误类型为 DlnaFail
  • complete(result):完成回调,成功或失败都会触发

生命周期

dlnaInit(options)

  • 作用:初始化插件内部状态和底层桥接。
  • 成功返回:{ ok: true }

dlnaDestroy(options)

  • 作用:销毁插件并清理已选择设备、监听状态与内部缓存。
  • 成功返回:{ ok: true }

发现与设备管理

dlnaStartDiscovery({ timeoutMs? })

  • 作用:开始扫描局域网内 DLNA 设备。
  • 默认值:timeoutMs = 5000
  • 成功返回:{ discovering: true }
  • 事件:扫描过程中会通过 onDlnaDeviceFound 持续回调设备。

dlnaStopDiscovery(options)

  • 作用:停止扫描。
  • 成功返回:{ discovering: false }

dlnaGetDevices(options)

  • 作用:获取当前已缓存的设备列表。
  • 成功返回:{ devices: DlnaDevice[] }

dlnaSelectDevice({ deviceId })

  • 作用:选择后续播放控制和状态监听的目标设备。
  • 成功返回:DlnaDevice
  • 常见失败:
    • deviceId 为空
    • deviceId 对应设备不存在

播放控制

dlnaPlay({ media })

  • 作用:向当前已选择设备发送播放指令。
  • 必填:media.url
  • 默认值:media.mimeType = 'video/mp4'
  • 成功返回:{ state: 'PLAYING' }

dlnaPause(options)

  • 作用:向当前已选择设备发送暂停指令。
  • 成功返回:{ state: 'PAUSED_PLAYBACK' }

dlnaStop(options)

  • 作用:向当前已选择设备发送停止指令。
  • 成功返回:{ state: 'STOPPED' }

dlnaSeek({ positionSec })

  • 作用:向当前已选择设备发送跳转指令,按绝对秒数定位播放进度。
  • 必填:positionSec,非负数字,单位为秒;支持小数秒,例如 90.5
  • 成功返回:{ positionSec: number }
  • 说明:
    • 插件内部会优先使用 REL_TIME,失败时回退到 ABS_TIME
    • 若目标设备或当前媒体不支持 seek,通常会以 90010005 返回控制失败。

接收端监听

dlnaStartReceiverMonitor({ intervalMs?, idleIntervalMs?, includeVolume?, preferSubscribe? })

  • 作用:开始监听当前接收端的连接状态、播放状态、进度与音量信息。
  • 默认值:
    • intervalMs = 1000
    • idleIntervalMs = 3000
    • includeVolume = false
    • preferSubscribe = false
  • 成功返回:{ monitoring: true, mode: 'poll' | 'subscribe' }
  • 前置条件:必须已调用 dlnaSelectDevice

说明:

  • preferSubscribe: true 时,Android / iOS 会优先尝试订阅式状态更新;若设备不支持订阅,或订阅失效,会自动回退到轮询兜底。
  • success 返回的 mode 是启动时的当前模式;运行过程中实际模式可能在 pollsubscribe 之间切换,最终以 onDlnaReceiverEventdlnaGetReceiverStatus 返回的 status.mode 为准。

dlnaStopReceiverMonitor(options)

  • 作用:停止监听。
  • 成功返回:{ monitoring: false }

dlnaGetReceiverStatus(options)

  • 作用:获取最近一次接收端状态;若当前没有缓存状态,会主动请求一次设备状态。
  • 成功返回:DlnaReceiverStatus
  • 前置条件:必须已调用 dlnaSelectDevice

onDlnaReceiverEvent(callback)

  • 作用:监听接收端状态事件。
  • 返回值:listenerId

事件类型:

  • STATUS_CHANGED
  • PROGRESS
  • SEEKED
  • DISCONNECTED
  • RECONNECTED
  • ENDED

补充说明:

  • SEEKED 表示接收端进度发生明显跳变,适合响应拖动进度条等场景。
  • ENDED 为 best-effort 判定,依赖设备返回的状态和进度信息,不保证所有 DLNA 设备行为完全一致。
  • includeVolumefalseDlnaReceiverStatus.volumeDlnaReceiverStatus.muted 可能为空。

事件监听

onDlnaDeviceFound(callback)

  • 作用:监听发现到的设备。
  • 返回值:listenerId

onDlnaError(callback)

  • 作用:监听插件内部错误和解析失败等异常。
  • 返回值:listenerId

offDlnaListener(listenerId)

  • 作用:按 listenerId 解绑监听器。

日志控制

setDlnaLogEnabled(enabled: boolean)

  • 作用:设置插件日志开关。

isDlnaLogEnabled()

  • 作用:读取当前插件日志开关状态。

示例:

setDlnaLogEnabled(true)
console.log('plugin log enabled:', isDlnaLogEnabled())

类型定义

错误类型

type DlnaErrorCode =
  | 90010001  // 参数错误
  | 90010002  // 未初始化
  | 90010003  // 未选择设备
  | 90010004  // 发现失败
  | 90010005  // SOAP 调用失败
  | 90010006  // 网络不可用
  | 90010007  // 请求超时
  | 90010008  // 平台不支持

interface DlnaFail extends IUniError {
  errCode: DlnaErrorCode
}

设备与媒体类型

type DlnaDevice = {
  id: string
  usn: string
  st: string
  location: string
  friendlyName: string
  manufacturer?: string
  modelName?: string
  avTransportControlURL?: string
  renderingControlURL?: string
}

type DlnaMedia = {
  url: string
  title?: string
  mimeType?: string
}

type DlnaTransportState =
  | 'STOPPED'
  | 'PLAYING'
  | 'PAUSED_PLAYBACK'
  | 'TRANSITIONING'
  | 'NO_MEDIA_PRESENT'
  | 'UNKNOWN'

接收端监听类型

type DlnaMonitorMode = 'subscribe' | 'poll'

type DlnaReceiverEventType =
  | 'STATUS_CHANGED'
  | 'PROGRESS'
  | 'SEEKED'
  | 'DISCONNECTED'
  | 'RECONNECTED'
  | 'ENDED'

type DlnaReceiverStatus = {
  deviceId: string
  connected: boolean
  mode: DlnaMonitorMode
  transportState: DlnaTransportState
  positionSec?: number
  durationSec?: number
  volume?: number
  muted?: boolean
  ended?: boolean
  updatedAt: number
}

type DlnaReceiverEvent = {
  type: DlnaReceiverEventType
  status: DlnaReceiverStatus
  prevStatus?: DlnaReceiverStatus
  message?: string
}

监听器类型

type DlnaListenerId = number

type DlnaOnDeviceFound = (callback: (res: DlnaDevice) => void) => DlnaListenerId
type DlnaOnError = (callback: (res: DlnaFail) => void) => DlnaListenerId
type DlnaOnReceiverEvent = (callback: (res: DlnaReceiverEvent) => void) => DlnaListenerId
type DlnaOffListener = (listenerId: DlnaListenerId) => void

type SetDlnaLogEnabled = (enabled: boolean) => void
type IsDlnaLogEnabled = () => boolean

API 选项类型

type DlnaInitOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaDestroyOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaStartDiscoveryOptions = {
  timeoutMs?: number
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaStopDiscoveryOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaGetDevicesResult = {
  devices: Array<DlnaDevice>
}

type DlnaGetDevicesOptions = {
  success?: (res: DlnaGetDevicesResult) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaSelectDeviceOptions = {
  deviceId: string
  success?: (res: DlnaDevice) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaPlayOptions = {
  media: DlnaMedia
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaPauseOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaStopOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaStartReceiverMonitorOptions = {
  intervalMs?: number
  idleIntervalMs?: number
  includeVolume?: boolean
  preferSubscribe?: boolean
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaStopReceiverMonitorOptions = {
  success?: (res: UTSJSONObject) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

type DlnaGetReceiverStatusOptions = {
  success?: (res: DlnaReceiverStatus) => void
  fail?: (res: DlnaFail) => void
  complete?: (res: any) => void
}

API 函数类型

type DlnaInit = (options: DlnaInitOptions) => void
type DlnaDestroy = (options: DlnaDestroyOptions) => void
type DlnaStartDiscovery = (options: DlnaStartDiscoveryOptions) => void
type DlnaStopDiscovery = (options: DlnaStopDiscoveryOptions) => void
type DlnaGetDevices = (options: DlnaGetDevicesOptions) => void
type DlnaSelectDevice = (options: DlnaSelectDeviceOptions) => void
type DlnaPlay = (options: DlnaPlayOptions) => void
type DlnaPause = (options: DlnaPauseOptions) => void
type DlnaStop = (options: DlnaStopOptions) => void
type DlnaStartReceiverMonitor = (options: DlnaStartReceiverMonitorOptions) => void
type DlnaStopReceiverMonitor = (options: DlnaStopReceiverMonitorOptions) => void
type DlnaGetReceiverStatus = (options: DlnaGetReceiverStatusOptions) => void

错误码

  • 90010001:参数错误,例如 deviceId 为空、media.url 为空、设备不存在
  • 90010002:未初始化
  • 90010003:未选择设备
  • 90010004:设备发现失败
  • 90010005:SOAP 调用失败或接收端状态解析失败
  • 90010006:网络不可用或初始化 / 销毁阶段底层异常
  • 90010007:请求超时
  • 90010008:平台不支持

权限与隐私说明

Android

插件已在 utssdk/app-android/AndroidManifest.xml 中声明以下权限:

  • android.permission.INTERNET
  • android.permission.ACCESS_NETWORK_STATE
  • android.permission.ACCESS_WIFI_STATE
  • android.permission.CHANGE_WIFI_MULTICAST_STATE

iOS

插件已在 utssdk/app-ios/Info.plist 中声明:

  • NSLocalNetworkUsageDescription
  • utssdk/app-ios/UTS.entitlements 中的 com.apple.developer.networking.multicast

说明:

  • 首次访问本地网络时,系统会弹出局域网权限提示。
  • DLNA 设备发现依赖 SSDP 组播,iOS 侧需要同时开启 multicast capability。
  • 若真机仍无法发送 M-SEARCH,请检查当前签名 / provisioning profile 是否允许该 capability。
  • 如果宿主应用需要自定义提示文案,可以在宿主工程侧覆盖该配置。

隐私声明

插件仅在局域网内发现 DLNA 设备并发送媒体投放控制指令,不采集或上传个人隐私数据。

调试建议

  • 推荐先开启插件日志:setDlnaLogEnabled(true)
  • 播放目标优先选择电视等真实 DLNA 接收端设备
  • 接入接收端监听时,建议在业务页面同时展示连接状态、播放状态、进度和音量信息,便于定位设备兼容性问题

隐私、权限声明

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

Android: INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATE、CHANGE_WIFI_MULTICAST_STATE;iOS: Local Network(用于局域网设备发现与投放控制)

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

插件仅在局域网内发现 DLNA 设备并发送媒体投放控制指令,不采集或上传个人隐私数据。

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