mpd-avr-client
Version:
AVR as a MPD client
247 lines (208 loc) • 9.31 kB
JavaScript
const { readFile } = require('fs');
const { of, Observable } = require('rxjs');
const { shareReplay, delay, switchMap, take } = require('rxjs/operators');
const { playRegExp } = require('../const');
const AvrService = require('../services/avr-service');
const MpService = require('../services/mp-service');
const tvServiceFactory = require('../services/tv-service-factory');
const ledServiceFactory = require('../services/led-service-factory');
const AvrAudioSourceSwitchReducer = function (_commandOptions) {
return ((commandOptions) => {
const {
handOverAudioToTvCecCommand,
audioVolumePresetForTv,
infraredRemoteControlMappingForTv,
} = /** @type {MainCommandOptions} */ commandOptions;
const avrService = new AvrService(commandOptions);
const mpService = new MpService();
const tvService = tvServiceFactory(commandOptions);
const ledService = ledServiceFactory(commandOptions);
const lircCodeIrccCodeMap$ = new Observable((subscriber) => {
readFile(infraredRemoteControlMappingForTv, (err, data) => {
if (err) {
return subscriber.error(err);
}
try {
const lircCodeIrccCodePairs = JSON.parse(data.toString())
.map(([_, lircCode, irccCode]) => [lircCode, irccCode])
.filter(([lircCode, irccCode]) => lircCode && irccCode);
subscriber.next(new Map(lircCodeIrccCodePairs));
subscriber.complete();
} catch (err) {
subscriber.error(err);
subscriber.complete();
}
});
return () => {};
}).pipe(shareReplay());
/**
* Get the initial state
* @returns {[AvrVolumeStatus, MpStatusStateTransition]} The initial state
*/
const getInitState = () => [[], [undefined, undefined]];
/**
* Request the AVR audio volume
* @returns {[AvrVolumeStatus, MpStatusStateTransition]} The AVR volume request state
*/
const onRequestAudioVolume = () => {
avrService.requestAudioVolume();
return [[undefined], [undefined, undefined]];
};
/**
* Handle the pause action
* @param {AppState} appState The current MPD playback state
* @param {AvrVolumeStatus} avrVolumeStatus The current AVR audio volume status
* @returns {[AvrVolumeStatus, MpStatusStateTransition]} Returns a tuple with the AVR volume status and the MP status states
*/
const onPause = ({ state }, avrVolumeStatus) => {
if (playRegExp.test(state)) {
avrService.updateOsd('pause');
/**
* @desc Unfortunately, there is a mysterious incompatibility issue between cec-client and netcat that prevents sending commands to them in proximity, some magic number is used here
*/
setTimeout(() => mpService.pause(), 500);
return [avrVolumeStatus, [state, undefined]];
}
switchAudioSource(avrVolumeStatus);
return [avrVolumeStatus, [state, state]];
};
/**
* Switch audio source
* @param {AvrVolumeStatus} avrVolumeStatus The current AVR Audio Status
* @returns {void} No output
*/
const switchAudioSource = (avrVolumeStatus) => {
avrService.runCommand(handOverAudioToTvCecCommand);
if (tvService.isEnabled()) {
tvService.wakeAndLaunchApp();
}
if (ledService.isEnabled()) {
ledService.wake();
}
if (audioVolumePresetForTv !== undefined) {
of(audioVolumePresetForTv)
.pipe(
/**
* @desc Unfortunately, there is a limitation to how frequently commands can be transmitted to the AVR, some magic number is used here
*/
delay(500),
switchMap(() =>
avrService.adjustAudioVolume(
avrVolumeStatus,
audioVolumePresetForTv
)
),
take(1)
)
.subscribe();
}
};
/**
* @desc The aim of this reducer is to switch audio source to a Smart TV which is connected to a non-HDMI audio input (on the AVR).
* The following objectives are listed chronologically, and they are executed sequentially to avoid timing issues
* - request AVR audio volume
* - request MP playback pause
* - request AVR audio source switch, request Smart TV to wake and launch app
* - request AVR audio volume adjustment
*/
return [
(acc, [event, appState]) => {
const [avrVolumeStatus, [fromMpStatusState, toMpStatusState]] =
/** @type {[AvrVolumeStatus, MpStatusStateTransition]} */ acc;
const { source } =
/** @type {CecClientEvent|MpClientEvent|LircClientEvent} */ event;
switch (source) {
case 'cecClient':
const cecClientEvent = /** @type CecClientEvent */ event;
const [_isAudioDeviceOn] =
avrService.decodeAvrPowerStatus(cecClientEvent);
// if the CEC transmission is regarding AVR toggling sleep mode
if (_isAudioDeviceOn !== undefined) {
// then reset the reducer and request the TV and LED strip to go to sleep
tvService.isEnabled() && tvService.standBy();
ledService.isEnabled() && ledService.standBy();
return getInitState();
}
// if the CEC transmission is not regarding AVR toggling sleep mode, and
// if the reducer is in the middle of pausing, or if the reducer has completed pausing playback
if (fromMpStatusState) {
// then ignore the CEC transmission
return acc;
}
// if the CEC transmission is not regarding AVR toggling sleep mode, and
// if the reducer has not requested a pause on playback, and
// if the reducer has not requested the AVR volume
if (!avrVolumeStatus.length) {
// then handle the CEC transmission as a potential request for AVR volume
return avrService.isAvrRequestSwitchAudioSource(cecClientEvent)
? onRequestAudioVolume()
: getInitState();
}
const _avrVolumeStatus =
avrService.decodeAvrVolumeStatus(cecClientEvent);
// if the CEC transmission is not regarding AVR toggling sleep mode, and
// if the reducer has not requested a pause on playback, and
// if the reducer has requested the AVR volume, and
// if the reducer has not received an AVR volume response,
// then handle the CEC transmission as a potential AVR volume response
return avrService.isAvrVolumeStatsValid(_avrVolumeStatus)
? onPause(appState, _avrVolumeStatus)
: acc;
case 'mpClient':
const mpClientEvent = /** @type MpClientEvent */ event;
// if the audio source has been switched
if (fromMpStatusState && toMpStatusState) {
// then ignore the MP status
return acc;
}
// if the audio source has not been switched, and
// if the reducer has not requested a pause on the playback
if (!fromMpStatusState) {
// then reset the reducer
return getInitState();
}
const { data: mpStatus } = mpClientEvent;
// if the audio source has not been switched, and
// if the reducer has requested a pause on the playback, and
// if MPD decides to keep playing
if (playRegExp.test(mpStatus.state)) {
// then reset the reducer
return getInitState();
}
// if the audio source has not been switched, and
// if the reducer has requested a pause on the playback, and
// if MPD confirms playback is paused,
// then switch the audio source
/**
* @desc Unfortunately, there is a mysterious incompatibility issue between cec-client and netcat that prevents sending commands to them in proximity, some magic number is used here
*/
setTimeout(() => switchAudioSource(avrVolumeStatus), 500);
return [avrVolumeStatus, [fromMpStatusState, mpStatus.state]];
case 'lircClient':
const lircClientEvent = /** @type LircClientEvent */ event;
// if the audio source has not switched
if (!fromMpStatusState || !toMpStatusState) {
// then no-op
return acc;
}
const { data: lircCode } = lircClientEvent;
// if the audio source has switched,
// then handle the infrared transmission
tvService.isEnabled() &&
lircCodeIrccCodeMap$.subscribe((lircCodeIrccCodeMap) => {
const irccCode = lircCodeIrccCodeMap.get(lircCode);
if (!irccCode) {
return;
}
tvService.sendIrccCode(irccCode);
});
return acc;
default:
return getInitState();
}
},
getInitState(),
];
})(_commandOptions);
};
module.exports = AvrAudioSourceSwitchReducer;