UDP Multicast in Python and BitTorrent Live

Aaron Cohen —  February 4, 2013 — 3 Comments

Introduction

Now that we have begun to send out invites to BitTorrent Live broadcaster applicants, it seemed like a great time to begin a series of technical articles discussing the product. We’re taking slow steps toward completely opening the floodgates, trying to make sure that we have a usable, rock solid product once we release it to the world. Right now, you can enjoy the channels of our first broadcasters without an invite.

Meanwhile, expect to see explanations of how Live operates internally, as well as tutorials on how to put it to use, here on the BitTorrent Engineering Blog.

The development of BitTorrent Live has consisted of work in essentially two camps. First is the Python-based ‘core’, which handles the actual functioning of the protocol, as well as the underlying cryptography used when transmitting and receiving data. Second is the ‘app’, which consists of a tight layer around the core, providing a command line interface, as well as a layer beyond that which provides a Windows, Mac, or Linux GUI.

As the core has become more stable over time, and major changes to the protocol less frequent, providing features like Local Peer Discovery has become a possibility. It is present in all major BitTorrent clients, and provides great performance and efficiency gains. Live is an entirely new protocol, written from the ground up, so adding support required a bit of planning.

Local Peer Discovery is a mechanism by which peer-to-peer (P2P) applications can discover other clients consuming identical content within a local network. Communication can take place far more efficiently between two machines on a LAN than with machines out on the internet, and as such, it makes sense to prioritize data transfer between two local machines whenever possible. Doing so can reduce load on the outbound link to the internet as well.BTLive Hero Image

BitTorrent Live streams video between clients in real-time, and thus requires a generally higher performance network than the original BitTorrent, which has the luxury of being able to send or receive data at slower than real-time when necessary. The speed of the incoming data to Live (from all peers) cannot be less than the bitrate of the video being played. Preferably, there should also be an amount of overhead (excess network capacity) to allow for any lost or damaged data.

Due to these relatively demanding requirements, Local Peer Discovery is particularly attractive option for Live. Effectively any number of machines would be able to watch on a local network, while only one copy of the playing video would need to enter the network from the outside. Even in less extreme circumstances, playback performance would benefit from having local, low-latency peers.

UDP Multicast for Local Peer Discovery

Local Peer Discovery is typically implemented through UDP Multicast, in common with (and similarly to) Bonjour/ZeroConf. UDP is an alternative transport protocol to TCP, allowing a ‘fire-and-forget’ approach to transmitting data. Packets are not guaranteed to arrive at their destination, but in return initial handshakes and maintenance of active connections are not required. Generally, use of UDP is desirable when sending ‘unimportant’ data, or when speed of delivery is more important than integrity of the data. BitTorrent Live uses UDP for nearly all of its communications, in the interest of minimizing latency and being able to rapidly hop between peers without constructing and maintaining long-lived connections with each.

Multicast is one of three commonly utilized IP routing schemes:

  • The first, unicast, is the most common. It provides a one-to-one link between two IP addresses.
  • Second, broadcast, transmits from one IP to all IPs on a local network. Broadcast is generally frowned upon by network administrators because it creates a large amount of traffic that may be sent to machines that don’t need to receive it.
  • Third is multicast, which transmits from one IP to any number of IPs that have requested the data. The transmission is repeated to the many subscribers at the router level, meaning that the transmitter only has to send it once. The sender and the receivers don’t need to know each other’s IP addresses, they just need to agree on a multicast IP address (explained shortly) and a port.

A set range of IP addresses are designated for multicast use. 224.0.0.0 through 239.255.255.255 are the full range, though some blocks within that are reserved. For local network use, an address within 239.255.0.0 – 239.255.255.255 should be chosen to be your multicast group. Selection is somewhat arbitrary. Just choose one that you like, and you’ll discover soon enough if another application is already using it.

The peer discovery communication in BitTorrent Live works as follows:

  1. Figure out your local IP address (A little convoluted to do in a reliable way within Python)
  2. Create a new UDP socket that subscribes to a specific multicast group (we use this for sending and receiving, because it’s convenient)
  3. Bind the socket to a network interface (which interface varies by OS)
  4. When a video channel is joined, announce via our socket to the group, “Hey! I’m SOMEIDENTIFIER, watching SOMECHANNEL!”. Periodically repeat the message.
  5. When you receive an announcement, check to make sure it’s a different client, not your own message being echoed back to you by the router. If it’s someone else who is watching a channel in common with you, add them to your list of peers.
  6. Request a full list of peers from the newly discovered peer via unicast (a normal connection, not multicast)

Implementation in Python

Use of UDP Multicast in Python (2.7 in this case) is generally straightforward, though there is an OS specific gotcha:

  • As mentioned in the comments within create_socket() below, binding has to be done differently on Mac vs Windows. OSX (for unknown reasons) will not receive any multicast data if bound to a specific interface, so it has to use ‘0.0.0.0’. On the other hand, Windows must be bound to a specific interface.

Now, some code. It should be somewhat more robust than many examples online, and should work on Windows, Mac, and Linux. This example is not multithreaded. I recommend using an event-based library for your network programming, such as Gevent or Twisted.

Because it just uses simple while loops, it can only operate in listen OR announce mode at any given time. You can start up multiple instances of it, though, to have a full demonstration on a single machine.

In a real application using one of the aforementioned event-based libs, you will be able to both send and receive using the same socket created in create_socket(). Because the example script structure can’t do both at the same time, I offset the bound port by one in announce mode so that you can run the script twice, once for each mode.

The full code is also available as a gist.

Additional Reading Material

Changing or reading settings with socket.setsockopt() or socket.getsockopt() can seem like a bit of a dark art, because the flags aren’t documented anywhere in the main Python docs. A list of the ones supported in Python can be found in the Python source code.

Also useful is the Linux Documentation Project’s writeup on multicast socket flags.

Conclusion

Hopefully this example will help shed some more light on how to build a robust multicast Python application. While not a complete implementation, the needed building blocks should be present. If you have any questions, comments, or corrections, please feel free to post them below. I’ll do my best to stay on top of them and pipe in when necessary.

Aaron Cohen

Posts

I'm a software engineer on the BitTorrent Live team, located in San Francisco, CA. I work in Python, Ruby, Objective-C, Javascript, or whatever it takes to get the job done. Media focused applications are my specialty.
  • http://profiles.google.com/tzotzioy Χρήστος Γεωργίου

    Assuming little-endian and IPv4 addresses, isn’t it cleaner (and more “correct” according to specifications) to:

    ipn = struct.unpack(“i”, socket.inet_aton(ip_addr))[0]

    (ipn & 0x000000ff) == 127 # 127. LOCAL
    (ipn & 0x000000ff) == 10 # 10. PRIVATE
    (ipn & 0x0000ffff) == 43200 # 192.168. PRIVATE
    (ipn & 0x0000f0ff) == 4268 # 172.16.-172.31. PRIVATE

    ?
    There’s a way to calculate the constants on module loading, so it works for big-endian architectures too.

    For example, every 127. address is a strictly local address. There’s nothing that differentiates 127.0.0.1 and 127.0.1.1. They’re equivalent.

  • Pingback: The future of video streaming?

  • havanna

    Your implementation works fine for me in my local network. But why it does not work communicating with my friend over internet, even we have the same provider?