UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

284 lines (252 loc) 7.38 kB
/** * * simple sound plugin based on the npm package play-sound. * Adds PowerShell mediaplayer support for Windows. * * @requires play-sound * @requires plugin * * @module sound/sound * @copyright Lasse Marburg 2026 * @license MIT */ /** * */ var player = null const { execFile, ChildProcess } = require("child_process") const path = require("path") const plugin = require("../plugin.js") const file = require("../../file.js") var schema = require("./schema.json") class Sound extends plugin.Plugin { constructor() { super(schema) this.autoconnect = true } async setup(config, game) { await super.setup(config, game) this.addTemplate("play", (payload, action) => { const title = `sound ${action.name}` let footer = { text: "" } let subtitle const body = [] if (payload.loop) { if (payload.loop < 0) { footer["text"] = "Repeat infinite " } else if (payload.loop != 1) { footer["text"] = `Repeat ${payload.loop}x ` } } for (let track of payload.tracks) { body.push({ text: `${track}` }) } if (payload.next) { footer["text"] += `then: ${payload.next}` footer["next"] = payload.next } if (footer.text != "") { body.push(footer) } return { title, subtitle, body } }) // @todo play a silent testsound to check if player is available this.connected = true return this.schema } async connect() { const selected_player = this.settings?.player if ( selected_player && selected_player !== "auto" && selected_player !== "powershell" ) { // Settings explicitly set a player player = require("play-sound")({ player: selected_player }) this.log.info(`Using ${selected_player} for audio playback`) } else if ( process.platform === "win32" && (!selected_player || selected_player === "auto" || selected_player === "powershell") ) { // Windows: no preference, auto, or powershell selected const powerShellPlayer = new PowerShellPlayer() if (await powerShellPlayer.test()) { this.log.info( "Using PowerShell mediaplayer for audio playback on Windows" ) player = powerShellPlayer } else { if (selected_player === "powershell") { // Selected powershell but it's not available this.log.warn( "PowerShell mediaplayer selected but unavailable, falling back to auto-detect" ) } player = require("play-sound")({}) this.log.info(`Using ${player.player} for audio playback`) } } else { // Non-Windows auto detect player = require("play-sound")({}) this.log.info(`Using ${player.player} for audio playback`) } } update(settings) { if (this.settings.player !== settings.player) { this.settings = settings this.connect() } } async play(data, session) { let playback = new Playback(data, session, this.game) await playback.start() return playback.cancel.bind(playback) } } class Playback { /** * @type {ChildProcess} * child process executing the current audio playback */ process constructor(properties, session, game) { Object.assign(this, properties) this.session = session this.game = game } async start() { this.donePlayingCallback = this.session.getCallback( this.donePlaying.bind(this) ) if (this.loop < 0) { this.loop = 99999 } if (!this.loop) { this.loop = 1 } this.play_index = 0 await this.playNext(this.tracks[0]) } playNext(track) { return new Promise((resolve, reject) => { if (this.status == "canceled") { return } if (!track || typeof track !== "string" || !track.trim()) { throw new adaptor.InvalidError(`Invalid audio track: "${track}"`) } this.current_audiofile = this.game.getFilePath(track) if (!file.exists(this.current_audiofile)) { throw new adaptor.NotFoundError( `Could not find audiofile ${this.current_audiofile}` ) } this.process = player.play(this.current_audiofile, async (err) => { if (this.status == "canceled") { return } this.donePlayingCallback(err) }) if (!(this.process instanceof ChildProcess)) { return reject( new Error( `Could not play audio ${this.current_audiofile}. Unable to spawn process with audioplayer: ${player.player}` ) ) } this.process.on("error", (err) => { return reject( new Error( `Could not play audio ${this.current_audiofile} with audioplayer: ${player.player}\n${err.message}` ) ) }) this.process.on("spawn", () => { this.session.log.info( `Play audio ${this.current_audiofile} with ${player.player}` ) return resolve() }) }) } async donePlaying(err) { if (err) { // err.code === 1 means unsupported format from PowerShell mediaplayer if (err.code === 1) { throw new adaptor.InvalidError( `Unsupported audio format: ${path.extname(this.current_audiofile)}` ) } throw new Error(err) } this.session.log("Done playing " + this.tracks[this.play_index]) this.play_index++ if (this.play_index >= this.tracks.length) { this.play_index = 0 this.loop-- if (this.loop <= 0) { this.session.log.debug("Played last track in playlist") if (this.next) { this.session.next(this.next) } return } } try { await this.playNext(this.tracks[this.play_index]) } catch (error) { throw error } } cancel() { this.status = "canceled" if (this.process) { this.process.kill() } } } class PowerShellPlayer { constructor() { this.player = "PowerShellMediaPlayer" } /** * Tests whether PowerShell's presentationCore MediaPlayer is available. * @returns {Promise<boolean>} */ test() { return new Promise((resolve) => { const ps = [ "Add-Type -AssemblyName presentationCore;", "New-Object system.windows.media.mediaplayer;", "exit 0" ].join(" ") execFile("powershell", ["-Command", ps], (err) => { resolve(!err) }) }) } /** * Play a file silently on Windows using PowerShell's MediaPlayer. * Returns the ChildProcess so it can be killed. * @param {string} filePath - Absolute path to the audio file * @param {function} cb - Callback(err) * @returns {ChildProcess} */ play(filePath, cb) { const abs = path.resolve(filePath).replace(/'/g, "''") const ps = [ "Add-Type -AssemblyName presentationCore;", "$p = New-Object system.windows.media.mediaplayer;", `$p.open('${abs}');`, "$p.Play();", "$timeout = 25; $i = 0;", "while ($p.HasAudio -eq $false -and $i -lt $timeout) { Start-Sleep -m 100; $i++ };", "if ($p.HasAudio -eq $false) { exit 1 };", "Start-Sleep -s $p.NaturalDuration.TimeSpan.TotalSeconds" ].join(" ") return execFile("powershell", ["-Command", ps], cb) } } module.exports.Plugin = Sound