 Thank you so much for coming, send me the bill later for that nice introduction, thank you. So, okay, so what I'm planning to do here is a little bit different, I would like to show you a way to write a web application that I think for some reason doesn't have so much popularity, a lot of people don't consider this a valid option. So, before I get into the coding, I'm going to give you just a quick introduction just to set your frame of mind so that you know how this is going to work. So I'm going to use the socket IO protocol. I'm not going to explain how it works under the hood, but all I'm going to say just to set you to relax is that it runs on top of WebSocket. So, that's what I'm going to say about how it works. There is documentation for this protocol in socket.io. This is a mostly JavaScript-based documentation. And then there's the Python port of it, which is on read.docs. Just so that you don't think this is an obscure thing that nobody knows about, socket IO clients are available for most platforms. So, obviously, the Python one, which comes into variants, you can use it in standard Python and async.io. There's JavaScript, which is the reference implementation that was the first, which runs in the browser or you can run in Node. And then there's a bunch of others, all the mobile platforms have it. And I'm sure there are more that I don't even know about. These are the ones that I'm familiar with. That was the clients. For servers, as I said, there's a JavaScript reference implementation. There's the Python one. There's the Java one. And those are the three that I know, there might be more. For this, I only get about the Python one. That's the one that I use all the time. And it pretty much works like this. The client connects to the server and unlike with HTTP and REST and those solutions, it's a permanent connection. It doesn't end, so it stays connected. So each time a client joins or leaves the server, the text that, and then it gives your application a chance to do something about it. This one is probably obvious. The client, since it's connected at any time, it can invoke an endpoint in the server. The server exposes an API that the client can use. But the one that's probably, you're not used to, is that the server can also invoke endpoints in the client. The client also exports an API and the server can make calls into it. So basically when you design an API for this, it's a double-sided API, if you will, where you design endpoints on both sides. And in fact, when the client connects to the server and they're connected, there's really not much of a difference between a client and the server. Both can talk to the other side and the lines are blurred a little bit. The only main difference between clients and servers is that there's one server and many clients. And for that reason, the server, when it invokes endpoints in the client, it has superpowers. And it can decide if it invokes an endpoint in one specific client, maybe all of clients, it can broadcast and then all the clients get this endpoint invoked. Or it can select a subset, whatever makes sense for the application, and only invoke an endpoint in those, okay? So now you know, pretty much everything there is to know about this. So with this, I'm going to build a multiplayer chess game. I'm gonna try to get as far as I can here in one hour. And if I don't make too many typos, I think there's hope. If not, we'll see. So in my console, I have, I created sort of a starter project with some boiler plates so that I don't waste a lot of time. So I'm gonna very quickly show you what I have. On the Python side, in terms of dependencies, there are four dependencies. The socket IO server and client. UV corn, which is a very nice async IO web server. That's what I'm going to use for this application. And then there's a chess package that I'm going to use to validate moves, that type of thing. And then finally there's watch God, which is a little process that watches the source code and anytime I make a change and save the file, it'll restart the server. So if you're using flask, pretty much like the reloader in flask, so that I don't have to restart it every time by hand. So server.py is gonna be my server, there's nothing in it so far. And then I have the front end, which is in the public folder. And here I have a one HTML file, which has a title and then four dependencies for the JavaScript side, which are jQuery, the socket IO client for the browser. And then two related to chess, one to show chess boards, and one to do move validation as well on this side. And that's it. And then I have an app.js, which is what I'm going to write. Which if I remembered, yeah, this should be, it is an empty file. So this doesn't exist yet. And then, what else do I have here? I have some CSS, some images, these are for the dependencies for the chess stuff. And that's pretty much it. So I'm going to start my reloader, which basically, I'll tell it to watch this function, the main function in the server. And now it's going, so even though there's nothing yet, then when I start putting stuff, it'll reload. So let's see. So I'll leave it there, in case I make a mistake, I'll see it there. So I'll just leave a little bit of that window. I'm going to activate environment here. So let's create a server. So, sorry, not a socket IO. I'm going to import async.io and uvicorn. And then I'm going to create the socket IO instance. It's going to be the async flavor. So this, once again, you don't have to use async.io if you don't want to. You can use, there's a class called server, just server, that implements the same functionality, but for traditional Python. And uvicorn is an ASCII server. For those who are not familiar, ASCII is the async improvements over whizzy. So there's a specification for ASCII and uvicorn supports that. So to connect the socket IO server with uvicorn, socket IO provides a wrapper that converts this application into an ASCII compliant application. There are many other wrappers for other things. You can do whizgy and a bunch of others, tornado and et cetera. So I'm going to create an application with the ASCII wrapper and pass my socket IO instance to it. And now I can just run the application. And it restarted and everything's looking good. So I have a bunch of static files, right? These front-end files in the public folder. As a convenience, this ASCII wrapper allows me to expose those files so that during development I can easily serve them to clients, to browsers. And of course, now I have to admit that I lied because this is going to use HTTP, right? The files are going to be served over HTTP. But none of the application logic will use HTTP. So this is going to be the slash URL. It's going to map to the public folder. Just like that, I think. Oh, I'm sorry. Static files. Very good. So if I now run this, I'm getting it, you know, the empty front-end. So this is looking pretty good. So before I get into the chess stuff, which you may or may not find interesting, I'm going to show you something that I hope will interest most of you. This is something that is incredibly difficult to do with HTTP. I want to show how many users are using the application at any given time. So think about it. How would you do it with HTTP? And if I think about it, I get a headache because it's nearly impossible. It's very hard. So let me show you how to do that this way. So I'm going to start with the index file. I'm going to add a place to show this number, the number of users. So an H2. And it's going to be something like that. Because I'm going to dynamically change the zero here. I'm going to put it in its own element. And I'm going to call it user count, say. So then I'm going to use jQuery to change this when appropriate. Okay. It should be very good. So actually, I'm going to do the client. So the client needs to connect, right? I haven't done any connections yet. So in the client, I'm going to create in the same way I did it for the server, I'm going to create an instance of socket IO for the client. In the client, it's actually very easy. And that's it. IO comes from the socket IO client. It's the main function that creates the instance. And if you don't pass any arguments, it connects to the same server that the page came from. So it uses the URL in the address bar. And you don't have to specify a connection URL. So now this client now connects and it remains connected. So in the server, the server is going to detect that. And it's going to want to notify my application. And whenever there's these types of notifications, socket IO calls them events. So I'm going to create an event. And I'm going to call it connect. And the name of the event is important. Basically the SIO event decorator uses the name to know, you know, which events are, you know, handlers for different aspects of the application. There are other ways to create events. If you don't want to use, if you don't want to be forced to use a given name for your functions, there are other ways to create them. But I find this the most convenient. And the connect handler gets two arguments. The SID or session ID is part of the socket IO protocol. Each time a client connects, it gets assigned a unique identifier. This is called a SID. So I get the SID number assigned to this client that is connected. And then Environ is basically their request. So you get information about headers and whatever else, cookies, all that stuff. And you can use this for authentication, you know, those type of things, which I'm not going to do here. But, you know, it's there in case you need to do that. That is a dictionary and it's formatted according to the whiskey specification. So anyway, here I still don't have the chess stuff going, but I know that for each user, I want to record, I want to assign the user to a game. So I'm going to have some storage, which for this exercise, it's going to be an in-memory dictionary. For a real application, it might be a database, it might be a Redis, that type of thing. But I'm going to simplify. And here I'm going to just write something for this user. So for now, I don't have a game yet. I'm going to put a game later on. For now, I'll just put that. And then, likewise, it was going to be a disconnect. The disconnect gets the SID of the user that's going away. And what I can do here is remove it. Oops, that's it, right? So this is the cool part. I want to have an endpoint in the client that updates the number of connected users in any time a user comes or leaves. And now I start getting into the async part. So I need to make this an asynchronous function. And I'm going to do an emit, which is the method that makes a call into the other side. Both sides can do emits. So I'm going to call this one userCountChanged. How about that? And then as an argument, I'm going to pass how many users I have. And then, oh, okay. So you don't have to worry too much, because when I save, I'll be told if I made a mistake, I'm prepared for this. So don't worry, don't worry too much. And this one still needs the async. There we go, okay? So what I'm doing here is invoking an endpoint in the client, which I didn't write yet, and passing an argument, which is the number, okay? So let's go to the client. In the client, the syntax is a little bit different. UserCountChanged. And then there's a callback that takes the number. There we go. And then when this is invoked, I can take the userCount element, which was the span that had the zero, and then set a new text to the number. And fingers crossed. Let's see if it works. So now we get one. So I'm going to start another one. Now let's do it. So let's see if I can do this. Maybe some of you don't believe me. So now you can see two at a time. I'm going to start a third one. So look at the number three on the left. I'm going to go ahead and close one of the two that I have on the right. You see, this is so easy, and it's so amazing. And if you had to do this with HTTP, I would not even know how to start. It's super messy. It will be super messy. So anyway, this is basically the way things work. So I'm going to continue with this, and this is going to be the style. I'm going to start deciding on things that I need to call on the other side and then go to the other side and implement them. So I'm going to spend maybe a minute putting up a chessboard on this page. I hope this isn't too boring. So this is going to be a board element. And then in the application, the game is going to be class chess. So I need to think that the game is going to be what validates moves and then there's going to be a board, which is what displays through the game. And this is chess board. And this one takes the ID of the element where the board goes. So let's see. So that's the board. It's kind of big. Let's make it a little bit smaller. Let's make it 400 pixels. That's better. Okay. So we have a board. I'm going to set some options in this board. First, the position. I'm going to set it to whatever the game is set to, which is going to be initially the initial position. For those who don't know, there's a notation for chess positions. That's called FEN or FEN. It's a standard position. It records everything about a chess position. So basically that's what these libraries use. So I'll set it to that. And then I'm going to make the other PCs draggable so that we can start moving things around. So now I get pieces. I can move, but there's nothing to validate yet. So now every user gets a board. So what I want to do now is I want to assign users as they come into the application to a game. I don't want to spend a lot of time on that since it's unrelated to the topic. So I'm going to do something simple. Each pair of users, when they connect, the first one will be white. The second one will be black. And then when I get another one, I open a new game and then I keep doing it that way. So as the user comes, I do white and black and white and black and so on. So let's do that in the server. So I'm going to have a list of games in addition to a list of or a dictionary of users. And for a game, I'm going to use a dictionary with the ID, the SID of the white player. So this, for example, will be setting up the game for the white player. So I set white to the SID of the connecting user. The black, I don't have it yet. I need to wait until someone else connects. So I'll set it none. And then I'm going to set up a board which would be the chess logic, which I'm going to get from this package chess. So chess board. And that's a game. So this is going to be the case of a white when the player that goes to the white side joins. So this is going to apply when either I have no games, so the first player ever, or if the last game has a black player already assigned. So in that case, we have this. And then I'm going to add it to my list. Okay? And then the black case, my game is going to be the last game that I have in the list. I'm going to set the black player to the SID. And I think that's it. I can put the game now. And then I assign the dictionary to the user. There's going to be two users in the system that point to the same dictionary, right? So these are the two users that are playing the game against each other. So I need now to tell the front end that the game is ready to go. So I'm going to use a new endpoint that I'm going to call new game. And then I'm going to pass the FEN position, the description of the board to the client. So that is going to be gameboard.FEN. You can see that all these libraries use the same standard. They're all kind of the same in that respect, because this is a widely available standard for chess. And then this one, the user count changed. I just said emit, and it went to everybody, right? But this one, the new game, I want to send only to the user that connected. So I send it by using the SID that was assigned to that client. So I send it to that person only. One more thing that I can do now that I think about it is the player that gets black, typically on a game of chess, the black pieces are at the bottom for the black player. So I'm going to just very quickly set the color. And then here, I'm going to send two arguments, which in the Python side, you have to send as a tuple. So send the FEN and the color that was assigned to that player, so that then the front end can turn the board around if necessary. Okay. And define game, where is it? Yeah, games. Okay, very good. So on the client side, now we need to implement new game. This is the endpoint that starts the game. And this function gets the FEN and the color. And what I can do here is I can take the game object and set the FEN to the position that server sends. So this library uses load to set a position. And then this is in the hidden part, the package that does the logic. I need to update the board as well. So for the board, I'm going to say, and of course it uses a different API for this. I'm going to do it like that. It uses the position. And then the board also has orientation. And you can set it to white or black. So I set it to color. And there we go. So now it's bringing our two browsers. I think I had an error. Where is it? Did you see it? In the Python side? 21. There we go. Sorry. So that was prepared. Let's see if that works better. No? There's still something going on. Sorry. I don't see it. The else? No, this is fine. It's probably in the other side. Oh, I didn't save. Really? Okay. Let's see. Sorry about that. Yeah. So, okay. Now I refresh two. And the second one gets to black. Okay. So that now players are assigned to games. If more players joins, they're going to get different games, right? New games. So what I want to do now is to make a move. And this is actually the most complex part of this. So I'm going to make a move on one of the front ends. And then that client is going to invoke an endpoint in the server. The server will validate the move. And then the server needs to invoke an endpoint in the other front end. The one that's playing with the first to update the move, right? And then the process will reverse. And then, you know, the two clients will switch places. And that's how we get the game going. So let's see how that works. The chessboard package has an on drop. That you provide a callback. So each time I move a piece and I drop it in a different place, then it will call this function. And it sends me the two locations start and end of the piece that was moved. So what I can do here is try to move. And this is actually very easy. So you just give it the start and end squares. And these are, if you look at the, in case you're not familiar, you can probably see or maybe not. But each square has a coordinate, which is a letter and a number. So the from and the to are each two characters, a letter and a number. And that basically determines from where to where you're moving. It's actually pretty simple. So I make the move and then I can check if the move was accepted. So if the move was not accepted, I can, and this is part of this package API, I can return snapback and that will undo the move as invalid. The other thing that I need to do is I need to make sure that not only that the move is valid, but it's valid for the color of the pieces that this player is assigned. So we don't want the white player to make a legal move for the black. So for that, we need to check that it is the turn of this player to move, not the other player. So for that, let's see, we can do turn, actually, no, it's easier. I know the code already. So I can put this in a variable. So now we know the piece color for this player. So if and then this board, the chest package returns, it returns a letter, either W or B for white or black. So what I can do is check the first letter, which is how you can make sure that it's the same, it's the player's turn. And if it's not, once again, snapback. Okay, what did I do now? Hold on. I got it, I got it. More? Perfect. So this, now if I try to move, this is it. What happened now? Is it? Okay. So my linter isn't doing, yeah, line. So let's put this on white, the other one on black. So now if I move the black, not accepted. If I make an invalid move, not accepted, the valid move is accepted. This one cannot move even if it's a valid move for the white because it's the black. So now that I have a valid move, I can send it to the server. So from the client is the same thing, emit. I can call it move made. And I pass the two coordinates from and to, of course. And then on the server, I write another event, move made. The events on the server always receive the seed of the client that sent the event as the first argument. And then you get the arguments for, you know, the arguments came with the event. So here we have from and to, and actually from is reserved in Python. It's from square to square, something like that. So here I need to validate again that the move is valid. Always when you do validation, validation done in the client is for user experience, but the real validation is done in the server. So we need to repeat the same thing. So actually, let's get the game. So that's the game. The turn here is game board turn. And here this chess library uses a Boolean. So true means white, false means black. So I'm going to do white, else black. And then I need to know whose color is this player playing. So I can do white if game white matches the seed for the event. And if not, it's got to be black, right? So if turn matches the piece color, then we're good to go, at least on the turn aspect. This player is allowed a move right now. So the next thing is to try to make the move and see if it flies. So in the chess package, the easiest way that I found to make a move is to use this notation called UCI, which is actually a concatenation of the two coordinates of four characters. And then once you have a move object, you can check if move is in the list of legal moves for the game at that point. So if all of this is true, then the move is valid, and then you can actually apply the move. There we go. So now the move is made. So there are a few considerations here. There might be an invalid move or the player trying to move when it's not the turn. So in all those cases, we need a way to tell the front end that the move was not accepted so that the front end can then get a new move from that user. So this is another cool feature of socket IO, which kind of matches what you can do with REST or HTTP, where you send a request, you always get a response. So you know if that request worked. So when you emit an event, you can also get a response, which I haven't used yet. So on the Python side, you send a response by returning something from the function. So you can return the position. So if the move was not accepted, I'm going to return the same position, and the board in the front end will revert to whatever it was before. So, okay, no errors, hopefully. And then on the client, this return value is given because this is JavaScript, you get it as a callback. So you can add a function at the end after your arguments, and this function gets whatever you return, it gets it as argument. And if you want to return multiple arguments, you return a tuple from Python. So here what I can do is I can basically set the... Actually, I do have it, yeah. It's these two lines. I need to repeat this. So I'm going to take these two, and I'm going to write a little grapple function. Update like that. And then here I don't have to repeat myself. So this is going to undo the move if it's invalid. So the part that remains is now the client now has an updated board, but the other player now needs to know what the move is. If after all the checks we apply the move, now we can emit... I'm going to call it opponent move. And I could send the from and the to squares, but really I don't have to. I can send the entire board since it's really a line. It's actually a pretty short representation. So I'm going to send the fan of the updated position. But this needs to go to the other player. So I need to find out who that is. So the other player is going to be game white if we are black. Else this is going to be game black. I'm not using it yet. Perfect. So then to other player. So this is a private message that goes to the other player in that game. So opponent move. So now back in the client, opponent move. This also receives a fan notation for the board. And all I need to do is once again update the board. Because everything else happens on its own, right? When the other player tries to move, we are going to reevaluate if it's their turn to move. So all I need to do is update the board. And one of the things that this fan notation has is who's turn is. So that's one of the things that are included in the representation. So based on past experience, I don't trust myself. I bet I made some mistake here. But let's see. So, oh, there you go. Okay? So, yeah. So, but now the real challenge is the black response. And they also get it. So basically we're done. We have a game here, right? So the things that I'm not going to do here, because there will be boring is detect when the game ends. All of that you can do fairly easily. It doesn't have any challenge. One thing that I didn't mention though that I'm going to show you. This callback thing that I did, this return, returning the position, not only applies to invalid moves, but chess has some complex moves where typically when you're playing with a physical chess board and chess pieces, some moves you need to move two pieces. For example, one is called castling, right? So in computer chess, the convention is that you don't move the two pieces. You only move one. And then the computer knows that the only possible thing is, you know, that the other piece needs to move to. So the computer moves the second piece. So by returning the updated board position, I basically, I solved that. And I'm going to show you. I'm going to just play a quick game against myself. Just enough to get to a position where I can do a castling. So here, the white king and the white rook are going to do that. But according to computer chess standards, you only move the king. And then because I'm returning the updated position, the chess package in Python who understands the logic of that move, it moved the rook as well. So returning that position refreshes the board. So even if I didn't have chess validation logic in the client, I will still get the correct move because the server is updating it. Makes sense? So time, okay. So what I want to show you now is that you can do a Python client as well. So I'm going to just create a new file. So this is going to be a client that's going to be playing chess as well. So somewhat similar. I'm going to create. So this one, the server I showed you, the async.io version for this one, just for fun. I'm going to do a traditional standard Python no async.io, since they kind of work the same. So I'm going to connect. In this case, I don't have context like the browser has. So I need to get it the location of the server. And then because this is all event-based, there's really nothing else that I need to do in the main thread of the application. So all I do is wait. And now events are going to start showing up. So for example, remember this user account changed event that was invoked every time a user joined or left. So we can do that one. User account changed. And then new account. On this side, you don't get to see it because there's only one client. We're looking at this from the client side. So you just get the arguments. And here we can just print something like that. So if I didn't make any mistakes, oh, I did make a mistake. Actually, I forgot to run the main function. It's restarting. That's not my fault. So let's bring... So let's say I'm going to start one more here. So each time someone comes or goes, the Python function runs. And I get a chance to do something regarding that event. You see? So we can actually very easily complete the game. So we had New Game. And this was Fen and Color. So here I can have a board. There we go. And the board is set with Set Fen. And then for the color, I'll just put it in a variable so that I know later when it comes the time to make a move. So global piece of color. There we go. So now if we get white, now it's our turn to move on this Python client. But I don't know, right? It could be white or black. So what we can do here is just make a function move if it is my turn. So turn is going to be white. If board turn else black. Remember that in the Python chess package, it's a Boolean. So I'm going to, just to make it more readable, I'm going to switch it into white and black. And then if piece color equals turn, that means that we need to move. So I can print the board. Luckily, there's a handler that prints a text version of the chess board when I print it. I can show the color so that you know who you play with. And then just prompt for a move. Just like that. So we can get the move with this UCI notation for characters. So the first move will be E2, E4. That will be the first move that I've made with the whites. Something like that. And then I'm not going to worry about validating because the server validates anyway. And then the server will let me know if the move was invalid. So move made was the event. And then I needed to pass the from and the to. So here I need to split this four character move into two parts. The first is the first two characters and the second is the third and the fourth. And this is when the server receives this, validates, notifies the other side, and then returns in that callback to the JavaScript client, it returned the updated board. So for this, I can do something like this and then put a function, which will be kind of similar to JavaScript. But we, Pythonists, we hate this, right? So there's a more Pythonic way to do this, which is instead of emit, you can use call. And call combines an emit with waiting for the callback. And then you get the response, like you're making a function call, but it's a function call across to the other side. So now what I can do is I can take this board and update it with this position. This position could, if the move was valid, now it's the turn of the other player. But if the move was invalid, it's my turn again. So what I can do here is I can say again, move if it is my turn, right? So then if it's not my turn, then this is going to end, and eventually we're going to get an event when the other side moves, which is actually the last thing that we need to do. Opponent move, what's it called? I think, and it receives once again the fin, and when I get that, all I need to do is same thing. Update the board, and then if it is my turn to move, prompt for a move. Let's see. Okay, something's not working. What did I miss here? So fin, great. Okay, I'm going to develop this. Exciting. See what I'm getting. Okay, there we go. So I'm going to make a move, but let me, let's put this JavaScript one on the other side. So this is the black for that game. It might be, yeah, it might be, let's see. So I moved the JavaScript client, and now it received here the move. I can respond E7, E5, and now Python and JavaScript playing with a Python server in the middle. So I think there was one more thing that I wanted to show only there was time, and there isn't, I think. I assume you have some questions, so I'm going to leave like five minutes for questions. So I'm just going to tell you what I was going to do next. The idea was add another option where, if you're playing a game here, you can specify that you want to watch all the other games that are going on. So that will basically put you in a, what I call a room. So there was a room called watchers. So all the clients that want to watch games ask the server to be put in that room. And then the server, each time there's a move, in addition to all the notifications to the white and the black players, it sends a notification to the watchers' room so that the client can show all the boards. So basically that was the idea that I don't think I have time to complete. It has a bunch of HTML to set up that, so it will be a little boring and I will make mistakes and so on. But anyway, what I can do is I can actually complete it and then all this code that I wrote, I'm going to commit to this. This is a GitHub repository. So if you want to play with it, after the talk I'll commit it there and you can play with it. So yeah, I'll leave it there and I think we have five minutes for questions. Thank you. So as Miguel said, we have five minutes for questions. You can line up at one of the two mics here or if you're in the middle somewhere, you can just put up a hand and I'll come around with a microphone. Hi, thank you. That was a brilliant talk. One question for me that may be a bit big question once more, but can you tell us a bit more about the drawbacks maybe of using WebSockets over HTTP, maybe code complexity, scalability, I don't know. Drawbacks of using WebSocket instead of... So in general. So one thing that I would say is that WebSocket is not appropriate for an application that is not a single-page application, because each time the page changes, all the connections are ongoing, they're broken, basically. All the connections are disconnected. So each time you switch the page, you have to connect again. So it seems practical. And that applies to Socket.io as well. I would not use it for something that is not a single-page app. WebSocket on its own doesn't have reconnection logic. So if you're on your phone and you're connected to WebSocket and somehow you lose Wi-Fi, there's nothing in the WebSocket that will reconnect you. So it's kind of annoying. Socket.io does have reconnection logic, though. So if you're using this and you pass through a no-signal zone or no Wi-Fi zone, when you get the signal back, you will be automatically reconnected. So this has, in my opinion, some interesting advantages over plain WebSocket. Thank you for the presentation. It was a really great overview. In there, you demonstrated that you can get a response for a WebSocket event. Is that the feature of WebSocket or a feature of Socket.io? This is a feature of Socket.io. Not a feature of WebSockets. No. Okay. Thank you. Yeah, in fact, the WebSocket API, it's very basic. Prepare. There isn't much you can do except sending and receiving. You know, that's pretty much it. So yeah, this does a lot of stuff on top. Thanks for the talk. One question. Would you ever find yourself in using WebSockets over Socket.io? Would you find the use for WebJust WebSockets and not to Socket.io? I never use WebSocket alone. It's been years since I've used it on its own. This is way more, you know, useful, more high level. So, yeah. And another question. Does it support binary data, raw binary data? Socket.io supports binary data, yes, absolutely. You can send images. You can send a video stream, for example, at a different talk a couple of years ago. I showed how to record audio and then stream it from the client that's recording it to the server, for example. So audio data, images, video, all of that works, yeah. Thank you. Any more questions? Going once, going twice. It looks like there's no more. So let's have another round of applause for Miguel. Thank you. Thank you so much.