Merge pull request #20 from icedream/impl/webchimera

Use LibVLC via WebChimera.js instead of VLC through its HTTP API.

This closes #17 and fixes #10, finishes two tasks of #2 and actually solves a few additional issues like:

- Not being able to resume playback from a stopped playlist.
- Unclear and buggy definition of volume range.
- Bot just showing "playing next playlist entry" instead of actually showing the title of the next playlist entry.
- Missing "previous" command to go to previous playlist entry.
- Missing "loop" command to loop the playlist. (Now exists in form of "loop on"/"loop off")
- Missing "stop-after" command to stop playback after current track ends.
- A small bug in ts3query.iced that didn't escape line breaks. (Was in use here for "playlist"/"list" commands which got removed in the process.)

Known issues:
- Starting with an empty playlist, adding an entry and then typing "next" will automatically play the first entry. This is not really a bug or even an issue but definitely unexpected behavior that is coming from VLC.
develop
Carl Kittelberger 2015-10-28 00:23:55 +01:00
commit 034d12df07
4 changed files with 109 additions and 114 deletions

118
app.iced
View File

@ -46,13 +46,16 @@ await services.find("pulseaudio").start defer err
if err
log.warn "PulseAudio could not start up, audio may not act as expected!"
# VLC HTTP API
await services.find("vlc").start defer err
# VLC via WebChimera.js
vlcService = services.find("vlc")
await vlcService.start defer err, vlc
if err
log.warn "VLC could not start up!"
await module.exports.shutdown defer()
process.exit 1
vlc = services.find("vlc").instance
# Cached information for tracks in playlist
vlcMediaInfo = {}
# TeamSpeak3
ts3clientService = services.find("ts3client")
@ -60,6 +63,16 @@ ts3clientService = services.find("ts3client")
ts3clientService.on "started", (ts3proc) =>
ts3query = ts3clientService.query
# VLC event handling
vlc.onPlaying = () =>
info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl]
ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info.originalUrl}]#{info.title}[/URL]."
vlc.onPaused = () => ts3query.sendtextmessage 2, 0, "Paused."
vlc.onForward = () => ts3query.sendtextmessage 2, 0, "Fast-forwarding..."
vlc.onBackward = () => ts3query.sendtextmessage 2, 0, "Rewinding..."
vlc.onEncounteredError = () => log.error "VLC has encountered an error! You will need to restart the bot.", arguments
vlc.onStopped = () => ts3query.sendtextmessage 2, 0, "Stopped."
ts3query.currentScHandlerID = 1
ts3query.mydata = {}
@ -145,7 +158,8 @@ ts3clientService.on "started", (ts3proc) =>
switch name.toLowerCase()
when "pause"
vlc.status.pause()
# now we can toggle-pause playback this easily! yay!
vlc.togglePause()
return
when "play"
inputBB = paramline.trim()
@ -153,7 +167,7 @@ ts3clientService.on "started", (ts3proc) =>
# we gonna interpret play without a url as an attempt to unpause the current song
if input.length <= 0
vlc.status.resume()
vlc.play()
return
# only allow playback from file if it's a preconfigured alias
@ -168,11 +182,7 @@ ts3clientService.on "started", (ts3proc) =>
# TODO: permission system to check if uid is allowed to play this url or alias
await vlc.status.empty defer(err)
if err
log.warn "Couldn't empty VLC playlist", err
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
return
vlc.playlist.clear()
# let's give youtube-dl a shot!
await youtubedl.getInfo input, [
@ -189,24 +199,51 @@ ts3clientService.on "started", (ts3proc) =>
if not info.url?
info.url = input
info.title = input # URL as title
info.originalUrl = input
vlcMediaInfo[info.url] = info
await vlc.status.play info.url, defer(err)
if err
vlc.status.empty()
log.warn "VLC API returned an error when trying to play", err
ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?"
# play it in VLC
vlc.play info.url
when "stop-after"
vlc.playlist.mode = vlc.playlist.Single
ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item."
when "loop"
inputBB = paramline
input = null
switch (removeBB paramline).toLowerCase().trim()
when ""
# just show current mode
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is #{if vlc.playlist.mode == vlc.playlist.Loop then "on" else "off"}."
when "on"
# enable looping
vlc.playlist.mode = vlc.playlist.Loop
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now on."
when "off"
# disable looping
vlc.playlist.mode = vlc.playlist.Normal
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now off."
else
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} on|off[/B] - Turns playlist looping on or off"
return
ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]."
when "next"
await vlc.status.next defer(err)
if err
vlc.status.empty()
log.warn "VLC API returned an error when trying to skip current song", err
ts3query.sendtextmessage args.targetmode, invoker.id, "This didn't work. Does the playlist have multiple songs?"
if vlc.playlist.items.count == 0
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
return
ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist."
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == vlc.playlist.items.count - 1
ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to next playlist item, this is the last one!"
return
vlc.playlist.next()
when "prev", "previous"
if vlc.playlist.items.count == 0
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
return
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem <= 0
ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to previous playlist item, this is the first one!"
return
vlc.playlist.prev()
when "empty", "clear"
vlc.playlist.clear()
ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist."
when "enqueue", "add", "append"
inputBB = paramline.trim()
input = (removeBB paramline).trim()
@ -242,36 +279,25 @@ ts3clientService.on "started", (ts3proc) =>
if not info.url?
info.url = input
info.title = input # URL as title
info.originalUrl = input
vlcMediaInfo[info.url] = info
await vlc.status.enqueue info.url, defer(err)
if err
vlc.status.empty()
log.warn "VLC API returned an error when trying to play", err
ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?"
return
# add it in VLC
vlc.playlist.add info.url
ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist."
# TODO: Do we need to make sure that vlc.playlist.mode is not set to "Single" here or is that handled automatically?
when "stop"
await vlc.status.stop defer(err)
vlc.status.empty()
ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback."
vlc.stop()
when "vol"
vol = parseInt paramline
if paramline.trim().length <= 0 or vol > 511 or vol < 0
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol <number>[/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup."
if paramline.trim().length <= 0 or vol == NaN or vol > 200 or vol < 0
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol <number>[/B] - takes a number between 0 (0%) and 200 (200%) to set the volume. 100% is 100. Defaults to 50 (50%) on startup."
return
await vlc.status.volume paramline, defer(err)
if err
log.warn "Failed to set volume", err
ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out."
return
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set."
vlc.audio.volume = vol
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%."
when "changenick"
nick = if paramline.length > params[0].length then paramline else params[0]
if nick.length < 3 or nick.length > 30

View File

@ -39,7 +39,7 @@
"string.prototype.startswith": "^0.2.0",
"sync": "^0.2.5",
"valid-url": "^1.0.9",
"vlc-api": "0.0.0",
"webchimera.js": "^0.1.38",
"which": "^1.1.2",
"winston": "^1.0.1",
"xvfb": "git://github.com/icedream/node-xvfb.git",

View File

@ -1,88 +1,43 @@
spawn = require("child_process").spawn
services = require("../services")
config = require("../config")
VLCApi = require("vlc-api")
wc = require("webchimera.js")
StreamSplitter = require("stream-splitter")
require_bin = require("../require_bin")
vlcBinPath = require_bin "vlc"
module.exports = class VLCService extends services.Service
dependencies: [
"pulseaudio"
]
constructor: -> super "VLC",
###
# Starts an instance of VLC and keeps it ready for service.
###
start: (cb) ->
if @_process
cb? null, @_process
if @_instance
cb? null, @_instance
return
calledCallback = false
proc = null
doStart = null
doStart = () =>
await services.find("pulseaudio").start defer(err)
if err
throw new Error "Dependency pulseaudio failed."
proc = spawn vlcBinPath, [
"-I", "http",
"--http-host", config.get("vlc-host"),
"--http-port", config.get("vlc-port"),
"--http-password", config.get("vlc-password")
instance = wc.createPlayer [
"--aout", "pulse",
"--volume", "128", # 50% volume
"--no-video"
],
stdio: ['ignore', 'pipe', 'pipe']
detached: true
]
instance.audio.volume = 50
# logging
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
stdoutTokenizer.encoding = "utf8";
stdoutTokenizer.on "token", (token) =>
token = token.trim() # get rid of \r
@log.debug token
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
stderrTokenizer.encoding = "utf8";
stderrTokenizer.on "token", (token) =>
token = token.trim() # get rid of \r
@log.debug token
proc.on "exit", () =>
if @state == "stopping"
return
if not calledCallback
calledCallback = true
@log.warn "VLC terminated unexpectedly during startup."
cb? new Error "VLC terminated unexpectedly."
@log.warn "VLC terminated unexpectedly, restarting."
doStart()
@_process = proc
doStart()
setTimeout (() =>
if not calledCallback
calledCallback = true
@instance = new VLCApi
host: ":#{encodeURIComponent config.get("vlc-password")}@#{config.get("vlc-host")}",
port: config.get("vlc-port")
cb? null, @instance), 1500 # TODO: Use some more stable condition
@_instance = instance
cb? null, @_instance
###
# Shuts down the VLC instance.
###
stop: (cb) ->
if not @_process
if not @_instance
cb?()
return
@instance = null
@_process.kill()
await @_process.once "exit", defer()
# TODO: Is there even a proper way to shut this down?
@_instance = null
cb?()

View File

@ -10,9 +10,23 @@ merge = require "merge"
parserLog = getLogger "parser"
escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s")
escape = (value) => value.toString()\
.replace(/\\/g, "\\\\")\
.replace(/\//g, "\\/")\
.replace(/\|/g, "\\|")\
.replace(/\n/g, "\\n")\
.replace(/\r/g, "\\r")\
.replace(/\t/g, "\\t")\
.replace(/\ /g, "\\s")
unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\")
unescape = (value) => value.toString()\
.replace(/\\s/g, " ")\
.replace(/\\t/g, "\t")\
.replace(/\\r/g, "\r")\
.replace(/\\n/g, "\n")\
.replace(/\\\|/g, "|")\
.replace(/\\\//g, "/")\
.replace(/\\\\/g, "\\")
buildCmd = (name, namedArgs, posArgs) =>
# TODO: Add support for collected arguments (aka lists)