-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
2,366 additions
and
257 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
Oops, something went wrong.
4cfdead
There was a problem hiding this comment.
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
4cfdead
There was a problem hiding this comment.
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?
4cfdead
There was a problem hiding this comment.
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 :)
4cfdead
There was a problem hiding this comment.
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?
4cfdead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is done: 5a80c28 :P