Speakers

This section explains the Media related functionality of speaker devices, for general information on how to write a driver for your speaker device checkout the Drivers section.

How speakers work

A speaker driver functions just like any other device driver on the Homey with the exception that a speaker driver has direct interaction with the Homey media platform. Most of the basic functionality is handled by the device capabilities but some extra features require the implementation of some of the methods you see below.

Initialisation

During the initialisation step of your driver app it is important that you register each device you want listed as a speaker by calling registerSpeaker() where the first argument contains the device_data of the already registered device , the second argument contains an Object holding the initial state of a speaker and the third argument a callback which returns the Speaker object once it has been created. A full list of properties that you can use to communicate the speaker state can be found in the table below.

/app.js

'use strict';

const Homey = require('homey');

class MySpeaker extends Homey.Device {

    /**
     * Gets called to init the device
     */
    onInit() {
        this._registerSpeaker()
    }

    /**
     * Register speaker with homey
     * @private
     */
    _registerSpeaker() {
        this.speaker = new Homey.Speaker(this);

        // set setTrack listener callback
        this.speaker.on('setTrack', this._setTrack.bind(this));
        // set setPosition listener callback
        this.speaker.on('setPosition', this._setPosition.bind(this));
        // set setActive listener callback
        this.speaker.on('setActive', this._setSpeakerActive.bind(this));

        // Register the speaker instance
        this.speaker.register({
            codecs: ['custom:codec:id', Homey.Codec.MP3],
        });
    }

    _setTrack(data, callback) {
        // ...
    }

    _setPosition(position, callback) {
        // ...
    }

    _setSpeakerActive(isActive, callback) {
         // ...
     }
}

Speaker status object

property description example
codecs An array of codecs that the speaker can accept [Homey.Codec.MP3, 'custom:codec:id']
position The current playback position within the active track in ms 1337
track The currently active track for more information see ManagerSpeaker#Track

Activation

After a speaker device has been registered it will need to be set as the active speaker before it can be used to communicate with the media platform directly. In order for your app to be aware of whether or not your speaker device is indeed active you will need to listen to a 'setActive' event that will be emitted by the device. the isActive value will indicate true if the speaker has been made active and false if has been switched to inactive.

/app.js

// set setActive listener
speaker.on('setActive', (isActive, callback) => {
    // check the isActive value to determine if the speakers has been made active/inactive for Homey media
});

The example below shows how a speaker driver app might choose to handle activation events. The speaker chosen for this example does not provide realtime updates to the driver so we instead choose to periodically update the app state by manually polling the speaker for information on a regular interval. Doing so is optional but greatly enhances the playback experience for a user.

/app.js

'use strict';

const Homey = require('homey');

class MySpeaker extends Homey.Device {

    // ...

    /**
     * Gets called when a speaker is selected as active speaker
     * This function will clear the current state of the speaker to be prepared for incoming setTrack events
     * @param device The device store device object
     * @param isActive Boolean, if user switched to/way from this speaker
     * @param callback Callback to be called when the speaker is ready
     * @private
     */
    _setSpeakerActive(isActive, callback) {
        // check if speaker should start or stop polling
        if (isActive) {
            // Set an interval to poll the state of the external speaker. This is usefull to let Homey know the exact
            // state of the speaker which enables Homey to better sync playback position and play/pause state
            this.updateStateInterval = setInterval(() => {
                this._getStatus()
                    .then(status => {
                        this.setCapabilityValue('speaker_playing', status.isPlaying);
                        // Update the position of the track so Homey can re-sync the progress bar if needed
                        if (status.currentTimeMS !== undefined) {
                            this.speaker.updateState({ position: Math.round(status.currentTimeMS) });
                        }
                    });
            }, 5000);
            // Return the callback with the new state of the speaker
            callback(null, isActive);
        } else {
            // Clear polling since inactive speakers are not interesting to listen to
            clearInterval(this.updateStateInterval);
            // When the speaker becomes inactive execute code to release the device from Homey control
            callback(null, false);
        }
    }

    /**
     * Gets status from my speaker api
     * @returns Promise
    */
    _getStatus() {
        // ...
    }
}

Changing tracks

After a speaker device has activated it will be free to send state updates to the Homey media platform directly but it will also be responsible for keeping the speaker and media state in sync, if Homey media requests an action the expected behaviour of the speaker will be to respond as quickly as possible in order to ensure a smooth experience for the end user.

The 'setTrack' event will notify a speaker driver of the desire by Homey media to change the currently active track to a newly provided one. The first argument is a data object which holds a track and an opts property.

/app.js

// set setTrack listener
speaker.on('setTrack', (data, callback) => {
    // set the provided Track onto the device
    // data = { track: ManagerMedia#Track }, opts: { startPlaying: boolean, position: number, delay: number } };
});

A full list of properties that you can expect can be found in the tables below.

setTrack Track object

attribute description example
id Unique identifier for the track "S9bCLPwzSC0"
title Title of the track "Some Random Song"
artist Array of artist objects [ { "name" : "Adele", "type" : "artist" } ]
album Name of the album for this track "The Ultimate Collection"
duration Duration of the track in ms 324000
artwork Object containing links to a small, medium and large version of the track artwork { "small" : "https://coverart.host.com/mycover/64.jpg", "medium" : "https://coverart.host.com/mycover/300.jpg", "large" : "https://coverart.host.com/mycover/640.jpg" }
genre Name of the genre of the track "Jazz"
release_date Date string that this track was released on "2016-05-16"
bitrate 160
codec Codec that this track is provided in "homey:codec:mp3"
bpm Beats per minute 90
options Optional options object that can store additional data blobs (ex. additional tokens) { uniqueProperty1: someData, uniqueProperty2: moreData }
stream_url The url the speaker can stream the track from https://api.streamingservice.com/tracks?YOUR_STREAM_QUERY
time_expires An epoch timestamp in ms at which the provided stream_url expires (optional) 1489153003933

setTrack opts object

property description example
position The current playback position within the active track in ms 1337
delay The amount of time left on the current track in ms. The track should be played after this time passes. This time can be used by speakers to prebuffer the next track. 5000
startPlaying Should the speaker start playback after switching tracks true

Depending on the type of speaker you hope to support with your app you might want to handle the 'setTrack' event differently. If your speaker does not natively support the pre-buffering or queueing of tracks it would greatly improve the user experience if a queueing system is implemented in the driver app. Since this is a common use case some sample code has been provided in the example below.

/app.js

'use strict';

const Homey = require('homey');

class MySpeaker extends Homey.Device {

    // ...

    _setTrack(data, callback) {
        // This handles the queuing of tracks. Homey will send the next track before the current track is done to enable
        // the speaker to start buffering the next track. If a speaker does not implement prebuffering so it can set a
        // timeout. It is important to handle the queued tracks being overwritten by a subsequent setTrack event.

        // Check if the device has an queuedCallback option indicating that there already is a track queued
        if (this.queuedCallback) {
            // Call the callback with the track that is queued with an error to indicate that the corresponding track is cancelled
            this.queuedCallback(new Error('setTrack debounced'));
            // Clear the callback from the device object
            this.queuedCallback = null;
            // Clear the timeout that was intended to play the track on the speaker
            clearTimeout(this.queuedTimeout);
        }

        // Check if there is a delay specified in the opts object
        if (data.opts.delay) {
            // if so, set the callback on the device object
            this.queuedCallback = callback;
            // Set a timeout function which will play the track on the speaker when the timeout fires
            this.queuedTimeout = setTimeout(() => {
                // When the timeout is fired clear the corresponding variables from the device object
                this.queuedCallback = null;
                this.queuedTimeout = null;
                // Call the function which will play the track
                this._playTrack(data.opts, data.track, callback);
            }, data.opts.delay); // set the timeout for the given delay in the opts object
        } else {
            // Call the function which will play the track
            this._playTrack(data.opts, data.track, callback);
        }
    }
}

Seeking

Once an activated speaker has an active track it should be prepared for user interactions such as seeking. In order for your app to do so it will need to listen to a 'setPosition' event that will be emitted by the device. the position value will indicate the exact position, in ms, of the track to seek to.

/app.js

// set setPosition listener
speaker.on('setPosition', (position, callback) => {
    // seek to specified ms position
});