diff --git a/mycroft/audio/audioservice.py b/mycroft/audio/audioservice.py index 7a8b662f05..7db4fe2736 100644 --- a/mycroft/audio/audioservice.py +++ b/mycroft/audio/audioservice.py @@ -344,7 +344,12 @@ class AudioService(object): the tracks. """ 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 if prefered_service and uri_type in prefered_service.supported_uris(): selected_service = prefered_service @@ -362,6 +367,8 @@ class AudioService(object): else: LOG.info('No service found for uri_type: ' + uri_type) 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.add_list(tracks) selected_service.play() diff --git a/mycroft/audio/services/__init__.py b/mycroft/audio/services/__init__.py index 10789fdcd5..c8d1508599 100644 --- a/mycroft/audio/services/__init__.py +++ b/mycroft/audio/services/__init__.py @@ -27,6 +27,7 @@ class AudioBackend(): def __init__(self, config, bus): self._track_start_callback = None + self.supports_mime_hints = False @abstractmethod def supported_uris(self): diff --git a/mycroft/audio/services/ogg123/__init__.py b/mycroft/audio/services/ogg123/__init__.py deleted file mode 100644 index 4ddf964ee3..0000000000 --- a/mycroft/audio/services/ogg123/__init__.py +++ /dev/null @@ -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 diff --git a/mycroft/audio/services/mpg123/__init__.py b/mycroft/audio/services/simple/__init__.py similarity index 53% rename from mycroft/audio/services/mpg123/__init__.py rename to mycroft/audio/services/simple/__init__.py index 61d04a9755..8195f33edd 100644 --- a/mycroft/audio/services/mpg123/__init__.py +++ b/mycroft/audio/services/simple/__init__.py @@ -18,24 +18,46 @@ from time import sleep from mycroft.audio.services import AudioBackend from mycroft.messagebus.message import Message 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 - only implements basic usage. + Simple Audio backend for both mpg123 and the ogg123 player. + This one is rather limited and only implements basic usage. """ - def __init__(self, config, bus, name='mpg123'): - super(Mpg123Service, self).__init__(config, bus) + def __init__(self, config, bus, name='simple'): + + super(SimpleAudioService, 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.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): return ['file', 'http'] @@ -52,20 +74,36 @@ class Mpg123Service(AudioBackend): This allows mpg123 service to use the "next method as well as basic play/stop. """ - LOG.info('Mpg123Service._play') + LOG.info('SimpleAudioService._play') 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 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(['mpg123', track]) - # Wait for completion or stop request - while self.process.poll() is None and not self._stop_signal: - sleep(0.25) + proc = None + if 'mpeg' in mime[1]: + proc = 'mpg123' + elif 'ogg' in mime[1]: + 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: self.process.terminate() @@ -76,25 +114,21 @@ class Mpg123Service(AudioBackend): self.index += 1 # if there are more tracks available play next if self.index < len(self.tracks): - self.bus.emit(Message('Mpg123ServicePlay')) + self.bus.emit(Message('SimpleAudioServicePlay')) else: self._is_playing = False def play(self): - LOG.info('Call Mpg123ServicePlay') + LOG.info('Call SimpleAudioServicePlay') self.index = 0 - self.bus.emit(Message('Mpg123ServicePlay')) + self.bus.emit(Message('SimpleAudioServicePlay')) def stop(self): - LOG.info('Mpg123ServiceStop') - if self._is_playing: - self._stop_signal = True - while self._is_playing: - sleep(0.01) - self._stop_signal = False - return True - else: - return False + LOG.info('SimpleAudioServiceStop') + self._stop_signal = True + while self._is_playing: + sleep(0.1) + self._stop_signal = False def pause(self): pass @@ -119,7 +153,7 @@ class Mpg123Service(AudioBackend): def load_service(base_config, bus): backends = base_config.get('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)] - instances = [Mpg123Service(s[1], bus, s[0]) for s in services] + instances = [SimpleAudioService(s[1], bus, s[0]) for s in services] return instances diff --git a/mycroft/configuration/mycroft.conf b/mycroft/configuration/mycroft.conf index 74ead7e749..1c47860655 100644 --- a/mycroft/configuration/mycroft.conf +++ b/mycroft/configuration/mycroft.conf @@ -291,11 +291,7 @@ "Audio": { "backends": { "local": { - "type": "mpg123", - "active": true - }, - "ogg": { - "type": "ogg123", + "type": "simple", "active": true }, "vlc": { diff --git a/mycroft/messagebus/client/ws.py b/mycroft/messagebus/client/ws.py index 35ec4c0134..fbb57c8a12 100644 --- a/mycroft/messagebus/client/ws.py +++ b/mycroft/messagebus/client/ws.py @@ -16,6 +16,7 @@ import json import time from multiprocessing.pool import ThreadPool from threading import Event +import traceback from pyee import EventEmitter from websocket import (WebSocketApp, WebSocketConnectionClosedException, @@ -144,7 +145,7 @@ class WebsocketClient(object): # ValueError occurs on pyee 1.0.1 removing handlers # registered with once. # KeyError may theoretically occur if the event occurs as - # the handler is removbed + # the handler is removed pass return None return response[0] @@ -157,9 +158,31 @@ class WebsocketClient(object): def remove(self, event_name, func): 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) 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): ''' diff --git a/mycroft/skills/audioservice.py b/mycroft/skills/audioservice.py index 36897542f5..de96abbbf3 100644 --- a/mycroft/skills/audioservice.py +++ b/mycroft/skills/audioservice.py @@ -28,10 +28,18 @@ def ensure_uri(s): Returns: if s is uri, s is returned otherwise file:// is prepended """ - if '://' not in s: - return 'file://' + abspath(s) + if isinstance(s, str): + 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: - return s + raise ValueError('Invalid track') class AudioService(object): @@ -74,11 +82,13 @@ class AudioService(object): Args: 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 audio service. """ tracks = tracks or [] - if isinstance(tracks, str): + if isinstance(tracks, (str, tuple)): tracks = [tracks] elif not isinstance(tracks, list): raise ValueError diff --git a/test/unittests/audio/services/working/__init__.py b/test/unittests/audio/services/working/__init__.py index c9b2986e8a..47f950849d 100644 --- a/test/unittests/audio/services/working/__init__.py +++ b/test/unittests/audio/services/working/__init__.py @@ -16,8 +16,9 @@ from mycroft.audio.services import AudioBackend class WorkingBackend(AudioBackend): - def __init__(self, config, emitter, name='Working'): - pass + def __init__(self, config, bus, name='Working'): + super(WorkingBackend, self).__init__(config, bus) + self.name = name def supported_uris(self): return ['file', 'http'] @@ -35,6 +36,6 @@ class WorkingBackend(AudioBackend): pass -def load_service(base_config, emitter): - instances = [WorkingBackend(base_config, emitter)] +def load_service(base_config, bus): + instances = [WorkingBackend(base_config, bus)] return instances