From 75a9fea723a1372df348198d58d6e9787eab849c Mon Sep 17 00:00:00 2001 From: Ezra Taimuty-Loomis Date: Sat, 20 Feb 2021 18:49:27 -0500 Subject: [PATCH] Audio fixes and exception handling --- client/py/audio_manager.py | 85 ++++++++++++++++++++++++++------- client/py/client.py | 97 +++++++++++++++++++++++++------------- client/py/common.py | 2 +- client/py/connect.py | 15 +++--- client/py/scan_client.ui | 4 +- client/py/scanner.py | 7 ++- 6 files changed, 149 insertions(+), 61 deletions(-) diff --git a/client/py/audio_manager.py b/client/py/audio_manager.py index de90baf..ecefc6e 100644 --- a/client/py/audio_manager.py +++ b/client/py/audio_manager.py @@ -1,5 +1,6 @@ import vlc import time +import threading #instance = vlc.Instance("-vvv", "--no-video", "--repeat") #player = instance.media_player_new() @@ -13,40 +14,90 @@ class AudioManager: vlc = None player = None media = None + do_play = False + monitor_thread = None + volume = 50 + mute = False def __init__(self): - self.vlc = vlc.Instance('--no-video', '--repeat', '-v') + self.vlc = vlc.Instance('--no-video', '--repeat')#, '-v') + self.vlc_lock = threading.Lock() def close(self): + if do_play: + self.stopRtspAudioStream() if self.player: - self.player.stop() self.player.release() self.vlc.release() - def startRtspAudioStream(self, host, port): - print('create media') - self.media = self.vlc.media_new('rtsp://' + host + ':' + port + '/audio', 'network-caching=50') - print('create new player') - self.player = self.media.player_new_from_media() - print('start player') - self.player.play() + def startRtspAudioStream(self, host, port) -> bool : + with self.vlc_lock: + try: + print('create media') + self.media = self.vlc.media_new('rtsp://' + host + ':' + str(port) + '/audio', 'network-caching=50') + #media.add_options('input-repeat=65535') # NOTE: repeat leads to the error: 'main input error: INPUT_CONTROL_SET_POSITION 0.0% failed' + print('create new player') + self.player = self.media.player_new_from_media() + print('start player') + self.player.play() + + self.player.audio_set_volume(self.volume) + self.player.audio_set_mute(self.mute) + + self.do_play = True + self.monitor_thread = threading.Thread(target=self.monitor, name='audio monitor') + self.monitor_thread.start() + return True + except: + print('error starting stream') + return False def stopRtspAudioStream(self): print('stopping audio') - if self.player: - self.player.stop() - self.player.release() - self.player = None - self.media.release() - self.media = None + + with self.vlc_lock: + if self.do_play and self.player: + self.do_play = False + self.player.stop() + self.player.release() + self.player = None + self.media.release() + self.media = None + + self.monitor_thread.join() + self.monitor_thread = None + print('stopped') def setAudioVolume(self, level : int): - self.player.audio_set_volume(level) + with self.vlc_lock: + self.volume = level + self.player.audio_set_volume(self.volume) def setAudioMute(self, mute : bool): - self.player.audio_set_mute(mute) + with self.vlc_lock: + self.mute = mute + self.player.audio_set_mute(self.mute) + + # NOTE: this is a hacky way to maintain stream playback; for some reason using vlc's repeat parameter doesn't work + def monitor(self): + print('start stream monitor thread') + while True: + with self.vlc_lock: + if not self.do_play: + break + state = self.player.get_state() + if state == vlc.State.Ended or state == vlc.State.Error: + print('attempting to restart stream') + self.player.stop() + self.player.release() + self.player = self.media.player_new_from_media() + self.player.play() + self.player.audio_set_volume(self.volume) + self.player.audio_set_mute(self.mute) + time.sleep(0.01) # sleep 10ms + print('exiting stream monitor') if __name__ == '__main__': audio = AudioManager() diff --git a/client/py/client.py b/client/py/client.py index 8822dd4..fb4f3f8 100644 --- a/client/py/client.py +++ b/client/py/client.py @@ -29,8 +29,9 @@ import google.protobuf.message as proto class PiScanClient(QWidget, common.AppInterface): dataReceived = Signal(bytes) manualDisconnectInitiated = False + manualDisconnectMessage = None - def __init__(self, parent=None, address=None, port=None): + def __init__(self, parent=None, address=None, port=None, use_audio=False, rtsp_port=None): super(PiScanClient, self).__init__(parent) common.setInstance(self) ui_file = 'scan_client.ui' @@ -61,6 +62,7 @@ class PiScanClient(QWidget, common.AppInterface): self.inmsg = messages_pb2.ServerToClient() + self.use_audio = use_audio self.audio = audio_manager.AudioManager() def mainWidget(self): @@ -76,7 +78,7 @@ class PiScanClient(QWidget, common.AppInterface): self.setWindowMode(common.WindowMode.DIALOG) def receive(self): - disconnectMessage = '' + disconnectMessage = None while True: try: @@ -89,13 +91,25 @@ class PiScanClient(QWidget, common.AppInterface): if not self.manualDisconnectInitiated: disconnectMessage = 'Connection aborted' break + except OSError as err: + if not self.manualDisconnectInitiated: + disconnectMessage = str(err) + break except: e = sys.exc_info()[0] disconnectMessage = 'Unhandled exception: ' + str(e) break - print("Closing connection. Reason: " + disconnectMessage) - self.audio.stopRtspAudioStream() + if not disconnectMessage: + disconnectMessage = self.manualDisconnectMessage + + if disconnectMessage: + print('Closing connection. Reason: ' + disconnectMessage) + else: + print('Disconnecting...') + + if self.use_audio: + self.audio.stopRtspAudioStream() self.manualDisconnectInitiated = False self.showConnectDialog(disconnectMessage) @@ -106,18 +120,18 @@ class PiScanClient(QWidget, common.AppInterface): def decodeMessage(self, message): #print(message) if message.WhichOneof('content') == 'systemInfo': - print('system info') + #print('system info') self.scanner.setSquelchRange(message.systemInfo.squelchScaleMin, message.systemInfo.squelchScaleMax) for mode in message.systemInfo.supportedModulations: self.dialogs.manualEntry.addModulation(mode) elif message.type == messages_pb2.ServerToClient.Type.Value('SCANNER_CONTEXT'): - print('scanner context') + #print('scanner context') self.scanner.updateScanContext(message.scannerContext) if self.contextWait: self.setWindowMode(common.WindowMode.SCANNER) self.contextWait = False elif message.type == messages_pb2.ServerToClient.Type.Value('DEMOD_CONTEXT'): - print('demod context') + #print('demod context') self.scanner.updateDemodContext(message.demodContext) #elif message.type == messages_pb2.ServerToClient.Type.Value('GENERAL_MESSAGE'): elif message.WhichOneof('content') == 'signalLevel': @@ -134,7 +148,7 @@ class PiScanClient(QWidget, common.AppInterface): def completeConnection(self, sock, host, use_audio, audio_port): self.sock = sock - self.rcv_thread = Thread(target=self.receive) + self.rcv_thread = Thread(target=self.receive, name='socket monitor') self.rcv_thread.start() self.contextWait = True self.connectDialog.contextWait() @@ -142,34 +156,34 @@ class PiScanClient(QWidget, common.AppInterface): message1 = messages_pb2.ClientToServer() message1.type = messages_pb2.ClientToServer.Type.Value('GENERAL_REQUEST') message1.generalRequest.type = request_pb2.GeneralRequest.RequestType.Value('SCANNER_CONTEXT') - self.sock.send(message1.SerializeToString()) + self.sendData(message1.SerializeToString()) sleep(0.25) message2 = messages_pb2.ClientToServer() message2.type = messages_pb2.ClientToServer.Type.Value('GENERAL_REQUEST') message2.generalRequest.type = request_pb2.GeneralRequest.RequestType.Value('DEMOD_CONTEXT') - self.sock.send(message2.SerializeToString()) + self.sendData(message2.SerializeToString()) sleep(0.25) message3 = messages_pb2.ClientToServer() message3.type = messages_pb2.ClientToServer.Type.Value('GENERAL_REQUEST') message3.generalRequest.type = request_pb2.GeneralRequest.RequestType.Value('SYSTEM_INFO') - self.sock.send(message3.SerializeToString()) + self.sendData(message3.SerializeToString()) if use_audio: - self.audio.startRtspAudioStream(host, audio_port) - self.setAudioVolume(50) - self.scanner.setVolumeVisible(use_audio) + self.use_audio = self.audio.startRtspAudioStream(host, audio_port) + #self.setAudioVolume(50) + self.scanner.setVolumeVisible(self.use_audio) def scan(self): message = messages_pb2.ClientToServer() message.type = messages_pb2.ClientToServer.Type.Value('SCANNER_STATE_REQUEST') message.scanStateRequest.state = request_pb2.ScannerStateRequest.NewState.Value('SCAN') - self.sock.send(message.SerializeToString()) + self.sendData(message.SerializeToString()) def hold(self): message = messages_pb2.ClientToServer() message.type = messages_pb2.ClientToServer.Type.Value('SCANNER_STATE_REQUEST') message.scanStateRequest.state = request_pb2.ScannerStateRequest.NewState.Value('HOLD') - self.sock.send(message.SerializeToString()) + self.sendData(message.SerializeToString()) def manualEntry(self, frequency, modulation): message = messages_pb2.ClientToServer() @@ -177,7 +191,7 @@ class PiScanClient(QWidget, common.AppInterface): message.scanStateRequest.state = request_pb2.ScannerStateRequest.NewState.Value('MANUAL') message.scanStateRequest.manFreq = frequency message.scanStateRequest.manModulation = modulation - self.sock.send(message.SerializeToString()) + self.sendData(message.SerializeToString()) def showConnectDialog(self, errorMessage = ''): self.clearWindowTitleInfo() @@ -205,20 +219,20 @@ class PiScanClient(QWidget, common.AppInterface): message.type = messages_pb2.ClientToServer.Type.Value('DEMOD_REQUEST') message.demodRequest.type = request_pb2.DemodRequest.DemodFunc.Value('SET_GAIN') message.demodRequest.level = value - self.sock.send(message.SerializeToString()) + self.sendData(message.SerializeToString()) def setSquelch(self, value): message = messages_pb2.ClientToServer() message.type = messages_pb2.ClientToServer.Type.Value('DEMOD_REQUEST') message.demodRequest.type = request_pb2.DemodRequest.DemodFunc.Value('SET_SQUELCH') message.demodRequest.level = value - self.sock.send(message.SerializeToString()) + self.sendData(message.SerializeToString()) def dialogClosed(self): self.setWindowMode(self.lastMode) - def tryConnect(self, address, port): - self.connectDialog.tryConnect(address, port) + def tryConnect(self, address, port, use_audio, rtsp_port): + self.connectDialog.tryConnect(address, port, use_audio, rtsp_port) def setWindowTitleInfo(self, message): if isinstance(self.parentWindow, QtWidgets.QMainWindow): @@ -232,26 +246,37 @@ class PiScanClient(QWidget, common.AppInterface): else: self.setWindowTitle('PiScan') - def disconnect(self): + def disconnect(self, message=''): self.manualDisconnectInitiated = True + self.manualDisconnectMessage = message try: if self.sock: self.sock.close() #self.rcv_thread.join() - except: - pass + except Exception as e: + print(e) def setAudioVolume(self, level): - self.audio.setAudioVolume(level) + if self.use_audio: + self.audio.setAudioVolume(level) def setAudioMute(self, mute): - self.audio.setAudioMute(mute) + if self.use_audio: + self.audio.setAudioMute(mute) + + def sendData(self, message): + try: + self.sock.send(message) + except OSError as err: + self.disconnect('Connection error: ' + str(err)) + except Exception as e: + print(e) class HostWindow(QtWidgets.QMainWindow): - def __init__(self, parent=None, address=None, port=None): + def __init__(self, parent=None, address=None, port=None, use_audio=False, rtsp_port=None): super(HostWindow, self).__init__(parent) - form = PiScanClient(self, address, port) + form = PiScanClient(self, address, port, use_audio, rtsp_port) mainWidget = form.mainWidget() self.setPalette(mainWidget.palette()) @@ -266,22 +291,24 @@ class HostWindow(QtWidgets.QMainWindow): if address: if not port: port = constants.DEFAULT_TCP_PORT - form.tryConnect(address, port) + form.tryConnect(address, port, use_audio, rtsp_port) def closeEvent(self, event): - print('exit') + #print('exit') common.getApp().closeEvent(event) event.accept() if __name__ == '__main__': - shortOpts = 'la:p:w' - longOpts = ['--local', '--address', '--port', '--pi_mode'] + shortOpts = 'la:p:wsr:' + longOpts = ['--local', '--address', '--port', '--pi_mode', '--audio', '--rtsp_port'] options, remainder = getopt.getopt(sys.argv[1:], shortOpts, longOpts) address = None port = None piMode = False + use_audio = False + rtsp_port = 8554 for opt, arg in options: if opt in ('-l', '--local'): @@ -292,9 +319,13 @@ if __name__ == '__main__': port = int(arg) elif opt in ('-w', '--pi_mode'): piMode = True + elif opt in ('-s', '--audio'): + use_audio = True + elif opt in ('-r', '--rtsp_port'): + rtsp_port = int(arg) app = QApplication(sys.argv) - window = HostWindow(address=address, port=port) + window = HostWindow(address=address, port=port, use_audio=use_audio, rtsp_port=rtsp_port) if piMode: flags = Qt.WindowFlags(Qt.CustomizeWindowHint | Qt.FramelessWindowHint | Qt.Tool) window.setWindowFlags(flags) diff --git a/client/py/common.py b/client/py/common.py index 389ce71..32d137b 100644 --- a/client/py/common.py +++ b/client/py/common.py @@ -67,7 +67,7 @@ class AppInterface: def closeEvent(self, event): pass - def tryConnect(self, address, port): + def tryConnect(self, address, port, use_audio, rtsp_port): pass def setWindowTitleInfo(self, message): diff --git a/client/py/connect.py b/client/py/connect.py index 38086ec..fdd4acc 100644 --- a/client/py/connect.py +++ b/client/py/connect.py @@ -11,7 +11,7 @@ import common import constants class ConnectDialog: - def __init__(self, parentWindow, address=None, port=None): + def __init__(self, parentWindow, address=None, port=None, use_audio=False, rtsp_port=None): self.widget = parentWindow.findChild(QWidget, 'connectPage') self.errorLabel = parentWindow.findChild(QLabel, 'connect_errorLabel') self.confirmButton = parentWindow.findChild(QPushButton, 'connect_confirmButton') @@ -47,15 +47,19 @@ class ConnectDialog: def onConfirm(self): host = self.hostLineEdit.text() port = int(self.portLineEdit.text()) - self.tryConnect(host, port) + audio = self.audioCheckBox.isChecked() + rtsp_port = int(self.rtspPortLineEdit.text()) + self.tryConnect(host, port, audio, rtsp_port) - def tryConnect(self, address, port): + def tryConnect(self, address, port, use_audio=False, rtsp_port=8554): print('connect confirm') try: self.connectIndicator.setVisible(True) self.errorLabel.setVisible(False) self.hostLineEdit.setText(address) self.portLineEdit.setText(str(port)) + self.audioCheckBox.setChecked(use_audio) + self.rtspPortLineEdit.setText(str(rtsp_port)) self.widget.repaint() print('Connecting to ', address, ':', port) @@ -67,10 +71,7 @@ class ConnectDialog: self.connectIndicator.setVisible(False) - use_audio = self.audioCheckBox.isChecked() - audio_port = self.rtspPortLineEdit.text() - - common.getApp().completeConnection(sock, address, use_audio, audio_port) + common.getApp().completeConnection(sock, address, use_audio, rtsp_port) except ConnectionRefusedError: self.connectFailed('Connect failed - Connection refused') except gaierror as err: diff --git a/client/py/scan_client.ui b/client/py/scan_client.ui index 95c21c2..42b93b8 100644 --- a/client/py/scan_client.ui +++ b/client/py/scan_client.ui @@ -481,7 +481,7 @@ - 0 + 1 @@ -2324,7 +2324,7 @@ 0 - + 0 diff --git a/client/py/scanner.py b/client/py/scanner.py index d3eb963..523492f 100644 --- a/client/py/scanner.py +++ b/client/py/scanner.py @@ -26,7 +26,9 @@ class Scanner: self.modulationLabel = parentWindow.findChild(QLabel, 'scanner_modulationLabel') self.systemTagLabel = parentWindow.findChild(QLabel, 'scanner_systemTagLabel') self.entryNumLabel = parentWindow.findChild(QLabel, 'scanner_entryNumLabel') + self.delayLabel = parentWindow.findChild(QLabel, 'scanner_delayLabel') self.lockoutDurationLabel = parentWindow.findChild(QLabel, 'scanner_lockoutDurationLabel') + self.scanModeLabel = parentWindow.findChild(QLabel, 'scanner_scanModeLabel') self.lockoutDurationButton = parentWindow.findChild(QPushButton, 'scanner_lockoutDurationButton') self.scanIndicator = parentWindow.findChild(QLabel, 'scanner_scanIndicator') self.gainSlider = parentWindow.findChild(QSlider, 'scanner_gainSlider') @@ -69,7 +71,7 @@ class Scanner: self.volumeSlider.valueChanged.connect(self.onVolumeSlider) self.muteButton.toggled.connect(self.onMuteButton) - #temporary since settins dialog is not yet implemented + # TODO TEMPORARY - hiding UI elements for features not added yet self.volumeControlPanel.setVisible(False) self.lockoutDurationButton.setVisible(False) self.lockoutDurationLabel.setVisible(False) @@ -77,6 +79,9 @@ class Scanner: self.settingsButton.setVisible(False) self.connectInfoButton.setVisible(False) self.entryEditButton.setVisible(False) + self.delayLabel.setVisible(False) + self.scanModeLabel.setVisible(False) + parentWindow.findChild(QWidget, 'line_4').setVisible(False) movie = QMovie("resources/bar-scan.gif") movie.start()