Skip to content

Now playing ​

device.nowPlaying holds what plays now, as plain snapshots of type NowPlaying. It comes from the Media Remote messages of the AirPlay session and works on both devices.

Reading ​

ts
const item = tv.nowPlaying.current;

if (item) {
    console.log(item.app.name, item.title, item.artist, item.state);
    console.log(`${tv.nowPlaying.elapsed.toFixed(0)} of ${item.duration} seconds`);
}

current is null when no app plays anything. elapsed is the position in seconds now, extrapolated from the playback rate while playing, so you can read it on a timer without asking the device.

The NowPlaying snapshot ​

FieldMeaning
appbundleId and name of the app that plays
playerThe player id within the app; MediaRemote-DefaultPlayer by default
stateplaying, paused, stopped, interrupted, seeking or unknown
title, artist, album, genreItem metadata; empty strings when absent
trackArtistThe track artist from the item metadata, for apps that leave artist empty
durationSeconds; 0 when unknown
ratePlayback rate; 1 is normal speed
repeatoff, one or all
shuffleoff, albums or songs
kindaudio, video or unknown
seriesname, season, episode and episodeTitle of a TV episode, or null
contentRating, contentIdentifierAs the app reports them
artworkIdIdentifies the artwork of the item, or null
storeAlbumIdThe iTunes Store album id, or null; feeds fetchAnimatedArtwork()
lyricsIdThe Apple Music song id, or null; feeds fetchLyrics()
isAlwaysLive, isAdvertisementFlags of the now playing info

Every change gives a new snapshot object. Keep it, compare it or send it over IPC. Nothing in it updates later.

Events ​

ts
tv.nowPlaying.on('change', item => {
    console.log('now playing', item?.title ?? 'nothing');
});

tv.nowPlaying.on('state', (state, previous, item) => {
    console.log(`${item.app.name}: ${previous} -> ${state}`);
});

tv.nowPlaying.on('artwork', item => {
    console.log('new artwork for', item.title);
});
EventFires when
changeThe snapshot differs from the last one this object emitted (deep equal)
stateThe playback state differs from the last one announced. Fires before the matching change. A change to null resets the announced state to unknown
artworkThe artwork of the current item looks different: another identifier or URL, or other image bytes

Every event compares public values with the last values this object announced. The device sends many messages for one visible change, and you get one event.

Across a reconnect ​

The announced values survive sessions. After a reconnect the device holds back:

  • When the new session reports the same item again, nothing fires.
  • When it reports another item, the device announces it once it has been stable for one second.
  • Five seconds after the new session is up at the latest, the device announces what it reports then, if that differs.

Meanwhile current and elapsed return the last announced item and position, and supports() answers as that item did. A network blip does not look like playback stopping and starting. A state that changed during the outage fires state once, right before the change that ends the wait.

This only holds for a reconnect after a drop. disconnect() forgets the announced item, state and artwork, so the next connect() announces them again as a new device does.

Artwork ​

device.artwork fetches the artwork of what plays now.

ts
const artwork = await tv.artwork.get(600);

if (artwork?.data) {
    console.log(artwork.mimeType, artwork.width, artwork.height, artwork.data.byteLength);
} else if (artwork?.url) {
    console.log(artwork.url);
}

get(width = 600, height = -1) resolves an Artwork with a url, the image data, or both, or null when the current item has none. A height of -1 keeps the aspect ratio. getAnimated(width, height) resolves the animated artwork formats the device knows, as AnimatedArtwork entries with a format and a url, or an empty list. Both throw UnavailableError while AirPlay is down, like every other capability. Check supports('artwork') first, or catch the error.

Listen to nowPlaying artwork events to know when to fetch again.

Lyrics ​

nowPlaying.lyrics() asks the playback queue for the lyrics of the current item.

ts
const lyrics = await tv.nowPlaying.lyrics();

if (lyrics?.text) {
    console.log(lyrics.text);
} else if (lyrics?.catalogId) {
    console.log('lyrics are in the catalog under', lyrics.catalogId);
}

It resolves a Lyrics record with the text when the device sends it, whether lyrics are available, a url and the catalogId. It throws UnavailableError without an AirPlay session. Many apps send no text. fetchLyrics() gets it from Apple Music instead.

CoreWire is released under the MIT license.