Merge branch 'forslund-feature/simple-audio' into dev
commit
2c7c51cea4
|
@ -344,7 +344,12 @@ class AudioService(object):
|
||||||
the tracks.
|
the tracks.
|
||||||
"""
|
"""
|
||||||
self._stop()
|
self._stop()
|
||||||
uri_type = tracks[0].split(':')[0]
|
|
||||||
|
if isinstance(tracks[0], str):
|
||||||
|
uri_type = tracks[0].split(':')[0]
|
||||||
|
else:
|
||||||
|
uri_type = tracks[0][0].split(':')[0]
|
||||||
|
|
||||||
# check if user requested a particular service
|
# check if user requested a particular service
|
||||||
if prefered_service and uri_type in prefered_service.supported_uris():
|
if prefered_service and uri_type in prefered_service.supported_uris():
|
||||||
selected_service = prefered_service
|
selected_service = prefered_service
|
||||||
|
@ -362,6 +367,8 @@ class AudioService(object):
|
||||||
else:
|
else:
|
||||||
LOG.info('No service found for uri_type: ' + uri_type)
|
LOG.info('No service found for uri_type: ' + uri_type)
|
||||||
return
|
return
|
||||||
|
if not selected_service.supports_mime_hints:
|
||||||
|
tracks = [t[0] if isinstance(t, list) else t for t in tracks]
|
||||||
selected_service.clear_list()
|
selected_service.clear_list()
|
||||||
selected_service.add_list(tracks)
|
selected_service.add_list(tracks)
|
||||||
selected_service.play()
|
selected_service.play()
|
||||||
|
|
|
@ -27,6 +27,7 @@ class AudioBackend():
|
||||||
|
|
||||||
def __init__(self, config, bus):
|
def __init__(self, config, bus):
|
||||||
self._track_start_callback = None
|
self._track_start_callback = None
|
||||||
|
self.supports_mime_hints = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supported_uris(self):
|
def supported_uris(self):
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
# Copyright 2017 Mycroft AI Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
import subprocess
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from mycroft.audio.services import AudioBackend
|
|
||||||
from mycroft.messagebus.message import Message
|
|
||||||
from mycroft.util.log import LOG
|
|
||||||
|
|
||||||
|
|
||||||
class Ogg123Service(AudioBackend):
|
|
||||||
"""
|
|
||||||
Audio backend for ogg123 player. This one is rather limited and
|
|
||||||
only implements basic usage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config, bus, name='ogg123'):
|
|
||||||
super(Ogg123Service, self).__init__(config, bus)
|
|
||||||
self.config = config
|
|
||||||
self.process = None
|
|
||||||
self.bus = bus
|
|
||||||
self.name = name
|
|
||||||
self._stop_signal = False
|
|
||||||
self._is_playing = False
|
|
||||||
self.index = 0
|
|
||||||
self.tracks = []
|
|
||||||
|
|
||||||
self.bus.on('Ogg123ServicePlay', self._play)
|
|
||||||
|
|
||||||
def supported_uris(self):
|
|
||||||
return ['file', 'http']
|
|
||||||
|
|
||||||
def clear_list(self):
|
|
||||||
self.tracks = []
|
|
||||||
|
|
||||||
def add_list(self, tracks):
|
|
||||||
self.tracks += tracks
|
|
||||||
LOG.info("Track list is " + str(tracks))
|
|
||||||
|
|
||||||
def _play(self, message=None):
|
|
||||||
""" Implementation specific async method to handle playback.
|
|
||||||
This allows ogg123 service to use the "next method as well
|
|
||||||
as basic play/stop.
|
|
||||||
"""
|
|
||||||
LOG.info('Ogg123Service._play')
|
|
||||||
self._is_playing = True
|
|
||||||
track = self.tracks[self.index]
|
|
||||||
# Indicate to audio service which track is being played
|
|
||||||
if self._track_start_callback:
|
|
||||||
self._track_start_callback(track)
|
|
||||||
|
|
||||||
# Replace file:// uri's with normal paths
|
|
||||||
track = track.replace('file://', '')
|
|
||||||
|
|
||||||
self.process = subprocess.Popen(['ogg123', track])
|
|
||||||
# Wait for completion or stop request
|
|
||||||
while self.process.poll() is None and not self._stop_signal:
|
|
||||||
sleep(0.25)
|
|
||||||
|
|
||||||
if self._stop_signal:
|
|
||||||
self.process.terminate()
|
|
||||||
self.process = None
|
|
||||||
self._is_playing = False
|
|
||||||
return
|
|
||||||
|
|
||||||
self.index += 1
|
|
||||||
# if there are more tracks available play next
|
|
||||||
if self.index < len(self.tracks):
|
|
||||||
self.bus.emit(Message('Ogg123ServicePlay'))
|
|
||||||
else:
|
|
||||||
self._is_playing = False
|
|
||||||
|
|
||||||
def play(self):
|
|
||||||
LOG.info('Call Ogg123ServicePlay')
|
|
||||||
self.index = 0
|
|
||||||
self.bus.emit(Message('Ogg123ServicePlay'))
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
LOG.info('Ogg123ServiceStop')
|
|
||||||
if self._is_playing:
|
|
||||||
self._stop_signal = True
|
|
||||||
while self._is_playing:
|
|
||||||
sleep(0.1)
|
|
||||||
self._stop_signal = False
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def pause(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
# Terminate process to continue to next
|
|
||||||
self.process.terminate()
|
|
||||||
|
|
||||||
def previous(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def lower_volume(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def restore_volume(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def load_service(base_config, bus):
|
|
||||||
backends = base_config.get('backends', [])
|
|
||||||
services = [(b, backends[b]) for b in backends
|
|
||||||
if backends[b]['type'] == 'ogg123' and
|
|
||||||
backends[b].get('active', True)]
|
|
||||||
instances = [Ogg123Service(s[1], bus, s[0]) for s in services]
|
|
||||||
return instances
|
|
|
@ -18,24 +18,46 @@ from time import sleep
|
||||||
from mycroft.audio.services import AudioBackend
|
from mycroft.audio.services import AudioBackend
|
||||||
from mycroft.messagebus.message import Message
|
from mycroft.messagebus.message import Message
|
||||||
from mycroft.util.log import LOG
|
from mycroft.util.log import LOG
|
||||||
|
import mimetypes
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
|
||||||
class Mpg123Service(AudioBackend):
|
def find_mime(path):
|
||||||
|
mime = None
|
||||||
|
if path.startswith('http'):
|
||||||
|
response = Session().head(path, allow_redirects=True)
|
||||||
|
if 200 <= response.status_code < 300:
|
||||||
|
mime = response.headers['content-type']
|
||||||
|
if not mime:
|
||||||
|
mime = mimetypes.guess_mime(path)[0]
|
||||||
|
|
||||||
|
if mime:
|
||||||
|
return mime.split('/')
|
||||||
|
else:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleAudioService(AudioBackend):
|
||||||
"""
|
"""
|
||||||
Audio backend for mpg123 player. This one is rather limited and
|
Simple Audio backend for both mpg123 and the ogg123 player.
|
||||||
only implements basic usage.
|
This one is rather limited and only implements basic usage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config, bus, name='mpg123'):
|
def __init__(self, config, bus, name='simple'):
|
||||||
super(Mpg123Service, self).__init__(config, bus)
|
|
||||||
|
super(SimpleAudioService, self).__init__(config, bus)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.process = None
|
self.process = None
|
||||||
self.bus = bus
|
self.bus = bus
|
||||||
self.name = name
|
self.name = name
|
||||||
self._stop_signal = False
|
self._stop_signal = False
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
|
self.tracks = []
|
||||||
|
self.index = 0
|
||||||
|
self.supports_mime_hints = True
|
||||||
|
mimetypes.init()
|
||||||
|
|
||||||
bus.on('Mpg123ServicePlay', self._play)
|
self.bus.on('SimpleAudioServicePlay', self._play)
|
||||||
|
|
||||||
def supported_uris(self):
|
def supported_uris(self):
|
||||||
return ['file', 'http']
|
return ['file', 'http']
|
||||||
|
@ -52,20 +74,36 @@ class Mpg123Service(AudioBackend):
|
||||||
This allows mpg123 service to use the "next method as well
|
This allows mpg123 service to use the "next method as well
|
||||||
as basic play/stop.
|
as basic play/stop.
|
||||||
"""
|
"""
|
||||||
LOG.info('Mpg123Service._play')
|
LOG.info('SimpleAudioService._play')
|
||||||
self._is_playing = True
|
self._is_playing = True
|
||||||
track = self.tracks[self.index]
|
if isinstance(self.tracks[self.index], list):
|
||||||
|
track = self.tracks[self.index][0]
|
||||||
|
mime = self.tracks[self.index][1]
|
||||||
|
mime = mime.split('/')
|
||||||
|
else: # Assume string
|
||||||
|
track = self.tracks[self.index]
|
||||||
|
mime = find_mime(track)
|
||||||
|
print('MIME: ' + str(mime))
|
||||||
# Indicate to audio service which track is being played
|
# Indicate to audio service which track is being played
|
||||||
if self._track_start_callback:
|
if self._track_start_callback:
|
||||||
self._track_start_callback(track)
|
self._track_start_callback(track)
|
||||||
|
|
||||||
# Replace file:// uri's with normal paths
|
# Replace file:// uri's with normal paths
|
||||||
track = track.replace('file://', '')
|
track = track.replace('file://', '')
|
||||||
|
proc = None
|
||||||
self.process = subprocess.Popen(['mpg123', track])
|
if 'mpeg' in mime[1]:
|
||||||
# Wait for completion or stop request
|
proc = 'mpg123'
|
||||||
while self.process.poll() is None and not self._stop_signal:
|
elif 'ogg' in mime[1]:
|
||||||
sleep(0.25)
|
proc = 'ogg123'
|
||||||
|
elif 'wav' in mime[1]:
|
||||||
|
proc = 'aplay'
|
||||||
|
else:
|
||||||
|
proc = 'mpg123' # If no mime info could be determined gues mp3
|
||||||
|
if proc:
|
||||||
|
self.process = subprocess.Popen([proc, track])
|
||||||
|
# Wait for completion or stop request
|
||||||
|
while self.process.poll() is None and not self._stop_signal:
|
||||||
|
sleep(0.25)
|
||||||
|
|
||||||
if self._stop_signal:
|
if self._stop_signal:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
|
@ -76,25 +114,21 @@ class Mpg123Service(AudioBackend):
|
||||||
self.index += 1
|
self.index += 1
|
||||||
# if there are more tracks available play next
|
# if there are more tracks available play next
|
||||||
if self.index < len(self.tracks):
|
if self.index < len(self.tracks):
|
||||||
self.bus.emit(Message('Mpg123ServicePlay'))
|
self.bus.emit(Message('SimpleAudioServicePlay'))
|
||||||
else:
|
else:
|
||||||
self._is_playing = False
|
self._is_playing = False
|
||||||
|
|
||||||
def play(self):
|
def play(self):
|
||||||
LOG.info('Call Mpg123ServicePlay')
|
LOG.info('Call SimpleAudioServicePlay')
|
||||||
self.index = 0
|
self.index = 0
|
||||||
self.bus.emit(Message('Mpg123ServicePlay'))
|
self.bus.emit(Message('SimpleAudioServicePlay'))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
LOG.info('Mpg123ServiceStop')
|
LOG.info('SimpleAudioServiceStop')
|
||||||
if self._is_playing:
|
self._stop_signal = True
|
||||||
self._stop_signal = True
|
while self._is_playing:
|
||||||
while self._is_playing:
|
sleep(0.1)
|
||||||
sleep(0.01)
|
self._stop_signal = False
|
||||||
self._stop_signal = False
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
pass
|
pass
|
||||||
|
@ -119,7 +153,7 @@ class Mpg123Service(AudioBackend):
|
||||||
def load_service(base_config, bus):
|
def load_service(base_config, bus):
|
||||||
backends = base_config.get('backends', [])
|
backends = base_config.get('backends', [])
|
||||||
services = [(b, backends[b]) for b in backends
|
services = [(b, backends[b]) for b in backends
|
||||||
if backends[b]['type'] == 'mpg123' and
|
if backends[b]['type'] == 'simple' and
|
||||||
backends[b].get('active', True)]
|
backends[b].get('active', True)]
|
||||||
instances = [Mpg123Service(s[1], bus, s[0]) for s in services]
|
instances = [SimpleAudioService(s[1], bus, s[0]) for s in services]
|
||||||
return instances
|
return instances
|
|
@ -291,11 +291,7 @@
|
||||||
"Audio": {
|
"Audio": {
|
||||||
"backends": {
|
"backends": {
|
||||||
"local": {
|
"local": {
|
||||||
"type": "mpg123",
|
"type": "simple",
|
||||||
"active": true
|
|
||||||
},
|
|
||||||
"ogg": {
|
|
||||||
"type": "ogg123",
|
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
"vlc": {
|
"vlc": {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import json
|
||||||
import time
|
import time
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from threading import Event
|
from threading import Event
|
||||||
|
import traceback
|
||||||
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from websocket import (WebSocketApp, WebSocketConnectionClosedException,
|
from websocket import (WebSocketApp, WebSocketConnectionClosedException,
|
||||||
|
@ -144,7 +145,7 @@ class WebsocketClient(object):
|
||||||
# ValueError occurs on pyee 1.0.1 removing handlers
|
# ValueError occurs on pyee 1.0.1 removing handlers
|
||||||
# registered with once.
|
# registered with once.
|
||||||
# KeyError may theoretically occur if the event occurs as
|
# KeyError may theoretically occur if the event occurs as
|
||||||
# the handler is removbed
|
# the handler is removed
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
return response[0]
|
return response[0]
|
||||||
|
@ -157,9 +158,31 @@ class WebsocketClient(object):
|
||||||
|
|
||||||
def remove(self, event_name, func):
|
def remove(self, event_name, func):
|
||||||
try:
|
try:
|
||||||
|
if event_name in self.emitter._events:
|
||||||
|
LOG.debug("Removing found '"+str(event_name)+"'")
|
||||||
|
else:
|
||||||
|
LOG.debug("Not able to find '"+str(event_name)+"'")
|
||||||
self.emitter.remove_listener(event_name, func)
|
self.emitter.remove_listener(event_name, func)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
LOG.warning('Failed to remove event {}: {}'.format(event_name, e))
|
import traceback
|
||||||
|
LOG.warning('Failed to remove event {}: {}'.format(event_name, str(func), e))
|
||||||
|
for line in traceback.format_stack():
|
||||||
|
LOG.warning(line.strip())
|
||||||
|
|
||||||
|
if event_name in self.emitter._events:
|
||||||
|
LOG.debug("Removing found '"+str(event_name)+"'")
|
||||||
|
else:
|
||||||
|
LOG.debug("Not able to find '"+str(event_name)+"'")
|
||||||
|
LOG.warning("Existing events: " + str(self.emitter._events))
|
||||||
|
for evt in self.emitter._events:
|
||||||
|
LOG.warning(" "+str(evt))
|
||||||
|
LOG.warning(" "+str(self.emitter._events[evt]))
|
||||||
|
if event_name in self.emitter._events:
|
||||||
|
LOG.debug("Removing found '"+str(event_name)+"'")
|
||||||
|
else:
|
||||||
|
LOG.debug("Not able to find '"+str(event_name)+"'")
|
||||||
|
LOG.warning('----- End dump -----')
|
||||||
|
|
||||||
|
|
||||||
def remove_all_listeners(self, event_name):
|
def remove_all_listeners(self, event_name):
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -28,10 +28,18 @@ def ensure_uri(s):
|
||||||
Returns:
|
Returns:
|
||||||
if s is uri, s is returned otherwise file:// is prepended
|
if s is uri, s is returned otherwise file:// is prepended
|
||||||
"""
|
"""
|
||||||
if '://' not in s:
|
if isinstance(s, str):
|
||||||
return 'file://' + abspath(s)
|
if '://' not in s:
|
||||||
|
return 'file://' + abspath(s)
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
elif isinstance(s, (tuple, list)):
|
||||||
|
if '://' not in s[0]:
|
||||||
|
return 'file://' + abspath(s[0]), s[1]
|
||||||
|
else:
|
||||||
|
return s
|
||||||
else:
|
else:
|
||||||
return s
|
raise ValueError('Invalid track')
|
||||||
|
|
||||||
|
|
||||||
class AudioService(object):
|
class AudioService(object):
|
||||||
|
@ -74,11 +82,13 @@ class AudioService(object):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tracks: track uri or list of track uri's
|
tracks: track uri or list of track uri's
|
||||||
|
Each track can be added as a tuple with (uri, mime)
|
||||||
|
to give a hint of the mime type to the system
|
||||||
utterance: forward utterance for further processing by the
|
utterance: forward utterance for further processing by the
|
||||||
audio service.
|
audio service.
|
||||||
"""
|
"""
|
||||||
tracks = tracks or []
|
tracks = tracks or []
|
||||||
if isinstance(tracks, str):
|
if isinstance(tracks, (str, tuple)):
|
||||||
tracks = [tracks]
|
tracks = [tracks]
|
||||||
elif not isinstance(tracks, list):
|
elif not isinstance(tracks, list):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
|
@ -16,8 +16,9 @@ from mycroft.audio.services import AudioBackend
|
||||||
|
|
||||||
|
|
||||||
class WorkingBackend(AudioBackend):
|
class WorkingBackend(AudioBackend):
|
||||||
def __init__(self, config, emitter, name='Working'):
|
def __init__(self, config, bus, name='Working'):
|
||||||
pass
|
super(WorkingBackend, self).__init__(config, bus)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def supported_uris(self):
|
def supported_uris(self):
|
||||||
return ['file', 'http']
|
return ['file', 'http']
|
||||||
|
@ -35,6 +36,6 @@ class WorkingBackend(AudioBackend):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def load_service(base_config, emitter):
|
def load_service(base_config, bus):
|
||||||
instances = [WorkingBackend(base_config, emitter)]
|
instances = [WorkingBackend(base_config, bus)]
|
||||||
return instances
|
return instances
|
||||||
|
|
Loading…
Reference in New Issue