Skip to content

@corewire/devices ​

ts
import { AppleTV, HomePod } from '@corewire/devices';

Devices ​

AppleDevice ​

abstract class AppleDevice<TCapability extends AppleTVCapability = Capability> extends EventEmitter<{status: [status: Status]}>

The base of AppleTV and HomePod. Its constructor is protected. See Apple TV and HomePod.

MemberDescription
address: stringThe current address
id: stringThe target id, or the address
model: DeviceModelFrom modelName
modelName: stringThe model identifier, such as AudioAccessory5,1; empty until known
name: stringFrom the TXT record or /info; the id until known
sessions: DeviceSessionsThe protocol sessions in use now
status: StatusThe current status
artwork: ArtworkControlArtwork
media: MediaControlURLs and audio streams
multiroom: MultiroomControlMultiroom
nowPlaying: NowPlayingControlNow playing
playback: PlaybackControlPlayback
remote: RemoteControlRemote
volume: VolumeControlVolume
connect(): Promise<void>Keeps the device connected until disconnect(). Resolves the first time it is connected or degraded; rejects when the first attempt and the quick retries failed. Works right after a disconnect() you did not await
disconnect(): Promise<void>Stops every retry, clears the announced values and emits disconnected at once, then resolves once every session closed. Idempotent
supports(capability: TCapability): booleanWhether the capability works now
event statusFires on every change of the status
protected reset(): voidClears the announced values at the start of disconnect(); AppleTV overrides it to reset power
protected mediaRemoteState, companionLinkStateThe protocol models of the device

AppleTV ​

class AppleTV extends AppleDevice<AppleTVCapability>

MemberDescription
constructor(target: DeviceTarget, options: AppleTVOptions = {})
accounts: AccountsControlUser accounts
apps: AppsControlApps
power: PowerControlPower state, wake and sleep
system: SystemControlSystem settings and Siri
text: TextControlText input
touch: TouchControlTouch surface
static pair(target: DeviceTarget, options: PairOptions = {}): Promise<Pairing>Starts a PIN pairing; the PIN shows on screen once this resolves

HomePod ​

class HomePod extends AppleDevice<HomePodCapability>

MemberDescription
constructor(target: DeviceTarget, options: DeviceOptions = {})A HomePod, HomePod mini or second generation HomePod

Discovery ​

ExportDescription
discover(options: DiscoverOptions = {}): Promise<DiscoveredDevice[]>One multicast scan for AirPlay, Companion Link and RAOP; services of one host merge by address
createDevice(found: DiscoveredDevice, options: AppleTVOptions = {}): AppleTV | HomePodAn AppleTV for the Apple TV family, a HomePod for every other receiver
WAKE_PORTS: readonly number[]Ports knocked on to wake a device: [7000, 3689, 49152, 32498]

Credentials and pairing ​

Credentials ​

class Credentials

MemberDescription
static from(credentials: AccessoryCredentials): CredentialsWraps credentials from a protocol pair()
static parse(value: string | Readonly<Record<string, unknown>>): CredentialsReads the string form, an object with the five fields (hex or base64), or JSON of such an object. Throws CredentialsError
toString(): stringcw1. and five base64url fields
toFields(): {accessoryIdentifier; accessoryLongTermPublicKey; pairingId; publicKey; secretKey}The five fields as strings, the binary ones as hex: the object form parse() reads. For storage an older release has to keep reading
toJSON(): stringThe same as toString()

The constructor is private.

Pairing ​

class Pairing

MemberDescription
static start(target: DeviceTarget, options?: PairOptions): Promise<Pairing>What AppleTV.pair() calls
pin(code: string): Promise<Credentials>Runs pair-setup with the PIN. Closes the connection on success; a wrong PIN keeps it open
close(): voidCloses the connection without credentials

Start one with AppleTV.pair(). The name the Apple TV lists for the pairing is identity.name, CoreWire when omitted.

Errors ​

The entry point re-exports every error class of @corewire/core, so all errors import from @corewire/devices. These are AppleProtocolError, AuthenticationError, CommandError, ConnectionClosedError, ConnectionError, ConnectionTimeoutError, CredentialsError, DecryptionError, DiscoveryError, EncryptionError, InvalidResponseError, PairingError, PlaybackError, SetupError, TimeoutError and TLV8PairingError. See Logging and errors for the hierarchy.

CommandRefusedError ​

class CommandRefusedError extends CommandError

The device refused a playback command, for example because nothing plays or the app does not handle it. Every playback.* method throws it.

MemberDescription
constructor(reason: string, status: string)
static from(error: SendCommandError): CommandRefusedErrorTranslates the protocol error of @corewire/airplay/mrp; that error becomes the cause
reason: stringThe send error in lower camel case, such as applicationNotFound, timedOut or noError
status: stringThe handler status, such as noActionableNowPlayingItem, commandFailed or success

UnavailableError ​

class UnavailableError extends AppleProtocolError

MemberDescription
constructor(capability: string, reason: 'disconnected' | 'unsupported', message?: string)
capability: stringThe capability that was called
reason: 'disconnected' | 'unsupported'Its protocol is down, or the device cannot do it

Device models ​

ExportDescription
enum DeviceModelUnknown, AppleTVGen2, AppleTVGen3, AppleTVHD, AppleTV4K, AppleTV4KGen2, AppleTV4KGen3, HomePod, HomePodMini, HomePodGen2, AirPortExpress, AirPortExpressGen2
enum DeviceFamilyUnknown, AppleTV, HomePod, AirPort
lookupDeviceModel(identifier: string): DeviceModelFrom a model identifier (AppleTV6,2) or an internal name (J305AP)
getDeviceModelName(model: DeviceModel): stringA display name, such as Apple TV 4K (2nd generation)
getDeviceFamily(model: DeviceModel): DeviceFamilyThe family of a model
isAppleTV(model: DeviceModel): boolean
isHomePod(model: DeviceModel): boolean
isAirPort(model: DeviceModel): boolean

Capability types ​

The capabilities are interfaces, and the classes behind them are internal. Reach them through a device. The ones with events extend EventEmitter with their own event map: NowPlayingEventMap, VolumeEventMap, PowerEventMap, TextEventMap and MultiroomEventMap.

AccountsControl ​

MemberDescription
list(): Promise<Account[]>The user accounts
switch(id: string): Promise<void>Makes an account the active user

AppsControl ​

MemberDescription
list(): Promise<App[]>The launchable apps
launch(bundleId: string): Promise<void>Opens an app
openUrl(url: string): Promise<void>Opens a universal link or an app URL scheme

ArtworkControl ​

MemberDescription
get(width = 600, height = -1): Promise<Artwork | null>The artwork of what plays now; a height of -1 keeps the aspect ratio. Null when the current item has none
getAnimated(width = 600, height = -1): Promise<AnimatedArtwork[]>The animated artwork formats the device knows; empty when the current item has none

Both throw UnavailableError while AirPlay is down.

MediaControl ​

MemberDescription
playUrl(url: string, options: PlayUrlOptions = {}): Promise<MediaPlayback>Has the device fetch and play a URL. Throws UnavailableError (unsupported) on a device that cannot, PlaybackError when it refuses the URL
streamAudio(source: AudioSource, options: StreamAudioOptions = {}): Promise<MediaPlayback>Streams audio to the device

MediaPlayback ​

An interface, so instanceof does not work on it.

MemberDescription
kind: 'url' | 'audio'
ended: Promise<void>Settles when playback ended; for a URL, watching starts at the first read
stop(): voidStops playback and closes its connection

MultiroomControl ​

extends EventEmitter<MultiroomEventMap>

MemberDescription
clusterId: string | nullThe group id
isClusterAware: boolean
isLeader: boolean
outputs: readonly OutputDevice[]The output devices that play along now, as the device last reported them
add(...deviceUids: string[]): Promise<void>Adds output devices to the group by uid
remove(...deviceUids: string[]): Promise<void>Removes output devices
set(...deviceUids: string[]): Promise<void>Replaces the whole group
getVolume(deviceUid: string): Promise<number>The volume of one output device, 0 to 1
setVolume(deviceUid: string, level: number): Promise<void>Sets the volume of one output device, clamped to 0 to 1
mute(deviceUid: string): Promise<void>, unmute(deviceUid: string): Promise<void>Mutes or unmutes one output device
event change: [outputs: readonly OutputDevice[]]The list differs from the last one announced

outputs survives a reconnect like the volume does, and disconnect() clears it. Every method throws UnavailableError while AirPlay is down.

NowPlayingControl ​

extends EventEmitter<NowPlayingEventMap>

MemberDescription
current: NowPlaying | nullThe current item; the last announced one while a reconnect restores, null after disconnect()
elapsed: numberElapsed seconds now, extrapolated while playing
supports(command: NowPlayingCommand): booleanWhether the now playing app accepts the command
lyrics(): Promise<Lyrics | null>Lyrics of the current item from the playback queue
event change: [item: NowPlaying | null]The item differs from the last one emitted
event state: [state: PlaybackState, previous: PlaybackState, item: NowPlaying]The playback state differs from the last one announced
event artwork: [item: NowPlaying]The artwork of the current item changed

PlaybackControl ​

Every method throws UnavailableError while AirPlay is down and CommandRefusedError when the device refuses the command.

MemberDescription
play(), pause(), togglePlayPause(), stop(): Promise<void>Transport
next(): Promise<void>, previous(): Promise<void>
skipForward(seconds = 15): Promise<void>, skipBackward(seconds = 15): Promise<void>
seek(seconds: number): Promise<void>Position from the start
setRepeat(mode: RepeatMode): Promise<void>
setShuffle(mode: ShuffleMode): Promise<void>
setRate(rate: number): Promise<void>1 is normal speed

PowerControl ​

extends EventEmitter<PowerEventMap>

MemberDescription
state: PowerStateThe last reported state; unknown before Companion Link reported one
wake(): Promise<void>The wake key over AirPlay
sleep(): Promise<void>The suspend key over AirPlay
event change: [state: PowerState]Every report, including the one each Companion Link connect fetches

RemoteControl ​

MemberDescription
press(key: RemoteKey, options: PressOptions = {}): Promise<void>Presses a key; throws UnavailableError when no connected protocol knows it

SystemControl ​

MemberDescription
toggleCaptions(): Promise<void>
setAppearance(mode: 'light' | 'dark'): Promise<void>
setReduceLoudSounds(enabled: boolean): Promise<void>
setFindingMode(enabled: boolean): Promise<void>Find My mode
startSiri(): Promise<void>, stopSiri(): Promise<void>

TextControl ​

extends EventEmitter<TextEventMap>

MemberDescription
state: TextStateThe text input state, from Companion Link or else AirPlay
set(text: string): Promise<void>Replaces the text of the focused field
append(text: string): Promise<void>
clear(): Promise<void>
event change: [state: TextState]

TouchControl ​

MemberDescription
tap(holdMs?: number): Promise<void>Selects what has focus; held when holdMs is given
swipe(direction: SwipeDirection, durationMs?: number): Promise<void>200 ms when omitted

VolumeControl ​

extends EventEmitter<VolumeEventMap>, VolumeState

MemberDescription
level: number, muted: boolean, available: boolean, mode: VolumeModeThe current values
set(level: number): Promise<void>Clamped to 0 to 1; needs mode absolute
up(): Promise<void>, down(): Promise<void>A 0.05 step, or the volume key in mode relative
mute(), unmute(), toggleMute(): Promise<void>
fade(level: number, durationMs: number): Promise<void>
event change: [volume: VolumeState]One of the values differs from the last one emitted

Types ​

TypeDefinition
DeviceTarget{address: string; port?: number; txt?: Readonly<Record<string, string>>; id?: string}
DeviceOptions{log?(entry: LogEntry): void; logLevels?: readonly LogLevel[]; timing?: TimingServer | false; resolve?(last: DeviceTarget, signal: AbortSignal): Promise<DeviceTarget | null>; identity?: ClientIdentity}. The signal of resolve aborts after 15 seconds or when disconnect() cancels the attempt
AppleTVOptionsDeviceOptions & {credentials?: Credentials; activeUserAccount?: string}
PairOptions{identity?: ClientIdentity; signal?: AbortSignal}
ClientIdentityPartial<{name; model; osName; osVersion; osBuildVersion; sourceVersion}>, all strings. name defaults to CoreWire: the name the Apple TV lists after a PIN pairing, and the name in the AirPlay SETUP, URL playback, MRP device info and Companion Link system info
LogLevel'debug' | 'error' | 'info' | 'net' | 'raw' | 'warn'
LogEntry{level: LogLevel; device: string; message: string; args: readonly unknown[]; timestamp: number}
DeviceState'disconnected' | 'connecting' | 'connected' | 'degraded' | 'recovering' | 'offline'
StatusReason'requested' | 'established' | 'remote' | 'timeout' | 'error' | 'failed' | 'scheduled'
LinkState'off' | 'connecting' | 'connected' | 'waiting'
RetryPhase'quick' | 'slow' | 'rare'
ProtocolStatus{state: LinkState; attempt: number; phase: RetryPhase | null; nextAttemptAt: number | null; error: Error | null}
Status{state: DeviceState; reason: StatusReason; error: Error | null; airplay: ProtocolStatus; companionLink?: ProtocolStatus}
Capability'remote' | 'nowPlaying' | 'playback' | 'volume' | 'artwork' | 'multiroom' | 'streamAudio' | 'playUrl'
HomePodCapabilityCapability
AppleTVCapabilityCapability | 'text' | 'power' | 'apps' | 'accounts' | 'system' | 'touch'
DeviceSessions{airplay?: AirPlaySession; mediaRemote?: MediaRemote; companionLink?: CompanionLinkSession}
PlaybackState'playing' | 'paused' | 'stopped' | 'interrupted' | 'seeking' | 'unknown'
RepeatMode'off' | 'one' | 'all'
ShuffleMode'off' | 'albums' | 'songs'
MediaKind'audio' | 'video' | 'unknown'
NowPlayingThe now playing snapshot; see the fields
NowPlayingSeries{name: string; season: number; episode: number; episodeTitle: string}
NowPlayingCommand'play' | 'pause' | 'togglePlayPause' | 'stop' | 'next' | 'previous' | 'skipForward' | 'skipBackward' | 'seek' | 'repeat' | 'shuffle' | 'rate'
Lyrics{identifier: string; text: string | null; available: boolean | null; url: string | null; catalogId: string | null}
VolumeMode'absolute' | 'relative' | 'none'
VolumeState{level: number; muted: boolean; available: boolean; mode: VolumeMode}
RemoteKeyThe 33 key names; see Remote
PressMode'tap' | 'double' | 'hold'
PressOptions{mode?: PressMode; holdMs?: number}
PowerState'unknown' | 'asleep' | 'screensaver' | 'awake' | 'idle'
TextState{active: boolean; text: string; secure: boolean; keyboardType: number; autocorrection: boolean; autocapitalization: boolean}
App{bundleId: string; name: string}
Account{id: string; name: string}
SwipeDirection'up' | 'down' | 'left' | 'right'
Artwork{url: string | null; data: Uint8Array | null; mimeType: string; identifier: string | null; width: number; height: number}
AnimatedArtwork{format: string; url: string | null; source?: 'device' | 'apple-music'}
AudioSourceThe audio source interface of @corewire/airplay, re-exported
PlayUrlOptions{position?: number; signal?: AbortSignal}
StreamAudioOptions{volume?: number; signal?: AbortSignal}; volume is 0 to 1
ServiceRecord{name: string; port: number; txt: Readonly<Record<string, string>>}
DiscoveredDevice{id; name; address; model: DeviceModel; modelName; family: DeviceFamily; mac: string | null; target: DeviceTarget; services: {airplay?; companionLink?; raop?: ServiceRecord}}
DiscoverOptions{useCache?: boolean}; default true. The type of @corewire/core/discovery, re-exported
OutputDevice{uid: string; name: string; model: string; groupId: string | null; isLocal: boolean; isGroupLeader: boolean; hasVolumeControl: boolean}; a speaker or TV in the multiroom group. isLocal marks the device the session runs on
NowPlayingEventMap{change: [item: NowPlaying | null]; state: [state: PlaybackState, previous: PlaybackState, item: NowPlaying]; artwork: [item: NowPlaying]}
VolumeEventMap{change: [volume: VolumeState]}
PowerEventMap{change: [state: PowerState]}
TextEventMap{change: [state: TextState]}
MultiroomEventMap{change: [outputs: readonly OutputDevice[]]}

@corewire/devices/catalog ​

ts
import { fetchAnimatedArtwork, fetchLyrics } from '@corewire/devices/catalog';
ExportDescription
fetchAnimatedArtwork(albumId: string, options: AnimatedArtworkOptions = {}): Promise<AnimatedArtwork[]>The animated artwork of an album, from its public Apple Music page
fetchLyrics(catalogId: string, options: AppleMusicLyricsOptions): Promise<string | null>The TTML lyrics of a song from the Apple Music catalog; null when it has none (404)
class CatalogError extends AppleProtocolErrorconstructor(reason: CatalogErrorReason, message: string, status?: number | null); reason, and status: the HTTP status, or null. Both functions throw it, also for a network failure or a timeout
type CatalogErrorReason'invalidArgument' | 'unauthorized' | 'forbidden' | 'requestFailed' | 'invalidResponse' | 'tooLarge'
type AnimatedArtworkOptions{storefront?: string; signal?: AbortSignal}; storefront us when omitted
type AppleMusicLyricsOptions{bearerToken: string; musicUserToken: string; storefront: string; language?: string; script?: string; signal?: AbortSignal}
type AnimatedArtworkThe same type as in the main entry point

See Apple Music catalog.

CoreWire is released under the MIT license.