├── .gitattributes ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── AirPlay.css ├── book.toml ├── ci └── build.sh ├── css ├── examples.css └── table.css ├── features.js ├── img └── bullet.gif ├── index.html ├── js ├── .idea │ ├── .gitignore │ ├── .name │ ├── ClojureProjectResolveSettings.xml │ ├── compiler.xml │ ├── encodings.xml │ ├── libraries │ │ ├── Leiningen__adzerk_boot_cljs_2_1_5.xml │ │ ├── Leiningen__adzerk_boot_cljs_repl_0_4_0.xml │ │ ├── Leiningen__adzerk_boot_reload_0_6_0.xml │ │ ├── Leiningen__args4j_2_0_26.xml │ │ ├── Leiningen__bendyorke_boot_postcss_0_0_1.xml │ │ ├── Leiningen__bidi_2_1_6.xml │ │ ├── Leiningen__binaryage_devtools_0_9_11.xml │ │ ├── Leiningen__binaryage_env_config_0_2_2.xml │ │ ├── Leiningen__boot_deps_0_1_9.xml │ │ ├── Leiningen__camel_snake_kebab_0_4_1.xml │ │ ├── Leiningen__cheshire_5_7_1.xml │ │ ├── Leiningen__cider_piggieback_0_3_9.xml │ │ ├── Leiningen__cljs_ajax_0_8_0.xml │ │ ├── Leiningen__cljsjs_boot_cljsjs_0_10_4.xml │ │ ├── Leiningen__cljsjs_classnames_2_2_5_1.xml │ │ ├── Leiningen__cljsjs_create_react_class_15_6_3_1.xml │ │ ├── Leiningen__cljsjs_date_fns_1_29_0_0.xml │ │ ├── Leiningen__cljsjs_marked_0_3_5_1.xml │ │ ├── Leiningen__cljsjs_moment_2_22_0_0.xml │ │ ├── Leiningen__cljsjs_popperjs_1_14_3_1.xml │ │ ├── Leiningen__cljsjs_prop_types_15_6_1_0.xml │ │ ├── Leiningen__cljsjs_react_16_9_0_1.xml │ │ ├── Leiningen__cljsjs_react_bootstrap_1_0_0_beta_14_0.xml │ │ ├── Leiningen__cljsjs_react_datepicker_2_3_0_0.xml │ │ ├── Leiningen__cljsjs_react_dom_16_9_0_1.xml │ │ ├── Leiningen__cljsjs_react_dom_server_16_9_0_1.xml │ │ ├── Leiningen__cljsjs_react_onclickoutside_6_7_1_1.xml │ │ ├── Leiningen__cljsjs_react_popper_1_0_2_0.xml │ │ ├── Leiningen__clojure_complete_0_2_5.xml │ │ ├── Leiningen__com_cognitect_transit_clj_0_8_309.xml │ │ ├── Leiningen__com_cognitect_transit_cljs_0_8_256.xml │ │ ├── Leiningen__com_cognitect_transit_java_0_8_332.xml │ │ ├── Leiningen__com_cognitect_transit_js_0_8_846.xml │ │ ├── Leiningen__com_fasterxml_jackson_core_jackson_core_2_8_6.xml │ │ ├── Leiningen__com_fasterxml_jackson_dataformat_jackson_dataformat_cbor_2_8_6.xml │ │ ├── Leiningen__com_fasterxml_jackson_dataformat_jackson_dataformat_smile_2_8_6.xml │ │ ├── Leiningen__com_google_code_findbugs_jsr305_3_0_1.xml │ │ ├── Leiningen__com_google_code_gson_gson_2_7.xml │ │ ├── Leiningen__com_google_errorprone_error_prone_annotations_2_0_18.xml │ │ ├── Leiningen__com_google_guava_guava_25_1_jre.xml │ │ ├── Leiningen__com_google_j2objc_j2objc_annotations_1_1.xml │ │ ├── Leiningen__com_google_javascript_closure_compiler_externs_v20180805.xml │ │ ├── Leiningen__com_google_javascript_closure_compiler_unshaded_v20180805.xml │ │ ├── Leiningen__com_google_jsinterop_jsinterop_annotations_1_0_0.xml │ │ ├── Leiningen__com_google_protobuf_protobuf_java_3_0_2.xml │ │ ├── Leiningen__com_googlecode_json_simple_json_simple_1_1_1.xml │ │ ├── Leiningen__commons_codec_1_10.xml │ │ ├── Leiningen__commons_logging_1_2.xml │ │ ├── Leiningen__compassus_1_0_0_alpha3.xml │ │ ├── Leiningen__devcards_0_2_6.xml │ │ ├── Leiningen__griff_boot2nix_1_1_0.xml │ │ ├── Leiningen__http_kit_2_1_18.xml │ │ ├── Leiningen__kibu_pushy_0_3_8.xml │ │ ├── Leiningen__net_cgrand_macrovich_0_2_1.xml │ │ ├── Leiningen__nrepl_0_4_5.xml │ │ ├── Leiningen__nrepl_bencode_1_0_0.xml │ │ ├── Leiningen__onetom_boot_lein_generate_0_1_3.xml │ │ ├── Leiningen__org_apache_httpcomponents_httpasyncclient_4_1_3.xml │ │ ├── Leiningen__org_apache_httpcomponents_httpclient_4_5_3.xml │ │ ├── Leiningen__org_apache_httpcomponents_httpcore_4_4_6.xml │ │ ├── Leiningen__org_apache_httpcomponents_httpcore_nio_4_4_6.xml │ │ ├── Leiningen__org_checkerframework_checker_qual_2_0_0.xml │ │ ├── Leiningen__org_clojure_clojure_1_10_1.xml │ │ ├── Leiningen__org_clojure_clojurescript_1_10_597.xml │ │ ├── Leiningen__org_clojure_core_async_0_6_532.xml │ │ ├── Leiningen__org_clojure_core_cache_0_8_2.xml │ │ ├── Leiningen__org_clojure_core_memoize_0_8_2.xml │ │ ├── Leiningen__org_clojure_core_specs_alpha_0_2_44.xml │ │ ├── Leiningen__org_clojure_data_codec_0_1_0.xml │ │ ├── Leiningen__org_clojure_data_json_0_2_6.xml │ │ ├── Leiningen__org_clojure_data_priority_map_0_0_7.xml │ │ ├── Leiningen__org_clojure_data_xml_0_2_0_alpha6.xml │ │ ├── Leiningen__org_clojure_google_closure_library_0_0_20170809_b9c14c6b.xml │ │ ├── Leiningen__org_clojure_google_closure_library_third_party_0_0_20170809_b9c14c6b.xml │ │ ├── Leiningen__org_clojure_spec_alpha_0_2_176.xml │ │ ├── Leiningen__org_clojure_tools_analyzer_0_7_0.xml │ │ ├── Leiningen__org_clojure_tools_analyzer_jvm_0_7_3.xml │ │ ├── Leiningen__org_clojure_tools_logging_0_4_1.xml │ │ ├── Leiningen__org_clojure_tools_reader_1_3_2.xml │ │ ├── Leiningen__org_codehaus_mojo_animal_sniffer_annotations_1_14.xml │ │ ├── Leiningen__org_javassist_javassist_3_18_1_GA.xml │ │ ├── Leiningen__org_mozilla_rhino_1_7R5.xml │ │ ├── Leiningen__org_msgpack_msgpack_0_6_12.xml │ │ ├── Leiningen__org_omcljs_om_1_0_0_beta2.xml │ │ ├── Leiningen__org_ow2_asm_asm_5_2.xml │ │ ├── Leiningen__pandeiro_boot_http_0_8_3.xml │ │ ├── Leiningen__prismatic_dommy_1_1_0.xml │ │ ├── Leiningen__prismatic_schema_1_1_7.xml │ │ ├── Leiningen__re_frame_0_11_0_rc3.xml │ │ ├── Leiningen__reagent_0_9_0_rc4.xml │ │ ├── Leiningen__sablono_0_8_6.xml │ │ ├── Leiningen__tigris_0_1_1.xml │ │ └── Leiningen__weasel_0_7_0.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── airplay-spec-js.iml ├── boot.properties ├── build.boot ├── html │ ├── cards.html │ └── index.html ├── libs │ └── .git-keep ├── project.clj ├── resources │ ├── app-repl.cljs.edn │ ├── app.cljs.edn │ ├── cards-repl.cljs.edn │ └── cards.cljs.edn └── src │ ├── airplay │ ├── bits.cljs │ ├── examples.cljs │ ├── examples │ │ └── data.cljs │ ├── features.cljs │ ├── main.cljs │ ├── math.cljs │ ├── plist.cljs │ ├── state.cljs │ └── ui │ │ ├── airplay_service.cljs │ │ ├── core.cljs │ │ ├── examples.cljs │ │ ├── features.cljs │ │ └── flags.cljs │ └── cards │ └── ui.cljs ├── long.js ├── nix ├── sources.json └── sources.nix ├── shell.nix ├── src ├── SUMMARY.md ├── audio │ ├── README.md │ ├── airport_express_authentication.md │ ├── info.md │ ├── metadata.md │ ├── remote_control.md │ ├── rtp_streams.md │ ├── rtsp_requests │ │ ├── README.md │ │ ├── announce.md │ │ ├── flush.md │ │ ├── flushbuffered.md │ │ ├── get_info.md │ │ ├── options.md │ │ ├── post_audio_mode.md │ │ ├── post_auth_setup.md │ │ ├── post_command.md │ │ ├── post_feedback.md │ │ ├── post_fp_setup.md │ │ ├── post_pair_setup.md │ │ ├── post_pair_verify.md │ │ ├── record.md │ │ ├── setpeers.md │ │ ├── setup.md │ │ └── teardown.md │ └── volume_control.md ├── contributing.md ├── data │ ├── RTSP-get-info-req.bin │ └── examples │ │ ├── AirServer │ │ ├── info.plist │ │ ├── info.plist.base64 │ │ └── info.plist.xml │ │ ├── AppleTV-4K │ │ ├── info.plist │ │ ├── info.plist.base64 │ │ └── info.plist.xml │ │ └── Denon-AVR-X3500H │ │ ├── info.plist │ │ ├── info.plist.base64 │ │ └── info.plist.xml ├── features.md ├── history.md ├── introduction.md ├── known_implementations.md ├── pairing │ ├── README.md │ ├── hkp.md │ └── legacy.md ├── photos │ ├── README.md │ ├── events.md │ ├── http_requests.md │ ├── photo_caching.md │ └── slideshows.md ├── resources.md ├── screen_mirroring │ ├── README.md │ ├── http_requests.md │ ├── stream_packets.md │ └── time_synchronization.md ├── service_discovery.md ├── status_flags.md └── video │ ├── README.md │ ├── events.md │ └── http_requests.md └── theme ├── book.js ├── css ├── chrome.css ├── general.css ├── print.css └── variables.css ├── favicon.png ├── highlight.css ├── highlight.js └── index.hbs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bin binary 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | pages: 9 | name: GitHub Pages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - uses: cachix/install-nix-action@v6 14 | - name: Install Rust (rustup) 15 | run: rustup update stable --no-self-update && rustup default stable 16 | - name: Build book 17 | run: nix-shell --run ci/build.sh 18 | - name: Deploy to GitHub 19 | env: 20 | GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }} 21 | run: | 22 | touch book/.nojekyll 23 | curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | nix-shell --run 'rustc - -o /tmp/deploy' 24 | cd book 25 | /tmp/deploy 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | /js/release 3 | /js/target 4 | /js/.nrepl-port 5 | -------------------------------------------------------------------------------- /AirPlay.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Segoe UI", "Lucida Grande", "Trebuchet MS", Arial, "lucida sans unicode", sans-serif; 3 | color: #333; 4 | background: #ccc none; 5 | padding: 0px 30px; 6 | } 7 | 8 | a { 9 | color: #686868; 10 | } 11 | 12 | ul.toc { 13 | margin-left: 4em; 14 | margin-right: 0px; 15 | padding-left: 0px; 16 | margin-bottom: 0px; 17 | } 18 | 19 | ul.toc ul { 20 | margin-left: 1em; 21 | margin-right: 0px; 22 | padding-left: 0px; 23 | margin-bottom: 0px; 24 | } 25 | 26 | ul.toc li { 27 | background: none; 28 | padding-left: 0px; 29 | list-style: none; 30 | } 31 | 32 | ul.toc li a { 33 | text-decoration: none; 34 | } 35 | 36 | a:hover { 37 | color: #09598a; 38 | } 39 | 40 | h1 { 41 | color: #09598a; 42 | margin-top: 50px; 43 | margin-bottom: 20px; 44 | border-top: 2px solid #09598a; 45 | border-bottom: 2px solid #09598a; 46 | padding: 10px 20px; 47 | background-color: #eff8fd; 48 | } 49 | 50 | h1.title { 51 | color: #424242; 52 | text-shadow: 0px 1px 3px #888; 53 | text-align: center; 54 | margin-top: 0px; 55 | margin-bottom: 20px; 56 | padding-bottom: 10px; 57 | border-bottom: 4px solid #09598a; 58 | border-top: 4px solid #09598a; 59 | } 60 | 61 | h2 { 62 | color: #09598a; 63 | margin-top: 25px; 64 | margin-bottom: 15px; 65 | border-bottom: 1px solid #09598a; 66 | } 67 | 68 | h3 { 69 | color: #09598A; 70 | margin-top: 10px; 71 | } 72 | 73 | em { 74 | color: #09598a; 75 | font-style: normal; 76 | font-weight: bold; 77 | } 78 | 79 | p { 80 | margin-top: 3px; 81 | margin-bottom: 15px; 82 | text-align: justify; 83 | } 84 | 85 | code { 86 | font-weight: bold; 87 | } 88 | 89 | pre { 90 | background-color: #eff8fd; 91 | border: 1px dashed #aaa; 92 | border-top: none; 93 | padding: 5px; 94 | margin: 0px 10px 30px 30px; 95 | width: 650px; 96 | overflow: auto; 97 | font-weight: bold; 98 | } 99 | 100 | p.caption { 101 | background-color: #ccccff; 102 | border: 1px dashed #aaa; 103 | border-bottom: none; 104 | padding: 5px; 105 | margin: 10px 10px 0px 30px; 106 | width: 650px; 107 | overflow: auto; 108 | font-size: 80%; 109 | font-variant: small-caps; 110 | font-weight: bold; 111 | } 112 | 113 | p.cli_srv { 114 | background-color: #ffcccc; 115 | border: 1px dashed #aaa; 116 | border-bottom: none; 117 | padding: 5px; 118 | margin: 10px 10px 0px 30px; 119 | width: 650px; 120 | overflow: auto; 121 | font-size: 80%; 122 | font-variant: small-caps; 123 | font-weight: bold; 124 | } 125 | 126 | p.srv_cli { 127 | background-color: #ccffcc; 128 | border: 1px dashed #aaa; 129 | border-bottom: none; 130 | padding: 5px; 131 | margin: 10px 10px 0px 30px; 132 | width: 650px; 133 | overflow: auto; 134 | font-size: 80%; 135 | font-variant: small-caps; 136 | font-weight: bold; 137 | } 138 | 139 | .ex { 140 | text-decoration: underline; 141 | font-weight: bold; 142 | } 143 | 144 | ul { 145 | margin-left: 1em; 146 | margin-right: 2em; 147 | padding-left: 0.2em; 148 | margin-bottom: 1em; 149 | } 150 | 151 | ul li { 152 | background: url(img/bullet.gif) 0em 0.5em no-repeat; 153 | padding-left: 0.8em; 154 | list-style: none; 155 | } 156 | 157 | table { 158 | border-collapse: separate; 159 | margin: 10px 10px 30px 30px; 160 | width: 650px; 161 | } 162 | 163 | table th { 164 | color: #4381a7; 165 | font-variant: small-caps; 166 | font-size: 12pt; 167 | height: 30px; 168 | background: #c8e3f9; 169 | } 170 | 171 | table td, table th { 172 | padding: 5px 5px; 173 | text-align: left; 174 | } 175 | 176 | table tr { 177 | background-color: #f1f8fd; 178 | } 179 | 180 | table tr:hover { 181 | background-color: #e4f2fc; 182 | } 183 | 184 | table tr.selected { 185 | background-color: #75bdf0; 186 | } 187 | 188 | table td { 189 | font-size: 9pt; 190 | } 191 | 192 | #content { 193 | padding: 20px; 194 | border: 1px solid #999; 195 | background: #fff none; 196 | color: #000; 197 | width: 800px; 198 | margin: auto; 199 | } 200 | 201 | #footer { 202 | font-size: 80%; 203 | border-top: 3px solid #09598a; 204 | padding-top: 10px; 205 | } 206 | 207 | .noscript { 208 | display: none; 209 | } 210 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Brian Olsen"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Unofficial AirPlay Specification" 7 | 8 | [output.html] 9 | additional-js = [] 10 | additional-css = ["css/table.css", "css/examples.css"] 11 | -------------------------------------------------------------------------------- /ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd js 4 | boot release 5 | cd - 6 | mdbook build 7 | mkdir book/js 8 | cp js/release/app.js* book/js 9 | -------------------------------------------------------------------------------- /css/examples.css: -------------------------------------------------------------------------------- 1 | .client_server, .server_client { 2 | border: 1px dashed #aaa; 3 | margin: 10px 10px 0px 30px; 4 | } 5 | .client_server p, .server_client p { 6 | margin: 0; 7 | padding: 5px; 8 | overflow: auto; 9 | font-size: 80%; 10 | font-variant: small-caps; 11 | font-weight: bold; 12 | } 13 | .client_server p { 14 | background-color: #ffcccc; 15 | } 16 | 17 | .server_client p { 18 | background-color: #ccffcc; 19 | } 20 | .client_server pre, .server_client pre { 21 | margin-top: 0; 22 | margin-bottom: 0; 23 | } 24 | 25 | .ex { 26 | text-decoration: underline; 27 | font-weight: bold; 28 | } 29 | -------------------------------------------------------------------------------- /css/table.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | table { 4 | border-collapse: separate; 5 | margin: 10px 10px 30px 30px; 6 | width: 650px; 7 | } 8 | */ 9 | 10 | table thead, table tbody.header { 11 | background: var(--table-header-bg); 12 | } 13 | table thead td, table tbody.header td { 14 | font-weight: 700; 15 | border: none; 16 | } 17 | table thead th, table tbody.header th { 18 | padding: 3px 20px; 19 | } 20 | table thead tr, table tbody.header tr { 21 | border: 1px var(--table-header-bg) solid; 22 | } 23 | 24 | table th { 25 | color: #4381a7; 26 | font-variant: small-caps; 27 | font-size: 12pt; 28 | height: 30px; 29 | /*background: #c8e3f9;*/ 30 | } 31 | 32 | table td, table th { 33 | /*padding: 5px 5px;*/ 34 | text-align: left; 35 | } 36 | 37 | /* 38 | table tbody.features-body tr { 39 | background-color: #f1f8fd; 40 | } 41 | */ 42 | 43 | table.bits tbody.rows tr:hover { 44 | background-color: #e4f2fc; 45 | } 46 | 47 | table.bits tbody tr.selected { 48 | background-color: #75bdf0; 49 | } 50 | 51 | table td { 52 | font-size: 9pt; 53 | } 54 | -------------------------------------------------------------------------------- /features.js: -------------------------------------------------------------------------------- 1 | function toLong(s) { 2 | var ret = s.trim(); 3 | try { 4 | if (ret.startsWith("0x")) { 5 | return Long.fromString(ret.substring(2), 16); 6 | } 7 | return Long.fromString(ret, 10); 8 | } catch (e) { 9 | return Long.ZERO; 10 | } 11 | } 12 | function setBitClassName(prefix, value, selected, notSelected) { 13 | for (var i=0; i<65; i++) { 14 | var b = Long.ONE.shiftLeft(i); 15 | var el = document.getElementById(prefix + "-bit" + i); 16 | if (el) { 17 | if (value.and(b).equals(b)) { 18 | el.className = selected; 19 | } else { 20 | el.className = notSelected; 21 | } 22 | } 23 | } 24 | } 25 | function updateFlagTable(prefix, value) { 26 | if (value.equals(Long.ZERO)) { 27 | document.getElementById(prefix + "-output").className = "noscript"; 28 | setBitClassName(prefix, Long.ZERO, "", "") 29 | document.getElementById(prefix).className = ""; 30 | } else { 31 | document.getElementById(prefix + "-output").className = ""; 32 | setBitClassName(prefix, value, "selected", "notSelected") 33 | document.getElementById(prefix).className = "selecting"; 34 | } 35 | 36 | document.getElementById(prefix + "-dec").innerHTML = value.toString(10); 37 | document.getElementById(prefix + "-hex").innerHTML = "0x" + value.toString(16); 38 | } 39 | 40 | function exampleChanged(prefix, fn) { 41 | var el = document.getElementById(prefix + "-examples"); 42 | document.getElementById(prefix + "-input").value = el.value; 43 | fn(el); 44 | } 45 | 46 | function inputChanged(prefix, fn, bit) { 47 | var el = document.getElementById(prefix + "-input"); 48 | document.getElementById(prefix + "-examples").value = ""; 49 | document.getElementById(prefix + "-interactive").className = ""; 50 | fn(el, bit); 51 | } 52 | 53 | function flipBit(prefix, fn, bit) { 54 | var el = document.getElementById(prefix + "-input"); 55 | document.getElementById(prefix + "-examples").value = ""; 56 | document.getElementById(prefix + "-interactive").className = ""; 57 | fn(el, bit) 58 | } 59 | 60 | function updateFeatures(el, bit) { 61 | var featuresS = el.value; 62 | var comma = featuresS.indexOf(","); 63 | var features = 0; 64 | if (comma >= 0) { 65 | var high = toLong(featuresS.substring(comma + 1)); 66 | var low = toLong(featuresS.substring(0, comma)); 67 | features = high.shiftLeft(32).or(low); 68 | } else { 69 | features = toLong(featuresS); 70 | } 71 | if (typeof bit != "undefined") { 72 | var b = Long.ONE.shiftLeft(bit); 73 | features = features.xor(b); 74 | el.value = "0x" + features.toString(16); 75 | } 76 | updateFlagTable("airplay-features", features); 77 | var txt = "0x" + features.and(Long.fromBits(0xffffffff, 0, true)).toString(16) + 78 | ",0x" + features.shiftRight(32).toString(16); 79 | document.getElementById("airplay-features-txt").innerHTML = txt; 80 | } 81 | 82 | function updateSystemFlags(el, bit) { 83 | var systemFlagsS = el.value; 84 | var systemFlags = 0; 85 | systemFlags = toLong(systemFlagsS); 86 | if (typeof bit != "undefined") { 87 | var b = Long.ONE.shiftLeft(bit); 88 | systemFlags = systemFlags.xor(b); 89 | el.value = "0x" + systemFlags.toString(16); 90 | } 91 | updateFlagTable("airplay-system-flags", systemFlags); 92 | } 93 | 94 | function installHandlers(prefix, fn) { 95 | var input = function() { 96 | inputChanged(prefix, fn); 97 | } 98 | var example = function() { 99 | exampleChanged(prefix, fn); 100 | } 101 | var changeBit = function(ev) { 102 | var target = ev.target; 103 | while (target != null && target.tagName != "TR") { 104 | target = target.parentElement; 105 | } 106 | if (target == null) { 107 | return; 108 | } 109 | var bit = parseInt(target.id.substring(prefix.length + "-bit".length)); 110 | inputChanged(prefix, fn, bit); 111 | } 112 | var el = document.getElementById(prefix + "-input"); 113 | el.onkeyup = input; 114 | el.onpaste = input; 115 | var el = document.getElementById(prefix + "-examples"); 116 | el.onchange = example; 117 | var el = document.getElementById(prefix + "-bit0"); 118 | el.parentNode.onclick = changeBit; 119 | input(); 120 | } 121 | installHandlers("airplay-features", updateFeatures) 122 | installHandlers("airplay-system-flags", updateSystemFlags) 123 | -------------------------------------------------------------------------------- /img/bullet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/img/bullet.gif -------------------------------------------------------------------------------- /js/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /js/.idea/.name: -------------------------------------------------------------------------------- 1 | airplay-spec -------------------------------------------------------------------------------- /js/.idea/ClojureProjectResolveSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IDE 5 | 6 | -------------------------------------------------------------------------------- /js/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /js/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__adzerk_boot_cljs_2_1_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__adzerk_boot_cljs_repl_0_4_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__adzerk_boot_reload_0_6_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__args4j_2_0_26.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__bendyorke_boot_postcss_0_0_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__bidi_2_1_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__binaryage_devtools_0_9_11.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__binaryage_env_config_0_2_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__boot_deps_0_1_9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__camel_snake_kebab_0_4_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cheshire_5_7_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cider_piggieback_0_3_9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljs_ajax_0_8_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_boot_cljsjs_0_10_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_classnames_2_2_5_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_create_react_class_15_6_3_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_date_fns_1_29_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_marked_0_3_5_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_moment_2_22_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_popperjs_1_14_3_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_prop_types_15_6_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_16_9_0_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_bootstrap_1_0_0_beta_14_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_datepicker_2_3_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_dom_16_9_0_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_dom_server_16_9_0_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_onclickoutside_6_7_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__cljsjs_react_popper_1_0_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__clojure_complete_0_2_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_cognitect_transit_clj_0_8_309.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_cognitect_transit_cljs_0_8_256.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_cognitect_transit_java_0_8_332.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_cognitect_transit_js_0_8_846.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_fasterxml_jackson_core_jackson_core_2_8_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_fasterxml_jackson_dataformat_jackson_dataformat_cbor_2_8_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_fasterxml_jackson_dataformat_jackson_dataformat_smile_2_8_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_code_findbugs_jsr305_3_0_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_code_gson_gson_2_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_errorprone_error_prone_annotations_2_0_18.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_guava_guava_25_1_jre.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_j2objc_j2objc_annotations_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_javascript_closure_compiler_externs_v20180805.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_javascript_closure_compiler_unshaded_v20180805.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_jsinterop_jsinterop_annotations_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_google_protobuf_protobuf_java_3_0_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__com_googlecode_json_simple_json_simple_1_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__commons_codec_1_10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__commons_logging_1_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__compassus_1_0_0_alpha3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__devcards_0_2_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__griff_boot2nix_1_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__http_kit_2_1_18.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__kibu_pushy_0_3_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__net_cgrand_macrovich_0_2_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__nrepl_0_4_5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__nrepl_bencode_1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__onetom_boot_lein_generate_0_1_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_apache_httpcomponents_httpasyncclient_4_1_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_apache_httpcomponents_httpclient_4_5_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_apache_httpcomponents_httpcore_4_4_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_apache_httpcomponents_httpcore_nio_4_4_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_checkerframework_checker_qual_2_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_clojure_1_10_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_clojurescript_1_10_597.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_core_async_0_6_532.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_core_cache_0_8_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_core_memoize_0_8_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_core_specs_alpha_0_2_44.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_data_codec_0_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_data_json_0_2_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_data_priority_map_0_0_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_data_xml_0_2_0_alpha6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_google_closure_library_0_0_20170809_b9c14c6b.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_google_closure_library_third_party_0_0_20170809_b9c14c6b.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_spec_alpha_0_2_176.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_tools_analyzer_0_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_tools_analyzer_jvm_0_7_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_tools_logging_0_4_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_clojure_tools_reader_1_3_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_codehaus_mojo_animal_sniffer_annotations_1_14.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_javassist_javassist_3_18_1_GA.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_mozilla_rhino_1_7R5.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_msgpack_msgpack_0_6_12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_omcljs_om_1_0_0_beta2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__org_ow2_asm_asm_5_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__pandeiro_boot_http_0_8_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__prismatic_dommy_1_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__prismatic_schema_1_1_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__re_frame_0_11_0_rc3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__reagent_0_9_0_rc4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__sablono_0_8_6.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__tigris_0_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/libraries/Leiningen__weasel_0_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /js/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /js/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /js/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /js/boot.properties: -------------------------------------------------------------------------------- 1 | #http://boot-clj.com 2 | #Sat Jan 28 19:01:13 CET 2017 3 | BOOT_CLOJURE_NAME=org.clojure/clojure 4 | BOOT_CLOJURE_VERSION=1.10.1 5 | BOOT_VERSION=2.8.3 6 | -------------------------------------------------------------------------------- /js/build.boot: -------------------------------------------------------------------------------- 1 | (task-options! 2 | pom {:project 'griff/airplay-spec-js 3 | :version "0.1.0-SNAPSHOT"}) 4 | 5 | (set-env! 6 | :source-paths #{"src"} 7 | :resource-paths #{"html" "libs" "resources"} 8 | :dependencies '[; Boot setup 9 | [adzerk/boot-cljs "2.1.5"] 10 | [adzerk/boot-cljs-repl "0.4.0"] 11 | [boot-deps "0.1.9"] 12 | [bendyorke/boot-postcss "0.0.1" :scope "test"] 13 | [adzerk/boot-reload "0.6.0" :scope "test"] 14 | [pandeiro/boot-http "0.8.3" :scope "test"] 15 | [cider/piggieback "0.3.9" :scope "test"] 16 | #_[com.cemerick/piggieback "0.2.2" :scope "test"] 17 | [weasel "0.7.0" :scope "test"] 18 | #_[org.clojure/tools.nrepl "0.2.13" :scope "test"] 19 | [nrepl "0.4.5" :scope "test"] 20 | [cljsjs/boot-cljsjs "0.10.4" :scope "test"] 21 | [griff/boot2nix "1.1.0" :scope "test"] 22 | [org.clojure/clojure "1.10.1"] 23 | [onetom/boot-lein-generate "0.1.3" :scope "test"] 24 | 25 | ; App dependencies 26 | [org.clojure/clojurescript "1.10.597"] 27 | [sablono "0.8.6"] 28 | [prismatic/dommy "1.1.0"] 29 | [reagent "0.9.0-rc4"] 30 | [re-frame "0.11.0-rc3"] 31 | #_[cljs-react-reload "0.1.1"] 32 | [cljs-ajax "0.8.0"] 33 | [compassus "1.0.0-alpha3"] 34 | [bidi "2.1.6"] 35 | [kibu/pushy "0.3.8"] 36 | [org.clojure/core.async "0.6.532"] 37 | [camel-snake-kebab "0.4.1"] 38 | [cljsjs/react-bootstrap "1.0.0-beta.14-0"] 39 | [cljsjs/react-datepicker "2.3.0-0"] 40 | [org.clojure/data.xml "0.2.0-alpha6"] 41 | 42 | ; Other dependencies 43 | [devcards "0.2.6"] 44 | [binaryage/devtools "0.9.11"]]) 45 | 46 | (require '[boot.lein]) 47 | (boot.lein/generate) 48 | 49 | (require '[adzerk.boot-reload :refer [reload]] 50 | '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]] 51 | '[adzerk.boot-cljs :refer [cljs]] 52 | '[griff.boot2nix :refer [boot2nix]] 53 | '[cljsjs.boot-cljsjs.packaging :refer [deps-cljs]] 54 | '[bendyorke.boot-postcss :refer [postcss]] 55 | '[pandeiro.boot-http :refer [serve]]) 56 | 57 | (deftask dev [] 58 | (comp (watch) 59 | (speak) 60 | (reload :ids #{"app"} 61 | :only-by-re [#"^app\.out/.*|css/.*|xterm/.*|.*\.html"] 62 | :on-jsload 'airplay.main/reload) 63 | (reload :ids #{"app-repl"} 64 | :only-by-re [#"^app-repl\.out/.*|css/.*|xterm/.*|.*\.html"] 65 | :on-jsload 'airplay.main/reload 66 | :asset-host "http://localhost:3002") 67 | (reload :ids #{"cards"} 68 | :only-by-re [#"^cards\.out/.*|css/.*|xterm/.*|.*\.html"] 69 | :on-jsload 'cards.ui/reload) 70 | (reload :ids #{"cards-repl"} 71 | :only-by-re [#"^cards-repl\.out/.*|css/.*|xterm/.*|.*\.html"] 72 | :on-jsload 'cards.ui/reload) 73 | (cljs-repl :ids #{"app-repl" "cards-repl"}) 74 | (cljs :optimizations :none 75 | :compiler-options {:devcards true 76 | :preloads '[devtools.preload]}) 77 | (target) 78 | (serve :dir "target" :port 3002))) 79 | 80 | (replace-task! 81 | [r dev] (fn [& xs] 82 | ;; we only want to have "dev" included for the REPL task 83 | (merge-env! :source-paths #{"dev"}) 84 | (apply r xs))) 85 | 86 | (deftask prod [] 87 | (comp 88 | (serve :dir "target/") 89 | (watch) 90 | (speak) 91 | (cljs :ids #{"app" "cards"} :source-map true :optimizations :advanced) 92 | (target))) 93 | 94 | (deftask build [] 95 | (comp 96 | (cljs :ids #{"app" "cards"} :source-map true :optimizations :advanced) 97 | (target))) 98 | 99 | (deftask release [] 100 | (comp 101 | (cljs :ids #{"app"} :source-map true :optimizations :advanced) 102 | (target :dir #{"release/"}))) 103 | -------------------------------------------------------------------------------- /js/html/cards.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /js/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ThoNix 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
bitnamedescription
0TestTesting
1Muhmore
25 |
26 | 27 |
<?xml version="1.0" encoding="UTF-8"?>
28 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
29 | <plist version="1.0">
30 | <dict>
31 | 	<key>PTPInfo</key>
32 | 	<string>OpenAVNU ArtAndLogic-aPTP-changes 1.0</string>
33 | 	<key>build</key>
34 | 	<string>17.0</string>
35 | 	<key>deviceID</key>
36 | 	<string>00:05:CD:D4:42:96</string>
37 | 	<key>features</key>
38 | 	<integer>496155769145856</integer>
39 | 	<key>firmwareBuildDate</key>
40 | 	<string>Jan 30 2019</string>
41 | 	<key>firmwareRevision</key>
42 | 	<string>1.505.130</string>
43 | 	<key>keepAliveLowPower</key>
44 | 	<true/>
45 | 	<key>keepAliveSendStatsAsBody</key>
46 | 	<true/>
47 | 	<key>manufacturer</key>
48 | 	<string>Sound United</string>
49 | 	<key>model</key>
50 | 	<string>AVR-X3500H</string>
51 | 	<key>name</key>
52 | 	<string>Daemon</string>
53 | 	<key>nameIsFactoryDefault</key>
54 | 	<false/>
55 | 	<key>pi</key>
56 | 	<string>650c20ee-842c-4c01-80ed-22b7fb239881</string>
57 | 	<key>protocolVersion</key>
58 | 	<string>1.1</string>
59 | 	<key>sdk</key>
60 | 	<string>AirPlay;2.0.2</string>
61 | 	<key>sourceVersion</key>
62 | 	<string>366.0</string>
63 | 	<key>statusFlags</key>
64 | 	<integer>4</integer>
65 | 	<key>txtAirPlay</key>
66 | 	<data>
67 | 	BWFjbD0wGmRldmljZWlkPTAwOjA1OkNEOkQ0OjQyOjk2G2ZlYXR1cmVzPTB4NDQ1RjhB
68 | 	MDAsMHgxQzM0MAdyc2Y9MHgwEGZ2PXAyMC4xLjUwNS4xMzAJZmxhZ3M9MHg0EG1vZGVs
69 | 	PUFWUi1YMzUwMEgZbWFudWZhY3R1cmVyPVNvdW5kIFVuaXRlZBtzZXJpYWxOdW1iZXI9
70 | 	QkJXMzYxODEyMTIzNjQNcHJvdG92ZXJzPTEuMQ1zcmN2ZXJzPTM2Ni4wJ3BpPTY1MGMy
71 | 	MGVlLTg0MmMtNGMwMS04MGVkLTIyYjdmYjIzOTg4MShnaWQ9NjUwYzIwZWUtODQyYy00
72 | 	YzAxLTgwZWQtMjJiN2ZiMjM5ODgxBmdjZ2w9MENwaz0wYWNkN2Q2MWIyODRjMGFmNzFj
73 | 	N2VmNGY3ZWE2NDRkZGRlYzIzOGVmMjdjM2MwYWQzZDVkM2JiOWM4YjMxZThm
74 | 	</data>
75 | </dict>
76 | </plist>
77 | 
78 | 79 | 87 | 88 | -------------------------------------------------------------------------------- /js/libs/.git-keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/js/libs/.git-keep -------------------------------------------------------------------------------- /js/project.clj: -------------------------------------------------------------------------------- 1 | (defproject 2 | griff/airplay-spec-js 3 | "0.1.0-SNAPSHOT" 4 | :repositories 5 | [["clojars" {:url "https://repo.clojars.org/"}] 6 | ["maven-central" {:url "https://repo1.maven.org/maven2"}]] 7 | :dependencies 8 | [[adzerk/boot-cljs "2.1.5"] 9 | [adzerk/boot-cljs-repl "0.4.0"] 10 | [boot-deps "0.1.9"] 11 | [bendyorke/boot-postcss "0.0.1" :scope "test"] 12 | [adzerk/boot-reload "0.6.0" :scope "test"] 13 | [pandeiro/boot-http "0.8.3" :scope "test"] 14 | [cider/piggieback "0.3.9" :scope "test"] 15 | [weasel "0.7.0" :scope "test"] 16 | [nrepl "0.4.5" :scope "test"] 17 | [cljsjs/boot-cljsjs "0.10.4" :scope "test"] 18 | [griff/boot2nix "1.1.0" :scope "test"] 19 | [org.clojure/clojure "1.10.1"] 20 | [onetom/boot-lein-generate "0.1.3" :scope "test"] 21 | [org.clojure/clojurescript "1.10.597"] 22 | [sablono "0.8.6"] 23 | [prismatic/dommy "1.1.0"] 24 | [reagent "0.9.0-rc4"] 25 | [re-frame "0.11.0-rc3"] 26 | [cljs-ajax "0.8.0"] 27 | [compassus "1.0.0-alpha3"] 28 | [bidi "2.1.6"] 29 | [kibu/pushy "0.3.8"] 30 | [org.clojure/core.async "0.6.532"] 31 | [camel-snake-kebab "0.4.1"] 32 | [cljsjs/react-bootstrap "1.0.0-beta.14-0"] 33 | [cljsjs/react-datepicker "2.3.0-0"] 34 | [org.clojure/data.xml "0.2.0-alpha6"] 35 | [devcards "0.2.6"] 36 | [binaryage/devtools "0.9.11"]] 37 | :source-paths 38 | ["src"] 39 | :resource-paths 40 | ["libs" "html" "resources"]) -------------------------------------------------------------------------------- /js/resources/app-repl.cljs.edn: -------------------------------------------------------------------------------- 1 | {:compiler-options {:language-in :ecmascript5 2 | :asset-path "http://localhost:3002/app-repl.out"}} 3 | -------------------------------------------------------------------------------- /js/resources/app.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [airplay.main] 2 | :compiler-options {:language-in :ecmascript5 3 | :static-fns true 4 | :asset-path "http://localhost:3002/app.out"}} 5 | -------------------------------------------------------------------------------- /js/resources/cards-repl.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [cards.ui] 2 | :init-fns [cards.ui/start] 3 | :compiler-options {:language-in :ecmascript5}} 4 | -------------------------------------------------------------------------------- /js/resources/cards.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [cards.ui] 2 | :init-fns [cards.ui/start] 3 | :compiler-options {:static-fns true}} 4 | -------------------------------------------------------------------------------- /js/src/airplay/bits.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.bits 2 | (:require [dommy.core :as dc :refer-macros [sel sel1]] 3 | [airplay.math :as am] 4 | [dommy.utils :as du])) 5 | 6 | (defn table->map [table] 7 | (let [names (rest (map dc/text (sel table [:thead :th]))) 8 | bits (->> (sel table [:tbody "td:first-child"]) 9 | (map dc/text) 10 | (map keyword))] 11 | (->> (sel table [:tbody :tr]) 12 | (map #(->> (sel %1 :td) 13 | rest 14 | (map dc/text) 15 | (zipmap names))) 16 | (zipmap bits) 17 | (into (sorted-map))))) 18 | 19 | (defn bits-table->map [table] 20 | (let [names (rest (map dc/text (sel table [:thead :th]))) 21 | bits (->> (sel table [:tbody "td:first-child"]) 22 | (map dc/text) 23 | (map am/parse-int))] 24 | (->> (sel table [:tbody :tr]) 25 | (map #(->> (sel %1 :td) 26 | rest 27 | (map dc/text) 28 | (zipmap names))) 29 | (zipmap bits) 30 | (into (sorted-map))))) 31 | 32 | (defn find-table* [el] 33 | (if (= "TABLE" (.-tagName el)) 34 | el 35 | (sel1 el :table))) 36 | 37 | (defn first-table [header] 38 | (if-let [f header] 39 | (let [h (dc/parent f)] 40 | (->> h 41 | dc/parent 42 | dc/children 43 | du/->Array 44 | (drop-while #(not (= % h))) 45 | (map find-table*) 46 | (filter some?) 47 | first)))) 48 | 49 | (comment 50 | (if-let [f (sel1 "#features")] 51 | (let [h (dc/parent f)] 52 | (->> h 53 | dc/parent 54 | dc/children 55 | du/->Array 56 | (drop-while #(not (= % h))) 57 | (map find-table*) 58 | (filter some?) 59 | first))) 60 | (first-table (sel1 "#features"))) -------------------------------------------------------------------------------- /js/src/airplay/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.examples 2 | (:require [clojure.string :as s])) 3 | 4 | -------------------------------------------------------------------------------- /js/src/airplay/features.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.features 2 | (:require [clojure.string :as cs] 3 | [airplay.math :as am])) 4 | 5 | (defn features->mdns [value] 6 | (let [low (am/bit-and value (am/bits->long 0xffffffff 0)) 7 | high (am/bit-shift-right value 32)] 8 | (str (am/->hex low) "," (am/->hex high)))) 9 | 10 | (defn str->features 11 | "Convert all possible ways to specify features into a long" 12 | [s] 13 | (let [[low high] (map cs/trim (cs/split s #"," 2))] 14 | (if high 15 | (am/bit-or (am/try-str->long low) (am/bit-shift-left (am/try-str->long high) 32)) 16 | (am/try-str->long low)))) 17 | -------------------------------------------------------------------------------- /js/src/airplay/main.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.main 2 | (:require [clojure.string :as cs] 3 | [dommy.core :as dc :refer-macros [sel sel1]] 4 | [reagent.core :as rc] 5 | [react :as react] 6 | [re-frame.core :as rf] 7 | [airplay.math :as am] 8 | [airplay.plist :as plist] 9 | [airplay.ui.features :as auf] 10 | [airplay.ui.examples :as aue] 11 | [airplay.ui.flags :as aus] 12 | [airplay.ui.airplay-service :as auas] 13 | [clojure.data.xml :as xml] 14 | [clojure.string :as s] 15 | [goog.crypt.base64 :as base64])) 16 | 17 | #_(reset! thonix/app (core/mount @thonix/app app/Root "app")) 18 | 19 | (js/console.log "UI") 20 | 21 | 22 | (comment 23 | (-> (sel1 "#info code") 24 | dc/text) 25 | (-> (sel1 "#info code") 26 | dc/text 27 | plist/parse) 28 | (let [txt (-> (sel1 "#info code") 29 | dc/text)] 30 | (= (-> txt 31 | plist/parse 32 | plist/emit-pretty-str) 33 | txt)) 34 | (first (filter xml/element? (:content (xml/parse-str (dc/text (sel1 "#info code")))))) 35 | (bit-set) 36 | (dc/closest) (map dc/text (sel [:table "td:first-child"])) :#testing 37 | (dc/selector [:thead :th]) 38 | (->> (sel (sel1 :table) [:tbody :tr]) 39 | (map #(sel1 %1 :td)) 40 | (map dc/text) 41 | ) 42 | (map #(sel1 %1 :td) (sel (sel1 :table) [:tbody :tr])) 43 | (map dc/text (sel (sel1 :table) [:thead :th])) 44 | (let [table (sel1 :table)] 45 | (if (not (dc/closest table :#features-list)) 46 | (bits-table->map table))) 47 | (bits-table->map (sel1 :table)) 48 | features 49 | (or (sel1 :#features-list) (sel1 :table))) 50 | 51 | (defn ^:export main [] 52 | (js/console.log "Main") 53 | (rf/dispatch-sync [:initialize]) 54 | (auf/init) 55 | (aue/init) 56 | (aus/init) 57 | #_(auas/init)) 58 | (main) 59 | 60 | (defn reload [] 61 | (rf/clear-subscription-cache!) 62 | #_(rc/force-update-all)) 63 | -------------------------------------------------------------------------------- /js/src/airplay/math.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.math 2 | (:refer-clojure :exclude [long bit-and bit-or bit-xor bit-not bit-shift-left bit-shift-right bit-test bit-set bit-flip]) 3 | (:import goog.math.Long)) 4 | 5 | (extend-type Long 6 | IEquiv 7 | (-equiv [o other] (.equals o other))) 8 | 9 | (defn parse-int 10 | ([s] (js/parseInt s)) 11 | ([s radix] (js/parseInt s radix))) 12 | 13 | (defn parse-float [s] 14 | (js/parseFloat s)) 15 | 16 | (defn long [x] 17 | (if (instance? Long x) 18 | x 19 | (condp = x 20 | 0 (.getZero Long) 21 | 1 (.getOne Long) 22 | -1 (.getNegOne Long) 23 | (.fromNumber Long x)))) 24 | 25 | (defn ->str 26 | "Convert long to string" 27 | ([v] (->str v 10)) 28 | ([v radix] (.toString (long v) radix))) 29 | 30 | (defn ->hex [value] 31 | (str "0x" (.toUpperCase (->str value 16)))) 32 | 33 | (defn bit-and 34 | "Bitwise and" 35 | ([x y] (.and (long x) (long y))) 36 | ([x y & more] (reduce bit-and (bit-and x y) more))) 37 | 38 | (defn bit-or 39 | "Bitwise or" 40 | ([x y] (.or (long x) (long y))) 41 | ([x y & more] (reduce bit-or (bit-or x y) more))) 42 | 43 | (defn bit-xor 44 | "Bitwise exclusive or" 45 | ([x y] (.xor (long x) (long y))) 46 | ([x y & more] (reduce bit-xor (bit-xor x y) more))) 47 | 48 | (defn bit-not 49 | "Bitwise complement" 50 | ([x y] (.not (long x) (long y))) 51 | ([x y & more] (reduce bit-not (bit-not x y) more))) 52 | 53 | (defn bit-shift-left 54 | "Bitwise shift left" 55 | ([x n] (.shiftLeft (long x) n))) 56 | 57 | (defn bit-shift-right 58 | "Bitwise shift right" 59 | [x n] (.shiftRight (long x) n)) 60 | 61 | (defn bit-test 62 | "Test bit at index n" 63 | [x n] 64 | (let [bitv (bit-shift-left 1 n)] 65 | (= bitv (bit-and x bitv)))) 66 | 67 | (defn bit-set 68 | "Set bit at index n" 69 | [x n] 70 | (bit-or x (bit-shift-left 1 n))) 71 | 72 | (defn bit-flip 73 | "Flips bit at index n" 74 | [x n] 75 | (bit-xor x (bit-shift-left 1 n))) 76 | 77 | (defn str->long 78 | "Convert string to long with radix support" 79 | ([s] (str->long s 10)) 80 | ([s radix] 81 | (.fromString Long s radix))) 82 | 83 | (defn try-str->long 84 | "Try parsing a string and return 0 if not parsable" 85 | [s] 86 | (try 87 | (if (= "0x" (subs s 0 2)) 88 | (str->long (subs s 2) 16) 89 | (str->long s)) 90 | (catch :default e 91 | (long 0)))) 92 | 93 | (defn bits->long 94 | "Convert low and high bits to long" 95 | [low high] (.fromBits Long low high)) 96 | 97 | (comment 98 | (= (long 12) 10) 99 | (bit-test 8 3) 100 | (bit-and 12 12) 101 | (bit-and ) 102 | (bit-or 10 (.getMaxValue Long)) 103 | (str->long "14") 104 | (->hex) (.shiftLeft (try-str->long "0x4A7FFFF7") 32) 105 | (.toString (long (.fromBits Long 0 0x4A7FFFF7)) 16) 106 | (->hex (bit-shift-left (try-str->long "0x1") 32))) -------------------------------------------------------------------------------- /js/src/airplay/plist.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.plist 2 | (:require [clojure.string :as s] 3 | [clojure.data.xml :as xml] 4 | [airplay.math :as am] 5 | [goog.crypt.base64 :as base64] 6 | [clojure.data.xml.node :as node] 7 | [clojure.data.xml.name :as name])) 8 | 9 | (defn decode-base64 [s] 10 | (base64/decodeStringToByteArray s)) 11 | (defn encode-base64 [array] 12 | (base64/encodeByteArray array)) 13 | 14 | (defrecord Data [data] 15 | Object 16 | (toString [_] (encode-base64 data))) 17 | 18 | (defn ->data [data] 19 | (Data. (decode-base64 data))) 20 | 21 | (declare dict->map) 22 | 23 | (defn elem->clj [e] 24 | (let [c (:content e)] 25 | (case (:tag e) 26 | :string (s/join "" c) 27 | :integer (am/str->long (s/join "" c)) 28 | :real (am/parse-float (s/join "" c)) 29 | :true true 30 | :false false 31 | :data (->data (s/join "" c)) 32 | :array (map elem->clj c) 33 | :dict (dict->map c) 34 | (throw "Invalid element")))) 35 | 36 | (defn dict->map [e] 37 | (->> e 38 | :content 39 | (filter xml/element?) 40 | (partition 2) 41 | (reduce (fn [val [key value]] 42 | (assoc val (s/join "" (:content key)) (elem->clj value))) 43 | {}))) 44 | 45 | (defn parse [s] 46 | (->> s 47 | xml/parse-str 48 | :content 49 | (filter xml/element?) 50 | first 51 | dict->map)) 52 | 53 | (declare xml-el) 54 | 55 | (defn- dict-xml 56 | ([el {:keys [indent indent-level] :as opts}] 57 | (let [full (apply str (repeat indent-level indent))] 58 | (mapcat (fn [[key value]] 59 | [full 60 | (node/element :key {} (if (keyword? key) (name key) (str key))) 61 | full 62 | (xml-el value opts)]) el)))) 63 | 64 | (defn- pretty-data [data indent] 65 | (if indent 66 | (->> (str data) 67 | (partition 68 68 []) 68 | (map #(str indent (apply str %) "\n")) 69 | (apply str)) 70 | (str data))) 71 | 72 | (comment 73 | (pretty-data "test" "\t")) 74 | 75 | (defn- xml-el 76 | ([el] (xml-el el {})) 77 | ([el {:keys [indent indent-level] :as opts}] 78 | (let [full (if indent (apply str (repeat indent-level indent))) 79 | next (assoc opts :indent-level (inc indent-level)) 80 | next-full (if indent (apply str (repeat (inc indent-level) indent)))] 81 | (cond 82 | (instance? Data el) (node/element :data {} (pretty-data el full)) 83 | (string? el) (node/element :string {} el) 84 | (int? el) (node/element :integer {} (str el)) 85 | (double? el) (node/element :real {} (str el)) 86 | (= true el) (node/element :true) 87 | (= false el) (node/element :false) 88 | (vector? el) (apply node/element :array {} (mapcat #(concat next-full [(xml-el % next)]) el)) 89 | (satisfies? ILookup el) (apply node/element :dict {} (dict-xml el next)) 90 | (satisfies? ISequential el) (apply node/element :array {} (mapcat #(concat next-full [(xml-el % next)]) el)) 91 | :else (throw (ex-info "Cannot coerce" {:form el})))))) 92 | 93 | (defn xml 94 | ([el] (xml el {})) 95 | ([el opts] 96 | (let [next (assoc opts :indent-level 0)] 97 | (node/element :plist {:version "1.0"} (xml-el el next))))) 98 | 99 | (defn emit-str [el] 100 | (str 101 | "" 102 | "" 103 | (xml/emit-str (xml el)))) 104 | 105 | (declare emit-pretty-xml-str) 106 | (defn- element-str [el {:keys [indent indent-level] :as opts}] 107 | (let [qname (name (:tag el)) 108 | content (seq (:content el)) 109 | full (apply str (repeat indent-level indent)) 110 | next (assoc opts :indent-level (inc indent-level)) 111 | content-map (if content (map #(emit-pretty-xml-str % next) content))] 112 | (apply str (concat ["<" qname] 113 | (mapcat (fn [[n a]] 114 | [" " (name n) "=" (pr-str a)]) 115 | (:attrs el)) 116 | (cond 117 | (nil? content) ["/>\n"] 118 | (some node/element? content) (concat [">\n"] content-map [full "\n"]) 119 | :else (if (re-find #"\n" (apply str content-map)) 120 | (concat [">\n"] content-map [full "\n"]) 121 | (concat [">"] content-map ["\n"]))))))) 122 | (defn- emit-pretty-xml-str [el opts] 123 | (cond 124 | (string? el) el 125 | (instance? node/Element el) (element-str el opts))) 126 | 127 | (defn emit-pretty-str [el] 128 | (str 129 | "\n" 130 | "\n" 131 | (emit-pretty-xml-str (xml el {:indent "\t"}) {:indent "\t" :indent-level -1}))) 132 | 133 | (comment 134 | (name/as-qname (:tag (xml [12 13]))) 135 | (emit-pretty-str (am/long 12)) 136 | (xml [12 13] {:indent "\t"}) 137 | (emit-pretty-str [12 13 {:sdk "test"}]) 138 | (emit-pretty-str {:sourceVersion "360.0"}) 139 | (emit-pretty-str {:test (->data (str "\tBWFjbD0wGmRldmljZWlkPTAwOjA1OkNEOkQ0OjQyOjk2G2ZlYXR1cmVzPTB4NDQ1RjhB\n" 140 | "\tMDAsMHgxQzM0MAdyc2Y9MHgwEGZ2PXAyMC4xLjUwNS4xMzAJZmxhZ3M9MHg0EG1vZGVs\n" 141 | "\tPUFWUi1YMzUwMEgZbWFudWZhY3R1cmVyPVNvdW5kIFVuaXRlZBtzZXJpYWxOdW1iZXI9\n" 142 | "\tQkJXMzYxODEyMTIzNjQNcHJvdG92ZXJzPTEuMQ1zcmN2ZXJzPTM2Ni4wJ3BpPTY1MGMy\n" 143 | "\tMGVlLTg0MmMtNGMwMS04MGVkLTIyYjdmYjIzOTg4MShnaWQ9NjUwYzIwZWUtODQyYy00\n" 144 | "\tYzAxLTgwZWQtMjJiN2ZiMjM5ODgxBmdjZ2w9MENwaz0wYWNkN2Q2MWIyODRjMGFmNzFj\n" 145 | "\tN2VmNGY3ZWE2NDRkZGRlYzIzOGVmMjdjM2MwYWQzZDVkM2JiOWM4YjMxZThm"))}) 146 | (re-find #"\n" "test") 147 | (re-find #"\n" 148 | (str "\tBWFjbD0wGmRldmljZWlkPTAwOjA1OkNEOkQ0OjQyOjk2G2ZlYXR1cmVzPTB4NDQ1RjhB\n" 149 | "\tMDAsMHgxQzM0MAdyc2Y9MHgwEGZ2PXAyMC4xLjUwNS4xMzAJZmxhZ3M9MHg0EG1vZGVs\n" 150 | "\tPUFWUi1YMzUwMEgZbWFudWZhY3R1cmVyPVNvdW5kIFVuaXRlZBtzZXJpYWxOdW1iZXI9\n" 151 | "\tQkJXMzYxODEyMTIzNjQNcHJvdG92ZXJzPTEuMQ1zcmN2ZXJzPTM2Ni4wJ3BpPTY1MGMy\n" 152 | "\tMGVlLTg0MmMtNGMwMS04MGVkLTIyYjdmYjIzOTg4MShnaWQ9NjUwYzIwZWUtODQyYy00\n" 153 | "\tYzAxLTgwZWQtMjJiN2ZiMjM5ODgxBmdjZ2w9MENwaz0wYWNkN2Q2MWIyODRjMGFmNzFj\n" 154 | "\tN2VmNGY3ZWE2NDRkZGRlYzIzOGVmMjdjM2MwYWQzZDVkM2JiOWM4YjMxZThm")) 155 | ) -------------------------------------------------------------------------------- /js/src/airplay/state.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.state 2 | (:require [react :as react] 3 | [reagent.core :as rc])) 4 | 5 | 6 | (defonce context (react/createContext "airplay-state")) 7 | (def Provider (.-Provider context)) 8 | (def Consumer (.-Consumer context)) 9 | 10 | (deftype App [db] 11 | ILookup 12 | (-lookup [o k] 13 | (case k 14 | :db db)) 15 | (-lookup [o k not-found] 16 | (case k 17 | :db db 18 | not-found)) 19 | IHash 20 | (-hash [this] (goog/getUid this))) 21 | (defn create-app [initial-value] 22 | (->App (rc/atom initial-value))) 23 | 24 | (defn render-app [app component container] 25 | (js/console.log "Rendering app" app (:db app)) 26 | (rc/render [:> Provider {:value app} 27 | [component] 28 | #_[:> Consumer {} 29 | (fn [v] 30 | (rc/as-element [:div "Context: " (get-in v [:db :current :features]) [component]]))]] 31 | container)) 32 | 33 | 34 | (defonce subscriptions (atom {})) 35 | 36 | (defn reg-sub [id query-fn] 37 | (swap! subscriptions assoc id query-fn)) 38 | 39 | (defonce events (atom {})) 40 | 41 | (defn reg-event-fx [id effect-fn] 42 | (swap! events assoc id effect-fn)) 43 | 44 | (defn subscribe [ctx query] 45 | (let [query-fn (get @subscriptions (first query)) 46 | db (:db ctx)] 47 | (js/console.log "subscribe Context" ctx query db) 48 | (assert query-fn 49 | (str "first value of query must be registered subscription " 50 | (pr-str (first query)))) 51 | (rc/cursor #(query-fn @db %) query))) 52 | 53 | 54 | (defn dispatch [ctx event] 55 | (let [effect-fn (get @events (first event)) 56 | db (:db ctx)] 57 | (js/console.log "dispatch Context" ctx event db) 58 | (assert effect-fn 59 | (str "first value of event must be registered event-fx " 60 | (pr-str (first event)))) 61 | (swap! db (fn [old] (:db (effect-fn {:db old} event)))))) 62 | 63 | (comment 64 | @subscriptions) -------------------------------------------------------------------------------- /js/src/airplay/ui/airplay_service.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.ui.airplay-service 2 | (:require [clojure.string :as cs] 3 | [dommy.core :as dc :refer-macros [sel sel1]] 4 | [dommy.utils :as du] 5 | [reagent.core :as rc] 6 | [react :as react] 7 | [airplay.features :as af] 8 | [airplay.math :as am] 9 | [airplay.plist :as plist] 10 | [airplay.bits :as ab] 11 | [re-frame.core :as as] 12 | [clojure.data.xml :as xml] 13 | [clojure.string :as s])) 14 | 15 | 16 | (defonce airplay-items 17 | (if-let [table (ab/first-table (sel1 "#_airplay_tcp"))] 18 | (if (not (dc/closest table :#airplay-list)) 19 | (ab/table->map table)))) 20 | 21 | (comment 22 | airplay-items) 23 | 24 | (defn airplay-table [{:keys [value on-input-changed on-change-bit items]}] 25 | (let [l-value (or @value {})] 26 | [:table {:className "bits"} 27 | ^{:key "head"} 28 | [:tbody {:className ["header"]} 29 | [:tr 30 | ^{:key "name"} [:th "name"] 31 | ^{:key "value"} [:th "value"] 32 | (for [name (->> items vals first keys)] 33 | ^{:key name} [:th name])]] 34 | ^{:key "body"} 35 | [:tbody {:className "rows"} 36 | (for [[key values] items] 37 | ^{:key (name key)} 38 | [:tr 39 | ^{:key "name"} [:td (name key)] 40 | ^{:key "value"} [:td (get l-value key "")] 41 | (for [[key value] values] 42 | ^{:key (name key)} [:td value])])]])) 43 | 44 | (as/reg-event-fx ::change-value 45 | (fn [{:keys [db]} [_ new-value]] 46 | {:db (assoc-in db [:current :services "_airplay._tcp" :txt] new-value) 47 | :dispatch [:airplay.ui.core/current-changed]})) 48 | (as/reg-event-fx ::flip-bit 49 | (fn [{:keys [db]} [_ bit]] 50 | {:db (update-in db [:current :services "_airplay._tcp" :txt] 51 | #(-> % am/try-str->long (am/bit-flip bit) am/->hex)) 52 | :dispatch [:airplay.ui.core/current-changed]})) 53 | (as/reg-sub ::value 54 | (fn [db _] 55 | (get-in db [:current :services "_airplay._tcp" :txt]))) 56 | 57 | (defn airplay-state [] 58 | (js/console.log "airplay-state") 59 | (let [#_ctx #_(.-context (rc/current-component)) 60 | value (as/subscribe #_ctx [::value])] 61 | (js/console.log "Component" (rc/current-component) value) 62 | [airplay-table {:items airplay-items 63 | :value value 64 | :on-input-changed (fn [i] 65 | (let [new-value (-> i .-target .-value)] 66 | (js/console.log "Change value" new-value) 67 | (as/dispatch #_ctx [::change-value new-value]))) 68 | :on-change-bit #(as/dispatch #_ctx [::flip-bit %])}])) 69 | 70 | (defn airplay-ratom [] 71 | (let [value (rc/atom "1")] 72 | [airplay-table {:status-flags airplay-items 73 | :value value 74 | :on-input-changed (fn [i] 75 | (let [new-value (-> i .-target .-value)] 76 | (js/console.log "Change value" new-value) 77 | (reset! value new-value))) 78 | :on-change-bit (fn [bit] (swap! value #(-> % am/try-str->long (am/bit-flip bit) am/->hex)))}])) 79 | 80 | 81 | (defn init [] 82 | (if-let [el (sel1 "#_airplay_tcp")] 83 | (let [main (-> el dc/parent) 84 | container (or (sel1 main "#airplay-list") 85 | (let [elem (dc/create-element "div")] 86 | (dc/set-attr! elem "id" "airplay-list") 87 | (dc/replace! (ab/first-table el) elem)))] 88 | (rc/render #_auc/app airplay-state container)))) 89 | -------------------------------------------------------------------------------- /js/src/airplay/ui/core.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.ui.core 2 | (:require [re-frame.core :as as] 3 | [airplay.examples.data :as aed] 4 | [re-frame.core :as rf])) 5 | 6 | #_(defonce app (as/create-app {:current {:features ""}})) 7 | 8 | (rf/reg-event-db ;; sets up initial application state 9 | :initialize ;; usage: (dispatch [:initialize]) 10 | (fn [_ _] ;; the two parameters are not important here, so use _ 11 | (js/console.log "initialize") 12 | {:current {:features ""} ;; What it returns becomes the new application state 13 | :examples nil 14 | :time-color "#f88"})) ;; so the application state will initially be a map with two keys 15 | 16 | (rf/reg-event-fx ::current-changed 17 | (fn [{:keys [db]} _] 18 | (let [current (:current db) 19 | example (some (fn [[k v]] 20 | (js/console.log "compare" k (= current v) v current) 21 | (if (= current v) k) 22 | ) aed/all)] 23 | (js/console.log "current changed " (pr example)) 24 | {:db (assoc db :example example)}))) 25 | -------------------------------------------------------------------------------- /js/src/airplay/ui/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.ui.examples 2 | (:require [re-frame.core :as as] 3 | [airplay.examples.data :as aed] 4 | [reagent.core :as rc] 5 | [dommy.core :as dc] 6 | [airplay.bits :as ab] 7 | [airplay.ui.core :as auc])) 8 | 9 | 10 | 11 | (defn examples-select [{:keys [value examples on-example-changed]}] 12 | (let [selected @value] 13 | (js/console.log "Render example" selected (if selected (name selected) "")) 14 | [:<> "Examples: " 15 | [:select {:on-change on-example-changed :value (if selected (name selected) "")} 16 | [:option {:key "" :value ""} "Custom"] 17 | (for [[id example] examples] 18 | [:option {:key (name id) :value (name id)} (:name example)])]])) 19 | 20 | (as/reg-event-fx ::change-value 21 | (fn [{:keys [db]} [_ new-value]] 22 | (let [example (if-not (= "" new-value) (keyword new-value)) 23 | selected (get aed/all example)] 24 | (js/console.log "Change example" example selected) 25 | {:db (assoc db 26 | :example example 27 | :current selected)}))) 28 | 29 | (as/reg-sub ::value 30 | (fn [db _] 31 | (get-in db [:example]))) 32 | 33 | (defn examples-select-state [] 34 | (rc/with-let [#_ctx #_(.-context (rc/current-component)) 35 | value (as/subscribe #_ctx [::value])] 36 | (js/console.log "Component" (rc/current-component) value) 37 | [examples-select {:examples aed/all 38 | :value value 39 | :on-example-changed (fn [i] 40 | (let [new-value (-> i .-target .-value)] 41 | (js/console.log "Example change value" new-value) 42 | (as/dispatch #_ctx [::change-value new-value])))}])) 43 | 44 | (defn init [] 45 | (if-let [el (dc/sel1 "#examples-select")] 46 | (rc/render #_auc/app examples-select-state el))) -------------------------------------------------------------------------------- /js/src/airplay/ui/features.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.ui.features 2 | (:require [clojure.string :as cs] 3 | [dommy.core :as dc :refer-macros [sel sel1]] 4 | [dommy.utils :as du] 5 | [reagent.core :as rc] 6 | [react :as react] 7 | [airplay.features :as af] 8 | [airplay.math :as am] 9 | [airplay.plist :as plist] 10 | [airplay.bits :as ab] 11 | [re-frame.core :as as] 12 | [clojure.data.xml :as xml] 13 | [clojure.string :as s])) 14 | 15 | 16 | (defonce features 17 | (if-let [table (ab/first-table (sel1 "#features"))] 18 | (if (not (dc/closest table :#features-list)) 19 | (ab/bits-table->map table)))) 20 | 21 | 22 | (defn features-table [{:keys [value on-input-changed on-change-bit features]}] 23 | (let [l-value (af/str->features (or @value ""))] 24 | [:table {:className "bits"} 25 | ^{:key "interactive"} 26 | [:tbody {:className ["header"]} 27 | [:tr 28 | [:th {:colSpan 3} 29 | "Example: " 30 | [:input {:type "text" 31 | :on-change on-input-changed 32 | :on-paste on-input-changed 33 | :value @value}]]]] 34 | ^{:key "output"} 35 | [:tbody 36 | [:tr 37 | [:td {:colSpan 3} 38 | "Decimal: " (am/->str l-value)]] 39 | [:tr 40 | [:td {:colSpan 3} 41 | "Hex: " (am/->hex l-value)]] 42 | [:tr 43 | [:td {:colSpan 3} 44 | "mDNS: " (af/features->mdns l-value)]]] 45 | ^{:key "head"} 46 | [:tbody {:className ["header"]} 47 | [:tr 48 | ^{:key "bit"} [:th "bit"] 49 | (for [name (->> features vals first keys)] 50 | ^{:key name} [:th name])]] 51 | ^{:key "body"} 52 | [:tbody {:className "rows"} 53 | (for [[bit values] features] 54 | ^{:key bit} 55 | [:tr {:class (if (am/bit-test l-value bit) "selected" "notSelected") 56 | :on-click #(on-change-bit bit)} 57 | ^{:key "bit"} [:td bit] 58 | (for [[name value] values] 59 | ^{:key name} [:td value])])]])) 60 | 61 | (as/reg-event-fx ::change-value 62 | (fn [{:keys [db]} [_ new-value]] 63 | {:db (assoc-in db [:current :features] new-value) 64 | :dispatch [:airplay.ui.core/current-changed]})) 65 | (as/reg-event-fx ::flip-bit 66 | (fn [{:keys [db]} [_ bit]] 67 | {:db (update-in db [:current :features] #(-> % af/str->features (am/bit-flip bit) af/features->mdns)) 68 | :dispatch [:airplay.ui.core/current-changed]})) 69 | (as/reg-sub ::value 70 | (fn [db _] 71 | (get-in db [:current :features]))) 72 | 73 | (defn features-state [] 74 | (js/console.log "features-state") 75 | (let [#_ctx #_(.-context (rc/current-component)) 76 | value (as/subscribe #_ctx [::value])] 77 | (js/console.log "Component" (rc/current-component) value) 78 | [features-table {:features features 79 | :value value 80 | :on-input-changed (fn [i] 81 | (let [new-value (-> i .-target .-value)] 82 | (js/console.log "Change value" new-value) 83 | (as/dispatch #_ctx [::change-value new-value]))) 84 | :on-change-bit #(as/dispatch #_ctx [::flip-bit %])}])) 85 | 86 | (defn features-ratom [] 87 | (let [value (rc/atom "1")] 88 | [features-table {:features features 89 | :value value 90 | :on-input-changed (fn [i] 91 | (let [new-value (-> i .-target .-value)] 92 | (js/console.log "Change value" new-value) 93 | (reset! value new-value))) 94 | :on-change-bit (fn [bit] (swap! value #(-> % af/str->features (am/bit-flip bit) af/features->mdns)))}])) 95 | 96 | 97 | (defn init [] 98 | (if-let [el (sel1 "#features")] 99 | (let [main (-> el dc/parent) 100 | container (or (sel1 main "#features-list") 101 | (let [elem (dc/create-element "div")] 102 | (dc/set-attr! elem "id" "features-list") 103 | (dc/replace! (ab/first-table el) elem)))] 104 | (rc/render #_auc/app features-state container)))) 105 | 106 | 107 | (comment 108 | (let [el (sel1 "#features")] 109 | (let [main (-> el dc/parent) 110 | container (or (sel1 main "#features-list") 111 | (let [elem (dc/create-element "div")] 112 | (dc/set-attr! elem "id" "features-list") 113 | (dc/replace! (ab/first-table el) elem)))] 114 | container)) 115 | (main) 116 | (ab/first-table (sel1 "#features")) 117 | (map cs/trim (cs/split "0x4A7FFFF7,0x4155FDE" #"," 2)) 118 | (am/bit-test (str->features "0x4A7FFFF0,0x4155FDE") 0) 119 | (am/->hex (am/try-str->long "0x4A7FFFF7")) 120 | (am/->hex (am/bit-shift-left (am/try-str->long "0x4A7FFFF7") 24)) 121 | (Long ) 122 | (let [[low high] (map cs/trim (cs/split "0x4A7FFFF7,0x4155FDE" #"," 2))] 123 | (if high 124 | (am/bit-or (am/try-str->long low) (am/bit-shift-left (am/try-str->long high) 32)) 125 | (am/try-str->long low))) 126 | (am/->hex (str->features "0x4A7FFFF7,0x4155FDE")) 127 | (-> (sel1 "#features") 128 | dc/parent 129 | dc/parent 130 | (sel1 :table))) 131 | 132 | (comment 133 | (-> (sel1 "#info code") 134 | dc/text) 135 | (-> (sel1 "#info code") 136 | dc/text 137 | plist/parse) 138 | (let [txt (-> (sel1 "#info code") 139 | dc/text)] 140 | (= (-> txt 141 | plist/parse 142 | plist/emit-pretty-str) 143 | txt)) 144 | (first (filter xml/element? (:content (xml/parse-str (dc/text (sel1 "#info code")))))) 145 | (bit-set) 146 | (dc/closest) (map dc/text (sel [:table "td:first-child"])) :#testing 147 | (dc/selector [:thead :th]) 148 | (->> (sel (sel1 :table) [:tbody :tr]) 149 | (map #(sel1 %1 :td)) 150 | (map dc/text) 151 | ) 152 | (map #(sel1 %1 :td) (sel (sel1 :table) [:tbody :tr])) 153 | (map dc/text (sel (sel1 :table) [:thead :th])) 154 | (let [table (sel1 :table)] 155 | (if (not (dc/closest table :#features-list)) 156 | (bits-table->map table))) 157 | (bits-table->map (sel1 :table)) 158 | features 159 | (or (sel1 :#features-list) (sel1 :table))) -------------------------------------------------------------------------------- /js/src/airplay/ui/flags.cljs: -------------------------------------------------------------------------------- 1 | (ns airplay.ui.flags 2 | (:require [clojure.string :as cs] 3 | [dommy.core :as dc :refer-macros [sel sel1]] 4 | [dommy.utils :as du] 5 | [reagent.core :as rc] 6 | [react :as react] 7 | [airplay.features :as af] 8 | [airplay.math :as am] 9 | [airplay.plist :as plist] 10 | [airplay.bits :as ab] 11 | [re-frame.core :as as] 12 | [clojure.data.xml :as xml] 13 | [clojure.string :as s])) 14 | 15 | 16 | (defonce status-flags 17 | (if-let [table (ab/first-table (sel1 "#status-flags"))] 18 | (if (not (dc/closest table :#status-flags-list)) 19 | (ab/bits-table->map table)))) 20 | 21 | 22 | (defn status-flags-table [{:keys [value on-input-changed on-change-bit status-flags]}] 23 | (let [l-value (af/str->features (or @value ""))] 24 | [:table {:className "bits"} 25 | ^{:key "interactive"} 26 | [:tbody {:className ["header"]} 27 | [:tr 28 | [:th {:colSpan 3} 29 | "Example: " 30 | [:input {:type "text" 31 | :on-change on-input-changed 32 | :on-paste on-input-changed 33 | :value @value}]]]] 34 | ^{:key "output"} 35 | [:tbody 36 | [:tr 37 | [:td {:colSpan 3} 38 | "Decimal: " (am/->str l-value)]] 39 | [:tr 40 | [:td {:colSpan 3} 41 | "Hex: " (am/->hex l-value)]]] 42 | ^{:key "head"} 43 | [:tbody {:className ["header"]} 44 | [:tr 45 | ^{:key "bit"} [:th "bit"] 46 | (for [name (->> status-flags vals first keys)] 47 | ^{:key name} [:th name])]] 48 | ^{:key "body"} 49 | [:tbody {:className "rows"} 50 | (for [[bit values] status-flags] 51 | ^{:key bit} 52 | [:tr {:class (if (am/bit-test l-value bit) "selected" "notSelected") 53 | :on-click #(on-change-bit bit)} 54 | ^{:key "bit"} [:td bit] 55 | (for [[name value] values] 56 | ^{:key name} [:td value])])]])) 57 | 58 | (as/reg-event-fx ::change-value 59 | (fn [{:keys [db]} [_ new-value]] 60 | {:db (assoc-in db [:current :flags] new-value) 61 | :dispatch [:airplay.ui.core/current-changed]})) 62 | (as/reg-event-fx ::flip-bit 63 | (fn [{:keys [db]} [_ bit]] 64 | {:db (update-in db [:current :flags] #(-> % am/try-str->long (am/bit-flip bit) am/->hex)) 65 | :dispatch [:airplay.ui.core/current-changed]})) 66 | (as/reg-sub ::value 67 | (fn [db _] 68 | (get-in db [:current :flags]))) 69 | 70 | (defn status-flags-state [] 71 | (js/console.log "status-flags-state") 72 | (let [#_ctx #_(.-context (rc/current-component)) 73 | value (as/subscribe #_ctx [::value])] 74 | (js/console.log "Component" (rc/current-component) value) 75 | [status-flags-table {:status-flags status-flags 76 | :value value 77 | :on-input-changed (fn [i] 78 | (let [new-value (-> i .-target .-value)] 79 | (js/console.log "Change value" new-value) 80 | (as/dispatch #_ctx [::change-value new-value]))) 81 | :on-change-bit #(as/dispatch #_ctx [::flip-bit %])}])) 82 | 83 | (defn status-flags-ratom [] 84 | (let [value (rc/atom "1")] 85 | [status-flags-table {:status-flags status-flags 86 | :value value 87 | :on-input-changed (fn [i] 88 | (let [new-value (-> i .-target .-value)] 89 | (js/console.log "Change value" new-value) 90 | (reset! value new-value))) 91 | :on-change-bit (fn [bit] (swap! value #(-> % am/try-str->long (am/bit-flip bit) am/->hex)))}])) 92 | 93 | 94 | (defn init [] 95 | (if-let [el (sel1 "#status-flags")] 96 | (let [main (-> el dc/parent) 97 | container (or (sel1 main "#status-flags-list") 98 | (let [elem (dc/create-element "div")] 99 | (dc/set-attr! elem "id" "status-flags-list") 100 | (dc/replace! (ab/first-table el) elem)))] 101 | (rc/render #_auc/app status-flags-state container)))) 102 | -------------------------------------------------------------------------------- /js/src/cards/ui.cljs: -------------------------------------------------------------------------------- 1 | (ns cards.ui 2 | (:require [devcards.core :as dc :include-macros true] 3 | [sablono.core :as sab]) 4 | (:require-macros 5 | [devcards.core :refer [defcard]])) 6 | 7 | (defn start [] 8 | (js/console.log "Start") 9 | (dc/start-devcard-ui!)) 10 | 11 | (js/console.log "UI") 12 | 13 | (defn reload [] 14 | (js/console.log "Reload")) 15 | 16 | (defcard my-first-card 17 | (sab/html [:h1 "Devcards is freaking awesome!"])) 18 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "niv": { 3 | "branch": "master", 4 | "description": "Easy dependency management for Nix projects", 5 | "homepage": "https://github.com/nmattia/niv", 6 | "owner": "nmattia", 7 | "repo": "niv", 8 | "rev": "f73bf8d584148677b01859677a63191c31911eae", 9 | "sha256": "0jlmrx633jvqrqlyhlzpvdrnim128gc81q5psz2lpp2af8p8q9qs", 10 | "type": "tarball", 11 | "url": "https://github.com/nmattia/niv/archive/f73bf8d584148677b01859677a63191c31911eae.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "nixpkgs": { 15 | "branch": "nixos-20.03", 16 | "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", 17 | "homepage": "https://github.com/NixOS/nixpkgs", 18 | "owner": "NixOS", 19 | "repo": "nixpkgs-channels", 20 | "rev": "db31e48c5c8d99dcaf4e5883a96181f6ac4ad6f6", 21 | "sha256": "1j5j7vbnq2i5zyl8498xrf490jca488iw6hylna3lfwji6rlcaqr", 22 | "type": "tarball", 23 | "url": "https://github.com/NixOS/nixpkgs-channels/archive/db31e48c5c8d99dcaf4e5883a96181f6ac4ad6f6.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: spec: 10 | if spec.builtin or true then 11 | builtins_fetchurl { inherit (spec) url sha256; } 12 | else 13 | pkgs.fetchurl { inherit (spec) url sha256; }; 14 | 15 | fetch_tarball = pkgs: name: spec: 16 | let 17 | ok = str: ! builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str); 18 | # sanitize the name, though nix will still fail if name starts with period 19 | name' = stringAsChars (x: if ! ok x then "-" else x) "${name}-src"; 20 | in 21 | if spec.builtin or true then 22 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 23 | else 24 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 25 | 26 | fetch_git = spec: 27 | builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; }; 28 | 29 | fetch_local = spec: spec.path; 30 | 31 | fetch_builtin-tarball = name: throw 32 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 33 | $ niv modify ${name} -a type=tarball -a builtin=true''; 34 | 35 | fetch_builtin-url = name: throw 36 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 37 | $ niv modify ${name} -a type=file -a builtin=true''; 38 | 39 | # 40 | # Various helpers 41 | # 42 | 43 | # The set of packages used when specs are fetched using non-builtins. 44 | mkPkgs = sources: 45 | let 46 | sourcesNixpkgs = 47 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {}; 48 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 49 | hasThisAsNixpkgsPath = == ./.; 50 | in 51 | if builtins.hasAttr "nixpkgs" sources 52 | then sourcesNixpkgs 53 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 54 | import {} 55 | else 56 | abort 57 | '' 58 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 59 | add a package called "nixpkgs" to your sources.json. 60 | ''; 61 | 62 | # The actual fetching function. 63 | fetch = pkgs: name: spec: 64 | 65 | if ! builtins.hasAttr "type" spec then 66 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 67 | else if spec.type == "file" then fetch_file pkgs spec 68 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 69 | else if spec.type == "git" then fetch_git spec 70 | else if spec.type == "local" then fetch_local spec 71 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 72 | else if spec.type == "builtin-url" then fetch_builtin-url name 73 | else 74 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 75 | 76 | # Ports of functions for older nix versions 77 | 78 | # a Nix version of mapAttrs if the built-in doesn't exist 79 | mapAttrs = builtins.mapAttrs or ( 80 | f: set: with builtins; 81 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 82 | ); 83 | 84 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 85 | range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); 86 | 87 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 88 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 89 | 90 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 91 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 92 | concatStrings = builtins.concatStringsSep ""; 93 | 94 | # fetchTarball version that is compatible between all the versions of Nix 95 | builtins_fetchTarball = { url, name, sha256 }@attrs: 96 | let 97 | inherit (builtins) lessThan nixVersion fetchTarball; 98 | in 99 | if lessThan nixVersion "1.12" then 100 | fetchTarball { inherit name url; } 101 | else 102 | fetchTarball attrs; 103 | 104 | # fetchurl version that is compatible between all the versions of Nix 105 | builtins_fetchurl = { url, sha256 }@attrs: 106 | let 107 | inherit (builtins) lessThan nixVersion fetchurl; 108 | in 109 | if lessThan nixVersion "1.12" then 110 | fetchurl { inherit url; } 111 | else 112 | fetchurl attrs; 113 | 114 | # Create the final "sources" from the config 115 | mkSources = config: 116 | mapAttrs ( 117 | name: spec: 118 | if builtins.hasAttr "outPath" spec 119 | then abort 120 | "The values in sources.json should not have an 'outPath' attribute" 121 | else 122 | spec // { outPath = fetch config.pkgs name spec; } 123 | ) config.sources; 124 | 125 | # The "config" used by the fetchers 126 | mkConfig = 127 | { sourcesFile ? ./sources.json 128 | , sources ? builtins.fromJSON (builtins.readFile sourcesFile) 129 | , pkgs ? mkPkgs sources 130 | }: rec { 131 | # The sources, i.e. the attribute set of spec name to spec 132 | inherit sources; 133 | 134 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 135 | inherit pkgs; 136 | }; 137 | in 138 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } 139 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | sources = import ./nix/sources.nix; 3 | nixpkgs = import sources.nixpkgs { 4 | overlays = [ (self: super: { niv = (import sources.niv {}).niv; }) ]; 5 | }; 6 | in 7 | with nixpkgs; 8 | stdenv.mkDerivation { 9 | name = "airplay-spec"; 10 | src = ./.; 11 | buildInputs = [ 12 | mdbook boot jdk12 niv 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | - [Introduction](./introduction.md) 3 | - [Service Discovery](./service_discovery.md) 4 | - [Features](./features.md) 5 | - [Status Flags](./status_flags.md) 6 | - [Photos](./photos/README.md) 7 | - [HTTP Requests](./photos/http_requests.md) 8 | - [Events](./photos/events.md) 9 | - [Photo Caching](./photos/photo_caching.md) 10 | - [Slideshows](./photos/slideshows.md) 11 | - [Video](./video/README.md) 12 | - [HTTP Requests](./video/http_requests.md) 13 | - [Events](./video/events.md) 14 | - [Audio](./audio/README.md) 15 | - [RTSP Requests](./audio/rtsp_requests/README.md) 16 | - [GET /info](./audio/rtsp_requests/get_info.md) 17 | - [POST /pair-setup](./audio/rtsp_requests/post_pair_setup.md) 18 | - [POST /pair-verify](./audio/rtsp_requests/post_pair_verify.md) 19 | - [POST /fp-setup](./audio/rtsp_requests/post_fp_setup.md) 20 | - [POST /auth-setup](./audio/rtsp_requests/post_auth_setup.md) 21 | - [OPTIONS](./audio/rtsp_requests/options.md) 22 | - [ANNOUNCE](./audio/rtsp_requests/announce.md) 23 | - [SETUP](./audio/rtsp_requests/setup.md) 24 | - [SETPEERS](./audio/rtsp_requests/setpeers.md) 25 | - [POST /command](./audio/rtsp_requests/post_command.md) 26 | - [POST /feedback](./audio/rtsp_requests/post_feedback.md) 27 | - [POST /audioMode](./audio/rtsp_requests/post_audio_mode.md) 28 | - [RECORD](./audio/rtsp_requests/record.md) 29 | - [FLUSH](./audio/rtsp_requests/flush.md) 30 | - [FLUSHBUFFERED](./audio/rtsp_requests/flushbuffered.md) 31 | - [TEARDOWN](./audio/rtsp_requests/teardown.md) 32 | - [RTP Streams](./audio/rtp_streams.md) 33 | - [Volume Control](./audio/volume_control.md) 34 | - [Metadata](./audio/metadata.md) 35 | - [AirPort Express Authentication](./audio/airport_express_authentication.md) 36 | - [Remote Control](./audio/remote_control.md) 37 | - [Screen Mirroring](./screen_mirroring/README.md) 38 | - [HTTP Requests](./screen_mirroring/http_requests.md) 39 | - [Stream Packets](./screen_mirroring/stream_packets.md) 40 | - [Time Synchronization](./screen_mirroring/time_synchronization.md) 41 | - [Pairing](./pairing/README.md) 42 | - [HomeKit Based Pairings](./pairing/hkp.md) 43 | - [Legacy Pairing](./pairing/legacy.md) 44 | - [Known Implementations](./known_implementations.md) 45 | - [Contributing](./contributing.md) 46 | - [History](./history.md) 47 | - [Resources](./resources.md) 48 | -------------------------------------------------------------------------------- /src/audio/README.md: -------------------------------------------------------------------------------- 1 | # Audio 2 | 3 | Audio streaming is supported using the [RTSP] protocol. 4 | -------------------------------------------------------------------------------- /src/audio/airport_express_authentication.md: -------------------------------------------------------------------------------- 1 | # AirPort Express Authentication 2 | 3 | Sending audio data to the AirPort Express requires a *RSA* based 4 | authentication. All binary data are encoded using [Base64] without padding. 5 | 6 | ## Client side 7 | 8 | - In the `ANNOUNCE` request, the client sends a 128-bit random number in 9 | the `Apple-Challenge` header. 10 | - A 128-bit *AES* key is generated, encrypted with the RSA public key 11 | using the *OAEP* encryption scheme, and sent along with an 12 | initialization vector in the `rsaaeskey` and `aesiv` SDP attributes. 13 | 14 | ## Server side 15 | 16 | - The AirPort Express decrypts the AES key with its RSA private key, it 17 | will be used to decrypt the audio payload. 18 | - The AirPort Express signs the `Apple-Challenge` number with its RSA 19 | private key using the *PKCS#1* signature scheme and send the result in 20 | the `Apple-Response` header. 21 | 22 | ## Client side 23 | 24 | - The client decrypts the `Apple-Response` value with the RSA public key, 25 | and checks that it is the same random number it has previously generated. 26 | 27 | Example: AirPort Express challenge/response 28 | 29 |
30 |

client → server

31 | 32 | ```http 33 | ANNOUNCE rtsp://10.0.1.101/3172942895 RTSP/1.0 34 | CSeq: 1 35 | Content-Type: application/sdp 36 | Content-Length: 567 37 | User-Agent: iTunes/4.6 (Windows; N) 38 | Client-Instance: 9FF35780A8BC8D2B 39 | Apple-Challenge: 09KF45soMYmvj6dpsUGiIg 40 | 41 | v=0 42 | o=iTunes 3172942895 0 IN IP4 10.0.1.101 43 | s=iTunes 44 | c=IN IP4 10.0.1.103 45 | t=0 0 46 | m=audio 0 RTP/AVP 96 47 | a=rtpmap:96 AppleLossless 48 | a=fmtp:96 4096 0 16 40 10 14 2 255 0 0 44100 49 | a=rsaaeskey:5QYIqmdZGTONY5SHjEJrqAhaa0W9wzDC5i6q221mdGZJ5ubO6Kg 50 | yhC6U83wpY87TFdPRdfPQl2kVC7+Uefmx1bXdIUo07ZcJsqMbgtje4w2JQw0b 51 | Uw2BlzNPmVGQOxfdpGc3LXZzNE0jI1D4conUEiW6rrzikXBhk7Y/i2naw13ayy 52 | xaSwtkiJ0ltBQGYGErbV2tx43QSNj7O0JIG9GrF2GZZ6/UHo4VH+ZXgQ4NZvP/ 53 | QXPCsLutZsvusFDzIEq7TN1fveINOiwrzlN+bckEixvhXlvoQTWE2tjbmQYhMvO 54 | FIly5gNbZiXi0l5AdolX4jDC2vndFHqWDks/3sPikNg 55 | a=aesiv:zcZmAZtqh7uGcEwPXk0QeA 56 | ``` 57 |
58 |
59 |

server → client

60 | 61 | ```http 62 | RTSP/1.0 200 OK 63 | CSeq: 1 64 | Apple-Response: u+msU8Cc7KBrVPjI/Ir8fOL8+C5D3Jsw1+acaW3MNTndrTQAeb/a 65 | 5m10UVBX6wb/DYQGY+b28ksSwBjN0nFOk4Y2cODEf83FAh7B 66 | mkLpmpkpplp7zVXQ+Z9DcB6gC60ZsS3t98aoR7tSzVLKZNgi2X2sC+vGsz 67 | utQxX03HK008VjcdngHv3g1p2knoETd07T6eVfZCmPqp6Ga7Dj8VIIj/GEP3 68 | AjjDx3lJnQBXUDmxM484YXLXZjWFXCiY8GJt6whjf7/2c3rIoT3Z7PQpEvPmM 69 | 1MXU9cv4NL59Y/q0OAVQ38foOz7eGAhfvjOsCnHU25aik7/7ToIYt1tyVtap/kA 70 | Audio-Jack-Status: connected; type=analog 71 | ``` 72 |
73 | -------------------------------------------------------------------------------- /src/audio/info.md: -------------------------------------------------------------------------------- 1 | # Get info 2 | 3 |
4 |

client → server

5 | 6 | ```http 7 | GET /info RTSP/1.0 8 | X-Apple-ProtocolVersion: 1 9 | CSeq: 0 10 | DACP-ID: ADA239F4521B1802 11 | Active-Remote: 753030410 12 | User-Agent: AirPlay/380.10.1 13 | ``` 14 |
15 | 16 |
17 |

server → client

18 | 19 | ```http 20 | RTSP/1.0 200 OK 21 | Content-Length: 927 22 | Content-Type: application/x-apple-binary-plist 23 | Server: AirTunes/366.0 24 | CSeq: 0 25 | 26 | 27 | ``` 28 |
29 | 30 | ```xml 31 | {{#include ../data/examples/Denon-AVR-X3500H/info.plist.xml}} 32 | ``` 33 | 34 | ```base64 35 | {{#include ../data/examples/Denon-AVR-X3500H/info.plist.base64}} 36 | ``` 37 |
38 |
39 | -------------------------------------------------------------------------------- /src/audio/metadata.md: -------------------------------------------------------------------------------- 1 | # Metadata 2 | 3 | Metadata for the current track are sent using `SET_PARAMETER` requests. 4 | This allows the Apple TV to show the track name, artist, album, cover 5 | artwork and timeline. The `RTP-Info` header contains a `rtptime` parameter 6 | with the RTP timestamp corresponding to the time from which the metadata 7 | is valid. 8 | 9 | | md | bit | description | 10 | |----|-----|-------------| 11 | | 0 | 17 | text | 12 | | 1 | 15 | artwork | 13 | | 2 | 16 | progress | 14 | | | 50 | bplist | 15 | 16 | 17 | ## Track Informations 18 | 19 | Informations about the current track are sent in the [DAAP] format, with 20 | `application/x-dmap-tagged` content type. 21 | 22 | The following DAAP attributes are displayed on Apple TV: 23 | 24 | | attributes | description | 25 | |-----------------|-------------| 26 | | dmap.itemname | track name | 27 | | daap.songartist | artist | 28 | | daap.songalbum | album | 29 | 30 | Example: send track informations 31 | 32 |
33 |

client → server

34 | 35 | ```http 36 | SET_PARAMETER rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 37 | CSeq: 8 38 | Session: 1 39 | Content-Type: application/x-dmap-tagged 40 | Content-Length: 3242 41 | RTP-Info: rtptime=1146549156 42 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 43 | Client-Instance: 56B29BB6CB904862 44 | DACP-ID: 56B29BB6CB904862 45 | Active-Remote: 1986535575 46 | 47 | 48 | ``` 49 |
50 |
51 |

server → client

52 | 53 | ```http 54 | RTSP/1.0 200 OK 55 | Server: AirTunes/130.14 56 | CSeq: 8 57 | ``` 58 |
59 | 60 | 61 | ## Cover Artwork 62 | 63 | Artworks are sent as *JPEG* pictures, with `image/jpeg` content type. 64 | 65 | Example: send cover artwork 66 | 67 |
68 |

client → server

69 | 70 | ```http 71 | SET_PARAMETER rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 72 | CSeq: 9 73 | Session: 1 74 | Content-Type: image/jpeg 75 | Content-Length: 34616 76 | RTP-Info: rtptime=1146549156 77 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 78 | Client-Instance: 56B29BB6CB904862 79 | DACP-ID: 56B29BB6CB904862 80 | Active-Remote: 1986535575 81 | 82 | 83 | ``` 84 |
85 |
86 |

server → client

87 | 88 | ```http 89 | RTSP/1.0 200 OK 90 | Server: AirTunes/130.14 91 | CSeq: 9 92 | ``` 93 |
94 | 95 | 96 | ## Playback Progress 97 | 98 | Playback progress is sent as `text/parameters`, with a `progress` parameter 99 | representing three absolute RTP timestamps values: 100 | `start` / `curr` / `end`. 101 | 102 | | timestamp | description | 103 | |-----------|--------------------------------| 104 | | start | beginning of the current track | 105 | | curr | current playback position | 106 | | end | end of the current track | 107 | 108 | The relative position and track duration can be computed as follows: 109 | 110 | - `position = rtptime_to_sec(curr - start)` 111 | - `duration = rtptime_to_sec(end - start)` 112 | 113 | Example: send playback progress 114 | 115 |
116 |

client → server

117 | 118 | ```http 119 | SET_PARAMETER rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 120 | CSeq: 10 121 | Session: 1 122 | Content-Type: text/parameters 123 | Content-Length: 44 124 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) 125 | AppleWebKit/535.18.5 126 | Client-Instance: 56B29BB6CB904862 127 | DACP-ID: 56B29BB6CB904862 128 | Active-Remote: 1986535575 129 | 130 | progress: 1146221540/1146549156/1195701740 131 | ``` 132 |
133 |
134 |

server → client

135 | 136 | ```http 137 | RTSP/1.0 200 OK 138 | Server: AirTunes/130.14 139 | CSeq: 10 140 | ``` 141 |
142 | -------------------------------------------------------------------------------- /src/audio/remote_control.md: -------------------------------------------------------------------------------- 1 | # Remote Control 2 | 3 | Audio speakers can send commands to the AirPlay client to change the 4 | current track, pause and resume playback, shuffle the playlist, and 5 | more. This uses a subset of [DACP]. 6 | An AirPlay client advertises this capability by including a `DACP-ID` header in 7 | its RTSP requests, with a 64-bit ID for the DACP server. An `Active-Remote` 8 | header is included as well, serving as an authentication token. 9 | 10 | The AirPlay server needs to browse the mDNS `_dacp._tcp` services for a 11 | matching DACP server. Server names look like `iTunes_Ctrl_$ID`. 12 | 13 |

DACP service from iTunes

14 | 15 | ``` 16 | name: iTunes_Ctrl_56B29BB6CB904862 17 | type: _dacp._tcp 18 | port: 3689 19 | txt: 20 | txtvers=1 21 | Ver=131075 22 | DbId=63B5E5C0C201542E 23 | OSsi=0x1F5 24 | ``` 25 | 26 | Once the DACP server has been identified, HTTP requests can be sent to the 27 | corresponding service port. The `Active-Remote` header must be included in 28 | these requests, so no additional pairing is required. The location for 29 | remote control commands is `/ctrl-int/1/$CMD`. The following commands are 30 | available: 31 | 32 | | command | description | 33 | |---------------|-----------------------------------| 34 | | beginff | begin fast forward | 35 | | beginrew | begin rewind | 36 | | mutetoggle | toggle mute status | 37 | | nextitem | play next item in playlist | 38 | | previtem | play previous item in playlist | 39 | | pause | pause playback | 40 | | playpause | toggle between play and pause | 41 | | play | start playback | 42 | | stop | stop playback | 43 | | playresume | play after fast forward or rewind | 44 | | shuffle_songs | shuffle playlist | 45 | | volumedown | turn audio volume down | 46 | | volumeup | turn audio volume up | 47 | 48 | Example: send a pause command 49 | 50 |
51 |

server → client

52 | 53 | ```http 54 | GET /ctrl-int/1/pause HTTP/1.1 55 | Host: starlight.local. 56 | Active-Remote: 1986535575 57 | ``` 58 |
59 |
60 |

client → server

61 | 62 | ```http 63 | HTTP/1.1 204 No Content 64 | Date: Tue, 06 Mar 2012 16:38:51 GMT 65 | DAAP-Server: iTunes/10.6 (Mac OS X) 66 | Content-Type: application/x-dmap-tagged 67 | Content-Length: 0 68 | ``` 69 |
70 | -------------------------------------------------------------------------------- /src/audio/rtp_streams.md: -------------------------------------------------------------------------------- 1 | # RTP Streams 2 | 3 | Audio packets are fully RTP compliant. Control and timing packets, 4 | however, do not seem to be fully compliant with the RTP standard. 5 | 6 | The following payload types are defined: 7 | 8 | | payload type | port | description | 9 | |--------------|--------------|--------------------| 10 | | 82 | timing_port | timing request | 11 | | 83 | timing_port | timing reply | 12 | | 84 | control_port | time sync | 13 | | 85 | control_port | retransmit request | 14 | | 86 | control_port | retransmit reply | 15 | | 96 | server_port | audio data | 16 | 17 | ## Audio packets 18 | 19 | Audio data is sent using the `DynamicRTP-Type-96` payload type. The `Marker` 20 | bit is set on the first packet sent after `RECORD` or `FLUSH` requests. The RTP 21 | payload contains optionally encrypted audio data. 22 | 23 | Example: encrypted audio packet 24 | 25 |
26 |

client → server

27 | 28 | ```hex 29 | 0000 80 e0 b1 91 f7 79 16 c2 e8 bb 6b 2c bb 5c 8e 51 30 | 0010 aa 7c d2 96 00 c3 fd 60 eb ae 6e 41 31 38 fe ae 31 | .... 32 | 03e0 cb 1c 73 bf e7 05 93 30 fa 85 7f 32 77 8d a8 97 33 | 03f0 a0 c7 c8 78 7b e5 81 a1 4f b4 3e a3 43 db 7c 34 | 35 | Real-Time Transport Protocol 36 | 10.. .... = Version: RFC 1889 Version (2) 37 | ..0. .... = Padding: False 38 | ...0 .... = Extension: False 39 | .... 0000 = Contributing source identifiers count: 0 40 | 1... .... = Marker: True 41 | Payload type: DynamicRTP-Type-96 (96) 42 | Sequence number: 45457 43 | Timestamp: 4151908034 44 | Synchronization Source identifier: 0xe8bb6b2c (3904596780) 45 | Payload: bb5c8e51aa7cd29600c3fd60ebae6e413138feae909b44f1... 46 | ``` 47 |
48 | 49 | ## Sync packets 50 | 51 | Sync packets are sent once per second to the control port. They are used to 52 | correlate the RTP timestamps currently used in the audio stream to the NTP time 53 | used for clock synchronization. Payload type is 84, the `Marker` bit is always 54 | set and the `Extension` bit is set on the first packet after `RECORD` or 55 | `FLUSH` requests. The `SSRC` field is not included in the RTP header. 56 | 57 | | bytes | description | 58 | |-------|-----------------------------------------| 59 | | 8 | RTP header without `SSRC` | 60 | | 8 | current NTP time | 61 | | 4 | RTP timestamp for the next audio packet | 62 | 63 | 64 | Example: sync packet 65 | 66 |
67 |

client → server

68 | 69 | ```hex 70 | 0000 80 d4 00 04 c7 cd 11 a8 83 ab 1c 49 2f e4 22 e2 71 | 0010 c7 ce 3f 1f 72 | 73 | Real-Time Transport Protocol 74 | 10.. .... = Version: RFC 1889 Version (2) 75 | ..0. .... = Padding: False 76 | ...0 .... = Extension: False 77 | .... 0000 = Contributing source identifiers count: 0 78 | 1... .... = Marker: True 79 | Payload type: Unassigned (84) 80 | Sequence number: 4 81 | Timestamp: 3352105384 82 | Synchronization Source identifier: 0x83ab1c49 (2209029193) 83 | Payload: 2fe422e2c7ce3f1f 84 | ``` 85 |
86 | 87 | ## Retransmit packets 88 | 89 | AirTunes supports resending audio packets which have been lost. Payload 90 | type is 85 for retransmit queries, the `Marker` bit is always set and the 91 | `SSRC` field is not included in the RTP header. 92 | 93 | | bytes | description | 94 | |-------|-------------------------------------------| 95 | | 8 | RTP header without `SSRC` | 96 | | 2 | sequence number for the first lost packet | 97 | | 2 | number of lost packets | 98 | 99 | Retransmit replies have payload type 86, with a full audio RTP packet 100 | after the sequence number. 101 | 102 | ## Timing packets 103 | 104 | Timing packets are used to synchronize a master clock for audio. This is 105 | useful for clock recovery and precise synchronization of several devices 106 | playing the same audio stream. 107 | 108 | Timing packets are sent at 3 second intervals. They always have the 109 | `Marker` bit set, and payload type 82 for queries and 83 for replies. 110 | The `SSRC` field is not included in the RTP header, so it takes only 8 111 | bytes, followed by three *NTP* timestamps: 112 | 113 | | bytes | description | 114 | |-------|---------------------------| 115 | | 8 | RTP header without `SSRC` | 116 | | 8 | origin timestamp | 117 | | 8 | receive timestamp | 118 | | 8 | transmit timestamp | 119 | 120 | Example: timing query/reply 121 | 122 |
123 |

server → client

124 | 125 | ```hex 126 | 0000 80 d2 00 07 00 00 00 00 00 00 00 00 00 00 00 00 127 | 0010 00 00 00 00 00 00 00 00 83 c1 17 cc af ba 9b 32 128 | 129 | Real-Time Transport Protocol 130 | 10.. .... = Version: RFC 1889 Version (2) 131 | ..0. .... = Padding: False 132 | ...0 .... = Extension: False 133 | .... 0000 = Contributing source identifiers count: 0 134 | 1... .... = Marker: True 135 | Payload type: Unassigned (82) 136 | Sequence number: 7 137 | Timestamp: 0 138 | Synchronization Source identifier: 0x00000000 (0) 139 | Payload: 00000000000000000000000083c117ccafba9b32 140 | ``` 141 |
142 |
143 |

client → server

144 | 145 | ```hex 146 | 0000 80 d3 00 07 00 00 00 00 83 c1 17 cc af ba 9b 32 147 | 0010 83 c1 17 cc b0 12 ce b6 83 c1 17 cc b0 14 10 47 148 | 149 | Real-Time Transport Protocol 150 | 10.. .... = Version: RFC 1889 Version (2) 151 | ..0. .... = Padding: False 152 | ...0 .... = Extension: False 153 | .... 0000 = Contributing source identifiers count: 0 154 | 1... .... = Marker: True 155 | Payload type: Unassigned (83) 156 | Sequence number: 7 157 | Timestamp: 0 158 | Synchronization Source identifier: 0x83c117cc (2210469836) 159 | Payload: afba9b3283c117ccb012ceb683c117ccb0141047 160 | ``` 161 |
162 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/README.md: -------------------------------------------------------------------------------- 1 | # RTSP Requests 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/announce.md: -------------------------------------------------------------------------------- 1 | # ANNOUNCE 2 | 3 | The `ANNOUNCE` request tells the RTSP server about stream properties using 4 | [SDP]. Codec informations and encryption keys are of particular interest. 5 | 6 | Example 1: `ANNOUNCE` for *Apple Lossless* audio from iTunes 7 | 8 |
9 |

client → server

10 | 11 | ```http 12 | ANNOUNCE rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 13 | CSeq: 3 14 | Content-Type: application/sdp 15 | Content-Length: 348 16 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 17 | Client-Instance: 56B29BB6CB904862 18 | DACP-ID: 56B29BB6CB904862 19 | Active-Remote: 1986535575 20 | 21 | v=0 22 | o=iTunes 3413821438 0 IN IP4 fe80::217:f2ff:fe0f:e0f6 23 | s=iTunes 24 | c=IN IP4 fe80::5a55:caff:fe1a:e187 25 | t=0 0 26 | m=audio 0 RTP/AVP 96 27 | a=rtpmap:96 AppleLossless 28 | a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100 29 | a=fpaeskey:RlBMWQECAQAAAAA8AAAAAPFOnNe+zWb5/n4L5KZkE2AAAAAQlDx69reTdwHF9LaNmhiRURTAbcL4brYAceAkZ49YirXm62N4 30 | a=aesiv:5b+YZi9Ikb845BmNhaVo+Q 31 | ``` 32 |
33 |
34 |

server → client

35 | 36 | ```http 37 | RTSP/1.0 200 OK 38 | Server: AirTunes/130.14 39 | CSeq: 3 40 | ``` 41 |
42 | 43 | Example 2: `ANNOUNCE` for *AAC* audio from an iOS 44 | device 45 | 46 |
47 |

client → server

48 | 49 | ```http 50 | ANNOUNCE rtsp://192.168.1.45/2699324803567405959 RTSP/1.0 51 | X-Apple-Device-ID: 0xa4d1d2800b68 52 | CSeq: 16 53 | DACP-ID: 14413BE4996FEA4D 54 | Active-Remote: 2543110914 55 | Content-Type: application/sdp 56 | Content-Length: 331 57 | 58 | v=0 59 | o=AirTunes 2699324803567405959 0 IN IP4 192.168.1.5 60 | s=AirTunes 61 | c=IN IP4 192.168.1.5 62 | t=0 0 63 | m=audio 0 RTP/AVP 96 64 | a=rtpmap:96 mpeg4-generic/44100/2 65 | a=fmtp:96 66 | a=fpaeskey:RlBMWQECAQAAAAA8AAAAAOG6c4aMdLkXAX+lbjp7EhgAAAAQeX5uqGyYkBmJX+gd5ANEr+amI8urqFmvcNo87pR0BXGJ4eLf 67 | a=aesiv:VZTaHn4wSJ84Jjzlb94m0Q== 68 | a=min-latency:11025 69 | ``` 70 |
71 |
72 |

server → client

73 | 74 | ```http 75 | RTSP/1.0 200 OK 76 | Server: AirTunes/130.14 77 | CSeq: 16 78 | ``` 79 |
80 | 81 | Example 3: `ANNOUNCE` for *AAC-ELD* audio and *H.264* 82 | video from an iOS device 83 | 84 |
85 |

client → server

86 | 87 | ```http 88 | ANNOUNCE rtsp://192.168.1.45/846700446248110360 RTSP/1.0 89 | X-Apple-Device-ID: 0xa4d1d2800b68 90 | CSeq: 27 91 | DACP-ID: 14413BE4996FEA4D 92 | Active-Remote: 2543110914 93 | Content-Type: application/sdp 94 | Content-Length: 415 95 | 96 | v=0 97 | o=AirTunes 846700446248110360 0 IN IP4 192.168.1.5 98 | s=AirTunes 99 | c=IN IP4 192.168.1.5 100 | t=0 0 101 | m=audio 0 RTP/AVP 96 102 | a=rtpmap:96 mpeg4-generic/44100/2 103 | a=fmtp:96 mode=AAC-eld; constantDuration=480 104 | a=fpaeskey:RlBMWQECAQAAAAA8AAAAAKKp+t27A+686xfviEphhw8AAAAQE/3LSqv9MHgnEKxkbKh1buE9+ylKg0YuqcyAC7fT0EqJNtdq 105 | a=aesiv:i/a3nUKYNDSIPP2fC+UKGQ== 106 | a=min-latency:4410 107 | m=video 0 RTP/AVP 97 108 | a=rtpmap:97 H264 109 | a=fmtp:97 110 | ``` 111 |
112 |
113 |

server → client

114 | 115 | ```http 116 | RTSP/1.0 200 OK 117 | Server: AirTunes/130.14 118 | CSeq: 27 119 | ``` 120 |
121 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/flush.md: -------------------------------------------------------------------------------- 1 | # FLUSH 2 | 3 | The `FLUSH` request stops the streaming. 4 | 5 | Example: pause the audio stream 6 | 7 |
8 |

client → server

9 | 10 | ```http 11 | FLUSH rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 12 | CSeq: 31 13 | Session: 1 14 | RTP-Info: seq=25009;rtptime=1148010660 15 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 16 | Client-Instance: 56B29BB6CB904862 17 | DACP-ID: 56B29BB6CB904862 18 | Active-Remote: 1986535575 19 | ``` 20 |
21 |
22 |

server → client

23 | 24 | ```http 25 | RTSP/1.0 200 OK 26 | RTP-Info: rtptime=1147914212 27 | Server: AirTunes/130.14 28 | CSeq: 31 29 | ``` 30 |
31 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/flushbuffered.md: -------------------------------------------------------------------------------- 1 | # FLUSHBUFFERED 2 | 3 | Applies to: Airplay2 4 | 5 | The `FLUSHBUFFERED` is used to request the flush of the buffer, in a buffered audio usage context (see SupportsBufferedAudio feature) 6 | 7 |
8 |

client → server

9 | 10 | ```http 11 | FLUSHBUFFERED rtsp://192.168.128.227/4361780817803627124 RTSP/1.0 12 | Content-Length: 87 13 | Content-Type: application/x-apple-binary-plist 14 | CSeq: 12 15 | DACP-ID: C0EF6588CF6D70AC 16 | Active-Remote: 1641836738 17 | User-Agent: AirPlay/387.2 18 | 19 | 20 | ``` 21 |
22 |
23 |

server → client

24 | 25 | ```http 26 | RTSP/1.0 200 OK 27 | Server: AirTunes/366.0 28 | CSeq: 12 29 | ``` 30 |
31 | 32 | binary plist includes the RTP sequence & timestamp to flush up to: 33 | ``` 34 | {'flushUntilSeq': 15363674, 'flushUntilTS': 239565966} 35 | ``` 36 | and optionally, the RTP sequence & timestamp to flush from: 37 | ``` 38 | {'flushFromSeq': 15380707, 39 | 'flushFromTS': 1554930586, 40 | 'flushUntilSeq': 15381800, 41 | 'flushUntilTS': 1556050842} 42 | ``` 43 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/get_info.md: -------------------------------------------------------------------------------- 1 | # GET /info 2 | 3 | |key |type |value |description | 4 | |----------|--------|------------------|-----------------| 5 | |PTPInfo|string| 6 | |build|string| 7 | |deviceID |string |58:55:CA:1A:E2:88 |MAC address | 8 | |features |integer |14839 | [features](../../features.md) bits as decimal value | 9 | |initialVolume|real||| 10 | |macAddress|string||| 11 | |firmwareBuildDate|string||| 12 | |firmwareRevision|string||| 13 | |keepAliveLowPower|boolean||| 14 | |keepAliveSendStatsAsBody|boolean||| 15 | |manufacturer|string|| 16 | |model |string |AppleTV2,1 |device model | 17 | |name|string||| 18 | |nameIsFactoryDefault|boolean||| 19 | |pi|string||| 20 | |pk|data||| 21 | |playbackCapabilities.supportsFPSSecureStop|boolean||| 22 | |playbackCapabilities.supportsUIForAudioOnlyContent|boolean||| 23 | |protocolVersion |string |1.0 |protocol version | 24 | |psi|string| 25 | |senderAddress|string| 26 | |sdk|string| 27 | |sourceVersion |string |120.2 |server version | 28 | |statusFlags|integer|4| [status flags](../../status_flags.md) as decimal value | 29 | |txtAirPlay|data|...|raw TXT record from [AirPlay service](../../service_discovery.md) mDNS record | 30 | |txtRAOP|data|...|raw TXT record from [AirTunes service](../../service_discovery.md) mDNS record | 31 | |volumeControlType|integer| 32 | |vv|integer| 33 | 34 |
35 |

client → server

36 | 37 | ```http 38 | GET /info RTSP/1.0 39 | X-Apple-ProtocolVersion: 1 40 | CSeq: 0 41 | DACP-ID: ADA239F4521B1802 42 | Active-Remote: 753030410 43 | User-Agent: AirPlay/380.10.1 44 | ``` 45 |
46 | 47 |
48 |

server → client

49 | 50 | ```http 51 | RTSP/1.0 200 OK 52 | Content-Length: 927 53 | Content-Type: application/x-apple-binary-plist 54 | Server: AirTunes/366.0 55 | CSeq: 0 56 | 57 | 58 | ``` 59 |
60 | 61 | ```xml 62 | {{#include ../../data/examples/Denon-AVR-X3500H/info.plist.xml}} 63 | ``` 64 | 65 | ```base64 66 | {{#include ../../data/examples/Denon-AVR-X3500H/info.plist.base64}} 67 | ``` 68 |
69 |
70 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/options.md: -------------------------------------------------------------------------------- 1 | # OPTIONS 2 | 3 | The `OPTIONS` request asks the RTSP server for its supported methods. 4 | Apple TV supports the following methods: `ANNOUNCE`, `SETUP`, `RECORD`, 5 | `PAUSE`, `FLUSH`, `TEARDOWN`, `OPTIONS`, `GET_PARAMETER`, `SET_PARAMETER`, 6 | `POST` and `GET`. 7 | 8 |
9 |

client → server

10 | 11 | ```http 12 | OPTIONS * RTSP/1.0 13 | CSeq: 3 14 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 15 | Client-Instance: 56B29BB6CB904862 16 | DACP-ID: 56B29BB6CB904862 17 | Active-Remote: 1986535575 18 | ``` 19 |
20 |
21 |

server → client

22 | 23 | ```http 24 | RTSP/1.0 200 OK 25 | Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, 26 | GET_PARAMETER, SET_PARAMETER, POST, GET 27 | Server: AirTunes/130.14 28 | CSeq: 3 29 | ``` 30 |
31 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_audio_mode.md: -------------------------------------------------------------------------------- 1 | # POST /audioMode 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_auth_setup.md: -------------------------------------------------------------------------------- 1 | # POST /auth-setup 2 | 3 | In case MFi authentication is supported (HasUnifiedAdvertiserInfo), it can be raised either with a auth-setup challenge or included in the pairing process if supported (SupportsUnifiedPairSetupAndMFi). 4 | This section describes the first option. 5 | 6 | Note that even if it is a server authentication (so that clients ensure MFi authenticity), with Airplay2 devices this step cannot be ignored from a client implementation point of view. Meaning, even if the 7 | authentication/signature is not checked on client side, the request has to be done, otherwise the server will deny further requests 8 | 9 | The challenge process is the following: 10 | 1. Generation of Curve25119 key pairs on both client and server 11 | 2. Client send its public key to server 12 | 3. Server append its public key with client's one, this will be the message to sign. It then gets the signature from Apple authentication IC. RSA-1024 is used, with SHA-1 hash algorithm. 13 | 4. Signature is encrypted with AES-128 in Counter mode with: 14 | - Key = 16 first bytes of `SHA1(<7:AES-KEY><32:Curve25119 Shared key>)` 15 | - IV = 16 first bytes of `SHA1(<6:AES-IV><32:Curve25119 Shared key>)` 16 | 5. Server respond with it's Curve25119 public key, the encrypted signature and the certificate. 17 | 18 | A pair-setup request body has the following format: 19 | 20 | `<1:Encryption Type>` 21 | `<32:Client’s Curve25119 public key>` 22 | 23 | Where encryption type is: 24 | 25 | |value |type | 26 | |----------|----------------------------| 27 | |0x00 |Invalid | 28 | |0x01 |Unencrypted | 29 | |0x10 |MFi-SAP-encrypted AES key. | 30 | 31 | A pair-setup response body has the following format: 32 | 33 | ``` 34 | <32:Server’s Curve25119 public key> 35 | <4:Certificate length (int32be)> 36 | 37 | <4:Signature length (int32be)> 38 | 39 | ``` 40 |
41 |

client → server

42 | 43 | ```http 44 | POST /auth-setup RTSP/1.0 45 | Content-Type: application/octet-stream 46 | Content-Length: 33 47 | CSeq: 1 48 | User-Agent: iTunes/7.6.2 (Windows; N;) 49 | Client-Instance: 050748FD6F0853AC 50 | DACP-ID: 124D322761E2EC16 51 | Client-instance-identifier: 5743a6b4-e835-412f-9894-14011e386cf2 52 | 53 | 54 | ``` 55 |
56 |
57 |

server → client

58 | 59 | ```http 60 | RTSP/1.0 200 OK 61 | Content-Length: 1076 62 | Content-Type: application/octet-stream 63 | Server: AirTunes/366.0 64 | CSeq: 1 65 | 66 | 67 | ``` 68 |
69 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_command.md: -------------------------------------------------------------------------------- 1 | # POST /command 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_feedback.md: -------------------------------------------------------------------------------- 1 | # POST /feedback 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_fp_setup.md: -------------------------------------------------------------------------------- 1 | # POST /fp-setup 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_pair_setup.md: -------------------------------------------------------------------------------- 1 | # POST /pair-setup 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/post_pair_verify.md: -------------------------------------------------------------------------------- 1 | # POST /pair-verify 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/record.md: -------------------------------------------------------------------------------- 1 | # RECORD 2 | 3 | The `RECORD` request starts the audio streaming. The `RTP-Info` header 4 | contains the following parameters: 5 | 6 | |name |size |description | 7 | |---------|--------|-----------------------------| 8 | | seq | 16-bit | initial RTP sequence number | 9 | | rtptime | 32-bit | initial RTP timestamp | 10 | 11 | Example: start audio stream 12 | 13 |
14 |

client → server

15 | 16 | ```http 17 | RECORD rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 18 | CSeq: 5 19 | Session: 1 20 | Range: npt=0- 21 | RTP-Info: seq=20857;rtptime=1146549156 22 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 23 | Client-Instance: 56B29BB6CB904862 24 | DACP-ID: 56B29BB6CB904862 25 | Active-Remote: 1986535575 26 | ``` 27 |
28 |
29 |

server → client

30 | 31 | ```http 32 | RTSP/1.0 200 OK 33 | Audio-Latency: 2205 34 | Server: AirTunes/130.14 35 | CSeq: 5 36 | ``` 37 |
38 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/setpeers.md: -------------------------------------------------------------------------------- 1 | # SETPEERS 2 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/setup.md: -------------------------------------------------------------------------------- 1 | # SETUP 2 | 3 | The `SETUP` request initializes a record session. It sends all the 4 | necessary transport informations. Three UDP channels are setup: 5 | 6 | | channel | description | 7 | |----------|------------------------------| 8 | | server | audio data | 9 | | control | sync and retransmit requests | 10 | | timing | master clock sync | 11 | 12 | Example: setup a record session 13 | 14 |
15 |

client → server

16 | 17 | ```http 18 | SETUP rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 19 | CSeq: 4 20 | Transport: RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;control_port=6001;timing_port=6002 21 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 22 | Client-Instance: 56B29BB6CB904862 23 | DACP-ID: 56B29BB6CB904862 24 | Active-Remote: 1986535575 25 | ``` 26 |
27 |
28 |

server → client

29 | 30 | ```http 31 | RTSP/1.0 200 OK 32 | Transport: RTP/AVP/UDP;unicast;mode=record;server_port=53561;control_port=63379;timing_port=50607 33 | Session: 1 34 | Audio-Jack-Status: connected 35 | Server: AirTunes/130.14 36 | CSeq: 4 37 | ``` 38 |
39 | -------------------------------------------------------------------------------- /src/audio/rtsp_requests/teardown.md: -------------------------------------------------------------------------------- 1 | # TEARDOWN 2 | 3 | The `TEARDOWN` request ends the RTSP session. 4 | 5 | Example: close session 1 6 | 7 |
8 |

client → server

9 | 10 | ```http 11 | TEARDOWN rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 12 | CSeq: 32 13 | Session: 1 14 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 15 | Client-Instance: 56B29BB6CB904862 16 | DACP-ID: 56B29BB6CB904862 17 | Active-Remote: 1986535575 18 | ``` 19 |
20 |
21 |

server → client

22 | 23 | ```http 24 | RTSP/1.0 200 OK 25 | Server: AirTunes/130.14 26 | CSeq: 32 27 | ``` 28 |
29 | -------------------------------------------------------------------------------- /src/audio/volume_control.md: -------------------------------------------------------------------------------- 1 | # Volume Control 2 | 3 | Audio volume can be changed using a `SET_PARAMETER` request. The volume 4 | is a float value representing the audio attenuation in dB. A value of 5 | –144 means the audio is muted. Then it goes from –30 to 0. 6 | 7 | Example: set audio volume 8 | 9 |
10 |

client → server

11 | 12 | ```http 13 | SET_PARAMETER rtsp://fe80::217:f2ff:fe0f:e0f6/3413821438 RTSP/1.0 14 | CSeq: 6 15 | Session: 1 16 | Content-Type: text/parameters 17 | Content-Length: 20 18 | User-Agent: iTunes/10.6 (Macintosh; Intel Mac OS X 10.7.3) AppleWebKit/535.18.5 19 | Client-Instance: 56B29BB6CB904862 20 | DACP-ID: 56B29BB6CB904862 21 | Active-Remote: 1986535575 22 | 23 | volume: -11.123877 24 | ``` 25 |
26 |
27 |

server → client

28 | 29 | ```http 30 | RTSP/1.0 200 OK 31 | Server: AirTunes/130.14 32 | CSeq: 6 33 | ``` 34 |
35 | -------------------------------------------------------------------------------- /src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Device Data 4 | 5 | If anybody wants to help out who has some Airplay compatible hardware we 6 | can always use some more data. Primarily we right now need mDNS records 7 | and the results from RTSP GET /info/ requests. If you have 8 | a computer other than a windows machine I have written some short guides 9 | below and if that is not enough I can probably help you if you send 10 | an e-mail to brian@maven-group.org. 11 | 12 | We right now have data for a couple of 4K Apple TVs, an AirPort Express, 13 | two stereo paired HomePods, the Libretone LTH200 and a Denon AVR-X3500X 14 | surround receiver so if your hardware is something else I am especially 15 | interested in the data from it. 16 | 17 | Any data that you send will only be viewed by me and I will change stuff 18 | like device names, ip addresses, MAC addresses, serial numbers and UUIDs 19 | before including the data in this spec. 20 | 21 | ### Linux 22 | 23 | If you are on Linux and have avahi installed you can run the following 24 | shell script to gather all the data. 25 | 26 | ```bash 27 | #!/bin/bash 28 | avahi-browse -prt _airplay._tcp | awk -v "FS=;" '{ print $7 }' | sed '/^\s*$/d' > devices.txt 29 | avahi-browse -aprt | grep -f devices.txt > mDNS.txt 30 | for host in $(cat devices.txt) ; do 31 | PORT="$(avahi-browse -prt _airplay._tcp | grep ";$host;" | awk -v "FS=;" '{print $9}' | sed '/^\s*$/d')" 32 | curl https://openairplay.github.io/airplay-spec/data/RTSP-get-info-req.bin | nc -w 2 "$host" $PORT > "RTSP-get-info-res-$host.bin" 33 | done 34 | tar czvf device-data.tar.gz mDNS.txt RTSP-get-info-res* 35 | ``` 36 | 37 | Then you can send the resulting file `device-data.tar.gz` 38 | as an e-mail to [brian@maven-group.org](mailto:brian@maven-group.org) 39 | 40 | 41 | ### macOS 42 | 43 | To find mDNS records on macOS I usually use the free 44 | [Discovery](https://itunes.apple.com/dk/app/discovery-dns-sd-browser/id1381004916?mt=12). 45 | From there you can find all the devices that publish a `_airplay._tcp` 46 | service, select and copy/paste the results into an e-mail. It is also 47 | relevant if you can find what other services those devices publish and 48 | include the service records for those services. 49 | 50 | To get the RTSP GET /info results you will need the ip address and 51 | port number your devices use for `_airplay._tcp` and in the command below 52 | replace `[ip address]`, `[port]` and `[device name]` with the correct values. 53 | 54 | 55 | ```bash 56 | curl https://openairplay.github.io/airplay-spec/data/RTSP-get-info-req.bin | nc -w 2 [ip address] [port] > RTSP-get-info-res-[device name].bin 57 | ``` 58 | 59 | 60 | Then you can send the mDNS records and `RTSP-get-info-res-[device name].bin` 61 | files in an e-mail to [brian@maven-group.org](mailto:brian@maven-group.org) 62 | -------------------------------------------------------------------------------- /src/data/RTSP-get-info-req.bin: -------------------------------------------------------------------------------- 1 | GET /info?txtAirPlay&txtRAOP RTSP/1.0 2 | X-Apple-ProtocolVersion: 1 3 | CSeq: 0 4 | DACP-ID: ADA239F4521B1802 5 | Active-Remote: 753030410 6 | Connection: close 7 | User-Agent: AirPlay/380.10.1 8 | 9 | -------------------------------------------------------------------------------- /src/data/examples/AirServer/info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/src/data/examples/AirServer/info.plist -------------------------------------------------------------------------------- /src/data/examples/AirServer/info.plist.base64: -------------------------------------------------------------------------------- 1 | YnBsaXN0MDDeAQIDBAUGBwgJCgsMDQ4PEBElLzAdMjMdNTw9PFhmZWF0dXJl 2 | c1RuYW1lWGRpc3BsYXlzXGF1ZGlvRm9ybWF0c1J2dltzdGF0dXNGbGFnc18Q 3 | EWtlZXBBbGl2ZUxvd1Bvd2VyXXNvdXJjZVZlcnNpb25ScGtfEBhrZWVwQWxp 4 | dmVTZW5kU3RhdHNBc0JvZHleYXVkaW9MYXRlbmNpZXNYZGV2aWNlSURVbW9k 5 | ZWxabWFjQWRkcmVzcxMAAAAOSn//91VBcmxlbqES2hMUFRYXGBkBGhscHR4f 6 | ICEeIiMkXxAScHJpbWFyeUlucHV0RGV2aWNlWHJvdGF0aW9uXXdpZHRoUGh5 7 | c2ljYWxUZWRpZFt3aWR0aFBpeGVsc1R1dWlkXmhlaWdodFBoeXNpY2FsXGhl 8 | aWdodFBpeGVsc1tvdmVyc2Nhbm5lZBABCRAATxCAAP///////wAGEA+gAAAA 9 | ABAVAQSlIRV4Am+xp1VMniUMUFQAAAABAQEBAQEBAQEBAQEBAQEB74NAoLAI 10 | NHAwIDYAS88QAAAaAAAA/ABDb2xvciBMQ0QKICAgAAAAEAAAAAAAAAAAAAAA 11 | AAAAAAAAEAAAAAAAAAAAAAAAAAAAAOcjQJ4AAAAAAABfECQwNjEwMGZhMC03 12 | YjBmLTQzMDUtOTg0Yi05NzRmNjc3YTE1MGIQHiNAkOAAAAAAAAiiJizTJygp 13 | KisrVHR5cGVfEBFhdWRpb0lucHV0Rm9ybWF0c18QEmF1ZGlvT3V0cHV0Rm9y 14 | bWF0cxBkEgP///zTJygpLSsuEGUjQY///+AAAAAQAhAECVYyMjAuNjhfEEBi 15 | NGJmMWU0N2U2YWFjMGI1YmI3Y2Y5ZTJjZDBmZTU5OTRkNGQyZjUwOGE1YmM2 16 | ZDUxODg1NmM4ODQyZjY5YzBhCaI2O9Q3Jzg5Hio6Hl8QE291dHB1dExhdGVu 17 | Y3lNaWNyb3NZYXVkaW9UeXBlXxASaW5wdXRMYXRlbmN5TWljcm9zV2RlZmF1 18 | bHTUNyc4OR4tOh5fEBEyODpDRjpFOToxOTpENjo4NlpBcHBsZVRWNSwzAAgA 19 | JQAuADMAPABJAEwAWABsAHoAfQCYAKcAsAC2AMEAygDQANIA5wD8AQUBEwEY 20 | ASQBKQE4AUUBUQFTAVQBVgHZAeICCQILAhQCFQIYAh8CJAI4Ak0CTwJUAlsC 21 | XQJmAmgCagJrAnICtQK2ArkCwgLYAuIC9wL/AwgDHAAAAAAAAAIBAAAAAAAA 22 | AD4AAAAAAAAAAAAAAAAAAAMn 23 | -------------------------------------------------------------------------------- /src/data/examples/AirServer/info.plist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | audioFormats 6 | 7 | 8 | audioInputFormats 9 | 67108860 10 | audioOutputFormats 11 | 67108860 12 | type 13 | 100 14 | 15 | 16 | audioInputFormats 17 | 67108860 18 | audioOutputFormats 19 | 67108860 20 | type 21 | 101 22 | 23 | 24 | audioLatencies 25 | 26 | 27 | audioType 28 | default 29 | inputLatencyMicros 30 | 0 31 | outputLatencyMicros 32 | 0 33 | type 34 | 100 35 | 36 | 37 | audioType 38 | default 39 | inputLatencyMicros 40 | 0 41 | outputLatencyMicros 42 | 0 43 | type 44 | 101 45 | 46 | 47 | deviceID 48 | 28:CF:E9:19:D6:86 49 | displays 50 | 51 | 52 | edid 53 | 54 | AP///////wAGEA+gAAAAABAVAQSlIRV4Am+xp1VMniUMUFQAAAAB 55 | AQEBAQEBAQEBAQEBAQEB74NAoLAINHAwIDYAS88QAAAaAAAA/ABD 56 | b2xvciBMQ0QKICAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA 57 | AAAAAAAAAAAAAOc= 58 | 59 | features 60 | 30 61 | heightPhysical 62 | 0 63 | heightPixels 64 | 1080 65 | overscanned 66 | 67 | primaryInputDevice 68 | 1 69 | rotation 70 | 71 | uuid 72 | 06100fa0-7b0f-4305-984b-974f677a150b 73 | widthPhysical 74 | 0 75 | widthPixels 76 | 1920 77 | 78 | 79 | features 80 | 61379444727 81 | keepAliveLowPower 82 | 83 | keepAliveSendStatsAsBody 84 | 85 | macAddress 86 | 28:CF:E9:19:D6:86 87 | model 88 | AppleTV5,3 89 | name 90 | Arlen 91 | pk 92 | b4bf1e47e6aac0b5bb7cf9e2cd0fe5994d4d2f508a5bc6d518856c8842f69c0a 93 | sourceVersion 94 | 220.68 95 | statusFlags 96 | 4 97 | vv 98 | 2 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/data/examples/AppleTV-4K/info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/src/data/examples/AppleTV-4K/info.plist -------------------------------------------------------------------------------- /src/data/examples/AppleTV-4K/info.plist.base64: -------------------------------------------------------------------------------- 1 | YnBsaXN0MDDfEBIBAgMEBQYHCAkKCwwNDg8QERITFBkaGxwdHh8gISIjFyUm 2 | JyhadHh0QWlyUGxheV8QFHBsYXliYWNrQ2FwYWJpbGl0aWVzWm1hY0FkZHJl 3 | c3Ndc2VuZGVyQWRkcmVzc1JwaVJ2dltzdGF0dXNGbGFnc18QEXZvbHVtZUNv 4 | bnRyb2xUeXBlXXNvdXJjZVZlcnNpb25TcHNpXWluaXRpYWxWb2x1bWVfEA9w 5 | cm90b2NvbFZlcnNpb25ScGtfEBhrZWVwQWxpdmVTZW5kU3RhdHNBc0JvZHlY 6 | ZGV2aWNlSURVbW9kZWxYZmVhdHVyZXNUbmFtZU8RAVsFYWNsPTAaZGV2aWNl 7 | aWQ9OTA6REQ6NUQ6OTg6RjA6MEEdZmVhdHVyZXM9MHg0QTdGRkZGNywweDQx 8 | NTVGREUNZmxhZ3M9MHgxMDY0NChnaWQ9MEVCNTVGREYtNDA5MC00Rjg5LTgw 9 | RTQtQkZFNkFCQjM4QkFCBWlnbD0xBmdjZ2w9MRBtb2RlbD1BcHBsZVRWNiwy 10 | DXByb3RvdmVycz0xLjEncGk9ZGUxNTk3NDItYzAyMi00NTE0LTkxNWItMjAz 11 | Y2I5OWY4YjcxKHBzaT0yNDYwOTg1OC0xODdGLTQyNEUtODE0Qi1BNDVCMEM3 12 | NTZCMjJDcGs9MjNhNGY4MjM4NWY0ZGU4ZjY4ODg4OTA4ZDcyZDZjYjVkMjI5 13 | ZTBlZTUwZmI5MzU5YjZiMjgwN2U2OGY0YWEyMxBzcmN2ZXJzPTM4MC4yMC4x 14 | DW9zdmVycz0xMi4yLjEEdnY9MtIVFhcXXxAVc3VwcG9ydHNGUFNTZWN1cmVT 15 | dG9wXxAdc3VwcG9ydHNVSUZvckF1ZGlvT25seUNvbnRlbnQJCV8QETkwOkRE 16 | OjVEOkEyOkQwOjIwXxAQMTAuNDIuMC4xNzo1NTU4N18QJGRlMTU5NzQyLWMw 17 | MjItNDUxNC05MTViLTIwM2NiOTlmOGI3MRACEgABBkQQBFgzODAuMjAuMV8Q 18 | JDI0NjA5ODU4LTE4N0YtNDI0RS04MTRCLUE0NUIwQzc1NkIyMiPANAAAAAAA 19 | AFMxLjFPECAjpPgjhfTej2iIiQjXLWy10ing7lD7k1m2soB+aPSqIwlfEBE5 20 | MDpERDo1RDo5ODpGMDowQVpBcHBsZVRWNiwyEwQVX95Kf//3VFN0dWUACAAv 21 | ADoAUQBcAGoAbQBwAHwAkACeAKIAsADCAMUA4ADpAO8A+AD9AlwCYQJ5ApkC 22 | mgKbAq8CwgLpAusC8ALyAvsDIgMrAy8DUgNTA2cDcgN7AAAAAAAAAgEAAAAA 23 | AAAAKQAAAAAAAAAAAAAAAAAAA4A= 24 | -------------------------------------------------------------------------------- /src/data/examples/AppleTV-4K/info.plist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | deviceID 6 | 90:DD:5D:98:F0:0A 7 | features 8 | 294246758999916535 9 | initialVolume 10 | -20 11 | keepAliveSendStatsAsBody 12 | 13 | macAddress 14 | 90:DD:5D:A2:D0:20 15 | model 16 | AppleTV6,2 17 | name 18 | Stue 19 | pi 20 | de159742-c022-4514-915b-203cb99f8b71 21 | pk 22 | 23 | I6T4I4X03o9oiIkI1y1stdIp4O5Q+5NZtrKAfmj0qiM= 24 | 25 | playbackCapabilities 26 | 27 | supportsFPSSecureStop 28 | 29 | supportsUIForAudioOnlyContent 30 | 31 | 32 | protocolVersion 33 | 1.1 34 | psi 35 | 24609858-187F-424E-814B-A45B0C756B22 36 | senderAddress 37 | 10.42.0.17:55587 38 | sourceVersion 39 | 380.20.1 40 | statusFlags 41 | 67140 42 | txtAirPlay 43 | 44 | BWFjbD0wGmRldmljZWlkPTkwOkREOjVEOjk4OkYwOjBBHWZlYXR1cmVzPTB4NEE3RkZG 45 | RjcsMHg0MTU1RkRFDWZsYWdzPTB4MTA2NDQoZ2lkPTBFQjU1RkRGLTQwOTAtNEY4OS04 46 | MEU0LUJGRTZBQkIzOEJBQgVpZ2w9MQZnY2dsPTEQbW9kZWw9QXBwbGVUVjYsMg1wcm90 47 | b3ZlcnM9MS4xJ3BpPWRlMTU5NzQyLWMwMjItNDUxNC05MTViLTIwM2NiOTlmOGI3MShw 48 | c2k9MjQ2MDk4NTgtMTg3Ri00MjRFLTgxNEItQTQ1QjBDNzU2QjIyQ3BrPTIzYTRmODIz 49 | ODVmNGRlOGY2ODg4ODkwOGQ3MmQ2Y2I1ZDIyOWUwZWU1MGZiOTM1OWI2YjI4MDdlNjhm 50 | NGFhMjMQc3JjdmVycz0zODAuMjAuMQ1vc3ZlcnM9MTIuMi4xBHZ2PTI= 51 | 52 | volumeControlType 53 | 4 54 | vv 55 | 2 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/data/examples/Denon-AVR-X3500H/info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/src/data/examples/Denon-AVR-X3500H/info.plist -------------------------------------------------------------------------------- /src/data/examples/Denon-AVR-X3500H/info.plist.base64: -------------------------------------------------------------------------------- 1 | YnBsaXN0MDDfEBIBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhcbHB0eHyAh 2 | IiNTc2RrUnBpXxAQZmlybXdhcmVSZXZpc2lvblxtYW51ZmFjdHVyZXJfEBFr 3 | ZWVwQWxpdmVMb3dQb3dlcl8QEWZpcm13YXJlQnVpbGREYXRlVW1vZGVsXxAU 4 | bmFtZUlzRmFjdG9yeURlZmF1bHRfEBhrZWVwQWxpdmVTZW5kU3RhdHNBc0Jv 5 | ZHlbc3RhdHVzRmxhZ3NYZGV2aWNlSURVYnVpbGRadHh0QWlyUGxheVdQVFBJ 6 | bmZvXxAPcHJvdG9jb2xWZXJzaW9uXXNvdXJjZVZlcnNpb25YZmVhdHVyZXNU 7 | bmFtZV1BaXJQbGF5OzIuMC4yXxAkNjUwYzIwZWUtODQyYy00YzAxLTgwZWQt 8 | MjJiN2ZiMjM5ODgxWTEuNTA1LjEzMFxTb3VuZCBVbml0ZWQJW0phbiAzMCAy 9 | MDE5WkFWUi1YMzUwMEgIEARfEBEwMDowNTpDRDpENDo0Mjo5NlQxNy4wTxEB 10 | XwVhY2w9MBpkZXZpY2VpZD0wMDowNTpDRDpENDo0Mjo5NhtmZWF0dXJlcz0w 11 | eDQ0NUY4QTAwLDB4MUMzNDAHcnNmPTB4MBBmdj1wMjAuMS41MDUuMTMwCWZs 12 | YWdzPTB4NBBtb2RlbD1BVlItWDM1MDBIGW1hbnVmYWN0dXJlcj1Tb3VuZCBV 13 | bml0ZWQbc2VyaWFsTnVtYmVyPUJCVzM2MTgxMjEyMzY0DXByb3RvdmVycz0x 14 | LjENc3JjdmVycz0zNjYuMCdwaT02NTBjMjBlZS04NDJjLTRjMDEtODBlZC0y 15 | MmI3ZmIyMzk4ODEoZ2lkPTY1MGMyMGVlLTg0MmMtNGMwMS04MGVkLTIyYjdm 16 | YjIzOTg4MQZnY2dsPTBDcGs9MGFjZDdkNjFiMjg0YzBhZjcxYzdlZjRmN2Vh 17 | NjQ0ZGRkZWMyMzhlZjI3YzNjMGFkM2Q1ZDNiYjljOGIzMWU4Zl8QJU9wZW5B 18 | Vk5VIEFydEFuZExvZ2ljLWFQVFAtY2hhbmdlcyAxLjBTMS4xVTM2Ni4wEwAB 19 | w0BEX4oAVkRhZW1vbgAIAC8AMwA2AEkAVgBqAH4AhACbALYAwgDLANEA3ADk 20 | APYBBAENARIBIAFHAVEBXgFfAWsBdgF3AXkBjQGSAvUDHQMhAycDMAAAAAAA 21 | AAIBAAAAAAAAACQAAAAAAAAAAAAAAAAAAAM3 22 | -------------------------------------------------------------------------------- /src/data/examples/Denon-AVR-X3500H/info.plist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PTPInfo 6 | OpenAVNU ArtAndLogic-aPTP-changes 1.0 7 | build 8 | 17.0 9 | deviceID 10 | 00:05:CD:D4:42:96 11 | features 12 | 496155769145856 13 | firmwareBuildDate 14 | Jan 30 2019 15 | firmwareRevision 16 | 1.505.130 17 | keepAliveLowPower 18 | 19 | keepAliveSendStatsAsBody 20 | 21 | manufacturer 22 | Sound United 23 | model 24 | AVR-X3500H 25 | name 26 | Daemon 27 | nameIsFactoryDefault 28 | 29 | pi 30 | 650c20ee-842c-4c01-80ed-22b7fb239881 31 | protocolVersion 32 | 1.1 33 | sdk 34 | AirPlay;2.0.2 35 | sourceVersion 36 | 366.0 37 | statusFlags 38 | 4 39 | txtAirPlay 40 | 41 | BWFjbD0wGmRldmljZWlkPTAwOjA1OkNEOkQ0OjQyOjk2G2ZlYXR1cmVzPTB4NDQ1RjhB 42 | MDAsMHgxQzM0MAdyc2Y9MHgwEGZ2PXAyMC4xLjUwNS4xMzAJZmxhZ3M9MHg0EG1vZGVs 43 | PUFWUi1YMzUwMEgZbWFudWZhY3R1cmVyPVNvdW5kIFVuaXRlZBtzZXJpYWxOdW1iZXI9 44 | QkJXMzYxODEyMTIzNjQNcHJvdG92ZXJzPTEuMQ1zcmN2ZXJzPTM2Ni4wJ3BpPTY1MGMy 45 | MGVlLTg0MmMtNGMwMS04MGVkLTIyYjdmYjIzOTg4MShnaWQ9NjUwYzIwZWUtODQyYy00 46 | YzAxLTgwZWQtMjJiN2ZiMjM5ODgxBmdjZ2w9MENwaz0wYWNkN2Q2MWIyODRjMGFmNzFj 47 | N2VmNGY3ZWE2NDRkZGRlYzIzOGVmMjdjM2MwYWQzZDVkM2JiOWM4YjMxZThm 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | |bit| name|description| 4 | |---|-------|-----------| 5 | | 0|Video|video supported| 6 | | 1|Photo|photo supported| 7 | | 2|VideoFairPlay|video protected with FairPlay DRM| 8 | | 3|VideoVolumeControl|volume control supported for videos| 9 | | 4|VideoHTTPLiveStreams|http live streaming supported| 10 | | 5|Slideshow|slideshow supported| 11 | | 6||| 12 | | 7|Screen|mirroring supported| 13 | | 8|ScreenRotate|screen rotation supported| 14 | | 9|Audio|audio supported| 15 | | 10||| 16 | | 11|AudioRedundant|audio packet redundancy supported| 17 | | 12|FPSAPv2pt5_AES_GCM|FairPlay secure auth supported| 18 | | 13|PhotoCaching|photo preloading supported| 19 | | 14|Authentication4|Authentication type 4. FairPlay authentication| 20 | | 15|MetadataFeature1|bit 1 of MetadataFeatures. Artwork.| 21 | | 16|MetadataFeature2|bit 2 of MetadataFeatures. Progress. | 22 | | 17|MetadataFeature0|bit 0 of MetadataFeatures. Text. | 23 | | 18|AudioFormat1|support for audio format 1| 24 | | 19|AudioFormat2|support for audio format 2. This bit must be set for AirPlay 2 connection to work| 25 | | 20|AudioFormat3|support for audio format 3. This bit must be set for AirPlay 2 connection to work| 26 | | 21|AudioFormat4|support for audio format 4| 27 | | 22||| 28 | | 23|Authentication1|Authentication type 1. RSA Authentication| 29 | | 24||| 30 | | 25||| 31 | | 26|HasUnifiedAdvertiserInfo|| 32 | | 27|SupportsLegacyPairing|| 33 | | 28||| 34 | | 29||| 35 | | 30|RAOP|RAOP is supported on this port. With this bit set your don't need the AirTunes service| 36 | | 31||| 37 | | 32|IsCarPlay / SupportsVolume|Don’t read key from `pk` record it is known| 38 | | 33|SupportsAirPlayVideoPlayQueue|| 39 | | 34|SupportsAirPlayFromCloud|| 40 | | 35||| 41 | | 36||| 42 | | 37||| 43 | | 38|SupportsCoreUtilsPairingAndEncryption| `SupportsHKPairingAndAccessControl`, `SupportsSystemPairing` and `SupportsTransientPairing` implies `SupportsCoreUtilsPairingAndEncryption`| 44 | | 39||| 45 | | 40|SupportsBufferedAudio|Bit needed for device to show as supporting multi-room audio| 46 | | 41|SupportsPTP|Bit needed for device to show as supporting multi-room audio| 47 | | 42|SupportsScreenMultiCodec|| 48 | | 43|SupportsSystemPairing|| 49 | | 44||| 50 | | 45||| 51 | | 46|SupportsHKPairingAndAccessControl|| 52 | | 47||| 53 | | 48|SupportsTransientPairing|`SupportsSystemPairing` implies `SupportsTransientPairing`| 54 | | 49||| 55 | | 50|MetadataFeature4|bit 4 of MetadataFeatures. binary plist.| 56 | | 51|SupportsUnifiedPairSetupAndMFi|Authentication type 8. MFi authentication| 57 | | 52|SupportsSetPeersExtendedMessage|| 58 | -------------------------------------------------------------------------------- /src/history.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | |Date|Changes| 4 | |----|-------| 5 | |2012–03–20|Initial version.| 6 | |2019–04–30|Updated with data from reverse engineering `AirPlaySender.framework`.| 7 | |2019–05–01|Cleaned up HTML and added a little interactivity.| 8 | |2019–05–01|Added data for Libretone 9 | |2019–05–01|Add contributing section| 10 | |2019–05–02|Add more example devices and click on flag to change| 11 | |2019–12–23|Rewrite as mdBook| 12 | -------------------------------------------------------------------------------- /src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | AirPlay is a family of protocols implemented by Apple to view various 4 | types of media content on the *Apple TV* from any iOS device or iTunes. 5 | In this documentation, “*iOS device*” refers to an iPhone, iPod touch or 6 | iPad. The following scenarios are supported by AirPlay: 7 | 8 | - Display photos and slideshows from an iOS device. 9 | - Stream audio from an iOS device or iTunes. 10 | - Display videos from an iOS device or iTunes. 11 | - Show the screen content from an iOS device or OS X Mountain Lion. 12 | This is called *AirPlay Mirroring*. It requires hardware capable of 13 | encoding live video without taking too much CPU, so it is only 14 | available on iPhone 4S, iPad 2, the new iPad, and Macs with Sandy 15 | Bridge CPUs. 16 | 17 | Audio streaming is also supported from an iOS device or iTunes to an 18 | AirPort Express base station or a 3rd party AirPlay-enabled audio 19 | device. Initially this was called *AirTunes*, but it was later renamed 20 | to AirPlay when Apple added video support for the Apple TV. 21 | 22 | This document describes these protocols, as implemented in Apple TV 23 | software version 5.0, iOS 5.1 and iTunes 10.6. They are based on 24 | well-known standard networking protocols such as *Multicast DNS*, 25 | *HTTP*, *RTSP*, *RTP*, *PTP* or *NTP*, with custom extensions. 26 | 27 | All these information have been gathered by using various techniques of 28 | reverse engineering, so they might be somewhat inaccurate and 29 | incomplete. 30 | -------------------------------------------------------------------------------- /src/known_implementations.md: -------------------------------------------------------------------------------- 1 | # Known Implementations 2 | 3 | - [https://github.com/mikebrady/shairport-sync](https://github.com/mikebrady/shairport-sync) 4 | - [https://github.com/juhovh/shairplay](https://github.com/juhovh/shairplay) 5 | - [https://github.com/ejurgensen/forked-daapd](https://github.com/ejurgensen/forked-daapd) 6 | - [https://github.com/Vluxe/nighthawk](https://github.com/Vluxe/nighthawk) 7 | - [https://github.com/philippe44/RAOP-Player](https://github.com/philippe44/RAOP-Player) 8 | - [https://github.com/funtax/AirPlayAuth](https://github.com/funtax/AirPlayAuth) 9 | - [https://github.com/espes/Slave-in-the-Magic-Mirror](ttps://github.com/espes/Slave-in-the-Magic-Mirror) 10 | - [https://github.com/robertoandrade/playfair](https://github.com/robertoandrade/playfair) 11 | - [https://github.com/phonegapX/AirPlay](https://github.com/phonegapX/AirPlay) 12 | 13 | 14 | https://htmlpreview.github.io/?https://github.com/philippe44/RAOP-Player/blob/master/doc/auth_protocol.html 15 | -------------------------------------------------------------------------------- /src/pairing/README.md: -------------------------------------------------------------------------------- 1 | # Pairing 2 | 3 | In all newer versions of the protocol pairing is required. 4 | -------------------------------------------------------------------------------- /src/pairing/hkp.md: -------------------------------------------------------------------------------- 1 | # HomeKit Based Pairings 2 | 3 | Default code is `3939`. 4 | 5 | 6 | ## Transient Pairing 7 | 8 | `POST /pair-setup` 9 | `EncryptionKey` is `SRP shared secret` 10 | 11 | ## Normal Pairing 12 | 13 | `POST /pair-setup` 14 | `POST /pair-verify` 15 | 16 | `EncryptionKey` is agreed `SessionKey` 17 | 18 | ## Encryption 19 | 20 | After successful pairing the connection switches to being encrypted using 21 | the format `N:n_bytes:tag` where `N` is a 16 bit Little Endian length that 22 | describes the number of bytes in `n_bytes` and `n_bytes` is encrypted using 23 | ChaCha20-Poly1305 with `tag` being the Poly1305 tag. 24 | 25 | Each direction uses its own key and nonce. 26 | 27 | The key for data sent from client to accessory is a HKDF-SHA-512 with the 28 | following parameters: 29 | 30 | ``` 31 | InputKey = 32 | Salt = ”Control-Salt” 33 | Info = ”Control-Write-Encryption-Key” 34 | OutputSize = 32 bytes 35 | ``` 36 | 37 | While the data sent from accessory to client is HKDF-SHA-512 with the following 38 | parameters: 39 | 40 | ``` 41 | InputKey = 42 | Salt = ”Control-Salt” 43 | Info = ”Control-Read-Encryption-Key” 44 | OutputSize = 32 bytes 45 | ``` 46 | 47 | The nonce is a 64 bit counter (i.e. the high order bits of the full 96 bit 48 | nonce is set to 0) starting with 0 and incrementing by 1 for each encrypted 49 | block. 50 | 51 | ## MFi Authentication 52 | 53 | When SupportsUnifiedPairSetupAndMFi is enabled and HKP is used, the device can authenticate when pairing. 54 | In such case M1 step is done with "Pair Setup with Auth" method. 55 | 56 | Also, during the M4 step of the pairing process, in addition of the PROOF TLV used in regular pair-setup, the following TLV is added: 57 | `TLV: 0x05,N,ENCRYPTED_DATA_WITH_TAG` where N (int16) is the length of ENCRYPTED_DATA_WITH_TAG 58 | 59 | ENCRYPTED_DATA_WITH_TAG has the following format: 60 | `` 61 | `<16:Poly1305 Tag>` 62 | 63 | ENCRYPTED_DATA is encrypted using a HKDF-SHA-512 key with the following parameters: 64 | ``` 65 | InputKey = 66 | Salt = ”Pair-Setup-Encrypt-Salt” 67 | Info = ”Pair-Setup-Encrypt-Info” 68 | Nonce = ”PS-Msg04” 69 | OutputSize = 32 bytes 70 | ``` 71 | Decrypted data contains TLVs, which contain MFi Signature (signed by Apple authenticator IC) and used MFi certificate. 72 | The message is signed using RSA-1024 with SHA-1 hash algorithm. The message signed is a HKDF-SHA-512 key with the following 73 | parameters: 74 | ``` 75 | InputKey = 76 | Salt = ”MFi-Pair-Setup-Salt” 77 | Info = ”MFi-Pair-Setup-Info” 78 | OutputSize = 32 bytes 79 | ``` -------------------------------------------------------------------------------- /src/pairing/legacy.md: -------------------------------------------------------------------------------- 1 | # Legacy Pairing 2 | 3 | TBD 4 | -------------------------------------------------------------------------------- /src/photos/README.md: -------------------------------------------------------------------------------- 1 | # Photos 2 | 3 | Photos are *JPEG* data transmitted using a `PUT` request to the AirPlay 4 | server. They can be displayed immediately, or cached for future use. 5 | -------------------------------------------------------------------------------- /src/photos/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | ## Photo 3 | 4 | This event notifies a client that a photo session has ended. Then the 5 | server can safely disconnect. 6 | 7 | |key |type |description| 8 | |----------|--------|-----------| 9 | |category |string |`photo` | 10 | |sessionID |integer |session ID | 11 | |state |string |`stopped` | 12 | 13 | Example: stop photo session 14 | 15 |
16 |

server → client

17 | 18 | ```http 19 | POST /event HTTP/1.1 20 | Content-Type: text/x-apple-plist+xml 21 | Content-Length: 277 22 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 23 | ``` 24 | ```xml 25 | 26 | 27 | 28 | 29 | category 30 | photo 31 | sessionID 32 | 38 33 | state 34 | stopped 35 | 36 | 37 | ``` 38 |
39 | 40 |
41 |

client → server

42 | 43 | ```http 44 | HTTP/1.1 200 OK 45 | Content-Length: 0 46 | ``` 47 |
48 | 49 | ## Slideshow 50 | 51 | Slideshow events are used to notify the server about the playback state. 52 | 53 | |key |type |description | 54 | |------------|--------|----------------------------------| 55 | |category |string |`slideshow` | 56 | |lastAssetID |integer |last asset ID | 57 | |sessionID |integer |session ID | 58 | |state |string |`loading`, `playing` or `stopped` | 59 | 60 | Example: slideshow is currently playing 61 | 62 |
63 |

server → client

64 | 65 | ```http 66 | POST /event HTTP/1.1 67 | Content-Type: text/x-apple-plist+xml 68 | Content-Length: 371 69 | X-Apple-Session-ID: f1634b51-5cae-4384-ade5-54f4159a15f1 70 | ``` 71 | ```xml 72 | 73 | 74 | 75 | 76 | category 77 | slideshow 78 | lastAssetID 79 | 5 80 | sessionID 81 | 4 82 | state 83 | playing 84 | 85 | 86 | ``` 87 |
88 |
89 |

client → server

90 | 91 | ```http 92 | HTTP/1.1 200 OK 93 | Content-Length: 0 94 | ``` 95 |
96 | -------------------------------------------------------------------------------- /src/photos/http_requests.md: -------------------------------------------------------------------------------- 1 | # HTTP requests 2 | 3 | ## GET /slideshow-features 4 | 5 | A client can fetch the list of available transitions for slideshows. 6 | Then it can let the user pick one, before starting a slideshow. The 7 | `Accept-Language` header is used to specify in which language the 8 | transition names should be. 9 | 10 |
11 |

client → server

12 | 13 | ```http 14 | GET /slideshow-features HTTP/1.1 15 | Accept-Language: English 16 | Content-Length: 0 17 | User-Agent: MediaControl/1.0 18 | X-Apple-Session-ID: cdda804c-33ae-4a0b-a5f2-f0e532fd5abd 19 | ``` 20 |
21 | 22 |
23 |

server → client

24 | 25 | ```http 26 | HTTP/1.1 200 OK 27 | Date: Thu, 23 Feb 2012 17:33:41 GMT 28 | Content-Type: text/x-apple-plist+xml 29 | Content-Length: 6411 30 | ``` 31 | ```xml 32 | 33 | 34 | 35 | 36 | themes 37 | 38 | 39 | key 40 | Reflections 41 | name 42 | Reflections 43 | 44 | ... 45 | 46 | 47 | 48 | ``` 49 |
50 | 51 | ### PUT /photo 52 | 53 | Send a JPEG picture to the server. The following headers are supported: 54 | 55 | |name|description| 56 | |----|-----------| 57 | |`X-Apple-AssetKey`|UUID for the picture| 58 | |`X-Apple-Transition`|transition that should be used to show the picture| 59 | |`X-Apple-AssetAction`|specify a caching operation| 60 | 61 | Example 1: show a picture without any transition (for the first time) 62 | 63 |
64 |

client → server

65 | 66 | ```http 67 | PUT /photo HTTP/1.1 68 | X-Apple-AssetKey: F92F9B91-954E-4D63-BB9A-EEC771ADE6E8 69 | Content-Length: 462848 70 | User-Agent: MediaControl/1.0 71 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 72 | 73 | 74 | ``` 75 |
76 | 77 |
78 |

server → client

79 | 80 | ```http 81 | HTTP/1.1 200 OK 82 | Date: Thu, 23 Feb 2012 17:33:42 GMT 83 | Content-Length: 0 84 | ``` 85 |
86 | 87 | 88 | Example 2: show a picture using the dissolve transition 89 | 90 |
91 |

client → server

92 | 93 | ```http 94 | PUT /photo HTTP/1.1 95 | X-Apple-AssetKey: F92F9B91-954E-4D63-BB9A-EEC771ADE6E8 96 | X-Apple-Transition: Dissolve 97 | Content-Length: 462848 98 | User-Agent: MediaControl/1.0 99 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 100 | 101 | 102 | ``` 103 |
104 | 105 |
106 |

server → client

107 | 108 | ```http 109 | HTTP/1.1 200 OK 110 | Date: Thu, 23 Feb 2012 17:33:42 GMT 111 | Content-Length: 0 112 | ``` 113 |
114 | 115 | ### PUT /slideshows/1 116 | 117 | Start or stop a slideshow session. When starting, slideshow settings 118 | such as the slide duration and selected transition theme are 119 | transmitted. The following parameters are sent in an XML property list: 120 | 121 | |key |type |description | 122 | |-----------------------|--------|---------------------------| 123 | |settings.slideDuration |integer | slide duration in seconds | 124 | |settings.theme |string | selected transition theme | 125 | |state |string | `playing` or `stopped` | 126 | 127 | Example: send slideshow settings 128 | 129 |
130 |

client → server

131 | 132 | ```http 133 | PUT /slideshows/1 HTTP/1.1 134 | Content-Type: text/x-apple-plist+xml 135 | Content-Length: 366 136 | User-Agent: MediaControl/1.0 137 | X-Apple-Session-ID: 98a7b246-8e00-49a6-8765-db57165f5b67 138 | ``` 139 | ```xml 140 | 141 | 142 | 143 | 144 | settings 145 | 146 | slideDuration 147 | 3 148 | theme 149 | Classic 150 | 151 | state 152 | playing 153 | 154 | 155 | ``` 156 |
157 | 158 |
159 |

server → client

160 | 161 | ```http 162 | HTTP/1.1 200 OK 163 | Date: Thu, 08 Mar 2012 16:30:01 GMT 164 | Content-Type: text/x-apple-plist+xml 165 | Content-Length: 181 166 | ``` 167 | ```xml 168 | 169 | 170 | 171 | 172 | 173 | ``` 174 |
175 | 176 | ### POST /stop 177 | 178 | Stop a photo or slideshow session. 179 | 180 |
181 |

client → server

182 | 183 | ```http 184 | POST /stop HTTP/1.1 185 | Content-Length: 0 186 | User-Agent: MediaControl/1.0 187 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 188 | ``` 189 |
190 |
191 |

server → client

192 | 193 | ```http 194 | HTTP/1.1 200 OK 195 | Date: Thu, 23 Feb 2012 17:33:55 GMT 196 | Content-Length: 0 197 | ``` 198 |
199 | -------------------------------------------------------------------------------- /src/photos/photo_caching.md: -------------------------------------------------------------------------------- 1 | # Photo Caching 2 | 3 | AirPlay supports preloading picture data to improve transition latency. 4 | This works by preloading a few pictures (most likely the ones before and 5 | after the current picture) just after displaying one. 6 | 7 | Preloading is achieved using the `cacheOnly` asset action. Upon 8 | receiving this request, a server stores the picture in its cache. Later, 9 | a client can request the display of this picture using the 10 | `displayCached` asset action and the same asset key. This is much faster 11 | than a full picture upload because no additional data is transmitted. 12 | 13 | When asked for a picture which is no longer in the cache, a server 14 | replies with an HTTP 412 error code (Precondition Failed). 15 | 16 | Example 1: cache a picture for future display 17 | 18 |
19 |

client → server

20 | 21 | ```http 22 | PUT /photo HTTP/1.1 23 | X-Apple-AssetAction: cacheOnly 24 | X-Apple-AssetKey: B0DDE2C0-6FDD-48F8-9E5B-29CE0618DF5B 25 | Content-Length: 462848 26 | User-Agent: MediaControl/1.0 27 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 28 | 29 | 30 | ``` 31 |
32 | 33 |
34 |

server → client

35 | 36 | ```http 37 | HTTP/1.1 200 OK 38 | Date: Thu, 23 Feb 2012 17:33:45 GMT 39 | Content-Length: 0 40 | ``` 41 |
42 | 43 | Example 2: show a cached picture 44 | 45 |
46 |

client → server

47 | 48 | ```http 49 | PUT /photo HTTP/1.1 50 | X-Apple-AssetAction: displayCached 51 | X-Apple-AssetKey: B0DDE2C0-6FDD-48F8-9E5B-29CE0618DF5B 52 | X-Apple-Transition: Dissolve 53 | Content-Length: 0 54 | User-Agent: MediaControl/1.0 55 | X-Apple-Session-ID: 1bd6ceeb-fffd-456c-a09c-996053a7a08c 56 | ``` 57 |
58 | 59 |
60 |

server → client

61 | 62 | ```http 63 | HTTP/1.1 200 OK 64 | Date: Thu, 23 Feb 2012 17:33:45 GMT 65 | Content-Length: 0 66 | ``` 67 |
68 | -------------------------------------------------------------------------------- /src/photos/slideshows.md: -------------------------------------------------------------------------------- 1 | # Slideshows 2 | 3 | Slideshows are using the reverse HTTP connection for asynchronous 4 | loading of pictures. Three connections are performed in parallel. The 5 | `X-Apple-Purpose` header is set to `slideshow`. A `GET` request to the 6 | `/slideshows/1/assets/1` location is issued to fetch a new picture from 7 | the AirPlay client. A binary property list with the following parameters 8 | is expected as reply: 9 | 10 | |key |type |description | 11 | |---------|--------|-------------| 12 | |data |data |JPEG picture | 13 | |info.id |integer |asset ID | 14 | |info.key |integer |1 | 15 | 16 | Example: fetch a new picture 17 | 18 |
19 |

server → client

20 | 21 | ```http 22 | GET /slideshows/1/assets/1 HTTP/1.1 23 | Content-Length: 0 24 | Accept: application/x-apple-binary-plist 25 | X-Apple-Session-ID: 98a7b246-8e00-49a6-8765-db57165f5b67 26 | ``` 27 |
28 | 29 |
30 |

client → server

31 | 32 | ```http 33 | HTTP/1.1 200 OK 34 | Content-Type: application/x-apple-binary-plist 35 | Content-Length: 58932 36 | 37 | 38 | ``` 39 | ```xml 40 | 41 | 42 | 43 | 44 | data 45 | 46 | ... 47 | 48 | info 49 | 50 | id 51 | 1 52 | key 53 | 1 54 | 55 | 56 | 57 | ``` 58 |
59 | -------------------------------------------------------------------------------- /src/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | ## IETF RFCs 3 | 4 | - [1] [RFC 2616](https://tools.ietf.org/html/rfc2616): Hypertext Transfer Protocol – HTTP/1.1 5 | - [HTTPBasic] [RFC 2617](https://tools.ietf.org/html/rfc2617): HTTP Authentication: Basic and Digest Access Authentication 6 | - [RTSP] [RFC 2326](https://tools.ietf.org/html/rfc2326): Real Time Streaming Protocol (RTSP) 7 | - [SDP] [RFC 4566](https://tools.ietf.org/html/rfc4566): SDP: Session Description Protocol 8 | - [RTP] [RFC 3550](https://tools.ietf.org/html/rfc3550): RTP: A Transport Protocol for Real-Time Applications 9 | - [NTP] [RFC 5905](https://tools.ietf.org/html/rfc5905): Network Time Protocol Version 4 10 | - [Base64] [RFC 4648](https://tools.ietf.org/html/rfc4648): The Base16, Base32, and Base64 Data Encodings 11 | - [mDNS] [RFC 6762](https://tools.ietf.org/html/rfc6762): Multicast DNS 12 | - [DNS-SD] [RFC 6763](https://tools.ietf.org/html/rfc6763): DNS-Based Service Discovery 13 | - [HLS] [RFC 8216](https://tools.ietf.org/html/rfc8216): HTTP Live Streaming 14 | 15 | 16 | ## IETF drafts 17 | 18 | - [Reverse HTTP](http://tools.ietf.org/id/draft-lentczner-rhttp-00.txt) 19 | 20 | 21 | ## Apple Protocols 22 | 23 | - [DAAP](http://en.wikipedia.org/wiki/Digital_Audio_Access_Protocol): Digital Audio Access Protocol 24 | - [DACP](http://en.wikipedia.org/wiki/Digital_Audio_Control_Protocol): Digital Audio Control Protocol 25 | - [RAOP](http://en.wikipedia.org/wiki/Remote_Audio_Output_Protocol): Remote Audio Output Protocol 26 | -------------------------------------------------------------------------------- /src/screen_mirroring/README.md: -------------------------------------------------------------------------------- 1 | # Screen Mirroring 2 | 3 | Screen mirroring is achieved by transmitting an *H.264* encoded video 4 | stream over a TCP connection. This stream is packetized with a 128-byte 5 | header. *AAC-ELD* audio is sent using the AirTunes protocol. As for the 6 | master clock, it is synchronized using *NTP*. 7 | 8 | Moreover, as soon as a client starts a video playback, a standard 9 | AirPlay connection is made to send the video URL, and mirroring is 10 | stopped. This avoids decoding and re-encoding the video, which would 11 | incur a quality loss. 12 | -------------------------------------------------------------------------------- /src/screen_mirroring/http_requests.md: -------------------------------------------------------------------------------- 1 | # HTTP requests 2 | 3 | Screen mirroring does not use the standard AirPlay service. Instead it 4 | connects to an apparently hard-coded port 7100. This is a HTTP server 5 | which supports the following requests: 6 | 7 | ## GET /stream.xml 8 | 9 | Retrieve information about the server capabilities. The server sends an 10 | XML property list with the following properties: 11 | 12 | | key | type | value | description | 13 | |-------------|---------|----------------|-----------------------------| 14 | | height | integer | 720 | vertical resolution | 15 | | width | integer | 1280 | horizontal resolution | 16 | | overscanned | boolean | true | is the display overscanned? | 17 | | refreshRate | real | 0.01666… | refresh rate 60 Hz (1/60) | 18 | | version | string | 130.14 | server version | 19 | 20 | These properties tell us that the AirPlay server is connected to a 21 | 1280x720, 60 Hz, overscanned display. 22 | 23 | Example: fetch mirroring server informations 24 | 25 |
26 |

client → server

27 | 28 | ```http 29 | GET /stream.xml HTTP/1.1 30 | Content-Length: 0 31 | ``` 32 |
33 |
34 |

server → client

35 | 36 | ```http 37 | HTTP/1.1 200 OK 38 | Date: Mon, 08 Mar 2012 15:30:27 GMT 39 | Content-Type: text/x-apple-plist+xml 40 | Content-Length: 411 41 | ``` 42 | ```xml 43 | 44 | 46 | 47 | 48 | height 49 | 720 50 | overscanned 51 | 52 | refreshRate 53 | 0.016666666666666666 54 | version 55 | 130.14 56 | width 57 | 1280 58 | 59 | 60 | ``` 61 |
62 | 63 | 64 | ## POST /stream 65 | 66 | Start the live video transmission. The client sends a binary property 67 | list with information about the stream, immediately followed by the 68 | stream itself. At this point, the connection is no longer a valid HTTP 69 | connection. 70 | 71 | The following parameters are sent: 72 | 73 | | key | type | value | description | 74 | |---------------|---------|------------------|----------------------------------| 75 | | deviceID | integer | 181221086727016 | MAC address (A4:D1:D2:80:0B:68) | 76 | | sessionID | integer | –808788724 | session ID (0xcfcadd0c) | 77 | | version | string | 130.16 | server version | 78 | | param1 | data | (72 bytes) | AES key, encrypted with FairPlay | 79 | | param2 | data | (16 bytes) | AES initialization vector | 80 | | latencyMs | integer | 90 | video latency in ms | 81 | | fpsInfo | array ||| 82 | | timestampInfo | array ||| 83 | 84 | The `param1` and `param2` parameters are optional. 85 | 86 | As soon as the server receives a `/stream` request, it will send NTP 87 | requests to the client on port 7010, which seems hard-coded as well. The 88 | client needs to export its master clock there, which will be used for 89 | audio/video synchronization and clock recovery. 90 | 91 | Example: send stream information 92 | 93 |
94 |

client → server

95 | 96 | ```http 97 | POST /stream HTTP/1.1 98 | X-Apple-Device-ID: 0xa4d1d2800b68 99 | Content-Length: 503 100 | 101 | 102 | ``` 103 | ```xml 104 | 105 | 107 | 108 | 109 | deviceID 110 | 181221086727016 111 | fpsInfo 112 | 113 | name SubS 114 | name B4En 115 | name EnDp 116 | name IdEn 117 | name IdDp 118 | name EQDp 119 | name QueF 120 | name Sent 121 | 122 | latencyMs 123 | 90 124 | param1 125 | 126 | RlBMWQECAQAAAAA8AAAAANvKuDizduszL1hG9IvIk+AAAAAQukdPJ5Jw/gGBAl22WZdF 127 | m9ujZEGIV7jm3ZByWm51HjpDwjYY 128 | 129 | param2 130 | 131 | 3qpOHtYWbBPyEWPnGt1BuQ== 132 | 133 | sessionID 134 | -808788724 135 | timestampInfo 136 | 137 | name SubSu 138 | name BePxT 139 | name AfPxT 140 | name BefEn 141 | name EmEnc 142 | name QueFr 143 | name SndFr 144 | 145 | version 146 | 130.16 147 | 148 | 149 | ``` 150 |
151 | -------------------------------------------------------------------------------- /src/screen_mirroring/stream_packets.md: -------------------------------------------------------------------------------- 1 | # Stream Packets 2 | 3 | The video stream is packetized using 128-byte headers, followed by an 4 | optional payload. Only the first 64 bytes of headers seem to be used. 5 | Headers start with the following little-endian fields: 6 | 7 | | size | description | 8 | |---------|--------------------------| 9 | | 4 bytes | payload size | 10 | | 2 bytes | payload type | 11 | | 2 bytes | 0x1e if type = 2, else 6 | 12 | | 8 bytes | NTP timestamp | 13 | 14 | There are 3 types of packets: 15 | 16 | | type | description | 17 | |------|-----------------| 18 | | 0 | video bitstream | 19 | | 1 | codec data | 20 | | 2 | heartbeat | 21 | 22 | 23 | ## Codec Data 24 | 25 | This packet contains the H.264 extra data in *avcC* format 26 | ([ISO/IEC 14496:15](http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=55980)). 27 | It is sent at the beginning of the stream, each time the 28 | video properties might change, when screen orientation changes, and when 29 | the screen is turned on or off. 30 | 31 |

H.264 codec data from iPad

32 | 33 | ```hex 34 | 0000 01 64 c0 28 ff e1 00 10 67 64 c0 28 ac 56 20 0d 35 | 0010 81 4f e5 9b 81 01 01 01 01 00 04 28 ee 3c b0 36 | ``` 37 | 38 | The H.264 codec data is interpreted as follows: 39 | 40 | | size | value | description | 41 | |----------|---------|---------------------------| 42 | | 1 byte | 1 | version | 43 | | 1 byte | 100 | profile (high) | 44 | | 1 byte | 0xc0 | compatibility | 45 | | 1 byte | 40 | level (4.0) | 46 | | 6 bits | 0x3f | reserved | 47 | | 2 bits | 3 | NAL units length size - 1 | 48 | | 3 bits | 0x7 | reserved | 49 | | 5 bits | 1 | number of SPS | 50 | | 2 bytes | 16 | length of SPS | 51 | | 16 bytes | … | Sequence parameter set | 52 | | 1 byte | 1 | number of PPS | 53 | | 2 bytes | 4 | length of PPS | 54 | | 4 bytes | … | Picture parameter set | 55 | 56 |

Codec data packet from iPad

57 | 58 | ```hex 59 | 0000 1f 00 00 00 01 00 06 00 1d 9a 9f 59 ef de 00 00 60 | 0010 00 00 58 44 00 00 22 44 00 00 00 00 00 00 00 00 61 | 0020 00 00 00 00 00 00 00 00 00 00 58 44 00 00 22 44 62 | 0030 00 00 50 43 00 00 10 42 00 c0 57 44 00 c0 21 44 63 | 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 | 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 65 | 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 66 | 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 67 | 0080 01 64 c0 28 ff e1 00 10 67 64 c0 28 ac 56 20 0d 68 | 0090 81 4f e5 9b 81 01 01 01 01 00 04 28 ee 3c b0 69 | ``` 70 | 71 | 72 | ## Video Bitstream 73 | 74 | This packet contains the video bitstream to be decoded. The payload can 75 | be optionally AES encrypted. The NTP timestamp found in the header 76 | serves as presentation timestamp. 77 | 78 |

Video bitstream packet from iPad

79 | 80 | ```hex 81 | 0000 c8 08 00 00 00 00 06 00 e9 e6 f5 ac 60 e0 00 00 82 | 0010 58 37 6e f9 40 01 00 00 00 00 00 00 00 00 00 00 83 | 0020 00 00 00 00 00 00 00 00 00 00 58 44 00 00 22 44 84 | 0030 00 00 50 43 00 00 10 42 00 c0 57 44 00 c0 21 44 85 | 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 86 | 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 87 | 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 88 | 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 89 | 0080 ... 90 | ``` 91 | 92 | 93 | ## Heartbeat 94 | 95 | Sent every second, this packet does not contain any payload. 96 | 97 |

Heartbeat packet from iPad

98 | 99 | ```hex 100 | 0000 00 00 00 00 02 00 1e 00 00 00 00 00 00 00 00 00 101 | 0010 4d d8 1a 41 00 00 00 00 00 00 20 41 86 c9 e2 36 102 | 0020 00 00 00 00 80 88 44 4b 00 00 00 00 00 00 00 00 103 | 0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 104 | 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 105 | 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 106 | 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 107 | 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 108 | ``` 109 | -------------------------------------------------------------------------------- /src/screen_mirroring/time_synchronization.md: -------------------------------------------------------------------------------- 1 | # Time Synchronization 2 | 3 | Time synchronization takes place on UDP ports 7010 (client) and 7011 4 | (server), using the [NTP] protocol. The AirPlay server runs 5 | an NTP client. Requests are sent to the AirPlay client at 3 second 6 | intervals. The reference date for the timestamps is the beginning of the 7 | mirroring session. 8 | 9 |
10 |

server → client

11 | 12 | ```http 13 | 0000 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 14 | 0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 15 | 0020 00 00 00 00 00 00 00 00 00 00 01 c4 c8 ac 5d b5 16 | 17 | Network Time Protocol 18 | Flags: 0x23 19 | 00.. .... = Leap Indicator: no warning (0) 20 | ..10 0... = Version number: NTP Version 4 (4) 21 | .... .011 = Mode: client (3) 22 | Peer Clock Stratum: unspecified or invalid (0) 23 | Peer Polling Interval: invalid (0) 24 | Peer Clock Precision: 1.000000 sec 25 | Root Delay: 0.0000 sec 26 | Root Dispersion: 0.0000 sec 27 | Reference ID: NULL 28 | Reference Timestamp: Jan 1, 1970 00:00:00.000000000 UTC 29 | Origin Timestamp: Jan 1, 1970 00:00:00.000000000 UTC 30 | Receive Timestamp: Jan 1, 1970 00:00:00.000000000 UTC 31 | Transmit Timestamp: Jan 1, 1900 00:07:32.783880000 UTC 32 | ``` 33 |
34 |
35 |

client → server

36 | 37 | ```http 38 | 0000 24 01 02 e8 00 00 00 00 00 00 00 00 41 49 52 50 39 | 0010 00 00 00 00 00 00 00 00 00 00 01 c4 c8 ac 5d b5 40 | 0020 00 00 01 c4 c9 6a 0b a1 00 00 01 c4 c9 78 73 d2 41 | 42 | Network Time Protocol 43 | Flags: 0x24 44 | 00.. .... = Leap Indicator: no warning (0) 45 | ..10 0... = Version number: NTP Version 4 (4) 46 | .... .100 = Mode: server (4) 47 | Peer Clock Stratum: primary reference (1) 48 | Peer Polling Interval: invalid (2) 49 | Peer Clock Precision: 0.000000 sec 50 | Root Delay: 0.0000 sec 51 | Root Dispersion: 0.0000 sec 52 | Reference ID: Unidentified reference source 'AIRP' 53 | Reference Timestamp: Jan 1, 1970 00:00:00.000000000 UTC 54 | Origin Timestamp: Jan 1, 1900 00:07:32.783880000 UTC 55 | Receive Timestamp: Jan 1, 1900 00:07:32.786774000 UTC 56 | Transmit Timestamp: Jan 1, 1900 00:07:32.786994000 UTC 57 | ``` 58 |
59 | -------------------------------------------------------------------------------- /src/service_discovery.md: -------------------------------------------------------------------------------- 1 | # Service Discovery 2 | 3 | AirPlay does not require any configuration to be able to find compatible 4 | devices on the network, thanks to [DNS-based service discovery], based 5 | on [Multicast DNS], aka *Bonjour*. 6 | 7 | An AirPlay device such as the Apple TV publishes two services. 8 | The first one is the *Airplay* for photo and video 9 | and in later versions also audio content and the other one is [RAOP], used for audio streaming and in later versions superseded by *Airplay* service. 10 | 11 | 12 | ## _airplay._tcp 13 | 14 | ``` 15 | name: Apple TV 16 | type: _airplay._tcp 17 | port: 7000 18 | txt: 19 | deviceid=58:55:CA:1A:E2:88 20 | features=0x39f7 21 | model=AppleTV2,1 22 | srcvers=130.14 23 | ``` 24 | 25 | The following fields are available in the TXT record: 26 | 27 | |name|type|description| 28 | |----|----|-----------| 29 | |model|string|device model| 30 | |manufacturer|string|device manufacturer| 31 | |serialNumber|string|device serial number| 32 | |fv|string|device firmware version| 33 | |osvers|string|device OS version| 34 | |deviceid|string|Device ID. Usually MAC address of the device| 35 | |features|32 bit hex number,optional high order 32 bit hex number| bitfield of supported [features](./features.md). This was originally a 32 bit value but it has since been expanded to a 64 bit value. To support both these types the mDNS value is encoded as two 32 bit values separated by comma with the comma and second 32 bit value being optional.| 36 | |pw|boolean|server is password protected| 37 | |acl|int64|Access control level| 38 | |srcvers|string|airplay version| 39 | |flags|20 bit hex number|bitfield of [status flags](./status_flags.md)| 40 | |pk|hex string|public key| 41 | |pi|UUID string|group_id / PublicCUAirPlayPairingIdentifier| 42 | |psi|UUID string|PublicCUSystemPairingIdentifier| 43 | |gid|UUID string|group UUID| 44 | |gcgl|boolean|group contains group leader / Group contains discoverable leader| 45 | |igl|boolean|is group leader| 46 | |gpn|string|group public name| 47 | |hgid|UUID string|home group UUID| 48 | |hmid|string|household ID| 49 | |pgcgl|boolean|parent group contains discoverable leader| 50 | |pgid|UUID string|parent group UUID| 51 | |tsid|UUID string|3008B5C8-9BD3-4479-A564-90BFB3D780C0|tight sync UUID| 52 | |rsf|64 bit hex number|required sender features| 53 | |protovers|string|protocol version| 54 | |vv|?|vodka version 55 | 56 | 57 | ## _raop._tcp 58 | 59 | ``` 60 | name: 5855CA1AE288@Apple TV 61 | type: _raop._tcp 62 | port: 49152 63 | txt: 64 | txtvers=1 65 | ch=2 66 | cn=0,1,2,3 67 | da=true 68 | et=0,3,5 69 | md=0,1,2 70 | pw=false 71 | sv=false 72 | sr=44100 73 | ss=16 74 | tp=UDP 75 | vn=65537 76 | vs=130.14 77 | am=AppleTV2,1 78 | sf=0x4 79 | ``` 80 | 81 | The name is formed using the MAC address of the device and the name of 82 | the remote speaker which will be shown by the clients. 83 | 84 | The following fields appear in the TXT record: 85 | 86 | |name|value|description| 87 | |----|-----|-----------| 88 | |txtvers|1|TXT record version 1| 89 | |ch|2|audio channels: stereo| 90 | |cn|0,1,2,3|audio codecs| 91 | |et|0,3,5|supported encryption types| 92 | |md|0,1,2|supported metadata types| 93 | |pw|false|does the speaker require a password?| 94 | |sr|44100|audio sample rate: 44100 Hz| 95 | |ss|16|audio sample size: 16-bit| 96 | |tp|UDP|supported transport: TCP or UDP| 97 | |vs|130.14|server version 130.14| 98 | |am|AppleTV2,1|device model| 99 | 100 | ### Audio codecs 101 | 102 | |cn|description | 103 | |--|----------------------------| 104 | | 0|PCM | 105 | | 1|Apple Lossless (ALAC) | 106 | | 2|AAC | 107 | | 3|AAC ELD (Enhanced Low Delay)| 108 | 109 | 110 | ### Encryption/Authentication Types 111 | 112 | |et|description | 113 | |--|--------------------------| 114 | | 0|no encryption | 115 | | 1|RSA (AirPort Express) | 116 | | 3|FairPlay | 117 | | 4|MFiSAP (3rd-party devices)| 118 | | 5|FairPlay SAPv2.5 | 119 | 120 | 121 | ### Metadata Types 122 | 123 | |md|bit|description| 124 | |--|---|-----------| 125 | | 0| 17|text | 126 | | 1| 15|artwork | 127 | | 2| 16|progress | 128 | | | 50|bplist | 129 | 130 | 131 | ### Subtype 132 | 133 | ``` 134 | AppleTV = deviceModel has prefix “AppleTV” 135 | HomePod = deviceModel has prefix “AudioAccessory” 136 | ThirdPartySpeaker = HasUnifiedAdvertiserInfo or SupportsUnifiedPairSetupAndMFi feature is set 137 | Unknown = otherwise 138 | ``` 139 | 140 | 141 | ### CanBeRemoteControlled 142 | 143 | `SupportsBufferedAudio` is set and `PINRequired` is not set 144 | 145 | 146 | ### State Changes 147 | 148 | Depending on the state of the device the mDNS record is changed to reflect 149 | this. Primarily it is the `flags`, `gid`, `igl`, `gcgl`, `pgid` and `pgcgl` 150 | fields that are changed. 151 | 152 | 153 | #### Apple TV not playing 154 | ``` 155 | flags=0x10644 156 | gid=712F0759-5D44-41E7-AB67-FAB0AD39E165 157 | igl=1 158 | gcgl=1 159 | ``` 160 | 161 | #### Apple TV receiving AirPlay audio 162 | ``` 163 | flags=0x30e44 164 | gid=19F5D4B2-8A06-4792-923E-8AFA83913238 165 | igl=0 166 | gcgl=0 167 | pgid=19F5D4B2-8A06-4792-923E-8AFA83913238 168 | pgcgl=0 169 | ``` 170 | 171 | #### Apple TV receiving AirPlay video 172 | ``` 173 | flags=0x30e44 174 | gid=19F5D4B2-8A06-4792-923E-8AFA83913238 175 | igl=0 176 | gcgl=0 177 | pgid=19F5D4B2-8A06-4792-923E-8AFA83913238 178 | pgcgl=0 179 | ``` 180 | 181 | #### Apple TV receiving AirPlay screen 182 | ``` 183 | flags=0x30644 184 | gid=712F0759-5D44-41E7-AB67-FAB0AD39E165 185 | igl=1 186 | gcgl=1 187 | ``` 188 | 189 | #### Apple TV playing local media 190 | ``` 191 | flags=0x10644 192 | gid=FA69F5A2-5574-4E4D-A676-CA7F61A904A3 193 | igl=1 194 | gcgl=1 195 | ``` 196 | -------------------------------------------------------------------------------- /src/status_flags.md: -------------------------------------------------------------------------------- 1 | # Status Flags 2 | 3 | |bit|name|description| 4 | |---|----|-----------| 5 | | 0|Problem has been detected|Defined in CarPlay section of MFi spec. Not seen set anywhere| 6 | | 1|Device is not configured|Defined in CarPlay section of MFi spec. Not seen set anywhere| 7 | | 2|Audio cable is attached|Defined in CarPlay section of MFi spec. Seen on AppleTV, Denon AVR, HomePod, Airport Express| 8 | | 3|PINRequired|| 9 | | 4|???|Not seen set anywhere| 10 | | 5|???|Not seen set anywhere| 11 | | 6|SupportsAirPlayFromCloud| 12 | | 7|PasswordRequired|| 13 | | 8|???|Not seen set anywhere| 14 | | 9|OneTimePairingRequired|| 15 | | 10|DeviceWasSetupForHKAccessControl|| 16 | | 11|DeviceSupportsRelay|Shows in logs as relayable. When set iOS will connect to the device to get currently playing track.| 17 | | 12|SilentPrimary|| 18 | | 13|TightSyncIsGroupLeader|| 19 | | 14|TightSyncBuddyNotReachable|| 20 | | 15|IsAppleMusicSubscriber|Shows in logs as music| 21 | | 16|CloudLibraryIsOn|Shows in logs as iCML| 22 | | 17|ReceiverSessionIsActive|Shows in logs as airplay-receiving. Set when Apple TV is receiving anything via AirPlay.| 23 | | 18|???|Not seen set anywhere| 24 | | 19|???|Not seen set anywhere| 25 | -------------------------------------------------------------------------------- /src/video/README.md: -------------------------------------------------------------------------------- 1 | # Video 2 | In order to play a video on an AirPlay server, HTTP requests are used to 3 | send a video URL, perform scrubbing, change the playback rate and update 4 | the timeline. 5 | -------------------------------------------------------------------------------- /src/video/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | This event is used to send the playback state to the client: 4 | 5 | |key |type |description | 6 | |----------|--------|-------------------------------------------| 7 | |category |string |`video` | 8 | |sessionID |integer |session id | 9 | |state |string |`loading`, `playing`, `paused` or `stopped` | 10 | 11 | Example: notify the client that video playback is paused 12 | 13 |
14 |

server → client

15 | 16 | ```http 17 | POST /event HTTP/1.1 18 | Content-Type: application/x-apple-plist 19 | Content-Length: 321 20 | X-Apple-Session-ID: 00000000-0000-0000-0000-000000000000 21 | ``` 22 | ```xml 23 | 24 | 25 | 26 | 27 | category 28 | video 29 | sessionID 30 | 13 31 | state 32 | paused 33 | 34 | 35 | ``` 36 |
37 | 38 |
39 |

client → server

40 | 41 | ```http 42 | HTTP/1.1 200 OK 43 | Content-Length: 0 44 | Date: Mon, 08 Mar 2012 18:07:43 GMT 45 | ``` 46 |
47 | -------------------------------------------------------------------------------- /theme/css/general.css: -------------------------------------------------------------------------------- 1 | /* Base styles and content styles */ 2 | 3 | @import 'variables.css'; 4 | 5 | :root { 6 | /* Browser default font-size is 16px, this way 1 rem = 10px */ 7 | font-size: 62.5%; 8 | } 9 | 10 | html { 11 | font-family: "Open Sans", sans-serif; 12 | color: var(--fg); 13 | background-color: var(--bg); 14 | text-size-adjust: none; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | font-size: 1.6rem; 20 | overflow-x: hidden; 21 | } 22 | 23 | code { 24 | font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; 25 | font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ 26 | } 27 | 28 | .left { float: left; } 29 | .right { float: right; } 30 | .boring { opacity: 0.6; } 31 | .hide-boring .boring { display: none; } 32 | .hidden { display: none; } 33 | 34 | h2, h3 { margin-top: 2.5em; } 35 | h4, h5 { margin-top: 2em; } 36 | 37 | .header + .header h3, 38 | .header + .header h4, 39 | .header + .header h5 { 40 | margin-top: 1em; 41 | } 42 | 43 | h1 a.header:target::before, 44 | h2 a.header:target::before, 45 | h3 a.header:target::before, 46 | h4 a.header:target::before { 47 | display: inline-block; 48 | content: "»"; 49 | margin-left: -30px; 50 | width: 30px; 51 | } 52 | 53 | h1 a.header:target, 54 | h2 a.header:target, 55 | h3 a.header:target, 56 | h4 a.header:target { 57 | scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); 58 | } 59 | 60 | .page { 61 | outline: 0; 62 | padding: 0 var(--page-padding); 63 | } 64 | .page-wrapper { 65 | box-sizing: border-box; 66 | } 67 | .js:not(.sidebar-resizing) .page-wrapper { 68 | transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ 69 | } 70 | 71 | .content { 72 | overflow-y: auto; 73 | padding: 0 15px; 74 | padding-bottom: 50px; 75 | } 76 | .content main { 77 | margin-left: auto; 78 | margin-right: auto; 79 | max-width: var(--content-max-width); 80 | } 81 | .content a { text-decoration: none; } 82 | .content a:hover { text-decoration: underline; } 83 | .content img { max-width: 100%; } 84 | .content .header:link, 85 | .content .header:visited { 86 | color: var(--fg); 87 | } 88 | .content .header:link, 89 | .content .header:visited:hover { 90 | text-decoration: none; 91 | } 92 | 93 | table { 94 | margin: 0 auto; 95 | border-collapse: collapse; 96 | } 97 | table td { 98 | padding: 3px 20px; 99 | border: 1px var(--table-border-color) solid; 100 | } 101 | table thead { 102 | background: var(--table-header-bg); 103 | } 104 | table thead td { 105 | font-weight: 700; 106 | border: none; 107 | } 108 | table thead th { 109 | padding: 3px 20px; 110 | } 111 | table thead tr { 112 | border: 1px var(--table-header-bg) solid; 113 | } 114 | /* Alternate background colors for rows */ 115 | table tbody tr:nth-child(2n) { 116 | background: var(--table-alternate-bg); 117 | } 118 | 119 | 120 | blockquote { 121 | margin: 20px 0; 122 | padding: 0 20px; 123 | color: var(--fg); 124 | background-color: var(--quote-bg); 125 | border-top: .1em solid var(--quote-border); 126 | border-bottom: .1em solid var(--quote-border); 127 | } 128 | 129 | 130 | :not(.footnote-definition) + .footnote-definition, 131 | .footnote-definition + :not(.footnote-definition) { 132 | margin-top: 2em; 133 | } 134 | .footnote-definition { 135 | font-size: 0.9em; 136 | margin: 0.5em 0; 137 | } 138 | .footnote-definition p { 139 | display: inline; 140 | } 141 | 142 | .tooltiptext { 143 | position: absolute; 144 | visibility: hidden; 145 | color: #fff; 146 | background-color: #333; 147 | transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */ 148 | left: -8px; /* Half of the width of the icon */ 149 | top: -35px; 150 | font-size: 0.8em; 151 | text-align: center; 152 | border-radius: 6px; 153 | padding: 5px 8px; 154 | margin: 5px; 155 | z-index: 1000; 156 | } 157 | .tooltipped .tooltiptext { 158 | visibility: visible; 159 | } 160 | -------------------------------------------------------------------------------- /theme/css/print.css: -------------------------------------------------------------------------------- 1 | 2 | #sidebar, 3 | #menu-bar, 4 | .nav-chapters, 5 | .mobile-nav-chapters { 6 | display: none; 7 | } 8 | 9 | #page-wrapper.page-wrapper { 10 | transform: none; 11 | margin-left: 0px; 12 | overflow-y: initial; 13 | } 14 | 15 | #content { 16 | max-width: none; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | .page { 22 | overflow-y: initial; 23 | } 24 | 25 | code { 26 | background-color: #666666; 27 | border-radius: 5px; 28 | 29 | /* Force background to be printed in Chrome */ 30 | -webkit-print-color-adjust: exact; 31 | } 32 | 33 | pre > .buttons { 34 | z-index: 2; 35 | } 36 | 37 | a, a:visited, a:active, a:hover { 38 | color: #4183c4; 39 | text-decoration: none; 40 | } 41 | 42 | h1, h2, h3, h4, h5, h6 { 43 | page-break-inside: avoid; 44 | page-break-after: avoid; 45 | } 46 | 47 | pre, code { 48 | page-break-inside: avoid; 49 | white-space: pre-wrap; 50 | } 51 | 52 | .fa { 53 | display: none !important; 54 | } 55 | -------------------------------------------------------------------------------- /theme/css/variables.css: -------------------------------------------------------------------------------- 1 | 2 | /* Globals */ 3 | 4 | :root { 5 | --sidebar-width: 300px; 6 | --page-padding: 15px; 7 | --content-max-width: 750px; 8 | --menu-bar-height: 50px; 9 | } 10 | 11 | /* Themes */ 12 | 13 | .ayu { 14 | --bg: hsl(210, 25%, 8%); 15 | --fg: #c5c5c5; 16 | 17 | --sidebar-bg: #14191f; 18 | --sidebar-fg: #c8c9db; 19 | --sidebar-non-existant: #5c6773; 20 | --sidebar-active: #ffb454; 21 | --sidebar-spacer: #2d334f; 22 | 23 | --scrollbar: var(--sidebar-fg); 24 | 25 | --icons: #737480; 26 | --icons-hover: #b7b9cc; 27 | 28 | --links: #0096cf; 29 | 30 | --inline-code-color: #ffb454; 31 | 32 | --theme-popup-bg: #14191f; 33 | --theme-popup-border: #5c6773; 34 | --theme-hover: #191f26; 35 | 36 | --quote-bg: hsl(226, 15%, 17%); 37 | --quote-border: hsl(226, 15%, 22%); 38 | 39 | --table-border-color: hsl(210, 25%, 13%); 40 | --table-header-bg: hsl(210, 25%, 28%); 41 | --table-alternate-bg: hsl(210, 25%, 11%); 42 | 43 | --searchbar-border-color: #848484; 44 | --searchbar-bg: #424242; 45 | --searchbar-fg: #fff; 46 | --searchbar-shadow-color: #d4c89f; 47 | --searchresults-header-fg: #666; 48 | --searchresults-border-color: #888; 49 | --searchresults-li-bg: #252932; 50 | --search-mark-bg: #e3b171; 51 | } 52 | 53 | .coal { 54 | --bg: hsl(200, 7%, 8%); 55 | --fg: #98a3ad; 56 | 57 | --sidebar-bg: #292c2f; 58 | --sidebar-fg: #a1adb8; 59 | --sidebar-non-existant: #505254; 60 | --sidebar-active: #3473ad; 61 | --sidebar-spacer: #393939; 62 | 63 | --scrollbar: var(--sidebar-fg); 64 | 65 | --icons: #43484d; 66 | --icons-hover: #b3c0cc; 67 | 68 | --links: #2b79a2; 69 | 70 | --inline-code-color: #c5c8c6;; 71 | 72 | --theme-popup-bg: #141617; 73 | --theme-popup-border: #43484d; 74 | --theme-hover: #1f2124; 75 | 76 | --quote-bg: hsl(234, 21%, 18%); 77 | --quote-border: hsl(234, 21%, 23%); 78 | 79 | --table-border-color: hsl(200, 7%, 13%); 80 | --table-header-bg: hsl(200, 7%, 28%); 81 | --table-alternate-bg: hsl(200, 7%, 11%); 82 | 83 | --searchbar-border-color: #aaa; 84 | --searchbar-bg: #b7b7b7; 85 | --searchbar-fg: #000; 86 | --searchbar-shadow-color: #aaa; 87 | --searchresults-header-fg: #666; 88 | --searchresults-border-color: #98a3ad; 89 | --searchresults-li-bg: #2b2b2f; 90 | --search-mark-bg: #355c7d; 91 | } 92 | 93 | .light { 94 | --bg: hsl(0, 0%, 100%); 95 | --fg: #333333; 96 | 97 | --sidebar-bg: #fafafa; 98 | --sidebar-fg: #364149; 99 | --sidebar-non-existant: #aaaaaa; 100 | --sidebar-active: #008cff; 101 | --sidebar-spacer: #f4f4f4; 102 | 103 | --scrollbar: #cccccc; 104 | 105 | --icons: #cccccc; 106 | --icons-hover: #333333; 107 | 108 | --links: #4183c4; 109 | 110 | --inline-code-color: #6e6b5e; 111 | 112 | --theme-popup-bg: #fafafa; 113 | --theme-popup-border: #cccccc; 114 | --theme-hover: #e6e6e6; 115 | 116 | --quote-bg: hsl(197, 37%, 96%); 117 | --quote-border: hsl(197, 37%, 91%); 118 | 119 | --table-border-color: hsl(0, 0%, 95%); 120 | --table-header-bg: hsl(0, 0%, 80%); 121 | --table-alternate-bg: hsl(0, 0%, 97%); 122 | 123 | --searchbar-border-color: #aaa; 124 | --searchbar-bg: #fafafa; 125 | --searchbar-fg: #000; 126 | --searchbar-shadow-color: #aaa; 127 | --searchresults-header-fg: #666; 128 | --searchresults-border-color: #888; 129 | --searchresults-li-bg: #e4f2fe; 130 | --search-mark-bg: #a2cff5; 131 | } 132 | 133 | .navy { 134 | --bg: hsl(226, 23%, 11%); 135 | --fg: #bcbdd0; 136 | 137 | --sidebar-bg: #282d3f; 138 | --sidebar-fg: #c8c9db; 139 | --sidebar-non-existant: #505274; 140 | --sidebar-active: #2b79a2; 141 | --sidebar-spacer: #2d334f; 142 | 143 | --scrollbar: var(--sidebar-fg); 144 | 145 | --icons: #737480; 146 | --icons-hover: #b7b9cc; 147 | 148 | --links: #2b79a2; 149 | 150 | --inline-code-color: #c5c8c6;; 151 | 152 | --theme-popup-bg: #161923; 153 | --theme-popup-border: #737480; 154 | --theme-hover: #282e40; 155 | 156 | --quote-bg: hsl(226, 15%, 17%); 157 | --quote-border: hsl(226, 15%, 22%); 158 | 159 | --table-border-color: hsl(226, 23%, 16%); 160 | --table-header-bg: hsl(226, 23%, 31%); 161 | --table-alternate-bg: hsl(226, 23%, 14%); 162 | 163 | --searchbar-border-color: #aaa; 164 | --searchbar-bg: #aeaec6; 165 | --searchbar-fg: #000; 166 | --searchbar-shadow-color: #aaa; 167 | --searchresults-header-fg: #5f5f71; 168 | --searchresults-border-color: #5c5c68; 169 | --searchresults-li-bg: #242430; 170 | --search-mark-bg: #a2cff5; 171 | } 172 | 173 | .rust { 174 | --bg: hsl(60, 9%, 87%); 175 | --fg: #262625; 176 | 177 | --sidebar-bg: #3b2e2a; 178 | --sidebar-fg: #c8c9db; 179 | --sidebar-non-existant: #505254; 180 | --sidebar-active: #e69f67; 181 | --sidebar-spacer: #45373a; 182 | 183 | --scrollbar: var(--sidebar-fg); 184 | 185 | --icons: #737480; 186 | --icons-hover: #262625; 187 | 188 | --links: #2b79a2; 189 | 190 | --inline-code-color: #6e6b5e; 191 | 192 | --theme-popup-bg: #e1e1db; 193 | --theme-popup-border: #b38f6b; 194 | --theme-hover: #99908a; 195 | 196 | --quote-bg: hsl(60, 5%, 75%); 197 | --quote-border: hsl(60, 5%, 70%); 198 | 199 | --table-border-color: hsl(60, 9%, 82%); 200 | --table-header-bg: #b3a497; 201 | --table-alternate-bg: hsl(60, 9%, 84%); 202 | 203 | --searchbar-border-color: #aaa; 204 | --searchbar-bg: #fafafa; 205 | --searchbar-fg: #000; 206 | --searchbar-shadow-color: #aaa; 207 | --searchresults-header-fg: #666; 208 | --searchresults-border-color: #888; 209 | --searchresults-li-bg: #dec2a2; 210 | --search-mark-bg: #e69f67; 211 | } 212 | 213 | @media (prefers-color-scheme: dark) { 214 | .light.no-js { 215 | --bg: hsl(200, 7%, 8%); 216 | --fg: #98a3ad; 217 | 218 | --sidebar-bg: #292c2f; 219 | --sidebar-fg: #a1adb8; 220 | --sidebar-non-existant: #505254; 221 | --sidebar-active: #3473ad; 222 | --sidebar-spacer: #393939; 223 | 224 | --scrollbar: var(--sidebar-fg); 225 | 226 | --icons: #43484d; 227 | --icons-hover: #b3c0cc; 228 | 229 | --links: #2b79a2; 230 | 231 | --inline-code-color: #c5c8c6;; 232 | 233 | --theme-popup-bg: #141617; 234 | --theme-popup-border: #43484d; 235 | --theme-hover: #1f2124; 236 | 237 | --quote-bg: hsl(234, 21%, 18%); 238 | --quote-border: hsl(234, 21%, 23%); 239 | 240 | --table-border-color: hsl(200, 7%, 13%); 241 | --table-header-bg: hsl(200, 7%, 28%); 242 | --table-alternate-bg: hsl(200, 7%, 11%); 243 | 244 | --searchbar-border-color: #aaa; 245 | --searchbar-bg: #b7b7b7; 246 | --searchbar-fg: #000; 247 | --searchbar-shadow-color: #aaa; 248 | --searchresults-header-fg: #666; 249 | --searchresults-border-color: #98a3ad; 250 | --searchresults-li-bg: #2b2b2f; 251 | --search-mark-bg: #355c7d; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /theme/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openairplay/airplay-spec/00063da0e7f16c20fbfb5d29331334da1a89590a/theme/favicon.png -------------------------------------------------------------------------------- /theme/highlight.css: -------------------------------------------------------------------------------- 1 | /* Base16 Atelier Dune Light - Theme */ 2 | /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ 3 | /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ 4 | 5 | /* Atelier-Dune Comment */ 6 | .hljs-comment, 7 | .hljs-quote { 8 | color: #AAA; 9 | } 10 | 11 | /* Atelier-Dune Red */ 12 | .hljs-variable, 13 | .hljs-template-variable, 14 | .hljs-attribute, 15 | .hljs-tag, 16 | .hljs-name, 17 | .hljs-regexp, 18 | .hljs-link, 19 | .hljs-name, 20 | .hljs-selector-id, 21 | .hljs-selector-class { 22 | color: #d73737; 23 | } 24 | 25 | /* Atelier-Dune Orange */ 26 | .hljs-number, 27 | .hljs-meta, 28 | .hljs-built_in, 29 | .hljs-builtin-name, 30 | .hljs-literal, 31 | .hljs-type, 32 | .hljs-params { 33 | color: #b65611; 34 | } 35 | 36 | /* Atelier-Dune Green */ 37 | .hljs-string, 38 | .hljs-symbol, 39 | .hljs-bullet { 40 | color: #60ac39; 41 | } 42 | 43 | /* Atelier-Dune Blue */ 44 | .hljs-title, 45 | .hljs-section { 46 | color: #6684e1; 47 | } 48 | 49 | /* Atelier-Dune Purple */ 50 | .hljs-keyword, 51 | .hljs-selector-tag { 52 | color: #b854d4; 53 | } 54 | 55 | .hljs { 56 | display: block; 57 | overflow-x: auto; 58 | background: #f1f1f1; 59 | color: #6e6b5e; 60 | padding: 0.5em; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | 67 | .hljs-strong { 68 | font-weight: bold; 69 | } 70 | 71 | .hljs-addition { 72 | color: #22863a; 73 | background-color: #f0fff4; 74 | } 75 | 76 | .hljs-deletion { 77 | color: #b31d28; 78 | background-color: #ffeef0; 79 | } 80 | --------------------------------------------------------------------------------