pyatv icon indicating copy to clipboard operation
pyatv copied to clipboard

Investigating Companion api Keyboard implementation

Open SightAndSound-Roger opened this issue 2 years ago • 18 comments

What to investigate?

Find out how to get keyboard updates and send keyboard text through the companion protocol.

So I've started debugging some stuff in the console with my iPhone connected, current progress is that I am able to subscribe to the right events and get actual changes by adding the following in the connect method:

await self.subscribe_event("_tiC") # fired when there's text input changes through on screen keyboard appletv
await self.subscribe_event("_tiStarted") # fired when keyboard is available
await self.subscribe_event("_tiStopped") # fired when keyboard is no longer available
await self._send_command("_tiStart", {}) # starts text input session

Now I'm trying to figure out the command to send actual text input. I've found that keyboard presses only send back a _tiC event in the console and I can't seem to figure out yet what the command should be to actually send text.

Some current events:

_tiStarted

{'_i': '_tiStarted', '_x': 568430212, '_c': {'_tiV': 1, '_tiD': b'bplist00\xd4\x01\x02\x03\x04\x05\x06\x07\x0eX$versionY$archiverT$topX$objects\x12\x00\x01\x86\xa0_\x10\x0fNSKeyedArchiver\xd3\x08\t\n\x0b\x0c\r^documentTraits[sessionUUID]documentState\x80\x06\x80\x0c\x80\x01\xad\x0f\x10\x15\x19\x1a!%/0159=U$null\xd2\x11\x12\x13\x14UdocStV$class\x80\x02\x80\x05\xd2\x16\x12\x17\x18_\x10\x12contextBeforeInput\x80\x03\x80\x04ZTesteraaaa\xd2\x1b\x1c\x1d\x1eZ$classnameX$classes_\x10\x0fTIDocumentState\xa2\x1f _\x10\x0fTIDocumentStateXNSObject\xd2\x1b\x1c"#_\x10\x10RTIDocumentState\xa2$ _\x10\x10RTIDocumentState\xd5&\'()\x12*++-.SbIdSappVlocAppXtiTraits\x80\x07\x80\x08\x80\x08\x80\t\x80\x0b_\x10\x12com.apple.TVSearchVSearch\xd22\x1234Uflags\x12T\x10\x1a\x86\x80\n\xd2\x1b\x1c67_\x10\x11TITextInputTraits\xa28 _\x10\x11TITextInputTraits\xd2\x1b\x1c:;_\x10\x11RTIDocumentTraits\xa2< _\x10\x11RTIDocumentTraitsO\x10\x106\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d\x00\x08\x00\x11\x00\x1a\x00$\x00)\x002\x007\x00I\x00P\x00_\x00k\x00y\x00{\x00}\x00\x7f\x00\x8d\x00\x93\x00\x98\x00\x9e\x00\xa5\x00\xa7\x00\xa9\x00\xae\x00\xc3\x00\xc5\x00\xc7\x00\xd2\x00\xd7\x00\xe2\x00\xeb\x00\xfd\x01\x00\x01\x12\x01\x1b\x01 \x013\x016\x01I\x01T\x01X\x01\\\x01c\x01l\x01n\x01p\x01r\x01t\x01v\x01\x8b\x01\x92\x01\x97\x01\x9d\x01\xa2\x01\xa4\x01\xa9\x01\xbd\x01\xc0\x01\xd4\x01\xd9\x01\xed\x01\xf0\x02\x04\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x17'}, '_t': 1}
2022-04-29 11:46:38 DEBUG [pyatv.protocols.companion.api]: Got event _tiStarted from device: {'_tiV': 1, '_tiD': b'bplist00\xd4\x01\x02\x03\x04\x05\x06\x07\x0eX$versionY$archiverT$topX$objects\x12\x00\x01\x86\xa0_\x10\x0fNSKeyedArchiver\xd3\x08\t\n\x0b\x0c\r^documentTraits[sessionUUID]documentState\x80\x06\x80\x0c\x80\x01\xad\x0f\x10\x15\x19\x1a!%/0159=U$null\xd2\x11\x12\x13\x14UdocStV$class\x80\x02\x80\x05\xd2\x16\x12\x17\x18_\x10\x12contextBeforeInput\x80\x03\x80\x04ZTesteraaaa\xd2\x1b\x1c\x1d\x1eZ$classnameX$classes_\x10\x0fTIDocumentState\xa2\x1f _\x10\x0fTIDocumentStateXNSObject\xd2\x1b\x1c"#_\x10\x10RTIDocumentState\xa2$ _\x10\x10RTIDocumentState\xd5&\'()\x12*++-.SbIdSappVlocAppXtiTraits\x80\x07\x80\x08\x80\x08\x80\t\x80\x0b_\x10\x12com.apple.TVSearchVSearch\xd22\x1234Uflags\x12T\x10\x1a\x86\x80\n\xd2\x1b\x1c67_\x10\x11TITextInputTraits\xa28 _\x10\x11TITextInputTraits\xd2\x1b\x1c:;_\x10\x11RTIDocumentTraits\xa2< _\x10\x11RTIDocumentTraitsO\x10\x106\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d\x00\x08\x00\x11\x00\x1a\x00$\x00)\x002\x007\x00I\x00P\x00_\x00k\x00y\x00{\x00}\x00\x7f\x00\x8d\x00\x93\x00\x98\x00\x9e\x00\xa5\x00\xa7\x00\xa9\x00\xae\x00\xc3\x00\xc5\x00\xc7\x00\xd2\x00\xd7\x00\xe2\x00\xeb\x00\xfd\x01\x00\x01\x12\x01\x1b\x01 \x013\x016\x01I\x01T\x01X\x01\\\x01c\x01l\x01n\x01p\x01r\x01t\x01v\x01\x8b\x01\x92\x01\x97\x01\x9d\x01\xa2\x01\xa4\x01\xa9\x01\xbd\x01\xc0\x01\xd4\x01\xd9\x01\xed\x01\xf0\x02\x04\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x17'}0

_tiStopped

{'_i': '_tiStopped', '_x': 568430233, '_c': {}, '_t': 1}

_tiC

{'_i': '_tiC', '_x': 568430219, '_c': {'_tiV': 1, '_tiD': b'bplist00\xd4\x01\x02\x03\x04\x05\x06\x07\x0eX$versionY$archiverT$topX$objects\x12\x00\x01\x86\xa0_\x10\x0fNSKeyedArchiver\xd3\x08\t\n\x0b\x0c\r^documentTraits[sessionUUID]documentState\x80\x06\x80\x0c\x80\x01\xad\x0f\x10\x15\x19\x1a!%/0159=U$null\xd2\x11\x12\x13\x14UdocStV$class\x80\x02\x80\x05\xd2\x16\x12\x17\x18_\x10\x12contextBeforeInput\x80\x03\x80\x04Qa\xd2\x1b\x1c\x1d\x1eZ$classnameX$classes_\x10\x0fTIDocumentState\xa2\x1f _\x10\x0fTIDocumentStateXNSObject\xd2\x1b\x1c"#_\x10\x10RTIDocumentState\xa2$ _\x10\x10RTIDocumentState\xd5&\'()\x12*++-.SbIdSappVlocAppXtiTraits\x80\x07\x80\x08\x80\x08\x80\t\x80\x0b_\x10\x12com.apple.TVSearchVSearch\xd22\x1234Uflags\x12T\x10\x1a\x86\x80\n\xd2\x1b\x1c67_\x10\x11TITextInputTraits\xa28 _\x10\x11TITextInputTraits\xd2\x1b\x1c:;_\x10\x11RTIDocumentTraits\xa2< _\x10\x11RTIDocumentTraitsO\x10\x106\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d\x00\x08\x00\x11\x00\x1a\x00$\x00)\x002\x007\x00I\x00P\x00_\x00k\x00y\x00{\x00}\x00\x7f\x00\x8d\x00\x93\x00\x98\x00\x9e\x00\xa5\x00\xa7\x00\xa9\x00\xae\x00\xc3\x00\xc5\x00\xc7\x00\xc9\x00\xce\x00\xd9\x00\xe2\x00\xf4\x00\xf7\x01\t\x01\x12\x01\x17\x01*\x01-\x01@\x01K\x01O\x01S\x01Z\x01c\x01e\x01g\x01i\x01k\x01m\x01\x82\x01\x89\x01\x8e\x01\x94\x01\x99\x01\x9b\x01\xa0\x01\xb4\x01\xb7\x01\xcb\x01\xd0\x01\xe4\x01\xe7\x01\xfb\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x0e'}, '_t': 1}
2022-04-29 11:47:10 DEBUG [pyatv.protocols.companion.api]: Got event _tiC from device: {'_tiV': 1, '_tiD': b'bplist00\xd4\x01\x02\x03\x04\x05\x06\x07\x0eX$versionY$archiverT$topX$objects\x12\x00\x01\x86\xa0_\x10\x0fNSKeyedArchiver\xd3\x08\t\n\x0b\x0c\r^documentTraits[sessionUUID]documentState\x80\x06\x80\x0c\x80\x01\xad\x0f\x10\x15\x19\x1a!%/0159=U$null\xd2\x11\x12\x13\x14UdocStV$class\x80\x02\x80\x05\xd2\x16\x12\x17\x18_\x10\x12contextBeforeInput\x80\x03\x80\x04Qa\xd2\x1b\x1c\x1d\x1eZ$classnameX$classes_\x10\x0fTIDocumentState\xa2\x1f _\x10\x0fTIDocumentStateXNSObject\xd2\x1b\x1c"#_\x10\x10RTIDocumentState\xa2$ _\x10\x10RTIDocumentState\xd5&\'()\x12*++-.SbIdSappVlocAppXtiTraits\x80\x07\x80\x08\x80\x08\x80\t\x80\x0b_\x10\x12com.apple.TVSearchVSearch\xd22\x1234Uflags\x12T\x10\x1a\x86\x80\n\xd2\x1b\x1c67_\x10\x11TITextInputTraits\xa28 _\x10\x11TITextInputTraits\xd2\x1b\x1c:;_\x10\x11RTIDocumentTraits\xa2< _\x10\x11RTIDocumentTraitsO\x10\x106\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d\x00\x08\x00\x11\x00\x1a\x00$\x00)\x002\x007\x00I\x00P\x00_\x00k\x00y\x00{\x00}\x00\x7f\x00\x8d\x00\x93\x00\x98\x00\x9e\x00\xa5\x00\xa7\x00\xa9\x00\xae\x00\xc3\x00\xc5\x00\xc7\x00\xc9\x00\xce\x00\xd9\x00\xe2\x00\xf4\x00\xf7\x01\t\x01\x12\x01\x17\x01*\x01-\x01@\x01K\x01O\x01S\x01Z\x01c\x01e\x01g\x01i\x01k\x01m\x01\x82\x01\x89\x01\x8e\x01\x94\x01\x99\x01\x9b\x01\xa0\x01\xb4\x01\xb7\x01\xcb\x01\xd0\x01\xe4\x01\xe7\x01\xfb\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x0e'}

Expected outcome

Have a working keyboard event and a keyboard input method on the companion api.

SightAndSound-Roger avatar Apr 29 '22 09:04 SightAndSound-Roger

So I've been looking into more logs and it looks like the input commands are passed through XPC? I've looked around and saw it pop up 2 or 3 times around issues here but nothing really specific about implementations. Hoping to find more digging into this. XPC: Client requests sending data payload <private> to identifier <private>

SightAndSound-Roger avatar Apr 29 '22 11:04 SightAndSound-Roger

So I've been able to decode the _tiC event using the nska_deserialize library. I didn't seem to get it working with other libraries.

loaded = nska_deserialize.deserialize_plist_from_string(msg['_tiD'])
        input_string = ''
        if len(loaded) >= 3:
            ds = loaded[2]['documentState']
            if ds.__contains__('docSt'):
                ds = ds['docSt']
                if ds.__contains__('contextBeforeInput'):
                    input_string = ds['contextBeforeInput']

I have also been trying to inser text using some HID conversion which seemed to work partially, I can't seem to send the "a" key though, which is a serious problem. So back to the drawing board, trying to find out how the remote on the iphone is sending it's data.

I have experimented sending my own _tiC event but that didn't seem to do anything. I've seen some _hidT events pass by with 5 arguments so that would be my next best bet, no idea as of yet what the arguments should be and how to trigger it.

SightAndSound-Roger avatar May 09 '22 08:05 SightAndSound-Roger

Sorry about my absence regarding your work, it's really great that you are looking in to this! I know a lot of people would like to see keyboard support (it would also be possible to implement in MRP and DMAP for older devices). Unfortunately I don't have any free time to help out with this right now, I will if I find a slot. But great work so far, appreciate it! 👍 Feel free to make updates to the documentation and send a PR!

postlund avatar May 11 '22 07:05 postlund

@SightAndSound-Roger Hey, are you still working on this? I would be interested to know how far you got? It sounded like you were getting close to solving this?

Keyboard support is the most important feature for me and I would love to offer my help anywhere possible (although I am not a python programmer in nature)

Fredde87 avatar Jul 20 '22 18:07 Fredde87

@Fredde87 unfortunately not, I've been searching for the way to send strings but to no avail. I've tried polyfilling with separate hid commanda but that didn't work for special chars and for some reason the a char... I've given up for now, after investigating the console on iOS and tvOS side I couldn't find a definitive solution. Still hoping for someone fresh to take a look!

SightAndSound-Roger avatar Jul 20 '22 18:07 SightAndSound-Roger

Keyboard support is possible via MRP as well, probably easier to implement as I have already created the protocol messages involved (base is here). So it's technically possible to go that route as well.

postlund avatar Jul 20 '22 20:07 postlund

I have tried the MRP (over Airplay) route but that doesn't seem to work with the keyboard. Perhaps I've been doing something wrong?

SightAndSound-Roger avatar Jul 20 '22 20:07 SightAndSound-Roger

I haven't dug into keyboard details, but looking at the log when accessing the keyboard on the Apple TV (possibly also making some input) should shed some light on how that work. Maybe TextInputMessage is used to set text? Trying everything is probably the naive solution. The proper one would be if I finished up the support in atvproxy some day so that we can look at the traffic again like we could when MRP was a stand-alone protocol.

postlund avatar Jul 20 '22 20:07 postlund

Having the proxy would be a bliss, I've took a shot at fixing that too but failed miserably. I've implemented the keyboard before with C# and the regular MRP protocol, the same messages did not go over Airplay :/

So I've looked it up, it required the GetKeyboardSessionMessage and then indeed the TextInputMessage, however during debugging I think I got some error about the amount of parameters in the message.

SightAndSound-Roger avatar Jul 20 '22 20:07 SightAndSound-Roger

These are the logs that happen around text input (using the control center remote): iOS image

tvOS image

I'ts not much but perhaps it rings a bell for someone else.

SightAndSound-Roger avatar Jul 21 '22 07:07 SightAndSound-Roger

I'm just assuming that everything that used to work when MRP was stand-alone also works now. That might of course not be the case but it would seem strange to me if keyboard didn't work. But of course, it's just guesses on my end.

postlund avatar Jul 21 '22 08:07 postlund

I believe the two keys in tiC are tiV and tiD, probably standing for Version and Data. The data is RTI data (I think RTI stands for ~Real~ Remote  Text Input) and version refers to the version of that data. I have no idea what kind of data we are talking about here, what it looks like or what is contains. But that's what I have found so far.

postlund avatar Jul 21 '22 08:07 postlund

tiD is some sort of NSKeyedArchive which I managed to deserialize with the nska_deserialize lib, however it appears that it's only feedback but looking at the logging it sort of looks like it goes both ways.

I've had a hard time actually serializing one of these archives to send back, I'm expecting it to need some sort of session id included which the _tiStart appears to return.

SightAndSound-Roger avatar Jul 21 '22 09:07 SightAndSound-Roger

I agree that I think tiD is some sort of feedback only, because I see in my iOS logs that it is only received post me sending any keys.

Sorry if I am being a bit dumb here. But looking at your _tiStarted/_tiC samples above, they appear to be binary plists. If I decode them using bplist I get the following,

_tiStarted

{'$version': 100000, '$archiver': b'NSKeyedArchiver', '$top': {'documentTraits': b'', 'sessionUUID': b'', 'documentState': b''}, '$objects': [b'$null', {'docSt': b'', '$class': b''}, {'contextBeforeInput': b'', '$class': b''}, b'Testeraaaa', {'$classname': b'TIDocumentState', '$classes': [b'TIDocumentState', b'NSObject']}, {'$classname': b'RTIDocumentState', '$classes': [b'RTIDocumentState', b'NSObject']}, {'bId': b'', 'app': b'', 'locApp': b'', 'tiTraits': b'', '$class': b''}, b'com.apple.TVSearch', b'Search', {'flags': 1410341510, '$class': b''}, {'$classname': b'TITextInputTraits', '$classes': [b'TITextInputTraits', b'NSObject']}, {'$classname': b'RTIDocumentTraits', '$classes': [b'RTIDocumentTraits', b'NSObject']}, b'6\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d']}

_tiC

{'$version': 100000, '$archiver': b'NSKeyedArchiver', '$top': {'documentTraits': b'', 'sessionUUID': b'', 'documentState': b''}, '$objects': [b'$null', {'docSt': b'', '$class': b''}, {'contextBeforeInput': b'', '$class': b''}, b'a', {'$classname': b'TIDocumentState', '$classes': [b'TIDocumentState', b'NSObject']}, {'$classname': b'RTIDocumentState', '$classes': [b'RTIDocumentState', b'NSObject']}, {'bId': b'', 'app': b'', 'locApp': b'', 'tiTraits': b'', '$class': b''}, b'com.apple.TVSearch', b'Search', {'flags': 1410341510, '$class': b''}, {'$classname': b'TITextInputTraits', '$classes': [b'TITextInputTraits', b'NSObject']}, {'$classname': b'RTIDocumentTraits', '$classes': [b'RTIDocumentTraits', b'NSObject']}, b'6\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d']}

So is the unique identifier you might need to include not just the last byte string? So the "6\xa7\xfa+\x03\xadL\x1d\xbd\x04\xab\xec\x96:6\x9d" value that is received at the end of each bplist?

I think in the _tiStart it tells you what is already on the screen of the AppleTV. So in your example you had previously searched for "Testeraaaa" I am guessing? And then you press the letter "a" which is seen in the _tiC plist.

So what happens if you decode the bplist, take the last value that seems to be the only unique value and created your own bplist and send it back using _tiC?

There doesn't seem to be much unique between each bplist so you could try just take the _tiStart, decode it, use it as a template and then just sent it back as a _tiC but change third object under $objects (which is equal to "Testeraaaa" and "a" in these examples).

Or is this what you have tried already?

Fredde87 avatar Aug 04 '22 03:08 Fredde87

I have actually sort of tried that, however I have not been certain the plist format I was sending was actually encoded any good. I haven't ran any deeper into the plists, only looked for some libs that could encode and decode them but every one of them gave different results going either way.

I have tried copying and injecting stuff in the bytestring using (I believe) the sessionUUID in there but did not get any luck as of yet.

I do agree tiC might be the one though.

SightAndSound-Roger avatar Aug 04 '22 06:08 SightAndSound-Roger

Would you be able to share some of the test code you have written to try sending _tiC, HID commands etc? I’m not a python programmer so that would save me some time getting a test bed up and running and then I can do a bit of trial and error after that to see if I get anywhere

Fredde87 avatar Aug 04 '22 06:08 Fredde87

HID commands are already in pyatv (see pyatv/protocols/companion/api.py def "hid_command")

Here's the last stuff that I hacked in. ( I see I was sending HID over mrp since it looked like it had more HID support) Also after a few quick tests, _tiC cannot be send as command, it is an event. I believe that isn't the one we're gonna be needing.

Here's a little test file + class I whipped up.

import asyncio

from pyatv import Protocol
from pyatv.interface import RemoteControl
from pyatv.protocols.companion import CompanionAPI
from pyatv.protocols.mrp import MrpProtocol
from pyatv.protocols.mrp import messages
from pyatv import connect as pyatv_connect, scan as pyatv_scan, pair as pyatv_pair
import logging

logging.basicConfig(
    level=logging.DEBUG,
    datefmt="%Y-%m-%d %H:%M:%S",
    format="%(asctime)s %(levelname)s [%(name)s]: %(message)s",
)

class AppleTVTester:

    def __init__(self, atv):
        self.atv = atv

    async def init_keyboard(self):
        api = self.get_companion_api()
        await api.subscribe_event("_tiC")
        await api.subscribe_event("_tiStarted")
        await api.subscribe_event("_tiStopped")

        return await api._send_command("_tiStart", {"_tiC": 1})

    def get_companion_api(self) -> CompanionAPI:
        setup_data = self.atv._protocol_handlers.get(Protocol.Companion)
        api: CompanionAPI = setup_data.interfaces[RemoteControl].api
        return api

    async def get_mrp_protocol(self) -> MrpProtocol:
        setup_data = self.atv._protocol_handlers.get(Protocol.MRP)
        remote_control = setup_data.interfaces[RemoteControl]
        return remote_control.protocol

    async def hid(self, page: int, command: int, down: bool):
        logging.info(f"hid {page} {command} {down}")
        protocol = await self.get_mrp_protocol()
        await protocol.send(messages.send_hid_event(page, command, down))

    async def hid_command(self, page: str, command: str):
        await self.hid(int(page), int(command, 16), True)
        await self.hid(int(page), int(command, 16), False)

    async def hid_up(self, page: str, command: str):
        return await self.hid(int(page), int(command, 16), False)

    async def hid_down(self, page: str, command: str):
        return await self.hid(int(page), int(command, 16), True)

    async def set_text(self, text: str):
        setup_data = self.atv._protocol_handlers.get(Protocol.Companion)
        api: CompanionAPI = setup_data.interfaces[RemoteControl].api

        logging.info("===================================================================== BEFORE SEND COMMAND =====================================================================================")
        res = await api._send_command("_tiStart", {"_tiC": 1, "_tiV": 1, "_tiS": "TEST"})
        logging.info(
            "===================================================================== AFTER SEND COMMAND =====================================================================================")
        logging.info(res)
        return 'done'

        await self.hid_command('7', '0x9c')

        for element in range(0, len(text)):
            char = text[element]
            await self.hid(0x07, ord(char), True)
            await self.hid(0x07, ord(char), False)




APPLETV_IP = ""
COMPANION_CREDENTIALS = ""
AIRPLAY_CREDENTIALS = ""

async def test():
    loop = asyncio.get_running_loop()

    try:
        results = await pyatv_scan(loop, hosts=[APPLETV_IP])
    except Exception as ex:
        logging.error(f'Failed scanning for device {APPLETV_IP}; {ex}')
        results = None

    if not results:
        logging.info(f"Device:{APPLETV_IP} not found!")
        return None
    else:
        logging.info(f'Found:{APPLETV_IP}')
        for service in results[0].services:
            logging.info(f'With protocol: {APPLETV_IP} {service.protocol.name}')

    atvConfig = results[0]
    atvConfig.set_credentials(Protocol.Companion, COMPANION_CREDENTIALS)
    atvConfig.set_credentials(Protocol.AirPlay, AIRPLAY_CREDENTIALS)

    atv = await pyatv_connect(atvConfig, loop)

    tester = AppleTVTester(atv)
    keyboard_start_response = await tester.init_keyboard()

    logging.info(keyboard_start_response)

    await tester.set_text("TEST")

    logging.info("===================================================================== DONE =====================================================================================")
    logging.info("===================================================================== DONE =====================================================================================")

asyncio.run(test())

SightAndSound-Roger avatar Aug 04 '22 07:08 SightAndSound-Roger

My best guess is that ti stands for "text input" and the C is likely for Command.

postlund avatar Aug 04 '22 08:08 postlund

I've created a PR that implements this "text input" method https://github.com/postlund/pyatv/pull/1905.

michalmo avatar Jan 24 '23 00:01 michalmo

You're a legend @michalmo , a quick test resulted in glorious success. I'll be testing this for a bit in the near future, hopefully adding the keyboard start and stop events too.

SightAndSound-Roger avatar Jan 24 '23 09:01 SightAndSound-Roger