« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/players.js
diff options
context:
space:
mode:
Diffstat (limited to 'players.js')
-rw-r--r--players.js206
1 files changed, 49 insertions, 157 deletions
diff --git a/players.js b/players.js
index be5205f..9941ce5 100644
--- a/players.js
+++ b/players.js
@@ -1,9 +1,4 @@
-// stolen from http-music
-
-const { spawn } = require('child_process')
-const FIFO = require('fifo-js')
 const EventEmitter = require('events')
-const { commandExists, killProcess } = require('./general-util')
 
 function getTimeStrings({curHour, curMin, curSec, lenHour, lenMin, lenSec}) {
   // Multiplication casts to numbers; addition prioritizes strings.
@@ -51,21 +46,6 @@ class Player extends EventEmitter {
     this.disablePlaybackStatus = false
   }
 
-  set process(newProcess) {
-    this._process = newProcess
-    this._process.on('exit', code => {
-      if (code !== 0 && !this._killed) {
-        this.emit('crashed', code)
-      }
-
-      this._killed = false
-    })
-  }
-
-  get process() {
-    return this._process
-  }
-
   playFile(file) {}
   seekAhead(secs) {}
   seekBack(secs) {}
@@ -73,12 +53,7 @@ class Player extends EventEmitter {
   volDown(amount) {}
   togglePause() {}
 
-  async kill() {
-    if (this.process) {
-      this._killed = true
-      await killProcess(this.process)
-    }
-  }
+  async kill() {}
 
   printStatusLine(data) {
     // Quick sanity check - we don't want to print the status line if it's
@@ -90,160 +65,77 @@ class Player extends EventEmitter {
   }
 }
 
-module.exports.MPVPlayer = class extends Player {
-  getMPVOptions(file) {
-    return ['--no-video', file]
-  }
-
-  playFile(file) {
-    // The more powerful MPV player. MPV is virtually impossible for a human
-    // being to install; if you're having trouble with it, try the SoX player.
-
-    this.process = spawn('mpv', this.getMPVOptions(file))
-
-    this.process.stderr.on('data', data => {
-      if (this.disablePlaybackStatus) {
-        return
-      }
-
-      const match = data.toString().match(
-        /(..):(..):(..) \/ (..):(..):(..) \(([0-9]+)%\)/
-      )
-
-      if (match) {
-        const [
-          curHour, curMin, curSec, // ##:##:##
-          lenHour, lenMin, lenSec, // ##:##:##
-          percent // ###%
-        ] = match.slice(1)
+module.exports.WebPlayer = class extends Player {
+  constructor() {
+    super()
 
-        this.printStatusLine(getTimeStrings({curHour, curMin, curSec, lenHour, lenMin, lenSec}))
-      }
+    const secToMore = time => ({
+      hour: Math.floor(time / 3600),
+      min: Math.floor((time % 3600) / 60),
+      sec: Math.floor(time % 60)
     })
 
-    return new Promise(resolve => {
-      this.process.once('close', resolve)
-    })
-  }
-}
+    setInterval(() => {
+      if (!this.audioEl) return
+
+      const { hour: curHour, min: curMin, sec: curSec } = secToMore(this.audioEl.currentTime)
+      const { hour: lenHour, min: lenMin, sec: lenSec } = secToMore(this.audioEl.duration)
 
-module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer {
-  getMPVOptions(file) {
-    return ['--input-file=' + this.fifo.path, ...super.getMPVOptions(file)]
+      this.printStatusLine(getTimeStrings({
+        curHour, curMin, curSec,
+        lenHour, lenMin, lenSec
+      }))
+    }, 50)
   }
 
   playFile(file) {
-    this.fifo = new FIFO()
-
-    return super.playFile(file)
-  }
+    this.audioEl = document.createElement('audio')
+    this.audioEl.src = file
+    this.audioEl.play()
+
+    return Promise.race([
+      new Promise(resolve => this.stopPromise = resolve),
+      new Promise(resolve => {
+        const handleEnded = () => {
+          this.audioEl.removeEventListener('ended', handleEnded)
+          resolve()
+        }
 
-  sendCommand(command) {
-    if (this.fifo) {
-      this.fifo.write(command)
-    }
+        this.audioEl.addEventListener('ended', handleEnded)
+      })
+    ])
   }
 
   seekAhead(secs) {
-    this.sendCommand(`seek +${parseFloat(secs)}`)
+    if (!this.audioEl) return
+    this.audioEl.currentTime += secs
   }
 
   seekBack(secs) {
-    this.sendCommand(`seek -${parseFloat(secs)}`)
-  }
-
-  volUp(amount) {
-    this.sendCommand(`add volume +${parseFloat(amount)}`)
-  }
-
-  volDown(amount) {
-    this.sendCommand(`add volume -${parseFloat(amount)}`)
+    if (!this.audioEl) return
+    this.audioEl.currentTime -= secs
   }
 
   togglePause() {
-    this.sendCommand('cycle pause')
+    if (!this.audioEl) return
+    if (this.audioEl.paused) {
+      this.audioEl.play()
+    } else {
+      this.audioEl.pause()
+    }
   }
 
   kill() {
-    if (this.fifo) {
-      this.fifo.close()
-      delete this.fifo
+    if (!this.audioEl) return
+    this.audioEl.currentTime = 0
+    this.audioEl.pause()
+    if (this.stopPromise) {
+      this.stopPromise()
     }
-
-    return super.kill()
-  }
-}
-
-module.exports.SoXPlayer = class extends Player {
-  playFile(file) {
-    // SoX's play command is useful for systems that don't have MPV. SoX is
-    // much easier to install (and probably more commonly installed, as well).
-    // You don't get keyboard controls such as seeking or volume adjusting
-    // with SoX, though.
-
-    this.process = spawn('play', [file])
-
-    this.process.stdout.on('data', data => {
-      process.stdout.write(data.toString())
-    })
-
-    // Most output from SoX is given to stderr, for some reason!
-    this.process.stderr.on('data', data => {
-      // The status line starts with "In:".
-      if (data.toString().trim().startsWith('In:')) {
-        if (this.disablePlaybackStatus) {
-          return
-        }
-
-        const timeRegex = '([0-9]*):([0-9]*):([0-9]*)\.([0-9]*)'
-        const match = data.toString().trim().match(new RegExp(
-          `^In:([0-9.]+%)\\s*${timeRegex}\\s*\\[${timeRegex}\\]`
-        ))
-
-        if (match) {
-          const percentStr = match[1]
-
-          // SoX takes a loooooot of math in order to actually figure out the
-          // duration, since it outputs the current time and the remaining time
-          // (but not the duration).
-
-          const [
-            curHour, curMin, curSec, curSecFrac, // ##:##:##.##
-            remHour, remMin, remSec, remSecFrac // ##:##:##.##
-          ] = match.slice(2).map(n => parseInt(n))
-
-          const duration = Math.round(
-            (curHour + remHour) * 3600 +
-            (curMin + remMin) * 60 +
-            (curSec + remSec) * 1 +
-            (curSecFrac + remSecFrac) / 100
-          )
-
-          const lenHour = Math.floor(duration / 3600)
-          const lenMin = Math.floor((duration - lenHour * 3600) / 60)
-          const lenSec = Math.floor(duration - lenHour * 3600 - lenMin * 60)
-
-          this.printStatusLine(getTimeStrings({curHour, curMin, curSec, lenHour, lenMin, lenSec}))
-        }
-      }
-    })
-
-    return new Promise(resolve => {
-      this.process.on('close', () => resolve())
-    })
+    delete this.audioEl
   }
 }
 
 module.exports.getPlayer = async function() {
-  if (await commandExists('mpv')) {
-    if (await commandExists('mkfifo')) {
-      return new module.exports.ControllableMPVPlayer()
-    } else {
-      return new module.exports.MPVPlayer()
-    }
-  } else if (await commandExists('play')) {
-    return new module.exports.SoXPlayer()
-  } else {
-    return null
-  }
+  return new module.exports.WebPlayer()
 }