@achingbrain/ssdp
Version:
Yet another SSDP implementation for node.js
92 lines (75 loc) • 3.19 kB
text/typescript
import { EventEmitter, on } from 'node:events'
import { createRequire } from 'node:module'
import { advertise } from './advertise/index.js'
import { adverts } from './adverts.js'
import { notify } from './commands/notify.js'
import { search } from './commands/search.js'
import { createSockets } from './create-sockets.js'
import { defaultSsdpOptions } from './default-ssdp-options.js'
import { discover } from './discover/index.js'
import { searchResponse } from './discover/search-response.js'
import { parseSsdpMessage } from './parse-ssdp-message.js'
import { sendSsdpMessage } from './send-ssdp-message.js'
import type { CachedAdvert } from './adverts.js'
import type { Advertisement, DiscoverOptions, Service, SSDP as SSDPInterface, SSDPOptions, SSDPSocket } from './index.js'
const req = createRequire(import.meta.url)
const { name, version } = req('../../package.json')
const DEFAULT_SSDP_SIGNATURE = `node.js/${process.version.substring(1)} UPnP/1.1 ${name}/${version}`
export class SSDP extends EventEmitter implements SSDPInterface {
public udn: string
public signature: string
public sockets: SSDPSocket[]
public readonly options: SSDPOptions
private readonly abortController: AbortController
constructor (options?: SSDPOptions) {
super()
this.options = defaultSsdpOptions(options)
this.udn = this.options.udn ?? `uuid:${crypto.randomUUID()}`
this.signature = this.options.signature ?? DEFAULT_SSDP_SIGNATURE
this.sockets = []
this.abortController = new AbortController()
}
async start (): Promise<void> {
// set up UDP sockets listening for SSDP broadcasts
this.sockets = await createSockets(this, this.abortController.signal)
// set up protocol listeners
this.on('transport:incoming-message', parseSsdpMessage.bind(null, this))
this.on('ssdp:send-message', sendSsdpMessage.bind(null, this))
this.on('ssdp:m-search', search.bind(null, this))
this.on('ssdp:notify', notify.bind(null, this))
this.on('ssdp:search-response', searchResponse.bind(null, this))
}
async stop (): Promise<void> {
await adverts.stopAll()
await Promise.all(
this.sockets.map(async socket => {
await new Promise<void>(resolve => {
socket.on('close', () => { resolve() })
socket.close()
socket.closed = true
})
})
)
this.abortController.abort()
}
async advertise (advert: Advertisement): Promise<CachedAdvert> {
return advertise(this, advert)
}
async * discover <Details = Record<string, any>> (serviceType?: string | DiscoverOptions): AsyncGenerator<Service<Details>, void, any> {
const opts: DiscoverOptions | undefined = typeof serviceType === 'string' ? { serviceType } : serviceType
discover(this, opts?.serviceType)
let interval: ReturnType<typeof globalThis.setInterval> | undefined
if (opts?.searchInterval != null) {
interval = setInterval(() => {
discover(this, opts?.serviceType)
}, opts.searchInterval)
}
try {
for await (const [service] of on(this, 'service:discover', opts)) {
yield service
}
} finally {
clearInterval(interval)
}
}
}