mirror of
				https://git.kernel.org/pub/scm/network/wireless/iwd.git
				synced 2025-11-04 08:57:29 +01:00 
			
		
		
		
	The Gtk.Switch representing the p2p.Device's Enabled property should use the "delayed state change" logic as described in Gtk.Switch docs, i.e. we need to use .set_state() instead of .set_active() when we get confirmation of the property having changed its value in the PropertiesChanged handler. The ::active property is automatically changed by Gtk.Switch on user input. This way the UI gives visual feedback of when the device enable/disable op starts and ends (or fails).
		
			
				
	
	
		
			1670 lines
		
	
	
		
			71 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1670 lines
		
	
	
		
			71 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#! /usr/bin/python3
 | 
						|
#
 | 
						|
# Copyright (C) 2020  Intel Corporation
 | 
						|
#
 | 
						|
# A simplified WFD source that streams the X11 screen using gstreamer
 | 
						|
# A more complete solution would create a virtual screen visible through the normal system calls, xrandr, etc.,
 | 
						|
# with its pixel aspect ratio, EDID data and what not.  This would allow the user to configure it like a real
 | 
						|
# display in mirror mode or side-by-side mode.
 | 
						|
 | 
						|
import sys
 | 
						|
import dbus
 | 
						|
import dbus.mainloop.glib
 | 
						|
import socket
 | 
						|
import collections
 | 
						|
import collections.abc
 | 
						|
import random
 | 
						|
import dataclasses
 | 
						|
import traceback
 | 
						|
import codecs
 | 
						|
try:
 | 
						|
    import netifaces
 | 
						|
except:
 | 
						|
    pass
 | 
						|
 | 
						|
import gi
 | 
						|
gi.require_version('GLib', '2.0')
 | 
						|
gi.require_version('Gst', '1.0')
 | 
						|
gi.require_version('Gtk', '3.0')
 | 
						|
from gi.repository import GLib, Gst, Gtk, Gdk, Pango, GObject
 | 
						|
 | 
						|
class WFDRTSPServer:
 | 
						|
    class RTSPException(Exception):
 | 
						|
        pass
 | 
						|
 | 
						|
    Prop = collections.namedtuple('Prop', ['name', 'desc', 'getter', 'setter', 'type', 'vals'])
 | 
						|
 | 
						|
    def __init__(self, port, state_handler, error_handler, init_values, prop_handler):
 | 
						|
        # Should start the TCP server only on the P2P connection's local IP but we won't
 | 
						|
        # know the IP or interface name until after the connection is established.  At that
 | 
						|
        # time the sink may try to make the TCP connection at any time so our listen
 | 
						|
        # socket should be up before this.
 | 
						|
        server_address = ('0.0.0.0', port)
 | 
						|
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
						|
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
 | 
						|
        self.server.bind(server_address)
 | 
						|
        self.server.listen(1)
 | 
						|
        GLib.io_add_watch(self.server, GLib.IO_IN, self.handle_connection)
 | 
						|
        self.conn = None
 | 
						|
        self.tx_queue = []
 | 
						|
        self.rx_queue = b''
 | 
						|
 | 
						|
        self.state_handler = state_handler
 | 
						|
        self.error_handler = error_handler
 | 
						|
        self.prop_handler = prop_handler
 | 
						|
        self.sm_init(init_values)
 | 
						|
 | 
						|
    def handle_data_out(self, conn, *args):
 | 
						|
        try:
 | 
						|
            cmd = self.tx_queue.pop(0)
 | 
						|
            sent = self.conn.send(cmd)
 | 
						|
 | 
						|
            if sent < len(cmd):
 | 
						|
                self.tx_queue.insert(0, cmd[sent:])
 | 
						|
 | 
						|
            return len(self.tx_queue) > 0
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def tx_queue_append(self, cmd):
 | 
						|
        if not self.tx_queue:
 | 
						|
            GLib.io_add_watch(self.conn.fileno(), GLib.IO_OUT, self.handle_data_out)
 | 
						|
 | 
						|
        self.tx_queue.append(cmd.encode('utf-8'))
 | 
						|
        self.debug('queued cmd: ' + cmd)
 | 
						|
 | 
						|
    def handle_data_hup(self, conn, *args):
 | 
						|
        try:
 | 
						|
            self.debug('HUP')
 | 
						|
            self.error('Disconnected')
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def handle_data_in(self, conn, *args):
 | 
						|
        try:
 | 
						|
            newdata = self.conn.recv(4096)
 | 
						|
            if len(newdata) == 0:
 | 
						|
                self.debug('recv returned 0 bytes')
 | 
						|
                # Disconnect from P2P
 | 
						|
                self.error('Disconnected')
 | 
						|
                return False
 | 
						|
 | 
						|
            self.debug('received data: ' + str(newdata))
 | 
						|
            self.rx_queue += newdata
 | 
						|
 | 
						|
            while b'\r\n\r\n' in self.rx_queue:
 | 
						|
                msg, content = self.rx_queue.split(b'\r\n\r\n', 1)
 | 
						|
                lines = msg.split(b'\r\n')
 | 
						|
 | 
						|
                headers = {}
 | 
						|
                for line in lines[1:]:
 | 
						|
                    if b':' not in line:
 | 
						|
                        # Bad syntax
 | 
						|
                        rxbuf = b''
 | 
						|
                        return True
 | 
						|
 | 
						|
                    name, value = line.decode('utf8').split(':', 1)
 | 
						|
                    name = name.lower()
 | 
						|
                    while len(value) and value[0] == ' ':
 | 
						|
                        value = value[1:]
 | 
						|
 | 
						|
                    if name in headers:
 | 
						|
                        # Duplicate
 | 
						|
                        rxbuf = b''
 | 
						|
                        return True
 | 
						|
 | 
						|
                    headers[name] = value
 | 
						|
 | 
						|
                cl = 0
 | 
						|
                if 'content-length' in headers:
 | 
						|
                    try:
 | 
						|
                        cl = int(headers['content-length'])
 | 
						|
                        if cl < 1 or cl > 1000:
 | 
						|
                            raise Exception('')
 | 
						|
                    except:
 | 
						|
                        # Bad syntax
 | 
						|
                        rxbuf = b''
 | 
						|
                        return True
 | 
						|
 | 
						|
                    if len(content) < cl:
 | 
						|
                        # Wait for more data
 | 
						|
                        return True
 | 
						|
 | 
						|
                top_line = lines[0].decode('utf8').split(None, 2)
 | 
						|
                self.rx_queue = self.rx_queue[len(msg) + 4 + cl:]
 | 
						|
                content = content[:cl]
 | 
						|
 | 
						|
                if top_line[2] == 'RTSP/1.0':
 | 
						|
                    self.source_handle_message(method=top_line[0], target=top_line[1], headers=headers, content=content)
 | 
						|
                elif top_line[0] == 'RTSP/1.0':
 | 
						|
                    try:
 | 
						|
                        status = int(top_line[1])
 | 
						|
                        if status < 1 or status > 999:
 | 
						|
                            raise Exception('Status out of range')
 | 
						|
                    except:
 | 
						|
                        self.error('Couldn\'t parse response status')
 | 
						|
 | 
						|
                    self.source_handle_message(status=status, reason=top_line[2], headers=headers, content=content)
 | 
						|
                else:
 | 
						|
                    # Bad protocol
 | 
						|
                    self.error('Unknown protocol in ' + str(top_line))
 | 
						|
 | 
						|
            return True
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def handle_connection(self, sock, *args):
 | 
						|
        try:
 | 
						|
            if self.conn:
 | 
						|
                return False
 | 
						|
            self.conn, addr = sock.accept()
 | 
						|
            self.debug('RTSP connection from: ' + str(addr))
 | 
						|
            self.remote_ip = addr[0]
 | 
						|
 | 
						|
            if self.expected_remote_ip and self.remote_ip != self.expected_remote_ip:
 | 
						|
                self.conn.close()
 | 
						|
                self.conn = None
 | 
						|
                self.debug('Connection refused, bad source address')
 | 
						|
                return True
 | 
						|
 | 
						|
            sock.close()
 | 
						|
            self.server = None
 | 
						|
            GLib.io_add_watch(self.conn.fileno(), GLib.IO_IN, self.handle_data_in)
 | 
						|
            GLib.io_add_watch(self.conn.fileno(), GLib.IO_HUP, self.handle_data_hup)
 | 
						|
 | 
						|
            self._state = 'init'
 | 
						|
            self.source_handle_message()
 | 
						|
            return False
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def error(self, msg):
 | 
						|
        self.enter_state('failed')
 | 
						|
        e = WFDRTSPServer.RTSPException('State ' + self._state + ': ' + msg)
 | 
						|
        self.debug('error: ' + msg)
 | 
						|
        raise e
 | 
						|
 | 
						|
    def warning(self, msg):
 | 
						|
        self.debug('warning: ' + msg)
 | 
						|
        print('Warning: ' + msg + '\n')
 | 
						|
 | 
						|
    def debug(self, msg):
 | 
						|
        pass
 | 
						|
 | 
						|
    @property
 | 
						|
    def state(self):
 | 
						|
        return self._state
 | 
						|
 | 
						|
    def enter_state(self, new_state):
 | 
						|
        self.debug('state change: ' + self._state + ' -> ' + new_state)
 | 
						|
        self._state = new_state
 | 
						|
        self.state_handler()
 | 
						|
 | 
						|
    @property
 | 
						|
    def ready(self):
 | 
						|
        return self._state in ['streaming', 'paused']
 | 
						|
 | 
						|
    @property
 | 
						|
    def props(self):
 | 
						|
        return self._props
 | 
						|
 | 
						|
    def sm_init(self, init_values):
 | 
						|
        self._state = 'waiting-rtsp'
 | 
						|
        self.local_params = {
 | 
						|
            'wfd_video_formats': '00 00 01 08 00000000 00000000 00000040 00 0000 0000 00 none none'
 | 
						|
        }
 | 
						|
        self.remote_params = {}
 | 
						|
        self.local_methods = [ 'org.wfa.wfd1.0', 'SET_PARAMETER', 'GET_PARAMETER', 'PLAY', 'SETUP', 'TEARDOWN' ]
 | 
						|
        self.presentation_url = [ 'rtsp://127.0.0.1/wfd1.0/streamid=0', 'none' ] # Table 88
 | 
						|
        self.alternative_urls = [ 'rtsp://localhost/wfd1.0/streamid=0' ]
 | 
						|
        self.session_stream_url = None
 | 
						|
        self.session_id = None
 | 
						|
        self.session_timeout = 60
 | 
						|
        self.local_cseq = 0
 | 
						|
        self.remote_cseq = None
 | 
						|
        self.last_method = None
 | 
						|
        self.last_require = []
 | 
						|
        self.last_params = []
 | 
						|
        self.remote_rtp_port = None
 | 
						|
        self.remote_rtcp_port = None
 | 
						|
        self.local_rtp_port = None
 | 
						|
        self.local_rtcp_port = None
 | 
						|
        self.use_tcp = None
 | 
						|
        self.rtp_pipeline = None
 | 
						|
        self.rtsp_keepalive = None
 | 
						|
        self.rtsp_keepalive_timeout = None
 | 
						|
        self.expected_remote_ip = None
 | 
						|
        self.remote_ip = None
 | 
						|
        self.init_width = init_values['width']
 | 
						|
        self.init_height = init_values['height']
 | 
						|
        self.rtcp_enabled = init_values['rtcp_enabled']
 | 
						|
 | 
						|
        self._props = []
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_init_props():
 | 
						|
        props = []
 | 
						|
        values = {
 | 
						|
            'width': 800,
 | 
						|
            'height': 600,
 | 
						|
            'rtcp_enabled': True
 | 
						|
        }
 | 
						|
 | 
						|
        def set_val(key, val):
 | 
						|
            values[key] = val
 | 
						|
        props.append(WFDRTSPServer.Prop('Output width', 'Scale the video stream to this X resolution for sending',
 | 
						|
            lambda: values['width'], lambda x: set_val('width', x), int, (640, 1920)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Output height', 'Scale the video stream to this Y resolution for sending',
 | 
						|
            lambda: values['height'], lambda x: set_val('height', x), int, (480, 1080)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Enable RTCP', 'Use RTCP if the Sink requests it during setup',
 | 
						|
            lambda: values['rtcp_enabled'], lambda x: set_val('rtcp_enabled', x), bool, None))
 | 
						|
        # TODO: Enable Audio
 | 
						|
        # TODO: Audio source
 | 
						|
 | 
						|
        return props, values
 | 
						|
 | 
						|
    def close(self):
 | 
						|
        # Avoid passing self to io watches so that the refcount can ever reach 0 and
 | 
						|
        # all this can be done in __del__
 | 
						|
        if self.rtsp_keepalive:
 | 
						|
            GLib.source_remove(self.rtsp_keepalive)
 | 
						|
            self.rtsp_keepalive = None
 | 
						|
        if self.rtsp_keepalive_timeout:
 | 
						|
            GLib.source_remove(self.rtsp_keepalive_timeout)
 | 
						|
            self.rtsp_keepalive_timeout = None
 | 
						|
        if self.rtp_pipeline:
 | 
						|
            self.rtp_pipeline.set_state(Gst.State.NULL)
 | 
						|
            self.rtp_pipeline = None
 | 
						|
        if self.server:
 | 
						|
            self.server.close()
 | 
						|
            self.server = None
 | 
						|
        if self.conn:
 | 
						|
            self.conn.close()
 | 
						|
            self.conn = None
 | 
						|
 | 
						|
    def set_local_interface(self, new_value):
 | 
						|
        try:
 | 
						|
            local_ip = netifaces.ifaddresses(new_value)[netifaces.AF_INET][0]['addr']
 | 
						|
            self.alternative_urls.append('rtsp://' + local_ip + '/wfd1.0/streamid=0')
 | 
						|
        except:
 | 
						|
            pass
 | 
						|
 | 
						|
    def set_remote_ip(self, new_value):
 | 
						|
        self.expected_remote_ip = new_value
 | 
						|
 | 
						|
        if self.conn and self.remote_ip != self.expected_remote_ip:
 | 
						|
            self.error_handler(WFDRTSPServer.RTSPException('Connection was from a wrong IP')) # TODO: do this in an idle cb
 | 
						|
 | 
						|
    def validate_msg(self, method, expected_method, status, reason, headers, target, content):
 | 
						|
        if expected_method is None:
 | 
						|
            # Expected a response, not a request
 | 
						|
            if method is not None:
 | 
						|
                self.error('Received a "' + method + '" request where a response was expected')
 | 
						|
            if status < 200 or status > 299:
 | 
						|
                self.error('Response status ' + str(status) + ' and reason: ' + reason)
 | 
						|
            if status != 200:
 | 
						|
                self.warning('Response status was ' + str(status) + ' ("' + reason + '") in state ' + self._state)
 | 
						|
 | 
						|
            try:
 | 
						|
                if int(headers['cseq']) != self.local_cseq:
 | 
						|
                    self.error('Response CSeq doesn\'t match')
 | 
						|
            except:
 | 
						|
                self.error('Missing or unparsable CSeq in a response')
 | 
						|
 | 
						|
            if self.last_method == 'OPTIONS':
 | 
						|
                if 'public' not in headers:
 | 
						|
                    self.error('Missing "Public" header in OPTIONS response')
 | 
						|
                public = [ m.strip() for m in headers['public'].split(',') ]
 | 
						|
                missing = [ m for m in self.last_require if m not in public ]
 | 
						|
                if missing:
 | 
						|
                    self.error('Missing required method(s) "' + '", "'.join(missing) + '" in OPTIONS response')
 | 
						|
 | 
						|
            if self.last_method == 'GET_PARAMETER':
 | 
						|
                params = {}
 | 
						|
                for line in content.split(b'\r\n'):
 | 
						|
                    if b':' not in line:
 | 
						|
                        continue
 | 
						|
                    k, v = line.decode('utf8').split(':', 1)
 | 
						|
                    if k.strip() in params:
 | 
						|
                        self.error('Duplicate key "' + k + '" in GET_PARAMETER response')
 | 
						|
                    params[k.strip()] = v.strip()
 | 
						|
                missing = [ p for p in self.last_params if p not in params ]
 | 
						|
                if missing: # Not an error
 | 
						|
                    self.warning('Missing key(s) "' + '", "'.join(missing) + '" in GET_PARAMETER response')
 | 
						|
                self.remote_params.update(params)
 | 
						|
 | 
						|
            return
 | 
						|
 | 
						|
        if method is None:
 | 
						|
            self.error('Received an RTSP response where a ' + expected_method + ' was expected')
 | 
						|
 | 
						|
        if method != expected_method:
 | 
						|
            self.error('Received a "' + method + '" request where a ' + expected_method + ' was expected')
 | 
						|
        try:
 | 
						|
            if self.remote_cseq is not None and int(headers['cseq']) <= self.remote_cseq:
 | 
						|
                self.error('Unchanged CSeq in a new request')
 | 
						|
            self.remote_cseq = int(headers['cseq'])
 | 
						|
        except:
 | 
						|
            self.error('Missing or unparsable CSeq in a new request')
 | 
						|
        if method == 'OPTIONS' and 'require' not in headers:
 | 
						|
            self.error('Missing "Require" header in OPTIONS request')
 | 
						|
        elif method == 'SETUP' and 'transport' not in headers:
 | 
						|
            self.error('Missing "Transport" header in SETUP request')
 | 
						|
        elif method == 'SETUP' and (target not in self.presentation_url + self.alternative_urls or target == 'none'):
 | 
						|
            self.error('Unknown target "' + target + '" in SETUP request')
 | 
						|
        elif method == 'PLAY' and ('session' not in headers  or headers['session'] != self.session_id):
 | 
						|
            self.error('Missing or invalid "Session" header in PLAY request')
 | 
						|
        elif method == 'PLAY' and target != self.session_stream_url:
 | 
						|
            self.error('Unknown target "' + target + '" in PLAY request')
 | 
						|
        elif method == 'PAUSE' and 'session' not in headers:
 | 
						|
            self.error('Missing "Session" header in PAUSE request')
 | 
						|
        elif method == 'PAUSE' and target != self.session_stream_url:
 | 
						|
            self.error('Unknown target "' + target + '" in PAUSE request')
 | 
						|
        elif method == 'TEARDOWN' and 'session' not in headers:
 | 
						|
            self.error('Missing "Session" header in TEARDOWN request')
 | 
						|
        elif method == 'TEARDOWN' and target != self.session_stream_url:
 | 
						|
            self.error('Unknown target "' + target + '" in TEARDOWN request')
 | 
						|
        elif method == 'SET_PARAMETER':
 | 
						|
            params = []
 | 
						|
            names = []
 | 
						|
            for line in content.split(b'\r\n'):
 | 
						|
                param = (line.decode('utf8').strip(), None)
 | 
						|
                if not param[0]:
 | 
						|
                    continue
 | 
						|
                if ':' in param[0]:
 | 
						|
                    k, v = param[0].split(':', 1)
 | 
						|
                    param = (k.strip(), v.strip())
 | 
						|
                if param[0] in names:
 | 
						|
                    self.error('Duplicate key "' + param[0] + '" in SET_PARAMETER response')
 | 
						|
 | 
						|
                names.append(param[0])
 | 
						|
                params.append(param)
 | 
						|
            self.last_params = params
 | 
						|
 | 
						|
    def request(self, method, target, require=[], params=[]):
 | 
						|
        content = ''
 | 
						|
        cmd = method + ' ' + target + ' RTSP/1.0\r\n'
 | 
						|
 | 
						|
        self.local_cseq += 1
 | 
						|
        cmd += 'CSeq: ' + str(self.local_cseq) + '\r\n'
 | 
						|
 | 
						|
        if require:
 | 
						|
            cmd += 'Require: ' + ', '.join(require) + '\r\n'
 | 
						|
 | 
						|
        if params:
 | 
						|
            if isinstance(params, collections.abc.Mapping):
 | 
						|
                content = ''.join([ k + ': ' + params[k] + '\r\n' for k in params ])
 | 
						|
            else:
 | 
						|
                content = ''.join([ k + '\r\n' for k in params ])
 | 
						|
            content_type = 'text/parameters'
 | 
						|
 | 
						|
        if content:
 | 
						|
            cmd += 'Content-Type: ' + content_type + '\r\n'
 | 
						|
            cmd += 'Content-Length: ' + str(len(content)) + '\r\n'
 | 
						|
 | 
						|
        cmd += '\r\n'
 | 
						|
        self.tx_queue_append(cmd + content)
 | 
						|
        self.last_method = method
 | 
						|
        self.last_require = require
 | 
						|
        self.last_params = params
 | 
						|
 | 
						|
    def response(self, public=[], session=None, transport=None):
 | 
						|
        cmd = 'RTSP/1.0 200 OK\r\n'
 | 
						|
 | 
						|
        cmd += 'CSeq: ' + str(self.remote_cseq) + '\r\n'
 | 
						|
 | 
						|
        if public:
 | 
						|
            cmd += 'Public: ' + ', '.join(public) + '\r\n'
 | 
						|
        if session is not None:
 | 
						|
            cmd += 'Session: ' + session + '\r\n'
 | 
						|
        if transport is not None:
 | 
						|
            cmd += 'Transport: ' + transport + '\r\n'
 | 
						|
 | 
						|
        cmd += '\r\n'
 | 
						|
        self.tx_queue_append(cmd)
 | 
						|
 | 
						|
    def parse_video_formats(self, value):
 | 
						|
        # TODO
 | 
						|
        pass
 | 
						|
 | 
						|
    def parse_client_rtp_ports(self, value):
 | 
						|
        profile, rtp_p0_str, rtp_p1_str, mode = value.split()
 | 
						|
        try:
 | 
						|
            rtp_p0 = int(rtp_p0_str)
 | 
						|
            rtp_p1 = int(rtp_p1_str)
 | 
						|
        except:
 | 
						|
            self.error('Can\'t parse rtp-port in wfd-client-rtp-ports: ' + value)
 | 
						|
        if rtp_p0 < 1 or rtp_p0 > 65535:
 | 
						|
            self.error('rtp-port0 not valid for Primary Sink: ' + rtp_p0_str)
 | 
						|
        if rtp_p1 != 0: # Table 90
 | 
						|
            self.error('rtp-port1 not valid for Primary Sink: ' + rtp_p1_str)
 | 
						|
        if profile not in ['RTP/AVP/UDP;unicast', 'RTP/AVP/TCP;unicast']:
 | 
						|
            self.error('Unknown RTP transport in wfd-client-rtp-ports: ' + profile)
 | 
						|
        if mode != 'mode=play':
 | 
						|
            self.error('Unknown mode in wfd-client-rtp-ports: ' + mode)
 | 
						|
        self.remote_rtp_port = rtp_p0
 | 
						|
        self.use_tcp = (profile == 'RTP/AVP/TCP;unicast')
 | 
						|
 | 
						|
    def parse_transport(self, value):
 | 
						|
        params = value.split(';')
 | 
						|
        if len(params) < 3:
 | 
						|
            self.error('Can\'t split SETUP Transport header into profile and port numbers: ' + value)
 | 
						|
        profile = ';'.join(params[0:2])
 | 
						|
        if profile not in ['RTP/AVP/UDP;unicast', 'RTP/AVP/TCP;unicast']:
 | 
						|
            self.error('Unknown RTP transport in SETUP Transport header: ' + profile)
 | 
						|
        if self.use_tcp != (profile == 'RTP/AVP/TCP;unicast'):
 | 
						|
            self.error('RTP transport in SETUP Transport header different from what we sent in M4: ' + profile)
 | 
						|
        client_port_strs = [p for p in params[2:] if p.startswith('client_port=')]
 | 
						|
        if len(client_port_strs) != 1:
 | 
						|
            self.error('Can\'t find client-port in SETUP Transport header: ' + value)
 | 
						|
        client_ports = client_port_strs[0].split('=', 1)[1].split('-')
 | 
						|
        try:
 | 
						|
            rtp_port = int(client_ports[0])
 | 
						|
            if len(client_ports) > 1:
 | 
						|
                rtcp_port = int(client_ports[1])
 | 
						|
        except:
 | 
						|
            self.error('Can\'t parse client-port in SETUP Transport header: ' + client_port_strs[0])
 | 
						|
        if rtp_port != self.remote_rtp_port:
 | 
						|
            self.error('client-port in SETUP Transport header doesn\'t match what we sent in M4: ' + str(rtp_port))
 | 
						|
        if len(client_ports) > 1:
 | 
						|
            if rtcp_port < 1 or rtcp_port > 65535 or rtcp_port == rtp_port: # Actually must be rtp_port + 1...
 | 
						|
                self.error('Optional RTCP port not valid in SETUP Transport header: ' + str(rtcp_port))
 | 
						|
            self.remote_rtcp_port = rtcp_port
 | 
						|
 | 
						|
        self._props.append(WFDRTSPServer.Prop('RTP transport', '', lambda: 'TCP' if self.use_tcp else 'UDP', None, str, None))
 | 
						|
        self._props.append(WFDRTSPServer.Prop('Remote RTP port', '', lambda: self.remote_rtp_port, None, int, None))
 | 
						|
        self._props.append(WFDRTSPServer.Prop('Remote RTCP port', '', lambda: self.remote_rtcp_port, None, int, None))
 | 
						|
 | 
						|
    def parse_display_edid(self):
 | 
						|
        try:
 | 
						|
            len_str, hex_str = self.remote_params['wfd_display_edid'].split(' ', 1)
 | 
						|
            if len(len_str.strip()) != 4:
 | 
						|
                raise Exception('edid-block-count length is not 4 hex digits')
 | 
						|
            blocks = int(len_str, 16)
 | 
						|
            edid = codecs.decode(hex_str.strip(), 'hex')
 | 
						|
            if blocks < 1 or blocks > 256 or blocks * 128 != len(edid):
 | 
						|
                raise Exception('edid-block-count value wrong')
 | 
						|
        except:
 | 
						|
            edid = None
 | 
						|
 | 
						|
        self._props.append(WFDRTSPServer.Prop('EDID info', 'Remote display\'s EDID data', lambda: edid, None, bytes, None))
 | 
						|
 | 
						|
    def create_running_props(self):
 | 
						|
        src = self.rtp_pipeline.get_by_name('src')
 | 
						|
        fps = self.rtp_pipeline.get_by_name('fps')
 | 
						|
        enc = self.rtp_pipeline.get_by_name('videnc')
 | 
						|
        res = self.rtp_pipeline.get_by_name('res')
 | 
						|
        sink = self.rtp_pipeline.get_by_name('sink')
 | 
						|
        self.pipeline_props = []
 | 
						|
 | 
						|
        srcpadcaps = src.srcpads[0].get_allowed_caps()
 | 
						|
        width = srcpadcaps[0]['width']
 | 
						|
        height = srcpadcaps[0]['height']
 | 
						|
        props = []
 | 
						|
        props.append(WFDRTSPServer.Prop('Local width', 'Local screen X resolution', lambda: width, None, int, None))
 | 
						|
        props.append(WFDRTSPServer.Prop('Local height', 'Local screen Y resolution', lambda: height, None, int, None))
 | 
						|
 | 
						|
        def set_use_damage(val):
 | 
						|
            src.props.use_damage = val
 | 
						|
        props.append(WFDRTSPServer.Prop('Use XDamage', 'Try to use XDamage to reduce bandwidth usage',
 | 
						|
            lambda: src.props.use_damage, set_use_damage, bool, None))
 | 
						|
 | 
						|
        src.props.endx = width
 | 
						|
        src.props.endy = height
 | 
						|
        def set_startx(val):
 | 
						|
            src.set_property('startx', min(val, src.props.endx - 1))
 | 
						|
        def set_starty(val):
 | 
						|
            src.set_property('starty', min(val, src.props.endy - 1))
 | 
						|
        def set_endx(val):
 | 
						|
            src.set_property('endx', max(val, src.props.startx + 1))
 | 
						|
        def set_endy(val):
 | 
						|
            src.set_property('endy', max(val, src.props.starty + 1))
 | 
						|
        props.append(WFDRTSPServer.Prop('Window min X', 'Skip this many pixels on the left side of the local screen',
 | 
						|
            lambda: src.props.startx, set_startx, int, (0, width - 1)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Window min Y', 'Skip this many pixels on the top of the local screen',
 | 
						|
            lambda: src.props.starty, set_starty, int, (0, height - 1)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Window max X', 'Send screen contents only up to this X coordinate',
 | 
						|
            lambda: src.props.endx, set_endx, int, (1, width)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Window max Y', 'Send screen contents only up to this Y coordinate',
 | 
						|
            lambda: src.props.endy, set_endy, int, (1, height)))
 | 
						|
 | 
						|
        def set_framerate(val):
 | 
						|
            fps.props.caps[0]['framerate'] = Gst.Fraction(val)
 | 
						|
        def set_width(val):
 | 
						|
            res.props.caps[0]['width'] = val
 | 
						|
        def set_height(val):
 | 
						|
            res.props.caps[0]['height'] = val
 | 
						|
        props.append(WFDRTSPServer.Prop('Framerate', 'Try to output this many frames per second',
 | 
						|
            lambda: int(fps.props.caps[0]['framerate'].num), set_framerate, int, (1, 30)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Output width', 'Scale the video stream to this X resolution for sending',
 | 
						|
            lambda: res.props.caps[0]['width'], set_width, int, (640, 1920)))
 | 
						|
        props.append(WFDRTSPServer.Prop('Output height', 'Scale the video stream to this Y resolution for sending',
 | 
						|
            lambda: res.props.caps[0]['height'], set_height, int, (480, 1080)))
 | 
						|
 | 
						|
        preset_values = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast', 'placebo']
 | 
						|
        preset_map = {'veryslow': 9, 'slower': 8, 'slow': 7, 'medium': 6, 'fast': 5, 'faster': 4, 'veryfast': 3, 'superfast': 2, 'ultrafast': 1, 'placebo': 10}
 | 
						|
 | 
						|
        def set_speed_preset(val):
 | 
						|
            enc.props.speed_preset = preset_map[val]
 | 
						|
        props.append(WFDRTSPServer.Prop('H.264 speed preset', 'Speed/quality setting of the H.264 encoder to optimise bandwidth/latency',
 | 
						|
            lambda: enc.props.speed_preset.value_nick, set_speed_preset, str, preset_values))
 | 
						|
 | 
						|
        def set_max_lateness(val):
 | 
						|
            if val <= 0:
 | 
						|
                sink.props.max_lateness = -1
 | 
						|
            else:
 | 
						|
                sink.props.max_lateness = val * 1000000 # milliseconds to nanoseconds
 | 
						|
        props.append(WFDRTSPServer.Prop('Max lateness', 'Maximum number of milliseconds that a buffer can be late before it is dropped, or 0 for unlimited',
 | 
						|
            lambda: 0 if sink.props.max_lateness == -1 else sink.props.max_lateness / 1000000, set_max_lateness, int, (-1, 3000)))
 | 
						|
 | 
						|
        return props
 | 
						|
 | 
						|
    def on_gst_message(self, bus, message):
 | 
						|
        t = message.type
 | 
						|
        if t == Gst.MessageType.EOS:
 | 
						|
            self.error('Gstreamer end-of-stream')
 | 
						|
        elif t == Gst.MessageType.STATE_CHANGED:
 | 
						|
            old, new, pending = message.parse_state_changed()
 | 
						|
            self.debug('Gstreamer state change for ' + message.src.name + ' from ' + str(old) + ' to ' + str(new) + ', pending=' + str(pending))
 | 
						|
            if message.src == self.rtp_pipeline:
 | 
						|
                self.prop_handler()
 | 
						|
        elif t == Gst.MessageType.INFO:
 | 
						|
            err, debug = message.parse_info()
 | 
						|
            self.debug('Gstreamer info for ' + message.src.name + ': ' + str(err) + '\nDebug: ' + str(debug))
 | 
						|
        elif t == Gst.MessageType.WARNING:
 | 
						|
            err, debug = message.parse_warning()
 | 
						|
            self.debug('Gstreamer warning for ' + message.src.name + ': ' + str(err) + '\nDebug: ' + str(debug))
 | 
						|
        elif t == Gst.MessageType.ERROR:
 | 
						|
            err, debug = message.parse_error()
 | 
						|
            self.error('Gstreamer error for ' + message.src.name + ': ' + str(err) + '\nDebug: ' + str(debug))
 | 
						|
        else:
 | 
						|
            self.debug('Gstreamer message of type ' + str(t) + ' for ' + message.src.name + ': ' + str(message))
 | 
						|
        return True
 | 
						|
 | 
						|
    def force_keyframe(self):
 | 
						|
        enc = self.rtp_pipeline.get_by_name('videnc')
 | 
						|
        sink = enc.get_static_pad('sink')
 | 
						|
        timestamp = Gst.CLOCK_TIME_NONE # can/should we use sink.query_position?
 | 
						|
 | 
						|
        s = Gst.Structure('GstForceKeyUnit')
 | 
						|
        s.set_value('timestamp', GObject.Value(GObject.TYPE_UINT64, timestamp))
 | 
						|
        s.set_value('stream-time', GObject.Value(GObject.TYPE_UINT64, timestamp))
 | 
						|
        s.set_value('all-headers', GObject.Value(GObject.TYPE_BOOLEAN, True))
 | 
						|
        # TODO: can we also send this event directly to the element instead of the pad?
 | 
						|
        sink.send_event(Gst.Event.new_custom(Gst.EventType.CUSTOM_DOWNSTREAM, s))
 | 
						|
 | 
						|
    def reset_stream(self):
 | 
						|
        if self.rtp_pipeline.get_state(timeout=0)[1] == Gst.State.PLAYING:
 | 
						|
            self.rtp_pipeline.set_state(Gst.State.PAUSED)
 | 
						|
            self.rtp_pipeline.set_state(Gst.State.PLAYING)
 | 
						|
 | 
						|
    def rtsp_keepalive_timeout_cb(self):
 | 
						|
        try:
 | 
						|
            self.rtsp_keepalive_timeout = None
 | 
						|
            self.error('Keep-alive response timed out')
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def rtsp_keepalive_cb(self):
 | 
						|
        try:
 | 
						|
            # Send M16
 | 
						|
            # May need to start being careful with other requests that may be running...
 | 
						|
            self.request('GET_PARAMETER', 'rtsp://localhost/wfd1.0')
 | 
						|
            self.rtsp_keepalive_timeout = GLib.timeout_add_seconds(5, self.rtsp_keepalive_timeout_cb)
 | 
						|
            return True
 | 
						|
        except Exception as e:
 | 
						|
            self.error_handler(e)
 | 
						|
            return False
 | 
						|
 | 
						|
    def source_handle_message(self, method=None, target=None, status=None, reason=None, headers={}, content=None):
 | 
						|
        # TODO: check the 6s timeouts as per Section 6.5
 | 
						|
        # Source side M1-M8 simplified state machine
 | 
						|
        if self._state == 'init':
 | 
						|
            # Send M1
 | 
						|
            self.request('OPTIONS', '*', require=['org.wfa.wfd1.0'])
 | 
						|
            self.enter_state('M1')
 | 
						|
        elif self._state == 'M1':
 | 
						|
            # Validate M1 response
 | 
						|
            self.validate_msg(method, None, status, reason, headers, None, content)
 | 
						|
            methods = [ m.strip() for m in headers['public'].split(',') ]
 | 
						|
            required = [ 'org.wfa.wfd1.0', 'SET_PARAMETER', 'GET_PARAMETER' ]
 | 
						|
            missing = [ m for m in required if m not in methods ]
 | 
						|
            if missing:
 | 
						|
                self.error('Missing required method(s) "' + '", "'.join(missing) + '" in OPTIONS response')
 | 
						|
            self.enter_state('M2')
 | 
						|
        elif self._state == 'M2':
 | 
						|
            # Validate M2
 | 
						|
            self.validate_msg(method, 'OPTIONS', status, reason, headers, target, content)
 | 
						|
            if target not in [ '*' ] + self.presentation_url:
 | 
						|
                self.error('Unknown OPTIONS target "' + target + '"')
 | 
						|
            required = [ m.strip() for m in headers['require'].split(',') ]
 | 
						|
            missing = [ m for m in required if m not in self.local_methods ]
 | 
						|
            if missing:
 | 
						|
                self.error('Required methods in OPTIONS request that we don\'t support: ' + ','.join(missing))
 | 
						|
 | 
						|
            # Send M2 response
 | 
						|
            self.response(public=self.local_methods)
 | 
						|
            # Send M3
 | 
						|
            params = ['wfd_audio_codecs', 'wfd_video_formats', 'wfd_client_rtp_ports', 'wfd_display_edid', 'wfd_uibc_capability']
 | 
						|
            self.request('GET_PARAMETER', 'rtsp://localhost/wfd1.0', params=params)
 | 
						|
            self.enter_state('M3')
 | 
						|
        elif self._state == 'M3':
 | 
						|
            # Validate M3 response
 | 
						|
            self.validate_msg(method, None, status, reason, headers, None, content)
 | 
						|
            if 'wfd_video_formats' not in self.remote_params or 'wfd_client_rtp_ports' not in self.remote_params:
 | 
						|
                self.error('Required parameters missing from GET_PARAMETER response')
 | 
						|
            self.parse_video_formats(self.remote_params['wfd_video_formats'])
 | 
						|
            self.parse_client_rtp_ports(self.remote_params['wfd_client_rtp_ports'])
 | 
						|
            self.parse_display_edid()
 | 
						|
            self.prop_handler()
 | 
						|
            # Send M4
 | 
						|
            params = {
 | 
						|
                'wfd_video_formats': self.local_params['wfd_video_formats'],
 | 
						|
                'wfd_client_rtp_ports': self.remote_params['wfd_client_rtp_ports'],
 | 
						|
                'wfd_presentation_URL': self.presentation_url[0] + ' ' + self.presentation_url[1],
 | 
						|
                # TODO: include wfd_audio_codecs if audio present, make video optional, too
 | 
						|
                # TODO: support wfd2_video_formats and wfd2_audio_codecs
 | 
						|
            }
 | 
						|
            self.request('SET_PARAMETER', 'rtsp://localhost/wfd1.0', params=params)
 | 
						|
            self.enter_state('M4')
 | 
						|
        elif self._state == 'M4':
 | 
						|
            # Validate M4 response
 | 
						|
            self.validate_msg(method, None, status, reason, headers, None, content)
 | 
						|
            # Send M5
 | 
						|
            self.request('SET_PARAMETER', 'rtsp://localhost/wfd1.0', params={'wfd_trigger_method': 'SETUP'})
 | 
						|
            self.enter_state('M5')
 | 
						|
        elif self._state == 'M5':
 | 
						|
            # Validate M5 response
 | 
						|
            self.validate_msg(method, None, status, reason, headers, None, content)
 | 
						|
            self.enter_state('M6')
 | 
						|
        elif self._state == 'M6':
 | 
						|
            # Validate M6
 | 
						|
            self.validate_msg(method, 'SETUP', status, reason, headers, target, content)
 | 
						|
            self.parse_transport(headers['transport'])
 | 
						|
            self.session_stream_url = target
 | 
						|
            self.session_id = str(random.randint(a=1, b=999999))
 | 
						|
            self.local_rtp_port = random.randint(a=20000, b=30000)
 | 
						|
            if self.remote_rtcp_port is not None and self.rtcp_enabled:
 | 
						|
                self.local_rtcp_port = self.local_rtp_port + 1
 | 
						|
            profile ='RTP/AVP/TCP;unicast' if self.use_tcp else 'RTP/AVP/UDP;unicast'
 | 
						|
            client_port = str(self.remote_rtp_port) + (('-' + str(self.remote_rtcp_port)) if self.remote_rtcp_port is not None else '')
 | 
						|
            server_port = str(self.local_rtp_port) + (('-' + str(self.local_rtcp_port)) if self.local_rtcp_port is not None else '')
 | 
						|
            transport = profile + ';client_port' + client_port + ';server_port=' + server_port
 | 
						|
            # Section B.1
 | 
						|
            pipeline = ('ximagesrc name=src use-damage=false do-timestamp=true ! capsfilter name=fps caps=video/x-raw,framerate=10/1' +
 | 
						|
                ' ! videoscale method=0 ! capsfilter name=res caps=video/x-raw,width=' + str(self.init_width) + ',height=' + str(self.init_height) +
 | 
						|
                ' ! videoconvert ! video/x-raw,format=I420 ! x264enc tune=zerolatency speed-preset=ultrafast name=videnc' +
 | 
						|
                ' ! queue' + # TODO: add leaky=downstream
 | 
						|
                ' ! mpegtsmux name=mux' +
 | 
						|
                ' ! rtpmp2tpay pt=33 mtu=1472 ! .send_rtp_sink rtpsession name=session .send_rtp_src' +
 | 
						|
                ' ! udpsink name=sink host=' + self.remote_ip + ' port=' + str(self.remote_rtp_port) + ' bind-port=' + str(self.local_rtp_port)) # TODO: bind-address
 | 
						|
            if self.local_rtcp_port is not None:
 | 
						|
                pipeline += ' session.send_rtcp_src ! udpsink name=rtcp_sink host=' + self.remote_ip + \
 | 
						|
                    ' port=' + str(self.remote_rtcp_port) + ' bind-port=' + str(self.local_rtcp_port) # TODO: bind-address
 | 
						|
            self._props.append(WFDRTSPServer.Prop('RTCP enabled', 'Whether we\'re currently sending RTCP data',
 | 
						|
                lambda: self.local_rtcp_port is not None, None, bool, None))
 | 
						|
 | 
						|
            self.rtp_pipeline = Gst.parse_launch(pipeline)
 | 
						|
            bus = self.rtp_pipeline.get_bus()
 | 
						|
            bus.enable_sync_message_emission()
 | 
						|
            bus.add_signal_watch()
 | 
						|
            bus.connect('message', self.on_gst_message)
 | 
						|
 | 
						|
            self._props += self.create_running_props()
 | 
						|
            self.prop_handler()
 | 
						|
 | 
						|
            # Send M6 response
 | 
						|
            self.response(session=self.session_id + ';timeout=' + str(self.session_timeout), transport=transport)
 | 
						|
            self.enter_state('M7')
 | 
						|
        elif self._state in ['M7', 'paused']:
 | 
						|
            # Validate M7
 | 
						|
            self.validate_msg(method, 'PLAY', status, reason, headers, target, content)
 | 
						|
            # Send M7 response
 | 
						|
            self.response()
 | 
						|
            self.rtp_pipeline.set_state(Gst.State.PLAYING)
 | 
						|
            # Set up the keep-alive timer, interval must be less than timeout minus 5 seconds
 | 
						|
            self.rtsp_keepalive = GLib.timeout_add_seconds(self.session_timeout - 10, self.rtsp_keepalive_cb)
 | 
						|
            self.enter_state('streaming')
 | 
						|
        elif self._state == 'streaming':
 | 
						|
            if method is None:
 | 
						|
                if self.rtsp_keepalive_timeout:
 | 
						|
                    # The M16 response is not to be validated (Section 6.4.16)
 | 
						|
                    GLib.source_remove(self.rtsp_keepalive_timeout)
 | 
						|
                    self.rtsp_keepalive_timeout = None
 | 
						|
                    return
 | 
						|
                self.error('Received an RTSP response where a request was expected')
 | 
						|
            if method == 'PAUSE':
 | 
						|
                self.validate_msg(method, 'PAUSE', status, reason, headers, target, content)
 | 
						|
                self.rtp_pipeline.set_state(Gst.State.PAUSED)
 | 
						|
                self.enter_state('paused')
 | 
						|
                self.response()
 | 
						|
                return
 | 
						|
            if method == 'SET_PARAMETER':
 | 
						|
                self.validate_msg(method, 'SET_PARAMETER', status, reason, headers, target, content)
 | 
						|
                for k, v in self.last_params:
 | 
						|
                    if k == 'wfd_idr_request' and v is None:
 | 
						|
                        self.force_keyframe()
 | 
						|
                        self.response()
 | 
						|
                    else:
 | 
						|
                        self.error('Unknown request "' + k + '" with value ' + repr(v))
 | 
						|
                return
 | 
						|
            if method == 'TEARDOWN':
 | 
						|
                # The spec suggests a more graceful teardown but we just close the connection
 | 
						|
                self.error('Teardown requested')
 | 
						|
            self.error('Unsupported method "' + method + '"')
 | 
						|
 | 
						|
WIPHY_IF = 'net.connman.iwd.Adapter'
 | 
						|
DEVICE_IF = 'net.connman.iwd.p2p.Device'
 | 
						|
PEER_IF = 'net.connman.iwd.p2p.Peer'
 | 
						|
WSC_IF = 'net.connman.iwd.SimpleConfiguration'
 | 
						|
WFD_IF = 'net.connman.iwd.p2p.Display'
 | 
						|
SVC_MGR_IF = 'net.connman.iwd.p2p.ServiceManager'
 | 
						|
 | 
						|
class WFDSource(Gtk.Window):
 | 
						|
    @dataclasses.dataclass
 | 
						|
    class Device:
 | 
						|
        props: dict
 | 
						|
        dev_proxy: dbus.Interface
 | 
						|
        props_proxy: dbus.Interface
 | 
						|
        peers: dict
 | 
						|
        sorted_peers: list
 | 
						|
        widget: Gtk.Widget
 | 
						|
        expanded: bool
 | 
						|
        scan_request: bool
 | 
						|
        selected_peer: object
 | 
						|
        connecting_peer: object
 | 
						|
        disconnecting_peer: object
 | 
						|
        connected: list
 | 
						|
        dbus_call: dbus.lowlevel.PendingCall
 | 
						|
 | 
						|
    @dataclasses.dataclass
 | 
						|
    class Peer:
 | 
						|
        peer_proxy: dbus.Interface
 | 
						|
        wfd_proxy: dbus.Interface
 | 
						|
        wsc_proxy: dbus.Interface
 | 
						|
        widget: Gtk.Widget
 | 
						|
        rtsp: WFDRTSPServer
 | 
						|
 | 
						|
    indent = '\xbb '
 | 
						|
 | 
						|
    def __init__(self):
 | 
						|
        Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL, title='WFD Source')
 | 
						|
        self.set_decorated(True)
 | 
						|
        self.set_resizable(False)
 | 
						|
        self.connect('destroy', self.on_destroy, "WM destroy")
 | 
						|
        self.set_size_request(900, 300)
 | 
						|
        self.device_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
 | 
						|
        leftscroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
 | 
						|
        leftscroll.add(self.device_box)
 | 
						|
        self.infopane = Gtk.FlowBox(orientation=Gtk.Orientation.VERTICAL)
 | 
						|
        self.infopane.set_selection_mode(Gtk.SelectionMode.NONE)
 | 
						|
        self.infopane.set_max_children_per_line(20)
 | 
						|
        self.infopane.set_min_children_per_line(3)
 | 
						|
        self.infopane.set_column_spacing(20)
 | 
						|
        self.infopane.set_row_spacing(5)
 | 
						|
        self.infopane.set_valign(Gtk.Align.START)
 | 
						|
        self.infopane.set_halign(Gtk.Align.START)
 | 
						|
        rightscroll = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
 | 
						|
        rightscroll.add(self.infopane)
 | 
						|
        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
 | 
						|
        paned.pack1(leftscroll, True, True)
 | 
						|
        paned.pack2(rightscroll, False, False)
 | 
						|
        paned.set_wide_handle(True)
 | 
						|
        paned.props.position = 400
 | 
						|
        paned.props.position_set = True
 | 
						|
        self.add(paned)
 | 
						|
        self.show_all()
 | 
						|
        self.connect('notify::is-active', self.on_notify_is_active)
 | 
						|
 | 
						|
        self.rtsp_props = None
 | 
						|
        self.rtsp_init_values = {}
 | 
						|
        self.rtsp_port = 7236
 | 
						|
        self.devices = None
 | 
						|
        self.objects = {}
 | 
						|
        self.populate_devices()
 | 
						|
        self.dbus = dbus.SystemBus()
 | 
						|
        self.dbus.watch_name_owner('net.connman.iwd', self.on_name_owner_change)
 | 
						|
        self.on_name_owner_change('dummy' if self.dbus.name_has_owner('net.connman.iwd') else '')
 | 
						|
 | 
						|
    def on_name_owner_change(self, new_name):
 | 
						|
        if not new_name:
 | 
						|
            if self.devices is None:
 | 
						|
                return True
 | 
						|
 | 
						|
            for dev_path in self.devices:
 | 
						|
                device = self.devices[dev_path]
 | 
						|
                if device.connecting_peer or device.disconnecting_peer:
 | 
						|
                    device.dbus_call.cancel()
 | 
						|
 | 
						|
                for peer_path in device.peers:
 | 
						|
                    peer = device.peers[peer_path]
 | 
						|
                    if peer.rtsp:
 | 
						|
                        peer.rtsp.close()
 | 
						|
 | 
						|
            self.devices = None
 | 
						|
            self.objects = {}
 | 
						|
            self.populate_devices()
 | 
						|
            self.dbus.remove_signal_receiver(self.on_properties_changed)
 | 
						|
            self.dbus.remove_signal_receiver(self.on_interfaces_added)
 | 
						|
            self.dbus.remove_signal_receiver(self.on_interfaces_removed)
 | 
						|
            return True
 | 
						|
 | 
						|
        if self.devices is not None:
 | 
						|
            return True
 | 
						|
 | 
						|
        manager = dbus.Interface(self.dbus.get_object('net.connman.iwd', '/'), 'org.freedesktop.DBus.ObjectManager')
 | 
						|
 | 
						|
        self.dbus.add_signal_receiver(self.on_properties_changed,
 | 
						|
            bus_name="net.connman.iwd",
 | 
						|
            dbus_interface="org.freedesktop.DBus.Properties",
 | 
						|
            signal_name="PropertiesChanged",
 | 
						|
            path_keyword="path")
 | 
						|
        self.dbus.add_signal_receiver(self.on_interfaces_added,
 | 
						|
            bus_name="net.connman.iwd",
 | 
						|
            dbus_interface="org.freedesktop.DBus.ObjectManager",
 | 
						|
            signal_name="InterfacesAdded")
 | 
						|
        self.dbus.add_signal_receiver(self.on_interfaces_removed,
 | 
						|
            bus_name="net.connman.iwd",
 | 
						|
            dbus_interface="org.freedesktop.DBus.ObjectManager",
 | 
						|
            signal_name="InterfacesRemoved")
 | 
						|
 | 
						|
        self.objects = manager.GetManagedObjects()
 | 
						|
        self.devices = {}
 | 
						|
 | 
						|
        for path in self.objects:
 | 
						|
            if DEVICE_IF in self.objects[path]:
 | 
						|
                self.add_dev(path)
 | 
						|
        for path in self.objects:
 | 
						|
            if PEER_IF in self.objects[path]:
 | 
						|
                self.add_peer(path)
 | 
						|
 | 
						|
        self.populate_devices()
 | 
						|
 | 
						|
        svc_mgr = dbus.Interface(self.dbus.get_object('net.connman.iwd', '/net/connman/iwd'), SVC_MGR_IF)
 | 
						|
        svc_mgr.RegisterDisplayService({
 | 
						|
            'Source': True,
 | 
						|
            'Port': dbus.UInt16(self.rtsp_port)
 | 
						|
        })
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def add_dev(self, path):
 | 
						|
        obj_proxy = self.dbus.get_object('net.connman.iwd', path)
 | 
						|
        # Default to expanded for first device found
 | 
						|
        expanded = len(self.devices) == 0
 | 
						|
        self.devices[path] = WFDSource.Device(
 | 
						|
            props=self.objects[path][DEVICE_IF],
 | 
						|
            dev_proxy=dbus.Interface(obj_proxy, DEVICE_IF),
 | 
						|
            props_proxy=dbus.Interface(obj_proxy, 'org.freedesktop.DBus.Properties'),
 | 
						|
            peers={},
 | 
						|
            sorted_peers=[],
 | 
						|
            widget=None,
 | 
						|
            expanded=expanded,
 | 
						|
            scan_request=False,
 | 
						|
            selected_peer=None,
 | 
						|
            connecting_peer=None,
 | 
						|
            disconnecting_peer=None,
 | 
						|
            connected=[],
 | 
						|
            dbus_call=None)
 | 
						|
 | 
						|
    def add_peer(self, path):
 | 
						|
        dev_path = self.objects[path][PEER_IF]['Device']
 | 
						|
        if dev_path not in self.devices or path in self.devices[dev_path].peers:
 | 
						|
            return False
 | 
						|
 | 
						|
        self.devices[dev_path].peers[path] = WFDSource.Peer(
 | 
						|
            peer_proxy=None,
 | 
						|
            wfd_proxy=None,
 | 
						|
            wsc_proxy=None,
 | 
						|
            widget=None,
 | 
						|
            rtsp=None)
 | 
						|
        return True
 | 
						|
 | 
						|
    def on_properties_changed(self, interface, changed, invalidated, path):
 | 
						|
        if path not in self.objects:
 | 
						|
            self.objects[path] = {}
 | 
						|
        if interface not in self.objects[path]:
 | 
						|
            self.objects[path][interface] = {}
 | 
						|
 | 
						|
        self.objects[path][interface].update(changed)
 | 
						|
        for prop in invalidated:
 | 
						|
            if prop in self.objects[path][interface]:
 | 
						|
                del self.objects[path][interface][prop]
 | 
						|
 | 
						|
        if path in self.devices:
 | 
						|
            self.update_dev_props(path)
 | 
						|
            if interface == DEVICE_IF and 'AvailableConnections' in changed:
 | 
						|
                self.update_selected_peer(path)
 | 
						|
 | 
						|
        if PEER_IF in self.objects[path]:
 | 
						|
            dev_path = self.objects[path][PEER_IF]['Device']
 | 
						|
            if dev_path in self.devices:
 | 
						|
                device = self.devices[dev_path]
 | 
						|
                if path in device.peers:
 | 
						|
                    peer = device.peers[path]
 | 
						|
                    if interface == PEER_IF and 'Connected' in changed:
 | 
						|
                        if changed['Connected'] and peer not in device.connected:
 | 
						|
                            device.connected.append(peer)
 | 
						|
                        elif not changed['Connected'] and peer in device.connected:
 | 
						|
                            device.connected.remove(peer)
 | 
						|
                        self.update_dev_props(dev_path)
 | 
						|
                        self.update_peer_props(dev_path, path)
 | 
						|
                        if peer != device.selected_peer:
 | 
						|
                            self.update_selected_peer(dev_path)
 | 
						|
                    if interface == PEER_IF and peer.rtsp:
 | 
						|
                        if 'ConnectedInterface' in changed:
 | 
						|
                            peer.rtsp.set_local_interface(changed['ConnectedInterface'])
 | 
						|
                        if 'ConnectedIp' in changed:
 | 
						|
                            peer.rtsp.set_remote_ip(changed['ConnectedIp'])
 | 
						|
 | 
						|
                self.update_peer_props(dev_path, path)
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    def on_interfaces_added(self, path, interfaces):
 | 
						|
        if path not in self.objects:
 | 
						|
            self.objects[path] = {}
 | 
						|
        self.objects[path].update(interfaces)
 | 
						|
 | 
						|
        if DEVICE_IF in interfaces:
 | 
						|
            self.add_dev(path)
 | 
						|
            # This should happen rarely enough that we can repopulate the whole list
 | 
						|
            self.populate_devices()
 | 
						|
 | 
						|
        update_dev_props = False
 | 
						|
        if PEER_IF in interfaces:
 | 
						|
            update_dev_props = self.add_peer(path)
 | 
						|
 | 
						|
        if PEER_IF in self.objects[path]:
 | 
						|
            dev_path = self.objects[path][PEER_IF]['Device']
 | 
						|
            if dev_path in self.devices:
 | 
						|
                if update_dev_props:
 | 
						|
                    # Update device's peer count
 | 
						|
                    self.update_dev_props(dev_path)
 | 
						|
                self.update_peer_props(dev_path, path)
 | 
						|
 | 
						|
    def on_interfaces_removed(self, path, interfaces):
 | 
						|
        if path not in self.objects:
 | 
						|
            return
 | 
						|
 | 
						|
        dev_path = None
 | 
						|
        if PEER_IF in interfaces or WFD_IF in interfaces or WSC_IF in interfaces:
 | 
						|
            if PEER_IF in self.objects[path]:
 | 
						|
                dev_path = self.objects[path][PEER_IF]['Device']
 | 
						|
 | 
						|
        for i in interfaces:
 | 
						|
            if i in self.objects[path]:
 | 
						|
                del self.objects[path][i]
 | 
						|
        if len(self.objects[path]) == 0:
 | 
						|
            del self.objects[path]
 | 
						|
 | 
						|
        if DEVICE_IF in interfaces and path in self.devices:
 | 
						|
            device = self.devices[path]
 | 
						|
            if device.connecting_peer or device.disconnecting_peer:
 | 
						|
                device.dbus_call.cancel()
 | 
						|
            # TODO: check if connected
 | 
						|
            del self.devices[path]
 | 
						|
            # This should happen rarely enough that we can repopulate the whole list
 | 
						|
            self.populate_devices()
 | 
						|
 | 
						|
        if dev_path is not None and dev_path in self.devices:
 | 
						|
            device = self.devices[dev_path]
 | 
						|
            if path in device.peers:
 | 
						|
                # Make sure the widget is removed
 | 
						|
                self.update_peer_props(dev_path, path)
 | 
						|
                if PEER_IF in interfaces:
 | 
						|
                    del device.peers[path]
 | 
						|
                    # Update device's peer count
 | 
						|
                    self.update_dev_props(dev_path)
 | 
						|
 | 
						|
    def populate_devices(self):
 | 
						|
        self.device_box.foreach(lambda x, y: self.device_box.remove(x), None)
 | 
						|
 | 
						|
        if self.devices is None:
 | 
						|
            label = Gtk.Label(label="Not connected to IWD")
 | 
						|
            self.device_box.pack_start(label, expand=True, fill=True, padding=0)
 | 
						|
            self.device_box.show_all()
 | 
						|
            return
 | 
						|
 | 
						|
        if len(self.devices) == 0:
 | 
						|
            label = Gtk.Label(label="No P2P-capable adapters :-(")
 | 
						|
            self.device_box.pack_start(label, expand=True, fill=True, padding=0)
 | 
						|
            self.device_box.show_all()
 | 
						|
            return
 | 
						|
 | 
						|
        for path in self.devices:
 | 
						|
            label = Gtk.Label()
 | 
						|
            label.set_halign(Gtk.Align.START)
 | 
						|
            label.set_line_wrap(False)
 | 
						|
            label.set_single_line_mode(False)
 | 
						|
            label.set_ellipsize(Pango.EllipsizeMode.END)
 | 
						|
            switch = Gtk.Switch()
 | 
						|
            switch.connect('state-set', self.on_dev_enabled, path)
 | 
						|
            switch.set_halign(Gtk.Align.END)
 | 
						|
            switch.set_valign(Gtk.Align.START)
 | 
						|
            box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
 | 
						|
            box.pack_start(label, expand=True, fill=True, padding=0)
 | 
						|
            box.pack_end(switch, expand=False, fill=False, padding=0)
 | 
						|
            peer_list = Gtk.ListBox() # can also use an IconView.. or make it switchable
 | 
						|
            peer_list.set_size_request(150, 120)
 | 
						|
            peer_list.set_selection_mode(Gtk.SelectionMode.SINGLE)
 | 
						|
            peer_list.set_placeholder(Gtk.Label(label='No Wi-Fi Displays discovered yet...'))
 | 
						|
            peer_list.connect('row-selected', self.on_peer_selected, path)
 | 
						|
            frame = Gtk.Frame()
 | 
						|
            frame.props.margin = 10
 | 
						|
            frame.add(peer_list)
 | 
						|
            expander = Gtk.Expander()
 | 
						|
            expander.set_label_fill(True)
 | 
						|
            expander.set_expanded(self.devices[path].expanded)
 | 
						|
            expander.set_label_widget(box)
 | 
						|
            expander.add(frame)
 | 
						|
            expander.connect('notify::expanded', self.on_dev_expanded, path)
 | 
						|
            expander.show_all()
 | 
						|
            self.device_box.add(expander)
 | 
						|
            self.devices[path].widget = expander
 | 
						|
            self.update_dev_props(path)
 | 
						|
            GLib.idle_add(self.expander_workaround, expander)
 | 
						|
 | 
						|
            for peer_path in self.devices[path].peers:
 | 
						|
                self.update_peer_props(path, peer_path)
 | 
						|
 | 
						|
            # Basically implement Gtk.Expander's set_label_fill which for some reason
 | 
						|
            # doesn't do anything.  Use size-allocate because configure-event doesn't work either...
 | 
						|
            self.margin_left = None
 | 
						|
            def on_exp_resize(widget, event):
 | 
						|
                if self.margin_left is None:
 | 
						|
                    self.margin_left = box.get_allocation().x
 | 
						|
                posx, posy = expander.translate_coordinates(self, 0, 0)
 | 
						|
                # Add posx to force the label widget (box) to be aligned to the left side of the
 | 
						|
                # window even if GTK already decided to push the expander off the left side with a
 | 
						|
                # negative allocation.x.  This way it won't push it any further left as the available
 | 
						|
                # space shrinks.
 | 
						|
                box.set_size_request(max(posx + expander.get_allocated_width() - self.margin_left - 1, 0), -1)
 | 
						|
                return False
 | 
						|
            expander.connect('size-allocate', on_exp_resize)
 | 
						|
 | 
						|
    def expander_workaround(self, widget):
 | 
						|
        box = widget.get_label_widget()
 | 
						|
        widget.set_label_widget(None)
 | 
						|
        widget.set_label_widget(box)
 | 
						|
        return False
 | 
						|
 | 
						|
    def update_dev_props(self, path):
 | 
						|
        device = self.devices[path]
 | 
						|
        if not device.props['Enabled']:
 | 
						|
            state = 'disabled'
 | 
						|
        elif device.disconnecting_peer is not None:
 | 
						|
            state = 'disconnecting...'
 | 
						|
        elif device.connecting_peer is not None:
 | 
						|
            state = 'connecting...'
 | 
						|
        elif len(device.connected) > 0:
 | 
						|
            if all([not peer.rtsp or peer.rtsp.ready for peer in device.connected]):
 | 
						|
                state = 'connected'
 | 
						|
            else:
 | 
						|
                state = 'negotiating...'
 | 
						|
        elif device.scan_request:
 | 
						|
            state = 'discovering... (' + str(len(device.peers)) + ')'
 | 
						|
        else:
 | 
						|
            state = 'idle'
 | 
						|
 | 
						|
        label, switch = device.widget.get_label_widget().get_children()
 | 
						|
        dev_str = self.get_dev_string(path)
 | 
						|
        name = str(device.props['Name'])
 | 
						|
        label.set_markup(dev_str + '\n<small>' + ('Local name: ' + name + '\n' if dev_str != name else '') + 'State: ' + state + '</small>')
 | 
						|
        switch.set_state(device.props['Enabled'])
 | 
						|
 | 
						|
    def update_peer_props(self, dev_path, path):
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        peer = device.peers[path]
 | 
						|
        props = self.objects[path] if path in self.objects else {}
 | 
						|
        peer_list = device.widget.get_child().get_child()
 | 
						|
        if peer.widget is None:
 | 
						|
            if PEER_IF not in props or WFD_IF not in props or WSC_IF not in props:
 | 
						|
                return
 | 
						|
            if not props[WFD_IF]['Sink']:
 | 
						|
                return
 | 
						|
 | 
						|
            name = str(props[PEER_IF]['Name'])
 | 
						|
            device.sorted_peers.append(name)
 | 
						|
            device.sorted_peers.sort()
 | 
						|
            index = device.sorted_peers.index(name)
 | 
						|
 | 
						|
            obj_proxy = self.dbus.get_object('net.connman.iwd', path)
 | 
						|
            peer.peer_proxy=dbus.Interface(obj_proxy, PEER_IF)
 | 
						|
            peer.wfd_proxy=dbus.Interface(obj_proxy, WFD_IF)
 | 
						|
            peer.wsc_proxy=dbus.Interface(obj_proxy, WSC_IF)
 | 
						|
            label = Gtk.Label()
 | 
						|
            label.set_halign(Gtk.Align.START)
 | 
						|
            label.set_single_line_mode(True)
 | 
						|
            label.set_ellipsize(Pango.EllipsizeMode.END)
 | 
						|
            event_box = Gtk.EventBox()
 | 
						|
            event_box.add(label)
 | 
						|
            event_box.connect('button-press-event', self.on_peer_click, (dev_path, path))
 | 
						|
            button = Gtk.Button()
 | 
						|
            button.set_use_stock(True)
 | 
						|
            button.connect('clicked', self.on_peer_button, (dev_path, path))
 | 
						|
            box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
 | 
						|
            box.props.margin = 5;
 | 
						|
            box.pack_start(event_box, expand=True, fill=True, padding=0)
 | 
						|
            box.pack_end(button, expand=False, fill=False, padding=0)
 | 
						|
            peer.widget = Gtk.ListBoxRow()
 | 
						|
            peer.widget.add(box)
 | 
						|
            peer_list.insert(peer.widget, index)
 | 
						|
            peer.widget.show_all()
 | 
						|
        elif (PEER_IF not in props or WFD_IF not in props or WSC_IF not in props or not props[WFD_IF]['Sink']) and peer.widget:
 | 
						|
            tmp = peer.widget
 | 
						|
            peer.widget = None
 | 
						|
            del device.sorted_peers[tmp.get_index()]
 | 
						|
            peer_list.remove(tmp)
 | 
						|
            if peer == device.selected_peer:
 | 
						|
                device.selected_peer = None
 | 
						|
                self.update_info_pane(dev_path, None)
 | 
						|
            if peer == device.connecting_peer:
 | 
						|
                device.dbus_call.cancel()
 | 
						|
                device.connecting_peer = None
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
            if peer == device.disconnecting_peer:
 | 
						|
                device.dbus_call.cancel()
 | 
						|
                device.disconnecting_peer = None
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
            if peer in device.connected:
 | 
						|
                device.connected.remove(peer)
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
            peer.peer_proxy = None
 | 
						|
            peer.wfd_proxy = None
 | 
						|
            peer.wsc_proxy = None
 | 
						|
            if peer.rtsp:
 | 
						|
                peer.rtsp.close()
 | 
						|
                peer.rtsp = None
 | 
						|
            return
 | 
						|
 | 
						|
        subcat = 'unknown type'
 | 
						|
        if 'DeviceSubcategory' in props[PEER_IF]:
 | 
						|
            subcat = props[PEER_IF]['DeviceSubcategory']
 | 
						|
 | 
						|
        weight = 'heavy' if peer in device.connected else 'normal'
 | 
						|
        box = peer.widget.get_child()
 | 
						|
        event_box, button = box.get_children()
 | 
						|
        label, = event_box.get_children()
 | 
						|
        label.set_markup('<span weight="' + weight + '">' + props[PEER_IF]['Name'] + '</span> <span foreground="grey" size="small">' + subcat + '</span>')
 | 
						|
 | 
						|
        if device.disconnecting_peer or (device.connecting_peer and peer != device.connecting_peer):
 | 
						|
            # This peer's row should not have any buttons
 | 
						|
            button.hide()
 | 
						|
        elif peer == device.connecting_peer:
 | 
						|
            button.set_label('Cancel')
 | 
						|
            button.show()
 | 
						|
        elif peer in device.connected:
 | 
						|
            if not peer.rtsp or peer.rtsp.ready:
 | 
						|
                button.set_label('Disconnect')
 | 
						|
            else:
 | 
						|
                button.set_label('Cancel')
 | 
						|
            button.show()
 | 
						|
        elif peer == device.selected_peer and device.props['AvailableConnections'] > 0:
 | 
						|
            button.set_label('Connect')
 | 
						|
            button.show()
 | 
						|
        else:
 | 
						|
            button.hide()
 | 
						|
 | 
						|
        if peer == device.selected_peer:
 | 
						|
            self.update_info_pane(dev_path, path)
 | 
						|
 | 
						|
    def update_selected_peer(self, dev_path):
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        if device.selected_peer:
 | 
						|
            sel_path = self.get_peer_path(device, device.selected_peer)
 | 
						|
            self.update_peer_props(dev_path, sel_path)
 | 
						|
 | 
						|
    def add_info(self, name, desc, valuewidget):
 | 
						|
        namelabel = Gtk.Label(label=name + ':', xalign=0)
 | 
						|
        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
 | 
						|
        box.pack_start(namelabel, expand=False, fill=False, padding=3)
 | 
						|
        if valuewidget:
 | 
						|
            box.pack_end(valuewidget, expand=False, fill=False, padding=3)
 | 
						|
        if desc:
 | 
						|
            box.set_tooltip_text(desc)
 | 
						|
        self.infopane.add(box)
 | 
						|
 | 
						|
    def add_info_str(self, name, value):
 | 
						|
        vlabel = Gtk.Label(xalign=0)
 | 
						|
        vlabel.set_markup('<span weight="bold">' + value + '</span>')
 | 
						|
        self.add_info(name, None, vlabel)
 | 
						|
 | 
						|
    def add_info_prop(self, prop):
 | 
						|
        val = prop.getter()
 | 
						|
        if prop.setter is None:
 | 
						|
            if val is None:
 | 
						|
                return
 | 
						|
            if prop.type == bool:
 | 
						|
                vals = prop.vals if prop.vals is not None else ['no', 'yes']
 | 
						|
                text = vals[val]
 | 
						|
            elif prop.name == 'EDID info':
 | 
						|
                text = WFDSource.edid_to_text(val)
 | 
						|
                if isinstance(text, collections.abc.Sequence):
 | 
						|
                    self.add_info(prop.name, prop.desc, None)
 | 
						|
                    for name, val in text:
 | 
						|
                        if val:
 | 
						|
                            v = Gtk.Label(xalign=0)
 | 
						|
                            v.set_markup('<span weight="bold">' + str(val) + '</span>')
 | 
						|
                        else:
 | 
						|
                            v = None
 | 
						|
                        self.add_info(self.indent + name, prop.desc, v)
 | 
						|
                    return
 | 
						|
            else:
 | 
						|
                text = str(val)
 | 
						|
            v = Gtk.Label(xalign=0)
 | 
						|
            v.set_markup('<span weight="bold">' + text + '</span>')
 | 
						|
        elif val is None:
 | 
						|
            return
 | 
						|
        elif prop.type == bool:
 | 
						|
            v = Gtk.Switch()
 | 
						|
            v.set_active(val)
 | 
						|
            v.connect('state-set', lambda switch, state: prop.setter(state))
 | 
						|
        elif prop.type == int:
 | 
						|
            v = Gtk.SpinButton.new_with_range(min=prop.vals[0], max=prop.vals[1], step=prop.vals[2] if len(prop.vals) > 2 else 1)
 | 
						|
            v.set_value(val)
 | 
						|
            v.connect('value-changed', lambda sb: prop.setter(int(sb.get_value())))
 | 
						|
        elif prop.type == str:
 | 
						|
            if prop.vals:
 | 
						|
                v = Gtk.ComboBoxText()
 | 
						|
                for option in prop.vals:
 | 
						|
                    v.append(option, option)
 | 
						|
                v.set_active_id(val)
 | 
						|
                v.connect('changed', lambda entry: prop.setter(entry.get_active_text()))
 | 
						|
            else:
 | 
						|
                v = Gtk.Entry(text=val)
 | 
						|
                v.connect('changed', lambda entry: prop.setter(entry.get_text()))
 | 
						|
        self.add_info(prop.name, prop.desc, v)
 | 
						|
 | 
						|
    def update_info_pane(self, dev_path, path):
 | 
						|
        self.infopane.foreach(lambda x, y: self.infopane.remove(x), None)
 | 
						|
 | 
						|
        if path is None:
 | 
						|
            return
 | 
						|
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        peer = device.peers[path]
 | 
						|
 | 
						|
        if peer == device.connecting_peer:
 | 
						|
            state = 'IWD connecting'
 | 
						|
        elif peer == device.disconnecting_peer:
 | 
						|
            state = 'disconnecting'
 | 
						|
        elif peer in device.connected:
 | 
						|
            if peer.rtsp is not None:
 | 
						|
                if peer.rtsp.ready:
 | 
						|
                    state = peer.rtsp.state
 | 
						|
                else:
 | 
						|
                    state = 'RTSP negotiation: ' + peer.rtsp.state
 | 
						|
            else:
 | 
						|
                state = 'connected'
 | 
						|
        else:
 | 
						|
            state = 'not connected'
 | 
						|
        self.add_info_str('Connection state', state)
 | 
						|
 | 
						|
        subcat = 'unknown'
 | 
						|
        if 'DeviceSubcategory' in self.objects[path][PEER_IF]:
 | 
						|
            subcat = self.objects[path][PEER_IF]['DeviceSubcategory']
 | 
						|
        self.add_info_str('Peer category', self.objects[path][PEER_IF]['DeviceCategory'])
 | 
						|
        self.add_info_str('Peer subcategory', subcat)
 | 
						|
 | 
						|
        if WFD_IF in self.objects[path]:
 | 
						|
            if self.objects[path][WFD_IF]['Source']:
 | 
						|
                if self.objects[path][WFD_IF]['Sink']:
 | 
						|
                    t = 'dual-role'
 | 
						|
                else:
 | 
						|
                    t = 'source'
 | 
						|
            else:
 | 
						|
                t = 'sink'
 | 
						|
            self.add_info_str('Peer WFD type', t)
 | 
						|
 | 
						|
            if self.objects[path][WFD_IF]['Sink']:
 | 
						|
                self.add_info_str('Peer audio support', 'yes' if self.objects[path][WFD_IF]['HasAudio'] else 'no')
 | 
						|
 | 
						|
            self.add_info_str('Peer UIBC support', 'yes' if self.objects[path][WFD_IF]['HasUIBC'] else 'no')
 | 
						|
 | 
						|
            self.add_info_str('Peer content protection', 'yes' if self.objects[path][WFD_IF]['HasContentProtection'] else 'no')
 | 
						|
 | 
						|
        if peer.rtsp is not None and peer.rtsp.ready:
 | 
						|
            def force_keyframe(widget):
 | 
						|
                peer.rtsp.force_keyframe()
 | 
						|
                return True
 | 
						|
            def reset_stream(widget):
 | 
						|
                peer.rtsp.reset_stream()
 | 
						|
                return True
 | 
						|
            # The idea for these buttons is to make sure any parameter changes get fully applied
 | 
						|
            button1 = Gtk.Button()
 | 
						|
            button1.set_label('Force keyframe')
 | 
						|
            button1.connect('clicked', force_keyframe)
 | 
						|
            button2 = Gtk.Button()
 | 
						|
            button2.set_label('Reset stream')
 | 
						|
            button2.connect('clicked', reset_stream)
 | 
						|
            box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
 | 
						|
            box.pack_start(button1, expand=False, fill=False, padding=3)
 | 
						|
            box.pack_start(button2, expand=False, fill=False, padding=3)
 | 
						|
            self.infopane.add(box)
 | 
						|
 | 
						|
        if self.rtsp_props is None:
 | 
						|
            self.rtsp_props, self.rtsp_init_values = WFDRTSPServer.get_init_props()
 | 
						|
 | 
						|
        if peer.rtsp is not None:
 | 
						|
            props = peer.rtsp.props
 | 
						|
        else:
 | 
						|
            props = self.rtsp_props
 | 
						|
 | 
						|
        for prop in props:
 | 
						|
            self.add_info_prop(prop)
 | 
						|
 | 
						|
        self.infopane.show_all()
 | 
						|
 | 
						|
    # Direct method calls on dbus.Interface's don't return dbus.lowlevel.PendingCall objects so
 | 
						|
    # we have to use bus.call_async to make cancellable async calls
 | 
						|
    def async_call(self, proxy, method, signature='', *args, **kwargs):
 | 
						|
        return self.dbus.call_async(proxy.bus_name, proxy.object_path, proxy.dbus_interface, method, signature, args, **kwargs)
 | 
						|
 | 
						|
    def connect_peer(self, dev_path, path):
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        peer = device.peers[path]
 | 
						|
 | 
						|
        def on_reply():
 | 
						|
            device.connected.append(peer)
 | 
						|
            device.connecting_peer = None
 | 
						|
            # Local interface and remote IP get set in the PropertiesChanged handler
 | 
						|
            self.update_dev_props(dev_path)
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            if peer != device.selected_peer:
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
        def on_error(excp):
 | 
						|
            device.connecting_peer = None
 | 
						|
            if peer.rtsp:
 | 
						|
                peer.rtsp.close()
 | 
						|
                peer.rtsp = None
 | 
						|
            self.update_dev_props(dev_path)
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            if peer != device.selected_peer:
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
            dialog = Gtk.MessageDialog(parent=self, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text='Connection failed')
 | 
						|
            dialog.format_secondary_text('Connection to ' + self.objects[path][PEER_IF]['Name'] + ' failed: ' + repr(excp))
 | 
						|
            dialog.show()
 | 
						|
 | 
						|
            def on_ok(response, *args):
 | 
						|
                dialog.destroy()
 | 
						|
 | 
						|
            dialog.connect('response', on_ok)
 | 
						|
 | 
						|
        def on_rtsp_state():
 | 
						|
            self.update_dev_props(dev_path)
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            if peer != device.selected_peer:
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
        def on_rtsp_error(excp):
 | 
						|
            self.disconnect_peer(dev_path, path)
 | 
						|
            tb = ''
 | 
						|
            try:
 | 
						|
                tb = '\n\nDebug info: ' + traceback.format_exc()
 | 
						|
            except:
 | 
						|
                pass
 | 
						|
            dialog = Gtk.MessageDialog(parent=self, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text='Negotiation failed')
 | 
						|
            dialog.format_secondary_text('RTSP error when talking to ' + self.objects[path][PEER_IF]['Name'] + ': ' + repr(excp) + tb)
 | 
						|
            dialog.show()
 | 
						|
 | 
						|
            def on_ok(response, *args):
 | 
						|
                dialog.destroy()
 | 
						|
 | 
						|
            dialog.connect('response', on_ok)
 | 
						|
 | 
						|
        def on_rtsp_props_changed():
 | 
						|
            # Should also check if the infopane is currently showing a selected peer on another device...
 | 
						|
            if peer == device.selected_peer:
 | 
						|
                self.update_info_pane(dev_path, path)
 | 
						|
 | 
						|
        # Cannot use peer.wsc_proxy.PushButton()
 | 
						|
        device.dbus_call = self.async_call(peer.wsc_proxy, 'PushButton', reply_handler=on_reply, error_handler=on_error, timeout=120)
 | 
						|
        device.connecting_peer = peer
 | 
						|
        # Create the RTSP server now so it's ready as soon as the P2P connection succeeds even if
 | 
						|
        # we haven't received the DBus reply yet
 | 
						|
        peer.rtsp = WFDRTSPServer(self.rtsp_port, on_rtsp_state, on_rtsp_error, self.rtsp_init_values, on_rtsp_props_changed)
 | 
						|
        self.update_dev_props(dev_path)
 | 
						|
        self.update_peer_props(dev_path, path)
 | 
						|
        if peer != device.selected_peer:
 | 
						|
            self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
    def disconnect_peer(self, dev_path, path):
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        peer = device.peers[path]
 | 
						|
 | 
						|
        def on_reply():
 | 
						|
            device.disconnecting_peer = None
 | 
						|
            self.update_dev_props(dev_path)
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            if peer != device.selected_peer:
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
        def on_error(excp):
 | 
						|
            device.disconnecting_peer = None
 | 
						|
            self.update_dev_props(dev_path)
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            if peer != device.selected_peer:
 | 
						|
                self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
            if isinstance(excp, dbus.exceptions.DBusException) and excp.get_dbus_name() == 'net.connman.iwd.NotConnected':
 | 
						|
                return
 | 
						|
 | 
						|
            dialog = Gtk.MessageDialog(parent=self, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text='Disconnecting failed')
 | 
						|
            dialog.format_secondary_text('Disconnecting from ' + self.objects[path][PEER_IF]['Name'] + ' failed: ' + repr(excp))
 | 
						|
            dialog.show()
 | 
						|
 | 
						|
            def on_ok(response, *args):
 | 
						|
                dialog.destroy()
 | 
						|
 | 
						|
            dialog.connect('response', on_ok)
 | 
						|
 | 
						|
        if peer == device.connecting_peer:
 | 
						|
            device.dbus_call.cancel()
 | 
						|
            device.connecting_peer = None
 | 
						|
 | 
						|
        if peer in device.connected:
 | 
						|
            device.connected.remove(peer)
 | 
						|
 | 
						|
        if peer.rtsp:
 | 
						|
            peer.rtsp.close()
 | 
						|
            peer.rtsp = None
 | 
						|
 | 
						|
        device.dbus_call = self.async_call(peer.peer_proxy, 'Disconnect', reply_handler=on_reply, error_handler=on_error)
 | 
						|
        device.disconnecting_peer = peer
 | 
						|
        self.update_dev_props(dev_path)
 | 
						|
        self.update_peer_props(dev_path, path)
 | 
						|
        if peer != device.selected_peer:
 | 
						|
            self.update_selected_peer(dev_path)
 | 
						|
 | 
						|
    def on_peer_click(self, widget, event, data):
 | 
						|
        if event.button != 1 or event.type != Gdk.EventType._2BUTTON_PRESS:
 | 
						|
            return False
 | 
						|
        dev_path, path = data
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        if device.disconnecting_peer:
 | 
						|
            return True
 | 
						|
        if device.connecting_peer or not device.props['AvailableConnections']:
 | 
						|
            # Should we auto-disconnect from the connected peer? Show an "Are you sure?" dialog?
 | 
						|
            return True
 | 
						|
        self.connect_peer(dev_path, path)
 | 
						|
        return True
 | 
						|
 | 
						|
    def on_peer_button(self, widget, data):
 | 
						|
        dev_path, path = data
 | 
						|
        action = widget.get_label()
 | 
						|
        device = self.devices[dev_path]
 | 
						|
        if device.disconnecting_peer:
 | 
						|
            return True
 | 
						|
        if action == 'Connect':
 | 
						|
            self.connect_peer(dev_path, path)
 | 
						|
        elif action in ['Disconnect', 'Cancel']:
 | 
						|
            self.disconnect_peer(dev_path, path)
 | 
						|
        return True
 | 
						|
 | 
						|
    def get_peer_path(self, device, peer):
 | 
						|
        for path in device.peers:
 | 
						|
            if device.peers[path] == device.selected_peer:
 | 
						|
                return path
 | 
						|
        return None
 | 
						|
 | 
						|
    def on_peer_selected(self, widget, row, dev_path):
 | 
						|
        device = self.devices[dev_path]
 | 
						|
 | 
						|
        if device.selected_peer is not None:
 | 
						|
            if device.selected_peer.widget == row:
 | 
						|
                return True
 | 
						|
 | 
						|
            path = self.get_peer_path(device, device.selected_peer)
 | 
						|
            device.selected_peer = None
 | 
						|
            self.update_peer_props(dev_path, path)
 | 
						|
            self.update_info_pane(dev_path, None)
 | 
						|
 | 
						|
        if row is None:
 | 
						|
            return True
 | 
						|
 | 
						|
        for path in device.peers:
 | 
						|
            if device.peers[path].widget == row:
 | 
						|
                device.selected_peer = device.peers[path]
 | 
						|
                self.update_peer_props(dev_path, path)
 | 
						|
                return True
 | 
						|
 | 
						|
    def update_dev_scan_request(self, path):
 | 
						|
        device = self.devices[path]
 | 
						|
        should_request = device.expanded and self.is_active()
 | 
						|
        if device.scan_request == should_request:
 | 
						|
            return
 | 
						|
 | 
						|
        device.scan_request = should_request
 | 
						|
        if device.scan_request:
 | 
						|
            device.dev_proxy.RequestDiscovery()
 | 
						|
        else:
 | 
						|
            device.dev_proxy.ReleaseDiscovery()
 | 
						|
        self.update_dev_props(path)
 | 
						|
 | 
						|
    def on_notify_is_active(self, window, value):
 | 
						|
        if self.devices is None:
 | 
						|
            return True
 | 
						|
 | 
						|
        for path in self.devices:
 | 
						|
            self.update_dev_scan_request(path)
 | 
						|
        return True
 | 
						|
 | 
						|
    def on_dev_enabled(self, switch, state, path):
 | 
						|
        device = self.devices[path]
 | 
						|
        if device.props['Enabled'] == state:
 | 
						|
            return
 | 
						|
        device.props_proxy.Set(DEVICE_IF, 'Enabled', state)
 | 
						|
        return True
 | 
						|
 | 
						|
    def on_dev_expanded(self, expander, value, path):
 | 
						|
        device = self.devices[path]
 | 
						|
        device.expanded = expander.get_expanded()
 | 
						|
        self.update_dev_scan_request(path)
 | 
						|
        return True
 | 
						|
 | 
						|
    def get_dev_string(self, path):
 | 
						|
        wiphy = self.objects[path][WIPHY_IF]
 | 
						|
        if 'Model' in wiphy:
 | 
						|
            return wiphy['Model']
 | 
						|
        if 'Vendor' in wiphy:
 | 
						|
            return wiphy['Vendor']
 | 
						|
        return wiphy['Name']
 | 
						|
 | 
						|
    def on_destroy(self, widget, data):
 | 
						|
        global mainloop
 | 
						|
        if self.devices is not None:
 | 
						|
            svc_mgr = dbus.Interface(self.dbus.get_object('net.connman.iwd', '/net/connman/iwd'), SVC_MGR_IF)
 | 
						|
            svc_mgr.UnregisterDisplayService()
 | 
						|
            self.on_name_owner_change('')
 | 
						|
        mainloop.quit()
 | 
						|
        return False
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def edid_to_text(edid):
 | 
						|
        if edid is None:
 | 
						|
            return 'unavailable'
 | 
						|
        if len(edid) < 128:
 | 
						|
            return 'invalid (too short)'
 | 
						|
        if edid[0:8] != b'\0\xff\xff\xff\xff\xff\xff\0':
 | 
						|
            return 'invalid (bad magic)'
 | 
						|
        if sum(edid[0:128]) & 255 != 0:
 | 
						|
            return 'invalid (bad checksum)'
 | 
						|
 | 
						|
        header = edid[0:20]
 | 
						|
        manf_id = (header[8] << 8) + header[9]
 | 
						|
        text = [('Header', '')]
 | 
						|
        text.append((WFDSource.indent + 'Version', str(header[18]) + '.' + str(header[19])))
 | 
						|
        text.append((WFDSource.indent + 'Manufacturer ID', chr(64 + ((manf_id >> 10) & 31)) + chr(64 + ((manf_id >> 5) & 31)) + chr(64 + ((manf_id >> 0) & 31))))
 | 
						|
        text.append((WFDSource.indent + 'Product code', hex((header[11] << 8) + header[10])))
 | 
						|
        text.append((WFDSource.indent + 'Serial', hex((header[15] << 24) +(header[14] << 16) +  (header[13] << 8) + header[12])))
 | 
						|
        text.append((WFDSource.indent + 'Manufactured', str(1990 + header[17]) + ' week ' + str(header[16])))
 | 
						|
 | 
						|
        basic_params = edid[20:25]
 | 
						|
        text.append(('Basic parameters', ''))
 | 
						|
        if basic_params[0] & 0x80:
 | 
						|
            intf_table = {
 | 
						|
                2: 'HDMIa',
 | 
						|
                3: 'HDMIb',
 | 
						|
                4: 'MDDI',
 | 
						|
                5: 'DisplayPort'
 | 
						|
            }
 | 
						|
            dt_table = {
 | 
						|
                0: 'RGB 4:4:4',
 | 
						|
                1: 'RGB 4:4:4 + YCrCb 4:4:4',
 | 
						|
                2: 'RGB 4:4:4 + YCrCb 4:2:2',
 | 
						|
                3: 'RGB 4:4:4 + YCrCb 4:4:4 + YCrCb 4:2:2'
 | 
						|
            }
 | 
						|
            bpp = (basic_params[0] >> 4) & 7
 | 
						|
            intf = (basic_params[0] >> 0) & 7
 | 
						|
 | 
						|
            text.append((WFDSource.indent + 'Video input type', 'digital'))
 | 
						|
            text.append((WFDSource.indent + 'Bit depth', 'undefined' if bpp in [0, 7] else str(4 + bpp * 2)))
 | 
						|
            text.append((WFDSource.indent + 'Interface', 'undefined' if intf not in intf_table else intf_table[intf]))
 | 
						|
        else:
 | 
						|
            level_table = {
 | 
						|
                0: '+0.7 / -0.3 V',
 | 
						|
                1: '+0.714 / -0.286 V',
 | 
						|
                2: '+1.0 / -0.4 V',
 | 
						|
                3: '+0.7 / 0 V'
 | 
						|
            }
 | 
						|
            dt_table = {
 | 
						|
                0: 'monochrome/grayscale',
 | 
						|
                1: 'RGB color',
 | 
						|
                2: 'non-RGB color',
 | 
						|
                3: 'undefined'
 | 
						|
            }
 | 
						|
            text.append((WFDSource.indent + 'Video input type', 'analog'))
 | 
						|
            text.append((WFDSource.indent + 'Video white/sync level', level_table[(basic_parmas[0] >> 5) & 3]))
 | 
						|
 | 
						|
        if basic_params[1] and basic_params[2]:
 | 
						|
            text.append((WFDSource.indent + 'Screen width', str(basic_params[1]) + ' cm'))
 | 
						|
            text.append((WFDSource.indent + 'Screen height', str(basic_params[2]) + ' cm'))
 | 
						|
        elif basic_params[2] == 0:
 | 
						|
            text.append((WFDSource.indent + 'Landscape aspect ratio', str((basic_params[1] + 99) * 0.01)))
 | 
						|
        else:
 | 
						|
            text.append((WFDSource.indent + 'Portrait aspect ratio', str(100.0 / (basic_params[2] + 99))))
 | 
						|
 | 
						|
        text.append((WFDSource.indent + 'Gamma', str((basic_params[3] + 100) * 0.01)))
 | 
						|
        text.append((WFDSource.indent + 'DPMS Standby', 'supported' if (basic_params[4] >> 7) & 1 else 'unsupported'))
 | 
						|
        text.append((WFDSource.indent + 'DPMS Suspend', 'supported' if (basic_params[4] >> 6) & 1 else 'unsupported'))
 | 
						|
        text.append((WFDSource.indent + 'DPMS Active-off', 'supported' if (basic_params[4] >> 5) & 1 else 'unsupported'))
 | 
						|
        text.append((WFDSource.indent + 'Color type', dt_table[(basic_params[4] >> 3) & 3]))
 | 
						|
        text.append((WFDSource.indent + 'sRGB color space', 'yes' if (basic_params[4] >> 2) & 1 else 'no'))
 | 
						|
        text.append((WFDSource.indent + 'Continuous timings', 'yes' if (basic_params[4] >> 0) & 1 else 'no'))
 | 
						|
 | 
						|
        # TODO: timing information and extensions
 | 
						|
        return text
 | 
						|
 | 
						|
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 | 
						|
Gst.init(None)
 | 
						|
WFDSource()
 | 
						|
mainloop = GLib.MainLoop()
 | 
						|
mainloop.run()
 |