1 | #!/usr/bin/env python |
||
2 | # -*- coding: utf-8 -*- |
||
3 | |||
4 | import logging |
||
5 | logger = logging.getLogger(__name__) |
||
6 | logger.debug("%s loaded", __name__) |
||
7 | |||
8 | import datetime |
||
9 | |||
10 | from AbstractBaseClass import SipphoneAbstractBaseClass, SIPPHONE_SECTION |
||
11 | import linphone as lin |
||
12 | |||
13 | from doorpi import DoorPi |
||
14 | from doorpi.sipphone.linphone_lib.CallBacks import LinphoneCallbacks |
||
15 | from doorpi.sipphone.linphone_lib.Player import LinphonePlayer |
||
16 | from doorpi.sipphone.linphone_lib.Recorder import LinphoneRecorder |
||
17 | from doorpi.media.CreateDialTone import generate_dial_tone |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
18 | |||
19 | conf = DoorPi().config |
||
20 | |||
21 | def log_handler(level, msg): |
||
22 | if "pylinphone_Core_instance_method_iterate" in msg: return |
||
23 | if "pylinphone_Core_get_current_call" in msg: return |
||
24 | if "pylinphone_Call_from_native_ptr" in msg: return |
||
25 | if ": keep alive sent to [" in msg: return |
||
26 | method = getattr(logger, level) |
||
27 | method(msg) |
||
28 | |||
29 | if logger.getEffectiveLevel() <= 5: lin.set_log_handler(log_handler) |
||
30 | |||
31 | def get(*args, **kwargs): return LinPhone(*args, **kwargs) |
||
32 | class LinPhone(SipphoneAbstractBaseClass): |
||
33 | |||
34 | @property |
||
35 | def name(self): return 'linphone wrapper' |
||
36 | |||
37 | @property |
||
38 | def lib(self): return self.__Lib |
||
39 | @property |
||
40 | def core(self): return self.__Lib |
||
41 | |||
42 | @property |
||
43 | def recorder(self): return self.__recorder |
||
44 | __recorder = None |
||
45 | |||
46 | @property |
||
47 | def player(self): return self.__player |
||
48 | __player = None |
||
49 | |||
50 | @property |
||
51 | def current_call(self): return self.core.current_call |
||
52 | |||
53 | @property |
||
54 | def video_devices(self): |
||
55 | try: |
||
56 | all_devices = [] |
||
57 | for video_device in self.core.video_devices: |
||
58 | all_devices.append({ |
||
59 | 'name': video_device |
||
60 | }) |
||
61 | return all_devices |
||
62 | except Exception: |
||
63 | return [] |
||
64 | |||
65 | @property |
||
66 | def sound_devices(self): |
||
67 | try: |
||
68 | all_devices = [] |
||
69 | for sound_device in self.core.sound_devices: |
||
70 | all_devices.append({ |
||
71 | 'name': sound_device, |
||
72 | 'capture': self.core.sound_device_can_capture(sound_device), |
||
73 | 'record': self.core.sound_device_can_playback(sound_device) |
||
74 | }) |
||
75 | return all_devices |
||
76 | except Exception as exp: |
||
77 | logger.exception(exp) |
||
78 | return [] |
||
79 | |||
80 | def _create_payload_enum(self, payloads): |
||
81 | |||
82 | try: |
||
83 | all_codecs = [] |
||
84 | for codec in payloads: |
||
85 | all_codecs.append({ |
||
86 | 'name': codec.mime_type, |
||
87 | 'channels': codec.channels, |
||
88 | 'bitrate': codec.normal_bitrate, |
||
89 | 'enable': self.core.payload_type_enabled(codec) |
||
90 | }) |
||
91 | return all_codecs |
||
92 | except Exception as exp: |
||
93 | logger.exception(exp) |
||
94 | return [] |
||
95 | |||
96 | @property |
||
97 | def video_codecs(self): |
||
98 | return self._create_payload_enum(self.core.video_codecs) |
||
99 | |||
100 | @property |
||
101 | def sound_codecs(self): |
||
102 | return self._create_payload_enum(self.core.audio_codecs) |
||
103 | |||
104 | @property |
||
105 | def current_call_duration(self): |
||
106 | if not self.current_call: return 0 |
||
107 | diff_start_and_now = datetime.datetime.utcnow() - self.__current_call_start_datetime |
||
108 | return diff_start_and_now.total_seconds() |
||
109 | |||
110 | @property |
||
111 | def current_call_dump(self): |
||
112 | try: |
||
113 | return { |
||
114 | 'direction': 'incoming' if self.current_call.dir == 0 else 'outgoing', |
||
115 | 'remote_uri': self.current_call.remote_address_as_string, |
||
116 | 'total_time': self.current_call_duration, |
||
117 | 'level_incoming': self.current_call.record_volume, |
||
118 | 'level_outgoing': self.current_call.play_volume, |
||
119 | 'camera': self.current_call.camera_enabled |
||
120 | } |
||
121 | except Exception: |
||
122 | return {} |
||
123 | |||
124 | #TODO: Datetime from linphone CallLog.start_date is more then 30 sec different to python datetime.utcnow()? |
||
125 | __current_call_start_datetime = datetime.datetime.utcnow() |
||
126 | |||
127 | @property |
||
128 | def base_config(self): |
||
129 | params = self.core.create_call_params(None) |
||
130 | params.record_file = self.recorder.parsed_record_filename |
||
131 | params.video_enabled = True |
||
132 | return params |
||
133 | |||
134 | def reset_call_start_datetime(self): |
||
135 | self.__current_call_start_datetime = datetime.datetime.utcnow() |
||
136 | logger.debug('reset current call start datetime to %s', self.__current_call_start_datetime) |
||
137 | return self.__current_call_start_datetime |
||
138 | |||
139 | def __init__(self, whitelist = list(), *args, **kwargs): |
||
0 ignored issues
–
show
|
|||
140 | logger.debug("__init__") |
||
141 | |||
142 | DoorPi().event_handler.register_action('OnShutdown', self.destroy) |
||
143 | |||
144 | DoorPi().event_handler.register_event('OnSipPhoneCreate', __name__) |
||
145 | DoorPi().event_handler.register_event('OnSipPhoneStart', __name__) |
||
146 | DoorPi().event_handler.register_event('OnSipPhoneDestroy', __name__) |
||
147 | |||
148 | DoorPi().event_handler.register_event('OnSipPhoneRecorderCreate', __name__) |
||
149 | DoorPi().event_handler.register_event('OnSipPhoneRecorderDestroy', __name__) |
||
150 | |||
151 | DoorPi().event_handler.register_event('BeforeSipPhoneMakeCall', __name__) |
||
152 | DoorPi().event_handler.register_event('OnSipPhoneMakeCall', __name__) |
||
153 | DoorPi().event_handler.register_event('OnSipPhoneMakeCallFailed', __name__) |
||
154 | DoorPi().event_handler.register_event('AfterSipPhoneMakeCall', __name__) |
||
155 | |||
156 | DoorPi().event_handler.register_event('OnSipPhoneCallTimeoutNoResponse', __name__) |
||
157 | DoorPi().event_handler.register_event('OnSipPhoneCallTimeoutMaxCalltime', __name__) |
||
158 | |||
159 | DoorPi().event_handler.register_event('OnPlayerCreated', __name__) |
||
160 | |||
161 | #http://pythonhosted.org/linphone/api_reference.html#linphone.Core.new |
||
162 | self.callback = LinphoneCallbacks() |
||
163 | config_path = None |
||
164 | factory_config_path = None |
||
165 | self.__Lib = lin.Core.new( |
||
166 | self.callback.used_callbacks, |
||
167 | config_path, |
||
168 | factory_config_path |
||
169 | ) |
||
170 | self.core.primary_contact = '%s <sip:[email protected]>'%conf.get(SIPPHONE_SECTION, "identity", 'DoorPi') |
||
171 | |||
172 | def start(self): |
||
173 | DoorPi().event_handler('OnSipPhoneCreate', __name__) |
||
174 | self.core.max_calls = conf.get_int(SIPPHONE_SECTION, 'ua.max_calls', 2) |
||
175 | self.core.echo_cancellation_enabled = conf.get_bool(SIPPHONE_SECTION, 'echo_cancellation_enabled', False) |
||
176 | |||
177 | # set local listen ports, default: random |
||
178 | self.core.sip_transports = lin.SipTransports(conf.get_int(SIPPHONE_SECTION, 'local_port', 5060), conf.get_int(SIPPHONE_SECTION, 'local_port', 5060), -1, -1) |
||
179 | |||
180 | self.core.video_display_enabled = conf.get_bool(SIPPHONE_SECTION, 'video_display_enabled', False) |
||
181 | self.core.stun_server = conf.get(SIPPHONE_SECTION, 'stun_server', '') |
||
182 | firewall_policy = conf.get(SIPPHONE_SECTION, 'FirewallPolicy', 'PolicyNoFirewall') |
||
183 | if firewall_policy == "PolicyNoFirewall": self.core.firewall_policy = lin.FirewallPolicy.PolicyNoFirewall |
||
184 | elif firewall_policy == "PolicyUseNatAddress": self.core.firewall_policy = lin.FirewallPolicy.PolicyUseNatAddress |
||
185 | elif firewall_policy == "PolicyUseStun": self.core.firewall_policy = lin.FirewallPolicy.PolicyUseStun |
||
186 | elif firewall_policy == "PolicyUseIce": self.core.firewall_policy = lin.FirewallPolicy.PolicyUseIce |
||
187 | elif firewall_policy == "PolicyUseUpnp": self.core.firewall_policy = lin.FirewallPolicy.PolicyUseUpnp |
||
188 | else: self.core.firewall_policy = lin.FirewallPolicy.PolicyNoFirewall |
||
189 | |||
190 | #http://pythonhosted.org/linphone/api_reference.html#linphone.Core.in_call_timeout |
||
191 | #After this timeout period, the call is automatically hangup. |
||
192 | self.core.in_call_timeout = conf.get_int(SIPPHONE_SECTION, 'max_call_time', 120) |
||
193 | #http://pythonhosted.org/linphone/api_reference.html#linphone.Core.inc_timeout |
||
194 | #If an incoming call isn’t answered for this timeout period, it is automatically declined. |
||
195 | self.core.inc_timeout = conf.get_int(SIPPHONE_SECTION, 'call_timeout', 15) |
||
196 | |||
197 | self.__player = LinphonePlayer() |
||
198 | self.core.ringback = self.player.player_filename |
||
199 | self.__recorder = LinphoneRecorder() |
||
200 | |||
201 | if len(self.core.sound_devices) == 0: |
||
202 | logger.warning('no audio devices available') |
||
203 | else: |
||
204 | self.core.capture_device = conf.get(SIPPHONE_SECTION, 'capture_device', self.core.capture_device) |
||
205 | self.core.playback_device = conf.get(SIPPHONE_SECTION, 'playback_device', self.core.playback_device) |
||
206 | logger.info("found %s possible sounddevices:", len(self.core.sound_devices)) |
||
207 | logger.debug("|rec|play| name") |
||
208 | logger.debug("------------------------------------") |
||
209 | for sound_device in self.core.sound_devices: |
||
210 | logger.debug("| %s | %s | %s", |
||
211 | 'X' if self.core.sound_device_can_capture(sound_device) else 'O', |
||
212 | 'X' if self.core.sound_device_can_playback(sound_device) else 'O', |
||
213 | sound_device |
||
214 | ) |
||
215 | logger.debug("------------------------------------") |
||
216 | logger.debug("using capture_device: %s", self.core.capture_device) |
||
217 | logger.debug("using playback_device: %s", self.core.playback_device) |
||
218 | |||
219 | # Only enable PCMU and PCMA audio codecs by default |
||
220 | config_audio_codecs = conf.get_list(SIPPHONE_SECTION, 'audio_codecs', 'PCMA,PCMU') |
||
221 | for codec in self.core.audio_codecs: |
||
222 | if codec.mime_type in config_audio_codecs: |
||
223 | logger.debug('enable audio codec %s', codec.mime_type) |
||
224 | self.core.enable_payload_type(codec, True) |
||
225 | else: |
||
226 | logger.debug('disable audio codec %s', codec.mime_type) |
||
227 | self.core.enable_payload_type(codec, False) |
||
228 | |||
229 | |||
230 | if len(self.core.video_devices) == 0: |
||
231 | self.core.video_capture_enabled = False |
||
232 | logger.warning('no video devices available') |
||
233 | else: |
||
234 | logger.info("found %s possible videodevices:", len(self.core.video_devices)) |
||
235 | logger.debug("| name") |
||
236 | logger.debug("------------------------------------") |
||
237 | for video_device in self.core.video_devices: |
||
238 | logger.debug("| %s ", video_device) |
||
239 | logger.debug("------------------------------------") |
||
240 | config_camera = conf.get(SIPPHONE_SECTION, 'video_device', self.core.video_devices[0]) |
||
241 | if config_camera not in self.core.video_devices: |
||
242 | logger.warning('camera "%s" from config does not exist in possible video devices.', config_camera) |
||
243 | logger.debug('switching to first possible video device "%s"', self.core.video_devices[0]) |
||
244 | config_camera = self.core.video_devices[0] |
||
245 | |||
246 | self.core.video_capture_enabled = True |
||
247 | self.core.video_device = config_camera |
||
248 | self.core.preferred_video_size_by_name = conf.get(SIPPHONE_SECTION, 'video_size', 'vga') |
||
249 | logger.debug("using video_device: %s", self.core.video_device) |
||
250 | |||
251 | # Only enable VP8 video codec |
||
252 | config_video_codecs = conf.get_list(SIPPHONE_SECTION, 'video_codecs', 'VP8') |
||
253 | for codec in self.core.video_codecs: |
||
254 | if codec.mime_type in config_video_codecs and self.core.video_capture_enabled: |
||
255 | logger.debug('enable video codec %s', codec.mime_type) |
||
256 | self.core.enable_payload_type(codec, True) |
||
257 | else: |
||
258 | logger.debug('disable video codec %s', codec.mime_type) |
||
259 | self.core.enable_payload_type(codec, False) |
||
260 | |||
261 | # Configure the SIP account |
||
262 | server = conf.get(SIPPHONE_SECTION, "sipserver_server") |
||
263 | username = conf.get(SIPPHONE_SECTION, "sipserver_username") |
||
264 | password = conf.get(SIPPHONE_SECTION, "sipserver_password", username) |
||
265 | realm = conf.get(SIPPHONE_SECTION, "sipserver_realm", server) |
||
266 | if server and username and password: |
||
267 | logger.info('using DoorPi with SIP-Server') |
||
268 | proxy_cfg = self.core.create_proxy_config() |
||
269 | proxy_cfg.identity_address = lin.Address.new("%s <sip:%s@%s>" % ( |
||
270 | conf.get(SIPPHONE_SECTION, "identity", 'DoorPi'), username, server) |
||
271 | ) |
||
272 | proxy_cfg.server_addr = "sip:%s"%server |
||
273 | proxy_cfg.register_enabled = True |
||
274 | self.core.add_proxy_config(proxy_cfg) |
||
275 | self.core.default_proxy_config = proxy_cfg |
||
276 | auth_info = self.core.create_auth_info(username, None, password, None, None, realm) |
||
277 | self.core.add_auth_info(auth_info) |
||
278 | else: |
||
279 | logger.info('using DoorPi without SIP-Server? Okay...') |
||
280 | proxy_cfg = self.core.create_proxy_config() |
||
281 | proxy_cfg.register_enabled = False |
||
282 | self.core.add_proxy_config(proxy_cfg) |
||
283 | self.core.default_proxy_config = proxy_cfg |
||
284 | logger.debug('%s',self.core.proxy_config_list) |
||
285 | |||
286 | logger.debug("start successfully") |
||
287 | |||
288 | def destroy(self): |
||
289 | logger.debug("destroy") |
||
290 | self.core.terminate_all_calls() |
||
291 | DoorPi().event_handler.fire_event_synchron('OnSipPhoneDestroy', __name__) |
||
292 | DoorPi().event_handler.unregister_source(__name__, True) |
||
293 | return |
||
294 | |||
295 | def self_check(self, *args, **kwargs): |
||
0 ignored issues
–
show
|
|||
296 | if not self.core: return |
||
297 | |||
298 | self.core.iterate() |
||
299 | |||
300 | if not self.current_call: return |
||
301 | |||
302 | if self.current_call.state < lin.CallState.Connected: |
||
303 | if self.current_call_duration > self.core.inc_timeout - 0.5: |
||
304 | logger.info("call timeout - hangup current call after %s seconds (max. %s)", self.current_call_duration, self.core.inc_timeout) |
||
305 | self.core.terminate_all_calls() |
||
306 | DoorPi().event_handler('OnSipPhoneCallTimeoutNoResponse', __name__) |
||
307 | else: |
||
308 | if int(self.current_call_duration) > self.core.in_call_timeout - 0.5: |
||
309 | logger.info("max call time reached - hangup current call after %s seconds (max. %s)", self.current_call_duration, self.core.in_call_timeout) |
||
310 | self.core.terminate_all_calls() |
||
311 | DoorPi().event_handler('OnSipPhoneCallTimeoutMaxCalltime', __name__) |
||
312 | |||
313 | def call(self, number): |
||
314 | DoorPi().event_handler('BeforeSipPhoneMakeCall', __name__, {'number':number}) |
||
315 | logger.debug("call (%s)",str(number)) |
||
316 | if not self.current_call: |
||
317 | logger.debug('no current call -> start new call') |
||
318 | self.reset_call_start_datetime() |
||
319 | if self.core.invite_with_params(number, self.base_config) is None: |
||
320 | if DoorPi().event_handler.db.get_event_log_entries_count('OnSipPhoneMakeCallFailed') > 5: |
||
321 | logger.error('failed to execute call five times') |
||
322 | else: |
||
323 | DoorPi().event_handler('OnSipPhoneMakeCallFailed', __name__, {'number':number}) |
||
324 | return None |
||
325 | DoorPi().event_handler('OnSipPhoneMakeCall', __name__, {'number':number}) |
||
326 | elif number in self.current_call.remote_address.as_string_uri_only(): |
||
327 | if self.current_call_duration <= 2: |
||
328 | logger.debug("same call %s again while call is running since %s seconds? -> skip", |
||
329 | self.core.current_call.remote_address.as_string_uri_only(), |
||
330 | self.current_call_duration |
||
331 | ) |
||
332 | else: |
||
333 | logger.debug("press twice with call duration > 1 second? Want to hangup current call? OK...") |
||
334 | self.core.terminate_all_calls() |
||
335 | else: |
||
336 | logger.debug("new call needed? hangup old first...") |
||
337 | self.core.terminate_all_calls() |
||
338 | self.call(number) |
||
339 | |||
340 | DoorPi().event_handler('AfterSipPhoneMakeCall', __name__, {'number':number}) |
||
341 | return self.current_call |
||
342 | |||
343 | def is_admin_number(self, remote_uri): |
||
344 | return self.callback.is_admin_number(remote_uri) |
||
345 | |||
346 | def hangup(self): |
||
347 | if self.current_call: |
||
348 | logger.debug("Received hangup request, cancelling current call") |
||
349 | self.core.terminate_call(self.current_call) |
||
350 | else: |
||
351 | logger.debug("Ignoring hangup request as there is no ongoing call") |
||
352 | |||
353 |