diff --git a/test/wfd-source b/test/wfd-source
index 09b47a27..bc5ce834 100755
--- a/test/wfd-source
+++ b/test/wfd-source
@@ -16,6 +16,7 @@ import collections.abc
import random
import dataclasses
import traceback
+import codecs
import gi
gi.require_version('GLib', '2.0')
@@ -27,7 +28,9 @@ class WFDRTSPServer:
class RTSPException(Exception):
pass
- def __init__(self, port, state_handler, error_handler):
+ 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
@@ -44,7 +47,8 @@ class WFDRTSPServer:
self.state_handler = state_handler
self.error_handler = error_handler
- self.sm_init()
+ self.prop_handler = prop_handler
+ self.sm_init(init_values)
def handle_data_out(self, conn, *args):
try:
@@ -200,7 +204,11 @@ class WFDRTSPServer:
def ready(self):
return self._state in ['streaming', 'paused']
- def sm_init(self):
+ @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'
@@ -226,6 +234,33 @@ class WFDRTSPServer:
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
@@ -431,6 +466,94 @@ class WFDRTSPServer:
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:
@@ -438,6 +561,8 @@ class WFDRTSPServer:
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))
@@ -511,7 +636,8 @@ class WFDRTSPServer:
# Send M2 response
self.response(public=self.local_methods)
# Send M3
- self.request('GET_PARAMETER', 'rtsp://localhost/wfd1.0', params=['wfd_audio_codecs', 'wfd_video_formats', 'wfd_client_rtp_ports', 'wfd_display_edid', 'wfd_uibc_capability'])
+ 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
@@ -520,6 +646,8 @@ class WFDRTSPServer:
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'],
@@ -547,7 +675,7 @@ class WFDRTSPServer:
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:
+ 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 '')
@@ -555,22 +683,26 @@ class WFDRTSPServer:
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=800,height=600' +
+ ' ! 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 host=' + self.remote_ip + ' port=' + str(self.remote_rtp_port) + ' bind-port=' + str(self.local_rtp_port)) # TODO: bind-address
-
+ ' ! 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('sync-message', self.on_gst_message)
+ 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)
@@ -644,6 +776,8 @@ class WFDSource(Gtk.Window):
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)
@@ -653,12 +787,16 @@ class WFDSource(Gtk.Window):
self.device_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
leftscroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER)
leftscroll.add(self.device_box)
- self.infolabel1 = Gtk.Label()
- self.infolabel1.set_ellipsize(Pango.EllipsizeMode.START)
- infopane = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
- infopane.pack_start(self.infolabel1, False, False, padding=10)
- rightscroll = Gtk.ScrolledWindow(hscrollbar_policy=Gtk.PolicyType.NEVER, vscrollbar_policy=Gtk.PolicyType.NEVER)
- rightscroll.add(infopane)
+ 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)
@@ -669,6 +807,8 @@ class WFDSource(Gtk.Window):
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 = {}
@@ -1001,11 +1141,13 @@ class WFDSource(Gtk.Window):
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:
- del device.sorted_peers[peer.widget.get_index()]
- peer_list.remove(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(dev_path, None)
+ self.update_info_pane(dev_path, None)
if peer == device.connecting_peer:
device.dbus_call.cancel()
device.connecting_peer = None
@@ -1020,7 +1162,6 @@ class WFDSource(Gtk.Window):
peer.peer_proxy = None
peer.wfd_proxy = None
peer.wsc_proxy = None
- peer.widget = None
if peer.rtsp:
peer.rtsp.close()
peer.rtsp = None
@@ -1055,7 +1196,7 @@ class WFDSource(Gtk.Window):
button.hide()
if peer == device.selected_peer:
- self.update_info(dev_path, path)
+ self.update_info_pane(dev_path, path)
def update_selected_peer(self, dev_path):
device = self.devices[dev_path]
@@ -1063,12 +1204,74 @@ class WFDSource(Gtk.Window):
sel_path = self.get_peer_path(device, device.selected_peer)
self.update_peer_props(dev_path, sel_path)
- def update_info(self, dev_path, path):
- device = self.devices[dev_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('' + value + '')
+ 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('' + str(val) + '')
+ 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('' + text + '')
+ 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:
- self.infolabel1.set_text('')
return
+ device = self.devices[dev_path]
peer = device.peers[path]
if peer == device.connecting_peer:
@@ -1085,14 +1288,13 @@ class WFDSource(Gtk.Window):
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']
-
- text = ('Connection state: ' + state + '\n' +
- 'Device category: ' + self.objects[path][PEER_IF]['DeviceCategory'] + '\n'
- 'Device subcategory: ' + subcat + '\n')
+ 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']:
@@ -1102,17 +1304,27 @@ class WFDSource(Gtk.Window):
t = 'source'
else:
t = 'sink'
- text += 'WFD device type: ' + t + '\n'
+ self.add_info_str('Peer WFD type', t)
if self.objects[path][WFD_IF]['Sink']:
- text += 'Audio: ' + ('yes' if self.objects[path][WFD_IF]['HasAudio'] else 'no') + '\n'
+ self.add_info_str('Peer audio support', 'yes' if self.objects[path][WFD_IF]['HasAudio'] else 'no')
- text += 'UIBC: ' + ('yes' if self.objects[path][WFD_IF]['HasUIBC'] else 'no') + '\n'
+ self.add_info_str('Peer UIBC support', 'yes' if self.objects[path][WFD_IF]['HasUIBC'] else 'no')
- text += 'Content protection: ' + ('yes' if self.objects[path][WFD_IF]['HasContentProtection'] else 'no') + '\n'
+ self.add_info_str('Peer content protection', 'yes' if self.objects[path][WFD_IF]['HasContentProtection'] else 'no')
- self.infolabel1.set_text(text)
- # TODO: more info in labels 2 and so on
+ 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
@@ -1172,12 +1384,17 @@ class WFDSource(Gtk.Window):
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)
+ 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:
@@ -1272,7 +1489,7 @@ class WFDSource(Gtk.Window):
path = self.get_peer_path(device, device.selected_peer)
device.selected_peer = None
self.update_peer_props(dev_path, path)
- self.update_info(dev_path, None)
+ self.update_info_pane(dev_path, None)
if row is None:
return True
@@ -1335,6 +1552,82 @@ class WFDSource(Gtk.Window):
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()