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

120
app.iced
View File

@ -46,13 +46,16 @@ await services.find("pulseaudio").start defer err
if err if err
log.warn "PulseAudio could not start up, audio may not act as expected!" log.warn "PulseAudio could not start up, audio may not act as expected!"
# VLC HTTP API # VLC via WebChimera.js
await services.find("vlc").start defer err vlcService = services.find("vlc")
await vlcService.start defer err, vlc
if err if err
log.warn "VLC could not start up!" log.warn "VLC could not start up!"
await module.exports.shutdown defer() await module.exports.shutdown defer()
process.exit 1 process.exit 1
vlc = services.find("vlc").instance
# Cached information for tracks in playlist
vlcMediaInfo = {}
# TeamSpeak3 # TeamSpeak3
ts3clientService = services.find("ts3client") ts3clientService = services.find("ts3client")
@ -60,6 +63,16 @@ ts3clientService = services.find("ts3client")
ts3clientService.on "started", (ts3proc) => ts3clientService.on "started", (ts3proc) =>
ts3query = ts3clientService.query 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.currentScHandlerID = 1
ts3query.mydata = {} ts3query.mydata = {}
@ -145,7 +158,8 @@ ts3clientService.on "started", (ts3proc) =>
switch name.toLowerCase() switch name.toLowerCase()
when "pause" when "pause"
vlc.status.pause() # now we can toggle-pause playback this easily! yay!
vlc.togglePause()
return return
when "play" when "play"
inputBB = paramline.trim() 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 # we gonna interpret play without a url as an attempt to unpause the current song
if input.length <= 0 if input.length <= 0
vlc.status.resume() vlc.play()
return return
# only allow playback from file if it's a preconfigured alias # 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 # TODO: permission system to check if uid is allowed to play this url or alias
await vlc.status.empty defer(err) vlc.playlist.clear()
if err
log.warn "Couldn't empty VLC playlist", err
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
return
# let's give youtube-dl a shot! # let's give youtube-dl a shot!
await youtubedl.getInfo input, [ await youtubedl.getInfo input, [
@ -189,24 +199,51 @@ ts3clientService.on "started", (ts3proc) =>
if not info.url? if not info.url?
info.url = input info.url = input
info.title = input # URL as title info.title = input # URL as title
info.originalUrl = input
vlcMediaInfo[info.url] = info
await vlc.status.play info.url, defer(err) # play it in VLC
if err vlc.play info.url
vlc.status.empty() when "stop-after"
log.warn "VLC API returned an error when trying to play", err vlc.playlist.mode = vlc.playlist.Single
ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?" ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item."
return when "loop"
inputBB = paramline
ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]." 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
when "next" when "next"
await vlc.status.next defer(err) if vlc.playlist.items.count == 0
if err ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
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?"
return return
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == vlc.playlist.items.count - 1
ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist." 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" when "enqueue", "add", "append"
inputBB = paramline.trim() inputBB = paramline.trim()
input = (removeBB paramline).trim() input = (removeBB paramline).trim()
@ -242,36 +279,25 @@ ts3clientService.on "started", (ts3proc) =>
if not info.url? if not info.url?
info.url = input info.url = input
info.title = input # URL as title info.title = input # URL as title
info.originalUrl = input
vlcMediaInfo[info.url] = info
await vlc.status.enqueue info.url, defer(err) # add it in VLC
if err vlc.playlist.add info.url
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
ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist." 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" when "stop"
await vlc.status.stop defer(err) vlc.stop()
vlc.status.empty()
ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback."
when "vol" when "vol"
vol = parseInt paramline vol = parseInt paramline
if paramline.trim().length <= 0 or vol > 511 or vol < 0 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 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." 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 return
await vlc.status.volume paramline, defer(err) vlc.audio.volume = vol
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%."
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."
when "changenick" when "changenick"
nick = if paramline.length > params[0].length then paramline else params[0] nick = if paramline.length > params[0].length then paramline else params[0]
if nick.length < 3 or nick.length > 30 if nick.length < 3 or nick.length > 30

View File

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

View File

@ -1,88 +1,43 @@
spawn = require("child_process").spawn spawn = require("child_process").spawn
services = require("../services") services = require("../services")
config = require("../config") config = require("../config")
VLCApi = require("vlc-api") wc = require("webchimera.js")
StreamSplitter = require("stream-splitter") StreamSplitter = require("stream-splitter")
require_bin = require("../require_bin")
vlcBinPath = require_bin "vlc"
module.exports = class VLCService extends services.Service module.exports = class VLCService extends services.Service
dependencies: [ dependencies: [
"pulseaudio" "pulseaudio"
] ]
constructor: -> super "VLC", constructor: -> super "VLC",
###
# Starts an instance of VLC and keeps it ready for service.
###
start: (cb) -> start: (cb) ->
if @_process if @_instance
cb? null, @_process cb? null, @_instance
return return
calledCallback = false calledCallback = false
proc = null instance = wc.createPlayer [
doStart = null "--aout", "pulse",
doStart = () => "--no-video"
await services.find("pulseaudio").start defer(err) ]
if err instance.audio.volume = 50
throw new Error "Dependency pulseaudio failed."
proc = spawn vlcBinPath, [ @_instance = instance
"-I", "http", cb? null, @_instance
"--http-host", config.get("vlc-host"),
"--http-port", config.get("vlc-port"),
"--http-password", config.get("vlc-password")
"--aout", "pulse",
"--volume", "128", # 50% volume
"--no-video"
],
stdio: ['ignore', 'pipe', 'pipe']
detached: true
# 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
###
# Shuts down the VLC instance.
###
stop: (cb) -> stop: (cb) ->
if not @_process if not @_instance
cb?() cb?()
return return
@instance = null # TODO: Is there even a proper way to shut this down?
@_instance = null
@_process.kill()
await @_process.once "exit", defer()
cb?() cb?()

View File

@ -10,9 +10,23 @@ merge = require "merge"
parserLog = getLogger "parser" 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) => buildCmd = (name, namedArgs, posArgs) =>
# TODO: Add support for collected arguments (aka lists) # TODO: Add support for collected arguments (aka lists)