Skip to content

Commit

Permalink
It's working!
Browse files Browse the repository at this point in the history
  • Loading branch information
espes committed Jan 17, 2015
1 parent ffaf34c commit 4cfdead
Show file tree
Hide file tree
Showing 9 changed files with 2,366 additions and 257 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Slave in the Magic Mirror
=========================

What?
-----

In short: Apple has a thing that lets you show what's on your iPhone or iPad or Mac on your Apple TV. This lets you see it on your Linux or Mac computer or media center too, maybe.

This is an open-source implementation of [Apple AirPlay Mirroring](https://en.wikipedia.org/wiki/AirPlay#AirPlay_Mirroring)

AirPlay Mirroring uses a funky mish-mash of standards wrapped in some DRM. The audio and video data is packed into a standard media container and handed to VLC. The DRM is handled by calling into the original AppleTV server binary using a pure-python ARM interpreter.

It's not exactly production-ready, but try it out!

How?
----

You need:

- [VLC](https://www.videolan.org/vlc/)
- [PyPy](http://pypy.org/) with [pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29)
- A copy of the `airtunesd` binary from AppleTV firmware for AppleTV2,1 build 9A334v. Put it in this directory.

Then:

```
pypy -m pip install construct biplist zeroconf cryptography
pypy airplay.py
```

![screenshot](https://i.imgur.com/w5hEgsT.png)


Known Issues
------------

- Audio doesn't work
- The audio is packed into the MPEG-TS stream (mostly?) to-spec. The problem is no player correctly supports AAC-ELD and LATM...
- Rotating the device and launching some games crashes VLC
- lol
- I've only tested it with iOS 7.1.2


Code Overview
-------------

`airplay.py` - Main implementation of the AirPlay protocol

`arm/` - Simple ARMv7 interpreter based on [arm-js](https://github.com/ozaki-r/arm-js)

`drm.py` - Implementation of FairPlay SAP by calling into airtunesd

`loader.py` `dyld_info.py` - Mach-O loader and minimal HLE for iOS binaries

`aac.py` `mp4.py` `mpegts.py` - Implementations of bits of ISO/IEC 14496 Part 3, 10 and ISO/IEC 13818 Part 1. Enough to dump the AirPlay packets into a useful container.
287 changes: 287 additions & 0 deletions aac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
# aac.py
#
# Copyright 2015, espes
#
# Licensed under GPL Version 2 or later
#

from construct import *
from construct_utils import *

# ISO/IEC 14496-3:2009
# Coding of audio-visual objects - Part 3: Audio

# 4.4.1
def GASpecificConfig(channelConfiguration,
audioObjectType):
return Struct("GASpecificConfig",
Flag("frameLengthFlag"),
Flag("dependsOnCoreCoder"),
If(this.dependsOnCoreCoder, Bits("coreCoderDelay", 14)),
Flag("extensionFlag"),
If(lambda ctx: channelConfiguration(ctx) == 0, Bork("program_config_element")),
If(lambda ctx: audioObjectType(ctx) in (6, 20), Bork("layerNr")),
# P("K"),
# If(this.extensionFlag, Bork("extension")),
# If(lambda ctx: audioObjectType(ctx._) in (17, 19, 20, 23), Bork("aacStuff")),
Flag("extensionFlag3"),
If(this.extensionFlag3, Bork("extensionFlag3")))

# 4.6.20.3
ELDEXT_TERM = 0
ELDSpecificConfig = Struct("ELDSpecificConfig",
Flag("frameLengthFlag"),
Flag("aacSectionDataResilienceFlag"),
Flag("aacScalefactorDataResilienceFlag"),
Flag("aacSpectralDataResilienceFlag"),

Flag("ldSbrPresentFlag"),
If(this.ldSbrPresentFlag,
Struct("ld",
Bits("ldSbrSamplingRate", 1),
Bits("ldSbrCrcFlag", 1),
Bork("ld_sbr_header"))),

RepeatUntil(lambda obj, ctx: obj.eldExtType == ELDEXT_TERM,
Struct("eldext",
Bits("eldExtType", 4),
If(lambda ctx: ctx.eldExtType != ELDEXT_TERM,
Bork("eldExt"))
# ...
)))

# 1.6.2.1
AudioObjectType = ExprAdapter(
Struct("audioObjectType",
Bits("audioObjectType", 5),
If(lambda ctx: ctx.audioObjectType == 31,
Bits("audioObjectTypeExt", 6))),
decoder = lambda obj, ctx: (obj.audioObjectType if obj.audioObjectType < 31
else 32+obj.audioObjectTypeExt),
encoder = lambda obj, ctx: Container(
audioObjectType = 31 if obj >= 31 else obj,
audioObjectTypeExt=obj - 32 if obj >= 32 else None)
)

# 1.6.3.4

frequencyIndex = [
97000,
88200,
64000,
48000,
44100,
32000,
24000,
22050,
16000,
12000,
11025,
8000,
7350
]
SamplingFrequency = ExprAdapter(
Struct("samplingFrequency",
Bits("samplingFrequencyIndex", 4),
# P("sampling"),
If(lambda ctx: ctx.samplingFrequencyIndex == 0xf,
Bits("samplingFrequency", 24))),
decoder = lambda obj, ctx: obj.samplingFrequency or frequencyIndex[obj.samplingFrequencyIndex],
encoder = lambda obj, ctx: Container(
samplingFrequencyIndex=frequencyIndex.index(obj) if obj in frequencyIndex else 0xf,
samplingFrequency=obj)
)

AudioSpecificConfig = Struct("AudioSpecificConfig",
AudioObjectType,
SamplingFrequency,
Bits("channelConfiguration", 4),
If(lambda ctx: ctx.audioObjectType in (5, 29),
Struct("extensionConfig",
Rename("extensionSamplingFrequency", SamplingFrequency),
Rename("extensionAudioObjectType", AudioObjectType),
If(lambda ctx: ctx.extensionAudioObjectType == 22,
Bits("extensionChannelConfiguration", 4)))),

# P("wut"),

Switch("config", this.audioObjectType, {
2: GASpecificConfig(this._.channelConfiguration, this._.audioObjectType), # AAC-LC
39: ELDSpecificConfig, # AAC-ELD
# TODO
}, default=Bork("unimplemented audioObjectType")),

If(lambda ctx: ctx.audioObjectType in (17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39),
Struct("ep",
Bits("epConfig", 2),
If(lambda ctx: ctx.epConfig in (2, 3), Bork("epConfig")))),
# TODO
)

AudioSpecificConfig_bytes = BitStruct("AudioSpecificConfig",
Embed(AudioSpecificConfig),
ByteAlign())


# 1.7.3

# This stuff is too messed up for construct. Really should be done manually...

StreamMuxConfig = Struct("cfg",
Bit("audioMuxVersion"),
If(this.audioMuxVersion,
Flag("audioMuxVersionA")),
If(this.audioMuxVersionA, Bork("audioMuxVersionA")),

If(this.audioMuxVersion, Bork("taraBufferFullnexx")),
Flag("allStreamsSameTimeFraming"),
Bits("numSubFrames", 6),
Bits("numProgram", 4),

If(lambda ctx: ctx.numProgram > 0, Bork("numProgram")),
Bits("numLayer", 3),
If(lambda ctx: ctx.numLayer > 0, Bork("numLayer")),
Struct("layer",
# useSameConfig
# if not useSameConfig
If(lambda ctx: ctx._.audioMuxVersion == 0,
AudioSpecificConfig),

Bits("frameLengthType", 3),
Embed(IfThenElse(None,
lambda ctx: ctx.frameLengthType == 0,
Struct(None,
Bits("latmBufferFullness", 8),
# TODO: coreFrameOffset is object type stuff
),
Bork("frameLengthType")
))
),

Flag("otherDataPresent"),
If(this.otherDataPresent, Bork("otherData")),
Flag("crcCheckPresent"),
If(this.crcCheckPresent, Bork("crcCheck")),

# Probe("SM", show_stream=False, show_stack=False)
)

AudioMuxElement_1 = BitStruct("AudioMuxElement",
Flag("useSameStreamMux"),
If(lambda ctx: not ctx.useSameStreamMux,
StreamMuxConfig),

# If(lambda ctx: ctx.cfg.numSubFrames != 0, Bork("numSubFrames")),

# PayloadLengthInfo
# If(lambda ctx: not ctx.cfg.allStreamsSameTimeFraming, Bork()),
# If(lambda ctx: ctx.cfg.layer.frameLengthType != 0, Bork()),
ExprAdapter(
RepeatUntil(lambda obj, ctx: obj != 255,
Bits("MuxSlotLengthBytes", 8)),
decoder = lambda obj, ctx: sum(obj),
encoder = lambda obj, ctx: [255] * (obj // 255) + [obj % 255]),
# PayloadMux
Array(this.MuxSlotLengthBytes, Bits('payload', 8)),

ByteAlign()
)

# 1.7.2

AudioSyncStream = StructWithLengthAdapter("AudioSyncStream",
StructLengthAdapter(
EmbeddedBitStruct(
Const(Bits("syncword", 11), 0x2b7),
Bits("audioMuxLengthBytes", 13)),
decoder = lambda obj, ctx: obj.audioMuxLengthBytes,
encoder = lambda length, obj, ctx: container_add(obj, audioMuxLengthBytes=length)
),
Embed(AudioMuxElement_1),
Terminator
)


def latm_mux(cfg, frames):
first_mux = DefaultingContainer(
useSameStreamMux = False,
cfg = cfg,
MuxSlotLengthBytes = len(frames[0]),
payload = map(ord, frames[0]))

r = [AudioSyncStream.build(first_mux)]

for f in frames[1:]:
mux = DefaultingContainer(
useSameStreamMux = True,
MuxSlotLengthBytes = len(f),
payload = map(ord, f))
r.append(AudioSyncStream.build(mux))

return r

def latm_mux_aac_lc(channels, sample_rate, frame_duration, frames):
assert frame_duration in (1024, 960)
assert len(frames) >= 1

cfg = Container(
audioMuxVersion = 0,
audioMuxVersionA = None,
allStreamsSameTimeFraming = True,
numSubFrames = 0,
numProgram = 0,
numLayer = 0,
layer = Container(
AudioSpecificConfig = Container(
audioObjectType = 2, # AAC-LC
samplingFrequency = sample_rate,
channelConfiguration = channels,
extensionConfig = None,
config = Container(
frameLengthFlag = frame_duration == 960,
dependsOnCoreCoder = False,
coreCoderDelay = None,
extensionFlag = True,
extensionFlag3 = False),
ep = Container(epConfig = 0)),
frameLengthType = 0,
latmBufferFullness = 0xff), #??
otherDataPresent = False,
crcCheckPresent = False)

return latm_mux(cfg, frames)


def latm_mux_aac_eld(channels, sample_rate, frame_duration, frames):
assert frame_duration in (512, 480)
assert len(frames) >= 1

cfg = Container(
audioMuxVersion = 0,
audioMuxVersionA = None,
allStreamsSameTimeFraming = True,
numSubFrames = 0,
numProgram = 0,
numLayer = 0,
layer = Container(
AudioSpecificConfig = Container(
audioObjectType = 39, # AAC-ELD
samplingFrequency = sample_rate,
channelConfiguration = channels,
extensionConfig = None,
config = Container(
frameLengthFlag = frame_duration == 480,
aacSectionDataResilienceFlag = False,
aacScalefactorDataResilienceFlag = False,
aacSpectralDataResilienceFlag = False,
ldSbrPresentFlag = False,
ld = None,
eldext = [Container(eldExtType=0)]),
ep = Container(epConfig = 0)),
frameLengthType = 0,
latmBufferFullness = 0xff), #??
otherDataPresent = False,
crcCheckPresent = False)

return latm_mux(cfg, frames)

Loading

5 comments on commit 4cfdead

@Memphiz
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grats - sounds promising. I wonder why your clients don't timeout during handshake phase. Calcing stage0 and stage1 of the handshake needs 10 secs each during my tests - ios6 and ios8 was not willing to wait that long most of the time

@espes
Copy link
Owner Author

@espes espes commented on 4cfdead Jan 19, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It takes about 5 seconds for me. Is this with PyPy?

@Memphiz
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope stock python ... will give pypy a try :)

@Memphiz
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok pypy is way faster. I still didn't get it to work with either ios6 or ios8 client. Basically the airtunes parsing of all the params just has to much ios7 specific assumptions but i think i might be able to adapt for this (or provide a wireshark dump for you in case i fail). Also if you could make the videostream available as http url you could make kodi play the url via jsonrpc ;) - well the fact that i don't have any ios7 client available motivates me for sure - if it wasn't the lack of time.

Also thinking a bit of the future. Do you think it would be possible to natively execute the airtunesd code on arm platforms? (i think the python emulator would be to slow on those maybe) Is some sort of asm passthrough possible?

@espes
Copy link
Owner Author

@espes espes commented on 4cfdead Mar 21, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be possible to natively execute the airtunesd code on arm platforms? (i think the python emulator would be to slow on those maybe) Is some sort of asm passthrough possible?

It is done: 5a80c28 :P

Please sign in to comment.