butterproject / butter-desktop

All the free parts of Popcorn Time
http://butterproject.org/
GNU Affero General Public License v3.0
4.29k stars 1.09k forks source link

streaming is broken on Apple TV4 #528

Open xaiki opened 7 years ago

xaiki commented 7 years ago

see @SteveJobzniak's comments on #437 we'd need some tcpdumps to see what's happening as nobody owns that hardware.

ghost commented 7 years ago

(This is copied from & extended from comments in the pull request discussion, to put it all in one place):


Here are the four AppleTV4 streaming problems in Butter, in order of severity from worst to least:

1. Butter defaults to using the IPv6 address as the streaming hostname in the URL it sends to the AirPlay.play() player function:

STATUS: FIXED BY https://github.com/butterproject/butter-desktop/pull/537

"[%cINFO%c] Available IPs: [\"fe80::6233:4bff:fe11:a4de\",\"10.0.1.167\"]"

Butter picks the "fe80" name. An AppleTV refuses to connect (whatsoever) if you are telling it the IPv6 stream URL. So for now, I had to manually run "networksetup -setv6off Wi-Fi" to turn off IPv6 to get Butter to advertise an IPv4 streaming URL instead.

There are 3 ways to fix that:

2. AppleTV4 connects to Butter and then disconnects when it sees unexpected HTTP headers:

STATUS: THE REASON HAS BEEN FOUND. WE NEED TO SUPPORT APPLE'S HTTP LIVE STREAMING. THE DEVICES SUPPORT ONLY MOV, MP4, M4V AND HLS (M3U8). ANY OTHER CONTAINERS OR UNSUPPORTED CODEC CAUSES THE SCREEN TO FLASH BLACK BRIEFLY AS IT CONNECTS AND THEN ABORTS THE ATTEMPT TO PLAY THE UNSUPPORTED MEDIA.

The AppleTV4 seems more restrictive than older generations of the AppleTV. It attempts to connect to Butter's HTTP server for streaming, and then gives up and disconnects again.

For testing, I was able to make my AppleTV4 connect and stream via a small HTTP server which has the following headers (so whatever header stuff is happening below, is stuff Butter may need to be doing too):

  var server = http.createServer(function (req, res) {
    var stat = fs.statSync(filename)
    var size = stat.size
    var range = req.headers.range
    range = range && rangeParser(size, range)[0]

    res.writeHead(206, {
      'Access-Control-Allow-Origin': '*',
      'Accept-Ranges': 'bytes',
      'Content-Length': (range.end - range.start + 1),
      'Content-Type': contentType,
      'Content-Range': util.format('bytes %d-%d/%d', range.start, range.end, size)
    })

    fs.createReadStream(filename, range).pipe(res)
  })

AppleTV4 connects fine to that. It only fails to connect to Butter's own HTTP server (which is based on webtorrent?). It may be that Butter's server doesn't have 'Access-Control-Allow-Origin': '*', or doesn't support byte-range commands, or content-length, or perhaps it needs to provide a ".mp4" filename in the stream URL in case the AppleTV4 doesn't use MIME to check its type, or perhaps it needs to send the proper mimetype, or any amount of other possible issues. Apparently the bug is in webtorrent, as per the discussion in this comment and the ones after it: https://github.com/butterproject/butter-desktop/pull/437#issuecomment-265910439.

I haven't got time for testing. Someone with AppleTV4 hardware and a Mac can easily do a tcpdump, and then developers can take a look to see why/when the AppleTV4 is disconnecting. Instructions for tcpdump are here, it's built into OSX: https://support.apple.com/en-us/HT202013

3. No scrubbing/seeking/playback position tracking:

STATUS: NOT REALLY WORTH WORKING ON IT UNTIL AIRPLAY IN BUTTER IS FURTHER ENHANCED WITH APPLE HLS SUPPORT. BUT CONTRIBUTIONS ARE ALWAYS WELCOME IF SOMEONE WANTS TO WORK ON IT NOW.

The groundwork for it has been done by the "_scrub()" function I added to the new airplay.js, but there was an unclear mess about what functions Butter actually requires for playback control, so I left that up to some other contributor.

What needs to be implemented: All scrubbing functions, and also the updating of playback position in Butter's GUI whenever the AppleTV returns position events to tell you where the user is in the video.

All discussions about the necessary scrubbing functions are in this pull request: https://github.com/butterproject/butter-desktop/pull/437 (but no conclusive answer was reached there about what functions are really needed).

Scrubbing will be easy to do via _scrub(), and for relative scrubbing (like -30 seconds) you would have to either keep track of the AirPlay device's (AppleTV's) playback position when the device sends you events, or query the device for that info via sending scrub() without arguments (the underlying function, not my wrapper).

Implementation of the automatic playback position event tracking can be done by analyzing @watson's "airplay-protocol" events, which has events for playback position/state updates. That requires a lot more time and work than I have available so I left that for someone else.

4. No subtitle support:

STATUS: WILL BE SUPPORTED WHENEVER WE HAVE APPLE HLS SUPPORT.

Unfortunately the AirPlay protocol has no "subtitle stream" support, so it's very hard to add support for subtitles over AirPlay, especially since Butter is streaming the data live from torrents and can't do container re-wrapping before streaming, since we don't have all data.

The only ways to support subtitles on AirPlay are by putting chunked mpeg2ts files in a m3u8 playlist (aka Apple HLS - HTTP Live Streaming), or by re-wrapping the media in a mp4 container on-the-fly (the desktop app "Airflow" does this). See this comment by @watson for both techniques: https://github.com/butterproject/butter-desktop/issues/434#issuecomment-248829694

The easiest way seems to be via Apple HLS. But m3u8 requires conversion of the video data into 30-second "mpeg2ts" transport stream containers. Those containers CAN hold h264 video, but the actual problem is MAKING those containers LIVE. You can read Apple's docs here: https://developer.apple.com/streaming/ (especially this page https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/Introduction/Introduction.html).

The bare minimum seems to be to use Apple's Live stream segmenter tools: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW1

Those tools CAN accept "live stream" files as-they-become-available, but it still seems like a lot of work to hack this into Butter. It might be the EASIEST way, though. Because with m3u8, you can just assign the subtitle as a separate "track" in the .m3u8 playlist file itsef.

Another way would be to somehow write a tool which rewrites the video container live and converts it to an mp4 with an internal subtitle track (that's what apps like Airflow do). But that would require someone to engineer a universal container-decoder (ie supporting .mp4, .avi, .mkv, etc), and having a subtitle-embedder, and then a container-creator (to final .mp4). And to do that LIVE. It would be very difficult, to say the least! For that reason, I think the best chance we have is to feed the data to Apple's "live stream segmenter tools" (links above) and letting IT take care of chunking the data into mpeg2ts... But again that's yet another huge project. So unless we get massive amounts of resources and developers with AppleTVs to help us out, I do not think we will see subtitle support in the AirPlay streamer.

Edit: One of the easiest ways may be to bundle the ffmpeg binary, and to do this:

  1. Make ffmpeg connect to the webtorrent live stream server.
  2. Make ffmpeg generate the .m3u8 and mpeg2ts chunks and make IT do the work of embedding the subtitle in that stream.
  3. Tell the AppleTV to connect to ffmpeg.

I really don't know if that's feasible. For instance, how would we know when the AppleTV wants to scrub in the media, when it's being proxied and converted by the intermediary ffmpeg step?. But it's worth researching. Maybe ffmpeg already supports scrubbing and passing those commands on to the source (Butter) that it's reading the data from.

Well, that's it from me. Now I've braindumped all the info I know about all 4 remaining AirPlay issues. ;-) I need to go. Very busy with real life.

xaiki commented 7 years ago

@vankasteelj the notes about rewrapping are good to keep in mind in the streamer rewrite.

xaiki commented 7 years ago

it seems things are currently broken because we resolve to an ipv6 address and that's why we were forcing the ipv4, comments @team-pct ?

team-pct commented 7 years ago

@SteveJobzniak I have changed the Generic and going on your OPTION 1 I will look at scrubbing if you want me to ( Loading.js need modification to include Airplay to the list of External )

ghost commented 7 years ago

@team-pct That's a very hacky way to do it and would break if someone has a future network with only IPv6 (it could happen? 💃 ).

Better to focus energy on perfecting this instead (option 3), which makes it possible to set the IP family (v4/v6) per device type: https://github.com/butterproject/butter-desktop/pull/537

ghost commented 7 years ago

@team-pct About scrubbing: Feel free to try. You need to do the following:

  1. Figure out which scrubbing events Butter requires a device to support. This wasn't clear in the last discussion.
  2. Use "airplay-protocol"'s event support (look at the library webpage by @watson) and try to see if it sends periodic "current playback position" feedback. If so, store that position value somewhere. I think the AppleTV does send that info, it would be weird if it didn't, since the device uses a reverse-HTTP server to send status updates to you (the AirPlay host) constantly. Don't give up on this option too easily. Research it in depth, because if the device sends playback status then we need to be aware of that (and update the media player's paused/playing/stopped/playback slider based on that info).
  3. If the device doesn't send position, you will have to call the airplay-protocol's "scrub()" function manually (that's in the underlying library, NOT my wrapper), whenever you need to do something related to a relative position. That will give you a callback that receives the current position when the device responds. Then you can use that value to calculate the next timestamp to jump to and call _scrub() to tell the device to perform the actual scrubbing.
  4. In airplay.js' relative scrubbing functions (+/- 30 seconds), you need to use the latest-seen playback position from the AppleTV (via method 2 above if possible, otherwise use step 3's method), and then calculate the new position (+30 or -30) in seconds, then cap it to 0 (if < 0) or the media's max time in seconds (if too far into the future).
  5. Then send the actual _scrub() command I added to airplay.js, with the offset you want to arrive at.

Look at the other device libraries for ideas how to do "relative percentage seek" etc... All of those things sound like they would take a few hours of careful coding and that's why I had no time. But you or someone else is welcome to take a look at it. Just be sure to do it cleanly and properly. No hacks. Hacks suck and lead to bugs. ;-)

Try to base the scrubbing code as closely as possible on dlna.js, because it has the most modern/cleanest code.

xaiki commented 7 years ago

@SteveJobzniak can you confirm this is working now on your AppleTV4 ?

ghost commented 7 years ago

@xaiki I will setup a build environment in a VM soon and try it out, to see if it gets me IPv4 URLs. From the code I already read, it seems fine. I'm busy with another project this evening and need to finish it first but I'll get this checked as soon as possible, probably today!

By the way, re-open this ticket. It was auto-closed by your "Fixes: #528" in the pull request. There's more in this ticket than just the IPv4 stuff. :)

Lastly: Check this out: https://github.com/butterproject/butter-desktop/pull/537#issuecomment-267685850

ghost commented 7 years ago

@xaiki I have confirmed that your IP-family code is working perfectly. Thank you for the excellent work. I only tested the code and its IP selection correctness, not streaming. It's late at night and my AppleTV is off.

That's 2 MAJOR things fixed:

  1. Swapped airplay.js to better libraries that actually find all generations of AppleTV and can talk perfect AirPlay and Bonjour mDNS languages. Thanks me, and especially @watson for coding the libraries. ;)
  2. Fixed the IP selection routines to always give an IPv4 streaming URL to the AppleTV, so that we no longer have to disable IPv6 on the network to be able to get AppleTV-compatible URLs. Thanks xaiki!

That means there are only 3 issues remaining, and only 1 of them is super serious (the inability to connect to the WebTorrent server to receive the streaming).

Later today I will try actually streaming to the AppleTV to see if it's able to connect now. It's been a few months since last time I tried (and that time I disabled IPv6 to force Butter to give device an IPv4 streaming URL and it still failed, so we'll see what happens this time, perhaps WebTorrent server libraries have been updated in the meantime and work now).

xaiki commented 7 years ago

i believe the webtorrent thing was a IP thing ? (could it be ?) anyway, you are very good at investigating, do you want to take that on and find what's happening (you can use webtorrent cmdline and your code to make some testing).

cheers.

ghost commented 7 years ago

The cause of the final remaining bugs has been found!

@xaiki Alright, the research is complete. It took a while to detect what triggered the error, but I finally found the reason for the AppleTV "black screen and aborts connection" issue.

It's caused by the fact that AppleTVs (all generations) only support 4 different containers:

Trying to play .MKV or .AVI files leads to the AppleTV connecting to our AirPlay source, seeing an unknown format (MKV), and disconnecting again.

Furthermore, the list of supported codecs within a container is very narrow. Scroll down to the bottom here for a list: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html.

Briefly summarized:

As for audio, they only accept HE-AAC, AAC-LC or MP3. They also have AC-3 passthrough (to a digital receiver) but no decoding natively in the AppleTV.

These Apple devices are a pain in the behind to deal with! They're IPv4-only, special containers only, and special codecs only! I guess they were only meant to be Netflix + iTunes Store consumer devices.

Here is the state of the world of AirPlay in Butter and WebTorrent:

I am also tagging @watson (AirPlay & Bonjour library author) and @feross (WebTorrent lead developer) since this issue applies to WebTorrent too. Butter relies on WebTorrent for its streaming server.

Implementation ideas:

So in short: The AirPlay protocol and Bonjour protocol are perfect. What is required is a media translation layer between the computer and the AppleTV to allow subtitles and unsupported containers and codecs to be transcoded into Apple HLS / MPEG2TS / H264+AAC on-the-fly.

That's going to need to be solved in WebTorrent, since that's the streaming server used in Butter.

It shouldn't be too hard to do. I've seen other applications (like Airflow) that use FFmpeg as a HLS transcoder proxy inbetween the AppleTV and the source media file, so it's definitely possible to let FFmpeg proxy the media in HLS mode.

I've got a huge task in real life which needs my full attention for several months, so I need to leave now, but I am happy to have helped us come this far. We're (Butter and WebTorrent) pretty much at the finish-line, with perfect AirPlay protocol and Bonjour/mDNS support, and now we're just lacking a HLS transcoding layer. And I am sure someone will feel like coding one someday. Maybe someone already did. There are tons of packages tagged HLS, but a brief skimming suggests that most are downloaders: https://www.npmjs.com/search?q=hls.

Well, that's it. We know exactly how to solve all remaining issues. All that's left now is for someone to have the energy to do it. Sounds like a fun task for those with lots of free time. I'm out, I need to go now to focus on a hugely important thing in real life which cannot wait any longer. ;-)

Take care and Merry Christmas everyone! Have a great new year! 👯 🎉